2026-05-21-09-29-47 修复自动拉伸基准与缩放步长
This commit is contained in:
@@ -107,10 +107,28 @@ const modelPoseLimits: Record<ModelPoseKey, { min: number; max: number }> = {
|
||||
translateZ: { min: -2, max: 2 },
|
||||
scale: { min: 0.5, max: 2.5 },
|
||||
};
|
||||
const modelPoseStepPrecision: Partial<Record<ModelPoseKey, number>> = {
|
||||
scale: 3,
|
||||
};
|
||||
|
||||
function clampModelPoseValue(key: ModelPoseKey, value: number) {
|
||||
const limit = modelPoseLimits[key];
|
||||
return Math.max(limit.min, Math.min(limit.max, value));
|
||||
const clampedValue = Math.max(limit.min, Math.min(limit.max, value));
|
||||
const precision = modelPoseStepPrecision[key];
|
||||
return typeof precision === 'number' ? Number(clampedValue.toFixed(precision)) : clampedValue;
|
||||
}
|
||||
|
||||
function getControlStepPrecision(step: number) {
|
||||
if (step >= 1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const text = step.toString();
|
||||
if (text.includes('e-')) {
|
||||
return Number(text.split('e-')[1] ?? 2);
|
||||
}
|
||||
|
||||
return text.split('.')[1]?.length ?? 0;
|
||||
}
|
||||
|
||||
function clampModelPose(next: ModelPose): ModelPose {
|
||||
@@ -1806,7 +1824,7 @@ export default function ProjectLibrary({
|
||||
{ key: 'translateX' as const, label: '平移 X', min: -2, max: 2, step: 0.05, value: modelPose.translateX, minus: '-X', plus: '+X', delta: 0.25 },
|
||||
{ key: 'translateY' as const, label: '平移 Y', min: -2, max: 2, step: 0.05, value: modelPose.translateY, minus: '-Y', plus: '+Y', delta: 0.25 },
|
||||
{ key: 'translateZ' as const, label: '平移 Z', min: -2, max: 2, step: 0.05, value: modelPose.translateZ, minus: '-Z', plus: '+Z', delta: 0.25 },
|
||||
{ key: 'scale' as const, label: '缩放', min: 0.5, max: 2.5, step: 0.05, value: modelPose.scale, minus: '-0.1', plus: '+0.1', delta: 0.1 },
|
||||
{ key: 'scale' as const, label: '缩放', min: 0.5, max: 2.5, step: 0.005, value: modelPose.scale, minus: '-0.005', plus: '+0.005', delta: 0.005 },
|
||||
].map((item) => (
|
||||
<div key={item.key} className="grid grid-cols-[48px_40px_1fr_40px_42px] items-center gap-2">
|
||||
<span className="text-[10px] font-bold text-slate-500">{item.label}</span>
|
||||
@@ -1833,7 +1851,7 @@ export default function ProjectLibrary({
|
||||
>
|
||||
{item.plus}
|
||||
</button>
|
||||
<span className="text-[10px] font-mono text-slate-400 text-right">{Number(item.value).toFixed(item.step < 1 ? 2 : 0)}</span>
|
||||
<span className="text-[10px] font-mono text-slate-400 text-right">{Number(item.value).toFixed(getControlStepPrecision(item.step))}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -80,7 +80,7 @@ const poseStepConfig: Record<ModelPoseKey, { min: number; max: number; step: num
|
||||
translateX: { min: -2, max: 2, step: 0.005, minus: '-X', plus: '+X' },
|
||||
translateY: { min: -2, max: 2, step: 0.005, minus: '-Y', plus: '+Y' },
|
||||
translateZ: { min: -2, max: 2, step: 0.005, minus: '-Z', plus: '+Z' },
|
||||
scale: { min: 0.5, max: 3, step: 0.05, minus: '-S', plus: '+S' },
|
||||
scale: { min: 0.5, max: 3, step: 0.005, minus: '-S', plus: '+S' },
|
||||
};
|
||||
|
||||
const defaultModelPose: ModelPose = {
|
||||
@@ -2585,19 +2585,19 @@ export default function ReverseWorkspace({
|
||||
return volumePayload;
|
||||
};
|
||||
|
||||
const loadVisibleModelBounds = async () => {
|
||||
const loadGlobalModelBounds = async () => {
|
||||
if (!project) {
|
||||
return null;
|
||||
}
|
||||
const visibleFiles = (project.stlFiles ?? []).filter((fileName) => moduleStyles[fileName]?.visible !== false);
|
||||
const cacheKey = `${project.id}:${visibleFiles.join('|')}`;
|
||||
const modelFiles = project.stlFiles ?? [];
|
||||
const cacheKey = `${project.id}:global:${modelFiles.join('|')}`;
|
||||
const cached = modelBoundsCacheRef.current.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const modelBox = new THREE.Box3();
|
||||
const results = await Promise.allSettled(visibleFiles.map((fileName) => (
|
||||
const results = await Promise.allSettled(modelFiles.map((fileName) => (
|
||||
getCachedModelPreview(project.id, fileName, 1000)
|
||||
)));
|
||||
results.forEach((result) => {
|
||||
@@ -2630,9 +2630,9 @@ export default function ReverseWorkspace({
|
||||
setStretchingAxis(axis);
|
||||
setFusionError('');
|
||||
try {
|
||||
const bounds = await loadVisibleModelBounds();
|
||||
const bounds = await loadGlobalModelBounds();
|
||||
if (!bounds) {
|
||||
throw new Error('未获取到可见 STL 构件边界');
|
||||
throw new Error('未获取到 STL 构件边界');
|
||||
}
|
||||
const rawSize = new THREE.Vector3().subVectors(bounds.max, bounds.min);
|
||||
const rotatedSize = getRotatedModelSize(bounds, modelPose);
|
||||
@@ -2651,10 +2651,21 @@ export default function ReverseWorkspace({
|
||||
};
|
||||
const baseScale = (Math.max(dicomSize.x, dicomSize.y, dicomSize.z) / maxModelSize) * 0.92;
|
||||
const rotatedAxisSize = Math.max(rotatedSize[axis], 1e-6);
|
||||
const nextScale = clampPoseValue('scale', dicomSize[axis] / (rotatedAxisSize * baseScale));
|
||||
const axisFitScale = dicomSize[axis] / (rotatedAxisSize * baseScale);
|
||||
const containmentScale = Math.min(
|
||||
dicomSize.x / (Math.max(rotatedSize.x, 1e-6) * baseScale),
|
||||
dicomSize.y / (Math.max(rotatedSize.y, 1e-6) * baseScale),
|
||||
dicomSize.z / (Math.max(rotatedSize.z, 1e-6) * baseScale),
|
||||
);
|
||||
const limitedByVolume = axisFitScale > containmentScale + 1e-6;
|
||||
const nextScale = clampPoseValue('scale', Math.min(axisFitScale, containmentScale));
|
||||
const nextPose = { ...modelPose, scale: nextScale };
|
||||
updateModelPose({ scale: nextScale }, { markCustom: !options.silentInitial, keepStatus: true });
|
||||
setPoseImportStatus(`已按 ${axis.toUpperCase()} 方向进行三维等比例拉伸`);
|
||||
setPoseImportStatus(
|
||||
limitedByVolume
|
||||
? `已按 ${axis.toUpperCase()} 方向拉伸,并限制在 DICOM 体范围内`
|
||||
: `已按 ${axis.toUpperCase()} 方向进行三维等比例拉伸`,
|
||||
);
|
||||
if (options.silentInitial) {
|
||||
savedWorkspaceSnapshotRef.current = createWorkspaceSnapshot({
|
||||
modelPose: nextPose,
|
||||
@@ -2792,7 +2803,8 @@ export default function ReverseWorkspace({
|
||||
|
||||
const clampPoseValue = (key: ModelPoseKey, value: number) => {
|
||||
const limit = poseStepConfig[key];
|
||||
return clamp(value, limit.min, limit.max);
|
||||
const precision = getStepPrecision(limit.step);
|
||||
return Number(clamp(value, limit.min, limit.max).toFixed(precision));
|
||||
};
|
||||
|
||||
const updateModelPose = (partial: Partial<ModelPose>, options: { markCustom?: boolean; keepStatus?: boolean } = {}) => {
|
||||
|
||||
54
工程分析/实现方案-2026-05-21-09-29-47.md
Normal file
54
工程分析/实现方案-2026-05-21-09-29-47.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# 实现方案-2026-05-21-09-29-47
|
||||
|
||||
实现方案文档路径:`工程分析/实现方案-2026-05-21-09-29-47.md`
|
||||
|
||||
## 修改目标
|
||||
|
||||
- 定位并修复自动拉伸按钮的尺度计算逻辑。
|
||||
- 将缩放控件的展示精度和按钮调整步长统一为三位小数、`0.005`。
|
||||
- 完成静态检查、构建和部署验证。
|
||||
|
||||
## 涉及路径
|
||||
|
||||
- `WebSite/src/components/ReverseWorkspace.tsx`
|
||||
- `WebSite/src/components/ProjectLibrary.tsx`
|
||||
- `工程分析/需求分析-2026-05-21-09-29-47.md`
|
||||
- `工程分析/实现方案-2026-05-21-09-29-47.md`
|
||||
- `工程分析/测试方案-2026-05-21-09-29-47.md`
|
||||
- `工程分析/经验记录.md`
|
||||
|
||||
## 技术路线
|
||||
|
||||
1. 查找 `Z拉伸`、`Y拉伸`、`stretch`、`poseStepConfig`、`scale` 相关实现。
|
||||
2. 对齐自动拉伸与三维融合视图的模型包围盒基准,避免可见构件和全局构件混用导致重复点击结果不同。
|
||||
3. 将自动拉伸改为基于原始模型全局包围盒和 DICOM 物理尺寸计算目标缩放,使操作幂等。
|
||||
4. 对单轴贴合增加 DICOM 体范围保护,避免 Y 等短轴贴合时把整体模型放大到超出视场。
|
||||
5. 将 `scale` 的步长改为 `0.005`,数值输入/展示统一三位小数。
|
||||
6. 运行 `npm run lint`、`npm run build`,并通过接口/页面验证部署。
|
||||
|
||||
## 执行步骤
|
||||
|
||||
- 阅读相关源码和当前状态。
|
||||
- 修改自动拉伸计算函数与缩放格式化函数。
|
||||
- 确认保存快照和导出仍使用数值型 `scale`。
|
||||
- 构建并部署。
|
||||
- 追加经验记录,提交并尝试推送 Gitea。
|
||||
|
||||
## 兼容性与回滚方案
|
||||
|
||||
- 修改仅限前端位姿计算与展示,不改变后端数据结构。
|
||||
- 若自动拉伸回归失败,可回退本次 `ReverseWorkspace.tsx` 改动。
|
||||
- 旧保存位姿仍可按数值读取,界面展示会统一为三位小数。
|
||||
|
||||
## 预计文件变更
|
||||
|
||||
- 修改 `WebSite/src/components/ReverseWorkspace.tsx`。
|
||||
- 修改 `WebSite/src/components/ProjectLibrary.tsx`。
|
||||
- 新增本次工程分析三件套。
|
||||
- 追加 `工程分析/经验记录.md`。
|
||||
|
||||
## 提交与部署策略
|
||||
|
||||
- 暂存本次代码与工程分析文档,避免历史删除、软著目录和压缩包进入提交。
|
||||
- Commit message 包含 `2026-05-21-09-29-47` 与简要说明。
|
||||
- 使用 `tmux` 会话 `revoxelseg-dicom` 重新部署端口 `4000`。
|
||||
52
工程分析/测试方案-2026-05-21-09-29-47.md
Normal file
52
工程分析/测试方案-2026-05-21-09-29-47.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# 测试方案-2026-05-21-09-29-47
|
||||
|
||||
测试方案文档路径:`工程分析/测试方案-2026-05-21-09-29-47.md`
|
||||
|
||||
## 静态检查
|
||||
|
||||
- `cd WebSite && npm run lint`
|
||||
|
||||
## 构建检查
|
||||
|
||||
- `cd WebSite && npm run build`
|
||||
|
||||
## 关键业务场景验证
|
||||
|
||||
- 缩放按钮每次 `+` / `-` 调整 `0.005`。
|
||||
- 缩放输入/展示为三位小数。
|
||||
- `Z拉伸` 对同一项目重复点击应保持幂等,不继续缩小或放大。
|
||||
- `Y拉伸` 不应明显异常放大到超出 DICOM 体范围。
|
||||
- 位姿保存和导出仍能读取 `scale` 数值。
|
||||
|
||||
## 医学影像数据相关边界验证
|
||||
|
||||
- 默认项目与项目 `123` 都不应修改原始 DICOM/STL 数据。
|
||||
- 自动拉伸只基于 DICOM 物理尺寸与原始模型包围盒计算,不依赖当前查看视角缩放。
|
||||
|
||||
## 部署验证
|
||||
|
||||
- `http://127.0.0.1:4000/api/health`
|
||||
- `http://127.0.0.1:4000/`
|
||||
|
||||
## Git/Gitea 备份验证
|
||||
|
||||
- 只提交本次相关代码和工程分析文档。
|
||||
- 不提交软著材料、`3279-STL.zip`、历史删除或运行态产物。
|
||||
|
||||
## 风险与回归关注点
|
||||
|
||||
- 自动拉伸和用户缩放均写入 `modelPose.scale`,需避免把“当前缩放结果”当成下一次计算基准。
|
||||
- 三位小数展示不能让内部数据变成字符串。
|
||||
|
||||
## 实际验证记录
|
||||
|
||||
- `npm run lint`:通过。
|
||||
- `npm run build`:通过,Vite 生成生产构建;仅保留既有大 chunk 警告。
|
||||
- 项目 `123` 尺寸复算:
|
||||
- X 轴单轴贴合缩放约为 `1.087`。
|
||||
- Y 轴单轴贴合缩放约为 `1.591`,会造成用户反馈的明显放大。
|
||||
- Z 轴贴合和 DICOM 三轴容纳上限约为 `0.937`。
|
||||
- 新逻辑会把 X/Y/Z 手动拉伸结果限制在 DICOM 体容纳上限内,避免 Y 拉伸越界放大,并让 Z 拉伸与初次自动 Z 拉伸保持一致。
|
||||
- 缩放控件:
|
||||
- 逆向工作区 `scale` 步长改为 `0.005`,输入框按三位小数展示。
|
||||
- 项目库 3D 模型视图 `scale` 步长改为 `0.005`,显示按三位小数格式化,并对内部数值做三位小数取整。
|
||||
18
工程分析/经验记录.md
18
工程分析/经验记录.md
@@ -1459,3 +1459,21 @@ C. 解决问题方案
|
||||
D. 后续如何避免问题
|
||||
|
||||
所有 WebGL/Three.js 视图都应把渲染器创建失败视为可恢复状态,不能让单个三维画布拖垮整个工作区。自动生成软著或验收截图时,除了设置浏览器窗口,还必须设置设备视口,并在生成后校验图片尺寸、非空像素、章节数量、图片引用和 docx 内嵌图片位置数量。
|
||||
|
||||
## 2026-05-21-09-29-47 自动拉伸要统一全局包围盒并限制短轴放大
|
||||
|
||||
A. 具体问题
|
||||
|
||||
项目 `123` 中,用户反馈初次进入逆向工作区时自动 Z 拉伸正常,但手动点击 `Z拉伸` 后仍像差一块;点击 `Y拉伸` 时模型整体会明显放大。同时用户要求 `缩放` 参数按三位小数展示,并且 `-` / `+` 每次只调整 `0.005`。
|
||||
|
||||
B. 产生问题原因
|
||||
|
||||
自动拉伸此前容易在不同路径中混用可见构件包围盒和三维融合视图使用的全局 STL 包围盒,重复点击时计算基准和实际渲染基准不完全一致。另一方面,单轴贴合本质是三维等比例缩放;当 Y 轴是模型短轴时,直接按 Y 轴填满 DICOM 会把 X/Z 同步放大,表现为一键明显放大并可能超出视场。缩放控件也沿用了 `0.05` 步长和两位小数显示,不适合精细配准。
|
||||
|
||||
C. 解决问题方案
|
||||
|
||||
逆向工作区自动拉伸统一读取项目全局 STL 包围盒,并用与三维融合视图一致的 DICOM 物理尺寸和基础比例计算目标缩放。单轴贴合时先计算所选轴目标缩放,再计算 X/Y/Z 三轴都能容纳在 DICOM 体内的上限,最终取两者较小值,保证 `Y拉伸` 不会越界放大。`scale` 控件步长改为 `0.005`,输入和显示按三位小数格式化;项目库 3D 模型位姿也同步改为 `0.005` 步长并对内部缩放值做三位小数取整。
|
||||
|
||||
D. 后续如何避免问题
|
||||
|
||||
凡是涉及 STL 与 DICOM 尺度自动匹配的逻辑,都必须明确使用“全局包围盒”还是“可见构件包围盒”,并与实际渲染组件保持一致。单轴等比例贴合要考虑其他轴的视场约束,短轴贴合不能无限放大长轴;位姿数值控件的步长、显示精度、内部取整和导出数据要同步设计,避免 UI 看起来精细但实际状态漂移。
|
||||
|
||||
49
工程分析/需求分析-2026-05-21-09-29-47.md
Normal file
49
工程分析/需求分析-2026-05-21-09-29-47.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# 需求分析-2026-05-21-09-29-47
|
||||
|
||||
开始时间:2026-05-21-09-29-47
|
||||
|
||||
## 原始需求摘要
|
||||
|
||||
用户反馈项目 `123` 中点击“Z拉伸”后,模型在 DICOM 体高度方向仍差一块;初次进入项目时默认 Z 拉伸正常,但手动点击 Z 拉伸后异常。点击“Y拉伸”时图像明显放大。用户同时要求模型位姿中的“缩放”数值改为三位小数,且通过 `-` / `+` 按钮每次只调整 `0.005`。
|
||||
|
||||
## 业务目标
|
||||
|
||||
- 修复三维等比例自动拉伸按钮在重复点击或切换轴向时的尺度计算异常。
|
||||
- 保持自动拉伸以未应用用户缩放前的模型/DICOM 基准为依据,不因当前 `modelPose.scale` 叠加而越拉越大或拉伸不足。
|
||||
- 当单轴贴合会造成其他方向明显越界时,优先保持模型仍落在 DICOM 体视场范围内。
|
||||
- 让缩放参数展示和微调粒度统一为 `0.005` 与三位小数,便于精细配准。
|
||||
|
||||
## 输入与输出
|
||||
|
||||
- 输入:
|
||||
- 项目 `123` 的导入 DICOM/STL 数据。
|
||||
- 当前逆向工作区模型位姿控件与自动拉伸逻辑。
|
||||
- 输出:
|
||||
- 修复后的自动拉伸算法。
|
||||
- 缩放输入、按钮和显示精度调整。
|
||||
- 测试、构建、部署与工程分析记录。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- `WebSite/src/components/ReverseWorkspace.tsx`
|
||||
- 逆向工作区与项目库复用的三维融合视图。
|
||||
- 模型位姿保存、导出和重新进入工作区时的缩放值。
|
||||
|
||||
## 关键约束
|
||||
|
||||
- 不改变 DICOM/STL 原始数据。
|
||||
- 不破坏当前旋转、平移、构件样式和逆向分割映射联动。
|
||||
- 保持自动拉伸仅在旋转角度满足 90° 整数倍时可用。
|
||||
|
||||
## 风险点
|
||||
|
||||
- 自动拉伸若混用“当前位姿缩放后的包围盒”和“原始模型基准包围盒”,会导致重复点击后尺度漂移。
|
||||
- 若只按短轴做等比例贴合,长轴会同步放大并超出 DICOM 体范围,表现为图像突然变大。
|
||||
- 不同项目 STL 数量、模型大小和 DICOM 物理尺寸差异较大,算法需基于物理目标尺寸与原始模型尺寸计算。
|
||||
- 缩放精度改动需同步输入框、按钮、滑条、保存快照和导出位姿。
|
||||
|
||||
## 默认假设
|
||||
|
||||
- “Z拉伸/Y拉伸”应是幂等操作:同一轴重复点击不应继续改变缩放。
|
||||
- 自动拉伸是三维等比例缩放,只改变 `modelPose.scale`,不改变旋转和平移。
|
||||
- 三位小数展示可接受对已有保存位姿做格式化显示,不改变内部数值精度。
|
||||
Reference in New Issue
Block a user