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,66 @@
## Context
图片工具没有实现自定义 Viewport Picker。普通点击由 keymap 调用 Blender `view3d.select`;Mask 与 Rectify 因为以 `PRESS` 启动 Operator,由 `resolve_image_edit_click()` 主动调用同一个原生选择。AnyImage 只比较点击前后的 active object,并验证 active source 是已选择的 Image Empty。
当前两条入口都把 `deselect_all=True` 传给原生选择。Image Empty 的透明像素可能没有拾取结果,因此透明区域与 Viewport 空白都会清除 active source。Mask、Rectify 和 Cutout 随后的起始动作便依赖第一点命中非透明图片内容。
## Goals / Non-Goals
**Goals:**
- 让 Mask、Rectify 与 Cutout 在已有 active Image Empty 时从透明区域或 Viewport 空白开始操作。
- 继续使用 Blender 原生 Picker 决定命中的对象、active object 和普通选择结果。
- 保持点击另一对象只切换选择、不同时启动图片操作。
- 将行为变化限制在三个绘制型工具,不改变 Frame 的多选工作流。
**Non-Goals:**
- 不实现只允许 Image Empty 的自定义 Picker、射线检测或 Alpha 命中。
- 不撤销或过滤 Mesh、Light 等非 Image Empty 的原生选择结果。
- 不保存隐藏的图片目标,也不在工具激活时冻结 source object。
- 不改变手势投影、图片范围裁切、可见内容判断或结果生成。
## Decisions
### 1. 原生 Picker 使用不清空选择的空命中策略
`resolve_image_edit_click()` 继续调用 `bpy.ops.view3d.select`,但使用 `deselect_all=False`。命中另一对象时 Blender 正常更新 active,函数返回该对象并结束本次 Operator;没有命中时 active 保持为原 Image Empty,函数把同一次事件解释为图片操作起点。
相比先执行默认选择再保存和恢复 Selection,这一方式不需要手动复制多选集合、写入 `select_set()` 或重建 active object。相比自定义射线检测,它继续继承 Blender 对遮挡、对象可选状态和 Viewport 的完整规则。
### 2. Cutout 单击使用同一空命中策略,Frame 保持原行为
Cutout 的 Lasso 由 `CLICK_DRAG` 启动,普通 `CLICK` 仍交给原生对象选择。公共 selection keymap 提供明确的 `deselect_on_empty` 策略:Cutout 传入 false,使空白单击保留 active source;Frame 沿用 true,使其多选图片集合仍可通过空白点击清空。
Mask 与 Rectify 的普通按下事件由 Operator 接管,空命中策略由 `resolve_image_edit_click()` 负责。Cutout 的整图双击也复用该解析入口。Shift 点击继续使用现有原生 toggle,不增加过滤规则。
不选择全局修改公共 keymap 默认值,因为 Frame 与三个单图片绘制工具的选择目的不同。
### 3. Source eligibility 与对象选择保持分离
命中任何另一对象时都保留 Blender 的选择结果,无论它是否为 Image Empty,本次不启动编辑。下一次操作仍通过 `active_image_edit_source()` 要求 active object 是已选择的 Image Empty;条件不满足时报告现有 warning。
不自动取消非 Image Empty,因为这会把 AnyImage 从业务资格验证扩展成对象选择管理,并使用户无法按 Blender 预期选择其他场景对象。
### 4. 起始点不判断图片范围或 Alpha
没有拾取结果只表示继续使用 active source,不表示起点已经落在图片内容内。Mask、Rectify 与 Cutout 继续把完整手势投影到图片平面,并在提交阶段执行既有的画布相交、Alpha 内容和有效几何检查。完全位于图片外的手势仍取消并报告对应 warning。
## Risks / Trade-offs
- [工具激活时空白点击不再 deselect all] → 这是三个绘制型工具维持操作对象的明确语义;用户仍可使用 Blender 的取消选择命令,Frame 也保留原行为。
- [点击非 Image Empty 会失去当前图片 active] → 保留原生选择结果比维护隐藏 source 更可预测;用户重新点击目标 Image Empty 后即可继续。
- [透明区域与真正的 Viewport 空白无法区分] → 两者都作为当前图片平面上的合法起始坐标处理,最终范围检查统一决定是否执行。
- [共享 keymap 的参数可能意外改变 Frame] → 测试分别固定 Cutout 的 false 与 Frame 的 true,并保持默认值表达 Frame 现状。
## Migration Plan
1. 为公共 selection keymap 增加空命中策略,并仅在 Cutout 选择入口关闭 deselect all。
2. 修改点击解析入口,使 Mask、Rectify 和 Cutout 双击在空命中时保留 active source。
3. 更新交互测试和当前内部文档,运行定向测试与完整测试。
回退时恢复两个原生选择调用的 `deselect_all=True`;不涉及持久数据、对象结构、Operator 参数或兼容层。
## Open Questions
无。

