UNPKG

@egjs/view3d

Version:

Fast & Customizable glTF 3D model viewer, packed with full of features!

1,204 lines (1,086 loc) 39.2 kB
/* * Copyright (c) 2020 NAVER Corp. * egjs projects are licensed under the MIT license */ import * as THREE from "three"; import Component from "@egjs/component"; import Renderer from "./core/Renderer"; import Scene from "./core/Scene"; import Camera from "./core/Camera"; import AutoResizer from "./core/AutoResizer"; import Model from "./core/Model"; import ModelAnimator from "./core/ModelAnimator"; import Skybox from "./core/Skybox"; import ARManager from "./core/ARManager"; import AnnotationManager from "./annotation/AnnotationManager"; import { ShadowOptions } from "./core/ShadowPlane"; import View3DError from "./core/View3DError"; import OrbitControl from "./control/OrbitControl"; import { RotateControlOptions } from "./control/RotateControl"; import { TranslateControlOptions } from "./control/TranslateControl"; import { ZoomControlOptions } from "./control/ZoomControl"; import AutoPlayer, { AutoplayOptions } from "./core/AutoPlayer"; import { WebARSessionOptions } from "./xr/WebARSession"; import { SceneViewerSessionOptions } from "./xr/SceneViewerSession"; import { QuickLookSessionOptions } from "./xr/QuickLookSession"; import { EVENTS, AUTO, AR_SESSION_TYPE, DEFAULT_CLASS, TONE_MAPPING, ANIMATION_REPEAT_MODE } from "./const/external"; import ERROR from "./const/error"; import * as DEFAULT from "./const/default"; import * as EVENT_TYPES from "./type/event"; import { View3DPlugin } from "./plugin"; import { getElement, getObjectOption, isCSSSelector, isElement } from "./utils"; import { LiteralUnion, OptionGetters, ValueOf } from "./type/utils"; import { LoadingItem } from "./type/external"; import { GLTFLoader } from "./loader"; /** * @interface * @see [Events](/docs/events/ready) page for detailed information */ export interface View3DEvents { [EVENTS.READY]: EVENT_TYPES.ReadyEvent; [EVENTS.LOAD_START]: EVENT_TYPES.LoadStartEvent; [EVENTS.LOAD]: EVENT_TYPES.LoadEvent; [EVENTS.LOAD_ERROR]: EVENT_TYPES.LoadErrorEvent; [EVENTS.LOAD_FINISH]: EVENT_TYPES.LoadFinishEvent; [EVENTS.MODEL_CHANGE]: EVENT_TYPES.ModelChangeEvent; [EVENTS.RESIZE]: EVENT_TYPES.ResizeEvent; [EVENTS.BEFORE_RENDER]: EVENT_TYPES.BeforeRenderEvent; [EVENTS.RENDER]: EVENT_TYPES.RenderEvent; [EVENTS.PROGRESS]: EVENT_TYPES.LoadProgressEvent; [EVENTS.INPUT_START]: EVENT_TYPES.InputStartEvent; [EVENTS.INPUT_END]: EVENT_TYPES.InputEndEvent; [EVENTS.CAMERA_CHANGE]: EVENT_TYPES.CameraChangeEvent; [EVENTS.ANIMATION_START]: EVENT_TYPES.AnimationStartEvent; [EVENTS.ANIMATION_LOOP]: EVENT_TYPES.AnimationLoopEvent; [EVENTS.ANIMATION_FINISHED]: EVENT_TYPES.AnimationFinishedEvent; [EVENTS.ANNOTATION_FOCUS]: EVENT_TYPES.AnnotationFocusEvent; [EVENTS.ANNOTATION_UNFOCUS]: EVENT_TYPES.AnnotationUnfocusEvent; [EVENTS.AR_START]: EVENT_TYPES.ARStartEvent; [EVENTS.AR_END]: EVENT_TYPES.AREndEvent; [EVENTS.AR_MODEL_PLACED]: EVENT_TYPES.ARModelPlacedEvent; [EVENTS.QUICK_LOOK_TAP]: EVENT_TYPES.QuickLookTapEvent; } /** * @interface * @see [Options](/docs/options/model/src) page for detailed information */ export interface View3DOptions { // Model src: string | string[] | null; iosSrc: string | null; variant: number | string | null; dracoPath: string; ktxPath: string; meshoptPath: string | null; fixSkinnedBbox: boolean; // Control fov: typeof AUTO | number; center: typeof AUTO | Array<number | string>; yaw: number; pitch: number; pivot: typeof AUTO | Array<number | string>; initialZoom: number | { axis: "x" | "y" | "z"; ratio: number; }; rotate: boolean | Partial<RotateControlOptions>; translate: boolean | Partial<TranslateControlOptions>; zoom: boolean | Partial<ZoomControlOptions>; autoplay: boolean | Partial<AutoplayOptions>; scrollable: boolean; wheelScrollable: boolean; useGrabCursor: boolean; ignoreCenterOnFit: boolean; // Environment skybox: string | null; envmap: string | null; background: number | string | null; exposure: number; shadow: boolean | Partial<ShadowOptions>; skyboxBlur: boolean; toneMapping: LiteralUnion<ValueOf<typeof TONE_MAPPING>, THREE.ToneMapping>; useDefaultEnv: boolean; // Animation defaultAnimationIndex: number; animationRepeatMode: ValueOf<typeof ANIMATION_REPEAT_MODE>; // Annotation annotationURL: string | null; annotationWrapper: HTMLElement | string; annotationSelector: string; annotationBreakpoints: Record<number, number>; annotationAutoUnfocus: boolean; // AR webAR: boolean | Partial<WebARSessionOptions>; sceneViewer: boolean | Partial<SceneViewerSessionOptions>; quickLook: boolean | Partial<QuickLookSessionOptions>; arPriority: Array<ValueOf<typeof AR_SESSION_TYPE>>; // Others poster: string | HTMLElement | null; canvasSelector: string; autoInit: boolean; autoResize: boolean; useResizeObserver: boolean; maintainSize: boolean; on: Partial<View3DEvents>; plugins: View3DPlugin[]; maxDeltaTime: number; } /** * @extends Component * @see https://naver.github.io/egjs-component/ */ class View3D extends Component<View3DEvents> implements OptionGetters<Omit<View3DOptions, "on">> { /** * Current version of the View3D * @type {string} * @readonly */ public static readonly VERSION: string = "#__VERSION__#"; // Internal Components private _renderer: Renderer; private _scene: Scene; private _camera: Camera; private _control: OrbitControl; private _autoPlayer: AutoPlayer; private _model: Model | null; private _animator: ModelAnimator; private _autoResizer: AutoResizer; private _arManager: ARManager; private _annotationManager: AnnotationManager; // Internal States private _rootEl: HTMLElement; private _plugins: View3DPlugin[]; private _initialized: boolean; private _loadingContext: LoadingItem[]; // Options private _src: View3DOptions["src"]; private _iosSrc: View3DOptions["iosSrc"]; private _variant: View3DOptions["variant"]; private _dracoPath: View3DOptions["dracoPath"]; private _ktxPath: View3DOptions["ktxPath"]; private _meshoptPath: View3DOptions["meshoptPath"]; private _fixSkinnedBbox: View3DOptions["fixSkinnedBbox"]; private _fov: View3DOptions["fov"]; private _center: View3DOptions["center"]; private _yaw: View3DOptions["yaw"]; private _pitch: View3DOptions["pitch"]; private _pivot: View3DOptions["pivot"]; private _initialZoom: View3DOptions["initialZoom"]; private _rotate: View3DOptions["rotate"]; private _translate: View3DOptions["translate"]; private _zoom: View3DOptions["zoom"]; private _autoplay: View3DOptions["autoplay"]; private _scrollable: View3DOptions["scrollable"]; private _wheelScrollable: View3DOptions["scrollable"]; private _useGrabCursor: View3DOptions["useGrabCursor"]; private _ignoreCenterOnFit: View3DOptions["ignoreCenterOnFit"]; private _skybox: View3DOptions["skybox"]; private _envmap: View3DOptions["envmap"]; private _background: View3DOptions["background"]; private _exposure: View3DOptions["exposure"]; private _shadow: View3DOptions["shadow"]; private _skyboxBlur: View3DOptions["skyboxBlur"]; private _toneMapping: View3DOptions["toneMapping"]; private _useDefaultEnv: View3DOptions["useDefaultEnv"]; private _defaultAnimationIndex: View3DOptions["defaultAnimationIndex"]; private _animationRepeatMode: View3DOptions["animationRepeatMode"]; private _annotationURL: View3DOptions["annotationURL"]; private _annotationWrapper: View3DOptions["annotationWrapper"]; private _annotationSelector: View3DOptions["annotationSelector"]; private _annotationBreakpoints: View3DOptions["annotationBreakpoints"]; private _annotationAutoUnfocus: View3DOptions["annotationAutoUnfocus"]; private _webAR: View3DOptions["webAR"]; private _sceneViewer: View3DOptions["sceneViewer"]; private _quickLook: View3DOptions["quickLook"]; private _arPriority: View3DOptions["arPriority"]; private _poster: View3DOptions["poster"]; private _canvasSelector: View3DOptions["canvasSelector"]; private _autoInit: View3DOptions["autoInit"]; private _autoResize: View3DOptions["autoResize"]; private _useResizeObserver: View3DOptions["useResizeObserver"]; private _maintainSize: View3DOptions["maintainSize"]; private _maxDeltaTime: View3DOptions["maxDeltaTime"]; // Internal Components Getter /** * {@link Renderer} instance of the View3D * @type {Renderer} * @readonly */ public get renderer() { return this._renderer; } /** * {@link Scene} instance of the View3D * @type {Scene} * @readonly */ public get scene() { return this._scene; } /** * {@link Camera} instance of the View3D * @type {Camera} * @readonly */ public get camera() { return this._camera; } /** * {@link OrbitControl} instance of the View3D * @type {OrbitControl} * @readonly */ public get control() { return this._control; } /** * {@link AutoPlayer} instance of the View3D * @type {AutoPlayer} * @readonly */ public get autoPlayer() { return this._autoPlayer; } /** * Current {@link Model} displaying. `null` if nothing is displayed on the canvas. * @type {Model | null} * @readonly */ public get model() { return this._model; } /** * {@link ModelAnimator} instance of the View3D * @type {ModelAnimator} * @readonly */ public get animator() { return this._animator; } /** * {@link ARManager} instance of the View3D * @type {ARManager} * @readonly */ public get ar() { return this._arManager; } /** * {@link AnnotationManager} instance of the View3D * @type {AnnotationManager} */ public get annotation() { return this._annotationManager; } // Internal State Getter /** * Root(Wrapper) element of View3D that given in the constructor * @type {HTMLElement} * @readonly */ public get rootEl() { return this._rootEl; } /** * Whether the View3D is initialized. This is set to `true` just before triggering "ready" event. * @type {boolean} * @readonly */ public get initialized() { return this._initialized; } /** * An array of loading status of assets. * @type {LoadingItem[]} * @readonly * @internal */ public get loadingContext() { return this._loadingContext; } /** * Active plugins of view3D * @type {View3DPlugin[]} * @readonly */ public get plugins() { return this._plugins; } // Options Getter /** * Source URL to fetch 3D model. `glb` / `glTF` models are supported. * @type {string | null} * @default null */ public get src() { return this._src; } /** * Source URL to fetch 3D model in iOS AR Quick Look. `usdz` models are supported. * @type {string | null} * @default null */ public get iosSrc() { return this._iosSrc; } /** * Active material variant of the model. * Either can be index of the variant(number), or the name of the variant(string) * Changing this value will change the material of the model * @default null * @see https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_variants/README.md */ public get variant() { return this._variant; } /** * URL to {@link https://github.com/google/draco Draco} decoder location. * @type {string} * @default https://www.gstatic.com/draco/versioned/decoders/1.4.1/ */ public get dracoPath() { return this._dracoPath; } /** * URL to {@link http://github.khronos.org/KTX-Specification/#basisu_gd KTX2 texture} transcoder location. * @type {string} * @default https://unpkg.com/three@0.134.0/examples/js/libs/basis/ */ public get ktxPath() { return this._ktxPath; } /** * URL to {@link https://github.com/zeux/meshoptimizer Meshoptimizer} decoder js path. * @type {string | null} * @default null */ public get meshoptPath() { return this._meshoptPath; } /** * Sometimes, some rigged model has the wrong bounding box that when displaying on three.js (usually converted glTF model from Sketchfab) * Enabling this option can resolve that issue by recalculating bounding box size with the influence of the skeleton weight. * @type {boolean} * @default false */ public get fixSkinnedBbox() { return this._fixSkinnedBbox; } /** * A vertical FOV(Field of View) value of the camera frustum, in degrees. * If `"auto"` is used, View3D will try to find the appropriate FOV value that model is not clipped at any angle. * @type {"auto" | number} * @default "auto" */ public get fov() { return this._fov; } /** * Specifies the center of the model. * If `"auto"` is given, it will use the center of the model's bounding box. * Else, you can use a number array as any world position. * Or, you can use a string array as a relative position to bounding box min/max. ex) ["0%", "100%", "50%"] * The difference to {@link View3D#pivot pivot} is model's bounding box and center position will be shown on screen at every rotation angle. * @type {"auto" | Array<number | string>} * @default "auto" */ public get center() { return this._center; } /** * Initial Y-axis rotation of the camera, in degrees. * Use {@link Camera#yaw view3D.camera.yaw} instead if you want current yaw value. * @type {number} * @default 0 */ public get yaw() { return this._yaw; } /** * Initial X-axis rotation of the camera, in degrees. * Should be a value from -90 to 90. * Use {@link Camera#pitch view3D.camera.pitch} instead if you want current pitch value. * @type {number} * @default 0 */ public get pitch() { return this._pitch; } /** * Initial origin point of rotation of the camera. * If `"auto"` is given, it will use {@link View3D#center model's center} as pivot. * Else, you can use a number array as any world position. * Or, you can use a string array as a relative position to bounding box min/max. ex) ["0%", "100%", "50%"] * Use {@link Camera#pivot view3D.camera.pivot} instead if you want current pivot value. * @type {"auto" | Array<number | string>} * @default "auto" */ public get pivot() { return this._pivot; } /** * Initial zoom value. * If `number` is given, positive value will make camera zoomed in and negative value will make camera zoomed out. * If `object` is given, it will fit model's bounding box's front / side face to the given ratio of the canvas height / width. * For example, `{ axis: "y", ratio: 0.5 }` will set the zoom of the camera so that the height of the model to 50% of the height of the canvas. * @type {number} * @default 0 */ public get initialZoom() { return this._initialZoom; } /** * Options for the {@link RotateControl}. * If `false` is given, it will disable the rotate control. * @type {boolean | RotateControlOptions} * @default true */ public get rotate() { return this._rotate; } /** * Options for the {@link TranslateControl}. * If `false` is given, it will disable the translate control. * @type {boolean | TranslateControlOptions} * @default true */ public get translate() { return this._translate; } /** * Options for the {@link ZoomControl}. * If `false` is given, it will disable the zoom control. * @type {boolean | ZoomControlOptions} * @default true */ public get zoom() { return this._zoom; } /** * Enable Y-axis rotation autoplay. * If `true` is given, it will enable autoplay with default values. * @type {boolean | AutoplayOptions} * @default true */ public get autoplay() { return this._autoplay; } /** * Enable browser scrolling with touch on the canvas area. * This will block the rotate(pitch) control if the user is currently scrolling. * @type {boolean} * @default true */ public get scrollable() { return this._scrollable; } /** * Enable browser scrolling with mouse wheel on the canvas area. * This will block the zoom control with mouse wheel. * @type {boolean} * @default false */ public get wheelScrollable() { return this._wheelScrollable; } /** * Enable CSS `cursor: grab` on the canvas element. * `cursor: grabbing` will be used on mouse click. * @type {boolean} * @default true */ public get useGrabCursor() { return this._useGrabCursor; } /** * When {@link Camera#pivot camera.fit} is called, View3D will adjust camera with the model so that the model is not clipped from any camera rotation by assuming {@link View3D#center center} as origin of the rotation by default. * This will ignore that behavior by forcing model's bbox center as center of the rotation while fitting the camera to the model. * @type {boolean} * @default false */ public get ignoreCenterOnFit() { return this._ignoreCenterOnFit; } /** * Source to the HDR texture image (RGBE), which will used as the scene environment map & background. * `envmap` will be ignored if this value is not `null`. * @type {string | null} * @default null */ public get skybox() { return this._skybox; } /** * Source to the HDR texture image (RGBE), which will used as the scene environment map. * @type {string | null} * @default null */ public get envmap() { return this._envmap; } /** * Color code / URL to a image to use as the background. * For transparent background, use `null`. (default value) * Can be enabled only when the `skybox` is `null`. * @type {number | string | null} * @default null */ public get background() { return this._background; } /** * Exposure value of the HDR envmap/skybox image. * @type {number} * @default 1 */ public get exposure() { return this._exposure; } /** * Enable shadow below the model. * If `true` is given, it will enable shadow with the default options. * If `false` is given, it will disable the shadow. * @type {boolean | ShadowOptions} * @default true */ public get shadow() { return this._shadow; } /** * Apply blur to the current skybox image. * @type {boolean} * @default false */ public get skyboxBlur() { return this._skyboxBlur; } /** * This is used to approximate the appearance of high dynamic range (HDR) on the low dynamic range medium of a standard computer monitor or mobile device's screen. * @type {number} * @see TONE_MAPPING * @default THREE.LinearToneMapping */ public get toneMapping() { return this._toneMapping; } /** * Whether to use generated default environment map. * @type {boolean} * @default true */ public get useDefaultEnv() { return this._useDefaultEnv; } /** * Index of the animation to play after the model is loaded * @type {number} * @default 0 */ public get defaultAnimationIndex() { return this._defaultAnimationIndex; } /** * Repeat mode of the animator. * "one" will repeat single animation, and "all" will repeat all animations. * "none" will make animation to automatically paused on its last frame. * @see ANIMATION_REPEAT_MODE * @type {string} * @default "one" */ public get animationRepeatMode() { return this._animationRepeatMode; } /** * An URL to the JSON file that has annotation informations. * @type {string | null} * @default null */ public get annotationURL() { return this._annotationURL; } /** * An element or CSS selector of the annotation wrapper element. * @type {HTMLElement | string} * @default ".view3d-annotation-wrapper" */ public get annotationWrapper() { return this._annotationWrapper; } /** * CSS selector of the annotation elements inside the root element * @type {string} * @default ".view3d-annotation" */ public get annotationSelector() { return this._annotationSelector; } /** * Breakpoints for the annotation opacity, mapped by degree between (camera-model center-annotation) as key. * @type {Record<number, number>} * @default { 165: 0, 135: 0.4, 0: 1 } */ public get annotationBreakpoints() { return this._annotationBreakpoints; } /** * Whether to automatically unfocus annotation on user input * @type {boolean} * @default true */ public get annotationAutoUnfocus() { return this._annotationAutoUnfocus; } /** * Options for the WebXR-based AR session. * If `false` is given, it will disable WebXR-based AR session. * @type {boolean | WebARSessionOptions} * @default true */ public get webAR() { return this._webAR; } /** * Options for the {@link https://developers.google.com/ar/develop/java/scene-viewer Google SceneViewer} based AR session. * If `false` is given, it will disable SceneViewer based AR session. * See {@link https://developers.google.com/ar/develop/java/scene-viewer#supported_intent_parameters Official Page} for the parameter details. * @type {boolean | SceneViewerSessionOptions} * @default true */ public get sceneViewer() { return this._sceneViewer; } /** * Options for the {@link https://developer.apple.com/augmented-reality/quick-look/ Apple AR Quick Look} based AR session. * If `false` is given, it will disable AR Quick Look based AR session. * @type {boolean | QuickLookSessionOptions} * @default true */ public get quickLook() { return this._quickLook; } /** * Priority array for the AR sessions. * If the two sessions are available in one environment, the session listed earlier will be used first. * If the session name is not included in this priority array, that session will be ignored. * See {@link AR_SESSION_TYPE} * @type {string[]} * @default ["webAR", "sceneViewer", "quickLook"] */ public get arPriority() { return this._arPriority; } /** * Poster image that will be displayed before the 3D model is loaded. * If `string` URL is given, View3D will temporarily show poster image element with that url as src before the first model is loaded * If `string` CSS selector of DOM element inside view3d-wrapper or `HTMLElement` is given, View3D will remove that element after the first model is loaded * @type {string | HTMLElement | null} * @default null */ public get poster() { return this._poster; } /** * CSS Selector for the canvas element. * @type {string} * @default "canvas" */ public get canvasSelector() { return this._canvasSelector; } /** * Call {@link View3D#init init()} automatically when creating View3D's instance * This option won't work if `src` is not given * @type {boolean} * @default true * @readonly */ public get autoInit() { return this._autoInit; } /** * Whether to automatically call {@link View3D#resize resize()} when the canvas element's size is changed * @type {boolean} * @default true */ public get autoResize() { return this._autoResize; } /** * Whether to listen {@link https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver ResizeObserver}'s event instead of Window's {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/resize_event resize} event when using the `autoResize` option * @type {boolean} * @default true */ public get useResizeObserver() { return this._useResizeObserver; } /** * Whether to retain 3D model's visual size on canvas resize * @type {boolean} * @default false */ public get maintainSize() { return this._maintainSize; } /** * Maximum delta time in any given frame * This can prevent a long frame hitch / lag * The default value is 1/30(30 fps). Set this value to `Infinity` to disable * @type {number} * @default 1/30 */ public get maxDeltaTime() { return this._maxDeltaTime; } public set iosSrc(val: View3DOptions["iosSrc"]) { this._iosSrc = val; } public set variant(val: View3DOptions["variant"]) { if (this._model) { this._model.selectVariant(val) .then(() => { this.renderer.renderSingleFrame(); }); } this._variant = val; } public set defaultAnimationIndex(val: View3DOptions["defaultAnimationIndex"]) { this._defaultAnimationIndex = val; } public set initialZoom(val: View3DOptions["initialZoom"]) { this._initialZoom = val; } public set skybox(val: View3DOptions["skybox"]) { void this._scene.setSkybox(val); this._skybox = val; if (!val && this._useDefaultEnv) { this._scene.setDefaultEnv(); } } public set envmap(val: View3DOptions["envmap"]) { void this._scene.setEnvMap(val); this._envmap = val; if (!val && this._useDefaultEnv) { this._scene.setDefaultEnv(); } } public set exposure(val: View3DOptions["exposure"]) { this._exposure = val; this._renderer.threeRenderer.toneMappingExposure = val; this._renderer.renderSingleFrame(); } public set skyboxBlur(val: View3DOptions["skyboxBlur"]) { this._skyboxBlur = val; const scene = this._scene; const root = scene.root; const origEnvmapTexture = scene.root.environment; if (origEnvmapTexture && root.background !== null) { if (val) { root.background = Skybox.createBlurredHDR(this, origEnvmapTexture); } else { root.background = origEnvmapTexture; } } } public set toneMapping(val: View3DOptions["toneMapping"]) { this._toneMapping = val; this._renderer.threeRenderer.toneMapping = val as THREE.ToneMapping; this._renderer.renderSingleFrame(); } public set useGrabCursor(val: View3DOptions["useGrabCursor"]) { this._useGrabCursor = val; this._control.updateCursor(); } public set animationRepeatMode(val: View3DOptions["animationRepeatMode"]) { this._animationRepeatMode = val; this._animator.updateRepeatMode(); } public set autoResize(val: View3DOptions["autoResize"]) { this._autoResize = val; if (val) { this._autoResizer.enable(); } else { this._autoResizer.disable(); } } public set maintainSize(val: View3DOptions["maintainSize"]) { this._maintainSize = val; } public set maxDeltaTime(val: View3DOptions["maxDeltaTime"]) { this._maxDeltaTime = val; } /** * Creates new View3D instance. * @param root A root element or selector of it to initialize View3D * @param {View3DOptions} [options={}] An options object for View3D * @throws {View3DError} */ public constructor(root: string | HTMLElement, { src = null, iosSrc = null, variant = null, dracoPath = DEFAULT.DRACO_DECODER_URL, ktxPath = DEFAULT.KTX_TRANSCODER_URL, meshoptPath = null, fixSkinnedBbox = false, fov = AUTO, center = AUTO, yaw = 0, pitch = 0, pivot = AUTO, initialZoom = 0, rotate = true, translate = true, zoom = true, autoplay = false, scrollable = true, wheelScrollable = false, useGrabCursor = true, ignoreCenterOnFit = false, skybox = null, envmap = null, background = null, exposure = 1, shadow = true, skyboxBlur = false, toneMapping = TONE_MAPPING.LINEAR, useDefaultEnv = true, defaultAnimationIndex = 0, animationRepeatMode = ANIMATION_REPEAT_MODE.ONE, annotationURL = null, annotationWrapper = `.${DEFAULT_CLASS.ANNOTATION_WRAPPER}`, annotationSelector = `.${DEFAULT_CLASS.ANNOTATION}`, annotationBreakpoints = DEFAULT.ANNOTATION_BREAKPOINT, annotationAutoUnfocus = true, webAR = true, sceneViewer = true, quickLook = true, arPriority = DEFAULT.AR_PRIORITY, poster = null, canvasSelector = "canvas", autoInit = true, autoResize = true, useResizeObserver = true, maintainSize = false, on = {}, plugins = [], maxDeltaTime = 1 / 30 }: Partial<View3DOptions> = {}) { super(); this._rootEl = getElement(root); // Bind options this._src = src; this._iosSrc = iosSrc; this._variant = variant; this._dracoPath = dracoPath; this._ktxPath = ktxPath; this._meshoptPath = meshoptPath; this._fixSkinnedBbox = fixSkinnedBbox; this._fov = fov; this._center = center; this._yaw = yaw; this._pitch = pitch; this._pivot = pivot; this._initialZoom = initialZoom; this._rotate = rotate; this._translate = translate; this._zoom = zoom; this._autoplay = autoplay; this._scrollable = scrollable; this._wheelScrollable = wheelScrollable; this._useGrabCursor = useGrabCursor; this._ignoreCenterOnFit = ignoreCenterOnFit; this._skybox = skybox; this._envmap = envmap; this._background = background; this._exposure = exposure; this._shadow = shadow; this._skyboxBlur = skyboxBlur; this._toneMapping = toneMapping; this._useDefaultEnv = useDefaultEnv; this._defaultAnimationIndex = defaultAnimationIndex; this._animationRepeatMode = animationRepeatMode; this._annotationURL = annotationURL; this._annotationWrapper = annotationWrapper; this._annotationSelector = annotationSelector; this._annotationBreakpoints = annotationBreakpoints; this._annotationAutoUnfocus = annotationAutoUnfocus; this._webAR = webAR; this._sceneViewer = sceneViewer; this._quickLook = quickLook; this._arPriority = arPriority; this._poster = poster; this._canvasSelector = canvasSelector; this._autoInit = autoInit; this._autoResize = autoResize; this._useResizeObserver = useResizeObserver; this._maintainSize = maintainSize; this._model = null; this._initialized = false; this._loadingContext = []; this._plugins = plugins; this._maxDeltaTime = maxDeltaTime; // Create internal components this._renderer = new Renderer(this); this._camera = new Camera(this); this._control = new OrbitControl(this); this._scene = new Scene(this); this._animator = new ModelAnimator(this); this._autoPlayer = new AutoPlayer(this, getObjectOption(autoplay)); this._autoResizer = new AutoResizer(this); this._arManager = new ARManager(this); this._annotationManager = new AnnotationManager(this); this._addEventHandlers(on); this._addPosterImage(); void (async () => { await this._initPlugins(plugins); if (src && autoInit) { await this.init(); } })(); } /** * Destroy View3D instance and remove all events attached to it * @returns {void} */ public destroy(): void { this._scene.reset(); this._renderer.destroy(); this._control.destroy(); this._autoResizer.disable(); this._animator.destroy(); this._annotationManager.destroy(); this._plugins.forEach(plugin => plugin.teardown(this)); this._plugins = []; } /** * Initialize View3d & load 3D model * @fires View3D#load * @returns {Promise<void>} */ public async init() { if (!this._src) { throw new View3DError(ERROR.MESSAGES.PROVIDE_SRC_FIRST, ERROR.CODES.PROVIDE_SRC_FIRST); } const scene = this._scene; const renderer = this._renderer; const control = this._control; const animator = this._animator; const annotationManager = this._annotationManager; const meshoptPath = this._meshoptPath; const tasks: Array<Promise<any>> = []; this.resize(); animator.init(); annotationManager.init(); if (this._autoResize) { this._autoResizer.enable(); } if (meshoptPath && !GLTFLoader.meshoptDecoder) { await GLTFLoader.setMeshoptDecoder(meshoptPath); } // Load & set skybox / envmap before displaying model tasks.push(...scene.initTextures()); const loadModel = this._loadModel(this._src); tasks.push(...loadModel); void this._resetLoadingContextOnFinish(tasks); await Promise.race(loadModel); if (this._annotationURL) { await this._annotationManager.load(this._annotationURL); } control.enable(); if (this._autoplay) { this._autoPlayer.enable(); } // Start rendering renderer.stopAnimationLoop(); renderer.setAnimationLoop(renderer.defaultRenderLoop); renderer.renderSingleFrame(); this._initialized = true; this.trigger(EVENTS.READY, { type: EVENTS.READY, target: this }); } /** * Resize View3D instance to fit current canvas size * @returns {void} */ public resize() { const renderer = this._renderer; const prevSize = this._initialized ? renderer.size : null; renderer.resize(); const newSize = renderer.size; this._camera.resize(newSize, prevSize); this._control.resize(newSize); this._annotationManager.resize(); // Prevent flickering on resize if (this._initialized) { renderer.renderSingleFrame(true); } this.trigger(EVENTS.RESIZE, { ...newSize, type: EVENTS.RESIZE, target: this }); } /** * Load a new 3D model and replace it with the current one * @param {string | string[]} src Source URL to fetch 3D model from * @param {object} [options={}] Options * @param {string | null} [options.iosSrc] Source URL to fetch 3D model in iOS AR Quick Look. `usdz` models are supported. */ public async load(src: string | string[], { iosSrc = null }: Partial<{ iosSrc: string | null; }> = {}) { if (this._initialized) { const loadModel = this._loadModel(src); void this._resetLoadingContextOnFinish(loadModel); await Promise.race(loadModel); // Change the src later as an error can occur while loading the model this._src = src; this._iosSrc = iosSrc; } else { this._src = src; this._iosSrc = iosSrc; await this.init(); } } /** * Display the given model in the canvas * @param {Model} model A model to display * @param {object} options Options for displaying model * @param {boolean} [options.resetCamera=true] Reset camera to default pose */ public display(model: Model, { resetCamera = true }: Partial<{ resetCamera: boolean; }> = {}): void { const renderer = this._renderer; const scene = this._scene; const camera = this._camera; const animator = this._animator; const annotationManager = this._annotationManager; const inXR = renderer.threeRenderer.xr.isPresenting; scene.reset(); scene.add(model.scene); scene.shadowPlane.updateDimensions(model); if (resetCamera) { camera.fit(model); void camera.reset(0); } animator.reset(); animator.setClips(model.animations); if (model.animations.length > 0) { animator.play(this._defaultAnimationIndex); } annotationManager.reset(); annotationManager.collect(); annotationManager.add(...model.annotations); this._model = model; if (inXR) { const activeSession = this._arManager.activeSession; if (activeSession) { activeSession.control.syncTargetModel(model); } } if (this._initialized) { renderer.renderSingleFrame(); } this.trigger(EVENTS.MODEL_CHANGE, { type: EVENTS.MODEL_CHANGE, target: this, model }); } /** * Add new plugins to View3D * @param {View3DPlugin[]} plugins Plugins to add * @returns {Promise<void>} A promise that resolves when all plugins are initialized */ public async loadPlugins(...plugins: View3DPlugin[]) { await this._initPlugins(plugins); this._plugins.push(...plugins); } /** * Remove plugins from View3D * @param {View3DPlugin[]} plugins Plugins to remove * @returns {Promise<void>} A promise that resolves when all plugins are removed */ public async removePlugins(...plugins: View3DPlugin[]) { await Promise.all(plugins.map(plugin => plugin.teardown(this))); plugins.forEach(plugin => { const pluginIdx = this._plugins.indexOf(plugin); if (pluginIdx < 0) return; this._plugins.splice(pluginIdx, 1); }); } /** * Take a screenshot of current rendered canvas image and download it */ public screenshot(fileName = "screenshot") { const canvas = this._renderer.canvas; const imgURL = canvas.toDataURL("png"); const anchorEl = document.createElement("a"); anchorEl.href = imgURL; anchorEl.download = fileName; anchorEl.click(); } private _loadModel(src: string | string[]): Array<Promise<void>> { const loader = new GLTFLoader(this); if (Array.isArray(src)) { const loaded = src.map(() => false); const loadModels = src.map((srcLevel, level) => this._loadSingleModel(loader, srcLevel, level, loaded)); return loadModels; } else { return [this._loadSingleModel(loader, src, 0, [false])]; } } private async _loadSingleModel(loader: GLTFLoader, src: string, level: number, loaded: boolean[]) { const maxLevel = loaded.length - 1; this.trigger(EVENTS.LOAD_START, { type: EVENTS.LOAD_START, target: this, src, level, maxLevel }); try { const model = await loader.load(src); const higherLevelLoaded = loaded.slice(level + 1).some(val => !!val); const modelLoadedBefore = loaded.some(val => !!val); this.trigger(EVENTS.LOAD, { type: EVENTS.LOAD, target: this, model, level, maxLevel }); loaded[level] = true; if (higherLevelLoaded) return; this.display(model, { resetCamera: !modelLoadedBefore }); } catch (error) { this.trigger(EVENTS.LOAD_ERROR, { type: EVENTS.LOAD_ERROR, target: this, level, maxLevel, error }); throw new View3DError(ERROR.MESSAGES.MODEL_FAIL_TO_LOAD(src), ERROR.CODES.MODEL_FAIL_TO_LOAD); } } private _addEventHandlers(events: Partial<View3DEvents>) { Object.keys(events).forEach((evtName: keyof typeof EVENT_TYPES) => { this.on(evtName, events[evtName]); }); } private _addPosterImage() { const poster = this._poster; const rootEl = this._rootEl; if (!poster) return; const isPosterEl = isElement(poster); let posterEl: HTMLElement; if (isPosterEl || isCSSSelector(poster)) { const elCandidate = isPosterEl ? poster : rootEl.querySelector(poster as any); if (!elCandidate) { throw new View3DError(ERROR.MESSAGES.ELEMENT_NOT_FOUND(poster as string), ERROR.CODES.ELEMENT_NOT_FOUND); } posterEl = elCandidate as HTMLElement; } else { const imgEl = document.createElement("img"); imgEl.className = DEFAULT_CLASS.POSTER; imgEl.src = poster as string; rootEl.appendChild(imgEl); posterEl = imgEl; this.once(EVENTS.READY, () => { if (imgEl.parentElement !== rootEl) return; rootEl.removeChild(imgEl); }); } this.once(EVENTS.READY, () => { if (!posterEl.parentElement) return; // Remove that element from the parent element posterEl.parentElement.removeChild(posterEl); }); } private async _initPlugins(plugins: View3DPlugin[]) { await Promise.all(plugins.map(plugin => plugin.init(this))); } private async _resetLoadingContextOnFinish(tasks: Array<Promise<any>>) { void Promise.all(tasks).then(() => { this.trigger(EVENTS.LOAD_FINISH, { type: EVENTS.LOAD_FINISH, target: this }); this._loadingContext = []; }); } } export default View3D;