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:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-01
|
||||
72
openspec/changes/process-image-selection-once/design.md
Normal file
72
openspec/changes/process-image-selection-once/design.md
Normal 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. 运行相关测试与全部测试;若回退,整体恢复原调用链,不保留双路径或兼容层。
|
||||
28
openspec/changes/process-image-selection-once/proposal.md
Normal file
28
openspec/changes/process-image-selection-once/proposal.md
Normal 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 传值继续保留。
|
||||
@@ -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 几何
|
||||
20
openspec/changes/process-image-selection-once/tasks.md
Normal file
20
openspec/changes/process-image-selection-once/tasks.md
Normal 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`,确认全部测试通过
|
||||
2
openspec/changes/rework-plane-conversion/.openspec.yaml
Normal file
2
openspec/changes/rework-plane-conversion/.openspec.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-01
|
||||
136
openspec/changes/rework-plane-conversion/design.md
Normal file
136
openspec/changes/rework-plane-conversion/design.md
Normal file
@@ -0,0 +1,136 @@
|
||||
## Context
|
||||
|
||||
Convert to Plane 当前使用 Image Empty 的局部显示边界直接创建 Mesh,再复制源对象矩阵。非居中的 `empty_image_offset` 因此被写入顶点坐标,结果外观位置正确,但对象原点不在面中心。
|
||||
|
||||
`O Image Depth Plane` 当前先让 `O Mesh Plane` 生成零厚度网格,再进行一次挤出并移动前侧。该拓扑没有沿厚度方向的中间环;运行时又把默认 `Subdivide` 写为 `0`,深度图通常没有足够顶点形成地形。它还使用与 Cutout 不一致的 `Depth Amount`、`Reference Depth` 和重复的 `Material` 输入。
|
||||
|
||||
对象创建代码已经把图像材质追加到 Mesh 第 0 个材质槽。`O Image Cutout` 直接依赖该槽,而 `O Mesh Plane` 额外通过 `Material` socket 和 Set Material 节点重复同一职责。
|
||||
|
||||
Depth Plane Job 与 Cutout Job 已共同调用 `generate_moge2_artifacts()`,但 Depth Plane 当前只请求 Z Depth。共享生成层能够从同一个 MoGe-2 `GeometryFrame` 同时写出 Z Depth 与 Tangent Normal,因此不需要第二次模型推理。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- 让普通 Plane 和 Depth Plane 以面中心为对象原点,并严格保留原显示矩形的世界空间位置。
|
||||
- 把 Depth Plane 建模为一面置换、背面固定、侧壁分层渐变的地形体块。
|
||||
- 统一 Depth Plane 与 Cutout 的 `Depth Scale`、`Base Plane Depth` 术语。
|
||||
- 让 Mesh 材质槽承担材质归属,并让纯生成的 Plane 几何保留输入 Mesh 的材质集合。
|
||||
- 让 Depth Plane 的几何置换与材质法线共享一次 MoGe-2 预测。
|
||||
- 用节点接口、求值几何和世界空间位置测试固定行为。
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- 不修改 MoGe-2 推理、Z Depth EXR、Validity Alpha 或 `depth.json` 协议。
|
||||
- 不按每张深度图归一化位移,也不让 `Thickness` 限制正向地形高度。
|
||||
- 不增加侧壁环数量、插值曲线或位移方向等新控制项。
|
||||
- 不修改 Cutout 的几何或材质行为。
|
||||
- 不合并 Cutout 与 Depth Plane 的顶层 Job 编排。
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. 通过局部重心与矩阵补偿移动对象原点
|
||||
|
||||
设 Image Empty 局部显示边界中心为 `C`、原矩阵为 `M`。新 Mesh 使用 `P - C` 作为顶点坐标,新对象矩阵使用 `M × Translation(C)`,因此:
|
||||
|
||||
```text
|
||||
(M × Translation(C)) × (P - C) = M × P
|
||||
```
|
||||
|
||||
普通 Plane 和 Depth Plane 共用同一套居中网格与放置计算,UV 仍覆盖 `0–1`。该方法不调用依赖当前选择和 Context 的 Origin Operator,也能正确保留旋转、非均匀缩放和 Image Empty 偏移。
|
||||
|
||||
不选择先创建对象再调用 `origin_set`,因为该 Operator 依赖 Blender Context,并会把稳定的坐标变换变成隐式场景操作。
|
||||
|
||||
### 2. 纯生成几何显式保留 Mesh 材质集合
|
||||
|
||||
从 `O Mesh Plane` 删除 `Material` interface socket;`O Image Depth Plane` 同步删除对应 socket 与转发。运行时继续在添加 Modifier 前执行 `mesh.materials.append(material)`。
|
||||
|
||||
Mesh Grid 和 Mesh Cube 是完全生成的几何,仅设置材质索引 `0` 不会自动保留输入 Mesh 的 Material Set。因此 `O Mesh Plane` 先在输入几何上捕获恒真匿名属性,再把输入几何与生成几何临时 Join,统一设为材质索引 `0`,最后按捕获属性删除全部输入点。输出只剩生成几何,但仍携带输入 Mesh 的材质集合。验证脚本将求值普通 Plane、厚度体块和 Depth Plane,检查求值 Mesh 保留第 `0` 个材质槽且所有面使用索引 `0`。
|
||||
|
||||
不保留隐藏 `Material` socket,因为它仍会形成第二条材质来源,并允许 Modifier 输入与对象材质槽漂移。
|
||||
|
||||
### 3. Depth Plane 直接复用 O Mesh Plane 的厚度拓扑
|
||||
|
||||
`O Image Depth Plane` 将输入 Geometry、`Subdivide` 和 `Thickness` 交给 `O Mesh Plane`。`O Mesh Plane` 的 Cube 路径已经按表面网格步长计算厚度方向分段;该计算增加最少侧壁分段,保证固定底面与可变面之间存在中间环,但不增加公开控制项。
|
||||
|
||||
`O Mesh Plane` 的体块以原平面为中心,Depth Plane 在内部沿局部 Z 平移 `Thickness / 2`,得到:
|
||||
|
||||
```text
|
||||
固定底面 z = 0
|
||||
未置换可变面 z = Thickness
|
||||
```
|
||||
|
||||
`Thickness = 0` 时保留单张置换表面,不生成重叠的背面或侧壁。
|
||||
|
||||
不继续使用单次 Extrude,因为它只生成跨越完整厚度的侧面,无法表达逐环渐变;也不复制一套 Grid/Cube 构建逻辑,避免与 `O Mesh Plane` 的纵横比分段和 UV 规则漂移。
|
||||
|
||||
### 4. 使用同一个标量场驱动正面和侧壁环
|
||||
|
||||
每个 XY 位置从 `z-depth.exr` 采样 Z Depth。隐藏的 `Uniform Scale` 把相机深度转换为对象局部距离,完整正面位移为:
|
||||
|
||||
```text
|
||||
depth = sampled_z × Uniform Scale
|
||||
full_offset = (Base Plane Depth - depth) × Depth Scale
|
||||
```
|
||||
|
||||
这使较近深度向局部正 Z 突出,较远深度向固定底面内缩,等于 `Base Plane Depth` 时位移为零。连续 Depth RGB 在 Validity 边界仍被采样,不用 Alpha 删除点,与当前 Artifact 协议保持一致。
|
||||
|
||||
体块顶点以未置换位置计算层权重:
|
||||
|
||||
```text
|
||||
layer_weight = clamp(z / Thickness, 0, 1)
|
||||
final_z = z + safe_full_offset × layer_weight
|
||||
```
|
||||
|
||||
固定底面权重为 `0`,可变面为 `1`,中间环线性递增。线性插值比 Smooth Step 更直接地表达各层置换比例,也让截面顺序容易验证。
|
||||
|
||||
### 5. 负向位移限幅,正向位移保持完整
|
||||
|
||||
`safe_full_offset` 的下限设为略大于 `-Thickness`。因为所有层使用同一限幅结果乘以单调层权重,侧壁环不会反序,可变面也不会与固定底面重合。
|
||||
|
||||
正向位移不钳制。`Thickness` 表示固定底面到未置换可变面的基础厚度;固定底面始终停在原图片平面,`Depth Scale = 0` 时实际厚度恰好等于 `Thickness`,启用正向置换后最终包围盒可以更高。
|
||||
|
||||
不把位移归一化到固定包围盒,因为这会让相同 `Depth Scale` 在不同图片上产生不同物理尺度,并破坏固定底面、基础厚度和自由 Depth Scale 的独立语义。
|
||||
|
||||
### 6. 一次性替换节点接口与默认值
|
||||
|
||||
`Depth Amount` 直接改为 `Depth Scale`,`Reference Depth` 直接改为 `Base Plane Depth`,不添加旧名称转发。创建 Depth Plane 时继续从 Metadata 初始化基础深度与 Uniform Scale,并将 Depth Plane 的 `Subdivide` 默认值设为 `6`;普通 Plane 的默认细分保持 `0`。
|
||||
|
||||
节点资产、Modifier 输入设置、验证、测试和文档在同一次变更中更新并重建,不保留旧 `.blend` 接口。
|
||||
|
||||
### 7. 一次预测同时生成几何深度与材质法线
|
||||
|
||||
Depth Plane 顶层 Job 继续负责完整图片的颜色复制和结果协议,并调用共享的 `generate_moge2_artifacts()`,同时传入:
|
||||
|
||||
```text
|
||||
depth_mode = Z
|
||||
normal_mode = TANGENT
|
||||
```
|
||||
|
||||
共享生成层只执行一次 MoGe-2 inference,并从同一个 `GeometryFrame` 写出 `z-depth.exr`、`depth.json` 和 `tangent-normal.png`。Cutout 顶层 Job 仍保留 Selection 裁切、可选 BEN2 Refine 和自身结果编排;两条工作流只共享与图片选择无关的 MoGe-2 Artifact 生成层,不把 Cutout 分支引入 Depth Plane。
|
||||
|
||||
Blender 响应阶段使用现有 Normal 图片加载入口把 `tangent-normal.png` 设为 Non-Color 并 Pack,再以 `normal_space="TANGENT"` 和 `ior=1.2` 传给 `create_image_material()`,使 Depth Plane 与 Cutout 使用相同的 Principled IOR。Normal Map 只参与材质着色,`z-depth.exr` 继续单独驱动 Geometry Nodes 置换;任一步失败时清理尚未被数据块使用的 Depth 与 Normal 图片。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [高默认细分与厚度环会增加求值几何量] → 继续使用 `O Mesh Plane` 的纵横比自适应分段,并让用户通过现有 `Subdivide` 降低精度。
|
||||
- [极端负向位移会形成非常薄的楔形侧壁] → 在固定底面前保留小间隙,并用求值测试检查层顺序和非穿透。
|
||||
- [删除 Material socket 后纯生成几何会丢失 Material Set] → 临时 Join 带匿名标记的输入几何,设置索引后删除输入点,并对普通 Plane、厚度体块、Depth Plane 和剪贴板 Plane 做真实 Blender 求值。
|
||||
- [Depth 与 Normal 分开生成会重复执行 MoGe-2] → 由共享 Artifact 生成层从同一个 `GeometryFrame` 写出两类文件,并测试 inference 只调用一次。
|
||||
- [Tangent Normal 可能未进入最终材质或使用错误色彩空间] → 检查图片为 Non-Color、已 Pack,材质 Normal Texture 使用 Tangent Space 且连接到现有 `O Image Layer` 输入。
|
||||
- [移动原点可能影响依赖旧局部坐标的外部脚本] → 这是预期的对象语义修正;世界空间几何保持不变,不提供兼容路径。
|
||||
- [节点资产文档与当前源码已有少量名称漂移] → 在本次变更中按最终接口同步更正对应 Plane 文档,不扩展到无关主题。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 先修改中心原点的 Mesh 与对象矩阵计算,并固定普通 Plane、Depth Plane 的世界空间回归测试。
|
||||
2. 删除 Plane 节点组的材质输入和运行时 Modifier 赋值,增加第 0 材质槽的求值验证。
|
||||
3. 重建 Depth Plane 节点图、接口名称、默认细分和负向限幅测试。
|
||||
4. 扩展 Depth Plane Job,让一次 MoGe-2 预测同时生成 Z Depth 与 Tangent Normal,并把 Normal 接入材质。
|
||||
5. 重建并验证 `O_AnyImage.blend`,运行完整测试,再同步 Plane 与节点资产内部文档。
|
||||
|
||||
回退必须同时回退节点构建脚本、发布 `.blend`、运行时输入名称和对应测试,避免节点接口与 Python 写入不一致。
|
||||
|
||||
## Open Questions
|
||||
|
||||
无。
|
||||
33
openspec/changes/rework-plane-conversion/proposal.md
Normal file
33
openspec/changes/rework-plane-conversion/proposal.md
Normal file
@@ -0,0 +1,33 @@
|
||||
## Why
|
||||
|
||||
Plane 转换目前保留了 Image Empty 的原点而不是生成面的中心,Depth Plane 也只有单次挤出的侧壁,默认细分不足以表达深度地形。节点组还重复承担材质赋值,导致接口和对象数据的职责不一致。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 普通 Plane 和 Depth Plane 在保持世界空间位置不变的同时,把对象原点统一放到面的中心。
|
||||
- 将 Depth Plane 重建为单面地形体块:固定底面留在原图片平面,`Thickness` 沿正法线移动置换面,中间侧壁环按厚度位置渐变置换。
|
||||
- 明确 `Thickness` 为基础厚度:`Depth Scale` 为零时,体块厚度等于 `Thickness`。
|
||||
- 让 `Base Plane Depth` 决定深度图的零位移位置,并限制负向置换不穿过固定底面。
|
||||
- **BREAKING**:将 Depth Plane 的 `Depth Amount` 改为 `Depth Scale`,将 `Reference Depth` 改为 `Base Plane Depth`,不保留旧接口。
|
||||
- **BREAKING**:从 `O Mesh Plane` 和 `O Image Depth Plane` 删除 `Material` socket;`O Mesh Plane` 在内部保留输入 Mesh 的材质集合,生成面统一使用第 `0` 个材质槽。
|
||||
- Depth Plane 通过一次共享的 MoGe-2 预测同时生成 `z-depth.exr`、`depth.json` 和 `tangent-normal.png`,并把 Tangent Normal 接入图像材质。
|
||||
- Depth Plane 图像材质与 Cutout 统一使用 Principled IOR `1.2`。
|
||||
- 将 Depth Plane 的默认 `Subdivide` 设为 `6`,在地形细节和求值几何量之间保持平衡。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `plane-conversion`: 规定 Plane 转换的中心原点、材质职责,以及 Depth Plane 地形体块的几何和控制语义。
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
无。
|
||||
|
||||
## Impact
|
||||
|
||||
- 影响 Plane 转换与 Depth Plane 结果创建;删除 `Material` socket 同时简化剪贴板 Plane 的 Modifier 输入。
|
||||
- 扩展 Depth Plane Job 的返回产物与 Blender 端材质创建,但继续复用 Cutout 已使用的通用 MoGe-2 Artifact 生成层。
|
||||
- 重建 `O Mesh Plane` 与 `O Image Depth Plane` 节点资产、验证脚本和发布 `.blend`。
|
||||
- 更新节点组接口、运行时 Modifier 输入、Blender 求值测试和内部文档。
|
||||
- 不改变 `z-depth.exr`、Depth Metadata 或 Cutout Job 协议。
|
||||
@@ -0,0 +1,109 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Converted planes use a centered origin
|
||||
系统 SHALL 让 Convert to Plane 和 Convert to Depth Plane 的结果对象以生成矩形面的中心为对象原点,同时保持源 Image Empty 所表示矩形的世界空间位置不变。
|
||||
|
||||
#### Scenario: Convert an offset Image Empty
|
||||
- **WHEN** 用户转换一个 `empty_image_offset` 不以原点为中心的 Image Empty
|
||||
- **THEN** 结果对象原点位于矩形面的中心
|
||||
- **AND** 结果面的四个世界空间角点与转换前 Image Empty 的显示范围一致
|
||||
|
||||
#### Scenario: Preserve transformed placement
|
||||
- **WHEN** 源 Image Empty 带有平移、旋转或缩放
|
||||
- **THEN** 普通 Plane 和 Depth Plane 均通过对象矩阵保留这些世界空间变换
|
||||
- **AND** 结果 Mesh 顶点以对象原点为中心分布
|
||||
|
||||
### Requirement: Plane node groups retain the Mesh material set
|
||||
`O Mesh Plane` 和 `O Image Depth Plane` SHALL 不公开 `Material` socket。创建对象的代码 SHALL 把图像材质放入 Mesh 的第 `0` 个材质槽;`O Mesh Plane` SHALL 在删除输入几何前把其材质集合传递给纯生成几何,并让生成面使用材质索引 `0`。
|
||||
|
||||
#### Scenario: Evaluate a generated Plane
|
||||
- **WHEN** 带有第 0 个材质槽的对象通过 `O Mesh Plane` 生成平面或体块
|
||||
- **THEN** 求值后的 Mesh 保留该材质槽
|
||||
- **AND** 所有面使用材质索引 `0`
|
||||
- **AND** Modifier 不包含 `Material` 输入
|
||||
|
||||
#### Scenario: Evaluate a generated Depth Plane
|
||||
- **WHEN** 带有第 0 个材质槽的对象通过 `O Image Depth Plane` 生成地形体块
|
||||
- **THEN** 求值后的 Mesh 保留该材质槽
|
||||
- **AND** 所有面使用材质索引 `0`
|
||||
- **AND** `O Image Depth Plane` 不向 `O Mesh Plane` 传递材质
|
||||
|
||||
### Requirement: Depth Plane exposes unified depth controls
|
||||
`O Image Depth Plane` SHALL 使用 `Thickness`、`Depth Scale`、`Base Plane Depth` 和 `Subdivide` 作为用户控制项。`Depth Image` 与 `Uniform Scale` SHALL 继续作为隐藏数据输入。旧的 `Depth Amount`、`Reference Depth` 和 `Material` 输入 SHALL 不再存在。
|
||||
|
||||
#### Scenario: Create a Depth Plane modifier
|
||||
- **WHEN** Convert to Depth Plane 创建 Modifier
|
||||
- **THEN** `Depth Scale` 默认值为 `1.0`
|
||||
- **AND** `Base Plane Depth` 取当前 Depth Metadata 的参考深度乘以 `Uniform Scale`
|
||||
- **AND** `Subdivide` 默认值为 `6`
|
||||
|
||||
#### Scenario: Inspect the node group interface
|
||||
- **WHEN** 加载发布资产中的 `O Image Depth Plane`
|
||||
- **THEN** 输入接口包含 `Geometry`、`Subdivide`、`Thickness`、`Depth Scale`、`Base Plane Depth`、`Uniform Scale` 和 `Depth Image`
|
||||
- **AND** 输入接口不包含 `Depth Amount`、`Reference Depth` 或 `Material`
|
||||
|
||||
### Requirement: Depth Plane shares one prediction for depth and material normal
|
||||
系统 SHALL 让 Depth Plane 通过通用 MoGe-2 Artifact 生成层执行一次模型预测,并从同一个 `GeometryFrame` 生成 `z-depth.exr`、`depth.json` 与 `tangent-normal.png`。Blender SHALL 将 Tangent Normal 作为 Non-Color 图片加载、Pack 并接入结果材质;Normal Map SHALL 只影响材质着色,不参与 Geometry Nodes 置换。
|
||||
|
||||
#### Scenario: Generate Depth Plane artifacts
|
||||
- **WHEN** Depth Plane Job 处理一张完整图片
|
||||
- **THEN** MoGe-2 inference 只执行一次
|
||||
- **AND** Job 返回 `z_depth`、`depth_metadata` 和 `tangent_normal`
|
||||
|
||||
#### Scenario: Create the Depth Plane material
|
||||
- **WHEN** Blender 接收成功的 Depth Plane Job 结果
|
||||
- **THEN** `tangent-normal.png` 以 Non-Color Tangent Space Normal 加载并 Pack
|
||||
- **AND** 结果材质的 Normal 输入使用该图片
|
||||
- **AND** 结果材质的 Principled IOR 为 `1.2`
|
||||
- **AND** `z-depth.exr` 继续单独驱动 Depth Plane 几何置换
|
||||
|
||||
#### Scenario: Keep workflow-specific job orchestration
|
||||
- **WHEN** Depth Plane 与 Cutout 请求 MoGe-2 产物
|
||||
- **THEN** 两者复用同一个通用 Artifact 生成层
|
||||
- **AND** Depth Plane Job 不承担 Cutout 的 Selection、Refine Selection 或裁切结果协议
|
||||
|
||||
### Requirement: Depth Plane is a single-sided terrain block
|
||||
系统 SHALL 从 `O Mesh Plane` 的有厚度几何生成 Depth Plane。固定底面 SHALL 位于原图片平面,未置换的可变面 SHALL 位于其正法线方向的 `Thickness` 距离处;只有可变面完整应用深度置换,固定底面 SHALL 完全不动,侧壁中间环 SHALL 按其厚度位置线性渐变置换。
|
||||
|
||||
#### Scenario: Disable depth displacement
|
||||
- **WHEN** `Depth Scale` 为 `0`
|
||||
- **THEN** 固定底面保持在原图片平面
|
||||
- **AND** 可变面位于正法线方向的 `Thickness` 距离处
|
||||
|
||||
#### Scenario: Change depth displacement
|
||||
- **WHEN** 用户改变 `Depth Scale` 或 `Base Plane Depth`
|
||||
- **THEN** 可变面按完整位移变化
|
||||
- **AND** 固定底面的所有顶点位置保持不变
|
||||
|
||||
#### Scenario: Interpolate side-wall rings
|
||||
- **WHEN** `Thickness` 大于 `0` 且 Depth Plane 发生置换
|
||||
- **THEN** 侧壁包含位于正面与背面之间的细分环
|
||||
- **AND** 每个环的位移按其从固定底面到可变面的归一化位置在零位移与完整位移之间线性插值
|
||||
|
||||
### Requirement: Depth displacement uses the calibrated zero plane
|
||||
系统 SHALL 以采样 Z Depth 乘以隐藏的 `Uniform Scale` 得到局部深度,并以 `Base Plane Depth` 为零位移位置。比零平面更近的采样 SHALL 向正法线方向置换,比零平面更远的采样 SHALL 向固定底面置换。
|
||||
|
||||
#### Scenario: Sample the base depth
|
||||
- **WHEN** 某个正面顶点采样的缩放深度等于 `Base Plane Depth`
|
||||
- **THEN** 该顶点的深度位移为零
|
||||
|
||||
#### Scenario: Sample a nearer depth
|
||||
- **WHEN** 某个正面顶点采样的缩放深度小于 `Base Plane Depth`
|
||||
- **THEN** 该顶点沿正法线方向产生与 `Depth Scale` 成比例的位移
|
||||
|
||||
#### Scenario: Sample a farther depth
|
||||
- **WHEN** 某个正面顶点采样的缩放深度大于 `Base Plane Depth`
|
||||
- **THEN** 该顶点向固定底面产生与 `Depth Scale` 成比例的位移
|
||||
|
||||
### Requirement: Inward displacement does not cross the fixed base
|
||||
系统 SHALL 把负向可变面位移限制在固定底面之前,并保留非零间隙以避免可变面、侧壁环和底面折叠或相交。正向位移 SHALL 不受 `Thickness` 限制,因此最终包围盒深度可以大于基础厚度。
|
||||
|
||||
#### Scenario: Excessive inward displacement
|
||||
- **WHEN** 深度公式产生绝对值大于或等于 `Thickness` 的负向位移
|
||||
- **THEN** 可变面停留在固定底面之前
|
||||
- **AND** 所有侧壁环仍按顺序位于固定底面与可变面之间
|
||||
|
||||
#### Scenario: Outward displacement exceeds the base thickness
|
||||
- **WHEN** 正向深度位移大于 `Thickness`
|
||||
- **THEN** 系统保留完整正向位移
|
||||
- **AND** 固定底面仍位于原图片平面
|
||||
43
openspec/changes/rework-plane-conversion/tasks.md
Normal file
43
openspec/changes/rework-plane-conversion/tasks.md
Normal file
@@ -0,0 +1,43 @@
|
||||
## 1. Center Plane Origins
|
||||
|
||||
- [x] 1.1 Refactor the shared Plane mesh placement so centered local vertices and the compensated object matrix preserve the source Image Empty rectangle in world space.
|
||||
- [x] 1.2 Apply the centered placement to Convert to Plane and Convert to Depth Plane without changing their UV range, selection replacement, names, or custom shape markers.
|
||||
- [x] 1.3 Add Blender tests for offset Image Empty bounds and transformed sources, verifying centered origins and unchanged world-space corners for both conversion paths.
|
||||
|
||||
## 2. Unify Plane Material Ownership
|
||||
|
||||
- [x] 2.1 Remove the `Material` interface, Group Input, and Set Material node from `O Mesh Plane`, leaving generated faces on material index `0`.
|
||||
- [x] 2.2 Remove the `Material` interface and forwarding from `O Image Depth Plane`, and simplify runtime Modifier input assignment while keeping image materials in Mesh slot `0`.
|
||||
- [x] 2.3 Update Plane, thickness block, Depth Plane, and clipboard Plane validations to prove evaluated faces resolve the Mesh slot `0` material without a Modifier material input.
|
||||
|
||||
## 3. Rebuild Depth Plane Geometry
|
||||
|
||||
- [x] 3.1 Replace the single-extrude Depth Plane graph with the `O Mesh Plane` thickness topology positioned from the original image plane toward the positive-normal displaced face.
|
||||
- [x] 3.2 Implement Z Depth sampling, `Base Plane Depth` zero calibration, `Depth Scale`, and linear layer weighting from the fixed base to the fully displaced face.
|
||||
- [x] 3.3 Clamp inward displacement before the fixed base with a nonzero safety gap, preserve unrestricted outward displacement, and retain a single displaced surface when Thickness is zero.
|
||||
- [x] 3.4 Ensure positive Thickness produces adaptive intermediate side-wall rings with a minimum useful ring count and stable UV/material indices.
|
||||
|
||||
## 4. Replace Interfaces and Defaults
|
||||
|
||||
- [x] 4.1 Replace `Depth Amount` with `Depth Scale` and `Reference Depth` with `Base Plane Depth` across the node builder, runtime Modifier inputs, validators, and tests without compatibility aliases.
|
||||
- [x] 4.2 Set Convert to Depth Plane and its generated job handoff to default `Subdivide` to `6`, while keeping ordinary Plane default `0` and Metadata-derived hidden data unchanged.
|
||||
- [x] 4.3 Add evaluated-geometry tests for zero Depth Scale thickness, fixed base vertices, zero-plane calibration, displacement direction and scale, side-ring interpolation, negative non-crossing, and outward displacement beyond the base thickness.
|
||||
|
||||
## 5. Rebuild Assets and Documentation
|
||||
|
||||
- [x] 5.1 Update node interface and layout validations, then rebuild `src/anyimage/assets/O_AnyImage.blend` with the final `O Mesh Plane` and `O Image Depth Plane` graphs.
|
||||
- [x] 5.2 Update the Plane conversion and node asset internal documentation to describe centered origins, Mesh-slot material ownership, terrain-block topology, and the final control names.
|
||||
- [x] 5.3 Run the node asset build pipeline and complete test suite, fixing any interface, topology, packaging, registration, or documentation consistency failures.
|
||||
|
||||
## 6. Correct Material Transfer and Thickness Anchor
|
||||
|
||||
- [x] 6.1 Preserve the source Mesh material set in generated `O Mesh Plane` geometry by joining a captured source marker, setting material index `0`, and deleting the source geometry.
|
||||
- [x] 6.2 Anchor the fixed Depth Plane base at the original image plane so `Thickness` only moves the displaced face and intermediate side-wall rings.
|
||||
- [x] 6.3 Update evaluated-geometry tests, OpenSpec artifacts, and documentation, then rebuild the node asset and run the complete validation suite.
|
||||
|
||||
## 7. Add Depth Plane Material Normal
|
||||
|
||||
- [x] 7.1 Make the Depth Plane Job request Z Depth and Tangent Normal from one shared `generate_moge2_artifacts()` call, returning `z_depth`, `depth_metadata`, and `tangent_normal` without a second inference.
|
||||
- [x] 7.2 Load and Pack the Tangent Normal as Non-Color data, connect it to the Depth Plane image material in Tangent Space, and clean up unused Depth and Normal images on failure.
|
||||
- [x] 7.3 Add server and Blender tests for the combined artifacts, material node connection, default `Subdivide` value, and failure cleanup; update documentation, rebuild the node asset, and run the complete validation suite.
|
||||
- [x] 7.4 Set the Depth Plane image material Principled IOR to `1.2`, matching Cutout, and update its response test and documentation.
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-01
|
||||
@@ -0,0 +1,82 @@
|
||||
## Context
|
||||
|
||||
`rasterize_selection_path()` 当前同时完成路径填充、抗锯齿、源 Alpha 相交、空内容判断和内容 bounds 收紧。调用方因此必须先通过 Blender `Image.pixels` 取得 RGBA,导致 `SelectionMask` 随图片透明度变化,而不是稳定表达用户圈选。
|
||||
|
||||
Image Tool 与 Cutout 的实际业务确实需要图片内容,但目的不同:本地编辑要把 Selection 乘入源 Alpha,Cutout 几何要用内容 Mask,AI 操作要把局部图片写成 Job 输入。需要 AI 时,现有代码先把 Lasso Mask 乘入临时 RGBA 并编码 PNG;响应后又读取源图并新建颜色 Image,造成同一操作中的第二次大图编码。实测 6288×5920 RGBA PNG 的 Selection 栅格化约 0.17–0.55 秒,提交前 PNG 保存约 1.79 秒,新建颜色 Image 的 Pack 约 1.74 秒。图片编码成本不属于 Selection,并且本设计接受生成新图片所需的单次编码成本。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- 让 SelectionPath 与 SelectionMask 只表达用户圈选几何,不受源图片 Alpha 或颜色影响。
|
||||
- 让 Image Tool、Cutout 与 AI 输入准备在各自业务边界显式消费源 Alpha 或 RGBA。
|
||||
- 所有 AI 操作统一使用 Selection 几何 bounds 内、未乘 Lasso Mask 的矩形局部 RGBA 文件。
|
||||
- 复用提交前输入或 Refine 输出的已有编码图片,避免 AI 响应阶段重新构建并编码相同颜色图。
|
||||
- 保持 non-zero winding、Invert、抗锯齿和局部画布语义。
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- 不优化生成新图片时不可避免的 PNG 编码、Pack 或磁盘写入耗时。
|
||||
- 不按 FILE、Packed、Generated 或 Dirty 来源建立不同的 Operator 主链。
|
||||
- 不引入 Pillow、GPU readback、后台 Blender 数据访问或新的运行时依赖。
|
||||
- 不改变 SelectionPath JSON、AI 模型、Cutout Shape 算法或 Job 进程边界。
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Selection 栅格化只产生几何 Mask
|
||||
|
||||
`rasterize_selection_path(image_size, selection_path, *, antialias=False)` 只根据路径、Invert、图片尺寸和抗锯齿生成 `SelectionMask(values, bounds)`。删除 `source_alpha` 与 `alpha_threshold` 参数,栅格器不导入 Blender Image helper,也不执行内容可见性检查。
|
||||
|
||||
普通 Selection 的 bounds 来自路径与抗锯齿 padding 裁切到图片范围;Invert 仍使用整张图片。相同 Path、尺寸和抗锯齿设置必须在透明、半透明与不透明图片上得到完全相同的 SelectionMask。
|
||||
|
||||
相比保留可选 Alpha 参数,彻底删除内容入口可以固定职责并阻止调用方继续把内容 bounds 误当成 Selection bounds;不保留兼容调用方式。
|
||||
|
||||
### 2. 图片业务直接合并局部 Alpha 与 Selection 值
|
||||
|
||||
Image Tool 在 `SelectionMask.bounds` 内裁切源 RGBA,并直接执行 `local_alpha *= selection_values`。最终内容 bounds、空内容和结果图片创建都属于 Image 编辑业务。实现应在已有局部数组上计算行列可见范围,不为每个可见像素生成坐标数组,也不把内容相交结果写回 SelectionMask。
|
||||
|
||||
本地 Cutout 在构建 BaseShape 时显式组合局部源 Alpha 与 Selection 值;颜色、几何和空内容判断使用该业务结果。SelectionMask 本身在调用链中保持不变。相比新增第二种与 SelectionMask 同形的公共值类型,业务直接消费两个一通道局部数组更简单,也避免把图片内容重新包装成“Selection”。
|
||||
|
||||
### 3. AI 输入是 Selection bounds 的原始矩形 RGBA
|
||||
|
||||
将当前“Selection 输入”改为“Bounds 图片输入”:公共图片 helper 接收源 RGBA 与几何 bounds,只裁切并写出矩形 RGBA,不把 SelectionMask 乘入 Alpha。Image Tool、Cutout Refine、Normal 与 Depth 都调用同一个入口;来源已经统一为 Blender Image,不在 Operator 中按文件、Packed 或生成图片分流。
|
||||
|
||||
Refine 把 Lasso 当作上下文范围。BEN2 可以在该矩形内保留 Lasso 外的内容,其输出 Alpha 是 Refine 业务结果,不再与原始 Lasso Mask 相交。Normal 或 Depth 也消费同一矩形输入,最终 Shape 仍由本地 Selection 几何约束。
|
||||
|
||||
Invert Selection 的几何 bounds 是全图,因此其 AI 输入自然退化为全图矩形,不增加特殊协议。
|
||||
|
||||
### 4. 已编码的 AI 图片直接成为颜色资产
|
||||
|
||||
AI 响应选择一个已有文件作为最终局部颜色图:启用 Refine 时使用 BEN2 输出;仅生成 Normal 或 Depth 时使用提交前的 Bounds 输入。Blender 直接加载该文件,继承源颜色空间和 Alpha 约定,Pack 已编码文件后交给 Image Empty 或 Cutout 材质。
|
||||
|
||||
Image Tool Refine 直接按响应 bounds 替换源 Image Empty,不再提取输出 Alpha、重新读取源 RGBA、创建 Pixel Result Image。Cutout 从同一个局部文件读取需要的 Alpha 构建几何,并把该已加载 Image 用作颜色纹理,不再调用 `_cropped_color_image()` 重建同一颜色内容。
|
||||
|
||||
输入临时文件必须保持到响应完成且 Blender 已加载并 Pack;随后由现有 Job 清理生命周期删除。相比继续从原图重建,这一方案保持颜色来源一致,并把已有文件 Pack 从大图重新编码变为文件收纳。
|
||||
|
||||
### 5. 内容错误由消费业务报告
|
||||
|
||||
纯 Selection 只在路径与图片范围没有几何交集时报告空 Selection。源 Alpha 全透明、Image 编辑结果为空、Refine 输出为空或 Cutout 内容 Mask 为空,分别由执行该内容操作的业务报告。这样错误发生位置与实际失败职责一致。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Refine 结果扩展到 Lasso 外] → 这是明确接受的产品语义;测试固定 Lasso 只提供矩形上下文,不作为 Refine 硬 Mask。
|
||||
- [Selection bounds 不再等于内容 bounds] → 在类型和文档中只把 SelectionMask bounds 称为几何 bounds,内容业务独立计算并传递 placement bounds。
|
||||
- [AI 输入矩形包含额外背景] → 最终本地 Shape 仍受 Selection 几何约束;Refine 则有意允许模型利用并保留矩形上下文。
|
||||
- [复用 PNG 改变颜色空间或 Alpha 配置] → 加载后显式继承源 Image 的颜色空间与 Alpha mode,并用结果等价测试覆盖。
|
||||
- [临时文件被过早清理] → 保持现有 Job cleanup 边界,只有成功加载并 Pack 或操作取消后才删除输入。
|
||||
- [全图或 Invert 仍需处理大图] → 接受单次新图片编码成本,本次只删除 Selection 的内容依赖和 AI 响应阶段的重复编码。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 修改 Selection 栅格 API 与测试,使其只产生几何 Mask,并迁移所有调用方。
|
||||
2. 将 Image Tool 与本地 Cutout 的源 Alpha 合并、内容 bounds 和空内容检查放入各自业务阶段。
|
||||
3. 将 `prepare_selection_input` 替换为统一的 Bounds 矩形 RGBA 输入,并更新 AI Job 输入语义和测试。
|
||||
4. 调整 Image Tool 与 Cutout AI 响应,直接加载并 Pack 输入或 Refine 输出,删除源图重读和颜色 Image 重建。
|
||||
5. 更新内部文档,运行相关测试、完整测试和真实高分辨率阶段基准。
|
||||
|
||||
回退时整体恢复旧栅格参数、Masked AI 输入和响应重建链,不保留双入口或兼容层。
|
||||
|
||||
## Open Questions
|
||||
|
||||
无。
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
## Why
|
||||
|
||||
当前 `SelectionPath → SelectionMask` 在栅格化阶段读取并合并源图 Alpha,使选区几何依赖图片内容,也迫使 Image Tool 与 Cutout 在业务开始前读取整图。Selection 应客观表达用户圈选;源 Alpha、图片裁切、AI 输入和最终图片持久化应由实际业务分别处理。
|
||||
|
||||
## What Changes
|
||||
|
||||
- **BREAKING**:Selection 栅格化只接收图片尺寸与 `SelectionPath`,删除源 Alpha、Alpha threshold 及内容可见性职责;`SelectionMask` 只保存 non-zero winding、Invert 与抗锯齿产生的几何值和几何 bounds。
|
||||
- Image Tool 与本地 Cutout 在实际操作中显式将局部源 Alpha 与 `SelectionMask.values` 相乘,并在需要时计算内容 bounds;公共 Selection 模块不读取 Blender Image。
|
||||
- 需要 AI 的 Image Tool 与 Cutout 统一按几何 bounds 写出一份未应用 Lasso Mask 的矩形局部 RGBA 文件。Refine 将 Lasso 作为上下文范围,允许 BEN2 结果保留矩形内、Lasso 外的内容。
|
||||
- AI 响应复用已经编码的局部图片:Refine 直接使用 BEN2 输出,只有 Normal 或 Depth 时直接使用提交前输入;不再重新读取源图、重建同一颜色 Image 后再次编码 Pack。
|
||||
- 删除由 Selection 栅格化隐式承担的空内容判断,改由消费源 Alpha 或 AI 结果的具体业务报告空内容。
|
||||
- 增加 Selection 纯几何等价、Alpha 合并边界、矩形 AI 输入和编码图片复用测试,并同步内部文档。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `selection-content-processing`: 规定纯几何 Selection Mask、业务侧源 Alpha 合并、Bounds 矩形 AI 输入及已有编码图片复用行为。
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
无。
|
||||
|
||||
## Impact
|
||||
|
||||
- 影响 `common/selection.py`、`common/image.py`、Image Tool、Cutout Tool、Cutout 颜色图创建、AI 输入/响应处理及相关测试和内部文档。
|
||||
- `rasterize_selection_path()` 的内部调用协议发生变化,不保留带 `source_alpha` 或 `alpha_threshold` 的兼容入口。
|
||||
- Cutout 与 Image Tool 的 AI 输入 Alpha 语义改变:输入为几何 bounds 内的原始矩形 RGBA,而不是预先乘入 Lasso Mask 的 RGBA。
|
||||
- 不新增依赖,不改变 SelectionPath JSON、填充规则、抗锯齿核、AI 模型或 Job Server 进程边界;不以本次变更优化不可避免的新图片编码耗时。
|
||||
@@ -0,0 +1,72 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Selection Mask is independent of image content
|
||||
系统 SHALL 仅根据 Selection Path、Invert、图片尺寸与抗锯齿设置栅格化 Selection Mask,并且 Selection 栅格化不得读取、接收或合并源图片 Alpha、颜色或 Alpha threshold。
|
||||
|
||||
#### Scenario: Images have different Alpha
|
||||
- **WHEN** 相同 Selection Path、图片尺寸与抗锯齿设置用于透明度内容不同的图片
|
||||
- **THEN** 系统产生逐值相同且 bounds 相同的 Selection Mask
|
||||
|
||||
#### Scenario: Selection is inverted
|
||||
- **WHEN** Selection Path 启用 Invert
|
||||
- **THEN** 系统在完整图片画布上产生路径外的纯几何 Selection Mask,而不检查源图片可见内容
|
||||
|
||||
### Requirement: Image businesses combine source Alpha explicitly
|
||||
系统 SHALL 由 Image Tool 与本地 Cutout 在实际业务阶段显式组合 Selection Mask 值和对应 Bounds 内的源 Alpha;Selection Mask MUST 在组合前后保持纯几何值不变。
|
||||
|
||||
#### Scenario: Image Tool edits a Selection
|
||||
- **WHEN** 用户提交未启用 Refine 的 Image Tool Selection
|
||||
- **THEN** Image Tool 在几何 bounds 内把 Selection 值乘入源 Alpha,并由编辑业务计算内容 bounds、空结果和最终图片
|
||||
|
||||
#### Scenario: Local Cutout builds a Shape
|
||||
- **WHEN** 用户选择不需要 AI 的 Cutout Shape
|
||||
- **THEN** Cutout 在构建 Shape 时组合局部源 Alpha 与 Selection 值,并使用业务结果约束几何和判断空内容
|
||||
|
||||
#### Scenario: Source content is transparent
|
||||
- **WHEN** 几何 Selection 有效但对应源 Alpha 没有可见内容
|
||||
- **THEN** 消费该内容的 Image 或 Cutout 业务报告空内容,Selection 栅格化本身仍成功
|
||||
|
||||
### Requirement: AI receives one rectangular Bounds image
|
||||
系统 SHALL 为需要 AI 的 Image Tool 或 Cutout 按 Selection 几何 bounds 写出一份矩形局部 RGBA 文件,并且该文件 MUST 保留 bounds 内的源 Alpha,不得预先乘入 Lasso Selection 值。
|
||||
|
||||
#### Scenario: Refine receives Lasso context
|
||||
- **WHEN** 用户对 Lasso Selection 启用 Refine
|
||||
- **THEN** BEN2 接收 Lasso 几何 bounds 对应的完整矩形 RGBA,矩形内、Lasso 外的源像素保持原样
|
||||
|
||||
#### Scenario: Normal or Depth requires AI
|
||||
- **WHEN** Cutout 仅因 Normal 或 Depth 进入 AI 流程
|
||||
- **THEN** Job 使用与 Refine 相同的 Bounds 矩形输入协议,不建立另一套 Masked Selection 输入
|
||||
|
||||
#### Scenario: Blender Image source varies
|
||||
- **WHEN** 源 Image 来自文件、Packed 数据或 Blender 内存
|
||||
- **THEN** Operator 通过同一个 Bounds 图片序列化入口产生 AI 输入,不按来源建立不同业务主链
|
||||
|
||||
### Requirement: Refine may expand within Selection Bounds
|
||||
系统 SHALL 将 Refine 的 Lasso 解释为矩形上下文范围,而不是 BEN2 输出的硬 Mask;最终 Refine Alpha SHALL 由 BEN2 在该矩形内的结果决定。
|
||||
|
||||
#### Scenario: BEN2 preserves content outside Lasso
|
||||
- **WHEN** BEN2 在 Selection bounds 内保留了 Lasso 路径外的像素
|
||||
- **THEN** 系统保留这些像素,不再与原始 Lasso Selection Mask 相交
|
||||
|
||||
#### Scenario: BEN2 returns no visible content
|
||||
- **WHEN** Refine 输出在矩形 bounds 内没有可见 Alpha
|
||||
- **THEN** Refine 业务报告空内容且不创建 Image 或 Cutout 结果
|
||||
|
||||
### Requirement: AI operations reuse encoded local images
|
||||
系统 SHALL 复用 AI 流程中已经编码的局部 RGBA 文件作为最终颜色 Image,不得重新读取完整源图并创建包含相同颜色内容的新内存 Image 后再次编码。
|
||||
|
||||
#### Scenario: Image Tool Refine completes
|
||||
- **WHEN** BEN2 返回已收紧 bounds 的 Refine RGBA 文件
|
||||
- **THEN** Image Tool 直接加载、配置并 Pack 该文件,然后按响应 bounds 替换源 Image Empty
|
||||
|
||||
#### Scenario: Cutout Refine completes
|
||||
- **WHEN** Cutout 的 BEN2 Refine 返回局部 RGBA 文件
|
||||
- **THEN** Cutout 使用该文件的 Alpha 构建结果几何,并使用同一个已加载 Image 作为颜色纹理
|
||||
|
||||
#### Scenario: Cutout generates only Normal or Depth
|
||||
- **WHEN** Cutout AI 流程没有 Refine 输出
|
||||
- **THEN** Cutout 直接加载并 Pack 提交前的 Bounds 输入作为颜色纹理
|
||||
|
||||
#### Scenario: AI operation cleans up
|
||||
- **WHEN** 已有输入或 Refine 输出成功加载并 Pack,或者操作取消
|
||||
- **THEN** 系统按 Job 生命周期清理临时文件,且 Blender 结果不再依赖临时路径
|
||||
@@ -0,0 +1,30 @@
|
||||
## 1. 纯几何 Selection
|
||||
|
||||
- [x] 1.1 删除 `rasterize_selection_path()` 的源 Alpha 与 Alpha threshold 参数,使其只按 Path、Invert、图片尺寸和抗锯齿返回几何 `SelectionMask`
|
||||
- [x] 1.2 调整 Selection 的空路径、越界、Invert、局部画布和 bounds 行为,确保栅格阶段不再承担图片内容可见性判断
|
||||
- [x] 1.3 更新 Selection 单元测试,验证不同源 Alpha 不参与接口且相同几何输入始终产生相同 values 与 bounds
|
||||
|
||||
## 2. 业务侧 Alpha 合并
|
||||
|
||||
- [x] 2.1 调整 Image Tool,在几何 bounds 内直接将 Selection 值乘入局部源 Alpha,并由编辑业务计算空内容与最终 placement bounds
|
||||
- [x] 2.2 移除 Image Tool 对已知局部结果的重复逐像素坐标扫描,使用按行列可见范围完成内容收紧
|
||||
- [x] 2.3 调整本地 Cutout,使 BaseShape、空内容与几何处理显式消费局部源 Alpha 和纯 Selection 值,不修改 SelectionMask
|
||||
- [x] 2.4 补充 Image Tool 与 Cutout 的透明、半透明、空内容、Invert 和内容 bounds 回归测试
|
||||
|
||||
## 3. 矩形 AI 输入
|
||||
|
||||
- [x] 3.1 将 `prepare_selection_input` 替换为统一的 Bounds 图片输入 helper,只裁切源 RGBA 并写出矩形局部文件,不乘入 Lasso Mask
|
||||
- [x] 3.2 迁移 Image Tool Refine 与 Cutout Refine、Normal、Depth 调用链,使其共享同一个几何 bounds 输入协议
|
||||
- [x] 3.3 更新 Server 与交互测试,验证 BEN2 可以保留 bounds 内、Lasso 外内容,Normal 与 Depth 不再使用另一套 Masked 输入
|
||||
|
||||
## 4. 复用 AI 编码图片
|
||||
|
||||
- [x] 4.1 建立加载、配置并 Pack 已编码局部 RGBA 的公共入口,继承源颜色空间、Alpha mode、命名和 placement bounds 所需信息
|
||||
- [x] 4.2 调整 Image Tool Refine 响应,直接使用 BEN2 输出替换源 Image Empty,删除输出 Alpha 提取、源 RGBA 重读和 Pixel Result Image 重建
|
||||
- [x] 4.3 调整 Cutout AI 响应:Refine 使用 BEN2 输出,只有 Normal 或 Depth 时使用提交前输入,并让同一局部 Image 同时服务几何 Alpha 与颜色材质
|
||||
- [x] 4.4 验证成功、取消与异常路径只在结果已加载并 Pack 后清理临时输入,且不残留无用户的 Blender Image
|
||||
|
||||
## 5. 文档与验证
|
||||
|
||||
- [x] 5.1 更新 Selection、Image Tool、Cutout Tool 与 AI 输出内部文档,区分几何 bounds、内容 bounds 和矩形上下文语义
|
||||
- [x] 5.2 运行相关测试与完整测试,并用真实高分辨率 RGBA 图片复测 Selection、AI 提交前准备和 AI 响应阶段,确认没有新增第二次图片编码
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-01
|
||||
78
openspec/changes/simplify-selection-rasterization/design.md
Normal file
78
openspec/changes/simplify-selection-rasterization/design.md
Normal file
@@ -0,0 +1,78 @@
|
||||
## Context
|
||||
|
||||
Selection 手势当前构建带 `points`、`invert`、`fill_rule` 和 JSON 方法的 `SelectionPath`。Image Tool 默认使用 `EVEN_ODD`,Cutout 使用 `NONZERO`;两者只在自交或重复绕行时不同。产品决定统一保留同方向重叠区域,因此删除 `EVEN_ODD`,同时保留 `SelectionPath` 及其 JSON 方法,继续把路径点和反选状态作为不可分割的值。
|
||||
|
||||
当前栅格器已经取得 Path bounds,却仍创建整图 Mask,并对每条路径边构造“扫描行块 × 选区宽度”的二维布尔数组。实测 5120×2880、1000 点 Lasso 的完整处理约为 4.07 秒。局部 non-zero scanline 原型通过每条边只记录实际穿过扫描行的区间端点,再对每行累计一次,在 960 组路径、Invert、抗锯齿、Alpha 和 bounds 组合中与目标 non-zero 结果逐值一致,同一 5K 用例约为 0.16 秒。
|
||||
|
||||
Cutout Pie Menu 的按钮会启动新的 Blender Operator,普通 Python 对象不能直接跨该 RNA 边界。现有完整 Path JSON 已同时保存 Points 与 Invert,千点路径编码实测约 0.6 ms;保留 `to_json()` / `from_json()` 比拆分多个 Property 或增加 RNA Collection 更直接。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- 所有 Selection 使用 non-zero winding,保留同方向重叠区域。
|
||||
- 运行时传递只包含普通二维点数组和 `invert` 的精简 `SelectionPath`,删除 `fill_rule`。
|
||||
- Cutout 只在 Pie Menu 到 Shape Operator 的必要边界编码一次完整 Selection Path。
|
||||
- 将 Path-to-Mask 的计算量从逐边二维像素广播改为扫描行交点累计,并尽可能限制在局部画布。
|
||||
- 保持现有像素中心、Invert、抗锯齿核、Alpha 阈值、空 Selection、最终 bounds 和下游输出语义。
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- 不缓存 Blender 图片像素或 Selection,不引入令牌和全局状态。
|
||||
- 不改变 Lasso 采样与简化容差,不增加 Pillow、OpenCV 或 GPU 路径。
|
||||
- 不优化 Blender `image.pixels.foreach_get()`、图片写回、Pack、Cutout 网格或 AI Job。
|
||||
- 不保留旧 `fill_rule` 或兼容填充分支。
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Selection Path 只保存 Points 与 Invert
|
||||
|
||||
`SelectionPath` 保留为不可变 dataclass,只包含 `points: tuple[tuple[float, float], ...]` 与 `invert: bool`,以及完整值的 `to_json()` / `from_json()`。构建时继续把点规范化为有限浮点二维数组、检查至少三个点,并固定反选值。共享手势完成边界提交一个完整 `SelectionPath`,Image Tool 在同一 Operator 调用栈内直接消费它。
|
||||
|
||||
`invert` 决定路径内部还是外部属于 Selection,与 `points` 共同定义同一个 Selection Path。保留二者的值对象及现有序列化边界可以避免调用链漏传或错配反选状态;删除 `fill_rule` 后,该类型不再承担工具填充策略。
|
||||
|
||||
### 2. Cutout 序列化完整 Selection Path
|
||||
|
||||
`open_shape_pie` 对 `selection_path` 调用一次 `to_json()`,把同一个字符串写入各 Shape Operator 的 `selection_path_json`。`CutoutSelectionToShape` 通过 `SelectionPath.from_json()` 恢复 Points 与 Invert 后交给栅格器。JSON 保留 `points` 和 `invert`,只删除 `fill_rule`。
|
||||
|
||||
相比拆分 Points JSON 与 Bool Property,此方案不会产生两个必须同步的传输值;相比 RNA Collection,它不增加 PropertyGroup、注册顺序和每按钮逐点复制;相比令牌或全局表,它没有生命周期与菜单取消清理问题。
|
||||
|
||||
### 3. 使用 non-zero scanline 端点累计
|
||||
|
||||
栅格器先裁切路径的几何 bounds,并以像素中心 `x + 0.5`、`y + 0.5` 判断边界。每条非水平边只处理其实际跨越的扫描行,计算交点对应的水平区间终点;按边方向在该行的起点和终点写入正负 winding 变化。全部边处理完成后,每行只执行一次水平累计,累计值非零的像素即为 Selection。
|
||||
|
||||
这与当前 `NONZERO` 的射线 crossing 定义相同,但不再为每条边生成二维 `selected` 数组。相比 Pillow,此实现可以明确固定 winding、严格不等号与像素中心语义,并且无需为自交行为依赖第三方未公开约定。
|
||||
|
||||
### 4. 非 Invert 使用局部画布
|
||||
|
||||
普通 Selection 只创建裁切到图片范围的 Path bounds;启用抗锯齿时在四周增加核半径 2 px,使局部卷积与整图卷积等价。源 Alpha 只切片到同一局部范围。由于 Invert 的有效区域可能覆盖路径外的整图,Invert 继续使用完整图片范围,但仍采用单次 scanline 累计。
|
||||
|
||||
最终可见 bounds 使用按行和按列的 `any()` 后查找首尾非空索引,不再通过 `np.nonzero()` 为每个命中像素创建两组坐标。返回值继续是局部 `SelectionMask(values, bounds)`。
|
||||
|
||||
### 5. 用结果等价和工作边界验证性能
|
||||
|
||||
小尺寸参考实现覆盖普通、凹、自交、重复绕行、越界、水平边、Invert、抗锯齿、Alpha 阈值和空 Selection,并逐值比较 Mask 与 bounds。高分辨率测试验证中间工作数组只按目标画布建立、每条边只产生其跨行事件,不设置依赖 CPU 的秒数断言。
|
||||
|
||||
开发验证继续记录代表性 5K/1000 点 Lasso 的基准,用于确认实际数量级改善,但不把机器耗时写成测试契约。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Image Tool 自交路径结果改变] → 这是明确的产品决策;更新预览和最终 Mask 测试,使重叠区始终保留。
|
||||
- [局部抗锯齿在边缘与旧结果不同] → 按固定核半径扩展局部画布,并用逐值测试覆盖图片边缘和越界路径。
|
||||
- [Invert 无法取得同等局部内存收益] → 保留整图画布以维持语义,但仍消除逐边二维广播和巨大 `nonzero` 坐标数组。
|
||||
- [扫描线交点在整数顶点重复计数] → 沿用半开 crossing 条件和像素中心判定,以参考实现测试水平边、共顶点与自交路径。
|
||||
- [Path JSON 仍包含已删除的填充规则] → 更新序列化测试,验证载荷只包含 Points 与 Invert,且不保留旧填充分支。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 将预览与 Mask 参考语义统一为 non-zero,调整差异测试为重叠区保留测试。
|
||||
2. 将 `SelectionPath` 精简为 Points 与 Invert,删除 `fill_rule` 并更新 JSON 载荷,迁移共享手势、Image Tool 与 Cutout 的参数边界。
|
||||
3. 保持 Cutout 的单一 Path JSON Property,由其恢复精简 Path,删除旧填充字段解析。
|
||||
4. 实现局部 scanline、局部抗锯齿/Alpha 和低内存 bounds,迁移所有调用方。
|
||||
5. 更新测试和内部文档,运行相关测试、完整测试及代表性 5K 基准。
|
||||
|
||||
回退时整体恢复旧 Selection Path 字段和栅格器,不保留两套入口或兼容层。
|
||||
|
||||
## Open Questions
|
||||
|
||||
无。
|
||||
@@ -0,0 +1,28 @@
|
||||
## Why
|
||||
|
||||
当前 Selection 栅格化按路径的每条边重复扫描二维像素区域,5K 图片上的千点 Lasso 单次处理约需 4 秒并产生大量整图临时数组。`SelectionPath` 需要共同保存路径点与反选状态,但其中的两种填充规则没有继续保留的价值;产品实际期望所有重叠圈选区域都被保留。
|
||||
|
||||
## What Changes
|
||||
|
||||
- **BREAKING**:所有 Selection 统一使用 non-zero winding 填充,删除 `EVEN_ODD`、`fill_rule` 及其分支;自交或重复绕行的同方向重叠区域始终保留。
|
||||
- **BREAKING**:将 `SelectionPath` 精简为只包含普通二维点数组和 `invert` 的不可变值,删除 `fill_rule`,保留完整 Path 的 `to_json()` / `from_json()`。
|
||||
- Cutout Pie Menu 继续把完整 `SelectionPath` 编码一次并交给 Shape Operator,不拆分 Points 与 Invert,也不引入 RNA Collection、令牌或额外状态。
|
||||
- 使用按扫描行交点累计的局部 scanline 算法替换逐边二维像素广播,并保持现有像素中心判定、Invert、抗锯齿、Alpha 阈值、空 Selection 与最终 bounds 语义。
|
||||
- 抗锯齿、源 Alpha 合并和可见 bounds 计算只处理必要的局部画布;Invert Selection 仍按完整图片范围处理。
|
||||
- 增加逐像素等价性、不同路径规模的复杂度边界和高分辨率性能回归测试,不使用机器相关的固定耗时阈值。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `selection-rasterization`: 规定精简 Selection Path 的统一 non-zero 填充语义、局部 scanline 栅格化结果和高分辨率复杂度边界。
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
无。
|
||||
|
||||
## Impact
|
||||
|
||||
- 影响 `common/selection.py`、`common/viewport.py`、Image Tool、Cutout Tool 及其交互测试和内部文档。
|
||||
- Cutout Operator 的内部 Selection 参数会改变;它不是公开用户 API,但旧 Operator 参数不再兼容。
|
||||
- 不新增运行时依赖,不修改 AI Job 协议、Cutout Shape、Mask 抗锯齿核或最终图片与几何处理链。
|
||||
@@ -0,0 +1,64 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Selection uses one overlap-preserving fill
|
||||
系统 SHALL 使用 non-zero winding 栅格化所有 Selection Path,使同方向自交或重复绕行的重叠区域保持选中,并且不得暴露可切换的填充规则。
|
||||
|
||||
#### Scenario: Lasso overlaps itself
|
||||
- **WHEN** Image Tool 或 Cutout 的 Selection 路径以同一方向重复包围一个区域
|
||||
- **THEN** 该重叠区域在 Viewport 预览和最终 Selection Mask 中均保持选中
|
||||
|
||||
#### Scenario: Simple path is selected
|
||||
- **WHEN** Selection 路径是普通矩形、半平面、凹多边形或不自交 Lasso
|
||||
- **THEN** 系统产生与既有 non-zero 填充相同的选中区域
|
||||
|
||||
### Requirement: Selection Path contains its inversion state
|
||||
系统 SHALL 使用只包含普通二维点数组和 Invert 的不可变 Selection Path 值在用户手势与实际 Image 或 Cutout 操作之间传递 Selection;该类型 SHALL 保留完整值的 JSON 编解码,但不得包含填充规则。
|
||||
|
||||
#### Scenario: Image Tool submits a Selection
|
||||
- **WHEN** 用户完成 Image Tool 手势
|
||||
- **THEN** 同一 Operator 把包含 Points 与 Invert 的 Selection Path 直接交给实际图片编辑
|
||||
|
||||
#### Scenario: Cutout menu creates Shape operations
|
||||
- **WHEN** Cutout Pie Menu 为当前点数组创建 Shape 按钮
|
||||
- **THEN** 菜单将包含 Points 与 Invert 的完整 Path 编码一次,并为每个 Shape Operator 复用同一字符串
|
||||
|
||||
#### Scenario: Cutout Shape operation restores the Selection
|
||||
- **WHEN** 用户点击任一 Cutout Shape 按钮
|
||||
- **THEN** Shape Operator 从单一 Path JSON 恢复 Points 与 Invert 后再进行栅格化
|
||||
|
||||
### Requirement: Selection rasterization uses scanline events
|
||||
系统 SHALL 通过路径边与扫描行的交点事件构建 non-zero winding Mask,每条路径边不得重复创建扫描行块乘以 Selection 宽度的二维像素比较数组。
|
||||
|
||||
#### Scenario: High-resolution Lasso is rasterized
|
||||
- **WHEN** 系统处理高分辨率图片上的多点 Lasso
|
||||
- **THEN** 每条边只为实际跨越的扫描行产生端点事件,所有端点完成后每行执行一次水平累计
|
||||
|
||||
#### Scenario: Path point count increases
|
||||
- **WHEN** Selection 画布不变而路径点数增加
|
||||
- **THEN** 栅格器增加交点事件处理,不按每个新增点重新扫描完整二维 Selection 像素区域
|
||||
|
||||
### Requirement: Non-inverted Selection processing stays local
|
||||
系统 SHALL 对非 Invert Selection 只在裁切后的路径范围内执行填充、抗锯齿、源 Alpha 合并和可见 bounds 计算;抗锯齿范围 MUST 包含固定卷积核所需的边缘 padding。
|
||||
|
||||
#### Scenario: Local Selection is rasterized
|
||||
- **WHEN** 非 Invert Selection 只覆盖源图的一部分
|
||||
- **THEN** 中间 Mask 和 Alpha 运算范围限制在路径 bounds 及必要的抗锯齿 padding
|
||||
|
||||
#### Scenario: Inverted Selection is rasterized
|
||||
- **WHEN** Selection 启用 Invert
|
||||
- **THEN** 系统使用完整图片范围表达路径外区域,但仍只累计一次 scanline 事件
|
||||
|
||||
### Requirement: Optimized rasterization preserves Mask semantics
|
||||
系统 MUST 保持现有像素中心判定、Invert、抗锯齿核、Alpha 阈值、空 Selection 警告和最终局部 Selection Mask bounds 语义。
|
||||
|
||||
#### Scenario: Selection has source Alpha
|
||||
- **WHEN** Selection Path 与源图 Alpha 一起栅格化
|
||||
- **THEN** 系统只保留 Alpha 高于阈值且 non-zero winding Mask 有效的像素,并返回其紧致 bounds
|
||||
|
||||
#### Scenario: Selection has no visible pixels
|
||||
- **WHEN** Selection 与图片范围或可见源 Alpha 没有交集
|
||||
- **THEN** 系统报告空 Selection,且不向后续 Image、Cutout 或 AI 操作提交结果
|
||||
|
||||
#### Scenario: Equivalent Selection is processed
|
||||
- **WHEN** 相同 Selection Path、源 Alpha、阈值和抗锯齿设置由参考实现与优化实现处理
|
||||
- **THEN** 两者产生逐值一致的 Mask values 和相同 bounds
|
||||
20
openspec/changes/simplify-selection-rasterization/tasks.md
Normal file
20
openspec/changes/simplify-selection-rasterization/tasks.md
Normal file
@@ -0,0 +1,20 @@
|
||||
## 1. 简化 Selection 值与填充语义
|
||||
|
||||
- [x] 1.1 将 Viewport 预览和最终 Mask 统一为 non-zero winding,删除 `EVEN_ODD`、`fill_rule` 参数与分支,并用自交和重复绕行测试验证重叠区始终保留
|
||||
- [x] 1.2 将 `SelectionPath` 精简为只包含 Points 与 Invert 的不可变值,保留完整值的 `to_json()` / `from_json()`,使共享手势向 Image Tool 和 Cutout Tool 提交完整 Path,并迁移反选相关测试
|
||||
- [x] 1.3 保持 Cutout Pie Menu 到 `CutoutSelectionToShape` 的单一 `selection_path_json` 边界,用交互测试验证只编码一次、各 Shape 按钮复用同一字符串且恢复相同 Points 与 Invert
|
||||
- [x] 1.4 删除 `fill_rule` 的序列化、解析和所有兼容入口,使用定向搜索验证业务代码与 Path JSON 均不再包含填充规则
|
||||
|
||||
## 2. 局部 Scanline 栅格化
|
||||
|
||||
- [x] 2.1 实现 non-zero scanline 端点累计,使每条边只处理实际跨越的像素中心扫描行,并用整数顶点、水平边、凹路径、自交与越界路径测试验证结果
|
||||
- [x] 2.2 对非 Invert Selection 只建立裁切后的局部画布,抗锯齿时按 5-tap 核增加 2 px padding;对 Invert 保留完整图片画布并验证路径外语义
|
||||
- [x] 2.3 将源 Alpha 合并限制到当前画布切片,并以按行、按列归约计算紧致 bounds,删除整图 `np.where` 与逐命中像素坐标数组
|
||||
- [x] 2.4 保持 `SelectionMask`、空 Selection 警告、Image 编辑、AI 输入和 Cutout 几何调用边界不变,用现有像素与几何测试验证下游结果
|
||||
|
||||
## 3. 等价性、性能边界与文档
|
||||
|
||||
- [x] 3.1 增加小尺寸参考栅格器测试,组合覆盖普通与随机路径、Invert、抗锯齿、随机 Alpha、阈值和空结果,逐值比较 Mask values 与 bounds
|
||||
- [x] 3.2 增加 5K 多点 Lasso 的工作边界测试,验证不出现逐边的扫描行块乘以宽度数组、局部 Selection 不建立整图 Mask,且不使用机器相关耗时阈值
|
||||
- [x] 3.3 更新 `docs/internals` 的 Selection 值、Cutout 传输边界、统一 non-zero 语义与局部 scanline 流程,不记录旧兼容路径
|
||||
- [x] 3.4 运行 Image Selection、Image Tool、Cutout 交互与几何相关测试,运行代表性 5K 基准,再运行 `uv run pytest` 确认全部通过
|
||||
Reference in New Issue
Block a user