227 lines
8.7 KiB
TypeScript
227 lines
8.7 KiB
TypeScript
type Vector3 = [number, number, number];
|
|
|
|
type SidecarReference = {
|
|
id?: string;
|
|
selectorType?: string;
|
|
label?: string;
|
|
center?: unknown;
|
|
normal?: unknown;
|
|
frame?: Record<string, unknown>;
|
|
bbox?: unknown;
|
|
surface_type?: string;
|
|
triangle_start?: number;
|
|
triangle_count?: number;
|
|
};
|
|
|
|
type Sidecar = { references?: SidecarReference[] };
|
|
|
|
const GLB_CAD_UNIT_SCALE = 1000;
|
|
const FACE_ID_NONE = 0xffffffff;
|
|
|
|
function vector(value: unknown): Vector3 | null {
|
|
if (!Array.isArray(value) || value.length < 3) return null;
|
|
const result = value.slice(0, 3).map(Number) as Vector3;
|
|
return result.every(Number.isFinite) ? result : null;
|
|
}
|
|
|
|
// The local preview runtime converts build123d's Y-up GLB positions into this
|
|
// CAD coordinate convention. Sidecar coordinates remain native CDSL mm.
|
|
function previewVector(value: Vector3): Vector3 {
|
|
return [value[0] * GLB_CAD_UNIT_SCALE, -value[2] * GLB_CAD_UNIT_SCALE, value[1] * GLB_CAD_UNIT_SCALE];
|
|
}
|
|
|
|
function normalized(value: Vector3): Vector3 | null {
|
|
const length = Math.hypot(value[0], value[1], value[2]);
|
|
return length > 1e-9 ? [value[0] / length, value[1] / length, value[2] / length] : null;
|
|
}
|
|
|
|
function dot(left: Vector3, right: Vector3) {
|
|
return left[0] * right[0] + left[1] * right[1] + left[2] * right[2];
|
|
}
|
|
|
|
function subtract(left: Vector3, right: Vector3): Vector3 {
|
|
return [left[0] - right[0], left[1] - right[1], left[2] - right[2]];
|
|
}
|
|
|
|
function cross(left: Vector3, right: Vector3): Vector3 {
|
|
return [
|
|
left[1] * right[2] - left[2] * right[1],
|
|
left[2] * right[0] - left[0] * right[2],
|
|
left[0] * right[1] - left[1] * right[0],
|
|
];
|
|
}
|
|
|
|
function mappedBBox(value: unknown) {
|
|
const bbox = value as { min?: unknown; max?: unknown } | null;
|
|
const min = vector(bbox?.min);
|
|
const max = vector(bbox?.max);
|
|
if (!min || !max) return null;
|
|
const points: Vector3[] = [];
|
|
for (const x of [min[0], max[0]]) {
|
|
for (const y of [min[1], max[1]]) {
|
|
for (const z of [min[2], max[2]]) points.push(previewVector([x, y, z]));
|
|
}
|
|
}
|
|
return {
|
|
min: [Math.min(...points.map((point) => point[0])), Math.min(...points.map((point) => point[1])), Math.min(...points.map((point) => point[2]))],
|
|
max: [Math.max(...points.map((point) => point[0])), Math.max(...points.map((point) => point[1])), Math.max(...points.map((point) => point[2]))],
|
|
};
|
|
}
|
|
|
|
function sourceFrame(reference: SidecarReference) {
|
|
const frame = reference.frame || {};
|
|
const origin = vector(frame.origin_mm) || vector(reference.center);
|
|
const normal = vector(frame.normal) || vector(reference.normal);
|
|
const xDir = vector(frame.x_dir) || [1, 0, 0] as Vector3;
|
|
const yDir = vector(frame.y_dir) || [0, 1, 0] as Vector3;
|
|
return origin && normal ? { origin_mm: origin, normal, x_dir: xDir, y_dir: yDir } : null;
|
|
}
|
|
|
|
function triangleRuns(faceIds: Uint32Array, occurrenceRow: number) {
|
|
const rows: number[] = [];
|
|
let index = 0;
|
|
while (index < faceIds.length) {
|
|
const faceRow = faceIds[index];
|
|
if (faceRow === FACE_ID_NONE) {
|
|
index += 1;
|
|
continue;
|
|
}
|
|
const start = index;
|
|
while (index < faceIds.length && faceIds[index] === faceRow) index += 1;
|
|
rows.push(occurrenceRow, 0, start, index - start, faceRow);
|
|
}
|
|
return new Uint32Array(rows);
|
|
}
|
|
|
|
export function buildCdslSelectorRuntime(sidecar: Sidecar, meshData: any) {
|
|
const sourceReferences = Array.isArray(sidecar?.references) ? sidecar.references : [];
|
|
const faces = sourceReferences
|
|
.filter((reference) => String(reference?.selectorType || "").toLowerCase() === "face")
|
|
.map((reference, rowIndex) => {
|
|
const frame = sourceFrame(reference);
|
|
if (!frame) return null;
|
|
const previewCenter = previewVector(frame.origin_mm);
|
|
const previewNormal = normalized(previewVector(frame.normal));
|
|
if (!previewNormal) return null;
|
|
const displaySelector = String(reference.id || `face_${rowIndex}`);
|
|
return {
|
|
id: displaySelector,
|
|
selectorType: "face",
|
|
normalizedSelector: displaySelector,
|
|
displaySelector,
|
|
label: String(reference.label || displaySelector),
|
|
summary: String(reference.label || displaySelector),
|
|
shortSummary: String(reference.label || displaySelector),
|
|
partId: "glb:0",
|
|
rowIndex,
|
|
pickData: {
|
|
selectorType: "face",
|
|
rowIndex,
|
|
bbox: mappedBBox(reference.bbox),
|
|
center: previewCenter,
|
|
normal: previewNormal,
|
|
surfaceType: String(reference.surface_type || "plane"),
|
|
surface: {
|
|
type: String(reference.surface_type || "plane"),
|
|
origin: previewCenter,
|
|
normal: previewNormal,
|
|
xDir: normalized(previewVector(frame.x_dir)) || [1, 0, 0],
|
|
yDir: normalized(previewVector(frame.y_dir)) || [0, 1, 0],
|
|
},
|
|
cdslFrame: frame,
|
|
cdslCoordinateSystem: "build123d_y_up_glb",
|
|
// Keep native CDSL millimetres for the deterministic edit compiler.
|
|
frame,
|
|
triangleStart: Math.max(0, Math.floor(Number(reference.triangle_start) || 0)),
|
|
triangleCount: Math.max(0, Math.floor(Number(reference.triangle_count) || 0)),
|
|
adjacentSelectors: [],
|
|
},
|
|
};
|
|
})
|
|
.filter(Boolean) as any[];
|
|
|
|
const vertices = meshData?.vertices;
|
|
const indices = meshData?.indices;
|
|
const triangleCount = Math.floor((indices?.length || 0) / 3);
|
|
const faceIds = new Uint32Array(triangleCount);
|
|
faceIds.fill(FACE_ID_NONE);
|
|
const explicitRanges = faces.some((face) => face.pickData.triangleCount > 0);
|
|
if (explicitRanges) {
|
|
for (const face of faces) {
|
|
const start = Math.min(face.pickData.triangleStart, triangleCount);
|
|
const end = Math.min(start + face.pickData.triangleCount, triangleCount);
|
|
faceIds.fill(face.rowIndex, start, end);
|
|
}
|
|
} else if (vertices && indices) {
|
|
const planeTolerance = 0.8 * GLB_CAD_UNIT_SCALE;
|
|
for (let triangleIndex = 0; triangleIndex < triangleCount; triangleIndex += 1) {
|
|
const positions = [0, 1, 2].map((corner) => {
|
|
const offset = Number(indices[(triangleIndex * 3) + corner]) * 3;
|
|
return [Number(vertices[offset]), Number(vertices[offset + 1]), Number(vertices[offset + 2])] as Vector3;
|
|
});
|
|
if (!positions.flat().every(Number.isFinite)) continue;
|
|
const center: Vector3 = [
|
|
(positions[0][0] + positions[1][0] + positions[2][0]) / 3,
|
|
(positions[0][1] + positions[1][1] + positions[2][1]) / 3,
|
|
(positions[0][2] + positions[1][2] + positions[2][2]) / 3,
|
|
];
|
|
const triangleNormal = normalized(cross(subtract(positions[1], positions[0]), subtract(positions[2], positions[0])));
|
|
let bestRow = FACE_ID_NONE;
|
|
let bestScore = Infinity;
|
|
for (const face of faces) {
|
|
const normal = face.pickData.normal as Vector3;
|
|
const distance = Math.abs(dot(subtract(center, face.pickData.center as Vector3), normal));
|
|
if (distance > planeTolerance) continue;
|
|
const alignment = triangleNormal ? Math.abs(dot(triangleNormal, normal)) : 1;
|
|
if (alignment < 0.7) continue;
|
|
const score = distance + ((1 - alignment) * planeTolerance);
|
|
if (score < bestScore) {
|
|
bestScore = score;
|
|
bestRow = face.rowIndex;
|
|
}
|
|
}
|
|
faceIds[triangleIndex] = bestRow;
|
|
}
|
|
}
|
|
|
|
const faceRuns = triangleRuns(faceIds, 0);
|
|
const references = faces;
|
|
return {
|
|
schemaVersion: 1,
|
|
surfaceEdgeRendering: false,
|
|
capabilities: null,
|
|
cadPath: "",
|
|
stepHash: "",
|
|
bbox: meshData?.bounds || null,
|
|
occurrences: [{ id: "glb:0" }],
|
|
shapes: [],
|
|
faces: references,
|
|
edges: [],
|
|
vertices: [],
|
|
references,
|
|
referenceMap: new Map(references.map((reference) => [reference.id, reference])),
|
|
referenceByNormalizedSelector: new Map(references.map((reference) => [reference.normalizedSelector, reference])),
|
|
referenceByDisplaySelector: new Map(references.map((reference) => [reference.displaySelector, reference])),
|
|
faceReferenceByRowIndex: new Map(references.map((reference) => [reference.rowIndex, reference])),
|
|
edgeReferenceByRowIndex: new Map(),
|
|
vertexReferenceByRowIndex: new Map(),
|
|
occurrenceIdByRowIndex: new Map([[0, "glb:0"]]),
|
|
faceReferenceMap: new Map(references.map((reference) => [reference.id, reference])),
|
|
edgeReferenceMap: new Map(),
|
|
vertexReferenceMap: new Map(),
|
|
singleOccurrenceId: "glb:0",
|
|
proxy: {
|
|
facePositions: new Float32Array(0),
|
|
faceIndices: new Uint32Array(0),
|
|
faceIds,
|
|
faceRuns,
|
|
faceRunColumns: ["occurrenceRow", "primitiveIndex", "triangleStart", "triangleCount", "faceRow"],
|
|
edgePositions: new Float32Array(0),
|
|
edgeIndices: new Uint32Array(0),
|
|
edgeIds: new Uint32Array(0),
|
|
faceEdgeRows: [],
|
|
edgeFaceRows: [],
|
|
},
|
|
};
|
|
}
|