feat(selection): separate selection geometry from image content processing

- Introduced a new design for selection rasterization that decouples the selection geometry from the image content, ensuring that the SelectionMask is independent of the source image's alpha and color.
- Updated the `rasterize_selection_path` function to only accept selection paths and image dimensions, removing parameters related to source alpha and alpha thresholds.
- Implemented a new approach for handling selection paths using non-zero winding fill, ensuring overlapping areas are preserved.
- Enhanced performance by utilizing a scanline algorithm for rasterization, significantly reducing processing time for high-resolution images.
- Updated internal documentation and added comprehensive tests to validate the new behavior and performance improvements.
- Ensured backward compatibility by maintaining existing interfaces where necessary while introducing breaking changes in the selection processing logic.
This commit is contained in:
2026-09-01 22:36:20 +08:00
parent 2ff7b04f56
commit 7f6e47432e
54 changed files with 2831 additions and 634 deletions

View File

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

View File

@@ -0,0 +1,72 @@
## Context
当前 Viewport 手势已经能够把屏幕路径转换为 `SelectionPath`,却会立即把它编码到 `selection_path_json` Property;Image Tool 随即反解同一个值。Cutout 还会在打开 Shape Pie Menu 前读取整图并将路径栅格化为 `SelectionMask` 做可见性验证;用户选定 Shape 后,`CutoutSelectionToShape` 再次读取和栅格化。Image Tool 的实际编辑阶段也会因 helper 隐式读取图片而重复取得同一 RGBA。
本文将用户所说的“序列化选区”明确为昂贵的 `SelectionPath → SelectionMask` 构建过程。`SelectionPath.to_json()` 只是 Blender Operator Property 之间传递轻量路径值的编码,不需要额外状态或缓存。
## Goals / Non-Goals
**Goals:**
- Image Tool 与 Cutout Tool 复用同一条手势主链,只构建一次 `SelectionPath`。
- Image Tool 在同一 Operator 内直接传递 `SelectionPath`,不做 JSON 往返。
- Image Tool 只在图片编辑执行阶段构建一次 `SelectionMask`。
- Cutout 只在 `CutoutSelectionToShape` 执行阶段构建一次 `SelectionMask`。
- 同一执行阶段显式复用源图 RGBA 与 Selection Mask,避免 helper 隐式重读或重建。
**Non-Goals:**
- 不引入令牌、全局表、Window 临时状态或 Selection 缓存。
- 不修改 Selection Path 的 JSON 格式、填充规则、抗锯齿核、Alpha 阈值或最终边界语义。
- 不修改 Cutout 网格算法、Shape Pie Menu 布局或 AI Job 协议。
- 本次不把全尺寸 Selection Mask 改为局部画布实现。
## Decisions
### 1. 手势阶段只构建并传递 Selection Path
`ImageSelectionGesture` 继续负责屏幕坐标到图片坐标的转换,并构建一次 `SelectionPath`。完成边界把该 Python 值交给可覆盖的实际操作提交方法,不再统一写入 `selection_path_json`。Image Tool 与 Cutout Tool 都复用该结果,不在手势完成回调中读取图片像素或调用 Selection Mask 栅格化。
Image Tool 在同一个 Operator 实例中保存内存 Selection Path,并直接进入图片编辑执行方法,因此删除自身的 `selection_path_json` Property 和 JSON 编解码。Cutout Tool 把同一个 Python 值交给 Shape Pie Menu;由于按钮会启动新的 Operator,菜单只在这里把 Path 编码一次,并将同一个 JSON 字符串设置到各 `CutoutSelectionToShape` 的 `selection_path_json` Property。菜单层不解析、验证或构建 Selection Mask,也不需要令牌中转。
相比令牌或临时表,直接传递现有 Operator Property 没有生命周期、失效、跨 Window 或菜单取消清理问题,并且已经满足 Blender UI Operator 的可序列化参数约束。
### 2. Selection Path 到 Selection Mask 的步骤只存在于实际执行阶段
Image Tool 的实际编辑方法直接接收手势阶段构建的 Selection Path、读取一次源图 RGBA,并将 Path 栅格化为一次 Selection Mask,再进入本地编辑或 Refine Selection 输入准备。`execute` 不再通过隐藏 JSON Property 恢复同一 Operator 已经持有的路径。
`CutoutSelectionToShape.execute` 是 Cutout 的实际生成边界:它直接接收 Pie Menu 传来的 Selection Path 值,读取一次源图 RGBA,并将 Path 栅格化为一次 Selection Mask,再进入 Surface、Balloon 或 AI Shape 主链。
Cutout 菜单前的 `validate_cutout_selection` 整体删除。路径结构仍由 `SelectionPath` 构建时验证;必须读取像素的可见 Alpha 与空 Selection 检查由实际执行阶段唯一一次栅格化完成。因此空 Selection 的提示会从打开菜单前移动到点击 Shape 后。
### 3. 同一执行阶段显式复用 RGBA 与 Selection Mask
完成一次读取和栅格化后,Operator 把现成的 RGBA 与 Selection Mask 显式传给图片编辑、Selection 输入导出、Cutout 颜色图和几何 helper。相关 helper 不再从 Blender Image 隐式读取同一像素,也不再接收 Path 后自行构建 Mask。
本地同步流程在一个调用栈内直接复用数据。AI Job 响应产生的 refined Selection 属于新的 Job 结果,不与提交前的手势 Selection 合并为同一个缓存生命周期;本次只移除同一阶段内语义相同的重复处理。
### 4. JSON 只保留在 Cutout 跨 Operator 边界
Lasso 或双击完成后,`SelectCutoutSelection` 把内存中的 `SelectionPath` 直接交给 `open_shape_pie`。菜单调用一次 `to_json()`,将得到的同一个字符串写入每个 `CutoutSelectionToShape` 按钮。用户点击按钮后,新 Operator 通过 `from_json()` 恢复 Path 并进入实际 Shape 生成。
保留这一次 JSON 编码是因为 Blender Operator Property 不能直接保存 Python dataclass,也没有适合任意长度套索点的简单标量 Property。彻底删除 JSON 需要全局状态、令牌或额外 PropertyGroup,复杂度高于这次轻量编码。`to_json()` / `from_json()` 因此保留,但只服务 Cutout Pie Menu 到 Shape Operator 的传值,不再属于通用用户圈选主链。
### 5. 用阶段边界和调用次数验证
交互测试替换像素读取与 `rasterize_selection_path` 入口,验证 Lasso、双击和 Pie Menu 构建阶段调用次数均为零;点击 Shape 或提交 Image Tool 后,每次实际操作各调用一次。结果等价性继续由现有 Image 编辑、Selection 与 Cutout 几何测试覆盖,不设置依赖机器性能的耗时阈值。
## Risks / Trade-offs
- [空 Selection 反馈推迟到点击 Shape 后] → 保留 Path 结构的即时验证,仅推迟必须读取整图的可见性检查。
- [helper 继续隐式读取像素导致单次处理失效] → 删除隐式回退参数并迁移全部调用方,以调用次数测试固定边界。
- [Path JSON 在菜单按钮间复制] → 只编码一次并复用同一个字符串;数据量仅与手势点数相关,远小于 RGBA 和 Mask。
- [Image Tool 的私有 Path 状态被 `execute` 重入] → 把实际图片编辑拆为显式接收 Selection Path 的业务方法,测试直接覆盖该方法,不依赖 Operator 重建后的临时属性。
## Migration Plan
1. 将共享手势完成边界改为传递内存 `SelectionPath`,让 Image Tool 直接进入实际编辑方法,删除其 JSON Property。
2. 调整 Cutout 提交与 `open_shape_pie`,仅在创建 Shape 按钮时编码一次 Path,并由 `CutoutSelectionToShape` 接收。
3. 删除菜单前 Selection 像素验证,让 `CutoutSelectionToShape.execute` 成为唯一 Path-to-Mask 边界。
4. 调整 Image Tool 与共享 helper,显式传递并复用 RGBA 和 Selection Mask。
5. 更新交互、调用次数、结果等价性测试和内部文档。
6. 运行相关测试与全部测试;若回退,整体恢复原调用链,不保留双路径或兼容层。

