TEdit 遮罩系统重构指南:跨工具解耦的 TileMaskSettings 架构与液体遮罩实现解析
TEdit 遮罩系统重构指南:跨工具解耦的 TileMaskSettings 架构与液体遮罩实现解析
本指南围绕 TEdit(Terraria 地图编辑器)规划中的 Masking System Overhaul(对应 issue #1274、#900,见 docs/todo/masking-system.md)展开,讲解其核心目标——把「遮罩」从当前编辑图层中解耦出来,使其成为独立作用于「哪些瓦片会被影响」的过滤器;并结合仓库中已落地的
TileMaskSettings实现(src/TEdit.Editor/TileMaskSettings.cs)讲解液体遮罩、属性匹配语义、掩码求值原理与 UI 重构方向。读完本文,你将掌握 TEdit 遮罩系统的数据模型、四种匹配模式的求值规则,以及如何在画笔、铅笔、桶填充、橡皮擦乃至导线模式下利用遮罩精确控制编辑范围。
一、背景与总体目标:从「图层耦合」到「解耦遮罩」
1.1 当前行为的痛点
在原需求文档中明确指出了现有遮罩系统的核心问题:遮罩与当前正在编辑的图层强耦合——编辑瓦片(Tile)时只有瓦片遮罩生效,编辑墙体(Wall)时只有墙体遮罩生效,即「遮罩只作用于与自身同类的图层」。
这种设计的局限在于:当用户想「在墙体编辑操作中只影响特定类型的瓦片」时无从下手,遮罩能力被操作类型锁死。
1.2 新行为的语义转变
需求文档给出了两项关键定义(原文引用):
- Masks apply regardless of which layers are being edited
- Mask is a filter on which tiles are affected, not what operation runs
翻译成实现语言即是:
- 遮罩不再绑定操作类型:例如设置瓦片遮罩为「Stone」,那么墙体编辑只会作用于「Stone」瓦片所在的位置;设置墙体遮罩为「Dirt Wall」,则放置执行器(Actuator)的操作只会发生在 Dirt Wall 瓦片上。
- 遮罩是「受影响范围」过滤器,而非「运行什么操作」的开关:操作(放置、擦除、涂色)由工具决定,遮罩只负责筛选目标瓦片。
从仓库源码看,这一「全局门控」模式已经在编辑核心 WorldEditor 中体现:在 src/TEdit.Editor/WorldEditor.cs 的 SetPixel 入口处,有一段注释为 Global mask gate: if any enabled mask fails, skip this tile entirely 的判断——if (!MaskSettings.Passes(curTile)) return;。也就是说,无论当前 PaintMode 是瓦片、墙体还是涂色,遮罩评估都先于操作执行,这正是「解耦遮罩」思想的代码落点。
1.3 配套的 UI 重构要求
文档同时要求重构遮罩选择器 UI:不再维护一套独立的遮罩选择控件,而是复用统一的瓦片选择器(Unified Tile Picker)控件,其交互规格详见配套需求文档 docs/todo/pattern-selection.md。该控件提供:
- 紧凑模式(Compact Mode):24×24 复合预览图,将瓦片、墙体、液体、瓦片涂色、墙体涂色、涂层、执行器状态全部合成渲染;带「擦除」红叉、「空/禁用」变暗虚线边框、执行器角标等状态指示。
- 完整模式(Full Mode):每个属性一个独立的下拉选择器(瓦片类型支持搜索/过滤、涂色下拉、液体下拉等),且每个属性有独立的启用/禁用开关与擦除开关。
- 两种模式是同一个
TilePickerUserControl 的DisplayMode属性切换,数据模型为TilePickerSlot。
掩码选择器将采用该控件的紧凑模式展示,点击后展开为完整模式,并为每个属性追加一个**匹配模式(Match Mode)**下拉框,另加一个 **Invert Mask(反转遮罩)**开关。
二、液体遮罩(Liquid Masking,#900)
液体遮罩是本轮重构新增的核心能力,需求要点如下(原文):
- Add liquid type to the mask options: Water, Lava, Honey, Shimmer
- Mask modes for liquid: Match (only affect tiles with this liquid), Ignore (skip tiles with this liquid)
- Works with all tools: brush, pencil, bucket fill, eraser
2.1 液体类型的枚举定义
需求中提到的四种液体,在仓库的 LiquidType 枚举中均有明确定义(src/TEdit.Terraria/LiquidType.cs):
| 液体 | 枚举值 |
|---|---|
| None(无液体) | 0x0 |
| Water(水) | 0x01 |
| Lava(岩浆) | 0x02 |
| Honey(蜂蜜) | 0x03 |
| Shimmer(微光,1.4.4 引入) | 0x08 |
2.2 液体在遮罩数据模型中的落点
在 TileMaskSettings 中,液体遮罩由两组属性承载:
- 液体类型遮罩:
LiquidTypeMaskMode(匹配模式)+LiquidTypeMaskValue(目标液体类型,默认LiquidType.Water); - 液体水平遮罩:
LiquidLevelMaskMode+LiquidLevelMaskValue(byte,0–255)。
液体水平并非需求文档原文提到的新增项,但仓库实现已将其纳入(src/TEdit.Editor/LiquidLevelMaskMode.cs 定义了四种比较模式),形成了「类型 + 液位」的双重液体过滤能力:
| 模式 | 语义 |
|---|---|
Ignore |
不对液体水平过滤(默认) |
GreaterThan |
仅作用于液体水平大于目标值的瓦片 |
LessThan |
仅作用于液体水平小于目标值的瓦片 |
Equal |
仅作用于液体水平等于目标值的瓦片 |
2.3 工具适用范围
需求文档明确液体遮罩需要「Works with all tools: brush, pencil, bucket fill, eraser」。从架构上看,这一要求由「全局遮罩门控」天然满足:由于 Passes(Tile) 在 SetPixel 入口统一执行,任何经由该路径的工具(画笔、铅笔、桶填充、橡皮擦,以及后续的导线放置)都会自动受到液体遮罩约束,无需每个工具单独实现液体过滤逻辑。
三、遮罩属性体系:统一数据模型与匹配模式
3.1 需求文档中的属性表
需求文档给出了遮罩属性与匹配模式的原始定义(原文表格,完整保留):
| Property | Match Mode | Description |
|---|---|---|
| Tile type | Exact / Any / Empty | Filter by tile ID |
| Tile paint | Exact / Any / None | Filter by tile paint color |
| Wall type | Exact / Any / Empty | Filter by wall ID |
| Wall paint | Exact / Any / None | Filter by wall paint color |
| Liquid | Exact / Any / None | Filter by liquid type |
| Actuator | On / Off / Any | Filter by actuator state |
| Coating | Exact / Any / None | Filter by coating type |
并给出三种匹配模式的语义定义(原文):
- Exact: only affect tiles matching this value
- Any: don't filter on this property
- Empty/None: only affect tiles where this property is unset
3.2 源码中的枚举对应关系
需求文档的「Exact / Any / Empty / None / On / Off」在仓库实现中被收敛为四个枚举值(src/TEdit.Editor/MaskMode.cs):
| 枚举值 | 说明 | 与需求文档的对应 |
|---|---|---|
Off |
关闭该属性的遮罩过滤 | Any(不过滤) |
Empty |
仅作用于该属性为「空」的瓦片 | Empty / None |
Match |
仅作用于与该属性值精确匹配的瓦片 | Exact / On |
NotMatching |
仅作用于与该属性值不匹配的瓦片(即反转) | 需求中的 Invert Mask 开关 |
3.3 完整属性清单(需求 + 实现)
需求文档表格列出的 7 类属性,在 src/TEdit.Editor/TileMaskSettings.cs 中被实现为「模式 + 值」成对出现的 Reactive 属性。结合源码,实际的遮罩属性全集如下(MaskMode 均可取上表四值之一):
| 类别 | 属性(模式 / 值) | 值语义 |
|---|---|---|
| 瓦片类型 | TileMaskMode / TileMaskValue |
瓦片 ID;-1 表示「空/未激活」 |
| 墙体类型 | WallMaskMode / WallMaskValue |
墙体 ID;0 表示「无墙体」 |
| 砖块样式 | BrickStyleMaskMode / BrickStyleMaskValue |
BrickStyle 枚举(如 Full 等) |
| 执行器 | ActuatorMaskMode |
瓦片是否含执行器 |
| 执行器未激活 | ActuatorInActiveMaskMode |
瓦片是否处于未激活(InActive)状态 |
| 瓦片涂色 | TilePaintMaskMode / TilePaintMaskValue |
涂色 ID;0 为无涂色 |
| 墙体涂色 | WallPaintMaskMode / WallPaintMaskValue |
墙体涂色 ID;0 为无涂色 |
| 瓦片 Echo 涂层 | TileEchoMaskMode |
瓦片是否隐形(InvisibleBlock) |
| 瓦片 Illuminant 涂层 | TileIlluminantMaskMode |
瓦片是否发光(FullBrightBlock) |
| 墙体 Echo 涂层 | WallEchoMaskMode |
墙体是否隐形(InvisibleWall) |
| 墙体 Illuminant 涂层 | WallIlluminantMaskMode |
墙体是否发光(FullBrightWall) |
| 红色导线 | WireRedMaskMode |
瓦片是否含红色导线 |
| 蓝色导线 | WireBlueMaskMode |
瓦片是否含蓝色导线 |
| 绿色导线 | WireGreenMaskMode |
瓦片是否含绿色导线 |
| 黄色导线 | WireYellowMaskMode |
瓦片是否含黄色导线 |
| 液体类型 | LiquidTypeMaskMode / LiquidTypeMaskValue |
LiquidType 枚举 |
| 液体水平 | LiquidLevelMaskMode / LiquidLevelMaskValue |
液位 0–255,比较模式见上文 |
可以看到,仓库实现比需求文档的原始列表多出砖块样式、执行器未激活、Echo/Illuminant 涂层、四种导线、液体水平等属性——说明遮罩数据模型在落地过程中已经按同一套「模式 + 值」范式做了横向扩展,四色导线属性也为「导线模式扩展」需求(见下文第六节)提前铺好了数据基础。
四、遮罩求值原理:Passes() 与 AND 语义
4.1 统一的求值入口
TileMaskSettings.Passes(Tile)(src/TEdit.Editor/TileMaskSettings.cs)是全部遮罩的统一求值函数,其文档注释明确了关键语义:
Evaluates all active masks against the given tile. Returns true if the tile passes all masks. All masks are AND'd: if any enabled mask fails, the tile is rejected.
即所有已启用的遮罩之间是 AND 关系:任何一个属性不满足,瓦片即被拒绝。这是保证「遮罩是可预测的范围过滤器」的基础——不会出现某个属性放行、另一个属性拦截的歧义。
4.2 快速路径:AllMasksOff
在 Passes 之上还有一个性能优化属性 AllMasksOff(同上文件 L62-L79),它枚举全部 17 个遮罩模式并检查是否全部处于 Off(液体水平处于 Ignore)。若全部关闭,Passes 直接短路返回 true,调用方(如整图替换逻辑)也可以据此跳过整轮遍历:
SetPixel中通过Passes逐瓦片过滤(src/TEdit.Editor/WorldEditor.cs);ReplaceAll(整图替换,src/TEdit.Editor/WorldEditor.cs)在入口处检查if (MaskSettings.AllMasksOff) return false;——所有遮罩关闭时没有可匹配的目标,直接返回失败,避免无意义的全图遍历;遍历循环内同样以if (!MaskSettings.Passes(curTile)) continue;过滤,且支持仅在当前选区(selectionOnly)内执行。
4.3 各类属性的求值细节
Passes 内部将属性分为四类求值模式,均有独立的私有求值器:
- 瓦片类型(PassesTileMask):处理特殊的
-1(空/未激活)情形——Match且值为-1时匹配「非激活瓦片」,Match且值 ≥ 0 时要求IsActive && Type == value,Empty时匹配非激活瓦片,NotMatching时取反; - 整数型属性(PassesIntMask,静态):墙体、涂色等 ID 值统一走此路径,
Match要求相等、Empty要求等于空值(墙体为 0、涂色为 0)、NotMatching要求不等; - 布尔型属性(PassesBoolMask,静态):执行器、涂层、导线等「有/无」属性,
Match要求为真、Empty要求为假、NotMatching取反; - 液体类:
PassesLiquidTypeMask处理液体类型(Empty时匹配LiquidAmount == 0或LiquidType == None);PassesLiquidLevelMask处理液位比较(Equal/GreaterThan/LessThan)。
4.4 一键取样与清空
数据模型还提供两个实用方法:
SetFromTile(Tile)(L140-L178):把目标瓦片的所有属性一次性「取样」为遮罩条件——将所有模式置为Match(布尔属性按瓦片实际状态置Match或Empty),值取自该瓦片,最终把MaskPreset置为ExactMatch。这对应「用吸管工具取样一个瓦片,然后用它作为全图匹配条件」的典型工作流(PickerTool即位于 src/TEdit/Editor/Tools/PickerTool.cs)。ClearAll()(L183-L210):将所有模式复位为Off/Ignore、值复位为默认值,并把MaskPreset置为Off。
4.5 预设机制
MaskPreset 枚举(src/TEdit.Editor/MaskPreset.cs)为遮罩组合提供命名状态:Off(关闭)、Custom(自定义组合)、ExactMatch(与某瓦片完全一致,由 SetFromTile 写入),供 UI 快速切换/恢复遮罩配置,默认定义见 src/TEdit.Editor/ToolDefaultData.cs。
4.6 测试印证
仓库测试 src/TEdit.Tests/Editor/CannonballSlopeTests.cs 展示了 WorldEditor 的构造方式:new WorldEditor(picker, mask, world, selection, undo, null),其中 mask = new TileMaskSettings() 作为独立依赖注入,印证了「遮罩是独立于工具/操作的核心组件」这一架构定位。
五、Mask Picker UI 重构方向
5.1 复用统一瓦片选择器
需求要求遮罩选择器直接复用统一瓦片选择器控件(详细交互见 docs/todo/pattern-selection.md):
- 以 Compact Mode(24×24 复合预览) 展示当前遮罩组合的合成效果;
- 点击后展开为 Full Mode,每个属性显示独立的匹配模式下拉框(Exact / Any / Empty);
- 追加 Invert Mask 开关——「影响除匹配之外的所有瓦片」,对应源码中的
MaskMode.NotMatching(即Passes中所有NotMatching分支取反的语义)。
5.2 现有实现的分区布局参考
仓库当前的面板 src/TEdit/View/Sidebar/Controls/MaskOptionsPanelControl.xaml 已经按照「属性分区」组织遮罩编辑界面,可作为重构后的布局基准:
- Type 分区:瓦片/墙体两个并排区域,各含匹配模式下拉(
MaskModeEnum资源,带图标模板)与TabbedPickerControl分页选择器(支持搜索/过滤,ModIdThreshold用于区分原版与 Mod 内容); - Brick Style 分区:砖块样式模式 + 样式值;
- Paint 分区:瓦片涂色 / 墙体涂色两组「模式 + 颜色下拉」;
- Coating 分区:Echo(隐形)与 Illuminant(发光)分别作用于瓦片/墙体;
- Extras 分区:Actuator(执行器)与 Inactive(未激活);
- Wires 分区:红/蓝/绿/黄四色导线的开关匹配;
- Liquid 分区:液体类型(模式 +
LiquidType下拉)与液体水平(模式下拉 + 0–255 滑块,IsSnapToTickEnabled步进 1)。
所有值均通过 TwoWay 绑定到 MaskSettings 对应属性,模式下拉绑定 MaskModeEnum、值下拉绑定 WorldConfiguration 提供的瓦片/墙体/涂色数据源(如 TileBricksMask、WallPropertiesMask、PaintProperties)。
六、导线模式扩展(Wire Mode Extension)
需求文档最后要求将解耦遮罩扩展到导线编辑模式:
- Extend decoupled masking to wire editing mode
- Example: place actuators only on tiles matching a specific tile ID
- Example: place wire only on tiles with a specific wall type
- All mask properties available in wire mode, same as other tool modes
这一需求的实现基础在数据层已经完备:
TileMaskSettings中已有四色导线遮罩(WireRedMaskMode/WireBlueMaskMode/WireGreenMaskMode/WireYellowMaskMode),可分别按「含/不含某色导线」过滤;- 与需求示例对应:放置执行器时以
TileMaskMode = Match限定瓦片 ID;放置导线时以WallMaskMode限定墙体类型; - 由于
Passes(Tile)是SetPixel路径上的全局门控,导线放置(同样经由编辑核心)天然继承全部遮罩属性,无需为导线模式单独复制一份遮罩逻辑——这正是「All mask properties available in wire mode, same as other tool modes」的架构保证。
七、与其他子系统的关系
- 统一瓦片选择器 / 图案填充:遮罩选择器与图案调色板共用同一个
TilePicker控件与TilePickerSlot数据模型(见 docs/todo/pattern-selection.md),三者(主编辑栏、图案槽、遮罩选择器)行为一致; - 整体规划:该重构属于 TEdit 发布路线图中的「Masking System Overhaul」(#2168),与统一瓦片选择器(#2169)、Morph 工具(#533)、tModLoader 支持(#1075)等并列,完整路线见 docs/todo/README.md;其中 #1274 已拆分为多个子任务(#2168、#2169、#2170、#2171)并分别跟进。
八、总结与使用要点
- 遮罩 = 范围过滤器:遮罩决定「哪些瓦片会被影响」,操作类型由工具决定,两者解耦;
- 四值匹配模式:
Off(不过滤)/Match(精确匹配,对应需求的 Exact/On)/Empty(匹配属性为空,对应 Empty/None)/NotMatching(反转,对应 Invert Mask 开关); - AND 语义:多个遮罩同时启用时需全部通过,
Passes(Tile)是唯一求值入口,AllMasksOff提供零开销快速路径; - 液体遮罩:类型(Water/Lava/Honey/Shimmer)+ 液位(Equal/GreaterThan/LessThan)双维度过滤,适用于画笔、铅笔、桶填充、橡皮擦等全部工具;
- 导线模式:四色导线遮罩数据已就绪,配合瓦片/墙体遮罩可实现「只在特定瓦片/墙体上放线或执行器」的精确布线;
- UI 演进:最终形态是复用统一瓦片选择器(紧凑 24×24 预览 + 完整模式),每个属性带匹配模式下拉与反转开关;当前 MaskOptionsPanelControl.xaml 的分区布局可视为过渡形态。
若要在自己的实现中复刻这套遮罩架构,推荐直接参照 src/TEdit.Editor/TileMaskSettings.cs 的「模式 + 值」成对属性、统一 Passes 求值入口与编辑器核心的全局门控模式,这是保证遮罩在所有工具间行为一致、可测试、可扩展的最小骨架。