435 lines
14 KiB
JavaScript
435 lines
14 KiB
JavaScript
function isFloat32Array(value) {
|
|
return value instanceof Float32Array;
|
|
}
|
|
|
|
function isUint32Array(value) {
|
|
return value instanceof Uint32Array;
|
|
}
|
|
|
|
function pointDistance(left, right) {
|
|
return Math.hypot(
|
|
Number(right?.[0] || 0) - Number(left?.[0] || 0),
|
|
Number(right?.[1] || 0) - Number(left?.[1] || 0),
|
|
Number(right?.[2] || 0) - Number(left?.[2] || 0)
|
|
);
|
|
}
|
|
|
|
function boundsForPolylines(polylines) {
|
|
const min = [Infinity, Infinity, Infinity];
|
|
const max = [-Infinity, -Infinity, -Infinity];
|
|
for (const positions of polylines) {
|
|
for (let offset = 0; offset + 2 < positions.length; offset += 3) {
|
|
min[0] = Math.min(min[0], positions[offset]);
|
|
min[1] = Math.min(min[1], positions[offset + 1]);
|
|
min[2] = Math.min(min[2], positions[offset + 2]);
|
|
max[0] = Math.max(max[0], positions[offset]);
|
|
max[1] = Math.max(max[1], positions[offset + 1]);
|
|
max[2] = Math.max(max[2], positions[offset + 2]);
|
|
}
|
|
}
|
|
if (!Number.isFinite(min[0]) || !Number.isFinite(max[0])) {
|
|
return null;
|
|
}
|
|
return { min, max };
|
|
}
|
|
|
|
function stitchToleranceForPolylines(polylines) {
|
|
const bounds = boundsForPolylines(polylines);
|
|
if (!bounds) {
|
|
return 1e-5;
|
|
}
|
|
return Math.max(1e-5, pointDistance(bounds.min, bounds.max) * 1e-7);
|
|
}
|
|
|
|
function pointFromProxy(proxy, vertexIndex) {
|
|
const offset = Number(vertexIndex) * 3;
|
|
if (
|
|
!Number.isInteger(vertexIndex) ||
|
|
offset < 0 ||
|
|
offset + 2 >= proxy.edgePositions.length
|
|
) {
|
|
return null;
|
|
}
|
|
return [
|
|
proxy.edgePositions[offset],
|
|
proxy.edgePositions[offset + 1],
|
|
proxy.edgePositions[offset + 2]
|
|
];
|
|
}
|
|
|
|
function normalizePartIdSet(value) {
|
|
return new Set(
|
|
(Array.isArray(value) ? value : [value])
|
|
.map((id) => String(id || "").trim())
|
|
.filter(Boolean)
|
|
);
|
|
}
|
|
|
|
function normalizeVisibilityClassSet(value) {
|
|
const values = (Array.isArray(value) ? value : [value])
|
|
.map((item) => String(item || "").trim().toLowerCase())
|
|
.filter(Boolean);
|
|
return values.length ? new Set(values) : null;
|
|
}
|
|
|
|
function occurrenceMatchesPartIds(occurrenceId, partIds) {
|
|
const normalizedOccurrenceId = String(occurrenceId || "").trim();
|
|
if (!normalizedOccurrenceId || !partIds?.size) {
|
|
return false;
|
|
}
|
|
for (const partId of partIds) {
|
|
if (normalizedOccurrenceId === partId || normalizedOccurrenceId.startsWith(`${partId}.`)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function edgeRowMatchesPartIds(row, partIds) {
|
|
return occurrenceMatchesPartIds(row?.occurrenceId || row?.partId || row?.id, partIds);
|
|
}
|
|
|
|
function edgeRowPassesPartFilter(row, includePartIds, excludePartIds) {
|
|
if (includePartIds.size && !edgeRowMatchesPartIds(row, includePartIds)) {
|
|
return false;
|
|
}
|
|
if (excludePartIds.size && edgeRowMatchesPartIds(row, excludePartIds)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function edgeRowPassesVisibilityClassFilter(row, visibilityClasses) {
|
|
if (!visibilityClasses?.size) {
|
|
return true;
|
|
}
|
|
const visibilityClass = String(row?.visibilityClass || "feature").trim().toLowerCase() || "feature";
|
|
return visibilityClasses.has(visibilityClass);
|
|
}
|
|
|
|
function edgeRowPassesFilters(row, includePartIds, excludePartIds, visibilityClasses) {
|
|
return (
|
|
edgeRowPassesPartFilter(row, includePartIds, excludePartIds) &&
|
|
edgeRowPassesVisibilityClassFilter(row, visibilityClasses)
|
|
);
|
|
}
|
|
|
|
function pushPoint(target, point, tolerance = 1e-7) {
|
|
if (!point) {
|
|
return;
|
|
}
|
|
const last = target.length >= 3
|
|
? [target[target.length - 3], target[target.length - 2], target[target.length - 1]]
|
|
: null;
|
|
if (!last || pointDistance(last, point) > tolerance) {
|
|
target.push(point[0], point[1], point[2]);
|
|
}
|
|
}
|
|
|
|
function buildTopologyPolylinesForRun(proxy, segmentStart, segmentCount) {
|
|
const polylines = [];
|
|
let current = [];
|
|
let lastPoint = null;
|
|
for (let segmentIndex = segmentStart; segmentIndex < segmentStart + segmentCount; segmentIndex += 1) {
|
|
const startVertexIndex = Number(proxy.edgeIndices[segmentIndex * 2]);
|
|
const endVertexIndex = Number(proxy.edgeIndices[(segmentIndex * 2) + 1]);
|
|
const startPoint = pointFromProxy(proxy, startVertexIndex);
|
|
const endPoint = pointFromProxy(proxy, endVertexIndex);
|
|
if (!startPoint || !endPoint || pointDistance(startPoint, endPoint) <= 1e-10) {
|
|
continue;
|
|
}
|
|
if (lastPoint && pointDistance(lastPoint, startPoint) > 1e-5) {
|
|
if (current.length >= 6) {
|
|
polylines.push(new Float32Array(current));
|
|
}
|
|
current = [];
|
|
lastPoint = null;
|
|
}
|
|
if (!lastPoint) {
|
|
pushPoint(current, startPoint);
|
|
}
|
|
pushPoint(current, endPoint);
|
|
lastPoint = endPoint;
|
|
}
|
|
if (current.length >= 6) {
|
|
polylines.push(new Float32Array(current));
|
|
}
|
|
return polylines;
|
|
}
|
|
|
|
function buildTopologyPolylinesForEdgeId(proxy, edgeIds, rowIndex, segmentCount) {
|
|
const polylines = [];
|
|
let runStart = -1;
|
|
let runCount = 0;
|
|
const flushRun = () => {
|
|
if (runStart >= 0 && runCount > 0) {
|
|
polylines.push(...buildTopologyPolylinesForRun(proxy, runStart, runCount));
|
|
}
|
|
runStart = -1;
|
|
runCount = 0;
|
|
};
|
|
for (let segmentIndex = 0; segmentIndex < segmentCount; segmentIndex += 1) {
|
|
if (Number(edgeIds[segmentIndex]) === rowIndex) {
|
|
if (runStart < 0) {
|
|
runStart = segmentIndex;
|
|
}
|
|
runCount += 1;
|
|
} else {
|
|
flushRun();
|
|
}
|
|
}
|
|
flushRun();
|
|
return polylines;
|
|
}
|
|
|
|
function polylinePoint(positions, vertexIndex) {
|
|
const offset = vertexIndex * 3;
|
|
return [
|
|
positions[offset],
|
|
positions[offset + 1],
|
|
positions[offset + 2]
|
|
];
|
|
}
|
|
|
|
function polylineStart(positions) {
|
|
return polylinePoint(positions, 0);
|
|
}
|
|
|
|
function polylineEnd(positions) {
|
|
return polylinePoint(positions, Math.floor(positions.length / 3) - 1);
|
|
}
|
|
|
|
function reversePolyline(positions) {
|
|
const reversed = [];
|
|
for (let offset = positions.length - 3; offset >= 0; offset -= 3) {
|
|
reversed.push(positions[offset], positions[offset + 1], positions[offset + 2]);
|
|
}
|
|
return reversed;
|
|
}
|
|
|
|
function appendPolyline(target, source, { skipFirst = true, tolerance = 1e-7 } = {}) {
|
|
const startOffset = skipFirst ? 3 : 0;
|
|
for (let offset = startOffset; offset + 2 < source.length; offset += 3) {
|
|
pushPoint(target, [source[offset], source[offset + 1], source[offset + 2]], tolerance);
|
|
}
|
|
}
|
|
|
|
function closePolylineIfNeeded(positions, tolerance) {
|
|
if (positions.length < 12) {
|
|
return positions;
|
|
}
|
|
const start = polylineStart(positions);
|
|
const end = polylineEnd(positions);
|
|
if (pointDistance(start, end) > tolerance) {
|
|
return positions;
|
|
}
|
|
const output = Array.from(positions);
|
|
output[output.length - 3] = output[0];
|
|
output[output.length - 2] = output[1];
|
|
output[output.length - 1] = output[2];
|
|
return new Float32Array(output);
|
|
}
|
|
|
|
export function stitchTopologyDisplayEdgePolylines(polylines) {
|
|
if (!Array.isArray(polylines) || polylines.length <= 1) {
|
|
return polylines || [];
|
|
}
|
|
const tolerance = stitchToleranceForPolylines(polylines);
|
|
const chains = polylines
|
|
.filter((positions) => positions?.length >= 6)
|
|
.map((positions) => Array.from(positions));
|
|
for (let index = 0; index < chains.length; index += 1) {
|
|
let changed = true;
|
|
while (changed) {
|
|
changed = false;
|
|
for (let candidateIndex = index + 1; candidateIndex < chains.length; candidateIndex += 1) {
|
|
const current = chains[index];
|
|
const candidate = chains[candidateIndex];
|
|
const currentStart = polylineStart(current);
|
|
const currentEnd = polylineEnd(current);
|
|
const candidateStart = polylineStart(candidate);
|
|
const candidateEnd = polylineEnd(candidate);
|
|
let merged = null;
|
|
if (pointDistance(currentEnd, candidateStart) <= tolerance) {
|
|
merged = [...current];
|
|
appendPolyline(merged, candidate, { tolerance });
|
|
} else if (pointDistance(currentEnd, candidateEnd) <= tolerance) {
|
|
merged = [...current];
|
|
appendPolyline(merged, reversePolyline(candidate), { tolerance });
|
|
} else if (pointDistance(currentStart, candidateEnd) <= tolerance) {
|
|
merged = [...candidate];
|
|
appendPolyline(merged, current, { tolerance });
|
|
} else if (pointDistance(currentStart, candidateStart) <= tolerance) {
|
|
merged = reversePolyline(candidate);
|
|
appendPolyline(merged, current, { tolerance });
|
|
}
|
|
if (merged) {
|
|
chains[index] = merged;
|
|
chains.splice(candidateIndex, 1);
|
|
changed = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return chains
|
|
.filter((positions) => positions.length >= 6)
|
|
.map((positions) => closePolylineIfNeeded(new Float32Array(positions), tolerance));
|
|
}
|
|
|
|
export function hasTopologyDisplayEdgeProxy(selectorRuntime) {
|
|
const proxy = selectorRuntime?.proxy || {};
|
|
return (
|
|
isFloat32Array(proxy.edgePositions) &&
|
|
isUint32Array(proxy.edgeIndices) &&
|
|
proxy.edgePositions.length >= 6 &&
|
|
proxy.edgeIndices.length >= 2
|
|
);
|
|
}
|
|
|
|
export function hasTopologyDisplayEdgeClassification(selectorRuntime) {
|
|
return hasTopologyDisplayEdgeProxy(selectorRuntime);
|
|
}
|
|
|
|
export function shouldUseTopologyDisplayEdges(selectorRuntime) {
|
|
return !selectorRuntime?.surfaceEdgeRendering &&
|
|
Number(selectorRuntime?.schemaVersion || 0) < 3 &&
|
|
hasTopologyDisplayEdgeProxy(selectorRuntime);
|
|
}
|
|
|
|
export function buildTopologyDisplayEdgePositions(selectorRuntime, {
|
|
includePartIds = [],
|
|
excludePartIds = [],
|
|
visibilityClasses = null
|
|
} = {}) {
|
|
if (!hasTopologyDisplayEdgeProxy(selectorRuntime)) {
|
|
return null;
|
|
}
|
|
const proxy = selectorRuntime.proxy;
|
|
const segmentCount = Math.floor(proxy.edgeIndices.length / 2);
|
|
if (segmentCount <= 0) {
|
|
return null;
|
|
}
|
|
const includePartIdSet = normalizePartIdSet(includePartIds);
|
|
const excludePartIdSet = normalizePartIdSet(excludePartIds);
|
|
const hasPartFilter = includePartIdSet.size > 0 || excludePartIdSet.size > 0;
|
|
const visibilityClassSet = normalizeVisibilityClassSet(visibilityClasses);
|
|
const edgeRows = Array.isArray(selectorRuntime.edges) ? selectorRuntime.edges : [];
|
|
const hasVisibilityClassFilter = Boolean(visibilityClassSet?.size && edgeRows.length);
|
|
const hasEdgeRowFilter = hasPartFilter || hasVisibilityClassFilter;
|
|
const edgeIds = isUint32Array(proxy.edgeIds) ? proxy.edgeIds : null;
|
|
const linePositions = new Float32Array(segmentCount * 6);
|
|
let writeOffset = 0;
|
|
|
|
const writeSegment = (segmentIndex) => {
|
|
const startVertexIndex = Number(proxy.edgeIndices[segmentIndex * 2]);
|
|
const endVertexIndex = Number(proxy.edgeIndices[(segmentIndex * 2) + 1]);
|
|
const startPoint = pointFromProxy(proxy, startVertexIndex);
|
|
const endPoint = pointFromProxy(proxy, endVertexIndex);
|
|
if (!startPoint || !endPoint || pointDistance(startPoint, endPoint) <= 1e-10) {
|
|
return;
|
|
}
|
|
linePositions[writeOffset] = startPoint[0];
|
|
linePositions[writeOffset + 1] = startPoint[1];
|
|
linePositions[writeOffset + 2] = startPoint[2];
|
|
linePositions[writeOffset + 3] = endPoint[0];
|
|
linePositions[writeOffset + 4] = endPoint[1];
|
|
linePositions[writeOffset + 5] = endPoint[2];
|
|
writeOffset += 6;
|
|
};
|
|
|
|
if (hasEdgeRowFilter) {
|
|
if (!edgeRows.length) {
|
|
return null;
|
|
}
|
|
for (let rowIndex = 0; rowIndex < edgeRows.length; rowIndex += 1) {
|
|
const row = edgeRows[rowIndex];
|
|
if (!edgeRowPassesFilters(row, includePartIdSet, excludePartIdSet, visibilityClassSet)) {
|
|
continue;
|
|
}
|
|
const start = Number(row?.segmentStart || 0);
|
|
const count = Number(row?.segmentCount || 0);
|
|
if (Number.isInteger(start) && Number.isInteger(count) && start >= 0 && count > 0 && start + count <= segmentCount) {
|
|
for (let segmentIndex = start; segmentIndex < start + count; segmentIndex += 1) {
|
|
writeSegment(segmentIndex);
|
|
}
|
|
continue;
|
|
}
|
|
if (edgeIds) {
|
|
for (let segmentIndex = 0; segmentIndex < segmentCount; segmentIndex += 1) {
|
|
if (Number(edgeIds[segmentIndex]) === rowIndex) {
|
|
writeSegment(segmentIndex);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
for (let segmentIndex = 0; segmentIndex < segmentCount; segmentIndex += 1) {
|
|
writeSegment(segmentIndex);
|
|
}
|
|
}
|
|
|
|
if (writeOffset <= 0) {
|
|
return null;
|
|
}
|
|
return writeOffset === linePositions.length ? linePositions : linePositions.subarray(0, writeOffset);
|
|
}
|
|
|
|
export function buildTopologyDisplayEdgePolylines(selectorRuntime, {
|
|
includePartIds = [],
|
|
excludePartIds = [],
|
|
visibilityClasses = null
|
|
} = {}) {
|
|
if (!hasTopologyDisplayEdgeProxy(selectorRuntime)) {
|
|
return [];
|
|
}
|
|
const proxy = selectorRuntime.proxy;
|
|
const segmentCount = Math.floor(proxy.edgeIndices.length / 2);
|
|
if (segmentCount <= 0) {
|
|
return [];
|
|
}
|
|
|
|
const edgeRows = Array.isArray(selectorRuntime.edges) ? selectorRuntime.edges : [];
|
|
const edgeIds = isUint32Array(proxy.edgeIds) ? proxy.edgeIds : null;
|
|
const includePartIdSet = normalizePartIdSet(includePartIds);
|
|
const excludePartIdSet = normalizePartIdSet(excludePartIds);
|
|
const hasPartFilter = includePartIdSet.size > 0 || excludePartIdSet.size > 0;
|
|
const visibilityClassSet = normalizeVisibilityClassSet(visibilityClasses);
|
|
const hasVisibilityClassFilter = Boolean(visibilityClassSet?.size && edgeRows.length);
|
|
const hasEdgeRowFilter = hasPartFilter || hasVisibilityClassFilter;
|
|
const polylines = [];
|
|
|
|
if (edgeRows.length) {
|
|
for (let rowIndex = 0; rowIndex < edgeRows.length; rowIndex += 1) {
|
|
const row = edgeRows[rowIndex];
|
|
if (!row) {
|
|
continue;
|
|
}
|
|
if (!edgeRowPassesFilters(row, includePartIdSet, excludePartIdSet, visibilityClassSet)) {
|
|
continue;
|
|
}
|
|
const start = Number(row.segmentStart || 0);
|
|
const count = Number(row.segmentCount || 0);
|
|
if (Number.isInteger(start) && Number.isInteger(count) && start >= 0 && count > 0 && start + count <= segmentCount) {
|
|
polylines.push(...buildTopologyPolylinesForRun(proxy, start, count));
|
|
} else if (edgeIds) {
|
|
polylines.push(...buildTopologyPolylinesForEdgeId(proxy, edgeIds, rowIndex, segmentCount));
|
|
}
|
|
}
|
|
return stitchTopologyDisplayEdgePolylines(polylines);
|
|
}
|
|
|
|
if (hasEdgeRowFilter) {
|
|
return [];
|
|
}
|
|
|
|
for (let segmentIndex = 0; segmentIndex < segmentCount; segmentIndex += 1) {
|
|
const rowIndex = edgeIds ? Number(edgeIds[segmentIndex]) : -1;
|
|
void rowIndex;
|
|
|
|
polylines.push(...buildTopologyPolylinesForRun(proxy, segmentIndex, 1));
|
|
}
|
|
|
|
return stitchTopologyDisplayEdgePolylines(polylines);
|
|
}
|