Add comprehensive tests for edge depth extension, empty image edits, and material view adaptation

- Introduced `test_edge_depth.py` to validate the functionality of edge depth extension, ensuring core alpha preservation, proper depth handling, and edge padding behavior.
- Created `test_empty_image_edits.py` to verify the behavior of image edits in Blender, focusing on shared edits, preservation of image properties, and handling of failed operations.
- Developed `test_material_view_adaptation.py` to test material adaptation preferences, ensuring color space and alpha settings are correctly applied and preserved across various scenarios.
This commit is contained in:
2026-09-06 21:43:47 +08:00
parent 1994d38ccd
commit 7e55af6722
58 changed files with 3607 additions and 200 deletions

View File

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

View File

@@ -0,0 +1,63 @@
## Context
`create_image_edit_result()` 和 Job 编辑结果加载入口先准备结果 Image,`replace_empty_image()` 再替换物体引用。当前仅在旧 Image 没有真实用户且没有 Fake User 时删除旧图并恢复源名。Frame 在替换活动物体之后删除合并源物体,随后再次清理图片。
## Goals
- 以其他 Empty Image 的真实引用决定结果是否独立。
- 共享编辑保护其他 Empty 的数据、显示和动画状态。
- 单独使用时保留源 Image 身份和完整名称。
- 统一结果提交、失败恢复与临时数据清理。
## Decisions
### 1. 按文件中的物体引用判断共享
在 `common/image.py` 复用 `is_image_empty()`,遍历 `bpy.data.objects`,检查当前物体以外是否存在 `data` 等于源 Image 的图片 Empty。比较 Blender 数据块身份;找到首个匹配即可结束。隐藏、未在当前 View Layer 显示、其他场景中的对象均参与判断。同一物体链接到多个集合仍是一个对象。
扫描在主线程最终提交时执行,使异步 Job 运行期间新增的共享物体也得到保护。Frame 提供本次成功后将删除的源物体集合,判断时排除这些物体。
`Image.users` 仅继续承担无引用数据清理等生命周期判断。Fake User 是保存设置,材质引用是图片消费者,两者均不代表另一个 Empty Image。
### 2. 结果先准备,按共享关系提交
```mermaid
flowchart TD
A[准备完整编辑结果] --> B[查找操作后仍保留的其他图片 Empty]
B --> C{是否共用源 Image}
C -->|是| D[当前 Empty 绑定独立结果]
C -->|否| E[将结果内容提交到源 Image]
D --> F[保留其他 Empty 与源图状态]
E --> G[保留源图身份与名称并释放临时结果]
```
共享分支沿用结果 Image 的创建、打包与 Blender 唯一名称规则,仅改变当前物体绑定。单独使用分支将准备好的结果内容写入源 Image,保留源名称和 Fake User 设置,统一入口返回最终实际使用的 Image。临时结果不作为额外的最终图片保留。
提交需覆盖像素尺寸、颜色空间、Alpha、Packed 内容、文件路径及图片类型;静态像素结果与动画 Job 结果按各自的数据表示处理。继续复用现有像素和动画辅助函数。调用方使用返回的最终 Image,避免保留已释放的临时结果引用。
AnyImage 材质贴图使用独立的 `_color.png` 等数据。用户手动引用源 Image 时,其引用关系保持原样;单独使用分支原位更新产生的内容变化按普通数据共享处理,不额外遍历或迁移材质引用。
### 3. Frame 使用提交后的存活物体集合
Frame 以活动物体为编辑目标,其他本次合并源物体按既有语义删除。只有合并之外仍保留的 Empty 共用活动源图时才分离。提交成功之后删除合并源物体,再清理其确实没有用户的旧图;活动结果名称由统一提交入口负责。
### 4. 提交具有失败恢复边界
完整结果准备好后才修改源数据。共享分支保留现有物体绑定、显示位置和动画设置恢复;原位分支还需保留可恢复的源内容、尺寸、编码及打包状态,直到提交成功。恢复数据应复用现有图片读取与加载能力,在成功和失败时均释放临时资源。取消编辑时不进入提交。
## Risks / Trade-offs
- [原位写入需要同时维护像素、Packed 数据与动画来源] → 通过真实 Blender 数据测试覆盖静态图片、尺寸变化、序列结果及保存重开,验证结果内容实际持久化。
- [中途失败留下部分写入] → 对内容写入和物体显示配置分别注入失败,验证源图及物体状态恢复。
- [Frame 的待删除物体导致错误分离] → 测试所有共享者均被合并、合并外仍有共享者两种情况。
- [已有未归档图片命名规格包含总用户数语义] → 本 change 明确 Empty 共享判定;归档整合时以本规则统一相关要求。
## Migration Plan
1. 增加真实 Blender 引用关系与结果提交回归用例。
2. 修改公共提交入口,并核对所有本地编辑与 Job 调用方的返回值和清理职责。
3. 整合 Frame 存活物体判断,运行相关测试和完整测试。
## Open Questions
无产品规则待确认;原位提交对静态与动画 Image 的具体写入步骤通过实现阶段的真实 Blender 回归验证。

