十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

deck.gl 多视图控制器(Per-View Controllers)设计演进与源码实现解析

deck.gl 多视图控制器(Per-View Controllers)设计演进与源码实现解析 deck.gl 多视图控制器Per-View Controllers设计演进与源码实现解析【免费下载链接】deck.glWebGL2 powered visualization framework项目地址: https://gitcode.com/GitHub_Trending/de/deck.gl本指南以 deck.gl 仓库中的《RFC - Per-View Controllers for Multiple Views》为骨架系统梳理 deck.gl 控制器Controller官方 API 的演进脉络从如何把交互事件限制在某个视图的区域内并正确映射坐标到多视图各自独立控制、多控制器并存再到源码中Controller基类、ViewManager路由机制的落地实现。读完本文你将掌握 deck.gl 多视图场景下控制器的配置方式viewId、view.controller、事件坐标映射原理以及从 RFC 到代码的完整对应关系。一、背景deck.gl 为什么需要官方的控制器 API在 deck.gl 早期版本中控制器Controller功能虽然已经存在但缺乏一个正式、统一的 API。它被拆散在若干内部组件如ViewportController及其 React 封装中开发者难以在纯 JS 场景下声明式地选择用哪个控制器、控制哪个视图。这份 per-view-controllers-rfc.md 于 2018 年 1 月提出作者 Ib Green状态Implemented目标就是为 deck.gl 的控制器功能提供一个官方入口。RFC 提出时多视图Multi-View已经能够渲染多个 viewport但控制器与 viewport 之间缺乏绑定关系控制器默认监听整个 canvas 的事件无法感知自己对应视图的尺寸与位置。由此引出了两大核心特性与若干前瞻性设计最终沉淀为当前modules/core中的控制器架构。二、核心特性一将控制器事件处理限制在视图区域内viewId2.1 需求本质RFC 指出控制器需要能够将事件处理限制在某个视图对应的屏幕区域内。原因在于不同控制器的交互语义不同某些控制器如只识别上下拖拽这类通用手势的控制器对事件坐标是否落在自身视图内并不敏感而地图控制器MapController则完全不同——鼠标点下的位置代表了抓取点或参考点尤其对平移panning和缩放zooming而言控制器必须看到相对自身 viewport、且被自身 viewport 约束的事件。换句话说事件坐标的正确映射直接影响交互体验控制器不应接收其 viewport 之外的事件坐标至少手势起点不能在外面但手势在控制器内部开始后应当允许继续到 viewport 之外、甚至窗口之外拖拽连续性。2.2 配置方式viewIdRFC 给出的示例是当承载MapView的地图并未铺满整个 canvas而应用希望使用与之匹配的 MapController 时只需在控制器上指定viewIdconst deck new Deck({ ..., views: [new MapView({id: map, height: 50%})], controller: [new MapController({viewId: map})], viewState: ... });RFC 同时给出了两条补充说明若控制器未指定viewId其默认尺寸可以回退到第一个 viewport或全屏控制器的显式尺寸配置理论上可以用百分比实现但当时并未列入计划。2.3 源码落地从viewId到View上的controller属性在实现时这一设计演化为更简洁的形式控制器不再通过独立数组 viewId绑定而是直接挂载在 View 上。查看 view.ts每个 View 的 props 都有一个controller字段支持四种取值null禁用交互默认true启用该 View 默认控制器如MapView默认使用MapController见 map-view.ts 中的get ControllerType()控制器类构造函数使用自定义控制器类对象控制器选项其中type指定控制器类其余选项合并进默认配置。View基类通过get controller()getterview.ts统一解析这三种写法最终产出{type, ...options}结构。这也是现代 deck.gl 的推荐写法const deck new Deck({ views: [ new MapView({ id: map, height: 50%, controller: true // 等价于 new MapController(...) }) ], viewState: {...} });三、核心特性二控制器事件处理匹配视图位置当目标 viewport 的原点不在 canvas 原点时例如y: 50%使视图位于下半屏控制器也必须能够正确处理。RFC 给出如下示例const deck new Deck({ ..., views: [new MapView({id: map, y: 50%, height: 50%})], controller: [new MapController({viewId: map})], viewState: ... });3.1 源码实现控制器拿到的是 viewport 的局部坐标系从源码结构看事件坐标的归零发生在ViewManager构建控制器之时。查看 view-manager.ts_updateController会把 viewport 的布局信息注入控制器 propsconst resolvedProps { ...viewState, ...controllerProps, id: view.id, x: viewport.x, y: viewport.y, width: viewport.width, height: viewport.height };也就是说每个控制器获得的x、y、width、height就是其关联 viewport 在 canvas 上的位置与尺寸。随后 controller.ts 的getCenter用 viewport 原点对事件坐标做平移getCenter(event: MjolnirGestureEvent | MjolnirWheelEvent): [number, number] { const {x, y} this.props; const {offsetCenter} event; return [offsetCenter.x - x, offsetCenter.y - y]; }isPointInBoundscontroller.ts则完成事件是否落在自身 viewport 内的判定坐标为负或超出width/height即视为界外并拒绝响应反之则调用event.stopPropagation()防止上层视图的控制器抢先处理。isPointInBounds(pos: [number, number], event: MjolnirEvent): boolean { const {width, height} this.props; if (event event.handled) { return false; } const inside pos[0] 0 pos[0] width pos[1] 0 pos[1] height; if (inside event) { event.stopPropagation(); } return inside; }这正是 RFC 所要求的手势起点必须在 viewport 内、手势一旦开始则可越界继续的落点起点事件panstart、pinchstart、dblclick、wheel全部经过isPointInBounds校验而进行中的移动事件panmove、pinchmove等只检查isDragging()状态不再校验坐标是否越界。3.2 事件注册与反向遍历ViewManager._rebuildViewportsview-manager.ts还有一个值得注意的细节控制器按视图逆序创建for (let i views.length; i--; )目的是让位于上层的视图优先收到事件。当一个新控制器加入时还会失效finalize其下方所有旧控制器并重新按正确顺序挂载避免事件注册顺序错乱导致上层视图收不到交互。四、前瞻设计多控制器并存与控制器切换4.1 多控制器支持RFC 设想拥有多个 viewport 的应用可能希望每个 viewport 使用不同的交互方式。例如上方是地图、下方是第一人称视图两者需要不同的控制器const deck new Deck({ ..., views: [ new MapView({id: map, height: 50%}), new FirstPersonView({id: firstPerson, y: 50%, height: 50%}) ], controllers: [new MapController({viewId: map}), FirstPersonView({id: first-person})], viewStates: {map: {}, firstPerson: {}} // TBD conceptual - might not be a map });RFC 特别提醒多个viewState必须彼此兼容即每个控制器输出的 view state 形态与对应 View 期望的一致。这一设计在现代 deck.gl 中同样被controller 挂在 View 上的模型吸收——每个 View 都可以独立声明自己的控制器类型与选项天然支持不同视图不同交互。与此对应的多 viewState 分发逻辑可以在 deck.spec.ts 的测试中看到三个MapViewdefault、map、minimap各自拥有独立的 viewId 与 viewStateonViewStateChange回调按viewId区分来源并分别更新互不干扰。4.2 控制器切换RFC 还讨论了在 viewport 之间切换时如何切换控制器核心难点在于要么选择支持相同形状viewState的 viewport 与控制器组合要么提供 viewState 转换函数要么为每个目标维护独立的viewState。这一思路后来演化为 deck.gl 的viewState/initialViewState受控与非受控切换机制以及不同 View 之间共享兼容 viewState 的实践。五、事件类型全景Controller 基类如何消费手势RFC 只定义了控制器应被限制在视图内的边界而真正的手势处理逻辑由 controller.ts 中的Controller抽象基类承担。该基类是理解整个控制器架构的关键事件类型清单controller.ts滚轮wheel、拖拽pan*、双指缩放pinch*、双指平移multipan*、双击dblclick、双击拖拽dblclickdrag*、键盘keydown统一入口handleEventcontroller.ts按事件类型分发给_onPanStart、_onPan、_onPinch、_onWheel、_onDoubleClick、_onKeyDown等私有处理方法交互选项ControllerOptionscontroller.tsscrollZoom、dragPan、dragRotate、doubleClickZoom、doubleClickDragZoom、touchZoom、touchRotate、multiTouchDrag、trackpadGesture、keyboard、dragMode、zoomAround、inertia、maxBounds、maxBoundsPadding、rubberBand等可在Deck的controllerprop 或 View 的controllerprop 中直接配置。各视图控制器类继承自该基类并通过ControllerState描述视图状态如何随手势变化控制器类源码路径对应视图状态类MapControllermap-controller.tsMapViewMapStateFirstPersonControllerfirst-person-controller.tsFirstPersonViewFirstPersonStateOrbitControllerorbit-controller.tsOrbitViewOrbitStateOrthographicControllerorthographic-controller.tsOrthographicViewOrthographicStateGlobeControllerglobe-controller.tsGlobeViewGlobeStateTerrainControllerterrain-controller.tsMapView地形场景MapState以MapController为例其transition配置map-controller.ts内建了 300ms 的默认转场时长与LinearInterpolator并对longitude/latitude/zoom/bearing/pitch/position做插值——这直接回应了 RFC 中转场Transitions的关切。六、关注点回响转场与 react-map-gl 对齐6.1 转场的收敛RFC 指出转场此前一部分由ViewportController及其 React 封装处理将 React 侧的代码并入DeckJS 组件是移植转场的第一步。这一规划在仓库中已完全落地TransitionManagertransition-manager.ts与ViewStateview-state.ts成为控制器内部一等公民所有控制器共享同一套转场管理逻辑Controller.setProps会调用transitionManager.processViewStateChange(props)controller.ts处理 view state 变化ViewManager.updateViewStates()周期性驱动每个控制器的updateTransition()view-manager.tsDeck组件在旧版 API 下还保留了向后兼容当用户通过Deck.controller传递控制器时会自动把它克隆到第一个视图上deck.ts。6.2 react-map-gl 对齐RFC 担心deck.gl 与 react-map-gl 在控制器上的分歧越深bug 的回移植就越困难。方案是共享 mjolnir.js 的基础事件处理层本仓库确实以mjolnir.js的EventManager为事件底座见 controller.ts 的引入同时要求任何控制器改动需经两个仓库维护者共同接受。这份谨慎后来演化为 deck.gl 独立演进、仅与 mapbox/maplibre 生态保持交互协议兼容的局面。七、实施计划回顾RFC 的两阶段路线RFC 末尾给出了明确的推进计划可作为理解代码演进时间线的索引Phase 1 —— 用既有ViewState类打通Deck.controllerAPI将ViewportControllerReact 封装折叠进DeckJS 组件打通转场transitions允许应用通过Deck.controller选择旧的ViewState子类。Phase 2 —— 合并新的Controller类层次结构引入以Controller为基类、MapController/OrbitController等为具体实现的层级并配套ControllerState状态机。对照当前 controllers 目录 可以看到Phase 2 的新控制器层次正是今天controller.tsview-state.ts 各视图控制器类的最终形态而Deck.controller的兼容分支deck.ts则保留了 Phase 1 的遗产。控制器相关的完整测试覆盖在 controllers.spec.ts含MapView({controller: true})、multiTouchDrag、maxBoundsrubberBand、scrollZoom.smooth、OrthographicView 的padding等用例与 deck.spec.ts多视图 viewState 路由中。八、总结与实践建议这份 RFC 定义了 deck.gl 控制器体系的两个核心原则且都在源码中得到了完整验证事件坐标始终相对控制器所属的 viewportViewManager将 viewport 的x/y/width/height注入控制器getCenter做坐标归零isPointInBounds约束手势起点从而保证多视图布局如上下分屏、小地图叠层下交互不串台控制器与 View 一一绑定view.controller支持true/ 类 / 选项对象三种写法每个视图可独立配置交互天然支持不同视图、不同控制器、不同 viewState的复合应用。实操要点单视图全屏交互直接new Deck({controller: true, ...})或views: [new MapView({controller: true})]多视图各自交互为每个 View 声明独立的id、布局x/y/width/height与controller配置并在viewState/initialViewState中按 viewId 提供对应状态需要精细化手势时在控制器选项中使用dragMode、zoomAround、inertia、maxBounds、rubberBand等ControllerOptions字段完整字段见 controller.ts定制交互逻辑时继承Controller基类并指定自己的ControllerState参考 custom-controller.spec.ts 的写法。至此从一份 2018 年的 RFC 到今天的控制器实现deck.gl 完成了官方 API、视图绑定、坐标归零、多控制器并存的完整闭环而viewId的概念也在演进中被更内聚的view.controller模型取代——理解这条演进线有助于你在自己的多视图应用中做出正确的控制器架构选择。【免费下载链接】deck.glWebGL2 powered visualization framework项目地址: https://gitcode.com/GitHub_Trending/de/deck.gl创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表