2026-05-20-02-55-11 修复头部STL实体导出

This commit is contained in:
2026-05-20 03:02:30 +08:00
parent 68fb0cb564
commit b9c0f17313
5 changed files with 332 additions and 13 deletions

View File

@@ -718,6 +718,58 @@ function intersectExportTriangleWithPlane(a: Point3DRecord, b: Point3DRecord, c:
return maxDistance > 1e-8 ? segment : null;
}
function readBinaryStlTriangleCount(buffer: Buffer, fileName: string) {
if (buffer.length < 84) {
throw new Error(`STL 文件内容为空或不完整:${fileName}`);
}
const triangleCount = buffer.readUInt32LE(80);
const expectedLength = 84 + triangleCount * 50;
if (triangleCount <= 0 || expectedLength > buffer.length + 1024) {
throw new Error(`当前仅支持二进制 STL${fileName}`);
}
return triangleCount;
}
function forEachBinaryStlTriangle(
filePath: string,
fileName: string,
callback: (
ax: number,
ay: number,
az: number,
bx: number,
by: number,
bz: number,
cx: number,
cy: number,
cz: number,
) => void,
) {
const buffer = fs.readFileSync(filePath);
const triangleCount = readBinaryStlTriangleCount(buffer, fileName);
for (let triangleIndex = 0; triangleIndex < triangleCount; triangleIndex += 1) {
const offset = 84 + triangleIndex * 50;
if (offset + 50 > buffer.length) {
break;
}
callback(
buffer.readFloatLE(offset + 12),
buffer.readFloatLE(offset + 16),
buffer.readFloatLE(offset + 20),
buffer.readFloatLE(offset + 24),
buffer.readFloatLE(offset + 28),
buffer.readFloatLE(offset + 32),
buffer.readFloatLE(offset + 36),
buffer.readFloatLE(offset + 40),
buffer.readFloatLE(offset + 44),
);
}
}
function addExportSegmentToRows(rows: number[][], width: number, height: number, segment: PlaneSegmentRecord) {
const deltaY = segment.b.y - segment.a.y;
if (Math.abs(deltaY) < 0.01) {
@@ -742,8 +794,61 @@ function addExportSegmentToRows(rows: number[][], width: number, height: number,
}
}
function fillExportInternalHoles(mask: Uint8Array, width: number, height: number) {
const outside = new Uint8Array(width * height);
const stack: number[] = [];
const pushIfEmpty = (x: number, y: number) => {
if (x < 0 || x >= width || y < 0 || y >= height) {
return;
}
const index = y * width + x;
if (outside[index] || mask[index]) {
return;
}
outside[index] = 1;
stack.push(index);
};
for (let x = 0; x < width; x += 1) {
pushIfEmpty(x, 0);
pushIfEmpty(x, height - 1);
}
for (let y = 0; y < height; y += 1) {
pushIfEmpty(0, y);
pushIfEmpty(width - 1, y);
}
while (stack.length) {
const index = stack.pop();
if (index === undefined) {
continue;
}
const x = index % width;
const y = Math.floor(index / width);
pushIfEmpty(x + 1, y);
pushIfEmpty(x - 1, y);
pushIfEmpty(x, y + 1);
pushIfEmpty(x, y - 1);
}
let patchedPixels = 0;
for (let index = 0; index < mask.length; index += 1) {
if (!outside[index] && !mask[index]) {
mask[index] = 1;
patchedPixels += 1;
}
}
return patchedPixels;
}
function fillExportRows(data: Buffer, width: number, height: number, slice: number, rows: number[][], label: number) {
const sliceOffset = slice * width * height;
const mask = new Uint8Array(width * height);
let filledPixels = 0;
rows.forEach((intersections, row) => {
if (intersections.length < 2) {
return;
@@ -768,10 +873,28 @@ function fillExportRows(data: Buffer, width: number, height: number, slice: numb
const startX = clampNumber(Math.ceil(rawStartX), 0, width - 1);
const endX = clampNumber(Math.floor(rawEndX), 0, width - 1);
for (let x = startX; x <= endX; x += 1) {
data[sliceOffset + row * width + x] = label;
const index = row * width + x;
if (!mask[index]) {
mask[index] = 1;
filledPixels += 1;
}
}
}
});
if (filledPixels === 0) {
return 0;
}
filledPixels += fillExportInternalHoles(mask, width, height);
const sliceOffset = slice * width * height;
for (let index = 0; index < mask.length; index += 1) {
if (mask[index]) {
data[sliceOffset + index] = label;
}
}
return filledPixels;
}
function getModuleStyle(project: ProjectRecord, fileName: string, index: number): ModuleStyleRecord {
@@ -836,25 +959,36 @@ function createSegmentationData(project: ProjectRecord, volume: DicomHuVolume, p
return rows;
};
for (let vertexIndex = 0; vertexIndex + 8 < payload.vertices.length; vertexIndex += 9) {
const filePath = path.join(modelDir, fileName);
forEachBinaryStlTriangle(filePath, fileName, (
ax,
ay,
az,
bx,
by,
bz,
cx,
cy,
cz,
) => {
const a = transformPointForExportPose(
payload.vertices[vertexIndex],
payload.vertices[vertexIndex + 1],
payload.vertices[vertexIndex + 2],
ax,
ay,
az,
metrics,
pose,
);
const b = transformPointForExportPose(
payload.vertices[vertexIndex + 3],
payload.vertices[vertexIndex + 4],
payload.vertices[vertexIndex + 5],
bx,
by,
bz,
metrics,
pose,
);
const c = transformPointForExportPose(
payload.vertices[vertexIndex + 6],
payload.vertices[vertexIndex + 7],
payload.vertices[vertexIndex + 8],
cx,
cy,
cz,
metrics,
pose,
);
@@ -872,7 +1006,7 @@ function createSegmentationData(project: ProjectRecord, volume: DicomHuVolume, p
b: mapPoint(segment.b),
});
}
}
});
rowsBySlice.forEach((rows, slice) => {
fillExportRows(data, volume.width, volume.height, slice, rows, label);