VectorText

Introduction

vtkVectorText generates vtkPolyData from an input string.
The TTF file needs to be parsed using opentype.js and then passed to
vtkVectorText via the setFont method.

Methods

extend

Method use to decorate a given object (publicAPI+model) with vtkVectorText characteristics.

Argument Type Required Description
publicAPI Yes object on which methods will be bounds (public)
model Yes object on which data structure will be bounds (protected)
initialValues IVectorTextInitialValues No (default: {})

getBevelEnabled

Returns whether beveling is enabled.

getBevelOffset

Returns the offset of the bevel.

getBevelSegments

Returns the number of segments used for the bevel geometry.

getBevelSize

Returns the size of the bevel.

getBevelThickness

Returns the thickness of the bevel.

getCurveSegments

Returns the number of curve segments used for the text geometry.

getDepth

Returns the extrusion depth of the text.

getFontSize

Returns the current font size.

getPerLetterFaceColors

Gets or sets the per-letter face color function.

Argument Type Required Description
fn Yes - Function mapping letter index to [r,g,b] color.

getSteps

Returns the number of steps used for the text geometry.

getText

Returns the current text string.

newInstance

Method use to create a new instance of vtkVectorText

Argument Type Required Description
initialValues IVectorTextInitialValues No for pre-setting some of its content

setBevelEnabled

Enables or disables beveling.

Argument Type Required Description
bevelEnabled Yes - True to enable beveling, false to disable.

setBevelOffset

Sets the offset of the bevel.

Argument Type Required Description
bevelOffset Yes - The bevel offset.

setBevelSegments

Sets the number of segments used for the bevel geometry.

Argument Type Required Description
bevelSegments Yes - The number of bevel segments.

setBevelSize

Sets the size of the bevel.

Argument Type Required Description
bevelSize Yes - The bevel size.

setBevelThickness

Sets the thickness of the bevel.

Argument Type Required Description
bevelThickness Yes - The bevel thickness.

setCurveSegments

Sets the number of curve segments used for the text geometry.

Argument Type Required Description
curveSegments Yes - The number of curve segments.

setDepth

Sets the extrusion depth of the text.

Argument Type Required Description
depth Yes - The new depth value.

setFont

Sets the font object used for rendering the text.
This should be a parsed font object from opentype.js.

Argument Type Required Description
font Yes - The font object.

setFontSize

Sets the font size.

Argument Type Required Description
fontSize Yes - The new font size.

setPerLetterFaceColors

Sets the per-letter face color function.

Argument Type Required Description
fn Yes - Function mapping letter index to [r,g,b] color.

setSteps

Sets the number of steps used for the text geometry.

Argument Type Required Description
steps Yes - The number of steps.

setText

Sets the text string.

Argument Type Required Description
text Yes - The new text to display.

Source

Utils.js
import * as vtkMath from 'vtk.js/Sources/Common/Core/Math';

/**
* Computes UV coordinates for top/bottom faces
* @param {Array} vertices - The vertices array
* @param {Number} iA - First index
* @param {Number} iB - Second index
* @param {Number} iC - Third index
* @returns {Array} Array of UV coordinates
*/
function computeFacesUV(vertices, iA, iB, iC) {
const ax = vertices[iA * 3];
const ay = vertices[iA * 3 + 1];
const bx = vertices[iB * 3];
const by = vertices[iB * 3 + 1];
const cx = vertices[iC * 3];
const cy = vertices[iC * 3 + 1];
return [
[ax, ay],
[bx, by],
[cx, cy],
];
}

/**
* Computes UV coordinates for side walls
* @param {Array} vertices - The vertices array
* @param {Number} iA - First index
* @param {Number} iB - Second index
* @param {Number} iC - Third index
* @param {Number} iD - Fourth index
* @returns {Array} Array of UV coordinates
*/
function computeSidesUV(vertices, iA, iB, iC, iD) {
const ax = vertices[iA * 3];
const ay = vertices[iA * 3 + 1];
const az = vertices[iA * 3 + 2];
const bx = vertices[iB * 3];
const by = vertices[iB * 3 + 1];
const bz = vertices[iB * 3 + 2];
const cx = vertices[iC * 3];
const cy = vertices[iC * 3 + 1];
const cz = vertices[iC * 3 + 2];
const dx = vertices[iD * 3];
const dy = vertices[iD * 3 + 1];
const dz = vertices[iD * 3 + 2];

// Determine the best UV mapping direction based on geometry
if (Math.abs(ay - by) < Math.abs(ax - bx)) {
return [
[ax, 1 - az],
[bx, 1 - bz],
[cx, 1 - cz],
[dx, 1 - dz],
];
}
return [
[ay, 1 - az],
[by, 1 - bz],
[cy, 1 - cz],
[dy, 1 - dz],
];
}

