Files
cadSet/text-to-cad/apps/cad-agent-studio/src/viewer-cadjs/lib/render/glbMeshData.js
T
2026-07-27 16:00:36 +08:00

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);
}