Skip to content

Commit 629a4d2

Browse files
dakerfinetjul
authored andcommitted
feat(TextActor): add vtkTextActor
1 parent 02a911a commit 629a4d2

File tree

9 files changed

+609
-0
lines changed

9 files changed

+609
-0
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<table>
2+
<tr>
3+
<td>Type your text</td>
4+
<td>
5+
<input id="text" class='text' type="text" value="Hello World!" />
6+
</td>
7+
</tr>
8+
</table>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import '@kitware/vtk.js/favicon';
2+
3+
// Load the rendering pieces we want to use (for both WebGL and WebGPU)
4+
import '@kitware/vtk.js/Rendering/Profiles/Geometry';
5+
6+
import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow';
7+
import vtkTextActor from '@kitware/vtk.js/Rendering/Core/TextActor';
8+
9+
import controlPanel from './controlPanel.html';
10+
11+
// ----------------------------------------------------------------------------
12+
// Standard rendering code setup
13+
// ----------------------------------------------------------------------------
14+
15+
const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance();
16+
const renderer = fullScreenRenderer.getRenderer();
17+
const renderWindow = fullScreenRenderer.getRenderWindow();
18+
19+
// ----------------------------------------------------------------------------
20+
// Example code
21+
// ----------------------------------------------------------------------------
22+
23+
const actor = vtkTextActor.newInstance();
24+
actor.setInput('Hello World!');
25+
actor.setDisplayPosition(20, 30);
26+
27+
renderer.addActor2D(actor);
28+
renderer.resetCamera();
29+
renderWindow.render();
30+
31+
// -----------------------------------------------------------
32+
// UI control handling
33+
// -----------------------------------------------------------
34+
35+
fullScreenRenderer.addController(controlPanel);
36+
37+
// -----------------------------------------------------------
38+
// Make some variables global so that you can inspect and
39+
// modify objects in your browser's developer console:
40+
// -----------------------------------------------------------
41+
42+
const textInput = document.getElementById('text');
43+
textInput.addEventListener('input', (event) => {
44+
const value = event.target.value;
45+
actor.setInput(value);
46+
renderWindow.render();
47+
});
48+
global.actor = actor;
49+
global.renderer = renderer;
50+
global.renderWindow = renderWindow;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import vtkActor2D, { IActor2DInitialValues } from '../Actor2D';
2+
import vtkTextProperty, { ITextPropertyInitialValues } from '../TextProperty';
3+
4+
export interface ITextActorInitialValues extends IActor2DInitialValues {
5+
property?: vtkTextProperty;
6+
}
7+
8+
export interface vtkTextActor extends vtkActor2D {
9+
/**
10+
* Get the property object that controls this actors properties.
11+
* @returns {vtkTextProperty} The vtkTextProperty instance.
12+
*/
13+
getProperty(): vtkTextProperty;
14+
15+
/**
16+
* Create a new property suitable for use with this type of TextActor.
17+
* @param {ITextPropertyInitialValues} [initialValues] (default: {})
18+
* @return {vtkTextProperty} A new vtkTextProperty instance.
19+
*/
20+
makeProperty(initialValues?: ITextPropertyInitialValues): vtkTextProperty;
21+
22+
/**
23+
* Set the text to be displayed by the actor.
24+
* @param input The text to be displayed by the actor.
25+
*/
26+
setInput(input: string): boolean;
27+
28+
/**
29+
* Set the property object that controls this actors properties.
30+
* @param {vtkTextProperty} property The vtkTextProperty instance.
31+
*/
32+
setProperty(property: vtkTextProperty): boolean;
33+
}
34+
35+
/**
36+
* Method used to decorate a given object (publicAPI+model) with vtkActor characteristics.
37+
*
38+
* @param publicAPI object on which methods will be bounds (public)
39+
* @param model object on which data structure will be bounds (protected)
40+
* @param {ITextActorInitialValues} [initialValues] (default: {})
41+
*/
42+
export function extend(
43+
publicAPI: object,
44+
model: object,
45+
initialValues?: ITextActorInitialValues
46+
): void;
47+
48+
/**
49+
* Method used to create a new instance of vtkTextActor
50+
*
51+
* @param {ITextActorInitialValues} [initialValues] for pre-setting some of its content
52+
*/
53+
export function newInstance(
54+
initialValues?: ITextActorInitialValues
55+
): vtkTextActor;
56+
57+
/**
58+
* vtkTextActor can be used to place text annotation into a window.
59+
*/
60+
export declare const vtkTextActor: {
61+
newInstance: typeof newInstance;
62+
extend: typeof extend;
63+
};
64+
export default vtkTextActor;
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import macro from 'vtk.js/Sources/macros';
2+
import vtkPlaneSource from 'vtk.js/Sources/Filters/Sources/PlaneSource';
3+
import vtkTexture from 'vtk.js/Sources/Rendering/Core/Texture';
4+
import vtkActor2D from 'vtk.js/Sources/Rendering/Core/Actor2D';
5+
import vtkMapper2D from 'vtk.js/Sources/Rendering/Core/Mapper2D';
6+
import vtkTextProperty from 'vtk.js/Sources/Rendering/Core/TextProperty';
7+
import ImageHelper from 'vtk.js/Sources/Common/Core/ImageHelper';
8+
import * as vtkMath from 'vtk.js/Sources/Common/Core/Math';
9+
10+
// ----------------------------------------------------------------------------
11+
// vtkTextActor methods
12+
// ----------------------------------------------------------------------------
13+
function vtkTextActor(publicAPI, model) {
14+
// Set our className
15+
model.classHierarchy.push('vtkTextActor');
16+
17+
publicAPI.makeProperty = vtkTextProperty.newInstance;
18+
19+
const texture = vtkTexture.newInstance({
20+
resizable: true,
21+
});
22+
const canvas = new OffscreenCanvas(1, 1);
23+
const mapper = vtkMapper2D.newInstance();
24+
const plane = vtkPlaneSource.newInstance({
25+
xResolution: 1,
26+
yResolution: 1,
27+
});
28+
29+
function createImageData(text) {
30+
const fontSizeScale = publicAPI.getProperty().getFontSizeScale();
31+
const fontStyle = publicAPI.getProperty().getFontStyle();
32+
const fontFamily = publicAPI.getProperty().getFontFamily();
33+
const fontColor = publicAPI.getProperty().getFontColor();
34+
const shadowColor = publicAPI.getProperty().getShadowColor();
35+
const shadowOffset = publicAPI.getProperty().getShadowOffset();
36+
const shadowBlur = publicAPI.getProperty().getShadowBlur();
37+
const resolution = publicAPI.getProperty().getResolution();
38+
const backgroundColor = publicAPI.getProperty().getBackgroundColor();
39+
40+
const dpr = Math.max(window.devicePixelRatio || 1, 1);
41+
const ctx = canvas.getContext('2d');
42+
43+
// Set the text properties to measure
44+
const textSize = fontSizeScale(resolution) * dpr;
45+
46+
ctx.font = `${fontStyle} ${textSize}px "${fontFamily}"`;
47+
ctx.textBaseline = 'middle';
48+
ctx.textAlign = 'center';
49+
50+
// Measure the text
51+
const metrics = ctx.measureText(text);
52+
const textWidth = metrics.width / dpr;
53+
54+
const {
55+
actualBoundingBoxLeft,
56+
actualBoundingBoxRight,
57+
actualBoundingBoxAscent,
58+
actualBoundingBoxDescent,
59+
} = metrics;
60+
const hAdjustment = (actualBoundingBoxLeft - actualBoundingBoxRight) / 2;
61+
const vAdjustment =
62+
(actualBoundingBoxAscent - actualBoundingBoxDescent) / 2;
63+
64+
const textHeight = textSize / dpr - vAdjustment;
65+
66+
// Update canvas size to fit text and ensure it is at least 1x1 pixel
67+
const width = Math.max(Math.round(textWidth * dpr), 1);
68+
const height = Math.max(Math.round(textHeight * dpr), 1);
69+
70+
canvas.width = width;
71+
canvas.height = height;
72+
73+
// Vertical flip
74+
ctx.translate(0, height);
75+
ctx.scale(1, -1);
76+
77+
// Clear the canvas
78+
ctx.clearRect(0, 0, width, height);
79+
80+
if (backgroundColor) {
81+
ctx.fillStyle = vtkMath.floatRGB2HexCode(backgroundColor);
82+
ctx.fillRect(0, 0, width, height);
83+
}
84+
85+
// Reset context after resize and prepare for rendering
86+
ctx.imageSmoothingEnabled = true;
87+
ctx.imageSmoothingQuality = 'high';
88+
ctx.font = `${fontStyle} ${textSize}px "${fontFamily}"`;
89+
ctx.fillStyle = vtkMath.floatRGB2HexCode(fontColor);
90+
ctx.textBaseline = 'middle';
91+
ctx.textAlign = 'center';
92+
93+
// Set shadow
94+
if (shadowColor) {
95+
ctx.shadowColor = vtkMath.floatRGB2HexCode(shadowColor);
96+
ctx.shadowOffsetX = shadowOffset[0];
97+
ctx.shadowOffsetY = shadowOffset[1];
98+
ctx.shadowBlur = shadowBlur;
99+
}
100+
101+
// Draw the text
102+
ctx.fillText(text, width / 2 + hAdjustment, height / 2 + vAdjustment);
103+
104+
// Update plane dimensions to match text size
105+
plane.set({
106+
point1: [width, 0, 0],
107+
point2: [0, height, 0],
108+
});
109+
110+
return ImageHelper.canvasToImageData(canvas);
111+
}
112+
113+
mapper.setInputConnection(plane.getOutputPort());
114+
115+
publicAPI.setMapper(mapper);
116+
publicAPI.addTexture(texture);
117+
118+
model._onInputChanged = (_publicAPI, _model, value) => {
119+
const image = createImageData(value);
120+
texture.setInputData(image, 0);
121+
};
122+
}
123+
124+
// Default property values
125+
const DEFAULT_VALUES = {
126+
mapper: null,
127+
property: null,
128+
};
129+
130+
export function extend(publicAPI, model, initialValues = {}) {
131+
Object.assign(model, DEFAULT_VALUES, initialValues);
132+
133+
// Inheritance
134+
vtkActor2D.extend(publicAPI, model, initialValues);
135+
136+
// Build VTK API
137+
macro.setGet(publicAPI, model, ['input']);
138+
139+
// Object methods
140+
vtkTextActor(publicAPI, model);
141+
}
142+
143+
export const newInstance = macro.newInstance(extend, 'vtkTextActor');
144+
145+
export default { newInstance, extend };
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import test from 'tape';
2+
import testUtils from 'vtk.js/Sources/Testing/testUtils';
3+
4+
import vtkTextActor from 'vtk.js/Sources/Rendering/Core/TextActor';
5+
import 'vtk.js/Sources/Rendering/Misc/RenderingAPIs';
6+
import vtkRenderer from 'vtk.js/Sources/Rendering/Core/Renderer';
7+
import vtkRenderWindow from 'vtk.js/Sources/Rendering/Core/RenderWindow';
8+
9+
import baseline from './testTextActor.png';
10+
11+
test.onlyIfWebGL('Test TextActor', (t) => {
12+
const gc = testUtils.createGarbageCollector();
13+
t.ok('rendering', 'vtkTextActor');
14+
15+
// Create some control UI
16+
const container = document.querySelector('body');
17+
const renderWindowContainer = gc.registerDOMElement(
18+
document.createElement('div')
19+
);
20+
container.appendChild(renderWindowContainer);
21+
22+
// create what we will view
23+
const renderWindow = gc.registerResource(vtkRenderWindow.newInstance());
24+
const renderer = gc.registerResource(vtkRenderer.newInstance());
25+
renderWindow.addRenderer(renderer);
26+
renderer.setBackground(0.32, 0.34, 0.43);
27+
28+
// ----------------------------------------------------------------------------
29+
// Test code
30+
// ----------------------------------------------------------------------------
31+
32+
const actor = gc.registerResource(vtkTextActor.newInstance());
33+
actor.getProperty().setResolution(100);
34+
actor.setDisplayPosition(20, 30);
35+
actor.setInput('vtk.js');
36+
37+
renderer.addActor2D(actor);
38+
renderer.resetCamera();
39+
40+
// -----------------------------------------------------------
41+
// Make some variables global so that you can inspect and
42+
// modify objects in your browser's developer console:
43+
// -----------------------------------------------------------
44+
45+
// create something to view it
46+
const glwindow = gc.registerResource(renderWindow.newAPISpecificView());
47+
glwindow.setContainer(renderWindowContainer);
48+
renderWindow.addView(glwindow);
49+
glwindow.setSize(400, 400);
50+
51+
const promise = glwindow
52+
.captureNextImage()
53+
.then((image) =>
54+
testUtils.compareImages(
55+
image,
56+
[baseline],
57+
'Rendering/Core/TextActor',
58+
t,
59+
1
60+
)
61+
)
62+
.finally(gc.releaseResources);
63+
renderWindow.render();
64+
return promise;
65+
});
7.14 KB
Loading

0 commit comments

Comments
 (0)