Compare commits
2 Commits
6076063be0
...
683e4d6c07
| Author | SHA1 | Date | |
|---|---|---|---|
| 683e4d6c07 | |||
| c1d4c1e4ce |
@@ -208,6 +208,7 @@ export default function AlgorithmRunPanel({ taskId, modelId, pointCloudLoaded: _
|
|||||||
const demoPluginIds = new Set(demoAllPluginIds);
|
const demoPluginIds = new Set(demoAllPluginIds);
|
||||||
const runningDemoResults = analysisResults.filter((r) => (r.status === 'processing' || r.status === 'pending') && r.type === 'plugin' && r.pluginId && demoPluginIds.has(r.pluginId));
|
const runningDemoResults = analysisResults.filter((r) => (r.status === 'processing' || r.status === 'pending') && r.type === 'plugin' && r.pluginId && demoPluginIds.has(r.pluginId));
|
||||||
const demoFlowRunning = runningDemoResults.length > 0;
|
const demoFlowRunning = runningDemoResults.length > 0;
|
||||||
|
const demoFlowActive = demoFlowRunning || runDemoAll.isPending;
|
||||||
|
|
||||||
const selectedPcCount = selectedPcFileIds?.length ?? 0;
|
const selectedPcCount = selectedPcFileIds?.length ?? 0;
|
||||||
const selectedCadCount = selectedCadFileIds?.length ?? 0;
|
const selectedCadCount = selectedCadFileIds?.length ?? 0;
|
||||||
@@ -490,7 +491,7 @@ export default function AlgorithmRunPanel({ taskId, modelId, pointCloudLoaded: _
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 未配准提示:有点云文件且选了测量算法,但所选点云均未配准 */}
|
{/* 未配准提示:有点云文件且选了测量算法,但所选点云均未配准 */}
|
||||||
{selectedMeasIds.length > 0 && !hasRegisteredCloud && (taskPcFileCount ?? 0) > 0 && (
|
{selectedMeasIds.length > 0 && !hasRegisteredCloud && (taskPcFileCount ?? 0) > 0 && !demoFlowActive && (
|
||||||
<div className="flex items-start gap-1.5 bg-amber-900/20 border border-amber-700/40 rounded p-2">
|
<div className="flex items-start gap-1.5 bg-amber-900/20 border border-amber-700/40 rounded p-2">
|
||||||
<AlertTriangle className="w-3 h-3 text-amber-400 flex-shrink-0 mt-0.5" />
|
<AlertTriangle className="w-3 h-3 text-amber-400 flex-shrink-0 mt-0.5" />
|
||||||
<span className="text-[10px] text-amber-300">
|
<span className="text-[10px] text-amber-300">
|
||||||
|
|||||||
@@ -55,7 +55,13 @@ interface Props {
|
|||||||
/** 外部分析任务是否正在提交中 */
|
/** 外部分析任务是否正在提交中 */
|
||||||
externalPending?: boolean;
|
externalPending?: boolean;
|
||||||
/** 插件叠加层:用户点击「在 3D 中查看叠加层」时回调 Home → PointCloudViewer */
|
/** 插件叠加层:用户点击「在 3D 中查看叠加层」时回调 Home → PointCloudViewer */
|
||||||
onViewPluginOverlay?: (args: { taskId: number; resultId: number; overlay: Overlay3D }) => void;
|
onViewPluginOverlay?: (args: {
|
||||||
|
taskId: number;
|
||||||
|
resultId: number;
|
||||||
|
overlay: Overlay3D;
|
||||||
|
inputPointCloudFileIds?: number[];
|
||||||
|
inputCadFileIds?: number[];
|
||||||
|
}) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Measurement = { name: string; value: number; unit: string; tolerance: number; status: string };
|
type Measurement = { name: string; value: number; unit: string; tolerance: number; status: string };
|
||||||
@@ -226,6 +232,8 @@ type DemoFlowMeta = {
|
|||||||
total?: number;
|
total?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SHOW_LEGACY_ANALYSIS_ACTIONS = false;
|
||||||
|
|
||||||
type AnalysisListItem =
|
type AnalysisListItem =
|
||||||
| { kind: 'result'; result: AnalysisResultRow }
|
| { kind: 'result'; result: AnalysisResultRow }
|
||||||
| { kind: 'demoFlow'; runId: string; results: AnalysisResultRow[] };
|
| { kind: 'demoFlow'; runId: string; results: AnalysisResultRow[] };
|
||||||
@@ -752,36 +760,44 @@ export default function AnalysisPanel({ taskId, onReplay, onViewSmoothHeatmap, o
|
|||||||
>
|
>
|
||||||
<Maximize2 className="w-3 h-3" />
|
<Maximize2 className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
<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' })}
|
装配检测仍有 legacy 后台任务,但暂不作为主流程按钮展示。
|
||||||
disabled={running}
|
偏差热力依赖 STL/OBJ,不适合当前以 STEP 为主的流程,暂时隐藏。 */}
|
||||||
title="误差统计分析"
|
{SHOW_LEGACY_ANALYSIS_ACTIONS && (
|
||||||
>
|
<>
|
||||||
{running ? <RefreshCw className="w-3 h-3 animate-spin" /> : <Play className="w-3 h-3" />}
|
<button
|
||||||
{running ? '分析中…' : '误差分析'}
|
className={`industrial-btn industrial-btn-primary py-0.5 text-[10px] flex items-center gap-0.5 ${running ? 'opacity-60 cursor-not-allowed' : ''}`}
|
||||||
</button>
|
onClick={() => !running && runAnalysis.mutate({ taskId, type: 'error_analysis' })}
|
||||||
{onRunAssembly && (
|
disabled={running}
|
||||||
<button
|
title="误差统计分析"
|
||||||
className={`industrial-btn py-0.5 text-[10px] flex items-center gap-0.5 ${externalPending ? 'opacity-60 cursor-wait' : ''}`}
|
>
|
||||||
onClick={onRunAssembly}
|
{running ? <RefreshCw className="w-3 h-3 animate-spin" /> : <Play className="w-3 h-3" />}
|
||||||
disabled={externalPending}
|
{running ? '分析中…' : '误差分析'}
|
||||||
title="需车型下已上传 STEP;后台解析零件并做最近零件归属与误差统计"
|
</button>
|
||||||
>
|
{onRunAssembly && (
|
||||||
<Layers className="w-3 h-3" />
|
<button
|
||||||
装配检测
|
className={`industrial-btn py-0.5 text-[10px] flex items-center gap-0.5 ${externalPending ? 'opacity-60 cursor-wait' : ''}`}
|
||||||
</button>
|
onClick={onRunAssembly}
|
||||||
)}
|
disabled={externalPending}
|
||||||
{onRunDeviation && (
|
title="需车型下已上传 STEP;后台解析零件并做最近零件归属与误差统计"
|
||||||
<button
|
>
|
||||||
className={`industrial-btn py-0.5 text-[10px] flex items-center gap-0.5 ${externalPending ? 'opacity-60 cursor-wait' : ''}`}
|
<Layers className="w-3 h-3" />
|
||||||
onClick={onRunDeviation}
|
装配检测
|
||||||
disabled={externalPending}
|
</button>
|
||||||
title="在 STL/OBJ 网格上按顶点到最近点云点的距离着色"
|
)}
|
||||||
>
|
{onRunDeviation && (
|
||||||
<Crosshair className="w-3 h-3" />
|
<button
|
||||||
偏差热力
|
className={`industrial-btn py-0.5 text-[10px] flex items-center gap-0.5 ${externalPending ? 'opacity-60 cursor-wait' : ''}`}
|
||||||
</button>
|
onClick={onRunDeviation}
|
||||||
|
disabled={externalPending}
|
||||||
|
title="在 STL/OBJ 网格上按顶点到最近点云点的距离着色"
|
||||||
|
>
|
||||||
|
<Crosshair className="w-3 h-3" />
|
||||||
|
偏差热力
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
import { useAuth } from "@/_core/hooks/useAuth";
|
import { useAuth } from "@/_core/hooks/useAuth";
|
||||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
SidebarFooter,
|
|
||||||
SidebarHeader,
|
SidebarHeader,
|
||||||
SidebarInset,
|
SidebarInset,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
@@ -21,7 +13,7 @@ import {
|
|||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { getLoginUrl } from "@/const";
|
import { getLoginUrl } from "@/const";
|
||||||
import { useIsMobile } from "@/hooks/useMobile";
|
import { useIsMobile } from "@/hooks/useMobile";
|
||||||
import { LayoutDashboard, LogOut, PanelLeft, Users } from "lucide-react";
|
import { LayoutDashboard, PanelLeft, Users } from "lucide-react";
|
||||||
import { CSSProperties, useEffect, useRef, useState } from "react";
|
import { CSSProperties, useEffect, useRef, useState } from "react";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { DashboardLayoutSkeleton } from './DashboardLayoutSkeleton';
|
import { DashboardLayoutSkeleton } from './DashboardLayoutSkeleton';
|
||||||
@@ -106,7 +98,6 @@ function DashboardLayoutContent({
|
|||||||
children,
|
children,
|
||||||
setSidebarWidth,
|
setSidebarWidth,
|
||||||
}: DashboardLayoutContentProps) {
|
}: DashboardLayoutContentProps) {
|
||||||
const { user, logout } = useAuth();
|
|
||||||
const [location, setLocation] = useLocation();
|
const [location, setLocation] = useLocation();
|
||||||
const { state, toggleSidebar } = useSidebar();
|
const { state, toggleSidebar } = useSidebar();
|
||||||
const isCollapsed = state === "collapsed";
|
const isCollapsed = state === "collapsed";
|
||||||
@@ -201,36 +192,6 @@ function DashboardLayoutContent({
|
|||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
|
|
||||||
<SidebarFooter className="p-3">
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<button className="flex items-center gap-3 rounded-lg px-1 py-1 hover:bg-accent/50 transition-colors w-full text-left group-data-[collapsible=icon]:justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-ring">
|
|
||||||
<Avatar className="h-9 w-9 border shrink-0">
|
|
||||||
<AvatarFallback className="text-xs font-medium">
|
|
||||||
{user?.name?.charAt(0).toUpperCase()}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="flex-1 min-w-0 group-data-[collapsible=icon]:hidden">
|
|
||||||
<p className="text-sm font-medium truncate leading-none">
|
|
||||||
{user?.name || "-"}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-muted-foreground truncate mt-1.5">
|
|
||||||
{user?.email || "-"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end" className="w-48">
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={logout}
|
|
||||||
className="cursor-pointer text-destructive focus:text-destructive"
|
|
||||||
>
|
|
||||||
<LogOut className="mr-2 h-4 w-4" />
|
|
||||||
<span>Sign out</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</SidebarFooter>
|
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
<div
|
<div
|
||||||
className={`absolute top-0 right-0 w-1 h-full cursor-col-resize hover:bg-primary/20 transition-colors ${isCollapsed ? "hidden" : ""}`}
|
className={`absolute top-0 right-0 w-1 h-full cursor-col-resize hover:bg-primary/20 transition-colors ${isCollapsed ? "hidden" : ""}`}
|
||||||
|
|||||||
@@ -38,7 +38,11 @@ interface ErrorStats {
|
|||||||
type Overlay3D =
|
type Overlay3D =
|
||||||
| { kind: 'none' }
|
| { kind: 'none' }
|
||||||
| { kind: 'vertexColors'; file: string; vertexCount: number; dispMin: number; dispMax: number }
|
| { kind: 'vertexColors'; file: string; vertexCount: number; dispMin: number; dispMax: number }
|
||||||
| { kind: 'ply'; file: string };
|
| { kind: 'ply'; file: string; files?: string[] };
|
||||||
|
|
||||||
|
type RawOverlay3D =
|
||||||
|
| Overlay3D
|
||||||
|
| { kind: 'vertexColors'; file?: string; url?: string; vertexCount?: number; dispMin?: number; dispMax?: number };
|
||||||
|
|
||||||
interface PluginResultLike {
|
interface PluginResultLike {
|
||||||
pluginId?: string;
|
pluginId?: string;
|
||||||
@@ -46,7 +50,9 @@ interface PluginResultLike {
|
|||||||
message?: string;
|
message?: string;
|
||||||
measurements?: Measurement[];
|
measurements?: Measurement[];
|
||||||
errorStats?: ErrorStats | null;
|
errorStats?: ErrorStats | null;
|
||||||
overlay3d?: Overlay3D;
|
overlay3d?: RawOverlay3D;
|
||||||
|
inputPointCloudFileIds?: unknown;
|
||||||
|
inputCadFileIds?: unknown;
|
||||||
images?: string[];
|
images?: string[];
|
||||||
/** outDir 下的报表产物(相对路径,如 report.xlsx),前端展示下载入口 */
|
/** outDir 下的报表产物(相对路径,如 report.xlsx),前端展示下载入口 */
|
||||||
files?: (string | { name?: string; path?: string })[];
|
files?: (string | { name?: string; path?: string })[];
|
||||||
@@ -61,7 +67,13 @@ interface Props {
|
|||||||
pluginName?: string;
|
pluginName?: string;
|
||||||
data: PluginResultLike;
|
data: PluginResultLike;
|
||||||
/** 用户点击「在 3D 中查看」时回调,把叠加层信息传给 Home → PointCloudViewer */
|
/** 用户点击「在 3D 中查看」时回调,把叠加层信息传给 Home → PointCloudViewer */
|
||||||
onViewOverlay?: (overlay: { taskId: number; resultId: number; overlay: Overlay3D }) => void;
|
onViewOverlay?: (overlay: {
|
||||||
|
taskId: number;
|
||||||
|
resultId: number;
|
||||||
|
overlay: Overlay3D;
|
||||||
|
inputPointCloudFileIds?: number[];
|
||||||
|
inputCadFileIds?: number[];
|
||||||
|
}) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusClass(s?: Measurement['status']) {
|
function statusClass(s?: Measurement['status']) {
|
||||||
@@ -97,6 +109,48 @@ function formatValue(v: number): string {
|
|||||||
return v.toFixed(3);
|
return v.toFixed(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function numberArrayFromUnknown(value: unknown): number[] | undefined {
|
||||||
|
if (!Array.isArray(value)) return undefined;
|
||||||
|
const ids = value.filter((id): id is number => typeof id === 'number' && Number.isInteger(id));
|
||||||
|
return ids.length > 0 ? ids : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeOverlay3D(overlay?: RawOverlay3D): Overlay3D {
|
||||||
|
if (!overlay || overlay.kind === 'none') return { kind: 'none' };
|
||||||
|
if (overlay.kind === 'ply') {
|
||||||
|
const files = Array.isArray(overlay.files)
|
||||||
|
? overlay.files.filter((f): f is string => typeof f === 'string' && f.length > 0)
|
||||||
|
: undefined;
|
||||||
|
return files && files.length > 0
|
||||||
|
? { kind: 'ply', file: overlay.file, files }
|
||||||
|
: { kind: 'ply', file: overlay.file };
|
||||||
|
}
|
||||||
|
|
||||||
|
const legacyUrl = 'url' in overlay ? overlay.url : undefined;
|
||||||
|
const file = typeof overlay.file === 'string' && overlay.file.length > 0
|
||||||
|
? overlay.file
|
||||||
|
: typeof legacyUrl === 'string' && legacyUrl.length > 0
|
||||||
|
? legacyUrl
|
||||||
|
: undefined;
|
||||||
|
if (!file) return { kind: 'none' };
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof overlay.vertexCount === 'number' &&
|
||||||
|
typeof overlay.dispMin === 'number' &&
|
||||||
|
typeof overlay.dispMax === 'number'
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
kind: 'vertexColors',
|
||||||
|
file,
|
||||||
|
vertexCount: overlay.vertexCount,
|
||||||
|
dispMin: overlay.dispMin,
|
||||||
|
dispMax: overlay.dispMax,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { kind: 'ply', file };
|
||||||
|
}
|
||||||
|
|
||||||
export default function GenericResultView({
|
export default function GenericResultView({
|
||||||
taskId,
|
taskId,
|
||||||
resultId,
|
resultId,
|
||||||
@@ -112,7 +166,9 @@ export default function GenericResultView({
|
|||||||
const files = (data.files ?? []).map((f) =>
|
const files = (data.files ?? []).map((f) =>
|
||||||
typeof f === 'string' ? f : (f as { name?: string; path?: string }).name ?? String(f)
|
typeof f === 'string' ? f : (f as { name?: string; path?: string }).name ?? String(f)
|
||||||
);
|
);
|
||||||
const overlay = data.overlay3d ?? { kind: 'none' };
|
const overlay = normalizeOverlay3D(data.overlay3d);
|
||||||
|
const inputPointCloudFileIds = numberArrayFromUnknown(data.inputPointCloudFileIds);
|
||||||
|
const inputCadFileIds = numberArrayFromUnknown(data.inputCadFileIds);
|
||||||
const displayPluginName = pluginName ?? pluginId ?? data.pluginId;
|
const displayPluginName = pluginName ?? pluginId ?? data.pluginId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -191,7 +247,7 @@ export default function GenericResultView({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="industrial-btn industrial-btn-primary py-1 text-[10px] flex items-center gap-1 w-full justify-center"
|
className="industrial-btn industrial-btn-primary py-1 text-[10px] flex items-center gap-1 w-full justify-center"
|
||||||
onClick={() => onViewOverlay({ taskId, resultId, overlay })}
|
onClick={() => onViewOverlay({ taskId, resultId, overlay, inputPointCloudFileIds, inputCadFileIds })}
|
||||||
>
|
>
|
||||||
<Box className="w-3 h-3" />
|
<Box className="w-3 h-3" />
|
||||||
在 3D 中查看叠加层
|
在 3D 中查看叠加层
|
||||||
|
|||||||
@@ -1023,7 +1023,7 @@ const CADModel = memo(function CADModel({
|
|||||||
}
|
}
|
||||||
}, [useVertexColors, geometry]);
|
}, [useVertexColors, geometry]);
|
||||||
|
|
||||||
if (!visible && !useVertexColors) return null;
|
if (!visible) return null;
|
||||||
|
|
||||||
if (!geometry && !loading) return null;
|
if (!geometry && !loading) return null;
|
||||||
|
|
||||||
@@ -1178,6 +1178,7 @@ interface SceneContentProps {
|
|||||||
pointCloudData?: PointCloudData | null;
|
pointCloudData?: PointCloudData | null;
|
||||||
pointSize: number;
|
pointSize: number;
|
||||||
showHeatmap: boolean;
|
showHeatmap: boolean;
|
||||||
|
showPointCloud: boolean;
|
||||||
pointCloudRenderMode: PointCloudRenderMode;
|
pointCloudRenderMode: PointCloudRenderMode;
|
||||||
enablePointPicking: boolean;
|
enablePointPicking: boolean;
|
||||||
showCADModel: boolean;
|
showCADModel: boolean;
|
||||||
@@ -1207,6 +1208,7 @@ const SceneContent = memo(function SceneContent({
|
|||||||
pointCloudData,
|
pointCloudData,
|
||||||
pointSize,
|
pointSize,
|
||||||
showHeatmap,
|
showHeatmap,
|
||||||
|
showPointCloud,
|
||||||
pointCloudRenderMode,
|
pointCloudRenderMode,
|
||||||
enablePointPicking,
|
enablePointPicking,
|
||||||
showCADModel,
|
showCADModel,
|
||||||
@@ -1253,9 +1255,15 @@ const SceneContent = memo(function SceneContent({
|
|||||||
const cadShellHeatmapOn =
|
const cadShellHeatmapOn =
|
||||||
pointCloudRenderMode === 'smoothHeatmap' &&
|
pointCloudRenderMode === 'smoothHeatmap' &&
|
||||||
Boolean(pointCloudData && cadSurfaceGeometry) &&
|
Boolean(pointCloudData && cadSurfaceGeometry) &&
|
||||||
|
showCADModel &&
|
||||||
!cadHeatmapSuppressClient &&
|
!cadHeatmapSuppressClient &&
|
||||||
!useDirectColoring;
|
!useDirectColoring;
|
||||||
const showCadReferenceMesh = (showCADModel && !cadShellHeatmapOn) || useDirectColoring;
|
const showCadReferenceMesh = showCADModel && !cadShellHeatmapOn;
|
||||||
|
const showSmoothPointFallback =
|
||||||
|
pointCloudData &&
|
||||||
|
showPointCloud &&
|
||||||
|
pointCloudRenderMode === 'smoothHeatmap' &&
|
||||||
|
(!cadSurfaceGeometry || cadHeatmapSuppressClient || !showCADModel);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -1292,7 +1300,7 @@ const SceneContent = memo(function SceneContent({
|
|||||||
{!lightweightScene && <AmbientParticles />}
|
{!lightweightScene && <AmbientParticles />}
|
||||||
|
|
||||||
{/* Point cloud - 只有当有数据时才渲染,完全禁用事件以提高性能 */}
|
{/* Point cloud - 只有当有数据时才渲染,完全禁用事件以提高性能 */}
|
||||||
{pointCloudData && pointCloudRenderMode === 'points' && (
|
{pointCloudData && showPointCloud && pointCloudRenderMode === 'points' && (
|
||||||
<PointCloudMesh
|
<PointCloudMesh
|
||||||
data={pointCloudData}
|
data={pointCloudData}
|
||||||
pointSize={pointSize}
|
pointSize={pointSize}
|
||||||
@@ -1305,6 +1313,7 @@ const SceneContent = memo(function SceneContent({
|
|||||||
{pointCloudData &&
|
{pointCloudData &&
|
||||||
pointCloudRenderMode === 'smoothHeatmap' &&
|
pointCloudRenderMode === 'smoothHeatmap' &&
|
||||||
cadSurfaceGeometry &&
|
cadSurfaceGeometry &&
|
||||||
|
showCADModel &&
|
||||||
!cadHeatmapSuppressClient &&
|
!cadHeatmapSuppressClient &&
|
||||||
!useDirectColoring && (
|
!useDirectColoring && (
|
||||||
<CadSurfaceHeatmapMesh
|
<CadSurfaceHeatmapMesh
|
||||||
@@ -1314,34 +1323,23 @@ const SceneContent = memo(function SceneContent({
|
|||||||
showWireframe={showWireframe}
|
showWireframe={showWireframe}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{pointCloudData &&
|
{showSmoothPointFallback && (
|
||||||
pointCloudRenderMode === 'smoothHeatmap' &&
|
|
||||||
cadSurfaceGeometry &&
|
|
||||||
cadHeatmapSuppressClient && (
|
|
||||||
<PointCloudMesh
|
|
||||||
data={pointCloudData}
|
|
||||||
pointSize={Math.min(pointSize * 2.8, 0.12)}
|
|
||||||
showHeatmap={showHeatmap}
|
|
||||||
enablePointPicking={false}
|
|
||||||
errorRangeMode="percentile"
|
|
||||||
pointOpacity={showHeatmap ? 0.38 : 0.55}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{pointCloudData && pointCloudRenderMode === 'smoothHeatmap' && !cadSurfaceGeometry && (
|
|
||||||
<>
|
<>
|
||||||
<PointCloudMesh
|
<PointCloudMesh
|
||||||
data={pointCloudData}
|
data={pointCloudData}
|
||||||
pointSize={Math.min(pointSize * 3.2, 0.14)}
|
pointSize={Math.min(pointSize * (cadSurfaceGeometry ? 2.8 : 3.2), cadSurfaceGeometry ? 0.12 : 0.14)}
|
||||||
showHeatmap={showHeatmap}
|
showHeatmap={showHeatmap}
|
||||||
enablePointPicking={false}
|
enablePointPicking={false}
|
||||||
errorRangeMode="percentile"
|
errorRangeMode="percentile"
|
||||||
pointOpacity={showHeatmap ? 0.42 : 0.6}
|
pointOpacity={showHeatmap ? (cadSurfaceGeometry ? 0.38 : 0.42) : (cadSurfaceGeometry ? 0.55 : 0.6)}
|
||||||
/>
|
|
||||||
<SmoothHeatmapMesh
|
|
||||||
data={pointCloudData}
|
|
||||||
showHeatmap={showHeatmap}
|
|
||||||
showWireframe={showWireframe}
|
|
||||||
/>
|
/>
|
||||||
|
{!cadSurfaceGeometry && (
|
||||||
|
<SmoothHeatmapMesh
|
||||||
|
data={pointCloudData}
|
||||||
|
showHeatmap={showHeatmap}
|
||||||
|
showWireframe={showWireframe}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1408,6 +1406,7 @@ const SceneContent = memo(function SceneContent({
|
|||||||
return prevProps.pointCloudData === nextProps.pointCloudData &&
|
return prevProps.pointCloudData === nextProps.pointCloudData &&
|
||||||
prevProps.pointSize === nextProps.pointSize &&
|
prevProps.pointSize === nextProps.pointSize &&
|
||||||
prevProps.showHeatmap === nextProps.showHeatmap &&
|
prevProps.showHeatmap === nextProps.showHeatmap &&
|
||||||
|
prevProps.showPointCloud === nextProps.showPointCloud &&
|
||||||
prevProps.pointCloudRenderMode === nextProps.pointCloudRenderMode &&
|
prevProps.pointCloudRenderMode === nextProps.pointCloudRenderMode &&
|
||||||
prevProps.enablePointPicking === nextProps.enablePointPicking &&
|
prevProps.enablePointPicking === nextProps.enablePointPicking &&
|
||||||
prevProps.showCADModel === nextProps.showCADModel &&
|
prevProps.showCADModel === nextProps.showCADModel &&
|
||||||
@@ -1578,6 +1577,7 @@ interface PointCloudViewerProps {
|
|||||||
*/
|
*/
|
||||||
loadKey?: number;
|
loadKey?: number;
|
||||||
showHeatmap?: boolean;
|
showHeatmap?: boolean;
|
||||||
|
showPointCloud?: boolean;
|
||||||
showCADModel?: boolean;
|
showCADModel?: boolean;
|
||||||
showWireframe?: boolean;
|
showWireframe?: boolean;
|
||||||
pointSize?: number;
|
pointSize?: number;
|
||||||
@@ -1614,7 +1614,7 @@ interface PointCloudViewerProps {
|
|||||||
overlay:
|
overlay:
|
||||||
| { kind: 'none' }
|
| { kind: 'none' }
|
||||||
| { kind: 'vertexColors'; file: string; vertexCount: number; dispMin: number; dispMax: number }
|
| { kind: 'vertexColors'; file: string; vertexCount: number; dispMin: number; dispMax: number }
|
||||||
| { kind: 'ply'; file: string };
|
| { kind: 'ply'; file: string; files?: string[] };
|
||||||
} | null;
|
} | null;
|
||||||
/**
|
/**
|
||||||
* ROI 坐标拾取模式。激活时,用户在 3D 视图中点击任意点,
|
* ROI 坐标拾取模式。激活时,用户在 3D 视图中点击任意点,
|
||||||
@@ -1630,6 +1630,7 @@ export default function PointCloudViewer({
|
|||||||
batchTaskId,
|
batchTaskId,
|
||||||
loadKey,
|
loadKey,
|
||||||
showHeatmap = true,
|
showHeatmap = true,
|
||||||
|
showPointCloud = true,
|
||||||
showCADModel = true,
|
showCADModel = true,
|
||||||
showWireframe = false,
|
showWireframe = false,
|
||||||
pointSize = 0.03,
|
pointSize = 0.03,
|
||||||
@@ -1769,6 +1770,7 @@ export default function PointCloudViewer({
|
|||||||
|
|
||||||
// PLY 叠加层状态(kind=ply)
|
// PLY 叠加层状态(kind=ply)
|
||||||
const [plyOverlayData, setPlyOverlayData] = useState<{ positions: Float32Array; colors: Float32Array } | null>(null);
|
const [plyOverlayData, setPlyOverlayData] = useState<{ positions: Float32Array; colors: Float32Array } | null>(null);
|
||||||
|
const [plyOverlayStatus, setPlyOverlayStatus] = useState<{ status: 'idle' | 'loading' | 'ready' | 'error'; message?: string }>({ status: 'idle' });
|
||||||
// 主点云的坐标变换参数,供 ply 叠加层对齐用
|
// 主点云的坐标变换参数,供 ply 叠加层对齐用
|
||||||
const pcTransformRef = useRef<{ cx: number; cy: number; cz: number; scale: number } | null>(null);
|
const pcTransformRef = useRef<{ cx: number; cy: number; cz: number; scale: number } | null>(null);
|
||||||
|
|
||||||
@@ -1776,16 +1778,28 @@ export default function PointCloudViewer({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pluginOverlay == null || pluginOverlay.overlay.kind !== 'ply') {
|
if (pluginOverlay == null || pluginOverlay.overlay.kind !== 'ply') {
|
||||||
setPlyOverlayData(null);
|
setPlyOverlayData(null);
|
||||||
|
setPlyOverlayStatus({ status: 'idle' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const t = pcTransformRef.current;
|
||||||
|
if (!t || !pointCloudData?.points?.length) {
|
||||||
|
setPlyOverlayData(null);
|
||||||
|
setPlyOverlayStatus({
|
||||||
|
status: 'error',
|
||||||
|
message: '请先加载本算法结果对应的点云文件,再查看 PLY 叠加层',
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { taskId, resultId, overlay } = pluginOverlay;
|
const { taskId, resultId, overlay } = pluginOverlay;
|
||||||
const ac = new AbortController();
|
const ac = new AbortController();
|
||||||
|
setPlyOverlayStatus({ status: 'loading', message: '正在加载 PLY 叠加层' });
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
// 用 /sample/batch 接口按 filePath 采样,最多 30 万点
|
// 用 /sample/batch 接口按 filePath 采样,最多 30 万点
|
||||||
// 支持多文件(各面板 PLY),合并采样
|
// 支持多文件(各面板 PLY),合并采样
|
||||||
const plyFiles: string[] = (overlay as { files?: string[]; file: string }).files
|
const plyFiles: string[] = Array.isArray(overlay.files) && overlay.files.length > 0
|
||||||
?? [overlay.file];
|
? overlay.files
|
||||||
|
: [overlay.file];
|
||||||
const resp = await fetch('/api/files/sample/batch', {
|
const resp = await fetch('/api/files/sample/batch', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -1801,7 +1815,10 @@ export default function PointCloudViewer({
|
|||||||
const buf = await resp.arrayBuffer();
|
const buf = await resp.arrayBuffer();
|
||||||
const floats = new Float32Array(buf);
|
const floats = new Float32Array(buf);
|
||||||
const count = Math.floor(floats.length / stride);
|
const count = Math.floor(floats.length / stride);
|
||||||
if (count === 0) return;
|
if (count === 0) {
|
||||||
|
setPlyOverlayStatus({ status: 'error', message: 'PLY 叠加层没有可渲染点' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const positions = new Float32Array(count * 3);
|
const positions = new Float32Array(count * 3);
|
||||||
const colors = new Float32Array(count * 3);
|
const colors = new Float32Array(count * 3);
|
||||||
if (hasColor) {
|
if (hasColor) {
|
||||||
@@ -1826,23 +1843,23 @@ export default function PointCloudViewer({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 用主点云的坐标变换对齐叠加层坐标
|
// 用主点云的坐标变换对齐叠加层坐标
|
||||||
const t = pcTransformRef.current;
|
for (let i = 0; i < count; i++) {
|
||||||
if (t) {
|
positions[i * 3] = (positions[i * 3] - t.cx) * t.scale;
|
||||||
for (let i = 0; i < count; i++) {
|
positions[i * 3 + 1] = (positions[i * 3 + 1] - t.cy) * t.scale;
|
||||||
positions[i * 3] = (positions[i * 3] - t.cx) * t.scale;
|
positions[i * 3 + 2] = (positions[i * 3 + 2] - t.cz) * 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 });
|
setPlyOverlayData({ positions, colors });
|
||||||
|
setPlyOverlayStatus({ status: 'ready' });
|
||||||
console.log(`[PlyOverlay] loaded ${count} pts, hasColor=${hasColor}, transform=${JSON.stringify(t)}`);
|
console.log(`[PlyOverlay] loaded ${count} pts, hasColor=${hasColor}, transform=${JSON.stringify(t)}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof DOMException && e.name === 'AbortError') return;
|
if (e instanceof DOMException && e.name === 'AbortError') return;
|
||||||
console.warn('[PlyOverlay] load failed:', e);
|
console.warn('[PlyOverlay] load failed:', e);
|
||||||
|
setPlyOverlayData(null);
|
||||||
|
setPlyOverlayStatus({ status: 'error', message: '加载 PLY 叠加层失败' });
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
return () => ac.abort();
|
return () => ac.abort();
|
||||||
}, [pluginOverlay]);
|
}, [pluginOverlay, pointCloudData]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 当插件叠加层激活时,跳过 legacy heatmap effect,避免与下面那个 effect 抢 ref
|
// 当插件叠加层激活时,跳过 legacy heatmap effect,避免与下面那个 effect 抢 ref
|
||||||
@@ -1947,16 +1964,21 @@ export default function PointCloudViewer({
|
|||||||
|
|
||||||
const heatmapLegendBounds = useMemo(() => {
|
const heatmapLegendBounds = useMemo(() => {
|
||||||
if (!showHeatmap) return null;
|
if (!showHeatmap) return null;
|
||||||
|
const hasVisibleCadHeatmap =
|
||||||
|
pointCloudRenderMode === 'smoothHeatmap' &&
|
||||||
|
serverCadHeatmap.status === 'ready' &&
|
||||||
|
showCADModel;
|
||||||
const pts = pointCloudData?.points;
|
const pts = pointCloudData?.points;
|
||||||
if (!pts?.length) return null;
|
if (!hasVisibleCadHeatmap && (!showPointCloud || !pts?.length)) return null;
|
||||||
if (pointCloudRenderMode === 'smoothHeatmap' && serverCadHeatmap.status === 'ready') {
|
if (pointCloudRenderMode === 'smoothHeatmap' && serverCadHeatmap.status === 'ready') {
|
||||||
const lo = serverCadHeatmap.dispMin;
|
const lo = serverCadHeatmap.dispMin;
|
||||||
const hi = serverCadHeatmap.dispMax;
|
const hi = serverCadHeatmap.dispMax;
|
||||||
const midTick = lo < 0 && hi > 0 ? '0' : ((lo + hi) / 2).toFixed(3);
|
const midTick = lo < 0 && hi > 0 ? '0' : ((lo + hi) / 2).toFixed(3);
|
||||||
return { min: lo, max: hi, midTick };
|
return { min: lo, max: hi, midTick };
|
||||||
}
|
}
|
||||||
|
if (!pts?.length) return null;
|
||||||
return getHeatmapLegendBounds(pts, pointCloudRenderMode);
|
return getHeatmapLegendBounds(pts, pointCloudRenderMode);
|
||||||
}, [showHeatmap, pointCloudData, pointCloudRenderMode, serverCadHeatmap]);
|
}, [showHeatmap, showPointCloud, showCADModel, pointCloudData, pointCloudRenderMode, serverCadHeatmap]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative ${className}`} style={{ background: 'oklch(0.08 0.006 240)' }}>
|
<div className={`relative ${className}`} style={{ background: 'oklch(0.08 0.006 240)' }}>
|
||||||
@@ -2010,6 +2032,29 @@ export default function PointCloudViewer({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{pluginOverlay?.overlay.kind === 'ply' && plyOverlayStatus.status !== 'idle' && plyOverlayStatus.status !== 'ready' && (
|
||||||
|
<div className="absolute inset-0 z-20 flex items-center justify-center pointer-events-none">
|
||||||
|
<div className="rounded-lg border border-border/60 bg-[oklch(0.1_0.01_240)]/90 px-5 py-3 shadow-lg backdrop-blur-sm flex items-center gap-3 max-w-sm">
|
||||||
|
{plyOverlayStatus.status === 'loading' && (
|
||||||
|
<svg className="animate-spin h-5 w-5 text-primary" viewBox="0 0 24 24" fill="none">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
<div className="text-sm">
|
||||||
|
<div className="text-foreground/90 font-medium">
|
||||||
|
{plyOverlayStatus.status === 'error' ? 'PLY 叠加层不可用' : '正在加载 PLY 叠加层…'}
|
||||||
|
</div>
|
||||||
|
{plyOverlayStatus.message && (
|
||||||
|
<div className="text-[10px] text-muted-foreground mt-0.5">
|
||||||
|
{plyOverlayStatus.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 热力图点击拾取信息 */}
|
{/* 热力图点击拾取信息 */}
|
||||||
{heatmapPick && (
|
{heatmapPick && (
|
||||||
<div className="absolute left-3 bottom-14 z-20 rounded-lg border border-border/60 bg-[oklch(0.1_0.01_240)]/90 px-4 py-2.5 shadow-lg backdrop-blur-sm max-w-[220px]">
|
<div className="absolute left-3 bottom-14 z-20 rounded-lg border border-border/60 bg-[oklch(0.1_0.01_240)]/90 px-4 py-2.5 shadow-lg backdrop-blur-sm max-w-[220px]">
|
||||||
@@ -2055,6 +2100,7 @@ export default function PointCloudViewer({
|
|||||||
pointCloudData={pointCloudData}
|
pointCloudData={pointCloudData}
|
||||||
pointSize={pointSize}
|
pointSize={pointSize}
|
||||||
showHeatmap={showHeatmap}
|
showHeatmap={showHeatmap}
|
||||||
|
showPointCloud={showPointCloud}
|
||||||
pointCloudRenderMode={pointCloudRenderMode}
|
pointCloudRenderMode={pointCloudRenderMode}
|
||||||
enablePointPicking={enableErrorPicking}
|
enablePointPicking={enableErrorPicking}
|
||||||
showCADModel={showCADModel}
|
showCADModel={showCADModel}
|
||||||
|
|||||||
@@ -616,20 +616,31 @@ export default function TaskManager({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<div className="flex items-center gap-1">
|
||||||
className="industrial-btn industrial-btn-primary w-full py-1 text-[10px] flex items-center justify-center gap-1"
|
<button
|
||||||
disabled={activeTaskId == null || selectedModelPcFileIds.size === 0 || linkPcFilesFromModel.isPending}
|
className="industrial-btn industrial-btn-primary flex-1 min-w-0 py-1 text-[10px] flex items-center justify-center gap-1"
|
||||||
onClick={() => {
|
disabled={activeTaskId == null || selectedModelPcFileIds.size === 0 || linkPcFilesFromModel.isPending}
|
||||||
if (activeTaskId == null || selectedModelPcFileIds.size === 0) return;
|
onClick={() => {
|
||||||
linkPcFilesFromModel.mutate({
|
if (activeTaskId == null || selectedModelPcFileIds.size === 0) return;
|
||||||
taskId: activeTaskId,
|
linkPcFilesFromModel.mutate({
|
||||||
modelPointcloudFileIds: Array.from(selectedModelPcFileIds),
|
taskId: activeTaskId,
|
||||||
});
|
modelPointcloudFileIds: Array.from(selectedModelPcFileIds),
|
||||||
}}
|
});
|
||||||
>
|
}}
|
||||||
<Plus className="w-3 h-3" />
|
>
|
||||||
添加到当前任务{selectedModelPcFileIds.size > 0 ? ` (${selectedModelPcFileIds.size})` : ''}
|
<Plus className="w-3 h-3" />
|
||||||
</button>
|
添加到当前任务{selectedModelPcFileIds.size > 0 ? ` (${selectedModelPcFileIds.size})` : ''}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="industrial-btn w-7 flex-shrink-0 px-0 py-1 text-[10px] flex items-center justify-center bg-red-900/55 border-red-500/60 text-red-100 hover:bg-red-800/70 hover:border-red-400"
|
||||||
|
title="关闭车型点云库"
|
||||||
|
aria-label="关闭车型点云库"
|
||||||
|
onClick={() => setPcLibraryOpen(false)}
|
||||||
|
>
|
||||||
|
<X className="w-3 h-3 flex-shrink-0" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -848,20 +859,31 @@ export default function TaskManager({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<div className="flex items-center gap-1">
|
||||||
className="industrial-btn industrial-btn-primary w-full py-1 text-[10px] flex items-center justify-center gap-1"
|
<button
|
||||||
disabled={activeTaskId == null || selectedModelCadFileIds.size === 0 || linkCadFilesFromModel.isPending}
|
className="industrial-btn industrial-btn-primary flex-1 min-w-0 py-1 text-[10px] flex items-center justify-center gap-1"
|
||||||
onClick={() => {
|
disabled={activeTaskId == null || selectedModelCadFileIds.size === 0 || linkCadFilesFromModel.isPending}
|
||||||
if (activeTaskId == null || selectedModelCadFileIds.size === 0) return;
|
onClick={() => {
|
||||||
linkCadFilesFromModel.mutate({
|
if (activeTaskId == null || selectedModelCadFileIds.size === 0) return;
|
||||||
taskId: activeTaskId,
|
linkCadFilesFromModel.mutate({
|
||||||
modelCadFileIds: Array.from(selectedModelCadFileIds),
|
taskId: activeTaskId,
|
||||||
});
|
modelCadFileIds: Array.from(selectedModelCadFileIds),
|
||||||
}}
|
});
|
||||||
>
|
}}
|
||||||
<Plus className="w-3 h-3" />
|
>
|
||||||
添加到当前任务{selectedModelCadFileIds.size > 0 ? ` (${selectedModelCadFileIds.size})` : ''}
|
<Plus className="w-3 h-3" />
|
||||||
</button>
|
添加到当前任务{selectedModelCadFileIds.size > 0 ? ` (${selectedModelCadFileIds.size})` : ''}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="industrial-btn w-7 flex-shrink-0 px-0 py-1 text-[10px] flex items-center justify-center bg-red-900/55 border-red-500/60 text-red-100 hover:bg-red-800/70 hover:border-red-400"
|
||||||
|
title="关闭车型 CAD 库"
|
||||||
|
aria-label="关闭车型 CAD 库"
|
||||||
|
onClick={() => setCadLibraryOpen(false)}
|
||||||
|
>
|
||||||
|
<X className="w-3 h-3 flex-shrink-0" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -10,20 +10,19 @@ import type { Overlay3D } from '@/components/GenericResultView';
|
|||||||
import { trpc } from '@/lib/trpc';
|
import { trpc } from '@/lib/trpc';
|
||||||
import {
|
import {
|
||||||
LogIn,
|
LogIn,
|
||||||
LogOut,
|
|
||||||
Box,
|
Box,
|
||||||
|
Cloud,
|
||||||
Eye,
|
Eye,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
Settings,
|
Settings,
|
||||||
Maximize2,
|
Maximize2,
|
||||||
Crosshair,
|
|
||||||
Train,
|
Train,
|
||||||
Loader2,
|
Loader2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { user, loading, isAuthenticated, logout } = useAuth();
|
const { loading, isAuthenticated } = useAuth();
|
||||||
const utils = trpc.useUtils();
|
const utils = trpc.useUtils();
|
||||||
|
|
||||||
// ── 树导航选中状态 ─────────────────────────────────────────────────────────
|
// ── 树导航选中状态 ─────────────────────────────────────────────────────────
|
||||||
@@ -69,7 +68,8 @@ export default function Home() {
|
|||||||
|
|
||||||
// ── 视图控制 ───────────────────────────────────────────────────────────────
|
// ── 视图控制 ───────────────────────────────────────────────────────────────
|
||||||
const [showHeatmap, setShowHeatmap] = useState(true);
|
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 [showWireframe, setShowWireframe] = useState(false);
|
||||||
const [pointSize, setPointSize] = useState(0.03);
|
const [pointSize, setPointSize] = useState(0.03);
|
||||||
/** 点云预览采样预算(非全量);调低可明显减轻卡顿 */
|
/** 点云预览采样预算(非全量);调低可明显减轻卡顿 */
|
||||||
@@ -88,7 +88,7 @@ export default function Home() {
|
|||||||
resultId: number;
|
resultId: number;
|
||||||
overlay: Overlay3D;
|
overlay: Overlay3D;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [errorPickEnabled, setErrorPickEnabled] = useState(false);
|
const [errorPickEnabled] = useState(false);
|
||||||
const [showLeftPanel, setShowLeftPanel] = useState(true);
|
const [showLeftPanel, setShowLeftPanel] = useState(true);
|
||||||
const [showRightPanel, setShowRightPanel] = useState(true);
|
const [showRightPanel, setShowRightPanel] = useState(true);
|
||||||
|
|
||||||
@@ -307,12 +307,47 @@ export default function Home() {
|
|||||||
setHeatmapViewResultId(resultId);
|
setHeatmapViewResultId(resultId);
|
||||||
setPointCloudRenderMode('smoothHeatmap');
|
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);
|
setHeatmapViewResultId(null);
|
||||||
setPluginOverlay({ taskId, resultId, overlay });
|
setPluginOverlay({ taskId, resultId, overlay });
|
||||||
if (overlay.kind === 'vertexColors') {
|
setPointCloudRenderMode(overlay.kind === 'vertexColors' ? 'smoothHeatmap' : 'points');
|
||||||
setPointCloudRenderMode('smoothHeatmap');
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
externalPending={runAnalysisJob.isPending}
|
externalPending={runAnalysisJob.isPending}
|
||||||
onRunAssembly={() => {
|
onRunAssembly={() => {
|
||||||
@@ -374,33 +409,22 @@ export default function Home() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 px-3 py-1 industrial-card">
|
{(pcLoading || cadLoading) && (
|
||||||
<span className={`status-led ${
|
<div className="flex items-center gap-2 px-3 py-1 industrial-card">
|
||||||
pcLoading ? 'status-led-processing' :
|
<span className="status-led status-led-processing" />
|
||||||
cadLoading ? 'status-led-processing' :
|
<span className="text-xs text-muted-foreground">
|
||||||
'status-led-idle'
|
{pcLoading ? `加载点云 ${pcProgress}%` : `加载 CAD ${cadLoadProgress}%`}
|
||||||
}`} />
|
</span>
|
||||||
<span className="text-xs text-muted-foreground">
|
</div>
|
||||||
{pcLoading ? `加载点云 ${pcProgress}%` :
|
)}
|
||||||
cadLoading ? `加载 CAD ${cadLoadProgress}%` :
|
|
||||||
'待机'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="w-7 h-7 rounded bg-secondary animate-pulse" />
|
<div className="w-7 h-7 rounded bg-secondary animate-pulse" />
|
||||||
) : isAuthenticated ? (
|
) : !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>
|
|
||||||
) : (
|
|
||||||
<a href={getLoginUrl()} className="industrial-btn industrial-btn-primary py-1.5">
|
<a href={getLoginUrl()} className="industrial-btn industrial-btn-primary py-1.5">
|
||||||
<LogIn className="w-3.5 h-3.5" />
|
<LogIn className="w-3.5 h-3.5" />
|
||||||
</a>
|
</a>
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -439,6 +463,7 @@ export default function Home() {
|
|||||||
pointCloudUrls={pointCloudUrlsStable}
|
pointCloudUrls={pointCloudUrlsStable}
|
||||||
batchTaskId={batchTaskId ?? undefined}
|
batchTaskId={batchTaskId ?? undefined}
|
||||||
loadKey={pcLoadKey}
|
loadKey={pcLoadKey}
|
||||||
|
showPointCloud={showPointCloud}
|
||||||
selectedPcFileIds={selectedPcFileIds.length > 0 ? selectedPcFileIds : undefined}
|
selectedPcFileIds={selectedPcFileIds.length > 0 ? selectedPcFileIds : undefined}
|
||||||
onCadProgress={handleCadProgress}
|
onCadProgress={handleCadProgress}
|
||||||
onCadLoaded={handleCadLoaded}
|
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">
|
<div className="absolute bottom-3 left-3 info-overlay flex items-center gap-4 text-[10px] flex-wrap">
|
||||||
{errorPickEnabled && pointCloudRenderMode === 'smoothHeatmap' && (
|
<span>左键旋转</span>
|
||||||
<span className="text-amber-400/90">平滑模式不支持选点,请切回「点云」</span>
|
<span>右键平移</span>
|
||||||
)}
|
<span>滚轮缩放</span>
|
||||||
{errorPickEnabled && pointCloudRenderMode === 'points'
|
|
||||||
? <><span>左键点击选点误差</span><span>右键平移</span><span>滚轮缩放</span></>
|
|
||||||
: <><span>左键旋转</span><span>右键平移</span><span>滚轮缩放</span></>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 选中点信息 */}
|
{/* 选中点信息 */}
|
||||||
@@ -522,15 +543,21 @@ export default function Home() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
type="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={() => {
|
onClick={() => {
|
||||||
setPointCloudRenderMode('points');
|
if (pointCloudRenderMode !== 'points') {
|
||||||
setHeatmapViewResultId(null);
|
setPointCloudRenderMode('points');
|
||||||
setPluginOverlay(null);
|
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>
|
</button>
|
||||||
{heatmapViewResultId != null && pointCloudRenderMode === 'smoothHeatmap' && (
|
{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">
|
<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>
|
</span>
|
||||||
)}
|
)}
|
||||||
<button
|
<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)}
|
onClick={() => setShowHeatmap(v => !v)}
|
||||||
>
|
>
|
||||||
{showHeatmap ? <Eye className="w-3.5 h-3.5" /> : <EyeOff className="w-3.5 h-3.5" />}
|
{showHeatmap ? <Eye className="w-3.5 h-3.5" /> : <EyeOff className="w-3.5 h-3.5" />}
|
||||||
<span className="ml-1">热力图</span>
|
<span>热力图</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>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -584,7 +597,7 @@ export default function Home() {
|
|||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min={100_000}
|
min={100_000}
|
||||||
max={5_000_000}
|
max={2_000_000}
|
||||||
step={100_000}
|
step={100_000}
|
||||||
value={renderPointBudget}
|
value={renderPointBudget}
|
||||||
onChange={e => setRenderPointBudget(Number(e.target.value))}
|
onChange={e => setRenderPointBudget(Number(e.target.value))}
|
||||||
|
|||||||
@@ -122,6 +122,8 @@ async function writeInputJson(
|
|||||||
params: Record<string, unknown>,
|
params: Record<string, unknown>,
|
||||||
pointCloudPaths: string[],
|
pointCloudPaths: string[],
|
||||||
cadPath: string | null,
|
cadPath: string | null,
|
||||||
|
inputPointCloudFileIds: number[],
|
||||||
|
inputCadFileIds: number[],
|
||||||
) {
|
) {
|
||||||
const inputJson = {
|
const inputJson = {
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
@@ -133,7 +135,9 @@ async function writeInputJson(
|
|||||||
outDir: workDir,
|
outDir: workDir,
|
||||||
inputs: {
|
inputs: {
|
||||||
pointCloudPaths,
|
pointCloudPaths,
|
||||||
|
pointCloudFileIds: inputPointCloudFileIds,
|
||||||
cadPath,
|
cadPath,
|
||||||
|
cadFileIds: inputCadFileIds,
|
||||||
config: params,
|
config: params,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -331,6 +335,9 @@ export async function runAlgorithmJob(
|
|||||||
pointCloudFileIds,
|
pointCloudFileIds,
|
||||||
cadFileIds,
|
cadFileIds,
|
||||||
);
|
);
|
||||||
|
const inputPointCloudFileIds = pointCloudRecords.map((r) => r.id);
|
||||||
|
const inputCadFileIds = Array.isArray(cadFileIds) ? cadFileIds.filter((id) => Number.isInteger(id)) : [];
|
||||||
|
|
||||||
const inputJsonPath = await writeInputJson(
|
const inputJsonPath = await writeInputJson(
|
||||||
workDir,
|
workDir,
|
||||||
taskId,
|
taskId,
|
||||||
@@ -339,6 +346,8 @@ export async function runAlgorithmJob(
|
|||||||
params,
|
params,
|
||||||
pointCloudPaths,
|
pointCloudPaths,
|
||||||
cadPath,
|
cadPath,
|
||||||
|
inputPointCloudFileIds,
|
||||||
|
inputCadFileIds,
|
||||||
);
|
);
|
||||||
|
|
||||||
let exitCode: number | null = 0;
|
let exitCode: number | null = 0;
|
||||||
@@ -363,6 +372,8 @@ export async function runAlgorithmJob(
|
|||||||
progress: finalStatus === 'completed' ? 1 : undefined,
|
progress: finalStatus === 'completed' ? 1 : undefined,
|
||||||
stage: finalStatus === 'completed' ? '完成' : '失败',
|
stage: finalStatus === 'completed' ? '完成' : '失败',
|
||||||
exitCode,
|
exitCode,
|
||||||
|
inputPointCloudFileIds,
|
||||||
|
inputCadFileIds,
|
||||||
// 把 result.json 内容平展到 resultData 里,方便前端直接使用
|
// 把 result.json 内容平展到 resultData 里,方便前端直接使用
|
||||||
schemaVersion: result?.schemaVersion ?? manifest.schemaVersion,
|
schemaVersion: result?.schemaVersion ?? manifest.schemaVersion,
|
||||||
message: result?.message,
|
message: result?.message,
|
||||||
|
|||||||
@@ -394,7 +394,7 @@ def main() -> int:
|
|||||||
overlay3d: dict = {"kind": "none"}
|
overlay3d: dict = {"kind": "none"}
|
||||||
for ply_key in ("contourPly", "curvaturePly"):
|
for ply_key in ("contourPly", "curvaturePly"):
|
||||||
if ply_key in extra:
|
if ply_key in extra:
|
||||||
overlay3d = {"kind": "vertexColors", "url": extra[ply_key]}
|
overlay3d = {"kind": "ply", "file": extra[ply_key]}
|
||||||
break
|
break
|
||||||
|
|
||||||
if skipped:
|
if skipped:
|
||||||
|
|||||||
@@ -141,6 +141,8 @@ export type PluginOverlay3D =
|
|||||||
/** 着色 PLY 叠加文件(同样相对 outDir) */
|
/** 着色 PLY 叠加文件(同样相对 outDir) */
|
||||||
kind: 'ply';
|
kind: 'ply';
|
||||||
file: string;
|
file: string;
|
||||||
|
/** 批量结果可包含多个 PLY 叠加文件 */
|
||||||
|
files?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 算法写出的 result.json(与 INTEGRATION.md 描述对齐) */
|
/** 算法写出的 result.json(与 INTEGRATION.md 描述对齐) */
|
||||||
@@ -177,4 +179,8 @@ export interface PluginResultData extends Partial<PluginResult> {
|
|||||||
error?: string;
|
error?: string;
|
||||||
/** 子进程已结束时的退出码 */
|
/** 子进程已结束时的退出码 */
|
||||||
exitCode?: number | null;
|
exitCode?: number | null;
|
||||||
|
/** 本次算法运行实际使用的任务点云文件 ID,用于前端叠加层匹配校验 */
|
||||||
|
inputPointCloudFileIds?: number[];
|
||||||
|
/** 本次算法运行前端选择的任务 CAD 文件 ID,用于后续匹配/追溯 */
|
||||||
|
inputCadFileIds?: number[];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user