Add comprehensive tests for image processing and mask functionalities

- Introduced tests for color image handling, ensuring source preservation and premultiplied alpha usage.
- Implemented tests for brush stroke operations, validating segment boundaries and brush anti-aliasing.
- Added tests for frame anti-aliasing, confirming correct rendering behavior with various alpha values and depth settings.
- Created tests for frame projection, ensuring accurate overlap calculations and half-plane clipping.
- Developed tests for image premultiplied retention, verifying behavior during loading and packing processes.
- Added undo functionality tests for mask operations, ensuring single transaction behavior during edits.
This commit is contained in:
2026-09-05 21:33:11 +08:00
parent bbaa4bff46
commit 1994d38ccd
111 changed files with 7691 additions and 2341 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-09-03

View File

@@ -0,0 +1,128 @@
## Context
Crop Box 当前复用 `CropSelectionGesture`:屏幕矩形被投影到源 Image Empty 平面、栅格化为 Selection Mask、乘入源 Alpha,最后按可见 Alpha 紧裁并通过 `placement_bounds` 放回源平面。这个数据流天然不能表达扩大 Canvas,也不会把倾斜 Image Empty 在当前视角中的梯形外观冻结为新的矩形图片。
现有 Crop Perspective 已具备 Homography、预乘 Alpha 插值、输出分辨率限制和 Image 编辑结果创建能力,但它由用户指定源图片平面上的 Quad,再将 Quad 矫正为矩形。Frame 的输入恰好相反:用户直接指定 Viewport 中的目标矩形,系统需要把所有选中 Image Empty 在该视图中的投射采样、按深度合成进矩形,并把 active 对象转到视平面。
## Goals / Non-Goals
**Goals:**
- 用 Frame 完整替换 Crop Box,并让名称、公开 ID、文件边界、注册与文档一致。
- 允许用户从 Viewport 任意位置画出大于或小于源投射的结果 Canvas。
- 只重采样选中 Image 的 RGBA,按逐像素空间深度与 Alpha 合成并冻结当前视角下的几何透视外观。
- 以当前 Viewport 像素尺寸决定输出分辨率,以 active Image Empty 统一决定结果对象和视图深度。
- 让结果继续使用 active Blender Object,并在成功后删除其他参与合并的 Image Empty。
- 删除 Frame 不再需要的 Refine Selection、Invert 和 Keep Original 状态。
- 复用 Crop Perspective 已验证的投影数学、插值和资源上限。
**Non-Goals:**
- 不把 Frame 实现为 Viewport 截图;场景遮挡、Grid、Gizmo、Overlay、Color Management 和 Image Empty 的显示 tint 不烘焙进图片。
- 不改变 Crop Lasso、Crop Polyline 或 Crop Perspective 的图片处理结果和独立选项。
- 不保留 Crop Box Tool ID、Operator、Scene Property 或 Python 导出兼容层。
- 不支持视频、序列图片或执行后随视角继续变化的动态投射。
- 不改变 Server Job、依赖、节点组或 Blender 资产。
## Decisions
### 1. Frame 使用 Selection 作为输入集合,active Image Empty 作为唯一基准
Frame 启动时要求 active object 是 Selection 内的 still Image Empty,并收集所有选中的 still Image Empty 作为参与源。仅保留 active 状态但未被选中的 Image Empty 不构成输入;active 不是 Image Empty 时,即使 Selection 含有图片也以 warning 取消。选中的非 Image Empty 不参与操作;任一选中 Image Empty 是视频或序列时整体取消,避免只合并 Selection 的一部分。active 必须属于参与集合,并同时作为结果对象、Image 命名/colorspace 和视图深度基准。
矩形可以从 Region 内任意位置开始,使大 Canvas 的任意角都能落在所有源投射之外。Frame 完成时必须与 active 的屏幕投射相交;相比仅要求与任意参与对象相交,这能保证承担结果的 active 确实属于结果内容。
### 2. 在手势完成时冻结一次投影状态
Frame 完成矩形时记录 Region 尺寸、`RegionView3D` 投影/视图矩阵、全部参与对象的世界矩阵与 Image 尺寸,并用这一组状态完成采样、合成与结果放置。矩形四角使用 Region 像素坐标,顺序固定为左上、右上、右下、左下。
不保存一个可持续更新的投影关系。Frame 是一次不可逆的像素编辑;冻结状态也避免异步或重绘期间的 Viewport 变化造成图片内容和对象朝向来自不同视图。
### 3. 每个参与对象独立投影采样,再逐像素深度合成
对每个参与 Image Empty,先把四个本地图片角变换到世界空间,再投影到 Region,得到各自的 `source image pixels → screen` 投射 Quad。Frame 像素中心按矩形线性映射到 screen,再通过每个投射 Homography 的逆变换映射到对应源图片像素;落在该源图片范围外的样本视为透明。
同一输出像素取得全部有效样本后,根据观察射线到各源平面交点的距离从远到近排序,并以预乘 Alpha source-over 合成。这样两个倾斜平面发生交叉时,前后关系也可以随像素变化。深度相同时 active 样本最后合成,使 active 明确位于其他输入之上;其他完全同深度样本只需使用稳定对象顺序保证结果可复现。
等价的数据流为:
```mermaid
flowchart TD
Sources["选中 Image RGBA"]
Objects["各 Image Empty 世界四角"]
View["冻结的 Viewport 投影"]
Frame["屏幕 Frame 矩形"]
Quads["各图片的屏幕投射 Quad"]
Samples["Frame 像素中心 → 各 Source Pixel + Depth"]
Composite["远到近预乘 Alpha 合成"]
Result["完整结果 RGBA Canvas"]
Sources --> Samples
Objects --> View --> Quads --> Samples
Frame --> Samples --> Composite --> Result
```
插值继续使用 Crop Perspective 的预乘 Alpha 双线性路径,避免透明边缘产生 RGB 色边。Homography 求解、分块采样和透明 RGB 扩展位于公共 Image 像素处理入口,由 Frame 与 Crop Perspective 共用;多源深度计算和合成留在 Frame 业务边界,不让 Perspective 承担 Selection 或对象生命周期语义。
Frame 与 active 投射 Quad 没有几何相交时以 warning 取消,防止在 active 替换语义下意外得到无基准内容的图片。非 active 投射可以位于 Frame 外,仍属于成功后被消费的参与输入。结果不调用 `crop_pixels_to_alpha()`,因此即使边缘或全部源像素本身透明,也不会改变用户所画 Canvas。
### 4. 输出分辨率使用当前 Viewport 像素
直接使用 Frame 在 Region 中的屏幕像素宽高作为输出尺寸,使结果等价于只渲染图片内容的 Viewport 局部 snapshot。结果保持 Frame 屏幕宽高比,宽高不超过当前 Region 对应维度,并按 Preferences 的 `max_frame_resolution` 再等比限制最长边。默认最大长边为 2048 px,公开范围为 64–8192 px。
这个尺寸不再受 active 图片分辨率、对象距离或透视缩放影响,因此远处对象和大范围 Frame 不会产生超出当前显示所需的巨大图片。整数取整造成的极小宽高比误差由结果 Image Empty 的实际像素宽高比统一承载。
### 4.1 透明边缘保留颜色扩展
投影采样和多图合成继续使用预乘 Alpha。转换回 Blender 使用的 straight RGBA 时,完全透明像素保持 Alpha 0,并让紧邻可见像素的一圈透明 RGB 使用相邻可见颜色。这样不改变透明 Canvas 或合成结果,但避免 Image Empty 的线性纹理过滤把外部 RGB 0 混入边缘而形成黑边。
### 5. 结果平面位于 active 对象原点的视图深度
使用冻结视图的右、上方向作为结果对象本地 `+X`、`+Y`,本地 `+Z` 指向观察者。以 active Image Empty 世界原点确定视图深度,在穿过该点且平行于视平面的平面上反投影 Frame 四角;其中心成为结果对象原点,宽高成为 Image Empty 的世界显示尺寸。
结果对象设置居中的 `empty_image_offset`,世界矩阵只表达 Frame 中心、视图朝向及 active 对象其余不参与图片平面尺寸的变换约定,显示大小由反投影矩形和结果像素宽高比共同确定。active Blender Object identity、名称、Collection、自定义属性和选择状态保留,Image 数据块通过公共 Image 编辑替换规则落地。
选择 active 原点深度而不是任一源平面与 Frame 中心射线的交点有两个理由:即使 Frame 中心落在 active 投射外也始终可定义;多源输入只有 active 提供明确且稳定的共同深度。代价是换视角后结果平面与各原倾斜平面不再重合,这是冻结投射的必然结果。
### 6. Frame 使用独立矩形 modal,不再进入 Selection 管线
删除 `box.py` 和 `CropImageSelection` 的 `BOX` 设置映射,新增 `frame.py` 承载 Frame WorkspaceTool、矩形 Overlay、投影采样和同步执行。公共矩形屏幕路径 helper 可以继续复用,但 Frame 不生成 `SelectionPath`、`SelectionMask`、Refine Job 或 `placement_bounds`。
`CropSelectionGesture` 继续服务 Crop Lasso、Crop Polyline 和 Cutout Lasso;删除只被旧 Crop Box 使用的 BOX gesture 分支,矩形路径 helper 改由 Frame 直接使用。Frame 在完成全部验证、采样、合成和结果 Image 创建之前不修改任何参与对象。
Frame、Crop Lasso 和 Cutout Lasso 的 WorkspaceTool keymap 使用 `CLICK_DRAG`,并显式绑定普通点击与 Shift 点击的 Viewport 对象选择。图片工具不启用 Blender fallback selection keymap,因此 Tool Settings 不出现容易与 Frame、Lasso 混淆的 `Drag: Select Box/Lasso` 设置。Frame 只有确认发生拖动后才收集 Selection 并进入矩形 modal。Polyline 与 Perspective 仍需单击录入点,其 Operator 自行执行对象点选;Shift 点击使用相同的显式追加选择绑定。
公共图片编辑源校验同时要求 active object 是 Image Empty 且属于当前 Selection。drag-only 工具在校验失败时直接 warning 并取消;Frame 的 poll 只限制 View3D 上下文,使无效 active 的拖动仍能进入 Operator 并得到明确提示,而不会静默失败。
### 7. 成功后由 active 承载结果并消费其他参与对象
结果 Image 完整创建并 Pack 后,先更新 active 对象的 Image、世界矩阵、offset 和显示尺寸,再删除其他参与合并的选中 Image Empty。选中的非 Image Empty 从未进入参与集合,因此不修改也不删除。其他对象仍引用某个源 Image 数据块时只删除参与 Object,不强制删除共享 Image;无用户且无 fake user 的输入 Image 按现有生命周期规则清理。
全部对象变更属于同一个 Blender Undo 操作。预计算或 Image 创建失败时直接清理临时资源;对象更新阶段异常时恢复 active 的 Image 与变换,并保留尚未删除的参与对象,避免部分合并状态。
### 8. 删除 Crop Box 状态并收紧 Crop Tool 入口
从 `AnyImageSettings`、设置同步/绘制映射和测试中删除 `crop_box_refine_selection`、`crop_box_invert`、`crop_box_keep_original`。Frame `draw_settings` 不绘制任何业务选项,keymap 只保留鼠标矩形操作,不注册字母快捷键。
注册顺序改为 Frame、Crop Lasso、Crop Polyline、Crop Perspective,`ActivateCropTool` 默认激活 `anyimage.frame`。按项目规则同步删除旧类、ID、文档描述和测试期望,不提供转发或迁移属性。
## Risks / Trade-offs
- [源平面接近与视线平行时 Homography 数值不稳定] → 在修改对象前验证投影点、凸性、矩阵条件数和有效输出尺寸;失败时给出用户可理解的提示并取消。
- [Frame 远大于 active 投射时透明区域较多] → 仍按 Viewport 像素、总像素上限和分块采样处理,范围外直接写透明 Alpha。
- [高 DPI 或超大 Viewport 仍可能产生较大图片] → 输出不超过 Region 像素尺寸,并由 Preferences 最大长边二次限制。
- [大量高分辨率参与图片增加采样时间和内存] → 按输出行分块逐源采样并直接累积预乘结果,不同时保留每个源的完整输出 Canvas。
- [完全共面的非 active 图片没有自然前后关系] → active 固定置顶,其余使用稳定对象顺序作为相同深度 tie-breaker;需要特定层级时由用户把目标设为 active 或拉开空间深度。
- [冻结后的结果换视角会与原倾斜平面明显不同] → 这是 Frame 的核心产品语义;文档明确它烘焙当前视角并生成 view-facing Image Empty。
- [Viewport 显示与原始 Image RGBA 受颜色管理影响而略有差异] → Frame 只处理图片数据和几何投射,不做屏幕截图;沿用源 Image colorspace 并排除显示 tint、遮挡和 Overlay。
- [删除 Crop Box Properties 后旧 `.blend` 仍包含未知保存数据] → Blender 忽略未注册的旧 ID Property;不读取、不迁移,也不影响其他 Crop Property。
- [替换过程中部分更新导致源对象损坏] → 先完整计算并创建、Pack 结果 Image,再一次性更新源对象;异常时恢复 Image 与矩阵并移除无用户的临时 Image。
## Migration Plan
实施时先提取 Frame 与 Crop Perspective 共用的 Homography/采样函数并保持 Perspective 测试通过,再加入 Frame 的多源投影、深度合成和事务式对象消费;随后切换注册、激活入口和属性,最后删除 Crop Box 文件、映射、测试与文档。代码与测试在同一版本发布,不需要数据迁移或节点资产重建。
回滚需要恢复 Crop Box 的工具、三个 Scene Property 和 Selection 路径。已经由 Frame 生成的普通 packed Image 与 Image Empty 不依赖运行时代码,回滚后仍可正常显示,但不会自动恢复操作前的倾斜平面或源图片。
## Open Questions
无。当前产品决策已经确定 Frame 合成所有选中的 still Image Empty,以 Viewport 决定分辨率,以 active 决定结果对象和视图深度,成功后删除其他参与对象。

