Merge branch 'master' into feat/design_agent_simple
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Repository checks (push) Successful in 2m48s
Project CI / Frontend tests (push) Successful in 3m26s
Project CI / Backend tests (push) Successful in 7m17s
Project CI / Native shell tests (push) Successful in 19m27s

This commit was merged in pull request #305.
This commit is contained in:
2026-09-09 16:34:16 +08:00
17 changed files with 201 additions and 58 deletions
+9 -1
View File
@@ -5869,7 +5869,7 @@ iframe.preview-frame {
height: var(--resource-card-height);
min-height: var(--resource-card-height);
padding: 0;
overflow: hidden;
overflow: visible;
border: 1px solid #eadbd4;
border-radius: 12px;
background: #fff;
@@ -5912,6 +5912,7 @@ iframe.preview-frame {
inset: 0;
display: grid;
overflow: hidden;
border-radius: inherit;
background: linear-gradient(145deg, #fffaf6, #f6ece6);
color: #c46a40;
place-items: center;
@@ -5973,6 +5974,13 @@ iframe.preview-frame {
text-overflow: ellipsis;
white-space: nowrap;
box-shadow: 0 8px 18px rgb(96 62 47 / 20%);
transform: scale(var(--genarrative-image-canvas-inverse-scale, 1));
transform-origin: top right;
}
.game-resource-card-media-control {
transform: scale(var(--genarrative-image-canvas-inverse-scale, 1));
transform-origin: bottom right;
}
.game-resource-card-open {
@@ -3,6 +3,7 @@ import {
type CanvasViewport,
resolveViewportFromWheel,
} from '@genarrative/image-canvas-core';
import { CanvasWorld } from '@genarrative/image-canvas-react';
import {
Code2,
FileText,
@@ -4906,17 +4907,12 @@ export default function ProjectDevelopmentView({
>
<RotateCcw size={15} aria-hidden="true" />
</button>
<div
<CanvasWorld
className="game-resource-page-world"
viewport={activeResourceCanvasViewport}
data-resource-boundary={`${resourceCanvasNavigationBounds.x},${resourceCanvasNavigationBounds.y},${resourceCanvasNavigationBounds.width},${resourceCanvasNavigationBounds.height}`}
data-resource-fit-boundary={`${resourceCanvasFitBounds.x},${resourceCanvasFitBounds.y},${resourceCanvasFitBounds.width},${resourceCanvasFitBounds.height}`}
data-resource-viewport={`${activeResourceCanvasViewport.x},${activeResourceCanvasViewport.y},${activeResourceCanvasViewport.scale}`}
style={{
width: `${resourceCanvasNavigationBounds.width}px`,
height: `${resourceCanvasNavigationBounds.height}px`,
transform: `translate(${activeResourceCanvasViewport.x}px, ${activeResourceCanvasViewport.y}px) scale(${activeResourceCanvasViewport.scale})`,
transformOrigin: '0 0',
}}
>
{sortMode === 'dependency' ? (
<ResourceDependencyOverlay
@@ -5016,7 +5012,7 @@ export default function ProjectDevelopmentView({
/>
);
})}
</div>
</CanvasWorld>
{(
visibleResourcesByCategory.get(
activePageCategory,
@@ -1986,6 +1986,9 @@ export function registerProjectWorkbenchFoundationTests() {
expect(styles).toMatch(
/\.game-resource-card-open\s*\{[^}]*cursor:\s*pointer[^}]*touch-action:\s*manipulation/s,
);
expect(styles).toMatch(
/\.game-resource-card-media-control\s*\{[^}]*transform:\s*scale\(var\(--genarrative-image-canvas-inverse-scale,\s*1\)\)[^}]*transform-origin:\s*bottom\s+right/s,
);
expect(styles).toMatch(
/\.game-resource-canvas\s*\{[^}]*user-select:\s*none/s,
);
@@ -11,6 +11,7 @@ import type {
ImageCanvasHostScope,
ImageCanvasProjectPort,
} from '@genarrative/image-canvas-core';
import { canvasViewportToWorldTransform } from '@genarrative/image-canvas-react';
import {
act,
fireEvent,
@@ -1125,7 +1126,7 @@ describe('Tauri 素材创作无限画布独立 Surface', () => {
expect(
document.querySelector<HTMLElement>('.genarrative-image-canvas__world')
?.style.transform,
).toBe('translate(-2570px, -2495px) scale(1)');
).toBe(canvasViewportToWorldTransform({ x: -2570, y: -2495, scale: 1 }));
expect(screen.getByText('已定位当前最终图')).toBeTruthy();
});
@@ -1170,7 +1171,7 @@ describe('Tauri 素材创作无限画布独立 Surface', () => {
expect(
document.querySelector<HTMLElement>('.genarrative-image-canvas__world')
?.style.transform,
).toBe('translate(-2570px, -2495px) scale(1)');
).toBe(canvasViewportToWorldTransform({ x: -2570, y: -2495, scale: 1 }));
expect(screen.getByText('已定位当前最终图')).toBeTruthy();
fireEvent.click(layerButton);
@@ -2062,7 +2063,9 @@ describe('Tauri 素材创作无限画布独立 Surface', () => {
clientY: 60,
});
await waitFor(() =>
expect(world.style.transform).toBe('translate(30px, 30px) scale(0.5)'),
expect(world.style.transform).toBe(
canvasViewportToWorldTransform({ x: 30, y: 30, scale: 0.5 }),
),
);
fireEvent.pointerMove(window, {
pointerId: 16,
@@ -2070,7 +2073,9 @@ describe('Tauri 素材创作无限画布独立 Surface', () => {
clientY: 30,
});
await waitFor(() =>
expect(world.style.transform).toBe('translate(0px, 0px) scale(0.5)'),
expect(world.style.transform).toBe(
canvasViewportToWorldTransform({ x: 0, y: 0, scale: 0.5 }),
),
);
fireEvent.pointerUp(window, {
pointerId: 16,
@@ -2315,7 +2320,9 @@ describe('Tauri 素材创作无限画布独立 Surface', () => {
const world = document.querySelector(
'.genarrative-image-canvas__world',
) as HTMLElement;
expect(world.style.transform).toBe('translate(0px, 0px) scale(0.5)');
expect(world.style.transform).toBe(
canvasViewportToWorldTransform({ x: 0, y: 0, scale: 0.5 }),
);
fireEvent.wheel(viewport, {
deltaX: 4,
@@ -2324,7 +2331,9 @@ describe('Tauri 素材创作无限画布独立 Surface', () => {
clientY: 80,
});
await waitFor(() =>
expect(world.style.transform).toBe('translate(-4px, -10px) scale(0.5)'),
expect(world.style.transform).toBe(
canvasViewportToWorldTransform({ x: -4, y: -10, scale: 0.5 }),
),
);
fireEvent.wheel(viewport, {
deltaX: 0,
@@ -2334,7 +2343,9 @@ describe('Tauri 素材创作无限画布独立 Surface', () => {
shiftKey: true,
});
await waitFor(() =>
expect(world.style.transform).toBe('translate(-14px, -10px) scale(0.5)'),
expect(world.style.transform).toBe(
canvasViewportToWorldTransform({ x: -14, y: -10, scale: 0.5 }),
),
);
fireEvent.wheel(viewport, {
deltaX: 0,
@@ -2344,7 +2355,13 @@ describe('Tauri 素材创作无限画布独立 Surface', () => {
ctrlKey: true,
});
await waitFor(() =>
expect(world.style.transform).not.toContain('scale(0.5)'),
expect(world.style.transform).toBe(
canvasViewportToWorldTransform({
x: 100 - 228 * 0.55,
y: 80 - 180 * 0.55,
scale: 0.55,
}),
),
);
});
@@ -87,13 +87,17 @@ function createCanvas(
};
}
function renderedScale(container: HTMLElement) {
const transform = (
container.querySelector('.genarrative-image-canvas__world') as HTMLElement
).style.transform;
const scale = /scale\(([^)]+)\)/.exec(transform)?.[1];
if (!scale) throw new Error(`无法从 ${transform} 读取缩放值`);
return Number(scale);
function logicalViewportScale(container: HTMLElement) {
const world = container.querySelector(
'.genarrative-image-canvas__world',
) as HTMLElement;
const inverseScale = Number(
world.style.getPropertyValue('--genarrative-image-canvas-inverse-scale'),
);
if (!Number.isFinite(inverseScale) || inverseScale <= 0) {
throw new Error(`无法从 world 读取逻辑缩放值`);
}
return 1 / inverseScale;
}
beforeEach(() => {
@@ -110,7 +114,7 @@ describe('PreviewWorkspace quick zoom', () => {
it('uses the toolbar zoom step while the preview is hovered', () => {
const rendered = render(<PreviewWorkspace canvas={createCanvas()} />);
const preview = screen.getByRole('region', { name: 'UI 预览画布' });
const before = renderedScale(rendered.container);
const before = logicalViewportScale(rendered.container);
fireEvent.pointerEnter(preview);
const wasNotCancelled = fireEvent.keyDown(window, {
@@ -121,14 +125,14 @@ describe('PreviewWorkspace quick zoom', () => {
});
expect(wasNotCancelled).toBe(false);
expect(renderedScale(rendered.container)).toBeCloseTo(before * 1.16);
expect(logicalViewportScale(rendered.container)).toBeCloseTo(before * 1.16);
});
it('requires hover or focus and ignores interactive targets', () => {
const rendered = render(<PreviewWorkspace canvas={createCanvas()} />);
const preview = screen.getByRole('region', { name: 'UI 预览画布' });
const zoomButton = screen.getByRole('button', { name: '放大画布' });
const initial = renderedScale(rendered.container);
const initial = logicalViewportScale(rendered.container);
expect(
fireEvent.keyDown(window, {
@@ -137,7 +141,7 @@ describe('PreviewWorkspace quick zoom', () => {
cancelable: true,
}),
).toBe(true);
expect(renderedScale(rendered.container)).toBe(initial);
expect(logicalViewportScale(rendered.container)).toBe(initial);
fireEvent.pointerEnter(preview);
expect(
@@ -147,7 +151,7 @@ describe('PreviewWorkspace quick zoom', () => {
cancelable: true,
}),
).toBe(true);
expect(renderedScale(rendered.container)).toBe(initial);
expect(logicalViewportScale(rendered.container)).toBe(initial);
fireEvent.pointerLeave(preview);
fireEvent.focus(preview);
@@ -157,13 +161,15 @@ describe('PreviewWorkspace quick zoom', () => {
ctrlKey: true,
cancelable: true,
});
expect(renderedScale(rendered.container)).toBeCloseTo(initial * 0.86);
expect(logicalViewportScale(rendered.container)).toBeCloseTo(
initial * 0.86,
);
});
it('keeps actual-size and fit shortcuts within the preview scope', () => {
const rendered = render(<PreviewWorkspace canvas={createCanvas()} />);
const preview = screen.getByRole('region', { name: 'UI 预览画布' });
const fitted = renderedScale(rendered.container);
const fitted = logicalViewportScale(rendered.container);
fireEvent.focus(preview);
fireEvent.keyDown(window, {
@@ -171,14 +177,14 @@ describe('PreviewWorkspace quick zoom', () => {
ctrlKey: true,
cancelable: true,
});
expect(renderedScale(rendered.container)).toBe(1);
expect(logicalViewportScale(rendered.container)).toBe(1);
fireEvent.keyDown(window, {
key: '0',
ctrlKey: true,
cancelable: true,
});
expect(renderedScale(rendered.container)).toBe(fitted);
expect(logicalViewportScale(rendered.container)).toBe(fitted);
});
it('leaves browser zoom untouched when the preview has no content', () => {