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-05

View File

@@ -0,0 +1,76 @@
## Context
动机见 proposal.md。Frame、Mask、Rectify 与 Cutout 共享 Blender 侧 Image 和手势函数,业务像素使用 straight RGBA;结果 Image 默认 PREMUL。当前审查以 460 个测试和 56 个子测试通过的工作区为基线,额外探针揭示了普通测试未覆盖的源缓存副作用、透视边界误判和长笔画开销。
## Goals / Non-Goals
设计将数据读取、几何覆盖、呈现缓存与最终提交分开:源数据只读,覆盖只表达几何,预览按变化更新,结果在完成时一次提交。现有 Alpha 合成公式、透明 RGB、工具选择行为及 Cutout 轴向契约作为回归基线。
## Decisions
### 1. 像素读取以当前有效数据为准
`image_pixels()` 成为无副作用入口。GENERATED 或 dirty Image 直接读取当前像素缓存;干净图片按其实际编码取得业务 RGBA。需要以 STRAIGHT 解释已重载 Packed PNG 时,在独立、临时 Image 副本上完成解释并在 finally 中释放。模式切换只作用于临时数据;原 Image 的 mode、pixels、is_dirty、packed bytes、users 和共享对象引用保持原状。
先用真实 bpy 固定 byte/float、GENERATED/FILE、packed/unpacked、dirty/clean 和原生 PREMUL 浮点源的读数契约,再实现最小读取分支。业务结果继续使用用户确认的 PREMUL 默认值。临时数据只承担解码,不形成常驻缓存或新增持久属性;保存重开后仍读取相同业务 RGB/Alpha,透明 RGB 参与验证。
### 2. Frame 在源平面坐标中完成半平面裁剪
将 Frame 四边写为齐次投射分量的线性不等式,把透视正深度条件与这些边界共同作用于源图片单位矩形。仅对裁剪后的有限交叠点做透视除法,最后计算屏幕面积与代表深度。正交路径继续接受有限的非正深度。
```mermaid
flowchart TD
Source[源图片单位矩形] --> View[透视正深度或正交有效域]
View --> Clip[Frame 四边的齐次半平面裁剪]
Clip --> Divide[有限交叠顶点做透视除法]
Divide --> Area[正面积与代表深度]
Area --> Samples[既有逐像素采样与合成]
```
公共半平面裁剪位于 `common/selection.py`,接受二维顶点和线性边界。Rectify 的图片矩形裁剪复用同一实现,Frame 保留自身投影与深度职责。退化、切边与无交叠判断统一采用尺度相关的数值判定。
### 3. Brush 路径简化约束整段原始采样
保留经过最小点距筛选的原始轨迹。简化每条候选线段时检查该段全部原始点到线段的距离,最大误差为 0.75 个 Viewport 像素;确认的前缀固定,当前尾段保留可更新状态。近共线判断只控制压缩,不能代替整段误差约束。
预览与提交消费同一简化路径及 `brush_footprint_polygons()` 图元。公共 Lasso 消费者使用相同误差契约;Polyline 已提交顶点按用户输入保留。原始轨迹和缓存均在取消或完成时释放。
### 4. Brush 完整覆盖先合并,最后抗锯齿
以所有投影图元的裁剪后联合 bounds 创建一次局部覆盖画布。图元先以硬覆盖合并,公共抗锯齿函数只处理最终并集一次,再得到一个 SelectionMask。这样内部图元边界不会变成半透明接缝,结果也不随相同几何的分段方式变化。
```mermaid
flowchart TD
Path[有误差约束的路径] --> Primitives[共享圆形印记和连接条]
Primitives --> Preview[增量屏幕并集预览]
Primitives --> Release[释放鼠标]
Release --> Project[投影到图片空间]
Project --> Union[局部硬覆盖并集]
Union --> AA[统一抗锯齿一次]
AA --> Alpha[现有 Set / Add / Subtract]
Alpha --> Commit[创建 Packed Image 并替换]
```
### 5. 预览缓存覆盖已提交前缀与活动尾段
屏幕扫描行采用固定原点。已确认路径前缀的图元、扫描行覆盖和外轮廓作为持久的操作内缓存;新增图元只更新其命中的局部扫描行,活动尾段改变时重算受影响范围及相邻轮廓连接。填充与虚线由同一覆盖缓存派生,使用一个版本标记。
静止重绘复用已有 GPU batch;路径变化时按受影响块更新批次。取消、完成与工具切换统一释放路径、覆盖和绘制缓存。性能验收同时记录 800/1600/3200 点耗时及新增段触及的计算范围,避免仅靠特定机器的毫秒阈值判断。
### 6. 提交入口与输出计算直接表达当前职责
- Mask 使用仅含 UNDO 的交互 Operator,移除 REGISTER 和 Operator draw;Scene 工具设置继续在 invoke 时冻结。
- Frame 直接写入已独立合成的 RGB/Alpha。保留 Alpha 的现有近零判定、源内透明 RGB 和源外 RGBA 全零,删除整图 blended_rgb 备份及后续预乘/反预乘/扩色回路。
- 删除 `scanline_fill_triangles()`、`preview_fill_triangles()` 转发入口,测试改为验证当前预览几何入口的行为。
- 删除只写不读的 `_region_size`,合并 `_fill_polygon`、`_outline_polygon` 的重复缓存标记。
## Risks / Trade-offs
- [不同 Blender 图像编码的 Alpha 解释不同] → 以真实 byte/float 和保存重开测试固定读取结果,验证原源脏状态与共享引用,临时副本总在 finally 释放。
- [接近观察平面的裁剪容易出现数值抵消] → 归一化半平面系数,覆盖焦距 1/1.7/2/4、30° 倾斜、极近前方、无交叠和正交负深度样例。
- [增量并集容易在尾段替换与自交处留下旧边界] → 与全量参考几何逐次对比,覆盖回折、重叠、长轨迹及模式颜色变化。
- [原始路径保留增加操作内内存] → 保留紧凑屏幕点,统一释放;读取源 RGBA 与图片级栅格化仍只在提交时发生。
## Migration Plan
先增加失败行为回归,再依次调整只读像素、公共裁剪、Brush 路径与覆盖、增量预览,最后清理旧入口并运行全量测试。全部改动采用当前接口的直接实现,无持久数据迁移;实施过程中保留本轮已有功能与测试。完成后归档本提案时,应与相关未归档功能规格核对同名业务约定。

