1
0

nodeOps.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. import { BufferAttribute, Object3D } from 'three'
  2. import { isRef, type RendererOptions } from 'vue'
  3. import { useLogger } from '../composables'
  4. import { attach, deepArrayEqual, doRemoveDeregister, doRemoveDetach, invalidateInstance, isHTMLTag, kebabToCamel, noop, prepareTresInstance, setPrimitiveObject, unboxTresPrimitive } from '../utils'
  5. import * as is from '../utils/is'
  6. import { createRetargetingProxy } from '../utils/primitive/createRetargetingProxy'
  7. import { catalogue } from './catalogue'
  8. import type { TresContext } from '../composables'
  9. import type { DisposeType, LocalState, TresInstance, TresObject, TresObject3D, TresPrimitive, WithMathProps } from '../types'
  10. const { logError } = useLogger()
  11. const supportedPointerEvents = [
  12. 'onClick',
  13. 'onContextMenu',
  14. 'onPointerMove',
  15. 'onPointerEnter',
  16. 'onPointerLeave',
  17. 'onPointerOver',
  18. 'onPointerOut',
  19. 'onDoubleClick',
  20. 'onPointerDown',
  21. 'onPointerUp',
  22. 'onPointerCancel',
  23. 'onPointerMissed',
  24. 'onLostPointerCapture',
  25. 'onWheel',
  26. ]
  27. export const nodeOps: (context: TresContext) => RendererOptions<TresObject, TresObject | null> = (context) => {
  28. const scene = context.scene.value
  29. function createElement(tag: string, _isSVG: undefined, _anchor: any, props: Partial<WithMathProps<TresObject>> | null): TresObject | null {
  30. if (!props) { props = {} }
  31. if (!props.args) {
  32. props.args = []
  33. }
  34. if (tag === 'template') { return null }
  35. if (isHTMLTag(tag)) { return null }
  36. let name = tag.replace('Tres', '')
  37. let obj: TresObject | null
  38. if (tag === 'primitive') {
  39. if (!is.obj(props.object) || isRef(props.object)) {
  40. logError(
  41. 'Tres primitives need an \'object\' prop, whose value is an object or shallowRef<object>',
  42. )
  43. }
  44. name = props.object.type
  45. const __tres = {}
  46. const primitive = createRetargetingProxy(
  47. props.object,
  48. {
  49. object: t => t,
  50. isPrimitive: () => true,
  51. __tres: () => __tres,
  52. },
  53. {
  54. object: (object: TresObject, _: unknown, primitive: TresPrimitive, setTarget: (nextObject: TresObject) => void) => {
  55. setPrimitiveObject(object, primitive, setTarget, { patchProp, remove, insert }, context)
  56. },
  57. __tres: (t: LocalState) => { Object.assign(__tres, t) },
  58. },
  59. )
  60. obj = primitive
  61. }
  62. else {
  63. const target = catalogue.value[name]
  64. if (!target) {
  65. logError(
  66. `${name} is not defined on the THREE namespace. Use extend to add it to the catalog.`,
  67. )
  68. }
  69. // eslint-disable-next-line new-cap
  70. obj = new target(...props.args) as TresObject
  71. }
  72. if (!obj) { return null }
  73. if (obj.isCamera) {
  74. if (!props?.position) {
  75. obj.position.set(3, 3, 3)
  76. }
  77. if (!props?.lookAt) {
  78. obj.lookAt(0, 0, 0)
  79. }
  80. }
  81. obj = prepareTresInstance(obj, {
  82. ...obj.__tres,
  83. type: name,
  84. memoizedProps: props,
  85. eventCount: 0,
  86. primitive: tag === 'primitive',
  87. attach: props.attach,
  88. }, context)
  89. return obj as TresObject
  90. }
  91. function insert(child: TresObject, parent: TresObject) {
  92. if (!child) { return }
  93. // TODO: Investigate and eventually remove `scene` fallback.
  94. // According to the signature, `parent` should always be
  95. // truthy. If it is not truthy, it may be due to a bug
  96. // elsewhere in Tres.
  97. parent = parent || scene
  98. const childInstance: TresInstance = (child.__tres ? child as TresInstance : prepareTresInstance(child, {}, context))
  99. const parentInstance: TresInstance = (parent.__tres ? parent as TresInstance : prepareTresInstance(parent, {}, context))
  100. child = unboxTresPrimitive(childInstance)
  101. parent = unboxTresPrimitive(parentInstance)
  102. if (child.__tres && child.__tres?.eventCount > 0) {
  103. context.eventManager?.registerObject(child)
  104. }
  105. context.registerCamera(child)
  106. // NOTE: Track onPointerMissed objects separate from the scene
  107. context.eventManager?.registerPointerMissedObject(child)
  108. if (childInstance.__tres.attach) {
  109. attach(parentInstance, childInstance, childInstance.__tres.attach)
  110. }
  111. else if (is.object3D(child) && is.object3D(parentInstance)) {
  112. parentInstance.add(child)
  113. child.dispatchEvent({ type: 'added' })
  114. }
  115. // NOTE: Update __tres parent/objects graph
  116. childInstance.__tres.parent = parentInstance
  117. if (parentInstance.__tres.objects && !parentInstance.__tres.objects.includes(childInstance)) {
  118. parentInstance.__tres.objects.push(childInstance)
  119. }
  120. }
  121. /**
  122. * @param node – the node root to remove
  123. * @param dispose – the disposal type
  124. */
  125. function remove(node: TresObject | null, dispose?: DisposeType) {
  126. // NOTE: `remove` is initially called by Vue only on
  127. // the root `node` of the tree to be removed. We will
  128. // recursively call the function on children, if necessary.
  129. // NOTE: Vue does not pass a `dispose` argument; it is
  130. // used by the recursive calls.
  131. if (!node) { return }
  132. // Remove from event manager if necessary
  133. if (node?.__tres && node.__tres?.eventCount > 0) {
  134. context.eventManager?.deregisterObject(node)
  135. }
  136. // NOTE: Derive `dispose` value for this `remove` call and
  137. // recursive remove calls.
  138. dispose = is.und(dispose) ? 'default' : dispose
  139. const userDispose = node.__tres?.dispose
  140. if (!is.und(userDispose)) {
  141. if (userDispose === null) {
  142. // NOTE: Treat as `false` to act like R3F
  143. dispose = false
  144. }
  145. else {
  146. // NOTE: Otherwise, if the user has defined a `dispose`, use it
  147. dispose = userDispose
  148. }
  149. }
  150. // NOTE: Create a `shouldDispose` boolean for readable predicates below.
  151. // 1) If `dispose` is "default", then:
  152. // - dispose declarative components, e.g., <TresMeshNormalMaterial />
  153. // - do *not* dispose primitives or their non-declarative children
  154. // 2) Otherwise, follow `dispose`
  155. const isPrimitive = node.__tres?.primitive
  156. const shouldDispose = dispose === 'default' ? !isPrimitive : !!(dispose)
  157. // NOTE: This function has 5 stages:
  158. // 1) Recursively remove `node`'s children
  159. // 2) Detach `node` from its parent
  160. // 3) Deregister `node` with `context` and invalidate
  161. // 4) Dispose `node`
  162. // 5) Remove `node`'s `LocalState`
  163. // NOTE: 1) Recursively remove `node`'s children
  164. // NOTE: Remove declarative children.
  165. if (node.__tres && 'objects' in node.__tres) {
  166. // NOTE: In the recursive `remove` calls, the array elements
  167. // will remove themselves from the array, resulting in skipped
  168. // elements. Make a shallow copy of the array.
  169. [...node.__tres.objects].forEach(obj => remove(obj, dispose))
  170. }
  171. // NOTE: Remove remaining THREE children.
  172. // On primitives, we do not remove THREE children unless disposing.
  173. // Otherwise we would alter the user's `:object`.
  174. if (shouldDispose) {
  175. // NOTE: In the recursive `remove` calls, the array elements
  176. // will remove themselves from the array, resulting in skipped
  177. // elements. Make a shallow copy of the array.
  178. if (node.children) {
  179. [...node.children].forEach(child => remove(child, dispose))
  180. }
  181. }
  182. // NOTE: 2) Detach `node` from its parent
  183. doRemoveDetach(node, context)
  184. // NOTE: 3) Deregister `node` THREE.Object3D children and invalidate `node`
  185. doRemoveDeregister(node, context)
  186. // NOTE: 4) Dispose `node`
  187. if (shouldDispose && !is.scene(node)) {
  188. if (is.fun(dispose)) {
  189. dispose(node as TresInstance)
  190. }
  191. else if (is.fun(node.dispose)) {
  192. try {
  193. node.dispose()
  194. }
  195. // eslint-disable-next-line unused-imports/no-unused-vars
  196. catch (e) {
  197. // NOTE: We must try/catch here. We want to remove/dispose
  198. // Vue/THREE children in bottom-up order. But THREE objects
  199. // will e.g., call `this.material.dispose` without checking
  200. // if the material exists, leading to an error.
  201. // See issue #721:
  202. // https://github.com/Tresjs/tres/issues/721
  203. // Cannot read properties of undefined (reading 'dispose') - GridHelper
  204. }
  205. }
  206. }
  207. // NOTE: 5) Remove `LocalState`
  208. if ('__tres' in node) {
  209. delete node.__tres
  210. }
  211. }
  212. function patchProp(node: TresObject, prop: string, prevValue: any, nextValue: any) {
  213. if (!node) { return }
  214. let root = node
  215. let key = prop
  216. // NOTE: Update memoizedProps with the new value
  217. if (node.__tres) { node.__tres.memoizedProps[prop] = nextValue }
  218. if (prop === 'attach') {
  219. // NOTE: `attach` is not a field on a TresObject.
  220. // `nextValue` is a string representing how Tres
  221. // should attach `node` to its parent – if the
  222. // parent exists.
  223. const maybeParent = node.__tres?.parent || node.parent
  224. remove(node)
  225. prepareTresInstance(node, { attach: nextValue }, context)
  226. if (maybeParent) { insert(node, maybeParent) }
  227. return
  228. }
  229. if (prop === 'dispose') {
  230. // NOTE: Add node.__tres, if necessary.
  231. if (!node.__tres) { node = prepareTresInstance(node, {}, context) }
  232. node.__tres!.dispose = nextValue
  233. return
  234. }
  235. if (is.object3D(node) && key === 'blocks-pointer-events') {
  236. if (nextValue || nextValue === '') { node[key] = nextValue }
  237. else { delete node[key] }
  238. return
  239. }
  240. // Has events
  241. if (supportedPointerEvents.includes(prop) && node.__tres) {
  242. node.__tres.eventCount += 1
  243. }
  244. let finalKey = kebabToCamel(key)
  245. let target = root?.[finalKey]
  246. if (key === 'args') {
  247. const prevNode = node as TresObject3D
  248. const prevArgs = prevValue ?? []
  249. const args = nextValue ?? []
  250. const instanceName = node.__tres?.type || node.type
  251. if (
  252. instanceName
  253. && prevArgs.length
  254. && !deepArrayEqual(prevArgs, args)
  255. ) {
  256. root = Object.assign(
  257. prevNode,
  258. new catalogue.value[instanceName](...nextValue),
  259. )
  260. }
  261. return
  262. }
  263. if (root.type === 'BufferGeometry') {
  264. if (key === 'args') { return }
  265. root.setAttribute(
  266. kebabToCamel(key),
  267. new BufferAttribute(...(nextValue as ConstructorParameters<typeof BufferAttribute>)),
  268. )
  269. return
  270. }
  271. // Traverse pierced props (e.g. foo-bar=value => foo.bar = value)
  272. if (key.includes('-') && target === undefined) {
  273. // TODO: A standalone function called `resolve` is
  274. // available in /src/utils/index.ts. It's covered by tests.
  275. // Refactor below to DRY.
  276. const chain = key.split('-')
  277. target = chain.reduce((acc, key) => acc[kebabToCamel(key)], root)
  278. key = chain.pop() as string
  279. finalKey = key
  280. if (!target?.set) { root = chain.reduce((acc, key) => acc[kebabToCamel(key)], root) }
  281. }
  282. let value = nextValue
  283. if (value === '') { value = true }
  284. // Set prop, prefer atomic methods if applicable
  285. if (is.fun(target)) {
  286. // don't call pointer event callback functions
  287. if (!supportedPointerEvents.includes(prop)) {
  288. if (is.arr(value)) { node[finalKey](...value) }
  289. else { node[finalKey](value) }
  290. }
  291. // NOTE: Set on* callbacks
  292. // Issue: https://github.com/Tresjs/tres/issues/360
  293. if (finalKey.startsWith('on') && is.fun(value)) {
  294. root[finalKey] = value
  295. }
  296. return
  297. }
  298. if (!target?.set && !is.fun(target)) { root[finalKey] = value }
  299. else if (target.constructor === value.constructor && target?.copy) { target?.copy(value) }
  300. else if (is.arr(value)) { target.set(...value) }
  301. else if (!target.isColor && target.setScalar) { target.setScalar(value) }
  302. else { target.set(value) }
  303. invalidateInstance(node as TresObject)
  304. }
  305. // eslint-disable-next-line unicorn/consistent-function-scoping
  306. function parentNode(node: TresObject): TresObject | null {
  307. return node?.__tres?.parent || null
  308. }
  309. /**
  310. * createComment
  311. *
  312. * Creates a comment object that can be used to represent a commented out string in a vue template
  313. * Used by Vue's internal runtime as a placeholder for v-if'd elements
  314. *
  315. * @param comment Any commented out string contaiend in a vue template, typically this is `v-if`
  316. * @returns TresObject
  317. */
  318. function createComment(comment: string): TresObject {
  319. // TODO: Add a custom type for comments instead of reusing Object3D. Comments should be light weight and not exist in the scene graph
  320. const commentObj = prepareTresInstance(new Object3D(), { type: 'Comment' }, context)
  321. commentObj.name = comment
  322. return commentObj
  323. }
  324. // nextSibling - Returns the next sibling of a TresObject
  325. function nextSibling(node: TresObject) {
  326. const parent = parentNode(node)
  327. const siblings = parent?.__tres?.objects || []
  328. const index = siblings.indexOf(node)
  329. // NOTE: If not found OR this is the last of the siblings ...
  330. if (index < 0 || index >= siblings.length - 1) { return null }
  331. return siblings[index + 1]
  332. }
  333. return {
  334. insert,
  335. remove,
  336. createElement,
  337. patchProp,
  338. parentNode,
  339. createText: () => noop('createText'),
  340. createComment,
  341. setText: () => noop('setText'),
  342. setElementText: () => noop('setElementText'),
  343. nextSibling,
  344. querySelector: () => noop('querySelector'),
  345. setScopeId: () => noop('setScopeId'),
  346. cloneNode: () => noop('cloneNode'),
  347. insertStaticContent: () => noop('insertStaticContent'),
  348. }
  349. }