@babylonjs/core
Version:
Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.
1,103 lines • 61.3 kB
JavaScript
/** This file must only contain pure code and pure imports */
import { __decorate } from "../../../tslib.es6.js";
import { editableInPropertyPage } from "../../../Decorators/nodeDecorator.js";
import { Vector3 } from "../../../Maths/math.vector.pure.js";
import { VertexData, VertexDataMaterialInfo } from "../../mesh.vertexData.js";
import { NodeGeometryBlockConnectionPointTypes } from "../Enums/nodeGeometryConnectionPointTypes.js";
import { NodeGeometryBlock } from "../nodeGeometryBlock.js";
import { RegisterClass } from "../../../Misc/typeStore.js";
const PositionEpsilon = 1e-5;
const OutputPositionEpsilon = 1e-4;
const NormalEpsilon = 1e-8;
const AngleEpsilon = 1e-7;
const TriangleAreaEpsilon = PositionEpsilon * PositionEpsilon * PositionEpsilon * PositionEpsilon;
function _Quantize(value) {
const quantized = Math.round(value / PositionEpsilon);
return quantized === 0 ? 0 : quantized;
}
function _PositionKey(x, y, z) {
return `${_Quantize(x)}:${_Quantize(y)}:${_Quantize(z)}`;
}
function _VectorKey(position) {
return _PositionKey(position.x, position.y, position.z);
}
function _OutputQuantize(value) {
const quantized = Math.round(value / OutputPositionEpsilon);
return quantized === 0 ? 0 : quantized;
}
function _OutputPositionKey(x, y, z) {
return `${x}:${y}:${z}`;
}
function _EdgeKey(v0, v1) {
return v0 < v1 ? `${v0}:${v1}` : `${v1}:${v0}`;
}
function _CloneVertexData(vertexData) {
const clone = vertexData.clone();
if (!clone.normals && clone.positions && clone.indices) {
const normals = [];
VertexData.ComputeNormals(clone.positions, clone.indices, normals);
clone.normals = normals;
}
return clone;
}
function _NormalizeNormalOrFallback(normal, fallback) {
return normal.lengthSquared() > NormalEpsilon ? normal.normalizeToNew() : fallback.normalizeToNew();
}
function _BuildAttributeDescriptors(vertexData, vertexCount) {
const descriptors = [];
const addDescriptor = (name, stride) => {
const source = vertexData[name];
if (!source || source.length < vertexCount * stride) {
return;
}
descriptors.push({
name,
source,
stride,
offset: descriptors.reduce((sum, descriptor) => sum + descriptor.stride, 0),
output: [],
});
};
addDescriptor("tangents", 4);
addDescriptor("uvs", 2);
addDescriptor("uvs2", 2);
addDescriptor("uvs3", 2);
addDescriptor("uvs4", 2);
addDescriptor("uvs5", 2);
addDescriptor("uvs6", 2);
if (vertexData.colors) {
addDescriptor("colors", vertexData.colors.length === vertexData.positions.length ? 3 : 4);
}
addDescriptor("matricesIndices", 4);
addDescriptor("matricesWeights", 4);
addDescriptor("matricesIndicesExtra", 4);
addDescriptor("matricesWeightsExtra", 4);
return descriptors;
}
function _GetAttributeLength(descriptors) {
return descriptors.reduce((sum, descriptor) => sum + descriptor.stride, 0);
}
function _GetVertexAttributes(descriptors, vertexIndex) {
const attributes = [];
for (const descriptor of descriptors) {
const sourceOffset = vertexIndex * descriptor.stride;
for (let index = 0; index < descriptor.stride; index++) {
attributes.push(descriptor.source[sourceOffset + index]);
}
}
return attributes;
}
function _InterpolateAttributes(start, end, amount) {
if (!start.length) {
return start;
}
return start.map((value, index) => value + (end[index] - value) * amount);
}
function _AverageAttributes(attributes, length) {
if (!length || !attributes.length) {
return [];
}
const result = new Array(length).fill(0);
for (const attribute of attributes) {
for (let index = 0; index < length; index++) {
result[index] += attribute[index] ?? 0;
}
}
for (let index = 0; index < length; index++) {
result[index] /= attributes.length;
}
return result;
}
function _AttributesMatch(left, right) {
if (left.length !== right.length) {
return false;
}
for (let index = 0; index < left.length; index++) {
if (Math.abs(left[index] - right[index]) > OutputPositionEpsilon) {
return false;
}
}
return true;
}
function _AssignAttributeOutputs(result, descriptors, vertexAttributes) {
for (const descriptor of descriptors) {
descriptor.output.length = 0;
for (const attributes of vertexAttributes) {
for (let index = 0; index < descriptor.stride; index++) {
descriptor.output.push(attributes[descriptor.offset + index] ?? 0);
}
}
switch (descriptor.name) {
case "tangents":
result.tangents = descriptor.output;
break;
case "uvs":
result.uvs = descriptor.output;
break;
case "uvs2":
result.uvs2 = descriptor.output;
break;
case "uvs3":
result.uvs3 = descriptor.output;
break;
case "uvs4":
result.uvs4 = descriptor.output;
break;
case "uvs5":
result.uvs5 = descriptor.output;
break;
case "uvs6":
result.uvs6 = descriptor.output;
break;
case "colors":
result.colors = descriptor.output;
break;
case "matricesIndices":
result.matricesIndices = descriptor.output;
break;
case "matricesWeights":
result.matricesWeights = descriptor.output;
break;
case "matricesIndicesExtra":
result.matricesIndicesExtra = descriptor.output;
break;
case "matricesWeightsExtra":
result.matricesWeightsExtra = descriptor.output;
break;
}
}
}
function _GetMaterialIndex(vertexData, indexStart) {
if (!vertexData.materialInfos) {
return 0;
}
for (const materialInfo of vertexData.materialInfos) {
if (indexStart >= materialInfo.indexStart && indexStart < materialInfo.indexStart + materialInfo.indexCount) {
return materialInfo.materialIndex;
}
}
return 0;
}
function _BuildMaterialInfoResult(vertexData, indices, materialIndices, vertexCount) {
if (!vertexData.materialInfos?.length) {
return { indices, materialInfos: null };
}
const materialOrder = vertexData.materialInfos.map((materialInfo) => materialInfo.materialIndex);
const groups = new Map();
for (let triangleIndex = 0; triangleIndex < materialIndices.length; triangleIndex++) {
const materialIndex = materialIndices[triangleIndex];
let group = groups.get(materialIndex);
if (!group) {
group = [];
groups.set(materialIndex, group);
if (!materialOrder.includes(materialIndex)) {
materialOrder.push(materialIndex);
}
}
const indexOffset = triangleIndex * 3;
group.push(indices[indexOffset], indices[indexOffset + 1], indices[indexOffset + 2]);
}
const groupedIndices = [];
const materialInfos = [];
for (const materialIndex of materialOrder) {
const group = groups.get(materialIndex);
if (!group?.length) {
continue;
}
const materialInfo = new VertexDataMaterialInfo();
materialInfo.materialIndex = materialIndex;
materialInfo.indexStart = groupedIndices.length;
materialInfo.indexCount = group.length;
materialInfo.verticesStart = 0;
materialInfo.verticesCount = vertexCount;
groupedIndices.push(...group);
materialInfos.push(materialInfo);
}
return { indices: groupedIndices, materialInfos };
}
function _IsFlatFace(face) {
return face.cornerNormals.every((normal) => Vector3.Dot(normal, face.normal) > 1 - PositionEpsilon);
}
function _IsBevelPolygonPoint(point) {
return point.position !== undefined;
}
function _GetCapPointPosition(point) {
return _IsBevelPolygonPoint(point) ? point.position : point;
}
function _GetCapPointNormal(point, fallback) {
return _IsBevelPolygonPoint(point) ? point.normal : fallback;
}
function _GetCapPointAttributes(point) {
return _IsBevelPolygonPoint(point) ? point.attributes : [];
}
function _GetCapPointMaterialIndex(point) {
return _IsBevelPolygonPoint(point) ? point.materialIndex : 0;
}
function _BuildTopology(vertexData) {
const positions = vertexData.positions;
const normals = vertexData.normals;
if (!positions || positions.length < 9) {
return null;
}
const vertexCount = positions.length / 3;
const indices = vertexData.indices && vertexData.indices.length ? Array.from(vertexData.indices) : Array.from({ length: vertexCount }, (_, index) => index);
const weldedPositionMap = new Map();
const originalToWelded = [];
const weldedPositions = [];
for (let index = 0; index < vertexCount; index++) {
const x = positions[index * 3];
const y = positions[index * 3 + 1];
const z = positions[index * 3 + 2];
const key = _PositionKey(x, y, z);
let weldedIndex = weldedPositionMap.get(key);
if (weldedIndex === undefined) {
weldedIndex = weldedPositions.length;
weldedPositionMap.set(key, weldedIndex);
weldedPositions.push(new Vector3(x, y, z));
}
originalToWelded[index] = weldedIndex;
}
const faces = [];
const edges = new Map();
const vertexFaces = new Map();
const edge0 = new Vector3();
const edge1 = new Vector3();
const normal = new Vector3();
for (let index = 0; index < indices.length; index += 3) {
let originalIndices = [indices[index], indices[index + 1], indices[index + 2]];
const i0 = originalToWelded[originalIndices[0]];
const i1 = originalToWelded[originalIndices[1]];
const i2 = originalToWelded[originalIndices[2]];
if (i0 === i1 || i1 === i2 || i2 === i0) {
continue;
}
let faceIndices = [i0, i1, i2];
const p0 = weldedPositions[faceIndices[0]];
const p1 = weldedPositions[faceIndices[1]];
const p2 = weldedPositions[faceIndices[2]];
p1.subtractToRef(p0, edge0);
p2.subtractToRef(p0, edge1);
Vector3.CrossToRef(edge0, edge1, normal);
if (normal.lengthSquared() < NormalEpsilon) {
continue;
}
let cornerNormals = normals
? [
_NormalizeNormalOrFallback(Vector3.FromArray(normals, originalIndices[0] * 3), normal),
_NormalizeNormalOrFallback(Vector3.FromArray(normals, originalIndices[1] * 3), normal),
_NormalizeNormalOrFallback(Vector3.FromArray(normals, originalIndices[2] * 3), normal),
]
: [normal.normalizeToNew(), normal.normalizeToNew(), normal.normalizeToNew()];
const averageCornerNormal = _NormalizeNormalOrFallback(cornerNormals[0].add(cornerNormals[1]).addInPlace(cornerNormals[2]), normal);
if (normals && Vector3.Dot(normal, averageCornerNormal) < 0) {
faceIndices = [i0, i2, i1];
originalIndices = [originalIndices[0], originalIndices[2], originalIndices[1]];
cornerNormals = [cornerNormals[0], cornerNormals[2], cornerNormals[1]];
normal.scaleInPlace(-1);
}
const faceNormal = normal.normalizeToNew();
if (!normals) {
cornerNormals = [faceNormal.clone(), faceNormal.clone(), faceNormal.clone()];
}
const faceIndex = faces.length;
faces.push({
indices: faceIndices,
originalIndices,
normal: faceNormal,
cornerNormals,
materialIndex: _GetMaterialIndex(vertexData, index),
});
for (const vertexIndex of faceIndices) {
let faceList = vertexFaces.get(vertexIndex);
if (!faceList) {
faceList = [];
vertexFaces.set(vertexIndex, faceList);
}
faceList.push(faceIndex);
}
for (let edgeIndex = 0; edgeIndex < 3; edgeIndex++) {
const v0 = faceIndices[edgeIndex];
const v1 = faceIndices[(edgeIndex + 1) % 3];
const key = _EdgeKey(v0, v1);
let edge = edges.get(key);
if (!edge) {
edge = {
key,
v0: Math.min(v0, v1),
v1: Math.max(v0, v1),
faces: [],
};
edges.set(key, edge);
}
edge.faces.push({ faceIndex });
}
}
if (!faces.length) {
return null;
}
return {
positions: weldedPositions,
faces,
edges,
vertexFaces,
};
}
function _ClonePolygonPoint(point) {
return {
position: point.position.clone(),
normal: point.normal.clone(),
attributes: point.attributes.slice(),
materialIndex: point.materialIndex,
};
}
function _InterpolatePolygonPoint(start, end, amount) {
return {
position: Vector3.Lerp(start.position, end.position, amount),
normal: _NormalizeNormalOrFallback(Vector3.Lerp(start.normal, end.normal, amount), start.normal),
attributes: _InterpolateAttributes(start.attributes, end.attributes, amount),
materialIndex: start.materialIndex,
};
}
function _InterpolateSegmentNormal(segment, t) {
const denominator = segment.tMax - segment.tMin;
if (denominator <= PositionEpsilon) {
return segment.minNormal.clone();
}
const amount = Math.min(1, Math.max(0, (t - segment.tMin) / denominator));
return _NormalizeNormalOrFallback(Vector3.Lerp(segment.minNormal, segment.maxNormal, amount), segment.minNormal);
}
function _InterpolateSegmentPoint(segment, t) {
if (t <= segment.tMin + PositionEpsilon) {
return segment.minPoint;
}
if (t >= segment.tMax - PositionEpsilon) {
return segment.maxPoint;
}
return Vector3.Lerp(segment.minPoint, segment.maxPoint, (t - segment.tMin) / (segment.tMax - segment.tMin));
}
function _InterpolateSegmentAttributes(segment, t) {
if (t <= segment.tMin + PositionEpsilon) {
return segment.minAttributes;
}
if (t >= segment.tMax - PositionEpsilon) {
return segment.maxAttributes;
}
return _InterpolateAttributes(segment.minAttributes, segment.maxAttributes, (t - segment.tMin) / (segment.tMax - segment.tMin));
}
function _ClipPolygonAgainstEdge(polygon, edgeStart, inward, amount) {
if (!polygon.length) {
return polygon;
}
const output = [];
let previous = polygon[polygon.length - 1];
let previousDistance = Vector3.Dot(previous.position.subtract(edgeStart), inward) - amount;
let previousInside = previousDistance >= -PositionEpsilon;
for (const current of polygon) {
const currentDistance = Vector3.Dot(current.position.subtract(edgeStart), inward) - amount;
const currentInside = currentDistance >= -PositionEpsilon;
if (currentInside !== previousInside) {
const denominator = previousDistance - currentDistance;
if (Math.abs(denominator) > NormalEpsilon) {
const t = previousDistance / denominator;
output.push(_InterpolatePolygonPoint(previous, current, t));
}
}
if (currentInside) {
output.push(_ClonePolygonPoint(current));
}
previous = current;
previousDistance = currentDistance;
previousInside = currentInside;
}
return output;
}
function _SlerpDirections(start, end, amount) {
const dot = Math.min(1, Math.max(-1, Vector3.Dot(start, end)));
if (dot > 1 - PositionEpsilon) {
return Vector3.Lerp(start, end, amount).normalize();
}
const theta = Math.acos(dot);
const sinTheta = Math.sin(theta);
if (Math.abs(sinTheta) < NormalEpsilon) {
return Vector3.Lerp(start, end, amount).normalize();
}
const startScale = Math.sin((1 - amount) * theta) / sinTheta;
const endScale = Math.sin(amount * theta) / sinTheta;
const result = start.scale(startScale).addInPlace(end.scale(endScale));
if (result.lengthSquared() < NormalEpsilon) {
return Vector3.Lerp(start, end, amount).normalize();
}
return result.normalize();
}
function _AddUniquePoint(points, point, normal, attributes, materialIndex) {
const key = _VectorKey(point);
const normalizedNormal = _NormalizeNormalOrFallback(normal, normal);
for (const existing of points) {
if (_VectorKey(_GetCapPointPosition(existing)) === key) {
if (_IsBevelPolygonPoint(existing)) {
existing.normal.addInPlace(normalizedNormal).normalize();
existing.attributes = _AverageAttributes([existing.attributes, attributes], attributes.length);
}
return;
}
}
points.push({
position: point.clone(),
normal: normalizedNormal,
attributes: attributes.slice(),
materialIndex,
});
}
function _AddUniqueNormal(normals, normal) {
for (const existing of normals) {
if (Vector3.Dot(existing, normal) > 1 - PositionEpsilon) {
return;
}
}
normals.push(normal.clone());
}
function _SolveThreePlaneIntersection(normals, distances) {
const cross12 = Vector3.Cross(normals[1], normals[2]);
const denominator = Vector3.Dot(normals[0], cross12);
if (Math.abs(denominator) < NormalEpsilon) {
return null;
}
const result = cross12.scale(distances[0]);
result.addInPlace(Vector3.Cross(normals[2], normals[0]).scale(distances[1]));
result.addInPlace(Vector3.Cross(normals[0], normals[1]).scale(distances[2]));
result.scaleInPlace(1 / denominator);
return result;
}
function _BuildCoplanarFaceClipEdges(topology, selectedEdges) {
const result = new Map();
const visitedFaces = new Set();
for (let startFaceIndex = 0; startFaceIndex < topology.faces.length; startFaceIndex++) {
if (visitedFaces.has(startFaceIndex)) {
continue;
}
const group = [];
const stack = [startFaceIndex];
const groupNormal = topology.faces[startFaceIndex].normal;
visitedFaces.add(startFaceIndex);
while (stack.length) {
const faceIndex = stack.pop();
const face = topology.faces[faceIndex];
group.push(faceIndex);
for (let edgeIndex = 0; edgeIndex < 3; edgeIndex++) {
const key = _EdgeKey(face.indices[edgeIndex], face.indices[(edgeIndex + 1) % 3]);
const edge = topology.edges.get(key);
if (!edge) {
continue;
}
for (const edgeFace of edge.faces) {
if (visitedFaces.has(edgeFace.faceIndex)) {
continue;
}
if (Vector3.Dot(groupNormal, topology.faces[edgeFace.faceIndex].normal) < 1 - PositionEpsilon) {
continue;
}
visitedFaces.add(edgeFace.faceIndex);
stack.push(edgeFace.faceIndex);
}
}
}
const clipEdges = [];
const addedClipEdges = new Set();
for (const faceIndex of group) {
const face = topology.faces[faceIndex];
for (let edgeIndex = 0; edgeIndex < 3; edgeIndex++) {
const start = face.indices[edgeIndex];
const end = face.indices[(edgeIndex + 1) % 3];
const key = _EdgeKey(start, end);
if (!selectedEdges.has(key) || addedClipEdges.has(key)) {
continue;
}
const edgeStart = topology.positions[start];
const edgeEnd = topology.positions[end];
const edgeDirection = edgeEnd.subtract(edgeStart).normalize();
const inward = Vector3.Cross(face.normal, edgeDirection).normalize();
clipEdges.push({ key, start, end, inward });
addedClipEdges.add(key);
}
}
for (const faceIndex of group) {
result.set(faceIndex, clipEdges);
}
}
return result;
}
function _InsertPointOnPolygonBoundary(polygon, point) {
for (const existing of polygon) {
if (existing.position.subtract(point).lengthSquared() <= PositionEpsilon * PositionEpsilon) {
return;
}
}
let bestEdgeIndex = -1;
let bestDistanceSquared = Number.MAX_VALUE;
let bestProjection = 0;
for (let index = 0; index < polygon.length; index++) {
const start = polygon[index].position;
const end = polygon[(index + 1) % polygon.length].position;
const edge = end.subtract(start);
const edgeLengthSquared = edge.lengthSquared();
if (edgeLengthSquared < PositionEpsilon * PositionEpsilon) {
continue;
}
const projection = Vector3.Dot(point.subtract(start), edge) / edgeLengthSquared;
if (projection < -PositionEpsilon || projection > 1 + PositionEpsilon) {
continue;
}
const closest = start.add(edge.scale(projection));
const distanceSquared = closest.subtract(point).lengthSquared();
if (distanceSquared < bestDistanceSquared) {
bestDistanceSquared = distanceSquared;
bestEdgeIndex = index;
bestProjection = projection;
}
}
if (bestEdgeIndex !== -1 && bestDistanceSquared <= OutputPositionEpsilon * OutputPositionEpsilon) {
const insertedPoint = _InterpolatePolygonPoint(polygon[bestEdgeIndex], polygon[(bestEdgeIndex + 1) % polygon.length], bestProjection);
insertedPoint.position = point.clone();
polygon.splice(bestEdgeIndex + 1, 0, insertedPoint);
}
}
function _BuildMergedBoundaryPolygon(polygons) {
const points = new Map();
const edgeUseCounts = new Map();
for (const polygon of polygons) {
for (const point of polygon) {
const key = _VectorKey(point.position);
const existing = points.get(key);
if (existing) {
existing.normal.addInPlace(point.normal).normalize();
}
else {
points.set(key, _ClonePolygonPoint(point));
}
}
for (let index = 0; index < polygon.length; index++) {
const key0 = _VectorKey(polygon[index].position);
const key1 = _VectorKey(polygon[(index + 1) % polygon.length].position);
if (key0 === key1) {
continue;
}
const edgeKey = key0 < key1 ? `${key0}|${key1}` : `${key1}|${key0}`;
const edgeUseCount = edgeUseCounts.get(edgeKey);
if (edgeUseCount) {
edgeUseCount.count++;
}
else {
edgeUseCounts.set(edgeKey, { count: 1, key0, key1 });
}
}
}
const adjacency = new Map();
for (const edge of Array.from(edgeUseCounts.values())) {
if (edge.count !== 1) {
continue;
}
let adjacency0 = adjacency.get(edge.key0);
if (!adjacency0) {
adjacency0 = [];
adjacency.set(edge.key0, adjacency0);
}
adjacency0.push(edge.key1);
let adjacency1 = adjacency.get(edge.key1);
if (!adjacency1) {
adjacency1 = [];
adjacency.set(edge.key1, adjacency1);
}
adjacency1.push(edge.key0);
}
if (!adjacency.size || Array.from(adjacency.values()).some((neighbors) => neighbors.length !== 2)) {
return null;
}
const startKey = Array.from(adjacency.keys()).sort()[0];
const orderedKeys = [];
let previousKey = "";
let currentKey = startKey;
do {
orderedKeys.push(currentKey);
const neighbors = adjacency.get(currentKey);
const nextKey = neighbors[0] === previousKey ? neighbors[1] : neighbors[0];
previousKey = currentKey;
currentKey = nextKey;
if (orderedKeys.length > adjacency.size) {
return null;
}
} while (currentKey !== startKey);
if (orderedKeys.length !== adjacency.size) {
return null;
}
return orderedKeys.map((key) => points.get(key));
}
function _BevelVertexData(vertexData, amount, segments, angle) {
const topology = _BuildTopology(vertexData);
if (!topology || amount <= PositionEpsilon || segments <= 0) {
return _CloneVertexData(vertexData);
}
const selectedEdges = new Set();
const selectedVertices = new Set();
const selectedEdgeCountPerVertex = new Map();
const threshold = Math.cos(angle + AngleEpsilon);
let shortestSelectedEdgeLength = Number.MAX_VALUE;
for (const edge of Array.from(topology.edges.values())) {
if (edge.faces.length !== 2) {
continue;
}
const face0 = topology.faces[edge.faces[0].faceIndex];
const face1 = topology.faces[edge.faces[1].faceIndex];
if (Vector3.Dot(face0.normal, face1.normal) < threshold) {
selectedEdges.add(edge.key);
selectedVertices.add(edge.v0);
selectedVertices.add(edge.v1);
selectedEdgeCountPerVertex.set(edge.v0, (selectedEdgeCountPerVertex.get(edge.v0) ?? 0) + 1);
selectedEdgeCountPerVertex.set(edge.v1, (selectedEdgeCountPerVertex.get(edge.v1) ?? 0) + 1);
shortestSelectedEdgeLength = Math.min(shortestSelectedEdgeLength, Vector3.Distance(topology.positions[edge.v0], topology.positions[edge.v1]));
}
}
if (!selectedEdges.size) {
return _CloneVertexData(vertexData);
}
const bevelAmount = Math.min(amount, shortestSelectedEdgeLength * 0.5);
if (bevelAmount <= PositionEpsilon) {
return _CloneVertexData(vertexData);
}
const faceClipEdges = _BuildCoplanarFaceClipEdges(topology, selectedEdges);
const attributeDescriptors = _BuildAttributeDescriptors(vertexData, vertexData.positions.length / 3);
const attributeLength = _GetAttributeLength(attributeDescriptors);
const outputPositions = [];
const outputNormals = [];
const outputNormalContributions = [];
const outputVertexAttributes = [];
const outputIndices = [];
const outputTriangleMaterialIndices = [];
const outputVertexBuckets = new Map();
const outputVertexGroups = [];
const faceEdgeSegments = new Map();
const capPoints = new Map();
const preparedFaces = [];
const edgeIntervals = new Map();
const getOrCreateVertex = (position, normal, smoothingGroup, attributes) => {
const normalizedNormal = normal.normalizeToNew();
const vertexAttributes = attributeLength ? (attributes.length === attributeLength ? attributes : new Array(attributeLength).fill(0)) : [];
const qx = _OutputQuantize(position.x);
const qy = _OutputQuantize(position.y);
const qz = _OutputQuantize(position.z);
let canonicalIndex = -1;
for (let dz = -1; dz <= 1; dz++) {
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
const bucket = outputVertexBuckets.get(_OutputPositionKey(qx + dx, qy + dy, qz + dz));
if (!bucket) {
continue;
}
for (const index of bucket) {
const outputIndex = index * 3;
const deltaX = outputPositions[outputIndex] - position.x;
const deltaY = outputPositions[outputIndex + 1] - position.y;
const deltaZ = outputPositions[outputIndex + 2] - position.z;
if (deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ <= OutputPositionEpsilon * OutputPositionEpsilon) {
if (canonicalIndex === -1) {
canonicalIndex = index;
}
if (outputVertexGroups[index] !== smoothingGroup) {
continue;
}
if (!_AttributesMatch(outputVertexAttributes[index], vertexAttributes)) {
continue;
}
const normalContributions = outputNormalContributions[index];
if (!normalContributions.some((normal) => Vector3.Dot(normal, normalizedNormal) > 1 - PositionEpsilon)) {
normalContributions.push(normalizedNormal);
outputNormals[index].addInPlace(normalizedNormal);
}
return index;
}
}
}
}
}
const index = outputPositions.length / 3;
const outputX = canonicalIndex === -1 ? position.x : outputPositions[canonicalIndex * 3];
const outputY = canonicalIndex === -1 ? position.y : outputPositions[canonicalIndex * 3 + 1];
const outputZ = canonicalIndex === -1 ? position.z : outputPositions[canonicalIndex * 3 + 2];
const key = _OutputPositionKey(_OutputQuantize(outputX), _OutputQuantize(outputY), _OutputQuantize(outputZ));
let bucket = outputVertexBuckets.get(key);
if (!bucket) {
bucket = [];
outputVertexBuckets.set(key, bucket);
}
bucket.push(index);
outputPositions.push(outputX, outputY, outputZ);
outputNormals.push(normalizedNormal);
outputNormalContributions.push([normalizedNormal.clone()]);
outputVertexAttributes.push(vertexAttributes.slice());
outputVertexGroups.push(smoothingGroup);
return index;
};
const addCapPoint = (vertexIndex, point, normal, attributes, materialIndex) => {
let points = capPoints.get(vertexIndex);
if (!points) {
points = [];
capPoints.set(vertexIndex, points);
}
_AddUniquePoint(points, point, normal, attributes, materialIndex);
};
const addTriangle = (p0, p1, p2, targetNormal, n0 = targetNormal, n1 = targetNormal, n2 = targetNormal, smoothingGroup = "smooth", p0Attributes = [], p1Attributes = [], p2Attributes = [], materialIndex = 0) => {
const edge0 = p1.subtract(p0);
const edge1 = p2.subtract(p0);
const normal = Vector3.Cross(edge0, edge1);
if (normal.lengthSquared() < TriangleAreaEpsilon) {
return;
}
let v1 = p1;
let v2 = p2;
let vn1 = n1;
let vn2 = n2;
let va1 = p1Attributes;
let va2 = p2Attributes;
if (Vector3.Dot(normal, targetNormal) > 0) {
v1 = p2;
v2 = p1;
vn1 = n2;
vn2 = n1;
va1 = p2Attributes;
va2 = p1Attributes;
}
const i0 = getOrCreateVertex(p0, n0, smoothingGroup, p0Attributes);
const i1 = getOrCreateVertex(v1, vn1, smoothingGroup, va1);
const i2 = getOrCreateVertex(v2, vn2, smoothingGroup, va2);
if (i0 === i1 || i1 === i2 || i2 === i0) {
return;
}
outputIndices.push(i0, i1, i2);
outputTriangleMaterialIndices.push(materialIndex);
};
const addFacePolygon = (polygon, normal, smoothingGroup, useFlatNormals) => {
const center = new Vector3();
const centerAttributes = _AverageAttributes(polygon.map((point) => point.attributes), attributeLength);
const materialIndex = polygon[0]?.materialIndex ?? 0;
const centerNormal = useFlatNormals
? normal
: _NormalizeNormalOrFallback(polygon.reduce((accumulator, point) => accumulator.addInPlace(point.normal), new Vector3()), normal);
for (const point of polygon) {
center.addInPlace(point.position);
}
center.scaleInPlace(1 / polygon.length);
for (let index = 0; index < polygon.length; index++) {
const nextIndex = (index + 1) % polygon.length;
addTriangle(center, polygon[index].position, polygon[nextIndex].position, normal, centerNormal, useFlatNormals ? normal : polygon[index].normal, useFlatNormals ? normal : polygon[nextIndex].normal, smoothingGroup, centerAttributes, polygon[index].attributes, polygon[nextIndex].attributes, materialIndex);
}
};
for (let faceIndex = 0; faceIndex < topology.faces.length; faceIndex++) {
const face = topology.faces[faceIndex];
const isFlat = _IsFlatFace(face);
let polygon = face.indices.map((index, cornerIndex) => ({
position: topology.positions[index].clone(),
normal: face.cornerNormals[cornerIndex].clone(),
attributes: _GetVertexAttributes(attributeDescriptors, face.originalIndices[cornerIndex]),
materialIndex: face.materialIndex,
}));
const selectedFaceEdges = [];
for (let edgeIndex = 0; edgeIndex < 3; edgeIndex++) {
const start = face.indices[edgeIndex];
const end = face.indices[(edgeIndex + 1) % 3];
const key = _EdgeKey(start, end);
if (!selectedEdges.has(key)) {
continue;
}
const edgeStart = topology.positions[start];
const edgeEnd = topology.positions[end];
const edgeDirection = edgeEnd.subtract(edgeStart).normalize();
const inward = Vector3.Cross(face.normal, edgeDirection).normalize();
selectedFaceEdges.push({ key, start, end, inward });
}
const clipEdges = faceClipEdges.get(faceIndex) ?? selectedFaceEdges;
for (const clipEdge of clipEdges) {
polygon = _ClipPolygonAgainstEdge(polygon, topology.positions[clipEdge.start], clipEdge.inward, bevelAmount);
}
if (isFlat) {
for (const point of polygon) {
point.normal = face.normal.clone();
}
}
preparedFaces.push({ faceIndex, polygon, selectedFaceEdges, isFlat });
for (const selectedFaceEdge of selectedFaceEdges) {
const edge = topology.edges.get(selectedFaceEdge.key);
const edgeStart = topology.positions[edge.v0];
const edgeEnd = topology.positions[edge.v1];
const axis = edgeEnd.subtract(edgeStart);
const edgeLength = axis.length();
if (edgeLength < PositionEpsilon) {
continue;
}
axis.normalize();
const pointsOnLine = [];
const orientedEdgeStart = topology.positions[selectedFaceEdge.start];
for (const point of polygon) {
const distance = Vector3.Dot(point.position.subtract(orientedEdgeStart), selectedFaceEdge.inward);
if (Math.abs(distance - bevelAmount) < PositionEpsilon * 10) {
const t = Vector3.Dot(point.position.subtract(edgeStart), axis);
if (t >= -PositionEpsilon && t <= edgeLength + PositionEpsilon) {
pointsOnLine.push({ point, t: Math.min(edgeLength, Math.max(0, t)) });
}
}
}
if (pointsOnLine.length < 2) {
continue;
}
pointsOnLine.sort((a, b) => a.t - b.t);
const minPoint = pointsOnLine[0].point.position;
const maxPoint = pointsOnLine[pointsOnLine.length - 1].point.position;
const minNormal = pointsOnLine[0].point.normal;
const maxNormal = pointsOnLine[pointsOnLine.length - 1].point.normal;
const minAttributes = pointsOnLine[0].point.attributes;
const maxAttributes = pointsOnLine[pointsOnLine.length - 1].point.attributes;
const segment = {
edgeKey: selectedFaceEdge.key,
faceIndex,
inward: selectedFaceEdge.inward,
tMin: pointsOnLine[0].t,
tMax: pointsOnLine[pointsOnLine.length - 1].t,
minPoint,
maxPoint,
minNormal,
maxNormal,
minAttributes,
maxAttributes,
materialIndex: face.materialIndex,
};
faceEdgeSegments.set(`${faceIndex}|${selectedFaceEdge.key}`, segment);
addCapPoint(edge.v0, minPoint, pointsOnLine[0].point.normal, minAttributes, face.materialIndex);
addCapPoint(edge.v1, maxPoint, pointsOnLine[pointsOnLine.length - 1].point.normal, maxAttributes, face.materialIndex);
}
}
for (const edgeKey of Array.from(selectedEdges)) {
const edge = topology.edges.get(edgeKey);
const face0 = edge.faces[0].faceIndex;
const face1 = edge.faces[1].faceIndex;
const segment0 = faceEdgeSegments.get(`${face0}|${edgeKey}`);
const segment1 = faceEdgeSegments.get(`${face1}|${edgeKey}`);
if (!segment0 || !segment1) {
continue;
}
const edgeStart = topology.positions[edge.v0];
const edgeEnd = topology.positions[edge.v1];
const axis = edgeEnd.subtract(edgeStart);
const edgeLength = axis.length();
if (edgeLength < PositionEpsilon) {
continue;
}
axis.normalize();
const tStart = Math.max(segment0.tMin, segment1.tMin);
const tEnd = Math.min(segment0.tMax, segment1.tMax);
if (tEnd - tStart <= PositionEpsilon) {
continue;
}
edgeIntervals.set(edgeKey, { tStart, tEnd });
}
for (const preparedFace of preparedFaces) {
for (const selectedFaceEdge of preparedFace.selectedFaceEdges) {
const edge = topology.edges.get(selectedFaceEdge.key);
const interval = edgeIntervals.get(selectedFaceEdge.key);
const segment = faceEdgeSegments.get(`${preparedFace.faceIndex}|${selectedFaceEdge.key}`);
if (!interval || !segment) {
continue;
}
const edgeStart = topology.positions[edge.v0];
const edgeEnd = topology.positions[edge.v1];
const axis = edgeEnd.subtract(edgeStart);
const edgeLength = axis.length();
if (edgeLength < PositionEpsilon) {
continue;
}
axis.normalize();
for (const t of [interval.tStart, interval.tEnd]) {
if (t <= segment.tMin + PositionEpsilon || t >= segment.tMax - PositionEpsilon) {
continue;
}
_InsertPointOnPolygonBoundary(preparedFace.polygon, edgeStart.add(axis.scale(t)).addInPlace(segment.inward.scale(bevelAmount)));
}
}
}
const emittedMergedFaces = new Set();
const preparedFacesByPlane = new Map();
for (const preparedFace of preparedFaces) {
if (!preparedFace.selectedFaceEdges.length || !preparedFace.isFlat) {
continue;
}
const face = topology.faces[preparedFace.faceIndex];
const distance = Vector3.Dot(topology.positions[face.indices[0]], face.normal);
const planeKey = `${_PositionKey(face.normal.x, face.normal.y, face.normal.z)}:${_Quantize(distance)}`;
let group = preparedFacesByPlane.get(planeKey);
if (!group) {
group = [];
preparedFacesByPlane.set(planeKey, group);
}
group.push(preparedFace);
}
for (const group of Array.from(preparedFacesByPlane.values())) {
if (group.length < 2) {
continue;
}
const mergedPolygon = _BuildMergedBoundaryPolygon(group.map((preparedFace) => preparedFace.polygon));
if (!mergedPolygon || mergedPolygon.length < 3) {
continue;
}
const face = topology.faces[group[0].faceIndex];
addFacePolygon(mergedPolygon, face.normal, `face-flat:${_PositionKey(face.normal.x, face.normal.y, face.normal.z)}`, true);
for (const preparedFace of group) {
emittedMergedFaces.add(preparedFace.faceIndex);
}
}
for (const preparedFace of preparedFaces) {
if (emittedMergedFaces.has(preparedFace.faceIndex)) {
continue;
}
const face = topology.faces[preparedFace.faceIndex];
const smoothingGroup = preparedFace.selectedFaceEdges.length && !preparedFace.isFlat ? "smooth" : `face-flat:${_PositionKey(face.normal.x, face.normal.y, face.normal.z)}`;
if (preparedFace.polygon.length >= 3) {
if (smoothingGroup === "smooth") {
addFacePolygon(preparedFace.polygon, face.normal, smoothingGroup, false);
}
else {
for (let index = 1; index < preparedFace.polygon.length - 1; index++) {
addTriangle(preparedFace.polygon[0].position, preparedFace.polygon[index].position, preparedFace.polygon[index + 1].position, face.normal, face.normal, face.normal, face.normal, smoothingGroup, preparedFace.polygon[0].attributes, preparedFace.polygon[index].attributes, preparedFace.polygon[index + 1].attributes, face.materialIndex);
}
}
}
}
for (const edgeKey of Array.from(selectedEdges)) {
const edge = topology.edges.get(edgeKey);
const face0 = edge.faces[0].faceIndex;
const face1 = edge.faces[1].faceIndex;
const segment0 = faceEdgeSegments.get(`${face0}|${edgeKey}`);
const segment1 = faceEdgeSegments.get(`${face1}|${edgeKey}`);
const interval = edgeIntervals.get(edgeKey);
if (!segment0 || !segment1 || !interval) {
continue;
}
const edgeStart = topology.positions[edge.v0];
const edgeEnd = topology.positions[edge.v1];
const axis = edgeEnd.subtract(edgeStart);
const edgeLength = axis.length();
if (edgeLength < PositionEpsilon) {
continue;
}
axis.normalize();
const { tStart, tEnd } = interval;
const faceNormal0 = topology.faces[segment0.faceIndex].normal;
const faceNormal1 = topology.faces[segment1.faceIndex].normal;
const segment0Start = _InterpolateSegmentPoint(segment0, tStart);
const segment1Start = _InterpolateSegmentPoint(segment1, tStart);
const segment0End = _InterpolateSegmentPoint(segment0, tEnd);
const segment1End = _InterpolateSegmentPoint(segment1, tEnd);
const segment0StartNormal = _InterpolateSegmentNormal(segment0, tStart);
const segment1StartNormal = _InterpolateSegmentNormal(segment1, tStart);
const segment0EndNormal = _InterpolateSegmentNormal(segment0, tEnd);
const segment1EndNormal = _InterpolateSegmentNormal(segment1, tEnd);
const segment0StartAttributes = _InterpolateSegmentAttributes(segment0, tStart);
const segment1StartAttributes = _InterpolateSegmentAttributes(segment1, tStart);
const segment0EndAttributes = _InterpolateSegmentAttributes(segment0, tEnd);
const segment1EndAttributes = _InterpolateSegmentAttributes(segment1, tEnd);
const centerStart = segment0Start
.subtract(faceNormal0.scale(bevelAmount))
.addInPlace(segment1Start.subtract(faceNormal1.scale(bevelAmount)))
.scaleInPlace(0.5);
const centerEnd = segment0End
.subtract(faceNormal0.scale(bevelAmount))
.addInPlace(segment1End.subtract(faceNormal1.scale(bevelAmount)))
.scaleInPlace(0.5);
const startProfilePoints = [];
const endProfilePoints = [];
const startProfileNormals = [];
const endProfileNormals = [];
const startProfileAttributes = [];
const endProfileAttributes = [];
for (let segmentIndex = 0; segmentIndex <= segments; segmentIndex++) {
const profileAmount = segmentIndex / segments;
const profileNormal = _SlerpDirections(faceNormal0, faceNormal1, profileAmount);
const startNormal = _SlerpDirections(segment0StartNormal, segment1StartNormal, profileAmount);
const endNormal = _SlerpDirections(segment0EndNormal, segment1EndNormal, profileAmount);
const startAttributes = _InterpolateAttributes(segment0StartAttributes, segment1StartAttributes, profileAmount);
const endAttributes = _InterpolateAttributes(segment0EndAttributes, segment1EndAttributes, profileAmount);
let startPoint = centerStart.add(profileNormal.scale(bevelAmount));
let endPoint = centerEnd.add(profileNormal.scale(bevelAmount));
if (segmentIndex === 0) {
startPoint = segment0Start;
endPoint = segment0End;
}
else if (segmentIndex === segments) {
startPoint = segment1Start;
endPoint = segment1End;
}
startProfilePoints.push(startPoint);
endProfilePoints.push(endPoint);
startProfileNormals.push(startNormal);
endProfileNormals.push(endNormal);
startProfileAttributes.push(startAttributes);
endProfileAttributes.push(endAttributes);
addCapPoint(edge.v0, startPoint, startNormal, startAttributes, segment0.materialIndex);
addCapPoint(edge.v1, endPoint, endNormal, endAttributes, segment0.materialIndex);
}
for (let segmentIndex = 0; segmentIndex < segments; segmentIndex++) {
const normalTarget = _SlerpDirections(faceNormal0, faceNormal1, (segmentIndex + 0.5) / segments);
addTriangle(startProfilePoints[segmentIndex], endProfilePoints[segmentIndex], endProfilePoints[segmentIndex + 1], normalTarget, startProfileNormals[segmentIndex], endProfileNormals[segmentIndex], endProfileNormals[segmentIndex + 1], "smooth", startProfileAttributes[segmentIndex], endProfileAttributes[segmentIndex], endProfileAttributes[segmentIndex + 1], segment0.materialIndex);
addTriangle(startProfilePoints[segmentIndex], endProfilePoints[segmentIndex + 1], startProfilePoints[segmentIndex + 1], normalTarget, startProfileNormals[segmentIndex], endProfileNormals[segmentIndex + 1], startProfileNormals[segmentIndex + 1], "smooth", startProfileAttributes[segmentIndex], endProfileAttributes[segmentIndex + 1], startProfileAttributes[segmentIndex + 1], segment0.materialIndex);
}
}
const addSphericalCornerPatch = (vertexIndex) => {
if ((selectedEdgeCountPerVertex.get(vertexIndex) ?? 0) < 3) {
return false;
}
const incidentNormals = [];
const vertexPosition = topology.positions[vertexIndex];
const cornerCapPoints = capPoints.get(vertexIndex) ?? [];
const cornerAttributes = _AverageAttributes(cornerCapPoints.map((point) => _GetCapPointAttributes(point)), attributeLength);
const materialIndex = cornerCapPoints.length ? _GetCapPointMaterialIndex(cornerCapPoints[0]) : 0;
for (const edgeKey of Array.from(selectedEdges)) {
const edge = topology.edges.get(edgeKey);
if (edge.v0 !== vertexIndex && edge.v1 !== vertexIndex) {
continue;
}
for (const edgeFace of edge.faces) {
_AddUniqueNormal(incidentNormals, topology.faces[edgeFace.faceIndex].normal);
}
}
if (incidentNormals.length !== 3) {
return false;
}
const averageNormal = incidentNormals[0].add(incidentNormals[1]).addInPlace(incidentNormals[2]).normalize();
const tangent = incidentNormals[0].subtract(averageNormal.scale(Vector3.Dot(incidentNormals[0], averageNormal))).normalize();
const bitangent = Vector3.Cross(averageNormal, tangent).normalize();
incidentNormals.sort((a, b) => Math.atan2(Vector3.Dot(a, bitangent), Vector3.Dot(a, tangent)) - Math.atan2(Vector3.Dot(b, bitangent), Vector3.Dot(b, tangent)));
const distances = incidentNormals.map((normal) => Vector3.Dot(vertexPosition, normal) - bevelAmount);
const center = _SolveThreePlaneIntersection(incidentNormals, distances);
if (!center) {
return false;
}
const rows = [];
for (let rowIndex = 0; rowIndex <= segments; rowIndex++) {
const rowAmount = rowIndex / segments;
const left = _SlerpDirections(incidentNormals[0], incidentNormals[1], rowAmount);
const right = _SlerpDirections(incidentNormals[0], incidentNormals[2], rowAmount);
const row = [];
for (let columnIndex = 0; columnIndex <= rowIndex; columnIndex++) {
const columnAmount = rowIndex === 0 ? 0 : columnIndex / rowIndex;
const direction = rowIndex === 0 ? incidentNormals[0].clone() : _SlerpDirections(left, right, columnAmount);
row.push({
position: center.add(direction.scale(bevelAmount)),
normal: direction,
attributes: cornerAttributes,
materialIndex,
});
}
rows.push(row);
}
for (let rowIndex = 0; rowIndex < segments; rowIndex++) {
const row = rows[rowIndex];
const nextRow = rows[rowIndex + 1];
for (let columnIndex = 0; columnIndex < row.length; columnIndex++) {
const targetNormal0 = row[columnIndex].position.subtract(center).normalize();
const targetNormal1 = nextRow[columnIndex].po