mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
472 lines
15 KiB
JavaScript
472 lines
15 KiB
JavaScript
import { buildCadRefToken } from "../cadRefs.js";
|
|
|
|
function isObject(value) {
|
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
}
|
|
|
|
function toRows(manifest, rowKey, columnsKey) {
|
|
const columns = manifest?.tables?.[columnsKey];
|
|
const rows = manifest?.[rowKey];
|
|
if (!Array.isArray(columns) || !Array.isArray(rows)) {
|
|
return [];
|
|
}
|
|
return rows
|
|
.filter(Array.isArray)
|
|
.map((row) => Object.fromEntries(columns.map((column, index) => [column, row[index]])));
|
|
}
|
|
|
|
function relationArray(manifest, buffers, relationKey, viewKey) {
|
|
const direct = manifest?.relations?.[relationKey];
|
|
if (Array.isArray(direct) || ArrayBuffer.isView(direct)) {
|
|
return direct;
|
|
}
|
|
const viewName = manifest?.relations?.[viewKey];
|
|
if (typeof viewName === "string" && buffers?.[viewName]) {
|
|
return buffers[viewName];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function selectorPrefix(singleOccurrenceId, selector) {
|
|
if (!singleOccurrenceId || !selector.startsWith(`${singleOccurrenceId}.`)) {
|
|
return selector;
|
|
}
|
|
const suffix = selector.slice(singleOccurrenceId.length + 1);
|
|
return suffix.startsWith("s") || suffix.startsWith("f") || suffix.startsWith("e") || suffix.startsWith("v") ? suffix : selector;
|
|
}
|
|
|
|
function selectorTypeLabel(selectorType) {
|
|
if (selectorType === "occurrence") {
|
|
return "Occurrence";
|
|
}
|
|
if (selectorType === "shape") {
|
|
return "Shape";
|
|
}
|
|
if (selectorType === "face") {
|
|
return "Face";
|
|
}
|
|
if (selectorType === "vertex") {
|
|
return "Corner";
|
|
}
|
|
return "Edge";
|
|
}
|
|
|
|
function transformPoint(transform, point) {
|
|
if (!Array.isArray(point) || point.length < 3) {
|
|
return point;
|
|
}
|
|
if (!Array.isArray(transform) || transform.length < 16) {
|
|
return [Number(point[0]), Number(point[1]), Number(point[2])];
|
|
}
|
|
const x = Number(point[0]);
|
|
const y = Number(point[1]);
|
|
const z = Number(point[2]);
|
|
return [
|
|
(transform[0] * x) + (transform[1] * y) + (transform[2] * z) + transform[3],
|
|
(transform[4] * x) + (transform[5] * y) + (transform[6] * z) + transform[7],
|
|
(transform[8] * x) + (transform[9] * y) + (transform[10] * z) + transform[11],
|
|
];
|
|
}
|
|
|
|
function normalizeVector(vector) {
|
|
const x = Number(vector?.[0] || 0);
|
|
const y = Number(vector?.[1] || 0);
|
|
const z = Number(vector?.[2] || 0);
|
|
const magnitude = Math.hypot(x, y, z);
|
|
if (magnitude <= 1e-9) {
|
|
return null;
|
|
}
|
|
return [x / magnitude, y / magnitude, z / magnitude];
|
|
}
|
|
|
|
function transformVector(transform, vector) {
|
|
if (!Array.isArray(vector) || vector.length < 3 || !Array.isArray(transform) || transform.length < 16) {
|
|
return normalizeVector(vector || []);
|
|
}
|
|
return normalizeVector([
|
|
(transform[0] * vector[0]) + (transform[1] * vector[1]) + (transform[2] * vector[2]),
|
|
(transform[4] * vector[0]) + (transform[5] * vector[1]) + (transform[6] * vector[2]),
|
|
(transform[8] * vector[0]) + (transform[9] * vector[1]) + (transform[10] * vector[2]),
|
|
]);
|
|
}
|
|
|
|
function transformBBox(transform, bbox) {
|
|
if (!isObject(bbox)) {
|
|
return bbox;
|
|
}
|
|
const min = Array.isArray(bbox.min) ? bbox.min : [0, 0, 0];
|
|
const max = Array.isArray(bbox.max) ? bbox.max : [0, 0, 0];
|
|
const corners = [
|
|
[min[0], min[1], min[2]],
|
|
[min[0], min[1], max[2]],
|
|
[min[0], max[1], min[2]],
|
|
[min[0], max[1], max[2]],
|
|
[max[0], min[1], min[2]],
|
|
[max[0], min[1], max[2]],
|
|
[max[0], max[1], min[2]],
|
|
[max[0], max[1], max[2]],
|
|
].map((point) => transformPoint(transform, point));
|
|
const xs = corners.map((point) => point[0]);
|
|
const ys = corners.map((point) => point[1]);
|
|
const zs = corners.map((point) => point[2]);
|
|
return {
|
|
min: [Math.min(...xs), Math.min(...ys), Math.min(...zs)],
|
|
max: [Math.max(...xs), Math.max(...ys), Math.max(...zs)],
|
|
};
|
|
}
|
|
|
|
function transformParams(transform, params) {
|
|
if (!isObject(params)) {
|
|
return params;
|
|
}
|
|
const pointKeys = new Set(["origin", "center", "location"]);
|
|
const vectorKeys = new Set(["axis", "direction", "normal"]);
|
|
return Object.fromEntries(Object.entries(params).map(([key, value]) => {
|
|
if (pointKeys.has(key) && Array.isArray(value) && value.length === 3) {
|
|
return [key, transformPoint(transform, value)];
|
|
}
|
|
if (vectorKeys.has(key) && Array.isArray(value) && value.length === 3) {
|
|
return [key, transformVector(transform, value)];
|
|
}
|
|
return [key, value];
|
|
}));
|
|
}
|
|
|
|
function transformPositions(values, transform) {
|
|
if (!(values instanceof Float32Array) || !Array.isArray(transform) || transform.length < 16) {
|
|
return values;
|
|
}
|
|
const next = new Float32Array(values.length);
|
|
for (let index = 0; index < values.length; index += 3) {
|
|
const point = transformPoint(transform, [values[index], values[index + 1], values[index + 2]]);
|
|
next[index] = point[0];
|
|
next[index + 1] = point[1];
|
|
next[index + 2] = point[2];
|
|
}
|
|
return next;
|
|
}
|
|
|
|
function referenceIdForRow(displaySelector, selectorType, partId) {
|
|
if (partId) {
|
|
return `topology|${partId}|${selectorType}|${displaySelector}`;
|
|
}
|
|
return displaySelector;
|
|
}
|
|
|
|
function referenceSummary(selectorType, row) {
|
|
if (selectorType === "occurrence") {
|
|
return String(row.name || row.sourceName || row.id || "").trim();
|
|
}
|
|
if (selectorType === "shape") {
|
|
return `${row.kind || "shape"}${row.volume ? ` volume=${row.volume}` : row.area ? ` area=${row.area}` : ""}`;
|
|
}
|
|
if (selectorType === "face") {
|
|
return `${row.surfaceType || "face"} area=${row.area ?? 0}`;
|
|
}
|
|
if (selectorType === "vertex") {
|
|
return `corner edges=${row.edgeCount ?? 0}`;
|
|
}
|
|
return `${row.curveType || "edge"} length=${row.length ?? 0}`;
|
|
}
|
|
|
|
function selectorForRow(selectorType, row, rowIndex, singleOccurrenceId, remapOccurrenceId = "") {
|
|
if (!row || !Number.isFinite(Number(rowIndex))) {
|
|
return "";
|
|
}
|
|
const occurrenceId = String(remapOccurrenceId || "").trim();
|
|
if (occurrenceId) {
|
|
if (selectorType === "occurrence") {
|
|
return occurrenceId;
|
|
}
|
|
const selectorKind = selectorType === "shape"
|
|
? "s"
|
|
: selectorType === "face"
|
|
? "f"
|
|
: selectorType === "vertex"
|
|
? "v"
|
|
: "e";
|
|
return `${occurrenceId}.${selectorKind}${rowIndex + 1}`;
|
|
}
|
|
return selectorPrefix(singleOccurrenceId, String(row?.id || "").trim());
|
|
}
|
|
|
|
function buildAdjacencySelectors(row, relationRows, targetRows, singleOccurrenceId, idKey, startKey, countKey, targetSelectorType, remapOccurrenceId) {
|
|
const start = Number(row?.[startKey] || 0);
|
|
const count = Number(row?.[countKey] || 0);
|
|
const selectors = [];
|
|
for (const rowIndex of Array.from(relationRows).slice(start, start + count)) {
|
|
const targetRowIndex = Number(rowIndex);
|
|
const targetRow = targetRows[targetRowIndex];
|
|
const selector = selectorForRow(
|
|
targetSelectorType,
|
|
targetRow,
|
|
targetRowIndex,
|
|
singleOccurrenceId,
|
|
remapOccurrenceId
|
|
) || String(targetRow?.[idKey] || "").trim();
|
|
if (selector) {
|
|
selectors.push(selector);
|
|
}
|
|
}
|
|
return selectors;
|
|
}
|
|
|
|
function buildReference({
|
|
selectorType,
|
|
row,
|
|
rowIndex,
|
|
singleOccurrenceId,
|
|
copyCadPath,
|
|
partId,
|
|
selectorTransform,
|
|
relationRows,
|
|
targetRows,
|
|
targetKey,
|
|
startKey,
|
|
countKey,
|
|
remapOccurrenceId = "",
|
|
targetSelectorType = "",
|
|
}) {
|
|
const normalizedSelector = selectorForRow(selectorType, row, rowIndex, singleOccurrenceId, remapOccurrenceId);
|
|
const displaySelector = normalizedSelector;
|
|
const id = referenceIdForRow(displaySelector, selectorType, partId);
|
|
const summary = referenceSummary(selectorType, row);
|
|
const copyText = buildCadRefToken({ cadPath: copyCadPath, selector: displaySelector });
|
|
const adjacentSelectors = relationRows && targetRows
|
|
? buildAdjacencySelectors(
|
|
row,
|
|
relationRows,
|
|
targetRows,
|
|
singleOccurrenceId,
|
|
targetKey,
|
|
startKey,
|
|
countKey,
|
|
targetSelectorType,
|
|
remapOccurrenceId
|
|
)
|
|
: [];
|
|
return {
|
|
id,
|
|
selectorType,
|
|
normalizedSelector,
|
|
displaySelector,
|
|
label: `${selectorTypeLabel(selectorType)} ${displaySelector}`,
|
|
summary,
|
|
shortSummary: summary,
|
|
copyText: summary ? `${copyText} ${summary}` : copyText,
|
|
partId,
|
|
occurrenceId: row.occurrenceId ? selectorPrefix(singleOccurrenceId, String(row.occurrenceId)) : "",
|
|
shapeId: row.shapeId ? selectorPrefix(singleOccurrenceId, String(row.shapeId)) : "",
|
|
rowIndex,
|
|
pickData: {
|
|
selectorType,
|
|
rowIndex,
|
|
bbox: row.bbox || null,
|
|
center: row.center || null,
|
|
normal: row.normal || null,
|
|
params: row.params || null,
|
|
triangleStart: row.triangleStart ?? 0,
|
|
triangleCount: row.triangleCount ?? 0,
|
|
segmentStart: row.segmentStart ?? 0,
|
|
segmentCount: row.segmentCount ?? 0,
|
|
adjacentSelectors,
|
|
transform: selectorTransform || null,
|
|
},
|
|
};
|
|
}
|
|
|
|
function buildLeafOccurrenceIds(shapes) {
|
|
return [...new Set(
|
|
shapes
|
|
.map((row) => String(row.occurrenceId || "").trim())
|
|
.filter(Boolean)
|
|
)].sort();
|
|
}
|
|
|
|
function transformRows(rows, transform) {
|
|
if (!Array.isArray(transform) || transform.length < 16) {
|
|
return rows;
|
|
}
|
|
return rows.map((row) => ({
|
|
...row,
|
|
transform: Array.isArray(row.transform) ? row.transform : transform,
|
|
bbox: row.bbox ? transformBBox(transform, row.bbox) : row.bbox,
|
|
center: Array.isArray(row.center) ? transformPoint(transform, row.center) : row.center,
|
|
normal: Array.isArray(row.normal) ? transformVector(transform, row.normal) : row.normal,
|
|
params: row.params ? transformParams(transform, row.params) : row.params,
|
|
}));
|
|
}
|
|
|
|
function applySequentialRelationStarts(rows, relationSpecs) {
|
|
const specs = Array.isArray(relationSpecs?.[0]) ? relationSpecs : [relationSpecs];
|
|
const nextStarts = specs.map(() => 0);
|
|
return rows.map((row) => {
|
|
const nextRow = { ...row };
|
|
specs.forEach(([startKey, countKey], specIndex) => {
|
|
const count = Math.max(0, Number(row?.[countKey] || 0));
|
|
nextRow[startKey] = nextStarts[specIndex];
|
|
nextRow[countKey] = count;
|
|
nextStarts[specIndex] += count;
|
|
});
|
|
return nextRow;
|
|
});
|
|
}
|
|
|
|
export function buildSelectorRuntime(bundle, {
|
|
copyCadPath = "",
|
|
partId = "",
|
|
transform = null,
|
|
remapOccurrenceId = "",
|
|
} = {}) {
|
|
const manifest = bundle?.manifest || {};
|
|
const buffers = bundle?.buffers || {};
|
|
const faceRelations = relationArray(manifest, buffers, "faceEdgeRows", "faceEdgeRowsView");
|
|
const edgeRelations = relationArray(manifest, buffers, "edgeFaceRows", "edgeFaceRowsView");
|
|
const edgeVertexRelations = relationArray(manifest, buffers, "edgeVertexRows", "edgeVertexRowsView");
|
|
const vertexEdgeRelations = relationArray(manifest, buffers, "vertexEdgeRows", "vertexEdgeRowsView");
|
|
const occurrences = transformRows(toRows(manifest, "occurrences", "occurrenceColumns"), transform);
|
|
const shapes = transformRows(toRows(manifest, "shapes", "shapeColumns"), transform);
|
|
const faces = applySequentialRelationStarts(
|
|
transformRows(toRows(manifest, "faces", "faceColumns"), transform),
|
|
[["edgeStart", "edgeCount"]]
|
|
);
|
|
const edges = applySequentialRelationStarts(
|
|
transformRows(toRows(manifest, "edges", "edgeColumns"), transform),
|
|
[["faceStart", "faceCount"], ["vertexStart", "vertexCount"]]
|
|
);
|
|
const vertices = applySequentialRelationStarts(
|
|
transformRows(toRows(manifest, "vertices", "vertexColumns"), transform),
|
|
[["edgeStart", "edgeCount"]]
|
|
);
|
|
const leafOccurrenceIds = buildLeafOccurrenceIds(shapes);
|
|
const singleOccurrenceId = leafOccurrenceIds.length === 1 ? leafOccurrenceIds[0] : "";
|
|
const selectorBuffers = {
|
|
facePositions: transformPositions(buffers.facePositions, transform),
|
|
faceIndices: buffers.faceIndices || new Uint32Array(0),
|
|
faceIds: buffers.faceIds || new Uint32Array(0),
|
|
edgePositions: transformPositions(buffers.edgePositions, transform),
|
|
edgeIndices: buffers.edgeIndices || new Uint32Array(0),
|
|
edgeIds: buffers.edgeIds || new Uint32Array(0),
|
|
vertexPositions: transformPositions(buffers.vertexPositions, transform),
|
|
vertexIds: buffers.vertexIds || new Uint32Array(0),
|
|
faceEdgeRows: faceRelations,
|
|
edgeFaceRows: edgeRelations,
|
|
edgeVertexRows: edgeVertexRelations,
|
|
vertexEdgeRows: vertexEdgeRelations,
|
|
};
|
|
|
|
const references = [];
|
|
references.push(...occurrences.map((row, rowIndex) => buildReference({
|
|
selectorType: "occurrence",
|
|
row,
|
|
rowIndex,
|
|
singleOccurrenceId,
|
|
copyCadPath,
|
|
partId,
|
|
selectorTransform: transform,
|
|
remapOccurrenceId,
|
|
})));
|
|
references.push(...shapes.map((row, rowIndex) => buildReference({
|
|
selectorType: "shape",
|
|
row,
|
|
rowIndex,
|
|
singleOccurrenceId,
|
|
copyCadPath,
|
|
partId,
|
|
selectorTransform: transform,
|
|
remapOccurrenceId,
|
|
})));
|
|
references.push(...faces.map((row, rowIndex) => buildReference({
|
|
selectorType: "face",
|
|
row,
|
|
rowIndex,
|
|
singleOccurrenceId,
|
|
copyCadPath,
|
|
partId,
|
|
selectorTransform: transform,
|
|
relationRows: faceRelations,
|
|
targetRows: edges,
|
|
targetKey: "id",
|
|
startKey: "edgeStart",
|
|
countKey: "edgeCount",
|
|
remapOccurrenceId,
|
|
targetSelectorType: "edge",
|
|
})));
|
|
references.push(...edges.map((row, rowIndex) => buildReference({
|
|
selectorType: "edge",
|
|
row,
|
|
rowIndex,
|
|
singleOccurrenceId,
|
|
copyCadPath,
|
|
partId,
|
|
selectorTransform: transform,
|
|
relationRows: edgeRelations,
|
|
targetRows: faces,
|
|
targetKey: "id",
|
|
startKey: "faceStart",
|
|
countKey: "faceCount",
|
|
remapOccurrenceId,
|
|
targetSelectorType: "face",
|
|
})));
|
|
references.push(...vertices.map((row, rowIndex) => buildReference({
|
|
selectorType: "vertex",
|
|
row,
|
|
rowIndex,
|
|
singleOccurrenceId,
|
|
copyCadPath,
|
|
partId,
|
|
selectorTransform: transform,
|
|
relationRows: vertexEdgeRelations,
|
|
targetRows: edges,
|
|
targetKey: "id",
|
|
startKey: "edgeStart",
|
|
countKey: "edgeCount",
|
|
remapOccurrenceId,
|
|
targetSelectorType: "edge",
|
|
})));
|
|
|
|
const referenceMap = new Map(references.map((reference) => [reference.id, reference]));
|
|
const referenceByNormalizedSelector = new Map(
|
|
references.map((reference) => [reference.normalizedSelector, reference])
|
|
);
|
|
const referenceByDisplaySelector = new Map(
|
|
references.map((reference) => [reference.displaySelector, reference])
|
|
);
|
|
const faceReferenceByRowIndex = new Map(
|
|
references
|
|
.filter((reference) => reference.selectorType === "face")
|
|
.map((reference) => [reference.rowIndex, reference])
|
|
);
|
|
const edgeReferenceByRowIndex = new Map(
|
|
references
|
|
.filter((reference) => reference.selectorType === "edge")
|
|
.map((reference) => [reference.rowIndex, reference])
|
|
);
|
|
const vertexReferenceByRowIndex = new Map(
|
|
references
|
|
.filter((reference) => reference.selectorType === "vertex")
|
|
.map((reference) => [reference.rowIndex, reference])
|
|
);
|
|
return {
|
|
cadPath: copyCadPath || String(manifest.cadRef || "").trim(),
|
|
stepHash: String(manifest.stepHash || ""),
|
|
bbox: transform ? transformBBox(transform, manifest.bbox || {}) : manifest.bbox,
|
|
occurrences,
|
|
shapes,
|
|
faces,
|
|
edges,
|
|
vertices,
|
|
references,
|
|
referenceMap,
|
|
referenceByNormalizedSelector,
|
|
referenceByDisplaySelector,
|
|
faceReferenceByRowIndex,
|
|
edgeReferenceByRowIndex,
|
|
vertexReferenceByRowIndex,
|
|
faceReferenceMap: new Map(references.filter((reference) => reference.selectorType === "face").map((reference) => [reference.id, reference])),
|
|
edgeReferenceMap: new Map(references.filter((reference) => reference.selectorType === "edge").map((reference) => [reference.id, reference])),
|
|
vertexReferenceMap: new Map(references.filter((reference) => reference.selectorType === "vertex").map((reference) => [reference.id, reference])),
|
|
singleOccurrenceId,
|
|
proxy: selectorBuffers,
|
|
};
|
|
}
|