feat: Update version to 0.13.0 in project files and adjust dependencies

This commit is contained in:
2026-09-02 19:50:55 +08:00
parent 8d279e9ab3
commit a6041cbe03
47 changed files with 1204 additions and 587 deletions

View File

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

View File

@@ -0,0 +1,102 @@
## Context
当前 `ImageBoxTool` 以 `group=True` 注册,Lasso、Line、Polyline 依次通过 `after` 加入同组;`ImagePerspectiveTool` 来自独立的 `perspective_tool/`,单独注册在该组之后;Cutout 再注册到 Perspective 后并带分隔。图片裁切因此在代码、工具栏和文档中分成 Image Tool 与 Perspective Tool 两套概念。
Box、Lasso、Line、Polyline 共用 `ImageSelectionGesture` 与 `EditImageMask`。Perspective 使用自己的 modal Operator,但最终同样创建裁切图片并调用 `replace_empty_image()`。Line 额外依赖共享 Viewport 中把一条直线转换为半平面多边形的分支。
Crop 选项尚未持久化。本地 Selection Crop 与 Perspective 都同步替换源对象;Box/Lasso 的 Refine Selection 在异步 Job 响应中替换源对象。Keep Original 必须覆盖四个最终 Crop 子工具的同步结果和两个可 Refine 子工具的异步结果。
Cutout 的 `BaseShape` 和 `O Image Cutout` 统一以局部 `+X` 为向内轴、YZ 为纸面;`CUTOUT_AXIS_TO_SOURCE` 把这套坐标映射到 Image Empty 的 XY 图片平面和 `-Z` 向内方向。结果世界位置正确,但 Mesh Object 的 local transform 与源 Empty 不同。Depth Surface 的 Object Space Normal 也按 `+X` 坐标编码。
## Goals / Non-Goals
**Goals:**
- 用一个 `crop_tool/` 业务边界承载 Box、Lasso、Polyline 与 Perspective,并在工具栏显示为一个四合一 Crop Tool 组。
- 彻底删除 Line 裁切和 Image / Perspective 的旧工具命名。
- 让 Crop Tool 组与 Cutout Tool 作为两个独立工具栏入口相邻。
- 分别保存四个 Crop 子工具的 Keep Original、三个 Selection Crop 子工具的 Invert 和 Box/Lasso 的 Refine Selection。
- 让 Selection Crop 从各自工具选项读取持久 Invert,不占用字母快捷键。
- 让本地和 AI Crop 共享一种 Keep Original 结果落地方式,并明确选择和 active object 状态。
- 让 Cutout 在不改变世界空间成品的前提下选择 `-Z` 或 `+X` 局部向内轴。
**Non-Goals:**
- 不为 Invert、Refine Selection、Keep Original 或 Inward Axis 增加 Crop 或 Cutout 字母快捷键。
- 不为 Crop Perspective 增加 Invert、Selection 或 Refine Selection 语义。
- 不改变 Crop 的 Selection 栅格化、Perspective 重采样、裁切 bounds 或结果图片内容。
- 不改变 Cutout 的 Shape Pie 或结果对象保留行为。
- 不把工具设置迁移为跨 `.blend` 文件的 Addon Preferences。
## Decisions
### 1. 统一迁移到 `crop_tool/` 并删除旧入口
将 Box、Lasso、Polyline、共享 Selection Crop Operator 与 Perspective 实现收拢到 `operators/crop_tool/`。WorkSpaceTool 使用 `CropBoxTool`、`CropLassoTool`、`CropPolylineTool`、`CropPerspectiveTool`,Tool ID 使用 `anyimage.crop_box`、`anyimage.crop_lasso`、`anyimage.crop_polyline`、`anyimage.crop_perspective`;Operator 和激活入口同步使用 Crop 动词与 `anyimage` 前缀。
删除 `image_tool/line.py`、`LINE` gesture、直线半平面构建函数及所有注册和文档。删除旧 `image_tool/`、`perspective_tool/` 路径、旧 Tool / Operator ID 和兼容导出。菜单中的 Image Tool 入口改为 Crop Tool,并默认激活 Crop Box。
相比只改 UI Label,这能让用户概念、源码目录、类名、公开 ID 和文档保持同一边界,符合项目不保留兼容层的约束。
### 2. 四个 Crop 子工具连续注册为一组,Cutout 作为第二个入口紧随其后
以 Crop Box 作为 `group=True` 的首项,Crop Lasso、Crop Polyline、Crop Perspective 依次通过前一个 Crop Tool ID 注册且不加 separator。完成 Crop 组注册后,直接调用 `register_tool(CutoutTool)`:不传 `after`、`group` 或 `separator`,使 Cutout 作为普通单项 ToolDef 追加在 Crop 组后,而不是 singleton tuple 或带分割线的入口。Cutout 使用 Blender Add UV Sphere 的 `ops.mesh.primitive_sphere_add_gizmo` 图标。卸载严格按 Cutout、Perspective、Polyline、Lasso、Box 的逆序执行。
注册测试直接验证工具类型、`after`、`group`、`separator` 和逆序注销,避免 Perspective 再次成为独立工具或 Cutout 被其他入口隔开。
### 3. 每个 Crop 子工具使用固定且唯一的 Scene 设置
`AnyImageSettings` 增加四个 `crop_*_keep_original`,以及 Box、Lasso、Polyline 三个 `crop_*_invert`;Box 与 Lasso 的 Refine Selection 设置同步改为 Crop 名称。所有布尔值默认关闭。Crop Perspective 只读取自己的 Keep Original,不声明或绘制 Invert / Refine Selection。
Selection Crop Operator 按 gesture 映射读取对应设置;Perspective Operator 直接读取自己的设置。固定映射比共享一个当前 Crop 状态更能保证切换子工具后恢复各自值,也便于逐项测试没有串用。
### 4. Crop Invert 只由工具选项控制,Cutout 不使用字母快捷键
Crop Box、Lasso、Polyline 的 WorkSpaceTool keymap 只保留鼠标启动操作,不注册 `F` 或其他字母键。共享 Crop Selection modal 删除现有 `F` 切换分支,每次开始手势时只读取当前子工具保存的 Invert。Crop Perspective 同样不注册选项快捷键;Cutout 删除现有 modal `F` 分支和对应状态提示。
### 5. 四个 Crop 子工具共用结果落地 helper
Keep Original 关闭时保持 `replace_empty_image(source_object, result_image, ...)` 路径。开启时,在结果就绪的 Blender 主线程复制源 Image Empty,将复制对象链接到源对象所在 Collection,再对复制对象调用同一图片替换逻辑。随后清理选择状态,选择并激活结果对象;未改变的源 Image Empty 保留但不继续选择。
Box、Lasso、Polyline 与 Perspective 都调用这个 helper,不各自实现对象复制。对象复制继承源变换和 Image Empty 显示属性;既有替换逻辑继续负责裁切尺寸、offset、图片用户数与命名。
### 6. 每次 Crop 冻结所属子工具的 Keep Original
Selection Crop 本地编辑与 Perspective 本地编辑分别把当前子工具的 Keep Original 传给结果落地 helper。Box/Lasso 启动 Refine Selection Job 时把该布尔值作为隐藏 Blender Operator Property 传入,并在响应阶段使用;不修改跨进程 Job request。
这保证异步执行期间切换工具设置不会改变已提交结果,也不会让一个 Crop 子工具读取另一个子工具的状态。
### 7. Cutout 在最终几何边界转换 Inward Axis
为 Cutout 增加 `NEGATIVE_Z` / `POSITIVE_X` Scene EnumProperty。保留 `BaseShape`、Depth 标定与 `O Image Cutout` 内部现有 `+X` 计算约定,只在节点组最终 Geometry 输出和对象矩阵边界应用选项:
- `+X` 使用现有 Geometry 输出与 `source_matrix @ crop_translation @ CUTOUT_AXIS_TO_SOURCE` 对象矩阵;
- `-Z` 在节点组末端用 `CUTOUT_AXIS_TO_SOURCE` 把最终 Geometry 转为 XY 纸面、`-Z` 向内,再使用 `source_matrix @ crop_translation` 对象矩阵。
两条路径的世界坐标都等于 `source_matrix @ crop_translation @ CUTOUT_AXIS_TO_SOURCE @ internal_geometry`,因此位置、大小、UV 和正反面保持一致。节点组增加由创建代码设置的 Inward Axis 输入,构建脚本、资产验证和接口测试同步更新;修改器输入保持 `SINGLE`。
### 8. Object Space Normal 随 Cutout 局部坐标转换
Depth Surface 的 Object Space Normal 文件继续由 Server 按内部 `+X` 约定生成,不扩展 Job 协议。选择 `-Z` 时,Blender 在结果导入边界把解码后的法线从 `(x, y, z)` 转为 `(-y, z, -x)` 并重新编码后 Pack;`+X` 直接使用现有图片。Tangent Space Normal 不依赖对象局部坐标轴,不执行转换。
### 9. UI 与状态文本只描述当前 Crop 子工具
Crop Box、Lasso、Polyline 的状态文本描述各自 Invert On/Off,但不提示字母快捷键;Crop Perspective 保留四点与宽高比交互提示,不显示 Invert。Invert、Refine Selection、Keep Original 与 Inward Axis 只通过工具设置 UI 修改。README 和内部 Operator 索引统一使用 Crop Tool;原 Image Tool 与 Perspective Tool 文档合并为 `crop-tool.md`。
## Risks / Trade-offs
- [旧 Workspace 保存了已删除的 Tool ID] → 不保留兼容 Tool;扩展注册后由 Blender 回退到可用工具,用户重新选择 Crop Tool。
- [Perspective 加入分组后入口发现性变化] → 使用明确的 Crop Perspective 名称和原有图标,并在 Crop Tool 文档中与三个 Selection Crop 并列说明。
- [删除 Line 影响既有使用习惯] → 这是明确的产品删除;不模拟半平面兼容行为,Polyline 继续承担自定义边界裁切。
- [四个 Keep Original 或三个 Invert 设置发生串用] → 每个 WorkSpaceTool 使用固定唯一的 Scene Property,参数化测试逐项切换并验证其余值不变。
- [复制对象可能遗漏 Collection 归属] → 明确链接到源对象的 Collection,并用测试验证结果与源位于预期 Collection。
- [异步 Refine 完成时源对象已被用户删除] → 沿用现有按名称解析源对象的失败处理,不创建失去来源的结果对象。
- [Keep Original 连续使用会创建大量对象和图片] → 这是选项的显式产物语义,默认关闭,不增加自动清理。
- [Inward Axis 同时影响 Geometry、对象矩阵和 Object Space Normal] → 在创建入口冻结一次选项值,并以相同值设置三处;覆盖四种 Shape、非均匀源变换和 Normal 的世界空间对照测试。
- [节点资产接口变化导致旧资产与代码不匹配] → 同步构建脚本、资产文件、结构验证与打包测试,不保留缺少新输入的兼容分支。
## Migration Plan
实施时先迁移 Crop 模块与名称、删除 Line,再切换注册和菜单入口,避免同一版本同时注册新旧 Tool ID。四个 Crop Keep Original 和三个 Invert 使用关闭默认值,因此旧文件加载后保持原有裁切结果语义,但不会迁移尚不存在的旧设置名称。Cutout Inward Axis 默认 `-Z`,会有意改变新建 Cutout 的 local transform;选择 `+X` 可取得旧坐标约定。
部署时同步代码与重建后的 `assets/O_AnyImage.blend`,不支持新代码搭配旧节点资产。回滚需要恢复旧模块、注册与节点资产;已创建的 Crop 结果和 Cutout Mesh 不会被自动迁移。

