TiXL t3 Symbol Browser:模糊搜索、类型过滤与使用频率排序的算子插入机制
【免费下载链接】t3TiXL is an open source software to create realtime motion graphics.项目地址: https://gitcode.com/GitHub_Trending/t3/t3
本文以 TiXL t3(开源实时动态图形工具,项目路径GitHub_Trending/t3/t3)编辑器的 Symbol Browser(算子浏览器)为主题,讲解如何通过长按/双击画布或 Tab 键唤起该弹窗、在图中选定位置放置算子,并结合 SymbolBrowser.cs、SymbolFilter.cs 与 SymbolAnalysis.cs 的源码,剖析模糊搜索、输入类型过滤、使用频率排序以及“拖线先行”自动连接等机制的底层实现。
一、Symbol Browser 是什么,如何唤起
按官方帮助文档 SymbolBrowser.md 的定义,Symbol browser 是你用来查找算子(operator/symbol)并将其放置到图(graph)上指定位置的弹窗。它的核心交互包括:
- 长按(或双击)空白画布打开浏览器,然后直接键入文字搜索;
- 搜索支持同义/缩写匹配——例如输入 "LFO" 即使不是算子的名字,也能找到振荡器(oscillator);
- 放置前可预览每个算子的产出效果;
- 若先拖出一条连接线,浏览器只列出能匹配该输入类型的算子;
- 在单个被选中的算子上按 Tab,可内联插入新算子;
- 候选项按“人们实际使用频率”排序,常用算子浮在顶部。
源码层面,该弹窗对应 SymbolBrowser 类,注释说明它“表示 GraphView 上新 GraphNode 的占位符,可与其他节点连接并提供搜索功能,本质上是 T2 的 CreateOperatorWindow”。它在 GraphView.cs#L107 中随视图构造创建:
graphView.SymbolBrowser = new SymbolBrowser(projectView, graphView);唤起入口是OpenAt方法(SymbolBrowser.cs#L39-L75),签名揭示了它承载的全部过滤语义:
public void OpenAt(Vector2 positionOnCanvas, Type filterInputType, // 只保留有该类型输入的算子 Type filterOutputType, // 只保留有该类型输出的算子 bool onlyMultiInputs, // 仅保留多输入槽算子 string startingSearchString = "", System.Action<Symbol> overrideCreate = null)其中positionOnCanvas就是文档所说的“at a chosen spot”——新算子最终会以该坐标落入图中(见下文CreateInstance)。OpenAt还做了一个体验优化:若放置点太靠近窗口边缘,会先调用Canvas.FitAreaOnCanvas平移画布,避免浏览器被裁切(L41-L55)。
Tab 内联插入的路径可以在Draw()的未打开分支中找到(SymbolBrowser.cs#L81-L128):
- 若当前恰好选中一个算子,按 Tab 会走
ConnectionMaker.OpenBrowserWithSingleSelection(定义于 ConnectionMaker.cs#L699),在新算子插入后自动建立与选中算子的连接——这就是文档所说的“inline insert”; - 若没有选中算子,则启动
ConnectionMaker.StartOperation(_graphView, "Add operator")并在鼠标位置打开浏览器(L91-L96)。
二、打字即搜:查询如何被编译成模糊正则
文档称搜索“matches synonyms too”(能匹配同义词/缩写)。从源码看,这一能力来自 SymbolFilter 的查询编译逻辑。UpdateFilters对输入串做了两步处理(SymbolFilter.cs#L69-L102):
1. 两段式语法分离“算子名 + 预设名”。若查询含空格,正则(.+?)\s+(.*)会把查询拆成symbolFilter(前段,用于匹配算子)和PresetFilterString(后段,用于筛选预设):
// Check if template search was initiated var twoPartSearchResult = new Regex(@"(.+?)\s+(.*)").Match(search); if (twoPartSearchResult.Success) { symbolFilter = twoPartSearchResult.Groups[1].Value; presetFilter = twoPartSearchResult.Groups[2].Value; }2. 逐字符插入.*构建模糊正则。搜索词"LFO"会被编译为L.*F.*O(忽略大小写):
var pattern = string.Join(".*", symbolFilter.ToCharArray()); searchRegex = new Regex(pattern, RegexOptions.IgnoreCase);这正是 “LFO 能找到振荡器”的机制:只要算子名中 L、F、O 三个字符按序出现(如 Low/Frequency/Oscillator 类命名)即可命中,无需字面包含 "LFO"。匹配时(L156-L160)满足以下任一条件即入选:
- 正则匹配算子名字(
_currentRegex.IsMatch(symbolUiSymbol.Name)); - 命名空间包含过滤词(忽略大小写的子串包含);
- 描述文本包含过滤词。
命中集合随后按相关度排序并截断到前 100 条(L174-L180),保证浏览器响应始终流畅。另外,UpdateMatchingSymbols会先把“当前组合及其所有祖先算子”的 Id 收集进parentSymbolIds并跳过(L106-L120)——这是为了防止把算子放进会形成循环引用的自己/祖先内部。
浏览器主列表的绘制在DrawResultsList(SymbolBrowser.cs#L264-L380):每项以算子第一个输出的数据类型取色作为背景,属于Lib.、Types.、Examples.Lib.、当前用户项目或当前组合命名空间的算子颜色饱和,其余命名空间的颜色淡出到 40%(color.Fade(0.4f)),让用户一眼区分核心库与边缘算子。支持方向键上下移动高亮、滚轮/滚动自动跟随,鼠标悬停也会即时切换选中项。
三、“拖线先行”:按连接类型过滤候选算子
文档中“如果先拖出连接线,浏览器只列出适配该输入的算子”对应OpenAt的filterInputType/filterOutputType/onlyMultiInputs参数,在 SymbolFilter.UpdateMatchingSymbols 中强制执行:
if (_inputType != null) { if (symbolUiSymbol.InputDefinitions.Count == 0) continue; // 优先第一个输入类型匹配;否则任意输入匹配 if (symbolUiSymbol.InputDefinitions[0].ValueType != _inputType) { var matchingInput = symbolUiSymbol.InputDefinitions.FirstOrDefault(i => i.ValueType == _inputType); if (matchingInput == null) continue; } var matchingInputDef = symbolUiSymbol.GetInputMatchingType(FilterInputType); if (matchingInputDef == null) continue; // 新连接落在第一个匹配类型的输入上,所以它必须是多输入槽... if (OnlyMultiInputs && !matchingInputDef.IsMultiInput) continue; }这段代码解释了OnlyMultiInputs存在的原因:新连接会落在第一个匹配类型的输入槽上,因此当用户从已有输入槽继续拖线(该槽可能已占位)时,只保留支持多输入(multi-input)的算子,确保新连接能挂接成功。
过滤生效后,浏览器列表顶部会打印类型过滤头(PrintTypeFilter,SymbolBrowser.cs#L382-L402),格式为输入类型[多输入标记] -> 输出类型,例如float[..] -> float,让操作者明确当前候选集合的约束。
四、排序算法:为什么“常用的算子浮在顶部”
文档称“suggestions are ranked by how often people actually use them”。实现位于SymbolFilter.ComputeRelevancy(SymbolFilter.cs#L199-L409),它为一个算子累积一系列乘性相关度因子。可确认的因子如下:
| 条件 | 因子 | 源码位置 |
|---|---|---|
| 单字符查询且算子名以其开头 | ×20 | L216-L219 |
| 名字与查询完全相等(忽略大小写) | ×8.6 | L227-L231 |
| 名字前缀匹配 | ×8.5 | L233-L237 |
| 名字包含查询 | ×8.4 | L240-L245 |
| 描述文本包含查询 | ×1.01 | L247-L252 |
| Pascal 缩写匹配(查询字符按序命中名字大写字母,如 "ds" → "DrawState") | ×4 | L255-L279 |
命名空间以Types.开头 | ×4 | L221-L225 |
命名空间以Lib开头 | ×3 | L287-L288 |
命名空间以examples开头 | ×2 | L290-L294 |
| 与当前项目同包(同 SymbolPackage) | ×2 | L310-L315 |
| 命名空间以当前项目/组合的根命名空间开头 | ×1.9 | L318-L335 |
属于用户可编辑项目(EditableSymbolProject) | ×1.9 | L339-L343 |
命名空间含dx11或下划线 | ×0.1 | L281-L285 |
名字以_开头(内部算子) | ×0.1 | L297-L301 |
名字含OBSOLETE | ×0.01 | L303-L304 |
| 标签为 Obsolete/NeedsFix/Research/Internal | ×0.3 | L385-L389 |
| 标签为 Advanced | ×0.8 | L391-L395 |
| 标签为 Essential | ×1.5 | L397-L401 |
两个“使用频率”因子最能体现文档所述的排序意图:
1. 高频连接组合加成(L345-L365)。SymbolAnalysis会统计所有项目中“输出槽 → 输入槽”连接对的哈希计数(SymbolAnalysis.cs#L24-L27 注释明确写着 “Used by SymbolBrowser for relevancy weighting of frequent combinations”)。若候选算子的输出槽恰好常与当前拖线来源的输入槽配对,相关度乘以1 + 4·count^(1/3)——即“社区里常用的一条接线”会显著上浮。
2. 全局使用量加成(L367-L383)。当算子经过上述检查后相关度已超过 10(即确实相关),再乘以其被实例化次数的加成:
var count = SymbolAnalysis.InformationForSymbolIds.TryGetValue(symbol.Id, out var info) ? info.UsageCount : 0; var totalUsageCountBoost = (float)(1 + (500.0 * (float)count / SymbolAnalysis.TotalUsageCount)); relevancy *= totalUsageCountBoost;UsageCount由 SymbolAnalysis.UpdateSymbolUsageCounts 遍历所有包的算子子项统计而来,因此被大量组合引用的算子会自然排在前面——与文档“ranked by how often people actually use them”的表述一一对应。
五、放置与创建流程:从点击到可撤销的“Insert Op”
选中某项后(回车确认,或单击/双击列表项),CreateInstance执行完整落盘流程(SymbolBrowser.cs#L551-L640):
- 按位置插入子节点:
AddSymbolChildCommand(parentSymbol, symbol.Id) { PosOnCanvas = PosOnCanvas }将新算子以浏览器打开时的画布坐标加入当前组合(L565-L567)——这就是“placed at a chosen spot”的实现; - 应用预设(如有):若两段式搜索选中了某个 preset,则
presetPool.Apply(newInstance, _selectedPreset)把该 Variation 预设写入新实例(L580-L584); - 自动连接拖线前留下的临时连接:遍历
ConnectionMaker.GetTempConnectionsFor得到的草稿连接,按类型在新算子上找匹配槽位(GetOutputMatchingType/GetInputMatchingType),为每条连接生成AddConnectionCommand(L593-L633); - 合并为一次可撤销操作:
ConnectionMaker.CompleteOperation(_graphView, commandsForUndo, "Insert Op " + ...)把“插入算子 + 全部连接”打包进撤销栈,一次 Ctrl+Z 全部回退(L637); - 自动弹出参数窗口:
ParameterPopUp.NodeIdRequestedForParameterWindowActivation = newSymbolChild.Id,放完即可调参(L638)。
面板自身的布局也有细节:搜索框下方 40 像素处渲染结果列表(BrowserPositionOffset),列表基准尺寸为250×300 × (2,1) × UiScaleFactor(L648-L656);ClampPanelToCanvas会把超出窗口右缘/下缘的面板回移或压缩(L404-L419);按住搜索框拖动还能整体平移浏览器位置(DrawSearchInput中的IsMouseDragging分支,L239-L242)。取消操作同样宽松:Esc、右键或点击窗口外都会Cancel(),内部调用ConnectionMaker.AbortOperation丢弃草稿连接(L223-L255)。
六、预设面板与描述/示例预览
文档提到放置前可预览算子的产出。除列表项本身的类型色预览外,源码还确认了右侧辅助面板(从源码结构看,这是浏览器提供“预览/上下文”的主要载体):
- 描述与示例面板
DrawDescriptionPanel(SymbolBrowser.cs#L473-L513):当高亮算子有Description或通过ExampleSymbolLinking.ExampleIdsForSymbolsId关联到示例算子时,在结果列表旁渲染说明文字与 "Example" 条目,帮助用户在放置前了解该算子能做什么; - 预设面板
DrawPresetPanel(SymbolBrowser.cs#L423-L471):当使用两段式搜索(如noise grain)且算子存在 Variation 预设时,列出标题包含预设过滤词的条目;点击即创建实例并应用该预设(CreateInstance中presetPool.Apply生效)。
面板定位由TryFindValidPanelPosition保证不越出窗口右缘,放不下时翻转到左侧(L515-L534)。
七、关键文件索引
| 关注点 | 文件 |
|---|---|
| 浏览器 UI、Tab 唤起、放置与创建流程 | SymbolBrowser.cs |
| 查询编译、类型过滤、相关度排序 | SymbolFilter.cs |
| 使用量/连接频率统计(排序数据源) | SymbolAnalysis.cs |
| 浏览器实例化入口 | GraphView.cs |
| Tab 单选内联插入与临时连接管理 | ConnectionMaker.cs |
| 官方行为描述(本文主体依据) | SymbolBrowser.md |
掌握以上内容后,你可以完整解释 t3 图中“长按画布 → 输入 LFO 命中振荡器 → 列表按使用频率排序 → 回车落点并自动连线”这一整条高频工作流:查询如何被编译为字符级模糊正则、拖线为何只剩“接得上”的算子、以及SymbolAnalysis的用量与连接统计如何把常用算子推到列表顶端。
【免费下载链接】t3TiXL is an open source software to create realtime motion graphics.项目地址: https://gitcode.com/GitHub_Trending/t3/t3
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考