diff --git a/.vscode/settings.json b/.vscode/settings.json index 62190d4..8b168d0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -50,5 +50,8 @@ }, "[vue]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" + }, + "[typescript]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" } } diff --git a/docs/components/collider.md b/docs/components/collider.md index ebed629..d841bb4 100644 --- a/docs/components/collider.md +++ b/docs/components/collider.md @@ -16,28 +16,82 @@ Be aware that the event will be emitted by the `RigidBody` parent ## Props -| Prop | Description | Default | -| :-------------- | :------------------------------------------------------------------------------------------------------------ | --------- | -| **shape** | shape of the collider | `cuboid` | -| **args** | The half-sizes of the collider shapes | `[1,1,1]` | -| **object** | Required for certain shapes like `trimesh`, `hull`, `heightfield`. | | -| **friction** | The friction coefficient of this collider. (automatic-collider) | `0.5` | -| **mass** | Mass of the collider. (automatic-collider) | `1` | -| **density** | Restitution controls how elastic (aka. bouncy) a contact is. (automatic-collider) | `0` | -| **restitution** | The collider density. If non-zero the collider's mass and angular inertia will be added. (automatic-collider) | `1` | -| **activeCollision** | To set the collider receiver/emitter collision events | `false` | -| **activeCollisionTypes** | Type of the collision event. | `ActiveCollisionTypes.DEFAULT` | -| **collisionGroups** | To specify collision groups. | `undefined` | - -:::info -You can access the [Collider](https://rapier.rs/docs/user_guides/javascript/colliders) instance -which offers full control over all the properties & methods available -by using [Template refs](https://vuejs.org/guide/essentials/template-refs.html#template-refs). -::: +| Prop | Description | Default | +| :----------------------- | :--------------------------- | :------- | +| **shape** | shape of the collider | `cuboid` | +| **args** | The half-sizes of the collider shapes | `[1,1,1]` | +| **object** | Required for certain shapes like `trimesh`, `hull`, `heightfield`. | | +| **friction** | The friction coefficient of this collider. (automatic-collider) | `0.5` | +| **mass** | Mass of the collider. (automatic-collider) | `1` | +| **density** | Restitution controls how elastic (aka. bouncy) a contact is. (automatic-collider) | `0` | +| **restitution** | The collider density. If non-zero the collider's mass and angular inertia will be added. (automatic-collider). | `1` | +| **activeCollision** | To set the collider receiver/emitter collision events | `false` | +| **activeCollisionTypes** | Type of the collision event. | `ActiveCollisionTypes.DEFAULT` | +| **collisionGroups** | To specify collision groups. | `undefined` | +| **sensor** | Set the collider as senor. More details [here](#sensor). | `undefined` | -## Expose object +## Events + +The `Collider` component comes with a set of useful events allowing actions based on collisions or intersections (aka sensor). + +### Sensor + +The **Sensor** feature allows events to be triggered when there's an intersection or in other words, when the collider is traversed by another collider. + +The traversed `Collider` (or the collider that will trigger events), is the sensor and should set the `activeCollision` and `sensor` properties to `true`. +By passing the above properties, the collider will no longer be affected by the physics law and will now start triggering the intersection events. + +> ::: info +> **@intersection-enter**: When another collider starts to traverse the *sensor* +> **@intersection-exit**: When another collider leave the *sensor* +> +> ℹ️ Note that you can directly pass the events to the **`RigidBody`** for **auto-colliders**. +> ::: + +```vue + + + + + + + + + ``` + + + +> ::: info +> You can access the [Collider](https://rapier.rs/docs/user_guides/javascript/colliders) instance +> which offers full control over all the properties & methods available +> by using [Template refs](https://vuejs.org/guide/essentials/template-refs.html#template-refs). +> ::: + +## Expose object + +```md { instance, colliderDesc, diff --git a/playground/src/pages/basics/SensorDemo.vue b/playground/src/pages/basics/SensorDemo.vue new file mode 100644 index 0000000..1a94fa9 --- /dev/null +++ b/playground/src/pages/basics/SensorDemo.vue @@ -0,0 +1,120 @@ + + + diff --git a/playground/src/router/routes/basics.ts b/playground/src/router/routes/basics.ts index fdd1696..ddbdaf3 100644 --- a/playground/src/router/routes/basics.ts +++ b/playground/src/router/routes/basics.ts @@ -34,4 +34,9 @@ export const basicsRoutes = [ name: 'Collision', component: () => import('../../pages/basics/CollisionDemo.vue'), }, + { + path: '/basics/sensor', + name: 'Sensor', + component: () => import('../../pages/basics/SensorDemo.vue'), + }, ] diff --git a/src/components/Debug.vue b/src/components/Debug.vue index c63829b..061a76a 100644 --- a/src/components/Debug.vue +++ b/src/components/Debug.vue @@ -11,7 +11,7 @@ const { onBeforeRender } = useLoop() const lineSegmentsRef = ref(null) onBeforeRender(() => { - if (!world || !lineSegmentsRef.value) { return } + if (!world || !lineSegmentsRef.value?.geometry?.boundingSphere) { return } const buffers = world.debugRender() diff --git a/src/components/Physics.vue b/src/components/Physics.vue index 386e783..6f8d836 100644 --- a/src/components/Physics.vue +++ b/src/components/Physics.vue @@ -4,10 +4,15 @@ import { useLoop, useTresContext } from '@tresjs/core' import { Vector3 } from 'three' import { watch } from 'vue' import type { VectorCoordinates } from '@tresjs/core' -import { useRapierContextProvider } from '../composables/useRapier' -import { GRAVITY } from '../constants/physics' +import { useRapierContextProvider } from '../composables' +import { GRAVITY } from '../constants' -import { collisionEmisor, get3DGroupFromSource, getSourceFromColliderHandle } from '../utils' +import { + collisionEmisor, + emitIntersection, + get3DGroupFromSource, + getSourceFromColliderHandle, +} from '../utils' import Debug from './Debug.vue' import type { PhysicsProps } from '../types' @@ -57,13 +62,22 @@ onBeforeRender(() => { const source2 = getSourceFromColliderHandle(world, handle2) const group1 = get3DGroupFromSource(source1, scene) const group2 = get3DGroupFromSource(source2, scene) - if (group1 && group2) { - collisionEmisor( - { object: group1, context: source1 }, - { object: group2, context: source2 }, - started, - ) + + if (!group1 || !group2) { + return } + + collisionEmisor( + { object: group1, context: source1 }, + { object: group2, context: source2 }, + started, + ) + + emitIntersection( + { object: group2, context: source2 }, + { object: group1, context: source1 }, + started && world.intersectionPair(source1.collider, source2.collider), + ) }) }) diff --git a/src/components/RigidBody.vue b/src/components/RigidBody.vue index fe7fc94..2963510 100644 --- a/src/components/RigidBody.vue +++ b/src/components/RigidBody.vue @@ -2,14 +2,26 @@ import { ActiveCollisionTypes } from '@dimforge/rapier3d-compat' import { useLoop } from '@tresjs/core' import { Object3D } from 'three' -import { nextTick, onUnmounted, onUpdated, provide, shallowRef, watch } from 'vue' - +import { + nextTick, + onUnmounted, + onUpdated, + provide, + shallowRef, + watch, +} from 'vue' import type { ShallowRef } from 'vue' + import { useRapierContext } from '../composables' import { createColliderPropsFromObject, createRigidBody } from '../core' import { makePropsWatcherRB } from '../utils/props' import { BaseCollider } from './colliders' -import type { ColliderProps, ExposedRigidBody, RigidBodyContext, RigidBodyProps } from '../types' +import type { + ColliderProps, + ExposedRigidBody, + RigidBodyContext, + RigidBodyProps, +} from '../types' const props = withDefaults(defineProps>(), { type: 'dynamic', @@ -19,22 +31,25 @@ const props = withDefaults(defineProps>(), { linearDamping: 0, angularDamping: 0, dominanceGroup: 0, + lockTranslations: false, + lockRotations: false, + enableCcd: false, linvel: () => ({ x: 0, y: 0, z: 0 }), angvel: () => ({ x: 0, y: 0, z: 0 }), enabledRotations: () => [true, true, true], enabledTranslations: () => [true, true, true], - lockTranslations: false, - lockRotations: false, - enableCcd: false, - // Automatic collider props + + // AUTOMATIC COLLIDERS PROPS friction: 0.5, mass: 1, restitution: 0, density: 1, activeCollision: false, activeCollisionTypes: ActiveCollisionTypes.DEFAULT, - collisionGroups: undefined, // this is not working + collisionGroups: undefined, // TODO: Make the `collisionGroups` (Not working yet). + sensor: false, }) + const { onBeforeRender } = useLoop() const { world } = useRapierContext() @@ -70,13 +85,23 @@ watch(bodyGroup, async (group) => { } if (props.collider !== false) { - const colliders = [] + const collidersProps: ColliderProps[] = [] for (const child of group.children) { - colliders.push(createColliderPropsFromObject(child, props.collider)) + const _props = createColliderPropsFromObject(child, props.collider) + _props.friction = props.friction + _props.mass = props.mass + _props.restitution = props.restitution + _props.density = props.density + _props.activeCollision = props.activeCollision + _props.activeCollisionTypes = props.activeCollisionTypes + _props.collisionGroups = props.collisionGroups + _props.sensor = props.sensor + + collidersProps.push(_props) } - autoColliderProps.value = colliders + autoColliderProps.value = collidersProps } instance.value = newPhysicsState.rigidBody @@ -84,7 +109,6 @@ watch(bodyGroup, async (group) => { bodyContext.value = newPhysicsState }, { once: true }) -// PROPS makePropsWatcherRB(props, [ 'gravityScale', 'additionalMass', @@ -146,13 +170,14 @@ onUnmounted(() => { :shape="_props.shape" :args="_props.args" :object="_props.object" - :friction="props.friction" - :mass="props.mass" - :restitution="props.restitution" - :density="props.density" - :activeCollision="props.activeCollision" - :activeCollisionTypes="activeCollisionTypes" - :collisionGroups="collisionGroups" + :friction="_props.friction" + :mass="_props.mass" + :restitution="_props.restitution" + :density="_props.density" + :activeCollision="_props.activeCollision" + :activeCollisionTypes="_props.activeCollisionTypes" + :collisionGroups="_props.collisionGroups" + :sensor="_props.sensor" /> diff --git a/src/components/colliders/BaseCollider.vue b/src/components/colliders/BaseCollider.vue index dedcbd2..aa9d639 100644 --- a/src/components/colliders/BaseCollider.vue +++ b/src/components/colliders/BaseCollider.vue @@ -1,10 +1,10 @@ diff --git a/src/types/collider.ts b/src/types/collider.ts index 35c60f1..1233360 100644 --- a/src/types/collider.ts +++ b/src/types/collider.ts @@ -22,7 +22,7 @@ export type ColliderShape = export interface ColliderProps { /** @description Set the {@link Collider} shape. */ - shape: ColliderShape + shape?: ColliderShape /** * @description Shape based {@link TresObject3D}. @@ -83,6 +83,11 @@ export interface ColliderProps { * @default undefined */ collisionGroups?: undefined | number + /** + * @description Whether this collider is a sensor. + * @default undefined + */ + sensor?: boolean } export interface ExposedCollider { diff --git a/src/types/collision.ts b/src/types/collision.ts index ee8ab5a..e940c3b 100644 --- a/src/types/collision.ts +++ b/src/types/collision.ts @@ -1,14 +1,14 @@ import type { Collider, RigidBody } from '@dimforge/rapier3d-compat' -import type { Object3D } from 'three' +import type { TresVNodeObject } from './object' export interface CollisionSource { collider: Collider rigidBody: RigidBody | undefined }; -export interface sourceTarget { - object: Object3D +export interface SourceTarget { + object: TresVNodeObject context: CollisionSource } -export type collisionType = 'enter' | 'exit' +export type CollisionType = 'enter' | 'exit' diff --git a/src/types/object.ts b/src/types/object.ts index cf7a9e4..1f22b32 100644 --- a/src/types/object.ts +++ b/src/types/object.ts @@ -1,3 +1,19 @@ +import type { TresObject } from '@tresjs/core' +import type { SetupContext, VNode } from 'vue' + +/** @description approximate {@link VNode} representation. */ +export type TresVNode = Omit & { + __vnode: VNode & { + children: Array + ctx: SetupContext + } + children: Array + ctx: SetupContext +} + +/** @description approximate {@link TresObject} representation. */ +export type TresVNodeObject = TresObject & { __vnode: TresVNode } + /** @description Utility type to exclude properties with the type `never` */ export type NonNever = { [K in keyof T as T[K] extends never ? never : K]: T[K]; @@ -8,4 +24,5 @@ export type Methods = NonNever<{ [K in keyof T]: T[K] extends (...args: any[]) => any ? T[K] : never; }> +/** @description Utility picking methods/functions withing object. */ export type CallableProps = Record, (...args: any[]) => unknown> diff --git a/src/types/rigid-body.ts b/src/types/rigid-body.ts index 3b12c55..5e45aed 100644 --- a/src/types/rigid-body.ts +++ b/src/types/rigid-body.ts @@ -1,7 +1,13 @@ -import type { ActiveCollisionTypes, Collider, ColliderDesc, RigidBody, RigidBodyDesc, World } from '@dimforge/rapier3d-compat' +import type { + Collider, + ColliderDesc, + RigidBody, + RigidBodyDesc, + World, +} from '@dimforge/rapier3d-compat' import type { TresObject3D, TresVector3, VectorCoordinates } from '@tresjs/core' -import type { ColliderShape } from './collider' +import type { ColliderProps, ColliderShape } from './collider' /** @description Tres Rapier supported `RigidBody` types. */ export type RigidBodyType = @@ -10,7 +16,17 @@ export type RigidBodyType = | 'kinematicVelocity' | 'fixed' -export interface RigidBodyProps { +export interface RigidBodyProps + extends Pick { /** @description Set the `RigidBody` type. */ type: RigidBodyType @@ -81,44 +97,6 @@ export interface RigidBodyProps { */ enableCcd?: boolean - // AUTOMATIC COLLIDERS - - /** - * @description The friction coefficient of this collider. - * @default 0.5 - */ - friction?: number - /** - * @description mass. - * @default 1 - */ - mass?: number - /** - * @description Restitution controls how elastic (aka. bouncy) a contact is. - * @default 0 - */ - restitution?: number - /** - * @description The collider density. If non-zero the collider's mass and angular inertia will be added. - * @default 1.0 - */ - density?: number - /** - * @description Enables collisions event. - * @default false - */ - activeCollision?: boolean - /** - * @description To set the collision type. - * @default ActiveCollisionTypes.DEFAULT - */ - activeCollisionTypes?: ActiveCollisionTypes.DEFAULT - /** - * @description To set the collision group. - * @default undefined - */ - collisionGroups?: undefined | number - } export interface ExposedRigidBody { diff --git a/src/utils/collisions.ts b/src/utils/collisions.ts index 62a777e..e269879 100644 --- a/src/utils/collisions.ts +++ b/src/utils/collisions.ts @@ -1,7 +1,7 @@ import type { ColliderHandle, World } from '@dimforge/rapier3d-compat' import type { Scene } from 'three' import type { Ref } from 'vue' -import type { CollisionSource, collisionType, sourceTarget } from '../types/collision' +import type { CollisionSource, CollisionType, SourceTarget, TresVNodeObject } from '../types' export const getSourceFromColliderHandle = (world: World, handle: ColliderHandle) => { const collider = world.getCollider(handle) @@ -20,16 +20,16 @@ export const getSourceFromColliderHandle = (world: World, handle: ColliderHandle export const get3DGroupFromSource = (source: CollisionSource, scene: Ref) => { const uuid = (source.rigidBody?.userData as { uuid?: string })?.uuid - const currentRigidBodyNode = scene.value.getObjectByProperty('uuid', uuid) + const currentRigidBodyNode = scene.value.getObjectByProperty('uuid', uuid) as TresVNodeObject return currentRigidBodyNode } export const collisionEmisor = ( - source: sourceTarget, - target: sourceTarget, + source: SourceTarget, + target: SourceTarget, started: boolean, ) => { - const collisionType: collisionType = started ? 'enter' : 'exit'; - (source.object as any)?.__vnode?.ctx?.emit?.(`collision-${collisionType}`, { source, target }) + const CollisionType: CollisionType = started ? 'enter' : 'exit' + source.object?.__vnode?.ctx?.emit?.(`collision-${CollisionType}`, { source, target }) } diff --git a/src/utils/index.ts b/src/utils/index.ts index 9dca4ae..07b1d3a 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,2 +1,4 @@ export * from './collider' export * from './collisions' +export * from './intersections' +export * from './props' diff --git a/src/utils/intersections.ts b/src/utils/intersections.ts new file mode 100644 index 0000000..2d05147 --- /dev/null +++ b/src/utils/intersections.ts @@ -0,0 +1,31 @@ +import type { ComponentInternalInstance } from 'vue' +import type { CollisionType, SourceTarget } from '../types' + +/** + * @description Make traversed {@link SourceTarget Source} emit an intersection event. + * + * Either `@intersection-start` or `intersection-exist` based on the {@link CollisionType}. + * + * @param source - The traversed source. + * @param target - The traversing target. + * @param started - Whether the intersection started or ended + */ +export const emitIntersection = ( + source: SourceTarget, + target: SourceTarget, + started: boolean, +) => { + const CollisionType: CollisionType = started ? 'enter' : 'exit' + const colliderNode = source.object?.__vnode?.children?.[1]?.children?.find(child => child?.component?.exposed?.instance?.value === source.context.collider) + + if ( + (source.object?.__vnode?.ctx as unknown as ComponentInternalInstance)?.props?.sensor + && colliderNode?.component === undefined + ) { + source.object?.__vnode?.ctx?.emit?.( + `intersection-${CollisionType}`, + { source, target }, + ) + } + colliderNode?.component?.emit?.(`intersection-${CollisionType}`, { source, target }) +}