View File

@@ -0,0 +1,32 @@
## Why
现有 Crop Box 把屏幕矩形当作 Selection,只能遮罩并紧裁图片内容,无法直接重定义 Image Empty 的完整矩形范围,也无法冻结当前视角下已经形成的透视外观。将它改为 Frame Tool,让用户以当前 Viewport 为基准重新取景,并能在同一次操作中扩大或缩小透明画布。
## What Changes
- **BREAKING**:以 Frame Tool 替换 Crop Box,统一改用 Frame 名称、Tool ID、Operator、源码边界、激活入口和文档,不保留 Crop Box 兼容入口。
- Frame 在当前 Viewport 中接受一个矩形,将该矩形定义为结果图片的完整 Canvas;矩形可小于或大于源 Image Empty 的当前投射范围。
- Frame 将所有选中的 still Image Empty 在当前视角中的投射按空间深度与 Alpha 合成为新 RGBA 图片;active Image Empty 作为结果对象和视图深度基准。
- Frame 输出尺寸以所画矩形在当前 Viewport 中的像素尺寸为依据,不超过当前 Region 分辨率,并受 Preferences 中可配置的最大长边限制。
- 结果 Image Empty 平行于当前视平面并覆盖所画矩形;所有源投射范围外保持透明。
- 结果透明边缘保留 Alpha 0,并用相邻可见颜色填充透明像素的 RGB,避免纹理插值产生黑边。
- Frame 不按可见 Alpha 紧裁结果,透明边缘属于用户定义的 Canvas。
- **BREAKING**:Frame 始终让 active Image Empty 承载合并结果,并删除其他参与合并的选中 Image Empty;不提供 Refine Selection、Invert 或 Keep Original,并删除 Crop Box 对应的三个持久 Scene Property。
- Crop Tool 组改为 Frame、Crop Lasso、Crop Polyline、Crop Perspective;其余三个 Crop 子工具的行为和独立选项保持不变。
- Frame、Crop Lasso 与 Cutout Lasso 只在拖动时进入手势;普通点击继续执行 Viewport 对象选择,但工具设置不显示 Blender fallback selection 的 Drag 选项。其他点选型 Crop 工具在首次点到另一对象时只切换选择,不开始编辑。
- 图片编辑手势只在 active object 是 Selection 内的 still Image Empty 时启动;active 未选中或不是 Image Empty 时取消并提示用户。
## Capabilities
### New Capabilities
- `frame-tool`: 规定 Frame 的工具入口、多选 Image Empty 投影合成、Viewport 分辨率基准、矩形 Canvas、透明扩展区域和结果对象放置语义。
### Modified Capabilities
## Impact
- 影响 Crop Tool 的注册、菜单激活入口、Scene Properties、Viewport 矩形交互、多源 RGBA 重采样与合成、Image Empty 删除和结果放置及相关测试。
- 主要涉及 `src/anyimage/operators/crop_tool/`、`src/anyimage/common/viewport.py`、`src/anyimage/common/image.py`、`src/anyimage/properties.py`、注册入口和 Crop Tool 内部文档。
- 不引入新的运行时依赖,不改变 Lasso、Polyline、Perspective、Server Job 或节点资产。