/**
* Creates a shape path object with methods for path operations
* @returns {Object} A shape path object with methods for manipulating paths
*/
function createShapePath() {
const curves = [];
const currentPoint = [0, 0];
const holes = [];

return {
curves,
currentPoint,
holes,
moveTo(x, y) {
currentPoint[0] = x;
currentPoint[1] = y;
},
lineTo(x, y) {
const start = [...currentPoint];
const end = [x, y];
curves.push({
curveType: 'LineCurve',
start,
end,
getPointAt(t) {
return [
start[0] + t * (end[0] - start[0]),
start[1] + t * (end[1] - start[1]),
];
},
getPoints(resolution) {
const points = [];
for (let i = 0; i <= resolution; i++) {
points.push(this.getPointAt(i / resolution));
}
return points;
},
});
currentPoint[0] = x;
currentPoint[1] = y;
},
quadraticCurveTo(cpX, cpY, x, y) {
const start = [...currentPoint];
const end = [x, y];
const cp = [cpX, cpY];
curves.push({
curveType: 'QuadraticBezierCurve',
cp,
start,
end,
getPointAt(t) {
const oneMinusT = 1 - t;
return [
oneMinusT * oneMinusT * start[0] +
2 * oneMinusT * t * cp[0] +
t * t * end[0],
oneMinusT * oneMinusT * start[1] +
2 * oneMinusT * t * cp[1] +
t * t * end[1],
];
},
getPoints(resolution) {
const points = [];
for (let i = 0; i <= resolution; i++) {
points.push(this.getPointAt(i / resolution));
}
return points;
},
});
currentPoint[0] = x;
currentPoint[1] = y;
},
bezierCurveTo(cp1X, cp1Y, cp2X, cp2Y, x, y) {
const start = [...currentPoint];
const end = [x, y];
const cp1 = [cp1X, cp1Y];
const cp2 = [cp2X, cp2Y];
curves.push({
curveType: 'BezierCurve',
cp1,
cp2,
start,
end,
getPointAt(t) {
const oneMinusT = 1 - t;
return [
oneMinusT * oneMinusT * oneMinusT * start[0] +
3 * oneMinusT * oneMinusT * t * cp1[0] +
3 * oneMinusT * t * t * cp2[0] +
t * t * t * end[0],
oneMinusT * oneMinusT * oneMinusT * start[1] +
3 * oneMinusT * oneMinusT * t * cp1[1] +
3 * oneMinusT * t * t * cp2[1] +
t * t * t * end[1],
];
},
getPoints(resolution) {
const points = [];
for (let i = 0; i <= resolution; i++) {
points.push(this.getPointAt(i / resolution));
}
return points;
},
});
currentPoint[0] = x;
currentPoint[1] = y;
},
/**
* Get points from the shape
* @param {*} divisions
* @returns
*/
getPoints(divisions) {
let last;
const points = [];

for (let i = 0; i < curves.length; i++) {
const curve = curves[i];
let resolution = divisions;

if (curve.curveType === 'EllipseCurve') {
resolution = divisions * 2;
} else if (curve.curveType === 'LineCurve') {
resolution = 1;
}

const pts = curve.getPoints(resolution);

for (let j = 0; j < pts.length; j++) {
const point = pts[j];
// eslint-disable-next-line no-continue
if (last && vtkMath.areEquals(last, point)) continue;
points.push(point);
last = point;
}
}

return points;
},

/**
* Extract points from the shape
* @param {*} divisions
* @returns
*/
extractPoints(divisions) {
const points = this.getPoints(divisions);
const holesPoints = this.holes.map((hole) => hole.getPoints(divisions));
return { shape: points, holes: holesPoints };
},
/**
* Defines if a given point is inside the polygon defines by the path
* @param {*} point
* @param {*} polygon
* @returns {boolean}
*/
isPointInside(point, polygon) {
const x = point[0];
const y = point[1];
let isInside = false;

for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi = polygon[i][0];
const yi = polygon[i][1];
const xj = polygon[j][0];
const yj = polygon[j][1];

const intersect =
yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
if (intersect) isInside = !isInside;
}

return isInside;
},
isIntersect(path) {
const pathA = this.getPoints(1, curves, false);
const pathB = path.getPoints(1);
return this.isPointInside(pathB[0], pathA);
},
};
}

/**
* Calculates the bounding box size for a set of shapes
* @param {Array} shapes - Array of shape objects
* @param {Number} depth - Depth of the 3D text
* @param {Number} curveSegments - Number of segments for curved paths
* @returns {Object} Object with min and max point coordinates
*/
function getBoundingSize(shapes, depth, curveSegments) {
const minPoint = [Infinity, Infinity, depth > 0 ? 0 : depth];
const maxPoint = [-Infinity, -Infinity, depth < 0 ? 0 : depth];

for (let i = 0; i < shapes.length; i++) {
const shape = shapes[i];
const shapePoints = shape.extractPoints(curveSegments);

for (let j = 0; j < shapePoints.shape.length; j++) {
const p = shapePoints.shape[j];
if (p[0] < minPoint[0]) minPoint[0] = p[0];
if (p[1] < minPoint[1]) minPoint[1] = p[1];
if (p[0] > maxPoint[0]) maxPoint[0] = p[0];
if (p[1] > maxPoint[1]) maxPoint[1] = p[1];
}
}

return { min: minPoint, max: maxPoint };
}