View File

@@ -0,0 +1,34 @@
## Why
现有图片编辑入口分散为四个 Image Selection Tool 和一个独立 Perspective Tool,其中 Line 与 Polyline 功能重叠,工具名称也没有直接表达裁切产物;同时编辑选项缺少稳定状态,Cutout 的局部向内轴还与源 Image Empty 不一致。将图片裁切工具收拢为一个明确的 Crop Tool 组,并统一持久选项与坐标约定,可以减少工具栏占用和后续编辑歧义。
## What Changes
- **BREAKING**:将 Image Box、Image Lasso、Image Polyline 与 Image Perspective 重命名并重组为同一个四合一 **Crop Tool** 工具组,子工具依次命名为 **Crop Box**、**Crop Lasso**、**Crop Polyline**、**Crop Perspective**。
- **BREAKING**:删除 Image Line WorkSpaceTool、对应半平面手势分支、公开 Tool ID、源码入口、测试与文档,不保留兼容入口。
- **BREAKING**:将原 `image_tool/` 与 `perspective_tool/` 的用户概念、Tool ID、Operator 名称和源码边界统一迁移为 `crop_tool/` 与 Crop 术语,不保留旧名称转发。
- 将 Crop Tool 注册为一个工具栏组,并让使用 Add UV Sphere 图标的 Cutout Tool 作为第二个独立工具栏入口紧接在该组之后;两个工具之间不显示分割线。
- 为 Crop Box、Crop Lasso 与 Crop Polyline 增加彼此独立保存的 **Invert** 工具选项,默认关闭;Crop Perspective 不提供 Invert,Crop Tool 不为选项注册字母快捷键。
- 为四个 Crop 子工具增加彼此独立保存的 **Keep Original** 工具选项,默认关闭;开启后每次裁切创建并激活结果 Image Empty,同时保留但不继续选择源 Image Empty。
- 为 Cutout 增加 **Inward Axis** 工具选项,可选择 `-Z` 或 `+X`;默认 `-Z`,使生成 Mesh 的局部坐标方向与源 Image Empty 一致,`+X` 保留当前 Cutout 坐标约定。
- 让 Cutout 的四种 Shape、Depth 几何与 Object Space Normal Map 统一遵循所选 Inward Axis,同时保持世界空间图片位置和正反面语义不变。
- 同步菜单、工具提示、状态栏、节点资产、测试、README 与内部文档。
## Capabilities
### New Capabilities
- `crop-tool-options`: 规定四合一 Crop Tool 的组成、命名、注册顺序、独立 Keep Original、适用子工具的独立 Invert,以及 Cutout 的相邻位置和 Inward Axis 坐标约定。
### Modified Capabilities
无。
## Impact
- 影响 `operators/image_tool/`、`operators/perspective_tool/`、共享 Viewport 手势、Scene 工具设置、菜单与扩展注册;实现迁移到统一的 `operators/crop_tool/` 边界。
- 旧 Image Tool / Perspective Tool 的 Tool ID、Operator ID、类名、模块路径和 Image Line 行为不再可用。
- Crop Selection 的本地与 Refine Selection 异步结果路径、Crop Perspective 的本地结果路径都需要支持保留源对象,并选择、激活结果对象。
- Cutout 的对象矩阵、统一 Geometry Nodes 资产和 Object Space Normal 转换需要接受所选 Inward Axis;本地与异步 Shape 路径必须传递同一选项。
- 需要更新 Blender 注册、交互、裁切、Cutout 对象与节点资产测试,以及用户文档、Crop Tool、Cutout 和节点资产内部文档。
- 不新增依赖,不改变 Cutout 的 Selection 栅格化或 AI Job 输入产物协议;删除其临时 `F` Invert 交互。

