index.ts 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. import type { Material, Mesh, Object3D, Texture } from 'three'
  2. import { DoubleSide, MeshBasicMaterial, Scene, Vector3 } from 'three'
  3. import type { TresObject } from 'src/types'
  4. import { HightlightMesh } from '../devtools/highlight'
  5. export function toSetMethodName(key: string) {
  6. return `set${key[0].toUpperCase()}${key.slice(1)}`
  7. }
  8. export const merge = (target: any, source: any) => {
  9. // Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties
  10. for (const key of Object.keys(source)) {
  11. if (source[key] instanceof Object) {
  12. Object.assign(source[key], merge(target[key], source[key]))
  13. }
  14. }
  15. // Join `target` and modified `source`
  16. Object.assign(target || {}, source)
  17. return target
  18. }
  19. const HTML_TAGS
  20. = 'html,body,base,head,link,meta,style,title,address,article,aside,footer,'
  21. + 'header,hgroup,h1,h2,h3,h4,h5,h6,nav,section,div,dd,dl,dt,figcaption,'
  22. + 'figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,'
  23. + 'data,dfn,em,i,kbd,mark,q,rp,rt,ruby,s,samp,small,span,strong,sub,sup,'
  24. + 'time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,'
  25. + 'canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,'
  26. + 'th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,'
  27. + 'option,output,progress,select,textarea,details,dialog,menu,'
  28. + 'summary,template,blockquote,iframe,tfoot'
  29. export const isHTMLTag = /* #__PURE__ */ makeMap(HTML_TAGS)
  30. export function isDOMElement(obj: any): obj is HTMLElement {
  31. return obj && obj.nodeType === 1
  32. }
  33. export function kebabToCamel(str: string) {
  34. return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase())
  35. }
  36. // CamelCase to kebab-case
  37. const hyphenateRE = /\B([A-Z])/g
  38. export function hyphenate(str: string) {
  39. return str.replace(hyphenateRE, '-$1').toLowerCase()
  40. }
  41. export function makeMap(str: string, expectsLowerCase?: boolean): (key: string) => boolean {
  42. const map: Record<string, boolean> = Object.create(null)
  43. const list: Array<string> = str.split(',')
  44. for (let i = 0; i < list.length; i++) {
  45. map[list[i]] = true
  46. }
  47. return expectsLowerCase ? val => !!map[val.toLowerCase()] : val => !!map[val]
  48. }
  49. export const uniqueBy = <T, K>(array: T[], iteratee: (value: T) => K): T[] => {
  50. const seen = new Set<K>()
  51. const result: T[] = []
  52. for (const item of array) {
  53. const identifier = iteratee(item)
  54. if (!seen.has(identifier)) {
  55. seen.add(identifier)
  56. result.push(item)
  57. }
  58. }
  59. return result
  60. }
  61. export const get = <T>(obj: any, path: string | string[]): T | undefined => {
  62. if (!path) {
  63. return undefined
  64. }
  65. // Regex explained: https://regexr.com/58j0k
  66. const pathArray = Array.isArray(path) ? path : path.match(/([^[.\]])+/g)
  67. return pathArray?.reduce((prevObj, key) => prevObj && prevObj[key], obj)
  68. }
  69. export const set = (obj: any, path: string | string[], value: any): void => {
  70. // Regex explained: https://regexr.com/58j0k
  71. const pathArray = Array.isArray(path) ? path : path.match(/([^[.\]])+/g)
  72. if (pathArray) {
  73. pathArray.reduce((acc, key, i) => {
  74. if (acc[key] === undefined) {
  75. acc[key] = {}
  76. }
  77. if (i === pathArray.length - 1) {
  78. acc[key] = value
  79. }
  80. return acc[key]
  81. }, obj)
  82. }
  83. }
  84. export function deepEqual(a: any, b: any): boolean {
  85. if (isDOMElement(a) && isDOMElement(b)) {
  86. const attrsA = a.attributes
  87. const attrsB = b.attributes
  88. if (attrsA.length !== attrsB.length) {
  89. return false
  90. }
  91. return Array.from(attrsA).every(({ name, value }) => b.getAttribute(name) === value)
  92. }
  93. // If both are primitives, return true if they are equal
  94. if (a === b) {
  95. return true
  96. }
  97. // If either of them is null or not an object, return false
  98. if (a === null || typeof a !== 'object' || b === null || typeof b !== 'object') {
  99. return false
  100. }
  101. // Get the keys of both objects
  102. const keysA = Object.keys(a); const keysB = Object.keys(b)
  103. // If they have different number of keys, they are not equal
  104. if (keysA.length !== keysB.length) {
  105. return false
  106. }
  107. // Check each key in A to see if it exists in B and its value is the same in both
  108. for (const key of keysA) {
  109. if (!keysB.includes(key) || !deepEqual(a[key], b[key])) {
  110. return false
  111. }
  112. }
  113. return true
  114. }
  115. export function deepArrayEqual(arr1: any[], arr2: any[]): boolean {
  116. // If they're not both arrays, return false
  117. if (!Array.isArray(arr1) || !Array.isArray(arr2)) {
  118. return false
  119. }
  120. // If they don't have the same length, they're not equal
  121. if (arr1.length !== arr2.length) {
  122. return false
  123. }
  124. // Check each element of arr1 against the corresponding element of arr2
  125. for (let i = 0; i < arr1.length; i++) {
  126. if (!deepEqual(arr1[i], arr2[i])) {
  127. return false
  128. }
  129. }
  130. return true
  131. }
  132. /**
  133. * TypeSafe version of Array.isArray
  134. */
  135. export const isArray = Array.isArray as (a: any) => a is any[] | readonly any[]
  136. export function editSceneObject(scene: Scene, objectUuid: string, propertyPath: string[], value: any): void {
  137. // Function to recursively find the object by UUID
  138. const findObjectByUuid = (node: Object3D): Object3D | undefined => {
  139. if (node.uuid === objectUuid) {
  140. return node
  141. }
  142. for (const child of node.children) {
  143. const found = findObjectByUuid(child)
  144. if (found) {
  145. return found
  146. }
  147. }
  148. return undefined
  149. }
  150. // Find the target object
  151. const targetObject = findObjectByUuid(scene)
  152. if (!targetObject) {
  153. console.warn('Object with UUID not found in the scene.')
  154. return
  155. }
  156. // Traverse the property path to get to the desired property
  157. let currentProperty: any = targetObject
  158. for (let i = 0; i < propertyPath.length - 1; i++) {
  159. if (currentProperty[propertyPath[i]] !== undefined) {
  160. currentProperty = currentProperty[propertyPath[i]]
  161. }
  162. else {
  163. console.warn(`Property path is not valid: ${propertyPath.join('.')}`)
  164. return
  165. }
  166. }
  167. // Set the new value
  168. const lastProperty = propertyPath[propertyPath.length - 1]
  169. if (currentProperty[lastProperty] !== undefined) {
  170. currentProperty[lastProperty] = value
  171. }
  172. else {
  173. console.warn(`Property path is not valid: ${propertyPath.join('.')}`)
  174. }
  175. }
  176. export function createHighlightMaterial(): MeshBasicMaterial {
  177. return new MeshBasicMaterial({
  178. color: 0xA7E6D7, // Highlight color, e.g., yellow
  179. transparent: true,
  180. opacity: 0.2,
  181. depthTest: false, // So the highlight is always visible
  182. side: DoubleSide, // To ensure the highlight is visible from all angles
  183. })
  184. }
  185. let animationFrameId: number | null = null
  186. export function animateHighlight(highlightMesh: Mesh, startTime: number): void {
  187. const currentTime = Date.now()
  188. const time = (currentTime - startTime) / 1000 // convert to seconds
  189. // Pulsing effect parameters
  190. const scaleAmplitude = 0.07 // Amplitude of the scale pulsation
  191. const pulseSpeed = 2.5 // Speed of the pulsation
  192. // Calculate the scale factor with a sine function for pulsing effect
  193. const scaleFactor = 1 + scaleAmplitude * Math.sin(pulseSpeed * time)
  194. // Apply the scale factor
  195. highlightMesh.scale.set(scaleFactor, scaleFactor, scaleFactor)
  196. // Update the animation frame ID
  197. animationFrameId = requestAnimationFrame(() => animateHighlight(highlightMesh, startTime))
  198. }
  199. export function stopHighlightAnimation(): void {
  200. if (animationFrameId !== null) {
  201. cancelAnimationFrame(animationFrameId)
  202. animationFrameId = null
  203. }
  204. }
  205. export function createHighlightMesh(object: TresObject): Mesh {
  206. const highlightMaterial = new MeshBasicMaterial({
  207. color: 0xA7E6D7, // Highlight color, e.g., yellow
  208. transparent: true,
  209. opacity: 0.2,
  210. depthTest: false, // So the highlight is always visible
  211. side: DoubleSide, // To e
  212. })
  213. // Clone the geometry of the object. You might need a more complex approach
  214. // if the object's geometry is not straightforward.
  215. const highlightMesh = new HightlightMesh(object.geometry.clone(), highlightMaterial)
  216. return highlightMesh
  217. }
  218. export function extractBindingPosition(binding: any): Vector3 {
  219. let observer = binding.value
  220. if (binding.value && binding.value?.isMesh) {
  221. observer = binding.value.position
  222. }
  223. if (Array.isArray(binding.value)) { observer = new Vector3(...observer) }
  224. return observer
  225. }
  226. function hasMap(material: Material): material is Material & { map: Texture | null } {
  227. return 'map' in material
  228. }
  229. export function disposeMaterial(material: Material): void {
  230. if (hasMap(material) && material.map) {
  231. material.map.dispose()
  232. }
  233. material.dispose()
  234. }
  235. export function disposeObject3D(object: TresObject): void {
  236. if (object.parent) {
  237. object.removeFromParent?.()
  238. }
  239. delete object.__tres
  240. // Clone the children array to safely iterate
  241. const children = [...object.children]
  242. children.forEach(child => disposeObject3D(child))
  243. if (object instanceof Scene) {
  244. // Optionally handle Scene-specific cleanup
  245. }
  246. else {
  247. const mesh = object as unknown as Partial<Mesh>
  248. if (mesh.geometry) {
  249. mesh.geometry.dispose()
  250. delete mesh.geometry
  251. }
  252. if (Array.isArray(mesh.material)) {
  253. mesh.material.forEach(material => disposeMaterial(material))
  254. delete mesh.material
  255. }
  256. else if (mesh.material) {
  257. disposeMaterial(mesh.material)
  258. delete mesh.material
  259. }
  260. object.dispose?.()
  261. }
  262. }