Peter 1 год назад
Родитель
Сommit
1df76f74a7

+ 2 - 0
package.json

@@ -73,6 +73,7 @@
     "@stackblitz/sdk": "^1.9.0",
     "@tresjs/cientos": "3.6.0",
     "@tresjs/eslint-config-vue": "^0.2.1",
+    "@types/cannon": "^0.1.12",
     "@types/three": "^0.159.0",
     "@typescript-eslint/eslint-plugin": "^6.14.0",
     "@typescript-eslint/parser": "^6.14.0",
@@ -80,6 +81,7 @@
     "@vitest/coverage-c8": "^0.33.0",
     "@vitest/ui": "^1.0.4",
     "@vue/test-utils": "^2.4.3",
+    "cannon-es": "^0.20.0",
     "eslint": "^8.55.0",
     "eslint-plugin-vue": "^9.19.2",
     "esno": "^4.0.0",

+ 64 - 62
playground/src/components/TheExperience.vue

@@ -1,86 +1,88 @@
 <script setup lang="ts">
-import { ref, watchEffect } from 'vue'
-import { BasicShadowMap, SRGBColorSpace, NoToneMapping } from 'three'
-import { TresCanvas } from '@tresjs/core'
+import {
+  TresCanvas,
+  pluginCore,
+  pluginCannon,
+  pluginText,
+} from '@tresjs/core'
 import { OrbitControls } from '@tresjs/cientos'
-import { TresLeches, useControls } from '@tresjs/leches'
-import '@tresjs/leches/styles'
-import TheSphere from './TheSphere.vue'
+import { MeshNormalMaterial, BoxGeometry } from 'three'
 
-const gl = {
-  clearColor: '#82DBC5',
-  shadows: true,
-  alpha: false,
-  shadowMapType: BasicShadowMap,
-  outputColorSpace: SRGBColorSpace,
-  toneMapping: NoToneMapping,
+function range(n: number) {
+  return Math.random() * 2 * n - n
 }
 
-const wireframe = ref(true)
-
-const canvas = ref()
-const meshRef = ref()
-
-const { isVisible } = useControls({
-  isVisible: true,
-})
-
-watchEffect(() => {
-  if (meshRef.value) {
-    console.log(meshRef.value)
-  }
-})
+const mat = new MeshNormalMaterial()
+const geo = new BoxGeometry(1)
 </script>
 
 <template>
-  <TresLeches />
   <TresCanvas
-    v-bind="gl"
-    ref="canvas"
-    class="awiwi"
-    :style="{ background: '#008080' }"
+    clear-color="#688"
+    :plugins="[pluginCore, pluginCannon, pluginText]"
   >
-    <TresPerspectiveCamera
-      :position="[7, 7, 7]"
-      :look-at="[0, 4, 0]"
-    />
+    <TresPerspectiveCamera :position="[25, 25, 25]" />
     <OrbitControls />
     <TresMesh
-      :position="[-2, 6, 0]"
-      :rotation="[0, Math.PI, 0]"
-      name="cone"
-      cast-shadow
+      :position="[0, 18, 0]"
+      :material="mat"
+    >
+      <Collider
+        shape="Box"
+        :angular-velocity="[range(10), range(10), range(10)]"
+        :args="[0.5, 0.5, 0.5]"
+      />
+      <TresBoxGeometry :args="[0.5, 0.5, 0.5]" />
+      First! 😆
+    </TresMesh>
+    <TresMesh
+      v-for="i of new Array(500).fill(0).map((_, i) => i)"
+      :key="i"
+      :position="[range(2), i + 22, range(2)]"
+      :material="mat"
+      :geometry="geo"
     >
-      <TresConeGeometry :args="[1, 1.5, 3]" />
-      <TresMeshToonMaterial color="#82DBC5" />
+      <Collider
+        shape="Box"
+        :angular-velocity="[range(10), range(10), range(10)]"
+      />
     </TresMesh>
+
     <TresMesh
-      :position="[0, 4, 0]"
-      cast-shadow
+      :position="[0, 8, 0]"
+      :material="mat"
+      :rotation="[-Math.PI * 0.25, 0, Math.PI * 0.25]"
     >
-      <TresBoxGeometry :args="[1.5, 1.5, 1.5]" />
-      <TresMeshToonMaterial
-        color="#4F4F4F"
-        :wireframe="wireframe"
+      <Collider
+        shape="Box"
+        :args="[8, 8, 8]"
+        :mass="0"
       />
+      <TresBoxGeometry :args="[8, 8, 8]" />
     </TresMesh>
+
     <TresMesh
-      ref="meshRef"
-      :rotation="[-Math.PI / 2, 0, Math.PI / 2]"
-      name="floor"
-      receive-shadow
+      :position="[0, 1, 0]"
+      :rotation="[0, Math.PI / 2, 0]"
+      :material="mat"
     >
-      <TresPlaneGeometry :args="[20, 20, 20]" />
-      <TresMeshToonMaterial
-        color="#D3FC8A"
+      <Collider
+        shape="Box"
+        :args="[30, 0.1, 30]"
+        :mass="0"
       />
+      <TresBoxGeometry :args="[30, 0.1, 30]" />
     </TresMesh>
-    <TheSphere v-if="isVisible" />
-    <TresAxesHelper :args="[1]" />
-    <TresDirectionalLight
-      :position="[0, 2, 4]"
-      :intensity="2"
-      cast-shadow
-    />
   </TresCanvas>
 </template>
+
+<style>
+.tres-text {
+  background-color: white;
+  color: #444;
+  font-size: 12px;
+  border-radius: 2px;
+  padding: 5px;
+  font-family: sans-serif;
+}
+</style>

+ 10 - 7
pnpm-lock.yaml

@@ -27,6 +27,9 @@ importers:
       '@tresjs/eslint-config-vue':
         specifier: ^0.2.1
         version: 0.2.1(@typescript-eslint/eslint-plugin@6.21.0)(eslint@8.57.0)(typescript@5.3.3)
+      '@types/cannon':
+        specifier: ^0.1.12
+        version: 0.1.12
       '@types/three':
         specifier: ^0.159.0
         version: 0.159.0
@@ -48,6 +51,9 @@ importers:
       '@vue/test-utils':
         specifier: ^2.4.3
         version: 2.4.4(vue@3.4.21)
+      cannon-es:
+        specifier: ^0.20.0
+        version: 0.20.0
       eslint:
         specifier: ^8.55.0
         version: 8.57.0
@@ -807,7 +813,6 @@ packages:
 
   /@esbuild/freebsd-x64@0.19.12:
     resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==}
