126 lines
3.9 KiB
TypeScript
126 lines
3.9 KiB
TypeScript
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||
import { tmpdir } from "node:os";
|
||
import { join } from "node:path";
|
||
import { afterEach, describe, expect, it } from "vitest";
|
||
import {
|
||
inspectPointcloudMetadata,
|
||
MAX_PLY_HEADER_BYTES,
|
||
parsePlyHeader,
|
||
PointcloudMetadataParseError,
|
||
} from "./pointcloudMetadata";
|
||
|
||
const temporaryDirectories: string[] = [];
|
||
|
||
afterEach(() => {
|
||
while (temporaryDirectories.length > 0) {
|
||
rmSync(temporaryDirectories.pop()!, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
function header(lines: string[], newline = "\n") {
|
||
return Buffer.from([...lines, "end_header", ""].join(newline), "ascii");
|
||
}
|
||
|
||
describe("parsePlyHeader", () => {
|
||
it("解析 ASCII PLY,并以 element vertex 作为精确点数", () => {
|
||
const result = parsePlyHeader(header([
|
||
"ply",
|
||
"format ascii 1.0",
|
||
"element vertex 1000000000",
|
||
"property float x",
|
||
"property float y",
|
||
"property double z",
|
||
"element face 999",
|
||
"property list uchar int vertex_indices",
|
||
]));
|
||
|
||
expect(result).toEqual({
|
||
pointCount: 1_000_000_000,
|
||
pointDataFormat: "ascii",
|
||
bytesPerPoint: 16,
|
||
pointAttributes: [
|
||
{ name: "x", type: "float32", bytes: 4 },
|
||
{ name: "y", type: "float32", bytes: 4 },
|
||
{ name: "z", type: "float64", bytes: 8 },
|
||
],
|
||
});
|
||
});
|
||
|
||
it.each([
|
||
["binary_little_endian", "binary_little_endian"],
|
||
["binary_big_endian", "binary_big_endian"],
|
||
] as const)("解析 %s PLY", (format, expected) => {
|
||
const result = parsePlyHeader(header([
|
||
"ply",
|
||
`format ${format} 1.0`,
|
||
"element vertex 42",
|
||
"property int16 x",
|
||
"property ushort intensity",
|
||
"property uchar red",
|
||
]));
|
||
expect(result.pointDataFormat).toBe(expected);
|
||
expect(result.bytesPerPoint).toBe(5);
|
||
});
|
||
|
||
it("支持 CRLF 文件头和类型别名", () => {
|
||
const result = parsePlyHeader(header([
|
||
"ply",
|
||
"format ascii 1.0",
|
||
"comment generated",
|
||
"element vertex 3",
|
||
"property char classification",
|
||
"property uint confidence",
|
||
"property float32 temperature",
|
||
], "\r\n"));
|
||
expect(result.pointAttributes).toEqual([
|
||
{ name: "classification", type: "int8", bytes: 1 },
|
||
{ name: "confidence", type: "uint32", bytes: 4 },
|
||
{ name: "temperature", type: "float32", bytes: 4 },
|
||
]);
|
||
});
|
||
|
||
it("拒绝无效文件头", () => {
|
||
expect(() => parsePlyHeader(Buffer.from("not-ply\n"))).toThrow(PointcloudMetadataParseError);
|
||
});
|
||
|
||
it("拒绝未知的顶点属性类型", () => {
|
||
expect(() => parsePlyHeader(header([
|
||
"ply",
|
||
"format ascii 1.0",
|
||
"element vertex 1",
|
||
"property half x",
|
||
]))).toThrow("未知的顶点属性类型");
|
||
});
|
||
|
||
it("拒绝列表型顶点属性,但忽略其他 element 的列表属性", () => {
|
||
expect(() => parsePlyHeader(header([
|
||
"ply",
|
||
"format ascii 1.0",
|
||
"element vertex 1",
|
||
"property list uchar float samples",
|
||
]))).toThrow("列表型顶点属性");
|
||
});
|
||
|
||
it("非 PLY 格式直接标记为 unsupported,不读取文件", () => {
|
||
expect(inspectPointcloudMetadata("/path/does/not/exist.txt", "cloud.txt")).toEqual({
|
||
metadataStatus: "unsupported",
|
||
pointCount: null,
|
||
pointAttributes: null,
|
||
pointDataFormat: null,
|
||
bytesPerPoint: null,
|
||
metadataError: null,
|
||
});
|
||
});
|
||
|
||
it("只读取 16KB 文件头,超长文件头标记为解析失败", () => {
|
||
const directory = mkdtempSync(join(tmpdir(), "pointcloud-metadata-"));
|
||
temporaryDirectories.push(directory);
|
||
const filePath = join(directory, "oversized.ply");
|
||
writeFileSync(filePath, `ply\nformat ascii 1.0\ncomment ${"x".repeat(MAX_PLY_HEADER_BYTES)}\nend_header\n`);
|
||
|
||
const result = inspectPointcloudMetadata(filePath, "oversized.ply");
|
||
expect(result.metadataStatus).toBe("error");
|
||
expect(result.metadataError).toContain("16KB");
|
||
});
|
||
});
|