重构预览网格步长计算

提取预览网格自适应步长到独立模块

补充缩放网格计算与背景样式测试

同步预览画布缩放交互文档
This commit is contained in:
2026-09-14 19:40:13 +08:00
parent 22ac1f4c0b
commit e9ddbf16da
4 changed files with 64 additions and 1 deletions
@@ -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`,保持世界原点对齐,不改变圆点样式。
- 保留快捷键与缩放焦点边界的组件级回归测试,不扩展端到端测试。
## 验收