-
Notifications
You must be signed in to change notification settings - Fork 20
/
decorators.ts
158 lines (150 loc) · 8.17 KB
/
decorators.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import {AnyFunction, getOrCall, safeSetProperty, ValOrFunc} from 'ts-browser-helpers'
/**
*
* @param uniforms - object for setting uniform value (like ShaderMaterial.uniforms
* @param propKey - uniform name
* @param thisTarget - if `this` is the uniform (because uniforms = this wont work). It also adds _ in front of the name
*/
export function uniform({uniforms, propKey, thisTarget = false, onChange}: {uniforms?: any, propKey?: string|symbol, thisTarget?:boolean, onChange?: (...args: any[]) => any} = {}): PropertyDecorator {
// backing up properties as values are different when called again, no idea why.
const cUniforms = !!uniforms
const cPropKey = !!propKey
const isThis = thisTarget
return (targetPrototype: any, propertyKey: string|symbol) => {
const getUniform = (target: any)=>{
const uniforms1 = isThis ? target : cUniforms ? uniforms : target.uniforms || target._uniforms || target.extraUniforms
let propKey1 = cPropKey ? propKey : propertyKey
if (isThis) propKey1 = '_' + (propKey1 as string)
let a = uniforms1[propKey1!]
if (!a) {
a = {value: null}
uniforms1[propKey1!] = a
}
return a
}
Object.defineProperty(targetPrototype, propertyKey, {
get() {
return getUniform(this).value
},
set(newVal: any) {
const u = getUniform(this)
const val = u.value
if (val === newVal) return
u.value = newVal
safeSetProperty(this, 'uniformsNeedUpdate', true, true)
onChange && callOnChange.call(this, onChange, [propertyKey, newVal])
},
// configurable: true,
// enumerable: true,
})
}
}
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)
}
/**
* Decorator to create a three.js style define in this.material or this and bind to a property.
* see also - {@link matDefineBool}
* @param key - define name
* @param customDefines - object for setting define value (like ShaderMaterial.defines), otherwise this.material.defines is taken
* @param thisMat - access this.defines instead of this.material.defines
* @param onChange - function to call when the value changes. The function is called with the following parameters: [key, newVal]. Note: needsUpdate is set to true for this/material if onChange is not provided.
* @param processVal - function that processes the value before setting it.
* @param invProcessVal - function that processes the value before returning it.
*/
export function matDefine(key?: string|symbol, customDefines?: any, thisMat = false, onChange?: (...args: any[]) => any, processVal?: (newVal: any)=>any, invProcessVal?: (val:any)=>any): PropertyDecorator {
// backing up properties as values are different when called again, no idea why.
const cDefines = !!customDefines
const cPropKey = !!key
return (targetPrototype: any, propertyKey: string|symbol) => {
const getTarget = (mat: any)=>{
const t = cDefines ? customDefines : mat.defines || mat._defines || mat.extraDefines
const p = cPropKey ? key : propertyKey
return {t, p}
}
Object.defineProperty(targetPrototype, propertyKey, {
get() {
const {t, p} = getTarget(thisMat ? this : this.material)
let res = t[p]
if (invProcessVal) res = invProcessVal(res)
return res
},
set(newVal: any) {
const {t, p} = getTarget(thisMat ? this : this.material)
if (processVal) newVal = processVal(newVal)
// boolean values are supported in material extender.
// else if (typeof newVal === 'boolean') { // just in case
// console.error('Boolean values are not supported for defines. Use @matDefineBool instead.')
// newVal = newVal ? '1' : '0'
// }
safeSetProperty(t, p, newVal, true)
if (newVal === undefined) delete t[p]
if (onChange && typeof onChange === 'function') {
callOnChange.call(this, onChange, [p, newVal])
} else {
safeSetProperty(thisMat ? this : this.material, 'needsUpdate', true, true)
}
},
// configurable: true,
// enumerable: true,
})
}
}
/**
* Same as {@link matDefine} but for boolean values. It sets the value to '1' or '0'/undefined.
* @param key - define name
* @param customDefines - object for setting define value (like ShaderMaterial.defines), otherwise this.material.defines is taken
* @param thisMat - access this.defines instead of this.material.defines
* @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 deleteOnFalse - sets to undefined instead of '0' when false. Note deleteOnFalse doesn't work with tweakpane ui because the value will be undefined.
*/
export function matDefineBool(key?: string|symbol, customDefines?: any, thisMat = false, onChange?: (...args: any[]) => any, deleteOnFalse = false): PropertyDecorator {
// noinspection RedundantConditionalExpressionJS
return matDefine(key, customDefines, thisMat, onChange, (v: any)=>v ? '1' : deleteOnFalse ? undefined : '0', (v: any|undefined)=>v && v !== '0' ? true : false)
}
/**
* 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,
})
}
}