2026-05-24-17-29-17 增加构件加载进度并修正滚轮事件
This commit is contained in:
@@ -2154,11 +2154,13 @@ export function VoxelizationMappingView({
|
||||
}) {
|
||||
const baseCanvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const overlayCanvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const mappingViewportRef = useRef<HTMLDivElement | null>(null);
|
||||
const [dicomPreview, setDicomPreview] = useState<DicomPreview | null>(null);
|
||||
const [modelPreviews, setModelPreviews] = useState<Record<string, ModelPreviewPayload>>({});
|
||||
const [dicomStatus, setDicomStatus] = useState('等待 DICOM 切片');
|
||||
const [overlayStatus, setOverlayStatus] = useState('等待 STL 映射');
|
||||
const [overlayStats, setOverlayStats] = useState<OverlayStats>({ activeModules: 0, filledPixels: 0, segmentCount: 0, modules: [] });
|
||||
const [overlayLoadState, setOverlayLoadState] = useState({ loading: false, loaded: 0, total: 0, phase: '' });
|
||||
const [mappingViewport, setMappingViewport] = useState({ scale: 1, offsetX: 0, offsetY: 0 });
|
||||
const mappingPanRef = useRef({
|
||||
active: false,
|
||||
@@ -2210,15 +2212,38 @@ export function VoxelizationMappingView({
|
||||
useEffect(() => {
|
||||
if (!project || !visibleStlFiles.length) {
|
||||
setModelPreviews({});
|
||||
setOverlayStats({ activeModules: 0, filledPixels: 0, segmentCount: 0, modules: [] });
|
||||
setOverlayLoadState({ loading: false, loaded: 0, total: 0, phase: '' });
|
||||
setOverlayStatus(stlFiles.length ? '当前没有可见 STL 构件' : '当前项目没有 STL 构件');
|
||||
return;
|
||||
}
|
||||
|
||||
let disposed = false;
|
||||
let loaded = 0;
|
||||
const total = visibleStlFiles.length;
|
||||
const previewLimit = Math.max(detailLimit, 500000);
|
||||
const updateLoadProgress = (phase: string) => {
|
||||
if (!disposed) {
|
||||
setOverlayLoadState({ loading: true, loaded, total, phase });
|
||||
}
|
||||
};
|
||||
|
||||
setModelPreviews({});
|
||||
setOverlayStats({ activeModules: 0, filledPixels: 0, segmentCount: 0, modules: [] });
|
||||
setOverlayLoadState({ loading: true, loaded: 0, total, phase: '正在载入可见构件' });
|
||||
setOverlayStatus('正在载入可见 STL 构件层级...');
|
||||
Promise.allSettled(visibleStlFiles.map((fileName) => (
|
||||
getCachedModelPreview(project.id, fileName, Math.max(detailLimit, 500000))
|
||||
.then((payload) => ({ fileName, payload }))
|
||||
getCachedModelPreview(project.id, fileName, previewLimit)
|
||||
.then((payload) => {
|
||||
loaded += 1;
|
||||
updateLoadProgress(fileName.replace(/\.stl$/i, ''));
|
||||
return { fileName, payload };
|
||||
})
|
||||
.catch((error) => {
|
||||
loaded += 1;
|
||||
updateLoadProgress(`${fileName.replace(/\.stl$/i, '')} 载入失败`);
|
||||
throw error;
|
||||
})
|
||||
))).then((results) => {
|
||||
if (disposed) return;
|
||||
const nextPreviews: Record<string, ModelPreviewPayload> = {};
|
||||
@@ -2228,6 +2253,7 @@ export function VoxelizationMappingView({
|
||||
}
|
||||
});
|
||||
setModelPreviews(nextPreviews);
|
||||
setOverlayLoadState({ loading: false, loaded: total, total, phase: '' });
|
||||
setOverlayStatus(Object.keys(nextPreviews).length ? 'Overlay Label Map 已就绪' : 'Overlay Layer 无可用 STL 数据');
|
||||
});
|
||||
|
||||
@@ -2283,6 +2309,27 @@ export function VoxelizationMappingView({
|
||||
totalSlices,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const element = mappingViewportRef.current;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleWheel = (event: WheelEvent) => {
|
||||
event.preventDefault();
|
||||
const scaleFactor = event.deltaY > 0 ? 0.9 : 1.1;
|
||||
setMappingViewport((current) => ({
|
||||
...current,
|
||||
scale: clamp(current.scale * scaleFactor, 0.45, 6),
|
||||
}));
|
||||
};
|
||||
|
||||
element.addEventListener('wheel', handleWheel, { passive: false });
|
||||
return () => {
|
||||
element.removeEventListener('wheel', handleWheel);
|
||||
};
|
||||
}, [isLibraryVariant]);
|
||||
|
||||
const stepSlice = (delta: number) => {
|
||||
onSliceChange(clamp(safeSlice + delta, 0, maxSlice));
|
||||
};
|
||||
@@ -2291,14 +2338,6 @@ export function VoxelizationMappingView({
|
||||
const resetMappingViewport = () => {
|
||||
setMappingViewport({ scale: 1, offsetX: 0, offsetY: 0 });
|
||||
};
|
||||
const handleMappingWheel = (event: React.WheelEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
const scaleFactor = event.deltaY > 0 ? 0.9 : 1.1;
|
||||
setMappingViewport((current) => ({
|
||||
...current,
|
||||
scale: clamp(current.scale * scaleFactor, 0.45, 6),
|
||||
}));
|
||||
};
|
||||
const handleMappingPointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
@@ -2334,6 +2373,40 @@ export function VoxelizationMappingView({
|
||||
event.currentTarget.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
};
|
||||
const renderOverlayLoadProgress = (tone: 'dark' | 'light') => {
|
||||
if (!overlayLoadState.loading || !overlayLoadState.total) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const percent = Math.round((overlayLoadState.loaded / overlayLoadState.total) * 100);
|
||||
const isDark = tone === 'dark';
|
||||
return (
|
||||
<div className={`pointer-events-none absolute left-4 top-4 z-20 w-64 rounded-xl border px-3 py-2 shadow-lg backdrop-blur-md ${
|
||||
isDark
|
||||
? 'border-white/10 bg-black/70 text-white'
|
||||
: 'border-slate-200 bg-white/90 text-slate-700'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between gap-3 text-[10px] font-bold">
|
||||
<span className={`min-w-0 truncate ${isDark ? 'text-cyan-100' : 'text-cyan-700'}`}>
|
||||
构件层级加载中
|
||||
</span>
|
||||
<span className={`font-mono ${isDark ? 'text-white/70' : 'text-slate-500'}`}>
|
||||
{overlayLoadState.loaded}/{overlayLoadState.total}
|
||||
</span>
|
||||
</div>
|
||||
<div className={`mt-2 h-1.5 overflow-hidden rounded-full ${isDark ? 'bg-white/10' : 'bg-slate-200'}`}>
|
||||
<div
|
||||
className="h-full rounded-full bg-cyan-400 transition-[width] duration-200"
|
||||
style={{ width: `${percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className={`mt-1 flex items-center justify-between gap-3 text-[9px] font-bold ${isDark ? 'text-white/45' : 'text-slate-400'}`}>
|
||||
<span className="min-w-0 truncate">{overlayLoadState.phase || overlayStatus}</span>
|
||||
<span className="font-mono">{percent}%</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const renderOverlaySummary = (placement: 'bottom' | 'side') => (
|
||||
<div className={`${placement === 'side' ? 'w-full rounded-2xl border border-white/10 bg-black/40 p-2' : 'border-t border-white/10 bg-[#030712] px-4 py-3'}`}>
|
||||
<div className={`mb-2 flex gap-2 text-[10px] font-bold text-white/60 ${placement === 'side' ? 'flex-col' : 'items-center justify-between'}`}>
|
||||
@@ -2389,8 +2462,8 @@ export function VoxelizationMappingView({
|
||||
className="flex min-h-0 flex-col"
|
||||
>
|
||||
<div
|
||||
className={`relative min-h-0 flex-1 overflow-hidden bg-black ${mappingPanRef.current.active ? 'cursor-grabbing' : 'cursor-grab'}`}
|
||||
onWheel={handleMappingWheel}
|
||||
ref={mappingViewportRef}
|
||||
className={`relative min-h-0 flex-1 touch-none overflow-hidden bg-black ${mappingPanRef.current.active ? 'cursor-grabbing' : 'cursor-grab'}`}
|
||||
onPointerDown={handleMappingPointerDown}
|
||||
onPointerMove={handleMappingPointerMove}
|
||||
onPointerUp={stopMappingPointerDrag}
|
||||
@@ -2412,6 +2485,7 @@ export function VoxelizationMappingView({
|
||||
{dicomStatus}
|
||||
</div>
|
||||
)}
|
||||
{renderOverlayLoadProgress('dark')}
|
||||
<div className="pointer-events-none absolute bottom-4 right-4 rounded-xl border border-white/10 bg-black/70 px-3 py-2 text-right shadow-lg">
|
||||
<p className="text-[9px] font-bold text-white/45">DICOM 切片位置</p>
|
||||
<p className="mt-1 font-mono text-[12px] font-bold text-cyan-100">
|
||||
@@ -2469,8 +2543,8 @@ export function VoxelizationMappingView({
|
||||
<div className="grid min-h-0 flex-1 grid-cols-[minmax(0,1fr)_110px]">
|
||||
<div className="flex min-h-0 flex-col">
|
||||
<div
|
||||
className={`relative min-h-0 flex-1 overflow-hidden bg-black ${mappingPanRef.current.active ? 'cursor-grabbing' : 'cursor-grab'}`}
|
||||
onWheel={handleMappingWheel}
|
||||
ref={mappingViewportRef}
|
||||
className={`relative min-h-0 flex-1 touch-none overflow-hidden bg-black ${mappingPanRef.current.active ? 'cursor-grabbing' : 'cursor-grab'}`}
|
||||
onPointerDown={handleMappingPointerDown}
|
||||
onPointerMove={handleMappingPointerMove}
|
||||
onPointerUp={stopMappingPointerDrag}
|
||||
@@ -2492,6 +2566,7 @@ export function VoxelizationMappingView({
|
||||
{dicomStatus}
|
||||
</div>
|
||||
)}
|
||||
{renderOverlayLoadProgress('dark')}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-slate-100 bg-white px-4 py-3">
|
||||
@@ -3910,19 +3985,14 @@ export default function ReverseWorkspace({
|
||||
<div key={item.key} className="grid grid-cols-[44px_28px_1fr_28px_72px] items-center gap-2 text-[10px] font-bold text-slate-500">
|
||||
<span>{item.label}</span>
|
||||
<button
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
onPointerDown={() => {
|
||||
startPoseRepeat(item.key, -poseStepConfig[item.key].step);
|
||||
}}
|
||||
onMouseUp={stopPoseRepeat}
|
||||
onMouseLeave={stopPoseRepeat}
|
||||
onTouchStart={(event) => {
|
||||
event.preventDefault();
|
||||
startPoseRepeat(item.key, -poseStepConfig[item.key].step);
|
||||
}}
|
||||
onTouchEnd={stopPoseRepeat}
|
||||
onPointerUp={stopPoseRepeat}
|
||||
onPointerLeave={stopPoseRepeat}
|
||||
onPointerCancel={stopPoseRepeat}
|
||||
onClick={() => nudgeModelPose(item.key, -poseStepConfig[item.key].step)}
|
||||
className="h-6 rounded-md bg-white text-[9px] font-bold text-slate-500 shadow-sm border border-slate-100 hover:text-blue-600 hover:bg-blue-50"
|
||||
className="h-6 touch-none rounded-md border border-slate-100 bg-white text-[9px] font-bold text-slate-500 shadow-sm hover:bg-blue-50 hover:text-blue-600"
|
||||
title={`单击移动最低刻度,长按连续移动。${poseStepConfig[item.key].minus}`}
|
||||
>
|
||||
-
|
||||
@@ -3937,19 +4007,14 @@ export default function ReverseWorkspace({
|
||||
className="accent-blue-600"
|
||||
/>
|
||||
<button
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
onPointerDown={() => {
|
||||
startPoseRepeat(item.key, poseStepConfig[item.key].step);
|
||||
}}
|
||||
onMouseUp={stopPoseRepeat}
|
||||
onMouseLeave={stopPoseRepeat}
|
||||
onTouchStart={(event) => {
|
||||
event.preventDefault();
|
||||
startPoseRepeat(item.key, poseStepConfig[item.key].step);
|
||||
}}
|
||||
onTouchEnd={stopPoseRepeat}
|
||||
onPointerUp={stopPoseRepeat}
|
||||
onPointerLeave={stopPoseRepeat}
|
||||
onPointerCancel={stopPoseRepeat}
|
||||
onClick={() => nudgeModelPose(item.key, poseStepConfig[item.key].step)}
|
||||
className="h-6 rounded-md bg-white text-[9px] font-bold text-slate-500 shadow-sm border border-slate-100 hover:text-blue-600 hover:bg-blue-50"
|
||||
className="h-6 touch-none rounded-md border border-slate-100 bg-white text-[9px] font-bold text-slate-500 shadow-sm hover:bg-blue-50 hover:text-blue-600"
|
||||
title={`单击移动最低刻度,长按连续移动。${poseStepConfig[item.key].plus}`}
|
||||
>
|
||||
+
|
||||
|
||||
Reference in New Issue
Block a user