抽出素材库框选几何模型
扩展 ImageCanvasAssetLibraryModel 承载文件夹命中和框选几何规则 让素材库 hook 保留 DOM 和后端副作用并调用纯模型 补充素材库几何模型单测覆盖置顶和反向框选 更新 TRACKING.md 记录第四十四执行批次验证
This commit is contained in:
@@ -167,3 +167,5 @@
|
|||||||
- 2026-06-17 前端拆分第四十一执行批次:继续收口 `useImageCanvasGenerationWorkflow`,新增 `ImageCanvasGenerationDialogModel`,把普通生图 / 规范 / 角色形象 / 图标素材生成对话框草稿、修改图片 / 快速编辑 / 角色动画面板草稿、角色 / 图标参考选择、规范表单更新、图标描述更新、角色动画时长更新以及生成器失焦 / 关闭规则从 hook 中抽成纯模型;workflow hook 保留真实 API 调用、生成结果落画布、侧栏 / 工具 / 选中态副作用和错误回写。新增模型单测覆盖各类草稿、失败态清理、引用选择、描述限制、动画时长和 composer 可见性;`useImageCanvasGenerationWorkflow.ts` 从 1075 行降至 870 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏标题为 `图层`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。
|
- 2026-06-17 前端拆分第四十一执行批次:继续收口 `useImageCanvasGenerationWorkflow`,新增 `ImageCanvasGenerationDialogModel`,把普通生图 / 规范 / 角色形象 / 图标素材生成对话框草稿、修改图片 / 快速编辑 / 角色动画面板草稿、角色 / 图标参考选择、规范表单更新、图标描述更新、角色动画时长更新以及生成器失焦 / 关闭规则从 hook 中抽成纯模型;workflow hook 保留真实 API 调用、生成结果落画布、侧栏 / 工具 / 选中态副作用和错误回写。新增模型单测覆盖各类草稿、失败态清理、引用选择、描述限制、动画时长和 composer 可见性;`useImageCanvasGenerationWorkflow.ts` 从 1075 行降至 870 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏标题为 `图层`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。
|
||||||
- 2026-06-17 前端拆分第四十二执行批次:继续收口 `useImageCanvasStageInteractions`,新增 `ImageCanvasStageInteractionModel`,把 pointer button / client / id 归一化、画布框选初始状态、抓手平移拖拽状态、多选图层选择与图层拖拽初始状态、生成器 composer 随图层点击显隐、生成占位拖拽状态、小地图拖拽状态和拖拽阈值从 hook 中抽成纯模型;stage hook 保留 DOM 事件拦截、pointer capture / release、React 状态写入、拖拽移动执行和回调副作用。新增模型单测覆盖 pointer 兜底、中键识别、框选 / 平移状态、多选 toggle、组拖拽初始层快照、生成器 composer 规则、生成占位状态和小地图 click / drag 分流;`useImageCanvasStageInteractions.ts` 从 610 行降至 521 行。只读子代理复核结论:当前没有同一轮必须顺手拆的第二个明显大块,`ImageCanvasEditorView.tsx` 已主要是组合层,generation / asset / upload hooks 剩余复杂度多为异步编排或持久化副作用,后续应随具体需求再拆,避免过度碎片化。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasStageInteractionModel.test.ts src/components/image-editor/useImageCanvasStageInteractions.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasStageInteractionModel.test.ts src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏标题为 `图层`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。
|
- 2026-06-17 前端拆分第四十二执行批次:继续收口 `useImageCanvasStageInteractions`,新增 `ImageCanvasStageInteractionModel`,把 pointer button / client / id 归一化、画布框选初始状态、抓手平移拖拽状态、多选图层选择与图层拖拽初始状态、生成器 composer 随图层点击显隐、生成占位拖拽状态、小地图拖拽状态和拖拽阈值从 hook 中抽成纯模型;stage hook 保留 DOM 事件拦截、pointer capture / release、React 状态写入、拖拽移动执行和回调副作用。新增模型单测覆盖 pointer 兜底、中键识别、框选 / 平移状态、多选 toggle、组拖拽初始层快照、生成器 composer 规则、生成占位状态和小地图 click / drag 分流;`useImageCanvasStageInteractions.ts` 从 610 行降至 521 行。只读子代理复核结论:当前没有同一轮必须顺手拆的第二个明显大块,`ImageCanvasEditorView.tsx` 已主要是组合层,generation / asset / upload hooks 剩余复杂度多为异步编排或持久化副作用,后续应随具体需求再拆,避免过度碎片化。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasStageInteractionModel.test.ts src/components/image-editor/useImageCanvasStageInteractions.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasStageInteractionModel.test.ts src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏标题为 `图层`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。
|
||||||
- 2026-06-17 前端拆分第四十三执行批次:继续收口 `useImageCanvasGenerationWorkflow`,新增 `useImageCanvasGenerationSubmissionWorkflow`,把普通生图 / 修改图 / 图标素材批量生成 / 快速编辑 / 角色动画的提交 API、提交中 / 失败 / 完成状态回写、生成结果落画布、选中图层、切换图层侧栏、适合视图和 last image model 记忆从入口 workflow 中抽成生成提交流水线 hook;原 workflow 保留生成入口、菜单开关、画布选取引用、表单字段更新、生成器关闭和删除图层后的状态清理,避免把 UI 选择态和提交态混在一起。新增 hook 单测覆盖 quick edit 成功 / 失败、edit 成功清理 modal、生成使用最新移动占位、icon 缺规范校验、icon 成功批量落图并记住模型、角色动画 completed / failed 状态机;同步修复新测试文件的 mock 隔离,避免 `vi.clearAllMocks()` 清空并行集成测试的调用记录;`useImageCanvasGenerationWorkflow.ts` 从 870 行降至 499 行。统一验证命令:`npm run test -- src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx`、`npm run test -- src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasGenerationLayerModel.test.ts src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasStageInteractionModel.test.ts src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasGenerationLayerModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;点击 `生成工具` 后 `Image Generator` 占位、`生成图片` 对话框和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。
|
- 2026-06-17 前端拆分第四十三执行批次:继续收口 `useImageCanvasGenerationWorkflow`,新增 `useImageCanvasGenerationSubmissionWorkflow`,把普通生图 / 修改图 / 图标素材批量生成 / 快速编辑 / 角色动画的提交 API、提交中 / 失败 / 完成状态回写、生成结果落画布、选中图层、切换图层侧栏、适合视图和 last image model 记忆从入口 workflow 中抽成生成提交流水线 hook;原 workflow 保留生成入口、菜单开关、画布选取引用、表单字段更新、生成器关闭和删除图层后的状态清理,避免把 UI 选择态和提交态混在一起。新增 hook 单测覆盖 quick edit 成功 / 失败、edit 成功清理 modal、生成使用最新移动占位、icon 缺规范校验、icon 成功批量落图并记住模型、角色动画 completed / failed 状态机;同步修复新测试文件的 mock 隔离,避免 `vi.clearAllMocks()` 清空并行集成测试的调用记录;`useImageCanvasGenerationWorkflow.ts` 从 870 行降至 499 行。统一验证命令:`npm run test -- src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx`、`npm run test -- src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasGenerationLayerModel.test.ts src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasStageInteractionModel.test.ts src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasGenerationLayerModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;点击 `生成工具` 后 `Image Generator` 占位、`生成图片` 对话框和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。
|
||||||
|
- 2026-06-17 前端拆分第四十四执行批次:继续收口 `useImageCanvasAssetLibrary`,扩展 `ImageCanvasAssetLibraryModel`,把素材文件夹坐标命中、素材拖拽目标文件夹置顶判断、素材框选起点 / 移动 / 反向拖拽矩形归一化、框选只命中 uploaded 素材和框选启动规则从 hook 中抽成纯几何模型;asset library hook 保留 DOM 查询、dataset 读取、pointer capture / release、React 状态写入、后端 CRUD 和登录弹窗副作用,避免把 DOM 与服务调用塞进模型。新增模型单测覆盖文件夹边界命中、列表外返回空、文件夹 header 视野外置顶、反向框选归一化、边缘相交命中、忽略 built-in / unknown 素材;只读子代理复核同意优先抽 Rect/Point + folder hit + pinned + marquee selection,不继续拆新 hook。验证命令:`npm run test -- src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/useImageCanvasAssetLibrary.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasAssetPointerDragBridge.test.tsx src/components/image-editor/useImageCanvasAssetCanvasBridge.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`。
|
||||||
|
- 2026-06-17 前端拆分第四十四执行批次验证补充:统一编辑器回归命令 `npm run test -- src/components/image-editor/ImageCanvasStageInteractionModel.test.ts src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasGenerationLayerModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasAssetPointerDragBridge.test.tsx src/components/image-editor/useImageCanvasAssetCanvasBridge.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx` 通过 33 个文件 / 219 个测试;`npm run typecheck`、`npm run check:encoding`、`git diff --check` 均通过。浏览器回归:`http://127.0.0.1:10007/editor/canvas` 使用 Playwright CLI headless 打开成功,未登录显示 `账号入口`;关闭登录后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;默认素材侧栏显示 `素材` / `项目素材` / `上传到项目素材`,切换 `打开图层` 后侧栏显示 `图层`,点击 `生成工具` 后 `Image Generator` 占位、`生成图片` 对话框和 `AI画布工具栏` 均可见;网络请求仅有预期未登录 `/api/auth/refresh` 401,其余 editor smoke 相关请求正常。
|
||||||
|
|||||||
@@ -6,18 +6,24 @@ import type {
|
|||||||
} from './ImageCanvasEditorTypes';
|
} from './ImageCanvasEditorTypes';
|
||||||
import {
|
import {
|
||||||
areAllSelectableAssetsSelected,
|
areAllSelectableAssetsSelected,
|
||||||
|
createAssetMarqueeFromPointer,
|
||||||
|
createAssetMarqueeSelectionRect,
|
||||||
createLocalAssetFolder,
|
createLocalAssetFolder,
|
||||||
deleteAssetFolderLocally,
|
deleteAssetFolderLocally,
|
||||||
getSelectableAssets,
|
getSelectableAssets,
|
||||||
groupAssetsByFolder,
|
groupAssetsByFolder,
|
||||||
moveAssetToFolderLocally,
|
moveAssetToFolderLocally,
|
||||||
|
moveAssetMarqueeToPointer,
|
||||||
removeAssetById,
|
removeAssetById,
|
||||||
removeSelectedAssets,
|
removeSelectedAssets,
|
||||||
renameAssetById,
|
renameAssetById,
|
||||||
renameAssetFolderById,
|
renameAssetFolderById,
|
||||||
replaceLocalAssetFolder,
|
replaceLocalAssetFolder,
|
||||||
resolveAllAssetSelection,
|
resolveAllAssetSelection,
|
||||||
|
resolveAssetFolderIdFromPoint,
|
||||||
resolveDefaultAssetFolder,
|
resolveDefaultAssetFolder,
|
||||||
|
resolvePinnedAssetMoveFolderId,
|
||||||
|
selectUploadedAssetsInRect,
|
||||||
toggleAssetFolderCollapsed,
|
toggleAssetFolderCollapsed,
|
||||||
toggleAssetSelection,
|
toggleAssetSelection,
|
||||||
} from './ImageCanvasAssetLibraryModel';
|
} from './ImageCanvasAssetLibraryModel';
|
||||||
@@ -184,4 +190,139 @@ describe('ImageCanvasAssetLibraryModel', () => {
|
|||||||
moveAssetToFolderLocally([createAsset()], 'asset-a', 'folder-role')[0],
|
moveAssetToFolderLocally([createAsset()], 'asset-a', 'folder-role')[0],
|
||||||
).toMatchObject({ folderId: 'folder-role' });
|
).toMatchObject({ folderId: 'folder-role' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('resolves folder hit targets from pointer coordinates', () => {
|
||||||
|
const folders = [
|
||||||
|
{
|
||||||
|
folderId: 'project',
|
||||||
|
rect: { left: 10, right: 110, top: 10, bottom: 80 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
folderId: 'folder-role',
|
||||||
|
rect: { left: 10, right: 110, top: 81, bottom: 150 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolveAssetFolderIdFromPoint({
|
||||||
|
point: { clientX: 40, clientY: 80 },
|
||||||
|
listRect: { left: 0, right: 140, top: 0, bottom: 180 },
|
||||||
|
folders,
|
||||||
|
}),
|
||||||
|
).toBe('project');
|
||||||
|
expect(
|
||||||
|
resolveAssetFolderIdFromPoint({
|
||||||
|
point: { clientX: 5, clientY: 40 },
|
||||||
|
listRect: { left: 10, right: 140, top: 0, bottom: 180 },
|
||||||
|
folders,
|
||||||
|
}),
|
||||||
|
).toBeNull();
|
||||||
|
expect(
|
||||||
|
resolveAssetFolderIdFromPoint({
|
||||||
|
point: { clientX: 120, clientY: 40 },
|
||||||
|
listRect: { left: 0, right: 140, top: 0, bottom: 180 },
|
||||||
|
folders,
|
||||||
|
}),
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('pins asset move folder labels only when their header is outside the list viewport', () => {
|
||||||
|
const listRect = { left: 0, right: 120, top: 100, bottom: 240 };
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolvePinnedAssetMoveFolderId({
|
||||||
|
folderId: 'folder-role',
|
||||||
|
listRect,
|
||||||
|
headerRect: { left: 0, right: 120, top: 40, bottom: 80 },
|
||||||
|
}),
|
||||||
|
).toBe('folder-role');
|
||||||
|
expect(
|
||||||
|
resolvePinnedAssetMoveFolderId({
|
||||||
|
folderId: 'folder-role',
|
||||||
|
listRect,
|
||||||
|
headerRect: { left: 0, right: 120, top: 260, bottom: 300 },
|
||||||
|
}),
|
||||||
|
).toBe('folder-role');
|
||||||
|
expect(
|
||||||
|
resolvePinnedAssetMoveFolderId({
|
||||||
|
folderId: 'folder-role',
|
||||||
|
listRect,
|
||||||
|
headerRect: { left: 0, right: 120, top: 120, bottom: 150 },
|
||||||
|
}),
|
||||||
|
).toBeNull();
|
||||||
|
expect(
|
||||||
|
resolvePinnedAssetMoveFolderId({
|
||||||
|
folderId: null,
|
||||||
|
listRect,
|
||||||
|
headerRect: { left: 0, right: 120, top: 40, bottom: 80 },
|
||||||
|
}),
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates marquee geometry from pointer coordinates and normalizes reverse drags', () => {
|
||||||
|
const marquee = createAssetMarqueeFromPointer({
|
||||||
|
pointerId: 5,
|
||||||
|
point: { clientX: 180, clientY: 220 },
|
||||||
|
containerRect: { left: 100, top: 200 },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(marquee).toEqual({
|
||||||
|
pointerId: 5,
|
||||||
|
startX: 80,
|
||||||
|
startY: 20,
|
||||||
|
currentX: 80,
|
||||||
|
currentY: 20,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
moveAssetMarqueeToPointer({
|
||||||
|
marquee,
|
||||||
|
point: { clientX: 120, clientY: 205 },
|
||||||
|
containerRect: { left: 100, top: 200 },
|
||||||
|
}),
|
||||||
|
).toMatchObject({ currentX: 20, currentY: 5 });
|
||||||
|
expect(
|
||||||
|
createAssetMarqueeSelectionRect({
|
||||||
|
marquee,
|
||||||
|
point: { clientX: 120, clientY: 205 },
|
||||||
|
containerRect: { left: 100, top: 200 },
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
left: 120,
|
||||||
|
right: 180,
|
||||||
|
top: 205,
|
||||||
|
bottom: 220,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selects only uploaded assets intersecting the marquee rectangle', () => {
|
||||||
|
const assets = [
|
||||||
|
createAsset({ id: 'asset-a' }),
|
||||||
|
createAsset({ id: 'asset-b' }),
|
||||||
|
createAsset({ id: 'built-in', sourceKind: 'built-in' }),
|
||||||
|
];
|
||||||
|
const selectedIds = selectUploadedAssetsInRect({
|
||||||
|
assets,
|
||||||
|
assetTargets: [
|
||||||
|
{
|
||||||
|
assetId: 'asset-a',
|
||||||
|
rect: { left: 10, right: 20, top: 10, bottom: 20 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
assetId: 'asset-b',
|
||||||
|
rect: { left: 30, right: 40, top: 30, bottom: 40 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
assetId: 'built-in',
|
||||||
|
rect: { left: 15, right: 25, top: 15, bottom: 25 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
assetId: 'missing',
|
||||||
|
rect: { left: 15, right: 25, top: 15, bottom: 25 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selectionRect: { left: 20, right: 35, top: 20, bottom: 35 },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect([...selectedIds]).toEqual(['asset-a', 'asset-b']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,178 @@
|
|||||||
import type {
|
import type {
|
||||||
|
AssetMarqueeState,
|
||||||
EditorAsset,
|
EditorAsset,
|
||||||
EditorAssetFolder,
|
EditorAssetFolder,
|
||||||
} from './ImageCanvasEditorTypes';
|
} from './ImageCanvasEditorTypes';
|
||||||
|
|
||||||
|
type ClientRectLike = Pick<DOMRect, 'bottom' | 'left' | 'right' | 'top'>;
|
||||||
|
|
||||||
|
type ClientPoint = {
|
||||||
|
clientX: number;
|
||||||
|
clientY: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AssetFolderHitTarget = {
|
||||||
|
folderId: string;
|
||||||
|
rect: ClientRectLike;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AssetHitTarget = {
|
||||||
|
assetId: string;
|
||||||
|
rect: ClientRectLike;
|
||||||
|
};
|
||||||
|
|
||||||
export type GroupedAssetFolder = EditorAssetFolder & {
|
export type GroupedAssetFolder = EditorAssetFolder & {
|
||||||
assets: EditorAsset[];
|
assets: EditorAsset[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function isPointInRect(point: ClientPoint, rect: ClientRectLike) {
|
||||||
|
return (
|
||||||
|
point.clientX >= rect.left &&
|
||||||
|
point.clientX <= rect.right &&
|
||||||
|
point.clientY >= rect.top &&
|
||||||
|
point.clientY <= rect.bottom
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function doRectsIntersect(rect: ClientRectLike, selectionRect: ClientRectLike) {
|
||||||
|
return (
|
||||||
|
rect.left <= selectionRect.right &&
|
||||||
|
rect.right >= selectionRect.left &&
|
||||||
|
rect.top <= selectionRect.bottom &&
|
||||||
|
rect.bottom >= selectionRect.top
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveAssetFolderIdFromPoint({
|
||||||
|
point,
|
||||||
|
listRect,
|
||||||
|
folders,
|
||||||
|
}: {
|
||||||
|
point: ClientPoint;
|
||||||
|
listRect: ClientRectLike | null | undefined;
|
||||||
|
folders: AssetFolderHitTarget[];
|
||||||
|
}) {
|
||||||
|
if (!listRect || !isPointInRect(point, listRect)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const matchedFolder = folders.find((folder) =>
|
||||||
|
isPointInRect(point, folder.rect),
|
||||||
|
);
|
||||||
|
return matchedFolder?.folderId ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolvePinnedAssetMoveFolderId({
|
||||||
|
folderId,
|
||||||
|
listRect,
|
||||||
|
headerRect,
|
||||||
|
}: {
|
||||||
|
folderId: string | null;
|
||||||
|
listRect: ClientRectLike | null | undefined;
|
||||||
|
headerRect: ClientRectLike | null | undefined;
|
||||||
|
}) {
|
||||||
|
if (
|
||||||
|
!folderId ||
|
||||||
|
!listRect ||
|
||||||
|
!headerRect ||
|
||||||
|
(headerRect.bottom >= listRect.top && headerRect.top <= listRect.bottom)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return folderId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldStartAssetMarquee({
|
||||||
|
isAssetSelectionMode,
|
||||||
|
button,
|
||||||
|
isBlockedTarget,
|
||||||
|
}: {
|
||||||
|
isAssetSelectionMode: boolean;
|
||||||
|
button: number;
|
||||||
|
isBlockedTarget: boolean;
|
||||||
|
}) {
|
||||||
|
return isAssetSelectionMode && button === 0 && !isBlockedTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAssetMarqueeFromPointer({
|
||||||
|
pointerId,
|
||||||
|
point,
|
||||||
|
containerRect,
|
||||||
|
}: {
|
||||||
|
pointerId: number;
|
||||||
|
point: ClientPoint;
|
||||||
|
containerRect: Pick<DOMRect, 'left' | 'top'> | null | undefined;
|
||||||
|
}): AssetMarqueeState {
|
||||||
|
const startX = point.clientX - (containerRect?.left ?? 0);
|
||||||
|
const startY = point.clientY - (containerRect?.top ?? 0);
|
||||||
|
return {
|
||||||
|
pointerId,
|
||||||
|
startX,
|
||||||
|
startY,
|
||||||
|
currentX: startX,
|
||||||
|
currentY: startY,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function moveAssetMarqueeToPointer({
|
||||||
|
marquee,
|
||||||
|
point,
|
||||||
|
containerRect,
|
||||||
|
}: {
|
||||||
|
marquee: AssetMarqueeState;
|
||||||
|
point: ClientPoint;
|
||||||
|
containerRect: Pick<DOMRect, 'left' | 'top'> | null | undefined;
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
...marquee,
|
||||||
|
currentX: point.clientX - (containerRect?.left ?? 0),
|
||||||
|
currentY: point.clientY - (containerRect?.top ?? 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAssetMarqueeSelectionRect({
|
||||||
|
marquee,
|
||||||
|
point,
|
||||||
|
containerRect,
|
||||||
|
}: {
|
||||||
|
marquee: AssetMarqueeState;
|
||||||
|
point: ClientPoint;
|
||||||
|
containerRect: Pick<DOMRect, 'left' | 'top'> | null | undefined;
|
||||||
|
}): ClientRectLike {
|
||||||
|
const startClientX = (containerRect?.left ?? 0) + marquee.startX;
|
||||||
|
const startClientY = (containerRect?.top ?? 0) + marquee.startY;
|
||||||
|
return {
|
||||||
|
left: Math.min(startClientX, point.clientX),
|
||||||
|
right: Math.max(startClientX, point.clientX),
|
||||||
|
top: Math.min(startClientY, point.clientY),
|
||||||
|
bottom: Math.max(startClientY, point.clientY),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectUploadedAssetsInRect({
|
||||||
|
assets,
|
||||||
|
assetTargets,
|
||||||
|
selectionRect,
|
||||||
|
}: {
|
||||||
|
assets: EditorAsset[];
|
||||||
|
assetTargets: AssetHitTarget[];
|
||||||
|
selectionRect: ClientRectLike;
|
||||||
|
}) {
|
||||||
|
const uploadedAssetIds = new Set(
|
||||||
|
assets
|
||||||
|
.filter((asset) => asset.sourceKind === 'uploaded')
|
||||||
|
.map((asset) => asset.id),
|
||||||
|
);
|
||||||
|
return new Set(
|
||||||
|
assetTargets
|
||||||
|
.filter(
|
||||||
|
(target) =>
|
||||||
|
uploadedAssetIds.has(target.assetId) &&
|
||||||
|
doRectsIntersect(target.rect, selectionRect),
|
||||||
|
)
|
||||||
|
.map((target) => target.assetId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function groupAssetsByFolder(
|
export function groupAssetsByFolder(
|
||||||
assetFolders: EditorAssetFolder[],
|
assetFolders: EditorAssetFolder[],
|
||||||
assets: EditorAsset[],
|
assets: EditorAsset[],
|
||||||
|
|||||||
@@ -25,11 +25,14 @@ import {
|
|||||||
} from './ImageCanvasEditorModel';
|
} from './ImageCanvasEditorModel';
|
||||||
import {
|
import {
|
||||||
areAllSelectableAssetsSelected,
|
areAllSelectableAssetsSelected,
|
||||||
|
createAssetMarqueeFromPointer,
|
||||||
|
createAssetMarqueeSelectionRect,
|
||||||
createLocalAssetFolder,
|
createLocalAssetFolder,
|
||||||
deleteAssetFolderLocally,
|
deleteAssetFolderLocally,
|
||||||
getSelectableAssets,
|
getSelectableAssets,
|
||||||
groupAssetsByFolder,
|
groupAssetsByFolder,
|
||||||
moveAssetToFolderLocally,
|
moveAssetToFolderLocally,
|
||||||
|
moveAssetMarqueeToPointer,
|
||||||
removeAssetById,
|
removeAssetById,
|
||||||
removeSelectedAssets,
|
removeSelectedAssets,
|
||||||
renameAssetById,
|
renameAssetById,
|
||||||
@@ -37,6 +40,10 @@ import {
|
|||||||
replaceLocalAssetFolder,
|
replaceLocalAssetFolder,
|
||||||
resolveAllAssetSelection,
|
resolveAllAssetSelection,
|
||||||
resolveDefaultAssetFolder,
|
resolveDefaultAssetFolder,
|
||||||
|
resolveAssetFolderIdFromPoint,
|
||||||
|
resolvePinnedAssetMoveFolderId,
|
||||||
|
selectUploadedAssetsInRect,
|
||||||
|
shouldStartAssetMarquee,
|
||||||
toggleAssetFolderCollapsed,
|
toggleAssetFolderCollapsed,
|
||||||
toggleAssetSelection,
|
toggleAssetSelection,
|
||||||
} from './ImageCanvasAssetLibraryModel';
|
} from './ImageCanvasAssetLibraryModel';
|
||||||
@@ -55,6 +62,43 @@ function isEditorAuthError(error: unknown) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ASSET_FOLDER_SELECTOR = '[data-asset-folder-id]';
|
||||||
|
const ASSET_ITEM_SELECTOR = '[data-asset-id]';
|
||||||
|
const ASSET_MARQUEE_BLOCKED_TARGET_SELECTOR =
|
||||||
|
'button, input, textarea, select, [data-asset-id]';
|
||||||
|
|
||||||
|
function readAssetFolderHitTargets(listElement: ParentNode | null) {
|
||||||
|
return [
|
||||||
|
...(listElement?.querySelectorAll<HTMLElement>(ASSET_FOLDER_SELECTOR) ?? []),
|
||||||
|
]
|
||||||
|
.map((element) => {
|
||||||
|
const folderId = element.dataset.assetFolderId;
|
||||||
|
return folderId
|
||||||
|
? {
|
||||||
|
folderId,
|
||||||
|
rect: element.getBoundingClientRect(),
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
})
|
||||||
|
.filter((target): target is NonNullable<typeof target> => Boolean(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
function readAssetHitTargets(listElement: ParentNode | null) {
|
||||||
|
return [
|
||||||
|
...(listElement?.querySelectorAll<HTMLElement>(ASSET_ITEM_SELECTOR) ?? []),
|
||||||
|
]
|
||||||
|
.map((element) => {
|
||||||
|
const assetId = element.dataset.assetId;
|
||||||
|
return assetId
|
||||||
|
? {
|
||||||
|
assetId,
|
||||||
|
rect: element.getBoundingClientRect(),
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
})
|
||||||
|
.filter((target): target is NonNullable<typeof target> => Boolean(target));
|
||||||
|
}
|
||||||
|
|
||||||
export function useImageCanvasAssetLibrary({
|
export function useImageCanvasAssetLibrary({
|
||||||
assetListRef,
|
assetListRef,
|
||||||
canAccessProtectedData,
|
canAccessProtectedData,
|
||||||
@@ -145,28 +189,11 @@ export function useImageCanvasAssetLibrary({
|
|||||||
if (!listElement) {
|
if (!listElement) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const listRect = listElement.getBoundingClientRect();
|
return resolveAssetFolderIdFromPoint({
|
||||||
if (
|
point: { clientX, clientY },
|
||||||
clientX < listRect.left ||
|
listRect: listElement.getBoundingClientRect(),
|
||||||
clientX > listRect.right ||
|
folders: readAssetFolderHitTargets(listElement),
|
||||||
clientY < listRect.top ||
|
|
||||||
clientY > listRect.bottom
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const folderElements = [
|
|
||||||
...listElement.querySelectorAll<HTMLElement>('[data-asset-folder-id]'),
|
|
||||||
];
|
|
||||||
const matchedFolder = folderElements.find((element) => {
|
|
||||||
const rect = element.getBoundingClientRect();
|
|
||||||
return (
|
|
||||||
clientX >= rect.left &&
|
|
||||||
clientX <= rect.right &&
|
|
||||||
clientY >= rect.top &&
|
|
||||||
clientY <= rect.bottom
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
return matchedFolder?.dataset.assetFolderId ?? null;
|
|
||||||
},
|
},
|
||||||
[assetListRef],
|
[assetListRef],
|
||||||
);
|
);
|
||||||
@@ -182,14 +209,12 @@ export function useImageCanvasAssetLibrary({
|
|||||||
const header = listElement?.querySelector<HTMLElement>(
|
const header = listElement?.querySelector<HTMLElement>(
|
||||||
`[data-asset-folder-header-id="${escapeCssIdentifier(folderId)}"]`,
|
`[data-asset-folder-header-id="${escapeCssIdentifier(folderId)}"]`,
|
||||||
);
|
);
|
||||||
const listRect = listElement?.getBoundingClientRect();
|
|
||||||
const headerRect = header?.getBoundingClientRect();
|
|
||||||
setPinnedAssetMoveFolderId(
|
setPinnedAssetMoveFolderId(
|
||||||
listRect &&
|
resolvePinnedAssetMoveFolderId({
|
||||||
headerRect &&
|
folderId,
|
||||||
(headerRect.bottom < listRect.top || headerRect.top > listRect.bottom)
|
listRect: listElement?.getBoundingClientRect(),
|
||||||
? folderId
|
headerRect: header?.getBoundingClientRect(),
|
||||||
: null,
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[assetListRef],
|
[assetListRef],
|
||||||
@@ -418,56 +443,40 @@ export function useImageCanvasAssetLibrary({
|
|||||||
top: number;
|
top: number;
|
||||||
bottom: number;
|
bottom: number;
|
||||||
}) => {
|
}) => {
|
||||||
const nextSelectedIds = new Set<string>();
|
setSelectedAssetIds(
|
||||||
assetListRef.current
|
selectUploadedAssetsInRect({
|
||||||
?.querySelectorAll<HTMLElement>('[data-asset-id]')
|
assets,
|
||||||
.forEach((element) => {
|
assetTargets: readAssetHitTargets(assetListRef.current),
|
||||||
const assetId = element.dataset.assetId;
|
selectionRect,
|
||||||
if (!assetId) {
|
}),
|
||||||
return;
|
|
||||||
}
|
|
||||||
const asset = assets.find(
|
|
||||||
(currentAsset) => currentAsset.id === assetId,
|
|
||||||
);
|
);
|
||||||
if (!asset || asset.sourceKind !== 'uploaded') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const rect = element.getBoundingClientRect();
|
|
||||||
const intersects =
|
|
||||||
rect.left <= selectionRect.right &&
|
|
||||||
rect.right >= selectionRect.left &&
|
|
||||||
rect.top <= selectionRect.bottom &&
|
|
||||||
rect.bottom >= selectionRect.top;
|
|
||||||
if (intersects) {
|
|
||||||
nextSelectedIds.add(assetId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setSelectedAssetIds(nextSelectedIds);
|
|
||||||
},
|
},
|
||||||
[assetListRef, assets],
|
[assetListRef, assets],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleAssetMarqueePointerDown = useCallback(
|
const handleAssetMarqueePointerDown = useCallback(
|
||||||
(event: ReactPointerEvent<HTMLDivElement>) => {
|
(event: ReactPointerEvent<HTMLDivElement>) => {
|
||||||
if (!isAssetSelectionMode || event.button !== 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
if (target.closest('button, input, textarea, select, [data-asset-id]')) {
|
if (
|
||||||
|
!shouldStartAssetMarquee({
|
||||||
|
isAssetSelectionMode,
|
||||||
|
button: event.button,
|
||||||
|
isBlockedTarget: Boolean(
|
||||||
|
target.closest(ASSET_MARQUEE_BLOCKED_TARGET_SELECTOR),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
assetListRef.current?.setPointerCapture?.(event.pointerId);
|
assetListRef.current?.setPointerCapture?.(event.pointerId);
|
||||||
const rect = assetListRef.current?.getBoundingClientRect();
|
setAssetMarquee(
|
||||||
const startX = event.clientX - (rect?.left ?? 0);
|
createAssetMarqueeFromPointer({
|
||||||
const startY = event.clientY - (rect?.top ?? 0);
|
|
||||||
setAssetMarquee({
|
|
||||||
pointerId: event.pointerId,
|
pointerId: event.pointerId,
|
||||||
startX,
|
point: event,
|
||||||
startY,
|
containerRect: assetListRef.current?.getBoundingClientRect(),
|
||||||
currentX: startX,
|
}),
|
||||||
currentY: startY,
|
);
|
||||||
});
|
|
||||||
setSelectedAssetIds(new Set());
|
setSelectedAssetIds(new Set());
|
||||||
},
|
},
|
||||||
[assetListRef, isAssetSelectionMode],
|
[assetListRef, isAssetSelectionMode],
|
||||||
@@ -480,25 +489,22 @@ export function useImageCanvasAssetLibrary({
|
|||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const containerRect = assetListRef.current?.getBoundingClientRect();
|
const containerRect = assetListRef.current?.getBoundingClientRect();
|
||||||
const currentX = event.clientX - (containerRect?.left ?? 0);
|
|
||||||
const currentY = event.clientY - (containerRect?.top ?? 0);
|
|
||||||
const startClientX = (containerRect?.left ?? 0) + assetMarquee.startX;
|
|
||||||
const startClientY = (containerRect?.top ?? 0) + assetMarquee.startY;
|
|
||||||
setAssetMarquee((currentMarquee) =>
|
setAssetMarquee((currentMarquee) =>
|
||||||
currentMarquee
|
currentMarquee
|
||||||
? {
|
? moveAssetMarqueeToPointer({
|
||||||
...currentMarquee,
|
marquee: currentMarquee,
|
||||||
currentX,
|
point: event,
|
||||||
currentY,
|
containerRect,
|
||||||
}
|
})
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
updateAssetSelectionFromMarquee({
|
updateAssetSelectionFromMarquee(
|
||||||
left: Math.min(startClientX, event.clientX),
|
createAssetMarqueeSelectionRect({
|
||||||
right: Math.max(startClientX, event.clientX),
|
marquee: assetMarquee,
|
||||||
top: Math.min(startClientY, event.clientY),
|
point: event,
|
||||||
bottom: Math.max(startClientY, event.clientY),
|
containerRect,
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[assetListRef, assetMarquee, updateAssetSelectionFromMarquee],
|
[assetListRef, assetMarquee, updateAssetSelectionFromMarquee],
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user