Files
massive-cloud/shared/cadHeatmapMath.ts

291 lines
8.5 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.
/**
* CAD 壳平滑热力:纯数学与色标(前后端共用,避免浏览器主线程重复实现)。
*/
export interface HeatmapPoint {
x: number;
y: number;
z: number;
error?: number;
}
/** 平滑热力专用:用采样分位数做色标,减弱离群点 */
export function displayErrorRangeSmoothHeatmap(points: HeatmapPoint[]): {
minError: number;
maxError: number;
} {
const cap = 12_000;
const errs: number[] = [];
const step = Math.max(1, Math.floor(points.length / cap));
for (let i = 0; i < points.length; i += step) {
errs.push(points[i].error ?? 0);
}
errs.sort((a, b) => a - b);
const m = errs.length;
const rawMin = errs[0] ?? 0;
const rawMax = errs[m - 1] ?? rawMin;
let lo = errs[Math.max(0, Math.floor(m * 0.04))] ?? 0;
let hi = errs[Math.min(m - 1, Math.ceil(m * 0.96) - 1)] ?? lo;
if (rawMin < 0 && lo > 0) {
lo = rawMin;
}
if (rawMax > 0 && hi < 0) {
hi = rawMax;
}
if (hi <= lo) return { minError: lo, maxError: lo + 1e-9 };
return { minError: lo, maxError: hi };
}
/** 偏差热力:距离数组的 4%–96% 分位数做色标 */
export function displayDistanceRange(distances: number[]): {
minDist: number;
maxDist: number;
} {
if (distances.length === 0) return { minDist: 0, maxDist: 1e-9 };
const sorted = distances.slice().sort((a, b) => a - b);
const m = sorted.length;
const lo = sorted[Math.max(0, Math.floor(m * 0.04))] ?? 0;
let hi = sorted[Math.min(m - 1, Math.ceil(m * 0.96) - 1)] ?? lo;
if (hi <= lo) hi = lo + 1e-9;
return { minDist: lo, maxDist: hi };
}
/** 偏差距离 → 冷暖色(蓝 → 绿 → 黄 → 红),t ∈ [0,1] */
export function distanceToRgb(
dist: number,
minDist: number,
maxDist: number,
): [number, number, number] {
const range = maxDist - minDist;
if (!(range > 0)) return [0.4, 0.74, 0.39];
const t = Math.max(0, Math.min(1, (dist - minDist) / range));
return sequentialRampRgb(t);
}
/** 顺序色标 t∈[0,1]:蓝(低) → 绿 → 黄 → 红(高) */
export function sequentialRampRgb(t: number): [number, number, number] {
t = Math.max(0, Math.min(1, t));
const stops: [number, number][] = [
[0, 0x2166ac],
[0.33, 0x4daf4a],
[0.66, 0xfee08b],
[1, 0xd73027],
];
if (t <= stops[0][0]) return hexToRgb(stops[0][1]);
for (let i = 0; i < stops.length - 1; i++) {
const [t0, h0] = stops[i];
const [t1, h1] = stops[i + 1];
if (t <= t1) {
const s = (t - t0) / (t1 - t0);
const [r0, g0, b0] = hexToRgb(h0);
const [r1, g1, b1] = hexToRgb(h1);
return [lerp(r0, r1, s), lerp(g0, g1, s), lerp(b0, b1, s)];
}
}
return hexToRgb(stops[stops.length - 1][1]);
}
function hexToRgb(h: number): [number, number, number] {
return [((h >> 16) & 255) / 255, ((h >> 8) & 255) / 255, (h & 255) / 255];
}
function lerp(a: number, b: number, t: number): number {
return a + (b - a) * t;
}
/** 发散色标 t∈[-1,1] */
export function divergingRampRgb(t: number): [number, number, number] {
t = Math.max(-1, Math.min(1, t));
const stops: [number, number][] = [
[-1, 0x2166ac],
[-0.45, 0x67a9cf],
[0, 0x4daf4a],
[0.45, 0xfee08b],
[1, 0xd73027],
];
if (t <= stops[0][0]) {
return hexToRgb(stops[0][1]);
}
for (let i = 0; i < stops.length - 1; i++) {
const [t0, h0] = stops[i];
const [t1, h1] = stops[i + 1];
if (t <= t1) {
const s = (t - t0) / (t1 - t0);
const [r0, g0, b0] = hexToRgb(h0);
const [r1, g1, b1] = hexToRgb(h1);
return [lerp(r0, r1, s), lerp(g0, g1, s), lerp(b0, b1, s)];
}
}
return hexToRgb(stops[stops.length - 1][1]);
}
export function errorToRgb(
error: number,
minError: number,
maxError: number,
): [number, number, number] {
const lo = minError;
const hi = maxError;
if (!(hi > lo)) {
return [0.4, 0.74, 0.39];
}
let mid: number;
let scale: number;
if (lo < 0 && hi > 0) {
mid = 0;
scale = Math.max(-lo, hi, 1e-12);
} else {
mid = (lo + hi) / 2;
scale = Math.max((hi - lo) / 2, 1e-12);
}
return divergingRampRgb((error - mid) / scale);
}
/** 点云 3D 均匀网格,用于最近邻查询 */
export interface PCGrid3D {
cell: number;
minX: number;
minY: number;
minZ: number;
map: Map<string, number[]>;
points: HeatmapPoint[];
}
export function buildPCGrid3D(points: HeatmapPoint[]): PCGrid3D {
let minX = Infinity;
let maxX = -Infinity;
let minY = Infinity;
let maxY = -Infinity;
let minZ = Infinity;
let maxZ = -Infinity;
for (let i = 0; i < points.length; i++) {
const p = points[i];
if (p.x < minX) minX = p.x;
if (p.x > maxX) maxX = p.x;
if (p.y < minY) minY = p.y;
if (p.y > maxY) maxY = p.y;
if (p.z < minZ) minZ = p.z;
if (p.z > maxZ) maxZ = p.z;
}
const n = points.length;
const vol = (maxX - minX) * (maxY - minY) * (maxZ - minZ) || 1e-9;
const cell = Math.max(1e-9, Math.cbrt(vol / Math.max(n, 1)) * 1.75);
const invCell = 1 / cell;
const map = new Map<string, number[]>();
for (let i = 0; i < n; i++) {
const p = points[i];
const cx = Math.floor((p.x - minX) * invCell);
const cy = Math.floor((p.y - minY) * invCell);
const cz = Math.floor((p.z - minZ) * invCell);
const k = `${cx},${cy},${cz}`;
let arr = map.get(k);
if (!arr) {
arr = [];
map.set(k, arr);
}
arr.push(i);
}
return { cell, minX, minY, minZ, map, points };
}
export function nearestPointInGrid(
g: PCGrid3D,
x: number,
y: number,
z: number,
excludeIdx: number = -1,
): { idx: number; dist: number } | null {
const { cell, minX, minY, minZ, map, points } = g;
const cx = Math.floor((x - minX) / cell);
const cy = Math.floor((y - minY) / cell);
const cz = Math.floor((z - minZ) / cell);
let bestD = Infinity;
let bestIdx = -1;
for (let R = 0; R <= 18; R++) {
for (let dx = -R; dx <= R; dx++) {
for (let dy = -R; dy <= R; dy++) {
for (let dz = -R; dz <= R; dz++) {
if (Math.max(Math.abs(dx), Math.abs(dy), Math.abs(dz)) !== R) continue;
const list = map.get(`${cx + dx},${cy + dy},${cz + dz}`);
if (!list) continue;
for (let j = 0; j < list.length; j++) {
const idx = list[j];
if (idx === excludeIdx) continue;
const p = points[idx];
const d = Math.hypot(p.x - x, p.y - y, p.z - z);
if (d < bestD) {
bestD = d;
bestIdx = idx;
}
}
}
}
}
if (bestD < Infinity && R > 0 && R * cell > bestD + cell) break;
}
if (bestIdx < 0) return null;
return { idx: bestIdx, dist: bestD };
}
export function medianNearestNeighbor3D(
points: HeatmapPoint[],
grid: PCGrid3D,
maxSamples?: number,
): number {
const n = points.length;
if (n === 0) return grid.cell;
const cap = maxSamples !== undefined ? Math.min(maxSamples, n) : n;
const dists: number[] = [];
if (cap >= n) {
for (let i = 0; i < n; i++) {
const p = points[i];
const nn = nearestPointInGrid(grid, p.x, p.y, p.z, i);
if (nn) dists.push(nn.dist);
}
} else {
for (let s = 0; s < cap; s++) {
const i = Math.min(n - 1, Math.floor(((s + 0.5) / cap) * n));
const p = points[i];
const nn = nearestPointInGrid(grid, p.x, p.y, p.z, i);
if (nn) dists.push(nn.dist);
}
}
if (dists.length === 0) return grid.cell;
dists.sort((a, b) => a - b);
return dists[Math.floor(dists.length / 2)] ?? grid.cell;
}
export const CAD_HEATMAP_SHELL_OFFSET_FRAC = 0.0022;
export const CAD_HEATMAP_MEDIAN_NN_SAMPLES = 8_192;
export const CAD_HEATMAP_MAX_VERTICES = 120_000;
/** 与前端 PointCloudViewer 加载点云后一致的归一化(居中 + 缩放到约 10 单位) */
export function normalizePointsLikeViewer<T extends { x: number; y: number; z: number }>(pts: T[]): void {
let minX = Infinity;
let minY = Infinity;
let minZ = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
let maxZ = -Infinity;
for (let i = 0; i < pts.length; i++) {
const p = pts[i];
if (p.x < minX) minX = p.x;
if (p.x > maxX) maxX = p.x;
if (p.y < minY) minY = p.y;
if (p.y > maxY) maxY = p.y;
if (p.z < minZ) minZ = p.z;
if (p.z > maxZ) maxZ = p.z;
}
const centerX = (minX + maxX) / 2;
const centerY = (minY + maxY) / 2;
const centerZ = (minZ + maxZ) / 2;
const maxDim = Math.max(maxX - minX, maxY - minY, maxZ - minZ);
const scale = maxDim > 0 ? 10 / maxDim : 1;
for (let i = 0; i < pts.length; i++) {
const p = pts[i];
p.x = (p.x - centerX) * scale;
p.y = (p.y - centerY) * scale;
p.z = (p.z - centerZ) * scale;
}
}