View File

@@ -0,0 +1,28 @@
## Why
高清图片提交 Image Tool 或 Cutout 选区时,当前主线程会重复读取整图像素、栅格化 Selection 并扫描 Alpha;Cutout 甚至在 Shape Pie Menu 打开前就完成一轮像素处理,导致未启用 AI 也出现明显卡顿。选区数据应在用户真正提交操作后只生成一次,并由后续步骤直接复用。
## What Changes
- Image Tool 与 Cutout Tool 复用同一条 Viewport 手势主链,只负责从手势构建一次内存中的 `SelectionPath`,不在用户圈选阶段执行 JSON 往返、读取图片或构建 `SelectionMask`。
- Image Tool 由同一个 Operator 在实际图片编辑阶段直接消费内存中的 `SelectionPath`,删除 `selection_path_json` Property,并在该阶段才构建一次 `SelectionMask`。
- Cutout 手势完成和双击全选将内存中的 `SelectionPath` 交给 Shape Pie Menu;菜单仅为跨 Operator 传值调用一次 `to_json()`,`CutoutSelectionToShape` 从按钮 Property 接收该值,不引入令牌、缓存或额外 Selection 生命周期。
- Cutout 在用户选定 Shape、实际执行生成时才把 `SelectionPath` 转换为一次 `SelectionMask`,并复用结果完成可见性检查、本地 Shape 创建或 AI 输入准备。
- 增加调用次数与高分辨率选区测试,确保菜单前零像素处理、提交后单次处理,并同步内部文档。
## Capabilities
### New Capabilities
- `image-selection-processing`: 规定 Image Tool 与 Cutout 对 Selection Path、Selection Mask 和源图像素的单次处理时机与复用行为。
### Modified Capabilities
无。
## Impact
- 影响 `common/selection.py`、`common/image.py`、Image Tool、Cutout Tool 的交互与转换主链,以及相关测试和内部文档。
- Image Tool 与 Cutout 圈选 Operator 不再保存 `selection_path_json`;Cutout Pie Menu 到 `CutoutSelectionToShape` 的跨 Operator 边界继续通过 String Property 传递 Selection Path JSON。
- 不改变用户界面、Shape 候选项、AI Job 协议或最终图像与几何结果。
- 不新增依赖;Image Tool 与圈选 Operator 的旧 Selection JSON 路径不保留兼容入口,Cutout Shape Operator 所需的跨 Operator JSON 传值继续保留。

