feat: improve plugin overlay workflow

This commit is contained in:
2026-07-08 23:17:59 +08:00
parent 6076063be0
commit c1d4c1e4ce
8 changed files with 286 additions and 137 deletions

View File

@@ -10,20 +10,19 @@ import type { Overlay3D } from '@/components/GenericResultView';
import { trpc } from '@/lib/trpc';
import {
LogIn,
LogOut,
Box,
Cloud,
Eye,
EyeOff,
Settings,
Maximize2,
Crosshair,
Train,
Loader2,
} from 'lucide-react';
import { toast } from 'sonner';
export default function Home() {
const { user, loading, isAuthenticated, logout } = useAuth();
const { loading, isAuthenticated } = useAuth();
const utils = trpc.useUtils();
// ── 树导航选中状态 ─────────────────────────────────────────────────────────
@@ -69,7 +68,8 @@ export default function Home() {
// ── 视图控制 ───────────────────────────────────────────────────────────────
const [showHeatmap, setShowHeatmap] = useState(true);
const [showCADModel, setShowCADModel] = useState(true);
const [showPointCloud, setShowPointCloud] = useState(true);
const [showCADModel] = useState(true);
const [showWireframe, setShowWireframe] = useState(false);
const [pointSize, setPointSize] = useState(0.03);
/** 点云预览采样预算(非全量);调低可明显减轻卡顿 */
@@ -88,7 +88,7 @@ export default function Home() {
resultId: number;
overlay: Overlay3D;
} | null>(null);
const [errorPickEnabled, setErrorPickEnabled] = useState(false);
const [errorPickEnabled] = useState(false);
const [showLeftPanel, setShowLeftPanel] = useState(true);
const [showRightPanel, setShowRightPanel] = useState(true);
@@ -307,12 +307,47 @@ export default function Home() {
setHeatmapViewResultId(resultId);
setPointCloudRenderMode('smoothHeatmap');
}}
onViewPluginOverlay={({ taskId, resultId, overlay }) => {
onViewPluginOverlay={({ taskId, resultId, overlay, inputPointCloudFileIds }) => {
const expectedIds = inputPointCloudFileIds ?? [];
const expectedFiles = expectedIds
.map((id) => taskPcFiles.find((f) => f.id === id))
.filter((f): f is NonNullable<typeof f> => f != null);
const expectedNames = expectedIds
.map((id) => taskPcFiles.find((f) => f.id === id)?.name ?? `#${id}`)
.join('、');
const expectedLabel = expectedFiles.length > 0 && expectedFiles.every((f) => (f as { source?: string }).source === 'registered')
? '配准后的点云'
: '本算法结果对应的点云文件';
const expectedHint = expectedNames ? `${expectedLabel}:${expectedNames}` : expectedLabel;
if (pointCloudTaskId == null || pointCloudUrls.length === 0) {
toast.error(`请先在左侧选择并加载${expectedHint}`);
return;
}
if (pcLoading) {
toast.warning('点云仍在加载中,请等待加载完成后再查看叠加层');
return;
}
if (pointCloudTaskId !== taskId) {
toast.error(`当前 3D 视口加载的点云不属于该算法结果,请先在左侧选择并加载${expectedHint}`);
return;
}
if (expectedIds.length === 0) {
toast.warning('无法确认匹配关系,请加载该结果运行时使用的点云');
} else {
const loadedIds = selectedPcFileIds.length > 0
? selectedPcFileIds
: taskPcFiles.map((f) => f.id);
const missingIds = expectedIds.filter((id) => !loadedIds.includes(id));
if (missingIds.length > 0) {
toast.error(`当前加载的点云与该结果不匹配,请在左侧选择并加载${expectedHint}`);
return;
}
}
setHeatmapViewResultId(null);
setPluginOverlay({ taskId, resultId, overlay });
if (overlay.kind === 'vertexColors') {
setPointCloudRenderMode('smoothHeatmap');
}
setPointCloudRenderMode(overlay.kind === 'vertexColors' ? 'smoothHeatmap' : 'points');
}}
externalPending={runAnalysisJob.isPending}
onRunAssembly={() => {
@@ -374,33 +409,22 @@ export default function Home() {
</button>
</div>
<div className="flex items-center gap-2 px-3 py-1 industrial-card">
<span className={`status-led ${
pcLoading ? 'status-led-processing' :
cadLoading ? 'status-led-processing' :
'status-led-idle'
}`} />
<span className="text-xs text-muted-foreground">
{pcLoading ? `加载点云 ${pcProgress}%` :
cadLoading ? `加载 CAD ${cadLoadProgress}%` :
'待机'}
</span>
</div>
{(pcLoading || cadLoading) && (
<div className="flex items-center gap-2 px-3 py-1 industrial-card">
<span className="status-led status-led-processing" />
<span className="text-xs text-muted-foreground">
{pcLoading ? `加载点云 ${pcProgress}%` : `加载 CAD ${cadLoadProgress}%`}
</span>
</div>
)}
{loading ? (
<div className="w-7 h-7 rounded bg-secondary animate-pulse" />
) : isAuthenticated ? (
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">{user?.name || '用户'}</span>
<button className="industrial-btn py-1.5" onClick={() => logout()}>
<LogOut className="w-3.5 h-3.5" />
</button>
</div>
) : (
) : !isAuthenticated ? (
<a href={getLoginUrl()} className="industrial-btn industrial-btn-primary py-1.5">
<LogIn className="w-3.5 h-3.5" />
</a>
)}
) : null}
</div>
</header>
@@ -439,6 +463,7 @@ export default function Home() {
pointCloudUrls={pointCloudUrlsStable}
batchTaskId={batchTaskId ?? undefined}
loadKey={pcLoadKey}
showPointCloud={showPointCloud}
selectedPcFileIds={selectedPcFileIds.length > 0 ? selectedPcFileIds : undefined}
onCadProgress={handleCadProgress}
onCadLoaded={handleCadLoaded}
@@ -474,13 +499,9 @@ export default function Home() {
{/* 操作提示 */}
<div className="absolute bottom-3 left-3 info-overlay flex items-center gap-4 text-[10px] flex-wrap">
{errorPickEnabled && pointCloudRenderMode === 'smoothHeatmap' && (
<span className="text-amber-400/90">平滑模式不支持选点,请切回「点云」</span>
)}
{errorPickEnabled && pointCloudRenderMode === 'points'
? <><span>左键点击选点误差</span><span>右键平移</span><span>滚轮缩放</span></>
: <><span>左键旋转</span><span>右键平移</span><span>滚轮缩放</span></>
}
<span>左键旋转</span>
<span>右键平移</span>
<span>滚轮缩放</span>
</div>
{/* 选中点信息 */}
@@ -522,15 +543,21 @@ export default function Home() {
<div className="flex items-center gap-2">
<button
type="button"
className={`industrial-btn py-1 ${pointCloudRenderMode === 'points' ? 'industrial-btn-primary' : ''}`}
className={`industrial-btn h-7 px-2 py-0 inline-flex items-center gap-1 text-[10px] ${showPointCloud ? 'industrial-btn-primary' : ''}`}
onClick={() => {
setPointCloudRenderMode('points');
setHeatmapViewResultId(null);
setPluginOverlay(null);
if (pointCloudRenderMode !== 'points') {
setPointCloudRenderMode('points');
setHeatmapViewResultId(null);
setPluginOverlay(null);
setShowPointCloud(true);
return;
}
setShowPointCloud(v => !v);
}}
title="逐点显示(支持误差拾取)"
title={showPointCloud ? '隐藏点云' : '显示点云'}
>
<span className="text-[10px]">点云</span>
<Cloud className="w-3.5 h-3.5" />
<span>点云</span>
</button>
{heatmapViewResultId != null && pointCloudRenderMode === 'smoothHeatmap' && (
<span className="text-[9px] text-primary/90 px-1.5 py-0.5 rounded bg-primary/10 border border-primary/20">
@@ -538,26 +565,12 @@ export default function Home() {
</span>
)}
<button
className={`industrial-btn py-1 ${showHeatmap ? 'industrial-btn-primary' : ''}`}
type="button"
className={`industrial-btn h-7 px-2 py-0 inline-flex items-center gap-1 text-[10px] ${showHeatmap ? 'industrial-btn-primary' : ''}`}
onClick={() => setShowHeatmap(v => !v)}
>
{showHeatmap ? <Eye className="w-3.5 h-3.5" /> : <EyeOff className="w-3.5 h-3.5" />}
<span className="ml-1">热力图</span>
</button>
<button
className={`industrial-btn py-1 ${showCADModel ? 'industrial-btn-primary' : ''}`}
onClick={() => setShowCADModel(v => !v)}
>
<Box className="w-3.5 h-3.5" />
<span className="ml-1">CAD</span>
</button>
<button
className={`industrial-btn py-1 ${errorPickEnabled ? 'industrial-btn-primary' : ''}`}
onClick={() => setErrorPickEnabled(v => !v)}
title="开启后左键点击显示该点误差"
>
<Crosshair className="w-3.5 h-3.5" />
<span className="ml-1">误差拾取</span>
<span>热力图</span>
</button>
</div>
@@ -584,7 +597,7 @@ export default function Home() {
<input
type="range"
min={100_000}
max={5_000_000}
max={2_000_000}
step={100_000}
value={renderPointBudget}
onChange={e => setRenderPointBudget(Number(e.target.value))}