UNPKG

threepipe

Version:

A 3D viewer framework built on top of three.js in TypeScript with a focus on quality rendering, modularity and extensibility.

534 lines 21.9 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, 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 } from '../object/IObjectUi'; import { CameraView } from './CameraView'; // todo: maybe change domElement to some wrapper/base class of viewer 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 trye 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(); this.refreshUi = iCameraCommons.refreshUi; this.refreshTarget = iCameraCommons.refreshTarget; this.activateMain = iCameraCommons.activateMain; this.deactivateMain = iCameraCommons.deactivateMain; // endregion // region controls // todo: move orbit to a plugin maybe? so that its not forced this._controlsCtors = new Map([['orbit', (object, domElement) => { const controls = new OrbitControls3(object, domElement ? !domElement.ownerDocument ? domElement.documentElement : domElement : document.body); // this._controls.enabled = false // this._controls.listenToKeyEvents(window as any) // optional // todo: this breaks keyboard events in UI like cursor left/right, make option for this // 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 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), ]; 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) { const size = this._interactionsDisabledBy.size; if (enabled) { this._interactionsDisabledBy.delete(by); } else { this._interactionsDisabledBy.add(by); } if (size !== this._interactionsDisabledBy.size) this.refreshCameraControls(true); } get canUserInteract() { return this._interactionsDisabledBy.size === 0 && this.isMainCamera && this.controlsMode !== ''; } // endregion // region refreshing setDirty(options) { if (!this._positionWorld) return; // class not initialized if (!options?.key || options?.key === 'fov' || options?.key === 'zoom') this.updateProjectionMatrix(); this.getWorldPosition(this._positionWorld); 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 } /** * 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 */ refreshAspect(setDirty = true) { if (this.autoAspect) { if (!this._canvas) console.error('PerspectiveCamera2: cannot calculate aspect ratio without canvas/container'); else { let aspect = this._canvas.clientWidth / this._canvas.clientHeight; if (!isFinite(aspect)) aspect = 1; this.aspect = aspect; this.updateProjectionMatrix?.(); } } if (setDirty) this.setDirty(); // console.log('refreshAspect', this._options.aspect) } _nearFarChanged() { if (this.view === undefined) return; // not initialized yet this.updateProjectionMatrix?.(); } setControlsCtor(key, ctor, replace = false) { if (!replace && this._controlsCtors.has(key)) { console.error(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) { // 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 baseOnly - Calls only super.toJSON, does internal three.js serialization. Set it to true only if you know what you are doing. */ toJSON(meta, baseOnly = false) { if (baseOnly) return super.toJSON(meta); // todo add camOptions for backwards compatibility? return ThreeSerialization.Serialize(this, meta, true); } 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.setDirty({ change: 'deserialize' }); return this; } // endregion // region camera views getView(worldSpace = true, _view) { const up = new Vector3(); this.updateWorldMatrix(true, false); const matrix = this.matrixWorld; up.x = matrix.elements[4]; up.y = matrix.elements[5]; up.z = matrix.elements[6]; up.normalize(); const view = _view || new CameraView(); view.name = this.name; view.position.copy(this.position); view.target.copy(this.target); view.quaternion.copy(this.quaternion); view.zoom = this.zoom; // view.up.copy(up) const parent = this.parent; if (parent) { if (worldSpace) { view.position.applyMatrix4(parent.matrixWorld); this.getWorldQuaternion(view.quaternion); // target, up is already in world space } else { up.transformDirection(parent.matrixWorld.clone().invert()); // pos is already in local space // target should always be in world space } } view.isWorldSpace = worldSpace; view.uiConfig?.uiRefresh?.(true, 'postFrame'); return view; } setView(view) { this.position.copy(view.position); this.target.copy(view.target); // this.up.copy(view.up) this.quaternion.copy(view.quaternion); this.zoom = view.zoom; this.setDirty(); } setViewFromCamera(camera, distanceFromTarget, worldSpace = true) { // todo: getView, setView can also be used, do we need copy? as that will copy all the properties this.copy(camera, undefined, distanceFromTarget, worldSpace); } setViewToMain(eventOptions) { this.dispatchEvent({ type: 'setView', ...eventOptions, camera: this, bubbleToParent: true }); } /** * See also cameraHelpers.glsl * @param material */ updateShaderProperties(material) { material.uniforms.cameraPositionWorld?.value?.copy(this._positionWorld); material.uniforms.cameraNearFar?.value?.set(this.near, this.far); if (material.uniforms.projection) material.uniforms.projection.value = this.projectionMatrix; // todo: rename to projectionMatrix2? material.defines.PERSPECTIVE_CAMERA = this.type === 'PerspectiveCamera' ? '1' : '0'; // material.defines.ORTHOGRAPHIC_CAMERA = this.type === 'OrthographicCamera' ? '1' : '0' // todo return this; } dispose() { this._disposeCameraControls(); // todo: anything else? // iObjectCommons.dispose and dispatch event dispose is called automatically because of updateObject3d } 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), uiSlider('FoV Zoom', [0.001, 10], 0.001), serialize() ], PerspectiveCamera2.prototype, "zoom", void 0); __decorate([ uiVector('Position', undefined, undefined, (that) => ({ onChange: () => that.setDirty() })), serialize() ], PerspectiveCamera2.prototype, "position", void 0); __decorate([ uiVector('Target', undefined, undefined, (that) => ({ onChange: () => that.setDirty() })), serialize() ], PerspectiveCamera2.prototype, "target", void 0); __decorate([ serialize(), onChange2(PerspectiveCamera2.prototype.refreshAspect), uiToggle('Auto Aspect') ], PerspectiveCamera2.prototype, "autoAspect", 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