2026-05-07-16-35-52 优化3D模型位姿控制

This commit is contained in:
2026-05-07 16:43:41 +08:00
parent aa0d51316e
commit e1c34f27bb
6 changed files with 386 additions and 41 deletions

View File

@@ -51,6 +51,8 @@ interface ModelPreviewPayload {
vertices: number[];
}
type ModelPoseKey = keyof ModelPose;
const defaultModuleColors = ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6', '#14b8a6', '#f97316', '#64748b', '#ec4899'];
const solidityOptions: Array<{ id: SolidityLevel; label: string; limit: number }> = [
{ id: 'preview', label: '预览', limit: 6000 },
@@ -67,6 +69,32 @@ const defaultModelPose: ModelPose = {
translateZ: 0,
scale: 1,
};
const modelPoseLimits: Record<ModelPoseKey, { min: number; max: number }> = {
rotateX: { min: -180, max: 180 },
rotateY: { min: -180, max: 180 },
rotateZ: { min: -180, max: 180 },
translateX: { min: -2, max: 2 },
translateY: { min: -2, max: 2 },
translateZ: { min: -2, max: 2 },
scale: { min: 0.5, max: 2.5 },
};
function clampModelPoseValue(key: ModelPoseKey, value: number) {
const limit = modelPoseLimits[key];
return Math.max(limit.min, Math.min(limit.max, value));
}
function clampModelPose(next: ModelPose): ModelPose {
return {
rotateX: clampModelPoseValue('rotateX', next.rotateX),
rotateY: clampModelPoseValue('rotateY', next.rotateY),
rotateZ: clampModelPoseValue('rotateZ', next.rotateZ),
translateX: clampModelPoseValue('translateX', next.translateX),
translateY: clampModelPoseValue('translateY', next.translateY),
translateZ: clampModelPoseValue('translateZ', next.translateZ),
scale: clampModelPoseValue('scale', next.scale),
};
}
function drawFallbackModelPreview(
canvas: HTMLCanvasElement,
@@ -240,15 +268,6 @@ function NativeStlViewer({
const container = containerRef.current;
if (!container) return;
const clampPose = (next: ModelPose): ModelPose => ({
rotateX: Math.max(-180, Math.min(180, next.rotateX)),
rotateY: Math.max(-180, Math.min(180, next.rotateY)),
rotateZ: Math.max(-180, Math.min(180, next.rotateZ)),
translateX: Math.max(-2, Math.min(2, next.translateX)),
translateY: Math.max(-2, Math.min(2, next.translateY)),
translateZ: Math.max(-2, Math.min(2, next.translateZ)),
scale: Math.max(0.5, Math.min(2.5, next.scale)),
});
const dragState = {
active: false,
mode: 'rotate' as 'rotate' | 'pan',
@@ -272,14 +291,14 @@ function NativeStlViewer({
const deltaX = event.clientX - dragState.startX;
const deltaY = event.clientY - dragState.startY;
if (dragState.mode === 'pan') {
onPoseChangeRef.current(clampPose({
onPoseChangeRef.current(clampModelPose({
...dragState.startPose,
translateX: dragState.startPose.translateX + deltaX * 0.006,
translateY: dragState.startPose.translateY - deltaY * 0.006,
}));
return;
}
onPoseChangeRef.current(clampPose({
onPoseChangeRef.current(clampModelPose({
...dragState.startPose,
rotateY: dragState.startPose.rotateY + deltaX * 0.35,
rotateX: dragState.startPose.rotateX + deltaY * 0.35,
@@ -294,7 +313,7 @@ function NativeStlViewer({
};
const handleWheel = (event: WheelEvent) => {
event.preventDefault();
onPoseChangeRef.current(clampPose({
onPoseChangeRef.current(clampModelPose({
...poseRef.current,
scale: poseRef.current.scale - event.deltaY * 0.001,
}));
@@ -398,9 +417,11 @@ function NativeStlViewer({
fillLight.position.set(-4, 2, -3);
scene.add(fillLight);
const group = new THREE.Group();
const poseGroup = new THREE.Group();
const pivotGroup = new THREE.Group();
poseGroup.add(pivotGroup);
let baseScale = 1;
scene.add(group);
scene.add(poseGroup);
let loaded = 0;
let failed = 0;
@@ -429,26 +450,27 @@ function NativeStlViewer({
side: THREE.DoubleSide,
}),
);
group.add(mesh);
pivotGroup.add(mesh);
loaded += 1;
setProgress(Math.round(((loaded + failed) / visibleFiles.length) * 100));
setStatus(`已加载 ${loaded} / ${visibleFiles.length} 个 STL 预览`);
if (loaded + failed === visibleFiles.length) {
const box = new THREE.Box3().setFromObject(group);
const box = new THREE.Box3().setFromObject(pivotGroup);
const center = box.getCenter(new THREE.Vector3());
const size = box.getSize(new THREE.Vector3());
const maxSize = Math.max(size.x, size.y, size.z) || 1;
group.traverse((object) => {
pivotGroup.traverse((object) => {
if (object instanceof THREE.Mesh) {
object.geometry.translate(-center.x, -center.y, -center.z);
object.geometry.computeBoundingSphere();
object.geometry.computeVertexNormals();
}
});
group.position.set(0, 0, 0);
poseGroup.position.set(0, 0, 0);
pivotGroup.position.set(0, 0, 0);
baseScale = 4.2 / maxSize;
group.scale.setScalar(baseScale * poseRef.current.scale);
pivotGroup.scale.setScalar(baseScale * poseRef.current.scale);
camera.lookAt(0, 0, 0);
setStatus(failed ? `完成,${failed} 个模型加载失败` : '模型加载完成');
}
@@ -472,13 +494,13 @@ function NativeStlViewer({
const animate = () => {
if (disposed) return;
const currentPose = poseRef.current;
group.rotation.set(
poseGroup.position.set(currentPose.translateX, currentPose.translateY, currentPose.translateZ);
pivotGroup.rotation.set(
THREE.MathUtils.degToRad(currentPose.rotateX),
THREE.MathUtils.degToRad(currentPose.rotateY),
THREE.MathUtils.degToRad(currentPose.rotateZ),
);
group.position.set(currentPose.translateX, currentPose.translateY, currentPose.translateZ);
group.scale.setScalar(baseScale * currentPose.scale);
pivotGroup.scale.setScalar(baseScale * currentPose.scale);
renderer.render(scene, camera);
animationId = window.requestAnimationFrame(animate);
};
@@ -489,7 +511,7 @@ function NativeStlViewer({
window.cancelAnimationFrame(animationId);
window.removeEventListener('resize', handleResize);
renderer.dispose();
group.traverse((object) => {
poseGroup.traverse((object) => {
if (object instanceof THREE.Mesh) {
object.geometry.dispose();
const material = object.material;
@@ -523,6 +545,23 @@ function NativeStlViewer({
{status}
</div>
)}
<div className="pointer-events-none absolute bottom-4 right-4 rounded-xl border border-slate-200 bg-white/85 px-3 py-2 shadow-sm">
<div className="mb-2 flex h-12 w-16 items-end justify-center">
<div className="relative h-10 w-10">
<span className="absolute left-5 top-5 h-px w-8 origin-left -rotate-[18deg] bg-red-500" />
<span className="absolute left-[19px] top-5 h-8 w-px origin-bottom bg-emerald-500" />
<span className="absolute left-5 top-5 h-px w-7 origin-left rotate-[42deg] bg-blue-500" />
<span className="absolute -right-3 top-3 text-[9px] font-black text-red-500">X</span>
<span className="absolute left-4 -top-2 text-[9px] font-black text-emerald-500">Y</span>
<span className="absolute right-0 bottom-0 text-[9px] font-black text-blue-500">Z</span>
</div>
</div>
<div className="space-y-0.5 font-mono text-[9px] text-slate-500">
<div>X {Math.round(pose.rotateX)}°</div>
<div>Y {Math.round(pose.rotateY)}°</div>
<div>Z {Math.round(pose.rotateZ)}°</div>
</div>
</div>
</div>
);
}
@@ -707,14 +746,36 @@ export default function ProjectLibrary({ onReverse }: { onReverse: (projId: stri
};
const updateModelPose = (partial: Partial<ModelPose>) => {
setModelPose((current) => ({
setModelPose((current) => clampModelPose({
...current,
...partial,
}));
};
const resetModelPose = () => {
setModelPose(defaultModelPose);
const nudgeModelPose = (key: ModelPoseKey, delta: number) => {
setModelPose((current) => clampModelPose({
...current,
[key]: clampModelPoseValue(key, current[key] + delta),
}));
};
const resetModelRotationPose = () => {
setModelPose((current) => ({
...current,
rotateX: 0,
rotateY: 0,
rotateZ: 0,
}));
};
const resetModelTransformPose = () => {
setModelPose((current) => ({
...current,
translateX: 0,
translateY: 0,
translateZ: 0,
scale: 1,
}));
};
const rotateDicom = (delta: number) => {
@@ -1145,26 +1206,41 @@ export default function ProjectLibrary({ onReverse }: { onReverse: (projId: stri
</div>
<div className="rounded-2xl bg-slate-50 border border-slate-100 p-4 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center justify-between gap-2">
<p className="text-xs font-bold text-slate-700">姿</p>
<button
onClick={resetModelPose}
className="text-[10px] font-bold text-blue-600 hover:text-blue-700"
>
姿
</button>
<div className="flex items-center gap-1">
<button
onClick={resetModelRotationPose}
className="rounded-md bg-white px-2 py-1 text-[10px] font-bold text-blue-600 shadow-sm border border-slate-100 hover:bg-blue-50"
>
姿
</button>
<button
onClick={resetModelTransformPose}
className="rounded-md bg-white px-2 py-1 text-[10px] font-bold text-slate-600 shadow-sm border border-slate-100 hover:bg-slate-100"
>
姿
</button>
</div>
</div>
{[
{ key: 'rotateX', label: '旋转 X', min: -180, max: 180, step: 1, value: modelPose.rotateX },
{ key: 'rotateY', label: '旋转 Y', min: -180, max: 180, step: 1, value: modelPose.rotateY },
{ key: 'rotateZ', label: '旋转 Z', min: -180, max: 180, step: 1, value: modelPose.rotateZ },
{ key: 'translateX', label: '平移 X', min: -2, max: 2, step: 0.05, value: modelPose.translateX },
{ key: 'translateY', label: '平移 Y', min: -2, max: 2, step: 0.05, value: modelPose.translateY },
{ key: 'translateZ', label: '平移 Z', min: -2, max: 2, step: 0.05, value: modelPose.translateZ },
{ key: 'scale', label: '缩放', min: 0.5, max: 2.5, step: 0.05, value: modelPose.scale },
{ key: 'rotateX' as const, label: '旋转 X', min: -180, max: 180, step: 1, value: modelPose.rotateX, minus: '-90°', plus: '+90°', delta: 90 },
{ key: 'rotateY' as const, label: '旋转 Y', min: -180, max: 180, step: 1, value: modelPose.rotateY, minus: '-90°', plus: '+90°', delta: 90 },
{ key: 'rotateZ' as const, label: '旋转 Z', min: -180, max: 180, step: 1, value: modelPose.rotateZ, minus: '-90°', plus: '+90°', delta: 90 },
{ 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 },
].map((item) => (
<div key={item.key} className="grid grid-cols-[48px_1fr_42px] items-center gap-2">
<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>
<button
onClick={() => nudgeModelPose(item.key, -item.delta)}
className="h-6 rounded-md bg-white text-[10px] font-bold text-slate-500 shadow-sm border border-slate-100 hover:text-blue-600 hover:bg-blue-50"
title={`${item.label} ${item.minus}`}
>
{item.minus}
</button>
<input
type="range"
min={item.min}
@@ -1174,6 +1250,13 @@ export default function ProjectLibrary({ onReverse }: { onReverse: (projId: stri
onChange={(event) => updateModelPose({ [item.key]: Number(event.target.value) } as Partial<ModelPose>)}
className="w-full accent-blue-600"
/>
<button
onClick={() => nudgeModelPose(item.key, item.delta)}
className="h-6 rounded-md bg-white text-[10px] font-bold text-slate-500 shadow-sm border border-slate-100 hover:text-blue-600 hover:bg-blue-50"
title={`${item.label} ${item.plus}`}
>
{item.plus}
</button>
<span className="text-[10px] font-mono text-slate-400 text-right">{Number(item.value).toFixed(item.step < 1 ? 2 : 0)}</span>
</div>
))}

View File

@@ -94,3 +94,9 @@
- `确认执行`
如果用户对方案提出修改意见,则先更新对应文档,再等待新的确认。
## 默认执行确认更新
- 2026-05-07-16-35-52 起,用户已确认“都确认,后续直接搞”。
- 后续项目修改需求仍必须记录时间、创建需求分析、实现方案、测试方案、读取经验记录、执行后更新经验记录、备份 commit、重新部署。
- 除非用户明确要求暂停或人工审核,后续不再在实现方案和测试方案阶段停等二次确认。

View File

@@ -0,0 +1,111 @@
# 实现方案 - 2026-05-07-16-35-52
## 修改目标
围绕项目库 3D 模型预览页,完善整体位姿控制、旋转中心和方位提示:
- 将单个 `重置位姿` 拆为 `重置旋转位姿``重置平移缩放位姿`
- 旋转 X/Y/Z 增加 `-90°``+90°` 快捷按钮。
- 平移 X/Y/Z 增加负向/正向快捷步进按钮,缩放增加缩小/放大快捷按钮。
- 将模型旋转中心稳定在所有 STL 拼装后整体包围盒的中心。
- 在 3D 模型画布右下角增加 X/Y/Z 方位坐标提示和当前旋转角度。
## 涉及路径
- `WebSite/src/components/ProjectLibrary.tsx`
## 技术路线
### 1. 拆分重置按钮
新增两个函数:
- `resetModelRotationPose()`
- 只重置 `rotateX``rotateY``rotateZ``0`
- 保留 `translateX/Y/Z``scale`
- `resetModelTransformPose()`
- 重置 `translateX``translateY``translateZ``0``scale``1`
- 保留 `rotateX/Y/Z`
`整体位姿` 标题右侧按钮改为两个小按钮:
- `重置旋转位姿`
- `重置平移缩放位姿`
### 2. 增加快捷调整按钮
为位姿控制行增加快捷按钮:
- 旋转 X/Y/Z
- `-90°`:当前角度减 90并限制在 `[-180, 180]`
- `+90°`:当前角度加 90并限制在 `[-180, 180]`
- 平移 X/Y/Z
- 负向按钮:每次减 `0.25`
- 正向按钮:每次加 `0.25`
- 仍限制在 `[-2, 2]`
- 缩放:
- 缩小按钮:每次减 `0.1`
- 放大按钮:每次加 `0.1`
- 仍限制在 `[0.5, 2.5]`
为避免数值越界和重复逻辑,新增统一的 `clampModelPoseValue()``nudgeModelPose()`
### 3. 修正模型旋转中心
当前 STL 预览已经会对 mesh geometry 进行中心化处理。本次进一步显式拆分 Three.js 层级:
- `poseGroup`:负责整体平移。
- `pivotGroup`:位于模型整体几何中心,负责旋转和缩放。
- 所有 STL mesh 挂载在 `pivotGroup` 下,并在写入顶点时先减去整体包围盒中心。
动画循环中:
- `poseGroup.position = translateX/Y/Z`
- `pivotGroup.rotation = rotateX/Y/Z`
- `pivotGroup.scale = baseScale * scale`
这样模型始终围绕整体包围盒中心旋转,平移不会改变旋转轴心。
### 4. 增加右下角 X/Y/Z 方位提示
`NativeStlViewer` 容器中增加绝对定位 overlay
- 显示三个彩色轴向X 红色、Y 绿色、Z 蓝色。
- 显示当前 `rotateX/Y/Z` 数值,随滑块、快捷按钮、鼠标拖拽同步刷新。
- 放置于画布右下角,避免遮挡左下角 `MODEL PATH` 状态信息。
## 数据流与交互流程
1. 用户点击快捷按钮或拖动滑块。
2. 前端调用 `updateModelPose()``nudgeModelPose()`
3. `modelPose` React 状态更新。
4. `NativeStlViewer` 通过 `poseRef` 在 Three.js 动画循环中读取最新状态。
5. Three.js 更新 `poseGroup``pivotGroup`
6. 右下角方位提示读取同一份 `modelPose` 并更新显示。
## 兼容性与回滚方案
- 修改只影响 3D 模型页面,不影响 DICOM 影像、分割结果、后端 API。
- 若旋转中心方案出现异常,可回滚到单 group 结构,同时保留 UI 快捷按钮。
- 若 overlay 影响视觉,可只保留文本角度提示,不影响核心模型显示。
## 预计文件变更
- `WebSite/src/components/ProjectLibrary.tsx`
- 新增位姿 clamp/nudge/reset 方法。
- 更新整体位姿控制区 UI。
- 调整 `NativeStlViewer` 的 group 层级。
- 增加右下角方位提示 overlay。
## 人工审核状态
- 实现方案:用户已确认。
- 确认信息:用户回复“都确认,后续直接搞”。
## 执行结果
- 已在 `WebSite/src/components/ProjectLibrary.tsx` 完成实现。
- 已拆分 `重置旋转位姿``重置平移缩放位姿`
- 已为旋转、平移、缩放增加快捷调整按钮。
- 已将 3D 模型渲染层级拆为平移容器和中心旋转容器。
- 已新增右下角 X/Y/Z 方位与旋转角度提示。

View File

@@ -0,0 +1,71 @@
# 测试方案 - 2026-05-07-16-35-52
## 静态检查
1. 执行前确认工作区状态:
- `git status --short --branch`
2. 前端类型和构建检查:
- `cd WebSite && npm run build`
3. 如项目存在 lint 脚本,执行:
- `cd WebSite && npm run lint`
## 单元或集成测试
当前项目未发现独立单元测试体系,本次以构建检查、运行时 API 检查和浏览器人工/截图验证为主。
## 关键业务场景验证
1. 打开 `http://192.168.3.11:4000/`
2. 进入 `项目库 - 3D 模型`
3. 验证 `整体位姿` 标题右侧出现:
- `重置旋转位姿`
- `重置平移缩放位姿`
4. 验证旋转 X/Y/Z
- 点击 `-90°` 后角度减少 90。
- 点击 `+90°` 后角度增加 90。
- 数值不会超过 `[-180, 180]`
5. 验证平移 X/Y/Z
- 点击负向按钮后对应轴负向移动。
- 点击正向按钮后对应轴正向移动。
- 数值不会超过 `[-2, 2]`
6. 验证缩放:
- 快捷按钮可放大/缩小。
- 数值不会超过 `[0.5, 2.5]`
7. 验证两个重置按钮:
- `重置旋转位姿` 只影响旋转值。
- `重置平移缩放位姿` 只影响平移和缩放值。
8. 验证鼠标交互:
- 左键拖拽旋转模型。
- 右键或 Shift 拖拽平移模型。
- 滚轮缩放模型。
- 控件数值与鼠标操作同步变化。
9. 验证旋转中心:
- 旋转时模型围绕整体几何中心转动。
- STL 构件相对拼装关系不被破坏。
10. 验证右下角方位提示:
- 显示 X/Y/Z 三轴。
- 显示当前旋转角度。
- 不遮挡模型状态信息和右侧控制面板。
## 医学影像数据相关边界验证
- 本次不修改 DICOM 数据解析、空间 spacing、mask 导出等医学影像数据链路。
- 回归确认项目列表、DICOM 影像页、分割结果页仍可进入。
## 回归风险
- Three.js group 层级调整可能影响模型初始视野。
- 旋转中心修正可能暴露部分 STL 原始坐标异常,需要通过默认项目 `Head_CT_ReConstruct` 验证。
- 快捷按钮布局可能在窄屏右侧面板中换行,需要确保不遮挡滑块和值。
## 人工审核状态
- 测试方案:用户已确认。
- 确认信息:用户回复“都确认,后续直接搞”。
## 执行记录
- `npm run build`:通过。
- `npm run lint`:通过,实际执行 `tsc --noEmit`
- `curl -I http://127.0.0.1:4000/`:返回 `HTTP/1.1 200 OK`
- `curl -s http://127.0.0.1:4000/api/projects`:返回默认项目 `头部 CT 模型逆向体素化演示`DICOM 300、STL 9。

View File

@@ -577,3 +577,21 @@ C. 解决问题方案
D. 后续如何避免问题
默认位姿应该由相机预设和模型位姿共同定义;如果用户提供标准视图截图,应优先匹配相机视角,再决定是否需要固定模型 Z 轴校正。
## 2026-05-07-16-35-52 3D 位姿重置与旋转中心
A. 具体问题
整体位姿只有一个重置按钮,无法分别恢复旋转和平移缩放;同时用户感觉模型旋转中心不在模型正中间,缺少右下角 XYZ 方位参考。
B. 产生问题原因
前端位姿状态虽包含旋转、平移和缩放,但重置操作仍使用单一 `defaultModelPose`Three.js 预览也使用单 group 同时承载旋转、平移和缩放,层级职责不够清晰;画布缺少固定的方位提示。
C. 解决问题方案
抽出统一的位姿 clamp 方法;新增 `重置旋转位姿``重置平移缩放位姿`;旋转项增加 ±90° 快捷按钮平移和缩放增加正负方向快捷步进Three.js 渲染拆为 `poseGroup` 负责平移、`pivotGroup` 负责围绕整体包围盒中心旋转和缩放;右下角增加 X/Y/Z 方位与旋转角度 overlay。
D. 后续如何避免问题
三维位姿控制应按旋转、平移、缩放分开管理;模型加载后必须先基于所有 STL 的整体包围盒做中心化,再把旋转和缩放应用到中心 pivot 上;涉及交互状态时,滑块、按钮和鼠标操作必须共享同一份 clamp 逻辑。

View File

@@ -0,0 +1,56 @@
# 需求分析 - 2026-05-07-16-35-52
## 原始需求摘要
用户要求继续严格使用代码编纂工作流处理本次 3D 模型位姿交互优化:
1.`整体位姿` 旁边的 `重置位姿` 拆分/改为两个按钮:`重置旋转位姿``平移缩放位姿`
2.`整体位姿` 中的旋转和平移控制新增左右 90 度或快捷调整按钮。
3. 将模型旋转中心设定为模型正中间,避免当前旋转中心偏移导致的交互异常。
4. 在模型右下方增加 X/Y/Z 三个轴的旋转坐标或方位提示,方便查看模型当前方位。
## 业务目标
- 让 3D 模型位姿调整更加符合医学三维模型浏览习惯。
- 区分旋转重置和平移/缩放重置,避免用户只想恢复一个维度时丢失全部位姿设置。
- 让模型围绕自身几何中心旋转,提升旋转、平移、缩放的可控性。
- 增加可视化方向参考,帮助用户判断当前 3D 模型朝向。
## 输入与输出
输入:
- 用户在 `项目库 - 3D 模型` 页面对整体位姿滑块、快捷按钮、鼠标拖拽、滚轮等交互。
- 当前项目中的 STL 文件及后端返回的 STL preview 数据。
输出:
- UI 中出现 `重置旋转位姿``平移缩放位姿` 两类按钮。
- 旋转项支持 ±90 度快捷调整;平移项支持正负方向快捷步进;缩放项支持快捷放大/缩小。
- 3D 模型以整体几何中心为旋转中心。
- 3D 预览右下角显示 X/Y/Z 方位坐标和当前旋转角度。
## 影响范围
- `WebSite/src/components/ProjectLibrary.tsx`
- `ModelPose` 状态更新逻辑。
- `NativeStlViewer` 中 Three.js group 层级、模型居中、旋转中心、位姿同步。
- `整体位姿` 控制区按钮和布局。
- 3D 画布右下角方位提示 overlay。
## 风险点
- 若旋转中心调整不当,可能导致 STL 构件之间相对位置被破坏。
- 若快捷按钮步进与滑块范围不一致,可能出现 UI 数值越界。
- 若右下角方位提示遮挡模型或底部状态栏,可能影响可视化体验。
- 平移没有“90 度”的物理含义,需要以正负方向固定步进方式解释。
## 待确认问题
1. “平移新增左右 90 度调整按钮”在空间变换中没有直接物理含义,本方案拟解释为平移 X/Y/Z 的正负方向快捷步进按钮,旋转 X/Y/Z 使用 ±90 度按钮。
2. `平移缩放位姿` 按钮文案是否需要完整写为 `重置平移缩放位姿`;实现方案中会优先使用完整文案,避免歧义。
## 人工审核状态
- 需求分析:用户已确认。
- 确认信息:用户回复“都确认,后续直接搞”。