123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396 |
- import { BufferAttribute, Object3D } from 'three'
- import { isRef, type RendererOptions } from 'vue'
- import { useLogger } from '../composables'
- import { attach, deepArrayEqual, doRemoveDeregister, doRemoveDetach, invalidateInstance, isHTMLTag, kebabToCamel, noop, prepareTresInstance, setPrimitiveObject, unboxTresPrimitive } from '../utils'
- import * as is from '../utils/is'
- import { createRetargetingProxy } from '../utils/primitive/createRetargetingProxy'
- import { catalogue } from './catalogue'
- import type { TresContext } from '../composables'
- import type { DisposeType, LocalState, TresInstance, TresObject, TresObject3D, TresPrimitive, WithMathProps } from '../types'
- const { logError } = useLogger()
- const supportedPointerEvents = [
- 'onClick',
- 'onContextMenu',
- 'onPointerMove',
- 'onPointerEnter',
- 'onPointerLeave',
- 'onPointerOver',
- 'onPointerOut',
- 'onDoubleClick',
- 'onPointerDown',
- 'onPointerUp',
- 'onPointerCancel',
- 'onPointerMissed',
- 'onLostPointerCapture',
- 'onWheel',
- ]
- export const nodeOps: (context: TresContext) => RendererOptions<TresObject, TresObject | null> = (context) => {
- const scene = context.scene.value
- function createElement(tag: string, _isSVG: undefined, _anchor: any, props: Partial<WithMathProps<TresObject>> | null): TresObject | null {
- if (!props) { props = {} }
- if (!props.args) {
- props.args = []
- }
- if (tag === 'template') { return null }
- if (isHTMLTag(tag)) { return null }
- let name = tag.replace('Tres', '')
- let obj: TresObject | null
- if (tag === 'primitive') {
- if (!is.obj(props.object) || isRef(props.object)) {
- logError(
- 'Tres primitives need an \'object\' prop, whose value is an object or shallowRef<object>',
- )
- }
- name = props.object.type
- const __tres = {}
- const primitive = createRetargetingProxy(
- props.object,
- {
- object: t => t,
- isPrimitive: () => true,
- __tres: () => __tres,
- },
- {
- object: (object: TresObject, _: unknown, primitive: TresPrimitive, setTarget: (nextObject: TresObject) => void) => {
- setPrimitiveObject(object, primitive, setTarget, { patchProp, remove, insert }, context)
- },
- __tres: (t: LocalState) => { Object.assign(__tres, t) },
- },
- )
- obj = primitive
- }
- else {
- const target = catalogue.value[name]
- if (!target) {
- logError(
- `${name} is not defined on the THREE namespace. Use extend to add it to the catalog.`,
- )
- }
- // eslint-disable-next-line new-cap
- obj = new target(...props.args) as TresObject
- }
- if (!obj) { return null }
- if (obj.isCamera) {
- if (!props?.position) {
- obj.position.set(3, 3, 3)
- }
- if (!props?.lookAt) {
- obj.lookAt(0, 0, 0)
- }
- }
- obj = prepareTresInstance(obj, {
- ...obj.__tres,
- type: name,
- memoizedProps: props,
- eventCount: 0,
- primitive: tag === 'primitive',
- attach: props.attach,
- }, context)
- return obj as TresObject
- }
- function insert(child: TresObject, parent: TresObject) {
- if (!child) { return }
- // TODO: Investigate and eventually remove `scene` fallback.
- // According to the signature, `parent` should always be
- // truthy. If it is not truthy, it may be due to a bug
- // elsewhere in Tres.
- parent = parent || scene
- const childInstance: TresInstance = (child.__tres ? child as TresInstance : prepareTresInstance(child, {}, context))
- const parentInstance: TresInstance = (parent.__tres ? parent as TresInstance : prepareTresInstance(parent, {}, context))
- child = unboxTresPrimitive(childInstance)
- parent = unboxTresPrimitive(parentInstance)
- if (child.__tres && child.__tres?.eventCount > 0) {
- context.eventManager?.registerObject(child)
- }
- context.registerCamera(child)
- // NOTE: Track onPointerMissed objects separate from the scene
- context.eventManager?.registerPointerMissedObject(child)
- if (childInstance.__tres.attach) {
- attach(parentInstance, childInstance, childInstance.__tres.attach)
- }
- else if (is.object3D(child) && is.object3D(parentInstance)) {
- parentInstance.add(child)
- child.dispatchEvent({ type: 'added' })
- }
- // NOTE: Update __tres parent/objects graph
- childInstance.__tres.parent = parentInstance
- if (parentInstance.__tres.objects && !parentInstance.__tres.objects.includes(childInstance)) {
- parentInstance.__tres.objects.push(childInstance)
- }
- }
- /**
- * @param node – the node root to remove
- * @param dispose – the disposal type
- */
- function remove(node: TresObject | null, dispose?: DisposeType) {
- // NOTE: `remove` is initially called by Vue only on
- // the root `node` of the tree to be removed. We will
- // recursively call the function on children, if necessary.
- // NOTE: Vue does not pass a `dispose` argument; it is
- // used by the recursive calls.
- if (!node) { return }
- // Remove from event manager if necessary
- if (node?.__tres && node.__tres?.eventCount > 0) {
- context.eventManager?.deregisterObject(node)
- }
- // NOTE: Derive `dispose` value for this `remove` call and
- // recursive remove calls.
- dispose = is.und(dispose) ? 'default' : dispose
- const userDispose = node.__tres?.dispose
- if (!is.und(userDispose)) {
- if (userDispose === null) {
- // NOTE: Treat as `false` to act like R3F
- dispose = false
- }
- else {
- // NOTE: Otherwise, if the user has defined a `dispose`, use it
- dispose = userDispose
- }
- }
- // NOTE: Create a `shouldDispose` boolean for readable predicates below.
- // 1) If `dispose` is "default", then:
- // - dispose declarative components, e.g., <TresMeshNormalMaterial />
- // - do *not* dispose primitives or their non-declarative children
- // 2) Otherwise, follow `dispose`
- const isPrimitive = node.__tres?.primitive
- const shouldDispose = dispose === 'default' ? !isPrimitive : !!(dispose)
- // NOTE: This function has 5 stages:
- // 1) Recursively remove `node`'s children
- // 2) Detach `node` from its parent
- // 3) Deregister `node` with `context` and invalidate
- // 4) Dispose `node`
- // 5) Remove `node`'s `LocalState`
- // NOTE: 1) Recursively remove `node`'s children
- // NOTE: Remove declarative children.
- if (node.__tres && 'objects' in node.__tres) {
- // NOTE: In the recursive `remove` calls, the array elements
- // will remove themselves from the array, resulting in skipped
- // elements. Make a shallow copy of the array.
- [...node.__tres.objects].forEach(obj => remove(obj, dispose))
- }
- // NOTE: Remove remaining THREE children.
- // On primitives, we do not remove THREE children unless disposing.
- // Otherwise we would alter the user's `:object`.
- if (shouldDispose) {
- // NOTE: In the recursive `remove` calls, the array elements
- // will remove themselves from the array, resulting in skipped
- // elements. Make a shallow copy of the array.
- if (node.children) {
- [...node.children].forEach(child => remove(child, dispose))
- }
- }
- // NOTE: 2) Detach `node` from its parent
- doRemoveDetach(node, context)
- // NOTE: 3) Deregister `node` THREE.Object3D children and invalidate `node`
- doRemoveDeregister(node, context)
- // NOTE: 4) Dispose `node`
- if (shouldDispose && !is.scene(node)) {
- if (is.fun(dispose)) {
- dispose(node as TresInstance)
- }
- else if (is.fun(node.dispose)) {
- try {
- node.dispose()
- }
- // eslint-disable-next-line unused-imports/no-unused-vars
- catch (e) {
- // NOTE: We must try/catch here. We want to remove/dispose
- // Vue/THREE children in bottom-up order. But THREE objects
- // will e.g., call `this.material.dispose` without checking
- // if the material exists, leading to an error.
- // See issue #721:
- // https://github.com/Tresjs/tres/issues/721
- // Cannot read properties of undefined (reading 'dispose') - GridHelper
- }
- }
- }
- // NOTE: 5) Remove `LocalState`
- if ('__tres' in node) {
- delete node.__tres
- }
- }
- function patchProp(node: TresObject, prop: string, prevValue: any, nextValue: any) {
- if (!node) { return }
- let root = node
- let key = prop
- // NOTE: Update memoizedProps with the new value
- if (node.__tres) { node.__tres.memoizedProps[prop] = nextValue }
- if (prop === 'attach') {
- // NOTE: `attach` is not a field on a TresObject.
- // `nextValue` is a string representing how Tres
- // should attach `node` to its parent – if the
- // parent exists.
- const maybeParent = node.__tres?.parent || node.parent
- remove(node)
- prepareTresInstance(node, { attach: nextValue }, context)
- if (maybeParent) { insert(node, maybeParent) }
- return
- }
- if (prop === 'dispose') {
- // NOTE: Add node.__tres, if necessary.
- if (!node.__tres) { node = prepareTresInstance(node, {}, context) }
- node.__tres!.dispose = nextValue
- return
- }
- if (is.object3D(node) && key === 'blocks-pointer-events') {
- if (nextValue || nextValue === '') { node[key] = nextValue }
- else { delete node[key] }
- return
- }
- // Has events
- if (supportedPointerEvents.includes(prop) && node.__tres) {
- node.__tres.eventCount += 1
- }
- let finalKey = kebabToCamel(key)
- let target = root?.[finalKey]
- if (key === 'args') {
- const prevNode = node as TresObject3D
- const prevArgs = prevValue ?? []
- const args = nextValue ?? []
- const instanceName = node.__tres?.type || node.type
- if (
- instanceName
- && prevArgs.length
- && !deepArrayEqual(prevArgs, args)
- ) {
- root = Object.assign(
- prevNode,
- new catalogue.value[instanceName](...nextValue),
- )
- }
- return
- }
- if (root.type === 'BufferGeometry') {
- if (key === 'args') { return }
- root.setAttribute(
- kebabToCamel(key),
- new BufferAttribute(...(nextValue as ConstructorParameters<typeof BufferAttribute>)),
- )
- return
- }
- // Traverse pierced props (e.g. foo-bar=value => foo.bar = value)
- if (key.includes('-') && target === undefined) {
- // TODO: A standalone function called `resolve` is
- // available in /src/utils/index.ts. It's covered by tests.
- // Refactor below to DRY.
- const chain = key.split('-')
- target = chain.reduce((acc, key) => acc[kebabToCamel(key)], root)
- key = chain.pop() as string
- finalKey = key
- if (!target?.set) { root = chain.reduce((acc, key) => acc[kebabToCamel(key)], root) }
- }
- let value = nextValue
- if (value === '') { value = true }
- // Set prop, prefer atomic methods if applicable
- if (is.fun(target)) {
- // don't call pointer event callback functions
- if (!supportedPointerEvents.includes(prop)) {
- if (is.arr(value)) { node[finalKey](...value) }
- else { node[finalKey](value) }
- }
- // NOTE: Set on* callbacks
- // Issue: https://github.com/Tresjs/tres/issues/360
- if (finalKey.startsWith('on') && is.fun(value)) {
- root[finalKey] = value
- }
- return
- }
- if (!target?.set && !is.fun(target)) { root[finalKey] = value }
- else if (target.constructor === value.constructor && target?.copy) { target?.copy(value) }
- else if (is.arr(value)) { target.set(...value) }
- else if (!target.isColor && target.setScalar) { target.setScalar(value) }
- else { target.set(value) }
- invalidateInstance(node as TresObject)
- }
- // eslint-disable-next-line unicorn/consistent-function-scoping
- function parentNode(node: TresObject): TresObject | null {
- return node?.__tres?.parent || null
- }
- /**
- * createComment
- *
- * Creates a comment object that can be used to represent a commented out string in a vue template
- * Used by Vue's internal runtime as a placeholder for v-if'd elements
- *
- * @param comment Any commented out string contaiend in a vue template, typically this is `v-if`
- * @returns TresObject
- */
- function createComment(comment: string): TresObject {
- // TODO: Add a custom type for comments instead of reusing Object3D. Comments should be light weight and not exist in the scene graph
- const commentObj = prepareTresInstance(new Object3D(), { type: 'Comment' }, context)
- commentObj.name = comment
- return commentObj
- }
- // nextSibling - Returns the next sibling of a TresObject
- function nextSibling(node: TresObject) {
- const parent = parentNode(node)
- const siblings = parent?.__tres?.objects || []
- const index = siblings.indexOf(node)
- // NOTE: If not found OR this is the last of the siblings ...
- if (index < 0 || index >= siblings.length - 1) { return null }
- return siblings[index + 1]
- }
- return {
- insert,
- remove,
- createElement,
- patchProp,
- parentNode,
- createText: () => noop('createText'),
- createComment,
- setText: () => noop('setText'),
- setElementText: () => noop('setElementText'),
- nextSibling,
- querySelector: () => noop('querySelector'),
- setScopeId: () => noop('setScopeId'),
- cloneNode: () => noop('cloneNode'),
- insertStaticContent: () => noop('insertStaticContent'),
- }
- }
|