-    engines: {node: '>=12'}
     cpu: [x64]
     os: [freebsd]
     requiresBuild: true
@@ -942,7 +947,6 @@ packages:
 
   /@esbuild/win32-x64@0.19.12:
     resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==}
-    engines: {node: '>=12'}
     cpu: [x64]
     os: [win32]
     requiresBuild: true
@@ -1214,7 +1218,6 @@ packages:
 
   /@octokit/plugin-request-log@4.0.1(@octokit/core@5.1.0):
     resolution: {integrity: sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA==}
-    engines: {node: '>= 18'}
     peerDependencies:
       '@octokit/core': '5'
     dependencies:
@@ -1654,6 +1657,10 @@ packages:
     resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==}
     dev: true
 
+  /@types/cannon@0.1.12:
+    resolution: {integrity: sha512-YzVDrjM+H1IH4pnw23nDFDXaWEyvYIq/Tmp75wg9CmtIiBJd3GtXzo5UXp+bAgk76fom9RhSlQIzHT8haF5VuQ==}
+    dev: true
+
   /@types/draco3d@1.4.9:
     resolution: {integrity: sha512-4MMUjMQb4yA5fJ4osXx+QxGHt0/ZSy4spT6jL1HM7Tn8OJEC35siqdnpOo+HxPhYjqEFumKfGVF9hJfdyKBIBA==}
 
@@ -2348,7 +2355,6 @@ packages:
 
   /@vue/language-core@1.8.27(typescript@5.3.3):
     resolution: {integrity: sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==}
-    peerDependencies:
       typescript: '*'
     peerDependenciesMeta:
       typescript:
@@ -4782,7 +4788,6 @@ packages:
 
   /globby@14.0.1:
     resolution: {integrity: sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==}
-    engines: {node: '>=18'}
     dependencies:
       '@sindresorhus/merge-streams': 2.3.0
       fast-glob: 3.3.2
@@ -4918,7 +4923,6 @@ packages:
 
   /hasown@2.0.1:
     resolution: {integrity: sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==}
-    engines: {node: '>= 0.4'}
     dependencies:
       function-bind: 1.1.2
     dev: true
@@ -5103,7 +5107,6 @@ packages:
 
   /internal-slot@1.0.7:
     resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==}
-    engines: {node: '>= 0.4'}
     dependencies:
       es-errors: 1.3.0
       hasown: 2.0.1

+ 8 - 2
src/components/TresCanvas.vue

