From 7695470705ed654aa80d4f8de885de6d2d245439 Mon Sep 17 00:00:00 2001 From: Alex Kanunnikov Date: Sat, 4 May 2024 22:30:55 +0300 Subject: [PATCH 1/7] tres-renderer --- package.json | 14 +- pnpm-lock.yaml | 40 ++++ src/utils/component.ts | 34 +--- src/utils/dom-api.ts | 76 +------- src/utils/renderers/dom/dom-api.ts | 102 ++++++++++ src/utils/renderers/tres/catalogue.ts | 8 + src/utils/renderers/tres/tres-api.ts | 257 +++++++++++++++++++++++++ src/utils/renderers/tres/types.d.ts | 185 ++++++++++++++++++ src/utils/renderers/tres/utils.ts | 262 ++++++++++++++++++++++++++ src/utils/ssr/ssr.ts | 2 +- 10 files changed, 868 insertions(+), 112 deletions(-) create mode 100644 src/utils/renderers/dom/dom-api.ts create mode 100644 src/utils/renderers/tres/catalogue.ts create mode 100644 src/utils/renderers/tres/tres-api.ts create mode 100644 src/utils/renderers/tres/types.d.ts create mode 100644 src/utils/renderers/tres/utils.ts diff --git a/package.json b/package.json index cf4eb26..6d91f66 100644 --- a/package.json +++ b/package.json @@ -74,8 +74,12 @@ "@playwright/test": "^1.40.1", "@types/babel__core": "^7.20.5", "@types/qunit": "^2.19.9", + "@types/three": "^0.164.0", "autoprefixer": "^10.4.16", "backburner.js": "^2.8.0", + "express": "^4.18.2", + "glint-environment-gxt": "file:./glint-environment-gxt", + "happy-dom": "^13.0.6", "nyc": "^15.1.0", "postcss": "^8.4.33", "prettier": "^3.1.1", @@ -93,16 +97,14 @@ "vite-plugin-circular-dependency": "^0.2.1", "vite-plugin-dts": "^3.7.0", "vitest": "^1.1.1", - "zx": "^7.2.3", - "express": "^4.18.2", - "happy-dom": "^13.0.6", - "glint-environment-gxt": "file:./glint-environment-gxt" + "zx": "^7.2.3" }, "dependencies": { "@babel/core": "^7.23.6", - "decorator-transforms": "1.1.0", "@babel/preset-typescript": "^7.23.3", "@glimmer/syntax": "^0.87.1", - "content-tag": "^1.2.2" + "content-tag": "^1.2.2", + "decorator-transforms": "1.1.0", + "three": "^0.164.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24c2672..c40bb05 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ dependencies: decorator-transforms: specifier: 1.1.0 version: 1.1.0(@babel/core@7.23.6) + three: + specifier: ^0.164.1 + version: 0.164.1 devDependencies: '@glint/core': @@ -40,6 +43,9 @@ devDependencies: '@types/qunit': specifier: ^2.19.9 version: 2.19.9 + '@types/three': + specifier: ^0.164.0 + version: 0.164.0 autoprefixer: specifier: ^10.4.16 version: 10.4.16(postcss@8.4.33) @@ -2426,6 +2432,10 @@ packages: resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} dev: true + /@tweenjs/tween.js@23.1.1: + resolution: {integrity: sha512-ZpboH7pCPPeyBWKf8c7TJswtCEQObFo3bOBYalm99NzZarATALYCo5OhbCa/n4RQyJyHfhkdx+hNrdL5ByFYDw==} + dev: true + /@types/argparse@1.0.38: resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} dev: true @@ -2542,10 +2552,28 @@ packages: '@types/node': 20.10.5 dev: true + /@types/stats.js@0.17.3: + resolution: {integrity: sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==} + dev: true + /@types/symlink-or-copy@1.2.2: resolution: {integrity: sha512-MQ1AnmTLOncwEf9IVU+B2e4Hchrku5N67NkgcAHW0p3sdzPe0FNMANxEm6OJUzPniEQGkeT3OROLlCwZJLWFZA==} dev: true + /@types/three@0.164.0: + resolution: {integrity: sha512-SFDofn9dJVrE+1DKta7xj7lc4ru7B3S3yf10NsxOserW57aQlB6GxtAS1UK5To3LfEMN5HUHMu3n5v+M5rApgA==} + dependencies: + '@tweenjs/tween.js': 23.1.1 + '@types/stats.js': 0.17.3 + '@types/webxr': 0.5.16 + fflate: 0.8.2 + meshoptimizer: 0.18.1 + dev: true + + /@types/webxr@0.5.16: + resolution: {integrity: sha512-0E0Cl84FECtzrB4qG19TNTqpunw0F1YF0QZZnFMF6pDw1kNKJtrlTKlVB34stGIsHbZsYQ7H0tNjPfZftkHHoA==} + dev: true + /@types/which@3.0.3: resolution: {integrity: sha512-2C1+XoY0huExTbs8MQv1DuS5FS86+SEjdM9F/+GS61gg5Hqbtj8ZiDSx8MfWcyei907fIPbfPGCOrNUTnVHY1g==} dev: true @@ -4399,6 +4427,10 @@ packages: web-streams-polyfill: 3.3.2 dev: true + /fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + dev: true + /figures@1.7.0: resolution: {integrity: sha512-UxKlfCRuCBxSXU4C6t9scbDyWZ4VlaFFdojKtzJuSkuOBQ5CNFum+zZXFwHjo+CxBC1t6zlYPgHIgFjL8ggoEQ==} engines: {node: '>=0.10.0'} @@ -5788,6 +5820,10 @@ packages: engines: {node: '>= 8'} dev: true + /meshoptimizer@0.18.1: + resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==} + dev: true + /methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} @@ -7400,6 +7436,10 @@ packages: any-promise: 1.3.0 dev: true + /three@0.164.1: + resolution: {integrity: sha512-iC/hUBbl1vzFny7f5GtqzVXYjMJKaTPxiCxXfrvVdBi1Sf+jhd1CAkitiFwC7mIBFCo3MrDLJG97yisoaWig0w==} + dev: false + /through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} dev: true diff --git a/src/utils/component.ts b/src/utils/component.ts index 13939ea..b9ee081 100644 --- a/src/utils/component.ts +++ b/src/utils/component.ts @@ -19,8 +19,6 @@ import { } from './shared'; import { addChild, getRoot, setRoot } from './dom'; -const FRAGMENT_TYPE = 11; // Node.DOCUMENT_FRAGMENT_NODE - export type ComponentRenderTarget = | HTMLElement | DocumentFragment @@ -163,31 +161,7 @@ export class Component template!: ComponentReturnType; } -function destroyNode(node: Node) { - if (IS_DEV_MODE) { - if (node === undefined) { - console.warn(`Trying to destroy undefined`); - return; - } else if (node.nodeType === FRAGMENT_TYPE) { - return; - } - const parent = node.parentNode; - if (parent !== null) { - parent.removeChild(node); - } else { - if (import.meta.env.SSR) { - console.warn(`Node is not in DOM`, node.nodeType, node.nodeName); - return; - } - throw new Error(`Node is not in DOM`); - } - } else { - if (node.nodeType === FRAGMENT_TYPE) { - return; - } - node.parentNode!.removeChild(node); - } -} + export function destroyElementSync( component: @@ -217,14 +191,14 @@ export function destroyElementSync( ); } } else { - destroyNode(component); + api.destroy(component); } } } function internalDestroyNode(el: Node | ComponentReturnType) { if ('nodeType' in el) { - destroyNode(el); + api.destroy(el); } else { destroyNodes(el[$nodes]); } @@ -271,7 +245,7 @@ export async function destroyElement( ); } } else { - await destroyNode(component); + api.destroy(component); } } } diff --git a/src/utils/dom-api.ts b/src/utils/dom-api.ts index f067249..4bc3c33 100644 --- a/src/utils/dom-api.ts +++ b/src/utils/dom-api.ts @@ -1,75 +1 @@ -import { getNodeCounter, incrementNodeCounter } from '@/utils/dom'; -import { IN_SSR_ENV, noop } from './shared'; - -let $doc = - typeof document !== 'undefined' - ? document - : (undefined as unknown as Document); -export function setDocument(newDocument: Document) { - $doc = newDocument; -} -export function getDocument() { - return $doc; -} -export const api = { - addEventListener(node: Node, eventName: string, fn: EventListener) { - if (import.meta.env.SSR) { - return noop; - } - node.addEventListener(eventName, fn); - if (RUN_EVENT_DESTRUCTORS_FOR_SCOPED_NODES) { - return () => { - node.removeEventListener(eventName, fn); - }; - } else { - return noop; - } - }, - attr(element: HTMLElement, name: string, value: string | null) { - element.setAttribute(name, value === null ? '' : value); - }, - prop(element: HTMLElement, name: string, value: any) { - // @ts-ignore - element[name] = value; - return value; - }, - comment(text = '') { - if (IN_SSR_ENV) { - incrementNodeCounter(); - return $doc.createComment(`${text} $[${getNodeCounter()}]`); - } else { - if (IS_DEV_MODE) { - return $doc.createComment(text); - } else { - return $doc.createComment(''); - } - } - }, - text(text: string | number = '') { - return $doc.createTextNode(text as string); - }, - textContent(node: Node, text: string) { - node.textContent = text; - }, - fragment() { - return $doc.createDocumentFragment(); - }, - element(tagName = ''): HTMLElement { - return $doc.createElement(tagName); - }, - append( - parent: HTMLElement | Node, - child: HTMLElement | Node, - // @ts-ignore - targetIndex: number = 0, - ) { - this.insert(parent, child, null); - }, - insert( - parent: HTMLElement | Node, - child: HTMLElement | Node, - anchor?: HTMLElement | Node | null, - ) { - parent.insertBefore(child, anchor || null); - }, -}; +export { api, getDocument, setDocument } from './renderers/dom/dom-api'; \ No newline at end of file diff --git a/src/utils/renderers/dom/dom-api.ts b/src/utils/renderers/dom/dom-api.ts new file mode 100644 index 0000000..362c685 --- /dev/null +++ b/src/utils/renderers/dom/dom-api.ts @@ -0,0 +1,102 @@ +import { getNodeCounter, incrementNodeCounter } from '@/utils/dom'; +import { IN_SSR_ENV, noop } from '../../shared'; + +const FRAGMENT_TYPE = 11; // Node.DOCUMENT_FRAGMENT_NODE + +let $doc = + typeof document !== 'undefined' + ? document + : (undefined as unknown as Document); +export function setDocument(newDocument: Document) { + $doc = newDocument; +} +export function getDocument() { + return $doc; +} +export const api = { + addEventListener(node: Node, eventName: string, fn: EventListener) { + if (import.meta.env.SSR) { + return noop; + } + node.addEventListener(eventName, fn); + if (RUN_EVENT_DESTRUCTORS_FOR_SCOPED_NODES) { + return () => { + node.removeEventListener(eventName, fn); + }; + } else { + return noop; + } + }, + attr(element: HTMLElement, name: string, value: string | null) { + element.setAttribute(name, value === null ? '' : value); + }, + prop(element: HTMLElement, name: string, value: any) { + // @ts-ignore + element[name] = value; + return value; + }, + comment(text = '') { + if (IN_SSR_ENV) { + incrementNodeCounter(); + return $doc.createComment(`${text} $[${getNodeCounter()}]`); + } else { + if (IS_DEV_MODE) { + return $doc.createComment(text); + } else { + return $doc.createComment(''); + } + } + }, + text(text: string | number = '') { + return $doc.createTextNode(text as string); + }, + textContent(node: Node, text: string) { + node.textContent = text; + }, + fragment() { + return $doc.createDocumentFragment(); + }, + element(tagName = ''): HTMLElement { + return $doc.createElement(tagName); + }, + append( + parent: HTMLElement | Node, + child: HTMLElement | Node, + // @ts-ignore + targetIndex: number = 0, + ) { + this.insert(parent, child, null); + }, + insert( + parent: HTMLElement | Node, + child: HTMLElement | Node, + anchor?: HTMLElement | Node | null, + ) { + parent.insertBefore(child, anchor || null); + }, + destroy(node: Node) { + if (IS_DEV_MODE) { + if (node === undefined) { + console.warn(`Trying to destroy undefined`); + return; + } else if (node.nodeType === FRAGMENT_TYPE) { + return; + } + const parent = node.parentNode; + if (parent !== null) { + parent.removeChild(node); + } else { + if (import.meta.env.SSR) { + console.warn(`Node is not in DOM`, node.nodeType, node.nodeName); + return; + } + throw new Error(`Node is not in DOM`); + } + } else { + if (node.nodeType === FRAGMENT_TYPE) { + return; + } + node.parentNode!.removeChild(node); + } + }, +}; diff --git a/src/utils/renderers/tres/catalogue.ts b/src/utils/renderers/tres/catalogue.ts new file mode 100644 index 0000000..0f0c512 --- /dev/null +++ b/src/utils/renderers/tres/catalogue.ts @@ -0,0 +1,8 @@ +import { cell, type Cell } from '@lifeart/gxt'; +import type { TresCatalogue } from './types' + +export const catalogue: Cell = cell({}) + +export const extend = (objects: any) => Object.assign(catalogue.value, objects) + +export default { catalogue, extend } \ No newline at end of file diff --git a/src/utils/renderers/tres/tres-api.ts b/src/utils/renderers/tres/tres-api.ts new file mode 100644 index 0000000..f6bd253 --- /dev/null +++ b/src/utils/renderers/tres/tres-api.ts @@ -0,0 +1,257 @@ +import { BufferAttribute } from 'three' +import type { Camera, Object3D } from 'three' +import { deepArrayEqual, isHTMLTag, kebabToCamel } from './utils' + +import type { TresObject, TresObject3D, TresScene } from './types' +import { catalogue } from './catalogue' + +function noop(fn: string): any { + // eslint-disable-next-line no-unused-expressions + fn +} + +let scene: TresScene | null = null + +const { logError } = { + logError() { + console.log(...arguments); + } +} + +const supportedPointerEvents = [ + 'onClick', + 'onPointerMove', + 'onPointerEnter', + 'onPointerLeave', +] + +export const api = { + createElement(tag: string) { + const props = { + args: [] + }; + if (tag === 'template') { return null } + if (isHTMLTag(tag)) { return null } + let name = tag.replace('Tres', '') + let instance + + if (tag === 'primitive') { + if (props?.object === undefined) { logError('Tres primitives need a prop \'object\'') } + const object = props.object as TresObject + name = object.type + instance = Object.assign(object, { type: name, attach: props.attach, primitive: true }) + } + 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 + instance = new target(...props.args) + } + + if (instance.isCamera) { + if (!props?.position) { + instance.position.set(3, 3, 3) + } + if (!props?.lookAt) { + instance.lookAt(0, 0, 0) + } + } + + if (props?.attach === undefined) { + if (instance.isMaterial) { instance.attach = 'material' } + else if (instance.isBufferGeometry) { instance.attach = 'geometry' } + } + + // determine whether the material was passed via prop to + // prevent it's disposal when node is removed later in it's lifecycle + + if (instance.isObject3D) { + if (props?.material?.isMaterial) { (instance as TresObject3D).userData.tres__materialViaProp = true } + if (props?.geometry?.isBufferGeometry) { (instance as TresObject3D).userData.tres__geometryViaProp = true } + } + + // Since THREE instances properties are not consistent, (Orbit Controls doesn't have a `type` property) + // we take the tag name and we save it on the userData for later use in the re-instancing process. + instance.userData = { + ...instance.userData, + tres__name: name, + } + + return instance + }, + insert(child, parent) { + if (parent && parent.isScene) { scene = parent as unknown as TresScene } + + const parentObject = parent || scene + + if (child?.isObject3D) { + if (child?.isCamera) { + if (!scene?.userData.tres__registerCamera) { throw new Error('could not find tres__registerCamera on scene\'s userData') } + + scene?.userData.tres__registerCamera?.(child as unknown as Camera) + } + + if ( + child && supportedPointerEvents.some(eventName => child[eventName]) + ) { + if (!scene?.userData.tres__registerAtPointerEventHandler) { throw new Error('could not find tres__registerAtPointerEventHandler on scene\'s userData') } + + scene?.userData.tres__registerAtPointerEventHandler?.(child as Object3D) + } + } + + if (child?.isObject3D && parentObject?.isObject3D) { + parentObject.add(child) + child.dispatchEvent({ type: 'added' }) + } + else if (child?.isFog) { + parentObject.fog = child + } + else if (typeof child?.attach === 'string') { + child.__previousAttach = child[parentObject?.attach as string] + if (parentObject) { + parentObject[child.attach] = child + } + } + }, + remove(node) { + if (!node) { return } + // remove is only called on the node being removed and not on child nodes. + + if (node.isObject3D) { + const object3D = node as unknown as Object3D + + const disposeMaterialsAndGeometries = (object3D: Object3D) => { + const tresObject3D = object3D as TresObject3D + + if (!object3D.userData.tres__materialViaProp) { + tresObject3D.material?.dispose() + tresObject3D.material = undefined + } + + if (!object3D.userData.tres__geometryViaProp) { + tresObject3D.geometry?.dispose() + tresObject3D.geometry = undefined + } + } + + const deregisterAtPointerEventHandler = scene?.userData.tres__deregisterAtPointerEventHandler + const deregisterBlockingObjectAtPointerEventHandler + = scene?.userData.tres__deregisterBlockingObjectAtPointerEventHandler + + const deregisterAtPointerEventHandlerIfRequired = (object: TresObject) => { + if (!deregisterBlockingObjectAtPointerEventHandler) { throw new Error('could not find tres__deregisterBlockingObjectAtPointerEventHandler on scene\'s userData') } + + scene?.userData.tres__deregisterBlockingObjectAtPointerEventHandler?.(object as Object3D) + + if (!deregisterAtPointerEventHandler) { throw new Error('could not find tres__deregisterAtPointerEventHandler on scene\'s userData') } + + if ( + object && supportedPointerEvents.some(eventName => object[eventName]) + ) { deregisterAtPointerEventHandler?.(object as Object3D) } + } + + const deregisterCameraIfRequired = (object: Object3D) => { + const deregisterCamera = scene?.userData.tres__deregisterCamera + + if (!deregisterCamera) { throw new Error('could not find tres__deregisterCamera on scene\'s userData') } + + if ((object as Camera).isCamera) { deregisterCamera?.(object as Camera) } + } + + node.removeFromParent?.() + object3D.traverse((child: Object3D) => { + disposeMaterialsAndGeometries(child) + deregisterCameraIfRequired(child) + deregisterAtPointerEventHandlerIfRequired?.(child as TresObject) + }) + + disposeMaterialsAndGeometries(object3D) + deregisterCameraIfRequired(object3D) + deregisterAtPointerEventHandlerIfRequired?.(object3D as TresObject) + } + + node.dispose?.() + }, + patchProp(node, prop, _prevValue, nextValue) { + if (node) { + let root = node + let key = prop + if (node.isObject3D && key === 'blocks-pointer-events') { + if (nextValue || nextValue === '') { scene?.userData.tres__registerBlockingObjectAtPointerEventHandler?.(node as Object3D) } + else { scene?.userData.tres__deregisterBlockingObjectAtPointerEventHandler?.(node as Object3D) } + + return + } + + 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.userData.tres__name || 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)), + ) + return + } + + // Traverse pierced props (e.g. foo-bar=value => foo.bar = value) + if (key.includes('-') && target === undefined) { + const chain = key.split('-') + target = chain.reduce((acc, key) => acc[kebabToCamel(key)], root) + key = chain.pop() as string + finalKey = key.toLowerCase() + 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 (isFunction(target)) { + // don't call pointer event callback functions + if (!supportedPointerEvents.includes(prop)) { + if (Array.isArray(value)) { node[finalKey](...value) } + else { node[finalKey](value) } + } + return + } + if (!target?.set && !isFunction(target)) { root[finalKey] = value } + else if (target.constructor === value.constructor && target?.copy) { target?.copy(value) } + else if (Array.isArray(value)) { target.set(...value) } + else if (!target.isColor && target.setScalar) { target.setScalar(value) } + else { target.set(value) } + } + }, + + parentNode(node) { + return node?.parent || null + }, + createText: () => noop('createText'), + createComment: () => noop('createComment'), + + setText: () => noop('setText'), + + setElementText: () => noop('setElementText'), + nextSibling: () => noop('nextSibling'), + + querySelector: () => noop('querySelector'), + + setScopeId: () => noop('setScopeId'), + cloneNode: () => noop('cloneNode'), + + insertStaticContent: () => noop('insertStaticContent'), +} \ No newline at end of file diff --git a/src/utils/renderers/tres/types.d.ts b/src/utils/renderers/tres/types.d.ts new file mode 100644 index 0000000..f977eb4 --- /dev/null +++ b/src/utils/renderers/tres/types.d.ts @@ -0,0 +1,185 @@ +import type * as THREE from 'three' +// import type { EventProps as PointerEventHandlerEventProps } from '../composables/usePointerEventHandler' + +// Based on React Three Fiber types by Pmndrs +// https://github.com/pmndrs/react-three-fiber/blob/v9/packages/fiber/src/three-types.ts + +export type AttachFnType = (parent: any, self: O) => () => void +export type AttachType = string | AttachFnType + +export type ConstructorRepresentation = new (...args: any[]) => any +export type NonFunctionKeys

= { [K in keyof P]-?: P[K] extends Function ? never : K }[keyof P] +export type Overwrite = Omit> & O +export type Properties = Pick> +export type Mutable

= { [K in keyof P]: P[K] | Readonly } +export type Args = T extends ConstructorRepresentation ? ConstructorParameters : any[] + +export interface TresCatalogue { + [name: string]: ConstructorRepresentation +} +export type TresCamera = THREE.OrthographicCamera | THREE.PerspectiveCamera + +export interface InstanceProps { + args?: Args

+ object?: T + visible?: boolean + dispose?: null + attach?: AttachType +} + +interface TresBaseObject { + attach?: string + removeFromParent?: () => void + dispose?: () => void + [prop: string]: any // for arbitrary properties +} + +// Custom type for geometry and material properties in Object3D +export interface TresObject3D extends THREE.Object3D { + geometry?: THREE.BufferGeometry & TresBaseObject + material?: THREE.Material & TresBaseObject + userData: { + tres__materialViaProp: boolean + tres__geometryViaProp: boolean + [key: string]: any + } +} + +export type TresObject = TresBaseObject & (TresObject3D | THREE.BufferGeometry | THREE.Material | THREE.Fog) + +export interface TresScene extends THREE.Scene { + userData: { + // keys are prefixed with tres__ to avoid name collisions + tres__registerCamera?: (newCamera: THREE.Camera, active?: boolean) => void + tres__deregisterCamera?: (camera: THREE.Camera) => void + tres__registerAtPointerEventHandler?: (object: THREE.Object3D & PointerEventHandlerEventProps) => void + tres__deregisterAtPointerEventHandler?: (object: THREE.Object3D) => void + tres__registerBlockingObjectAtPointerEventHandler?: (object: THREE.Object3D) => void + tres__deregisterBlockingObjectAtPointerEventHandler?: (object: THREE.Object3D) => void + [key: string]: any + } +} + +// Events + +export interface Intersection extends THREE.Intersection { + /** The event source (the object which registered the handler) */ + eventObject: TresObject +} + +export interface IntersectionEvent extends Intersection { + /** The event source (the object which registered the handler) */ + eventObject: TresObject + /** An array of intersections */ + intersections: Intersection[] + /** vec3.set(pointer.x, pointer.y, 0).unproject(camera) */ + unprojectedPoint: THREE.Vector3 + /** Normalized event coordinates */ + pointer: THREE.Vector2 + /** Delta between first click and this event */ + delta: number + /** The ray that pierced it */ + ray: THREE.Ray + /** The camera that was used by the raycaster */ + camera: TresCamera + /** stopPropagation will stop underlying handlers from firing */ + stopPropagation: () => void + /** The original host event */ + nativeEvent: TSourceEvent + /** If the event was stopped by calling stopPropagation */ + stopped: boolean +} + +export type ThreeEvent = IntersectionEvent & Properties +export type DomEvent = PointerEvent | MouseEvent | WheelEvent + +export interface Events { + onClick: EventListener + onContextMenu: EventListener + onDoubleClick: EventListener + onWheel: EventListener + onPointerDown: EventListener + onPointerUp: EventListener + onPointerLeave: EventListener + onPointerMove: EventListener + onPointerCancel: EventListener + onLostPointerCapture: EventListener +} + +export interface EventHandlers { + onClick?: (event: ThreeEvent) => void + onContextMenu?: (event: ThreeEvent) => void + onDoubleClick?: (event: ThreeEvent) => void + onPointerUp?: (event: ThreeEvent) => void + onPointerDown?: (event: ThreeEvent) => void + onPointerOver?: (event: ThreeEvent) => void + onPointerOut?: (event: ThreeEvent) => void + onPointerEnter?: (event: ThreeEvent) => void + onPointerLeave?: (event: ThreeEvent) => void + onPointerMove?: (event: ThreeEvent) => void + onPointerMissed?: (event: MouseEvent) => void + onPointerCancel?: (event: ThreeEvent) => void + onWheel?: (event: ThreeEvent) => void +} + +interface MathRepresentation { + set: (...args: number[] | [THREE.ColorRepresentation]) => any +} +interface VectorRepresentation extends MathRepresentation { + setScalar: (s: number) => any +} + +export interface VectorCoordinates { + x: number + y: number + z: number +} + +export type MathType = T extends THREE.Color + ? ConstructorParameters | THREE.ColorRepresentation + + : T extends VectorRepresentation | THREE.Layers | THREE.Euler ? T | Parameters | number | VectorCoordinates : T | Parameters + +export type TresVector2 = MathType +export type TresVector3 = MathType +export type TresVector4 = MathType +export type TresColor = MathType +export type TresLayers = MathType +export type TresQuaternion = MathType +export type TresEuler = MathType + +type WithMathProps

= { [K in keyof P]: P[K] extends MathRepresentation | THREE.Euler ? MathType : P[K] } + +interface RaycastableRepresentation { + raycast: (raycaster: THREE.Raycaster, intersects: THREE.Intersection[]) => void +} +type EventProps

= P extends RaycastableRepresentation ? Partial : unknown + +export interface VueProps

{ + children?: VNode

[] + ref?: VNodeRef + key?: string | number | symbol +} + +type ElementProps> = Partial< + Overwrite, VueProps

& EventProps

> +> + +export type ThreeElement = Mutable< + Overwrite, Omit, T>, 'object'>> +> + +type ThreeExports = typeof THREE +type ThreeInstancesImpl = { + [K in keyof ThreeExports as Uncapitalize]: ThreeExports[K] extends ConstructorRepresentation + ? ThreeElement + : never +} + +export interface ThreeInstances extends ThreeInstancesImpl { + primitive: Omit, 'args'> & { object: object } +} + +type TresComponents = { + [K in keyof ThreeInstances as `Tres${Capitalize}`]: DefineComponent +} diff --git a/src/utils/renderers/tres/utils.ts b/src/utils/renderers/tres/utils.ts new file mode 100644 index 0000000..11c233f --- /dev/null +++ b/src/utils/renderers/tres/utils.ts @@ -0,0 +1,262 @@ +import { DoubleSide, MeshBasicMaterial, Vector3 } from 'three' +import type { Mesh, Object3D, Scene } from 'three' + +export function toSetMethodName(key: string) { + return `set${key[0].toUpperCase()}${key.slice(1)}` +} + +export const merge = (target: any, source: any) => { + // Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties + for (const key of Object.keys(source)) { + if (source[key] instanceof Object) { + Object.assign(source[key], merge(target[key], source[key])) + } + } + + // Join `target` and modified `source` + Object.assign(target || {}, source) + return target +} + +const HTML_TAGS + = 'html,body,base,head,link,meta,style,title,address,article,aside,footer,' + + 'header,hgroup,h1,h2,h3,h4,h5,h6,nav,section,div,dd,dl,dt,figcaption,' + + 'figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,' + + 'data,dfn,em,i,kbd,mark,q,rp,rt,ruby,s,samp,small,span,strong,sub,sup,' + + 'time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,' + + 'canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,' + + 'th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,' + + 'option,output,progress,select,textarea,details,dialog,menu,' + + 'summary,template,blockquote,iframe,tfoot' + +export const isHTMLTag = /* #__PURE__ */ makeMap(HTML_TAGS) + +export function isDOMElement(obj: any): obj is HTMLElement { + return obj && obj.nodeType === 1 +} + +export function kebabToCamel(str: string) { + return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase()) +} + +export function makeMap(str: string, expectsLowerCase?: boolean): (key: string) => boolean { + const map: Record = Object.create(null) + const list: Array = str.split(',') + for (let i = 0; i < list.length; i++) { + map[list[i]] = true + } + return expectsLowerCase ? val => !!map[val.toLowerCase()] : val => !!map[val] +} + +export const uniqueBy = (array: T[], iteratee: (value: T) => K): T[] => { + const seen = new Set() + const result: T[] = [] + + for (const item of array) { + const identifier = iteratee(item) + if (!seen.has(identifier)) { + seen.add(identifier) + result.push(item) + } + } + + return result +} + +export const get = (obj: any, path: string | string[]): T | undefined => { + if (!path) { + return undefined + } + + // Regex explained: https://regexr.com/58j0k + const pathArray = Array.isArray(path) ? path : path.match(/([^[.\]])+/g) + + return pathArray?.reduce((prevObj, key) => prevObj && prevObj[key], obj) +} + +export const set = (obj: any, path: string | string[], value: any): void => { + // Regex explained: https://regexr.com/58j0k + const pathArray = Array.isArray(path) ? path : path.match(/([^[.\]])+/g) + + if (pathArray) { + pathArray.reduce((acc, key, i) => { + if (acc[key] === undefined) { + acc[key] = {} + } + if (i === pathArray.length - 1) { + acc[key] = value + } + return acc[key] + }, obj) + } +} + +export function deepEqual(a: any, b: any): boolean { + if (isDOMElement(a) && isDOMElement(b)) { + const attrsA = a.attributes + const attrsB = b.attributes + + if (attrsA.length !== attrsB.length) { + return false + } + + return Array.from(attrsA).every(({ name, value }) => b.getAttribute(name) === value) + } + // If both are primitives, return true if they are equal + if (a === b) { + return true + } + + // If either of them is null or not an object, return false + if (a === null || typeof a !== 'object' || b === null || typeof b !== 'object') { + return false + } + + // Get the keys of both objects + const keysA = Object.keys(a); const keysB = Object.keys(b) + + // If they have different number of keys, they are not equal + if (keysA.length !== keysB.length) { + return false + } + + // Check each key in A to see if it exists in B and its value is the same in both + for (const key of keysA) { + if (!keysB.includes(key) || !deepEqual(a[key], b[key])) { + return false + } + } + + return true +} + +export function deepArrayEqual(arr1: any[], arr2: any[]): boolean { + // If they're not both arrays, return false + if (!Array.isArray(arr1) || !Array.isArray(arr2)) { + return false + } + + // If they don't have the same length, they're not equal + if (arr1.length !== arr2.length) { + return false + } + + // Check each element of arr1 against the corresponding element of arr2 + for (let i = 0; i < arr1.length; i++) { + if (!deepEqual(arr1[i], arr2[i])) { + return false + } + } + + return true +} + +/** + * TypeSafe version of Array.isArray + */ +export const isArray = Array.isArray as (a: any) => a is any[] | readonly any[] + +export function editSceneObject(scene: Scene, objectUuid: string, propertyPath: string[], value: any): void { + // Function to recursively find the object by UUID + const findObjectByUuid = (node: Object3D): Object3D | undefined => { + if (node.uuid === objectUuid) { + return node + } + + for (const child of node.children) { + const found = findObjectByUuid(child) + if (found) { + return found + } + } + + return undefined + } + + // Find the target object + const targetObject = findObjectByUuid(scene) + if (!targetObject) { + console.warn('Object with UUID not found in the scene.') + return + } + + // Traverse the property path to get to the desired property + let currentProperty: any = targetObject + for (let i = 0; i < propertyPath.length - 1; i++) { + if (currentProperty[propertyPath[i]] !== undefined) { + currentProperty = currentProperty[propertyPath[i]] + } + else { + console.warn(`Property path is not valid: ${propertyPath.join('.')}`) + return + } + } + + // Set the new value + const lastProperty = propertyPath[propertyPath.length - 1] + if (currentProperty[lastProperty] !== undefined) { + currentProperty[lastProperty] = value + } + else { + console.warn(`Property path is not valid: ${propertyPath.join('.')}`) + } +} + +export function createHighlightMaterial(): MeshBasicMaterial { + return new MeshBasicMaterial({ + color: 0xA7E6D7, // Highlight color, e.g., yellow + transparent: true, + opacity: 0.2, + depthTest: false, // So the highlight is always visible + side: DoubleSide, // To ensure the highlight is visible from all angles + }) +} +let animationFrameId: number | null = null +export function animateHighlight(highlightMesh: Mesh, startTime: number): void { + const currentTime = Date.now() + const time = (currentTime - startTime) / 1000 // convert to seconds + + // Pulsing effect parameters + const scaleAmplitude = 0.07 // Amplitude of the scale pulsation + const pulseSpeed = 2.5 // Speed of the pulsation + + // Calculate the scale factor with a sine function for pulsing effect + const scaleFactor = 1 + scaleAmplitude * Math.sin(pulseSpeed * time) + + // Apply the scale factor + highlightMesh.scale.set(scaleFactor, scaleFactor, scaleFactor) + + // Update the animation frame ID + animationFrameId = requestAnimationFrame(() => animateHighlight(highlightMesh, startTime)) +} + +export function stopHighlightAnimation(): void { + if (animationFrameId !== null) { + cancelAnimationFrame(animationFrameId) + animationFrameId = null + } +} + +export function createHighlightMesh(object: Object3D): Mesh { + const highlightMaterial = new MeshBasicMaterial({ + color: 0xA7E6D7, // Highlight color, e.g., yellow + transparent: true, + opacity: 0.2, + depthTest: false, // So the highlight is always visible + side: DoubleSide, // To e + }) + // Clone the geometry of the object. You might need a more complex approach + // if the object's geometry is not straightforward. + const highlightMesh = new HightlightMesh(object.geometry.clone(), highlightMaterial) + + return highlightMesh +} + +export function extractBindingPosition(binding: any): Vector3 { + let observer = binding.value + if (binding.value && binding.value?.isMesh) { + observer = binding.value.position + } + if (Array.isArray(binding.value)) { observer = new Vector3(...observer) } + return observer +} \ No newline at end of file diff --git a/src/utils/ssr/ssr.ts b/src/utils/ssr/ssr.ts index 473ec1d..8f5cfd0 100644 --- a/src/utils/ssr/ssr.ts +++ b/src/utils/ssr/ssr.ts @@ -3,7 +3,7 @@ import { runDestructors, type ComponentReturnType, } from '@/utils/component'; -import { setDocument, getDocument } from '../dom-api'; +import { setDocument, getDocument } from '../renderers/dom/dom-api'; import { getRoot, resetNodeCounter, resetRoot } from '@/utils/dom'; type EnvironmentParams = { From 1474d56d30a649ed113cd8a7ec51c27c7c78e6e7 Mon Sep 17 00:00:00 2001 From: Alex Kanunnikov Date: Sat, 4 May 2024 23:08:51 +0300 Subject: [PATCH 2/7] + --- plugins/utils.ts | 14 +++++++-- src/utils/control-flow/if.ts | 8 ++--- src/utils/control-flow/list.ts | 12 ++++---- src/utils/dom.ts | 44 +++++++++++----------------- src/utils/hmr.ts | 3 +- src/utils/renderers/dom/dom-api.ts | 7 +++-- src/utils/renderers/tres/tres-api.ts | 29 +++++++++++------- src/utils/types.d.ts | 22 ++++++++++++++ 8 files changed, 87 insertions(+), 52 deletions(-) create mode 100644 src/utils/types.d.ts diff --git a/plugins/utils.ts b/plugins/utils.ts index ccc91b5..1376266 100644 --- a/plugins/utils.ts +++ b/plugins/utils.ts @@ -344,6 +344,16 @@ function hasStableChildsForControlNode( return hasStableChild; } +export function isComponentNode(node: HBSNode) { + if (node.tag && node.tag.toLowerCase() !== node.tag) { + return true; + } + if (node.tag.startsWith('Tres')) { + return true; + } + return false; +} + export function serializeNode( node: string | null | HBSNode | HBSControlExpression | ComplexJSType, ctxName = 'this', @@ -439,9 +449,7 @@ export function serializeNode( return `${SYMBOLS.IF}(${arrayName}, ${trueBranch}, ${falseBranch}, ${ctxName})`; } } else if ( - typeof node === 'object' && - node.tag && - node.tag.toLowerCase() !== node.tag + typeof node === 'object' && isComponentNode(node) ) { const hasSplatAttrs = node.attributes.find((attr) => { return attr[0] === '...attributes'; diff --git a/src/utils/control-flow/if.ts b/src/utils/control-flow/if.ts index be5354d..67db67e 100644 --- a/src/utils/control-flow/if.ts +++ b/src/utils/control-flow/if.ts @@ -79,7 +79,7 @@ export function ifCondition( associateDestroyable(ctx, [ () => { if (placeholder.isConnected) { - placeholder.parentNode!.removeChild(placeholder); + api.destroy(placeholder); } }, runExistingDestructors, @@ -93,7 +93,7 @@ export function ifCondition( const newPlaceholder = IS_DEV_MODE ? api.comment('if-error-placeholder') : api.comment(''); - api.insert(placeholder.parentNode!, newPlaceholder, placeholder); + api.insert(api.parentNode(placeholder)!, newPlaceholder, placeholder); runExistingDestructors().then(async () => { removeDestructor(ctx, runExistingDestructors); if (!newPlaceholder.isConnected) { @@ -137,7 +137,7 @@ export function ifCondition( $DEBUG_REACTIVE_CONTEXTS.pop(); } renderElement( - placeholder.parentNode || target, + api.parentNode(placeholder) || target, prevComponent, placeholder, ); @@ -184,7 +184,7 @@ export function ifCondition( $DEBUG_REACTIVE_CONTEXTS.pop(); } renderElement( - placeholder.parentNode || target, + api.parentNode(placeholder) || target, prevComponent, placeholder, ); diff --git a/src/utils/control-flow/list.ts b/src/utils/control-flow/list.ts index 4bcf6b6..0ee58e9 100644 --- a/src/utils/control-flow/list.ts +++ b/src/utils/control-flow/list.ts @@ -248,8 +248,9 @@ export class BasicListComponent { const row = ItemComponent(item, idx, this as unknown as Component); keyMap.set(key, row); indexMap.set(key, index); + const parentNode = api.parentNode(targetNode)!; row.forEach((item) => { - renderElement(targetNode.parentNode!, item, targetNode); + renderElement(parentNode, item, targetNode); }); } else { seenKeys++; @@ -265,23 +266,24 @@ export class BasicListComponent { rowsToMove.forEach(([row, index]) => { const nextItem = items[index + 1]; if (nextItem === undefined) { - renderElement(bottomMarker.parentNode!, row, bottomMarker); + renderElement(api.parentNode(bottomMarker)!, row, bottomMarker); } else { const nextKey = keyForItem(nextItem); const nextRow = keyMap.get(nextKey)!; const firstNode = getFirstNode(nextRow); if (nextRow !== undefined && firstNode !== undefined) { - const parent = firstNode.parentNode!; + const parent = api.parentNode(firstNode)!; renderElement(parent, row, firstNode); } } }); if (targetNode !== bottomMarker) { - const parent = targetNode.parentNode!; - const trueParent = bottomMarker.parentNode!; + const parent = api.parentNode(targetNode)!; + const trueParent = api.parentNode(bottomMarker)!; // parent may not exist in rehydration if (!IN_SSR_ENV) { + // TODO: move to dom API parent && parent.removeChild(targetNode); } if (trueParent !== parent) { diff --git a/src/utils/dom.ts b/src/utils/dom.ts index 200331c..b1d2e99 100644 --- a/src/utils/dom.ts +++ b/src/utils/dom.ts @@ -45,29 +45,16 @@ import { } from './shared'; import { isRehydrationScheduled } from './ssr/rehydration'; import { createHotReload } from './hmr'; - -type RenderableType = Node | ComponentReturnType | string | number; -type ShadowRootMode = 'open' | 'closed' | null; -type ModifierFn = ( - element: HTMLElement, - ...args: unknown[] -) => void | DestructorFn; - -type Attr = - | MergedCell - | Cell - | string - | ((element: HTMLElement, attribute: string) => void); - -type TagAttr = [string, Attr]; -type TagProp = [string, Attr]; -type TagEvent = [string, EventListener | ModifierFn]; -type FwType = [TagProp[], TagAttr[], TagEvent[]]; -type Props = [TagProp[], TagAttr[], TagEvent[], FwType?]; - -type Fn = () => unknown; -type InElementFnArg = () => HTMLElement; -type BranchCb = () => ComponentReturnType | Node; +import type { + Props, + ModifierFn, + InElementFnArg, + ShadowRootMode, + BranchCb, + FwType, + RenderableType, + Fn, +} from './types'; // EMPTY DOM PROPS export const $_edp = [[], [], []] as Props; @@ -365,7 +352,7 @@ function _DOM( ctx: any, ): Node { NODE_COUNTER++; - const element = api.element(tag); + const element = api.element(tag, '', ctx, tagProps); if (IS_DEV_MODE) { $DEBUG_REACTIVE_CONTEXTS.push(`${tag}`); } @@ -752,7 +739,10 @@ function mergeComponents( } } if (isFn(component)) { - component = text(resolveRenderable(component, 'merge-components'), $destructors); + component = text( + resolveRenderable(component, 'merge-components'), + $destructors, + ); } if (isEmpty(component)) { return; @@ -825,7 +815,7 @@ function slot(name: string, params: () => unknown[], $slot: Slots, ctx: any) { $destructors.push(() => { destroyElement(slotRoots); }); - renderElement(slotPlaceholder.parentNode!, slotRoots, slotPlaceholder); + renderElement(api.parentNode(slotPlaceholder)!, slotRoots, slotPlaceholder); isRendered = true; }, get() { @@ -1053,7 +1043,7 @@ export function $_dc( destroyElementSync(result); result = component(value, args, ctx); result![$nodes].push(target!); - renderElement(target!.parentNode!, result, target!); + renderElement(api.parentNode(target!)!, result, target!); } else { result = component(value, args, ctx); } diff --git a/src/utils/hmr.ts b/src/utils/hmr.ts index 56107a7..46a71da 100644 --- a/src/utils/hmr.ts +++ b/src/utils/hmr.ts @@ -6,6 +6,7 @@ import { type Component, type ComponentReturnType, } from '@/utils/component'; +import { api } from './dom-api'; export function createHotReload( component: ( @@ -28,7 +29,7 @@ export function createHotReload( renderedBuckets.forEach(({ parent, instance, args }) => { const newCmp = component(newKlass, args, parent); const firstElement = getFirstNode(instance); - const parentElement = firstElement.parentNode; + const parentElement = api.parentNode(firstElement); if (!parentElement) { return; } diff --git a/src/utils/renderers/dom/dom-api.ts b/src/utils/renderers/dom/dom-api.ts index 362c685..53007c7 100644 --- a/src/utils/renderers/dom/dom-api.ts +++ b/src/utils/renderers/dom/dom-api.ts @@ -1,6 +1,6 @@ import { getNodeCounter, incrementNodeCounter } from '@/utils/dom'; import { IN_SSR_ENV, noop } from '../../shared'; - +import type { Props } from '../../types'; const FRAGMENT_TYPE = 11; // Node.DOCUMENT_FRAGMENT_NODE let $doc = @@ -35,6 +35,9 @@ export const api = { element[name] = value; return value; }, + parentNode(element: Node) { + return element.parentNode; + }, comment(text = '') { if (IN_SSR_ENV) { incrementNodeCounter(); @@ -56,7 +59,7 @@ export const api = { fragment() { return $doc.createDocumentFragment(); }, - element(tagName = ''): HTMLElement { + element(tagName = '', namespace = '', ctx: any = null, props: Props = [[],[],[]]): HTMLElement { return $doc.createElement(tagName); }, append( diff --git a/src/utils/renderers/tres/tres-api.ts b/src/utils/renderers/tres/tres-api.ts index f6bd253..bb01243 100644 --- a/src/utils/renderers/tres/tres-api.ts +++ b/src/utils/renderers/tres/tres-api.ts @@ -4,6 +4,8 @@ import { deepArrayEqual, isHTMLTag, kebabToCamel } from './utils' import type { TresObject, TresObject3D, TresScene } from './types' import { catalogue } from './catalogue' +import { Props } from '@/utils/types' +import { isFn } from '@/utils/shared' function noop(fn: string): any { // eslint-disable-next-line no-unused-expressions @@ -26,10 +28,17 @@ const supportedPointerEvents = [ ] export const api = { - createElement(tag: string) { - const props = { - args: [] - }; + element(tag: string, _isSVG: string, _anchor: any, _props: Props) { + let props = {}; + let args = _props[1]; + args.forEach((arg) => { + props[arg[0]] = arg[1]; + }); + if (!props) { props = {} } + + if (!props.args) { + props.args = [] + } if (tag === 'template') { return null } if (isHTMLTag(tag)) { return null } let name = tag.replace('Tres', '') @@ -81,7 +90,7 @@ export const api = { return instance }, - insert(child, parent) { + append(parent, child) { if (parent && parent.isScene) { scene = parent as unknown as TresScene } const parentObject = parent || scene @@ -116,7 +125,7 @@ export const api = { } } }, - remove(node) { + destroy(node) { if (!node) { return } // remove is only called on the node being removed and not on child nodes. @@ -175,7 +184,7 @@ export const api = { node.dispose?.() }, - patchProp(node, prop, _prevValue, nextValue) { + prop(node, prop, nextValue) { if (node) { let root = node let key = prop @@ -191,7 +200,7 @@ export const api = { if (key === 'args') { const prevNode = node as TresObject3D - const prevArgs = _prevValue ?? [] + const prevArgs: any[] = []; const args = nextValue ?? [] const instanceName = node.userData.tres__name || node.type @@ -221,7 +230,7 @@ export const api = { let value = nextValue if (value === '') { value = true } // Set prop, prefer atomic methods if applicable - if (isFunction(target)) { + if (isFn(target)) { // don't call pointer event callback functions if (!supportedPointerEvents.includes(prop)) { if (Array.isArray(value)) { node[finalKey](...value) } @@ -229,7 +238,7 @@ export const api = { } return } - if (!target?.set && !isFunction(target)) { root[finalKey] = value } + if (!target?.set && !isFn(target)) { root[finalKey] = value } else if (target.constructor === value.constructor && target?.copy) { target?.copy(value) } else if (Array.isArray(value)) { target.set(...value) } else if (!target.isColor && target.setScalar) { target.setScalar(value) } diff --git a/src/utils/types.d.ts b/src/utils/types.d.ts new file mode 100644 index 0000000..d31b767 --- /dev/null +++ b/src/utils/types.d.ts @@ -0,0 +1,22 @@ +export type RenderableType = Node | ComponentReturnType | string | number; +export type ShadowRootMode = 'open' | 'closed' | null; +export type ModifierFn = ( + element: HTMLElement, + ...args: unknown[] +) => void | DestructorFn; + +export type Attr = + | MergedCell + | Cell + | string + | ((element: HTMLElement, attribute: string) => void); + +export type TagAttr = [string, Attr]; +export type TagProp = [string, Attr]; +export type TagEvent = [string, EventListener | ModifierFn]; +export type FwType = [TagProp[], TagAttr[], TagEvent[]]; +export type Props = [TagProp[], TagAttr[], TagEvent[], FwType?]; + +export type Fn = () => unknown; +export type InElementFnArg = () => HTMLElement; +export type BranchCb = () => ComponentReturnType | Node; From 94133a7dd1283b1d28b8aa97f40d5d9f1b3f0cd3 Mon Sep 17 00:00:00 2001 From: Alex Kanunnikov Date: Sat, 4 May 2024 23:17:31 +0300 Subject: [PATCH 3/7] + --- src/utils/renderers/tres/tres-api.ts | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/utils/renderers/tres/tres-api.ts b/src/utils/renderers/tres/tres-api.ts index bb01243..6db0809 100644 --- a/src/utils/renderers/tres/tres-api.ts +++ b/src/utils/renderers/tres/tres-api.ts @@ -245,22 +245,7 @@ export const api = { else { target.set(value) } } }, - parentNode(node) { return node?.parent || null - }, - createText: () => noop('createText'), - createComment: () => noop('createComment'), - - setText: () => noop('setText'), - - setElementText: () => noop('setElementText'), - nextSibling: () => noop('nextSibling'), - - querySelector: () => noop('querySelector'), - - setScopeId: () => noop('setScopeId'), - cloneNode: () => noop('cloneNode'), - - insertStaticContent: () => noop('insertStaticContent'), + } } \ No newline at end of file From 6fd9830f0db57d441d0601161964b06130c3dfde Mon Sep 17 00:00:00 2001 From: Alex Kanunnikov Date: Sat, 4 May 2024 23:27:55 +0300 Subject: [PATCH 4/7] + --- src/utils/dom.ts | 4 ++-- src/utils/renderers/dom/dom-api.ts | 3 ++- src/utils/renderers/tres/tres-api.ts | 26 ++++++++++++++++++++------ src/utils/renderers/tres/utils.ts | 1 + 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/utils/dom.ts b/src/utils/dom.ts index b1d2e99..3ace5bf 100644 --- a/src/utils/dom.ts +++ b/src/utils/dom.ts @@ -228,7 +228,7 @@ export function addChild( } const isObject = typeof child === 'object'; if (isObject && $nodes in child) { - child[$nodes].forEach((node, i) => { + child[$nodes].forEach((node: Node, i: number) => { addChild(element, node, destructors, index + i); }); } else if (isPrimitive(child)) { @@ -352,7 +352,7 @@ function _DOM( ctx: any, ): Node { NODE_COUNTER++; - const element = api.element(tag, '', ctx, tagProps); + const element = api.element(tag); if (IS_DEV_MODE) { $DEBUG_REACTIVE_CONTEXTS.push(`${tag}`); } diff --git a/src/utils/renderers/dom/dom-api.ts b/src/utils/renderers/dom/dom-api.ts index 53007c7..0890346 100644 --- a/src/utils/renderers/dom/dom-api.ts +++ b/src/utils/renderers/dom/dom-api.ts @@ -59,7 +59,8 @@ export const api = { fragment() { return $doc.createDocumentFragment(); }, - element(tagName = '', namespace = '', ctx: any = null, props: Props = [[],[],[]]): HTMLElement { + // @ts-expect-error + element(tagName = '', namespace?: string, ctx?: any, props?: Props): HTMLElement { return $doc.createElement(tagName); }, append( diff --git a/src/utils/renderers/tres/tres-api.ts b/src/utils/renderers/tres/tres-api.ts index 6db0809..e84abdc 100644 --- a/src/utils/renderers/tres/tres-api.ts +++ b/src/utils/renderers/tres/tres-api.ts @@ -7,16 +7,12 @@ import { catalogue } from './catalogue' import { Props } from '@/utils/types' import { isFn } from '@/utils/shared' -function noop(fn: string): any { - // eslint-disable-next-line no-unused-expressions - fn -} let scene: TresScene | null = null const { logError } = { - logError() { - console.log(...arguments); + logError(msg: string) { + console.log(msg); } } @@ -32,11 +28,14 @@ export const api = { let props = {}; let args = _props[1]; args.forEach((arg) => { + // @ts-expect-error props[arg[0]] = arg[1]; }); if (!props) { props = {} } + // @ts-expect-error if (!props.args) { + // @ts-expect-error props.args = [] } if (tag === 'template') { return null } @@ -45,9 +44,12 @@ export const api = { let instance if (tag === 'primitive') { + // @ts-expect-error if (props?.object === undefined) { logError('Tres primitives need a prop \'object\'') } + // @ts-expect-error const object = props.object as TresObject name = object.type + // @ts-expect-error instance = Object.assign(object, { type: name, attach: props.attach, primitive: true }) } else { @@ -56,18 +58,22 @@ export const api = { logError(`${name} is not defined on the THREE namespace. Use extend to add it to the catalog.`) } // eslint-disable-next-line new-cap + // @ts-expect-error instance = new target(...props.args) } if (instance.isCamera) { + // @ts-expect-error if (!props?.position) { instance.position.set(3, 3, 3) } + // @ts-expect-error if (!props?.lookAt) { instance.lookAt(0, 0, 0) } } + // @ts-expect-error if (props?.attach === undefined) { if (instance.isMaterial) { instance.attach = 'material' } else if (instance.isBufferGeometry) { instance.attach = 'geometry' } @@ -77,7 +83,9 @@ export const api = { // prevent it's disposal when node is removed later in it's lifecycle if (instance.isObject3D) { + // @ts-expect-error if (props?.material?.isMaterial) { (instance as TresObject3D).userData.tres__materialViaProp = true } + // @ts-expect-error if (props?.geometry?.isBufferGeometry) { (instance as TresObject3D).userData.tres__geometryViaProp = true } } @@ -90,6 +98,7 @@ export const api = { return instance }, + // @ts-expect-error append(parent, child) { if (parent && parent.isScene) { scene = parent as unknown as TresScene } @@ -125,6 +134,7 @@ export const api = { } } }, + // @ts-expect-error destroy(node) { if (!node) { return } // remove is only called on the node being removed and not on child nodes. @@ -184,6 +194,7 @@ export const api = { node.dispose?.() }, + // @ts-expect-error prop(node, prop, nextValue) { if (node) { let root = node @@ -222,9 +233,11 @@ export const api = { // Traverse pierced props (e.g. foo-bar=value => foo.bar = value) if (key.includes('-') && target === undefined) { const chain = key.split('-') + // @ts-expect-error target = chain.reduce((acc, key) => acc[kebabToCamel(key)], root) key = chain.pop() as string finalKey = key.toLowerCase() + // @ts-expect-error if (!target?.set) { root = chain.reduce((acc, key) => acc[kebabToCamel(key)], root) } } let value = nextValue @@ -245,6 +258,7 @@ export const api = { else { target.set(value) } } }, + // @ts-expect-error parentNode(node) { return node?.parent || null } diff --git a/src/utils/renderers/tres/utils.ts b/src/utils/renderers/tres/utils.ts index 11c233f..7bbfe5a 100644 --- a/src/utils/renderers/tres/utils.ts +++ b/src/utils/renderers/tres/utils.ts @@ -247,6 +247,7 @@ export function createHighlightMesh(object: Object3D): Mesh { }) // Clone the geometry of the object. You might need a more complex approach // if the object's geometry is not straightforward. + // @ts-expect-error const highlightMesh = new HightlightMesh(object.geometry.clone(), highlightMaterial) return highlightMesh From eba412717177da97c992ef5e98fb04b40a328487 Mon Sep 17 00:00:00 2001 From: Alex Kanunnikov Date: Sun, 5 May 2024 00:21:31 +0300 Subject: [PATCH 5/7] + --- src/utils/control-flow/list.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/utils/control-flow/list.ts b/src/utils/control-flow/list.ts index 0ee58e9..7671e30 100644 --- a/src/utils/control-flow/list.ts +++ b/src/utils/control-flow/list.ts @@ -221,11 +221,13 @@ export class BasicListComponent { ? this.getTargetNode(amountOfExistingKeys) : bottomMarker; let seenKeys = 0; + let targetParent = api.parentNode(targetNode)!; items.forEach((item, index) => { // @todo - fix here if (seenKeys === amountOfExistingKeys && targetNode === bottomMarker) { // optimization for appending items case targetNode = this.getTargetNode(0); + targetParent = api.parentNode(targetNode)!; } const key = keyForItem(item); const maybeRow = keyMap.get(key); @@ -248,9 +250,8 @@ export class BasicListComponent { const row = ItemComponent(item, idx, this as unknown as Component); keyMap.set(key, row); indexMap.set(key, index); - const parentNode = api.parentNode(targetNode)!; row.forEach((item) => { - renderElement(parentNode, item, targetNode); + renderElement(targetParent, item, targetNode); }); } else { seenKeys++; From c9375d023ba64913d77f42d03476c12c01b97a2d Mon Sep 17 00:00:00 2001 From: Alex Kanunnikov Date: Sun, 5 May 2024 00:24:04 +0300 Subject: [PATCH 6/7] + --- src/utils/dom.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/dom.ts b/src/utils/dom.ts index 3ace5bf..39c9de8 100644 --- a/src/utils/dom.ts +++ b/src/utils/dom.ts @@ -854,7 +854,6 @@ function text( } else if (isPrimitive(result)) { return api.text(result); } else { - // @ts-expect-error return cellToText(typeof text === 'function' ? result : text, destructors); } } From da634be78fee534a9106808b65be9a5d4516b1b5 Mon Sep 17 00:00:00 2001 From: Alex Kanunnikov Date: Sun, 5 May 2024 00:29:44 +0300 Subject: [PATCH 7/7] + --- src/utils/component.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utils/component.ts b/src/utils/component.ts index b9ee081..ccd55f8 100644 --- a/src/utils/component.ts +++ b/src/utils/component.ts @@ -234,7 +234,9 @@ export async function destroyElement( if (component.ctx) { const destructors: Array> = []; runDestructors(component.ctx, destructors); - await Promise.all(destructors); + if (destructors.length) { + await Promise.all(destructors); + } } try { destroyNodes(component[$nodes]);