From 6c9787803c599db77051a0a124adfcb2c75bdde6 Mon Sep 17 00:00:00 2001 From: admin <572701190@qq.com> Date: Wed, 20 May 2026 16:08:07 +0800 Subject: [PATCH] =?UTF-8?q?2026-05-20-15-54-46=20=E9=80=86=E5=90=91?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E5=8C=BA=E8=A7=86=E5=9B=BE=E4=BA=A4=E4=BA=92?= =?UTF-8?q?=E4=B8=8E=E5=B8=83=E5=B1=80=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebSite/src/components/ReverseWorkspace.tsx | 362 ++++++++++++++++---- WebSite/src/index.css | 12 +- 工程分析/实现方案-2026-05-20-15-54-46.md | 65 ++++ 工程分析/测试方案-2026-05-20-15-54-46.md | 64 ++++ 工程分析/经验记录.md | 18 + 工程分析/需求分析-2026-05-20-15-54-46.md | 64 ++++ 6 files changed, 514 insertions(+), 71 deletions(-) create mode 100644 工程分析/实现方案-2026-05-20-15-54-46.md create mode 100644 工程分析/测试方案-2026-05-20-15-54-46.md create mode 100644 工程分析/需求分析-2026-05-20-15-54-46.md 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({
-