View File

@@ -0,0 +1,33 @@
## Why
本轮审查发现,图片读取会重载并覆盖 Blender 像素缓存,Frame 会误判有效透视交叠,Brush 的路径简化、覆盖合并和预览更新存在精度与性能问题。需要修复这些可复现行为,并清理同一调用链中的旧入口和重复计算。
## What Changes
- 让业务像素读取保持源 Image 的像素、Alpha 模式、脏状态与共享引用不变,保留已确认的结果 PREMUL 默认值。
- Frame 在齐次坐标中裁剪有效投射,再判断连续几何交叠。
- Brush 使用具有整段误差约束的屏幕路径;完整图元先合并覆盖,再统一抗锯齿。
- Brush 增量更新屏幕预览,复用填充与外轮廓的单一缓存。
- Mask 保留单步 Undo,移除没有 execute 实现的 REGISTER 与重做参数面板。
- Frame 直接输出独立合成的 RGB/Alpha,清理已被覆盖的预乘、反预乘与扩色处理。
- 将 Frame、Rectify 的矩形裁剪收敛到公共半平面裁剪;删除只供测试调用的转发函数和无消费者状态。
## Capabilities
### New Capabilities
- `image-pixel-read-integrity`: 业务像素读取的无副作用约束与脏缓存保留。
- `frame-projection-clipping`: Frame 连续有效交叠的齐次裁剪与合成输出约束。
- `brush-stroke-processing`: Brush 路径误差、覆盖合并、增量预览与 Mask 提交行为。
### Modified Capabilities
无。当前 `openspec/specs/` 为空;相关功能仍记录在未归档变更中。本提案补充其修复契约,实施以当前工作区为基线,与 `enable-image-alpha-blending-and-premul`、`support-frame-view-plane-intersections`、`add-live-mask-preview-and-polyline`、`preserve-frame-bottom-layer-rgb` 共同验收。
## Impact
- `common/image.py`:像素读取及结果数据保留。
- `common/selection.py`、`common/viewport.py`:裁剪、路径、覆盖和预览缓存。
- `operators/image_edit_tool/frame.py`、`mask.py`、`rectify.py`:交叠、提交与共享能力调用。
- Cutout Lasso 作为公共手势消费者参加回归验证;轴向与节点资产沿用当前实现。
- 增补行为回归与结构性性能测试,运行全量 pytest;实施仅修改代码和测试,不增加依赖、不构建文档或发布产物。