View File

@@ -0,0 +1,118 @@
## Purpose
规定四合一 Crop Tool 的工具栏结构、裁切工具命名和独立编辑选项,以及相邻 Cutout Tool 成品相对源图片平面的局部向内轴约定。
## ADDED Requirements
### Requirement: Crop Tool is one four-tool toolbar group
系统 SHALL 在 3D Viewport 工具栏中提供两个独立 AnyImage 入口:一个 **Crop Tool** 工具组和一个普通单项 **Cutout Tool**。Crop Tool SHALL 按 **Crop Box**、**Crop Lasso**、**Crop Polyline**、**Crop Perspective** 的顺序包含四个子工具;Cutout Tool SHALL 不属于任何工具组,并紧接在 Crop Tool 组之后。
#### Scenario: User opens the Crop Tool group
- **WHEN** 用户展开 Crop Tool
- **THEN** 工具组只显示 Crop Box、Crop Lasso、Crop Polyline 与 Crop Perspective
- **AND** Crop Box 是该组的首个子工具
#### Scenario: User views adjacent AnyImage tools
- **WHEN** Crop Tool 组和 Cutout Tool 已注册到 3D Viewport
- **THEN** 工具栏显示 Crop Tool 组与 Cutout Tool 两个独立入口
- **AND** Cutout Tool 紧接在 Crop Tool 组之后
- **AND** Cutout Tool 是单个 WorkSpaceTool,不是单项工具组
- **AND** 两个入口之间没有分割线
- **AND** Cutout Tool 使用 Add UV Sphere 图标
### Requirement: Crop terminology replaces Image Tool terminology
系统 SHALL 对四个裁切子工具、激活入口、Tool ID、Operator、设置属性、源码边界和文档统一使用 Crop 术语,不得保留旧 Image Tool 或独立 Perspective Tool 的兼容名称和转发入口。
#### Scenario: User activates the crop entry
- **WHEN** 用户从 AnyImage 入口激活图片裁切功能
- **THEN** 系统激活 Crop Box 并以 Crop Tool 命名该功能
#### Scenario: Extension registers crop tools
- **WHEN** AnyImage 扩展完成注册
- **THEN** 四个 Crop 子工具使用 `anyimage` 前缀的 Crop Tool ID 注册为同一组
- **AND** 旧 Image Tool 与独立 Perspective Tool ID 不再注册
### Requirement: Image Line is removed
系统 SHALL 删除 Image Line 及其按直线划分图片半平面的裁切行为,不得在 Crop Tool、菜单、快捷入口或公开 Operator 中提供该工具。
#### Scenario: User opens Crop Tool
- **WHEN** 用户查看 Crop Tool 的子工具
- **THEN** 列表中没有 Image Line 或 Crop Line
### Requirement: Crop options persist independently per sub-tool
系统 SHALL 为 Crop Box、Crop Lasso、Crop Polyline 与 Crop Perspective 分别保存彼此独立的 **Keep Original**,并为前三个 Selection Crop Tool 分别保存彼此独立的 **Invert**;所有选项默认关闭,切换一个子工具的值不得改变其他子工具的值,Crop Perspective SHALL NOT 提供 Invert。
#### Scenario: User returns to a Crop sub-tool
- **WHEN** 用户修改一个子工具支持的 Invert 或 Keep Original,切换到其他 Crop 子工具后再返回
- **THEN** 该子工具恢复自己上次保存的选项值
#### Scenario: User uses a Crop sub-tool for the first time
- **WHEN** 当前文件尚未修改该子工具支持的 Invert 或 Keep Original
- **THEN** 该子工具支持的选项均为关闭
#### Scenario: User opens Crop Perspective settings
- **WHEN** 用户激活 Crop Perspective
- **THEN** 工具设置提供独立的 Keep Original
- **AND** 工具设置不提供 Invert 或 Refine Selection
### Requirement: Invert follows the persistent Crop sub-tool option
Crop Box、Crop Lasso 与 Crop Polyline SHALL 使用当前子工具保存的 **Invert** 值决定保留 Selection 内部还是外部,并只通过工具选项修改该值;Crop Perspective 不参与该行为,Crop Tool 的 keymap 与 modal 交互 SHALL NOT 将 `F` 或其他字母键绑定为选项快捷键。
#### Scenario: User changes Invert in tool settings
- **WHEN** 用户在任一 Selection Crop Tool 的工具设置中修改 Invert
- **THEN** 系统保存当前子工具的 Invert,下一次裁切使用新值
#### Scenario: User operates a Crop tool with a letter key
- **WHEN** Crop Tool 空闲或正在执行 modal 交互
- **THEN** Crop Tool 不使用 `F` 或其他字母键切换 Invert、Keep Original 或 Refine Selection
### Requirement: Cutout has no letter option shortcuts
Cutout Tool 的 keymap 与 modal 交互 SHALL NOT 使用 `F` 或其他字母键切换选项。
#### Scenario: User presses F during Cutout selection
- **WHEN** Cutout Tool 正在执行 Selection modal 交互
- **THEN** `F` 不切换 Invert 或其他选项
### Requirement: Keep Original preserves the crop source
四个 Crop 子工具 SHALL 在各自的 **Keep Original** 关闭时以裁切结果替换源 Image Empty;开启时 SHALL 为本次裁切创建一个结果 Image Empty,并保留源 Image Empty 的对象、图片、变换和显示设置。
#### Scenario: User crops with Keep Original disabled
- **WHEN** 用户在 Keep Original 关闭时完成任一 Crop 操作
- **THEN** 裁切结果替换源 Image Empty,场景中不额外保留操作前的源对象
#### Scenario: User crops with Keep Original enabled
- **WHEN** 用户在 Keep Original 开启时完成任一 Crop 操作
- **THEN** 场景同时包含未改变的源 Image Empty 和本次裁切的结果 Image Empty
- **AND** 结果 Image Empty 被选中并作为 active object
- **AND** 源 Image Empty 不再保持选中
#### Scenario: User continues cropping the result
- **WHEN** 用户在 Keep Original 开启时完成一次裁切后继续操作当前 active object
- **THEN** 下一次裁切以刚创建的结果 Image Empty 作为输入
- **AND** 前一次源 Image Empty 仍保留在场景中
### Requirement: Cutout exposes the inward local axis
Cutout Tool SHALL 提供持久保存的 **Inward Axis** 选项,可选 `-Z` 与 `+X`,默认值 SHALL 为 `-Z`;该选项 SHALL 决定生成 Mesh Object 在局部坐标中从纸面指向内部的轴向。
#### Scenario: User creates a Cutout with the default axis
- **WHEN** 用户未修改 Inward Axis 并创建任一 Cutout Shape
- **THEN** 结果 Mesh 的纸面位于局部 XY 平面,向内方向为局部 `-Z`
- **AND** 结果的局部坐标轴与源 Image Empty 的局部坐标轴一致
#### Scenario: User chooses the legacy axis
- **WHEN** 用户把 Inward Axis 设为 `+X` 并创建任一 Cutout Shape
- **THEN** 结果 Mesh 的纸面位于局部 YZ 平面,向内方向为局部 `+X`
- **AND** 结果保持变更前的 Cutout 局部坐标约定
### Requirement: Cutout orientation preserves world-space output
Cutout SHALL 对 Surface、Balloon、Depth Balloon 与 Depth Surface 一致应用所选 Inward Axis,同时保持结果在源 Image Empty 图片平面上的世界空间位置、尺寸、纹理方向和纸面正反语义不变。
#### Scenario: Equivalent shapes use different local axes
- **WHEN** 用户从同一源 Image Empty 和 Selection 分别以 `-Z` 与 `+X` 创建相同 Shape
- **THEN** 两个结果的对应表面点在世界空间重合
- **AND** 两个结果只在 Mesh Object 的局部坐标与对象变换约定上不同
#### Scenario: Oriented Depth Surface uses an Object Space Normal Map
- **WHEN** 用户以任一 Inward Axis 创建启用 Normal Map 的 Depth Surface
- **THEN** Object Space Normal Map 按所选局部坐标约定着色
- **AND** 可见表面法线方向与对应几何法线一致

