feat: secure analysis PLY sampling

This commit is contained in:
2026-08-13 18:16:57 +08:00
parent 29a69793db
commit 9f4482bea1
5 changed files with 350 additions and 61 deletions

View File

@@ -3,6 +3,14 @@ import * as path from 'path';
export type SampledPoint = { x: number; y: number; z: number; error?: number; r?: number; g?: number; b?: number };
export type SampledPointcloudBatch = {
buffer: Buffer;
pointCount: number;
hasColor: boolean;
hasError: boolean;
fileStats: Array<{ name: string; sizeGB: number; sampled: number }>;
};
/** 与 fileRoutes 批量采样一致:随机 seek 文本点云行 */
export async function randomSeekSample(
filePath: string,
@@ -356,3 +364,70 @@ export async function samplePointcloudFile(
// .txt / .xyz / .pts / etc. — existing text line sampler
return randomSeekSample(filePath, fileSize, k);
}
/**
* 对一组已校验的点云文件按文件大小分配总采样配额,并编码为浏览器消费的二进制协议。
* 每点固定包含 x/y/z/error;任一文件带颜色时,所有点再包含归一化后的 r/g/b。
*/
export async function samplePointcloudFiles(
filePaths: string[],
requestedTotalPoints: number,
maxTotalPoints = 2_000_000,
): Promise<SampledPointcloudBatch> {
const totalPoints = Number.isFinite(requestedTotalPoints)
? Math.max(0, Math.min(Math.floor(requestedTotalPoints), maxTotalPoints))
: 0;
const fileInfos = filePaths.map((filePath) => ({
filePath,
name: path.basename(filePath),
size: fs.statSync(filePath).size,
}));
const totalBytes = fileInfos.reduce((sum, file) => sum + file.size, 0);
// floor 后把剩余名额依次补给文件,保证所有配额之和严格不超过总预算。
const quotas = fileInfos.map((file) => totalBytes > 0
? Math.floor((file.size / totalBytes) * totalPoints)
: 0);
let remainder = totalPoints - quotas.reduce((sum, quota) => sum + quota, 0);
for (let index = 0; remainder > 0 && fileInfos.length > 0; index = (index + 1) % fileInfos.length) {
quotas[index] += 1;
remainder -= 1;
}
const sampledPerFile = await Promise.all(
fileInfos.map((file, index) => samplePointcloudFile(file.filePath, file.size, quotas[index])),
);
const pointCount = sampledPerFile.reduce((sum, points) => sum + points.length, 0);
const hasError = sampledPerFile.some((points) => points.some((point) => Number.isFinite(point.error)));
const hasColor = sampledPerFile.some((points) => points.some((point) => point.r !== undefined));
const bytesPerPoint = hasColor ? 28 : 16;
const buffer = Buffer.allocUnsafe(pointCount * bytesPerPoint);
let offset = 0;
for (const points of sampledPerFile) {
for (const point of points) {
buffer.writeFloatLE(point.x, offset);
buffer.writeFloatLE(point.y, offset + 4);
buffer.writeFloatLE(point.z, offset + 8);
buffer.writeFloatLE(point.error ?? Number.NaN, offset + 12);
if (hasColor) {
buffer.writeFloatLE((point.r ?? 128) / 255, offset + 16);
buffer.writeFloatLE((point.g ?? 128) / 255, offset + 20);
buffer.writeFloatLE((point.b ?? 128) / 255, offset + 24);
}
offset += bytesPerPoint;
}
}
return {
buffer,
pointCount,
hasColor,
hasError,
fileStats: fileInfos.map((file, index) => ({
name: file.name,
sizeGB: +(file.size / 1e9).toFixed(3),
sampled: sampledPerFile[index].length,
})),
};
}