View File

@@ -0,0 +1,25 @@
## Why
Mask、Rectify 与 Cutout 当前把空白点击交给带 `deselect_all` 的 Blender 原生选择,导致透明像素或 Viewport 空白会清除 active Image Empty,后续手势只能从可拾取的可见像素开始。图片操作的起始坐标不应同时承担维持源对象选择的职责。
## What Changes
- 保留 Blender 原生对象拾取:命中另一对象时正常切换 active,本次不启动图片操作。
- Mask、Rectify 与 Cutout 在没有命中对象时保留当前 active Image Empty,使手势可从透明区域或 Viewport 空白开始。
- 继续由各工具在提交阶段判断手势是否与图片画布或可见内容有效相交。
- Frame 保持现有空白点击取消选择的行为;不建立只允许 Image Empty 的自定义 Picker,也不自动撤销非 Image Empty 的原生选择结果。
## Capabilities
### New Capabilities
- `image-tool-interaction`: 定义图片工具的原生对象选择、空白起始和源对象有效性规则。
### Modified Capabilities
## Impact
- 修改 `common/viewport.py` 中图片工具点击解析与 selection keymap 的空白点击策略。
- 调整 Mask、Rectify、Cutout 的交互测试与 Image Edit、Cutout、Common 内部文档。
- 不改变图片、SelectionMask、Cutout Shape、Job 协议、依赖或 Blender 数据结构。

View File