/**
* Removes duplicate end points in a points array
* @param {Array} points - Array of points
*/
function removeDupEndPoints(points) {
const l = points.length;
const isEqual = vtkMath.areEquals(points[l - 1], points[0]);
if (l > 2 && isEqual) {
points.pop();
}
}

/**
* Checks if the points are in a clockwise order
* @param {Array} points - Array of points [x, y]
* @returns {Boolean} True if points are in clockwise order
*/
function isClockWise(points) {
let sum = 0.0;
const n = points.length;
for (let p = n - 1, q = 0; q < n; p = q++) {
sum += points[p][0] * points[q][1] - points[q][0] * points[p][1];
}
// Positive signed area means counter-clockwise, so return true if area is negative
return sum * 0.5 < 0;
}

/**
* Computes the bevel vector for a point in a shape.
* @param {Array} pt - Current point [x, y]
* @param {Array} prev - Previous point [x, y]
* @param {Array} next - Next point [x, y]
* @returns {Array} Normalized bevel vector [x, y]
*/
function computeBevelVector(pt, prev, next) {
const vPrevX = pt[0] - prev[0];
const vPrevY = pt[1] - prev[1];
const vNextX = next[0] - pt[0];
const vNextY = next[1] - pt[1];

// Check collinearity
const cross = vPrevX * vNextY - vPrevY * vNextX;
let tx;
let ty;
let shrinkBy;

if (Math.abs(cross) > Number.EPSILON) {
// non‐collinear
const lenPrev = Math.hypot(vPrevX, vPrevY);
const lenNext = Math.hypot(vNextX, vNextY);

// shift prev and next perpendicular to themselves
const prevShiftX = prev[0] - vPrevY / lenPrev;
const prevShiftY = prev[1] + vPrevX / lenPrev;
const nextShiftX = next[0] - vNextY / lenNext;
const nextShiftY = next[1] + vNextX / lenNext;

// intersection factor
const sf =
((nextShiftX - prevShiftX) * vNextY -
(nextShiftY - prevShiftY) * vNextX) /
(vPrevX * vNextY - vPrevY * vNextX);

tx = prevShiftX + vPrevX * sf - pt[0];
ty = prevShiftY + vPrevY * sf - pt[1];

const lensq = tx * tx + ty * ty;
if (lensq <= 2) {
return [tx, ty];
}
shrinkBy = Math.sqrt(lensq / 2);
} else {
// collinear or opposing
const sameDir =
(vPrevX > 0 && vNextX > 0) ||
(vPrevX < 0 && vNextX < 0) ||
Math.sign(vPrevY) === Math.sign(vNextY);

if (sameDir) {
// perpendicular to prev
tx = -vPrevY;
ty = vPrevX;
shrinkBy = Math.hypot(vPrevX, vPrevY);
} else {
// just offset along prev
tx = vPrevX;
ty = vPrevY;
shrinkBy = Math.sqrt((vPrevX * vPrevX + vPrevY * vPrevY) / 2);
}
}

return [tx / shrinkBy, ty / shrinkBy];
}

/**
* Triangulates a shape with holes
* @param {Array} contour - Array of contour points
* @param {Array} holes - Array of hole paths
* @returns {Array} Array of triangle faces as arrays of indices
*/
function triangulateShape(earcut, contour, holes) {
const faces = [];
const vertices = [];
const holeIndices = [];

removeDupEndPoints(contour);

for (let i = 0; i < contour.length; i++) {
vertices.push(contour[i][0], contour[i][1]);
}

let holeIndex = contour.length;
holes.forEach(removeDupEndPoints);

for (let i = 0; i < holes.length; i++) {
holeIndices.push(holeIndex);

const hole = holes[i];
holeIndex += hole.length;

for (let j = 0; j < hole.length; j++) {
vertices.push(hole[j][0], hole[j][1]);
}
}

const triangles = earcut(vertices, holeIndices);
for (let i = 0; i < triangles.length; i += 3) {
faces.push(triangles.slice(i, i + 3));
}

return faces;
}

/**
* Scales a point along a vector
* @param {Array} pt - Point to scale [x, y]
* @param {Array} vec - Direction vector [x, y]
* @param {Number} size - Scale amount
* @returns {Array} Scaled point [x, y]
*/
function scalePoint(pt, vec, size) {
const rt = [pt[0], pt[1]];
rt[0] += vec[0] * size;
rt[1] += vec[1] * size;
return rt;
}

