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

19
shared/_core/errors.ts Normal file
View File

@@ -0,0 +1,19 @@
/**
* Base HTTP error class with status code.
* Throw this from route handlers to send specific HTTP errors.
*/
export class HttpError extends Error {
constructor(
public statusCode: number,
message: string
) {
super(message);
this.name = "HttpError";
}
}
// Convenience constructors
export const BadRequestError = (msg: string) => new HttpError(400, msg);
export const UnauthorizedError = (msg: string) => new HttpError(401, msg);
export const ForbiddenError = (msg: string) => new HttpError(403, msg);
export const NotFoundError = (msg: string) => new HttpError(404, msg);

290
shared/cadHeatmapMath.ts Normal file
View File

@@ -0,0 +1,290 @@
/**
* 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;
}
}

5
shared/const.ts Normal file
View File

@@ -0,0 +1,5 @@
export const COOKIE_NAME = "app_session_id";
export const ONE_YEAR_MS = 1000 * 60 * 60 * 24 * 365;
export const AXIOS_TIMEOUT_MS = 30_000;
export const UNAUTHED_ERR_MSG = 'Please login (10001)';
export const NOT_ADMIN_ERR_MSG = 'You do not have required permission (10002)';

7
shared/types.ts Normal file
View File

@@ -0,0 +1,7 @@
/**
* Unified type exports
* Import shared types from this single entry point.
*/
export type * from "../drizzle/schema";
export * from "./_core/errors";