2026-05-08-03-23-51 精简模型入口和切分显示
This commit is contained in:
@@ -28,7 +28,7 @@ export default function App() {
|
||||
|
||||
// Automatically collapse main sidebar when entering Project Library or Workspace
|
||||
useEffect(() => {
|
||||
if (activeView === ViewType.PROJECTS || activeView === ViewType.MODELS || activeView === ViewType.WORKSPACE) {
|
||||
if (activeView === ViewType.PROJECTS || activeView === ViewType.WORKSPACE) {
|
||||
setSidebarCollapsed(true);
|
||||
} else {
|
||||
setSidebarCollapsed(false);
|
||||
@@ -106,7 +106,6 @@ export default function App() {
|
||||
<span className="text-slate-900 font-bold capitalize">
|
||||
{activeView === ViewType.OVERVIEW && '总体概况'}
|
||||
{activeView === ViewType.PROJECTS && '项目库'}
|
||||
{activeView === ViewType.MODELS && '模型库'}
|
||||
{activeView === ViewType.WORKSPACE && '逆向工作区'}
|
||||
{activeView === ViewType.SYSTEM && '系统管理工作区'}
|
||||
</span>
|
||||
@@ -136,15 +135,6 @@ export default function App() {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{activeView === ViewType.MODELS && (
|
||||
<ProjectLibrary
|
||||
initialViewMode="model"
|
||||
onReverse={(projectId) => {
|
||||
setActiveProjectId(projectId);
|
||||
setActiveView(ViewType.WORKSPACE);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{activeView === ViewType.WORKSPACE && <ReverseWorkspace projectId={activeProjectId} />}
|
||||
{activeView === ViewType.SYSTEM && <UserManagement />}
|
||||
</motion.div>
|
||||
|
||||
@@ -219,21 +219,6 @@ function FusionThreeView({
|
||||
const upperCutZ = sliceToZ(cutRangeEnd);
|
||||
const lowerClippingPlane = new THREE.Plane();
|
||||
const upperClippingPlane = new THREE.Plane();
|
||||
const createCutPlaneMaterial = () => new THREE.MeshBasicMaterial({
|
||||
color: '#f97316',
|
||||
transparent: true,
|
||||
opacity: cutEnabled ? 0.16 : 0,
|
||||
side: THREE.DoubleSide,
|
||||
depthWrite: false,
|
||||
});
|
||||
const lowerCutPlane = new THREE.Mesh(new THREE.PlaneGeometry(dicomWidth, dicomHeight), createCutPlaneMaterial());
|
||||
lowerCutPlane.position.set(0, 0, lowerCutZ);
|
||||
lowerCutPlane.visible = cutEnabled;
|
||||
dicomGroup.add(lowerCutPlane);
|
||||
const upperCutPlane = new THREE.Mesh(new THREE.PlaneGeometry(dicomWidth, dicomHeight), createCutPlaneMaterial());
|
||||
upperCutPlane.position.set(0, 0, upperCutZ);
|
||||
upperCutPlane.visible = cutEnabled;
|
||||
dicomGroup.add(upperCutPlane);
|
||||
|
||||
const textures: THREE.Texture[] = [];
|
||||
volume.frames.forEach((frame, index) => {
|
||||
@@ -532,7 +517,6 @@ export default function ReverseWorkspace({ projectId }: { projectId: string }) {
|
||||
const [fusionError, setFusionError] = useState('');
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [exportMessage, setExportMessage] = useState('准备就绪');
|
||||
const [preloadMessage, setPreloadMessage] = useState('缓存空闲');
|
||||
const fusionVolumeCacheRef = useRef(new Map<string, DicomFusionVolume>());
|
||||
const poseRepeatRef = useRef<{ timeout: number | null; interval: number | null }>({ timeout: null, interval: null });
|
||||
|
||||
@@ -594,7 +578,6 @@ export default function ReverseWorkspace({ projectId }: { projectId: string }) {
|
||||
const cached = fusionVolumeCacheRef.current.get(cacheKey);
|
||||
if (useCache && cached) {
|
||||
setFusionVolume(cached);
|
||||
setPreloadMessage(`已使用缓存范围 ${safeStart + 1}-${rangeEnd + 1}`);
|
||||
return cached;
|
||||
}
|
||||
const volumePayload = await api.getDicomFusionVolume(project.id, safeStart, rangeEnd, 'soft');
|
||||
@@ -769,29 +752,6 @@ export default function ReverseWorkspace({ projectId }: { projectId: string }) {
|
||||
const rangeEndPercent = maxSlice > 0 ? (displayEnd / maxSlice) * 100 : 0;
|
||||
const selectedDisplay = displayOptions.find((item) => item.id === displayLevel) ?? displayOptions[0];
|
||||
const selectedDicomOpacity = dicomOpacityOptions.find((item) => item.id === dicomOpacityLevel) ?? dicomOpacityOptions[0];
|
||||
const preloadPoints = [0.2, 0.4, 0.6, 0.8, 1].map((ratio) => clamp(Math.max(0, Math.round((project?.dicomCount ?? 1) * ratio) - 1), 0, maxSlice));
|
||||
|
||||
const preloadFusionPoint = async (end: number) => {
|
||||
if (!project) return;
|
||||
const safeEnd = clamp(end, 0, maxSlice);
|
||||
setPreloadMessage(`正在预存第 ${safeEnd + 1} 张...`);
|
||||
try {
|
||||
await loadFusionVolume(safeEnd, safeEnd, false);
|
||||
setPreloadMessage(`已预存第 ${safeEnd + 1} 张`);
|
||||
} catch (error) {
|
||||
setPreloadMessage(error instanceof Error ? error.message : '预存失败');
|
||||
}
|
||||
};
|
||||
|
||||
const preloadAllFusionPoints = async () => {
|
||||
setPreloadMessage('正在预存五个点位...');
|
||||
try {
|
||||
await Promise.all(preloadPoints.map((point) => loadFusionVolume(point, point, true)));
|
||||
setPreloadMessage('五个点位已预存');
|
||||
} catch (error) {
|
||||
setPreloadMessage(error instanceof Error ? error.message : '五点预存失败');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full min-h-0 overflow-y-auto pr-2 flex flex-col gap-6">
|
||||
@@ -911,36 +871,6 @@ export default function ReverseWorkspace({ projectId }: { projectId: string }) {
|
||||
<span className="text-right">终点 {safeSliceEnd + 1}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-3 text-[10px] leading-5 text-slate-400">
|
||||
显示范围支持 M-N,两个端点可双向调整;范围变化只改变可视化切片,不改变模型原始位姿。
|
||||
</p>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2">
|
||||
{preloadPoints.map((point, index) => {
|
||||
const cached = project ? fusionVolumeCacheRef.current.has(getFusionCacheKey(project.id, point, point)) : false;
|
||||
return (
|
||||
<button
|
||||
key={`${point}-${index}`}
|
||||
onClick={() => {
|
||||
setSliceStart(point);
|
||||
setSliceEnd(point);
|
||||
preloadFusionPoint(point);
|
||||
}}
|
||||
className={`rounded-lg border px-2 py-1 text-[10px] font-bold transition-all ${
|
||||
project && cached ? 'border-emerald-200 bg-emerald-50 text-emerald-600' : 'border-slate-200 bg-white text-slate-500 hover:text-blue-600'
|
||||
}`}
|
||||
>
|
||||
点位 {index + 1} · {point + 1}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
onClick={preloadAllFusionPoints}
|
||||
className="rounded-lg bg-blue-600 px-2 py-1 text-[10px] font-bold text-white hover:bg-blue-700"
|
||||
>
|
||||
预存五点
|
||||
</button>
|
||||
<span className="text-[10px] font-bold text-slate-400">{preloadMessage}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { motion } from 'motion/react';
|
||||
import {
|
||||
BarChart3,
|
||||
FolderRoot,
|
||||
Box,
|
||||
Workflow,
|
||||
Settings,
|
||||
LogOut,
|
||||
@@ -32,7 +31,6 @@ export default function Sidebar({
|
||||
const menuItems = [
|
||||
{ id: ViewType.OVERVIEW, icon: BarChart3, label: '总体概况' },
|
||||
{ id: ViewType.PROJECTS, icon: FolderRoot, label: '项目库' },
|
||||
{ id: ViewType.MODELS, icon: Box, label: '模型库' },
|
||||
{ id: ViewType.WORKSPACE, icon: Workflow, label: '逆向工作区' },
|
||||
{ id: ViewType.SYSTEM, icon: Settings, label: '系统管理工作区' },
|
||||
];
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export enum ViewType {
|
||||
OVERVIEW = 'overview',
|
||||
PROJECTS = 'projects',
|
||||
MODELS = 'models',
|
||||
WORKSPACE = 'workspace',
|
||||
SYSTEM = 'system',
|
||||
}
|
||||
|
||||
51
工程分析/实现方案-2026-05-08-03-23-51.md
Normal file
51
工程分析/实现方案-2026-05-08-03-23-51.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# 实现方案:精简导航与切分显示
|
||||
|
||||
时间戳:2026-05-08-03-23-51
|
||||
|
||||
## 修改目标
|
||||
|
||||
删除重复“模型库”入口,简化逆向工作区 DICOM 范围卡片,并去掉模型切分时的红色辅助平面。
|
||||
|
||||
## 涉及路径
|
||||
|
||||
- `WebSite/src/types.ts`
|
||||
- `WebSite/src/components/Sidebar.tsx`
|
||||
- `WebSite/src/App.tsx`
|
||||
- `WebSite/src/components/ReverseWorkspace.tsx`
|
||||
- `工程分析/经验记录.md`
|
||||
|
||||
## 技术路线
|
||||
|
||||
1. 从 `ViewType` 中删除 `MODELS`。
|
||||
2. 从侧边栏 `menuItems` 中删除“模型库”,并移除不再使用的图标导入。
|
||||
3. 从 `App.tsx` 中删除 `ViewType.MODELS` 的标题和渲染分支,保留“项目库”中的模型查看能力。
|
||||
4. 删除 `ReverseWorkspace.tsx` 中 DICOM 范围说明文案、五点预存按钮、预存状态和相关函数。
|
||||
5. 保留 `fusionVolumeCacheRef`,让初始 `1~最终` 和用户实际访问过的范围自动进入缓存。
|
||||
6. 删除 `lowerCutPlane/upperCutPlane` Mesh 创建与加入场景逻辑,只保留 `lowerClippingPlane/upperClippingPlane` 用于真实裁切。
|
||||
|
||||
## 数据流或交互流程
|
||||
|
||||
- 项目入口统一从“项目库”进入。
|
||||
- 逆向工作区加载项目后默认请求完整 DICOM 范围,返回后写入缓存。
|
||||
- 启用模型切分后,STL 材质继续使用两张 clipping plane 裁切,不再渲染红色平面。
|
||||
|
||||
## 兼容性与回滚方案
|
||||
|
||||
- 若用户仍需要独立模型入口,可恢复 `ViewType.MODELS`、侧边栏项和 `App.tsx` 对应分支。
|
||||
- 若需要切割面可视化,可重新添加半透明辅助平面;本次不影响 clipping plane 逻辑。
|
||||
|
||||
## 风险控制
|
||||
|
||||
- 使用 `rg` 确认不再存在 `ViewType.MODELS`、`模型库`、`预存五点`、红色切面 Mesh。
|
||||
- 执行 `npm run lint` 和 `npm run build`。
|
||||
- 重新部署并从 dev server 拉取源码确认变更生效。
|
||||
|
||||
## 预计文件变更
|
||||
|
||||
- 修改 4 个前端源码文件。
|
||||
- 新增 3 个工程分析文档。
|
||||
- 追加经验记录。
|
||||
|
||||
## 人工审核状态
|
||||
|
||||
用户已在项目工作流历史中确认后续直接执行,本次不等待二次人工审核。
|
||||
41
工程分析/测试方案-2026-05-08-03-23-51.md
Normal file
41
工程分析/测试方案-2026-05-08-03-23-51.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# 测试方案:精简导航与切分显示
|
||||
|
||||
时间戳:2026-05-08-03-23-51
|
||||
|
||||
## 静态检查
|
||||
|
||||
- 执行 `npm run lint`。
|
||||
- 执行 `npm run build`。
|
||||
|
||||
## 关键业务场景验证
|
||||
|
||||
- 侧边栏不再显示“模型库”。
|
||||
- 顶部标题不再出现“模型库”分支。
|
||||
- “项目库”仍能进入项目浏览,并可查看模型相关内容。
|
||||
- 逆向工作区 DICOM 范围卡片不再显示说明文案和预存五点按钮。
|
||||
- 启用模型切分后不再出现红色平面,但模型仍按 DICOM 范围裁切。
|
||||
|
||||
## 回归风险
|
||||
|
||||
- 删除 `MODELS` 枚举可能影响路由分支,需要通过 TypeScript 检查覆盖。
|
||||
- 删除红色平面只应影响显示,不应影响 clipping plane。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- `rg` 不再命中 `ViewType.MODELS`、侧边栏 `模型库`、`预存五点`、`lowerCutPlane`、`upperCutPlane`。
|
||||
- `npm run lint` 和 `npm run build` 均通过。
|
||||
- 重新部署后访问 `http://192.168.3.11:4000/` 返回 200。
|
||||
|
||||
## 无法测试的风险
|
||||
|
||||
- 当前无法直接在用户浏览器里观察红色平面是否消失,需要用户刷新页面后确认 WebGL 视觉效果。
|
||||
|
||||
## 人工审核状态
|
||||
|
||||
用户已在项目工作流历史中确认后续直接执行,本次不等待二次人工审核。
|
||||
|
||||
## 执行结果
|
||||
|
||||
- `npm run lint`:首次发现删除预存状态后残留 `setPreloadMessage` 调用;清理后通过。
|
||||
- `npm run build`:通过;仅保留 Vite chunk 大小提示。
|
||||
- `rg` 验证:源码和最新构建产物不再包含 `ViewType.MODELS`、`模型库`、`预存五点`、`preloadMessage`、`lowerCutPlane`、`upperCutPlane`、`显示范围支持`。
|
||||
18
工程分析/经验记录.md
18
工程分析/经验记录.md
@@ -757,3 +757,21 @@ C. 解决问题方案
|
||||
D. 后续如何避免问题
|
||||
|
||||
所有模型切分、分割预览、Mask 展示都必须明确数据来源。没有真实体素化或真实语义分割结果时,不应使用伪造 mask 图像;同一业务动作只保留一套权威控制状态,模型切分应默认由 DICOM 切片范围驱动。
|
||||
|
||||
## 2026-05-08-03-23-51 重复入口与辅助显示清理
|
||||
|
||||
A. 具体问题
|
||||
|
||||
系统同时提供“项目库”和“模型库”两个入口,实际模型库只是复用项目库的模型页,信息架构重复;逆向工作区的 DICOM 范围说明和五点预存按钮已经不必要;模型切分启用后出现红色辅助平面,干扰观察。
|
||||
|
||||
B. 产生问题原因
|
||||
|
||||
早期为了快速进入模型页新增了独立 `MODELS` 路由和侧边栏入口;DICOM 范围控件在调试缓存阶段加入了预存点位;模型切分为了强调切割位置渲染了半透明平面,但用户当前更需要干净的真实裁切效果。
|
||||
|
||||
C. 解决问题方案
|
||||
|
||||
删除 `ViewType.MODELS`、侧边栏“模型库”和 `App.tsx` 中对应分支,保留“项目库”统一承载 DICOM、模型、Mask 视图;删除 DICOM 范围说明、五点预存状态和按钮,保留 `fusionVolumeCacheRef` 对实际加载范围的缓存;删除切分辅助平面 Mesh,只保留两张 clipping plane 裁切模型。
|
||||
|
||||
D. 后续如何避免问题
|
||||
|
||||
新增导航入口前先判断是否是独立业务对象和独立工作流,避免用多个入口指向同一组件状态;辅助视觉元素应在用户确认有价值后保留,医学三维视图中默认少放装饰或调试平面,避免遮挡真实数据。
|
||||
|
||||
45
工程分析/需求分析-2026-05-08-03-23-51.md
Normal file
45
工程分析/需求分析-2026-05-08-03-23-51.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# 需求分析:清理重复入口、范围说明和切分平面
|
||||
|
||||
时间戳:2026-05-08-03-23-51
|
||||
|
||||
## 原始需求
|
||||
|
||||
1. 系统中同时有“项目库”和“模型库”,功能重复,删除一个。
|
||||
2. 逆向工作区中删除“显示范围支持 M-N,两个端点可双向调整;范围变化只改变可视化切片,不改变模型原始位姿。”相关内容;缓存保持最初始 `1~最终` 的情况即可。
|
||||
3. 点击模型切分后出现一层很红的平面,需要删除这层平面。
|
||||
|
||||
## 业务目标
|
||||
|
||||
- 保留更通用的“项目库”,移除重复的“模型库”导航和路由。
|
||||
- 简化 DICOM 切片范围区域,只保留范围条和当前范围显示。
|
||||
- 删除五点预存按钮与预存状态,保留现有按范围加载后的缓存机制;初始完整范围会自然进入缓存。
|
||||
- 模型切分仍继续使用 DICOM 范围裁切 STL,但不渲染红色辅助平面。
|
||||
|
||||
## 输入与输出
|
||||
|
||||
- 输入:用户在侧边栏和逆向工作区的交互。
|
||||
- 输出:侧边栏只有“项目库”入口;DICOM 范围卡片更简洁;模型切分无红色平面,仅模型本身被裁切。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- `WebSite/src/App.tsx`
|
||||
- `WebSite/src/components/Sidebar.tsx`
|
||||
- `WebSite/src/types.ts`
|
||||
- `WebSite/src/components/ReverseWorkspace.tsx`
|
||||
- `工程分析/经验记录.md`
|
||||
|
||||
## 约束
|
||||
|
||||
- 不删除项目库里的模型查看能力,只删除重复的独立“模型库”入口。
|
||||
- 不改变 DICOM/STL 后端接口。
|
||||
- 不恢复伪 mask 或独立切分帧滑块。
|
||||
|
||||
## 风险点
|
||||
|
||||
- 删除 `ViewType.MODELS` 后必须清理所有引用,避免 TypeScript 报错。
|
||||
- 删除手动预存 UI 后,需确认 `fusionVolumeCacheRef` 仍用于当前加载范围缓存。
|
||||
- 删除红色平面时不能删除 clipping plane 本身,否则模型切分会失效。
|
||||
|
||||
## 待确认事项
|
||||
|
||||
- 用户已确认后续直接执行,本次不等待二次人工审核。
|
||||
Reference in New Issue
Block a user