UNPKG

@kitware/vtk.js

Version:

Visualization Toolkit for the Web

413 lines (385 loc) 13.3 kB
import { m as macro } from '../../macros2.js'; import { d as dot, s as subtract, j as cross, k as add, l as normalize, f as distance2BetweenPoints, n as norm } from '../Core/Math/index.js'; import vtkLine from './Line.js'; import vtkPlane from './Plane.js'; import vtkPriorityQueue from '../Core/PriorityQueue.js'; import vtkBoundingBox from './BoundingBox.js'; import { IntersectionState } from './Line/Constants.js'; import { PolygonWithPointIntersectionState, FLOAT_EPSILON, TOLERANCE, EPSILON } from './Polygon/Constants.js'; // ---------------------------------------------------------------------------- // Global methods // ---------------------------------------------------------------------------- // Given the line (p0,p1), determine if the given point is located to the left // of, on, or to the right of a line (with the function returning >0, ==0, or // <0 respectively). The points are assumed 3D points, but projected into // one of x-y-z planes; hence the indices axis0 and axis1 specify which plane // the computation is to be performed on. function pointLocation(axis0, axis1, p0, p1, point) { return (p1[axis0] - p0[axis0]) * (point[axis1] - p0[axis1]) - (point[axis0] - p0[axis0]) * (p1[axis1] - p0[axis1]); } //------------------------------------------------------------------------------ function pointInPolygon(point, vertices, bounds, normal) { // Do a quick bounds check to throw out trivial cases. // winding plane. if (point[0] < bounds[0] || point[0] > bounds[1] || point[1] < bounds[2] || point[1] > bounds[3] || point[2] < bounds[4] || point[2] > bounds[5]) { return PolygonWithPointIntersectionState.OUTSIDE; } // Check that the normal is non-zero. if (normalize(normal) <= FLOAT_EPSILON) { return PolygonWithPointIntersectionState.FAILURE; } // Assess whether the point lies on the boundary of the polygon. Points on // the boundary are considered inside the polygon. Need to define a small // tolerance relative to the bounding box diagonal length of the polygon. let tol2 = TOLERANCE * ((bounds[1] - bounds[0]) * (bounds[1] - bounds[0]) + (bounds[3] - bounds[2]) * (bounds[3] - bounds[2]) + (bounds[5] - bounds[4]) * (bounds[5] - bounds[4])); tol2 *= tol2; tol2 = tol2 === 0.0 ? FLOAT_EPSILON : tol2; const p0 = []; const p1 = []; for (let i = 0; i < vertices.length;) { // Check coincidence to polygon vertices p0[0] = vertices[i++]; p0[1] = vertices[i++]; p0[2] = vertices[i++]; if (distance2BetweenPoints(point, p0) <= tol2) { return PolygonWithPointIntersectionState.INSIDE; } // Check coincidence to polygon edges const { distance, t } = vtkLine.distanceToLine(point, p0, p1); if (distance <= tol2 && t > 0.0 && t < 1.0) { return PolygonWithPointIntersectionState.INSIDE; } } // If here, begin computation of the winding number. This method works for // points/polygons arbitrarily oriented in 3D space. Hence a projection // onto one of the x-y-z coordinate planes using the maximum normal // component. The computation will be performed in the (axis0, axis1) plane. let axis0; let axis1; if (Math.abs(normal[0]) > Math.abs(normal[1])) { if (Math.abs(normal[0]) > Math.abs(normal[2])) { axis0 = 1; axis1 = 2; } else { axis0 = 0; axis1 = 1; } } else if (Math.abs(normal[1]) > Math.abs(normal[2])) { axis0 = 0; axis1 = 2; } else { axis0 = 0; axis1 = 1; } // Compute the winding number wn. If after processing all polygon edges // wn == 0, then the point is outside. Otherwise, the point is inside the // polygon. Process all polygon edges determining if there are ascending or // descending crossings of the line axis1=constant. let wn = 0; for (let i = 0; i < vertices.length;) { p0[0] = vertices[i++]; p0[1] = vertices[i++]; p0[2] = vertices[i++]; if (i < vertices.length) { p1[0] = vertices[i]; p1[1] = vertices[i + 1]; p1[2] = vertices[i + 2]; } else { p1[0] = vertices[0]; p1[1] = vertices[1]; p1[2] = vertices[2]; } if (p0[axis1] <= point[axis1]) { if (p1[axis1] > point[axis1]) { // if an upward crossing if (pointLocation(axis0, axis1, p0, p1, point) > 0) { // if x left of edge ++wn; // a valid up intersect, increment the winding number } } } else if (p1[axis1] <= point[axis1]) { // if a downward crossing if (pointLocation(axis0, axis1, p0, p1, point) < 0) { // if x right of edge --wn; // a valid down intersect, decrement the winding number } } } // Over all polygon edges // A winding number == 0 is outside the polygon return wn === 0 ? PolygonWithPointIntersectionState.OUTSIDE : PolygonWithPointIntersectionState.INSIDE; } // --------------------------------------------------- /** * Simple utility method for computing polygon bounds. * Returns the sum of the squares of the dimensions. * Requires a poly with at least one point. * * @param {Array<Number>|TypedArray<Number>} poly * @param {vtkPoints} points * @param {Bound} bounds */ function getBounds(poly, points, bounds) { const n = poly.length; const p = []; points.getPoint(poly[0], p); bounds[0] = p[0]; bounds[1] = p[0]; bounds[2] = p[1]; bounds[3] = p[1]; bounds[4] = p[2]; bounds[5] = p[2]; for (let j = 1; j < n; j++) { points.getPoint(poly[j], p); vtkBoundingBox.addPoint(bounds, ...p); } const length = vtkBoundingBox.getLengths(bounds); return dot(length, length); } // --------------------------------------------------- /** * Compute the normal of a polygon and return its norm. * * TBD: This does the same thing as vtkPolygon.computeNormal, * but in a more generic way. Maybe we can keep the public * static method somehow and have the private method use it instead. * * @param {Array<Number>|TypedArray<Number>} poly * @param {vtkPoints} points * @param {Vector3} normal * @returns {Number} */ function getNormal(poly, points, normal) { normal.length = 3; normal[0] = 0.0; normal[1] = 0.0; normal[2] = 0.0; const p0 = []; let p1 = []; let p2 = []; const v1 = []; const v2 = []; points.getPoint(poly[0], p0); points.getPoint(poly[1], p1); for (let j = 2; j < poly.length; j++) { points.getPoint(poly[j], p2); subtract(p2, p1, v1); subtract(p0, p1, v2); const n = [0, 0, 0]; cross(v1, v2, n); add(normal, n, normal); [p1, p2] = [p2, p1]; } return normalize(normal); } /** * Compute the centroid of a polygon. * @param {Array<number>} poly - Array of point indices for the polygon * @param {vtkPoints} points - vtkPoints instance * @param {Vector3} [centroid] - Optional output array (length 3) * @returns {Vector3} The centroid as [x, y, z] */ function computeCentroid(poly, points, centroid = [0, 0, 0]) { centroid[0] = 0; centroid[1] = 0; centroid[2] = 0; const n = poly.length; const p = []; for (let i = 0; i < n; i++) { points.getPoint(poly[i], p); centroid[0] += p[0]; centroid[1] += p[1]; centroid[2] += p[2]; } centroid[0] /= n; centroid[1] /= n; centroid[2] /= n; return centroid; } // ---------------------------------------------------------------------------- // Static API // ---------------------------------------------------------------------------- const STATIC = { PolygonWithPointIntersectionState, pointInPolygon, getBounds, getNormal, computeCentroid }; // ---------------------------------------------------------------------------- // vtkPolygon methods // ---------------------------------------------------------------------------- function vtkPolygon(publicAPI, model) { // Set our classname model.classHierarchy.push('vtkPolygon'); function computeNormal() { const v1 = [0, 0, 0]; const v2 = [0, 0, 0]; model.normal = [0, 0, 0]; const anchor = [...model.firstPoint.point]; let point = model.firstPoint; for (let i = 0; i < model.pointCount; i++) { subtract(point.point, anchor, v1); subtract(point.next.point, anchor, v2); const n = [0, 0, 0]; cross(v1, v2, n); add(model.normal, n, model.normal); point = point.next; } return normalize(model.normal); } function computeMeasure(point) { const v1 = [0, 0, 0]; const v2 = [0, 0, 0]; const v3 = [0, 0, 0]; const v4 = [0, 0, 0]; subtract(point.point, point.previous.point, v1); subtract(point.next.point, point.point, v2); subtract(point.previous.point, point.next.point, v3); cross(v1, v2, v4); const area = dot(v4, model.normal); if (area <= 0) { return -1; } const perimeter = norm(v1) + norm(v2) + norm(v3); return perimeter * perimeter / area; } function canRemoveVertex(point) { if (model.pointCount <= 3) { return true; } const previous = point.previous; const next = point.next; const v = [0, 0, 0]; subtract(next.point, previous.point, v); const sN = [0, 0, 0]; cross(v, model.normal, sN); normalize(sN); if (norm(sN) === 0) { return false; } let val = vtkPlane.evaluate(sN, previous.point, next.next.point); // eslint-disable-next-line no-nested-ternary let currentSign = val > EPSILON ? 1 : val < -EPSILON ? -1 : 0; let oneNegative = currentSign < 0 ? 1 : 0; for (let vertex = next.next.next; vertex.id !== previous.id; vertex = vertex.next) { const previousVertex = vertex.previous; val = vtkPlane.evaluate(sN, previous.point, vertex.point); // eslint-disable-next-line no-nested-ternary const sign = val > EPSILON ? 1 : val < -EPSILON ? -1 : 0; if (sign !== currentSign) { if (!oneNegative) { oneNegative = sign <= 0 ? 1 : 0; } if (vtkLine.intersection(previous.point, next.point, vertex.point, previousVertex.point, [0], [0]) === IntersectionState.YES_INTERSECTION) { return false; } currentSign = sign; } } return oneNegative === 1; } function removePoint(point, queue) { model.pointCount -= 1; const previous = point.previous; const next = point.next; model.tris = model.tris.concat(point.point); model.tris = model.tris.concat(next.point); model.tris = model.tris.concat(previous.point); previous.next = next; next.previous = previous; queue.deleteById(previous.id); queue.deleteById(next.id); const previousMeasure = computeMeasure(previous); if (previousMeasure > 0) { queue.push(previousMeasure, previous); } const nextMeasure = computeMeasure(next); if (nextMeasure > 0) { queue.push(nextMeasure, next); } if (point.id === model.firstPoint.id) { model.firstPoint = next; } } function earCutTriangulation() { computeNormal(); const vertexQueue = vtkPriorityQueue.newInstance(); let point = model.firstPoint; for (let i = 0; i < model.pointCount; i++) { const measure = computeMeasure(point); if (measure > 0) { vertexQueue.push(measure, point); } point = point.next; } while (model.pointCount > 2 && vertexQueue.length() > 0) { if (model.pointCount === vertexQueue.length()) { // convex const pointToRemove = vertexQueue.pop(); removePoint(pointToRemove, vertexQueue); } else { // concave const pointToRemove = vertexQueue.pop(); if (canRemoveVertex(pointToRemove)) { removePoint(pointToRemove, vertexQueue); } } } return model.pointCount <= 2; } publicAPI.triangulate = () => { if (!model.firstPoint) { return null; } return earCutTriangulation(); }; publicAPI.setPoints = points => { model.pointCount = points.length; model.firstPoint = { id: 0, point: points[0], next: null, previous: null }; let currentPoint = model.firstPoint; for (let i = 1; i < model.pointCount; i++) { currentPoint.next = { id: i, point: points[i], next: null, previous: currentPoint }; currentPoint = currentPoint.next; } model.firstPoint.previous = currentPoint; currentPoint.next = model.firstPoint; }; publicAPI.getPointArray = () => model.tris; } // ---------------------------------------------------------------------------- // Object factory // ---------------------------------------------------------------------------- const DEFAULT_VALUES = { firstPoint: null, pointCount: 0, tris: [] }; // ---------------------------------------------------------------------------- function extend(publicAPI, model, initialValues = {}) { Object.assign(model, DEFAULT_VALUES, initialValues); // Build VTK API macro.obj(publicAPI, model); vtkPolygon(publicAPI, model); } // ---------------------------------------------------------------------------- const newInstance = macro.newInstance(extend, 'vtkPolygon'); // ---------------------------------------------------------------------------- var vtkPolygon$1 = { newInstance, extend, ...STATIC }; export { computeCentroid, vtkPolygon$1 as default, extend, getBounds, getNormal, newInstance };