View File

@@ -0,0 +1,26 @@
## Why
图片编辑目前通过旧 Image 的总引用数决定是否保留旧数据,无法直接表达用户关心的其他 Empty Image 是否共用原图。需要以真实物体引用关系决定图片分离,使编辑当前 Empty 时其他共用原图的 Empty 保持原样。
## What Changes
- 在整个文件中查找引用同一 Image 的其他 Empty Image,以此决定编辑结果是否需要独立数据块。
- 存在其他 Empty Image 时,仅将当前物体切换到结果 Image;不存在时更新当前 Image,保留身份与完整名称。
- 隐藏物体和其他场景中的 Empty Image 同样参与判断;Frame 按合并后仍保留的物体判断共享。
- 统一本地像素编辑与 Job 结果提交,保留成功、取消和失败时的图片及物体状态约定。
## Capabilities
### New Capabilities
- `empty-image-edit-isolation`: 定义以 Empty Image 引用关系为依据的编辑分离、单独使用时的更新、Frame 合并及失败恢复规则。
### Modified Capabilities
无。当前 `openspec/specs/` 为空;本能力承接已有 change 中的图片编辑命名与替换约定。
## Impact
- `src/anyimage/common/image.py`:共享判断与统一结果提交入口。
- `src/anyimage/operators/image_edit_tool/` 及调用图片替换入口的 Job 响应:结果提交与 Frame 合并。
- `tests/test_image_data.py`、`tests/test_alpha_presentation.py`、`tests/test_blender_addon.py` 及相关图片编辑测试。

View File

