three.js Inspector 扩展指南:深入解析 Tab 标签页基类与自定义面板开发
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
three.js 官方示例中内置了一个可直接叠加到渲染器上的 Inspector(性能剖析器)UI 套件,位于examples/jsm/inspector。本文以其 API 参考文档(docs/pages/Tab.html.md)为主线,深入剖析该套件中所有标签页(Performance、Memory、Console、Viewer 等)的共同基类Tab的构造函数、选项参数、状态机与生命周期方法,并结合 Tab.js、Profiler.js、Inspector.js 等源码,讲清楚如何利用Tab派生自定义标签页、内置(builtin)迷你面板以及"拖拽分离成独立窗口"的完整机制。读完本文,你将能够编写并注册属于自己的 Inspector 扩展标签页。
一、Tab 在整个 Inspector 架构中的位置
在深入构造函数之前,先明确Tab的调用关系。从源码结构看,Inspector 的 UI 体系是三层嵌套:
- Inspector(examples/jsm/inspector/Inspector.js):对外暴露的入口类,提供
addTab()、removeTab()、setActiveTab()等高层 API; - Profiler(examples/jsm/inspector/ui/Profiler.js):负责管理 DOM 外壳(切换按钮、标签栏、内容面板、mini-panel),维护
tabs字典与activeTabId; - Tab(examples/jsm/inspector/ui/Tab.js):单个标签页的抽象基类,负责创建自己的标签按钮与内容容器,并实现显示/隐藏、内置(builtin)迷你化、拖拽分离等行为。
在 Inspector.js 的构造函数中,七个默认标签页被依次创建并注册:
const parameters = new Parameters( { builtin: true, icon: '<svg>...</svg>' } ); parameters.hide(); profiler.addTab( parameters ); const viewer = new Viewer(); viewer.hide(); profiler.addTab( viewer ); const performance = new Performance(); profiler.addTab( performance ); const memory = new Memory(); profiler.addTab( memory ); const timeline = new Timeline(); profiler.addTab( timeline ); const consoleTab = new Console(); profiler.addTab( consoleTab ); const settings = new Settings(); profiler.addTab( settings );其中 Performance.js、Memory.js、Console.js、Timeline.js、Viewer.js、Parameters.js 中的各个标签页类均直接以class X extends Tab方式派生,可见Tab就是整个 Inspector 扩展生态的"基座"。
二、构造函数new Tab( title, options )与选项参数
构造签名如下(对应文档 Tab.html 的 Constructor 章节,完整实现见 Tab.js):
new Tab( title : string, options : Object )2.1title—— 标签标题
标签的显示标题,类型为字符串。构造函数会根据标题自动派生标签的内部唯一 ID:
this.id = title.toLowerCase().replace( /\s+/g, '-' );即标题转小写、空白替换为连字符,例如'My Stats'会得到id = 'my-stats'。该 id 同时被用作:
Profiler.tabs字典的键(profiler.tabs[ tab.id ]);- 内容容器的附加 class(
this.content.classList.add( \${ this.id }-content` )`)。
2.2options—— 选项
options为可选对象,所有字段均有默认值。构造函数通过options.x !== undefined ? options.x : defaultValue的方式读取,因此显式传入undefined或省略都会回退到默认值:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
allowDetach | boolean | true | 标签是否允许被拖拽分离为独立的浮窗(detached window) |
builtin | boolean | false | 该标签是否出现在 profiler-toggle 切换按钮中,作为"内置快捷标签"使用 |
icon | string | null | 当builtin: true时,切换按钮上使用的 SVG 图标 HTML |
三者的作用机制在源码中有明确对应:
allowDetach直接决定 Profiler.js 的addTab与拖拽逻辑:为false时按钮会加上no-detachclass,且setupTabDragAndDrop会提前返回、禁用拖拽;detachTab方法内部也有if ( tab.allowDetach === false ) return;的保护判断(Profiler.js);builtin决定标签按钮是否加入主面板标签栏:非 builtin 标签的按钮被追加进.profiler-tabs,builtin 标签则走addBuiltinTab流程,其按钮(.builtin-tab-btn)被放进 profiler-toggle 内的.builtin-tabs-container(Profiler.js);icon供 builtin 按钮使用:有icon时将其直接写入按钮innerHTML,否则退化为取标题首字母大写作为按钮内容。
2.3 构造时创建的 DOM 结构
构造函数会为每个 Tab 创建一对 DOM 节点并挂到实例上:
this.button = document.createElement( 'button' ); // class: 'tab-btn' this.content = document.createElement( 'div' ); // class: 'profiler-content {id}-content'button承载标签标题文本,用于点击切换与拖拽分离;content是标签页内容的容器,所有自定义 UI 都应追加到this.content上。二者真正进入文档树则是在Profiler.addTab()中被分别挂到.profiler-tabs与.profiler-content-wrapper之下。
三、Tab 的运行时状态与只读属性
构造函数还会初始化一组状态字段(Tab.js),理解它们对正确使用show/hide/isActive至关重要:
| 字段 | 初始值 | 含义 |
|---|---|---|
_isActive | false | 是否处于激活态(真正的"可读"状态由下方 getter 决定) |
isVisible | true | 标签是否对用户可见 |
isDetached | false | 标签是否已被拖拽为独立浮窗 |
detachedWindow | null | 分离后的浮窗对象引用 |
builtinButton | null | 指向 profiler-toggle 中内置按钮的引用 |
miniContent | null | 指向 mini-panel 中对应迷你内容容器的引用 |
profiler | null | 所属 Profiler 实例引用(addTab时注入) |
onVisibilityChange | null | 可见性变化回调(addTab时被赋值为Profiler.updatePanelSize) |
值得特别注意的isActive访问器(Tab.js):
get isActive() { if ( this.isDetached && this.isVisible ) return true; const isProfilerVisible = this.profiler && this.profiler.panel.classList.contains( 'visible' ); if ( ! isProfilerVisible ) return false; return this._isActive; }它有两层语义:
- 分离态的标签:只要
isDetached && isVisible,即使主面板折叠也视为激活(因为内容显示在浮窗中); - 常规态的标签:主面板不可见(没有
visibleclass)时一律视为非激活,只有主面板可见时才返回内部_isActive。
因此诸如 Console.js 中! this.isActive(判断是否有未读消息)、Timeline.js 中"仅激活时采样/绘图"等逻辑都会自动兼容面板折叠与标签分离两种场景。实例上还暴露了inspector便捷 getter:return this.profiler.inspector;,用于从标签内直接访问 Inspector 主对象。
四、生命周期方法:init / update / setActive / dispose
Tab提供了两个默认空实现的可覆写钩子与一组公开方法:
init( /*inspector*/ ) { } update( /*inspector*/ ) { } dispose() { }它们的调用时机来自 Inspector 的帧循环。在 Inspector.js 的updateTabs()中,每一帧都会遍历profiler.tabs:
const tabs = Object.values( this.profiler.tabs ); for ( const tab of tabs ) { let tabData = this.extensionsData.get( tab ); if ( tabData === undefined ) { tab.init( this ); // 首次注册时只调用一次 tabData = {}; this.extensionsData.set( tab, tabData ); } tab.update( this ); // 每帧调用 }即:init每个标签只执行一次(首次进入更新循环时),update每帧执行一次。派生类通常在init中做一次性 DOM/数据装配,在update中按isActive条件刷新数据,避免后台标签空耗性能。例如内置的 Parameters.js 在构造函数里就建好List,而各性能类标签在update中刷新图表。dispose()默认也为空实现,在Inspector.removeTab()中会先调用它再做 DOM 清理(Inspector.js),派生类如 Extension.js 会用它移除事件监听、释放引用。
setActive( isActive )
切换激活态并同步 CSS class:
setActive( isActive ) { this.button.classList.toggle( 'active', isActive ); this.content.classList.toggle( 'active', isActive ); this.isActive = isActive; }注意它写的是_isActive(通过 setter),而读取方应使用isActivegetter 以获得"考虑面板可见性与分离态"后的真实值。派生类(如 ColorGrading.js 与 Console.js)通常在覆写时先super.setActive( isActive )再处理自身逻辑(如激活时才启动采样)。
五、显示 / 隐藏:show()、hide() 与内置迷你面板
5.1 常规 show / hide
show()(Tab.js)与hide()(Tab.js)成对出现,行为包括:
- 切换
content与button的display样式并更新isVisible; - 若标签处于分离态,同步显示/隐藏其
detachedWindow.panel; - 触发
onVisibilityChange()回调(即Profiler.updatePanelSize,让主面板在所有标签都隐藏时收缩到最小尺寸); - 末尾调用
showBuiltin()/hideBuiltin()同步内置按钮状态。
5.2 builtin 标签与迷你面板的换入换出
这是builtin: true标签特有的行为。当主面板被折叠后,builtin 标签的内容会被"搬"到 profiler-toggle 按钮旁边的 mini-panel(.profiler-mini-panel)中,实现轻量级快捷面板。
showBuiltin()(Tab.js)做五件事:
- 让
profiler.builtinTabsContainer(位于 toggle 按钮内)可见; - 显示自己的
builtinButton; - 隐藏 mini-panel 中其他所有
.mini-panel-content,并清除所有.builtin-tab-btn的activeclass; - 将
this.content的子节点逐个子树整体搬移到miniContent(仅在miniContent为空时执行:while ( this.content.firstChild ) miniContent.appendChild( ... )); - 显示
miniContent并给 mini-panel 加上visibleclass。
hideBuiltin()(Tab.js)执行相反操作:隐藏按钮、把子节点从miniContent搬回content、移除activeclass,并且当 mini-panel 中已无可见内容或 toggle 中已无可见内置按钮时,自动隐藏对应容器。这种"内容在主面板与迷你面板之间搬家"的设计,保证同一份 DOM 内容不会在两个位置重复渲染。
Profiler.show(tab)/Profiler.hide()(Profiler.js)是 builtin 按钮点击后的实际调度方,点击.builtin-tab-btn时会在"展开该迷你面板"与"收起"之间切换,并通过e.stopPropagation()避免误触发整个面板的 toggle。
六、与 Profiler 的协作:注册、拖拽分离与移除
6.1Profiler.addTab注册流程
无论是内置标签还是扩展标签,最终都要经由Profiler.addTab( tab )(Profiler.js)完成注册,其内部顺序值得关注:
- 以
tab.id为键存入this.tabs,并分配tab.originalIndex(记录添加顺序,供布局持久化排序用); allowDetach === false时给按钮加no-detachclass(UI 上的锁定视觉指示);- 把
tab.onVisibilityChange绑定为() => this.updatePanelSize(); - 调用
setupTabDragAndDrop( tab )注册点击与拖拽事件; - 非 builtin 标签:按钮追加到
.profiler-tabs;builtin 标签:按钮与迷你内容容器由addBuiltinTab创建并挂到 toggle / mini-panel; - 无论哪种类型,
tab.content都统一追加到contentWrapper,并同步当前isVisible状态到 DOM; - 注入
tab.profiler = this; - 若
tab.id与上次持久化布局中记录的activeTabId一致,立即setActiveTab。
作为入口封装,Inspector.addTab( tab )(Inspector.js)直接委托给profiler.addTab并返回this支持链式调用。
6.2 拖拽分离(detach)与回嵌(reattach)
分离交互同样由 Profiler 承载。setupTabDragAndDrop中设置了10px 位移阈值:指针按下后移动超过阈值才判定为拖拽,否则视为普通点击切换激活。拖拽过程中会创建一个半透明"预览窗口"跟随指针(Profiler.js),松手后调用detachTab( tab, x, y )(Profiler.js):
- 再次校验
allowDetach(双保险); - 若被分离的是当前激活标签,则按"先左邻后右邻"的策略选择新的激活标签;
- 把按钮与内容从主面板 DOM 中摘除,调用
createDetachedWindow生成独立浮窗(默认约 400×300,初始位置会被约束在视口内),并推入detachedWindows数组管理; - 置
tab.isDetached = true、tab.detachedWindow = detachedWindow,随后updatePanelSize()与saveLayout()持久化布局。
浮窗标题栏上会提供 reattach 按钮(reattachTab),将标签重新放回主面板。此外 Profiler 在window.resize时会对所有分离浮窗执行constrainWindowToBounds,允许浮窗最多一半越出屏幕边缘。
6.3removeTab与清理
Profiler.removeTab会移除 tabs 字典项、按钮、miniContent、内容容器;若该标签当前激活,会尝试激活剩余的第一个可见非分离标签;最后清空onVisibilityChange与profiler引用(Profiler.js)。完整的"先释放再移除"顺序由Inspector.removeTab保证:先tab.dispose()再由 Profiler 做 DOM 清理。
七、内置标签如何用这三个选项:以 Parameters 为例
Parameters标签是builtin + icon选项组合的典型实例(Inspector.js):
const parameters = new Parameters( { builtin: true, icon: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" ...>...</svg>' } ); parameters.hide(); profiler.addTab( parameters );含义解读:
builtin: true:它的标签按钮不占用主标签栏,而是作为一个带图标的快捷按钮常驻在profiler-toggle(FPS 切换按钮)内,即使主面板收起,也能点击展开迷你面板快速查看/编辑参数;icon:自定义的滑块类 SVG 图标会直接作为该 builtin 按钮的 HTML 内容(若缺省,则会退化为显示标题首字母);parameters.hide():注册前先隐藏,使布局恢复时保持收起状态,而 builtin 按钮仍可被随时唤起(Profiler.addTab会把isVisible = false同步为隐藏 builtin 按钮与迷你内容,见 Profiler.js)。
这也是 Tab.js 注释中展示的推荐用法:
// 可分离标签(默认行为) const tab1 = new Tab( 'My Tab' ); // 不可分离的固定标签 const tab2 = new Tab( 'Fixed Tab', { allowDetach: false } ); // 出现在 profiler-toggle 中的内置标签 const tab3 = new Tab( 'Builtin Tab', { builtin: true } ); // 带自定义图标的内置标签 const tab4 = new Tab( 'Settings', { builtin: true, icon: '<svg>...</svg>' } ); // 控制内置标签的可见性 tab3.showBuiltin(); tab3.hideBuiltin();八、编写自己的扩展标签页
扩展标签页有两种层次:直接extends Tab的自定义标签,以及基于 Inspector 扩展机制的Extension。
8.1 最简单的自定义 Tab
参考 Memory.js、Console.js 的写法:
import { Tab } from 'three/addons/inspector/ui/Tab.js'; export class MyStatsTab extends Tab { constructor( options = {} ) { super( options.name || 'My Stats', options ); this.output = document.createElement( 'div' ); this.content.appendChild( this.output ); } update( inspector ) { // 面板折叠或分离态之外的"真激活"判断 if ( this.isActive ) { this.output.textContent = `active tab update at frame ${ inspector.frameId }`; } } }使用方拿到 Inspector 实例后即可注册:
import { Inspector } from 'three/addons/inspector/Inspector.js'; import { MyStatsTab } from './MyStatsTab.js'; const inspector = new Inspector( renderer, app ); // 按 Inspector 实际构造方式初始化 const tab = new MyStatsTab( { allowDetach: false } ); inspector.addTab( tab );要点回顾:DOM 一律挂在this.content;首次装配逻辑放init,每帧刷新放update;读激活状态用isActivegetter;allowDetach: false可做成不可分离的"固定页签"。
8.2 带持久化的 Extension 扩展
需要随 Inspector 布局一起持久化配置的扩展,应继承 examples/jsm/inspector/Extension.js 中export class Extension extends Tab。Extension在init阶段自动接入 Inspector 的resize/layoutchange/orientationchange事件,并从 localStorage(getItem(this.name))恢复数据;子类覆写serialize()/deserialize()即可获得配置记忆,save()负责写回。仓库中的两个官方扩展即遵循此模式:
- 调色扩展 color-grading/ColorGrading.js;
- TSL 图编辑器 tsl-graph/TSLGraphEditor.js。
扩展的装载与卸载由 Settings.js 的_loadExtension/_unloadExtension完成:通过动态import解析扩展模块,new ExtensionClass()创建标签,再走inspector.addTab( extensionTab );卸载则调用inspector.removeTab( extension.tab ),触发 Tab.js 中的dispose()钩子做资源清理。
九、小结
Tab虽名为"标签页基类",实际承担了 three.js Inspector 中从 DOM 创建、ID 派生、状态判定到分离浮窗协作的整套基础能力。本文覆盖的要点可归纳为:
- 构造参数:
title(含自动派生id)、options.allowDetach(默认true,控制可否分离)、options.builtin(默认false,控制是否进入 profiler-toggle)、options.icon(builtin 按钮的 SVG 图标); - 派生约定:UI 挂
this.content,装配放init()(仅一次),刷新放update()(每帧),销毁放dispose(),激活判断用isActivegetter; - 与 Profiler 的关系:
addTab完成 DOM 挂载与状态注入,拖拽超 10px 触发分离为独立浮窗,mini-panel 负责主面板收起时 builtin 内容的换入换出; - 扩展两种形态:
extends Tab适合纯自绘标签,extends Extension额外获得配置序列化与 Inspector 事件联动。
如需查阅更精确的 API 签名,可对照官方生成的类文档 docs/pages/Tab.html;动手实验时,可直接阅读 examples/jsm/inspector/ui/Tab.js 以及各内置标签实现,作为编写自定义面板的参照模板。
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考