feat: initial commit - massive-cloud point cloud processing system

This commit is contained in:
2026-05-27 09:51:42 +00:00
commit 16811100d1
391 changed files with 78338 additions and 0 deletions

View File

@@ -0,0 +1,483 @@
import { useState } from 'react';
import { trpc } from '@/lib/trpc';
import { BarChart2, Ruler, Zap, Play, Trash2, ChevronDown, ChevronRight, RefreshCw, Flame, Box, Layers, Crosshair, Cpu } from 'lucide-react';
import GenericResultView, { type Overlay3D } from './GenericResultView';
interface Props {
taskId: number;
onReplay?: (resultId: number) => void;
/** 在 3D 视口显示该条平滑热力分析结果 */
onViewSmoothHeatmap?: (resultId: number) => void;
/** 触发装配检测(来自 Home.tsx 的 runAnalysisJob) */
onRunAssembly?: () => void;
/** 触发偏差热力(来自 Home.tsx 的 runAnalysisJob) */
onRunDeviation?: () => void;
/** 外部分析任务是否正在提交中 */
externalPending?: boolean;
/** 插件叠加层:用户点击「在 3D 中查看叠加层」时回调 Home → PointCloudViewer */
onViewPluginOverlay?: (args: { taskId: number; resultId: number; overlay: Overlay3D }) => void;
}
type Measurement = { name: string; value: number; unit: string; tolerance: number; status: string };
type ErrorStats = { min: number; max: number; mean: number; stdDev: number; totalPoints: number; withinTolerance: number; outOfTolerance: number };
function ErrorStatCard({ stats }: { stats: ErrorStats }) {
const pct = stats.totalPoints > 0
? ((stats.withinTolerance / stats.totalPoints) * 100).toFixed(1)
: '0';
return (
<div className="space-y-2">
<div className="grid grid-cols-2 gap-1.5">
{[
{ label: '最小误差', value: stats.min.toFixed(4) + ' mm' },
{ label: '最大误差', value: stats.max.toFixed(4) + ' mm' },
{ label: '平均误差', value: stats.mean.toFixed(4) + ' mm' },
{ label: '标准差', value: stats.stdDev.toFixed(4) + ' mm' },
].map(({ label, value }) => (
<div key={label} className="industrial-card p-2">
<div className="text-[10px] text-muted-foreground">{label}</div>
<div className="text-xs font-mono mt-0.5">{value}</div>
</div>
))}
</div>
<div className="industrial-card p-2">
<div className="flex items-center justify-between text-[10px] mb-1">
<span className="text-muted-foreground">合格率</span>
<span className={parseFloat(pct) >= 95 ? 'text-green-400' : parseFloat(pct) >= 85 ? 'text-amber-400' : 'text-red-400'}>
{pct}%
</span>
</div>
<div className="industrial-progress">
<div
className="industrial-progress-bar"
style={{ width: `${pct}%`, background: parseFloat(pct) >= 95 ? 'oklch(0.62 0.16 145)' : parseFloat(pct) >= 85 ? 'oklch(0.72 0.16 85)' : 'oklch(0.55 0.20 28)' }}
/>
</div>
<div className="flex justify-between text-[10px] text-muted-foreground mt-1">
<span>合格 {stats.withinTolerance.toLocaleString()}</span>
<span>超差 {stats.outOfTolerance.toLocaleString()}</span>
</div>
</div>
</div>
);
}
function MeasurementTable({ items }: { items: Measurement[] }) {
return (
<table className="w-full text-[10px] border-collapse">
<thead>
<tr className="text-muted-foreground">
<th className="text-left py-1 font-normal">项目</th>
<th className="text-right py-1 font-normal">值</th>
<th className="text-right py-1 font-normal">公差</th>
<th className="text-right py-1 font-normal">结果</th>
</tr>
</thead>
<tbody>
{items.map((m, i) => (
<tr key={i} className="border-t border-border/40">
<td className="py-1">{m.name}</td>
<td className="text-right font-mono">{m.value.toFixed(3)} {m.unit}</td>
<td className="text-right text-muted-foreground">±{m.tolerance}</td>
<td className="text-right">
<span className={`px-1 py-0.5 rounded text-[9px] ${
m.status === 'pass' ? 'bg-green-900/40 text-green-400' :
m.status === 'warning' ? 'bg-amber-900/40 text-amber-400' :
'bg-red-900/40 text-red-400'
}`}>
{m.status === 'pass' ? '合格' : m.status === 'warning' ? '警告' : '超差'}
</span>
</td>
</tr>
))}
</tbody>
</table>
);
}
type SmoothHeatmapMeta = {
kind?: string;
/** 0–1,processing 时子进程写入 */
progress?: number;
stage?: string;
vertexCount?: number;
dispMin?: number;
dispMax?: number;
error?: string;
};
type AssemblyPartRow = {
partId: string;
name: string;
min: number;
max: number;
mean: number;
stdDev: number;
passRate: number;
pointCount: number;
vertexHeatmapBin?: string;
};
type AssemblyMeta = SmoothHeatmapMeta & {
toleranceMm?: number;
samplePoints?: number;
parts?: AssemblyPartRow[];
};
export default function AnalysisPanel({ taskId, onReplay, onViewSmoothHeatmap, onRunAssembly, onRunDeviation, externalPending, onViewPluginOverlay }: Props) {
const utils = trpc.useUtils();
const [expandedIds, setExpandedIds] = useState<Set<number>>(new Set());
const { data: results = [], isLoading } = trpc.analysisResult.list.useQuery(
{ taskId },
{ refetchInterval: 3000 },
);
const runAnalysis = trpc.analysisResult.runAnalysis.useMutation({
onSuccess: () => utils.analysisResult.list.invalidate({ taskId }),
});
const deleteResult = trpc.analysisResult.delete.useMutation({
onSuccess: () => utils.analysisResult.list.invalidate({ taskId }),
});
function toggleExpand(id: number) {
setExpandedIds(prev => {
const n = new Set(prev);
n.has(id) ? n.delete(id) : n.add(id);
return n;
});
}
const running = runAnalysis.isPending;
return (
<div className="space-y-3">
<div className="flex items-center justify-between gap-1 flex-wrap">
<span className="industrial-label flex items-center gap-1.5">
<BarChart2 className="w-3.5 h-3.5" /> 分析结果
</span>
<div className="flex items-center gap-1">
<button
className={`industrial-btn industrial-btn-primary py-0.5 text-[10px] flex items-center gap-0.5 ${running ? 'opacity-60 cursor-not-allowed' : ''}`}
onClick={() => !running && runAnalysis.mutate({ taskId, type: 'error_analysis' })}
disabled={running}
title="误差统计分析"
>
{running ? <RefreshCw className="w-3 h-3 animate-spin" /> : <Play className="w-3 h-3" />}
{running ? '分析中…' : '误差分析'}
</button>
{onRunAssembly && (
<button
className={`industrial-btn py-0.5 text-[10px] flex items-center gap-0.5 ${externalPending ? 'opacity-60 cursor-wait' : ''}`}
onClick={onRunAssembly}
disabled={externalPending}
title="需车型下已上传 STEP;后台解析零件并做最近零件归属与误差统计"
>
<Layers className="w-3 h-3" />
装配检测
</button>
)}
{onRunDeviation && (
<button
className={`industrial-btn py-0.5 text-[10px] flex items-center gap-0.5 ${externalPending ? 'opacity-60 cursor-wait' : ''}`}
onClick={onRunDeviation}
disabled={externalPending}
title="在 STL/OBJ 网格上按顶点到最近点云点的距离着色"
>
<Crosshair className="w-3 h-3" />
偏差热力
</button>
)}
</div>
</div>
{isLoading && (
<div className="text-xs text-muted-foreground text-center py-4">加载中…</div>
)}
{!isLoading && results.length === 0 && (
<div className="text-xs text-muted-foreground text-center py-4">
暂无分析结果,点击运行分析
</div>
)}
<div className="space-y-1.5">
{results.map(r => {
const expanded = expandedIds.has(r.id);
const errorStats = r.errorStats as ErrorStats | null;
const measurements = r.measurements as Measurement[] | null;
return (
<div key={r.id} className="industrial-card rounded overflow-hidden">
{/* 标题行 */}
<div
className="flex items-center gap-2 px-2 py-1.5 cursor-pointer hover:bg-secondary/40 group"
onClick={() => toggleExpand(r.id)}
>
{expanded ? <ChevronDown className="w-3 h-3 text-muted-foreground" /> : <ChevronRight className="w-3 h-3 text-muted-foreground" />}
{r.type === 'error_analysis' && <BarChart2 className="w-3.5 h-3.5 text-blue-400 flex-shrink-0" />}
{r.type === 'measurement' && <Ruler className="w-3.5 h-3.5 text-green-400 flex-shrink-0" />}
{r.type === 'compression' && <Zap className="w-3.5 h-3.5 text-amber-400 flex-shrink-0" />}
{r.type === 'smooth_heatmap' && <Flame className="w-3.5 h-3.5 text-orange-400 flex-shrink-0" />}
{r.type === 'assembly_inspection' && <Layers className="w-3.5 h-3.5 text-cyan-400 flex-shrink-0" />}
{r.type === 'obj_deviation' && <Crosshair className="w-3.5 h-3.5 text-rose-400 flex-shrink-0" />}
{r.type === 'plugin' && <Cpu className="w-3.5 h-3.5 text-violet-400 flex-shrink-0" />}
<span className="text-xs flex-1 truncate">
{r.type === 'error_analysis'
? '误差分析'
: r.type === 'measurement'
? '尺寸测量'
: r.type === 'smooth_heatmap'
? '平滑热力'
: r.type === 'assembly_inspection'
? '装配检测'
: r.type === 'obj_deviation'
? '偏差热力'
: r.type === 'plugin'
? `算法 · ${(r as { pluginId?: string }).pluginId ?? '插件'}`
: '压缩'}
</span>
<span className="text-[10px] text-muted-foreground">
{new Date(r.createdAt).toLocaleDateString('zh-CN')}
</span>
<span className={`text-[9px] px-1.5 py-0.5 rounded flex-shrink-0 ${
r.status === 'completed' ? 'bg-green-900/40 text-green-400' :
r.status === 'processing' ? 'bg-blue-900/40 text-blue-400 animate-pulse' :
r.status === 'error' ? 'bg-red-900/40 text-red-400' :
'bg-secondary text-muted-foreground'
}`}>{r.status}</span>
{r.status === 'processing' &&
(r.type === 'smooth_heatmap' || r.type === 'assembly_inspection' || r.type === 'obj_deviation' || r.type === 'plugin') &&
(() => {
const hm = r.resultData as SmoothHeatmapMeta | null;
const pct = hm?.progress != null ? Math.round(hm.progress * 100) : null;
return pct != null ? (
<span className="text-[9px] font-mono text-primary tabular-nums">{pct}%</span>
) : null;
})()}
<span className="hidden group-hover:flex items-center gap-0.5">
{onReplay && r.status === 'completed' && (
<button
className="p-0.5 rounded hover:bg-secondary"
title="回放此结果"
onClick={e => { e.stopPropagation(); onReplay(r.id); }}
>
<Play className="w-3 h-3 text-primary" />
</button>
)}
<button
className="p-0.5 rounded hover:bg-secondary text-destructive"
title="删除"
onClick={e => { e.stopPropagation(); deleteResult.mutate({ id: r.id }); }}
>
<Trash2 className="w-3 h-3" />
</button>
</span>
</div>
{/* 展开内容 */}
{expanded && r.status === 'completed' && r.type !== 'smooth_heatmap' && r.type !== 'assembly_inspection' && r.type !== 'obj_deviation' && r.type !== 'plugin' && (
<div className="px-3 pb-3 pt-1 border-t border-border/40 space-y-3">
{errorStats && <ErrorStatCard stats={errorStats} />}
{measurements && measurements.length > 0 && (
<div>
<div className="text-[10px] text-muted-foreground mb-1">尺寸测量</div>
<MeasurementTable items={measurements} />
</div>
)}
</div>
)}
{expanded &&
(r.type === 'smooth_heatmap' || r.type === 'assembly_inspection' || r.type === 'obj_deviation' || r.type === 'plugin') &&
r.status === 'processing' &&
(() => {
const hm = r.resultData as SmoothHeatmapMeta | null;
const p = hm?.progress ?? 0;
const stage = hm?.stage ?? '排队或启动中…';
return (
<div className="px-3 pb-3 pt-1 border-t border-border/40 space-y-2">
<div className="text-[10px] text-muted-foreground">{stage}</div>
<div className="industrial-progress h-1.5">
<div
className="industrial-progress-bar transition-[width] duration-300"
style={{
width: `${Math.min(100, Math.max(0, p * 100))}%`,
background: 'oklch(0.55 0.14 250)',
}}
/>
</div>
<p className="text-[9px] text-muted-foreground/80">
后台子进程执行,页面可继续操作;进度约每 1.2s 刷新。
</p>
</div>
);
})()}
{expanded && r.type === 'smooth_heatmap' && r.status === 'completed' && (
<div className="px-3 pb-3 pt-1 border-t border-border/40 space-y-2">
{(() => {
const hm = r.resultData as SmoothHeatmapMeta | null;
return (
<>
<div className="text-[10px] text-muted-foreground space-y-0.5">
{hm?.vertexCount != null && (
<div>网格顶点约 {hm.vertexCount.toLocaleString()}</div>
)}
{hm?.dispMin != null && hm?.dispMax != null && (
<div>
色标误差约 {hm.dispMin.toFixed(4)} ~ {hm.dispMax.toFixed(4)} mm
</div>
)}
</div>
<button
type="button"
className="industrial-btn industrial-btn-primary py-1 text-[10px] flex items-center gap-1 w-full justify-center"
onClick={(e) => {
e.stopPropagation();
onViewSmoothHeatmap?.(r.id);
}}
>
<Box className="w-3 h-3" />
在 3D 中查看
</button>
</>
);
})()}
</div>
)}
{expanded && r.type === 'smooth_heatmap' && r.status === 'error' && (
<div className="px-3 pb-3 pt-1 border-t border-border/40">
<p className="text-[10px] text-red-400/90">
{(r.resultData as SmoothHeatmapMeta | null)?.error ?? '分析失败'}
</p>
</div>
)}
{expanded && r.type === 'assembly_inspection' && r.status === 'completed' && (
<div className="px-3 pb-3 pt-1 border-t border-border/40 space-y-2">
{(() => {
const am = r.resultData as AssemblyMeta | null;
const tol = am?.toleranceMm ?? 0.3;
const parts = am?.parts ?? [];
return (
<>
<div className="text-[10px] text-muted-foreground">
阈值 &lt; {tol} mm 为合格;测点归属为「到各零件网格距离最小」的零件。
</div>
<div className="overflow-x-auto max-h-48 overflow-y-auto">
<table className="w-full text-[9px] border-collapse">
<thead>
<tr className="text-muted-foreground text-left">
<th className="py-0.5 pr-1">零件</th>
<th className="py-0.5 pr-1 text-right">点数</th>
<th className="py-0.5 pr-1 text-right">min</th>
<th className="py-0.5 pr-1 text-right">max</th>
<th className="py-0.5 pr-1 text-right">mean</th>
<th className="py-0.5 text-right">合格率</th>
</tr>
</thead>
<tbody>
{parts.map((p) => (
<tr key={p.partId} className="border-t border-border/30">
<td className="py-0.5 pr-1 font-mono truncate max-w-[100px]" title={p.name}>
{p.name}
</td>
<td className="py-0.5 pr-1 text-right">{p.pointCount}</td>
<td className="py-0.5 pr-1 text-right">{p.min.toFixed(4)}</td>
<td className="py-0.5 pr-1 text-right">{p.max.toFixed(4)}</td>
<td className="py-0.5 pr-1 text-right">{p.mean.toFixed(4)}</td>
<td className="py-0.5 text-right">
<span
className={
p.passRate >= 0.95
? 'text-green-400'
: p.passRate >= 0.85
? 'text-amber-400'
: 'text-red-400'
}
>
{(p.passRate * 100).toFixed(1)}%
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</>
);
})()}
</div>
)}
{expanded && r.type === 'assembly_inspection' && r.status === 'error' && (
<div className="px-3 pb-3 pt-1 border-t border-border/40">
<p className="text-[10px] text-red-400/90">
{(r.resultData as AssemblyMeta | null)?.error ?? '装配检测失败'}
</p>
</div>
)}
{expanded && r.type === 'obj_deviation' && r.status === 'completed' && (
<div className="px-3 pb-3 pt-1 border-t border-border/40 space-y-2">
{(() => {
const hm = r.resultData as SmoothHeatmapMeta | null;
return (
<>
<div className="text-[10px] text-muted-foreground space-y-0.5">
<div>按顶点到最近点云点的距离着色(蓝=近 → 红=远)</div>
{hm?.vertexCount != null && (
<div>网格顶点约 {hm.vertexCount.toLocaleString()}</div>
)}
{hm?.dispMin != null && hm?.dispMax != null && (
<div>
偏差范围约 {hm.dispMin.toFixed(4)} ~ {hm.dispMax.toFixed(4)}
</div>
)}
</div>
<button
type="button"
className="industrial-btn industrial-btn-primary py-1 text-[10px] flex items-center gap-1 w-full justify-center"
onClick={(e) => {
e.stopPropagation();
onViewSmoothHeatmap?.(r.id);
}}
>
<Box className="w-3 h-3" />
在 3D 中查看
</button>
</>
);
})()}
</div>
)}
{expanded && r.type === 'obj_deviation' && r.status === 'error' && (
<div className="px-3 pb-3 pt-1 border-t border-border/40">
<p className="text-[10px] text-red-400/90">
{(r.resultData as SmoothHeatmapMeta | null)?.error ?? '偏差分析失败'}
</p>
</div>
)}
{expanded && r.type === 'plugin' && (r.status === 'completed' || r.status === 'error') && (
<div className="px-3 pb-3 pt-1 border-t border-border/40">
<GenericResultView
taskId={taskId}
resultId={r.id}
pluginId={(r as { pluginId?: string }).pluginId ?? undefined}
pluginName={(r as { pluginId?: string }).pluginId ?? '算法插件'}
data={(r.resultData as Parameters<typeof GenericResultView>[0]['data']) ?? {}}
onViewOverlay={onViewPluginOverlay}
/>
</div>
)}
</div>
);
})}
</div>
</div>
);
}