From 97edf35bd089ea0f80c251eb2a96759dfc2f6f9c Mon Sep 17 00:00:00 2001 From: admin <572701190@qq.com> Date: Thu, 7 May 2026 18:55:14 +0800 Subject: [PATCH] =?UTF-8?q?2026-05-07-18-42-53=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=8F=AF=E8=A7=86=E5=8C=96=E5=B7=A5=E5=85=B7=E6=A0=8F=E5=92=8C?= =?UTF-8?q?=E6=9E=84=E4=BB=B6ID=E8=81=94=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebSite/server.ts | 72 ++++++- WebSite/src/components/ProjectLibrary.tsx | 74 +++---- WebSite/src/components/ReverseWorkspace.tsx | 201 ++++++++------------ WebSite/src/lib/api.ts | 7 +- WebSite/src/types.ts | 8 + 工程分析/实现方案-2026-05-07-18-42-53.md | 79 ++++++++ 工程分析/测试方案-2026-05-07-18-42-53.md | 46 +++++ 工程分析/经验记录.md | 18 ++ 工程分析/需求分析-2026-05-07-18-42-53.md | 43 +++++ 9 files changed, 393 insertions(+), 155 deletions(-) create mode 100644 工程分析/实现方案-2026-05-07-18-42-53.md create mode 100644 工程分析/测试方案-2026-05-07-18-42-53.md create mode 100644 工程分析/需求分析-2026-05-07-18-42-53.md diff --git a/WebSite/server.ts b/WebSite/server.ts index d161d04..ba821aa 100644 --- a/WebSite/server.ts +++ b/WebSite/server.ts @@ -10,6 +10,13 @@ type ProjectStatus = 'pending' | 'completed' | 'processing'; type DicomPlane = 'axial' | 'sagittal' | 'coronal'; type DicomDisplayMode = 'default' | 'bone' | 'soft' | 'contrast'; +interface ModuleStyleRecord { + visible: boolean; + color: string; + opacity: number; + partId: number; +} + interface UserRecord { id: number; name: string; @@ -33,6 +40,7 @@ interface ProjectRecord { maskFormats: Array<'nii' | 'nii.gz'>; exportedMaskCount: number; isDefault?: boolean; + moduleStyles: Record; } interface SessionRecord { @@ -70,6 +78,7 @@ const dicomVolumeCache = new Map(); const modelPreviewCache = new Map(); +const defaultModuleColors = ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6', '#14b8a6', '#f97316', '#64748b', '#ec4899']; interface DicomAttributes { patientName: string; @@ -146,6 +155,37 @@ function publicSession(state: AppState) { }; } +function clampNumber(value: number, min: number, max: number) { + return Math.max(min, Math.min(max, value)); +} + +function normalizeModuleStyle( + style: Partial | undefined, + index: number, +): ModuleStyleRecord { + const opacity = typeof style?.opacity === 'number' && Number.isFinite(style.opacity) ? style.opacity : 0.72; + const partId = typeof style?.partId === 'number' && Number.isFinite(style.partId) ? style.partId : index + 1; + + return { + visible: typeof style?.visible === 'boolean' ? style.visible : true, + color: typeof style?.color === 'string' && /^#[0-9a-fA-F]{6}$/.test(style.color) + ? style.color + : defaultModuleColors[index % defaultModuleColors.length], + opacity: clampNumber(opacity, 0.1, 1), + partId: clampNumber(Math.round(partId), 1, 255), + }; +} + +function buildModuleStyles( + stlFiles: string[], + existing?: Record>, +) { + return stlFiles.reduce>((acc, fileName, index) => { + acc[fileName] = normalizeModuleStyle(existing?.[fileName], index); + return acc; + }, {}); +} + function buildDefaultProject(): ProjectRecord { const stlFiles = listFiles(modelDir, '.stl'); @@ -163,6 +203,7 @@ function buildDefaultProject(): ProjectRecord { maskFormats: ['nii', 'nii.gz'], exportedMaskCount: 0, isDefault: true, + moduleStyles: buildModuleStyles(stlFiles), }; } @@ -180,6 +221,7 @@ function buildEmptyProject(name: string): ProjectRecord { stlFiles: [], maskFormats: ['nii', 'nii.gz'], exportedMaskCount: 0, + moduleStyles: {}, }; } @@ -197,13 +239,16 @@ function defaultState(): AppState { function normalizeState(state: AppState): AppState { const defaultProject = buildDefaultProject(); + const savedDefaultProject = state.projects?.find((project) => project.id === defaultProject.id); const customProjects = Array.isArray(state.projects) ? state.projects .filter((project) => project.id !== defaultProject.id) .map((project) => ({ ...project, + stlFiles: Array.isArray(project.stlFiles) ? project.stlFiles : [], exportedMaskCount: project.exportedMaskCount ?? 0, maskFormats: project.maskFormats ?? ['nii', 'nii.gz'], + moduleStyles: buildModuleStyles(Array.isArray(project.stlFiles) ? project.stlFiles : [], project.moduleStyles), })) : []; @@ -212,8 +257,9 @@ function normalizeState(state: AppState): AppState { projects: [ { ...defaultProject, - name: state.projects?.find((project) => project.id === defaultProject.id)?.name ?? defaultProject.name, - exportedMaskCount: state.projects?.find((project) => project.id === defaultProject.id)?.exportedMaskCount ?? 0, + name: savedDefaultProject?.name ?? defaultProject.name, + exportedMaskCount: savedDefaultProject?.exportedMaskCount ?? 0, + moduleStyles: buildModuleStyles(defaultProject.stlFiles, savedDefaultProject?.moduleStyles), }, ...customProjects, ], @@ -1052,6 +1098,28 @@ async function startServer() { res.json({ ok: true, deletedId: deleted.id }); }); + app.patch('/api/projects/:projectId/module-styles', (req, res) => { + const incoming = req.body?.moduleStyles; + if (!incoming || typeof incoming !== 'object' || Array.isArray(incoming)) { + res.status(400).json({ message: '构件样式数据无效' }); + return; + } + + const state = readState(); + const project = findProject(state, req.params.projectId); + if (!project) { + res.status(404).json({ message: '项目不存在' }); + return; + } + + project.moduleStyles = buildModuleStyles(project.stlFiles, { + ...(project.moduleStyles ?? {}), + ...(incoming as Record>), + }); + writeState(state); + res.json(project); + }); + app.get('/api/projects/:projectId/dicom-preview', (req, res) => { const project = findProject(readState(), req.params.projectId); if (!project) { diff --git a/WebSite/src/components/ProjectLibrary.tsx b/WebSite/src/components/ProjectLibrary.tsx index ada9910..92d579a 100644 --- a/WebSite/src/components/ProjectLibrary.tsx +++ b/WebSite/src/components/ProjectLibrary.tsx @@ -21,20 +21,13 @@ import { Upload } from 'lucide-react'; import * as THREE from 'three'; -import { DicomInfo, DicomPreview, Project } from '../types'; +import { DicomInfo, DicomPreview, ModuleStyle, Project } from '../types'; import { api, downloadDicomArchive, downloadMask } from '../lib/api'; type Plane = 'axial' | 'sagittal' | 'coronal'; type DisplayMode = DicomPreview['mode']; type SolidityLevel = 'standard' | 'fine' | 'ultra' | 'solid'; -interface ModuleStyle { - visible: boolean; - color: string; - opacity: number; - partId: number; -} - interface ModelPose { rotateX: number; rotateY: number; @@ -708,6 +701,28 @@ export default function ProjectLibrary({ const sliceTotal = dicomPreview?.total ?? selectedProject?.dicomCount ?? 0; const selectedSolidity = solidityOptions.find((option) => option.id === solidityLevel) ?? solidityOptions[0]; + const makeDefaultModuleStyle = (index: number, fallback?: Partial): ModuleStyle => ({ + visible: fallback?.visible ?? true, + color: fallback?.color ?? defaultModuleColors[index % defaultModuleColors.length], + opacity: fallback?.opacity ?? 0.72, + partId: Math.max(1, Math.min(255, Math.round(fallback?.partId ?? index + 1))), + }); + + const commitModuleStyles = (next: Record) => { + setModuleStyles(next); + if (!selectedProject) { + return; + } + api.updateProjectModuleStyles(selectedProject.id, next) + .then((updated) => { + setSelectedProject(updated); + setProjects((items) => items.map((item) => (item.id === updated.id ? updated : item))); + }) + .catch((error) => { + setActionMessage(error instanceof Error ? error.message : '构件样式保存失败'); + }); + }; + useEffect(() => { setViewMode(initialViewMode); }, [initialViewMode]); @@ -715,12 +730,7 @@ export default function ProjectLibrary({ useEffect(() => { const next: Record = {}; stlFiles.forEach((fileName, index) => { - next[fileName] = moduleStyles[fileName] ?? { - visible: true, - color: defaultModuleColors[index % defaultModuleColors.length], - opacity: 0.72, - partId: index + 1, - }; + next[fileName] = makeDefaultModuleStyle(index, selectedProject?.moduleStyles?.[fileName] ?? moduleStyles[fileName]); }); setModuleStyles(next); setSliceIndex(0); @@ -773,17 +783,15 @@ export default function ProjectLibrary({ }, [sliceIndex, sliceTotal]); const updateModuleStyle = (fileName: string, partial: Partial) => { - setModuleStyles(prev => ({ - ...prev, - [fileName]: { - visible: true, - color: '#3b82f6', - opacity: 0.72, - partId: 1, - ...(prev[fileName] ?? {}), + const index = Math.max(0, stlFiles.indexOf(fileName)); + const next = { + ...moduleStyles, + [fileName]: makeDefaultModuleStyle(index, { + ...(moduleStyles[fileName] ?? selectedProject?.moduleStyles?.[fileName]), ...partial, - }, - })); + }), + }; + commitModuleStyles(next); }; const updateModulePartId = (fileName: string, value: number) => { @@ -793,18 +801,14 @@ export default function ProjectLibrary({ const toggleAllModules = () => { const nextVisible = !allModulesVisible; - setModuleStyles(prev => { - const next = { ...prev }; - stlFiles.forEach((fileName, index) => { - next[fileName] = { - visible: nextVisible, - color: next[fileName]?.color ?? defaultModuleColors[index % defaultModuleColors.length], - opacity: next[fileName]?.opacity ?? 0.72, - partId: next[fileName]?.partId ?? index + 1, - }; + const next = { ...moduleStyles }; + stlFiles.forEach((fileName, index) => { + next[fileName] = makeDefaultModuleStyle(index, { + ...(next[fileName] ?? selectedProject?.moduleStyles?.[fileName]), + visible: nextVisible, }); - return next; }); + commitModuleStyles(next); }; const stepSlice = (delta: number) => { @@ -1290,7 +1294,7 @@ export default function ProjectLibrary({
-

整体位姿

+

模型位姿

)} -
-
-
-

