feat: add local access control and managed point clouds

This commit is contained in:
2026-07-22 11:05:48 +08:00
parent 683e4d6c07
commit 8555aa2f2e
46 changed files with 1982 additions and 884 deletions

View File

@@ -0,0 +1,153 @@
import * as fs from "node:fs";
import * as path from "node:path";
import type {
PointAttribute,
PointcloudDataFormat,
PointcloudMetadataValues,
} from "../shared/pointcloudMetadata";
export const MAX_PLY_HEADER_BYTES = 16 * 1024;
type ScalarType = { type: string; bytes: number };
const PLY_SCALAR_TYPES: Record<string, ScalarType> = {
char: { type: "int8", bytes: 1 },
int8: { type: "int8", bytes: 1 },
uchar: { type: "uint8", bytes: 1 },
uint8: { type: "uint8", bytes: 1 },
short: { type: "int16", bytes: 2 },
int16: { type: "int16", bytes: 2 },
ushort: { type: "uint16", bytes: 2 },
uint16: { type: "uint16", bytes: 2 },
int: { type: "int32", bytes: 4 },
int32: { type: "int32", bytes: 4 },
uint: { type: "uint32", bytes: 4 },
uint32: { type: "uint32", bytes: 4 },
float: { type: "float32", bytes: 4 },
float32: { type: "float32", bytes: 4 },
double: { type: "float64", bytes: 8 },
float64: { type: "float64", bytes: 8 },
};
export class PointcloudMetadataParseError extends Error {}
export type ParsedPlyMetadata = {
pointCount: number;
pointAttributes: PointAttribute[];
pointDataFormat: PointcloudDataFormat;
bytesPerPoint: number;
};
/** 解析内存中的 PLY 文件头。调用方应保证输入不超过 MAX_PLY_HEADER_BYTES。 */
export function parsePlyHeader(buffer: Buffer): ParsedPlyMetadata {
const text = buffer.toString("latin1");
const endMatch = /(?:^|\r?\n)end_header(?:\r?\n|$)/.exec(text);
if (!endMatch) {
throw new PointcloudMetadataParseError("未在 16KB 内找到 PLY 文件头结束标记");
}
const headerEnd = endMatch.index + endMatch[0].length;
const lines = text.slice(0, headerEnd).split(/\r?\n/).map((line) => line.trim());
if (lines[0] !== "ply") {
throw new PointcloudMetadataParseError("不是有效的 PLY 文件");
}
let pointDataFormat: PointcloudDataFormat | null = null;
let pointCount: number | null = null;
let currentElement: string | null = null;
const pointAttributes: PointAttribute[] = [];
for (const line of lines.slice(1)) {
if (!line || line === "end_header" || line.startsWith("comment ") || line.startsWith("obj_info ")) {
continue;
}
const tokens = line.split(/\s+/);
if (tokens[0] === "format") {
const value = tokens[1];
if (value !== "ascii" && value !== "binary_little_endian" && value !== "binary_big_endian") {
throw new PointcloudMetadataParseError("不支持的 PLY 编码");
}
pointDataFormat = value;
continue;
}
if (tokens[0] === "element") {
currentElement = tokens[1] ?? null;
if (currentElement === "vertex") {
const count = Number(tokens[2]);
if (!Number.isSafeInteger(count) || count < 0) {
throw new PointcloudMetadataParseError("PLY 顶点数无效");
}
pointCount = count;
}
continue;
}
if (tokens[0] === "property" && currentElement === "vertex") {
if (tokens[1] === "list") {
throw new PointcloudMetadataParseError("暂不支持列表型顶点属性");
}
const scalar = PLY_SCALAR_TYPES[tokens[1]];
const name = tokens[2];
if (!scalar || !name) {
throw new PointcloudMetadataParseError("PLY 包含未知的顶点属性类型");
}
pointAttributes.push({ name, type: scalar.type, bytes: scalar.bytes });
}
}
if (!pointDataFormat) {
throw new PointcloudMetadataParseError("PLY 文件头缺少编码信息");
}
if (pointCount === null) {
throw new PointcloudMetadataParseError("PLY 文件头缺少顶点数量");
}
if (pointAttributes.length === 0) {
throw new PointcloudMetadataParseError("PLY 文件头缺少顶点属性");
}
return {
pointCount,
pointAttributes,
pointDataFormat,
bytesPerPoint: pointAttributes.reduce((sum, attribute) => sum + attribute.bytes, 0),
};
}
export function inspectPointcloudMetadata(filePath: string, displayName = filePath): PointcloudMetadataValues {
const extension = path.extname(displayName || filePath).toLowerCase();
if (extension !== ".ply") {
return {
metadataStatus: "unsupported",
pointCount: null,
pointAttributes: null,
pointDataFormat: null,
bytesPerPoint: null,
metadataError: null,
};
}
try {
const fd = fs.openSync(filePath, "r");
try {
const buffer = Buffer.allocUnsafe(MAX_PLY_HEADER_BYTES);
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
const parsed = parsePlyHeader(buffer.subarray(0, bytesRead));
return { metadataStatus: "ready", ...parsed, metadataError: null };
} finally {
fs.closeSync(fd);
}
} catch (error) {
return {
metadataStatus: "error",
pointCount: null,
pointAttributes: null,
pointDataFormat: null,
bytesPerPoint: null,
metadataError: error instanceof PointcloudMetadataParseError
? error.message
: "PLY 文件头解析失败",
};
}
}