| @@ -71,6 +71,11 @@ export interface ICamera<E extends ICameraEvent = ICameraEvent, ET extends ICame | |||
| controlsMode?: TCameraControlsMode; // todo add more. | |||
| // controlsEnabled: boolean; // use controlsMode = '' instead | |||
| // also in userData | |||
| autoNearFar: boolean // default = true | |||
| minNearPlane: number // default = 0.2 | |||
| maxFarPlane: number // default = 1000 | |||
| // todo | |||
| // Note: for userData: add _ in front of for private use, which is preserved while cloning but not serialisation, and __ for private use, which is not preserved while cloning and serialisation | |||
| userData: ICameraUserData | |||
| @@ -8,6 +8,7 @@ import {OrbitControls3} from '../../three' | |||
| import {IObject3D} from '../IObject' | |||
| import {ThreeSerialization} from '../../utils' | |||
| import {iCameraCommons} from '../object/iCameraCommons' | |||
| import {bindToValue} from '../../three/utils/decorators' | |||
| // todo: maybe change domElement to some wrapper/base class of viewer | |||
| export class PerspectiveCamera2 extends PerspectiveCamera implements ICamera { | |||
| @@ -58,17 +59,41 @@ export class PerspectiveCamera2 extends PerspectiveCamera implements ICamera { | |||
| autoAspect: boolean | |||
| /** | |||
| * Near clipping plane. This is managed by RootScene for active cameras | |||
| * 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} | |||
| */ | |||
| @onChange2(PerspectiveCamera2.prototype._nearFarChanged) | |||
| near = 0.01 | |||
| /** | |||
| * Far clipping plane. This is managed by RootScene for active cameras | |||
| * 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} | |||
| */ | |||
| @onChange2(PerspectiveCamera2.prototype._nearFarChanged) | |||
| far = 50 | |||
| /** | |||
| * Automatically manage near and far clipping planes based on scene size. | |||
| */ | |||
| @bindToValue({obj: 'userData', onChange: 'setDirty'}) | |||
| autoNearFar = true | |||
| /** | |||
| * Minimum near clipping plane allowed. (Distance from camera) | |||
| * @default 0.2 | |||
| */ | |||
| @bindToValue({obj: 'userData', onChange: 'setDirty'}) | |||
| minNearPlane = 0.2 | |||
| /** | |||
| * Maximum far clipping plane allowed. (Distance from camera) | |||
| */ | |||
| @bindToValue({obj: 'userData', onChange: 'setDirty'}) | |||
| maxFarPlane = 1000 | |||
| constructor(controlsMode?: TCameraControlsMode, domElement?: HTMLCanvasElement, autoAspect?: boolean, fov?: number, aspect?: number) { | |||
| super(fov, aspect) | |||
| this._canvas = domElement | |||
| @@ -261,13 +286,13 @@ export class PerspectiveCamera2 extends PerspectiveCamera implements ICamera { | |||
| 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.controlsMode) data.controlsMode = op.controlsMode | |||
| // if (op.controlsEnabled) data.controlsEnabled = op.controlsEnabled | |||
| delete data.camOptions | |||
| } | |||
| @@ -312,17 +337,18 @@ export class PerspectiveCamera2 extends PerspectiveCamera implements ICamera { | |||
| ...generateUiConfig(this), | |||
| { | |||
| type: 'input', | |||
| label: 'Min Near', | |||
| getValue: () => this.userData.minNearPlane ?? 0.2, | |||
| setValue: (v) => this.userData.minNearPlane = v, | |||
| onChange: () => this.setDirty(), | |||
| label: ()=>(this.autoNearFar ? 'Min' : '') + ' Near', | |||
| property: [this, 'minNearPlane'], | |||
| }, | |||
| { | |||
| type: 'input', | |||
| label: ()=>(this.autoNearFar ? 'Max' : '') + ' Far', | |||
| property: [this, 'maxFarPlane'], | |||
| }, | |||
| { | |||
| type: 'input', | |||
| label: 'Max Far', | |||
| getValue: () => this.userData.maxFarPlane ?? 1000, | |||
| setValue: (v) => this.userData.maxFarPlane = v, | |||
| onChange: () => this.setDirty(), | |||
| label: 'Auto Near Far', | |||
| property: [this, 'autoNearFar'], | |||
| }, | |||
| ()=>({ // because _controlsCtors can change | |||
| type: 'dropdown', | |||
| @@ -342,16 +368,12 @@ export class PerspectiveCamera2 extends PerspectiveCamera implements ICamera { | |||
| ()=>(this._controls as OrbitControls3)?.zoomIn ? { | |||
| type: 'button', | |||
| label: 'Zoom in', | |||
| value: ()=>{ | |||
| (this._controls as OrbitControls3)?.zoomIn(1) | |||
| }, | |||
| value: ()=> (this._controls as OrbitControls3)?.zoomIn(1), | |||
| } : {}, | |||
| ()=>(this._controls as OrbitControls3)?.zoomOut ? { | |||
| type: 'button', | |||
| label: 'Zoom out', | |||
| value: ()=>{ | |||
| (this._controls as OrbitControls3)?.zoomOut(1) | |||
| }, | |||
| value: ()=> (this._controls as OrbitControls3)?.zoomOut(1), | |||
| } : {}, | |||
| ()=>this._controls?.uiConfig, | |||
| ], | |||
| @@ -1,4 +1,4 @@ | |||
| import {AnyFunction, safeSetProperty} from 'ts-browser-helpers' | |||
| import {AnyFunction, getOrCall, safeSetProperty, ValOrFunc} from 'ts-browser-helpers' | |||
| /** | |||
| * | |||
| @@ -38,6 +38,18 @@ export function uniform({uniforms, propKey, thisTarget = false}: {uniforms?: any | |||
| } | |||
| } | |||
| function callOnChange(this: any, onChange: (...args: any[]) => any, params: any[]) { | |||
| // same logic as onChange in ts-browser-helpers. todo: loop through object prototype chain like in onChange? | |||
| if (onChange.name) { | |||
| const fn: AnyFunction = this[onChange.name] | |||
| if (fn === onChange) | |||
| onChange.call(this, ...params) | |||
| else if (fn.name.endsWith(`bound ${onChange.name}`)) | |||
| fn(...params) | |||
| else onChange(...params) | |||
| } else onChange(...params) | |||
| } | |||
| /** | |||
| * | |||
| * @param customDefines - object for setting define value (like ShaderMaterial.defines), otherwise this.material.defines is taken | |||
| @@ -68,18 +80,7 @@ export function matDefine(key?: string|symbol, customDefines?: any, thisMat = fa | |||
| safeSetProperty(t, p, newVal, true) | |||
| if (newVal === undefined) delete t[p] | |||
| if (onChange && typeof onChange === 'function') { | |||
| const params = [p, newVal] | |||
| // same logic as onChange in ts-browser-helpers. todo: loop through object prototype chain like in onChange? | |||
| if (onChange.name) { | |||
| const fn: AnyFunction = this[onChange.name] | |||
| if (fn === onChange) | |||
| onChange.call(this, ...params) | |||
| else if (fn.name.endsWith(`bound ${onChange.name}`)) | |||
| fn(...params) | |||
| else onChange(...params) | |||
| } else { | |||
| onChange(...params) | |||
| } | |||
| callOnChange.call(this, onChange, [p, newVal]) | |||
| } else { | |||
| safeSetProperty(thisMat ? this : this.material, 'needsUpdate', true, true) | |||
| } | |||
| @@ -89,3 +90,43 @@ export function matDefine(key?: string|symbol, customDefines?: any, thisMat = fa | |||
| }) | |||
| } | |||
| } | |||
| /** | |||
| * Binds a property to a value in an object. If the object is a string, it is used as a property name in `this`. | |||
| * @param obj - object to bind to. If a string, it is used as a property name in `this`. If a function, it is called and the result is used as the object/string. | |||
| * @param key - key to bind to. If a string, it is used as a property name in `this`. If a function, it is called and the result is used as the key/string. | |||
| * @param onChange - function to call when the value changes. If a string, it is used as a property name in `this` and called. If a function, it is called. The function is called with the following parameters: key, newVal | |||
| * @param processVal - function that processes the value before setting it. | |||
| * @param invProcessVal - function that processes the value before returning it. | |||
| */ | |||
| export function bindToValue({obj, key, onChange, processVal, invProcessVal}: {obj?: ValOrFunc<any>, key?: ValOrFunc<string | symbol>, onChange?: ((...args: any[]) => any)|string, processVal?: (newVal: any) => any, invProcessVal?: (val: any) => any}): PropertyDecorator { | |||
| const cPropKey = !!key | |||
| return (targetPrototype: any, propertyKey: string|symbol) => { | |||
| const getTarget = (_this: any)=>{ | |||
| let t = getOrCall(obj) || _this | |||
| if (typeof t === 'string') t = _this[t] | |||
| const p = cPropKey ? getOrCall(key) || propertyKey : propertyKey | |||
| return {t, p} | |||
| } | |||
| Object.defineProperty(targetPrototype, propertyKey, { | |||
| get() { | |||
| const {t, p} = getTarget(this) | |||
| let res = t[p] | |||
| if (invProcessVal) res = invProcessVal(res) | |||
| return res | |||
| }, | |||
| set(newVal: any) { | |||
| const {t, p} = getTarget(this) | |||
| if (processVal) newVal = processVal(newVal) | |||
| safeSetProperty(t, p, newVal, true) | |||
| if (newVal === undefined) delete t[p] | |||
| let oc = onChange | |||
| if (oc && (typeof oc === 'string' || typeof oc === 'symbol')) oc = this[oc] | |||
| if (oc && typeof oc === 'function') callOnChange.call(this, oc, [p, newVal]) | |||
| }, | |||
| // configurable: true, | |||
| // enumerable: true, | |||
| }) | |||
| } | |||
| } | |||