@kitware/vtk.js
Version:
Visualization Toolkit for the Web
397 lines (354 loc) • 13.4 kB
JavaScript
import { m as macro } from '../../macros2.js';
import { s as subtract } from '../../Common/Core/Math/index.js';
import vtkPolyData from '../../Common/DataModel/PolyData.js';
import vtkDataArray from '../../Common/Core/DataArray.js';
import vtkCellArray from '../../Common/Core/CellArray.js';
import { isClockWise, getBoundingSize, triangulateShape, computeBevelVector, scalePoint, buildLidFaces, buildSideFaces, createShapePath } from './VectorText/Utils.js';
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];
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]
};
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);
}
const newInstance = macro.newInstance(extend, 'vtkVectorText');
var vtkVector = {
newInstance,
extend
};
export { vtkVector as default, extend, newInstance };