diff --git a/Docker部署/README.md b/Docker部署/README.md index 9782c7f..46ef0b7 100644 --- a/Docker部署/README.md +++ b/Docker部署/README.md @@ -19,7 +19,7 @@ - 项目库与工作区的 DICOM 切片编号按医学影像顺序显示,滑条使用非进度条样式。 - 项目库支持锁定/解锁项目、筛选未上锁项目,并在锁定时保存位姿快照到 `项目数据/锁定结果/`。 - 逆向工作区“构件层级”支持一键显示或隐藏全部构件;切片滑条顶部为第 1 张,向下查看到第 N 张。 -- 逆向分割映射视图按当前可见构件加载高精度 STL 预览;实体模式最高使用 80 万三角面预览,“可见类别 + 构件分别导出”严格只导出当前眼睛打开的构件。 +- 逆向分割映射视图按当前可见构件加载高精度 STL 预览,并始终用全部 STL 边界保持统一模型坐标系;实体模式最高使用 80 万三角面预览,“可见类别 + 构件分别导出”严格只导出当前眼睛打开的构件。 ## 一、本机部署 diff --git a/WebSite/src/components/ReverseWorkspace.tsx b/WebSite/src/components/ReverseWorkspace.tsx index dc56476..04a7c34 100644 --- a/WebSite/src/components/ReverseWorkspace.tsx +++ b/WebSite/src/components/ReverseWorkspace.tsx @@ -2110,6 +2110,8 @@ function drawVoxelOverlayLayer( preview: DicomPreview, files: string[], previews: Record, + metricFiles: string[], + metricPreviews: Record, moduleStyles: Record, modelPose: ModelPose, slice: number, @@ -2124,7 +2126,8 @@ function drawVoxelOverlayLayer( } context.clearRect(0, 0, fovCanvas.width, fovCanvas.height); - const metrics = getModelSceneMetrics(files, previews, preview, totalSlices); + const metrics = getModelSceneMetrics(metricFiles, metricPreviews, preview, totalSlices) + ?? getModelSceneMetrics(files, previews, preview, totalSlices); if (!metrics) { return { activeModules: 0, filledPixels: 0, segmentCount: 0, modules: [] }; } @@ -2243,6 +2246,8 @@ export function VoxelizationMappingView({ const mappingViewportRef = useRef(null); const [dicomPreview, setDicomPreview] = useState(null); const [modelPreviews, setModelPreviews] = useState>({}); + const [metricPreviews, setMetricPreviews] = useState>({}); + const [metricPreviewsLoaded, setMetricPreviewsLoaded] = useState(false); const [dicomStatus, setDicomStatus] = useState('等待 DICOM 切片'); const [overlayStatus, setOverlayStatus] = useState('等待 STL 映射'); const [overlayStats, setOverlayStats] = useState({ activeModules: 0, filledPixels: 0, segmentCount: 0, modules: [] }); @@ -2259,9 +2264,11 @@ export function VoxelizationMappingView({ const maxSlice = Math.max(totalSlices - 1, 0); const safeSlice = clamp(slice, 0, maxSlice); const stlFiles = project?.stlFiles ?? []; + const stlFileSignature = stlFiles.join('|'); const visibleStlFiles = stlFiles.filter((fileName) => moduleStyles[fileName]?.visible !== false); const visibleStlFileSignature = visibleStlFiles.join('|'); const visibleModuleCount = visibleStlFiles.length; + const metricPreviewsReady = !stlFiles.length || metricPreviewsLoaded; const isLibraryVariant = variant === 'library'; const activeOverlayPlacement = overlayPlacement ?? (isLibraryVariant ? 'side' : 'bottom'); @@ -2295,6 +2302,36 @@ export function VoxelizationMappingView({ }; }, [project?.id, project?.dicomCount, safeSlice, displayMode]); + useEffect(() => { + if (!project || !stlFiles.length) { + setMetricPreviews({}); + setMetricPreviewsLoaded(true); + return; + } + + let disposed = false; + setMetricPreviews({}); + setMetricPreviewsLoaded(false); + Promise.allSettled(stlFiles.map((fileName) => ( + getCachedModelPreview(project.id, fileName, 1000) + .then((payload) => ({ fileName, payload })) + ))).then((results) => { + if (disposed) return; + const nextPreviews: Record = {}; + results.forEach((result) => { + if (result.status === 'fulfilled') { + nextPreviews[result.value.fileName] = result.value.payload; + } + }); + setMetricPreviews(nextPreviews); + setMetricPreviewsLoaded(true); + }); + + return () => { + disposed = true; + }; + }, [project?.id, stlFileSignature]); + useEffect(() => { if (!project || !visibleStlFiles.length) { setModelPreviews({}); @@ -2346,7 +2383,7 @@ export function VoxelizationMappingView({ return () => { disposed = true; }; - }, [project?.id, stlFiles.length, visibleStlFileSignature, detailLimit]); + }, [project?.id, stlFileSignature, visibleStlFileSignature, detailLimit]); useEffect(() => { const canvas = baseCanvasRef.current; @@ -2361,12 +2398,21 @@ export function VoxelizationMappingView({ if (!canvas || !dicomPreview) { return; } + if (!metricPreviewsReady) { + const context = canvas.getContext('2d'); + context?.clearRect(0, 0, canvas.width, canvas.height); + setOverlayStats({ activeModules: 0, filledPixels: 0, segmentCount: 0, modules: [] }); + setOverlayStatus('正在计算全局模型边界...'); + return; + } const frame = window.requestAnimationFrame(() => { const stats = drawVoxelOverlayLayer( canvas, dicomPreview, visibleStlFiles, modelPreviews, + stlFiles, + metricPreviews, moduleStyles, modelPose, safeSlice, @@ -2378,8 +2424,11 @@ export function VoxelizationMappingView({ return () => window.cancelAnimationFrame(frame); }, [ dicomPreview, + stlFileSignature, visibleStlFileSignature, modelPreviews, + metricPreviews, + metricPreviewsReady, JSON.stringify(moduleStyles), modelPose.rotateX, modelPose.rotateY, diff --git a/工程分析/实现方案-2026-05-24-20-06-08.md b/工程分析/实现方案-2026-05-24-20-06-08.md new file mode 100644 index 0000000..d9604df --- /dev/null +++ b/工程分析/实现方案-2026-05-24-20-06-08.md @@ -0,0 +1,53 @@ +# 实现方案-2026-05-24-20-06-08 + +## 实现方案文档路径 + +`工程分析/实现方案-2026-05-24-20-06-08.md` + +## 修改目标 + +- 修复隐藏 `liver_artery` 后 `liver_segment_S2/S3` 不显示或位置错乱的问题。 +- 保证可见构件 preview 结果按 `fileName` 稳定关联,而不是依赖异步数组顺序。 +- 同步工程经验和 Docker 部署说明。 + +## 涉及路径 + +- `WebSite/src/components/ReverseWorkspace.tsx` +- `Docker部署/README.md` +- `工程分析/经验记录.md` + +## 技术路线 + +- 定位 `visibleOverlayModules`、`overlayModelPreviews`、`VoxelizationMappingView` 的数据流。 +- 检查构件切换时 preview 加载是否按索引、请求完成顺序或旧缓存组合。 +- 若存在错配,改为用 `fileName` 做稳定 key,将 preview 结果保存在 map 中,再按当前可见构件列表重组。 +- 保留加载进度条和旧请求丢弃机制,避免旧异步结果覆盖新状态。 + +## 执行步骤 + +1. 搜索逆向映射视图和构件 preview 加载逻辑。 +2. 定位并修正可见构件与 STL preview 的关联方式。 +3. 视情况补充小型纯函数或 key 映射,减少索引错配风险。 +4. 更新 Docker 部署说明和经验记录。 +5. 运行类型检查、构建和关键搜索。 +6. 重新部署并验证本机与公网入口。 +7. 提交并推送到 Gitea。 + +## 兼容性与回滚方案 + +- 不改变 API 契约和项目状态字段。 +- 若修正引入显示异常,可回滚本次 commit 恢复旧数组加载方式。 +- 旧缓存仍可继续使用,但必须通过 `fileName` 命中。 + +## 预计文件变更 + +- 1 个前端组件文件。 +- 1 个 Docker 部署说明文件。 +- 3 个工程分析当次文档。 +- 1 个经验记录追加。 + +## 提交与部署策略 + +- Commit message 使用 `2026-05-24-20-06-08 修正可见构件映射错位`。 +- 构建通过后重启 `tmux` 会话 `revoxelseg-dicom`。 +- 验证本机和公网入口。 diff --git a/工程分析/测试方案-2026-05-24-20-06-08.md b/工程分析/测试方案-2026-05-24-20-06-08.md new file mode 100644 index 0000000..579d0c0 --- /dev/null +++ b/工程分析/测试方案-2026-05-24-20-06-08.md @@ -0,0 +1,47 @@ +# 测试方案-2026-05-24-20-06-08 + +## 测试方案文档路径 + +`工程分析/测试方案-2026-05-24-20-06-08.md` + +## 静态检查 + +- 执行 `cd WebSite && npm run lint`。 +- 搜索 overlay preview 加载逻辑,确认以 `fileName` 作为稳定关联 key。 + +## 构建检查 + +- 执行 `cd WebSite && npm run build`。 +- 确认生产构建成功。 + +## 关键业务场景验证 + +- 关闭 `liver_artery`,保持 `liver_segment_S2/S3` 眼睛打开时,`S2/S3` 仍可在对应切片显示。 +- 打开 `liver_artery` 后,`S2/S3` 的显示位置不发生跳层或整体偏移。 +- 多次快速切换构件眼睛时,加载进度正常,旧请求不覆盖新结果。 +- Overlay 统计中的构件名称、ID、颜色和像素计数与当前显示构件对应。 + +## 医学影像数据相关边界验证 + +- 本次不修改 DICOM/STL 原始数据和体素化导出算法。 +- 切片位置仍使用当前 DICOM 顺序规则。 +- 模型位姿、镜像和缩放继续作用于所有可见构件。 + +## 部署验证 + +- 重启 `tmux` 会话 `revoxelseg-dicom`。 +- 验证 `http://127.0.0.1:4000/api/health`。 +- 验证 `http://127.0.0.1:4000/`。 +- 验证 `https://revoxel.huijutec.cn/api/health` 与 `https://revoxel.huijutec.cn/`。 + +## Git/Gitea 备份验证 + +- 暂存本次相关源码、Docker 说明和工程分析文档。 +- 提交 message 包含本次时间戳。 +- 推送到 Gitea `main` 后确认本地分支与远端同步。 + +## 风险与回归关注点 + +- 不应把隐藏构件重新纳入 overlay。 +- 不应让构件名和 STL preview 结果错配。 +- 不应改变导出“可见类别”的过滤语义。 diff --git a/工程分析/经验记录.md b/工程分析/经验记录.md index e5416d4..276df07 100644 --- a/工程分析/经验记录.md +++ b/工程分析/经验记录.md @@ -1815,3 +1815,21 @@ C. 解决问题方案 D. 后续如何避免问题 微调控件应把“单击精度”和“长按速度”作为两个参数设计;不要为了长按快而增大最小 step,也不要为了单击精细而让长按只能按最小 step 慢慢跑。调整 repeat 参数后要确认松开、移出和取消 pointer 时定时器会停止。 + +## 2026-05-24-20-06-08 可见构件不能改变映射坐标系 + +A. 具体问题 + +用户反馈不显示 `liver_artery` 时,`liver_segment_S2`、`liver_segment_S3` 不显示或需要跳到更靠后的切片才显示;打开 `liver_artery` 后,`S2/S3` 又回到原先层位附近。 + +B. 产生问题原因 + +逆向分割映射视图为了减少高面数 STL 加载量,只加载当前眼睛打开的高精度 preview;但绘制前的 `getModelSceneMetrics` 也使用了同一组可见文件计算模型全局 bounds。隐藏 `liver_artery` 后,全局中心、基础缩放和 DICOM Z 轴映射被重新计算,其他肝段构件等于换了一个坐标系,所以出现消失或跳层。 + +C. 解决问题方案 + +将“绘制用高精度 preview”和“坐标系用全局 bounds preview”拆开:高精度 preview 仍只加载当前可见构件;另行用低抽样 preview 读取全部 STL 的 bounds,并在 `drawVoxelOverlayLayer` 中使用全部 STL bounds 计算统一 `ModelSceneMetrics`。这样关闭 `liver_artery` 只会隐藏它自己,不会让 `liver_segment_S2/S3` 的层位和位置改变。 + +D. 后续如何避免问题 + +涉及 DICOM/STL 配准的坐标系必须独立于可见性、筛选条件和 UI 状态;性能优化可以只加载可见构件的高精度网格,但全局中心、缩放、slice Z 映射应来自稳定的全部模型边界。凡是用户反馈“隐藏某构件后其他构件位置变了”,优先检查 bounds 是否被可见列表重算。 diff --git a/工程分析/需求分析-2026-05-24-20-06-08.md b/工程分析/需求分析-2026-05-24-20-06-08.md new file mode 100644 index 0000000..28e9c5a --- /dev/null +++ b/工程分析/需求分析-2026-05-24-20-06-08.md @@ -0,0 +1,43 @@ +# 需求分析-2026-05-24-20-06-08 + +## 开始时间 + +2026-05-24-20-06-08 + +## 原始需求摘要 + +用户反馈在逆向工作区中,如果不点击 `liver_artery`,`liver_segment_S2`、`liver_segment_S3` 不显示;同时在 `liver_artery` 未显示时,`liver_segment_S2/S3` 的二维映射位置疑似偏移,原本约 70 多层可见的内容跑到 100 多层。 + +## 业务目标 + +- 构件显示隐藏应彼此独立,关闭 `liver_artery` 不应导致 `liver_segment_S2/S3` 消失。 +- 逆向分割映射视图中,每个构件应使用自己的 STL preview、颜色和 partId,不应因可见构件增减发生错位。 +- 切片层号和模型位姿不应因隐藏某个构件而漂移。 + +## 输入与输出 + +- 输入:用户在构件层级中切换 `liver_artery`、`liver_segment_S2`、`liver_segment_S3` 等眼睛状态,并浏览 DICOM 切片。 +- 输出:仅眼睛开启且当前切片有交集的构件显示;关闭任一构件只影响该构件,不改变其他构件的映射位置和切片覆盖范围。 + +## 影响范围 + +- `WebSite/src/components/ReverseWorkspace.tsx`:逆向工作区构件可见列表、STL preview 加载、overlay 统计和绘制。 +- `Docker部署/README.md`:同步说明可见构件映射独立性。 +- `工程分析/经验记录.md`:记录异步可见构件加载不能按数组索引错配的经验。 + +## 关键约束 + +- 不改变用户现有位姿数据、DICOM/STL 原始数据和导出文件结构。 +- 不回退“只加载当前可见构件”的性能优化。 +- 保证可见构件增减时 overlay 加载状态明确,且最终绘制只使用最新请求结果。 + +## 风险点 + +- 如果 preview 结果按数组顺序写回,某个构件隐藏后可能让后续构件拿到错误 STL 顶点,造成消失或位置漂移。 +- 如果异步请求返回顺序晚于最新可见状态,旧结果可能覆盖新 overlay。 +- 如果过滤逻辑把 partId、fileName 或 label 映射错位,Overlay 统计和导出语义会不一致。 + +## 待确认问题或默认假设 + +- 默认本次聚焦逆向工作区右侧“逆向分割映射视图”的显示和切片位置,不改变项目库导出范围逻辑。 +- 默认 `liver_segment_S2/S3` 的眼睛打开时,应与 `liver_artery` 是否打开无关。