/**
* Creates triangle faces with specified indices
* @param {Array} layers - The layers array with vertex positions
* @param {Number} a - First index
* @param {Number} b - Second index
* @param {Number} c - Third index
* @param {Array} verticesArray - The output vertices array
* @param {Array} uvArray - The output UV array
* @param {Array} colorArray - The output color array
* @param {Array} color - The color [r, g, b]
* @param {Boolean} perFaceUV - Flag for per-face UV mapping
* @param {Number} faceIndex - Index of the face for UV mapping
*/
function addTriangle(
layers,
a,
b,
c,
verticesArray,
uvArray,
colorArray,
color
) {
const tri = [a, c, b];
tri.forEach((i) => {
verticesArray.push(layers[i * 3], layers[i * 3 + 1], layers[i * 3 + 2]);
});

const nextIndex = verticesArray.length / 3;
const uvs = computeFacesUV(
verticesArray,
nextIndex - 3,
nextIndex - 2,
nextIndex - 1
);

// Add each UV coordinate pair to the array
uvs.forEach((uv) => {
uvArray.push(uv[0], uv[1]);
});
if (colorArray && color) {
for (let i = 0; i < 3; ++i)
colorArray.push(color[0] * 255, color[1] * 255, color[2] * 255);
}
}

/**
* Creates quad faces with specified indices
* @param {Array} layers - The layers array with vertex positions
* @param {Number} a - First index
* @param {Number} b - Second index
* @param {Number} c - Third index
* @param {Number} d - Fourth index
* @param {Array} verticesArray - The output vertices array
* @param {Array} uvArray - The output UV array
* @param {Array} colorArray - The output color array
* @param {Array} color - The color [r, g, b]
*/
function addQuad(
layers,
a,
b,
c,
d,
verticesArray,
uvArray,
colorArray,
color
) {
const quad = [a, d, b, b, d, c];
quad.forEach((i) =>
verticesArray.push(layers[i * 3], layers[i * 3 + 1], layers[i * 3 + 2])
);

const nextIndex = verticesArray.length / 3;
const uvs = computeSidesUV(
verticesArray,
nextIndex - 6,
nextIndex - 3,
nextIndex - 2,
nextIndex - 1
);

// UV coordinates for both triangles of the quad
// First triangle
uvArray.push(uvs[0][0], uvs[0][1]);
uvArray.push(uvs[1][0], uvs[1][1]);
uvArray.push(uvs[3][0], uvs[3][1]);

// Second triangle
uvArray.push(uvs[1][0], uvs[1][1]);
uvArray.push(uvs[2][0], uvs[2][1]);
uvArray.push(uvs[3][0], uvs[3][1]);

if (colorArray && color) {
for (let i = 0; i < 6; ++i)
colorArray.push(color[0] * 255, color[1] * 255, color[2] * 255);
}
}

/**
* Creates the faces for the top and bottom of the 3D text
* @param {Array} layers - The layers array with vertex positions
* @param {Array} faces - The triangulated faces
* @param {Number} vlen - The number of vertices
* @param {Number} steps - The number of steps
* @param {Boolean} bevelEnabled - Whether bevel is enabled
* @param {Number} bevelSegments - Number of bevel segments
* @param {Array} verticesArray - The output vertices array
* @param {Array} uvArray - The output UV array
*/
function buildLidFaces(
layers,
faces,
vlen,
steps,
bevelEnabled,
bevelSegments,
verticesArray,
uvArray,
colorArray,
color
) {
if (bevelEnabled) {
let layer = 0;
let offset = vlen * layer; // Bottom faces
faces.forEach(([a, b, c]) => {
addTriangle(
layers,
c + offset,
b + offset,
a + offset,
verticesArray,
uvArray,
colorArray,
color
);
});

layer = steps + bevelSegments * 2;
offset = vlen * layer;

// Top faces
faces.forEach(([a, b, c]) => {
addTriangle(
layers,
a + offset,
b + offset,
c + offset,
verticesArray,
uvArray,
colorArray,
color
);
});
} else {
// Bottom faces
faces.forEach(([a, b, c]) => {
addTriangle(layers, c, b, a, verticesArray, uvArray, colorArray, color);
});

// Top faces
const offset = vlen * steps;
faces.forEach(([a, b, c]) => {
addTriangle(
layers,
a + offset,
b + offset,
c + offset,
verticesArray,
uvArray,
colorArray,
color
);
});
}
}