View File

@@ -0,0 +1,34 @@
## 1. Crop Tool 重组
- [x] 1.1 将 Box、Lasso、Polyline、共享 Selection Crop 与 Perspective 实现迁移到 `operators/crop_tool/`,统一类名、Tool ID、Operator ID、激活入口和导出为 Crop 术语;用导入、注册和 ID 测试验证旧 `image_tool/`、`perspective_tool/` 路径及旧公开 ID 不再存在
- [x] 1.2 删除 Image Line WorkSpaceTool、`LINE` gesture、半平面多边形函数及对应状态和测试分支;用搜索与交互测试验证 Crop Tool、菜单和公开 Operator 不再提供 Line
- [x] 1.3 按 Crop Box、Crop Lasso、Crop Polyline、Crop Perspective 注册一个连续 WorkSpaceTool 组,再将不传 `after`、`group`、`separator` 的普通单项 Cutout Tool 紧接该组并使用 Add UV Sphere 图标;用 Blender 实际工具结构和注册测试验证工具类型、图标、顺序及严格逆序注销
- [x] 1.4 将 AnyImage 图片编辑入口改名为 Crop Tool 并默认激活 Crop Box;用菜单和 Operator 测试验证显示名、描述与激活目标
## 2. 独立工具设置与 Invert
- [x] 2.1 为四个 Crop 子工具增加唯一的 Keep Original Scene Property,为 Crop Box、Lasso、Polyline 增加唯一的 Invert,为 Box/Lasso 保留改为 Crop 名称的 Refine Selection,并为 Cutout 增加默认 `-Z` 的 Inward Axis;用属性测试验证默认值、保存状态、四个 Keep Original 与三个 Invert 互不影响且 Perspective 没有 Invert
- [x] 2.2 扩展设置读取与工具栏绘制,让每个 Crop 子工具只显示自己的 Keep Original,三个 Selection Crop 显示各自 Invert,Box/Lasso 额外显示 Refine Selection,Crop Perspective 不显示 Selection 选项,Cutout 显示 Inward Axis;用工具设置测试验证属性顺序、名称与映射
- [x] 2.3 删除 Crop 与 Cutout Selection modal 的 `F` 切换分支,不为两个工具栏入口注册任何选项字母快捷键;用 keymap 与交互测试验证 `F` 不再切换选项
- [x] 2.4 更新 Crop 与 Cutout 的状态文本,不提示字母快捷键;用状态文本测试验证各工具的交互提示边界
## 3. 四个 Crop 子工具的 Keep Original
- [x] 3.1 增加 Crop 共用的 Image Empty 结果落地逻辑:关闭时替换源对象,开启时复制源对象承载结果、保留但取消选择源对象,并选择和激活结果对象;用对象与图片生命周期测试验证变换、显示设置、Collection、选择和命名行为
- [x] 3.2 将各自 Keep Original 接入 Box、Lasso、Polyline 本地裁切路径,并用参数化编辑测试验证默认替换、开启后保留源对象、连续裁切以新结果为输入且子工具状态互不串用
- [x] 3.3 将提交时的 Box/Lasso Keep Original 传入 Refine Selection Blender Operator 并在异步响应落地结果,不改变 Job request;用响应和 Undo 测试验证源对象保留、结果激活状态与异常清理
- [x] 3.4 将 Crop Perspective 的独立 Keep Original 接入透视结果落地;用 Perspective 交互与执行测试验证默认替换、开启后保留源 Empty 并激活结果,且不改变其他三个 Keep Original
## 4. Cutout Inward Axis
- [x] 4.1 将提交 Shape 时的 Inward Axis 固定到 Cutout Operator,并贯通本地与异步创建路径;用 Pie、Operator 和响应测试验证四种 Shape 都取得提交时的同一选项且 Job request 不增加该字段
- [x] 4.2 扩展 `O Image Cutout` 构建脚本,在最终 Geometry 边界按 Inward Axis 切换 `-Z` 与 `+X` 坐标约定,并同步 `assets/O_AnyImage.blend`、结构验证和节点资产文档;运行 `uv run build_node` 验证资产
- [x] 4.3 调整 Cutout 对象矩阵,使默认 `-Z` 使用源 Image Empty 的 local transform、`+X` 保留现有轴变换;用四种 Shape、裁切范围及旋转和非均匀缩放源对象测试验证两种选项的对应世界空间点重合
- [x] 4.4 在 `-Z` 模式导入 Depth Surface Object Space Normal 时执行 `(-y, z, -x)` 通道转换,保持 `+X` 与 Tangent Space 路径不变;用法线像素和材质着色方向测试验证两种坐标约定
## 5. 文档与回归验证
- [x] 5.1 将原 Image Tool 与 Perspective Tool 内部文档合并为 `docs/internals/operators/crop-tool.md`,用流程图说明四合一工具组、三个独立 Invert、四个独立 Keep Original 及同步和异步结果分支,并更新 Operator 索引与公共 Viewport 文档
- [x] 5.2 更新 README、`docs/index.md`、架构和相关用户表达,统一使用 Crop Tool / Crop Box / Crop Lasso / Crop Polyline / Crop Perspective,确认没有 Image Line、Image Tool 或独立 Perspective Tool 的当前功能描述
- [x] 5.3 更新 `docs/internals/operators/cutout-tool.md`,使用流程图说明内部 `+X` 几何、最终 Inward Axis 转换、对象矩阵与 Object Space Normal 的对应关系
- [x] 5.4 运行 Crop、Cutout 交互与对象、属性、菜单、注册、节点资产、打包和 Blender Addon 相关测试,再运行 `uv run pytest`,确认完整测试集通过