From 10ced996518ecde864af7b6612c6999ddc34eaa9 Mon Sep 17 00:00:00 2001 From: Jaime Torrealba Date: Mon, 22 Dec 2025 15:05:10 +0000 Subject: [PATCH 1/6] feature(cientos): drag controls. This is a PR ported from the old cientos repo: https://github.com/Tresjs/cientos/pull/456 --- .../cientos/controls/DragControlsDemo.vue | 32 ++++++++++ .../src/router/routes/cientos/controls.ts | 5 ++ .../src/core/controls/DragControls.vue | 60 +++++++++++++++++++ packages/cientos/src/core/controls/index.ts | 2 + 4 files changed, 99 insertions(+) create mode 100644 apps/playground/src/pages/cientos/controls/DragControlsDemo.vue create mode 100644 packages/cientos/src/core/controls/DragControls.vue diff --git a/apps/playground/src/pages/cientos/controls/DragControlsDemo.vue b/apps/playground/src/pages/cientos/controls/DragControlsDemo.vue new file mode 100644 index 000000000..22b2a8273 --- /dev/null +++ b/apps/playground/src/pages/cientos/controls/DragControlsDemo.vue @@ -0,0 +1,32 @@ + + + diff --git a/apps/playground/src/router/routes/cientos/controls.ts b/apps/playground/src/router/routes/cientos/controls.ts index b8e660091..b323d803e 100644 --- a/apps/playground/src/router/routes/cientos/controls.ts +++ b/apps/playground/src/router/routes/cientos/controls.ts @@ -39,4 +39,9 @@ export const controlsRoutes = [ name: 'Helper', component: () => import('@/pages/cientos/controls/HelperDemo.vue'), }, + { + path: '/cientos/controls/drag-controls', + name: 'DragControls', + component: () => import('@/pages/cientos/controls/DragControlsDemo.vue'), + }, ] diff --git a/packages/cientos/src/core/controls/DragControls.vue b/packages/cientos/src/core/controls/DragControls.vue new file mode 100644 index 000000000..44df212c7 --- /dev/null +++ b/packages/cientos/src/core/controls/DragControls.vue @@ -0,0 +1,60 @@ + + + diff --git a/packages/cientos/src/core/controls/index.ts b/packages/cientos/src/core/controls/index.ts index 07887bf4d..625dbc381 100644 --- a/packages/cientos/src/core/controls/index.ts +++ b/packages/cientos/src/core/controls/index.ts @@ -6,10 +6,12 @@ import OrbitControls from './OrbitControls.vue' import PointerLockControls from './PointerLockControls.vue' import ScrollControls from './ScrollControls.vue' import TransformControls from './TransformControls.vue' +import DragControls from './DragControls.vue' export { BaseCameraControls, CameraControls, + DragControls, Helper, KeyboardControls, MapControls, From 1abb06e1c513c2c8f746bfd5b82839cae107fdff Mon Sep 17 00:00:00 2001 From: Jaime Torrealba Date: Mon, 22 Dec 2025 21:02:39 +0000 Subject: [PATCH 2/6] follows up, it look like drag controls can be use entirely with extend. --- .../cientos/controls/DragControlsDemo.vue | 4 +- .../cientos/controls/OrbitControlsDemo.vue | 3 +- .../src/core/controls/DragControls.vue | 52 ++++++++++--------- 3 files changed, 31 insertions(+), 28 deletions(-) diff --git a/apps/playground/src/pages/cientos/controls/DragControlsDemo.vue b/apps/playground/src/pages/cientos/controls/DragControlsDemo.vue index 22b2a8273..b5f20c37d 100644 --- a/apps/playground/src/pages/cientos/controls/DragControlsDemo.vue +++ b/apps/playground/src/pages/cientos/controls/DragControlsDemo.vue @@ -13,7 +13,7 @@ const gl = { const boxRef = shallowRef() -const onHover = (event: any) => { +function onDrag(event: any) { console.log('hover', event) } @@ -21,11 +21,11 @@ const onHover = (event: any) => { diff --git a/packages/cientos/src/core/controls/DragControls.ts b/packages/cientos/src/core/controls/DragControls.ts index 682054546..400229b1a 100644 --- a/packages/cientos/src/core/controls/DragControls.ts +++ b/packages/cientos/src/core/controls/DragControls.ts @@ -1,8 +1,7 @@ import { defineComponent, onUnmounted, shallowRef, watch } from 'vue' import type { Camera, Object3D, Vector3 } from 'three' import { useTres } from '@tresjs/core' -import { useEventListener } from '@vueuse/core' -import { DragControls as TypesDragControls } from 'three-stdlib' +import { DragControls as ThreeDragControls } from 'three-stdlib' export interface DragControlsProps { objects: Object3D[] @@ -45,85 +44,72 @@ export const DragControls = defineComponent({ setup(props, { expose, emit }) { const { camera: activeCamera, renderer } = useTres() - const controlsRef = shallowRef(null) + const controlsRef = shallowRef(null) const initialPositions = new WeakMap() watch( () => props.objects, - () => { - controlsRef.value = new TypesDragControls( - props.objects, + (objects) => { + const validObjects = (objects ?? []).filter(Boolean) + if (!validObjects.length) { return } + + if (controlsRef.value) { + controlsRef.value.dispose() + } + + controlsRef.value = new ThreeDragControls( + validObjects, props.camera || activeCamera.value!, props.domElement || renderer.domElement, ) - // Apply initial enabled state if provided - if (props.enabled !== undefined && controlsRef.value) { - (controlsRef.value as any).enabled = props.enabled + + if (props.enabled !== undefined) { + controlsRef.value.enabled = props.enabled } - addEventListeners() + + addEventListeners(controlsRef.value) }, { immediate: true }, ) - function addEventListeners() { - useEventListener(controlsRef.value as any, 'dragstart', (e: any) => { - const obj = e?.object as Object3D | undefined - if (obj) { - initialPositions.set(obj, obj.position.clone()) - } - emit('dragstart', controlsRef.value) + function addEventListeners(controls: ThreeDragControls) { + controls.addEventListener('dragstart', (e) => { + initialPositions.set(e.object, e.object.position.clone()) + emit('dragstart', controls) }) - useEventListener(controlsRef.value as any, 'drag', (e: any) => { - const obj = e?.object as Object3D | undefined - if (props.enabled === false && obj) { + controls.addEventListener('drag', (e) => { + const obj = e.object + if (props.enabled === false) { const origin = initialPositions.get(obj) - if (origin && obj) { + if (origin) { obj.position.set(origin.x, origin.y, origin.z) } } - if (obj && props.lock && props.lock !== 'none') { + if (props.lock && props.lock !== 'none') { const origin = initialPositions.get(obj) if (origin) { - if (props.lock === 'x') { - obj.position.x = origin.x - } - else if (props.lock === 'y') { - obj.position.y = origin.y - } - else if (props.lock === 'z') { - obj.position.z = origin.z - } + if (props.lock === 'x') { obj.position.x = origin.x } + else if (props.lock === 'y') { obj.position.y = origin.y } + else if (props.lock === 'z') { obj.position.z = origin.z } } } - - if (obj && props.dragLimits) { + if (props.dragLimits) { const [xLim, yLim, zLim] = props.dragLimits - if (xLim) { - obj.position.x = Math.max(Math.min(obj.position.x, xLim[1]), xLim[0]) - } - if (yLim) { - obj.position.y = Math.max(Math.min(obj.position.y, yLim[1]), yLim[0]) - } - if (zLim) { - obj.position.z = Math.max(Math.min(obj.position.z, zLim[1]), zLim[0]) - } + if (xLim) { obj.position.x = Math.max(Math.min(obj.position.x, xLim[1]), xLim[0]) } + if (yLim) { obj.position.y = Math.max(Math.min(obj.position.y, yLim[1]), yLim[0]) } + if (zLim) { obj.position.z = Math.max(Math.min(obj.position.z, zLim[1]), zLim[0]) } } - emit('drag', controlsRef.value) + emit('drag', controls) }) - useEventListener(controlsRef.value as any, 'dragend', (e: any) => { - const obj = e?.object as Object3D | undefined - if (obj) { - initialPositions.delete(obj) - } - emit('dragend', controlsRef.value) + controls.addEventListener('dragend', (e) => { + initialPositions.delete(e.object) + emit('dragend', controls) }) - useEventListener(controlsRef.value as any, 'hoveron', () => - emit('hoveron', controlsRef.value)) - useEventListener(controlsRef.value as any, 'hoveroff', () => - emit('hoveroff', controlsRef.value)) + controls.addEventListener('hoveron', () => emit('hoveron', controls)) + controls.addEventListener('hoveroff', () => emit('hoveroff', controls)) } onUnmounted(() => { @@ -134,4 +120,5 @@ export const DragControls = defineComponent({ expose({ instance: controlsRef }) }, + render() { return null }, }) From e8cb8bfe935fda16881c0677de46c1b9b298786c Mon Sep 17 00:00:00 2001 From: Jaime Torrealba Date: Tue, 14 Apr 2026 22:24:57 +0100 Subject: [PATCH 5/6] docs(cientos): add documentation and demo, for new dragControls --- .../app/components/controls/DragControls.vue | 77 ++++++++++++ .../content/2.api/2.controls/drag-controls.md | 92 ++++++++++++++ apps/playground/auto-imports.d.ts | 116 +++++++++--------- .../cientos/controls/DragControlsDemo.vue | 1 - 4 files changed, 227 insertions(+), 59 deletions(-) create mode 100644 apps/cientos-docs/app/components/controls/DragControls.vue create mode 100644 apps/cientos-docs/content/2.api/2.controls/drag-controls.md diff --git a/apps/cientos-docs/app/components/controls/DragControls.vue b/apps/cientos-docs/app/components/controls/DragControls.vue new file mode 100644 index 000000000..9a891c492 --- /dev/null +++ b/apps/cientos-docs/app/components/controls/DragControls.vue @@ -0,0 +1,77 @@ + + + + diff --git a/apps/cientos-docs/content/2.api/2.controls/drag-controls.md b/apps/cientos-docs/content/2.api/2.controls/drag-controls.md new file mode 100644 index 000000000..95608fa97 --- /dev/null +++ b/apps/cientos-docs/content/2.api/2.controls/drag-controls.md @@ -0,0 +1,92 @@ +--- +title: Drag Controls +description: Drag and drop 3D objects in your scene with pointer events +--- + +The [Drag Controls](https://threejs.org/docs/#examples/en/controls/DragControls) allow you to drag and move 3D objects in your scene using pointer events. You can optionally lock movement to a single axis, set drag limits per axis, and listen to drag lifecycle events. + +::SceneControlsWrapper + ::ControlsDragControls + :: +:: + +## Usage + +The `objects` prop accepts an array of `Object3D` instances. You can pass template refs directly — Vue will unwrap them automatically. + +```vue{3} + + + +``` + +::prose-warning +If you are using OrbitControls alongside DragControls, they will interfere with each other. Set `make-default` on **OrbitControls** to prevent conflicts while dragging. +:: + +## Props + +| Prop | Description | Default | +| :------------- | :------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | +| **objects** | Array of [Object3D](https://threejs.org/docs/index.html#api/en/core/Object3D) instances to make draggable. | `[]` | +| **camera** | The camera used for raycasting. Defaults to the scene's active camera. | `undefined` | +| **enabled** | If `false`, dragging is disabled and the object snaps back to its position on each drag event. | `true` | +| **lock** | Locks movement along one axis. Can be `'x'`, `'y'`, `'z'`, or `'none'`. | `'none'` | +| **dragLimits** | Per-axis position limits as `[[xMin, xMax], [yMin, yMax] \| undefined, [zMin, zMax] \| undefined]`. Pass `undefined` for axes with no limit. | `undefined` | +| **domElement** | The DOM element that listens for pointer events. | `undefined` | + +## Events + +| Event | Description | Payload | +| :------------ | :---------------------------------------------------- | :------------------ | +| **dragstart** | Fired when the user starts dragging an object. | `ThreeDragControls` | +| **drag** | Fired every frame while an object is being dragged. | `ThreeDragControls` | +| **dragend** | Fired when the user releases a dragged object. | `ThreeDragControls` | +| **hoveron** | Fired when the pointer moves over a draggable object. | `ThreeDragControls` | +| **hoveroff** | Fired when the pointer leaves a draggable object. | `ThreeDragControls` | + +## Tip +### Grid snapping + +There is no built-in snap prop, but you can implement snapping in the `@drag` event handler by rounding the object's position to the desired interval: + +```vue + + + +``` diff --git a/apps/playground/auto-imports.d.ts b/apps/playground/auto-imports.d.ts index 4b442093b..ac1561158 100644 --- a/apps/playground/auto-imports.d.ts +++ b/apps/playground/auto-imports.d.ts @@ -6,64 +6,64 @@ // biome-ignore lint: disable export {} declare global { - const EffectScope: typeof import('vue')['EffectScope'] - const computed: typeof import('vue')['computed'] - const createApp: typeof import('vue')['createApp'] - const customRef: typeof import('vue')['customRef'] - const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] - const defineComponent: typeof import('vue')['defineComponent'] - const effectScope: typeof import('vue')['effectScope'] - const getCurrentInstance: typeof import('vue')['getCurrentInstance'] - const getCurrentScope: typeof import('vue')['getCurrentScope'] - const getCurrentWatcher: typeof import('vue')['getCurrentWatcher'] - const h: typeof import('vue')['h'] - const inject: typeof import('vue')['inject'] - const isProxy: typeof import('vue')['isProxy'] - const isReactive: typeof import('vue')['isReactive'] - const isReadonly: typeof import('vue')['isReadonly'] - const isRef: typeof import('vue')['isRef'] - const isShallow: typeof import('vue')['isShallow'] - const markRaw: typeof import('vue')['markRaw'] - const nextTick: typeof import('vue')['nextTick'] - const onActivated: typeof import('vue')['onActivated'] - const onBeforeMount: typeof import('vue')['onBeforeMount'] - const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] - const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] - const onDeactivated: typeof import('vue')['onDeactivated'] - const onErrorCaptured: typeof import('vue')['onErrorCaptured'] - const onMounted: typeof import('vue')['onMounted'] - const onRenderTracked: typeof import('vue')['onRenderTracked'] - const onRenderTriggered: typeof import('vue')['onRenderTriggered'] - const onScopeDispose: typeof import('vue')['onScopeDispose'] - const onServerPrefetch: typeof import('vue')['onServerPrefetch'] - const onUnmounted: typeof import('vue')['onUnmounted'] - const onUpdated: typeof import('vue')['onUpdated'] - const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] - const provide: typeof import('vue')['provide'] - const reactive: typeof import('vue')['reactive'] - const readonly: typeof import('vue')['readonly'] - const ref: typeof import('vue')['ref'] - const resolveComponent: typeof import('vue')['resolveComponent'] - const shallowReactive: typeof import('vue')['shallowReactive'] - const shallowReadonly: typeof import('vue')['shallowReadonly'] - const shallowRef: typeof import('vue')['shallowRef'] - const toRaw: typeof import('vue')['toRaw'] - const toRef: typeof import('vue')['toRef'] - const toRefs: typeof import('vue')['toRefs'] - const toValue: typeof import('vue')['toValue'] - const triggerRef: typeof import('vue')['triggerRef'] - const unref: typeof import('vue')['unref'] - const useAttrs: typeof import('vue')['useAttrs'] - const useCssModule: typeof import('vue')['useCssModule'] - const useCssVars: typeof import('vue')['useCssVars'] - const useId: typeof import('vue')['useId'] - const useModel: typeof import('vue')['useModel'] - const useSlots: typeof import('vue')['useSlots'] - const useTemplateRef: typeof import('vue')['useTemplateRef'] - const watch: typeof import('vue')['watch'] - const watchEffect: typeof import('vue')['watchEffect'] - const watchPostEffect: typeof import('vue')['watchPostEffect'] - const watchSyncEffect: typeof import('vue')['watchSyncEffect'] + const EffectScope: typeof import('vue').EffectScope + const computed: typeof import('vue').computed + const createApp: typeof import('vue').createApp + const customRef: typeof import('vue').customRef + const defineAsyncComponent: typeof import('vue').defineAsyncComponent + const defineComponent: typeof import('vue').defineComponent + const effectScope: typeof import('vue').effectScope + const getCurrentInstance: typeof import('vue').getCurrentInstance + const getCurrentScope: typeof import('vue').getCurrentScope + const getCurrentWatcher: typeof import('vue').getCurrentWatcher + const h: typeof import('vue').h + const inject: typeof import('vue').inject + const isProxy: typeof import('vue').isProxy + const isReactive: typeof import('vue').isReactive + const isReadonly: typeof import('vue').isReadonly + const isRef: typeof import('vue').isRef + const isShallow: typeof import('vue').isShallow + const markRaw: typeof import('vue').markRaw + const nextTick: typeof import('vue').nextTick + const onActivated: typeof import('vue').onActivated + const onBeforeMount: typeof import('vue').onBeforeMount + const onBeforeUnmount: typeof import('vue').onBeforeUnmount + const onBeforeUpdate: typeof import('vue').onBeforeUpdate + const onDeactivated: typeof import('vue').onDeactivated + const onErrorCaptured: typeof import('vue').onErrorCaptured + const onMounted: typeof import('vue').onMounted + const onRenderTracked: typeof import('vue').onRenderTracked + const onRenderTriggered: typeof import('vue').onRenderTriggered + const onScopeDispose: typeof import('vue').onScopeDispose + const onServerPrefetch: typeof import('vue').onServerPrefetch + const onUnmounted: typeof import('vue').onUnmounted + const onUpdated: typeof import('vue').onUpdated + const onWatcherCleanup: typeof import('vue').onWatcherCleanup + const provide: typeof import('vue').provide + const reactive: typeof import('vue').reactive + const readonly: typeof import('vue').readonly + const ref: typeof import('vue').ref + const resolveComponent: typeof import('vue').resolveComponent + const shallowReactive: typeof import('vue').shallowReactive + const shallowReadonly: typeof import('vue').shallowReadonly + const shallowRef: typeof import('vue').shallowRef + const toRaw: typeof import('vue').toRaw + const toRef: typeof import('vue').toRef + const toRefs: typeof import('vue').toRefs + const toValue: typeof import('vue').toValue + const triggerRef: typeof import('vue').triggerRef + const unref: typeof import('vue').unref + const useAttrs: typeof import('vue').useAttrs + const useCssModule: typeof import('vue').useCssModule + const useCssVars: typeof import('vue').useCssVars + const useId: typeof import('vue').useId + const useModel: typeof import('vue').useModel + const useSlots: typeof import('vue').useSlots + const useTemplateRef: typeof import('vue').useTemplateRef + const watch: typeof import('vue').watch + const watchEffect: typeof import('vue').watchEffect + const watchPostEffect: typeof import('vue').watchPostEffect + const watchSyncEffect: typeof import('vue').watchSyncEffect } // for type re-export declare global { diff --git a/apps/playground/src/pages/cientos/controls/DragControlsDemo.vue b/apps/playground/src/pages/cientos/controls/DragControlsDemo.vue index 2777ec54c..7b51fba7f 100644 --- a/apps/playground/src/pages/cientos/controls/DragControlsDemo.vue +++ b/apps/playground/src/pages/cientos/controls/DragControlsDemo.vue @@ -98,7 +98,6 @@ function onHoverOff(e: ThreeDragControls) { @hoveroff="onHoverOff" /> Date: Tue, 14 Apr 2026 22:26:52 +0100 Subject: [PATCH 6/6] remove unnecessary watcher --- .../src/pages/cientos/controls/DragControlsDemo.vue | 9 --------- 1 file changed, 9 deletions(-) diff --git a/apps/playground/src/pages/cientos/controls/DragControlsDemo.vue b/apps/playground/src/pages/cientos/controls/DragControlsDemo.vue index 7b51fba7f..3ebebdd7a 100644 --- a/apps/playground/src/pages/cientos/controls/DragControlsDemo.vue +++ b/apps/playground/src/pages/cientos/controls/DragControlsDemo.vue @@ -38,15 +38,6 @@ const { lock, enabled, dragLimitsMin, dragLimitsMax } = useControls({ }, }) -watch( - () => controlsRef.value?.instance, - (newVal) => { - if (newVal) { - console.log('instance', newVal) - } - }, -) - function onDragStart(e: ThreeDragControls) { console.log('dragstart', e) }