/**
* Creates side walls for contour or hole
* @param {Array} layers - The layers array
* @param {Array} contour - The contour points
* @param {Number} layerOffset - Offset for the layer
* @param {Number} vlen - The number of vertices
* @param {Number} steps - The number of steps
* @param {Number} bevelSegments - The number of bevel segments
* @param {Array} verticesArray - The output vertices array
* @param {Array} uvArray - The output UV array
*/
function buildWalls(
layers,
contour,
layerOffset,
vlen,
steps,
bevelSegments,
verticesArray,
uvArray,
colorArray,
color
) {
const totalLayers = steps + bevelSegments * 2;
for (let i = 0; i < contour.length; i++) {
const j = i;
const k = i === 0 ? contour.length - 1 : i - 1;

for (let s = 0; s < totalLayers; s++) {
const slen1 = vlen * s;
const slen2 = vlen * (s + 1);

const a = layerOffset + j + slen1;
const b = layerOffset + k + slen1;
const c = layerOffset + k + slen2;
const d = layerOffset + j + slen2;

addQuad(layers, a, b, c, d, verticesArray, uvArray, colorArray, color);
}
}
}

/**
* Builds the side faces of the 3D text
* @param {Array} layers - The layers array
* @param {Array} contour - The contour points
* @param {Array} holes - The holes
* @param {Number} vlen - The number of vertices
* @param {Number} steps - The number of steps
* @param {Number} bevelSegments - The number of bevel segments
* @param {Array} verticesArray - The output vertices array
* @param {Array} uvArray - The output UV array
*/
function buildSideFaces(
layers,
contour,
holes,
vlen,
steps,
bevelSegments,
verticesArray,
uvArray,
colorArray,
color
) {
let layerOffset = 0;
// Create contour walls
buildWalls(
layers,
contour,
layerOffset,
vlen,
steps,
bevelSegments,
verticesArray,
uvArray,
colorArray,
color
);
layerOffset += contour.length;

// Create hole walls
for (let i = 0; i < holes.length; i++) {
const ahole = holes[i];
buildWalls(
layers,
ahole,
layerOffset,
vlen,
steps,
bevelSegments,
verticesArray,
uvArray,
colorArray,
color
);
layerOffset += ahole.length;
}
}

export {
addTriangle,
addQuad,
buildLidFaces,
buildWalls,
buildSideFaces,
computeBevelVector,
computeFacesUV,
computeSidesUV,
createShapePath,
getBoundingSize,
isClockWise,
scalePoint,
triangulateShape,
};
index.d.ts
import { vtkAlgorithm, vtkObject } from '../../../interfaces';
import { Nullable, RGBColor } from '../../../types';

export interface IVectorTextInitialValues {
fontSize?: number;
text?: string;
depth?: number;
steps?: number;
bevelEnabled?: boolean;
curveSegments?: number;
bevelThickness?: number;
bevelSize?: number;
bevelOffset?: number;
bevelSegments?: number;
font?: any;
earcut?: any; // Earcut module for triangulation
perLetterFaceColors?: (letterIndex: number) => [number, number, number];
}

type vtkVectorTextBase = vtkObject & vtkAlgorithm;

export interface vtkVectorText extends vtkVectorTextBase {
/**
* Returns whether beveling is enabled.
*/
getBevelEnabled(): boolean;

/**
* Returns the number of segments used for the bevel geometry.
*/
getBevelSegments(): number;

/**
* Returns the size of the bevel.
*/
getBevelSize(): number;

/**
* Returns the thickness of the bevel.
*/
getBevelThickness(): number;

/**
* Returns the offset of the bevel.
*/
getBevelOffset(): number;

/**
* Returns the number of curve segments used for the text geometry.
*/
getCurveSegments(): number;

/**
* Returns the extrusion depth of the text.
*/
getDepth(): number;

/**
* Returns the current font size.
*/
getFontSize(): number;

/**
* Returns the number of steps used for the text geometry.
*/
getSteps(): number;

/**
* Returns the current text string.
*/
getText(): string;

/**
* Gets or sets the per-letter face color function.
* @param fn - Function mapping letter index to [r,g,b] color.
*/
getPerLetterFaceColors(): Nullable<(letterIndex: number) => RGBColor>;

/**
* Enables or disables beveling.
* @param bevelEnabled - True to enable beveling, false to disable.
*/
setBevelEnabled(bevelEnabled: boolean): boolean;

/**
* Sets the number of segments used for the bevel geometry.
* @param bevelSegments - The number of bevel segments.
*/
setBevelSegments(bevelSegments: number): boolean;

/**
* Sets the size of the bevel.
* @param bevelSize - The bevel size.
*/
setBevelSize(bevelSize: number): boolean;

/**
* Sets the thickness of the bevel.
* @param bevelThickness - The bevel thickness.
*/
setBevelThickness(bevelThickness: number): boolean;

/**
* Sets the offset of the bevel.
* @param bevelOffset - The bevel offset.
*/
setBevelOffset(bevelOffset: number): boolean;

/**
* Sets the number of curve segments used for the text geometry.
* @param curveSegments - The number of curve segments.
*/
setCurveSegments(curveSegments: number): boolean;

/**
* Sets the extrusion depth of the text.
* @param depth - The new depth value.
*/
setDepth(depth: number): boolean;

/**
* Sets the font object used for rendering the text.
* This should be a parsed font object from opentype.js.
* @param font - The font object.
*/
setFont(font: any): boolean;

/**
* Sets the font size.
* @param fontSize - The new font size.
*/
setFontSize(fontSize: number): boolean;

/**
* Sets the number of steps used for the text geometry.
* @param steps - The number of steps.
*/
setSteps(steps: number): boolean;

/**
* Sets the text string.
* @param text - The new text to display.
*/
setText(text: string): boolean;

/**
* Sets the per-letter face color function.
* @param fn - Function mapping letter index to [r,g,b] color.
*/
setPerLetterFaceColors(fn: (letterIndex: number) => RGBColor): boolean;
}