View File

@@ -0,0 +1,75 @@
## Purpose
规定 Image Tool 与 Cutout 先共享构建轻量 Selection Path,并只在图片编辑或 Shape 生成的执行阶段将其转换为一次 Selection Mask,使高分辨率源图避免重复处理。
## ADDED Requirements
### Requirement: Image editing prepares one Selection
系统 SHALL 在一次 Image Tool 编辑中只读取一次源图 RGBA 并只将 Selection Path 栅格化为一次 Selection Mask,后续本地编辑或 AI 输入准备 MUST 复用该次处理结果。
#### Scenario: User commits a local image edit
- **WHEN** 用户完成 Image Tool 手势且未启用 Refine Selection
- **THEN** 系统使用同一次源图读取和同一个 Selection Mask 完成 Alpha 编辑、裁剪与结果创建
#### Scenario: User commits a refined image edit
- **WHEN** 用户完成 Image Tool 手势且启用 Refine Selection
- **THEN** 系统使用同一次源图读取和同一个 Selection Mask 准备 AI 输入,不为输入导出重复读取或栅格化
### Requirement: Tools share Selection Path construction
系统 SHALL 让 Image Tool 与 Cutout Tool 复用同一条手势主链构建一次内存中的 Selection Path,用户圈选阶段不得为了工具执行而构建 Selection Mask。
#### Scenario: Image Tool gesture completes
- **WHEN** 用户完成任一 Image Tool 手势
- **THEN** 同一个 Operator 将内存中的 Selection Path 直接交给图片编辑执行阶段,不执行 JSON 编解码
#### Scenario: Cutout gesture completes
- **WHEN** 用户完成 Cutout Lasso 或双击全选
- **THEN** 手势主链将内存中的 Selection Path 交给 Shape Pie Menu,且尚未构建 Selection Mask
### Requirement: Selection Path serialization is limited to the Cutout operator boundary
系统 SHALL 仅在 Shape Pie Menu 向 `CutoutSelectionToShape` 传递任意长度 Selection Path 时,将路径编码为一次可由 Blender Operator Property 保存的 JSON 值。
#### Scenario: Cutout Shape buttons are built
- **WHEN** Shape Pie Menu 为当前 Selection Path 创建 Shape 按钮
- **THEN** 菜单将该 Path 编码一次,并把同一个值直接设置给各 `CutoutSelectionToShape` 按钮
#### Scenario: Cutout Shape operation starts
- **WHEN** 用户点击 Shape 按钮
- **THEN** `CutoutSelectionToShape` 从自身 Property 取得并解析 Selection Path,且不依赖令牌、全局状态或 Selection 缓存
### Requirement: Cutout defers Selection Mask construction until Shape selection
系统 SHALL 在 Cutout Shape Pie Menu 打开前只构建并传递轻量的 Selection Path,不得读取源图像素、栅格化 Selection Mask 或扫描可见 Alpha。
#### Scenario: User finishes a Cutout lasso
- **WHEN** 用户松开 Cutout Lasso 并形成有效路径
- **THEN** 系统立即打开 Shape Pie Menu,且尚未执行全分辨率 Selection 像素处理
#### Scenario: User double-clicks a Cutout image
- **WHEN** 用户双击 Image Empty 以选择完整图片
- **THEN** 系统以完整图片的 Selection Path 打开 Shape Pie Menu,且尚未读取或扫描完整图片像素
#### Scenario: User abandons the Shape menu
- **WHEN** 用户关闭 Shape Pie Menu 而未选择 Shape
- **THEN** 系统不读取源图像素、不构建 Selection Mask,也不执行可见 Alpha 扫描
### Requirement: Selected Cutout Shape consumes one Selection
系统 SHALL 让 `CutoutSelectionToShape` 直接接收 Selection Path 的序列化值,并在用户选定 Cutout Shape 后只将其转换为一次可供本地或 AI 主链复用的 Selection Mask。
#### Scenario: User chooses a local Cutout Shape
- **WHEN** 用户选择不需要 AI 的 Surface 或 Balloon
- **THEN** 系统只读取一次源图 RGBA、栅格化一次 Selection Mask,并复用结果完成验证、颜色图和几何创建
#### Scenario: User chooses an AI Cutout Shape
- **WHEN** 用户选择需要 Refine Selection、Depth 或 Normal Map 的 Shape
- **THEN** 系统只读取一次源图 RGBA、栅格化一次 Selection Mask,并复用结果准备持久化的选区输入供后续 Job 阶段消费
#### Scenario: Selected Cutout contains no visible pixels
- **WHEN** 用户选定 Shape 后,Selection 与源图可见 Alpha 没有交集
- **THEN** 系统在该 Shape 操作中报告空 Selection,且不启动 AI Job 或创建 Cutout 对象
### Requirement: Selection optimization preserves results
系统 MUST 保持现有 Image Tool 输出边界、Cutout Shape 候选项、AI 参数和最终图像及几何语义不变。
#### Scenario: Same Selection is processed after optimization
- **WHEN** 相同源图、Selection Path 和工具设置在优化前后提交
- **THEN** 系统产生等价的 Selection 边界、Alpha 结果、AI 输入与 Cutout 几何

