diff --git a/apps/ai-game-creator-shell/src/view/ui-editor/components/Inspector/Transform/TransformEditor.tsx b/apps/ai-game-creator-shell/src/view/ui-editor/components/Inspector/Transform/TransformEditor.tsx index c99a1020b..62e68347c 100644 --- a/apps/ai-game-creator-shell/src/view/ui-editor/components/Inspector/Transform/TransformEditor.tsx +++ b/apps/ai-game-creator-shell/src/view/ui-editor/components/Inspector/Transform/TransformEditor.tsx @@ -73,6 +73,35 @@ const CORNERS: readonly { }, ]; +function CornerIcon({ + corner, + active = false, +}: { + corner: Corner; + active?: boolean; +}) { + const paths: Record = { + 'top-left': 'M5 11V5h6', + 'top-right': 'M13 11V5H7', + 'bottom-left': 'M5 9v6h6', + 'bottom-right': 'M13 9v6H7', + }; + return ( + + ); +} + const PRESETS: AnchorPreset[] = ROW_MODES.flatMap((y) => COLUMN_MODES.map((x) => ({ id: `${y}-${x}`, @@ -218,6 +247,8 @@ function updateCorner( function VectorInputRow({ label, hint, + hideLabel = false, + stacked = false, values, step, readOnly, @@ -225,24 +256,45 @@ function VectorInputRow({ }: { label: string; hint: string; + hideLabel?: boolean; + stacked?: boolean; values: readonly [number, number]; step: number; readOnly: boolean; onCommit: (axis: Axis, value: number) => void; }) { + const fields = AXIS_LABELS.map((axisLabel, axis) => ( + onCommit(axis as Axis, value)} + /> + )); + + if (stacked) { + return ( +
+ {hideLabel ? ( + {label} + ) : ( + + )} +
{fields}
+
+ ); + } + return (
- - {AXIS_LABELS.map((axisLabel, axis) => ( - onCommit(axis as Axis, value)} - /> - ))} + {hideLabel ? ( + {label} + ) : ( + + )} + {fields}
); } @@ -250,7 +302,7 @@ function VectorInputRow({ function FieldLabel({ label, hint }: { label: string; hint: string }) { return ( {label} @@ -262,12 +314,6 @@ function FieldLabel({ label, hint }: { label: string; hint: string }) { > ); } @@ -425,7 +471,7 @@ function DirectionButton({ type="button" aria-label={ariaLabel} disabled={disabled} - className="grid size-10 place-items-center rounded-xl border border-emerald-200 bg-emerald-50 text-emerald-700 transition hover:border-emerald-300 hover:bg-emerald-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-300 disabled:cursor-default disabled:opacity-40" + className="grid size-8 place-items-center rounded-lg border border-(--platform-subpanel-border) bg-white/70 text-(--platform-text-soft) transition hover:border-orange-300 hover:bg-orange-50 hover:text-orange-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-orange-200 disabled:cursor-default disabled:opacity-40" onPointerDown={(event) => { event.preventDefault(); startRepeating(event.shiftKey ? 10 : 1); @@ -673,91 +719,100 @@ export function TransformEditor({ ) : null} -
-
-
- 偏移角点 -
- {CORNERS.map((corner) => { - const active = corner.id === selectedCorner; - return ( - - ); - })} -
- - - onChange(updateCorner(transform, selectedCorner, axis, value)) - } +
+ -
- - - adjustSelectedCorner(1, -1, multiplier * 1) - } - > - - - - adjustSelectedCorner(0, -1, multiplier * 1) - } - > - -
- {selectedCornerLabel} +
+
+ {CORNERS.map((corner) => { + const active = corner.id === selectedCorner; + return ( + + ); + })} +
+ +
+
+ + + adjustSelectedCorner(1, -1, multiplier * 1) + } + > + + + + adjustSelectedCorner(0, -1, multiplier * 1) + } + > + +
+ +
+ + adjustSelectedCorner(0, 1, multiplier * 1) + } + > + + + + adjustSelectedCorner(1, 1, multiplier * 1) + } + > + + +
+ + + onChange(updateCorner(transform, selectedCorner, axis, value)) + } + />
- - adjustSelectedCorner(0, 1, multiplier * 1) - } - > - - - - adjustSelectedCorner(1, 1, multiplier * 1) - } - > - -
diff --git a/apps/ai-game-creator-shell/tests/TransformEditor.test.tsx b/apps/ai-game-creator-shell/tests/TransformEditor.test.tsx index e8b28f0dc..62683bea3 100644 --- a/apps/ai-game-creator-shell/tests/TransformEditor.test.tsx +++ b/apps/ai-game-creator-shell/tests/TransformEditor.test.tsx @@ -25,20 +25,20 @@ describe('TransformEditor corner offset controls', () => { renderEditor(); expect( - screen.getByRole('spinbutton', { name: '左上角偏移 X' }), + screen.getByRole('spinbutton', { name: '位置微调 X' }), ).toHaveProperty('value', '10'); expect( - screen.getByRole('spinbutton', { name: '左上角偏移 Y' }), + screen.getByRole('spinbutton', { name: '位置微调 Y' }), ).toHaveProperty('value', '20'); expect(screen.getAllByRole('spinbutton')).toHaveLength(2); fireEvent.click(screen.getByRole('button', { name: '右下角' })); expect( - screen.getByRole('spinbutton', { name: '右下角偏移 X' }), + screen.getByRole('spinbutton', { name: '位置微调 X' }), ).toHaveProperty('value', '30'); expect( - screen.getByRole('spinbutton', { name: '右下角偏移 Y' }), + screen.getByRole('spinbutton', { name: '位置微调 Y' }), ).toHaveProperty('value', '40'); }); @@ -46,9 +46,7 @@ describe('TransformEditor corner offset controls', () => { const { onChange } = renderEditor(); fireEvent.click(screen.getByRole('button', { name: '右上角' })); - const xInput = screen.getByRole('spinbutton', { - name: '右上角偏移 X', - }); + const xInput = screen.getByRole('spinbutton', { name: '位置微调 X' }); fireEvent.change(xInput, { target: { value: '55' } }); fireEvent.blur(xInput); diff --git a/docs/technical/【设计】UI编辑器变换角点偏移编辑器-2026-09-03.md b/docs/technical/【设计】UI编辑器变换角点偏移编辑器-2026-09-03.md index 2d7728779..00fc90bb4 100644 --- a/docs/technical/【设计】UI编辑器变换角点偏移编辑器-2026-09-03.md +++ b/docs/technical/【设计】UI编辑器变换角点偏移编辑器-2026-09-03.md @@ -12,12 +12,12 @@ 四角是现有两个 offset 向量的投影,不是四组独立数据: -| 选中角 | X 输入写入 | Y 输入写入 | -| --- | --- | --- | -| 左上 | `offset_min[0]` | `offset_min[1]` | -| 右上 | `offset_max[0]` | `offset_min[1]` | -| 左下 | `offset_min[0]` | `offset_max[1]` | -| 右下 | `offset_max[0]` | `offset_max[1]` | +| 选中角 | X 输入写入 | Y 输入写入 | +| ------ | --------------- | --------------- | +| 左上 | `offset_min[0]` | `offset_min[1]` | +| 右上 | `offset_max[0]` | `offset_min[1]` | +| 左下 | `offset_min[0]` | `offset_max[1]` | +| 右下 | `offset_max[0]` | `offset_max[1]` | 输入框始终显示对应 `Transform` 的原始 offset 值,保留现有正负号;不转换成 CSS `right`/`bottom` 语义。切换角时读取同一组共享分量,修改后其他受影响角会随数据更新而同步。 @@ -31,6 +31,20 @@ - 数字输入沿用现有行为:失焦或回车提交,Escape 恢复当前值,非法或空值不写回;只读状态禁用所有变更入口。 - 锚点预设、自定义锚点、无效矩形 warning、根节点禁止修改等现有行为保持不变。 +## 视觉约束 + +- 四角选择器使用 icon-only 的 L 形角标,按左上、右上、左下、右下方向旋转;不在按钮内显示角点文字。 +- 角点示意图中心不显示说明文字,仅保留淡色矩形和角点高亮。 +- 输入区不显示“偏移”标签,只保留 X/Y 字段;说明信息按钮 `i` 放在“位置微调”标题旁,完整语义通过 `aria-label` 和 `title` 提供。 +- 说明只使用浏览器原生 `title` 提示,不再渲染自定义悬浮气泡。 +- 方向键保持十字布局,使用紧凑的中性 icon-only 按钮;橙色只用于选中态和焦点/悬停强调,避免大面积绿色填充。 +- 角点选择器和方向键的可点击尺寸不小于 28px。 +- 主控区采用两列布局:左侧为无分隔线的方形四角选择器,右侧上方为四方向键、下方为唯一一组 X/Y 输入。 +- 四角选择器不绘制连接线或虚线框,依靠方形容器、角标图标和选中态表达空间关系。 +- “位置微调”作为右侧控制区的短标题显示在两列之上;两列内容从同一条顶线开始对齐。 +- 当前角图标只保留在方向键十字中心,数字输入行不再重复显示角点指示器。 +- 左侧四角选择器保持较小的正方形,并在右侧方向键、纵向 X/Y 输入区的整体高度中垂直居中;X/Y 输入在右侧纵向排列,以避免窄面板中的横向拥挤。 + ## 非目标 - 不新增四角持久化字段,不修改 `Transform` schema 或后端接口。