/**
* Method use to decorate a given object (publicAPI+model) with vtkVectorText characteristics.
*
* @param publicAPI object on which methods will be bounds (public)
* @param model object on which data structure will be bounds (protected)
* @param {IVectorTextInitialValues} [initialValues] (default: {})
*/
export function extend(
publicAPI: object,
model: object,
initialValues?: IVectorTextInitialValues
): void;

/**
* Method use to create a new instance of vtkVectorText
* @param {IVectorTextInitialValues} [initialValues] for pre-setting some of its content
*/
export function newInstance(
initialValues?: IVectorTextInitialValues
): vtkVectorText;

/**
* vtkVectorText generates vtkPolyData from an input string.
* The TTF file needs to be parsed using opentype.js and then passed to
* vtkVectorText via the setFont method.
*/
export declare const vtkVectorText: {
newInstance: typeof newInstance;
extend: typeof extend;
};
export default vtkVectorText;
index.js
import macro from 'vtk.js/Sources/macros';
import * as vtkMath from 'vtk.js/Sources/Common/Core/Math';
import vtkPolyData from 'vtk.js/Sources/Common/DataModel/PolyData';
import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray';
import vtkCellArray from 'vtk.js/Sources/Common/Core/CellArray';

import {
buildLidFaces,
buildSideFaces,
computeBevelVector,
createShapePath,
getBoundingSize,
isClockWise,
scalePoint,
triangulateShape,
} from './Utils';

const { vtkErrorMacro, vtkWarningMacro } = macro;

// ----------------------------------------------------------------------------
// vtkVectorText methods
// ----------------------------------------------------------------------------

