@kitware/vtk.js 34.0.0 → 34.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Rendering/Core/VectorText/Utils.js +520 -0
- package/Rendering/Core/VectorText.d.ts +183 -0
- package/Rendering/Core/VectorText.js +397 -0
- package/Rendering/Core.js +2 -0
- package/Rendering/OpenGL/Texture.d.ts +8 -1
- package/Rendering/OpenGL/Texture.js +80 -18
- package/index.d.ts +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
import { D as areEquals } from '../../../Common/Core/Math/index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Computes UV coordinates for top/bottom faces
|
|
5
|
+
* @param {Array} vertices - The vertices array
|
|
6
|
+
* @param {Number} iA - First index
|
|
7
|
+
* @param {Number} iB - Second index
|
|
8
|
+
* @param {Number} iC - Third index
|
|
9
|
+
* @returns {Array} Array of UV coordinates
|
|
10
|
+
*/
|
|
11
|
+
function computeFacesUV(vertices, iA, iB, iC) {
|
|
12
|
+
const ax = vertices[iA * 3];
|
|
13
|
+
const ay = vertices[iA * 3 + 1];
|
|
14
|
+
const bx = vertices[iB * 3];
|
|
15
|
+
const by = vertices[iB * 3 + 1];
|
|
16
|
+
const cx = vertices[iC * 3];
|
|
17
|
+
const cy = vertices[iC * 3 + 1];
|
|
18
|
+
return [[ax, ay], [bx, by], [cx, cy]];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Computes UV coordinates for side walls
|
|
23
|
+
* @param {Array} vertices - The vertices array
|
|
24
|
+
* @param {Number} iA - First index
|
|
25
|
+
* @param {Number} iB - Second index
|
|
26
|
+
* @param {Number} iC - Third index
|
|
27
|
+
* @param {Number} iD - Fourth index
|
|
28
|
+
* @returns {Array} Array of UV coordinates
|
|
29
|
+
*/
|
|
30
|
+
function computeSidesUV(vertices, iA, iB, iC, iD) {
|
|
31
|
+
const ax = vertices[iA * 3];
|
|
32
|
+
const ay = vertices[iA * 3 + 1];
|
|
33
|
+
const az = vertices[iA * 3 + 2];
|
|
34
|
+
const bx = vertices[iB * 3];
|
|
35
|
+
const by = vertices[iB * 3 + 1];
|
|
36
|
+
const bz = vertices[iB * 3 + 2];
|
|
37
|
+
const cx = vertices[iC * 3];
|
|
38
|
+
const cy = vertices[iC * 3 + 1];
|
|
39
|
+
const cz = vertices[iC * 3 + 2];
|
|
40
|
+
const dx = vertices[iD * 3];
|
|
41
|
+
const dy = vertices[iD * 3 + 1];
|
|
42
|
+
const dz = vertices[iD * 3 + 2];
|
|
43
|
+
|
|
44
|
+
// Determine the best UV mapping direction based on geometry
|
|
45
|
+
if (Math.abs(ay - by) < Math.abs(ax - bx)) {
|
|
46
|
+
return [[ax, 1 - az], [bx, 1 - bz], [cx, 1 - cz], [dx, 1 - dz]];
|
|
47
|
+
}
|
|
48
|
+
return [[ay, 1 - az], [by, 1 - bz], [cy, 1 - cz], [dy, 1 - dz]];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Creates a shape path object with methods for path operations
|
|
53
|
+
* @returns {Object} A shape path object with methods for manipulating paths
|
|
54
|
+
*/
|
|
55
|
+
function createShapePath() {
|
|
56
|
+
const curves = [];
|
|
57
|
+
const currentPoint = [0, 0];
|
|
58
|
+
const holes = [];
|
|
59
|
+
return {
|
|
60
|
+
curves,
|
|
61
|
+
currentPoint,
|
|
62
|
+
holes,
|
|
63
|
+
moveTo(x, y) {
|
|
64
|
+
currentPoint[0] = x;
|
|
65
|
+
currentPoint[1] = y;
|
|
66
|
+
},
|
|
67
|
+
lineTo(x, y) {
|
|
68
|
+
const start = [...currentPoint];
|
|
69
|
+
const end = [x, y];
|
|
70
|
+
curves.push({
|
|
71
|
+
curveType: 'LineCurve',
|
|
72
|
+
start,
|
|
73
|
+
end,
|
|
74
|
+
getPointAt(t) {
|
|
75
|
+
return [start[0] + t * (end[0] - start[0]), start[1] + t * (end[1] - start[1])];
|
|
76
|
+
},
|
|
77
|
+
getPoints(resolution) {
|
|
78
|
+
const points = [];
|
|
79
|
+
for (let i = 0; i <= resolution; i++) {
|
|
80
|
+
points.push(this.getPointAt(i / resolution));
|
|
81
|
+
}
|
|
82
|
+
return points;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
currentPoint[0] = x;
|
|
86
|
+
currentPoint[1] = y;
|
|
87
|
+
},
|
|
88
|
+
quadraticCurveTo(cpX, cpY, x, y) {
|
|
89
|
+
const start = [...currentPoint];
|
|
90
|
+
const end = [x, y];
|
|
91
|
+
const cp = [cpX, cpY];
|
|
92
|
+
curves.push({
|
|
93
|
+
curveType: 'QuadraticBezierCurve',
|
|
94
|
+
cp,
|
|
95
|
+
start,
|
|
96
|
+
end,
|
|
97
|
+
getPointAt(t) {
|
|
98
|
+
const oneMinusT = 1 - t;
|
|
99
|
+
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]];
|
|
100
|
+
},
|
|
101
|
+
getPoints(resolution) {
|
|
102
|
+
const points = [];
|
|
103
|
+
for (let i = 0; i <= resolution; i++) {
|
|
104
|
+
points.push(this.getPointAt(i / resolution));
|
|
105
|
+
}
|
|
106
|
+
return points;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
currentPoint[0] = x;
|
|
110
|
+
currentPoint[1] = y;
|
|
111
|
+
},
|
|
112
|
+
bezierCurveTo(cp1X, cp1Y, cp2X, cp2Y, x, y) {
|
|
113
|
+
const start = [...currentPoint];
|
|
114
|
+
const end = [x, y];
|
|
115
|
+
const cp1 = [cp1X, cp1Y];
|
|
116
|
+
const cp2 = [cp2X, cp2Y];
|
|
117
|
+
curves.push({
|
|
118
|
+
curveType: 'BezierCurve',
|
|
119
|
+
cp1,
|
|
120
|
+
cp2,
|
|
121
|
+
start,
|
|
122
|
+
end,
|
|
123
|
+
getPointAt(t) {
|
|
124
|
+
const oneMinusT = 1 - t;
|
|
125
|
+
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]];
|
|
126
|
+
},
|
|
127
|
+
getPoints(resolution) {
|
|
128
|
+
const points = [];
|
|
129
|
+
for (let i = 0; i <= resolution; i++) {
|
|
130
|
+
points.push(this.getPointAt(i / resolution));
|
|
131
|
+
}
|
|
132
|
+
return points;
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
currentPoint[0] = x;
|
|
136
|
+
currentPoint[1] = y;
|
|
137
|
+
},
|
|
138
|
+
/**
|
|
139
|
+
* Get points from the shape
|
|
140
|
+
* @param {*} divisions
|
|
141
|
+
* @returns
|
|
142
|
+
*/
|
|
143
|
+
getPoints(divisions) {
|
|
144
|
+
let last;
|
|
145
|
+
const points = [];
|
|
146
|
+
for (let i = 0; i < curves.length; i++) {
|
|
147
|
+
const curve = curves[i];
|
|
148
|
+
let resolution = divisions;
|
|
149
|
+
if (curve.curveType === 'EllipseCurve') {
|
|
150
|
+
resolution = divisions * 2;
|
|
151
|
+
} else if (curve.curveType === 'LineCurve') {
|
|
152
|
+
resolution = 1;
|
|
153
|
+
}
|
|
154
|
+
const pts = curve.getPoints(resolution);
|
|
155
|
+
for (let j = 0; j < pts.length; j++) {
|
|
156
|
+
const point = pts[j];
|
|
157
|
+
// eslint-disable-next-line no-continue
|
|
158
|
+
if (last && areEquals(last, point)) continue;
|
|
159
|
+
points.push(point);
|
|
160
|
+
last = point;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return points;
|
|
164
|
+
},
|
|
165
|
+
/**
|
|
166
|
+
* Extract points from the shape
|
|
167
|
+
* @param {*} divisions
|
|
168
|
+
* @returns
|
|
169
|
+
*/
|
|
170
|
+
extractPoints(divisions) {
|
|
171
|
+
const points = this.getPoints(divisions);
|
|
172
|
+
const holesPoints = this.holes.map(hole => hole.getPoints(divisions));
|
|
173
|
+
return {
|
|
174
|
+
shape: points,
|
|
175
|
+
holes: holesPoints
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
/**
|
|
179
|
+
* Defines if a given point is inside the polygon defines by the path
|
|
180
|
+
* @param {*} point
|
|
181
|
+
* @param {*} polygon
|
|
182
|
+
* @returns {boolean}
|
|
183
|
+
*/
|
|
184
|
+
isPointInside(point, polygon) {
|
|
185
|
+
const x = point[0];
|
|
186
|
+
const y = point[1];
|
|
187
|
+
let isInside = false;
|
|
188
|
+
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
|
189
|
+
const xi = polygon[i][0];
|
|
190
|
+
const yi = polygon[i][1];
|
|
191
|
+
const xj = polygon[j][0];
|
|
192
|
+
const yj = polygon[j][1];
|
|
193
|
+
const intersect = yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi;
|
|
194
|
+
if (intersect) isInside = !isInside;
|
|
195
|
+
}
|
|
196
|
+
return isInside;
|
|
197
|
+
},
|
|
198
|
+
isIntersect(path) {
|
|
199
|
+
const pathA = this.getPoints(1, curves, false);
|
|
200
|
+
const pathB = path.getPoints(1);
|
|
201
|
+
return this.isPointInside(pathB[0], pathA);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Calculates the bounding box size for a set of shapes
|
|
208
|
+
* @param {Array} shapes - Array of shape objects
|
|
209
|
+
* @param {Number} depth - Depth of the 3D text
|
|
210
|
+
* @param {Number} curveSegments - Number of segments for curved paths
|
|
211
|
+
* @returns {Object} Object with min and max point coordinates
|
|
212
|
+
*/
|
|
213
|
+
function getBoundingSize(shapes, depth, curveSegments) {
|
|
214
|
+
const minPoint = [Infinity, Infinity, depth > 0 ? 0 : depth];
|
|
215
|
+
const maxPoint = [-Infinity, -Infinity, depth < 0 ? 0 : depth];
|
|
216
|
+
for (let i = 0; i < shapes.length; i++) {
|
|
217
|
+
const shape = shapes[i];
|
|
218
|
+
const shapePoints = shape.extractPoints(curveSegments);
|
|
219
|
+
for (let j = 0; j < shapePoints.shape.length; j++) {
|
|
220
|
+
const p = shapePoints.shape[j];
|
|
221
|
+
if (p[0] < minPoint[0]) minPoint[0] = p[0];
|
|
222
|
+
if (p[1] < minPoint[1]) minPoint[1] = p[1];
|
|
223
|
+
if (p[0] > maxPoint[0]) maxPoint[0] = p[0];
|
|
224
|
+
if (p[1] > maxPoint[1]) maxPoint[1] = p[1];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
min: minPoint,
|
|
229
|
+
max: maxPoint
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Removes duplicate end points in a points array
|
|
235
|
+
* @param {Array} points - Array of points
|
|
236
|
+
*/
|
|
237
|
+
function removeDupEndPoints(points) {
|
|
238
|
+
const l = points.length;
|
|
239
|
+
const isEqual = areEquals(points[l - 1], points[0]);
|
|
240
|
+
if (l > 2 && isEqual) {
|
|
241
|
+
points.pop();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Checks if the points are in a clockwise order
|
|
247
|
+
* @param {Array} points - Array of points [x, y]
|
|
248
|
+
* @returns {Boolean} True if points are in clockwise order
|
|
249
|
+
*/
|
|
250
|
+
function isClockWise(points) {
|
|
251
|
+
let sum = 0.0;
|
|
252
|
+
const n = points.length;
|
|
253
|
+
for (let p = n - 1, q = 0; q < n; p = q++) {
|
|
254
|
+
sum += points[p][0] * points[q][1] - points[q][0] * points[p][1];
|
|
255
|
+
}
|
|
256
|
+
// Positive signed area means counter-clockwise, so return true if area is negative
|
|
257
|
+
return sum * 0.5 < 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Computes the bevel vector for a point in a shape.
|
|
262
|
+
* @param {Array} pt - Current point [x, y]
|
|
263
|
+
* @param {Array} prev - Previous point [x, y]
|
|
264
|
+
* @param {Array} next - Next point [x, y]
|
|
265
|
+
* @returns {Array} Normalized bevel vector [x, y]
|
|
266
|
+
*/
|
|
267
|
+
function computeBevelVector(pt, prev, next) {
|
|
268
|
+
const vPrevX = pt[0] - prev[0];
|
|
269
|
+
const vPrevY = pt[1] - prev[1];
|
|
270
|
+
const vNextX = next[0] - pt[0];
|
|
271
|
+
const vNextY = next[1] - pt[1];
|
|
272
|
+
|
|
273
|
+
// Check collinearity
|
|
274
|
+
const cross = vPrevX * vNextY - vPrevY * vNextX;
|
|
275
|
+
let tx;
|
|
276
|
+
let ty;
|
|
277
|
+
let shrinkBy;
|
|
278
|
+
if (Math.abs(cross) > Number.EPSILON) {
|
|
279
|
+
// non‐collinear
|
|
280
|
+
const lenPrev = Math.hypot(vPrevX, vPrevY);
|
|
281
|
+
const lenNext = Math.hypot(vNextX, vNextY);
|
|
282
|
+
|
|
283
|
+
// shift prev and next perpendicular to themselves
|
|
284
|
+
const prevShiftX = prev[0] - vPrevY / lenPrev;
|
|
285
|
+
const prevShiftY = prev[1] + vPrevX / lenPrev;
|
|
286
|
+
const nextShiftX = next[0] - vNextY / lenNext;
|
|
287
|
+
const nextShiftY = next[1] + vNextX / lenNext;
|
|
288
|
+
|
|
289
|
+
// intersection factor
|
|
290
|
+
const sf = ((nextShiftX - prevShiftX) * vNextY - (nextShiftY - prevShiftY) * vNextX) / (vPrevX * vNextY - vPrevY * vNextX);
|
|
291
|
+
tx = prevShiftX + vPrevX * sf - pt[0];
|
|
292
|
+
ty = prevShiftY + vPrevY * sf - pt[1];
|
|
293
|
+
const lensq = tx * tx + ty * ty;
|
|
294
|
+
if (lensq <= 2) {
|
|
295
|
+
return [tx, ty];
|
|
296
|
+
}
|
|
297
|
+
shrinkBy = Math.sqrt(lensq / 2);
|
|
298
|
+
} else {
|
|
299
|
+
// collinear or opposing
|
|
300
|
+
const sameDir = vPrevX > 0 && vNextX > 0 || vPrevX < 0 && vNextX < 0 || Math.sign(vPrevY) === Math.sign(vNextY);
|
|
301
|
+
if (sameDir) {
|
|
302
|
+
// perpendicular to prev
|
|
303
|
+
tx = -vPrevY;
|
|
304
|
+
ty = vPrevX;
|
|
305
|
+
shrinkBy = Math.hypot(vPrevX, vPrevY);
|
|
306
|
+
} else {
|
|
307
|
+
// just offset along prev
|
|
308
|
+
tx = vPrevX;
|
|
309
|
+
ty = vPrevY;
|
|
310
|
+
shrinkBy = Math.sqrt((vPrevX * vPrevX + vPrevY * vPrevY) / 2);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return [tx / shrinkBy, ty / shrinkBy];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Triangulates a shape with holes
|
|
318
|
+
* @param {Array} contour - Array of contour points
|
|
319
|
+
* @param {Array} holes - Array of hole paths
|
|
320
|
+
* @returns {Array} Array of triangle faces as arrays of indices
|
|
321
|
+
*/
|
|
322
|
+
function triangulateShape(earcut, contour, holes) {
|
|
323
|
+
const faces = [];
|
|
324
|
+
const vertices = [];
|
|
325
|
+
const holeIndices = [];
|
|
326
|
+
removeDupEndPoints(contour);
|
|
327
|
+
for (let i = 0; i < contour.length; i++) {
|
|
328
|
+
vertices.push(contour[i][0], contour[i][1]);
|
|
329
|
+
}
|
|
330
|
+
let holeIndex = contour.length;
|
|
331
|
+
holes.forEach(removeDupEndPoints);
|
|
332
|
+
for (let i = 0; i < holes.length; i++) {
|
|
333
|
+
holeIndices.push(holeIndex);
|
|
334
|
+
const hole = holes[i];
|
|
335
|
+
holeIndex += hole.length;
|
|
336
|
+
for (let j = 0; j < hole.length; j++) {
|
|
337
|
+
vertices.push(hole[j][0], hole[j][1]);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const triangles = earcut(vertices, holeIndices);
|
|
341
|
+
for (let i = 0; i < triangles.length; i += 3) {
|
|
342
|
+
faces.push(triangles.slice(i, i + 3));
|
|
343
|
+
}
|
|
344
|
+
return faces;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Scales a point along a vector
|
|
349
|
+
* @param {Array} pt - Point to scale [x, y]
|
|
350
|
+
* @param {Array} vec - Direction vector [x, y]
|
|
351
|
+
* @param {Number} size - Scale amount
|
|
352
|
+
* @returns {Array} Scaled point [x, y]
|
|
353
|
+
*/
|
|
354
|
+
function scalePoint(pt, vec, size) {
|
|
355
|
+
const rt = [pt[0], pt[1]];
|
|
356
|
+
rt[0] += vec[0] * size;
|
|
357
|
+
rt[1] += vec[1] * size;
|
|
358
|
+
return rt;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Creates triangle faces with specified indices
|
|
363
|
+
* @param {Array} layers - The layers array with vertex positions
|
|
364
|
+
* @param {Number} a - First index
|
|
365
|
+
* @param {Number} b - Second index
|
|
366
|
+
* @param {Number} c - Third index
|
|
367
|
+
* @param {Array} verticesArray - The output vertices array
|
|
368
|
+
* @param {Array} uvArray - The output UV array
|
|
369
|
+
* @param {Array} colorArray - The output color array
|
|
370
|
+
* @param {Array} color - The color [r, g, b]
|
|
371
|
+
* @param {Boolean} perFaceUV - Flag for per-face UV mapping
|
|
372
|
+
* @param {Number} faceIndex - Index of the face for UV mapping
|
|
373
|
+
*/
|
|
374
|
+
function addTriangle(layers, a, b, c, verticesArray, uvArray, colorArray, color) {
|
|
375
|
+
const tri = [a, c, b];
|
|
376
|
+
tri.forEach(i => {
|
|
377
|
+
verticesArray.push(layers[i * 3], layers[i * 3 + 1], layers[i * 3 + 2]);
|
|
378
|
+
});
|
|
379
|
+
const nextIndex = verticesArray.length / 3;
|
|
380
|
+
const uvs = computeFacesUV(verticesArray, nextIndex - 3, nextIndex - 2, nextIndex - 1);
|
|
381
|
+
|
|
382
|
+
// Add each UV coordinate pair to the array
|
|
383
|
+
uvs.forEach(uv => {
|
|
384
|
+
uvArray.push(uv[0], uv[1]);
|
|
385
|
+
});
|
|
386
|
+
if (colorArray && color) {
|
|
387
|
+
for (let i = 0; i < 3; ++i) colorArray.push(color[0], color[1], color[2]);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Creates quad faces with specified indices
|
|
393
|
+
* @param {Array} layers - The layers array with vertex positions
|
|
394
|
+
* @param {Number} a - First index
|
|
395
|
+
* @param {Number} b - Second index
|
|
396
|
+
* @param {Number} c - Third index
|
|
397
|
+
* @param {Number} d - Fourth index
|
|
398
|
+
* @param {Array} verticesArray - The output vertices array
|
|
399
|
+
* @param {Array} uvArray - The output UV array
|
|
400
|
+
* @param {Array} colorArray - The output color array
|
|
401
|
+
* @param {Array} color - The color [r, g, b]
|
|
402
|
+
*/
|
|
403
|
+
function addQuad(layers, a, b, c, d, verticesArray, uvArray, colorArray, color) {
|
|
404
|
+
const quad = [a, d, b, b, d, c];
|
|
405
|
+
quad.forEach(i => verticesArray.push(layers[i * 3], layers[i * 3 + 1], layers[i * 3 + 2]));
|
|
406
|
+
const nextIndex = verticesArray.length / 3;
|
|
407
|
+
const uvs = computeSidesUV(verticesArray, nextIndex - 6, nextIndex - 3, nextIndex - 2, nextIndex - 1);
|
|
408
|
+
|
|
409
|
+
// UV coordinates for both triangles of the quad
|
|
410
|
+
// First triangle
|
|
411
|
+
uvArray.push(uvs[0][0], uvs[0][1]);
|
|
412
|
+
uvArray.push(uvs[1][0], uvs[1][1]);
|
|
413
|
+
uvArray.push(uvs[3][0], uvs[3][1]);
|
|
414
|
+
|
|
415
|
+
// Second triangle
|
|
416
|
+
uvArray.push(uvs[1][0], uvs[1][1]);
|
|
417
|
+
uvArray.push(uvs[2][0], uvs[2][1]);
|
|
418
|
+
uvArray.push(uvs[3][0], uvs[3][1]);
|
|
419
|
+
if (colorArray && color) {
|
|
420
|
+
for (let i = 0; i < 6; ++i) colorArray.push(color[0], color[1], color[2]);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Creates the faces for the top and bottom of the 3D text
|
|
426
|
+
* @param {Array} layers - The layers array with vertex positions
|
|
427
|
+
* @param {Array} faces - The triangulated faces
|
|
428
|
+
* @param {Number} vlen - The number of vertices
|
|
429
|
+
* @param {Number} steps - The number of steps
|
|
430
|
+
* @param {Boolean} bevelEnabled - Whether bevel is enabled
|
|
431
|
+
* @param {Number} bevelSegments - Number of bevel segments
|
|
432
|
+
* @param {Array} verticesArray - The output vertices array
|
|
433
|
+
* @param {Array} uvArray - The output UV array
|
|
434
|
+
*/
|
|
435
|
+
function buildLidFaces(layers, faces, vlen, steps, bevelEnabled, bevelSegments, verticesArray, uvArray, colorArray, color) {
|
|
436
|
+
if (bevelEnabled) {
|
|
437
|
+
let layer = 0;
|
|
438
|
+
let offset = vlen * layer; // Bottom faces
|
|
439
|
+
faces.forEach(_ref => {
|
|
440
|
+
let [a, b, c] = _ref;
|
|
441
|
+
addTriangle(layers, c + offset, b + offset, a + offset, verticesArray, uvArray, colorArray, color);
|
|
442
|
+
});
|
|
443
|
+
layer = steps + bevelSegments * 2;
|
|
444
|
+
offset = vlen * layer;
|
|
445
|
+
|
|
446
|
+
// Top faces
|
|
447
|
+
faces.forEach(_ref2 => {
|
|
448
|
+
let [a, b, c] = _ref2;
|
|
449
|
+
addTriangle(layers, a + offset, b + offset, c + offset, verticesArray, uvArray, colorArray, color);
|
|
450
|
+
});
|
|
451
|
+
} else {
|
|
452
|
+
// Bottom faces
|
|
453
|
+
faces.forEach(_ref3 => {
|
|
454
|
+
let [a, b, c] = _ref3;
|
|
455
|
+
addTriangle(layers, c, b, a, verticesArray, uvArray, colorArray, color);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// Top faces
|
|
459
|
+
const offset = vlen * steps;
|
|
460
|
+
faces.forEach(_ref4 => {
|
|
461
|
+
let [a, b, c] = _ref4;
|
|
462
|
+
addTriangle(layers, a + offset, b + offset, c + offset, verticesArray, uvArray, colorArray, color);
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Creates side walls for contour or hole
|
|
469
|
+
* @param {Array} layers - The layers array
|
|
470
|
+
* @param {Array} contour - The contour points
|
|
471
|
+
* @param {Number} layerOffset - Offset for the layer
|
|
472
|
+
* @param {Number} vlen - The number of vertices
|
|
473
|
+
* @param {Number} steps - The number of steps
|
|
474
|
+
* @param {Number} bevelSegments - The number of bevel segments
|
|
475
|
+
* @param {Array} verticesArray - The output vertices array
|
|
476
|
+
* @param {Array} uvArray - The output UV array
|
|
477
|
+
*/
|
|
478
|
+
function buildWalls(layers, contour, layerOffset, vlen, steps, bevelSegments, verticesArray, uvArray, colorArray, color) {
|
|
479
|
+
const totalLayers = steps + bevelSegments * 2;
|
|
480
|
+
for (let i = 0; i < contour.length; i++) {
|
|
481
|
+
const j = i;
|
|
482
|
+
const k = i === 0 ? contour.length - 1 : i - 1;
|
|
483
|
+
for (let s = 0; s < totalLayers; s++) {
|
|
484
|
+
const slen1 = vlen * s;
|
|
485
|
+
const slen2 = vlen * (s + 1);
|
|
486
|
+
const a = layerOffset + j + slen1;
|
|
487
|
+
const b = layerOffset + k + slen1;
|
|
488
|
+
const c = layerOffset + k + slen2;
|
|
489
|
+
const d = layerOffset + j + slen2;
|
|
490
|
+
addQuad(layers, a, b, c, d, verticesArray, uvArray, colorArray, color);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Builds the side faces of the 3D text
|
|
497
|
+
* @param {Array} layers - The layers array
|
|
498
|
+
* @param {Array} contour - The contour points
|
|
499
|
+
* @param {Array} holes - The holes
|
|
500
|
+
* @param {Number} vlen - The number of vertices
|
|
501
|
+
* @param {Number} steps - The number of steps
|
|
502
|
+
* @param {Number} bevelSegments - The number of bevel segments
|
|
503
|
+
* @param {Array} verticesArray - The output vertices array
|
|
504
|
+
* @param {Array} uvArray - The output UV array
|
|
505
|
+
*/
|
|
506
|
+
function buildSideFaces(layers, contour, holes, vlen, steps, bevelSegments, verticesArray, uvArray, colorArray, color) {
|
|
507
|
+
let layerOffset = 0;
|
|
508
|
+
// Create contour walls
|
|
509
|
+
buildWalls(layers, contour, layerOffset, vlen, steps, bevelSegments, verticesArray, uvArray, colorArray, color);
|
|
510
|
+
layerOffset += contour.length;
|
|
511
|
+
|
|
512
|
+
// Create hole walls
|
|
513
|
+
for (let i = 0; i < holes.length; i++) {
|
|
514
|
+
const ahole = holes[i];
|
|
515
|
+
buildWalls(layers, ahole, layerOffset, vlen, steps, bevelSegments, verticesArray, uvArray, colorArray, color);
|
|
516
|
+
layerOffset += ahole.length;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export { addQuad, addTriangle, buildLidFaces, buildSideFaces, buildWalls, computeBevelVector, computeFacesUV, computeSidesUV, createShapePath, getBoundingSize, isClockWise, scalePoint, triangulateShape };
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { vtkAlgorithm, vtkObject } from './../../interfaces';
|
|
2
|
+
import { Nullable, RGBColor } from './../../types';
|
|
3
|
+
|
|
4
|
+
export interface IVectorTextInitialValues {
|
|
5
|
+
fontSize?: number;
|
|
6
|
+
text?: string;
|
|
7
|
+
depth?: number;
|
|
8
|
+
steps?: number;
|
|
9
|
+
bevelEnabled?: boolean;
|
|
10
|
+
curveSegments?: number;
|
|
11
|
+
bevelThickness?: number;
|
|
12
|
+
bevelSize?: number;
|
|
13
|
+
bevelOffset?: number;
|
|
14
|
+
bevelSegments?: number;
|
|
15
|
+
font?: any;
|
|
16
|
+
earcut?: any; // Earcut module for triangulation
|
|
17
|
+
perLetterFaceColors?: (letterIndex: number) => [number, number, number];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type vtkVectorTextBase = vtkObject & vtkAlgorithm;
|
|
21
|
+
|
|
22
|
+
export interface vtkVectorText extends vtkVectorTextBase {
|
|
23
|
+
/**
|
|
24
|
+
* Returns whether beveling is enabled.
|
|
25
|
+
*/
|
|
26
|
+
getBevelEnabled(): boolean;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns the number of segments used for the bevel geometry.
|
|
30
|
+
*/
|
|
31
|
+
getBevelSegments(): number;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Returns the size of the bevel.
|
|
35
|
+
*/
|
|
36
|
+
getBevelSize(): number;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Returns the thickness of the bevel.
|
|
40
|
+
*/
|
|
41
|
+
getBevelThickness(): number;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns the offset of the bevel.
|
|
45
|
+
*/
|
|
46
|
+
getBevelOffset(): number;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Returns the number of curve segments used for the text geometry.
|
|
50
|
+
*/
|
|
51
|
+
getCurveSegments(): number;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Returns the extrusion depth of the text.
|
|
55
|
+
*/
|
|
56
|
+
getDepth(): number;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Returns the current font size.
|
|
60
|
+
*/
|
|
61
|
+
getFontSize(): number;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Returns the number of steps used for the text geometry.
|
|
65
|
+
*/
|
|
66
|
+
getSteps(): number;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Returns the current text string.
|
|
70
|
+
*/
|
|
71
|
+
getText(): string;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Gets or sets the per-letter face color function.
|
|
75
|
+
* @param fn - Function mapping letter index to [r,g,b] color.
|
|
76
|
+
*/
|
|
77
|
+
getPerLetterFaceColors(): Nullable<(letterIndex: number) => RGBColor>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Enables or disables beveling.
|
|
81
|
+
* @param bevelEnabled - True to enable beveling, false to disable.
|
|
82
|
+
*/
|
|
83
|
+
setBevelEnabled(bevelEnabled: boolean): boolean;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Sets the number of segments used for the bevel geometry.
|
|
87
|
+
* @param bevelSegments - The number of bevel segments.
|
|
88
|
+
*/
|
|
89
|
+
setBevelSegments(bevelSegments: number): boolean;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Sets the size of the bevel.
|
|
93
|
+
* @param bevelSize - The bevel size.
|
|
94
|
+
*/
|
|
95
|
+
setBevelSize(bevelSize: number): boolean;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Sets the thickness of the bevel.
|
|
99
|
+
* @param bevelThickness - The bevel thickness.
|
|
100
|
+
*/
|
|
101
|
+
setBevelThickness(bevelThickness: number): boolean;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Sets the offset of the bevel.
|
|
105
|
+
* @param bevelOffset - The bevel offset.
|
|
106
|
+
*/
|
|
107
|
+
setBevelOffset(bevelOffset: number): boolean;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Sets the number of curve segments used for the text geometry.
|
|
111
|
+
* @param curveSegments - The number of curve segments.
|
|
112
|
+
*/
|
|
113
|
+
setCurveSegments(curveSegments: number): boolean;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Sets the extrusion depth of the text.
|
|
117
|
+
* @param depth - The new depth value.
|
|
118
|
+
*/
|
|
119
|
+
setDepth(depth: number): boolean;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Sets the font object used for rendering the text.
|
|
123
|
+
* This should be a parsed font object from opentype.js.
|
|
124
|
+
* @param font - The font object.
|
|
125
|
+
*/
|
|
126
|
+
setFont(font: any): boolean;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Sets the font size.
|
|
130
|
+
* @param fontSize - The new font size.
|
|
131
|
+
*/
|
|
132
|
+
setFontSize(fontSize: number): boolean;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Sets the number of steps used for the text geometry.
|
|
136
|
+
* @param steps - The number of steps.
|
|
137
|
+
*/
|
|
138
|
+
setSteps(steps: number): boolean;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Sets the text string.
|
|
142
|
+
* @param text - The new text to display.
|
|
143
|
+
*/
|
|
144
|
+
setText(text: string): boolean;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Sets the per-letter face color function.
|
|
148
|
+
* @param fn - Function mapping letter index to [r,g,b] color.
|
|
149
|
+
*/
|
|
150
|
+
setPerLetterFaceColors(fn: (letterIndex: number) => RGBColor): boolean;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Method use to decorate a given object (publicAPI+model) with vtkVectorText characteristics.
|
|
155
|
+
*
|
|
156
|
+
* @param publicAPI object on which methods will be bounds (public)
|
|
157
|
+
* @param model object on which data structure will be bounds (protected)
|
|
158
|
+
* @param {IVectorTextInitialValues} [initialValues] (default: {})
|
|
159
|
+
*/
|
|
160
|
+
export function extend(
|
|
161
|
+
publicAPI: object,
|
|
162
|
+
model: object,
|
|
163
|
+
initialValues?: IVectorTextInitialValues
|
|
164
|
+
): void;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Method use to create a new instance of vtkVectorText
|
|
168
|
+
* @param {IVectorTextInitialValues} [initialValues] for pre-setting some of its content
|
|
169
|
+
*/
|
|
170
|
+
export function newInstance(
|
|
171
|
+
initialValues?: IVectorTextInitialValues
|
|
172
|
+
): vtkVectorText;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* vtkVectorText generates vtkPolyData from an input string.
|
|
176
|
+
* The TTF file needs to be parsed using opentype.js and then passed to
|
|
177
|
+
* vtkVectorText via the setFont method.
|
|
178
|
+
*/
|
|
179
|
+
export declare const vtkVectorText: {
|
|
180
|
+
newInstance: typeof newInstance;
|
|
181
|
+
extend: typeof extend;
|
|
182
|
+
};
|
|
183
|
+
export default vtkVectorText;
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { m as macro } from '../../macros2.js';
|
|
2
|
+
import { s as subtract } from '../../Common/Core/Math/index.js';
|
|
3
|
+
import vtkPolyData from '../../Common/DataModel/PolyData.js';
|
|
4
|
+
import vtkDataArray from '../../Common/Core/DataArray.js';
|
|
5
|
+
import vtkCellArray from '../../Common/Core/CellArray.js';
|
|
6
|
+
import { isClockWise, getBoundingSize, triangulateShape, computeBevelVector, scalePoint, buildLidFaces, buildSideFaces, createShapePath } from './VectorText/Utils.js';
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
vtkErrorMacro,
|
|
10
|
+
vtkWarningMacro
|
|
11
|
+
} = macro;
|
|
12
|
+
|
|
13
|
+
// ----------------------------------------------------------------------------
|
|
14
|
+
// vtkVectorText methods
|
|
15
|
+
// ----------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
function vtkVectorText(publicAPI, model) {
|
|
18
|
+
// Set our className
|
|
19
|
+
model.classHierarchy.push('vtkVectorText');
|
|
20
|
+
|
|
21
|
+
// -------------------------------------------------------------------------
|
|
22
|
+
// Private methods
|
|
23
|
+
// -------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Process a shape into 3D geometry
|
|
27
|
+
* @param {Object} shape - The shape to process
|
|
28
|
+
* @param {Array} offsetSize - The offset size for positioning the shape
|
|
29
|
+
* @param {Array} letterColor - The color for the shape
|
|
30
|
+
*/
|
|
31
|
+
function addShape(shape, offsetSize, letterColor) {
|
|
32
|
+
// extract contour + holes, offset them
|
|
33
|
+
const curveSegments = model.curveSegments;
|
|
34
|
+
const steps = model.steps;
|
|
35
|
+
const depth = model.depth;
|
|
36
|
+
|
|
37
|
+
// Calculate bevel parameters
|
|
38
|
+
const bevelEnabled = model.bevelEnabled;
|
|
39
|
+
let bevelThickness = model.bevelThickness;
|
|
40
|
+
let bevelSize = bevelThickness - 0.1;
|
|
41
|
+
let bevelOffset = model.bevelOffset;
|
|
42
|
+
let bevelSegments = model.bevelSegments;
|
|
43
|
+
if (!bevelEnabled) {
|
|
44
|
+
bevelSegments = 0;
|
|
45
|
+
bevelThickness = 0;
|
|
46
|
+
bevelSize = 0;
|
|
47
|
+
bevelOffset = 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Extract points from shape
|
|
51
|
+
const shapePoints = shape.extractPoints(curveSegments);
|
|
52
|
+
let vertices = shapePoints.shape;
|
|
53
|
+
const holes = shapePoints.holes;
|
|
54
|
+
|
|
55
|
+
// Offset points to the correct position
|
|
56
|
+
vertices.forEach(p => {
|
|
57
|
+
p[0] += offsetSize[0];
|
|
58
|
+
p[1] += offsetSize[1];
|
|
59
|
+
});
|
|
60
|
+
holes.forEach(hole => {
|
|
61
|
+
hole.forEach(p => {
|
|
62
|
+
p[0] += offsetSize[0];
|
|
63
|
+
p[1] += offsetSize[1];
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Check if we have enough points to create a shape
|
|
68
|
+
if (vertices.length < 3) {
|
|
69
|
+
vtkWarningMacro('Not enough points to create a shape');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Triangulate the shape
|
|
74
|
+
const faces = triangulateShape(model.earcut, vertices, holes);
|
|
75
|
+
const contour = vertices;
|
|
76
|
+
|
|
77
|
+
// Combine all vertices (contour and holes)
|
|
78
|
+
vertices = [...vertices, ...holes.flat()];
|
|
79
|
+
const vlen = vertices.length;
|
|
80
|
+
|
|
81
|
+
// Calculate bevel vectors for the contour
|
|
82
|
+
const contourMovements = [];
|
|
83
|
+
for (let i = 0, j = contour.length - 1, k = i + 1; i < contour.length; i++, j++, k++) {
|
|
84
|
+
if (j === contour.length) j = 0;
|
|
85
|
+
if (k === contour.length) k = 0;
|
|
86
|
+
contourMovements[i] = computeBevelVector(contour[i], contour[j], contour[k]);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Calculate bevel vectors for the holes
|
|
90
|
+
const holesMovements = [];
|
|
91
|
+
let oneHoleMovements;
|
|
92
|
+
let verticesMovements = [...contourMovements];
|
|
93
|
+
for (let h = 0, hl = holes.length; h < hl; h++) {
|
|
94
|
+
const ahole = holes[h];
|
|
95
|
+
oneHoleMovements = [];
|
|
96
|
+
for (let i = 0, j = ahole.length - 1, k = i + 1; i < ahole.length; i++, j++, k++) {
|
|
97
|
+
if (j === ahole.length) j = 0;
|
|
98
|
+
if (k === ahole.length) k = 0;
|
|
99
|
+
oneHoleMovements[i] = computeBevelVector(ahole[i], ahole[j], ahole[k]);
|
|
100
|
+
}
|
|
101
|
+
holesMovements.push(oneHoleMovements);
|
|
102
|
+
verticesMovements = [...verticesMovements, ...oneHoleMovements];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Generate all the layers of points
|
|
106
|
+
const layers = [];
|
|
107
|
+
|
|
108
|
+
// Bottom bevel layers
|
|
109
|
+
for (let b = 0; b < bevelSegments; b++) {
|
|
110
|
+
const t = b / bevelSegments;
|
|
111
|
+
const z = bevelThickness * Math.cos(t * Math.PI / 2);
|
|
112
|
+
const bs = bevelSize * Math.sin(t * Math.PI / 2) + bevelOffset;
|
|
113
|
+
|
|
114
|
+
// Add points for contour and holes
|
|
115
|
+
for (let i = 0; i < contour.length; i++) {
|
|
116
|
+
const vert = scalePoint(contour[i], contourMovements[i], bs);
|
|
117
|
+
layers.push(vert[0], vert[1], -z + offsetSize[2]);
|
|
118
|
+
}
|
|
119
|
+
for (let h = 0, hl = holes.length; h < hl; h++) {
|
|
120
|
+
const ahole = holes[h];
|
|
121
|
+
oneHoleMovements = holesMovements[h];
|
|
122
|
+
for (let i = 0; i < ahole.length; i++) {
|
|
123
|
+
const vert = scalePoint(ahole[i], oneHoleMovements[i], bs);
|
|
124
|
+
layers.push(vert[0], vert[1], -z + offsetSize[2]);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Base layer (z=0)
|
|
130
|
+
const bs = bevelSize + bevelOffset;
|
|
131
|
+
for (let i = 0; i < vlen; i++) {
|
|
132
|
+
const vert = bevelEnabled ? scalePoint(vertices[i], verticesMovements[i], bs) : vertices[i];
|
|
133
|
+
layers.push(vert[0], vert[1], 0 + offsetSize[2]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Middle layers
|
|
137
|
+
for (let s = 1; s <= steps; s++) {
|
|
138
|
+
for (let i = 0; i < vlen; i++) {
|
|
139
|
+
const vert = bevelEnabled ? scalePoint(vertices[i], verticesMovements[i], bs) : vertices[i];
|
|
140
|
+
layers.push(vert[0], vert[1], depth / steps * s + offsetSize[2]);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Top bevel layers
|
|
145
|
+
for (let b = bevelSegments - 1; b >= 0; b--) {
|
|
146
|
+
const t = b / bevelSegments;
|
|
147
|
+
const z = bevelThickness * Math.cos(t * Math.PI / 2);
|
|
148
|
+
const topBevelSize = bevelSize * Math.sin(t * Math.PI / 2) + bevelOffset;
|
|
149
|
+
for (let i = 0, il = contour.length; i < il; i++) {
|
|
150
|
+
const vert = scalePoint(contour[i], contourMovements[i], topBevelSize);
|
|
151
|
+
layers.push(vert[0], vert[1], depth + z + offsetSize[2]);
|
|
152
|
+
}
|
|
153
|
+
for (let h = 0, hl = holes.length; h < hl; h++) {
|
|
154
|
+
const ahole = holes[h];
|
|
155
|
+
oneHoleMovements = holesMovements[h];
|
|
156
|
+
for (let i = 0, il = ahole.length; i < il; i++) {
|
|
157
|
+
const vert = scalePoint(ahole[i], oneHoleMovements[i], topBevelSize);
|
|
158
|
+
layers.push(vert[0], vert[1], depth + z + offsetSize[2]);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Build all the faces
|
|
164
|
+
buildLidFaces(layers, faces, vlen, steps, bevelEnabled, bevelSegments, model.verticesArray, model.uvArray, model.colorArray, letterColor);
|
|
165
|
+
buildSideFaces(layers, contour, holes, vlen, steps, bevelSegments, model.verticesArray, model.uvArray, model.colorArray, letterColor);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Creates shape paths from the font and text
|
|
170
|
+
*/
|
|
171
|
+
function buildShape() {
|
|
172
|
+
model.shapes = [];
|
|
173
|
+
if (!model.font || !model.text) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const path = model.font.getPath(model.text, 0, 0, model.fontSize);
|
|
177
|
+
if (!path || !path.commands || !path.commands.length) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
let first;
|
|
181
|
+
let shapePath = createShapePath();
|
|
182
|
+
const commands = path.commands;
|
|
183
|
+
for (let i = 0; i < commands.length; i++) {
|
|
184
|
+
const command = commands[i];
|
|
185
|
+
|
|
186
|
+
// start a fresh shape if the previous one was closed
|
|
187
|
+
shapePath = shapePath || createShapePath();
|
|
188
|
+
switch (command.type) {
|
|
189
|
+
case 'M':
|
|
190
|
+
// Move to
|
|
191
|
+
shapePath.moveTo(command.x, -command.y);
|
|
192
|
+
first = command;
|
|
193
|
+
break;
|
|
194
|
+
case 'L':
|
|
195
|
+
// Line to
|
|
196
|
+
shapePath.lineTo(command.x, -command.y);
|
|
197
|
+
break;
|
|
198
|
+
case 'C':
|
|
199
|
+
// Cubic bezier curve
|
|
200
|
+
shapePath.bezierCurveTo(command.x1, -command.y1, command.x2, -command.y2, command.x, -command.y);
|
|
201
|
+
break;
|
|
202
|
+
case 'Q':
|
|
203
|
+
// Quadratic bezier curve
|
|
204
|
+
shapePath.quadraticCurveTo(command.x1, -command.y1, command.x, -command.y);
|
|
205
|
+
break;
|
|
206
|
+
case 'Z':
|
|
207
|
+
// Close path
|
|
208
|
+
// Close the contour
|
|
209
|
+
shapePath.lineTo(first.x, -first.y);
|
|
210
|
+
|
|
211
|
+
// Determine if this path is a clockwise contour (shape) or a counter-clockwise hole
|
|
212
|
+
if (isClockWise(shapePath.getPoints(1))) {
|
|
213
|
+
model.shapes.push(shapePath);
|
|
214
|
+
} else {
|
|
215
|
+
// Find which shape this hole belongs to
|
|
216
|
+
for (let j = 0; j < model.shapes.length; j++) {
|
|
217
|
+
const shape = model.shapes[j];
|
|
218
|
+
if (shape.isIntersect(shapePath)) {
|
|
219
|
+
shape.holes.push(shapePath);
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Mark for restart on next iteration
|
|
226
|
+
shapePath = null;
|
|
227
|
+
break;
|
|
228
|
+
default:
|
|
229
|
+
console.warn(`Unknown path command: ${command.type}`);
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// If there's an unclosed shape, add it
|
|
235
|
+
if (shapePath) {
|
|
236
|
+
model.shapes.push(shapePath);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Creates a vtkPolyData from the processed shapes
|
|
242
|
+
* @returns {Object} vtkPolyData instance
|
|
243
|
+
*/
|
|
244
|
+
function buildPolyData() {
|
|
245
|
+
model.verticesArray = [];
|
|
246
|
+
model.uvArray = [];
|
|
247
|
+
model.colorArray = [];
|
|
248
|
+
const polyData = vtkPolyData.newInstance();
|
|
249
|
+
const cells = vtkCellArray.newInstance();
|
|
250
|
+
const pointData = polyData.getPointData();
|
|
251
|
+
|
|
252
|
+
// Calculate the bounding box to center the text
|
|
253
|
+
const boundingSize = getBoundingSize(model.shapes, model.depth, model.curveSegments);
|
|
254
|
+
const offsetSize = [0, 0, 0];
|
|
255
|
+
subtract(boundingSize.min, boundingSize.max, offsetSize);
|
|
256
|
+
|
|
257
|
+
// Process each shape
|
|
258
|
+
let letterIndex = 0;
|
|
259
|
+
model.shapes.forEach(shape => {
|
|
260
|
+
let color = null;
|
|
261
|
+
if (typeof model.perLetterFaceColors === 'function') {
|
|
262
|
+
color = model.perLetterFaceColors(letterIndex) || [1, 1, 1];
|
|
263
|
+
}
|
|
264
|
+
addShape(shape, offsetSize, color);
|
|
265
|
+
letterIndex++;
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Create triangle indices
|
|
269
|
+
const vertexCount = model.verticesArray.length / 3;
|
|
270
|
+
const indices = [];
|
|
271
|
+
|
|
272
|
+
// Generate indices for triangles
|
|
273
|
+
for (let i = 0; i < vertexCount; i += 3) {
|
|
274
|
+
indices.push(i, i + 2, i + 1);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Create cells for polydata
|
|
278
|
+
const cellSize = indices.length;
|
|
279
|
+
cells.resize(cellSize + cellSize / 3); // Allocate space for cells (+1 for size per cell)
|
|
280
|
+
|
|
281
|
+
// Add triangles to cells
|
|
282
|
+
for (let i = 0; i < indices.length; i += 3) {
|
|
283
|
+
cells.insertNextCell([indices[i], indices[i + 1], indices[i + 2]]);
|
|
284
|
+
}
|
|
285
|
+
polyData.setPolys(cells);
|
|
286
|
+
|
|
287
|
+
// Set points (vertices)
|
|
288
|
+
polyData.getPoints().setData(new Float32Array(model.verticesArray), 3);
|
|
289
|
+
|
|
290
|
+
// Set texture coordinates
|
|
291
|
+
const da = vtkDataArray.newInstance({
|
|
292
|
+
numberOfComponents: 2,
|
|
293
|
+
values: new Float32Array(model.uvArray),
|
|
294
|
+
name: 'TEXCOORD_0'
|
|
295
|
+
});
|
|
296
|
+
pointData.addArray(da);
|
|
297
|
+
pointData.setActiveTCoords(da.getName());
|
|
298
|
+
|
|
299
|
+
// Set color array if present
|
|
300
|
+
if (model.colorArray && model.colorArray.length) {
|
|
301
|
+
const ca = vtkDataArray.newInstance({
|
|
302
|
+
numberOfComponents: 3,
|
|
303
|
+
values: new Float32Array(model.colorArray),
|
|
304
|
+
name: 'Colors'
|
|
305
|
+
});
|
|
306
|
+
pointData.addArray(ca);
|
|
307
|
+
pointData.setActiveScalars(ca.getName());
|
|
308
|
+
}
|
|
309
|
+
return polyData;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// -------------------------------------------------------------------------
|
|
313
|
+
// Public methods
|
|
314
|
+
// -------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Handles the request to generate vector text data
|
|
318
|
+
* @param {Object} inData - Input data (not used)
|
|
319
|
+
* @param {Object} outData - Output data target
|
|
320
|
+
*/
|
|
321
|
+
publicAPI.requestData = (inData, outData) => {
|
|
322
|
+
if (!model.font) {
|
|
323
|
+
vtkErrorMacro('Font object not set, make sure the TTF file is parsed using opentype.js.');
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (!model.text) {
|
|
327
|
+
vtkErrorMacro('Text not set. Cannot generate vector text.');
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
buildShape();
|
|
331
|
+
outData[0] = buildPolyData();
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ----------------------------------------------------------------------------
|
|
336
|
+
// Object factory
|
|
337
|
+
// ----------------------------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Default values for the VectorText model
|
|
341
|
+
* shapes: Array to store shape paths
|
|
342
|
+
* verticesArray: Array of vertex coordinates
|
|
343
|
+
* uvArray: Array of texture coordinates
|
|
344
|
+
* font: Font object (from opentype.js)
|
|
345
|
+
* earcut: Earcut module for triangulation
|
|
346
|
+
* fontSize: Font size in points
|
|
347
|
+
* depth: Depth of the extruded text
|
|
348
|
+
* steps: Number of steps in extrusion (for curved surfaces)
|
|
349
|
+
* bevelEnabled: Whether to add beveled edges
|
|
350
|
+
* curveSegments: Number of segments for curved paths
|
|
351
|
+
* bevelThickness: Thickness of the bevel
|
|
352
|
+
* bevelSize: Size of the bevel
|
|
353
|
+
* bevelOffset: Offset of the bevel
|
|
354
|
+
* bevelSegments: Number of segments in the bevel
|
|
355
|
+
* text: The text to render
|
|
356
|
+
* perLetterFaceColors: Function to get per-letter face colors
|
|
357
|
+
*/
|
|
358
|
+
const DEFAULT_VALUES = {
|
|
359
|
+
shapes: [],
|
|
360
|
+
verticesArray: [],
|
|
361
|
+
uvArray: [],
|
|
362
|
+
font: null,
|
|
363
|
+
earcut: null,
|
|
364
|
+
// Earcut module for triangulation
|
|
365
|
+
fontSize: 10,
|
|
366
|
+
depth: 1,
|
|
367
|
+
steps: 1,
|
|
368
|
+
bevelEnabled: false,
|
|
369
|
+
curveSegments: 12,
|
|
370
|
+
bevelThickness: 0.2,
|
|
371
|
+
bevelSize: 0.1,
|
|
372
|
+
bevelOffset: 0,
|
|
373
|
+
bevelSegments: 1,
|
|
374
|
+
text: null,
|
|
375
|
+
perLetterFaceColors: null // (letterIndex: number) => [r,g,b]
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
function extend(publicAPI, model) {
|
|
379
|
+
let initialValues = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
|
|
380
|
+
Object.assign(model, DEFAULT_VALUES, initialValues);
|
|
381
|
+
|
|
382
|
+
// Object methods
|
|
383
|
+
macro.obj(publicAPI, model);
|
|
384
|
+
macro.algo(publicAPI, model, 0, 1);
|
|
385
|
+
|
|
386
|
+
// Build VTK API with automatic getters/setters
|
|
387
|
+
macro.setGet(publicAPI, model, ['fontSize', 'text', 'depth', 'steps', 'bevelEnabled', 'curveSegments', 'bevelThickness', 'bevelSize', 'bevelOffset', 'bevelSegments', 'perLetterFaceColors']);
|
|
388
|
+
macro.set(publicAPI, model, ['font']);
|
|
389
|
+
vtkVectorText(publicAPI, model);
|
|
390
|
+
}
|
|
391
|
+
const newInstance = macro.newInstance(extend, 'vtkVectorText');
|
|
392
|
+
var vtkVector = {
|
|
393
|
+
newInstance,
|
|
394
|
+
extend
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
export { vtkVector as default, extend, newInstance };
|
package/Rendering/Core.js
CHANGED
|
@@ -38,6 +38,7 @@ import vtkSkybox from './Core/Skybox.js';
|
|
|
38
38
|
import vtkSphereMapper from './Core/SphereMapper.js';
|
|
39
39
|
import vtkStickMapper from './Core/StickMapper.js';
|
|
40
40
|
import vtkTexture from './Core/Texture.js';
|
|
41
|
+
import vtkVector from './Core/VectorText.js';
|
|
41
42
|
import vtkViewport from './Core/Viewport.js';
|
|
42
43
|
import vtkVolume from './Core/Volume.js';
|
|
43
44
|
import vtkVolumeMapper from './Core/VolumeMapper.js';
|
|
@@ -87,6 +88,7 @@ var Core = {
|
|
|
87
88
|
vtkSphereMapper,
|
|
88
89
|
vtkStickMapper,
|
|
89
90
|
vtkTexture,
|
|
91
|
+
vtkVector,
|
|
90
92
|
vtkViewport,
|
|
91
93
|
vtkVolume,
|
|
92
94
|
vtkVolumeMapper,
|
|
@@ -246,7 +246,14 @@ export interface vtkOpenGLTexture extends vtkViewNode {
|
|
|
246
246
|
* @param image The image to use for the texture.
|
|
247
247
|
* @returns {boolean} True if the texture was successfully created, false otherwise.
|
|
248
248
|
*/
|
|
249
|
-
create2DFromImage(image:
|
|
249
|
+
create2DFromImage(image: HTMLImageElement): boolean;
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Creates a 2D texture from an ImageBitmap.
|
|
253
|
+
* @param imageBitmap The ImageBitmap to use for the texture.
|
|
254
|
+
* @returns {boolean} True if the texture was successfully created, false otherwise.
|
|
255
|
+
*/
|
|
256
|
+
create2DFromImageBitmap(imageBitmap: ImageBitmap): boolean;
|
|
250
257
|
|
|
251
258
|
/**
|
|
252
259
|
* Creates a 2D filterable texture from raw data, with a preference for size over accuracy if necessary.
|
|
@@ -75,6 +75,19 @@ function vtkOpenGLTexture(publicAPI, model) {
|
|
|
75
75
|
}
|
|
76
76
|
// create the texture if it is not done already
|
|
77
77
|
if (!model.handle || model.renderable.getMTime() > model.textureBuildTime.getMTime()) {
|
|
78
|
+
if (model.renderable.getImageBitmap() !== null) {
|
|
79
|
+
if (model.renderable.getInterpolate()) {
|
|
80
|
+
model.generateMipmap = true;
|
|
81
|
+
publicAPI.setMinificationFilter(Filter.LINEAR_MIPMAP_LINEAR);
|
|
82
|
+
}
|
|
83
|
+
// Have an Image which may not be complete
|
|
84
|
+
if (model.renderable.getImageBitmap() && model.renderable.getImageLoaded()) {
|
|
85
|
+
publicAPI.create2DFromImageBitmap(model.renderable.getImageBitmap());
|
|
86
|
+
publicAPI.activate();
|
|
87
|
+
publicAPI.sendParameters();
|
|
88
|
+
model.textureBuildTime.modified();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
78
91
|
// if we have an Image
|
|
79
92
|
if (model.renderable.getImage() !== null) {
|
|
80
93
|
if (model.renderable.getInterpolate()) {
|
|
@@ -1013,7 +1026,7 @@ function vtkOpenGLTexture(publicAPI, model) {
|
|
|
1013
1026
|
|
|
1014
1027
|
//----------------------------------------------------------------------------
|
|
1015
1028
|
publicAPI.create2DFromImage = image => {
|
|
1016
|
-
//
|
|
1029
|
+
// Determine the texture parameters using the arguments.
|
|
1017
1030
|
publicAPI.getOpenGLDataType(VtkDataTypes.UNSIGNED_CHAR);
|
|
1018
1031
|
publicAPI.getInternalFormat(VtkDataTypes.UNSIGNED_CHAR, 4);
|
|
1019
1032
|
publicAPI.getFormat(VtkDataTypes.UNSIGNED_CHAR, 4);
|
|
@@ -1028,30 +1041,79 @@ function vtkOpenGLTexture(publicAPI, model) {
|
|
|
1028
1041
|
model._openGLRenderWindow.activateTexture(publicAPI);
|
|
1029
1042
|
publicAPI.createTexture();
|
|
1030
1043
|
publicAPI.bind();
|
|
1044
|
+
const needNearestPowerOfTwo = !model._openGLRenderWindow.getWebgl2() && (!isPowerOfTwo(image.width) || !isPowerOfTwo(image.height));
|
|
1045
|
+
let textureSource = image;
|
|
1046
|
+
let targetWidth = image.width;
|
|
1047
|
+
let targetHeight = image.height;
|
|
1048
|
+
let flipY = true;
|
|
1049
|
+
|
|
1050
|
+
// For WebGL1, we need to scale the image to the nearest power of two
|
|
1051
|
+
// dimensions if the image is not already a power of two. For WebGL2, we can
|
|
1052
|
+
// use the image as is. Note: Chrome has a perf issue where the path
|
|
1053
|
+
// HTMLImageElement -> Canvas -> texSubImage2D is faster than
|
|
1054
|
+
// HTMLImageElement -> texSubImage2D directly. See
|
|
1055
|
+
// https://issues.chromium.org/issues/41311312#comment7
|
|
1056
|
+
// Tested on Chrome 137.0.7151.104 Windows 11
|
|
1057
|
+
const isChrome = window.chrome;
|
|
1058
|
+
if (needNearestPowerOfTwo || isChrome) {
|
|
1059
|
+
const canvas = new OffscreenCanvas(nearestPowerOfTwo(image.width), nearestPowerOfTwo(image.height));
|
|
1060
|
+
targetWidth = canvas.width;
|
|
1061
|
+
targetHeight = canvas.height;
|
|
1062
|
+
const ctx = canvas.getContext('2d');
|
|
1063
|
+
ctx.translate(0, canvas.height);
|
|
1064
|
+
ctx.scale(1, -1);
|
|
1065
|
+
ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height);
|
|
1066
|
+
textureSource = canvas;
|
|
1067
|
+
flipY = false; // we are flipping the image manually using translate/scale
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
model.width = targetWidth;
|
|
1071
|
+
model.height = targetHeight;
|
|
1031
1072
|
|
|
1032
1073
|
// Source texture data from the PBO.
|
|
1033
|
-
|
|
1074
|
+
model.context.pixelStorei(model.context.UNPACK_FLIP_Y_WEBGL, flipY);
|
|
1034
1075
|
model.context.pixelStorei(model.context.UNPACK_ALIGNMENT, 1);
|
|
1076
|
+
if (useTexStorage(VtkDataTypes.UNSIGNED_CHAR)) {
|
|
1077
|
+
model.context.texStorage2D(model.target, 1, model.internalFormat, model.width, model.height);
|
|
1078
|
+
model.context.texSubImage2D(model.target, 0, 0, 0, model.width, model.height, model.format, model.openGLDataType, textureSource);
|
|
1079
|
+
} else {
|
|
1080
|
+
model.context.texImage2D(model.target, 0, model.internalFormat, model.width, model.height, 0, model.format, model.openGLDataType, textureSource);
|
|
1081
|
+
}
|
|
1082
|
+
if (model.generateMipmap) {
|
|
1083
|
+
model.context.generateMipmap(model.target);
|
|
1084
|
+
}
|
|
1085
|
+
model.allocatedGPUMemoryInBytes = model.width * model.height * model.depth * model.components * model._openGLRenderWindow.getDefaultTextureByteSize(VtkDataTypes.UNSIGNED_CHAR, getNorm16Ext(), publicAPI.useHalfFloat());
|
|
1086
|
+
publicAPI.deactivate();
|
|
1087
|
+
return true;
|
|
1088
|
+
};
|
|
1035
1089
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
model.
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1090
|
+
//----------------------------------------------------------------------------
|
|
1091
|
+
publicAPI.create2DFromImageBitmap = imageBitmap => {
|
|
1092
|
+
// Determine the texture parameters.
|
|
1093
|
+
publicAPI.getOpenGLDataType(VtkDataTypes.UNSIGNED_CHAR);
|
|
1094
|
+
publicAPI.getInternalFormat(VtkDataTypes.UNSIGNED_CHAR, 4);
|
|
1095
|
+
publicAPI.getFormat(VtkDataTypes.UNSIGNED_CHAR, 4);
|
|
1096
|
+
if (!model.internalFormat || !model.format || !model.openGLDataType) {
|
|
1097
|
+
vtkErrorMacro('Failed to determine texture parameters.');
|
|
1098
|
+
return false;
|
|
1099
|
+
}
|
|
1100
|
+
model.target = model.context.TEXTURE_2D;
|
|
1101
|
+
model.components = 4;
|
|
1102
|
+
model.depth = 1;
|
|
1103
|
+
model.numberOfDimensions = 2;
|
|
1104
|
+
model._openGLRenderWindow.activateTexture(publicAPI);
|
|
1105
|
+
publicAPI.createTexture();
|
|
1106
|
+
publicAPI.bind();
|
|
1107
|
+
|
|
1108
|
+
// Prepare texture unpack alignment
|
|
1109
|
+
model.context.pixelStorei(model.context.UNPACK_ALIGNMENT, 1);
|
|
1110
|
+
model.width = imageBitmap.width;
|
|
1111
|
+
model.height = imageBitmap.height;
|
|
1048
1112
|
if (useTexStorage(VtkDataTypes.UNSIGNED_CHAR)) {
|
|
1049
1113
|
model.context.texStorage2D(model.target, 1, model.internalFormat, model.width, model.height);
|
|
1050
|
-
|
|
1051
|
-
model.context.texSubImage2D(model.target, 0, 0, 0, model.width, model.height, model.format, model.openGLDataType, safeImage);
|
|
1052
|
-
}
|
|
1114
|
+
model.context.texSubImage2D(model.target, 0, 0, 0, model.width, model.height, model.format, model.openGLDataType, imageBitmap);
|
|
1053
1115
|
} else {
|
|
1054
|
-
model.context.texImage2D(model.target, 0, model.internalFormat, model.width, model.height, 0, model.format, model.openGLDataType,
|
|
1116
|
+
model.context.texImage2D(model.target, 0, model.internalFormat, model.width, model.height, 0, model.format, model.openGLDataType, imageBitmap);
|
|
1055
1117
|
}
|
|
1056
1118
|
if (model.generateMipmap) {
|
|
1057
1119
|
model.context.generateMipmap(model.target);
|
package/index.d.ts
CHANGED
|
@@ -206,6 +206,7 @@
|
|
|
206
206
|
/// <reference path="./Rendering/Core/SphereMapper.d.ts" />
|
|
207
207
|
/// <reference path="./Rendering/Core/StickMapper.d.ts" />
|
|
208
208
|
/// <reference path="./Rendering/Core/Texture.d.ts" />
|
|
209
|
+
/// <reference path="./Rendering/Core/VectorText.d.ts" />
|
|
209
210
|
/// <reference path="./Rendering/Core/Viewport.d.ts" />
|
|
210
211
|
/// <reference path="./Rendering/Core/Volume.d.ts" />
|
|
211
212
|
/// <reference path="./Rendering/Core/VolumeMapper/Constants.d.ts" />
|