diff --git a/WebSite/src/App.tsx b/WebSite/src/App.tsx index bc62e14..6bffcbc 100644 --- a/WebSite/src/App.tsx +++ b/WebSite/src/App.tsx @@ -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.WORKSPACE) { + if (activeView === ViewType.PROJECTS || activeView === ViewType.MODELS || activeView === ViewType.WORKSPACE) { setSidebarCollapsed(true); } else { setSidebarCollapsed(false); @@ -106,6 +106,7 @@ export default function App() { {activeView === ViewType.OVERVIEW && '总体概况'} {activeView === ViewType.PROJECTS && '项目库'} + {activeView === ViewType.MODELS && '模型库'} {activeView === ViewType.WORKSPACE && '逆向工作区'} {activeView === ViewType.SYSTEM && '系统管理工作区'} @@ -135,6 +136,15 @@ export default function App() { }} /> )} + {activeView === ViewType.MODELS && ( + { + setActiveProjectId(projectId); + setActiveView(ViewType.WORKSPACE); + }} + /> + )} {activeView === ViewType.WORKSPACE && } {activeView === ViewType.SYSTEM && } diff --git a/WebSite/src/components/ProjectLibrary.tsx b/WebSite/src/components/ProjectLibrary.tsx index 8d4920b..ada9910 100644 --- a/WebSite/src/components/ProjectLibrary.tsx +++ b/WebSite/src/components/ProjectLibrary.tsx @@ -32,6 +32,7 @@ interface ModuleStyle { visible: boolean; color: string; opacity: number; + partId: number; } interface ModelPose { @@ -430,7 +431,7 @@ function NativeStlViewer({ }) .then((payload) => ({ payload, - style: styles[fileName] ?? { color: '#3b82f6', opacity: 0.72, visible: true }, + style: styles[fileName] ?? { color: '#3b82f6', opacity: 0.72, visible: true, partId: 1 }, })), ), ).then((results) => { @@ -492,7 +493,7 @@ function NativeStlViewer({ const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.Float32BufferAttribute(payload.vertices, 3)); geometry.computeVertexNormals(); - const style = styles[fileName] ?? { color: '#3b82f6', opacity: 0.72, visible: true }; + const style = styles[fileName] ?? { color: '#3b82f6', opacity: 0.72, visible: true, partId: 1 }; const materialOpacity = solidMode ? Math.max(style.opacity, 0.94) : style.opacity; const mesh = new THREE.Mesh( geometry, @@ -629,12 +630,18 @@ function NativeStlViewer({ ); } -export default function ProjectLibrary({ onReverse }: { onReverse: (projId: string) => void }) { +export default function ProjectLibrary({ + onReverse, + initialViewMode = 'dicom', +}: { + onReverse: (projId: string) => void; + initialViewMode?: 'dicom' | 'model' | 'mask'; +}) { const [search, setSearch] = useState(''); const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(true); const [selectedProject, setSelectedProject] = useState(null); - const [viewMode, setViewMode] = useState<'dicom' | 'model' | 'mask'>('dicom'); + const [viewMode, setViewMode] = useState<'dicom' | 'model' | 'mask'>(initialViewMode); const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); const [sliceIndex, setSliceIndex] = useState(0); const [plane, setPlane] = useState('axial'); @@ -701,6 +708,10 @@ export default function ProjectLibrary({ onReverse }: { onReverse: (projId: stri const sliceTotal = dicomPreview?.total ?? selectedProject?.dicomCount ?? 0; const selectedSolidity = solidityOptions.find((option) => option.id === solidityLevel) ?? solidityOptions[0]; + useEffect(() => { + setViewMode(initialViewMode); + }, [initialViewMode]); + useEffect(() => { const next: Record = {}; stlFiles.forEach((fileName, index) => { @@ -708,6 +719,7 @@ export default function ProjectLibrary({ onReverse }: { onReverse: (projId: stri visible: true, color: defaultModuleColors[index % defaultModuleColors.length], opacity: 0.72, + partId: index + 1, }; }); setModuleStyles(next); @@ -767,12 +779,18 @@ export default function ProjectLibrary({ onReverse }: { onReverse: (projId: stri visible: true, color: '#3b82f6', opacity: 0.72, + partId: 1, ...(prev[fileName] ?? {}), ...partial, }, })); }; + const updateModulePartId = (fileName: string, value: number) => { + const nextId = Math.max(1, Math.min(255, Math.round(Number.isFinite(value) ? value : 1))); + updateModuleStyle(fileName, { partId: nextId }); + }; + const toggleAllModules = () => { const nextVisible = !allModulesVisible; setModuleStyles(prev => { @@ -782,6 +800,7 @@ export default function ProjectLibrary({ onReverse }: { onReverse: (projId: stri visible: nextVisible, color: next[fileName]?.color ?? defaultModuleColors[index % defaultModuleColors.length], opacity: next[fileName]?.opacity ?? 0.72, + partId: next[fileName]?.partId ?? index + 1, }; }); return next; @@ -1340,7 +1359,7 @@ export default function ProjectLibrary({ onReverse }: { onReverse: (projId: stri
{stlFiles.map((fileName, i) => { const name = fileName.replace(/\.stl$/i, ''); - const style = moduleStyles[fileName] ?? { visible: true, color: defaultModuleColors[i % defaultModuleColors.length], opacity: 0.72 }; + const style = moduleStyles[fileName] ?? { visible: true, color: defaultModuleColors[i % defaultModuleColors.length], opacity: 0.72, partId: i + 1 }; return (

{name}

-

STL | {fileName}

+
+

STL | {fileName}

+ +
透明度 = [ + { id: 'standard', label: '标准', limit: 16000 }, + { id: 'fine', label: '精细', limit: 36000 }, + { id: 'ultra', label: '超精细', limit: 72000 }, + { id: 'solid', label: '实体', limit: 200000 }, +]; + const defaultModelPose: ModelPose = { rotateX: 0, rotateY: 0, @@ -87,16 +102,19 @@ function FusionThreeView({ project, volume, modelPose, - onModelPoseChange, + moduleStyles, + detailLimit, + solidMode, }: { project: Project; volume: DicomFusionVolume | null; modelPose: ModelPose; - onModelPoseChange: React.Dispatch>; + moduleStyles: Record; + detailLimit: number; + solidMode: boolean; }) { const containerRef = useRef(null); const modelPoseRef = useRef(modelPose); - const onModelPoseChangeRef = useRef(onModelPoseChange); const [status, setStatus] = useState('准备融合 DICOM 与 STL'); const [loadProgress, setLoadProgress] = useState(0); @@ -104,10 +122,6 @@ function FusionThreeView({ modelPoseRef.current = modelPose; }, [modelPose]); - useEffect(() => { - onModelPoseChangeRef.current = onModelPoseChange; - }, [onModelPoseChange]); - useEffect(() => { const container = containerRef.current; if (!container || !volume) return; @@ -190,14 +204,14 @@ function FusionThreeView({ }); setLoadProgress(42); - const stlFiles = project.stlFiles ?? []; + const stlFiles = (project.stlFiles ?? []).filter((fileName) => moduleStyles[fileName]?.visible !== false); let modelBaseScale = 1; let loadedModels = 0; let failedModels = 0; const loadedBounds: Array<{ min: THREE.Vector3; max: THREE.Vector3 }> = []; Promise.allSettled(stlFiles.map((fileName, index) => ( - fetch(`/api/projects/${project.id}/models/${encodeURIComponent(fileName)}/preview?limit=72000`) + fetch(`/api/projects/${project.id}/models/${encodeURIComponent(fileName)}/preview?limit=${detailLimit}`) .then((response) => { if (!response.ok) throw new Error('模型预览加载失败'); return response.json() as Promise; @@ -207,11 +221,18 @@ function FusionThreeView({ const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.Float32BufferAttribute(payload.vertices, 3)); geometry.computeVertexNormals(); - const material = new THREE.MeshStandardMaterial({ + const style = moduleStyles[fileName] ?? { + visible: true, color: moduleColors[index % moduleColors.length], - transparent: true, opacity: 0.72, - roughness: 0.48, + partId: index + 1, + }; + const materialOpacity = solidMode ? Math.max(style.opacity, 0.94) : style.opacity; + const material = new THREE.MeshStandardMaterial({ + color: style.color, + transparent: true, + opacity: materialOpacity, + roughness: solidMode ? 0.56 : 0.48, metalness: 0.03, side: THREE.DoubleSide, }); @@ -369,7 +390,7 @@ function FusionThreeView({ renderer.dispose(); container.innerHTML = ''; }; - }, [project.id, project.stlFiles?.join('|'), volume]); + }, [project.id, project.stlFiles?.join('|'), volume, JSON.stringify(moduleStyles), detailLimit, solidMode]); return (
@@ -404,6 +425,14 @@ export default function ReverseWorkspace({ projectId }: { projectId: string }) { const [sliceStart, setSliceStart] = useState(0); const [sliceEnd, setSliceEnd] = useState(49); const [modelPose, setModelPose] = useState(defaultModelPose); + const [displayLevel, setDisplayLevel] = useState('standard'); + const [moduleStyles, setModuleStyles] = useState>({}); + const [savedPoses, setSavedPoses] = useState>([ + { id: 'default', name: '默认', pose: defaultModelPose }, + { id: 'top', name: '俯视', pose: { ...defaultModelPose, rotateX: 0, rotateY: 0, rotateZ: 0 } }, + { id: 'side', name: '侧视', pose: { ...defaultModelPose, rotateX: 0, rotateY: 90, rotateZ: 0 } }, + ]); + const [selectedPoseId, setSelectedPoseId] = useState('default'); const [isRegistering, setIsRegistering] = useState(false); const [progress, setProgress] = useState(0); const [project, setProject] = useState(null); @@ -443,6 +472,16 @@ export default function ReverseWorkspace({ projectId }: { projectId: string }) { setSliceStart(0); setSliceEnd(end); setModelPose(defaultModelPose); + const nextStyles: Record = {}; + (item.stlFiles ?? []).forEach((fileName, index) => { + nextStyles[fileName] = { + visible: true, + color: moduleColors[index % moduleColors.length], + opacity: 0.72, + partId: index + 1, + }; + }); + setModuleStyles(nextStyles); }).catch(() => { setProject(null); setFusionVolume(null); @@ -482,11 +521,48 @@ export default function ReverseWorkspace({ projectId }: { projectId: string }) { ...current, ...partial, })); + setSelectedPoseId('custom'); + }; + + const updateModuleStyle = (fileName: string, partial: Partial) => { + setModuleStyles((current) => ({ + ...current, + [fileName]: { + visible: true, + color: '#3b82f6', + opacity: 0.72, + partId: 1, + ...(current[fileName] ?? {}), + ...partial, + }, + })); + }; + + const updateModulePartId = (fileName: string, value: number) => { + updateModuleStyle(fileName, { partId: clamp(Math.round(Number.isFinite(value) ? value : 1), 1, 255) }); + }; + + const saveCurrentPose = () => { + const nextPose = { + id: `pose-${Date.now()}`, + name: `位姿${savedPoses.length - 2}`, + pose: { ...modelPose }, + }; + setSavedPoses((current) => [...current, nextPose]); + setSelectedPoseId(nextPose.id); + }; + + const selectPose = (poseId: string) => { + const selected = savedPoses.find((item) => item.id === poseId); + if (!selected) return; + setSelectedPoseId(poseId); + setModelPose(selected.pose); }; const maxSlice = Math.max((project?.dicomCount ?? 1) - 1, 0); const displayStart = Math.min(sliceStart, sliceEnd); const displayEnd = Math.max(sliceStart, sliceEnd); + const selectedDisplay = displayOptions.find((item) => item.id === displayLevel) ?? displayOptions[0]; return (
@@ -540,7 +616,9 @@ export default function ReverseWorkspace({ projectId }: { projectId: string }) { project={project} volume={fusionVolume} modelPose={modelPose} - onModelPoseChange={setModelPose} + moduleStyles={moduleStyles} + detailLimit={selectedDisplay.limit} + solidMode={displayLevel === 'solid'} /> ) : (
@@ -636,37 +714,122 @@ export default function ReverseWorkspace({ projectId }: { projectId: string }) {

- - 分割 Mask + + 可视化工具栏

-
- {mappings.map((mapping, index) => ( - - ))} +
+
+

模型显示

+
+ {displayOptions.map((option) => ( + + ))} +
+
- +
+
+

整体位姿

+ +
+ + +
+ +
+
+

构件层级

+ {project?.stlFiles?.length ?? 0} +
+
+ {(project?.stlFiles ?? []).map((fileName, index) => { + const style = moduleStyles[fileName] ?? { + visible: true, + color: moduleColors[index % moduleColors.length], + opacity: 0.72, + partId: index + 1, + }; + return ( +
+
+ updateModuleStyle(fileName, { color: event.target.value })} + className="h-7 w-7 shrink-0 rounded border border-white bg-white p-0.5" + title="模型颜色" + /> +
+

{fileName.replace(/\.stl$/i, '')}

+ +
+ +
+
+ 透明度 + updateModuleStyle(fileName, { opacity: Number(event.target.value) })} + className="min-w-0 flex-1 accent-blue-600" + /> + {Math.round(style.opacity * 100)}% +
+
+ ); + })} +
+
@@ -675,7 +838,7 @@ export default function ReverseWorkspace({ projectId }: { projectId: string }) { Metadata
-                {`{ project: "${project?.id ?? projectId}", format: "nii.gz" }`}
+                {`{ project: "${project?.id ?? projectId}", display: "${selectedDisplay.label}" }`}
               
diff --git a/WebSite/src/components/Sidebar.tsx b/WebSite/src/components/Sidebar.tsx index 756fd8d..5d44640 100644 --- a/WebSite/src/components/Sidebar.tsx +++ b/WebSite/src/components/Sidebar.tsx @@ -31,6 +31,7 @@ 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: Box, label: '逆向工作区' }, { id: ViewType.SYSTEM, icon: Settings, label: '系统管理工作区' }, ]; diff --git a/WebSite/src/types.ts b/WebSite/src/types.ts index f233845..dab133f 100644 --- a/WebSite/src/types.ts +++ b/WebSite/src/types.ts @@ -1,6 +1,7 @@ export enum ViewType { OVERVIEW = 'overview', PROJECTS = 'projects', + MODELS = 'models', WORKSPACE = 'workspace', SYSTEM = 'system', } diff --git a/工程分析/实现方案-2026-05-07-18-11-12.md b/工程分析/实现方案-2026-05-07-18-11-12.md new file mode 100644 index 0000000..ca4cdd7 --- /dev/null +++ b/工程分析/实现方案-2026-05-07-18-11-12.md @@ -0,0 +1,83 @@ +# 实现方案 - 2026-05-07-18-11-12 + +## 修改目标 + +- 修复项目库构件层级眼睛按钮。 +- 为构件层级增加可编辑 ID。 +- 左侧导航新增 `模型库`。 +- 逆向工作区将 `分割 Mask` 替换为 `可视化工具栏`,并加入模型显示、整体位姿保存/选择、构件层级。 + +## 涉及路径 + +- `WebSite/src/types.ts` +- `WebSite/src/App.tsx` +- `WebSite/src/components/Sidebar.tsx` +- `WebSite/src/components/ProjectLibrary.tsx` +- `WebSite/src/components/ReverseWorkspace.tsx` + +## 技术路线 + +### 1. 左侧新增模型库 + +- 在 `ViewType` 中新增 `MODELS`。 +- Sidebar 增加 `模型库` 导航项。 +- App 中点击 `模型库` 时复用 `ProjectLibrary`,但通过 `initialViewMode="model"` 默认进入 3D 模型页。 + +### 2. 项目库构件层级眼睛修复与 ID + +- `ModuleStyle` 增加 `partId`。 +- 初始化构件层级时按文件顺序写入 `partId=index+1`。 +- `toggleModuleVisibility()` 明确保留 `partId/color/opacity`,只切换 `visible`。 +- 构件眼睛按钮阻止冒泡,避免父级点击干扰。 +- 每个构件显示 `ID` 输入框,输入值限制为 1 到 255 的整数。 + +### 3. 逆向工作区可视化工具栏 + +- 将标题 `分割 Mask` 改为 `可视化工具栏`。 +- 增加模型显示档位:标准、精细、超精细、实体。 +- 增加整体位姿保存/选择: + - 默认预设:默认、俯视、侧视。 + - 用户可保存当前位姿为 `位姿N`。 + - 选择后更新 `modelPose`。 +- 增加构件层级: + - 眼睛显示/隐藏。 + - 颜色。 + - 透明度。 + - ID 1 到 255。 +- 将逆向工作区 Three.js 模型加载逻辑接入构件样式和显示档位。 + +## 数据流或交互流程 + +1. 用户进入 `模型库` 或项目库 3D 模型页。 +2. 构件样式状态按 STL 文件初始化。 +3. 用户点击眼睛按钮,前端更新对应构件 `visible`,Three.js 重新加载/渲染可见构件。 +4. 用户修改 ID,前端 clamp 到 1 到 255 并显示。 +5. 用户进入逆向工作区,工具栏显示模型显示、位姿预设、构件层级。 +6. 用户保存或选择整体位姿,融合视角模型位姿更新。 + +## 兼容性与回滚方案 + +- `模型库` 复用项目库,不新增后端路由。 +- 构件 ID 仅在前端状态中使用,若后续需要持久化可再扩展后端项目状态。 +- 若逆向工作区工具栏过长,可后续拆成 tabs 或折叠区。 + +## 预计文件变更 + +- `types.ts`:新增 ViewType。 +- `Sidebar.tsx`:新增导航项。 +- `App.tsx`:支持模型库视图和 ProjectLibrary 初始 tab。 +- `ProjectLibrary.tsx`:构件 ID、眼睛修复、初始 viewMode 参数。 +- `ReverseWorkspace.tsx`:可视化工具栏与模型样式/位姿预设。 + +## 人工审核状态 + +- 本次免二次确认,方案写入后直接执行。 + +## 执行结果 + +- 已新增左侧导航 `模型库`,默认进入项目库的 3D 模型页。 +- 已为项目库构件层级增加 `ID` 输入,默认 1 到 N,输入限制为 1 到 255。 +- 已修正项目库构件眼睛按钮切换时保留颜色、透明度、ID 状态。 +- 已将逆向工作区 `分割 Mask` 改为 `可视化工具栏`。 +- 已在可视化工具栏加入模型显示档位、整体位姿保存/选择、构件层级。 +- 已将逆向工作区融合视角的 STL 加载接入构件显示、颜色、透明度和模型显示档位。 diff --git a/工程分析/测试方案-2026-05-07-18-11-12.md b/工程分析/测试方案-2026-05-07-18-11-12.md new file mode 100644 index 0000000..2f00dbd --- /dev/null +++ b/工程分析/测试方案-2026-05-07-18-11-12.md @@ -0,0 +1,51 @@ +# 测试方案 - 2026-05-07-18-11-12 + +## 静态检查 + +1. `git status --short --branch` +2. `cd WebSite && npm run build` +3. `cd WebSite && npm run lint` + +## 单元或集成测试 + +当前项目没有独立单元测试体系,本次采用构建、类型检查、API 冒烟和页面运行验证。 + +## 关键业务场景验证 + +1. 左侧导航显示 `模型库`。 +2. 点击 `模型库` 默认进入项目库 3D 模型页。 +3. 项目库构件层级: + - 点击单个构件眼睛,模型可隐藏/显示。 + - 顶部眼睛可全隐藏/全显示。 + - 每个构件显示 `ID`。 + - ID 可修改为 1 到 255。 + - ID 修改为 0 时应被限制为 1。 +4. 逆向工作区: + - `分割 Mask` 文案变为 `可视化工具栏`。 + - 工具栏包含模型显示、整体位姿保存/选择、构件层级。 + - 模型显示档位切换后场景仍可加载。 + - 保存位姿后可在选择框中切回。 + - 构件眼睛、颜色、透明度可影响融合视角中的模型显示。 + +## 医学影像数据相关边界验证 + +- 本次不修改 DICOM 解析和融合体接口。 +- 回归确认 `/api/projects` 和 `/api/projects/head-ct-demo/dicom-fusion-volume` 可访问。 + +## 回归风险 + +- 逆向工作区模型样式状态变化会触发 Three.js 重建,可能带来短暂加载。 +- 左侧新增导航项可能影响当前 header 文案和 activeView 判断。 +- 项目库构件 ID 目前未持久化,刷新页面后回到默认 1 到 N。 + +## 人工审核状态 + +- 本次免二次确认。 + +## 执行记录 + +- `npm run lint`:通过,实际执行 `tsc --noEmit`。 +- `npm run build`:通过。 +- 重新部署后 `curl -I http://127.0.0.1:4000/`:返回 `HTTP/1.1 200 OK`。 +- `GET /api/projects`:正常返回默认项目,包含 9 个 STL 文件。 +- `GET /api/projects/head-ct-demo/dicom-fusion-volume?start=0&end=10&mode=soft`:正常返回 DICOM 融合体数据。 diff --git a/工程分析/经验记录.md b/工程分析/经验记录.md index 79316e4..3626c95 100644 --- a/工程分析/经验记录.md +++ b/工程分析/经验记录.md @@ -649,3 +649,21 @@ C. 解决问题方案 D. 后续如何避免问题 融合、配准、体素化相关视图应优先使用三维数据结构,而不是二维示意图;DICOM 体数据接口必须限制切片数量和纹理尺寸,保证浏览器交互稳定;模型相对 DICOM 的调整和整体场景观察要分开管理。 + +## 2026-05-07-18-11-12 构件层级状态与可视化工具栏 + +A. 具体问题 + +项目库构件层级的单个眼睛按钮表现不稳定;构件缺少可编辑的 1 到 255 ID;逆向工作区仍显示 `分割 Mask`,缺少模型显示、位姿保存/选择和构件层级等可视化控制。 + +B. 产生问题原因 + +构件样式状态只记录 visible/color/opacity,部分更新路径会用默认值重建对象,容易丢失扩展字段;逆向工作区三维融合视角初版只加载模型,没有把项目库中的模型显示和构件层级控制模式迁移过来。 + +C. 解决问题方案 + +扩展构件样式状态为 visible/color/opacity/partId,所有更新都保留既有字段;项目库和逆向工作区均使用 1 到 255 的 ID clamp;新增 `模型库` 复用项目库并默认进入模型页;逆向工作区将中间栏改为 `可视化工具栏`,加入模型显示档位、位姿保存/选择和构件层级,并把这些状态传入 Three.js 模型加载逻辑。 + +D. 后续如何避免问题 + +构件层级状态应作为统一结构在不同页面复用;任何新增构件字段都要检查初始化、单项更新、全局更新和默认 fallback 四条路径;逆向工作区的可视化控制应与项目库模型页保持一致,避免用户在两个页面学两套交互。 diff --git a/工程分析/需求分析-2026-05-07-18-11-12.md b/工程分析/需求分析-2026-05-07-18-11-12.md new file mode 100644 index 0000000..94847e7 --- /dev/null +++ b/工程分析/需求分析-2026-05-07-18-11-12.md @@ -0,0 +1,56 @@ +# 需求分析 - 2026-05-07-18-11-12 + +## 原始需求摘要 + +用户要求: + +1. 修复 `项目库 - 3D模型 - 构件层级` 右侧眼睛按钮不好使的问题。 +2. 左侧导航增加一个功能:`模型库`。 +3. `项目库 - 3D模型 - 构件层级` 中每个层级显示 `ID: XX`,默认从 1 到 N;ID 可修改为 1 到 255 的整数,不能改成 0。 +4. 逆向工作区中 `分割 Mask` 文案改为 `可视化工具栏`。 +5. 在 `可视化工具栏` 中加入模型显示、整体位姿保存和选择、构件层级功能。 +6. 本次需求分析、实现方案、测试方案、执行修改都不需要人工二次确认。 + +## 业务目标 + +- 让模型构件显示/隐藏、颜色、透明度和 ID 管理成为稳定可用的可视化控制基础。 +- 将模型相关控制从项目库扩展到逆向工作区,使三维融合视角具备更完整的模型可视化工具栏。 +- 让用户可以保存常用整体位姿并快速切换,降低重复调整成本。 + +## 输入与输出 + +输入: + +- 用户点击构件层级眼睛按钮。 +- 用户修改构件 ID。 +- 用户在逆向工作区调整模型显示、模型位姿、保存/选择位姿。 + +输出: + +- 构件眼睛按钮能正确隐藏/显示对应 STL。 +- 构件层级显示并可编辑 1 到 255 的 ID。 +- 左侧导航出现 `模型库`,点击后进入项目库的模型页。 +- 逆向工作区中出现 `可视化工具栏`,包含模型显示、整体位姿保存/选择、构件层级。 + +## 影响范围 + +- `WebSite/src/types.ts` +- `WebSite/src/App.tsx` +- `WebSite/src/components/Sidebar.tsx` +- `WebSite/src/components/ProjectLibrary.tsx` +- `WebSite/src/components/ReverseWorkspace.tsx` + +## 风险点 + +- 项目库和逆向工作区各自维护构件状态,若状态结构不一致会导致行为差异。 +- 构件 ID 是前端可视化 ID,不应被误认为已写入 mask 标签或后端分割 ID。 +- 新增 `模型库` 导航若没有正确带入初始 tab,可能和 `项目库` 行为混淆。 +- 逆向工作区可视化工具栏内容较多,需要避免布局过挤。 + +## 待确认问题 + +- 本次用户已明确免二次确认,直接执行。 + +## 人工审核状态 + +- 本次免二次确认。