function vtkVectorText(publicAPI, model) {
// Set our className
model.classHierarchy.push('vtkVectorText');

// -------------------------------------------------------------------------
// Private methods
// -------------------------------------------------------------------------

/**
* Process a shape into 3D geometry
* @param {Object} shape - The shape to process
* @param {Array} offsetSize - The offset size for positioning the shape
* @param {Array} letterColor - The color for the shape
*/
function addShape(shape, offsetSize, letterColor) {
// extract contour + holes, offset them
const curveSegments = model.curveSegments;
const steps = model.steps;
const depth = model.depth;

// Calculate bevel parameters
const bevelEnabled = model.bevelEnabled;
let bevelThickness = model.bevelThickness;
let bevelSize = bevelThickness - 0.1;
let bevelOffset = model.bevelOffset;
let bevelSegments = model.bevelSegments;

if (!bevelEnabled) {
bevelSegments = 0;
bevelThickness = 0;
bevelSize = 0;
bevelOffset = 0;
}

// Extract points from shape
const shapePoints = shape.extractPoints(curveSegments);
let vertices = shapePoints.shape;
const holes = shapePoints.holes;

// Offset points to the correct position
vertices.forEach((p) => {
p[0] += offsetSize[0];
p[1] += offsetSize[1];
});
holes.forEach((hole) => {
hole.forEach((p) => {
p[0] += offsetSize[0];
p[1] += offsetSize[1];
});
});

// Check if we have enough points to create a shape
if (vertices.length < 3) {
vtkWarningMacro('Not enough points to create a shape');
return;
}

// Triangulate the shape
const faces = triangulateShape(model.earcut, vertices, holes);
const contour = vertices;

// Combine all vertices (contour and holes)
vertices = [...vertices, ...holes.flat()];
const vlen = vertices.length;

// Calculate bevel vectors for the contour
const contourMovements = [];
for (
let i = 0, j = contour.length - 1, k = i + 1;
i < contour.length;
i++, j++, k++
) {
if (j === contour.length) j = 0;
if (k === contour.length) k = 0;
contourMovements[i] = computeBevelVector(
contour[i],
contour[j],
contour[k]
);
}

// Calculate bevel vectors for the holes
const holesMovements = [];
let oneHoleMovements;
let verticesMovements = [...contourMovements];
for (let h = 0, hl = holes.length; h < hl; h++) {
const ahole = holes[h];
oneHoleMovements = [];
for (
let i = 0, j = ahole.length - 1, k = i + 1;
i < ahole.length;
i++, j++, k++
) {
if (j === ahole.length) j = 0;
if (k === ahole.length) k = 0;
oneHoleMovements[i] = computeBevelVector(ahole[i], ahole[j], ahole[k]);
}
holesMovements.push(oneHoleMovements);
verticesMovements = [...verticesMovements, ...oneHoleMovements];
}

// Generate all the layers of points
const layers = [];

// Bottom bevel layers
for (let b = 0; b < bevelSegments; b++) {
const t = b / bevelSegments;
const z = bevelThickness * Math.cos((t * Math.PI) / 2);
const bs = bevelSize * Math.sin((t * Math.PI) / 2) + bevelOffset;

// Add points for contour and holes
for (let i = 0; i < contour.length; i++) {
const vert = scalePoint(contour[i], contourMovements[i], bs);
layers.push(vert[0], vert[1], -z + offsetSize[2]);
}

for (let h = 0, hl = holes.length; h < hl; h++) {
const ahole = holes[h];
oneHoleMovements = holesMovements[h];
for (let i = 0; i < ahole.length; i++) {
const vert = scalePoint(ahole[i], oneHoleMovements[i], bs);
layers.push(vert[0], vert[1], -z + offsetSize[2]);
}
}
}

// Base layer (z=0)
const bs = bevelSize + bevelOffset;
for (let i = 0; i < vlen; i++) {
const vert = bevelEnabled
? scalePoint(vertices[i], verticesMovements[i], bs)
: vertices[i];
layers.push(vert[0], vert[1], 0 + offsetSize[2]);
}

// Middle layers
for (let s = 1; s <= steps; s++) {
for (let i = 0; i < vlen; i++) {
const vert = bevelEnabled
? scalePoint(vertices[i], verticesMovements[i], bs)
: vertices[i];
layers.push(vert[0], vert[1], (depth / steps) * s + offsetSize[2]);
}
}

// Top bevel layers
for (let b = bevelSegments - 1; b >= 0; b--) {
const t = b / bevelSegments;
const z = bevelThickness * Math.cos((t * Math.PI) / 2);
const topBevelSize =
bevelSize * Math.sin((t * Math.PI) / 2) + bevelOffset;

for (let i = 0, il = contour.length; i < il; i++) {
const vert = scalePoint(contour[i], contourMovements[i], topBevelSize);
layers.push(vert[0], vert[1], depth + z + offsetSize[2]);
}

for (let h = 0, hl = holes.length; h < hl; h++) {
const ahole = holes[h];
oneHoleMovements = holesMovements[h];
for (let i = 0, il = ahole.length; i < il; i++) {
const vert = scalePoint(ahole[i], oneHoleMovements[i], topBevelSize);
layers.push(vert[0], vert[1], depth + z + offsetSize[2]);
}
}
}

// Build all the faces
buildLidFaces(
layers,
faces,
vlen,
steps,
bevelEnabled,
bevelSegments,
model.verticesArray,
model.uvArray,
model.colorArray,
letterColor
);
buildSideFaces(
layers,
contour,
holes,
vlen,
steps,
bevelSegments,
model.verticesArray,
model.uvArray,
model.colorArray,
letterColor
);
}

/**
* Creates shape paths from the font and text
*/
function buildShape() {
model.shapes = [];
if (!model.font || !model.text) {
return;
}

const path = model.font.getPath(model.text, 0, 0, model.fontSize);
if (!path || !path.commands || !path.commands.length) {
return;
}

let first;
let shapePath = createShapePath();
const commands = path.commands;

for (let i = 0; i < commands.length; i++) {
const command = commands[i];

// start a fresh shape if the previous one was closed
shapePath = shapePath || createShapePath();

switch (command.type) {
case 'M': // Move to
shapePath.moveTo(command.x, -command.y);
first = command;
break;

case 'L': // Line to
shapePath.lineTo(command.x, -command.y);
break;

case 'C': // Cubic bezier curve
shapePath.bezierCurveTo(
command.x1,
-command.y1,
command.x2,
-command.y2,
command.x,
-command.y
);
break;

case 'Q': // Quadratic bezier curve
shapePath.quadraticCurveTo(
command.x1,
-command.y1,
command.x,
-command.y
);
break;

case 'Z': // Close path
// Close the contour
shapePath.lineTo(first.x, -first.y);

// Determine if this path is a clockwise contour (shape) or a counter-clockwise hole
if (isClockWise(shapePath.getPoints(1))) {
model.shapes.push(shapePath);
} else {
// Find which shape this hole belongs to
for (let j = 0; j < model.shapes.length; j++) {
const shape = model.shapes[j];
if (shape.isIntersect(shapePath)) {
shape.holes.push(shapePath);
break;
}
}
}

// Mark for restart on next iteration
shapePath = null;
break;

default:
console.warn(`Unknown path command: ${command.type}`);
break;
}
}

// If there's an unclosed shape, add it
if (shapePath) {
model.shapes.push(shapePath);
}
}

/**
* Creates a vtkPolyData from the processed shapes
* @returns {Object} vtkPolyData instance
*/
function buildPolyData(polyData) {
model.verticesArray = [];
model.uvArray = [];
model.colorArray = [];
const cells = vtkCellArray.newInstance();
const pointData = polyData.getPointData();

// Calculate the bounding box to center the text
const boundingSize = getBoundingSize(
model.shapes,
model.depth,
model.curveSegments
);
const offsetSize = [0, 0, 0];
vtkMath.subtract(boundingSize.min, boundingSize.max, offsetSize);

// Process each shape
let letterIndex = 0;
model.shapes.forEach((shape) => {
let color = null;
if (model.perLetterFaceColors) {
color = model.perLetterFaceColors(letterIndex) || [1, 1, 1];
}
addShape(shape, offsetSize, color);
letterIndex++;
});

// Create triangle indices
const vertexCount = model.verticesArray.length / 3;
const indices = [];

// Generate indices for triangles
for (let i = 0; i < vertexCount; i += 3) {
indices.push(i, i + 2, i + 1);
}

// Create cells for polydata
const cellSize = indices.length;
cells.resize(cellSize + cellSize / 3); // Allocate space for cells (+1 for size per cell)

// Add triangles to cells
for (let i = 0; i < indices.length; i += 3) {
cells.insertNextCell([indices[i], indices[i + 1], indices[i + 2]]);
}

polyData.setPolys(cells);

// Set points (vertices)
polyData.getPoints().setData(Float32Array.from(model.verticesArray), 3);

// Set texture coordinates
const da = vtkDataArray.newInstance({
numberOfComponents: 2,
values: Float32Array.from(model.uvArray),
name: 'TEXCOORD_0',
});
pointData.addArray(da);
pointData.setActiveTCoords(da.getName());

// Set color array if present
if (model.colorArray && model.colorArray.length) {
const ca = vtkDataArray.newInstance({
numberOfComponents: 3,
values: Uint8Array.from(model.colorArray),
name: 'Colors',
});
pointData.addArray(ca);
pointData.setActiveScalars(ca.getName());
}

return polyData;
}

// -------------------------------------------------------------------------
// Public methods
// -------------------------------------------------------------------------

/**
* Handles the request to generate vector text data
* @param {Object} inData - Input data (not used)
* @param {Object} outData - Output data target
*/
publicAPI.requestData = (inData, outData) => {
if (!model.font) {
vtkErrorMacro(
'Font object not set, make sure the TTF file is parsed using opentype.js.'
);
return;
}

if (!model.text) {
vtkErrorMacro('Text not set. Cannot generate vector text.');
return;
}

buildShape();
const polyData = outData[0]?.initialize() || vtkPolyData.newInstance();
buildPolyData(polyData);
outData[0] = polyData;
};
}

// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------

/**
* Default values for the VectorText model
* shapes: Array to store shape paths
* verticesArray: Array of vertex coordinates
* uvArray: Array of texture coordinates
* font: Font object (from opentype.js)
* earcut: Earcut module for triangulation
* fontSize: Font size in points
* depth: Depth of the extruded text
* steps: Number of steps in extrusion (for curved surfaces)
* bevelEnabled: Whether to add beveled edges
* curveSegments: Number of segments for curved paths
* bevelThickness: Thickness of the bevel
* bevelSize: Size of the bevel
* bevelOffset: Offset of the bevel
* bevelSegments: Number of segments in the bevel
* text: The text to render
* perLetterFaceColors: Function to get per-letter face colors
*/
const DEFAULT_VALUES = {
shapes: [],
verticesArray: [],
uvArray: [],
font: null,
earcut: null, // Earcut module for triangulation
fontSize: 10,
depth: 1,
steps: 1,
bevelEnabled: false,
curveSegments: 12,
bevelThickness: 0.2,
bevelSize: 0.1,
bevelOffset: 0,
bevelSegments: 1,
text: null,
perLetterFaceColors: null, // (letterIndex: number) => [r,g,b]
};

export function extend(publicAPI, model, initialValues = {}) {
Object.assign(model, DEFAULT_VALUES, initialValues);

// Object methods
macro.obj(publicAPI, model);

macro.algo(publicAPI, model, 0, 1);

// Build VTK API with automatic getters/setters
macro.setGet(publicAPI, model, [
'fontSize',
'text',
'depth',
'steps',
'bevelEnabled',
'curveSegments',
'bevelThickness',
'bevelSize',
'bevelOffset',
'bevelSegments',
'perLetterFaceColors',
]);

macro.set(publicAPI, model, ['font']);

vtkVectorText(publicAPI, model);
}

export const newInstance = macro.newInstance(extend, 'vtkVectorText');

export default { newInstance, extend };