DICOM 切片范围

- - {displayStart + 1} - {displayEnd + 1} - -
-
- - -
-

- DICOM 以黑色体数据长方体显示,表面贴附当前范围的最后一张 CT 切片。 -

-
- -
-
-

模型位姿

- -
-
- {[ - { key: 'rotateX' as const, label: '旋转 X', min: -180, max: 180, step: 1, value: modelPose.rotateX }, - { key: 'rotateY' as const, label: '旋转 Y', min: -180, max: 180, step: 1, value: modelPose.rotateY }, - { key: 'rotateZ' as const, label: '旋转 Z', min: -180, max: 180, step: 1, value: modelPose.rotateZ }, - { key: 'translateX' as const, label: '平移 X', min: -2, max: 2, step: 0.05, value: modelPose.translateX }, - { key: 'translateY' as const, label: '平移 Y', min: -2, max: 2, step: 0.05, value: modelPose.translateY }, - { key: 'translateZ' as const, label: '平移 Z', min: -2, max: 2, step: 0.05, value: modelPose.translateZ }, - { key: 'scale' as const, label: '缩放', min: 0.5, max: 2, step: 0.05, value: modelPose.scale }, - ].map((item) => ( - - ))} -
+
+
+

DICOM 切片范围

+ + {displayStart + 1} - {displayEnd + 1} / {project?.dicomCount ?? 0} +
+ +

