UNPKG

threepipe

Version:

A modern 3D viewer framework built on top of three.js, written in TypeScript, designed to make creating high-quality, modular, and extensible 3D experiences on the web simple and enjoyable.

496 lines 20.8 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { Object3D, PerspectiveCamera, Vector3 } from 'three'; import { generateUiConfig, uiInput, uiNumber, uiSlider, uiToggle, uiVector } from 'uiconfig.js'; import { onChange, onChange2, onChange3, serialize } from 'ts-browser-helpers'; import { OrbitControls3 } from '../../three/controls/OrbitControls3'; import { ThreeSerialization } from '../../utils'; import { iCameraCommons } from '../object/iCameraCommons'; import { bindToValue } from '../../three/utils/decorators'; import { makeICameraCommonUiConfig, objectExtensionsUiConfig } from '../object/IObjectUi'; // todo: maybe change domElement to some wrapper/base class of viewer /** * A camera class that extends {@link PerspectiveCamera} with additional features and built-in control support. */ export class PerspectiveCamera2 extends PerspectiveCamera { get controls() { return this._controls; } get isMainCamera() { return this.userData ? this.userData.__isMainCamera || false : false; } constructor(controlsMode, domElement, autoAspect, fov, aspect) { super(fov, aspect); this.assetType = 'camera'; this._currentControlsMode = ''; this.userData = {}; /** * The target position of the camera (where the camera looks at). Also syncs with the controls.target, so it's not required to set that separately. * Note: this is always in world-space * Note: {@link autoLookAtTarget} must be set to `true` to make the camera look at the target when no controls are enabled */ this.target = new Vector3(0, 0, 0); /** * Near clipping plane. * This is managed by RootScene for active cameras * To change the minimum that's possible set {@link minNearPlane} * To use a fixed value set {@link autoNearFar} to false and set {@link minNearPlane} */ this.near = 0.01; /** * Far clipping plane. * This is managed by RootScene for active cameras * To change the maximum that's possible set {@link maxFarPlane} * To use a fixed value set {@link autoNearFar} to false and set {@link maxFarPlane} */ this.far = 50; /** * Automatically make the camera look at the {@link target} on {@link setDirty} call * Defaults to false. Note that this must be set to true to make the camera look at the target without any controls */ this.autoLookAtTarget = false; // bound to userData so that it's saved in the glb. /** * Automatically manage near and far clipping planes based on scene size. */ this.autoNearFar = true; // bound to userData so that it's saved in the glb. /** * Minimum near clipping plane allowed. (Distance from camera) * Used in RootScene when {@link autoNearFar} is true. * @default 0.2 */ this.minNearPlane = 0.5; /** * Maximum far clipping plane allowed. (Distance from camera) * Used in RootScene when {@link autoNearFar} is `true`. */ this.maxFarPlane = 1000; /** * Automatically move the camera(dolly) when the field of view(fov) changes. * Works when controls are enabled or `autoLookAtTarget` is `true`. * * Note - this is not exact */ this.dollyFov = false; // bound to userData so that it's saved in the glb. // @serialize('camOptions') //todo handle deserialization of this // region interactionsEnabled // private _interactionsEnabled = true // // get interactionsEnabled(): boolean { // return this._interactionsEnabled // } // // set interactionsEnabled(value: boolean) { // if (this._interactionsEnabled !== value) { // this._interactionsEnabled = value // this.refreshCameraControls(true) // } // } this._interactionsDisabledBy = new Set(); /** * when aspect ratio is set to auto it must be refreshed on resize, this is done by the viewer for the main camera. * @param setDirty */ this.refreshAspect = iCameraCommons.refreshAspect; this.refreshUi = iCameraCommons.refreshUi; this.refreshTarget = iCameraCommons.refreshTarget; this.activateMain = iCameraCommons.activateMain; this.deactivateMain = iCameraCommons.deactivateMain; // @ts-expect-error ts issue this.updateShaderProperties = iCameraCommons.updateShaderProperties; // endregion // region controls // todo: move orbit to a plugin maybe? so that its not forced this._controlsCtors = new Map([['orbit', (object, domElement) => { const elem = domElement ? !domElement.ownerDocument ? domElement.documentElement : domElement : document.body; const controls = new OrbitControls3(object, elem); // this._controls.enabled = false // set tab index so that we get keyboard events if (elem.tabIndex === -1) { elem.tabIndex = 1000; // disable focus outline elem.style.outline = 'none'; } controls.listenToKeyEvents(elem); // optional // todo: make option for this // controls.enableKeys = true controls.screenSpacePanning = true; return controls; }]]); this._controlsChanged = () => { if (this._controls && this._controls.target) this.refreshTarget(undefined, false); this.setDirty({ change: 'controls' }); }; // endregion // region camera views this.getView = iCameraCommons.getView; this.setView = iCameraCommons.setView; this.setViewFromCamera = iCameraCommons.setViewFromCamera; this.setViewToMain = iCameraCommons.setViewToMain; // endregion // region utils/others // for shader prop updater this['_positionWorld'] = new Vector3(); // endregion // region ui this._camUi = [ ...generateUiConfig(this) || [], { type: 'input', label: () => (this.autoNearFar ? 'Min' : '') + ' Near', property: [this, 'minNearPlane'], }, { type: 'input', label: () => (this.autoNearFar ? 'Max' : '') + ' Far', property: [this, 'maxFarPlane'], }, { type: 'input', label: 'Auto Near Far', property: [this, 'autoNearFar'], }, { type: 'input', label: 'Dolly FoV', property: [this, 'dollyFov'], }, () => ({ type: 'dropdown', label: 'Controls Mode', property: [this, 'controlsMode'], children: ['', 'orbit', ...this._controlsCtors.keys()].map(v => ({ label: v === '' ? 'none' : v, value: v })), onChange: () => this.refreshCameraControls(), }), () => makeICameraCommonUiConfig.call(this, this.uiConfig), objectExtensionsUiConfig.call(this), ]; this.uiConfig = { type: 'folder', label: () => this.name || 'Camera', children: [ ...this._camUi, // todo hack for zoom in and out for now. () => this._controls?.zoomIn ? { type: 'button', label: 'Zoom in', value: () => this._controls?.zoomIn(1), } : {}, () => this._controls?.zoomOut ? { type: 'button', label: 'Zoom out', value: () => this._controls?.zoomOut(1), } : {}, () => this._controls?.uiConfig, ], }; this._canvas = domElement; this.autoAspect = autoAspect ?? !!domElement; iCameraCommons.upgradeCamera.call(this); // todo: test if autoUpgrade = false works as expected if we call upgradeObject3D externally after constructor, because we have setDirty, refreshTarget below. this.controlsMode = controlsMode || ''; this.refreshTarget(undefined, false); // if (!camera) // this.targetUpdated(false) this.setDirty(); // if (domElement) // domElement.style.touchAction = 'none' // this is done in orbit controls anyway // const ae = this._canvas.addEventListener // todo: this breaks tweakpane UI. // this._canvas.addEventListener = (type: string, listener: any, options1: any) => { // see https://github.com/mrdoob/three.js/pull/19782 // ae(type, listener, type === 'wheel' && typeof options1 !== 'boolean' ? { // ...typeof options1 === 'object' ? options1 : {}, // capture: false, // passive: false, // } : options1) // } // this.refreshCameraControls() // this is done on set controlsMode // const target = this.target } /** * If interactions are enabled for this camera. It can be disabled by some code or plugin. * see also {@link setInteractions} * @deprecated use {@link canUserInteract} to check if the user can interact with this camera * @readonly */ get interactionsEnabled() { return this._interactionsDisabledBy.size === 0; } setInteractions(enabled, by, setDirty = true) { const size = this._interactionsDisabledBy.size; if (enabled) { this._interactionsDisabledBy.delete(by); } else { this._interactionsDisabledBy.add(by); } if (size !== this._interactionsDisabledBy.size) this.refreshCameraControls(setDirty); } get canUserInteract() { return this._interactionsDisabledBy.size === 0 && this.isMainCamera && this.controlsMode !== ''; } // endregion // region refreshing setDirty(options) { iCameraCommons.setDirty.call(this, options); if (options?.last !== false) this._camUi?.forEach(u => u?.uiRefresh?.(false, 'postFrame', 1)); // because camera changes a lot. so we dont want to deep refresh ui on every change } _nearFarChanged() { if (this.view === undefined) return; // not initialized yet this.updateProjectionMatrix && this.updateProjectionMatrix(); } setControlsCtor(key, ctor, replace = false) { if (!replace && this._controlsCtors.has(key)) { console.error('PerspectiveCamera2: ' + key + ' already exists.'); return; } this._controlsCtors.set(key, ctor); } removeControlsCtor(key) { this._controlsCtors.delete(key); } _initCameraControls() { const mode = this.controlsMode; this._controls = this._controlsCtors.get(mode)?.(this, this._canvas) ?? undefined; if (!this._controls && mode !== '') console.error('PerspectiveCamera2 - Unable to create controls with mode ' + mode + '. Are you missing a plugin?'); this._controls?.addEventListener('change', this._controlsChanged); this._currentControlsMode = this._controls ? mode : ''; // todo maybe set target like this: // if (this._controls) this._controls.target = this.target } _disposeCameraControls() { if (this._controls) { if (this._controls.target === this.target) this._controls.target = new Vector3(); // just in case this._controls?.removeEventListener('change', this._controlsChanged); this._controls?.dispose(); } this._currentControlsMode = ''; this._controls = undefined; } refreshCameraControls(setDirty = true) { if (!this._controlsCtors) return; // class not initialized if (this._controls) { if (this._currentControlsMode !== this.controlsMode || this !== this._controls.object || this._controls.domElement && this._canvas !== this._controls.domElement) { // in-case camera changed or mode changed this._disposeCameraControls(); this._initCameraControls(); } } else { this._initCameraControls(); } // todo: only for orbit control like controls? if (this._controls) { const ce = this.canUserInteract; this._controls.enabled = ce; if (ce) this.up.copy(Object3D.DEFAULT_UP); } if (setDirty) this.setDirty(); this.refreshUi(); } // endregion // region serialization /** * Serializes this camera with controls to JSON. * @param meta - metadata for serialization * @param _internal - Calls only super.toJSON, does internal three.js serialization and `@serialize` tags. Set it to true only if you know what you are doing. This is used in Serialization->serializer */ toJSON(meta, _internal = false) { if (_internal) return { ...super.toJSON(meta), ...ThreeSerialization.Serialize(this, meta, true), // this will serialize the properties of this class(like defined with @serialize and @serialize attribute) }; // todo add camOptions for backwards compatibility? return ThreeSerialization.Serialize(this, meta, false); // this will call toJSON again, but with _internal=true, that's why we set isThis to false. } fromJSON(data, meta) { if (data.camOptions || data.aspect === 'auto') data = { ...data }; if (data.camOptions) { const op = data.camOptions; if (op.fov) data.fov = op.fov; if (op.focus) data.focus = op.focus; if (op.zoom) data.zoom = op.zoom; if (op.aspect) data.aspect = op.aspect; if (op.controlsMode) data.controlsMode = op.controlsMode; // todo: add support for this // if (op.left) data.left = op.left // if (op.right) data.right = op.right // if (op.top) data.top = op.top // if (op.bottom) data.bottom = op.bottom // if (op.frustumSize) data.frustumSize = op.frustumSize // if (op.controlsEnabled) data.controlsEnabled = op.controlsEnabled delete data.camOptions; } if (data.aspect === 'auto') { data.aspect = this.aspect; this.autoAspect = true; } // if (data.cameraObject) this._camera.fromJSON(data.cameraObject) // todo: add check for OrbitControls being not deserialized(inited properly) if it doesn't exist yet (if it is not inited properly) // console.log(JSON.parse(JSON.stringify(data))) ThreeSerialization.Deserialize(data, this, meta, true); this.refreshAspect(false); this.setDirty({ change: 'deserialize' }); return this; } dispose() { this._disposeCameraControls(); // todo: anything else? // iObjectCommons.dispose and dispatch event dispose is called automatically because of updateObject3d } setCanvas(canvas, refresh = true) { this._canvas = canvas; if (!refresh) return; this.refreshCameraControls(); this.refreshAspect(false); } get isActiveCamera() { return this.isMainCamera; } /** * @deprecated use `<T>camera.controls` instead */ getControls() { return this._controls; } /** * @deprecated use `this` instead */ get cameraObject() { return this; } /** * @deprecated use `this` instead */ get modelObject() { return this; } /** * @deprecated - use setDirty directly * @param setDirty */ targetUpdated(setDirty = true) { if (setDirty) this.setDirty(); } } __decorate([ uiInput('Name') ], PerspectiveCamera2.prototype, "name", void 0); __decorate([ serialize('camControls') ], PerspectiveCamera2.prototype, "_controls", void 0); __decorate([ onChange2(PerspectiveCamera2.prototype.refreshCameraControls) ], PerspectiveCamera2.prototype, "controlsMode", void 0); __decorate([ serialize() ], PerspectiveCamera2.prototype, "userData", void 0); __decorate([ onChange3(PerspectiveCamera2.prototype.setDirty), uiSlider('Field Of View', [1, 180], 0.001), serialize() ], PerspectiveCamera2.prototype, "fov", void 0); __decorate([ onChange3(PerspectiveCamera2.prototype.setDirty), serialize() ], PerspectiveCamera2.prototype, "focus", void 0); __decorate([ onChange3(PerspectiveCamera2.prototype.setDirty), uiNumber('FoV Zoom'), serialize() ], PerspectiveCamera2.prototype, "zoom", void 0); __decorate([ uiVector('Position', undefined, undefined, (t) => ({ onChange: () => t.setDirty({ change: 'position' }) })), serialize() ], PerspectiveCamera2.prototype, "position", void 0); __decorate([ uiVector('Up', undefined, undefined, (t) => ({ onChange: () => t.setDirty({ change: 'up' }) })), serialize() ], PerspectiveCamera2.prototype, "up", void 0); __decorate([ uiVector('Rotation', undefined, undefined, (t) => ({ onChange: () => t.setDirty({ change: 'rotation' }), disabled: () => t.autoLookAtTarget })) /* @serialize()*/ ], PerspectiveCamera2.prototype, "rotation", void 0); __decorate([ uiVector('Target', undefined, undefined, (t) => ({ onChange: () => t.setDirty({ change: 'target' }), disabled: () => !t.autoLookAtTarget })), serialize() ], PerspectiveCamera2.prototype, "target", void 0); __decorate([ serialize(), onChange2('refreshAspect'), uiToggle('Auto Aspect') ], PerspectiveCamera2.prototype, "autoAspect", void 0); __decorate([ serialize(), onChange2('refreshAspect'), uiNumber('Aspect Ratio', (t) => ({ hidden: () => t.autoAspect })) ], PerspectiveCamera2.prototype, "aspect", void 0); __decorate([ onChange2(PerspectiveCamera2.prototype._nearFarChanged) ], PerspectiveCamera2.prototype, "near", void 0); __decorate([ onChange2(PerspectiveCamera2.prototype._nearFarChanged) ], PerspectiveCamera2.prototype, "far", void 0); __decorate([ bindToValue({ obj: 'userData', onChange: 'setDirty' }) ], PerspectiveCamera2.prototype, "autoLookAtTarget", void 0); __decorate([ bindToValue({ obj: 'userData', onChange: 'setDirty' }) ], PerspectiveCamera2.prototype, "autoNearFar", void 0); __decorate([ bindToValue({ obj: 'userData', onChange: 'setDirty' }) ], PerspectiveCamera2.prototype, "minNearPlane", void 0); __decorate([ bindToValue({ obj: 'userData', onChange: 'setDirty' }) ], PerspectiveCamera2.prototype, "maxFarPlane", void 0); __decorate([ bindToValue({ obj: 'userData' }) ], PerspectiveCamera2.prototype, "dollyFov", void 0); __decorate([ onChange((k, v) => { if (!v) console.warn('Setting camera invisible is not supported', k, v); }) ], PerspectiveCamera2.prototype, "visible", void 0); /** * Empty class with the constructor same as PerspectiveCamera in three.js. * This can be used to remain compatible with three.js construct signature. */ export class PerspectiveCamera0 extends PerspectiveCamera2 { constructor(fov, aspect, near, far) { super(undefined, undefined, undefined, fov, aspect || 1); this.dollyFov = false; if (near || far) { this.autoNearFar = false; if (near) { this.near = near; this.minNearPlane = near; } if (far) { this.far = far; this.maxFarPlane = far; } } } } //# sourceMappingURL=PerspectiveCamera2.js.map