diff --git a/WebSite/src/components/ReverseWorkspace.tsx b/WebSite/src/components/ReverseWorkspace.tsx index 690bbfc..ac599f7 100644 --- a/WebSite/src/components/ReverseWorkspace.tsx +++ b/WebSite/src/components/ReverseWorkspace.tsx @@ -10,6 +10,8 @@ import { ChevronUp, Eye, Layers, + Maximize2, + RefreshCcw, Save, Upload, } from 'lucide-react'; @@ -70,7 +72,7 @@ const poseStepConfig: Record { + [bounds.min.y, bounds.max.y].forEach((y) => { + [bounds.min.z, bounds.max.z].forEach((z) => { + const point = new THREE.Vector3(x, y, z).sub(center).applyMatrix4(rotationMatrix); + rotatedBox.expandByPoint(point); + }); + }); + }); + return rotatedBox.getSize(new THREE.Vector3()); +} + function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } @@ -424,6 +456,7 @@ export function FusionThreeView({ const [loadProgress, setLoadProgress] = useState(0); const [axisProjection, setAxisProjection] = useState(defaultAxisProjection); const axisProjectionSignatureRef = useRef(axisProjectionSignature(defaultAxisProjection)); + const resetFusionViewRef = useRef<() => void>(() => undefined); useEffect(() => { modelPoseRef.current = modelPose; @@ -615,12 +648,12 @@ export function FusionThreeView({ modelPivot.add(modelBounds); modelBaseScale = (Math.max(dicomWidth, dicomHeight, dicomDepth) / maxModelSize) * 0.92; modelPoseGroup.position.set(0, 0, 0); - modelPivot.position.set(0, 0, dicomDepth * 0.08); + modelPivot.position.set(0, 0, 0); setLoadProgress(100); setStatus(visibleStlFiles.length ? '三维融合场景已就绪' : 'DICOM 三维体已就绪,当前没有显示的 STL 构件'); }); - const rootPose = { + const defaultRootPose = { rotateX: THREE.MathUtils.degToRad(58), rotateY: 0, rotateZ: THREE.MathUtils.degToRad(-18), @@ -628,6 +661,7 @@ export function FusionThreeView({ translateY: 0, scale: 1, }; + const rootPose = { ...defaultRootPose }; const dragState = { active: false, mode: 'rotate' as 'rotate' | 'pan', @@ -636,6 +670,10 @@ export function FusionThreeView({ startY: 0, root: { ...rootPose }, }; + resetFusionViewRef.current = () => { + Object.assign(rootPose, defaultRootPose); + setStatus('三维融合视角已复位'); + }; const handlePointerDown = (event: PointerEvent) => { dragState.active = true; @@ -749,6 +787,7 @@ export function FusionThreeView({ } }); renderer.dispose(); + resetFusionViewRef.current = () => undefined; container.innerHTML = ''; }; }, [ @@ -776,6 +815,14 @@ export function FusionThreeView({
DICOM {volume ? `${volume.start + 1}-${volume.end + 1}/${volume.total}` : '加载中'} · STL {project.modelCount ?? 0}
+ {loadProgress < 100 && (
@@ -952,7 +999,7 @@ function CutSectionPreview({ }); modelBaseScale = (Math.max(dicomWidth, dicomHeight, dicomDepth) / maxModelSize) * 0.98; modelPoseGroup.position.set(0, 0, 0); - modelPivot.position.set(0, 0, dicomDepth * 0.08); + modelPivot.position.set(0, 0, 0); }); const rootPose = { @@ -1765,6 +1812,15 @@ export function VoxelizationMappingView({ const [dicomStatus, setDicomStatus] = useState('等待 DICOM 切片'); const [overlayStatus, setOverlayStatus] = useState('等待 STL 映射'); const [overlayStats, setOverlayStats] = useState({ activeModules: 0, filledPixels: 0, segmentCount: 0, modules: [] }); + const [mappingViewport, setMappingViewport] = useState({ scale: 1, offsetX: 0, offsetY: 0 }); + const mappingPanRef = useRef({ + active: false, + pointerId: 0, + startX: 0, + startY: 0, + offsetX: 0, + offsetY: 0, + }); const maxSlice = Math.max(totalSlices - 1, 0); const safeSlice = clamp(slice, 0, maxSlice); const stlFiles = project?.stlFiles ?? []; @@ -1879,30 +1935,94 @@ export function VoxelizationMappingView({ onSliceChange(clamp(safeSlice + delta, 0, maxSlice)); }; const slicePercent = maxSlice > 0 ? (safeSlice / maxSlice) * 100 : 0; + const resetMappingViewport = () => { + setMappingViewport({ scale: 1, offsetX: 0, offsetY: 0 }); + }; + const handleMappingWheel = (event: React.WheelEvent) => { + 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) => { + if (event.button !== 0) { + return; + } + mappingPanRef.current = { + active: true, + pointerId: event.pointerId, + startX: event.clientX, + startY: event.clientY, + offsetX: mappingViewport.offsetX, + offsetY: mappingViewport.offsetY, + }; + event.currentTarget.setPointerCapture(event.pointerId); + }; + const handleMappingPointerMove = (event: React.PointerEvent) => { + const dragState = mappingPanRef.current; + if (!dragState.active || dragState.pointerId !== event.pointerId) { + return; + } + setMappingViewport((current) => ({ + ...current, + offsetX: dragState.offsetX + event.clientX - dragState.startX, + offsetY: dragState.offsetY + event.clientY - dragState.startY, + })); + }; + const stopMappingPointerDrag = (event: React.PointerEvent) => { + const dragState = mappingPanRef.current; + if (!dragState.active || dragState.pointerId !== event.pointerId) { + return; + } + mappingPanRef.current = { ...dragState, active: false }; + if (event.currentTarget.hasPointerCapture(event.pointerId)) { + event.currentTarget.releasePointerCapture(event.pointerId); + } + }; return ( -
-
-
- +
+
+
+ Base DICOM - + Overlay Label Map + + Z {safeSlice + 1}/{Math.max(totalSlices, 1)} +
-
- Z {safeSlice + 1}/{Math.max(totalSlices, 1)} -
+
-
+
-
+
{dicomPreview ? (
@@ -1914,10 +2034,10 @@ export function VoxelizationMappingView({ )}
-
-
- {overlayStatus} - +
+
+ Overlay Label Map · {overlayStatus} + {overlayStats.activeModules}/{visibleModuleCount} 构件 · {overlayStats.segmentCount} 边 · {overlayStats.filledPixels} px
@@ -1925,17 +2045,17 @@ export function VoxelizationMappingView({ {overlayStats.modules.length ? (
{overlayStats.modules.map((item) => ( -
- +
+ {item.name} - ID {item.partId} - {item.segmentCount} 边 - {item.filledPixels} px + ID {item.partId} + {item.segmentCount} 边 + {item.filledPixels} px
))}
) : ( -
+
当前切片暂无可见构件
)} @@ -1943,25 +2063,25 @@ export function VoxelizationMappingView({
-