diff --git a/docs/examples/text-3d.md b/docs/examples/text-3d.md index 422c53d1c..0f806c789 100644 --- a/docs/examples/text-3d.md +++ b/docs/examples/text-3d.md @@ -95,7 +95,7 @@ So the final code would be something like this: diff --git a/packages/tres/src/components/Shapes.vue b/packages/tres/src/components/Shapes.vue deleted file mode 100644 index dedeb6511..000000000 --- a/packages/tres/src/components/Shapes.vue +++ /dev/null @@ -1,133 +0,0 @@ - - - diff --git a/packages/tres/src/components/TestSphere.vue b/packages/tres/src/components/TestSphere.vue deleted file mode 100644 index 327de11d8..000000000 --- a/packages/tres/src/components/TestSphere.vue +++ /dev/null @@ -1,30 +0,0 @@ - - diff --git a/packages/tres/src/components/TresCanvas.ts b/packages/tres/src/components/TresCanvas.ts new file mode 100644 index 000000000..ee2db2d69 --- /dev/null +++ b/packages/tres/src/components/TresCanvas.ts @@ -0,0 +1,125 @@ +import { defineComponent, h, PropType, ref, watch } from 'vue' +import * as THREE from 'three' +import { ShadowMapType, TextureEncoding, ToneMapping } from 'three' +import { createTres } from '/@/core/renderer' +import { useLogger } from '/@/composables' +import { useCamera, useRenderer, useRenderLoop, useRaycaster, useTres } from '/@/composables' +import { TresObject } from '../types' +import { extend } from '../core/catalogue' +import { RendererPresetsType } from '../composables/useRenderer/const' + +export interface TresCanvasProps { + shadows?: boolean + shadowMapType?: ShadowMapType + physicallyCorrectLights?: boolean + useLegacyLights?: boolean + outputEncoding?: TextureEncoding + toneMapping?: ToneMapping + toneMappingExposure?: number + context?: WebGLRenderingContext + powerPreference?: 'high-performance' | 'low-power' | 'default' + preserveDrawingBuffer?: boolean + clearColor?: string + windowSize?: boolean + preset?: RendererPresetsType +} +/** + * Vue component for rendering a Tres component. + */ + +const { logWarning } = useLogger() + +export const TresCanvas = defineComponent({ + name: 'TresCanvas', + props: [ + 'shadows', + 'shadowMapType', + 'physicallyCorrectLights', + 'useLegacyLights', + 'outputEncoding', + 'toneMapping', + 'toneMappingExposure', + 'context', + 'powerPreference', + 'preserveDrawingBuffer', + 'clearColor', + 'windowSize', + 'preset', + ] as unknown as undefined, + setup(props, { slots, expose }) { + if (props.physicallyCorrectLights === true) { + logWarning('physicallyCorrectLights is deprecated, useLegacyLights is now false by default') + } + const container = ref() + const canvas = ref() + const scene = new THREE.Scene() + + watch(canvas, () => { + const { renderer } = useRenderer(canvas, container, props) + const { activeCamera } = useCamera() + + const { onLoop } = useRenderLoop() + + const { raycaster, pointer } = useRaycaster() + + onLoop(() => { + if (!activeCamera.value) return + + raycaster.value.setFromCamera(pointer.value, activeCamera.value) + renderer.value?.render(scene, activeCamera.value) + }) + }) + + const app = createTres(slots) + app.provide('useTres', useTres()) + app.provide('extend', extend) + app.mount(scene as unknown as TresObject) + expose({ + scene, + }) + + return () => { + return h( + h( + 'div', + { + ref: container, + style: { + position: 'relative', + width: '100%', + height: '100%', + pointerEvents: 'auto', + touchAction: 'none', + }, + }, + [ + h( + 'div', + { + style: { + width: '100%', + height: '100%', + }, + }, + [ + h('canvas', { + ref: canvas, + style: { + display: 'block', + width: '100%', + height: '100%', + position: props.windowSize ? 'fixed' : 'absolute', + top: 0, + left: 0, + }, + }), + ], + ), + ], + ), + ) + } + }, +}) + +export default TresCanvas diff --git a/packages/tres/src/composables/index.ts b/packages/tres/src/composables/index.ts index 93a88d834..c6c2d8ffa 100644 --- a/packages/tres/src/composables/index.ts +++ b/packages/tres/src/composables/index.ts @@ -1 +1,8 @@ +export * from './useCamera' +export * from './useRenderLoop/' +export * from './useRenderer/' +export * from './useLoader' +export * from './useTexture' +export * from './useTres' +export * from './useRaycaster' export * from './useLogger' diff --git a/packages/tres/src/core/useCamera/index.ts b/packages/tres/src/composables/useCamera/index.ts similarity index 94% rename from packages/tres/src/core/useCamera/index.ts rename to packages/tres/src/composables/useCamera/index.ts index 2c18232e5..fe1238fff 100644 --- a/packages/tres/src/core/useCamera/index.ts +++ b/packages/tres/src/composables/useCamera/index.ts @@ -1,7 +1,7 @@ -import { useTres } from '/@/core/' +import { useTres } from '/@/composables/' import { PerspectiveCamera, OrthographicCamera } from 'three' -import { toRef, watch, Ref, inject } from 'vue' +import { toRef, Ref, watchEffect } from 'vue' export enum CameraType { Perspective = 'Perspective', @@ -104,8 +104,8 @@ let camera: Camera * @return {*} {UseCameraReturn} */ export function useCamera(): UseCameraReturn { - const { state, setState } = useTres() - const aspectRatio = inject('aspect-ratio') + const { state, setState, aspectRatio } = useTres() + /* const aspectRatio = inject('aspect-ratio') */ /** * Create camera and push to Tres `state.cameras` array * @@ -182,9 +182,12 @@ export function useCamera(): UseCameraReturn { function clearCameras() { state.cameras = [] } - if (aspectRatio) { - watch(aspectRatio, updateCamera) - } + + watchEffect(() => { + if (aspectRatio?.value) { + updateCamera() + } + }) return { activeCamera: toRef(state, 'camera'), diff --git a/packages/tres/src/core/useCamera/useCamera.test.ts b/packages/tres/src/composables/useCamera/useCamera.test.ts similarity index 100% rename from packages/tres/src/core/useCamera/useCamera.test.ts rename to packages/tres/src/composables/useCamera/useCamera.test.ts diff --git a/packages/tres/src/core/useLoader/index.ts b/packages/tres/src/composables/useLoader/index.ts similarity index 100% rename from packages/tres/src/core/useLoader/index.ts rename to packages/tres/src/composables/useLoader/index.ts diff --git a/packages/tres/src/core/useLoader/useLoader.test.ts b/packages/tres/src/composables/useLoader/useLoader.test.ts similarity index 100% rename from packages/tres/src/core/useLoader/useLoader.test.ts rename to packages/tres/src/composables/useLoader/useLoader.test.ts diff --git a/packages/tres/src/core/useRaycaster/index.ts b/packages/tres/src/composables/useRaycaster/index.ts similarity index 84% rename from packages/tres/src/core/useRaycaster/index.ts rename to packages/tres/src/composables/useRaycaster/index.ts index d3d7f31c9..05a146832 100644 --- a/packages/tres/src/core/useRaycaster/index.ts +++ b/packages/tres/src/composables/useRaycaster/index.ts @@ -1,6 +1,6 @@ import { Raycaster, Vector2 } from 'three' -import { onUnmounted, provide, Ref, ref, ShallowRef, shallowRef } from 'vue' -import { useTres } from '/@/core' +import { Ref, ref, ShallowRef, shallowRef } from 'vue' +import { useTres } from '/@/composables' const raycaster = shallowRef(new Raycaster()) const pointer = ref(new Vector2()) @@ -42,10 +42,6 @@ export function useRaycaster(): UseRaycasterReturn { setState('pointer', pointer) setState('currentInstance', currentInstance) - provide('raycaster', raycaster) - provide('pointer', pointer) - provide('currentInstance', currentInstance) - function onPointerMove(event: MouseEvent) { pointer.value.x = (event.clientX / window.innerWidth) * 2 - 1 pointer.value.y = -(event.clientY / window.innerHeight) * 2 + 1 @@ -53,9 +49,9 @@ export function useRaycaster(): UseRaycasterReturn { window.addEventListener('pointermove', onPointerMove) - onUnmounted(() => { + /* onUnmounted(() => { window.removeEventListener('pointermove', onPointerMove) - }) + }) */ return { raycaster, pointer, diff --git a/packages/tres/src/core/useRaycaster/useRaycaster.test.ts b/packages/tres/src/composables/useRaycaster/useRaycaster.test.ts similarity index 100% rename from packages/tres/src/core/useRaycaster/useRaycaster.test.ts rename to packages/tres/src/composables/useRaycaster/useRaycaster.test.ts diff --git a/packages/tres/src/core/useRenderLoop/index.ts b/packages/tres/src/composables/useRenderLoop/index.ts similarity index 100% rename from packages/tres/src/core/useRenderLoop/index.ts rename to packages/tres/src/composables/useRenderLoop/index.ts diff --git a/packages/tres/src/core/useRenderer/const.ts b/packages/tres/src/composables/useRenderer/const.ts similarity index 100% rename from packages/tres/src/core/useRenderer/const.ts rename to packages/tres/src/composables/useRenderer/const.ts diff --git a/packages/tres/src/core/useRenderer/index.ts b/packages/tres/src/composables/useRenderer/index.ts similarity index 99% rename from packages/tres/src/core/useRenderer/index.ts rename to packages/tres/src/composables/useRenderer/index.ts index a2e5b28a7..8d463ee05 100644 --- a/packages/tres/src/core/useRenderer/index.ts +++ b/packages/tres/src/composables/useRenderer/index.ts @@ -19,7 +19,7 @@ import { Clock, } from 'three' import type { TextureEncoding, ToneMapping } from 'three' -import { useRenderLoop, useTres } from '/@/core/' +import { useRenderLoop, useTres } from '/@/composables/' import { normalizeColor } from '/@/utils/normalize' import { TresColor } from '/@/types' import { rendererPresets, RendererPresetsType } from './const' diff --git a/packages/tres/src/core/useTexture/index.ts b/packages/tres/src/composables/useTexture/index.ts similarity index 100% rename from packages/tres/src/core/useTexture/index.ts rename to packages/tres/src/composables/useTexture/index.ts diff --git a/packages/tres/src/core/useTexture/useTexture.test.ts b/packages/tres/src/composables/useTexture/useTexture.test.ts similarity index 100% rename from packages/tres/src/core/useTexture/useTexture.test.ts rename to packages/tres/src/composables/useTexture/useTexture.test.ts diff --git a/packages/tres/src/core/useTres/index.ts b/packages/tres/src/composables/useTres/index.ts similarity index 98% rename from packages/tres/src/core/useTres/index.ts rename to packages/tres/src/composables/useTres/index.ts index 22dcee14e..2fc05b117 100644 --- a/packages/tres/src/core/useTres/index.ts +++ b/packages/tres/src/composables/useTres/index.ts @@ -1,6 +1,6 @@ import { Clock, EventDispatcher, Raycaster, Scene, Vector2, WebGLRenderer } from 'three' import { computed, ComputedRef, shallowReactive, toRefs } from 'vue' -import { Camera } from '/@/core' +import { Camera } from '/@/composables' export interface TresState { /** diff --git a/packages/tres/src/core/useTres/useTres.test.ts b/packages/tres/src/composables/useTres/useTres.test.ts similarity index 100% rename from packages/tres/src/core/useTres/useTres.test.ts rename to packages/tres/src/composables/useTres/useTres.test.ts diff --git a/packages/tres/src/core/catalogue.test.ts b/packages/tres/src/core/catalogue.test.ts new file mode 100644 index 000000000..242de926d --- /dev/null +++ b/packages/tres/src/core/catalogue.test.ts @@ -0,0 +1,15 @@ +import { useTres } from '.' +import { catalogue, extend } from './catalogue' +import * as THREE from 'three' + +describe('catalog', () => { + it('should return a autogenerated uuid', () => { + expect(catalogue.value.uuid).toBeDefined() + }) + it('should return a catalog of objects when extended', () => { + extend(THREE) + + expect(catalogue.value).toHaveProperty('Mesh') + expect(catalogue.value).toHaveProperty('MeshBasicMaterial') + }) +}) diff --git a/packages/tres/src/core/catalogue.ts b/packages/tres/src/core/catalogue.ts new file mode 100644 index 000000000..bc62f45d1 --- /dev/null +++ b/packages/tres/src/core/catalogue.ts @@ -0,0 +1,9 @@ +import { MathUtils } from 'three' +import { Ref, ref } from 'vue' +import { TresCatalogue } from '../types' + +export const catalogue: Ref = ref({ uuid: MathUtils.generateUUID() }) + +export const extend = (objects: any) => void Object.assign(catalogue.value, objects) + +export default { catalogue, extend } diff --git a/packages/tres/src/core/index.ts b/packages/tres/src/core/index.ts deleted file mode 100644 index c2df1a6ba..000000000 --- a/packages/tres/src/core/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from './useCamera' -export * from './useCatalogue' -export * from './useInstanceCreator' -export * from './useRenderLoop/' -export * from './useRenderer/' -export * from './useScene/' -export * from './useLoader' -export * from './useTexture' -export * from './useTres' -export * from './useRaycaster' diff --git a/packages/tres/src/core/nodeOps.ts b/packages/tres/src/core/nodeOps.ts new file mode 100644 index 000000000..45ef51c0d --- /dev/null +++ b/packages/tres/src/core/nodeOps.ts @@ -0,0 +1,175 @@ +import { Mesh } from 'three' +import { useCamera, useRaycaster, useRenderLoop, useLogger } from '/@/composables' +import { RendererOptions } from 'vue' +import { catalogue } from './catalogue' +import { isFunction, useEventListener } from '@vueuse/core' +import { TresEvent, TresObject } from '../types' +import { isHTMLTag, kebabToCamel } from '../utils' + +const { logWarning } = useLogger() + +function hasEvents(obj: any) { + for (const prop in obj) { + if (prop.indexOf('on') === 0) { + return true + } + } + return false +} + +function noop(fn: string): any { + fn +} + +let scene: TresObject | null = null + +export const nodeOps: RendererOptions = { + createElement(tag, _isSVG, _anchor, props) { + if (tag === 'template') return null + if (isHTMLTag(tag)) return null + let instance + + if (props === null) props = {} + + if (props?.args) { + instance = new catalogue.value[tag.replace('Tres', '')](...props.args) + } else { + instance = new catalogue.value[tag.replace('Tres', '')]() + } + + if (instance.isCamera) { + // Let users know that camera is in the center of the scene + if (!props?.position || props?.position.every((v: number) => v == 0)) { + logWarning( + // eslint-disable-next-line max-len + 'Camera is positioned at the center of the scene [0,0,0], if this is not intentional try setting a position if your scene seems empty 🤗', + ) + } + const { pushCamera } = useCamera() + pushCamera(instance) + } + + if (props?.attach === undefined) { + if (instance.isMaterial) instance.attach = 'material' + else if (instance.isBufferGeometry) instance.attach = 'geometry' + } + + return instance + }, + insert(child, parent, anchor) { + if (scene === null && parent.isScene) scene = parent + if (parent === null) parent = scene as TresObject + //vue core + /* parent.insertBefore(child, anchor || null) */ + if (parent?.isObject3D && child?.isObject3D) { + const index = anchor ? parent.children.indexOf(anchor) : 0 + child.parent = parent + parent.children.splice(index, 0, child) + child.dispatchEvent({ type: 'added' }) + } else if (typeof child?.attach === 'string') { + child.__previousAttach = child[parent?.attach] + if (parent) { + parent[child.attach] = child + } + } + + const { onLoop } = useRenderLoop() + + // RayCasting + let prevInstance: TresEvent | null = null + let currentInstance: TresEvent | null = null + + const { raycaster } = useRaycaster() + if (child && child instanceof Mesh && hasEvents(child)) { + onLoop(() => { + if (parent?.children && child && raycaster) { + const intersects = raycaster.value.intersectObjects(parent.children) + + if (intersects.length > 0 && intersects[0].object.uuid === child.uuid) { + currentInstance = intersects[0] + + if (prevInstance === null || prevInstance.object.uuid !== currentInstance?.object.uuid) { + child.onPointerEnter?.(currentInstance) + } + + child.onPointerMove?.(currentInstance) + } else { + currentInstance = null + if (prevInstance !== null) { + child.onPointerLeave?.(prevInstance) + } + } + + prevInstance = currentInstance + } + }) + + useEventListener(window, 'click', () => { + if (currentInstance === null) return + child.onClick?.(currentInstance) + }) + } + }, + remove(node) { + if (!node) return + const parent = node.parentNode + if (parent) { + parent.removeChild(node) + } + }, + patchProp(node, prop, _prevValue, nextValue) { + if (node) { + /* if (node.isCamera && prop === 'look-at') { + debugger + } */ + let root = node + let key = prop + const camelKey = kebabToCamel(key) + let target = root?.[camelKey] + + if (!node.parent) { + node.parent = scene as TresObject + } + + // 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 + + 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)) { + /* if (Array.isArray(value)) target(...value) + else target(value) */ + return + } + if (!target?.set && !isFunction(target)) root[camelKey] = 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'), +} diff --git a/packages/tres/src/core/nodeOpts.test.ts b/packages/tres/src/core/nodeOpts.test.ts new file mode 100644 index 000000000..7ae4e4912 --- /dev/null +++ b/packages/tres/src/core/nodeOpts.test.ts @@ -0,0 +1,174 @@ +import * as THREE from 'three' +import { nodeOps } from './nodeOps' +import { TresObject } from '../types' +import { extend } from './catalogue' +import { Mesh, Scene } from 'three' + +describe('nodeOps', () => { + beforeAll(() => { + // Setup + extend(THREE) + }) + it('createElement should create an instance with given tag', async () => { + // Setup + const tag = 'TresMesh' + const props = { args: [] } + + // Test + const instance = nodeOps.createElement(tag, false, null, props) + + // Assert + expect(instance.isObject3D).toBeTruthy() + expect(instance).toBeInstanceOf(Mesh) + }) + + it('createElement should create an instance with given tag and props', async () => { + // Setup + const tag = 'TresTorusGeometry' + const props = { args: [10, 3, 16, 100] } + + // Test + const instance = nodeOps.createElement(tag, false, null, props) + + // Assert + expect(instance.parameters.radius).toBe(10) + expect(instance.parameters.tube).toBe(3) + expect(instance.parameters.radialSegments).toBe(16) + expect(instance.parameters.tubularSegments).toBe(100) + }) + + it('createElement should create an camera instance', async () => { + // Setup + const tag = 'TresPerspectiveCamera' + const props = { args: [75, 2, 0.1, 5] } + + // Test + const instance = nodeOps.createElement(tag, false, null, props) + + // Assert + expect(instance.isCamera).toBeTruthy() + expect(instance).toBeInstanceOf(THREE.PerspectiveCamera) + }) + + it('createElement should log a warning if the camera doesnt have a position', async () => { + // Setup + const tag = 'TresPerspectiveCamera' + const props = { args: [75, 2, 0.1, 5] } + + // Spy + const consoleWarnSpy = vi.spyOn(console, 'warn') + consoleWarnSpy.mockImplementation(() => {}) + + // Test + const instance = nodeOps.createElement(tag, false, null, props) + + // Assert + expect(instance.isCamera).toBeTruthy() + expect(instance).toBeInstanceOf(THREE.PerspectiveCamera) + expect(consoleWarnSpy).toHaveBeenCalled() + }) + + it('createElement should add attach material propety if instance is a material', () => { + // Setup + const tag = 'TresMeshStandardMaterial' + const props = { args: [] } + + // Test + const instance = nodeOps.createElement(tag, false, null, props) + + // Assert + expect(instance.isMaterial).toBeTruthy() + expect(instance.attach).toBe('material') + }) + + it('createElement should add attach geometry propety if instance is a geometry', () => { + // Setup + const tag = 'TresTorusGeometry' + const props = { args: [] } + + // Test + const instance = nodeOps.createElement(tag, false, null, props) + + // Assert + expect(instance.isBufferGeometry).toBeTruthy() + expect(instance.attach).toBe('geometry') + }) + + it('insert should insert child into parent', async () => { + // Setup + const parent: TresObject = new Scene() + const child: TresObject = new Mesh() + + // Test + nodeOps.insert(child, parent, null) + + // Assert + expect(parent.children.includes(child)).toBeTruthy() + }) + + it('remove: removes child from parent', async () => { + // Setup + const parent: TresObject = new Scene() + const child: TresObject = new Mesh() + parent.children.push(child) + + // Test + nodeOps.remove(child) + + // Assert + expect(!parent.children.includes(child)) + }) + + it('patchProp should patch property of node', async () => { + // Setup + const node: TresObject = new Mesh() + const prop = 'visible' + const nextValue = false + + // Test + nodeOps.patchProp(node, prop, null, nextValue) + + // Assert + expect(node.visible === nextValue) + }) + + it('patchProp should patch traverse pierced props', async () => { + // Setup + const node: TresObject = new Mesh() + const prop = 'position-x' + const nextValue = 5 + + // Test + nodeOps.patchProp(node, prop, null, nextValue) + + // Assert + expect(node.position.x === nextValue) + }) + + it('patchProp it should not patch traverse pierced props of existing dashed properties', async () => { + // Setup + const node: TresObject = new Mesh() + const prop = 'cast-shadow' + const nextValue = true + + // Test + nodeOps.patchProp(node, prop, null, nextValue) + + // Assert + expect(node.castShadow === nextValue) + }) + + it('parentNode: returns parent of a node', async () => { + // Setup + const parent: TresObject = new Scene() + const child: TresObject = new Mesh() + parent.children.push(child) + child.parent = parent + + // Test + const parentNode = nodeOps.parentNode(child) + + // Assert + expect(parentNode === parent) + }) +}) diff --git a/packages/tres/src/core/renderer.ts b/packages/tres/src/core/renderer.ts new file mode 100644 index 000000000..9e9893fe1 --- /dev/null +++ b/packages/tres/src/core/renderer.ts @@ -0,0 +1,19 @@ +import * as THREE from 'three' + +import { createRenderer, Slots } from 'vue' +import { extend } from './catalogue' +import { nodeOps } from './nodeOps' + +export const { createApp } = createRenderer(nodeOps) + +export const createTres = (slots: Slots) => { + const app = createApp(internalFnComponent) + function internalFnComponent() { + return slots && slots.default ? slots.default() : [] + } + return app +} + +extend(THREE) + +export default { createTres, extend } diff --git a/packages/tres/src/core/useCatalogue/index.ts b/packages/tres/src/core/useCatalogue/index.ts deleted file mode 100644 index dd8463397..000000000 --- a/packages/tres/src/core/useCatalogue/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { App, ref, Component, Ref } from 'vue' -import * as THREE from 'three' -import { useInstanceCreator } from '/@/core' -import { useLogger } from '/@/composables' -import { TresCatalogue } from '/@/types' - -const catalogue: Ref = ref({ ...THREE, uuid: THREE.MathUtils.generateUUID() }) - -delete catalogue.value.Scene - -let localApp: App - -/** - * State for the catalogue of THREE objects - * - * ```ts - * const { catalogue } = useCatalogue() - * - * console.log(catalogue.value.Mesh) // Mesh - * ``` - * - * @export - * @param {App} [app] - * @param {string} [prefix='Tres'] - * @return {*} - */ -export function useCatalogue(app?: App, prefix = 'Tres') { - const { logError } = useLogger() - if (!localApp && app) { - localApp = app - } - const { createComponentInstances } = useInstanceCreator(prefix) - - /** - * Extend the catalogue with new THREE objects - * - * ```ts - * const { catalog, extend } = useCatalogue() - * - * extend({ MyObject: { foo: 'bar' } }) - * - * console.log(catalog.value.MyObject.foo) // bar - * ``` - * - * @param {*} objects - */ - const extend = (objects: any) => { - if (!objects) { - logError('No objects provided to extend catalogue') - return - } - catalogue.value = Object.assign(catalogue.value, objects) - const components = createComponentInstances(ref(objects)) - - if (localApp) { - components.forEach(([key, cmp]) => { - // If the component is not already registered, register it - if (!localApp._context.components[key as string]) { - localApp.component(key as string, cmp as Component) - } - }) - } - } - - return { - extend, - catalogue, - } -} diff --git a/packages/tres/src/core/useCatalogue/useCatalogue.test.ts b/packages/tres/src/core/useCatalogue/useCatalogue.test.ts deleted file mode 100644 index de557ba50..000000000 --- a/packages/tres/src/core/useCatalogue/useCatalogue.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { createApp } from 'vue' -import { withSetup } from '/@/utils/test-utils' -import { useCatalogue } from './' -const [composable, app] = withSetup(() => useCatalogue()) - -describe('useCatalogue', () => { - it('should fill the catalogue with THREE objects', () => { - const { catalogue } = composable - - expect(catalogue.value).toHaveProperty('Mesh') - expect(catalogue.value).toHaveProperty('MeshBasicMaterial') - }) - it('should skip Scene object', () => { - const { catalogue } = composable - - expect(catalogue.value).not.toHaveProperty('Scene') - }) - it('should extend the catalogue with objects', () => { - const app = createApp({}) - const { extend, catalogue } = useCatalogue(app) - - extend({ MyObject: { foo: 'bar' } }) - - expect(catalogue.value.MyObject.foo).toEqual('bar') - }) - - // TODO: find a way to mock createComponentInstances to test the component registration -}) diff --git a/packages/tres/src/core/useInstanceCreator/index.ts b/packages/tres/src/core/useInstanceCreator/index.ts deleted file mode 100644 index 6eda00bfc..000000000 --- a/packages/tres/src/core/useInstanceCreator/index.ts +++ /dev/null @@ -1,348 +0,0 @@ -/* eslint-disable new-cap */ -/* eslint-disable @typescript-eslint/no-empty-function */ -import { BufferAttribute, Fog, FogBase, Mesh, OrthographicCamera, PerspectiveCamera } from 'three' -import { defineComponent, inject, onUnmounted, Ref } from 'vue' -import { useEventListener } from '@vueuse/core' - -import { isArray, isDefined, isFunction } from '@alvarosabu/utils' -import { normalizeVectorFlexibleParam } from '/@/utils/normalize' -import { useCamera, useCatalogue, useRenderLoop, useTres } from '/@/core/' -import { useLogger } from '/@/composables' -import { TresAttributes, TresCatalogue, TresInstance, TresVNode, TresVNodeType, TresEvent } from '/@/types' - -const VECTOR3_PROPS = ['rotation', 'scale', 'position'] -const VECTOR3_AXIS = ['X', 'Y', 'Z'] -const COLOR_PROPS = ['color'] -const COLOR_KEYS = ['r', 'g', 'b'] - -/** - * Composable responsible for creating instances out of Three.js objects. - * - * @export - * @param {string} prefix - * @return {*} - */ -export function useInstanceCreator(prefix: string) { - const { /* logMessage, */ logError } = useLogger() - - /** - * Process props to `.setAttribute` on instance. - * - * @example `position` prop will be converted to `setPosition` method call. - * - * @param {Record} props - * @param {TresInstance} instance - */ - function processSetAttributes(props: Record, instance: TresInstance) { - if (!isDefined(props)) return - if (!isDefined(instance)) return - - Object.entries(props).forEach(([key, value]) => { - const camelKey = key.replace(/(-\w)/g, m => m[1].toUpperCase()) - instance.setAttribute(camelKey, new BufferAttribute(...(value as ConstructorParameters))) - }) - } - - /** - * Process props to set properties on instance. - * - * It will also normalize vector3 props and check if the instances property has a `set` method. - * If it does, it will call the `set` method with the value, spread if it's an array. - * - * @example `position=[0,0,0]` prop will be converted to `instance.position.set(0,0,0)` property. - * - * @param {Record} props - * @param {TresInstance} instance - */ - function processProps(props: Record, instance: TresInstance) { - if (!isDefined(props)) return - if (!isDefined(instance)) return - - Object.entries(props).forEach(([key, value]) => { - const camelKey = key.replace(/(-\w)/g, m => m[1].toUpperCase()) - let transformProps - let transformAxis - let colorProps - let colorKey - // Ignore property args which is use for initial instance construction - if (camelKey === 'args' || value === undefined) return - - // Normalize vector3 props - if (VECTOR3_PROPS.includes(camelKey) && value) { - value = normalizeVectorFlexibleParam(value) - } else { - VECTOR3_PROPS.forEach(vecProps => { - // Check if the props starts with one of the transform props - // and is followed only with one of the axis - if (camelKey.startsWith(vecProps) && camelKey.length === vecProps.length + 1) { - transformProps = vecProps - transformAxis = camelKey.substring(vecProps.length) - if (!VECTOR3_AXIS.includes(transformAxis)) { - logError( - // eslint-disable-next-line max-len - `There was an error setting ${key} property, ${transformAxis} is not a valid axis for ${transformProps}`, - ) - } - } - }) - } - COLOR_PROPS.forEach(props => { - // Check if the props starts with one of the color props - // and is followed only with one of the key - if (camelKey.startsWith(props) && camelKey.length === props.length + 1) { - colorProps = props - colorKey = camelKey.substring(props.length).toLowerCase() - if (!COLOR_KEYS.includes(colorKey)) { - logError(`There was an error setting ${key} property , ${colorKey} is not a valid axis for ${colorProps}`) - } - } - }) - - if (props.ref) { - props.ref = instance - } - - try { - // Check if the property has a "set" method - if (instance[camelKey] && isDefined(instance[camelKey].set)) { - // Call the "set" method with the value, spread if it's an array - instance[camelKey].set(...(isArray(value) ? value : [value])) - } else if ( - // Check if the property has a "setAxis" method - transformProps && - instance[transformProps] - ) { - // Check if setAxis function exist - // if it doesn't check if props is rotation - if (isDefined(instance[transformProps][`set${transformAxis}`])) { - instance[transformProps][`set${transformAxis}`](value) - } else if (isDefined(instance[`rotate${transformAxis}`])) { - instance[`rotate${transformAxis}`](value) - } - } else if ( - // Check if the instance has a "color" property - colorProps && - colorKey && - instance[colorProps] && - instance[colorProps][colorKey] - ) { - instance[colorProps][colorKey] = value - } else { - // Convert empty strings to `true` - if (value === '') { - value = true - } - - // Check if the property is a function - if (isFunction(instance[camelKey])) { - if (key === 'center' && !value) return - // Call the function with the value, spread if it's an array - instance[camelKey](...(isArray(value) ? value : [value])) - return - } - - // Set the property to the value - instance[camelKey] = value - } - } catch (error: unknown) { - logError(`There was an error setting ${camelKey} property`, error as Error) - } - }) - } - - /** - * Proccess slots to add children to instance. - * - * @param {TresVNode} vnode - * @return {*} {(TresInstance | TresInstance[] | undefined)} - */ - function createInstanceFromVNode(vnode: TresVNode): TresInstance | TresInstance[] | undefined { - const fragmentRegex = /^Symbol\(Fragment\)$/g - const textRegex = /^Symbol\(Text\)$/g - const commentRegex = /^Symbol\(Comment\)$/g - // Check if the vnode is a Fragment - if (fragmentRegex.test(vnode.type.toString())) { - return vnode.children.map(child => createInstanceFromVNode(child as TresVNode)) as TresInstance[] - } else if (textRegex.test(vnode.type.toString()) || commentRegex.test(vnode.type.toString())) { - return - } else { - const vNodeType = ((vnode.type as TresVNodeType).name as string).replace(prefix, '') - - const catalogue = inject>('catalogue') - - // check if args prop is defined on the vnode - let internalInstance - if (catalogue) { - if ((vnode.children as unknown as { default: any })?.default) { - const internal = (vnode.children as unknown as { default: any }) - .default() - .map((child: TresVNode) => createInstanceFromVNode(child)) as TresInstance[] - - internalInstance = new catalogue.value[vNodeType](...internal.flat().filter(Boolean)) - } else if (vnode?.props?.args) { - // if args prop is defined, create new instance of catalogue[vNodeType] with the provided arguments - if (catalogue?.value[vNodeType]) { - internalInstance = new catalogue.value[vNodeType](...vnode.props.args) - } else { - logError(`There is no ${vNodeType} in the catalogue`, catalogue?.value.uuid) - } - } else { - // if args prop is not defined, create a new instance of catalogue[vNodeType] without arguments - internalInstance = new catalogue.value[vNodeType]() - } - } - - // check if props is defined on the vnode - if (vnode?.props) { - // if props is defined, process the props and pass the internalInstance to update its properties - if (vNodeType === 'BufferGeometry') { - processSetAttributes(vnode.props, internalInstance) - } else { - processProps(vnode.props, internalInstance) - } - } - return internalInstance - } - } - - /** - * Create a new instance of a ThreeJS object based on the component attrs and slots. - * - * Checks if the component has slots, - * if it does, it will create a new Object3D instance passing the slots instances as properties - * Example: - * - * ```vue - * - * - * - * - * ``` - * - * will create a new Mesh instance with a BoxGeometry and a MeshBasicMaterial - * const mesh = new Mesh(new BoxGeometry(), new MeshBasicMaterial()) - * - * @param {*} threeObj - * @param {TresAttributes} attrs - * @param {Record} slots - * @return {*} {TresInstance} - */ - function createInstance(threeObj: any, attrs: TresAttributes, slots: Record): TresInstance { - if (slots.default && slots?.default()) { - const internal = slots.default().map((vnode: TresVNode) => createInstanceFromVNode(vnode)) - if (threeObj.name === 'Group') { - const group = new threeObj() - internal.forEach((child: TresInstance) => { - group.add(child) - }) - return group - } else { - return new threeObj(...internal.flat().filter(Boolean)) - } - } else { - // Creates a new THREE instance, if args is present, spread it on the constructor - return attrs.args ? new threeObj(...attrs.args) : new threeObj() - } - } - - /** - * Creates a new component instance for each object in the catalogue - * - * @param {Ref} catalogue - * @return {*} - */ - function createComponentInstances(catalogue: Ref) { - return ( - Object.entries(catalogue.value) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - .filter(([_key, value]) => (value as { prototype: any })?.prototype?.constructor?.toString().includes('class')) - .map(([key, threeObj]) => { - const name = `${prefix}${key}` - const cmp = defineComponent({ - name, - setup(_props, { slots, attrs, ...ctx }) { - const { state } = useTres() - const { onLoop } = useRenderLoop() - const scene = state.scene - const raycaster = state.raycaster - - let instance = createInstance(threeObj, attrs, slots) - processProps(attrs, instance) - // If the instance is a camera, push it to the camera stack - if (instance instanceof PerspectiveCamera || instance instanceof OrthographicCamera) { - const { pushCamera } = useCamera() - pushCamera(instance) - } - - // If the instance is a valid Object3D, add it to the scene - if (instance.isObject3D) { - scene?.add(instance) - } - - let prevInstance: TresEvent | null = null - let currentInstance: TresEvent | null = null - if (instance instanceof Mesh) { - onLoop(() => { - if (instance && raycaster && scene?.children) { - const intersects = raycaster.intersectObjects(scene?.children) - - if (intersects.length > 0) { - currentInstance = intersects[0] - - if (prevInstance === null || prevInstance.object.uuid !== currentInstance?.object.uuid) { - ctx.emit('pointer-enter', currentInstance) - } - - ctx.emit('pointer-move', currentInstance) - } else { - currentInstance = null - if (prevInstance !== null) { - ctx.emit('pointer-leave', prevInstance) - } - } - - prevInstance = currentInstance - } - }) - - const clickEventListener = useEventListener(window, 'click', () => { - ctx.emit('click', prevInstance) - }) - - onUnmounted(() => { - clickEventListener() - }) - } - - if (scene && instance instanceof Fog) { - scene.fog = instance as unknown as FogBase - } - - if (import.meta.hot) { - import.meta.hot.on('vite:afterUpdate', () => { - instance = createInstance(threeObj, attrs, slots) - processProps(attrs, instance) - - if (instance.isObject3D) { - scene?.add(instance) - } - }) - } - - ctx.expose(instance) - - return () => {} - }, - }) - - return [name, cmp] - }) - ) - } - - return { - createComponentInstances, - processProps, - createInstanceFromVNode, - } -} diff --git a/packages/tres/src/core/useInstanceCreator/useInstanceCreator.test.ts b/packages/tres/src/core/useInstanceCreator/useInstanceCreator.test.ts deleted file mode 100644 index 298ca77d3..000000000 --- a/packages/tres/src/core/useInstanceCreator/useInstanceCreator.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { describe } from 'vitest' -import { shallowRef } from 'vue' -import { useInstanceCreator } from '.' -import { useTres } from '../useTres' -import { withSetup } from '/@/utils/test-utils' - -const [composable, app] = withSetup(() => useInstanceCreator('Tres')) - -describe('useInstanceCreator', () => { - // TODO: understand why this is not working - it.todo('should create component instances', () => { - const { createComponentInstances } = composable - const catalogue = shallowRef({ - TresBox: { name: 'TresBox' }, - TresSphere: { name: 'TresSphere' }, - TresPlane: { name: 'TresPlane' }, - }) - app.provide('catalogue', catalogue) - - app.provide('useTres', useTres()) - const components = createComponentInstances(catalogue) - expect(components).toHaveLength(3) - expect(components[0][0]).toBe('TresBox') - expect(components[1][0]).toBe('TresSphere') - expect(components[2][0]).toBe('TresPlane') - }) -}) diff --git a/packages/tres/src/core/useRenderer/component.ts b/packages/tres/src/core/useRenderer/component.ts deleted file mode 100644 index 919019e2e..000000000 --- a/packages/tres/src/core/useRenderer/component.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { RendererPresetsType } from './const' -import { ShadowMapType, TextureEncoding, ToneMapping } from 'three' -import { h, defineComponent, ref, provide, onBeforeUnmount, PropType } from 'vue' -import { useRenderer } from '.' -import { useLogger } from '/@/composables' -import { TresVNodeType } from '/@/types' - -/** - * Vue component for rendering a Tres component. - */ - -const { logError, logWarning } = useLogger() - -export const TresCanvas = defineComponent({ - name: 'TresCanvas', - props: { - shadows: Boolean, - shadowMapType: Number as PropType, - physicallyCorrectLights: { - type: Boolean, - default: false, - validator: (value: boolean) => { - if (value) { - logWarning('physicallyCorrectLights is deprecated. Use useLegacyLights instead.') - } - return true - }, - }, - useLegacyLights: Boolean, - outputEncoding: Number as PropType, - toneMapping: Number as PropType, - toneMappingExposure: Number, - context: Object as PropType, - powerPreference: String as PropType<'high-performance' | 'low-power' | 'default'>, - preserveDrawingBuffer: Boolean, - clearColor: String, - windowSize: { type: Boolean, default: false }, - preset: String as PropType, - }, - setup(props, { slots, attrs }) { - const canvas = ref() - const container = ref() - - const { renderer, dispose, aspectRatio } = useRenderer(canvas, container, props) - - provide('aspect-ratio', aspectRatio) - provide('renderer', renderer) - - if (slots.default && !slots.default().some(node => (node.type as TresVNodeType).name === 'Scene')) { - logError('TresCanvas must contain a Scene component.') - } - if (slots.default && !slots.default().some(node => (node.type as TresVNodeType).name?.includes('Camera'))) { - logError('Scene must contain a Camera component.') - } - - onBeforeUnmount(() => dispose()) - - return () => { - if (slots.default) { - return h( - 'div', - { - ref: container, - style: { - position: 'relative', - width: '100%', - height: '100%', - overflow: 'hidden', - pointerEvents: 'auto', - touchAction: 'none', - ...(attrs.style as Record), - }, - }, - [ - h( - 'div', - { - style: { - width: '100%', - height: '100%', - }, - }, - [ - h('canvas', { - ref: canvas, - style: { - display: 'block', - width: '100%', - height: '100%', - position: props.windowSize ? 'fixed' : 'absolute', - top: 0, - left: 0, - }, - }), - slots.default(), - ], - ), - ], - ) - } - } - }, -}) diff --git a/packages/tres/src/core/useScene/component.ts b/packages/tres/src/core/useScene/component.ts deleted file mode 100644 index 2b707e55e..000000000 --- a/packages/tres/src/core/useScene/component.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { defineComponent, inject, provide, Ref } from 'vue' -import type { Renderer } from 'three' -import { useCamera, useTres, useRenderLoop, useScene, useRaycaster } from '/@/core/' - -/** - * Vue component for rendering a Tres component. - */ -export const Scene = defineComponent({ - name: 'Scene', - setup(_props, { slots }) { - const { setState } = useTres() - const { scene } = useScene() - const renderer = inject>('renderer') - const { activeCamera } = useCamera() - const { raycaster, pointer } = useRaycaster() - const { onLoop } = useRenderLoop() - - provide('local-scene', scene) - setState('scene', scene.value) - - onLoop(() => { - if (!activeCamera.value) return - raycaster.value.setFromCamera(pointer.value, activeCamera.value) - - if (renderer?.value && activeCamera && scene?.value) { - renderer.value.render(scene?.value, activeCamera.value) - } - }) - - if (import.meta.hot) { - import.meta.hot.on('vite:afterUpdate', () => { - scene.value.children = [] - }) - } - - return () => { - if (slots.default) { - return slots.default() - } - } - }, -}) diff --git a/packages/tres/src/core/useScene/index.ts b/packages/tres/src/core/useScene/index.ts deleted file mode 100644 index 10e2ebfce..000000000 --- a/packages/tres/src/core/useScene/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Scene } from 'three' -import { shallowRef } from 'vue' - -const scene = shallowRef(new Scene()) - -/** - * Composable for accessing the scene. - * - * @export - * @return {*} {ShallowRef} - */ -export function useScene() { - return { - scene, - } -} diff --git a/packages/tres/src/core/useScene/useScene.test.ts b/packages/tres/src/core/useScene/useScene.test.ts deleted file mode 100644 index 7d3131b08..000000000 --- a/packages/tres/src/core/useScene/useScene.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Scene } from 'three' -import { describe, test, expect } from 'vitest' -import { useScene } from './' - -describe('useScene()', () => { - test('should init a scene', () => { - const { scene } = useScene() - expect(scene.value).toBeInstanceOf(Scene) - }) -}) diff --git a/packages/tres/src/demos/AkuAku.vue b/packages/tres/src/demos/AkuAku.vue new file mode 100644 index 000000000..689d08e09 --- /dev/null +++ b/packages/tres/src/demos/AkuAku.vue @@ -0,0 +1,28 @@ + + + diff --git a/packages/tres/src/components/AnimatedModel.vue b/packages/tres/src/demos/AnimatedModel.vue similarity index 100% rename from packages/tres/src/components/AnimatedModel.vue rename to packages/tres/src/demos/AnimatedModel.vue diff --git a/packages/tres/src/components/FBXModels.vue b/packages/tres/src/demos/FBXModels.vue similarity index 51% rename from packages/tres/src/components/FBXModels.vue rename to packages/tres/src/demos/FBXModels.vue index 8a44d60da..59fcea378 100644 --- a/packages/tres/src/components/FBXModels.vue +++ b/packages/tres/src/demos/FBXModels.vue @@ -1,7 +1,7 @@ diff --git a/packages/tres/src/components/Responsiveness.vue b/packages/tres/src/demos/Responsiveness.vue similarity index 100% rename from packages/tres/src/components/Responsiveness.vue rename to packages/tres/src/demos/Responsiveness.vue diff --git a/packages/tres/src/demos/Shapes.vue b/packages/tres/src/demos/Shapes.vue new file mode 100644 index 000000000..0329396e0 --- /dev/null +++ b/packages/tres/src/demos/Shapes.vue @@ -0,0 +1,133 @@ + + + diff --git a/packages/tres/src/demos/TestSphere.vue b/packages/tres/src/demos/TestSphere.vue new file mode 100644 index 000000000..67d63e2eb --- /dev/null +++ b/packages/tres/src/demos/TestSphere.vue @@ -0,0 +1,32 @@ + + + diff --git a/packages/tres/src/components/Text3D.vue b/packages/tres/src/demos/Text3D.vue similarity index 88% rename from packages/tres/src/components/Text3D.vue rename to packages/tres/src/demos/Text3D.vue index 5057b6634..c0a89cb61 100644 --- a/packages/tres/src/components/Text3D.vue +++ b/packages/tres/src/demos/Text3D.vue @@ -1,10 +1,10 @@