2026-05-20-15-33-38 逆向映射导航外置与遮挡优化

This commit is contained in:
2026-05-20 15:41:12 +08:00
parent 1f353e97c0
commit 27cff93711
6 changed files with 308 additions and 62 deletions

View File

@@ -6,8 +6,8 @@ import {
RotateCw, RotateCw,
Rotate3d, Rotate3d,
AlertCircle, AlertCircle,
ChevronLeft, ChevronDown,
ChevronRight, ChevronUp,
Eye, Eye,
Layers, Layers,
Save, Save,
@@ -1882,8 +1882,8 @@ export function VoxelizationMappingView({
return ( return (
<div className="relative flex h-full min-h-[420px] flex-col overflow-hidden rounded-3xl border border-slate-900 bg-slate-950 shadow-2xl"> <div className="relative flex h-full min-h-[420px] flex-col overflow-hidden rounded-3xl border border-slate-900 bg-slate-950 shadow-2xl">
<div className="relative min-h-0 flex-1 overflow-hidden bg-black"> <div className="flex items-center justify-between gap-3 border-b border-white/10 bg-slate-950 px-4 py-3">
<div className="absolute left-4 top-4 z-10 flex flex-wrap gap-2"> <div className="flex min-w-0 flex-wrap gap-2">
<span className="rounded-lg border border-white/10 bg-black/65 px-2.5 py-1 text-[9px] font-bold uppercase tracking-widest text-slate-200"> <span className="rounded-lg border border-white/10 bg-black/65 px-2.5 py-1 text-[9px] font-bold uppercase tracking-widest text-slate-200">
Base DICOM Base DICOM
</span> </span>
@@ -1891,9 +1891,14 @@ export function VoxelizationMappingView({
Overlay Label Map Overlay Label Map
</span> </span>
</div> </div>
<div className="absolute right-4 top-4 z-10 rounded-lg border border-white/10 bg-black/65 px-2.5 py-1 text-[10px] font-mono text-white/70"> <div className="shrink-0 rounded-lg border border-white/10 bg-black/65 px-2.5 py-1 text-[10px] font-mono text-white/70">
Z {safeSlice + 1}/{Math.max(totalSlices, 1)} Z {safeSlice + 1}/{Math.max(totalSlices, 1)}
</div> </div>
</div>
<div className="grid min-h-0 flex-1 grid-cols-[minmax(0,1fr)_76px]">
<div className="flex min-h-0 flex-col">
<div className="relative min-h-0 flex-1 overflow-hidden bg-black">
{dicomPreview ? ( {dicomPreview ? (
<div <div
className="absolute inset-0 flex items-center justify-center" className="absolute inset-0 flex items-center justify-center"
@@ -1907,14 +1912,16 @@ export function VoxelizationMappingView({
{dicomStatus} {dicomStatus}
</div> </div>
)} )}
<div className="pointer-events-none absolute inset-x-4 bottom-4 z-10 rounded-2xl border border-white/10 bg-black/70 p-3 backdrop-blur-sm"> </div>
<div className="border-t border-white/10 bg-slate-950 px-4 py-3">
<div className="mb-2 flex items-center justify-between gap-3 text-[10px] font-bold text-white/70"> <div className="mb-2 flex items-center justify-between gap-3 text-[10px] font-bold text-white/70">
<span className="truncate">{overlayStatus}</span> <span className="truncate">{overlayStatus}</span>
<span className="font-mono text-cyan-100"> <span className="font-mono text-cyan-100">
{overlayStats.activeModules}/{visibleModuleCount} · {overlayStats.segmentCount} · {overlayStats.filledPixels} px {overlayStats.activeModules}/{visibleModuleCount} · {overlayStats.segmentCount} · {overlayStats.filledPixels} px
</span> </span>
</div> </div>
<div className="max-h-20 overflow-auto pr-1"> <div className="max-h-24 overflow-auto pr-1">
{overlayStats.modules.length ? ( {overlayStats.modules.length ? (
<div className="grid grid-cols-2 gap-1.5 xl:grid-cols-3"> <div className="grid grid-cols-2 gap-1.5 xl:grid-cols-3">
{overlayStats.modules.map((item) => ( {overlayStats.modules.map((item) => (
@@ -1936,27 +1943,26 @@ export function VoxelizationMappingView({
</div> </div>
</div> </div>
<div className="border-t border-white/10 bg-slate-950 px-4 py-3"> <aside className="flex min-h-0 flex-col items-center gap-3 border-l border-white/10 bg-slate-900/95 px-3 py-4">
<div className="mb-2 flex items-center justify-between"> <div className="w-full rounded-2xl border border-white/10 bg-slate-950/90 px-2 py-3 text-center">
<p className="text-[10px] font-bold uppercase tracking-widest text-slate-400">Slice Navigator</p> <p className="text-[10px] font-bold text-slate-300">DICOM </p>
<span className="font-mono text-[10px] font-bold text-cyan-100"> <span className="mt-1 block font-mono text-[10px] font-bold text-cyan-100">
{safeSlice + 1} / {Math.max(totalSlices, 1)} {safeSlice + 1} / {Math.max(totalSlices, 1)}
</span> </span>
</div> </div>
<div className="grid grid-cols-[28px_1fr_28px] items-center gap-3">
<button <button
onClick={() => stepSlice(-1)} onClick={() => stepSlice(-1)}
disabled={safeSlice <= 0} disabled={safeSlice <= 0}
className="flex h-7 w-7 items-center justify-center rounded-lg border border-slate-700 bg-slate-900 text-slate-200 hover:border-cyan-400 hover:text-cyan-100 disabled:opacity-35" className="flex h-8 w-8 items-center justify-center rounded-xl border border-slate-700 bg-slate-950 text-slate-200 hover:border-cyan-400 hover:text-cyan-100 disabled:opacity-35"
title="上一层" title="上一层"
> >
<ChevronLeft size={15} /> <ChevronUp size={16} />
</button> </button>
<div className="relative h-8"> <div className="relative min-h-[240px] w-10 flex-1">
<div className="absolute inset-x-0 top-1/2 h-2 -translate-y-1/2 rounded-full bg-slate-800" /> <div className="absolute inset-y-0 left-1/2 w-2 -translate-x-1/2 rounded-full bg-slate-800" />
<div <div
className="absolute top-1/2 h-2 -translate-y-1/2 rounded-full bg-cyan-400" className="absolute bottom-0 left-1/2 w-2 -translate-x-1/2 rounded-full bg-cyan-400"
style={{ left: 0, width: `${slicePercent}%` }} style={{ height: `${slicePercent}%` }}
/> />
<input <input
type="range" type="range"
@@ -1964,19 +1970,24 @@ export function VoxelizationMappingView({
max={maxSlice} max={maxSlice}
value={safeSlice} value={safeSlice}
onChange={(event) => onSliceChange(Number(event.target.value))} onChange={(event) => onSliceChange(Number(event.target.value))}
className="mapping-slice-input" className="mapping-slice-vertical-input"
aria-label="逆向分割映射视图切片导航" aria-label="逆向分割映射视图切片导航"
/> />
</div> </div>
<button <button
onClick={() => stepSlice(1)} onClick={() => stepSlice(1)}
disabled={safeSlice >= maxSlice} disabled={safeSlice >= maxSlice}
className="flex h-7 w-7 items-center justify-center rounded-lg border border-slate-700 bg-slate-900 text-slate-200 hover:border-cyan-400 hover:text-cyan-100 disabled:opacity-35" className="flex h-8 w-8 items-center justify-center rounded-xl border border-slate-700 bg-slate-950 text-slate-200 hover:border-cyan-400 hover:text-cyan-100 disabled:opacity-35"
title="下一层" title="下一层"
> >
<ChevronRight size={15} /> <ChevronDown size={16} />
</button> </button>
<div className="grid w-full grid-cols-1 gap-1 text-center text-[9px] font-bold text-slate-500">
<span> {Math.max(totalSlices, 1)}</span>
<span className="text-cyan-200"> {safeSlice + 1}</span>
<span> 1</span>
</div> </div>
</aside>
</div> </div>
</div> </div>
); );

View File

@@ -116,3 +116,61 @@
.mapping-slice-input:active::-moz-range-thumb { .mapping-slice-input:active::-moz-range-thumb {
cursor: grabbing; cursor: grabbing;
} }
.mapping-slice-vertical-input {
appearance: none;
-webkit-appearance: none;
background: transparent;
height: 100%;
inset: 0;
position: absolute;
width: 100%;
direction: rtl;
writing-mode: vertical-rl;
}
.mapping-slice-vertical-input:focus {
outline: none;
}
.mapping-slice-vertical-input::-webkit-slider-runnable-track {
background: transparent;
border: 0;
width: 8px;
}
.mapping-slice-vertical-input::-webkit-slider-thumb {
appearance: none;
-webkit-appearance: none;
background: #22d3ee;
border: 3px solid #0f172a;
border-radius: 9999px;
box-shadow: 0 0 0 4px rgba(34, 211, 238, 0.16), 0 8px 18px rgba(8, 47, 73, 0.45);
cursor: grab;
height: 22px;
width: 22px;
}
.mapping-slice-vertical-input::-moz-range-track {
background: transparent;
border: 0;
width: 8px;
}
.mapping-slice-vertical-input::-moz-range-thumb {
background: #22d3ee;
border: 3px solid #0f172a;
border-radius: 9999px;
box-shadow: 0 0 0 4px rgba(34, 211, 238, 0.16), 0 8px 18px rgba(8, 47, 73, 0.45);
cursor: grab;
height: 16px;
width: 16px;
}
.mapping-slice-vertical-input:active::-webkit-slider-thumb {
cursor: grabbing;
}
.mapping-slice-vertical-input:active::-moz-range-thumb {
cursor: grabbing;
}

View File

@@ -0,0 +1,51 @@
# 实现方案:右侧竖向 Slice Navigator 与影像遮挡治理
实现方案文档路径:`工程分析/实现方案-2026-05-20-15-33-38.md`
## 修改目标
调整逆向分割映射视图布局:将切片导航从影像底部迁移到右侧竖向栏,并把 Overlay Label Map 的构件统计面板从影像内部移到影像下方,避免遮挡 DICOM 影像。
## 涉及路径
- `WebSite/src/components/ReverseWorkspace.tsx`
- `工程分析/需求分析-2026-05-20-15-33-38.md`
- `工程分析/实现方案-2026-05-20-15-33-38.md`
- `工程分析/测试方案-2026-05-20-15-33-38.md`
- `工程分析/经验记录.md`
## 技术路线
1. 定位 `VoxelizationMappingView` 中当前底部 `Slice Navigator` 与影像内 Overlay 状态面板。
2. 将组件整体布局改为横向网格:左侧影像与独立信息区,右侧竖向切片导航栏。
3. 保留顶部 `Base DICOM``Overlay Label Map``Z` 状态标签,但缩小为影像角落提示,避免大面积遮挡。
4. 将构件统计列表改为影像下方独立面板,显示当前 Overlay 状态、构件数、边数、像素数和当前切片构件表。
5. 为竖向 range 增加内联样式或类名,使用 `writingMode: 'vertical-rl'``direction: 'rtl'` 实现从上到下浏览切片。
6. 执行类型检查、构建、部署验证。
## 执行步骤
- 阅读当前 `VoxelizationMappingView` JSX 与相关样式。
- 修改布局结构和样式。
- 检查 `mapping-slice-input` 是否有全局 CSS 影响,必要时增加独立竖向类。
- 运行 `npm run lint``npm run build`
- 重启 `tmux` 服务并验证健康接口和首页。
- 更新测试方案与经验记录。
- 精确暂存、提交并推送 Gitea。
## 兼容性与回滚方案
- 该改动只影响前端布局,不改变数据结构和 API。
- 如竖向 `range` 在某浏览器显示异常,可回滚为右侧竖排按钮加数值输入,或补充 WebKit 专用 CSS。
- 项目库复用同一组件,因此无需额外复制样式逻辑。
## 预计文件变更
- `WebSite/src/components/ReverseWorkspace.tsx`
- 本轮工程分析文档与 `工程分析/经验记录.md`
## 提交与部署策略
- 暂存本轮相关代码和工程分析文档。
- commit message 包含 `2026-05-20-15-33-38`
- 推送 Gitea 后重启 `revoxelseg-dicom` 服务,验证 `http://127.0.0.1:4000/api/health` 与首页。

View File

@@ -0,0 +1,55 @@
# 测试方案Slice Navigator 竖向外置与 Overlay 遮挡检查
测试方案文档路径:`工程分析/测试方案-2026-05-20-15-33-38.md`
## 静态检查
- 确认 `VoxelizationMappingView``Slice Navigator` 不再位于影像画布底部内部。
- 确认切片导航栏位于影像右侧,并使用竖向 `range`
- 确认构件统计面板不再用绝对定位覆盖 DICOM 影像。
- 确认 `Base DICOM``Overlay Label Map` 仅作为小标签显示,不遮挡主体区域。
## 构建检查
-`WebSite/` 执行 `npm run lint`
-`WebSite/` 执行 `npm run build`
## 关键业务场景验证
- 逆向工作区中拖动右侧竖向切片条可以切换当前 Z 层。
- 上下按钮仍可逐层切换。
- DICOM 原始影像和 Overlay Label Map 不被构件列表遮挡。
- 项目库中复用的逆向分割映射视图保持可用。
## 医学影像数据相关边界验证
- 不修改 DICOM/STL 原始数据。
- 不改变体素化、求交、光栅化和导出逻辑。
- 仅调整浏览控件与信息面板的位置,保证影像主体可审查。
## 部署验证
- 验证 `http://127.0.0.1:4000/api/health`
- 验证 `http://127.0.0.1:4000/` 返回 200。
## Git/Gitea 备份验证
- commit message 包含 `2026-05-20-15-33-38`
- 推送 Gitea 成功后记录 commit。
- 确认未暂存历史删除状态、软著材料和运行态文件。
## 风险与回归关注点
- 竖向 range 的浏览器兼容性。
- 小屏幕下右侧导航栏是否挤压影像宽度。
- 项目库复用组件后的宽度约束是否仍能正常显示。
## 执行结果
- `npm run lint`通过TypeScript 无报错。
- `npm run build`通过Vite 完成生产构建;仅保留当前项目已有的大 chunk 体积提示。
- 静态确认:`VoxelizationMappingView` 已移除底部横向 `Slice Navigator`,改为右侧竖向 `mapping-slice-vertical-input`
- 静态确认Overlay 构件统计面板已从影像绝对定位层移到影像下方独立区域,不再遮挡 DICOM 画布。
- 部署验证:已重建 `tmux` 会话 `revoxelseg-dicom`,执行 `npm run serve -- --host 0.0.0.0 --port 4000`
- `curl -fsS http://127.0.0.1:4000/api/health`:通过,返回 `{"ok":true,"service":"revoxelseg-dicom",...}`
- `curl -I -fsS http://127.0.0.1:4000/`:通过,返回 `HTTP/1.1 200 OK`

View File

@@ -1297,3 +1297,21 @@ C. 解决问题方案
D. 后续如何避免问题 D. 后续如何避免问题
凡是用户要求两个页面“效果一致”,优先抽取或导出已有真实组件,不再维护第二套近似 UI。涉及密码、权限、删除等高风险管理操作时必须把资料编辑和凭据修改拆成不同入口并通过显式标签、校验和反馈减少误操作。 凡是用户要求两个页面“效果一致”,优先抽取或导出已有真实组件,不再维护第二套近似 UI。涉及密码、权限、删除等高风险管理操作时必须把资料编辑和凭据修改拆成不同入口并通过显式标签、校验和反馈减少误操作。
## 2026-05-20-15-33-38 医学影像视图控件不要覆盖审查画布
A. 具体问题
用户指出逆向工作区的 `Slice Navigator` 格式与“DICOM 切片范围”不统一,并且 Overlay Label Map 的构件统计区域会遮挡 DICOM 影像,影响对二维切片和叠加分割结果的审查。
B. 产生问题原因
此前 `VoxelizationMappingView` 为了紧凑展示,把切片导航放在视图底部,同时将 Overlay 状态和构件列表使用绝对定位压在影像画布底部。该设计在普通预览中节省空间,但在医学影像核验场景中会遮挡 DICOM 主体区域。
C. 解决问题方案
将视图结构调整为“影像画布 + 右侧竖向切片导航 + 下方 Overlay 统计面板”:右侧竖向导航使用独立 `mapping-slice-vertical-input`,显示当前 DICOM 切片位置和上下层按钮Overlay 构件列表移到画布下方,不再覆盖 DICOM 影像。
D. 后续如何避免问题
医学影像主画布应优先保持无遮挡,状态、统计、导航控件默认放在画布外侧。若必须悬浮在影像上,只能使用小尺寸状态标识,并在提交前检查是否遮挡解剖结构或分割边界。

View File

@@ -0,0 +1,53 @@
# 需求分析:逆向映射视图切片导航外置与遮挡优化
开始时间:`2026-05-20-15-33-38`
## 原始需求摘要
用户要求修改逆向工作区:
1. 将逆向工作区中 `Slice Navigator` 的格式与“DICOM 切片范围”的格式统一,不要放在图片里面。
2. `Slice Navigator` 调整为竖向滚动条,放在图片右侧。
3. `Overlay Label Map` 部分不要遮挡 DICOM 影像。
## 业务目标
- 让二维逆向分割映射视图的切片浏览控件不压住医学影像主体。
- 保持切片导航与中部工具栏内“DICOM 切片范围”控件在视觉语义上统一。
- 避免状态说明、构件统计面板遮挡 DICOM 原始影像和分割叠加区域,提升临床审查可读性。
## 输入与输出
输入:
- `WebSite/src/components/ReverseWorkspace.tsx`
- 复用该组件的 `WebSite/src/components/ProjectLibrary.tsx`
输出:
- `VoxelizationMappingView` 的切片导航从底部内嵌区域改为右侧竖向导航栏。
- 右侧导航栏显示当前层数、竖向 range 控件和上下切片按钮。
- 构件统计面板移出影像画布覆盖层,作为影像下方独立信息区展示。
## 影响范围
- 逆向工作区“逆向分割映射视图”。
- 项目库中复用的“逆向分割映射视图”。
- 相关 Tailwind 样式和 TypeScript 类型检查。
## 关键约束
- 不改动 STL/DICOM 映射算法,只调整控件布局和遮挡关系。
- 影像主体区域仍需保持 Base DICOM 与 Overlay Label Map 的标签提示。
- 竖向切片条需要可拖动、可点击上下按钮、可通过键盘/辅助技术识别。
- 不能把无关工作区历史删除和软著材料纳入提交。
## 风险点
- 原生 `range` 竖向显示在不同浏览器上需要兼容写法。
- 切片导航移出底部后,需要保证容器高度和图片区域不会被挤压到不可用。
- 项目库复用同一组件,布局变化会同步影响项目库,需要保持宽窄视口可用。
## 默认假设
- 用户所说“Overlay Label Map 部分”主要指当前切片构件统计和状态面板遮挡 DICOM 影像,而不是取消分割掩码本身;分割掩码仍应叠加显示。