UNPKG

@turbox3d/renderer-core

Version:

Large-scale declarative graphic ui core renderer

795 lines (707 loc) 24.4 kB
/* eslint-disable react/no-unused-class-component-methods */ /* eslint-disable react/no-unused-prop-types */ /* eslint-disable react/require-default-props */ import { CoordinateController, CoordinateType, InteractiveConfig, InteractiveController, InteractiveType, SceneEvent, EventType, ViewEntity } from '@turbox3d/event-manager'; import { remove, Vec2, Vec3, getContextParam, getContextEnv } from '@turbox3d/shared'; import { SceneTool, CommandManager } from '@turbox3d/command-manager'; import { Component, ComponentProps } from './component'; declare global { interface Window { $$turbox_hot: boolean; } } interface IInteractiveControllerInfo<Container, DisplayObject, Renderer> { [id: string]: InteractiveController<Container, DisplayObject, Renderer>; } interface ICanvasInfo<Renderer> { height: number; width: number; renderer: Renderer; } export interface ViewportInfo { x: number; y: number; width: number; height: number; visible: boolean; resolution?: number; } export enum SceneType { Scene2D = '2d', Scene3D = '3d', } export interface BaseSceneProps { /** * 用以标示当前场景的 ID * 当多个不同 id 的场景关联到同一容器中时,可用于区分场景和隔离交互控制器 */ id: string; /** * 用以标示当前场景的类型 */ type?: symbol; /** * 画布容器标识 * 多个不同 id 的场景可关联同一个容器/渲染器上下文 */ container: string; /** * 画布背景色,使用十六进制。默认:0xffffff */ backgroundColor?: number; /** * 画布背景图,优先级高于背景色 */ backgroundImage?: string; /** * 画布背景透明度。默认:1 */ backgroundAlpha?: number; /** * 画布是否可缩放。默认:true */ scalable?: boolean; /** * 画布是否可拖拽。默认:true */ draggable?: boolean; /** * 点击场景未选中任何目标的回调 */ onClickNothing?: (event: SceneEvent) => void; /** * 处理本场景的 commandMgr */ commandMgr?: CommandManager; /** 相机的初始位置 */ cameraPosition?: Vec3 | Vec2; /** 坐标系类型 */ coordinateType?: 'top' | 'front' | 'left'; /** resizeTo 适配的 dom id 或元素引用 */ resizeTo?: string | Window; /** 分辨率 */ resolution?: number; /** 保留绘制缓存数据,用来截图 */ preserveDrawingBuffer?: boolean; /** 颜色输出模式 renderer.outputColorSpace */ outputColorSpace?: string; /** 最大帧率限制 */ maxFPS?: number; /** 禁用 resize */ disableResize?: boolean; /** 渲染标志(用来打开或关闭渲染 ticker,若为 false,则当前帧不渲染) */ renderFlag?: boolean; /** 场景初始化完成的回调 */ initialized?: (sceneTool: SceneTool) => void; /** 场景缩放系数限制([0.1, 2]) */ zoomRange?: number[]; } export interface ViewInfo { position: Vec2 | Vec3; scale: Vec2 | Vec3; visible: boolean; width?: number; height?: number; } export interface SceneContext { getCommandManager: () => CommandManager | undefined; getSceneTools: () => SceneTool; } export abstract class BaseScene< ApplicationContext, Renderer, Scene, Camera, Raycaster, Container, DisplayObject, Viewport > extends Component<BaseSceneProps> { /** 默认背景色 */ static BACKGROUND_COLOR = 0xffffff; /** 默认背景透明度 */ static BACKGROUND_ALPHA = 1; /** 默认画布宽度 */ static DEFAULT_WIDTH = 600; /** 默认画布高度 */ static DEFAULT_HEIGHT = 400; // static DEFAULT_SCALE_NUM = 0.01; static SCALE_BIGGER = 1.1; static SCALE_SMALLER = 1 / 1.1; /** 挂载点与应用上下文的映射关系 */ static appMap = new Map<string, any>(); /** 应用上下文与不同视角交互控制器的映射关系 */ static interactiveMap = new Map<any, IInteractiveControllerInfo<any, any, any>>(); /** 应用上下文是否已经挂载的状态 */ static appMountedStatus = new Map<string, boolean>(); /** 模型实际容器 */ view: Container; /** 默认的场景视图类型标识 */ abstract defaultSceneViewType: symbol; /** 模型视口容器(有多视图的时候才有) */ viewport?: Viewport; /** 场景类型,标识是 2d 还是 3d 场景 */ abstract sceneType: SceneType; /** 场景实例,仅 3d 下有 */ scene?: Scene; /** 相机实例,仅 3d 下有 */ camera?: Camera; /** 射线实例,仅 3d 下有 */ raycaster?: Raycaster; tickers: Array<(deltaTime: number) => void> = []; width: number = BaseScene.DEFAULT_WIDTH; height: number = BaseScene.DEFAULT_HEIGHT; coordinate: CoordinateController; mountTimer: number; sceneContext: SceneContext; resolution: number; backgroundImage: DisplayObject; maxFPS: number; renderFlag = true; constructor(props: Exclude<ComponentProps<BaseSceneProps>, ViewEntity>) { super(props); this.maxFPS = this.props.maxFPS || 60; if (this.props.viewport) { this.resolution = this.props.viewport.resolution || getContextParam('devicePixelRatio'); } else { this.resolution = this.props.resolution || getContextParam('devicePixelRatio'); } this.updateRenderFlag(this.props.renderFlag); if (!BaseScene.appMap.has(this.props.container)) { const app = this.createApp(); // 关闭默认交互 this.destroyRendererInteraction(app); // 缓存,挂载到同一元素的使用一个 gl 上下文 BaseScene.appMap.set(this.props.container, app); } this.view = this.createView(); // 初始化坐标系系统 const currentApp = this.getCurrentApp(); if (currentApp) { this.coordinate = this.createCoordinateController(currentApp); } // 初始化交互控制器 this.initInteractiveController(); this.sceneContext = { getCommandManager: () => this.props.commandMgr, getSceneTools: this.getSceneTools, }; } componentDidMount() { this.loadCanvas(); } componentDidUpdate() { const { viewport, disableResize = false, renderFlag = true } = this.props; const app = this.getCurrentApp(); if (!app) { return; } this.updateRenderFlag(renderFlag); this.setBackGroundImage(app); if (this.sceneType === SceneType.Scene3D) { this.updateCameraInfo(); } if (!viewport) { this.resolution = this.props.resolution || getContextParam('devicePixelRatio'); this.updateResolution(app); !disableResize && this.resizeStageByCanvas(app); return; } const ctrl = this.getCurrentInteractiveController(); if (ctrl) { ctrl.updateViewportInfo(viewport); } this.resolution = viewport.resolution || getContextParam('devicePixelRatio'); this.updateResolution(app); !disableResize && this.resizeStageByCanvas(app); // 重新计算 viewport this.computeViewport(); } /** * 卸载画布的钩子(主要做一些清理回收工作,浏览器环境下无需另外实现) */ abstract unmountCanvas(): void; componentWillUnmount() { if (getContextParam('$$turbox_hot')) { const app = this.getCurrentApp(); if (app) { this.removeAppChildrenView(app); } return; } // 取消事件监听 const ctrl = this.getCurrentInteractiveController(); if (ctrl) { ctrl.removeAllListener(); } if (BaseScene.appMap.has(this.props.container)) { const app = this.getCurrentApp(); if (app) { this.destroyApp(app); } BaseScene.appMap.delete(this.props.container); BaseScene.appMountedStatus.delete(this.props.container); } if (getContextEnv() === 'browser') { this.mountTimer && cancelAnimationFrame(this.mountTimer); window.removeEventListener('resize', this.resizeHandler, false); } else { // vm 环境 this.unmountCanvas(); } } /** 移除应用子视图 */ abstract removeAppChildrenView(app: ApplicationContext): void; /** 销毁 gl app 及相关清理工作的实现 */ abstract destroyApp(app: ApplicationContext): void; /** 创建 gl app instance,并返回 */ abstract createApp(): ApplicationContext; /** 创建根视图,给 view 设值 */ abstract createView(): Container; /** 获取根视图的基本信息(position、scale、visible) */ abstract getViewInfo(): ViewInfo; /** 添加子视图的接口 */ abstract addChildView(view: DisplayObject): void; /** 关闭渲染器的默认交互 */ abstract destroyRendererInteraction(app: ApplicationContext): void; abstract getCanvasView(app: ApplicationContext): ICanvasInfo<Renderer>; /** 获取 hitTarget 的原始实现方法 */ abstract getHitTargetOriginal(): ( point: Vec2, container: Container, configMap: Map<DisplayObject, InteractiveConfig>, interactiveType: InteractiveType ) => { /** event 的直接对象 */ originalTarget?: DisplayObject; /** originalTarget 或 originalTarget 的祖先中的第一个可被交互的元素 */ target?: DisplayObject; /** 选中对象的具体场景鼠标位置 */ originalTargetPoint?: Vec2 | Vec3; }; /** 更新分辨率 */ abstract updateResolution(app: ApplicationContext): void; private initInteractiveController() { const app = this.getCurrentApp(); if (!app) { return; } const interactiveController = InteractiveController.create<Container, DisplayObject, Renderer>({ renderer: this.getCanvasView(app).renderer, container: this.view, viewport: this.props.viewport, coordinateType: this.props.coordinateType, canvasHandler: { onClick: this.onClick, onDBClick: this.onDBClick, onRightClick: this.onRightClick, onDragStart: this.onDragStart, onDragMove: this.onDragMove, onDragEnd: this.onDragEnd, onPinchStart: this.onPinchStart, onPinch: this.onPinch, onPinchEnd: this.onPinchEnd, onRotateStart: this.onRotateStart, onRotate: this.onRotate, onRotateEnd: this.onRotateEnd, onPress: this.onPress, onPressUp: this.onPressUp, onPointerMove: this.onPointerMove, onPointerUp: this.onPointerUp, onWheel: this.onWheel, }, getCoordinateCtrl: this.getCoordinateCtrl, getHitTargetOriginal: this.getHitTargetOriginal(), maxFPS: this.maxFPS, }); const obj = BaseScene.interactiveMap.get(app); if (!obj) { BaseScene.interactiveMap.set(app, { [this.props.id]: interactiveController, }); } else { const ic = obj[this.props.id]; ic && ic.removeAllListener(); obj[this.props.id] = interactiveController; } } /** 更新渲染状态 */ private updateRenderFlag(flag = true) { this.renderFlag = flag; } private getCoordinateCtrl = () => this.coordinate; private getViewEntity() { return { id: this.props.id, type: this.props.type || this.defaultSceneViewType, }; } getCurrentInteractiveController = () => { const app = this.getCurrentApp(); if (!app) { return; } const obj = BaseScene.interactiveMap.get(app); if (!obj) { return; } return obj[this.props.id]; }; getCurrentApp = () => BaseScene.appMap.get(this.props.container) as ApplicationContext | undefined; private updateCursorHandler = (cursor = 'inherit') => { const app = this.getCurrentApp(); if (!app) { return; } this.updateCursor(app, cursor); }; abstract updateCursor(app: ApplicationContext, cursor: string): void; private getSceneTools = () => { const ctrl = this.getCurrentInteractiveController(); return { // eslint-disable-next-line @typescript-eslint/no-empty-function updateInteractiveObject: ctrl ? ctrl.updateInteractiveObject : () => {}, updateCursor: this.updateCursorHandler, hitTarget: ctrl ? ctrl.hitTarget : ((() => {}) as (point: Vec2) => undefined), coordinateTransform: this.coordinateTransform, getCamera: () => this.camera, getRaycaster: () => this.raycaster, getScene: () => this.scene, getRootView: () => this.view, getScreenShot: ( sx?: number, sy?: number, w?: number, h?: number, fileType?: string, quality?: number, isBase64?: boolean ) => this.getScreenShot(sx, sy, w, h, fileType, quality, isBase64), getApp: () => this.getCurrentApp(), addTicker: (ticker: (deltaTime: number) => void) => { this.tickers.push(ticker); }, removeTicker: (ticker: (deltaTime: number) => void) => { remove(this.tickers, ticker); }, }; }; /** * 坐标转化 */ private coordinateTransform = (point: Vec2, type: CoordinateType, z?: number) => (this.coordinate ? this.coordinate.transform(point, type, z) : { x: 0, y: 0 }); /** * 将渲染器/画布挂载到视图布局上(浏览器环境下无需另外实现) */ abstract mountCanvas(renderer: Renderer): void; private loadCanvas() { const app = this.getCurrentApp(); if (!app) { return; } const renderer = this.getCanvasView(app).renderer; if (getContextEnv() === 'browser') { const { container } = this.props; const el = document.getElementById(container); // 如果容器 DOM 没有准备好,留到下一帧再检测 if (!el) { this.mountTimer = requestAnimationFrame(this.loadCanvas); return; } // 挂载 if (!BaseScene.appMountedStatus.has(container)) { BaseScene.appMountedStatus.set(container, true); if (getContextParam('$$turbox_hot')) { el.innerHTML = ''; } el.appendChild(renderer as unknown as HTMLCanvasElement); } window.addEventListener('resize', this.resizeHandler, false); } else { // vm 环境 const { container } = this.props; if (!BaseScene.appMountedStatus.has(container)) { BaseScene.appMountedStatus.set(container, true); this.mountCanvas(renderer); } } // 根据容器大小确定画布大小 this.resizeStageByCanvas(app); // 初始化背景图 this.setBackGroundImage(app); if (this.sceneType === SceneType.Scene3D) { // 更新相机信息 this.updateCameraInfo(); // 初始化天空盒 this.initSkyBox(app); } // 初始化模型容器 this.addRootViewContainer(app); // 开始监听画布上的交互 const ctrl = this.getCurrentInteractiveController(); ctrl && ctrl.startListener(); this.props.initialized && this.props.initialized(this.getSceneTools()); } private updateCameraInfo() { const { cameraPosition } = this.props; this.updateCameraPosition({ x: cameraPosition?.x || 0, y: cameraPosition?.y || 0, z: (cameraPosition as Vec3)?.z || 0, }); } /** 更新相机位置 */ abstract updateCameraPosition(position: Vec3): void; private resizeHandler = () => { const { disableResize = false } = this.props; const app = this.getCurrentApp(); if (!app) { return; } !disableResize && this.resizeStageByCanvas(app); }; /** 初始化坐标系控制系统 */ abstract createCoordinateController(app: ApplicationContext): CoordinateController; private resizeStageByCanvas(app: ApplicationContext) { const { viewport, coordinateType } = this.props; if (viewport) { this.resizeViewport(app); } this.resizeStage(app); this.width = this.getCanvasView(app).width / this.resolution; this.height = this.getCanvasView(app).height / this.resolution; if (this.sceneType === SceneType.Scene2D && coordinateType) { this.compute2dOriginView(); } } /** 初始化天空盒 */ abstract initSkyBox(app: ApplicationContext): void; /** * resize 场景大小 */ abstract resizeStage(app: ApplicationContext): void; /** 设置背景图 */ abstract setBackGroundImage(app: ApplicationContext): void; /** * 计算2d原始视图的缩放和位置 */ private compute2dOriginView = () => { const { camera2dSize, cameraPosition, coordinateType, viewport } = this.props; const width = viewport ? viewport.width : this.width; const height = viewport ? viewport.height : this.height; const rangeW = (camera2dSize && camera2dSize.x) || width; const rangeH = (camera2dSize && camera2dSize.y) || height; // 根据相机范围计算模型容器的缩放比例 // 取二者中较小值,保证可以完整的展示场景 const scale = Math.min(width / rangeW, height / rangeH); const centerX = width / 2; const centerY = height / 2; const pX = cameraPosition?.x === void 0 ? centerX : cameraPosition.x; let pY: number; if (cameraPosition?.y === void 0) { pY = centerY; } else if (coordinateType === 'front' || coordinateType === 'left') { pY = -cameraPosition.y; } else { pY = cameraPosition.y; } // 模型世界坐标以画布中心为原点 const x = centerX - pX * scale; const y = centerY - pY * scale; if (viewport) { this.setViewVisible(viewport.visible); } this.setViewPosition({ x, y }); if (coordinateType === 'front' || coordinateType === 'left') { this.setViewScale({ x: scale, y: -scale }); } else { this.setViewScale({ x: scale, y: scale }); } }; /** 计算视口的缩放和位置、可见性 */ private computeViewport() { const { viewport } = this.props; if (!viewport) { return; } // resize 提高性能做法:放大对应视口的精灵,先让显示逻辑正确 const viewportInfo = this.getViewportInfo(); if ( viewportInfo && viewportInfo.width && viewportInfo.width > 0 && viewportInfo.height && viewportInfo.height > 0 ) { const ratio = Math.min(viewport.width / viewportInfo.width, viewport.height / viewportInfo.height); const scale = viewportInfo!.scale; this.setViewportScale({ x: scale.x * ratio, y: scale.y * ratio }); } // 重新计算对应视口的精灵 this.setViewportPosition({ x: viewport.x, y: viewport.y }); this.setViewportVisible(viewport.visible); } /** resize 视口 */ abstract resizeViewport(app: ApplicationContext): void; /** 设置根视图的位置 */ abstract setViewPosition(position: Partial<Vec3>): void; /** 设置根视图的缩放 */ abstract setViewScale(scale: Partial<Vec3>): void; /** 设置根视图是否可见 */ abstract setViewVisible(visible: boolean): void; /** 添加根视图容器到 app stage 的实现 */ abstract addRootViewContainer(app: ApplicationContext): void; onClick = (event: SceneEvent) => { const { onClickNothing, commandMgr } = this.props; if (onClickNothing) { onClickNothing(event); } if (commandMgr) { commandMgr.distributeEvent(EventType.onClick, this.getViewEntity(), event, this.getSceneTools()); } }; onDBClick = (event: SceneEvent) => { const { onClickNothing, commandMgr } = this.props; if (onClickNothing) { onClickNothing(event); } if (commandMgr) { commandMgr.distributeEvent(EventType.onDBClick, this.getViewEntity(), event, this.getSceneTools()); } }; onRightClick = (event: SceneEvent) => { const { onClickNothing, commandMgr } = this.props; if (onClickNothing) { onClickNothing(event); } if (commandMgr) { commandMgr.distributeEvent(EventType.onRightClick, this.getViewEntity(), event, this.getSceneTools()); } }; onDragStart = (event: SceneEvent) => { const { draggable = true, commandMgr } = this.props; if (draggable) { this.canvasDragImpl(event, 'start'); } if (commandMgr) { commandMgr.distributeEvent(EventType.onDragStart, this.getViewEntity(), event, this.getSceneTools()); } }; onDragMove = (event: SceneEvent) => { const { draggable = true, commandMgr } = this.props; if (draggable) { this.canvasDragImpl(event, 'move'); } if (commandMgr) { commandMgr.distributeEvent(EventType.onDragMove, this.getViewEntity(), event, this.getSceneTools()); } }; onDragEnd = (event: SceneEvent) => { const { draggable = true, commandMgr } = this.props; if (draggable) { this.canvasDragImpl(event, 'end'); } if (commandMgr) { commandMgr.distributeEvent(EventType.onDragEnd, this.getViewEntity(), event, this.getSceneTools()); } }; onPinchStart = (event: SceneEvent) => { const { commandMgr } = this.props; if (commandMgr) { commandMgr.distributeEvent(EventType.onPinchStart, this.getViewEntity(), event, this.getSceneTools()); } }; onPinch = (event: SceneEvent) => { const { scalable = true, commandMgr } = this.props; if (scalable) { this.canvasScaleImpl(event); } if (commandMgr) { commandMgr.distributeEvent(EventType.onPinch, this.getViewEntity(), event, this.getSceneTools()); } }; onPinchEnd = (event: SceneEvent) => { const { commandMgr } = this.props; if (commandMgr) { commandMgr.distributeEvent(EventType.onPinchEnd, this.getViewEntity(), event, this.getSceneTools()); } }; onRotateStart = (event: SceneEvent) => { const { commandMgr } = this.props; if (commandMgr) { commandMgr.distributeEvent(EventType.onRotateStart, this.getViewEntity(), event, this.getSceneTools()); } }; onRotate = (event: SceneEvent) => { const { commandMgr } = this.props; if (commandMgr) { commandMgr.distributeEvent(EventType.onRotate, this.getViewEntity(), event, this.getSceneTools()); } }; onRotateEnd = (event: SceneEvent) => { const { commandMgr } = this.props; if (commandMgr) { commandMgr.distributeEvent(EventType.onRotateEnd, this.getViewEntity(), event, this.getSceneTools()); } }; onPress = (event: SceneEvent) => { const { commandMgr } = this.props; if (commandMgr) { commandMgr.distributeEvent(EventType.onPress, this.getViewEntity(), event, this.getSceneTools()); } }; onPressUp = (event: SceneEvent) => { const { commandMgr } = this.props; if (commandMgr) { commandMgr.distributeEvent(EventType.onPressUp, this.getViewEntity(), event, this.getSceneTools()); } }; onPointerMove = (event: SceneEvent) => { const { commandMgr } = this.props; if (commandMgr) { commandMgr.distributeEvent(EventType.onCarriageMove, this.getViewEntity(), event, this.getSceneTools()); } }; onPointerUp = (event: SceneEvent) => { const { commandMgr } = this.props; if (commandMgr) { commandMgr.distributeEvent(EventType.onCarriageEnd, this.getViewEntity(), event, this.getSceneTools()); } }; onWheel = (event: SceneEvent) => { const { scalable = true, commandMgr } = this.props; if (scalable) { this.canvasScaleImpl(event); } const ctrl = this.getCurrentInteractiveController(); if (commandMgr && ctrl) { commandMgr.distributeEvent(EventType.onWheel, this.getViewEntity(), event, this.getSceneTools()); } }; /** 画布拖拽功能的实现,注意:如果已经引入其他交互插件则无需重复实现,避免实现上的冲突 */ abstract canvasDragImpl(event: SceneEvent, type: 'start' | 'move' | 'end'): void; /** 画布缩放功能的实现,注意:如果已经引入其他交互插件则无需重复实现,避免实现上的冲突 */ abstract canvasScaleImpl(event: SceneEvent): void; /** 获取视口的实现 */ abstract getViewport(): Viewport | undefined; /** 获取视口基础信息的实现 */ abstract getViewportInfo(): ViewInfo | undefined; /** 初始化视口,给 viewport 设值 */ abstract createViewport(): void; /** 设置视口的位置 */ abstract setViewportPosition(position: Partial<Vec3>): void; /** 设置视口的缩放 */ abstract setViewportScale(scale: Partial<Vec3>): void; /** 设置视口是否可见 */ abstract setViewportVisible(visible: boolean): void; /** 获取截图 */ abstract getScreenShot( sx?: number, sy?: number, w?: number, h?: number, fileType?: string, quality?: number, isBase64?: boolean ): Promise<string | Blob>; }