View File

@@ -0,0 +1,52 @@
## Purpose
保证 Brush 的笔画范围准确跟随屏幕轨迹,预览与最终覆盖使用一致的几何并集,使长笔画持续响应,并提供清晰的完成、取消和单步撤销行为。
## ADDED Requirements
### Requirement: Stroke simplification bounds the whole segment error
Brush 与共享自由手绘手势 SHALL 将每个保留原始采样点到简化线段的偏差限制在 0.75 个 Viewport 像素以内。Polyline 的已提交顶点 SHALL 按用户输入保留。
#### Scenario: Dense curved stroke is simplified
- **WHEN** 用户以高密度采样绘制弯曲轨迹
- **THEN** 整段简化误差不超过 0.75 px,连续尾点替换不会累计放大偏差
- **AND** 预览与提交使用同一有效路径
### Requirement: Brush antialiases the complete geometric union
Brush SHALL 对全部圆形印记和连接条的完整几何并集应用一次抗锯齿。几何并集相同的图元分段方式 SHALL 产生相同覆盖值。
#### Scenario: Stroke turns through a right angle
- **WHEN** 半径为 2、5 或 25 px 的 Brush 形成直角连接或自交
- **THEN** 内部连接处不会因图元各自的抗锯齿边界减少覆盖
- **AND** 提交覆盖符合完整并集的抗锯齿参考结果
#### Scenario: Equivalent geometry has different segmentation
- **WHEN** 同一个直线 footprint 由不同数量的共线连接条描述
- **THEN** 输出 Alpha 覆盖在浮点误差内一致
### Requirement: Long stroke preview remains responsive
Brush SHALL 在每次有效路径变化后显示整条待提交笔画的合并填充和外轮廓。局部新增笔画的预览计算 SHALL 限定于变化范围,静止重绘 SHALL 复用已有预览结果。
#### Scenario: A short segment is appended to a long stroke
- **WHEN** 已有 800、1600 或 3200 个屏幕点,用户追加只影响局部范围的短线段
- **THEN** 预览更新覆盖新范围并保留其他区域,计算范围不扩展为完整历史笔画
- **AND** 预览与相同路径的完整几何参考一致
#### Scenario: Tail changes or crosses previous geometry
- **WHEN** 活动尾段变化或与已有笔画相交
- **THEN** 预览清除失效尾段的覆盖及内部轮廓,呈现当前完整 footprint
### Requirement: Mask provides one transactional gesture edit
Mask SHALL 在启动时冻结 Scene 的 Gesture、Mode 和 Radius;完成时一次提交 Packed Image 并支持单步 Undo。Mask SHALL NOT 暴露无法执行的操作后参数重做面板。
#### Scenario: Stroke is completed and undone
- **WHEN** 用户完成一个 Brush、Lasso 或 Polyline Mask 后执行 Undo
- **THEN** 源图片与对象状态恢复,整次手势只产生一个编辑步骤
#### Scenario: Stroke is cancelled or tool changes
- **WHEN** 用户按 Esc、RMB,或切换工具取消未提交手势
- **THEN** 原 Image 不变,所有操作内路径与预览资源释放
#### Scenario: User edits tool settings
- **WHEN** 用户修改 Scene 中的 Mask 工具设置
- **THEN** 新手势采用新设置,进行中的手势保持启动时设置

View File

