493 lines
17 KiB
JavaScript
493 lines
17 KiB
JavaScript
const GLB_CAD_UNIT_SCALE = 1000;
|
|
const CAD_EDGE_BARYCENTRIC_ATTRIBUTE_NAMES = Object.freeze([
|
|
"_cad_edge_barycentric",
|
|
"_CAD_EDGE_BARYCENTRIC"
|
|
]);
|
|
const CAD_EDGE_CLASS_ATTRIBUTE_NAMES = Object.freeze([
|
|
"_cad_edge_class",
|
|
"_CAD_EDGE_CLASS"
|
|
]);
|
|
const GENERATED_STEP_DEFAULT_BASE_COLOR = Object.freeze([0.72, 0.72, 0.72, 1]);
|
|
const BASE_COLOR_EPSILON = 1e-6;
|
|
|
|
function createBoundsAccumulator() {
|
|
return {
|
|
min: [Infinity, Infinity, Infinity],
|
|
max: [-Infinity, -Infinity, -Infinity],
|
|
};
|
|
}
|
|
|
|
function includeBoundsPoint(bounds, x, y, z) {
|
|
if (![x, y, z].every(Number.isFinite)) {
|
|
return;
|
|
}
|
|
bounds.min[0] = Math.min(bounds.min[0], x);
|
|
bounds.min[1] = Math.min(bounds.min[1], y);
|
|
bounds.min[2] = Math.min(bounds.min[2], z);
|
|
bounds.max[0] = Math.max(bounds.max[0], x);
|
|
bounds.max[1] = Math.max(bounds.max[1], y);
|
|
bounds.max[2] = Math.max(bounds.max[2], z);
|
|
}
|
|
|
|
function boundsFromAccumulator(bounds) {
|
|
if (!bounds?.min?.every(Number.isFinite) || !bounds?.max?.every(Number.isFinite)) {
|
|
return {
|
|
min: [0, 0, 0],
|
|
max: [0, 0, 0],
|
|
};
|
|
}
|
|
return {
|
|
min: [...bounds.min],
|
|
max: [...bounds.max],
|
|
};
|
|
}
|
|
|
|
function rawBaseColorFactor(rawMaterial) {
|
|
const value = rawMaterial?.pbrMetallicRoughness?.baseColorFactor;
|
|
return Array.isArray(value) ? value : null;
|
|
}
|
|
|
|
function colorFactorsEqual(left, right) {
|
|
if (!Array.isArray(left) || !Array.isArray(right)) {
|
|
return false;
|
|
}
|
|
return right.every((expected, index) => (
|
|
Math.abs(Number(left[index] ?? (index === 3 ? 1 : 0)) - expected) <= BASE_COLOR_EPSILON
|
|
));
|
|
}
|
|
|
|
function rawMaterialSourceColorHint(rawMaterial) {
|
|
const value = rawMaterial?.extras?.cadSourceColor;
|
|
return value === true || value === false ? value : null;
|
|
}
|
|
|
|
function materialSourceColorHint(material) {
|
|
const value = material?.userData?.cadSourceColor;
|
|
return value === true || value === false ? value : null;
|
|
}
|
|
|
|
function isGeneratedStepDefaultMaterial(rawMaterial, material) {
|
|
return (
|
|
colorFactorsEqual(rawBaseColorFactor(rawMaterial), GENERATED_STEP_DEFAULT_BASE_COLOR) &&
|
|
material?.color?.getHexString?.() === "dddddd"
|
|
);
|
|
}
|
|
|
|
function colorFromMaterial(material, useSourceColors, {
|
|
rawMaterial = null,
|
|
stepTopology = false
|
|
} = {}) {
|
|
if (!useSourceColors || !material?.color) {
|
|
return null;
|
|
}
|
|
const sourceColorHint = materialSourceColorHint(material) ?? rawMaterialSourceColorHint(rawMaterial);
|
|
if (sourceColorHint === false) {
|
|
return null;
|
|
}
|
|
if (stepTopology && sourceColorHint !== true && isGeneratedStepDefaultMaterial(rawMaterial, material)) {
|
|
return null;
|
|
}
|
|
return {
|
|
rgb: [material.color.r, material.color.g, material.color.b],
|
|
hex: `#${material.color.getHexString()}`,
|
|
opacity: materialOpacity(material, rawMaterial),
|
|
};
|
|
}
|
|
|
|
function materialOpacity(material, rawMaterial = null) {
|
|
const materialValue = Number(material?.opacity);
|
|
if (Number.isFinite(materialValue)) {
|
|
return Math.min(Math.max(materialValue, 0), 1);
|
|
}
|
|
const rawAlpha = rawBaseColorFactor(rawMaterial)?.[3];
|
|
const rawValue = Number(rawAlpha);
|
|
return Number.isFinite(rawValue) ? Math.min(Math.max(rawValue, 0), 1) : 1;
|
|
}
|
|
|
|
function materialForGroup(material, group) {
|
|
if (Array.isArray(material)) {
|
|
const materialIndex = Number.isInteger(group?.materialIndex) ? group.materialIndex : 0;
|
|
return material[materialIndex] || material[0] || null;
|
|
}
|
|
return material || null;
|
|
}
|
|
|
|
function materialIndexForGroup(group) {
|
|
return Number.isInteger(group?.materialIndex) ? group.materialIndex : 0;
|
|
}
|
|
|
|
function rawMaterialForGroup(rawMaterials, group) {
|
|
if (!Array.isArray(rawMaterials) || rawMaterials.length <= 0) {
|
|
return null;
|
|
}
|
|
return rawMaterials[materialIndexForGroup(group)] || rawMaterials[0] || null;
|
|
}
|
|
|
|
function isBuild123dAxisCorrectionMatrix(matrix) {
|
|
const elements = matrix?.elements;
|
|
if (!Array.isArray(elements) && !(elements instanceof Float32Array)) {
|
|
return false;
|
|
}
|
|
const expected = [
|
|
1, 0, 0, 0,
|
|
0, 0, -1, 0,
|
|
0, 1, 0, 0,
|
|
0, 0, 0, 1,
|
|
];
|
|
return expected.every((value, index) => Math.abs(Number(elements[index]) - value) < 1e-6);
|
|
}
|
|
|
|
function buildGlbCadRootCorrection(THREE, scene) {
|
|
scene?.updateWorldMatrix?.(true, true);
|
|
const children = Array.isArray(scene?.children) ? scene.children : [];
|
|
if (children.length !== 1 || !isBuild123dAxisCorrectionMatrix(children[0]?.matrixWorld)) {
|
|
return null;
|
|
}
|
|
return new THREE.Matrix4().copy(children[0].matrixWorld).invert();
|
|
}
|
|
|
|
function cadOccurrenceIdForObject(object) {
|
|
let current = object || null;
|
|
while (current) {
|
|
const rawOccurrenceId = String(current.userData?.cadOccurrenceId || "").trim();
|
|
if (rawOccurrenceId) {
|
|
return rawOccurrenceId;
|
|
}
|
|
current = current.parent || null;
|
|
}
|
|
const objectName = String(object?.name || "").trim();
|
|
return /^o\d+(?:\.\d+)*$/.test(objectName) ? objectName : "";
|
|
}
|
|
|
|
function sceneHasCadOccurrenceIds(scene) {
|
|
let found = false;
|
|
scene?.traverse?.((object) => {
|
|
if (found) {
|
|
return;
|
|
}
|
|
found = Boolean(String(object?.userData?.cadOccurrenceId || "").trim());
|
|
});
|
|
return found;
|
|
}
|
|
|
|
function cadVectorFromGlbVector(vector, convertYUpToCad) {
|
|
const x = vector.x * GLB_CAD_UNIT_SCALE;
|
|
const y = vector.y * GLB_CAD_UNIT_SCALE;
|
|
const z = vector.z * GLB_CAD_UNIT_SCALE;
|
|
return convertYUpToCad
|
|
? { x, y: -z, z: y }
|
|
: { x, y, z };
|
|
}
|
|
|
|
function sourceIndexForSlot(indexAttribute, slot) {
|
|
return indexAttribute ? indexAttribute.getX(slot) : slot;
|
|
}
|
|
|
|
function geometryAttributeByName(geometry, names) {
|
|
for (const name of names) {
|
|
const attribute = geometry?.getAttribute?.(name);
|
|
if (attribute) {
|
|
return attribute;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function isValidTriangleSource(positions, indexAttribute, sourceStart) {
|
|
for (let offset = 0; offset < 3; offset += 1) {
|
|
const sourceIndex = sourceIndexForSlot(indexAttribute, sourceStart + offset);
|
|
if (sourceIndex < 0 || sourceIndex >= positions.count) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function countPrimitiveOutputVertices(positions, indexAttribute, sourceStart, triangleVertexCount) {
|
|
let vertexCount = 0;
|
|
for (let localIndex = 0; localIndex < triangleVertexCount; localIndex += 3) {
|
|
if (isValidTriangleSource(positions, indexAttribute, sourceStart + localIndex)) {
|
|
vertexCount += 3;
|
|
}
|
|
}
|
|
return vertexCount;
|
|
}
|
|
|
|
function inspectGlbPrimitive(
|
|
THREE,
|
|
mesh,
|
|
group,
|
|
material,
|
|
useSourceColors,
|
|
rootCorrection,
|
|
convertYUpToCad,
|
|
primitiveIndex = 0,
|
|
partIndex = 0,
|
|
{ rawMaterial = null, stepTopology = false } = {}
|
|
) {
|
|
const geometry = mesh?.geometry;
|
|
const positions = geometry?.getAttribute?.("position");
|
|
if (!positions || positions.itemSize !== 3 || positions.count <= 0) {
|
|
return null;
|
|
}
|
|
mesh.updateWorldMatrix?.(true, false);
|
|
const matrixWorld = mesh.matrixWorld
|
|
? (
|
|
rootCorrection
|
|
? new THREE.Matrix4().multiplyMatrices(rootCorrection, mesh.matrixWorld)
|
|
: new THREE.Matrix4().copy(mesh.matrixWorld)
|
|
)
|
|
: null;
|
|
const normalMatrix = matrixWorld ? new THREE.Matrix3().getNormalMatrix(matrixWorld) : null;
|
|
const normals = geometry.getAttribute("normal");
|
|
const surfaceEdgeBarycentric = geometryAttributeByName(geometry, CAD_EDGE_BARYCENTRIC_ATTRIBUTE_NAMES);
|
|
const surfaceEdgeClass = geometryAttributeByName(geometry, CAD_EDGE_CLASS_ATTRIBUTE_NAMES);
|
|
const indexAttribute = geometry.getIndex?.();
|
|
const sourceStart = Math.max(0, Math.floor(Number(group?.start || 0)));
|
|
const availableCount = indexAttribute?.count || positions.count;
|
|
const rawCount = Math.floor(Number(group?.count || (availableCount - sourceStart)));
|
|
const sourceCount = Math.max(0, Math.min(rawCount, availableCount - sourceStart));
|
|
const triangleVertexCount = sourceCount - (sourceCount % 3);
|
|
if (triangleVertexCount <= 0) {
|
|
return null;
|
|
}
|
|
const vertexCount = countPrimitiveOutputVertices(positions, indexAttribute, sourceStart, triangleVertexCount);
|
|
const triangleCount = Math.floor(vertexCount / 3);
|
|
if (vertexCount <= 0 || triangleCount <= 0) {
|
|
return null;
|
|
}
|
|
|
|
const color = colorFromMaterial(material, useSourceColors, {
|
|
rawMaterial,
|
|
stepTopology
|
|
});
|
|
const cadOccurrenceId = cadOccurrenceIdForObject(mesh);
|
|
const label = String(cadOccurrenceId || mesh?.name || mesh?.parent?.name || `glb:${partIndex}`).trim();
|
|
const id = cadOccurrenceId || `glb:${partIndex}`;
|
|
return {
|
|
mesh,
|
|
positions,
|
|
normals,
|
|
surfaceEdgeBarycentric,
|
|
surfaceEdgeClass,
|
|
indexAttribute,
|
|
sourceStart,
|
|
triangleVertexCount,
|
|
matrixWorld,
|
|
normalMatrix,
|
|
convertYUpToCad,
|
|
id,
|
|
occurrenceId: id,
|
|
primitiveIndex: Math.max(0, Math.floor(Number(primitiveIndex) || 0)),
|
|
name: label || id,
|
|
label: label || id,
|
|
color: color?.hex || "",
|
|
opacity: color ? color.opacity : 1,
|
|
hasSourceColors: Boolean(color),
|
|
vertexCount,
|
|
triangleCount,
|
|
};
|
|
}
|
|
|
|
function writeGlbPrimitive(THREE, descriptor, output, offsets) {
|
|
const partBounds = createBoundsAccumulator();
|
|
const positionVector = new THREE.Vector3();
|
|
const normalVector = new THREE.Vector3();
|
|
const vertexOffset = offsets.vertexOffset;
|
|
const triangleOffset = Math.floor(offsets.indexOffset / 3);
|
|
let localVertexCount = 0;
|
|
|
|
for (let localIndex = 0; localIndex < descriptor.triangleVertexCount; localIndex += 3) {
|
|
if (!isValidTriangleSource(
|
|
descriptor.positions,
|
|
descriptor.indexAttribute,
|
|
descriptor.sourceStart + localIndex
|
|
)) {
|
|
continue;
|
|
}
|
|
for (let triangleOffsetIndex = 0; triangleOffsetIndex < 3; triangleOffsetIndex += 1) {
|
|
const sourceSlot = descriptor.sourceStart + localIndex + triangleOffsetIndex;
|
|
const sourceIndex = sourceIndexForSlot(descriptor.indexAttribute, sourceSlot);
|
|
const outputVertexIndex = vertexOffset + localVertexCount;
|
|
const outputComponentIndex = outputVertexIndex * 3;
|
|
positionVector.set(
|
|
descriptor.positions.getX(sourceIndex),
|
|
descriptor.positions.getY(sourceIndex),
|
|
descriptor.positions.getZ(sourceIndex)
|
|
);
|
|
if (descriptor.matrixWorld) {
|
|
positionVector.applyMatrix4(descriptor.matrixWorld);
|
|
}
|
|
const cadPosition = cadVectorFromGlbVector(positionVector, descriptor.convertYUpToCad);
|
|
const x = cadPosition.x;
|
|
const y = cadPosition.y;
|
|
const z = cadPosition.z;
|
|
output.vertices[outputComponentIndex] = x;
|
|
output.vertices[outputComponentIndex + 1] = y;
|
|
output.vertices[outputComponentIndex + 2] = z;
|
|
includeBoundsPoint(partBounds, x, y, z);
|
|
includeBoundsPoint(output.bounds, x, y, z);
|
|
|
|
if (descriptor.normals?.itemSize === 3 && sourceIndex < descriptor.normals.count) {
|
|
normalVector.set(
|
|
descriptor.normals.getX(sourceIndex),
|
|
descriptor.normals.getY(sourceIndex),
|
|
descriptor.normals.getZ(sourceIndex)
|
|
);
|
|
if (descriptor.normalMatrix) {
|
|
normalVector.applyMatrix3(descriptor.normalMatrix).normalize();
|
|
}
|
|
const cadNormal = descriptor.convertYUpToCad
|
|
? { x: normalVector.x, y: -normalVector.z, z: normalVector.y }
|
|
: normalVector;
|
|
output.normals[outputComponentIndex] = cadNormal.x;
|
|
output.normals[outputComponentIndex + 1] = cadNormal.y;
|
|
output.normals[outputComponentIndex + 2] = cadNormal.z;
|
|
}
|
|
if (output.surfaceEdgeBarycentric && descriptor.surfaceEdgeBarycentric?.itemSize === 3 && sourceIndex < descriptor.surfaceEdgeBarycentric.count) {
|
|
output.surfaceEdgeBarycentric[outputComponentIndex] = descriptor.surfaceEdgeBarycentric.getX(sourceIndex);
|
|
output.surfaceEdgeBarycentric[outputComponentIndex + 1] = descriptor.surfaceEdgeBarycentric.getY(sourceIndex);
|
|
output.surfaceEdgeBarycentric[outputComponentIndex + 2] = descriptor.surfaceEdgeBarycentric.getZ(sourceIndex);
|
|
}
|
|
if (output.surfaceEdgeClass && descriptor.surfaceEdgeClass?.itemSize === 3 && sourceIndex < descriptor.surfaceEdgeClass.count) {
|
|
output.surfaceEdgeClass[outputComponentIndex] = descriptor.surfaceEdgeClass.getX(sourceIndex);
|
|
output.surfaceEdgeClass[outputComponentIndex + 1] = descriptor.surfaceEdgeClass.getY(sourceIndex);
|
|
output.surfaceEdgeClass[outputComponentIndex + 2] = descriptor.surfaceEdgeClass.getZ(sourceIndex);
|
|
}
|
|
output.indices[offsets.indexOffset + localVertexCount] = outputVertexIndex;
|
|
localVertexCount += 1;
|
|
}
|
|
}
|
|
|
|
const triangleCount = Math.floor(localVertexCount / 3);
|
|
const part = {
|
|
id: descriptor.id,
|
|
occurrenceId: descriptor.occurrenceId,
|
|
primitiveIndex: descriptor.primitiveIndex,
|
|
name: descriptor.name,
|
|
label: descriptor.label,
|
|
nodeType: "part",
|
|
color: descriptor.color,
|
|
opacity: descriptor.opacity,
|
|
hasSourceColors: descriptor.hasSourceColors,
|
|
bounds: boundsFromAccumulator(partBounds),
|
|
vertexOffset,
|
|
vertexCount: localVertexCount,
|
|
triangleOffset,
|
|
triangleCount,
|
|
edgeIndexOffset: 0,
|
|
edgeIndexCount: 0,
|
|
};
|
|
|
|
offsets.vertexOffset += localVertexCount;
|
|
offsets.indexOffset += triangleCount * 3;
|
|
return part;
|
|
}
|
|
|
|
function buildMeshDataFromGltf(THREE, gltf) {
|
|
const declaredMaterials = Array.isArray(gltf?.parser?.json?.materials) && gltf.parser.json.materials.length > 0;
|
|
const rawMaterials = Array.isArray(gltf?.parser?.json?.materials) ? gltf.parser.json.materials : [];
|
|
const rootCorrection = buildGlbCadRootCorrection(THREE, gltf?.scene);
|
|
const hasStepTopology = !!gltf?.parser?.json?.extensions?.STEP_topology;
|
|
const convertYUpToCad = !hasStepTopology && !sceneHasCadOccurrenceIds(gltf?.scene) && !rootCorrection;
|
|
const descriptors = [];
|
|
const colorSet = new Set();
|
|
let totalVertexCount = 0;
|
|
let totalIndexCount = 0;
|
|
let hasSurfaceEdgeAttributes = false;
|
|
const nextPrimitiveIndexByOccurrence = new Map();
|
|
gltf?.scene?.traverse?.((object) => {
|
|
if (!object?.isMesh || !object.geometry) {
|
|
return;
|
|
}
|
|
const occurrenceId = cadOccurrenceIdForObject(object) || String(object?.name || `glb:${descriptors.length}`).trim();
|
|
const primitiveIndexBase = nextPrimitiveIndexByOccurrence.get(occurrenceId) || 0;
|
|
const groups = Array.isArray(object.geometry.groups) && object.geometry.groups.length
|
|
? object.geometry.groups
|
|
: [null];
|
|
groups.forEach((group, primitiveIndex) => {
|
|
const descriptor = inspectGlbPrimitive(
|
|
THREE,
|
|
object,
|
|
group,
|
|
materialForGroup(object.material, group),
|
|
declaredMaterials,
|
|
rootCorrection,
|
|
convertYUpToCad,
|
|
primitiveIndexBase + primitiveIndex,
|
|
descriptors.length,
|
|
{
|
|
rawMaterial: rawMaterialForGroup(rawMaterials, group),
|
|
stepTopology: hasStepTopology
|
|
}
|
|
);
|
|
if (!descriptor) {
|
|
return;
|
|
}
|
|
descriptors.push(descriptor);
|
|
totalVertexCount += descriptor.vertexCount;
|
|
totalIndexCount += descriptor.triangleCount * 3;
|
|
hasSurfaceEdgeAttributes ||= Boolean(descriptor.surfaceEdgeBarycentric && descriptor.surfaceEdgeClass);
|
|
if (descriptor.color) {
|
|
colorSet.add(descriptor.color.toLowerCase());
|
|
}
|
|
});
|
|
nextPrimitiveIndexByOccurrence.set(occurrenceId, primitiveIndexBase + groups.length);
|
|
});
|
|
const vertices = new Float32Array(totalVertexCount * 3);
|
|
const indices = new Uint32Array(totalIndexCount);
|
|
const normals = new Float32Array(totalVertexCount * 3);
|
|
const surfaceEdgeBarycentric = hasSurfaceEdgeAttributes
|
|
? new Float32Array(totalVertexCount * 3)
|
|
: null;
|
|
const surfaceEdgeClass = hasSurfaceEdgeAttributes
|
|
? new Uint8Array(totalVertexCount * 3)
|
|
: null;
|
|
const parts = [];
|
|
const output = {
|
|
vertices,
|
|
indices,
|
|
normals,
|
|
surfaceEdgeBarycentric,
|
|
surfaceEdgeClass,
|
|
bounds: createBoundsAccumulator(),
|
|
};
|
|
const offsets = {
|
|
vertexOffset: 0,
|
|
indexOffset: 0,
|
|
};
|
|
for (const descriptor of descriptors) {
|
|
parts.push(writeGlbPrimitive(THREE, descriptor, output, offsets));
|
|
}
|
|
const colors = new Float32Array(0);
|
|
return {
|
|
vertices,
|
|
indices,
|
|
normals,
|
|
surfaceEdgeBarycentric: surfaceEdgeBarycentric || new Float32Array(0),
|
|
surfaceEdgeClass: surfaceEdgeClass || new Uint8Array(0),
|
|
colors,
|
|
edge_indices: new Uint32Array(0),
|
|
bounds: boundsFromAccumulator(output.bounds),
|
|
parts,
|
|
has_source_colors: colorSet.size > 0,
|
|
sourceColor: colorSet.size === 1 ? [...colorSet][0] : "",
|
|
};
|
|
}
|
|
|
|
function parseGlb(GLTFLoader, buffer) {
|
|
const loader = new GLTFLoader();
|
|
return new Promise((resolve, reject) => {
|
|
loader.parse(buffer, "", resolve, reject);
|
|
});
|
|
}
|
|
|
|
export async function buildMeshDataFromGlbBuffer(buffer) {
|
|
const [THREE, { GLTFLoader }] = await Promise.all([
|
|
import("three"),
|
|
import("three/examples/jsm/loaders/GLTFLoader.js"),
|
|
]);
|
|
const gltf = await parseGlb(GLTFLoader, buffer);
|
|
return buildMeshDataFromGltf(THREE, gltf);
|
|
}
|