@@ -20,6 +20,7 @@ import {
   defineComponent,
   h, 
   getCurrentInstance,
+  createRenderer,
 } from 'vue'
 import pkg from '../../package.json'
 import {
@@ -30,11 +31,13 @@ import {
   type TresContext,
 } from '../composables'
 import { extend } from '../core/catalogue'
-import { render } from '../core/renderer'
 
 import type { RendererPresetsType } from '../composables/useRenderer/const'
 import type { TresCamera, TresObject } from '../types/'
 import { registerTresDevtools } from '../devtools'
+import { plugin as tresCorePlugin } from '../plugins/tres.nodeOps.plugin'
+import { useNodeOpsWithContext } from '../core/nodeOps'
+import type { TresNodeOpsPlugin } from '../core/nodeOps'
 
 export interface TresCanvasProps
   extends Omit<WebGLRendererParameters, 'canvas'> {
@@ -52,6 +55,7 @@ export interface TresCanvasProps
   preset?: RendererPresetsType
   windowSize?: boolean
   disableRender?: boolean
+  plugins?: TresNodeOpsPlugin<any, any, TresContext> | TresNodeOpsPlugin<any, any, TresContext>[]
 }
 
 const props = withDefaults(defineProps<TresCanvasProps>(), {
@@ -66,6 +70,7 @@ const props = withDefaults(defineProps<TresCanvasProps>(), {
   preserveDrawingBuffer: undefined,
   logarithmicDepthBuffer: undefined,
   failIfMajorPerformanceCaveat: undefined,
+  plugins: () => [tresCorePlugin],
 })
 
 const { logWarning } = useLogger()
@@ -104,7 +109,8 @@ const createInternalComponent = (context: TresContext) =>
 
 const mountCustomRenderer = (context: TresContext) => {
   const InternalComponent = createInternalComponent(context)
-  render(h(InternalComponent), scene.value as unknown as TresObject)
+  const { render } = createRenderer(useNodeOpsWithContext(context, props.plugins))
+  render(h(InternalComponent), context.scene.value as unknown as TresObject)
 }
 
 const dispose = (context: TresContext, force = false) => {

+ 206 - 0
src/core/nodeOps.test.ts

@@ -0,0 +1,206 @@
+import {
+  useNodeOpsWithContext,
+  forImplementationTests as implementation,
+  TresNodeOpsPlugin,
+  TresNodeOpsPluginStore,
+} from "./nodeOps";
+
+const myPlugin = {
+  name: "My Plugin",
+  setup: () => {},
+  createElement: (context: Object) => (tag: string) => {
+    return { tag };
+  },
+};
+
+describe("nodeOps", () => {
+  describe("useNodeOpsWithContext", () => {
+    it("returns an object that exposes the Vue RendererOptions API", () => {
+      const nodeOps = useNodeOpsWithContext({});
+      expect(nodeOps.patchProp).toBeTypeOf("function");
+      expect(nodeOps.insert).toBeTypeOf("function");
+      expect(nodeOps.remove).toBeTypeOf("function");
+      expect(nodeOps.createElement).toBeTypeOf("function");
+      expect(nodeOps.createText).toBeTypeOf("function");
+      expect(nodeOps.createComment).toBeTypeOf("function");
+      expect(nodeOps.setText).toBeTypeOf("function");
+      expect(nodeOps.setElementText).toBeTypeOf("function");
+      expect(nodeOps.parentNode).toBeTypeOf("function");
+      expect(nodeOps.nextSibling).toBeTypeOf("function");
+    });
+    it("returns an object that exposes the PluginStore API", () => {
+      const nodeOps = useNodeOpsWithContext({});
+      expect(nodeOps.hasPlugin).toBeTypeOf("function");
+    });
+    it("returns the same nodeOps for a given context", () => {
+      const contextA = {};
+      const nodeOpsA0 = useNodeOpsWithContext(contextA);
+      const nodeOpsA1 = useNodeOpsWithContext(contextA);
+      expect(nodeOpsA0).equals(nodeOpsA1);
+    });
+    it("returns different nodeOps for different contexts", () => {
+      const contextA = {};
+      const contextB = {};
+      const nodeOpsA = useNodeOpsWithContext(contextA);
+      const nodeOpsB = useNodeOpsWithContext(contextB);
+      expect(nodeOpsA).not.equals(nodeOpsB);
+    });
+  });
+
+  describe("nodeOps.addPlugin", () => {
+    it("adds a plugin", () => {
+      const plugin = () => ({
+        name: "mySimplePlugin",
+        createElement: () => ({}),
+      });
+      const nodeOps = useNodeOpsWithContext({}, plugin);
+      assert(nodeOps.hasPlugin("mySimplePlugin"));
+    });
+
+    it("adds an array of plugins", () => {
+      const plugin1: TresNodeOpsPlugin<Object, Object, Object> = () => ({
+        name: "p1",
+        createElement: {
+          fn: () => ({}),
+          weight: 1,
+        },
+      });
+      const plugin2: TresNodeOpsPlugin<Object, Object, Object> = () => ({
+        name: "p2",
+        createElement: () => ({}),
+      });
+      const nodeOps = useNodeOpsWithContext({}, [plugin1, plugin2]);
+      expect(nodeOps.hasPlugin("p1")).equals(true);
+      expect(nodeOps.hasPlugin("p2")).equals(true);
+    });
+
+    it("throws an error if two plugins with the same name are added", () => {
+      const plugin1: TresNodeOpsPlugin<Object, Object, Object> = () => ({
+        name: "p1",
+        createElement: {
+          fn: () => ({}),
+          weight: 1,
+        },
+      });
+      const plugin2: TresNodeOpsPlugin<Object, Object, Object> = () => ({
+        name: "p1",
+        createElement: () => ({}),
+      });
+      const addsPluginWithSameName = () =>
+        useNodeOpsWithContext({}, [plugin1, plugin2]);
+      expect(addsPluginWithSameName).toThrowError();
+    });
+
+    it("filters createElement calls by tag", () => {
+      const plugin1: TresNodeOpsPlugin<
+        { text: string },
+        { text: string },
+        Object
+      > = () => ({
+        name: "p1",
+        filter: {
+          tag: (tag: string) => tag === "Awesome tag",
+        },
+        createElement: {
+          fn: (tag) => ({ text: tag + " good job" }),
+          weight: 1,
+        },
+      });
+      const plugin2: TresNodeOpsPlugin<
+        { text: string },
+        { text: string },
+        Object
+      > = () => ({
+        name: "p2",
+        filter: {
+          tag: (tag: string) => tag === "Ok tag",
+        },
+        createElement: (tag) => ({ text: tag + " alright job" }),
+      });
+      const nodeOps = useNodeOpsWithContext({}, [plugin1, plugin2]);
+      const returnValue = { element: "I'm an element" };
+
+      const noMatchingFilter = nodeOps.createElement("Bad tag", false, "", {
+        visible: false,
+      });
+      expect(noMatchingFilter).equals(null);
+
+      const matchesFilter = nodeOps.createElement("Awesome tag", false, "", {
+        visible: false,
+      });
+      expect(matchesFilter.text).equals("Awesome tag good job");
+
+      const alsoMatchesFilter = nodeOps.createElement("Ok tag", false, "", {
+        visible: false,
+      });
+      expect(alsoMatchesFilter.text).equals("Ok tag alright job");
+    });
+  });
+
+  describe("implementation", () => {
+    describe("doAddPlugin", () => {
+      it("adds plugins and returns new store", () => {
+        const plugin0 = () => ({
+          name: "simplePluginA",
+        });
+        const plugin1 = () => ({
+          name: "simplePluginB",
+          createElement: () => ({}),
+        });
+        const context = {};
+        const pluginStore0 = {};
+        const pluginStore1 = implementation.doAddPlugin(plugin0, pluginStore0, context);
+        const pluginStore2 = implementation.doAddPlugin(plugin1, pluginStore1, context);
+        expect(pluginStore0["simplePluginA"]).not.toBeDefined();
+        expect(pluginStore0["simplePluginB"]).not.toBeDefined();
+        expect(pluginStore1["simplePluginA"]).toBeDefined();
+        expect(pluginStore1["simplePluginB"]).not.toBeDefined();
+        expect(pluginStore2["simplePluginA"]).toBeDefined();
+        expect(pluginStore2["simplePluginB"]).toBeDefined();
+      });
+
+      it("normalizes and binds context to plugins", () => {
+        const onBind = vitest.fn();
+        const onCreateElement = vitest.fn()
+        const onRemoveElement = vitest.fn()
+
+        const context = {
+          onCreateElement,
+          onRemoveElement,
+        };
+
+        const pluginToAdd = (ctx) => {
+          onBind(ctx);
+          return {
+            name: "simplePlugin",
+            weight: -1,
+            createElement: {
+              fn: (tag: string) => ctx.onCreateElement(tag),
+              weight: -111,
+            },
+            remove: {
+              fn: (a:any) => ctx.onRemoveElement(a),
+            },
+          };
+        };
+
+        const pluginStore: TresNodeOpsPluginStore<Object, Object> =
+          implementation.doAddPlugin(pluginToAdd, {}, context);
+        const boundPlugin = implementation.doGetPluginFromPluginStore(
+          "simplePlugin",
+          pluginStore
+        );
+
+        expect(onBind).toBeCalledWith(context);
+        expect(Object.keys(pluginStore)[0]).equals("simplePlugin");
+        expect(boundPlugin).not.toBeNull();
+
+        const el = boundPlugin.createElement.fn("myTag");
+        boundPlugin.remove.fn(el);
+
+        expect(onCreateElement).toBeCalledWith("myTag");
+        expect(onRemoveElement).toBeCalledWith(el);
+      });
+    });
+  });
+});

+ 338 - 228
src/core/nodeOps.ts

@@ -1,266 +1,376 @@
-import type { RendererOptions } from 'vue'
-import { BufferAttribute } from 'three'
-import { isFunction } from '@alvarosabu/utils'
-import type { Object3D, Camera } from 'three'
-import { useLogger } from '../composables'
-import { deepArrayEqual, isHTMLTag, kebabToCamel } from '../utils'
-
-import type { TresObject, TresObject3D, TresScene } from '../types'
-import { catalogue } from './catalogue'
-
-function noop(fn: string): any {
-  fn
+import { type RendererElement, type RendererNode, type RendererOptions } from 'vue'
+
+/**
+ * Main API
+ */
+
+const CONTEXT_TO_NODE_OPS = new WeakMap()
+
+/**
+ * NOTE: 
+ * Regarding the generic types used below:
+ * HostNode, HostElement types are from Vue's RendererOptions. They are the types RendererOptions functions return.
+ * HostContext is passed to the plugin as plugin(c: HostContext). In the case of Tres, it's TresContext.
+ */
+export type TresNodeOpsPluginStore< N extends RendererNode, E extends RendererElement> = PluginStore<N, E>
+export type TresNodeOpsPlugin<N extends RendererNode, E extends RendererNode, C extends WeakKey> = 
+  Plugin<N, E, C>
+
+export function useNodeOpsWithContext<
+  HostNode extends RendererNode,
+  HostElement extends RendererElement,
+  HostContext extends WeakKey,
+>(
+  context: HostContext = {} as HostContext,
+  pluginOrPlugins: Plugin<HostNode, HostElement, HostContext> 
+  | Plugin<HostNode, HostElement, HostContext>[] = [],
+): PluggableRendererOptions<HostNode, HostElement, HostContext> {
+  if (CONTEXT_TO_NODE_OPS.has(context)) {
+    return CONTEXT_TO_NODE_OPS.get(context)
+  }
+
+  const pluginStore: PluginStore<HostNode, HostElement> = doAddPlugin(pluginOrPlugins, {}, context)
+  const result: PluggableRendererOptions<HostNode, HostElement, HostContext>
+    = Object.assign({}, 
+      noopRendererOptions, 
+      { 
+        hasPlugin: (pluginName: string) => pluginStore.hasOwnProperty(pluginName),
+        dispose: () => Object.values(pluginStore).forEach(p => p.dispose()),
+      }, 
+      doGetRendererOptionsFromPluginStore(pluginStore),
+    )
+  
+  CONTEXT_TO_NODE_OPS.set(context, result)
+
+  return result
 }
 
-let scene: TresScene | null = null
-
-const { logError } = useLogger()
-
-const supportedPointerEvents = [
-  'onClick',
-  'onPointerMove',
-  'onPointerEnter',
-  'onPointerLeave',
-]
-
-export const nodeOps: RendererOptions<TresObject, TresObject> = {
-  createElement(tag, _isSVG, _anchor, props) {
-    if (!props) props = {}
-
-    if (!props.args) {
-      props.args = []
+/**
+ * NOTE: Helper functions
+ */
+
+function doGetRendererOptionsFromPluginStore<
+  HostNode extends RendererNode,
+  HostElement extends RendererElement,
+>(
+  pluginStore: PluginStore<HostNode, HostElement>,
+): RendererOptions<HostNode, HostElement> {
+  const result = Object.assign({}, noopRendererOptions)
+  const createElementPluginEntries = getSortedPluginEntriesFromStore(pluginStore, 'createElement')
+  const patchPropPluginEntries = getSortedPluginEntriesFromStore(pluginStore, 'patchProp')
+  const insertPluginEntries = getSortedPluginEntriesFromStore(pluginStore, 'insert')
+  const removePluginEntries = getSortedPluginEntriesFromStore(pluginStore, 'remove')
+  const createTextPluginEntries = getSortedPluginEntriesFromStore(pluginStore, 'createText')
+  const createCommentPluginEntries = getSortedPluginEntriesFromStore(pluginStore, 'createComment')
+  const setTextPluginEntries = getSortedPluginEntriesFromStore(pluginStore, 'setText')
+  const setElementTextPluginEntries = getSortedPluginEntriesFromStore(pluginStore, 'setElementText')
+  const parentNodePluginEntries = getSortedPluginEntriesFromStore(pluginStore, 'parentNode')
+
+  result.createElement = (tag: string, ...rest) => {
+    for (const entry of createElementPluginEntries) {
+      if (entry.filter.tag(tag)) {
+        const result = entry.fn(tag, ... rest)
+        if (result) {
+          return result
+        }
+      }
     }
-    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 })
+    return noopRendererOptions['createElement']
+  }
+
+  result.patchProp = (
+    el: HostElement,
+    key: string,
+    prevValue: any,
+    nextValue: any,
+    // the rest is unused for most custom renderers
+    ...rest
+  ) => {
+    for (const entry of patchPropPluginEntries) {
+      if (entry.filter.element(el)) {
+        entry.fn(el, key, prevValue, nextValue, ...rest)
+      }
     }
-    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.`)
+  }
+
+  result.insert = (
+    el: HostNode,
+    parent: HostElement,
+    anchor?: HostNode | null,
+  ) => {
+    for (const entry of insertPluginEntries) {
+      if (entry.filter.node(el)) {
+        entry.fn(el, parent, anchor)
       }
-      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)
+  result.remove = (el: HostNode) => {
+    for (const entry of removePluginEntries) {
+      if (entry.filter.node(el)) {
+        entry.fn(el)
       }
     }
+  }
 
-    if (props?.attach === undefined) {
-      if (instance.isMaterial) instance.attach = 'material'
-      else if (instance.isBufferGeometry) instance.attach = 'geometry'
+  result.parentNode = (el: HostNode) => {
+    for (const entry of parentNodePluginEntries) {
+      const result = entry.fn(el)
+      if (result) return result
     }
+    return null
+  }
 
-    // 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
+  result.createText = (text: string) => {
+    for (const entry of createTextPluginEntries) {
+      const result = entry.fn(text)
+      if (result) return result
     }
+    return text
+  }
 
-    // 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,
+  result.createComment = (text: string) => {
+    for (const entry of createCommentPluginEntries) {
+      const result = entry.fn(text)
+      if (result) return result
     }
+    return null
+  }
 
-    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 '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 'could not find tres__registerAtPointerEventHandler on scene\'s userData'
-
-        scene?.userData.tres__registerAtPointerEventHandler?.(child as Object3D)
-      }
+  result.setText = (node: HostNode, text: string) => {
+    for (const entry of setTextPluginEntries) {
+      entry.fn(node, text)
     }
+  }
 
-    if (child?.isObject3D && parentObject?.isObject3D) {
-      parentObject.add(child)
-      child.dispatchEvent({ type: 'added' })
+  result.setElementText = (el: HostElement, text: string) => {
+    for (const entry of setElementTextPluginEntries) {
+      entry.fn(el, text)
     }
-    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) => {
+  result.nextSibling = () => noop('nextSibling')
+  result.querySelector = () => noop('querySelector')
+  result.setScopeId = () => noop('setScopeId')
+  result.cloneNode = () => noop('cloneNode')
+  result.insertStaticContent = () => noop('insertStaticContent')
 
-        if (!deregisterBlockingObjectAtPointerEventHandler)
-          throw 'could not find tres__deregisterBlockingObjectAtPointerEventHandler on scene\'s userData'
-
-        scene?.userData.tres__deregisterBlockingObjectAtPointerEventHandler?.(object as Object3D)
-
-        if (!deregisterAtPointerEventHandler)
-          throw '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 '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)
-      })
+  return result
+}
 
-      disposeMaterialsAndGeometries(object3D)
-      deregisterCameraIfRequired(object3D)
-      deregisterAtPointerEventHandlerIfRequired?.(object3D as TresObject)
+function getSortedPluginEntriesFromStore<
+  HostNode extends RendererNode,
+  HostElement extends RendererElement,
+  T extends RendererOptionsFunctionKey,
+>(store: PluginStore<HostNode, HostElement>, key: T) {
+  const result: PNE<T>[] = []
+  for (const plugin of Object.values(store)) {
+    if (plugin.hasOwnProperty(key)) {
+      result.push(plugin[key] as PNE<T>)
     }
+  }
 
-    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
-      }
+  result.sort((a: PNE<T>, b: PNE<T>) => a.weight - b.weight)
 
-      let finalKey = kebabToCamel(key)
-      let target = root?.[finalKey]
+  return result
 
-      if (key === 'args') {
-        const prevNode = node as TresObject3D
-        const prevArgs = _prevValue ?? []
-        const args = nextValue ?? []
-        const instanceName = node.userData.tres__name || node.type
+  type PNE<T extends RendererOptionsFunctionKey> = PluginEntryNormalized<
+    HostNode,
+    HostElement,
+    T
+  >
+}
 
-        if (instanceName && prevArgs.length && !deepArrayEqual(prevArgs, args)) {
-          root = Object.assign(prevNode, new catalogue.value[instanceName](...nextValue))
-        }
-        return
-      }
+function doAddPlugin<
+  HostNode extends RendererNode,
+  HostElement extends RendererElement,
+  HostContext,
+>(
+  plugin:
+  | Plugin<HostNode, HostElement, HostContext >
+  | Plugin<HostNode, HostElement, HostContext>[],
+  pluginStore: PluginStore<HostNode, HostElement>,
+  context: HostContext,
+): PluginStore<HostNode, HostElement> {
+
+  if (Array.isArray(plugin)) {
+    let result: PS = Object.assign({}, pluginStore)
+    for (const p of plugin) {
+      result = doAddPlugin(p, result, context)
+    }
+    return result
+  }
+  else {
+    const result: PS = Object.assign({}, pluginStore)
+    const boundPlugin = doBindAndNormalizePlugin( plugin, context )
+
+    if (result.hasOwnProperty(boundPlugin.name)) {
+      throw new Error(
+        `Plugin store already contains plugin named ${
+          boundPlugin.name
+        }`,
+      )
+    }
+    result[boundPlugin.name] = boundPlugin
+    return result
+  }
 
-      if (root.type === 'BufferGeometry') {
-        if (key === 'args') return
-        root.setAttribute(
-          kebabToCamel(key),
-          new BufferAttribute(...(nextValue as ConstructorParameters<typeof BufferAttribute>)),
-        )
-        return
-      }
+  type PS = PluginStore<HostNode, HostElement>
+}
 
-      // 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)
+function doBindAndNormalizePlugin<
+  HostNode extends RendererNode,
+  HostElement extends RendererElement,
+  HostContext,
+>(
+  plugin: Plugin<HostNode, HostElement, HostContext>,
+  context: HostContext,
+): PluginNormalized<HostNode, HostElement> {
+  const boundPlugin = plugin(context)
+  const name = boundPlugin.name
+  const weight = boundPlugin.weight ?? 0
+  const filter = {
+    tag: boundPlugin.filter?.tag ?? ((tag: string) => true),
+    node: boundPlugin.filter?.node ?? ((a: any): a is HostNode => true),
+    element: boundPlugin.filter?.element ?? ((a: any): a is HostElement => true),
+  }
+  const dispose = boundPlugin.dispose ?? (() => {})
+
+  const result: PluginNormalized<HostNode, HostElement> = { name, dispose }
+
+  for (const key of rendererOptionsFunctionKeys) {
+    if (key in boundPlugin) {
+      const entry = boundPlugin[key]
+      if (typeof entry === 'object' && typeof entry.fn === 'function') {
+        result[key] = { fn: entry.fn, weight: entry.weight ?? weight, name, filter }
+      } 
+      else if (typeof entry === 'function') {
+        result[key] = { fn: entry, weight, name, filter }
       }
-      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'),
+  return result as PluginNormalized<HostNode, HostElement>
+}
 
-  setText: () => noop('setText'),
+export const forImplementationTests = {
+  doAddPlugin,
+  doGetPluginFromPluginStore: (
+    name: string, 
+    store: PluginStore<any, any>,
+  ) => (store[name] as PluginNormalized<any, any>) ?? null,
+}
+
+/**
+ * NOTE: Misc. data
+ */
+
+const noopRendererOptions: RendererOptions<any, any> = {
+  patchProp: (... rest) => {},
+  insert: () => {},
+  remove: () => {},
+  createElement: () => null,
+  createText: () => ({}),
+  createComment: (text: string) => ({}),
+  setText: () => {},
+  setElementText: () => {},
+  parentNode: () => null,
+  nextSibling: () => null,
+}
 
-  setElementText: () => noop('setElementText'),
-  nextSibling: () => noop('nextSibling'),
+function noop(fn: string): any { }
+
+const rendererOptionsFunctionKeys: Set<RendererOptionsFunctionKey> = new Set([
+  'patchProp',
+  'insert',
+  'remove',
+  'createElement',
+  'createText',
+  'createComment',
+  'setText',
+  'setElementText',
+  'parentNode',
+  'nextSibling',
+])
+
+/**
+ * Types
+ */
+
+type PluggableRendererOptions<
+  HostNode extends RendererNode,
+  HostElement extends RendererElement,
+  HostContext,
+> = RendererOptions<HostNode, HostElement> &
+Pluggable<HostNode, HostElement, HostContext>
+
+interface Pluggable<
+  HostNode extends RendererNode,
+  HostElement extends RendererElement,
+  HostContext,
+> {
+  hasPlugin: (s: string) => boolean
+  dispose: () => void
+}
 
-  querySelector: () => noop('querySelector'),
+type FunctionKeys<T> = {
+  [K in keyof T]: T[K] extends (...a: any) => any ? K : never;
+}[keyof T]
+type RendererOptionsFunctionKey = Exclude<
+  FunctionKeys<RendererOptions>,
+  undefined
+>
+
+type Plugin< HostNode extends RendererNode, HostElement extends RendererElement, HostContext> = 
+(context: HostContext) => {
+  name: string
+  weight?: number
+  filter?: PluginFilter<HostNode, HostElement>
+  dispose?: () => void
+} & Partial<{
+  [K in RendererOptionsFunctionKey]: PluginEntry< K >;
+}>
+
+type PluginEntry< K extends RendererOptionsFunctionKey > = {
+  weight?: number
+  fn: RendererOptions[K]
+} | RendererOptions[K]
+
+type PluginNormalized<
+  HostNode extends RendererNode,
+  HostElement extends RendererElement,
+> = {
+  name: string
+  dispose: () => void
+} &
+Partial<{ 
+  [K in RendererOptionsFunctionKey]: PluginEntryNormalized<
+    HostNode,
+    HostElement,
+    K
+  >;
+}>
+
+interface PluginEntryNormalized<
+  HostNode extends RendererNode,
+  HostElement extends RendererElement,
+  K extends RendererOptionsFunctionKey,
+> {
+  name: string
+  weight: number
+  filter: PluginFilterNormalized<HostNode, HostElement>
+  fn: RendererOptions[K]
+}
 
-  setScopeId: () => noop('setScopeId'),
-  cloneNode: () => noop('cloneNode'),
+type PluginFilter<HostNode, HostElement> = Partial<PluginFilterNormalized<HostNode, HostElement>>
 
-  insertStaticContent: () => noop('insertStaticContent'),
+interface PluginFilterNormalized<HostNode, HostElement> {
+  tag: (tag: string) => boolean
+  node: (a: any) => a is HostNode
+  element: (a: any) => a is HostElement
 }
+
+type PluginStore<
+  HostNode extends RendererNode,
+  HostElement extends RendererElement,
+> = Record<string, PluginNormalized<HostNode, HostElement>>

+ 0 - 12
src/core/renderer.ts

@@ -1,12 +0,0 @@
-import * as THREE from 'three'
-
-import { createRenderer } from 'vue'
-import { extend } from './catalogue'
-import { nodeOps } from './nodeOps'
-
-export const { render } = createRenderer(nodeOps)
-
-// Creates the catalogue of components based on THREE namespace
-extend(THREE)
-
-export default { extend }

+ 1 - 0
src/index.ts

@@ -8,6 +8,7 @@ export * from './core/catalogue'
 export * from './components'
 export * from './types'
 export * from './directives'
+export * from './plugins'
 
 export interface TresOptions {
   extends?: Record<string, unknown>

+ 92 - 0
src/plugins/cannon.nodeOps.plugin.ts

@@ -0,0 +1,92 @@
+import type { TresNodeOpsPlugin } from 'src/core/nodeOps'
+import {
+  Body,
+  Box,
+  World,
+  NaiveBroadphase,
+  Vec3 as CannonVec3,
+} from 'cannon-es'
+import type { Object3D } from 'three'
+import type { TresContext } from '../composables'
+import { useRenderLoop } from '../composables'
+
+function setup() {
+  const store = new Map()
+  const world = new World()
+  world.gravity.set(0, -9.8, 0)
+  world.broadphase = new NaiveBroadphase()
+  const loopAPI = useRenderLoop().onLoop(({ delta }) => {
+    world.step(delta)
+    for (const [body, object3D] of store) {
+      object3D.position.copy(body.position)
+      object3D.setRotationFromQuaternion(body.quaternion)
+    }
+  })
+
+  const dispose = () => {
+    world.bodies.forEach(b => world.removeBody(b))
+    store.clear()
+    loopAPI.off()
+  }
+
+  return { store, world, dispose }
+}
+
+export const plugin: TresNodeOpsPlugin<Body, Body, TresContext> = (
+  context: TresContext,
+) => {
+
+  const { store, world, dispose } = setup()
+
+  return {
+    name: 'Cannon Physics plugin',
+    filter: {
+      tag: (tag: string) => tag === 'Collider',
+      element: (a: any): a is Body => a && a.aabb,
+      node: (a: any): a is Body => a && a.aabb,
+    },
+    createElement: (_: any, __: any, ___: any, props: Record<string, any>) => {
+      props = props ?? {}
+      const args = props.args ?? []
+      const mass = props.mass ?? 1
+      const angularVelocity = props['angular-velocity'] ?? [0, 0, 0]
+      const velocity = props.velocity ?? [0, 0, 0]
+      const shapeType = props.shape ?? 'Box'
+
+      const shape = (() => {
+        if ('Box' === shapeType) {
+          const xyz: number[] = args.every((n: any) => typeof n === 'number') ? args : [1., 1., 1.]
+          while (xyz.length < 3) xyz.push(1.0)
+          return new Box(new CannonVec3(... xyz.map(n => 0.5 * n)))
+        }
+        return new Box(new CannonVec3(1, 1, 1))
+      })()
+
+      const body = new Body({ mass, shape })
+      body.angularVelocity.set(
+        angularVelocity[0],
+        angularVelocity[1],
+        angularVelocity[2],
+      )
+      body.velocity.set(velocity[0], velocity[1], velocity[2])
+
+      return body
+    },
+    insert: (body: Body, parent) => {
+      if (parent.isObject3D) {
+        const p = (<THREE.Object3D>parent).position
+        const r = (<THREE.Object3D>parent).quaternion
+        body.position.set(p.x, p.y, p.z)
+        body.quaternion.set(r.x, r.y, r.z, r.w)
+
+        world.addBody(body)
+        store.set(body, parent as Object3D)
+      }
+    },
+    remove: (body: Body) => {
+      world.removeBody(body)
+      store.delete(body)
+    },
+    dispose,
+  }
+}

+ 9 - 0
src/plugins/index.ts

@@ -0,0 +1,9 @@
+import { plugin as pluginCore } from './tres.nodeOps.plugin'
+import { plugin as pluginCannon } from './cannon.nodeOps.plugin'
+import { plugin as pluginText } from './text.nodeOps.plugin'
+
+export {
+  pluginCore,
+  pluginCannon,
+  pluginText,
+}

+ 97 - 0
src/plugins/text.nodeOps.plugin.ts

@@ -0,0 +1,97 @@
+import type { TresNodeOpsPlugin } from 'src/core/nodeOps'
+import type { Object3D } from 'three'
+import type { TresContext } from '../composables'
+import { useRenderLoop } from '../composables'
+
+function setup(context: TresContext) {
+  const divToObject3D = new Map<HTMLDivElement, Object3D | null>()
+
+  const loopAPI = useRenderLoop().onLoop(({ delta }) => {
+    const width = window.innerWidth, height = window.innerHeight
+    const widthHalf = width / 2, heightHalf = height / 2
+    for (const [div, obj] of divToObject3D.entries()) {
+      if (obj && context.camera.value) {
+        const pos = obj.position.clone()
+        pos.project(context.camera.value)
+        pos.x = ( pos.x * widthHalf ) + widthHalf
+        pos.y = - ( pos.y * heightHalf ) + heightHalf
+        if (pos.y < 0 || pos.y > height - 30 || pos.x < 0 || pos.x > width - 60) {
+          div.style.display = 'none'
+        }
+        else {
+          div.style.display = 'block'
+          div.style.left = `${(pos.x + 10).toString()}px`
+          div.style.top = `${(pos.y + 5).toString()}px`
+        }
+      } 
+      else {
+        div.style.display = 'none'
+      }
+    }
+  })
+
+  const create = (text: string): HTMLDivElement | null => {
+    if (!text) return null
+    const result = document.createElement('div')
+    result.className = 'tres-text'
+    result.innerText = text
+    result.style.position = 'absolute'
+    result.style.zIndex = '2'
+    divToObject3D.set(result, null)
+    return result
+  }
+  
+  const insert = (div: HTMLDivElement, obj: Object3D) => {
+    divToObject3D.set(div, obj)
+    if (context.renderer.value.domElement) {
+      context.renderer.value.domElement?.parentNode?.prepend(div)
+    }
+  }
+
+  const remove = (div: HTMLDivElement) => {
+    divToObject3D.delete(div)
+  }
+
+  const patch = (div: HTMLDivElement, text: string) => {
+    div.textContent = text
+  }
+
+  const has = (div: HTMLDivElement) => divToObject3D.has(div)
+
+  const dispose = () => {
+    for (const div of divToObject3D.keys()) {
+      remove(div)
+    }
+    loopAPI.off()
+  }
+
+  return { create, insert, remove, patch, has, dispose }
+}
+
+export const plugin: TresNodeOpsPlugin<HTMLElement, HTMLElement, TresContext> = (
+  context: TresContext,
+) => {
+  const { create: createText, insert, remove, patch, has, dispose } = setup(context)
+
+  return {
+    name: 'Text plugin',
+    filter: {
+      node: (a: any): a is HTMLDivElement => has(a),
+      element: (a: any): a is HTMLDivElement => has(a),
+    },
+    createText,
+    insert(child: HTMLDivElement, parent: any) {
+      if (parent && parent.isObject3D) {
+        insert(child, parent)
+      }
+    },
+    remove,
+    setText: (node: HTMLDivElement, text: string) => {
+      patch(node, text)
+    },
+    setElementText: (el: HTMLDivElement, text: string) => {
+      patch(el, text)
+    },
+    dispose,
+  }
+}

+ 9 - 4
src/core/nodeOpts.test.ts → src/plugins/tres.nodeOps.plugin.test.ts

@@ -1,13 +1,18 @@
 import * as THREE from 'three'
-import { nodeOps } from './nodeOps'
+import { useNodeOpsWithContext } from './../core/nodeOps'
+import { plugin as tresCorePlugin } from './tres.nodeOps.plugin'
+import { extend } from '../core/catalogue'
 import { TresObject } from '../types'
-import { extend } from './catalogue'
 import { Mesh, Scene } from 'three'
 
+let nodeOps;
+
+const countKey = new Object()
+
 describe('nodeOps', () => {
   beforeAll(() => {
     // Setup
-    extend(THREE)
+    nodeOps = useNodeOpsWithContext({scene: new THREE.Scene(), extend}, tresCorePlugin)
   })
   it('createElement should create an instance with given tag', async () => {
     // Setup
@@ -181,4 +186,4 @@ describe('nodeOps', () => {
     // Assert
     expect(parentNode === parent)
   })
-})
+})

+ 302 - 0
src/plugins/tres.nodeOps.plugin.ts

@@ -0,0 +1,302 @@
+import * as THREE from 'three'
+import { BufferAttribute } from 'three'
+import { isFunction } from '@alvarosabu/utils'
+import type { Object3D, Camera } from 'three'
+import type { TresNodeOpsPlugin } from '../core/nodeOps'
+import type { TresContext } from '../composables'
+import { useLogger } from '../composables'
+import { deepArrayEqual, kebabToCamel } from '../utils'
+
+import type { TresObject, TresObject3D } from '../types'
+import { normalizeVectorFlexibleParam } from '../utils/normalize'
+import { catalogue } from './../core/catalogue'
+
+export const plugin: TresNodeOpsPlugin<TresObject, TresObject, TresContext> = (
+  context: TresContext,
+) => {
+  context.extend(THREE)
+  const { logError } = useLogger()
+  const supportedPointerEvents = [
+    'onClick',
+    'onPointerMove',
+    'onPointerEnter',
+    'onPointerLeave',
+  ]
+
+  return {
+    name: 'Tres/Three Core NodeOps plugin',
+    filter: {
+      tag: (tag: string) => {
+        tag = tag.startsWith('Tres') ? tag.substring(4) : tag
+        return tag in catalogue.value || tag === 'primitive'
+      },
+      element: (a: any): a is TresObject =>
+        a && (a.isObject3D || a.isBufferGeometry || a.isMaterial || a.isFog),
+      node: (a: any): a is TresObject =>
+        a && (a.isObject3D || a.isBufferGeometry || a.isMaterial || a.isFog),
+    },
+    createElement: (tag, _isSVG, _anchor, props) => {
+      if (!props) props = {}
+      if (!props.args) props.args = []
+
+      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.`,
+          )
+        }
+        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
+
+        if (props.position)
+          instance.position.set(
+            props.position[0],
+            props.position[1],
+            props.position[2],
+          )
+        if (props.rotation) {
+          const r = new THREE.Euler(
+            ...normalizeVectorFlexibleParam(props.rotation),
+          );
+          (<Object3D>instance).setRotationFromEuler(r)
+        }
+      }
+
+      // 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) => {
+      const parentObject = parent || context.scene.value
+
+      if (child?.isObject3D) {
+        if (child?.isCamera) {
+          if (!context.registerCamera)
+            throw 'could not find tres__registerCamera on scene\'s userData'
+
+          context.registerCamera(child as unknown as Camera)
+        }
+
+        if (
+          child
+          && supportedPointerEvents.some(eventName => child[eventName])
+        ) {
+          if (
+            !context.scene.value?.userData.tres__registerAtPointerEventHandler
+          )
+            throw 'could not find tres__registerAtPointerEventHandler on scene\'s userData'
+
+          context.scene.value?.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: TresObject) => {
+      if (node.isObject3D) {
+        const scene = context.scene?.value
+        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 'could not find tres__deregisterBlockingObjectAtPointerEventHandler on scene\'s userData'
+
+          scene?.userData.tres__deregisterBlockingObjectAtPointerEventHandler?.(
+            object as Object3D,
+          )
+
+          if (!deregisterAtPointerEventHandler)
+            throw '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 '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: TresObject, prop, prevValue, nextValue) => {
+      const scene = context.scene.value
+      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<typeof BufferAttribute>),
+          ),
+        )
+        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: TresObject) => node?.parent || null,
+  }
+}