2026-05-24-20-06-08 修正可见构件映射错位
This commit is contained in:
@@ -19,7 +19,7 @@
|
||||
- 项目库与工作区的 DICOM 切片编号按医学影像顺序显示,滑条使用非进度条样式。
|
||||
- 项目库支持锁定/解锁项目、筛选未上锁项目,并在锁定时保存位姿快照到 `项目数据/锁定结果/`。
|
||||
- 逆向工作区“构件层级”支持一键显示或隐藏全部构件;切片滑条顶部为第 1 张,向下查看到第 N 张。
|
||||
- 逆向分割映射视图按当前可见构件加载高精度 STL 预览;实体模式最高使用 80 万三角面预览,“可见类别 + 构件分别导出”严格只导出当前眼睛打开的构件。
|
||||
- 逆向分割映射视图按当前可见构件加载高精度 STL 预览,并始终用全部 STL 边界保持统一模型坐标系;实体模式最高使用 80 万三角面预览,“可见类别 + 构件分别导出”严格只导出当前眼睛打开的构件。
|
||||
|
||||
## 一、本机部署
|
||||
|
||||
|
||||
@@ -2110,6 +2110,8 @@ function drawVoxelOverlayLayer(
|
||||
preview: DicomPreview,
|
||||
files: string[],
|
||||
previews: Record<string, ModelPreviewPayload>,
|
||||
metricFiles: string[],
|
||||
metricPreviews: Record<string, ModelPreviewPayload>,
|
||||
moduleStyles: Record<string, ModuleStyle>,
|
||||
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<HTMLDivElement | null>(null);
|
||||
const [dicomPreview, setDicomPreview] = useState<DicomPreview | null>(null);
|
||||
const [modelPreviews, setModelPreviews] = useState<Record<string, ModelPreviewPayload>>({});
|
||||
const [metricPreviews, setMetricPreviews] = useState<Record<string, ModelPreviewPayload>>({});
|
||||
const [metricPreviewsLoaded, setMetricPreviewsLoaded] = useState(false);
|
||||
const [dicomStatus, setDicomStatus] = useState('等待 DICOM 切片');
|
||||
const [overlayStatus, setOverlayStatus] = useState('等待 STL 映射');
|
||||
const [overlayStats, setOverlayStats] = useState<OverlayStats>({ 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<string, ModelPreviewPayload> = {};
|
||||
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,
|
||||
|
||||
53
工程分析/实现方案-2026-05-24-20-06-08.md
Normal file
53
工程分析/实现方案-2026-05-24-20-06-08.md
Normal file
@@ -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`。
|
||||
- 验证本机和公网入口。
|
||||
47
工程分析/测试方案-2026-05-24-20-06-08.md
Normal file
47
工程分析/测试方案-2026-05-24-20-06-08.md
Normal file
@@ -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 结果错配。
|
||||
- 不应改变导出“可见类别”的过滤语义。
|
||||
18
工程分析/经验记录.md
18
工程分析/经验记录.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 是否被可见列表重算。
|
||||
|
||||
43
工程分析/需求分析-2026-05-24-20-06-08.md
Normal file
43
工程分析/需求分析-2026-05-24-20-06-08.md
Normal file
@@ -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` 是否打开无关。
|
||||
Reference in New Issue
Block a user