2026-05-07-17-05-43 增加3D实体显示档位
This commit is contained in:
@@ -701,7 +701,7 @@ function createStlPreview(filePath: string, fileName: string, limit: number) {
|
|||||||
throw new Error('当前仅支持二进制 STL 预览');
|
throw new Error('当前仅支持二进制 STL 预览');
|
||||||
}
|
}
|
||||||
|
|
||||||
const sampleLimit = Math.max(100, Math.min(limit, 72000));
|
const sampleLimit = Math.max(100, Math.min(limit, 200000));
|
||||||
const step = Math.max(1, Math.ceil(triangleCount / sampleLimit));
|
const step = Math.max(1, Math.ceil(triangleCount / sampleLimit));
|
||||||
const vertices: number[] = [];
|
const vertices: number[] = [];
|
||||||
let sampledTriangles = 0;
|
let sampledTriangles = 0;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import { api, downloadDicomArchive, downloadMask } from '../lib/api';
|
|||||||
|
|
||||||
type Plane = 'axial' | 'sagittal' | 'coronal';
|
type Plane = 'axial' | 'sagittal' | 'coronal';
|
||||||
type DisplayMode = DicomPreview['mode'];
|
type DisplayMode = DicomPreview['mode'];
|
||||||
type SolidityLevel = 'preview' | 'standard' | 'fine' | 'ultra';
|
type SolidityLevel = 'standard' | 'fine' | 'ultra' | 'solid';
|
||||||
|
|
||||||
interface ModuleStyle {
|
interface ModuleStyle {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@@ -59,10 +59,10 @@ type ModelPoseKey = keyof ModelPose;
|
|||||||
|
|
||||||
const defaultModuleColors = ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6', '#14b8a6', '#f97316', '#64748b', '#ec4899'];
|
const defaultModuleColors = ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6', '#14b8a6', '#f97316', '#64748b', '#ec4899'];
|
||||||
const solidityOptions: Array<{ id: SolidityLevel; label: string; limit: number }> = [
|
const solidityOptions: Array<{ id: SolidityLevel; label: string; limit: number }> = [
|
||||||
{ id: 'preview', label: '预览', limit: 6000 },
|
|
||||||
{ id: 'standard', label: '标准', limit: 16000 },
|
{ id: 'standard', label: '标准', limit: 16000 },
|
||||||
{ id: 'fine', label: '精细', limit: 36000 },
|
{ id: 'fine', label: '精细', limit: 36000 },
|
||||||
{ id: 'ultra', label: '超精细', limit: 72000 },
|
{ id: 'ultra', label: '超精细', limit: 72000 },
|
||||||
|
{ id: 'solid', label: '实体', limit: 200000 },
|
||||||
];
|
];
|
||||||
const defaultModelPose: ModelPose = {
|
const defaultModelPose: ModelPose = {
|
||||||
rotateX: 0,
|
rotateX: 0,
|
||||||
@@ -291,6 +291,7 @@ function NativeStlViewer({
|
|||||||
files,
|
files,
|
||||||
styles,
|
styles,
|
||||||
detailLimit,
|
detailLimit,
|
||||||
|
solidMode,
|
||||||
pose,
|
pose,
|
||||||
onPoseChange,
|
onPoseChange,
|
||||||
}: {
|
}: {
|
||||||
@@ -298,6 +299,7 @@ function NativeStlViewer({
|
|||||||
files: string[];
|
files: string[];
|
||||||
styles: Record<string, ModuleStyle>;
|
styles: Record<string, ModuleStyle>;
|
||||||
detailLimit: number;
|
detailLimit: number;
|
||||||
|
solidMode: boolean;
|
||||||
pose: ModelPose;
|
pose: ModelPose;
|
||||||
onPoseChange: React.Dispatch<React.SetStateAction<ModelPose>>;
|
onPoseChange: React.Dispatch<React.SetStateAction<ModelPose>>;
|
||||||
}) {
|
}) {
|
||||||
@@ -491,13 +493,14 @@ function NativeStlViewer({
|
|||||||
geometry.setAttribute('position', new THREE.Float32BufferAttribute(payload.vertices, 3));
|
geometry.setAttribute('position', new THREE.Float32BufferAttribute(payload.vertices, 3));
|
||||||
geometry.computeVertexNormals();
|
geometry.computeVertexNormals();
|
||||||
const style = styles[fileName] ?? { color: '#3b82f6', opacity: 0.72, visible: true };
|
const style = styles[fileName] ?? { color: '#3b82f6', opacity: 0.72, visible: true };
|
||||||
|
const materialOpacity = solidMode ? Math.max(style.opacity, 0.94) : style.opacity;
|
||||||
const mesh = new THREE.Mesh(
|
const mesh = new THREE.Mesh(
|
||||||
geometry,
|
geometry,
|
||||||
new THREE.MeshStandardMaterial({
|
new THREE.MeshStandardMaterial({
|
||||||
color: style.color,
|
color: style.color,
|
||||||
opacity: style.opacity,
|
opacity: materialOpacity,
|
||||||
transparent: style.opacity < 1,
|
transparent: materialOpacity < 1,
|
||||||
roughness: 0.42,
|
roughness: solidMode ? 0.56 : 0.42,
|
||||||
metalness: 0.04,
|
metalness: 0.04,
|
||||||
side: THREE.DoubleSide,
|
side: THREE.DoubleSide,
|
||||||
}),
|
}),
|
||||||
@@ -600,7 +603,7 @@ function NativeStlViewer({
|
|||||||
});
|
});
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
};
|
};
|
||||||
}, [projectId, files.join('|'), JSON.stringify(styles), detailLimit]);
|
}, [projectId, files.join('|'), JSON.stringify(styles), detailLimit, solidMode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full relative cursor-grab active:cursor-grabbing">
|
<div className="h-full w-full relative cursor-grab active:cursor-grabbing">
|
||||||
@@ -696,7 +699,7 @@ export default function ProjectLibrary({ onReverse }: { onReverse: (projId: stri
|
|||||||
];
|
];
|
||||||
const allModulesVisible = stlFiles.length > 0 && stlFiles.every((file) => moduleStyles[file]?.visible !== false);
|
const allModulesVisible = stlFiles.length > 0 && stlFiles.every((file) => moduleStyles[file]?.visible !== false);
|
||||||
const sliceTotal = dicomPreview?.total ?? selectedProject?.dicomCount ?? 0;
|
const sliceTotal = dicomPreview?.total ?? selectedProject?.dicomCount ?? 0;
|
||||||
const selectedSolidity = solidityOptions.find((option) => option.id === solidityLevel) ?? solidityOptions[1];
|
const selectedSolidity = solidityOptions.find((option) => option.id === solidityLevel) ?? solidityOptions[0];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const next: Record<string, ModuleStyle> = {};
|
const next: Record<string, ModuleStyle> = {};
|
||||||
@@ -1235,6 +1238,7 @@ export default function ProjectLibrary({ onReverse }: { onReverse: (projId: stri
|
|||||||
files={stlFiles}
|
files={stlFiles}
|
||||||
styles={moduleStyles}
|
styles={moduleStyles}
|
||||||
detailLimit={selectedSolidity.limit}
|
detailLimit={selectedSolidity.limit}
|
||||||
|
solidMode={solidityLevel === 'solid'}
|
||||||
pose={modelPose}
|
pose={modelPose}
|
||||||
onPoseChange={setModelPose}
|
onPoseChange={setModelPose}
|
||||||
/>
|
/>
|
||||||
|
|||||||
64
工程分析/实现方案-2026-05-07-17-05-43.md
Normal file
64
工程分析/实现方案-2026-05-07-17-05-43.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# 实现方案 - 2026-05-07-17-05-43
|
||||||
|
|
||||||
|
## 修改目标
|
||||||
|
|
||||||
|
将 3D 模型显示档位由 `预览 / 标准 / 精细 / 超精细` 改为 `标准 / 精细 / 超精细 / 实体`,并让 `实体` 更接近完整 STL 面片渲染。
|
||||||
|
|
||||||
|
## 涉及路径
|
||||||
|
|
||||||
|
- `WebSite/src/components/ProjectLibrary.tsx`
|
||||||
|
- `WebSite/server.ts`
|
||||||
|
|
||||||
|
## 技术路线
|
||||||
|
|
||||||
|
### 1. 更新档位类型和配置
|
||||||
|
|
||||||
|
- 将 `SolidityLevel` 从 `preview | standard | fine | ultra` 改为 `standard | fine | ultra | solid`。
|
||||||
|
- 删除 `预览` 选项。
|
||||||
|
- 新增 `实体` 选项。
|
||||||
|
- 设置 `实体` 的 `limit` 为更高值,尽量覆盖默认 9 个 STL 的完整三角面。
|
||||||
|
|
||||||
|
### 2. 提升后端 STL preview 上限
|
||||||
|
|
||||||
|
- 后端 `createStlPreview()` 当前最大抽样上限为 `72000`。
|
||||||
|
- 将上限提升到 `200000`,支持实体档位请求更多三角面。
|
||||||
|
- 继续保留 `sampleLimit` clamp,避免异常超大请求拖垮服务。
|
||||||
|
|
||||||
|
### 3. 优化实体档位材质
|
||||||
|
|
||||||
|
- 在 `NativeStlViewer` 中判断当前档位是否为 `solid`。
|
||||||
|
- `solid` 档位下使用更高的不透明度下限,例如 `0.92`,增强实体感。
|
||||||
|
- 其他档位继续使用构件层级中的用户透明度设置。
|
||||||
|
|
||||||
|
## 数据流或交互流程
|
||||||
|
|
||||||
|
1. 用户点击 `实体`。
|
||||||
|
2. 前端 `solidityLevel` 切换为 `solid`。
|
||||||
|
3. `selectedSolidity.limit` 变大,`NativeStlViewer` 重新请求 STL preview。
|
||||||
|
4. 后端按更高上限返回更多三角面和稳定 `bounds`。
|
||||||
|
5. 前端以实体材质重新渲染模型。
|
||||||
|
|
||||||
|
## 兼容性与回滚方案
|
||||||
|
|
||||||
|
- 若实体档位渲染压力过大,可降低 `solid.limit` 或后端最大上限。
|
||||||
|
- 若用户仍需要低质量快速预览,可恢复 `preview` 配置。
|
||||||
|
|
||||||
|
## 预计文件变更
|
||||||
|
|
||||||
|
- `WebSite/src/components/ProjectLibrary.tsx`
|
||||||
|
- 更新 `SolidityLevel`、`solidityOptions`。
|
||||||
|
- `NativeStlViewer` 增加 `solidMode` 参数。
|
||||||
|
- 实体档位材质增强不透明度。
|
||||||
|
- `WebSite/server.ts`
|
||||||
|
- 提升 STL preview 最大抽样上限。
|
||||||
|
|
||||||
|
## 人工审核状态
|
||||||
|
|
||||||
|
- 本次免二次确认,方案写入后直接执行。
|
||||||
|
|
||||||
|
## 执行结果
|
||||||
|
|
||||||
|
- 已删除 `模型显示` 中的 `预览` 档位。
|
||||||
|
- 已新增 `实体` 档位,前端请求上限为 `200000`。
|
||||||
|
- 已将后端 STL preview 最大抽样上限从 `72000` 提升到 `200000`。
|
||||||
|
- 已在实体档位提高材质不透明度下限,增强实体面片视觉。
|
||||||
44
工程分析/测试方案-2026-05-07-17-05-43.md
Normal file
44
工程分析/测试方案-2026-05-07-17-05-43.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# 测试方案 - 2026-05-07-17-05-43
|
||||||
|
|
||||||
|
## 静态检查
|
||||||
|
|
||||||
|
1. `git status --short --branch`
|
||||||
|
2. `cd WebSite && npm run build`
|
||||||
|
3. `cd WebSite && npm run lint`
|
||||||
|
|
||||||
|
## 单元或集成测试
|
||||||
|
|
||||||
|
当前项目没有独立单元测试体系,本次采用构建、类型检查、API 冒烟和页面人工验证。
|
||||||
|
|
||||||
|
## 关键业务场景验证
|
||||||
|
|
||||||
|
1. 打开 `http://192.168.3.11:4000/`。
|
||||||
|
2. 进入 `项目库 - 3D 模型`。
|
||||||
|
3. 验证 `模型显示` 中不再出现 `预览`。
|
||||||
|
4. 验证 `模型显示` 中出现 `实体`。
|
||||||
|
5. 点击 `标准 / 精细 / 超精细 / 实体`:
|
||||||
|
- 模型都可以加载完成。
|
||||||
|
- 当前位姿不丢失。
|
||||||
|
- 实体档位视觉上更接近面片实体,而不是稀疏预览。
|
||||||
|
6. 验证右侧构件透明度、颜色、眼睛显示控制仍可用。
|
||||||
|
|
||||||
|
## 医学影像数据相关边界验证
|
||||||
|
|
||||||
|
- 本次不修改 DICOM 解析、切片显示、分割结果导出。
|
||||||
|
- 回归确认 `/api/projects` 正常返回默认项目。
|
||||||
|
|
||||||
|
## 回归风险
|
||||||
|
|
||||||
|
- 实体档位请求更多 STL 顶点,首次加载可能更慢。
|
||||||
|
- 若浏览器 WebGL 性能不足,实体档位可能出现卡顿。
|
||||||
|
|
||||||
|
## 人工审核状态
|
||||||
|
|
||||||
|
- 本次免二次确认。
|
||||||
|
|
||||||
|
## 执行记录
|
||||||
|
|
||||||
|
- `npm run build`:通过。
|
||||||
|
- `npm run lint`:通过,实际执行 `tsc --noEmit`。
|
||||||
|
- 重新部署后 `curl -I http://127.0.0.1:4000/`:返回 `HTTP/1.1 200 OK`。
|
||||||
|
- 重新部署后请求 `会厌.stl` preview 且 `limit=200000`:返回 `triangleCount=17384`、`sampledTriangles=17384`,确认实体档位可返回完整三角面。
|
||||||
18
工程分析/经验记录.md
18
工程分析/经验记录.md
@@ -613,3 +613,21 @@ C. 解决问题方案
|
|||||||
D. 后续如何避免问题
|
D. 后续如何避免问题
|
||||||
|
|
||||||
任何用于配准、视角稳定、旋转中心的几何参数都必须来自全量模型或稳定元数据,不能来自随性能档位变化的抽样数据;坐标系、状态文字和模型实际位姿应共用同一份 pose 状态。
|
任何用于配准、视角稳定、旋转中心的几何参数都必须来自全量模型或稳定元数据,不能来自随性能档位变化的抽样数据;坐标系、状态文字和模型实际位姿应共用同一份 pose 状态。
|
||||||
|
|
||||||
|
## 2026-05-07-17-05-43 模型显示实体档位
|
||||||
|
|
||||||
|
A. 具体问题
|
||||||
|
|
||||||
|
用户不再需要低质量 `预览` 档位,希望在 `模型显示` 中增加更接近 STL 实体面片的 `实体` 档位。
|
||||||
|
|
||||||
|
B. 产生问题原因
|
||||||
|
|
||||||
|
原有档位以性能抽样为主,最高 `超精细` 仍受后端 `72000` 三角面上限约束,且材质继续使用用户设置的半透明度,视觉上仍偏点状或半透明预览。
|
||||||
|
|
||||||
|
C. 解决问题方案
|
||||||
|
|
||||||
|
前端删除 `预览` 档位,新增 `实体` 档位并设置更高请求上限 `200000`;后端 STL preview clamp 同步提升到 `200000`;实体档位下将材质不透明度下限提高到 `0.94`,在保留构件颜色的同时增强实体感。
|
||||||
|
|
||||||
|
D. 后续如何避免问题
|
||||||
|
|
||||||
|
模型显示档位应明确区分“性能抽样”和“实体查看”;若新增高质量档位,需要同步检查前端请求 limit、后端 clamp、材质表现和浏览器性能边界。
|
||||||
|
|||||||
50
工程分析/需求分析-2026-05-07-17-05-43.md
Normal file
50
工程分析/需求分析-2026-05-07-17-05-43.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# 需求分析 - 2026-05-07-17-05-43
|
||||||
|
|
||||||
|
## 原始需求摘要
|
||||||
|
|
||||||
|
用户要求在 `项目库 - 3D 模型` 的 `模型显示` 中:
|
||||||
|
|
||||||
|
1. 去掉 `预览` 档位。
|
||||||
|
2. 增加 `实体` 档位。
|
||||||
|
3. 本次需求分析、实现方案、测试方案、执行修改都不需要人工二次确认。
|
||||||
|
|
||||||
|
## 业务目标
|
||||||
|
|
||||||
|
- 让模型显示档位更符合当前用户对实体化浏览的期望。
|
||||||
|
- 减少低质量 `预览` 档位对视觉判断的干扰。
|
||||||
|
- 提供更接近 STL 实体面的模型显示效果。
|
||||||
|
|
||||||
|
## 输入与输出
|
||||||
|
|
||||||
|
输入:
|
||||||
|
|
||||||
|
- 用户在 3D 模型页点击 `模型显示` 档位。
|
||||||
|
|
||||||
|
输出:
|
||||||
|
|
||||||
|
- 档位变为 `标准 / 精细 / 超精细 / 实体`。
|
||||||
|
- `实体` 档位请求更高数量的 STL 三角面,并使用更接近实体的材质透明度。
|
||||||
|
- 页面底部 `MODEL PATH` 状态显示当前档位为 `实体`。
|
||||||
|
|
||||||
|
## 影响范围
|
||||||
|
|
||||||
|
- `WebSite/src/components/ProjectLibrary.tsx`
|
||||||
|
- `SolidityLevel` 类型。
|
||||||
|
- `solidityOptions` 配置。
|
||||||
|
- STL 材质透明度逻辑。
|
||||||
|
- `WebSite/server.ts`
|
||||||
|
- STL preview 的最大抽样上限。
|
||||||
|
|
||||||
|
## 风险点
|
||||||
|
|
||||||
|
- 实体档位会请求更多三角面,浏览器渲染压力更高。
|
||||||
|
- 若所有 STL 全量三角面过大,接口响应和前端渲染可能变慢。
|
||||||
|
- 半透明构件在实体档位可能仍呈现一定透视感,需要统一提升最低不透明度。
|
||||||
|
|
||||||
|
## 待确认问题
|
||||||
|
|
||||||
|
- 本次用户已明确免二次确认,直接执行。
|
||||||
|
|
||||||
|
## 人工审核状态
|
||||||
|
|
||||||
|
- 本次免二次确认。
|
||||||
Reference in New Issue
Block a user