Files
massive-cloud/server/cadHeatmapCompute.ts

306 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { BufferGeometry, Group, Mesh, Vector3 } from 'three';
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
import { mergeGeometries, mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
import {
buildPCGrid3D,
CAD_HEATMAP_SHELL_OFFSET_FRAC,
displayDistanceRange,
displayErrorRangeSmoothHeatmap,
distanceToRgb,
errorToRgb,
medianNearestNeighbor3D,
nearestPointInGrid,
normalizePointsLikeViewer,
type HeatmapPoint,
} from '../shared/cadHeatmapMath.js';
import type { SampledPoint } from './pointcloudSample.js';
/**
* 服务端平滑热力:默认「先求快、可接受稍粗网格」(与前端实时预览的 shared 常量可不同)。
* - CAD_HEATMAP_MAX_VERTICES:焊接后顶点上限(默认 60000;要更细可设 120000)。
* - CAD_HEATMAP_MEDIAN_NN_SAMPLES:尺度估计采样(默认 4096;要更稳可设 8192)。
*/
function parseHeatmapInt(envVal: string | undefined, fallback: number, min: number, max: number): number {
if (envVal == null || envVal === '') return fallback;
const n = Number(envVal);
if (!Number.isFinite(n)) return fallback;
return Math.min(max, Math.max(min, Math.floor(n)));
}
/** 默认偏快:约半顶点上限,merge 更早停 */
const SERVER_FAST_DEFAULT_MAX_VERTICES = 60_000;
const SERVER_FAST_DEFAULT_MEDIAN_NN = 4_096;
const SERVER_MAX_VERTICES = parseHeatmapInt(
process.env.CAD_HEATMAP_MAX_VERTICES,
SERVER_FAST_DEFAULT_MAX_VERTICES,
8_000,
500_000,
);
const SERVER_MEDIAN_NN_SAMPLES = parseHeatmapInt(
process.env.CAD_HEATMAP_MEDIAN_NN_SAMPLES,
SERVER_FAST_DEFAULT_MEDIAN_NN,
512,
32_768,
);
function normalizeCadGeometryLikeViewer(geo: BufferGeometry): void {
geo.computeBoundingBox();
geo.center();
const box = geo.boundingBox;
if (box) {
const size = new Vector3();
box.getSize(size);
const maxDim = Math.max(size.x, size.y, size.z);
if (maxDim > 0) {
const s = 10 / maxDim;
geo.scale(s, s, s);
}
}
}
function buildHeatmapPoints(sampledPoints: SampledPoint[]): HeatmapPoint[] {
const pts: HeatmapPoint[] = sampledPoints.map((p) => ({
x: p.x,
y: p.y,
z: p.z,
error: p.error,
}));
normalizePointsLikeViewer(pts);
return pts;
}
function parseObjBufferToGeometry(buffer: Buffer): BufferGeometry | null {
const text = buffer.toString('utf8');
if (!text.trim()) return null;
const loader = new OBJLoader();
const group = loader.parse(text) as Group;
const geometries: BufferGeometry[] = [];
group.traverse((child) => {
if (child instanceof Mesh && child.geometry) {
geometries.push(child.geometry);
}
});
if (geometries.length === 0) return null;
if (geometries.length === 1) return geometries[0].clone();
try {
const merged = mergeGeometries(geometries, false);
if (merged) return merged;
} catch {
/* fall through */
}
return geometries[0].clone();
}
/**
* 对已解析的 BufferGeometry(尚未 normalizeCad)计算顶点色。
*/
function computeCadShellHeatmapCore(
geo: BufferGeometry,
pts: HeatmapPoint[],
): { colors: Float32Array; dispMin: number; dispMax: number; vertexCount: number } | null {
normalizeCadGeometryLikeViewer(geo);
try {
mergeVertices(geo);
} catch {
/* ignore */
}
geo.computeVertexNormals();
let weldTol = 2e-4;
while (
geo.attributes.position &&
geo.attributes.position.count > SERVER_MAX_VERTICES &&
weldTol < 0.12
) {
mergeVertices(geo, weldTol);
weldTol *= 1.85;
geo.computeVertexNormals();
}
const posAttr = geo.attributes.position;
const norAttr = geo.attributes.normal;
if (!posAttr || posAttr.count === 0) return null;
const nV = posAttr.count;
const pos = posAttr.array as Float32Array;
const nor = norAttr?.array as Float32Array | undefined;
const grid = buildPCGrid3D(pts);
const nnMed = medianNearestNeighbor3D(pts, grid, SERVER_MEDIAN_NN_SAMPLES);
const { minError: dispMin, maxError: dispMax } = displayErrorRangeSmoothHeatmap(pts);
geo.computeBoundingBox();
const bb = geo.boundingBox;
const diag = bb ? Math.hypot(bb.max.x - bb.min.x, bb.max.y - bb.min.y, bb.max.z - bb.min.z) : 1;
const maxBind = Math.max(nnMed * 14, diag * 0.045);
const shellOffset = Math.max(diag * CAD_HEATMAP_SHELL_OFFSET_FRAC, nnMed * 0.35);
const colors = new Float32Array(nV * 3);
const noData = [0.32, 0.34, 0.38] as const;
for (let i = 0; i < nV; i++) {
const i3 = i * 3;
const x = pos[i3];
const y = pos[i3 + 1];
const z = pos[i3 + 2];
const hit = nearestPointInGrid(grid, x, y, z);
if (!hit || hit.dist > maxBind) {
colors[i3] = noData[0];
colors[i3 + 1] = noData[1];
colors[i3 + 2] = noData[2];
} else {
const err = pts[hit.idx].error ?? 0;
const [r, g, b] = errorToRgb(err, dispMin, dispMax);
colors[i3] = r;
colors[i3 + 1] = g;
colors[i3 + 2] = b;
}
if (nor && nor.length >= nV * 3) {
pos[i3] = x + nor[i3] * shellOffset;
pos[i3 + 1] = y + nor[i3 + 1] * shellOffset;
pos[i3 + 2] = z + nor[i3 + 2] * shellOffset;
}
}
posAttr.needsUpdate = true;
return {
colors,
dispMin,
dispMax,
vertexCount: nV,
};
}
/**
* 在服务端对 STL 与(已采样的)测点做与前端一致的 CAD 壳热力,返回每顶点 RGB。
*/
export function computeCadShellHeatmapFromStl(
stlBuffer: Buffer,
sampledPoints: SampledPoint[],
): { colors: Float32Array; dispMin: number; dispMax: number; vertexCount: number } | null {
if (sampledPoints.length < 1) return null;
const pts = buildHeatmapPoints(sampledPoints);
const u8 = new Uint8Array(stlBuffer);
const ab = u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer;
const loader = new STLLoader();
const geo = loader.parse(ab);
return computeCadShellHeatmapCore(geo, pts);
}
/** OBJ(多 mesh 会尝试合并) */
export function computeCadShellHeatmapFromObj(
objBuffer: Buffer,
sampledPoints: SampledPoint[],
): { colors: Float32Array; dispMin: number; dispMax: number; vertexCount: number } | null {
if (sampledPoints.length < 1) return null;
const pts = buildHeatmapPoints(sampledPoints);
const geo = parseObjBufferToGeometry(objBuffer);
if (!geo) return null;
return computeCadShellHeatmapCore(geo, pts);
}
// ─── 偏差热力:用顶点到最近点的距离着色 ────────────────────────────────────────
function computeDeviationHeatmapCore(
geo: BufferGeometry,
pts: HeatmapPoint[],
): { colors: Float32Array; dispMin: number; dispMax: number; vertexCount: number } | null {
normalizeCadGeometryLikeViewer(geo);
// 不做 mergeVertices —— 保持与客户端 Three.js Loader 一致的非索引顶点布局,
// 使输出的颜色数组长度 == 客户端 rawCount,触发快速路径而非主线程 merge 卡死。
geo.computeVertexNormals();
const posAttr = geo.attributes.position;
const norAttr = geo.attributes.normal;
if (!posAttr || posAttr.count === 0) return null;
const nV = posAttr.count;
const pos = posAttr.array as Float32Array;
const nor = norAttr?.array as Float32Array | undefined;
const grid = buildPCGrid3D(pts);
const nnMed = medianNearestNeighbor3D(pts, grid, SERVER_MEDIAN_NN_SAMPLES);
geo.computeBoundingBox();
const bb = geo.boundingBox;
const diag = bb ? Math.hypot(bb.max.x - bb.min.x, bb.max.y - bb.min.y, bb.max.z - bb.min.z) : 1;
const maxBind = Math.max(nnMed * 14, diag * 0.045);
const shellOffset = Math.max(diag * CAD_HEATMAP_SHELL_OFFSET_FRAC, nnMed * 0.35);
const distances: number[] = [];
const vertexDists = new Float64Array(nV);
for (let i = 0; i < nV; i++) {
const i3 = i * 3;
const hit = nearestPointInGrid(grid, pos[i3], pos[i3 + 1], pos[i3 + 2]);
if (hit && hit.dist <= maxBind) {
vertexDists[i] = hit.dist;
distances.push(hit.dist);
} else {
vertexDists[i] = -1;
}
}
const { minDist: dispMin, maxDist: dispMax } = displayDistanceRange(distances);
const colors = new Float32Array(nV * 3);
const noData = [0.32, 0.34, 0.38] as const;
for (let i = 0; i < nV; i++) {
const i3 = i * 3;
const d = vertexDists[i];
if (d < 0) {
colors[i3] = noData[0];
colors[i3 + 1] = noData[1];
colors[i3 + 2] = noData[2];
} else {
const [r, g, b] = distanceToRgb(d, dispMin, dispMax);
colors[i3] = r;
colors[i3 + 1] = g;
colors[i3 + 2] = b;
}
if (nor && nor.length >= nV * 3) {
pos[i3] = pos[i3] + nor[i3] * shellOffset;
pos[i3 + 1] = pos[i3 + 1] + nor[i3 + 1] * shellOffset;
pos[i3 + 2] = pos[i3 + 2] + nor[i3 + 2] * shellOffset;
}
}
posAttr.needsUpdate = true;
return { colors, dispMin, dispMax, vertexCount: nV };
}
export function computeDeviationHeatmapFromStl(
stlBuffer: Buffer,
sampledPoints: SampledPoint[],
): { colors: Float32Array; dispMin: number; dispMax: number; vertexCount: number } | null {
if (sampledPoints.length < 1) return null;
const pts = buildHeatmapPoints(sampledPoints);
const u8 = new Uint8Array(stlBuffer);
const ab = u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer;
const loader = new STLLoader();
const geo = loader.parse(ab);
return computeDeviationHeatmapCore(geo, pts);
}
export function computeDeviationHeatmapFromObj(
objBuffer: Buffer,
sampledPoints: SampledPoint[],
): { colors: Float32Array; dispMin: number; dispMax: number; vertexCount: number } | null {
if (sampledPoints.length < 1) return null;
const pts = buildHeatmapPoints(sampledPoints);
const geo = parseObjBufferToGeometry(objBuffer);
if (!geo) return null;
return computeDeviationHeatmapCore(geo, pts);
}