View File

@@ -0,0 +1,172 @@
## ADDED Requirements
### Requirement: Frame replaces Crop Box
系统 SHALL 以 **Frame** 替换 **Crop Box**,并 SHALL 在 Crop Tool 组中按 Frame、Crop Lasso、Crop Polyline、Crop Perspective 的顺序注册四个子工具。Frame 的显示名称、Tool ID、Operator、激活入口、源码边界和文档 SHALL 统一使用 Frame 术语,不得保留 Crop Box 的公开入口或兼容名称。
#### Scenario: User opens the Crop Tool group
- **WHEN** 用户展开 Crop Tool 组
- **THEN** 首个子工具显示为 Frame
- **AND** 其后依次为 Crop Lasso、Crop Polyline 和 Crop Perspective
- **AND** 工具组中不再显示 Crop Box
#### Scenario: User activates the crop entry
- **WHEN** 用户从 AnyImage 菜单激活 Crop Tool
- **THEN** 系统激活 Frame
### Requirement: Frame defines the complete output canvas
Frame SHALL 将用户在当前 3D Viewport 中拖出的矩形定义为结果图片的完整矩形 Canvas。该矩形可以小于或大于选中 Image Empty 的当前屏幕投射范围;Canvas 位于所有参与源投射范围外的像素 SHALL 使用透明 RGBA,系统 SHALL NOT 根据可见 Alpha 再次紧裁结果。
#### Scenario: User draws a smaller frame
- **WHEN** 用户围绕选中 Image Empty 当前投射的一部分拖出 Frame
- **THEN** 结果图片只包含 Frame 内采样到的投射图案
- **AND** 结果 Canvas 严格对应用户所画 Frame
#### Scenario: User draws a larger frame
- **WHEN** 用户拖出的 Frame 超出所有参与 Image Empty 的当前投射范围
- **THEN** 结果图片保留 Frame 的完整范围
- **AND** 未被源投射覆盖的区域为透明
- **AND** 系统不移除这些透明边缘
### Requirement: Frame bakes and composites selected viewport projections
Frame SHALL 使用完成矩形手势时的 Viewport 投影、视图朝向、区域尺寸和每个参与 Image Empty 的世界变换,把所有参与图片的当前投射重采样为结果 RGBA。每个输出像素 SHALL 按观察射线上的几何深度将有效样本从远到近排序,再使用预乘 Alpha source-over 合成;active Image Empty SHALL 在相同深度的样本中位于其他参与对象之上。采样 SHALL 使用预乘 Alpha 插值,源图片范围外 SHALL 采样为透明。
#### Scenario: One selected image is oblique to the view
- **WHEN** 倾斜的选中 Image Empty 在当前透视视图中呈现为梯形投射,用户完成 Frame
- **THEN** 结果图片烘焙该梯形投射在 Frame 内的当前可见外观
- **AND** 操作完成瞬间的屏幕图案与操作前一致
#### Scenario: Selected images overlap in depth
- **WHEN** 多个参与 Image Empty 的投射在 Frame 内重叠
- **THEN** 系统逐像素按空间深度从远到近合成 RGBA
- **AND** 较近图片的透明区域仍显示后方图片
#### Scenario: Active image overlaps another image at the same depth
- **WHEN** active Image Empty 与另一参与对象在同一深度产生重叠样本
- **THEN** active Image Empty 的样本合成在另一对象之上
#### Scenario: Frame extends beyond the projected source
- **WHEN** Frame 同时覆盖参与源投射内部和外部区域
- **THEN** 系统重采样并合成内部的源 RGBA
- **AND** 系统为外部区域写入透明 RGBA
#### Scenario: Frame has no active projection overlap
- **WHEN** 完成的 Frame 与 active Image Empty 的屏幕投射没有几何重叠
- **THEN** 系统以 warning 提醒用户
- **AND** 系统取消操作并保留所有选中对象不变
### Requirement: Viewport defines output resolution
Frame SHALL 使用所画矩形在当前 Region 中的屏幕像素宽高作为结果尺寸依据,并 SHALL 保持 Frame 的屏幕宽高比。结果宽高 SHALL NOT 超过当前 Region 对应维度,最长边 SHALL NOT 超过 Preferences 中可配置的 Frame 最大长边。active 或其他参与图片的原始分辨率、对象距离和透视缩放 SHALL NOT 直接放大结果尺寸。
#### Scenario: Other selected image has a higher resolution
- **WHEN** 非 active 参与图片的分辨率高于 active 图片
- **THEN** 结果尺寸仍只由当前 Viewport 中的 Frame 像素尺寸决定
- **AND** 非 active 图片重采样到该结果 Canvas
#### Scenario: Frame is drawn around a distant image
- **WHEN** active Image Empty 距离视角很远或投射尺度很小
- **THEN** 结果尺寸仍对应 Frame 的当前屏幕像素尺寸
- **AND** 对象距离不会产生超出 Viewport 分辨率的图片
#### Scenario: Frame exceeds the configured maximum
- **WHEN** Frame 屏幕像素最长边超过 Preferences 配置上限
- **THEN** 系统等比限制结果尺寸
- **AND** 结果宽高比继续对应 Frame
### Requirement: Transparent output edges avoid black interpolation
Frame SHALL 保持未覆盖区域的 Alpha 为 0,并 SHALL 使用相邻可见颜色扩展紧邻可见内容的透明像素 RGB。颜色扩展 SHALL NOT 改变任何像素的 Alpha 或 Frame Canvas 范围。
#### Scenario: Projected image ends inside the frame
- **WHEN** 不透明或半透明图片边缘外侧是 Frame 的透明区域
- **THEN** 外侧像素 Alpha 保持为 0
- **AND** 线性纹理插值不会因外侧 RGB 0 形成明显黑边
### Requirement: Frame creates a view-facing Image Empty
Frame SHALL 使用 active Image Empty 的原 Blender Object 承载结果 Image,并更新其世界变换和 Image Empty 显示范围,使本地 XY 图片平面平行于操作完成时的 Viewport 视平面、本地正面朝向该视角,且结果矩形在该视角中覆盖用户所画 Frame。结果平面 SHALL 位于穿过 active Image Empty 世界原点、并与视平面平行的深度平面上。
#### Scenario: User finishes a frame in perspective view
- **WHEN** 用户在透视 Viewport 中完成 Frame
- **THEN** 结果 Image Empty 朝向操作时的视角
- **AND** 结果四边在该视角中对应 Frame 的四边
- **AND** 结果使用 active Image Empty 世界原点所在的视图深度
#### Scenario: User finishes a frame in orthographic view
- **WHEN** 用户在正交 Viewport 中完成 Frame
- **THEN** 结果 Image Empty 平行于该正交视平面并覆盖 Frame
- **AND** 烘焙后的图案保持操作时的屏幕外观
### Requirement: Frame operates on selected still Image Empties
Frame SHALL 以当前 Selection 内的 active still Image Empty 为基准对象,并将所有选中的 still Image Empty 作为参与源;选中的非 Image Empty 对象 SHALL 被忽略。Frame SHALL 允许矩形手势从 Viewport 任意位置开始。active object 未被选中、不是 still Image Empty、任一选中 Image Empty 不是 still image、Frame 尺寸无效或任一参与投影无法稳定求解时,系统 SHALL 取消整个操作并保持所有对象不变。
#### Scenario: User starts outside the source projection
- **WHEN** active object 是 still Image Empty,且用户从源投射范围外开始拖出一个与源投射相交的 Frame
- **THEN** 系统使用 active Image Empty 及其他选中的 still Image Empty 完成 Frame 操作
#### Scenario: Active object is unsupported
- **WHEN** active object 不是 still Image Empty
- **THEN** Frame 不开始图片重采样、合成或对象替换
- **AND** 系统以 warning 提醒用户选择一个 active Image Empty
#### Scenario: Active image is not selected
- **WHEN** active object 是 still Image Empty,但它不属于当前 Selection
- **THEN** Frame 不进入矩形操作
- **AND** 系统以 warning 提醒用户选择 active Image Empty
#### Scenario: Selection contains images but active object is not an image
- **WHEN** Selection 含有 still Image Empty,但 active object 是非图片对象
- **THEN** Frame 不使用 Selection 中的其他图片作为隐式基准
- **AND** 系统以 warning 取消操作
#### Scenario: Selection contains a non-image object
- **WHEN** active object 是 still Image Empty,且 Selection 还包含非 Image Empty 对象
- **THEN** Frame 忽略非 Image Empty 对象
- **AND** 非 Image Empty 对象不参与合成或删除
#### Scenario: A selected Image Empty is animated
- **WHEN** 任一选中的 Image Empty 使用视频或图片序列
- **THEN** 系统取消整个 Frame 操作
- **AND** 所有选中对象保持不变
### Requirement: Frame has no edit options
Frame SHALL 始终让 active Image Empty 承载结果,并在结果完整创建和放置后删除其他参与合并的选中 Image Empty。整个替换与删除 SHALL 属于同一个 Undo 操作;失败时 SHALL 保留全部输入对象。Frame SHALL NOT 提供 Refine Selection、Invert 或 Keep Original。系统 SHALL 删除 Crop Box 对应的 `crop_box_refine_selection`、`crop_box_invert` 和 `crop_box_keep_original` Scene Properties;Crop Lasso、Crop Polyline 和 Crop Perspective 的独立选项 SHALL 保持不变。
#### Scenario: User opens Frame settings
- **WHEN** 用户激活 Frame
- **THEN** 工具设置不显示 Refine Selection、Invert 或 Keep Original
#### Scenario: User completes a frame
- **WHEN** Frame 成功生成结果
- **THEN** active Blender Object 改为承载合成结果 Image 和新的视平面对齐变换
- **AND** 其他参与合并的 Image Empty 被删除
- **AND** 被忽略的非 Image Empty 对象保持不变
#### Scenario: Multi-image merge is undone
- **WHEN** 用户撤销一次成功的多图 Frame
- **THEN** active Image Empty 恢复操作前的 Image 与变换
- **AND** 其他被删除的参与 Image Empty 恢复
#### Scenario: Existing file contains removed Crop Box properties
- **WHEN** 扩展加载曾保存 Crop Box 选项的 `.blend` 文件
- **THEN** 运行时不注册或读取这些已删除属性
- **AND** 其他 Crop 子工具的设置仍按各自属性工作
### Requirement: Image tools preserve ordinary click selection
Frame、Crop Lasso 与 Cutout Lasso SHALL 只在鼠标发生拖动后启动图片手势,普通点击 SHALL 使用 Viewport 对象选择行为。Polyline 与 Perspective 首次点击另一对象时 SHALL 只切换 active selection,不开始图片编辑。图片工具 SHALL NOT 启用或显示 Blender fallback selection 的 Drag 设置。
所有图片编辑手势 SHALL 只在 active object 是当前 Selection 内的 Image Empty 时启动;否则系统 SHALL 以 warning 取消该次手势。
#### Scenario: User clicks another Image Empty while Frame is active
- **WHEN** 已有一个 active Image Empty,用户普通点击另一 Image Empty
- **THEN** Blender 按原生规则选择该对象并使其 active
- **AND** Frame 不开始、不显示矩形 Overlay,也不生成图片
#### Scenario: User shift-clicks another object
- **WHEN** 任一图片工具处于 active,用户 Shift 单击另一对象
- **THEN** Blender 保留原生追加或切换选择行为
#### Scenario: User drags while Frame is active
- **WHEN** 用户按住左键并移动到 Blender 的 drag threshold 之外
- **THEN** Frame 才收集当前 Selection 并进入矩形操作
#### Scenario: User opens image tool settings
- **WHEN** 用户激活 Frame、Crop Lasso、Crop Polyline、Crop Perspective 或 Cutout Lasso
- **THEN** Tool Settings 不显示 `Drag: Select Box/Lasso` 等 fallback selection 设置

