重构预览网格步长计算
提取预览网格自适应步长到独立模块 补充缩放网格计算与背景样式测试 同步预览画布缩放交互文档
This commit is contained in:
+3
-1
@@ -34,6 +34,7 @@ import type { Node as UiNode } from '../../../../features/ui-editor/types/Node';
|
||||
import type { NodeId } from '../../../../features/ui-editor/types/NodeId';
|
||||
import type { UiEditorCanvasProjection } from '../../useUiEditorPage';
|
||||
import { UiNodeContextMenu } from '../UiNodeContextMenu';
|
||||
import { resolvePreviewGridStep } from './previewGrid';
|
||||
import {
|
||||
handlePreviewZoomKeyDown,
|
||||
isPreviewZoomInteractiveTarget,
|
||||
@@ -239,6 +240,7 @@ export function PreviewWorkspace({
|
||||
}, [scaleViewportFromCenter]);
|
||||
|
||||
const displayPercent = formatCanvasDisplayScalePercent(viewport.scale);
|
||||
const previewGridStep = resolvePreviewGridStep(viewport.scale);
|
||||
|
||||
const zoomToDisplayScale = useCallback(
|
||||
(displayScale: number) => {
|
||||
@@ -479,7 +481,7 @@ export function PreviewWorkspace({
|
||||
className="size-full select-none outline-none bg-[radial-gradient(#b8aaa0_1.5px,transparent_1.5px)]"
|
||||
style={{
|
||||
backgroundPosition: `${viewport.x}px ${viewport.y}px`,
|
||||
backgroundSize: `${28 * viewport.scale}px ${28 * viewport.scale}px`,
|
||||
backgroundSize: `${previewGridStep * viewport.scale}px ${previewGridStep * viewport.scale}px`,
|
||||
}}
|
||||
role="region"
|
||||
aria-label="UI 预览画布"
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/** 背景网格的基础世界步长;缩放时按 2 倍档位调整屏幕密度。 */
|
||||
export const PREVIEW_GRID_BASE_STEP = 28;
|
||||
const PREVIEW_GRID_MIN_SCREEN_SPACING = 20;
|
||||
const PREVIEW_GRID_MAX_SCREEN_SPACING = 40;
|
||||
|
||||
/**
|
||||
* 选择一个离散的世界步长,让网格圆点在屏幕上保持可读密度。
|
||||
* 背景位置仍使用 viewport 的屏幕平移量,因此切档不会破坏世界原点对齐。
|
||||
*/
|
||||
export function resolvePreviewGridStep(viewportScale: number): number {
|
||||
if (!Number.isFinite(viewportScale) || viewportScale <= 0) {
|
||||
return PREVIEW_GRID_BASE_STEP;
|
||||
}
|
||||
|
||||
let step = PREVIEW_GRID_BASE_STEP;
|
||||
let screenSpacing = step * viewportScale;
|
||||
while (screenSpacing < PREVIEW_GRID_MIN_SCREEN_SPACING) {
|
||||
step *= 2;
|
||||
screenSpacing *= 2;
|
||||
}
|
||||
while (screenSpacing >= PREVIEW_GRID_MAX_SCREEN_SPACING) {
|
||||
step /= 2;
|
||||
screenSpacing /= 2;
|
||||
}
|
||||
return step;
|
||||
}
|
||||
@@ -5,6 +5,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { Node as UiNode } from '../src/features/ui-editor/types/Node';
|
||||
import type { UIDesignImage } from '../src/features/ui-editor/types/UIDesignImage';
|
||||
import {
|
||||
PREVIEW_GRID_BASE_STEP,
|
||||
resolvePreviewGridStep,
|
||||
} from '../src/view/ui-editor/components/preview/previewGrid';
|
||||
import { PreviewWorkspace } from '../src/view/ui-editor/components/preview/PreviewWorkspace';
|
||||
import type { UiEditorCanvasProjection } from '../src/view/ui-editor/useUiEditorPage';
|
||||
|
||||
@@ -41,6 +45,10 @@ const root: UiNode = {
|
||||
component: null,
|
||||
children_display_mode: 'Stack',
|
||||
children: [],
|
||||
offset: {
|
||||
min: [0, 0],
|
||||
max: [1200, 800],
|
||||
},
|
||||
};
|
||||
|
||||
const activeImage: UIDesignImage = {
|
||||
@@ -62,6 +70,7 @@ function createCanvas(
|
||||
isLocked: false,
|
||||
activeImage,
|
||||
activeImageId: 'page-1',
|
||||
uiTrees: [{ src_ui_design: 'page-1', root }],
|
||||
previewUrls: { 'page-1': 'data:image/png;base64,' },
|
||||
images: { 'page-1': activeImage },
|
||||
sprites: {},
|
||||
@@ -111,6 +120,31 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe('PreviewWorkspace quick zoom', () => {
|
||||
it('chooses doubled or halved world steps to keep the grid readable', () => {
|
||||
expect(resolvePreviewGridStep(1)).toBe(PREVIEW_GRID_BASE_STEP);
|
||||
expect(resolvePreviewGridStep(0.5)).toBe(56);
|
||||
expect(resolvePreviewGridStep(0.125)).toBe(224);
|
||||
expect(resolvePreviewGridStep(2)).toBe(14);
|
||||
expect(resolvePreviewGridStep(4)).toBe(7);
|
||||
|
||||
for (const scale of [0.125, 0.25, 0.5, 1, 2, 4, 8]) {
|
||||
const spacing = resolvePreviewGridStep(scale) * scale;
|
||||
expect(spacing).toBeGreaterThanOrEqual(20);
|
||||
expect(spacing).toBeLessThan(40);
|
||||
}
|
||||
});
|
||||
|
||||
it('applies the adaptive screen-space spacing to the preview background', () => {
|
||||
const rendered = render(<PreviewWorkspace canvas={createCanvas()} />);
|
||||
const preview = screen.getByRole('region', { name: 'UI 预览画布' });
|
||||
const scale = logicalViewportScale(rendered.container);
|
||||
const [backgroundWidth] = preview.style.backgroundSize.split('px');
|
||||
|
||||
expect(Number(backgroundWidth)).toBeCloseTo(
|
||||
resolvePreviewGridStep(scale) * scale,
|
||||
);
|
||||
});
|
||||
|
||||
it('uses the toolbar zoom step while the preview is hovered', () => {
|
||||
const rendered = render(<PreviewWorkspace canvas={createCanvas()} />);
|
||||
const preview = screen.getByRole('region', { name: 'UI 预览画布' });
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
- 仅在存在可缩放视口且确认命中快捷键时调用 `preventDefault()` 与 `stopPropagation()`,防止浏览器页面同时缩放。
|
||||
- 保留现有 `Ctrl/Cmd+0` 适配画布与 `Ctrl/Cmd+1` 恢复 100% 行为,不增加其他重置快捷键。缩放仍是当前预览实例的临时 UI 状态。
|
||||
- 快捷键和百分比使用逻辑 `viewport.scale` 作为缩放真相;共享 `CanvasWorld` 的渲染 transform 可能包含超采样换算,验证时不得直接把 CSS transform 值当作用户可见比例。
|
||||
- 预览画布背景继续使用圆点网格,并按 `14/28/56/112…` 世界步长的 2 倍档位自适应缩放;屏幕间距目标约为 20–40px,缩放过小时自动加倍步长、放大时自动减半步长。网格背景位置继续跟随 `viewport.x/y`,保持世界原点对齐,不改变圆点样式。
|
||||
- 保留快捷键与缩放焦点边界的组件级回归测试,不扩展端到端测试。
|
||||
|
||||
## 验收
|
||||
|
||||
Reference in New Issue
Block a user