@@ -0,0 +1,60 @@
## ADDED Requirements
### Requirement: Image tools preserve Blender object picking
Mask、Rectify 与 Cutout SHALL 使用 Blender 原生 Viewport Picker 决定点击命中的对象和 active object。系统 MUST NOT 按对象类型过滤原生命中结果,也 MUST NOT 在命中非 Image Empty 后恢复先前的图片选择。
#### Scenario: Another Image Empty is clicked
- **WHEN** 一个 Image Empty 为 active,用户使用 Mask、Rectify 或 Cutout 点击另一可拾取的 Image Empty
- **THEN** Blender 原生选择 SHALL 激活被点击的 Image Empty,且本次事件不得启动图片操作
#### Scenario: A non-image object is clicked
- **WHEN** 一个 Image Empty 为 active,用户使用 Mask、Rectify 或 Cutout 点击可拾取的 Mesh、Light 或其他非 Image Empty
- **THEN** 系统 SHALL 保留 Blender 对该对象的原生选择结果,且本次事件不得启动图片操作
### Requirement: Drawing image tools preserve the active source on an empty hit
当已选择的 active object 是 Image Empty 时,Mask、Rectify 与 Cutout MUST 在原生 Picker 没有命中对象时保留该 active source,并 MUST NOT 因 `deselect_all` 清空选择。透明图片像素与 Viewport 空白 SHALL 使用相同的空命中语义。
#### Scenario: Mask starts on a transparent or empty location
- **WHEN** 已选择的 active Image Empty 存在,用户以 Mask 在没有原生对象命中的位置按下鼠标
- **THEN** 系统 SHALL 保持该 Image Empty active 并以该位置开始当前 Mask Gesture
#### Scenario: Rectify starts on a transparent or empty location
- **WHEN** 已选择的 active Image Empty 存在,用户以 Rectify 在没有原生对象命中的位置按下鼠标
- **THEN** 系统 SHALL 保持该 Image Empty active 并把该位置作为第一个 Rectify 点
#### Scenario: Cutout lasso starts outside pickable image content
- **WHEN** 已选择的 active Image Empty 存在,用户以 Cutout 从没有原生对象命中的位置开始拖动
- **THEN** 系统 SHALL 保持该 Image Empty active 并开始 Cutout Lasso
#### Scenario: Cutout full image starts from an empty location
- **WHEN** 已选择的 active Image Empty 存在,用户以 Cutout 在没有原生对象命中的位置双击
- **THEN** 系统 SHALL 保持该 Image Empty active 并打开整图 Cutout Shape 选择
#### Scenario: Cutout empty click keeps the source
- **WHEN** 已选择的 active Image Empty 存在,用户以 Cutout 单击没有原生对象命中的位置且没有形成拖动或双击
- **THEN** 系统 SHALL 保持当前选择且不启动 Cutout Shape 选择
### Requirement: Image operation eligibility uses the active selected Image Empty
Mask、Rectify 与 Cutout SHALL 仅在当前 active object 是已选择的 Image Empty 时开始图片操作。系统 MUST NOT 在工具激活时保存隐藏 source,也 MUST NOT 使用先前 active 的图片绕过当前选择。
#### Scenario: Active object is not an Image Empty
- **WHEN** 当前 active object 是非 Image Empty 或没有已选择的 active object,用户尝试开始 Mask、Rectify 或 Cutout
- **THEN** 系统 SHALL 取消本次图片操作并提示选择 active Image Empty
### Requirement: Gesture validity is decided after collection
Mask、Rectify 与 Cutout MUST NOT 根据起始点是否位于图片画布或是否命中非零 Alpha 决定能否开始手势。系统 SHALL 在手势提交阶段使用各工具现有的图片相交、可见内容和有效几何规则决定是否执行。
#### Scenario: Gesture starts outside and later overlaps the image
- **WHEN** Mask、Rectify 或 Cutout 的起始点位于图片外,但完成的手势与图片有效相交
- **THEN** 系统 SHALL 按完整手势执行对应图片操作
#### Scenario: Completed gesture remains outside the image
- **WHEN** 完成的 Mask、Rectify 或 Cutout 手势与图片没有有效相交
- **THEN** 系统 SHALL 在提交阶段取消操作并报告对应 warning
### Requirement: Frame keeps its existing empty-click selection behavior
Frame SHALL 保持 Blender 风格的空白点击取消选择行为,不得因 Mask、Rectify 与 Cutout 的空命中策略而改变。
#### Scenario: Frame clicks an empty viewport location
- **WHEN** 用户在 Frame 激活时普通点击没有原生对象命中的 Viewport 位置
- **THEN** Frame 的原生选择 keymap SHALL 继续请求 deselect all

View File

@@ -0,0 +1,18 @@
## 1. 原生选择策略
- [x] 1.1 为图片工具公共 selection keymap 增加明确的空命中 deselect 策略,保持 Frame 默认请求 deselect all
- [x] 1.2 让 `resolve_image_edit_click()` 在原生 Picker 空命中时保留 active object,同时保持命中另一对象时只完成原生选择
- [x] 1.3 将 Cutout 的普通点击配置为不在空命中时 deselect,并保持 Lasso、整图双击与 Shift toggle 的既有入口
## 2. 交互验证
- [x] 2.1 补充点击解析测试,覆盖空命中保留 active Image Empty、另一 Image Empty 和非图片对象保留原生选择结果
- [x] 2.2 补充 Mask 与 Rectify invoke 测试,验证透明或空白起点进入 Modal,而无有效 active Image Empty 时取消
- [x] 2.3 补充 Cutout keymap、Lasso 和整图双击测试,验证空白点击不清空 source、任意起点可进入既有提交链
- [x] 2.4 固定 Frame 仍以空白点击 deselect all,并验证 Shift 点击继续使用原生 toggle
## 3. 文档与校验
- [x] 3.1 更新 Image Edit、Cutout 与 Common 内部文档,说明原生对象拾取、空命中保留 source 和提交阶段有效性判断
- [x] 3.2 运行图片交互、Cutout 与 Blender Addon 定向测试并修正失败
- [x] 3.3 运行 `uv run pytest` 与 OpenSpec strict validation,不构建文档、节点资产或发布产物