View File

@@ -0,0 +1,20 @@
## 1. Selection Path 共享主链
- [x] 1.1 整理 `ImageSelectionGesture` 的完成边界,使其向实际操作提交方法传递一次内存中的 `SelectionPath`,并用 Viewport 手势测试验证各类手势与双击全选的路径值不变
- [x] 1.2 删除 Image Tool 和 Cutout 圈选 Operator 的 `selection_path_json` Property,让 Image Tool 直接消费内存 Path,并用测试验证同一 Operator 内不发生 JSON 编解码
- [x] 1.3 调整 Cutout 手势与 `open_shape_pie`,仅在创建 Shape 按钮时调用一次 `to_json()`,把同一个值设置给各 `CutoutSelectionToShape`,用交互测试验证没有令牌或额外状态
- [x] 1.4 删除 Cutout Pie Menu 前的 Selection 像素验证,用 Lasso、双击与放弃菜单测试验证 Shape 选定前不读取 RGBA、不栅格化 Mask、不扫描 Alpha
## 2. 执行阶段单次构建 Selection
- [x] 2.1 将 Image Tool 实际编辑拆为显式接收 Selection Path 的业务方法,使其成为唯一 Path-to-Mask 边界,并用本地编辑与 Refine Selection 调用次数测试验证每次操作只读取和栅格化一次
- [x] 2.2 让 `CutoutSelectionToShape.execute` 直接消费 Pie Menu 传入的 Selection Path 值并成为 Cutout 唯一的 Path-to-Mask 边界,用本地与 AI Shape 测试验证每次操作只读取和栅格化一次
- [x] 2.3 将空 Selection 的可见性检查合并到实际执行阶段的唯一栅格化结果,用测试验证点击 Shape 后正确取消且不会启动 Job 或创建对象
- [x] 2.4 修改图片编辑、Selection 输入导出与 Cutout 创建 helper,要求显式接收已读取的 RGBA 和 Selection Mask,并用像素及几何测试验证不存在内部重读或重建且结果保持等价
## 3. 清理与验证
- [x] 3.1 删除 `validate_cutout_selection`、圈选 Operator 的 JSON Property 和 helper 的隐式重读入口,使用定向 `rg` 验证 `to_json()` / `from_json()` 只剩 Cutout Pie Menu 到 Shape Operator 的跨边界用途
- [x] 3.2 增加高分辨率尺寸的调用边界回归测试,验证菜单前读取与栅格化为零、实际 Image Tool 或 Cutout 操作为一次,且不使用机器相关的耗时阈值
- [x] 3.3 更新 `docs/internals` 中 Image Tool、Cutout 与共享 Selection 流程,人工核对流程图明确“构建 SelectionPath”与“执行阶段构建 SelectionMask”的边界
- [x] 3.4 运行 Image Selection、Image Tool、Cutout 交互与几何相关测试,再运行 `uv run pytest`,确认全部测试通过