feat: update algorithm integrations and point cloud overlays
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user