feat: update algorithm integrations and point cloud overlays

This commit is contained in:
2026-06-30 16:46:36 +08:00
parent ddca7e5436
commit 7e2e56a50d
64 changed files with 4356 additions and 2781 deletions

View File

@@ -415,7 +415,12 @@ const PointCloudMesh = memo(function PointCloudMesh({
positions[i3 + 1] = point.y;
positions[i3 + 2] = point.z;
if (showHeatmap) {
if (point.r !== undefined && point.g !== undefined && point.b !== undefined) {
// 点云自带颜色(如平整度热力图)直接使用,不走 errorToColor
colors[i3] = point.r;
colors[i3 + 1] = point.g;
colors[i3 + 2] = point.b;
} else if (showHeatmap) {
const color = errorToColor(point.error ?? 0, minError, maxError);
colors[i3] = color.r;
colors[i3 + 1] = color.g;
@@ -1389,7 +1394,7 @@ const SceneContent = memo(function SceneContent({
/>
</bufferGeometry>
<pointsMaterial
size={0.04}
size={pointSize ?? 0.03}
vertexColors
sizeAttenuation
transparent
@@ -1529,11 +1534,13 @@ async function fetchBatchSampled(
} catch { /* ignore */ }
}
// 接收二进制,直接解析为 Float32Array,无 JSON 开销
// 接收二进制,根据 X-Has-Color 决定每点 stride(4 floats 或 7 floats)
const hasColor = resp.headers.get('X-Has-Color') === '1';
const stride = hasColor ? 7 : 4;
const arrayBuffer = await resp.arrayBuffer();
const floats = new Float32Array(arrayBuffer);
const count = Math.floor(floats.length / 4);
console.log(`[PointCloudViewer] Binary received: ${count} pts, ${(arrayBuffer.byteLength / 1e6).toFixed(1)} MB`);
const count = Math.floor(floats.length / stride);
console.log(`[PointCloudViewer] Binary received: ${count} pts, hasColor=${hasColor}, ${(arrayBuffer.byteLength / 1e6).toFixed(1)} MB`);
if (count === 0) return { points: [] };
@@ -1543,9 +1550,15 @@ async function fetchBatchSampled(
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
for (let i = 0; i < count; i++) {
const base = i * 4;
const base = i * stride;
const x = floats[base], y = floats[base + 1], z = floats[base + 2], error = floats[base + 3];
pts[i] = { x, y, z, error };
const pt: Point = { x, y, z, error };
if (hasColor) {
pt.r = floats[base + 4];
pt.g = floats[base + 5];
pt.b = floats[base + 6];
}
pts[i] = pt;
if (x < minX) minX = x; if (x > maxX) maxX = x;
if (y < minY) minY = y; if (y > maxY) maxY = y;
if (z < minZ) minZ = z; if (z > maxZ) maxZ = z;
@@ -1700,6 +1713,9 @@ export default function PointCloudViewer({
const maxDim = Math.max(maxX - minX, maxY - minY, maxZ - minZ);
const scale = maxDim > 0 ? 10 / maxDim : 1;
// 保存变换参数,供 ply 叠加层对齐用
pcTransformRef.current = { cx: centerX, cy: centerY, cz: centerZ, scale };
for (let i = 0; i < pts.length; i++) {
const p = pts[i];
p.x = (p.x - centerX) * scale;
@@ -1753,23 +1769,73 @@ export default function PointCloudViewer({
// PLY 叠加层状态(kind=ply)
const [plyOverlayData, setPlyOverlayData] = useState<{ positions: Float32Array; colors: Float32Array } | null>(null);
// 主点云的坐标变换参数,供 ply 叠加层对齐用
const pcTransformRef = useRef<{ cx: number; cy: number; cz: number; scale: number } | null>(null);
// 加载 PLY 叠加层
// 加载 PLY 叠加层:走采样接口,避免浏览器加载 15GB 文件
useEffect(() => {
if (pluginOverlay == null || pluginOverlay.overlay.kind !== 'ply') {
setPlyOverlayData(null);
return;
}
const { taskId, resultId, overlay } = pluginOverlay;
const url = `/api/files/read/analysis/${taskId}/${resultId}/${overlay.file}`;
const ac = new AbortController();
(async () => {
try {
const resp = await fetch(url, { signal: ac.signal });
// 用 /sample/batch 接口按 filePath 采样,最多 30 万点
// 支持多文件(各面板 PLY),合并采样
const plyFiles: string[] = (overlay as { files?: string[]; file: string }).files
?? [overlay.file];
const resp = await fetch('/api/files/sample/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
signal: ac.signal,
body: JSON.stringify({
filePaths: plyFiles.map(f => `/data/tasks/${taskId}/analysis/${resultId}/${f}`),
totalPoints: 300_000,
}),
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const text = await resp.text();
const parsed = parsePlyText(text);
if (parsed) setPlyOverlayData(parsed);
const hasColor = resp.headers.get('X-Has-Color') === '1';
const stride = hasColor ? 7 : 4;
const buf = await resp.arrayBuffer();
const floats = new Float32Array(buf);
const count = Math.floor(floats.length / stride);
if (count === 0) return;
const positions = new Float32Array(count * 3);
const colors = new Float32Array(count * 3);
if (hasColor) {
// stride=7: [x,y,z,error,r,g,b] — 用批量提取代替逐点循环
for (let i = 0; i < count; i++) {
const s = i * 7;
positions[i * 3] = floats[s];
positions[i * 3 + 1] = floats[s + 1];
positions[i * 3 + 2] = floats[s + 2];
colors[i * 3] = floats[s + 4];
colors[i * 3 + 1] = floats[s + 5];
colors[i * 3 + 2] = floats[s + 6];
}
} else {
// stride=4: [x,y,z,error] — 只提取 xyz,颜色统一蓝色
for (let i = 0; i < count; i++) {
const s = i * 4;
positions[i * 3] = floats[s];
positions[i * 3 + 1] = floats[s + 1];
positions[i * 3 + 2] = floats[s + 2];
colors[i * 3] = 0.4; colors[i * 3 + 1] = 0.8; colors[i * 3 + 2] = 1.0;
}
}
// 用主点云的坐标变换对齐叠加层坐标
const t = pcTransformRef.current;
if (t) {
for (let i = 0; i < count; i++) {
positions[i * 3] = (positions[i * 3] - t.cx) * t.scale;
positions[i * 3 + 1] = (positions[i * 3 + 1] - t.cy) * t.scale;
positions[i * 3 + 2] = (positions[i * 3 + 2] - t.cz) * t.scale;
}
}
setPlyOverlayData({ positions, colors });
console.log(`[PlyOverlay] loaded ${count} pts, hasColor=${hasColor}, transform=${JSON.stringify(t)}`);
} catch (e) {
if (e instanceof DOMException && e.name === 'AbortError') return;
console.warn('[PlyOverlay] load failed:', e);