@kitware/vtk.js
Version:
Visualization Toolkit for the Web
521 lines (495 loc) • 17.4 kB
JavaScript
import { E as areEquals } from '../../../Common/Core/Math/index.js';
/**
* 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 && 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 = 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(_ref => {
let [a, b, c] = _ref;
addTriangle(layers, c + offset, b + offset, a + offset, verticesArray, uvArray, colorArray, color);
});
layer = steps + bevelSegments * 2;
offset = vlen * layer;
// Top faces
faces.forEach(_ref2 => {
let [a, b, c] = _ref2;
addTriangle(layers, a + offset, b + offset, c + offset, verticesArray, uvArray, colorArray, color);
});
} else {
// Bottom faces
faces.forEach(_ref3 => {
let [a, b, c] = _ref3;
addTriangle(layers, c, b, a, verticesArray, uvArray, colorArray, color);
});
// Top faces
const offset = vlen * steps;
faces.forEach(_ref4 => {
let [a, b, c] = _ref4;
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 { addQuad, addTriangle, buildLidFaces, buildSideFaces, buildWalls, computeBevelVector, computeFacesUV, computeSidesUV, createShapePath, getBoundingSize, isClockWise, scalePoint, triangulateShape };