@@ -0,0 +1,36 @@
## Purpose
定义 Frame 在透视观察平面附近的连续交叠判定,确保可见且可稳定求交的图片区域能够参与取景,并固定结果透明颜色和无效交叠时的对象保留行为。
## ADDED Requirements
### Requirement: Frame accepts a finite visible intersection
Frame SHALL 依据画框内的连续有效投射判断正面积交叠。完整源图片的角点接近无穷远 SHALL NOT 导致画框内稳定有效的交叠被拒绝。
#### Scenario: Tilted source crosses the perspective origin
- **WHEN** 30° 倾斜源平面位于视图深度 0.2 且部分跨越观察平面,透视 X/Y 焦距系数为 1.7、2 或 4,Frame 内有正面积前方交叠
- **THEN** Frame 接受交叠并生成结果
- **AND** 对应有效源坐标与实际采样保持一致
#### Scenario: Source lies extremely close in front
- **WHEN** 源平面非常接近观察原点但画框内仍存在数值可稳定表达的正面积前方交叠
- **THEN** Frame 接受交叠,不因源角点的远距离屏幕投射取消操作
#### Scenario: Orthographic source has non-positive depth
- **WHEN** 正交 Frame 与具有有限非正视图深度的源平面存在稳定正面积交叠
- **THEN** Frame 正常完成
### Requirement: Invalid active overlap preserves inputs
Frame SHALL 在 active 图片与画框没有稳定正面积交叠时以 warning 取消,并保留全部参与对象的图片、变换和存在状态。
#### Scenario: Frame only touches an edge or misses the source
- **WHEN** Frame 仅接触 active 图片投射边缘,或没有有效交叠
- **THEN** 操作取消,源对象与源图片均保持不变
### Requirement: Frame preserves independent color and alpha output
Frame SHALL 保留底层原色与前景混合得到的 RGB、独立累积的 Alpha 及边界子像素覆盖。源内透明 RGB SHALL 保留,完全未覆盖 Canvas SHALL 为 RGBA 全零。
#### Scenario: Frame result is revealed by Mask Add
- **WHEN** 多层 Frame 含透明底色与半透明前景,随后执行 Mask Add
- **THEN** 恢复区域显示原合成 RGB,源外透明 Canvas 的 RGB 保持为零
- **AND** 分块高度变化不会改变合成结果

View File

@@ -0,0 +1,37 @@
## Purpose
保证图片编辑与网格生成取得当前有效的业务像素,同时保护 Blender 源图片的未保存内容、透明颜色、呈现模式与共享引用,使连续编辑和保存重开具有一致的数据结果。
## ADDED Requirements
### Requirement: Reading business pixels preserves source state
系统 SHALL 在读取业务 RGBA 时保留源 Image 的像素缓存、Alpha 模式、脏状态、Packed 内容和对象引用。读取 SHALL NOT 通过重载源图片覆盖当前有效内容。
#### Scenario: Dirty packed image is read
- **WHEN** 用户修改 Packed Image 像素但尚未重新 Pack,图片编辑工具读取该图片
- **THEN** 读取结果包含最近修改的 RGB/Alpha
- **AND** 源像素、脏状态及原 Packed 内容保持读取前状态
#### Scenario: Shared source is read before an edit is cancelled
- **WHEN** 两个对象共享同一 PREMUL Image,其中一个工具读取图片后取消或报错
- **THEN** 两个对象仍引用相同源数据,源像素缓存与呈现设置保持不变
### Requirement: Business pixel interpretation survives persistence
系统 SHALL 保留既有 PREMUL 结果默认值,并在即时读取、Pack 和 blend 保存重开后取得相同业务 RGB/Alpha。误差 SHALL 仅来自既有编码精度,完全透明 RGB SHALL 可以通过 Mask Add 恢复。
#### Scenario: Edited image is reopened and read repeatedly
- **WHEN** 含透明及半透明像素的编辑结果保存重开并被多次读取
- **THEN** 每次返回相同业务像素,结果 Alpha 模式保持 PREMUL
- **AND** 读取不会改变源缓存或产生累计颜色变化
#### Scenario: Floating point source is consumed
- **WHEN** 工具读取生成或加载的浮点图片,包括原生预乘 Alpha 数据
- **THEN** 返回值符合源编码的业务 RGBA 语义并保留当前有效精度
- **AND** 源图片的像素及模式保持不变
### Requirement: Temporary decoding data has bounded lifetime
系统 SHALL 在单次业务读取结束或失败时释放该读取拥有的临时图片数据。
#### Scenario: Repeated read or decode failure
- **WHEN** 用户重复触发读取,或读取中的解码发生错误
- **THEN** 不留下额外 Image 数据块,原图片及共享引用保持不变

