修复UI编辑器等比角点缩放
抽取等比缩放主轴与尺寸推导算法 锁定单次拖拽的主导轴并保持对角锚点 补充比例缩放回归测试与交互规则文档
This commit is contained in:
+14
-19
@@ -1,4 +1,6 @@
|
||||
import type { Node as UiNode } from '../../../../features/ui-editor/types/Node';
|
||||
import type { ResizeAxis } from './proportionalResize';
|
||||
import { resolveProportionalResize } from './proportionalResize';
|
||||
|
||||
export type ResizeHandle = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w';
|
||||
|
||||
@@ -76,6 +78,7 @@ export function resizePageRect(
|
||||
handle: ResizeHandle,
|
||||
delta: [number, number],
|
||||
keepRatio: boolean,
|
||||
ratioAxis?: ResizeAxis,
|
||||
): PageRect {
|
||||
const next: PageRect = {
|
||||
min: [startRect.min[0], startRect.min[1]],
|
||||
@@ -105,25 +108,17 @@ export function resizePageRect(
|
||||
}
|
||||
|
||||
if (keepRatio && handle.length === 2) {
|
||||
const startWidth = Math.max(
|
||||
MIN_NODE_SIZE,
|
||||
startRect.max[0] - startRect.min[0],
|
||||
);
|
||||
const startHeight = Math.max(
|
||||
MIN_NODE_SIZE,
|
||||
startRect.max[1] - startRect.min[1],
|
||||
);
|
||||
const ratio = startWidth / startHeight;
|
||||
const rawWidth = Math.max(
|
||||
MIN_NODE_SIZE,
|
||||
Math.abs(next.max[0] - next.min[0]),
|
||||
);
|
||||
const rawHeight = Math.max(
|
||||
MIN_NODE_SIZE,
|
||||
Math.abs(next.max[1] - next.min[1]),
|
||||
);
|
||||
const width = Math.max(MIN_NODE_SIZE, rawWidth, rawHeight * ratio);
|
||||
const height = Math.max(MIN_NODE_SIZE, width / ratio);
|
||||
const {
|
||||
size: [width, height],
|
||||
} = resolveProportionalResize({
|
||||
startSize: [
|
||||
startRect.max[0] - startRect.min[0],
|
||||
startRect.max[1] - startRect.min[1],
|
||||
],
|
||||
size: [next.max[0] - next.min[0], next.max[1] - next.min[1]],
|
||||
delta,
|
||||
axis: ratioAxis,
|
||||
});
|
||||
if (handle.includes('w')) next.min[0] = startRect.max[0] - width;
|
||||
else next.max[0] = startRect.min[0] + width;
|
||||
if (handle.includes('n')) next.min[1] = startRect.max[1] - height;
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
export type ResizeAxis = 'horizontal' | 'vertical';
|
||||
|
||||
const MIN_SIZE = 1;
|
||||
|
||||
export type ProportionalResizeInput = {
|
||||
startSize: [number, number];
|
||||
size: [number, number];
|
||||
delta: [number, number];
|
||||
axis?: ResizeAxis;
|
||||
};
|
||||
|
||||
export type ProportionalResizeResult = {
|
||||
axis: ResizeAxis;
|
||||
size: [number, number];
|
||||
};
|
||||
|
||||
export function resolveProportionalResizeAxis(
|
||||
startSize: [number, number],
|
||||
delta: [number, number],
|
||||
): ResizeAxis {
|
||||
const startWidth = Math.max(MIN_SIZE, startSize[0]);
|
||||
const startHeight = Math.max(MIN_SIZE, startSize[1]);
|
||||
return Math.abs(delta[0]) / startWidth >= Math.abs(delta[1]) / startHeight
|
||||
? 'horizontal'
|
||||
: 'vertical';
|
||||
}
|
||||
|
||||
export function resolveProportionalResize({
|
||||
startSize,
|
||||
size,
|
||||
delta,
|
||||
axis,
|
||||
}: ProportionalResizeInput): ProportionalResizeResult {
|
||||
const startWidth = Math.max(MIN_SIZE, startSize[0]);
|
||||
const startHeight = Math.max(MIN_SIZE, startSize[1]);
|
||||
const resolvedAxis = axis ?? resolveProportionalResizeAxis(startSize, delta);
|
||||
const ratio = startWidth / startHeight;
|
||||
const width =
|
||||
resolvedAxis === 'horizontal'
|
||||
? Math.max(MIN_SIZE, size[0])
|
||||
: Math.max(MIN_SIZE, size[1]) * ratio;
|
||||
return {
|
||||
axis: resolvedAxis,
|
||||
size: [width, width / ratio],
|
||||
};
|
||||
}
|
||||
+21
-4
@@ -15,6 +15,10 @@ import {
|
||||
resizePageRect,
|
||||
setOffsetsForPageRect,
|
||||
} from './nodeTransformGeometry';
|
||||
import {
|
||||
type ResizeAxis,
|
||||
resolveProportionalResizeAxis,
|
||||
} from './proportionalResize';
|
||||
|
||||
type ViewportScale = { scale: number };
|
||||
|
||||
@@ -36,6 +40,7 @@ type NodeResizeState = {
|
||||
startTransform: UiNode['layout']['transform'];
|
||||
startRect: PageRect;
|
||||
parentRect: PageRect;
|
||||
ratioAxis: ResizeAxis | null;
|
||||
hasMoved: boolean;
|
||||
};
|
||||
|
||||
@@ -163,6 +168,7 @@ export function useNodeTransformInteraction({
|
||||
startTransform: structuredClone(node.layout.transform),
|
||||
startRect: context.rect,
|
||||
parentRect: context.parentRect,
|
||||
ratioAxis: null,
|
||||
hasMoved: false,
|
||||
};
|
||||
},
|
||||
@@ -184,14 +190,25 @@ export function useNodeTransformInteraction({
|
||||
}
|
||||
resize.hasMoved = true;
|
||||
const scale = viewportRef.current?.scale ?? 1;
|
||||
const delta: [number, number] = [
|
||||
(event.clientX - resize.startClientX) / scale,
|
||||
(event.clientY - resize.startClientY) / scale,
|
||||
];
|
||||
if (event.shiftKey && !resize.ratioAxis) {
|
||||
resize.ratioAxis = resolveProportionalResizeAxis(
|
||||
[
|
||||
resize.startRect.max[0] - resize.startRect.min[0],
|
||||
resize.startRect.max[1] - resize.startRect.min[1],
|
||||
],
|
||||
delta,
|
||||
);
|
||||
}
|
||||
const nextRect = resizePageRect(
|
||||
resize.startRect,
|
||||
resize.handle,
|
||||
[
|
||||
(event.clientX - resize.startClientX) / scale,
|
||||
(event.clientY - resize.startClientY) / scale,
|
||||
],
|
||||
delta,
|
||||
event.shiftKey,
|
||||
resize.ratioAxis ?? undefined,
|
||||
);
|
||||
controller.updateNodeTransform(
|
||||
activeImageId,
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
resolveProportionalResize,
|
||||
resolveProportionalResizeAxis,
|
||||
} from '../src/view/ui-editor/components/preview/proportionalResize';
|
||||
|
||||
describe('proportional resize', () => {
|
||||
it('selects the axis with the greater relative size change', () => {
|
||||
expect(resolveProportionalResizeAxis([100, 100], [30, -20])).toBe(
|
||||
'horizontal',
|
||||
);
|
||||
expect(resolveProportionalResizeAxis([100, 100], [20, -30])).toBe(
|
||||
'vertical',
|
||||
);
|
||||
});
|
||||
|
||||
it('uses the supplied locked axis instead of the larger candidate size', () => {
|
||||
expect(
|
||||
resolveProportionalResize({
|
||||
startSize: [100, 100],
|
||||
size: [150, 90],
|
||||
delta: [50, -10],
|
||||
axis: 'horizontal',
|
||||
}),
|
||||
).toEqual({ axis: 'horizontal', size: [150, 150] });
|
||||
});
|
||||
|
||||
it('uses the locked vertical axis for an opposite-sign diagonal drag', () => {
|
||||
expect(
|
||||
resolveProportionalResize({
|
||||
startSize: [100, 100],
|
||||
size: [110, 50],
|
||||
delta: [10, -50],
|
||||
axis: 'vertical',
|
||||
}),
|
||||
).toEqual({ axis: 'vertical', size: [50, 50] });
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,9 @@
|
||||
# AI 游戏创作智能体 App 实施计划
|
||||
|
||||
## 2026-08-19 UI Editor 预览等比角点缩放
|
||||
|
||||
UI Editor 预览中按住 Shift 拖动四个角点时,以本次进入等比缩放时相对原尺寸变化更大的轴作为主轴(完全相等时取水平轴),并在本次指针拖动内锁定。主轴按手柄方向和位移符号决定放大或缩小;另一轴只由初始宽高比推导,对角保持固定。不得再从两个轴各自推导的候选尺寸中取较大值,否则一轴放大、另一轴缩小时会覆盖较小轴的输入,导致控制点看似朝鼠标反方向跳动。锁定后,受比例驱动的从轴可以不跟随鼠标在该轴的位移,这是等比约束的明确结果。
|
||||
|
||||
## 2026-08-18 UI Editor Inspector 统一锁定态
|
||||
|
||||
UI Editor Inspector 的全局只读状态唯一来源是 `controller.editor.isLocked`。`InspectorSidebar` 在根层提供共享只读 Context,并在顶部只显示一次状态徽章:锁定时使用现有 slate 风格与“只读”锁图标,解锁时显示“可编辑”;整个 Inspector 内容区同步使用锁定表面。节点根的 Transform 局部只读继续仅约束该 Transform 区域,不能被解释为整个 Inspector 已锁定。
|
||||
|
||||
Reference in New Issue
Block a user