@@ -0,0 +1,72 @@
## ADDED Requirements
### Requirement: Sharing is determined by other image Empty objects
系统 SHALL 在编辑结果提交时,以整个文件中是否存在其他保留的 Empty Image 引用同一源 Image 判断共享,覆盖隐藏物体及其他场景中的物体。判断 SHALL 使用物体与数据块身份,且 SHALL NOT 使用总引用数、Fake User 或界面计数显示作为分离依据。
#### Scenario: A hidden object shares the source
- **WHEN** 当前物体与一个隐藏的 Empty Image 引用同一 Image,且界面没有显示引用数量
- **THEN** 系统识别为共享图片
#### Scenario: An object in another scene shares the source
- **WHEN** 另一场景中的独立 Empty Image 引用当前源 Image
- **THEN** 系统识别为共享图片
#### Scenario: One object is linked to multiple collections
- **WHEN** 仅当前 Empty 引用源 Image,且该物体链接到多个集合
- **THEN** 系统按单独使用处理
#### Scenario: Non-Empty references exist
- **WHEN** 仅当前 Empty Image 引用源图,源图另有 Fake User 或用户手动建立的材质引用
- **THEN** 系统按单独使用处理
#### Scenario: Sharing changes during a job
- **WHEN** Job 提交后、结果应用前,另一个 Empty Image 开始引用源图
- **THEN** 最终提交按共享图片处理
### Requirement: Shared edits update only the current Empty
存在其他保留的 Empty Image 共用源图时,系统 SHALL 仅将当前物体绑定到独立编辑结果,并保持其他 Empty 的 Image 绑定、原图内容和显示状态。结果 SHALL 请求源 Image 的完整名称,由 Blender 处理唯一性后缀。
#### Scenario: Two Empty objects share one image
- **WHEN** A 与 B 共用同一 Image,用户编辑 A
- **THEN** A 使用编辑后的独立 Image,B 仍引用原 Image,原图像素、Packed 内容、名称及 B 的显示和动画设置保持原样
### Requirement: A sole image Empty retains its Image identity
没有其他保留的 Empty Image 共用源图时,系统 SHALL 将编辑结果提交到源 Image,保留数据块身份、完整名称与 Fake User 设置,且成功后 SHALL 释放本次临时结果数据。用户手动建立的其他引用 SHALL 保持原绑定。
#### Scenario: A sole Empty is edited
- **WHEN** 唯一使用源图的 Empty 完成图片编辑
- **THEN** 其 Image 身份和完整名称保持不变,内容及尺寸更新为结果,保存重开后内容正确,且不保留额外结果 Image
#### Scenario: A user manually references the source in a material
- **WHEN** 唯一 Empty 的源图另被用户手动用于材质
- **THEN** 编辑按单独使用提交,材质引用仍绑定同一 Image
#### Scenario: An animated job result is committed
- **WHEN** 单独使用的 Empty 收到动画图片编辑结果
- **THEN** 源 Image 身份和名称保持不变,帧来源及打包结果更新正确,物体动画播放设置按现有工具语义配置
### Requirement: Frame considers surviving Empty objects
Frame SHALL 按成功合并后仍保留的 Empty Image 判断活动源图是否共享,并在结果提交成功后执行合并源物体删除。
#### Scenario: All other sharing objects are merged
- **WHEN** A 与 B 共用源图,Frame 将 B 合并到活动物体 A,且没有其他 Empty 使用源图
- **THEN** A 的源 Image 原位更新,B 按合并规则删除
#### Scenario: A sharing object survives outside the merge
- **WHEN** Frame 将 B 合并到 A,合并之外的 C 仍引用 A 的源图
- **THEN** A 使用独立结果,C 及其源 Image 保持原样
### Requirement: Failed or cancelled edits preserve source state
统一提交入口 SHALL 覆盖本地编辑与 Job 图片编辑。取消或提交失败时,系统 SHALL 保留或恢复源 Image 内容与身份、物体绑定、显示及动画设置,并释放本次临时资源。
#### Scenario: Sole-image commit fails
- **WHEN** 原位提交在写入内容或配置显示期间失败
- **THEN** 源图内容、尺寸、Packed 数据及物体状态恢复到提交前状态,临时结果被清理
#### Scenario: Shared-image commit fails
- **WHEN** 共用图片的当前 Empty 提交失败
- **THEN** 当前物体恢复原绑定,其他 Empty 及原图保持原样,临时结果被清理
#### Scenario: An edit is cancelled
- **WHEN** 用户预览编辑后取消
- **THEN** 原图和所有 Empty 的绑定及显示状态保持原样

View File

@@ -0,0 +1,22 @@
## 1. 固定共享判断与身份约定
- [x] 1.1 增加真实 Blender 数据测试,覆盖两个 Empty 共享、隐藏共享者、跨场景共享者及同一物体链接多个集合,按数据块身份验证判断结果。
- [x] 1.2 增加单独使用回归,覆盖普通图片、Fake User、手动材质引用,验证结果内容、源 Image 身份、完整名称及临时 Image 数量。
- [x] 1.3 在 `common/image.py` 复用 `is_image_empty()` 实现其他存活 Empty 的引用查找,支持 Frame 排除待删除对象,并验证提交时重新判断共享。
## 2. 统一图片编辑提交
- [x] 2.1 调整公共结果提交入口,共享分支只更新当前物体绑定,验证其他 Empty 与原图像素、打包及动画状态保持不变。
- [x] 2.2 实现单独使用时的静态结果原位提交及临时结果释放,覆盖尺寸变化、Alpha、颜色空间、Packed 内容、名称和 Fake User 保留。
- [x] 2.3 实现动画 Job 结果原位提交,验证帧来源、打包内容、播放设置及保存重开;复用现有图片与动画辅助函数。
- [x] 2.4 为内容写入和显示配置注入失败,完善源图内容及物体状态恢复,验证失败与取消不残留临时数据。
## 3. 整合工具与 Frame
- [x] 3.1 检查全部 `replace_empty_image()` 调用方,包括 Mask、Rectify、Frame、Remove Background 和 Upscale,使用最终返回 Image 并统一临时结果清理职责。
- [x] 3.2 Frame 按合并后存活对象判断共享,移除重复命名处理,覆盖共享者全部参与合并及外部共享者保留两种情况。
## 4. 验证
- [x] 4.1 运行相关图片数据、Alpha、图片编辑及 Operator 回归,验证连续编辑、保存重开、共享保护和异常恢复。
- [x] 4.2 运行 `uv run pytest`,确认全部测试通过。