Prechádzať zdrojové kódy

Merge pull request #12 from Tresjs/feature/3-transform-pivot-controls-for-cientos

feat(cientos): transform controls for cientos
Alvaro Saburido 2 rokov pred
rodič
commit
489d158f6c

+ 2 - 6
.eslintrc.js

@@ -7,12 +7,7 @@ module.exports = {
   },
   parser: 'vue-eslint-parser',
   plugins: ['vue', '@typescript-eslint'],
-  extends: [
-    'eslint:recommended',
-    'plugin:@typescript-eslint/recommended',
-    'plugin:vue/vue3-recommended',
-    'prettier',
-  ],
+  extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:vue/vue3-recommended', 'prettier'],
   parserOptions: {
     tsconfigRootDir: __dirname,
     parser: '@typescript-eslint/parser',
@@ -37,5 +32,6 @@ module.exports = {
     'vue/multi-word-component-names': 0,
     'vue/no-multiple-template-root': 'off',
     'vue/first-attribute-linebreak': 'off',
+    'vue/setup-compiler-macros': 0,
   },
 }

+ 4 - 1
docs/.vitepress/config.ts

@@ -64,7 +64,10 @@ export default defineConfig({
           },
           {
             text: 'Controls',
-            items: [{ text: 'OrbitControls', link: '/cientos/controls/orbit-controls' }],
+            items: [
+              { text: 'OrbitControls', link: '/cientos/controls/orbit-controls' },
+              { text: 'TransformControls', link: '/cientos/controls/transform-controls' },
+            ],
           },
           {
             text: 'Loaders',

+ 91 - 0
docs/cientos/controls/transform-controls.md

@@ -0,0 +1,91 @@
+# Transform Controls
+
+The [Transform Controls](https://threejs.org/docs/#examples/en/controls/TransformControls) are a set of three gizmos that can be used to translate, rotate and scale objects in the scene. It adapts a similar interaction model of DCC tools like Blender
+
+<StackBlitzEmbed projectId="tresjs-transform-controls" />
+
+## Usage
+
+To use the Transform Controls, simply add the `TransformControls` component to your scene. You can pass the `templateRef`of the instance you want to control as a prop.
+
+```vue{2,6,8}
+<script setup>
+const boxRef = shallowRef()
+</script>
+<template>
+  <TresCanvas>
+    <OrbitControls make-default />
+    <TresScene>
+        <TransformControls :object="boxRef" />
+        <TresMesh ref="boxRef" :position="[0, 4, 0]" cast-shadow>
+            <TresBoxGeometry :args="[1.5, 1.5, 1.5]" />
+            <TresMeshToonMaterial color="#4F4F4F" />
+        </TresMesh>
+    </TresScene>
+  </TresCanvas>
+</template>
+```
+
+::: warning
+If you are using other controls ([OrbitControls](/cientos/controls/orbit-controls)) they will interfere with each other when dragging. To avoid this, you can set the `makeDefault` prop to `true` on the **OrbitControls**.
+:::
+
+## Modes
+
+The Transform Controls can be used in three different modes:
+
+### Translate
+
+![Translate](/cientos/transform-controls-translate.png)
+
+The default mode allows you to move the object around the scene.
+
+```html
+<TransformControls mode="translate" :object="sphereRef" />
+```
+
+### Rotate
+
+![Rotate](/cientos/transform-controls-rotate.png)
+
+The rotate mode allows you to rotate the object around the scene.
+
+```html
+<TransformControls mode="rotate" :object="boxRef" />
+```
+
+### Scale
+
+![Scale](/cientos/transform-controls-scale.png)
+
+The scale mode allows you to scale the object around the scene.
+
+```html
+<TransformControls mode="scale" :object="sphereRef" />
+```
+
+## Props
+
+| Prop                | Description                                                                                   | Default     |
+| :------------------ | :-------------------------------------------------------------------------------------------- | ----------- |
+|  **object**         | The instance [Object3D](https://threejs.org/docs/index.html#api/en/core/Object3D) to control. | `null`      |
+| **mode**            | The mode of the controls. Can be `translate`, `rotate` or `scale`.                            | `translate` |
+| **enabled**         | If `true`, the controls will be enabled.                                                      | `true`      |
+| **axis**            | The axis to use for the controls. Can be `X`, `Y`, `Z`, `XY`, `YZ`, `XZ`, `XYZ`.              | `XYZ`       |
+| **space**           | The space to use for the controls. Can be `local` or `world`.                                 | `local`     |
+| **size**            | The size of the controls.                                                                     | `1`         |
+| **translationSnap** | The distance to snap to when translating. (World units)                                       | `null`      |
+| **rotationSnap**    | The angle to snap to when rotating. (Radians)                                                 | `null`      |
+| **scaleSnap**       | The scale to snap to when scaling.                                                            | `null`      |
+| **showX**           | If `true`, the X-axis helper will be shown.                                                   | `true`      |
+| **showY**           | If `true`, the Y-axis helper will be shown.                                                   | `true`      |
+| **showZ**           | If `true`, the Z-axis helper will be shown.                                                   | `true`      |
+
+<style scoped>
+img {
+    aspect-ratio: 16/9;
+    object-fit: cover;
+    object-position: top;
+    border-radius: 8px;
+}
+</style>

BIN
docs/public/cientos/transform-controls-rotate.png


BIN
docs/public/cientos/transform-controls-scale.png


BIN
docs/public/cientos/transform-controls-translate.png


+ 13 - 2
packages/cientos/src/core/OrbitControls.vue

@@ -1,11 +1,12 @@
 <script lang="ts" setup>
+import { useTres } from '@tresjs/core'
 import { Camera, Vector3, WebGLRenderer } from 'three'
 import { OrbitControls } from 'three-stdlib'
-import { inject, ref, type Ref } from 'vue'
+import { inject, ref, watch, type Ref } from 'vue'
 
 import { useCientos } from './useCientos'
 
-withDefaults(
+const props = withDefaults(
   defineProps<{
     makeDefault?: boolean
     camera?: Camera
@@ -18,12 +19,22 @@ withDefaults(
   },
 )
 
+const { setState } = useTres()
+
 const controls = ref(null)
 const camera = inject<Ref<Camera>>('camera')
 const renderer = inject<Ref<WebGLRenderer>>('renderer')
 
 const { extend } = useCientos()
 extend({ OrbitControls })
+
+watch(controls, value => {
+  if (value && props.makeDefault) {
+    setState('controls', value)
+  } else {
+    setState('controls', null)
+  }
+})
 </script>
 
 <template>

+ 119 - 0
packages/cientos/src/core/TransformControls.vue

@@ -0,0 +1,119 @@
+<script setup lang="ts">
+import { useTres } from '@tresjs/core'
+import { Camera, Object3D, Scene, WebGLRenderer, type Event } from 'three'
+import { TransformControls as TransformControlsImp } from 'three-stdlib'
+import { inject, computed, type Ref, unref, watch, shallowRef, ShallowRef, onUnmounted } from 'vue'
+import { pick } from '../utils'
+
+const props = withDefaults(
+  defineProps<{
+    object: Object3D
+    mode?: string
+    enabled?: boolean
+    axis?: 'X' | 'Y' | 'Z' | 'XY' | 'YZ' | 'XZ' | 'XYZ'
+    translationSnap?: number
+    rotationSnap?: number
+    scaleSnap?: number
+    space?: 'local' | 'world'
+    size?: number
+    showX?: boolean
+    showY?: boolean
+    showZ?: boolean
+  }>(),
+  {
+    enabled: true,
+  },
+)
+
+const emit = defineEmits(['dragging', 'change', 'mouseDown', 'mouseUp', 'objectChange'])
+
+let controls: ShallowRef<TransformControlsImp | undefined> = shallowRef()
+
+const camera = inject<Ref<Camera>>('camera')
+const renderer = inject<Ref<WebGLRenderer>>('renderer')
+const scene = inject<Ref<Scene>>('local-scene')
+
+const transformProps = computed(() =>
+  pick(props, [
+    'enabled',
+    'axis',
+    'mode',
+    'translationSnap',
+    'rotationSnap',
+    'scaleSnap',
+    'space',
+    'size',
+    'showX',
+    'showY',
+    'showZ',
+  ]),
+)
+const { state } = useTres()
+
+const onChange = () => emit('change', controls.value)
+const onMouseDown = () => emit('mouseDown', controls.value)
+const onMouseUp = () => emit('mouseUp', controls.value)
+const onObjectChange = () => emit('objectChange', controls.value)
+
+const onDragingChange = (e: Event) => {
+  if (state.controls) state.controls.enabled = !e.value
+  emit('dragging', e.value)
+}
+
+function addEventListeners(controls: TransformControlsImp) {
+  controls.addEventListener('dragging-changed', onDragingChange)
+  controls.addEventListener('change', onChange)
+  controls.addEventListener('mouseDown', onMouseDown)
+  controls.addEventListener('mouseUp', onMouseUp)
+  controls.addEventListener('objectChange', onObjectChange)
+}
+
+watch(
+  () => props.object,
+  () => {
+    if (camera?.value && renderer?.value && scene?.value && props.object) {
+      controls.value = new TransformControlsImp(camera.value, unref(renderer).domElement)
+
+      controls.value.attach(unref(props.object))
+      scene.value.add(unref(controls) as TransformControlsImp)
+
+      addEventListeners(unref(controls) as TransformControlsImp)
+    }
+  },
+  {
+    deep: true,
+  },
+)
+
+watch(
+  [transformProps, controls],
+  // TODO: properly type this
+  ([value, controlsValue]: [any, any]) => {
+    if (value && controlsValue) {
+      for (const key in value) {
+        const methodName = `set${key[0].toUpperCase()}${key.slice(1)}`
+
+        if (typeof controlsValue[methodName] === 'function' && value[key] !== undefined) {
+          ;(controlsValue[methodName] as (param: any) => void)(value[key])
+        }
+      }
+    }
+  },
+  {
+    immediate: true,
+  },
+)
+
+onUnmounted(() => {
+  if (controls.value) {
+    controls.value.removeEventListener('dragging-changed', onDragingChange)
+    controls.value.removeEventListener('change', onChange)
+    controls.value.removeEventListener('mouseDown', onMouseDown)
+    controls.value.removeEventListener('mouseUp', onMouseUp)
+    controls.value.removeEventListener('objectChange', onObjectChange)
+  }
+})
+</script>
+<template>
+  <slot />
+</template>

+ 3 - 1
packages/cientos/src/index.ts

@@ -1,7 +1,9 @@
 import OrbitControls from './core/OrbitControls.vue'
+import TransformControls from './core/TransformControls.vue'
 import { useTweakPane } from './core/useTweakPane'
 import { GLTFModel } from './core/useGLTF/component'
 import Text3D from './core/Text3D.vue'
 
 export * from './core/useGLTF'
-export { OrbitControls, useTweakPane, GLTFModel, Text3D }
+
+export { OrbitControls, TransformControls, useTweakPane, GLTFModel, Text3D }

+ 10 - 0
packages/cientos/src/utils/index.ts

@@ -0,0 +1,10 @@
+// Update the function signature to explicitly specify the type of the props parameter
+export function pick<T extends object, K extends keyof T>(obj: T, props: K[]): Pick<T, K> {
+  const pickedProperties = {} as Pick<T, K>
+  for (const prop of props) {
+    if (Object.prototype.hasOwnProperty.call(obj, prop)) {
+      pickedProperties[prop] = obj[prop]
+    }
+  }
+  return pickedProperties
+}

+ 15 - 9
packages/tres/src/components/TheExperience.vue

@@ -13,8 +13,8 @@ import {
   ACESFilmicToneMapping,
   CustomToneMapping,
 } from 'three'
-import { reactive } from 'vue'
-import { OrbitControls, useTweakPane } from '../../../cientos/src/'
+import { reactive, ref } from 'vue'
+import { OrbitControls, useTweakPane, TransformControls } from '../../../cientos/src/'
 import { TresCanvas } from '../core/useRenderer/component'
 /* import { OrbitControls, GLTFModel } from '@tresjs/cientos' */
 
@@ -28,6 +28,9 @@ const state = reactive({
   toneMapping: NoToneMapping,
 })
 
+const boxRef = ref()
+const sphereRef = ref()
+
 const { pane } = useTweakPane()
 
 pane.addInput(state, 'clearColor', {
@@ -93,25 +96,28 @@ pane
 <template>
   <TresCanvas v-bind="state">
     <TresPerspectiveCamera :position="[5, 5, 5]" :fov="45" :near="0.1" :far="1000" :look-at="[-8, 3, -3]" />
+    <OrbitControls make-default />
     <TresScene>
-      <OrbitControls />
       <TresAmbientLight :intensity="0.5" />
-      <TresMesh :position="[-2, 6, 0]" :rotation="[0, Math.PI, 0]" cast-shadow>
+      <!--  <TresMesh :position="[-2, 6, 0]" :rotation="[0, Math.PI, 0]" cast-shadow>
         <TresConeGeometry :args="[1, 1.5, 3]" />
         <TresMeshToonMaterial color="#82DBC5" />
-      </TresMesh>
-      <TresMesh :position="[0, 4, 0]" cast-shadow>
+      </TresMesh> -->
+      <!-- <TransformControls :object="boxRef" mode="rotate" />
+      <TresMesh ref="boxRef" :position="[0, 4, 0]" cast-shadow>
         <TresBoxGeometry :args="[1.5, 1.5, 1.5]" />
         <TresMeshToonMaterial color="#4F4F4F" />
-      </TresMesh>
-      <TresMesh :position="[2, 2, 0]" cast-shadow>
+      </TresMesh> -->
+      <TransformControls mode="scale" :object="sphereRef" />
+
+      <TresMesh ref="sphereRef" :position="[0, 4, 0]" cast-shadow>
         <TresSphereGeometry />
         <TresMeshToonMaterial color="#FBB03B" />
       </TresMesh>
       <TresDirectionalLight :position="[0, 8, 4]" :intensity="0.7" cast-shadow />
       <TresMesh :rotation="[-Math.PI / 2, 0, 0]" receive-shadow>
         <TresPlaneGeometry :args="[10, 10, 10, 10]" />
-        <TresMeshToonMaterial :color="floorTeal" />
+        <TresMeshToonMaterial />
       </TresMesh>
       <TresDirectionalLight :position="[0, 2, 4]" :intensity="1" cast-shadow />
     </TresScene>

+ 31 - 0
packages/tres/src/examples/cientos/controls/TransformControls.story.vue

@@ -0,0 +1,31 @@
+<script setup lang="ts">
+import { TransformControls } from '@tresjs/cientos'
+/* import { TransformControls, useTweakPane } from '../../../../../cientos/src' */
+import { reactive, ref } from 'vue'
+
+const boxRef = ref()
+</script>
+<template>
+  <Story title="cientos/controls/TransformControls">
+    <Variant title="playground">
+      <TresCanvas clear-color="#82DBC5" shadows alpha>
+        <TresPerspectiveCamera
+          :position="[5, 5, 5]"
+          :look-at="[0, 0, 0]"
+          :fov="45"
+          :aspect="1"
+          :near="0.1"
+          :far="1000"
+        />
+        <TresScene>
+          <TransformControls :object="boxRef" />
+          <TresMesh ref="boxRef" cast-shadow>
+            <TresBoxGeometry :args="[1.5, 1.5, 1.5]" />
+            <TresMeshToonMaterial color="#4F4F4F" />
+          </TresMesh>
+          <TresDirectionalLight :position="[0, 8, 4]" :intensity="0.7" cast-shadow />
+        </TresScene>
+      </TresCanvas>
+    </Variant>
+  </Story>
+</template>