feat: initial commit - massive-cloud point cloud processing system

This commit is contained in:
2026-05-27 09:51:42 +00:00
commit 16811100d1
391 changed files with 78338 additions and 0 deletions

305
server/cadHeatmapCompute.ts Normal file
View File

@@ -0,0 +1,305 @@
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);
}