View File

@@ -0,0 +1,38 @@
## 1. 共用投影采样
- [x] 1.1 将 Homography 求解、预乘 Alpha 双线性重采样和输出资源限制整理为 Frame 与 Crop Perspective 可复用的公共 Image 像素函数,删除具体工具之间的反向依赖,并用数值、透明边缘、范围外采样、退化矩阵和上限测试验证
- [x] 1.2 让 Crop Perspective 改用公共投影采样函数,移除原模块中的重复实现,并运行既有 Perspective 输出、预览、Alpha、placement 和异常测试确认行为不变
## 2. Frame 像素与空间计算
- [x] 2.1 实现每个选中 Image Empty 的世界四角到 Region 投影、Frame 与 active 投射 Quad 相交验证,以及 Frame 像素中心到各源图片像素的映射;用透视、正交、范围内、部分越界、无 active 相交和近退化视角测试验证
- [x] 2.2 实现基于 Frame 屏幕像素、Region 尺寸与 Preferences 最大长边的输出尺寸计算,保留完整透明 Canvas 且不执行 Alpha 紧裁;用其他源分辨率变化、较小 Frame、较大 Frame、透明源边缘、整数宽高比和输出上限测试验证
- [x] 2.3 实现多源分块采样、逐像素几何深度排序和预乘 Alpha source-over 合成,active 在相同深度置顶;用前后遮挡、透明叠加、倾斜平面交叉、共面 active 和 Frame 外样本测试验证
- [x] 2.4 实现穿过 active 对象世界原点的视平面反投影与结果 Image Empty 矩阵/显示范围计算,确保本地 XY 平行视平面、正面朝向观察者、屏幕四边对应 Frame;用透视、正交、平移 active 对象和非均匀源变换测试验证
## 3. Frame Tool
- [x] 3.1 新增 `crop_tool/frame.py`,实现以 active still Image Empty 为基准、收集所有选中 still Image Empty、忽略非图片对象且可从 Region 任意位置开始的矩形 modal、Overlay、状态文本、取消和错误清理;用单选、多选、无效 active、选中视频、混合对象、微小矩形、工具切换和取消测试验证
- [x] 3.2 在 Frame 完成时冻结视图和所有参与对象状态,创建 packed 合成 Image,并以事务式更新让 active Object 承载新的 Image、世界矩阵、居中 offset 和显示尺寸,再删除其他参与 Image Empty;用对象 identity、名称、colorspace、Collection、共享 Image 生命周期、被忽略对象、异常回滚和单步 Undo 测试验证
- [x] 3.3 验证多张倾斜源图片在 Frame 前后的操作视角投射与 Alpha 层叠一致,并验证改变视角后结果保持已烘焙合成图而不再跟随原投射平面
## 4. 替换 Crop Box
- [x] 4.1 将 Crop Tool 组、注册导出和默认激活入口改为 Frame、Crop Lasso、Crop Polyline、Crop Perspective,统一使用 `FrameTool`、`anyimage.frame` 和 Frame Operator ID,并用注册顺序、ToolDef、菜单与激活测试验证
- [x] 4.2 从 Crop Selection Operator 删除 BOX 的 Refine Selection、Invert 和 Keep Original 映射,删除 Crop Box 专用文件、类、ID、公开导出和 BOX gesture 分支,同时保留 Crop Lasso、Crop Polyline 与 Cutout Lasso 的公共 Selection 交互;用搜索和交互测试验证边界
- [x] 4.3 从 `AnyImageSettings`、设置同步和工具栏绘制中删除 `crop_box_refine_selection`、`crop_box_invert`、`crop_box_keep_original`,确认 Frame 无业务选项且其他三个 Crop 子工具的独立设置不变
## 5. 文档与完整验证
- [x] 5.1 更新用户文档、架构、Operator 索引、Crop Tool 与公共 Viewport/Image 内部文档,用流程图正面描述 Frame 的多选图片、Viewport 分辨率、active 对象/深度基准、逐像素深度合成、输入对象消费、透明 Canvas、view-facing 结果和无选项行为,并移除当前功能描述中的 Crop Box
- [x] 5.2 运行 Crop、菜单、属性、注册、对象放置、打包与 Blender Addon 相关测试,再运行 `uv run pytest`,确认完整测试集通过且不构建文档或扩展产物
## 6. 试用反馈修正
- [x] 6.1 更新 proposal、design 与 Frame spec,明确 Viewport snapshot 分辨率、Preferences 最大长边、透明 RGB 边缘扩展、无重叠 warning 和原生点击选择语义
- [x] 6.2 实现 Frame 的 Viewport 像素输出尺寸和 `max_frame_resolution` Preferences,并补充远距离、Region 与配置上限测试
- [x] 6.3 统一投影结果的透明 RGB 边缘扩展,将 Frame 无 active overlap 降级为 warning,并补充像素及报告级别测试
- [x] 6.4 让 Frame、Crop Lasso、Cutout Lasso 使用 drag-only 交互,修正点选型工具的首次对象切换,并用 ToolDef 与交互测试覆盖普通点击、Shift 加选和拖动启动
- [x] 6.5 运行 Crop、Preferences、注册与 Blender Addon 相关测试,再运行 `uv run pytest`,确认完整测试集通过且不构建文档或扩展产物
- [x] 6.6 统一要求图片编辑源同时为 active 和 selected Image Empty,让 Frame 在无效 active 时报告 warning,并用 active 未选中、active 非图片但 Selection 含图片及正常混合 Selection 测试验证
- [x] 6.7 删除所有图片 WorkspaceTool 的 fallback selection 选项,以公共显式 keymap 保留普通点击与 Shift 点击选择,并验证 Tool Settings 不再暴露 `Drag: Select Box/Lasso`