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.
446 lines • 18.8 kB
JavaScript
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, OrthographicCamera, Vector3 } from 'three';
import { generateUiConfig, uiInput, uiNumber, 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: extract out common functions with perspective camera into iCameraCommons
// todo: maybe change domElement to some wrapper/base class of viewer
export class OrthographicCamera2 extends OrthographicCamera {
get controls() {
return this._controls;
}
get isMainCamera() {
return this.userData ? this.userData.__isMainCamera || false : false;
}
/**
* Frustum size of the camera. This is used to calculate bounds (left, right, top, bottom) based on aspect ratio.
* Set to 0 (or negative) value to disable automatic, and to set the bounds manually.
*/
get frustumSize() {
return this._frustumSize ?? 0;
}
set frustumSize(value) {
this._frustumSize = value <= 0 ? undefined : value;
this.refreshFrustum(false);
this.setDirty();
}
constructor(controlsMode, domElement, autoAspect, frustumSize, left, right, top, bottom, near, far, aspect) {
super(left, right, top, bottom, near, far);
this.assetType = 'camera';
this._currentControlsMode = '';
this.userData = {};
this._frustumSize = undefined;
/**
* 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;
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: '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,
() => this._controls?.uiConfig,
],
};
this._canvas = domElement;
this.aspect = aspect || 1;
this._frustumSize = frustumSize ?? 4;
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);
this.refreshFrustum(false);
// if (!camera)
// this.targetUpdated(false)
this.setDirty();
// if (domElement)
// domElement.style.touchAction = 'none' // this is done in orbit controls anyway
// 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) {
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();
}
refreshFrustum(setDirty = true) {
if (this._frustumSize === undefined)
return;
this.top = this._frustumSize / 2;
this.bottom = -this.top;
this.left = this.bottom * this.aspect;
this.right = this.top * this.aspect;
setDirty && this.setDirty();
}
setControlsCtor(key, ctor, replace = false) {
if (!replace && this._controlsCtors.has(key)) {
console.error('OrthographicCamera2: ' + 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('OrthographicCamera2 - 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)
};
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) {
ThreeSerialization.Deserialize(data, this, meta, true);
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')
], OrthographicCamera2.prototype, "name", void 0);
__decorate([
serialize('camControls')
], OrthographicCamera2.prototype, "_controls", void 0);
__decorate([
onChange2(OrthographicCamera2.prototype.refreshCameraControls)
], OrthographicCamera2.prototype, "controlsMode", void 0);
__decorate([
serialize()
], OrthographicCamera2.prototype, "userData", void 0);
__decorate([
onChange3(OrthographicCamera2.prototype.setDirty),
uiNumber('Zoom'),
serialize()
], OrthographicCamera2.prototype, "zoom", void 0);
__decorate([
onChange3(OrthographicCamera2.prototype.setDirty),
uiNumber('Left', (t) => ({ hidden: () => t._frustumSize !== undefined })),
serialize()
], OrthographicCamera2.prototype, "left", void 0);
__decorate([
onChange3(OrthographicCamera2.prototype.setDirty),
uiNumber('Right', (t) => ({ hidden: () => t._frustumSize !== undefined })),
serialize()
], OrthographicCamera2.prototype, "right", void 0);
__decorate([
onChange3(OrthographicCamera2.prototype.setDirty),
uiNumber('Top', (t) => ({ hidden: () => t._frustumSize !== undefined })),
serialize()
], OrthographicCamera2.prototype, "top", void 0);
__decorate([
onChange3(OrthographicCamera2.prototype.setDirty),
uiNumber('Bottom', (t) => ({ hidden: () => t._frustumSize !== undefined })),
serialize()
], OrthographicCamera2.prototype, "bottom", void 0);
__decorate([
uiInput('Frustum Size' /* , (t)=>({hidden: ()=>t.frustumSize === undefined})*/)
], OrthographicCamera2.prototype, "frustumSize", null);
__decorate([
uiVector('Position', undefined, undefined, (t) => ({ onChange: () => t.setDirty({ change: 'position' }) })),
serialize()
], OrthographicCamera2.prototype, "position", void 0);
__decorate([
uiVector('Up', undefined, undefined, (t) => ({ onChange: () => t.setDirty({ change: 'up' }) })),
serialize()
], OrthographicCamera2.prototype, "up", void 0);
__decorate([
uiVector('Rotation', undefined, undefined, (t) => ({ onChange: () => t.setDirty({ change: 'rotation' }), disabled: () => t.autoLookAtTarget }))
/* @serialize()*/
], OrthographicCamera2.prototype, "rotation", void 0);
__decorate([
uiVector('Target', undefined, undefined, (t) => ({ onChange: () => t.setDirty({ change: 'target' }), disabled: () => !t.autoLookAtTarget })),
serialize()
], OrthographicCamera2.prototype, "target", void 0);
__decorate([
serialize(),
onChange2('refreshAspect'),
uiToggle('Auto Aspect')
], OrthographicCamera2.prototype, "autoAspect", void 0);
__decorate([
serialize(),
onChange2('refreshAspect'),
uiNumber('Aspect Ratio', (t) => ({ hidden: () => t.autoAspect }))
], OrthographicCamera2.prototype, "aspect", void 0);
__decorate([
onChange2(OrthographicCamera2.prototype._nearFarChanged)
], OrthographicCamera2.prototype, "near", void 0);
__decorate([
onChange2(OrthographicCamera2.prototype._nearFarChanged)
], OrthographicCamera2.prototype, "far", void 0);
__decorate([
bindToValue({ obj: 'userData', onChange: 'setDirty' })
], OrthographicCamera2.prototype, "autoLookAtTarget", void 0);
__decorate([
bindToValue({ obj: 'userData', onChange: 'setDirty' })
], OrthographicCamera2.prototype, "autoNearFar", void 0);
__decorate([
bindToValue({ obj: 'userData', onChange: 'setDirty' })
], OrthographicCamera2.prototype, "minNearPlane", void 0);
__decorate([
bindToValue({ obj: 'userData', onChange: 'setDirty' })
], OrthographicCamera2.prototype, "maxFarPlane", void 0);
__decorate([
onChange((k, v) => {
if (!v)
console.warn('Setting camera invisible is not supported', k, v);
})
], OrthographicCamera2.prototype, "visible", void 0);
/**
* Empty class with the constructor same as OrthographicCamera in three.js.
* This can be used to remain compatible with three.js construct signature.
*/
export class OrthographicCamera0 extends OrthographicCamera2 {
constructor(left, right, top, bottom, near, far) {
super(undefined, undefined, undefined, undefined, left, right, top, bottom, near, far, 1);
if (near !== undefined || far) {
this.autoNearFar = false;
if (near) {
this.near = near;
this.minNearPlane = near;
}
if (far) {
this.far = far;
this.maxFarPlane = far;
}
}
}
}
//# sourceMappingURL=OrthographicCamera2.js.map