Files
cdsl-cad/frontend/src/lib/cdsl-selector-runtime.ts
T
2026-08-19 19:34:30 +08:00

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: [],
},
};
}