修复画布内层空白区域框选
框选命中判断支持 world 的真实后代元素 回归测试覆盖共享 CanvasWorld 内层 pointerdown 冒泡 同步记录共享画布框选命中排障经验
This commit is contained in:
@@ -4983,3 +4983,11 @@
|
||||
- 原因:Tauri Windows bundler 执行自己的 `<tauri_tools_path>\NSIS\makensis.exe`,默认位于当前用户 `%LOCALAPPDATA%\tauri`,不使用 PATH 中预装的 `makensis.exe`;Jenkins LocalSystem/systemprofile 的 AppData 可能无法启动该缓存程序。
|
||||
- 处理:Windows 专用 Tauri 配置设置 `bundle.useLocalToolsDir: true`,把工具缓存到 `src-tauri/target/.tauri/NSIS`;Jenkins 预检验证实际用户、项目工具目录可写,并在构建失败时打印实际缓存路径和绝对路径执行结果。
|
||||
- 验证:不要把 PATH 中 `makensis` 可发现当作 Tauri bundler 工具可执行的充分证据;需要在 Windows Agent 上检查 `target/.tauri/NSIS/makensis.exe`、ACL、EDR/Defender 和直接 `-VERSION` 结果。
|
||||
|
||||
## 共享画布框选需要识别 world 的真实后代命中
|
||||
|
||||
- 现象:共享 `CanvasWorld` 在 world 外层增加 `.genarrative-image-canvas__world-content` 后,点击或拖拽空白画布时事件 `target` 是内层 div;框选逻辑若只检查外层 world 本身,就会只清除焦点而不创建选框。
|
||||
- 原因:viewport 上的 pointer 事件通过冒泡接收,`event.currentTarget` 是 viewport,空白区域的 `event.target` 可能是 world 的任意后代,不保证命中外层元素。
|
||||
- 处理:框选命中判断使用 `Element.closest('.genarrative-image-canvas__world')`,并保留 viewport 自身命中路径;回归测试通过真实 `CanvasWorld` DOM 的 `pointerdown` 冒泡覆盖内层 world content。
|
||||
- 验证:`src/components/image-editor/useImageCanvasStageInteractions.test.tsx` 覆盖内层 world content 命中,定向交互测试通过。
|
||||
- 关联:`packages/image-canvas-react/src/useImageCanvasStageInteractions.ts`、`packages/image-canvas-react/src/CanvasWorld.tsx`。
|
||||
|
||||
@@ -342,11 +342,12 @@ export function useImageCanvasStageInteractions({
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
const target = event.target as HTMLElement;
|
||||
const target = event.target;
|
||||
if (
|
||||
effectiveTool === 'select' &&
|
||||
(event.target === event.currentTarget ||
|
||||
target.classList.contains('genarrative-image-canvas__world'))
|
||||
(target instanceof Element &&
|
||||
target.closest('.genarrative-image-canvas__world') !== null))
|
||||
) {
|
||||
event.preventDefault();
|
||||
const rect = canvasViewportRef.current?.getBoundingClientRect();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { CanvasWorld } from '@genarrative/image-canvas-react';
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import {
|
||||
type PointerEvent as ReactPointerEvent,
|
||||
@@ -150,7 +151,6 @@ function StageInteractionsHarness({
|
||||
) => void;
|
||||
}) {
|
||||
const canvasViewportRef = useRef<HTMLDivElement | null>(null);
|
||||
const worldRef = useRef<HTMLDivElement | null>(null);
|
||||
const [activeTool, setActiveTool] = useState<'select' | 'hand'>('select');
|
||||
const [layers, setLayers] = useState<CanvasLayer[]>(() => [
|
||||
createLayer({ id: 'first', x: 40, y: 40, zIndex: 1 }),
|
||||
@@ -288,11 +288,9 @@ function StageInteractionsHarness({
|
||||
onPointerMove={interaction.handlePointerMove}
|
||||
onPointerUp={interaction.finishDrag}
|
||||
>
|
||||
<div
|
||||
ref={worldRef}
|
||||
className="genarrative-image-canvas__world"
|
||||
data-testid="world"
|
||||
/>
|
||||
<CanvasWorld viewport={viewport} data-testid="world">
|
||||
<span aria-hidden="true" />
|
||||
</CanvasWorld>
|
||||
{layers.map((layer) => (
|
||||
<button
|
||||
key={layer.id}
|
||||
@@ -565,7 +563,8 @@ function StageInteractionsHarness({
|
||||
pointerId: 3,
|
||||
clientX: 20,
|
||||
clientY: 20,
|
||||
target: worldRef.current ?? undefined,
|
||||
target:
|
||||
document.querySelector('[data-testid="world"]') ?? undefined,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
@@ -1240,6 +1239,28 @@ describe('useImageCanvasStageInteractions', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('starts a marquee when the real world content receives the pointer down', () => {
|
||||
render(<StageInteractionsHarness />);
|
||||
|
||||
const worldContent = screen
|
||||
.getByTestId('world')
|
||||
.querySelector('.genarrative-image-canvas__world-content');
|
||||
expect(worldContent).not.toBeNull();
|
||||
const pointerDown = new Event('pointerdown', { bubbles: true });
|
||||
Object.defineProperties(pointerDown, {
|
||||
pointerId: { configurable: true, value: 30 },
|
||||
clientX: { configurable: true, value: 20 },
|
||||
clientY: { configurable: true, value: 20 },
|
||||
button: { configurable: true, value: 0 },
|
||||
buttons: { configurable: true, value: 1 },
|
||||
});
|
||||
act(() => {
|
||||
worldContent?.dispatchEvent(pointerDown);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('marquee').textContent).toBe('20,20');
|
||||
});
|
||||
|
||||
it('shows snap guides when generation frames drag near layer alignment', () => {
|
||||
render(<StageInteractionsHarness />);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user