+ 默认从第 1 张开始显示,滑条控制融合体使用到第几张切片。 +

@@ -740,7 +692,7 @@ export default function ReverseWorkspace({ projectId }: { projectId: string }) {
-

整体位姿

+

模型位姿

+
+ {[ + { key: 'rotateX' as const, label: '旋转 X', min: -180, max: 180, step: 1, value: modelPose.rotateX }, + { key: 'rotateY' as const, label: '旋转 Y', min: -180, max: 180, step: 1, value: modelPose.rotateY }, + { key: 'rotateZ' as const, label: '旋转 Z', min: -180, max: 180, step: 1, value: modelPose.rotateZ }, + { key: 'translateX' as const, label: '平移 X', min: -2, max: 2, step: 0.05, value: modelPose.translateX }, + { key: 'translateY' as const, label: '平移 Y', min: -2, max: 2, step: 0.05, value: modelPose.translateY }, + { key: 'translateZ' as const, label: '平移 Z', min: -2, max: 2, step: 0.05, value: modelPose.translateZ }, + { key: 'scale' as const, label: '缩放', min: 0.5, max: 2, step: 0.05, value: modelPose.scale }, + ].map((item) => ( + + ))} +
@@ -831,16 +808,6 @@ export default function ReverseWorkspace({ projectId }: { projectId: string }) {
- -
-
- - Metadata -
-
-                {`{ project: "${project?.id ?? projectId}", display: "${selectedDisplay.label}" }`}
-              
-
diff --git a/WebSite/src/lib/api.ts b/WebSite/src/lib/api.ts index 06cd7f6..f6d0195 100644 --- a/WebSite/src/lib/api.ts +++ b/WebSite/src/lib/api.ts @@ -1,4 +1,4 @@ -import { DicomFusionVolume, DicomInfo, DicomPreview, OverviewSummary, Project, SessionState, UserRecord } from '../types'; +import { DicomFusionVolume, DicomInfo, DicomPreview, ModuleStyle, OverviewSummary, Project, SessionState, UserRecord } from '../types'; async function request(path: string, options: RequestInit = {}): Promise { const response = await fetch(path, { @@ -50,6 +50,11 @@ export const api = { request<{ ok: boolean; deletedId: string }>(`/api/projects/${projectId}`, { method: 'DELETE', }), + updateProjectModuleStyles: (projectId: string, moduleStyles: Record) => + request(`/api/projects/${projectId}/module-styles`, { + method: 'PATCH', + body: JSON.stringify({ moduleStyles }), + }), getDicomPreview: (projectId: string, slice: number, plane: DicomPreview['plane'] = 'axial', mode: DicomPreview['mode'] = 'default') => request(`/api/projects/${projectId}/dicom-preview?slice=${slice}&plane=${plane}&mode=${mode}`), getDicomFusionVolume: (projectId: string, start: number, end: number, mode: DicomPreview['mode'] = 'soft') => diff --git a/WebSite/src/types.ts b/WebSite/src/types.ts index dab133f..0df885a 100644 --- a/WebSite/src/types.ts +++ b/WebSite/src/types.ts @@ -21,6 +21,14 @@ export interface Project { maskFormats?: Array<'nii' | 'nii.gz'>; exportedMaskCount?: number; isDefault?: boolean; + moduleStyles?: Record; +} + +export interface ModuleStyle { + visible: boolean; + color: string; + opacity: number; + partId: number; } export interface MaskMapping { diff --git a/工程分析/实现方案-2026-05-07-18-42-53.md b/工程分析/实现方案-2026-05-07-18-42-53.md new file mode 100644 index 0000000..531e6ea --- /dev/null +++ b/工程分析/实现方案-2026-05-07-18-42-53.md @@ -0,0 +1,79 @@ +# 实现方案 - 2026-05-07-18-42-53 + +## 修改目标 + +1. 移除逆向工作区可视化工具栏底部 Metadata 信息块。 +2. 将 DICOM 融合切片范围控制改为融合视角下方单滑条,滑条值表示展示到第几张切片。 +3. 将整体位姿控制区统一命名为“模型位姿”,并保留现有位姿保存、选择、重置和调节能力。 +4. 将构件 ID 从纯组件状态提升为后端项目状态字段,使项目库 3D 模型与逆向工作区可视化工具栏联动。 + +## 涉及路径 + +- `WebSite/src/components/ReverseWorkspace.tsx` +- `WebSite/src/components/ProjectLibrary.tsx` +- `WebSite/src/server.ts` +- `WebSite/src/types.ts` +- `工程分析/经验记录.md` + +## 技术路线 + +### 构件 ID 联动 + +- 在后端项目数据中增加 `moduleStyles` 字段,存储每个 STL 文件的: + - `visible` + - `color` + - `opacity` + - `partId` +- 项目库与逆向工作区读取项目时都使用同一个 `project.moduleStyles` 初始化。 +- 任一页面修改 ID、颜色、透明度、显示状态时,通过 `PATCH /api/projects/:projectId/module-styles` 写回后端。 +- 前端本地状态更新后同步调用 API,保证跨页面和跨浏览器一致。 + +### 融合切片范围 + +- 保留后端接口需要的 `start/end` 参数。 +- 前端只展示一个滑条 `sliceEnd`,内部固定 `start = 0`,`end = sliceEnd`。 +- 滑条放在影像与模型融合视角下方,便于操作与结果对应。 + +### 位姿模块整合 + +- 将工具栏内的“整体位姿”标题改为“模型位姿”。 +- 保留模型显示、位姿保存/选择、旋转、平移、缩放与重置功能,但减少重复命名。 + +### UI 清理 + +- 删除可视化工具栏底部 Metadata 代码块。 + +## 数据流 + +1. 后端读取或重置项目时生成默认 `moduleStyles`,`partId` 默认从 1 到 N。 +2. 项目库修改构件 ID 后调用 API 保存。 +3. 逆向工作区加载项目时读取最新 `moduleStyles`。 +4. 逆向工作区修改构件 ID 后同样调用 API 保存。 +5. 融合视角滑条变化后触发 `dicom-fusion-volume?start=0&end=` 重新加载体数据。 + +## 兼容性与回滚方案 + +- 对旧 `state.json` 做兼容:项目没有 `moduleStyles` 时由后端按 STL 列表自动补齐。 +- 若新增 API 出现问题,前端仍可回退为本地状态,但 ID 联动会退化。 +- 所有修改集中在项目状态、项目库与逆向工作区,回滚本次 commit 即可恢复。 + +## 预计文件变更 + +- `server.ts`:补齐项目 moduleStyles、增加更新 API。 +- `types.ts`:补充项目 moduleStyles 类型。 +- `ProjectLibrary.tsx`:读取/保存共享 moduleStyles。 +- `ReverseWorkspace.tsx`:读取/保存共享 moduleStyles,调整切片范围 UI,删除 Metadata,重命名位姿模块。 + +## 人工审核状态 + +用户已声明本次不需要二次人工确认,按默认执行确认规则直接执行。 + +## 执行记录 + +- 已新增共享构件样式数据 `moduleStyles`,包含 `visible/color/opacity/partId`。 +- 已新增 `PATCH /api/projects/:projectId/module-styles`,用于项目库与逆向工作区同步构件 ID、颜色、透明度、显示隐藏状态。 +- 已将项目库和逆向工作区的构件 ID 读写改为同一后端项目状态。 +- 已删除逆向工作区可视化工具栏下方 `Metadata` 信息块。 +- 已将融合视角下方 DICOM 切片范围改为单一进度条,内部固定从第 1 张开始显示。 +- 已将可视化工具栏中的“整体位姿”改为“模型位姿”,并合并位姿保存、选择和各项旋转/平移/缩放控制。 +- 已同步将项目库 3D 模型中的“整体位姿”标题改为“模型位姿”。 diff --git a/工程分析/测试方案-2026-05-07-18-42-53.md b/工程分析/测试方案-2026-05-07-18-42-53.md new file mode 100644 index 0000000..cd77bdc --- /dev/null +++ b/工程分析/测试方案-2026-05-07-18-42-53.md @@ -0,0 +1,46 @@ +# 测试方案 - 2026-05-07-18-42-53 + +## 静态检查 + +- 执行 `npm run lint`,确认 TypeScript 类型检查通过。 +- 执行 `npm run build`,确认生产构建通过。 + +## 单元或集成测试 + +- 使用现有 API 和页面构建流程做集成验证: + - `GET /api/projects` 返回项目数据且包含共享 `moduleStyles`。 + - `PATCH /api/projects/:projectId/module-styles` 可保存构件 ID。 + - 重新读取项目后构件 ID 保持一致。 + +## 关键业务场景验证 + +1. 项目库 3D 模型构件层级修改 ID 后,进入逆向工作区可视化工具栏显示同一 ID。 +2. 逆向工作区修改 ID 后,返回项目库 3D 模型显示同一 ID。 +3. 可视化工具栏不再显示 Metadata。 +4. 融合视角下方只有一个 DICOM 切片范围滑条,不再暴露起点/终点输入。 +5. 位姿控制区域标题显示“模型位姿”。 + +## 医学影像数据相关边界验证 + +- 切片范围滑条最小值不小于 1,最大值不超过 DICOM 总切片数。 +- API 请求仍使用合理 `start/end`,避免空体数据。 +- 构件 ID 限定为 `1~255`,防止与背景 `0` 冲突。 + +## 回归风险 + +- 共享 moduleStyles 可能影响已有颜色、透明度、显示隐藏控制。 +- 融合体请求频繁变化可能导致加载状态更新较频繁。 + +## 人工审核状态 + +用户已声明本次不需要二次人工确认,按默认执行确认规则直接执行。 + +## 执行结果 + +- `npm run lint`:通过。 +- `npm run build`:通过,仅保留 Vite 大 chunk 体积提醒。 +- 重新部署:已通过 `tmux` 重启 `revoxelseg-dicom` 服务,运行在 `http://0.0.0.0:4000/`。 +- `curl -I http://127.0.0.1:4000/`:返回 `HTTP/1.1 200 OK`。 +- `GET /api/projects/head-ct-demo`:返回默认项目,包含 9 个 STL 构件的 `moduleStyles`。 +- `PATCH /api/projects/head-ct-demo/module-styles`:可修改构件 ID 和可见性。 +- 构件 ID 边界:提交 `partId=0` 后服务端返回 `partId=1`,符合不可修改为 0 的约束。 diff --git a/工程分析/经验记录.md b/工程分析/经验记录.md index 3626c95..ecc99d7 100644 --- a/工程分析/经验记录.md +++ b/工程分析/经验记录.md @@ -667,3 +667,21 @@ C. 解决问题方案 D. 后续如何避免问题 构件层级状态应作为统一结构在不同页面复用;任何新增构件字段都要检查初始化、单项更新、全局更新和默认 fallback 四条路径;逆向工作区的可视化控制应与项目库模型页保持一致,避免用户在两个页面学两套交互。 + +## 2026-05-07-18-42-53 构件 ID 跨页面联动 + +A. 具体问题 + +项目库 3D 模型和逆向工作区可视化工具栏都能修改构件 ID,但两边原先分别维护本地状态,无法保证 ID、显示隐藏、颜色和透明度一致。 + +B. 产生问题原因 + +构件样式属于项目级配置,但旧实现只保存在 React 组件 state 中,页面切换或不同浏览器访问时不会自动共享。 + +C. 解决问题方案 + +在项目数据中新增 `moduleStyles` 字段,并新增 `PATCH /api/projects/:projectId/module-styles` 接口。项目库和逆向工作区都读写同一后端状态,服务端统一补齐默认值并将 `partId` 限制在 `1~255`。 + +D. 后续如何避免问题 + +凡是需要跨页面、跨浏览器一致的配置项,优先放入后端项目状态;前端可以保留本地 state 提升响应速度,但必须通过 API 持久化并从项目数据恢复。 diff --git a/工程分析/需求分析-2026-05-07-18-42-53.md b/工程分析/需求分析-2026-05-07-18-42-53.md new file mode 100644 index 0000000..fb4d5ca --- /dev/null +++ b/工程分析/需求分析-2026-05-07-18-42-53.md @@ -0,0 +1,43 @@ +# 需求分析 - 2026-05-07-18-42-53 + +## 原始需求摘要 + +本次需要优化逆向工作区与项目库之间的可视化控制体验: + +1. 删除可视化工具栏下方的 `Metadata` 信息块。 +2. DICOM 切片范围不再使用起点/终点输入,改为影像与模型融合视角下方的一条进度条控制切片范围。 +3. 将模型位姿模块与整体位姿整合,统一命名为“模型位姿”。 +4. 项目库 3D 模型中的构件 ID 与可视化工具栏中的构件 ID 保持联动。 + +## 业务目标 + +- 让影像与模型融合视角更聚焦实际操作,减少无效信息。 +- 降低 DICOM 切片范围操作复杂度,使用单进度条表达当前展示范围。 +- 统一模型位姿概念,避免“模型位姿”和“整体位姿”并存造成理解负担。 +- 保证同一项目的构件 ID 在项目库和逆向工作区中一致。 + +## 输入与输出 + +- 输入:用户在项目库或逆向工作区中修改构件 ID、切换切片范围、调整模型位姿。 +- 输出: + - 可视化工具栏不再展示 Metadata。 + - 融合视角下方展示 DICOM 切片范围滑条。 + - 位姿控制区域统一显示为“模型位姿”。 + - 项目库和可视化工具栏中的构件 ID 双向同步。 + +## 影响范围 + +- `WebSite/src/components/ReverseWorkspace.tsx` +- `WebSite/src/components/ProjectLibrary.tsx` +- 可能涉及后端项目状态 API 与类型定义,用于持久化或共享构件 ID。 +- `工程分析/经验记录.md` + +## 风险点 + +- 如果构件 ID 只保存在组件本地状态,项目库和逆向工作区可能仍不同步。 +- 切片范围由两个输入改为一个滑条后,需要保持后端融合体数据请求仍有合理 `start/end`。 +- 位姿模块重命名和合并时需要避免破坏现有旋转、平移、缩放、保存位姿功能。 + +## 待确认问题 + +用户已明确本次需求分析、实现方案、测试方案、执行修改均不需要二次人工确认,因此按默认执行确认规则直接实施。