threepipe
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

decorators.ts 7.7KB

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