View File

@@ -0,0 +1,34 @@
## 1. 源像素读取完整性(审查 1)
- [x] 1.1 增加真实 bpy 回归,覆盖读取前后 pixels、alpha_mode、is_dirty、packed bytes 和共享 users,确认当前 dirty PREMUL 样例能复现内容丢失。
- [x] 1.2 明确 byte/float、GENERATED/FILE、dirty/clean、packed/unpacked 的读取分支,实现无源副作用的业务 RGBA 读取;以当前脏像素优先及原生浮点 PREMUL 样例验证数值语义。
- [x] 1.3 验证独立解码副本在成功与异常时释放,并完成 Mask Subtract → Pack → 保存重开 → Mask Add、重复 Frame 及共享源取消回归,确认透明 RGB 和结果 PREMUL 默认值保留。
## 2. 公共裁剪与 Frame 交叠(审查 2、8)
- [x] 2.1 在 common/selection.py 收敛二维半平面裁剪,用矩形完全包含、相离、边接触和退化多边形测试验证统一边界行为。
- [x] 2.2 改为在源单位矩形上按透视深度与 Frame 齐次边界裁剪,验证 30° 倾斜、深度 0.2、焦距系数 1/1.7/2/4 的交叠与采样一致,并覆盖极近前方、正交负深度和无效交叠。
- [x] 2.3 Rectify 复用公共裁剪并删除本地重复算法,运行四角规范化、部分交叠和图片外四边形测试,确认输出范围和放置语义保持一致。
## 3. Brush 路径与覆盖(审查 3、4)
- [x] 3.1 保留原始手势采样并实现整段误差受限的简化,使用弯曲密集采样检查每个原始点到对应简化段的距离不超过 0.75 px,回归 Lasso 与 Polyline 顶点行为。
- [x] 3.2 以联合局部 bounds 合并 Brush 硬覆盖,并复用公共抗锯齿处理完整并集一次;用半径 2/5/25 的直角、自交和等价分段样例验证覆盖一致及接缝消除。
- [x] 3.3 验证预览与提交消费同一路径和 footprint,检查拖动期间没有 RGBA 读取或图片级计算,释放后只有一次 Packed 结果提交。
## 4. 增量预览与状态清理(审查 6、9)
- [x] 4.1 建立固定屏幕扫描行的已确认前缀缓存及活动尾段,局部更新覆盖、填充和外轮廓;逐次与全量参考对比回折、自交、尾段变化和图元重叠结果。
- [x] 4.2 静止重绘复用 GPU batch,按变化块更新绘制;记录 800/1600/3200 点的基线与更新耗时,并用计算范围检查确认局部新增段不会重算完整历史。
- [x] 4.3 合并填充和轮廓缓存标记,删除 _region_size 与仅测试调用的 scanline_fill_triangles、preview_fill_triangles 转发入口;通过全仓引用搜索和当前预览入口测试验证清理完整。
- [x] 4.4 统一完成、取消、异常和工具切换的路径及预览资源释放;以资源计数、原 Image 引用和连续启动新手势验证没有残留状态。
## 5. Mask 提交与 Frame 输出简化(审查 5、7)
- [x] 5.1 移除 Mask REGISTER 与无效重做 draw,保留 Scene 设置和 UNDO;检查 Blender 类型注册/注销,并验证三种手势完成后的单步撤销及设置冻结。
- [x] 5.2 Frame 直接写入独立 RGB/Alpha,删除 blended_rgb 整图备份和被覆盖的颜色转换链;用透明底色、半透明前景、源外全零、极小 Alpha 与不同分块高度测试验证结果一致。
## 6. 整体回归
- [x] 6.1 运行 `uv run pytest`,覆盖 Image Edit、Cutout、Plane、Job 颜色加载和打包内容测试;确认全部通过,并核对本轮已有失败探针均成为通过的回归。
- [x] 6.2 审查最终 diff,确认九项发现均有对应修复与验证、无旧转发入口或重复裁剪;实施改动限于代码、测试及本提案任务状态,不产生产品文档或构建产物。