znlgis 博客

GIS开发与技术分享 — GDAL · GeoServer · PostGIS · QGIS · OpenLayers · Cesium · FreeCAD · NPOI

第09章:源码架构、事件系统与开发环境搭建

从本章起进入开发篇。我们将搭建 SuperSplat 的本地开发环境,并系统剖析它的整体架构、事件驱动机制与构建流程,为下一章的二次开发打基础。

1. 搭建本地开发环境

按官方 README 的步骤即可。前置要求:安装 Node.js 20.19.0 或更高版本package.jsonengines.node 要求 >=20.19.0)。

# 1. 克隆仓库
git clone https://github.com/playcanvas/supersplat.git
cd supersplat

# 2. 安装依赖
npm install

# 3. 构建并启动本地开发服务器
npm run develop

npm run develop 实际执行(见 package.json):

"develop": "cross-env BUILD_TYPE=debug concurrently --kill-others \"npm run watch\" \"npm run serve\""

即同时启动 Rollup 监听构建(watch)本地静态服务器(serve dist)。源代码变更会被自动重新构建,刷新浏览器即可看到效果。

启动后访问:http://localhost:3000

重要:调试时务必关闭缓存。SuperSplat 注册了 Service Worker(src/sw.ts),开发时需在浏览器开发者工具中启用 “Update on reload” 与 “Bypass for network”(Chrome 的 Application → Service Workers),或清空缓存,否则可能看到旧版本。

其他脚本:

  • npm run build:生产构建(rollup -c);
  • npm run watch:仅监听构建;
  • npm run serve:仅启动静态服务器;
  • npm run lint:ESLint 检查 src

2. 应用启动流程(main.ts)

src/main.ts 是应用入口,负责按顺序装配整个编辑器。其核心步骤大致是:

  1. 创建图形设备:用 PlayCanvas 的 createGraphicsDevice 初始化 WebGL2/WebGPU;
  2. 创建事件总线const events = new Events()
  3. 创建场景new Scene(events, ...)
  4. 注册各模块事件:调用一系列 registerXxxEvents(events, ...),包括:
    • registerEditorEvents(编辑器核心,editor.ts);
    • registerSelectionEventsregisterRenderEventsregisterTimelineEventsregisterPublishEventsregisterDocEventsregisterCameraPosesEventsregisterTrackManagerEventsregisterTransformHandlerEvents 等;
  5. 创建工具管理器并注册工具new ToolManager(events),注册 BoxSelectionBrushSelectionRectSelectionLassoSelectionPolygonSelectionSphereSelectionFloodSelectionEyedropperSelectionMoveToolRotateToolScaleToolMeasureTool 等;
  6. 创建编辑历史new EditHistory(events)
  7. 初始化快捷键new ShortcutManager(events)
  8. 初始化国际化localizeInit()
  9. 构建 UInew EditorUI(events, ...)
  10. 初始化文件处理、iframe API 等。

可见整个应用通过一个共享的 events 实例把彼此解耦的模块粘合起来——这是理解全部源码的钥匙。

3. 事件驱动架构(events.ts)

src/events.ts 定义的 Events 类继承自 PlayCanvas 的 EventHandler,并在其基础上扩展了函数注册/调用机制:

class Events extends EventHandler {
    functions = new Map<string, FunctionCallback>();

    // 注册一个可被查询的函数
    function(name: string, fn: FunctionCallback) {
        if (this.functions.has(name)) {
            throw new Error(`error: function ${name} already exists`);
        }
        this.functions.set(name, fn);
    }

    // 调用已注册的函数并返回结果
    invoke(name: string, ...args: any[]) {
        const fn = this.functions.get(name);
        if (!fn) {
            console.log(`error: function not found '${name}'`);
            return;
        }
        return fn(...args);
    }
}

因此 SuperSplat 中存在两套通信方式,理解二者区别至关重要:

机制 API 语义 用途
事件(继承自 EventHandler) events.on(name, cb) / events.fire(name, ...) 广播,无返回值,多订阅者 通知“发生了某事”(如 scene.cleartimeline.frame
函数(扩展) events.function(name, fn) / events.invoke(name, ...) 查询,有返回值,单一提供者 获取状态/服务(如 scene.dirtyselectiontool.active

例如:

// 注册“当前选中模型”查询
const selected = events.invoke('selection') as Splat;

// 监听场景清空事件
events.on('scene.clear', () => { /* ... */ });

// 触发隐藏选区
events.fire('select.hide');

这种“事件 + 函数”的混合总线让模块之间零直接依赖:UI 只管 fire/invoke,具体逻辑由各 register*Events 提供,便于扩展与测试。

4. 场景与元素(scene.ts / element.ts)

src/scene.tsScene 基于 PlayCanvas 应用(PCAppsrc/pc-app.ts),管理一组 Elementsrc/element.ts)。元素类型(ElementType)包括高斯模型(Splat)、相机(Camera)、网格(grid)、叠加层等。

Scene 的关键职责:

  • 维护元素列表,提供添加/移除(移除时 fire('scene.elementRemoved', element));
  • 驱动每帧更新与渲染;
  • 高斯泼溅的深度排序:源码 specialSort 按每个 meshInstance 包围盒 8 个角中沿相机视线方向最远的距离排序,保证半透明高斯的正确叠加顺序(自定义排序模式 SORTMODE_CUSTOM)。

高斯模型封装在 src/splat.tsSplat 类中,内部持有 PlayCanvas 的 GSplat 资源与每点状态(splat-state.ts)。

5. 编辑操作与历史(edit-ops.ts / edit-history.ts)

所有可撤销的修改都实现统一的 EditOp 接口:

interface EditOp {
    name: string;
    do(): void | Promise<void>;
    undo(): void | Promise<void>;
    destroy?(): void;
}

edit-ops.ts 中定义了大量操作:SelectAllOpSelectNoneOpSelectInvertOpSelectOpHideSelectionOpUnhideAllOpDeleteSelectionOpResetOpAddSplatOpMultiOp(组合多个操作)等。底层的 StateOp 通过位运算(SET/CLEAR/TOGGLE)批量修改高斯点状态位:

class StateOp {
    private apply(op: BitOp) {
        const { state } = this.splat;
        switch (op) {
            case BitOp.SET:    state.setBits(this.ranges, this.mask); break;
            case BitOp.CLEAR:  state.clearBits(this.ranges, this.mask); break;
            case BitOp.TOGGLE: state.toggleBits(this.ranges, this.mask); break;
        }
    }
}

EditHistoryedit-history.ts)维护 do/undo 栈,Ctrl+Z / Ctrl+Shift+Z 即调用栈中操作的 undo() / do()。新增任何编辑功能时,只需实现 EditOp 并交给 EditHistory,即可自动获得撤销/重做能力。

6. UI 层(ui/ 目录,基于 PCUI)

界面用 @playcanvas/pcui 组件库构建。src/ui/editor.tsEditorUI 是 UI 骨架,组装菜单、工具栏、各类面板。每个面板(如 data-panel.tscolor-panel.tstimeline-panel.ts):

  • 用 PCUI 的 ContainerLabelSliderInputSelectInputBooleanInputColorPicker 等构建控件;
  • 通过 events.fire/events.invoke 与业务逻辑通信,自身不直接持有业务对象。

例如颜色面板的滑块改变时,fire 一个事件,由 color-grade 逻辑响应——UI 与逻辑彻底分离。

7. 国际化(localization.ts)

src/ui/localization.tsi18next 实现多语言,语言文件位于 static/locales/*.json

i18next.use(Backend).use(LanguageDetector).init({
    detection: { order: ['querystring', 'navigator', 'htmlTag'] },
    backend: { loadPath: './static/locales/.json' },
    supportedLngs: ['de', 'en', 'es', 'fr', 'ja', 'ko', 'pt-BR', 'ru', 'zh-CN'],
    fallbackLng: 'en'
});

UI 中所有文案通过 localize('some.key') 获取。注意已内置简体中文 zh-CN。可用 URL 参数 ?lng=zh-CN 强制指定语言进行测试。

8. 构建流程(Rollup + SCSS + TS)

SuperSplat 用 Rollup(rollup.config.mjs)打包:

  • @rollup/plugin-typescript 编译 TypeScript;
  • @rollup/plugin-node-resolve 解析依赖;
  • rollup-plugin-scss 编译 SCSS 样式(src/ui/scss/);
  • @rollup/plugin-image@rollup/plugin-json 处理图片与 JSON;
  • 生产构建用 @rollup/plugin-terser 压缩,@rollup/plugin-strip 去除调试代码;
  • BUILD_TYPE=debug 环境变量切换调试/生产模式。

输出到 dist/,由 serve 提供静态服务。

9. 本章小结

本章搭建了开发环境并剖析了架构:

  • 用 Node ≥20.19 + npm install + npm run develop 启动本地开发,注意关闭 Service Worker 缓存;
  • main.ts 用一个共享 events 总线装配所有模块;
  • Events 提供“事件(on/fire,广播)”与“函数(function/invoke,查询)”两套机制,实现模块解耦;
  • Scene 管理 Element,并对高斯做自定义深度排序;
  • 编辑操作统一实现 EditOp 接口并由 EditHistory 管理撤销/重做;
  • UI 基于 PCUI,与逻辑通过事件解耦;国际化用 i18next;构建用 Rollup。

下一章我们动手二次开发:新增工具、自定义编辑操作、对接 iframe API、增加语言并集成到自有产品。


← 上一章 目录 下一章 →