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,231 @@
/**
* 通用算法结果渲染(Route B)
* ------------------------------------------------------------
* 输入:一条 task_analysis_results.resultData(PluginResultData)记录。
* 输出:统一的「误差统计 + 测量值表 + 图片缩略图 + 3D 叠加按钮 + 原始字段折叠」。
*
* 设计目标:新增算法不需要为它写新的 React 组件;只要 result.json 字段对齐
* PluginResult,就能自动渲染。
*/
import { useState } from 'react';
import { Box, ChevronDown, ChevronRight, Image as ImageIcon } from 'lucide-react';
interface Measurement {
name: string;
value: number;
unit?: string;
tolerance?: number;
status?: 'pass' | 'warning' | 'fail' | 'info';
}
interface ErrorStats {
min: number;
max: number;
mean: number;
stdDev: number;
totalPoints?: number;
withinTolerance?: number;
outOfTolerance?: number;
}
type Overlay3D =
| { kind: 'none' }
| { kind: 'vertexColors'; file: string; vertexCount: number; dispMin: number; dispMax: number }
| { kind: 'ply'; file: string };
interface PluginResultLike {
pluginId?: string;
schemaVersion?: number;
message?: string;
measurements?: Measurement[];
errorStats?: ErrorStats | null;
overlay3d?: Overlay3D;
images?: string[];
extra?: Record<string, unknown>;
error?: string;
}
interface Props {
taskId: number;
resultId: number;
pluginId?: string;
pluginName?: string;
data: PluginResultLike;
/** 用户点击「在 3D 中查看」时回调,把叠加层信息传给 Home → PointCloudViewer */
onViewOverlay?: (overlay: { taskId: number; resultId: number; overlay: Overlay3D }) => void;
}
function statusClass(s?: Measurement['status']) {
switch (s) {
case 'pass':
return 'bg-green-900/40 text-green-400';
case 'warning':
return 'bg-amber-900/40 text-amber-400';
case 'fail':
return 'bg-red-900/40 text-red-400';
default:
return 'bg-secondary text-muted-foreground';
}
}
function statusLabel(s?: Measurement['status']) {
switch (s) {
case 'pass':
return '合格';
case 'warning':
return '警告';
case 'fail':
return '超差';
default:
return '—';
}
}
function formatValue(v: number): string {
if (!Number.isFinite(v)) return '—';
const abs = Math.abs(v);
if (abs !== 0 && (abs < 0.001 || abs >= 1e6)) return v.toExponential(3);
return v.toFixed(3);
}
export default function GenericResultView({
taskId,
resultId,
pluginName,
data,
onViewOverlay,
}: Props) {
const [showExtra, setShowExtra] = useState(false);
const errorStats = data.errorStats ?? null;
const measurements = data.measurements ?? [];
const images = data.images ?? [];
const overlay = data.overlay3d ?? { kind: 'none' };
return (
<div className="space-y-3">
{pluginName && (
<div className="text-[10px] text-muted-foreground">
{pluginName}
{data.pluginId ? ` · ${data.pluginId}` : ''}
{data.schemaVersion != null ? ` · schema v${data.schemaVersion}` : ''}
</div>
)}
{data.message && (
<div className="text-[10px] text-muted-foreground italic">{data.message}</div>
)}
{errorStats && (
<div className="grid grid-cols-2 gap-1.5">
{[
{ label: '最小误差', value: formatValue(errorStats.min) + ' mm' },
{ label: '最大误差', value: formatValue(errorStats.max) + ' mm' },
{ label: '平均误差', value: formatValue(errorStats.mean) + ' mm' },
{ label: '标准差', value: formatValue(errorStats.stdDev) + ' 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>
)}
{measurements.length > 0 && (
<div>
<div className="text-[10px] text-muted-foreground mb-1">测量值</div>
<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>
{measurements.map((m, i) => (
<tr key={`${m.name}-${i}`} className="border-t border-border/40">
<td className="py-1 truncate max-w-[140px]" title={m.name}>
{m.name}
</td>
<td className="text-right font-mono">
{formatValue(m.value)} {m.unit ?? ''}
</td>
<td className="text-right text-muted-foreground">
{m.tolerance != null ? `±${m.tolerance}` : '—'}
</td>
<td className="text-right">
<span className={`px-1 py-0.5 rounded text-[9px] ${statusClass(m.status)}`}>
{m.status === 'info' ? '参考' : statusLabel(m.status)}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{overlay.kind !== 'none' && onViewOverlay && (
<button
type="button"
className="industrial-btn industrial-btn-primary py-1 text-[10px] flex items-center gap-1 w-full justify-center"
onClick={() => onViewOverlay({ taskId, resultId, overlay })}
>
<Box className="w-3 h-3" />
在 3D 中查看叠加层
</button>
)}
{images.length > 0 && (
<div>
<div className="text-[10px] text-muted-foreground mb-1">产物图片</div>
<div className="grid grid-cols-2 gap-1.5">
{images.map((rel) => {
const url = `/api/files/read/analysis/${taskId}/${resultId}/${encodeURI(rel)}`;
return (
<a
key={rel}
href={url}
target="_blank"
rel="noreferrer"
className="industrial-card p-1 flex items-center gap-1 hover:bg-secondary/40 text-[10px]"
title={rel}
>
<ImageIcon className="w-3 h-3 flex-shrink-0" />
<span className="truncate">{rel}</span>
</a>
);
})}
</div>
</div>
)}
{data.extra && Object.keys(data.extra).length > 0 && (
<div>
<button
type="button"
className="flex items-center gap-1 text-[10px] text-muted-foreground hover:text-foreground"
onClick={() => setShowExtra((v) => !v)}
>
{showExtra ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
原始字段 (extra)
</button>
{showExtra && (
<pre className="industrial-card p-2 text-[9px] font-mono whitespace-pre-wrap break-all max-h-48 overflow-y-auto mt-1">
{JSON.stringify(data.extra, null, 2)}
</pre>
)}
</div>
)}
{data.error && (
<div className="industrial-card p-2 border border-red-900/40">
<div className="text-[10px] text-red-400/90">{data.error}</div>
</div>
)}
</div>
);
}
export type { PluginResultLike, Overlay3D, Measurement, ErrorStats };