diff --git a/package.json b/package.json index 7dbc988..731a9ce 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,27 @@ { - "name": "react-three-jolt-root", - "private": true, - "version": "0.0.0", - "repository": "https://github.com/isaac-mason/react-three-jolt.git", - "author": "Dennis Smolek", - "contributors": [ - "Dennis Smolek", - "Isaac Mason " - ], - "license": "MIT", - "packageManager": "yarn@4.1.1", - "workspaces": [ - "packages/*", - "apps/*" - ], - "scripts": { - "test": "yarn workspaces foreach -A -t run test", - "build": "yarn workspaces foreach -A -t run build", - "change": "yarn changeset", - "publish": "yarn build && yarn test && changeset publish", - "version": "yarn changeset version && yarn install --mode update-lockfile" - }, - "devDependencies": { - "@changesets/cli": "^2.26.2" - } + "name": "react-three-jolt-root", + "private": true, + "version": "0.0.0", + "repository": "https://github.com/isaac-mason/react-three-jolt.git", + "author": "Dennis Smolek", + "contributors": [ + "Dennis Smolek", + "Isaac Mason " + ], + "license": "MIT", + "packageManager": "yarn@4.1.1", + "workspaces": [ + "packages/*", + "apps/*" + ], + "scripts": { + "test": "yarn workspaces foreach -A -t run test", + "build": "yarn workspaces foreach -A -t run build", + "change": "yarn changeset", + "publish": "yarn build && yarn test && changeset publish", + "version": "yarn changeset version && yarn install --mode update-lockfile" + }, + "devDependencies": { + "@changesets/cli": "^2.26.2" + } } diff --git a/packages/react-three-jolt-addons/.prettierrc b/packages/react-three-jolt-addons/.prettierrc index 7bec3b4..f68f5a7 100644 --- a/packages/react-three-jolt-addons/.prettierrc +++ b/packages/react-three-jolt-addons/.prettierrc @@ -1,7 +1,7 @@ { - "singleQuote": true, - "trailingComma": "none", - "printWidth": 100, - "tabWidth": 4, - "bracketSameLine": true -} \ No newline at end of file + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "tabWidth": 4, + "bracketSameLine": true +} diff --git a/packages/react-three-jolt-addons/package.json b/packages/react-three-jolt-addons/package.json index f04ea54..e79dff8 100644 --- a/packages/react-three-jolt-addons/package.json +++ b/packages/react-three-jolt-addons/package.json @@ -1,71 +1,72 @@ { - "name": "@react-three/jolt-addons", - "description": "Addons for @react-three/jolt", - "keywords": [ - "physics", - "jolt-physics", - "react-three-fiber", - "react", - "three" - ], - "version": "0.0.1", - "author": "Dennis Smolek", - "contributors": [ - "Dennis Smolek", - "Isaac Mason " - ], - "license": "MIT", - "homepage": "https://github.com/pmndrs/react-three-jolt", - "bugs": { - "url": "https://github.com/pmndrs/react-three-jolt/issues" - }, - "main": "./dist/index.cjs", - "module": "./dist/index.mjs", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "require": "./dist/index.cjs" + "name": "@react-three/jolt-addons", + "description": "Addons for @react-three/jolt", + "keywords": [ + "physics", + "jolt-physics", + "react-three-fiber", + "react", + "three" + ], + "version": "0.0.1", + "author": "Dennis Smolek", + "contributors": [ + "Dennis Smolek", + "Isaac Mason " + ], + "license": "MIT", + "homepage": "https://github.com/pmndrs/react-three-jolt", + "bugs": { + "url": "https://github.com/pmndrs/react-three-jolt/issues" + }, + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" + } + }, + "files": [ + "dist/**", + "README.md", + "LICENSE" + ], + "scripts": { + "test": "vitest run --coverage", + "build": "tsc && rollup --config rollup.config.mjs", + "format": "prettier --write ." + }, + "peerDependencies": { + "@react-three/fiber": ">=8.9.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "dependencies": { + "@react-three/jolt": "0.0.1" + }, + "devDependencies": { + "@react-three/fiber": "8.16.1", + "@react-three/test-renderer": "^8.2.1", + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-node-resolve": "^15.0.1", + "@rollup/plugin-typescript": "^11.1.6", + "@types/three": "^0.163.0", + "@typescript-eslint/eslint-plugin": "^6.10.0", + "@typescript-eslint/parser": "^7.5.0", + "@vitest/coverage-v8": "^1.4.0", + "eslint": "^9.0.0", + "happy-dom": "^14.7.0", + "prettier": "^3.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "rollup": "^4.14.0", + "rollup-plugin-copy": "^3.5.0", + "rollup-plugin-filesize": "^10.0.0", + "three": "^0.163.0", + "typescript": "^5.4.4", + "vitest": "^1.5.0" } - }, - "files": [ - "dist/**", - "README.md", - "LICENSE" - ], - "scripts": { - "test": "vitest run --coverage", - "build": "tsc && rollup --config rollup.config.mjs" - }, - "peerDependencies": { - "@react-three/fiber": ">=8.9.0", - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "dependencies": { - "@react-three/jolt": "0.0.1" - }, - "devDependencies": { - "@react-three/fiber": "8.16.1", - "@react-three/test-renderer": "^8.2.1", - "@rollup/plugin-commonjs": "^25.0.7", - "@rollup/plugin-node-resolve": "^15.0.1", - "@rollup/plugin-typescript": "^11.1.6", - "@types/three": "^0.163.0", - "@typescript-eslint/eslint-plugin": "^6.10.0", - "@typescript-eslint/parser": "^7.5.0", - "@vitest/coverage-v8": "^1.4.0", - "eslint": "^9.0.0", - "happy-dom": "^14.7.0", - "prettier": "^3.1.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "rollup": "^4.14.0", - "rollup-plugin-copy": "^3.5.0", - "rollup-plugin-filesize": "^10.0.0", - "three": "^0.163.0", - "typescript": "^5.4.4", - "vitest": "^1.5.0" - } } diff --git a/packages/react-three-jolt-addons/tsconfig.json b/packages/react-three-jolt-addons/tsconfig.json index b5d2437..7fd4e37 100644 --- a/packages/react-three-jolt-addons/tsconfig.json +++ b/packages/react-three-jolt-addons/tsconfig.json @@ -1,27 +1,27 @@ { - "compilerOptions": { - "target": "ES2022", - "useDefineForClassFields": true, - "module": "ES2022", - "lib": ["ES2022", "DOM"], - "moduleResolution": "Bundler", - "strict": true, - "sourceMap": true, - "resolveJsonModule": true, - "isolatedModules": true, - "esModuleInterop": true, - "noEmit": true, - "declaration": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "skipLibCheck": true, - "typeRoots": ["./types"], - "baseUrl": "./", - "rootDir": "./src", - "allowSyntheticDefaultImports": true, - "outDir": "./dist", - "jsx": "react" - }, - "files": ["./src/index.ts"] + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ES2022", + "lib": ["ES2022", "DOM"], + "moduleResolution": "Bundler", + "strict": true, + "sourceMap": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": true, + "declaration": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "skipLibCheck": true, + "typeRoots": ["./types"], + "baseUrl": "./", + "rootDir": "./src", + "allowSyntheticDefaultImports": true, + "outDir": "./dist", + "jsx": "react" + }, + "files": ["./src/index.ts"] } diff --git a/packages/react-three-jolt-controllers/.prettierrc b/packages/react-three-jolt-controllers/.prettierrc index 7bec3b4..f68f5a7 100644 --- a/packages/react-three-jolt-controllers/.prettierrc +++ b/packages/react-three-jolt-controllers/.prettierrc @@ -1,7 +1,7 @@ { - "singleQuote": true, - "trailingComma": "none", - "printWidth": 100, - "tabWidth": 4, - "bracketSameLine": true -} \ No newline at end of file + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "tabWidth": 4, + "bracketSameLine": true +} diff --git a/packages/react-three-jolt-controllers/package.json b/packages/react-three-jolt-controllers/package.json index d0137bb..ac5885e 100644 --- a/packages/react-three-jolt-controllers/package.json +++ b/packages/react-three-jolt-controllers/package.json @@ -1,71 +1,72 @@ { - "name": "@react-three/jolt-controllers", - "description": "Controllers for @react-three/jolt", - "keywords": [ - "physics", - "jolt-physics", - "react-three-fiber", - "react", - "three" - ], - "version": "0.0.1", - "author": "Dennis Smolek", - "contributors": [ - "Dennis Smolek", - "Isaac Mason " - ], - "license": "MIT", - "homepage": "https://github.com/pmndrs/react-three-jolt", - "bugs": { - "url": "https://github.com/pmndrs/react-three-jolt/issues" - }, - "main": "./dist/index.cjs", - "module": "./dist/index.mjs", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "require": "./dist/index.cjs" + "name": "@react-three/jolt-controllers", + "description": "Controllers for @react-three/jolt", + "keywords": [ + "physics", + "jolt-physics", + "react-three-fiber", + "react", + "three" + ], + "version": "0.0.1", + "author": "Dennis Smolek", + "contributors": [ + "Dennis Smolek", + "Isaac Mason " + ], + "license": "MIT", + "homepage": "https://github.com/pmndrs/react-three-jolt", + "bugs": { + "url": "https://github.com/pmndrs/react-three-jolt/issues" + }, + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" + } + }, + "files": [ + "dist/**", + "README.md", + "LICENSE" + ], + "scripts": { + "test": "vitest run --coverage", + "build": "tsc && rollup --config rollup.config.mjs", + "format": "prettier --write ." + }, + "peerDependencies": { + "@react-three/fiber": ">=8.9.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "dependencies": { + "@react-three/jolt": "0.0.1" + }, + "devDependencies": { + "@react-three/fiber": "8.16.1", + "@react-three/test-renderer": "^8.2.1", + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-node-resolve": "^15.0.1", + "@rollup/plugin-typescript": "^11.1.6", + "@types/three": "^0.163.0", + "@typescript-eslint/eslint-plugin": "^6.10.0", + "@typescript-eslint/parser": "^7.5.0", + "@vitest/coverage-v8": "^1.4.0", + "eslint": "^9.0.0", + "happy-dom": "^14.7.0", + "prettier": "^3.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "rollup": "^4.14.0", + "rollup-plugin-copy": "^3.5.0", + "rollup-plugin-filesize": "^10.0.0", + "three": "^0.163.0", + "typescript": "^5.4.4", + "vitest": "^1.5.0" } - }, - "files": [ - "dist/**", - "README.md", - "LICENSE" - ], - "scripts": { - "test": "vitest run --coverage", - "build": "tsc && rollup --config rollup.config.mjs" - }, - "peerDependencies": { - "@react-three/fiber": ">=8.9.0", - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "dependencies": { - "@react-three/jolt": "0.0.1" - }, - "devDependencies": { - "@react-three/fiber": "8.16.1", - "@react-three/test-renderer": "^8.2.1", - "@rollup/plugin-commonjs": "^25.0.7", - "@rollup/plugin-node-resolve": "^15.0.1", - "@rollup/plugin-typescript": "^11.1.6", - "@types/three": "^0.163.0", - "@typescript-eslint/eslint-plugin": "^6.10.0", - "@typescript-eslint/parser": "^7.5.0", - "@vitest/coverage-v8": "^1.4.0", - "eslint": "^9.0.0", - "happy-dom": "^14.7.0", - "prettier": "^3.1.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "rollup": "^4.14.0", - "rollup-plugin-copy": "^3.5.0", - "rollup-plugin-filesize": "^10.0.0", - "three": "^0.163.0", - "typescript": "^5.4.4", - "vitest": "^1.5.0" - } } diff --git a/packages/react-three-jolt-controllers/tsconfig.json b/packages/react-three-jolt-controllers/tsconfig.json index b5d2437..7fd4e37 100644 --- a/packages/react-three-jolt-controllers/tsconfig.json +++ b/packages/react-three-jolt-controllers/tsconfig.json @@ -1,27 +1,27 @@ { - "compilerOptions": { - "target": "ES2022", - "useDefineForClassFields": true, - "module": "ES2022", - "lib": ["ES2022", "DOM"], - "moduleResolution": "Bundler", - "strict": true, - "sourceMap": true, - "resolveJsonModule": true, - "isolatedModules": true, - "esModuleInterop": true, - "noEmit": true, - "declaration": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "skipLibCheck": true, - "typeRoots": ["./types"], - "baseUrl": "./", - "rootDir": "./src", - "allowSyntheticDefaultImports": true, - "outDir": "./dist", - "jsx": "react" - }, - "files": ["./src/index.ts"] + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ES2022", + "lib": ["ES2022", "DOM"], + "moduleResolution": "Bundler", + "strict": true, + "sourceMap": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": true, + "declaration": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "skipLibCheck": true, + "typeRoots": ["./types"], + "baseUrl": "./", + "rootDir": "./src", + "allowSyntheticDefaultImports": true, + "outDir": "./dist", + "jsx": "react" + }, + "files": ["./src/index.ts"] } diff --git a/packages/react-three-jolt/.prettierrc b/packages/react-three-jolt/.prettierrc index 7bec3b4..f68f5a7 100644 --- a/packages/react-three-jolt/.prettierrc +++ b/packages/react-three-jolt/.prettierrc @@ -1,7 +1,7 @@ { - "singleQuote": true, - "trailingComma": "none", - "printWidth": 100, - "tabWidth": 4, - "bracketSameLine": true -} \ No newline at end of file + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "tabWidth": 4, + "bracketSameLine": true +} diff --git a/packages/react-three-jolt/README.md b/packages/react-three-jolt/README.md index 6297ae6..55a3dd9 100644 --- a/packages/react-three-jolt/README.md +++ b/packages/react-three-jolt/README.md @@ -25,8 +25,7 @@ For contributions, please read the - + + + + ``` + #### About Shapes: -You can however set the shape property directly with `shape=’box’` etc. + +You can however set the shape property directly with `shape=’box’` etc. If the system can’t determine the shape, it will unwrap the mesh and build a convex shape (giftwrapping) of the mesh. As of right now it’s fairly basic convex generation. ### About Trimeshes: + To get a Trimesh shape you must specify trimesh. We do this because trimesh is actually the most difficult shape and most likely to not work correctly. For example Jolt docs say Dynamic & Kinematic Trimeshes cannot collide with each other or heightmaps, doing so will throw errors. #### Compound Shapes: To get a compound shape simply add multiple meshes and position them as if they are inside a group ( in local space). + ```tsx @@ -122,36 +136,41 @@ To get a compound shape simply add multiple meshes and position them as if they ``` #### Ignoring Meshes in shapes + You can have meshes or items within the rigid body but not generate shapes by adding the ignore attribute. - (NOTE: this may change in the future as we look to add a `` component to generate the shape but not display) +(NOTE: this may change in the future as we look to add a `` component to generate the shape but not display) #### Properties (Component Level) -Many of the properties and options can be set at the component level. + +Many of the properties and options can be set at the component level. // example of rb props -- type (Dynamic, static, kinetic, rig) -- shape -- position -Setting position or rotation will teleport the body immediately -- rotation -- debug -Debug can be set on a per-object basis and wont trigger the entire system to go into debug. However, changing this prop wont disable debug at the global level. - -- *Events* -onContactAdded, onContactRemoved, onContactPersisted -*future* -- isSensor -- onSleep -- onWake + +- type (Dynamic, static, kinetic, rig) +- shape +- position + Setting position or rotation will teleport the body immediately +- rotation +- debug + Debug can be set on a per-object basis and wont trigger the entire system to go into debug. However, changing this prop wont disable debug at the global level. + +- _Events_ + onContactAdded, onContactRemoved, onContactPersisted + _future_ +- isSensor +- onSleep +- onWake #### BodyState + However some will need to be set on the BodyState directly. To get the BodyState for a body, you can either access it via the ref property, or request the body from the BodySystem. As a ref: + ```tsx const myBody = useRef(); useEffect(() => { - myBody.current.applyImpulse([2,3,1]); -},[]); + myBody.current.applyImpulse([2, 3, 1]); +}, []); return ( <> @@ -159,16 +178,18 @@ return ( ); ``` + // accessing BodyState with bodySystem + ```tsx const bodyHandle = props.bodyHandle; const { bodySystem } = useJolt(); useEffect(() => { - bodySystem.dynamicBodies.forEach((body: BodyState) => body.applyImpulse([0,2,1])); - const myBody = bodySystem.getBody(bodyHandle); - if(myBody) myBody.position = new THREE.Vector3(0, 3, 2); -},[]); + bodySystem.dynamicBodies.forEach((body: BodyState) => body.applyImpulse([0, 2, 1])); + const myBody = bodySystem.getBody(bodyHandle); + if (myBody) myBody.position = new THREE.Vector3(0, 3, 2); +}, []); ``` // props and methods on the BodyState @@ -176,46 +197,50 @@ useEffect(() => { --- ### InstancedRigidBodyMesh -Instancing lets you display many of the same meshes at a time with a single draw call. R3/Jolt supports that by creating bodies for each instance and automatically updating their positions. + +Instancing lets you display many of the same meshes at a time with a single draw call. R3/Jolt supports that by creating bodies for each instance and automatically updating their positions. R3/Jolt works a little different than r3/rapier in how we handle instances. We replace the normal component with -Anything inside is treated like RigidBody and creates the shape and mesh for the subsequent instances. +Anything inside is treated like RigidBody and creates the shape and mesh for the subsequent instances. The only other prop we need is the count of instances. Once setup, R3/Jolt automatically generates an array of all the BodyStates for each instance and this is what you’ll get on the ref // instances example -This may seem odd. But this actually gives you significantly more control over the instance than simply forcing the position and rotation. Or only passing them at creation. +This may seem odd. But this actually gives you significantly more control over the instance than simply forcing the position and rotation. Or only passing them at creation. There is some concern if you don’t specify a position, as it will try to put all the bodies in roughly the same position.(We actually add a slight jitter) and the physics system will push them out with force as the next item comes into position. (This may be what you want with a shape fountain) -In the future we may allow passing an initial position array. -We also plan to allow passing an instance matrix directly. We know that when you use instancing with a GLTF that it automatically creates the matrix containing all position and color data automatically. It’s annoying to convert this to an array and would be easier to just pass directly. We’re working on it.. +In the future we may allow passing an initial position array. +We also plan to allow passing an instance matrix directly. We know that when you use instancing with a GLTF that it automatically creates the matrix containing all position and color data automatically. It’s annoying to convert this to an array and would be easier to just pass directly. We’re working on it.. --- ### Raycasting -R3/Jolt actually has a pretty robust raycaster. It works similarly to ThreeJS’s raycaster with some additional options and built in debugging. + +R3/Jolt actually has a pretty robust raycaster. It works similarly to ThreeJS’s raycaster with some additional options and built in debugging. At the moment to best see the raycaster see the raycasting demo. Each cast shows some of the features. Raycasting also has a multicaster. Which allows you to setup multiple ray origins and/or destinations to cast many rays with a single request. This is common with sweeps and detections -Shapecasting is coming soon. +Shapecasting is coming soon. --- ### Heightfield -The heightfield component generates a plane and automatically creates a heightfield mesh and rigidbody. + +The heightfield component generates a plane and automatically creates a heightfield mesh and rigidbody. The heightfield can be an image URL, image, or texture // testing needed for other types -And can be updated after creation // should be fine, test. +And can be updated after creation // should be fine, test. -Heightfields are heavy, so don’t make them too big or use too many. It’s best to update and use other static bodies together to create the effect you want. +Heightfields are heavy, so don’t make them too big or use too many. It’s best to update and use other static bodies together to create the effect you want. -Be warry of contact events on heightfields. Honestly, best not to even use them as they fire for every single triangle in the mesh and can easily confuse the contact listener. Meaning you’ll never correctly detect when the item stops contacting. +Be warry of contact events on heightfields. Honestly, best not to even use them as they fire for every single triangle in the mesh and can easily confuse the contact listener. Meaning you’ll never correctly detect when the item stops contacting. -It’s unclear if this is true from the perspective of the other shape, but know contact listening heightfields is currently buggy. +It’s unclear if this is true from the perspective of the other shape, but know contact listening heightfields is currently buggy. --- ### Helper components + #### Floor #### MeshFloor @@ -223,53 +248,61 @@ It’s unclear if this is true from the perspective of the other shape, but know --- ### useJolt(); -If you want to interface with any of R3/Jolts active systems or access the Jolt system directly use this hook anywhere inside the context. +If you want to interface with any of R3/Jolts active systems or access the Jolt system directly use this hook anywhere inside the context. #### physicsSystem -This is the core physics system. All other systems link here and you can get to them through this system if you really needed. This also holds the active interfaces, and most of the api to manage the simulation. + +This is the core physics system. All other systems link here and you can get to them through this system if you really needed. This also holds the active interfaces, and most of the api to manage the simulation. //TODO make a physicsSystem API docs #### bodySystem -This is a shortener for physicsSystem.bodySystem. Many times you dont need to mess with the physicsSystem but the bodies. Doing -// code example + +This is a shortener for physicsSystem.bodySystem. Many times you dont need to mess with the physicsSystem but the bodies. Doing +// code example Makes access easier // TODO: api on bodySystem #### debug + This is the debug state of the system and is updated reactively. // TODO test if this is true, also test if it can be set directly as a setter ### paused -This is the paused state of the system and is updated reactively. + +This is the paused state of the system and is updated reactively. // TODO test if this is true, also test if it can be set directly as a setter ### jolt + This is the core WASM module. Be VERY CAREFUL when messing with this directly. R3/Jolt can’t save you if you mess it up. ### joltInterface -This is the running Jolt Interface for this simulation. Be VERY CAREFUL when messing with this directly. + +This is the running Jolt Interface for this simulation. Be VERY CAREFUL when messing with this directly. ### step + This is the running step call. Currently manual stepping isn’t setup, but when it is calling this function will progress the simulation a single step. ---- +--- ### Other library items: ### Vehicles: -We have working four wheel and two wheel controllers. +We have working four wheel and two wheel controllers. They need some minor attention before being released. ### Character controller. + The controller actually works, but needs some attention ### Camera rig + This goes hand in hand with the character controller. It’s pretty advanced but may need a refactor. Heightfield tools -This will eventually allow you to generate heightfields in realtime from noise algorithms. - +This will eventually allow you to generate heightfields in realtime from noise algorithms. --- @@ -278,13 +311,17 @@ This will eventually allow you to generate heightfields in realtime from noise a There are 4 phases planned for this library. We are currently in Phase 0 (Pre-Alpha) ### [Alpha:Stable Library](https://github.com/pmndrs/react-three-jolt/milestone/1) + This phase is to stabilize building, deployment, and planning among any interested devs while also providing the minimum level of usability and performance. ### [1.0 Feature Parity.](https://github.com/pmndrs/react-three-jolt/milestone/2) + R3/Jolt is heavily inspired by sibling libraries [R3/Rapier](https://github.com/pmndrs/react-three-rapier/) and [useCannon/Cannon](https://github.com/pmndrs/use-cannon). While Jolt itself has many more features and capabilities, we should focus first on being comparable with these other libraries. We should also have plenty of documentation as well as examples (both functional and stylistic) ### [2.0 Advanced Jolt Features](https://github.com/pmndrs/react-three-jolt/milestone/3) + Jolt is a highly capable, powerful library. There are many features and usages we will want to include and provide. Pulleys, Buoyancy, etc. ### [3.0 Kinematic Rigs.](https://github.com/pmndrs/react-three-jolt/milestone/4) + Jolt provides a full skeleton animation system to control rigid body models. The most common use would be ragdoll/character models. diff --git a/packages/react-three-jolt/package.json b/packages/react-three-jolt/package.json index 704470f..676abbb 100644 --- a/packages/react-three-jolt/package.json +++ b/packages/react-three-jolt/package.json @@ -1,72 +1,73 @@ { - "name": "@react-three/jolt", - "description": "Jolt physics in React", - "keywords": [ - "physics", - "jolt-physics", - "react-three-fiber", - "react", - "three" - ], - "version": "0.0.1", - "author": "Dennis Smolek", - "contributors": [ - "Dennis Smolek", - "Isaac Mason " - ], - "license": "MIT", - "homepage": "https://github.com/pmndrs/react-three-jolt", - "bugs": { - "url": "https://github.com/pmndrs/react-three-jolt/issues" - }, - "main": "./dist/index.cjs", - "module": "./dist/index.mjs", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "require": "./dist/index.cjs" + "name": "@react-three/jolt", + "description": "Jolt physics in React", + "keywords": [ + "physics", + "jolt-physics", + "react-three-fiber", + "react", + "three" + ], + "version": "0.0.1", + "author": "Dennis Smolek", + "contributors": [ + "Dennis Smolek", + "Isaac Mason " + ], + "license": "MIT", + "homepage": "https://github.com/pmndrs/react-three-jolt", + "bugs": { + "url": "https://github.com/pmndrs/react-three-jolt/issues" + }, + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" + } + }, + "files": [ + "dist/**", + "README.md", + "LICENSE" + ], + "scripts": { + "test": "vitest run --coverage", + "build": "tsc && rollup --config rollup.config.mjs", + "format": "prettier --write ." + }, + "peerDependencies": { + "@react-three/fiber": ">=8.9.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "dependencies": { + "gamepad.js": "^2.1.0", + "jolt-physics": "^0.22.0" + }, + "devDependencies": { + "@react-three/fiber": "8.16.1", + "@react-three/test-renderer": "^8.2.1", + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-node-resolve": "^15.0.1", + "@rollup/plugin-typescript": "^11.1.6", + "@types/three": "^0.163.0", + "@typescript-eslint/eslint-plugin": "^6.10.0", + "@typescript-eslint/parser": "^7.5.0", + "@vitest/coverage-v8": "^1.4.0", + "eslint": "^9.0.0", + "happy-dom": "^14.7.0", + "prettier": "^3.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "rollup": "^4.14.0", + "rollup-plugin-copy": "^3.5.0", + "rollup-plugin-filesize": "^10.0.0", + "three": "^0.163.0", + "typescript": "^5.4.4", + "vitest": "^1.5.0" } - }, - "files": [ - "dist/**", - "README.md", - "LICENSE" - ], - "scripts": { - "test": "vitest run --coverage", - "build": "tsc && rollup --config rollup.config.mjs" - }, - "peerDependencies": { - "@react-three/fiber": ">=8.9.0", - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "dependencies": { - "gamepad.js": "^2.1.0", - "jolt-physics": "^0.22.0" - }, - "devDependencies": { - "@react-three/fiber": "8.16.1", - "@react-three/test-renderer": "^8.2.1", - "@rollup/plugin-commonjs": "^25.0.7", - "@rollup/plugin-node-resolve": "^15.0.1", - "@rollup/plugin-typescript": "^11.1.6", - "@types/three": "^0.163.0", - "@typescript-eslint/eslint-plugin": "^6.10.0", - "@typescript-eslint/parser": "^7.5.0", - "@vitest/coverage-v8": "^1.4.0", - "eslint": "^9.0.0", - "happy-dom": "^14.7.0", - "prettier": "^3.1.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "rollup": "^4.14.0", - "rollup-plugin-copy": "^3.5.0", - "rollup-plugin-filesize": "^10.0.0", - "three": "^0.163.0", - "typescript": "^5.4.4", - "vitest": "^1.5.0" - } } diff --git a/packages/react-three-jolt/src/constants.ts b/packages/react-three-jolt/src/constants.ts index 4fde6cb..d31f34b 100644 --- a/packages/react-three-jolt/src/constants.ts +++ b/packages/react-three-jolt/src/constants.ts @@ -3,6 +3,7 @@ export const Layer = { MOVING: 0, NON_MOVING: 1, KINEMATIC: 2, - RIG: 3, - }; - export const NUM_OBJECT_LAYERS = 3; \ No newline at end of file + RIG: 3 +}; + +export const NUM_OBJECT_LAYERS = 3; diff --git a/packages/react-three-jolt/src/heightField/generators-save.ts b/packages/react-three-jolt/src/heightField/generators-save.ts index 801307f..3487f69 100644 --- a/packages/react-three-jolt/src/heightField/generators-save.ts +++ b/packages/react-three-jolt/src/heightField/generators-save.ts @@ -17,10 +17,7 @@ export function textureToCanvas(texture: THREE.Texture) { return canvas; } -export async function imageUrlToImageData( - url: string, - scalingFactor?: number, -): Promise { +export async function imageUrlToImageData(url: string, scalingFactor?: number): Promise { return new Promise((resolve, reject) => { const image = new Image(); image.onload = () => { @@ -30,12 +27,8 @@ export async function imageUrlToImageData( reject(new Error('No context')); return; } - const width = scalingFactor - ? image.width * scalingFactor - : image.width; - const height = scalingFactor - ? image.height * scalingFactor - : image.height; + const width = scalingFactor ? image.width * scalingFactor : image.width; + const height = scalingFactor ? image.height * scalingFactor : image.height; canvas.width = width; canvas.height = height; context.drawImage(image, 0, 0, width, height); @@ -63,7 +56,7 @@ export function textureToImageData(texture: THREE.Texture): ImageData { height / 2, height / -2, 1, - 1000, + 1000 ); const planeGeometry = new THREE.PlaneGeometry(width, height); const planeMaterial = new THREE.MeshBasicMaterial({ map: texture }); @@ -84,7 +77,7 @@ export function textureToImageData(texture: THREE.Texture): ImageData { export function applyHeightmapImgDataToPlane( plane: THREE.Mesh, heightmap: ImageData, - displacementScale: number, + displacementScale: number ) { const { width, height } = heightmap; const geometry = plane.geometry as THREE.PlaneGeometry; @@ -107,7 +100,7 @@ export function applyHeightmapImgDataToPlane( export async function applyHeightmapToPlane( plane: THREE.Mesh, heightmap: string | THREE.Texture, - displacementScale: number, + displacementScale: number ) { let heightmapImgData: ImageData; if (typeof heightmap === 'string') { diff --git a/packages/react-three-jolt/src/heightField/heightfield-worker.ts b/packages/react-three-jolt/src/heightField/heightfield-worker.ts index 20e140d..75d3c53 100644 --- a/packages/react-three-jolt/src/heightField/heightfield-worker.ts +++ b/packages/react-three-jolt/src/heightField/heightfield-worker.ts @@ -39,11 +39,7 @@ function handleMessage(message) { } // setup the canvas and renderer -function initializeRenderer( - newCanvas: OffscreenCanvas, - height = 1024, - width = 1024, -) { +function initializeRenderer(newCanvas: OffscreenCanvas, height = 1024, width = 1024) { if (!newCanvas) console.log('WORKER: no canvas sent, creating a new one'); canvas = newCanvas || new OffscreenCanvas(width, height); renderer = new THREE.WebGLRenderer({ canvas }); @@ -68,7 +64,7 @@ function initializeScene(width = 1024, height = 1024, resolution = 1) { width, height, width / resolution, - height / resolution, + height / resolution ); const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); plane = new THREE.Mesh(geometry, material); diff --git a/packages/react-three-jolt/src/heightField/heightfieldManager.ts b/packages/react-three-jolt/src/heightField/heightfieldManager.ts index 55ab8b9..29da6f5 100644 --- a/packages/react-three-jolt/src/heightField/heightfieldManager.ts +++ b/packages/react-three-jolt/src/heightField/heightfieldManager.ts @@ -2,7 +2,7 @@ import * as THREE from 'three'; // setup a single worker regardless of how many heightfields we have const worker = new Worker(new URL('./heightfield-worker.ts', import.meta.url), { - type: 'module', + type: 'module' }); export class HeightfieldManager { diff --git a/packages/react-three-jolt/src/hooks/use-forwarded-ref.ts b/packages/react-three-jolt/src/hooks/use-forwarded-ref.ts index f86a54a..fc87038 100644 --- a/packages/react-three-jolt/src/hooks/use-forwarded-ref.ts +++ b/packages/react-three-jolt/src/hooks/use-forwarded-ref.ts @@ -4,7 +4,7 @@ import { ForwardedRef, MutableRefObject, useRef } from 'react'; // Need to catch the case where forwardedRef is a function... how to do that? export const useForwardedRef = ( forwardedRef: ForwardedRef, - defaultValue: T | null = null, + defaultValue: T | null = null ): MutableRefObject => { const innerRef = useRef(defaultValue); diff --git a/packages/react-three-jolt/src/systems/camera-rig-system-archive.ts b/packages/react-three-jolt/src/systems/camera-rig-system-archive.ts index 547349f..fd93c56 100644 --- a/packages/react-three-jolt/src/systems/camera-rig-system-archive.ts +++ b/packages/react-three-jolt/src/systems/camera-rig-system-archive.ts @@ -143,9 +143,7 @@ export class CameraRigManager { // trigger the camera change listeners private triggerCameraChange() { console.log('triggering camera change', this.activeCamera); - this.cameraChangeListeners.forEach((listener) => - listener(this.activeCamera) - ); + this.cameraChangeListeners.forEach((listener) => listener(this.activeCamera)); } //attach a camera to a point @@ -195,24 +193,16 @@ export class CameraRigManager { createCollar() { const collar = this.createRigPoint('collar', { color: '#0098DE' }); - const joint = this.constraintSystem.addConstraint( - 'hinge', - this.base, - collar, - { - axis: new THREE.Vector3(0, 1, 0), - normal: new THREE.Vector3(0, 0, 1), + const joint = this.constraintSystem.addConstraint('hinge', this.base, collar, { + axis: new THREE.Vector3(0, 1, 0), + normal: new THREE.Vector3(0, 0, 1), - spring: { - strength: 1 - } + spring: { + strength: 1 } - ); + }); // TODO convert this to the constraint map - this.collarConstraint = Raw.module.castObject( - joint, - Raw.module.HingeConstraint - ); + this.collarConstraint = Raw.module.castObject(joint, Raw.module.HingeConstraint); } //create the camera mount @@ -225,10 +215,7 @@ export class CameraRigManager { mGravityFactor: 0 } }; - const mountHandle = this.physicsSystem.bodySystem.addBody( - mesh, - options - ); + const mountHandle = this.physicsSystem.bodySystem.addBody(mesh, options); const mount = this.physicsSystem.bodySystem.getBody(mountHandle); //change the mass this.physicsSystem.bodySystem.setMass(mountHandle, 0.00001); @@ -341,10 +328,7 @@ export class CameraRigManager { this.timeRecentering += deltaTime; const newPosition = !this.allowFollowDelay ? anchorPosition - : basePosition.lerp( - anchorPosition, - smoothFactor * deltaTime * this.cameraFollowSpeed - ); + : basePosition.lerp(anchorPosition, smoothFactor * deltaTime * this.cameraFollowSpeed); this.base.setPosition(newPosition); } @@ -441,11 +425,7 @@ export class CameraRigManager { //TODO move this to the body system //create rig points createRigPoint(name, options?): BodyState { - const { - color = '#767B91', - type = 'sphere', - motionType - } = options || {}; + const { color = '#767B91', type = 'sphere', motionType } = options || {}; /* / TODO Cylinder throws errors const geometry = type == 'sphere' diff --git a/packages/react-three-jolt/src/systems/character-controller.ts b/packages/react-three-jolt/src/systems/character-controller.ts index 7113082..c056c72 100644 --- a/packages/react-three-jolt/src/systems/character-controller.ts +++ b/packages/react-three-jolt/src/systems/character-controller.ts @@ -5,7 +5,7 @@ import * as THREE from 'three'; import { MathUtils } from 'three'; import Jolt from 'jolt-physics'; import { Raw } from '../raw'; -import { Layer } from '../constants' +import { Layer } from '../constants'; import { PhysicsSystem } from './physics-system'; import { _matrix4, _position, _quaternion, _rotation, _scale, _vector3 } from '../tmp'; import { quat, vec3 } from '../utils'; diff --git a/packages/react-three-jolt/src/systems/shape-system.ts b/packages/react-three-jolt/src/systems/shape-system.ts index df3928d..71d3943 100644 --- a/packages/react-three-jolt/src/systems/shape-system.ts +++ b/packages/react-three-jolt/src/systems/shape-system.ts @@ -8,314 +8,294 @@ import Jolt from 'jolt-physics'; import * as THREE from 'three'; import { BufferGeometry, Mesh, Object3D, Vector3 } from 'three'; -import { - SphereGeometry, - BoxGeometry, - CapsuleGeometry, - CylinderGeometry, -} from 'three'; +import { SphereGeometry, BoxGeometry, CapsuleGeometry, CylinderGeometry } from 'three'; import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js'; import { Raw } from '../raw'; import { quat, vec3 } from '../utils'; export class ShapeSystem { - private physicsSystem: Jolt.PhysicsSystem; - //@ts-ignore - private bodyInterface: Jolt.BodyInterface; - constructor(physicsSystem: Jolt.PhysicsSystem) { - this.physicsSystem = physicsSystem; - this.bodyInterface = this.physicsSystem.GetBodyInterface(); - } + private physicsSystem: Jolt.PhysicsSystem; + //@ts-ignore + private bodyInterface: Jolt.BodyInterface; + constructor(physicsSystem: Jolt.PhysicsSystem) { + this.physicsSystem = physicsSystem; + this.bodyInterface = this.physicsSystem.GetBodyInterface(); + } } export type AutoShape = - | 'box' - | 'sphere' - | 'capsule' - | 'cylinder' - | 'convex' - | 'trimesh' - | 'compound' - | 'heightfield'; + | 'box' + | 'sphere' + | 'capsule' + | 'cylinder' + | 'convex' + | 'trimesh' + | 'compound' + | 'heightfield'; export const getShapeSettingsFromObject = ( - object: Object3D, - // why do I need this here? - shapeType?: AutoShape + object: Object3D, + // why do I need this here? + shapeType?: AutoShape ) => { - // TODO: Add types here - const shapes: any = []; - - object.traverse((child) => { - if (child instanceof Object3D) { - const geometry = (child as Mesh)?.geometry; - // adding ignore to meshes skips the shape generator - // TODO: Typescript HATES the ignore property. - //@ts-ignore - if (!child.ignore && geometry) { - // TODO: Until we understand the offsets we are going to get both here - const shapeSettingsAndOffset = getShapeSettingsFromGeometry( - geometry, - shapeType - ); - - if (shapeSettingsAndOffset) { - const shape = { - shapeSettings: shapeSettingsAndOffset.shapeSettings, - offset: shapeSettingsAndOffset.offset, - position: vec3.threeToJolt(child.position), - quaternion: quat.threeToJolt(child.quaternion), - }; - - shapes.push(shape); + // TODO: Add types here + const shapes: any = []; + + object.traverse((child) => { + if (child instanceof Object3D) { + const geometry = (child as Mesh)?.geometry; + // adding ignore to meshes skips the shape generator + // TODO: Typescript HATES the ignore property. + //@ts-ignore + if (!child.ignore && geometry) { + // TODO: Until we understand the offsets we are going to get both here + const shapeSettingsAndOffset = getShapeSettingsFromGeometry(geometry, shapeType); + + if (shapeSettingsAndOffset) { + const shape = { + shapeSettings: shapeSettingsAndOffset.shapeSettings, + offset: shapeSettingsAndOffset.offset, + position: vec3.threeToJolt(child.position), + quaternion: quat.threeToJolt(child.quaternion) + }; + + shapes.push(shape); + } + } } - } + }); + + // BAIL IF EMPTY + // if (shapes.length === 0) return undefined; + //console.log('shapes', shapes); + // if theres only one, return it + if (shapes.length === 1) return shapes[0].shapeSettings; + console.log('Generating compound shape for:', shapes); + const compoundShapeSettings = new Raw.module.StaticCompoundShapeSettings(); + + // Note: offset also available + for (const { shapeSettings, position, quaternion } of shapes) { + compoundShapeSettings.AddShape(position, quaternion, shapeSettings, 0); + + Raw.module.destroy(position); + Raw.module.destroy(quaternion); } - }); - - // BAIL IF EMPTY - // if (shapes.length === 0) return undefined; - //console.log('shapes', shapes); - // if theres only one, return it - if (shapes.length === 1) return shapes[0].shapeSettings; - console.log('Generating compound shape for:', shapes); - const compoundShapeSettings = new Raw.module.StaticCompoundShapeSettings(); - // Note: offset also available - for (const { shapeSettings, position, quaternion } of shapes) { - compoundShapeSettings.AddShape(position, quaternion, shapeSettings, 0); - - Raw.module.destroy(position); - Raw.module.destroy(quaternion); - } - - return compoundShapeSettings; + return compoundShapeSettings; }; // TODO: move this type later type PossibleGeometry = - | BufferGeometry - | BoxGeometry - | SphereGeometry - | CapsuleGeometry - | CylinderGeometry; + | BufferGeometry + | BoxGeometry + | SphereGeometry + | CapsuleGeometry + | CylinderGeometry; // check the instanceOf value against known three geometries const getShapeTypeFromGeometry = (geometry: PossibleGeometry): AutoShape => { - //hack the switch statement to check the instanceOf value - switch (true) { - case geometry instanceof BoxGeometry: - return 'box'; - case geometry instanceof SphereGeometry: - return 'sphere'; - case geometry instanceof CapsuleGeometry: - return 'capsule'; - case geometry instanceof CylinderGeometry: - return 'cylinder'; - // if unknown do a convex hull - case geometry instanceof BufferGeometry: - return 'convex'; - default: - // bail out with sphere - return 'convex'; - } + //hack the switch statement to check the instanceOf value + switch (true) { + case geometry instanceof BoxGeometry: + return 'box'; + case geometry instanceof SphereGeometry: + return 'sphere'; + case geometry instanceof CapsuleGeometry: + return 'capsule'; + case geometry instanceof CylinderGeometry: + return 'cylinder'; + // if unknown do a convex hull + case geometry instanceof BufferGeometry: + return 'convex'; + default: + // bail out with sphere + return 'convex'; + } }; // We use shape settings because it lets us reuse this fn in compound shape generation export const getShapeSettingsFromGeometry = ( - geometry: PossibleGeometry, - shapeType?: AutoShape + geometry: PossibleGeometry, + shapeType?: AutoShape ): - | { - shapeSettings: Jolt.ShapeSettings | undefined; - offset: Vector3 | undefined; - } - | undefined => { - const jolt = Raw.module; - let shapeSettings, offset; - - // if the user passes the shape use that, if not, try to infer it from the geometry - if (!shapeType) shapeType = getShapeTypeFromGeometry(geometry); - switch (shapeType) { - case 'box': { - geometry.computeBoundingBox(); - const { boundingBox } = geometry; - let size; - // if the geometry is a box, use it's parameters not the bounding box - if (geometry instanceof BoxGeometry) { - const { width, height, depth } = geometry.parameters; - size = new Vector3(width, height, depth); - } else size = boundingBox!.getSize(new Vector3()); - - const shapeSize = new jolt.Vec3(size.x / 2, size.y / 2, size.z / 2); - shapeSettings = new jolt.BoxShapeSettings(shapeSize); - // jolt sucks at memory management - jolt.destroy(shapeSize); - - offset = boundingBox!.getCenter(new Vector3()); - break; - } - - case 'sphere': { - geometry.computeBoundingSphere(); - const { boundingSphere } = geometry; - const radius = boundingSphere!.radius; + | { + shapeSettings: Jolt.ShapeSettings | undefined; + offset: Vector3 | undefined; + } + | undefined => { + const jolt = Raw.module; + let shapeSettings, offset; + + // if the user passes the shape use that, if not, try to infer it from the geometry + if (!shapeType) shapeType = getShapeTypeFromGeometry(geometry); + switch (shapeType) { + case 'box': { + geometry.computeBoundingBox(); + const { boundingBox } = geometry; + let size; + // if the geometry is a box, use it's parameters not the bounding box + if (geometry instanceof BoxGeometry) { + const { width, height, depth } = geometry.parameters; + size = new Vector3(width, height, depth); + } else size = boundingBox!.getSize(new Vector3()); + + const shapeSize = new jolt.Vec3(size.x / 2, size.y / 2, size.z / 2); + shapeSettings = new jolt.BoxShapeSettings(shapeSize); + // jolt sucks at memory management + jolt.destroy(shapeSize); + + offset = boundingBox!.getCenter(new Vector3()); + break; + } - shapeSettings = new jolt.SphereShapeSettings(radius); - offset = boundingSphere!.center; - break; - } - case 'capsule': { - // values set by parameters - const { radius, length: height } = (geometry as CapsuleGeometry) - .parameters; - shapeSettings = new jolt.CapsuleShapeSettings(height / 2, radius); - offset = new Vector3(0, height / 2, 0); - break; - } + case 'sphere': { + geometry.computeBoundingSphere(); + const { boundingSphere } = geometry; + const radius = boundingSphere!.radius; - case 'cylinder': { - // Jolt Cylinder doesn't take a top and bottom radius, so we'll just use the top radius - const { radiusTop, height } = (geometry as CylinderGeometry).parameters; - - shapeSettings = new jolt.CylinderShapeSettings( - height / 2, - radiusTop, - 0.5 - ); - offset = new Vector3(0, height / 2, 0); - break; - } - // ConvexHull from points - // this won't be determined from geometry automatically, but the user can pass it - case 'convex': { - // generate a new geometry to hold the simplified geo - const simplifiedGeo = geometry.clone(); - // not sure this is needed. - //TODO: Check and cleanup if we need normals. if not merge from root geo - simplifiedGeo.computeVertexNormals(); - // merge points - const mergedPoints = BufferGeometryUtils.mergeVertices(simplifiedGeo); - const points = mergedPoints.getAttribute('position').array; - - // create the hull - shapeSettings = new jolt.ConvexHullShapeSettings(); - // add the points - for (let i = 0; i < points.length; i += 3) { - shapeSettings.mPoints.push_back( - new jolt.Vec3(points[i], points[i + 1], points[i + 2]) - ); - } - // Do we need to destroy the points, or will it destroy when the settings does? - // we can probably destroy all the three helper objects - // TODO: kill three objects + shapeSettings = new jolt.SphereShapeSettings(radius); + offset = boundingSphere!.center; + break; + } + case 'capsule': { + // values set by parameters + const { radius, length: height } = (geometry as CapsuleGeometry).parameters; + shapeSettings = new jolt.CapsuleShapeSettings(height / 2, radius); + offset = new Vector3(0, height / 2, 0); + break; + } - break; - } - // trimesh as default if nothing else passed - // using the buffer directly? which is better, array or direct? - // base pulled from: https://github.com/sajal353/r3f-jolt/blob/main/src/Jolt/useTrimesh.ts - default: { - const vertices = geometry.getAttribute('position'); - const indices = geometry.index!.array; - const verts = new jolt.VertexList(); - // loop over the bufferAttribute - for (let i = 0; i < vertices.count; i++) { - verts.push_back( - new jolt.Float3(vertices.getX(i), vertices.getY(i), vertices.getZ(i)) - ); - } - // make the triangle list - const tris = new jolt.IndexedTriangleList(); - for (let i = 0; i < indices.length; i += 3) { - tris.push_back( - new jolt.IndexedTriangle( - indices[i], - indices[i + 1], - indices[i + 2], - 0 - ) - ); - } - // not sure we need these mats - const mats = new jolt.PhysicsMaterialList(); - mats.push_back(new jolt.PhysicsMaterial()); + case 'cylinder': { + // Jolt Cylinder doesn't take a top and bottom radius, so we'll just use the top radius + const { radiusTop, height } = (geometry as CylinderGeometry).parameters; - shapeSettings = new jolt.MeshShapeSettings(verts, tris, mats); + shapeSettings = new jolt.CylinderShapeSettings(height / 2, radiusTop, 0.5); + offset = new Vector3(0, height / 2, 0); + break; + } + // ConvexHull from points + // this won't be determined from geometry automatically, but the user can pass it + case 'convex': { + // generate a new geometry to hold the simplified geo + const simplifiedGeo = geometry.clone(); + // not sure this is needed. + //TODO: Check and cleanup if we need normals. if not merge from root geo + simplifiedGeo.computeVertexNormals(); + // merge points + const mergedPoints = BufferGeometryUtils.mergeVertices(simplifiedGeo); + const points = mergedPoints.getAttribute('position').array; + + // create the hull + shapeSettings = new jolt.ConvexHullShapeSettings(); + // add the points + for (let i = 0; i < points.length; i += 3) { + shapeSettings.mPoints.push_back( + new jolt.Vec3(points[i], points[i + 1], points[i + 2]) + ); + } + // Do we need to destroy the points, or will it destroy when the settings does? + // we can probably destroy all the three helper objects + // TODO: kill three objects + + break; + } + // trimesh as default if nothing else passed + // using the buffer directly? which is better, array or direct? + // base pulled from: https://github.com/sajal353/r3f-jolt/blob/main/src/Jolt/useTrimesh.ts + default: { + const vertices = geometry.getAttribute('position'); + const indices = geometry.index!.array; + const verts = new jolt.VertexList(); + // loop over the bufferAttribute + for (let i = 0; i < vertices.count; i++) { + verts.push_back( + new jolt.Float3(vertices.getX(i), vertices.getY(i), vertices.getZ(i)) + ); + } + // make the triangle list + const tris = new jolt.IndexedTriangleList(); + for (let i = 0; i < indices.length; i += 3) { + tris.push_back( + new jolt.IndexedTriangle(indices[i], indices[i + 1], indices[i + 2], 0) + ); + } + // not sure we need these mats + const mats = new jolt.PhysicsMaterialList(); + mats.push_back(new jolt.PhysicsMaterial()); + + shapeSettings = new jolt.MeshShapeSettings(verts, tris, mats); + } } - } - return { shapeSettings, offset }; + return { shapeSettings, offset }; }; // take a threejs plane that is a heightfield and generate a Jolt heightfield shape // this is a WIP -export const generateHeightfieldShapeFromThree = ( - heightfieldPlane: THREE.Mesh -) => { - //TODO: resolve what these props do - //const mapScale = 0.35; - const BLOCK_SIZE = 2; - - const jolt = Raw.module; - const geometry = heightfieldPlane.geometry as THREE.PlaneGeometry; - const vertices = geometry.attributes.position.array as Float32Array; - const vertexCount = vertices.length / 3; - const size = Math.sqrt(vertexCount); - const planeWidth = geometry.parameters.width; - const scale = planeWidth / size; - //const positionVal = -size * scale * 0.5; - - // create the heightfield - const shapeSettings = new jolt.HeightFieldShapeSettings(); - shapeSettings.mOffset.Set(0, 0, 0); - shapeSettings.mScale.Set(scale, 1, scale); - shapeSettings.mSampleCount = size; - shapeSettings.mBlockSize = BLOCK_SIZE; - shapeSettings.mHeightSamples.resize(vertexCount); - - const heightSamples = new Float32Array( - jolt.HEAPF32.buffer, - jolt.getPointer(shapeSettings.mHeightSamples.data()), - vertexCount - ); // Convert the height samples into a Float32Array - //@ts-ignore - heightSamples.forEach((o, i) => { - heightSamples[i] = vertices[i * 3 + 1]; - //heightSamples[i] = 1; - // TODO: NOTE, this implementation does not allow holes in the map, which Jolt supports - //heightSamples[i] = Jolt.HeightFieldShapeConstantValues.prototype.cNoCollisionValue; // Invisible pixels make holes - }); - return shapeSettings; +export const generateHeightfieldShapeFromThree = (heightfieldPlane: THREE.Mesh) => { + //TODO: resolve what these props do + //const mapScale = 0.35; + const BLOCK_SIZE = 2; + + const jolt = Raw.module; + const geometry = heightfieldPlane.geometry as THREE.PlaneGeometry; + const vertices = geometry.attributes.position.array as Float32Array; + const vertexCount = vertices.length / 3; + const size = Math.sqrt(vertexCount); + const planeWidth = geometry.parameters.width; + const scale = planeWidth / size; + //const positionVal = -size * scale * 0.5; + + // create the heightfield + const shapeSettings = new jolt.HeightFieldShapeSettings(); + shapeSettings.mOffset.Set(0, 0, 0); + shapeSettings.mScale.Set(scale, 1, scale); + shapeSettings.mSampleCount = size; + shapeSettings.mBlockSize = BLOCK_SIZE; + shapeSettings.mHeightSamples.resize(vertexCount); + + const heightSamples = new Float32Array( + jolt.HEAPF32.buffer, + jolt.getPointer(shapeSettings.mHeightSamples.data()), + vertexCount + ); // Convert the height samples into a Float32Array + //@ts-ignore + heightSamples.forEach((o, i) => { + heightSamples[i] = vertices[i * 3 + 1]; + //heightSamples[i] = 1; + // TODO: NOTE, this implementation does not allow holes in the map, which Jolt supports + //heightSamples[i] = Jolt.HeightFieldShapeConstantValues.prototype.cNoCollisionValue; // Invisible pixels make holes + }); + return shapeSettings; }; // Take a complex Jolt shape and generate a ThreeJS mesh //taken from jolt examples export function createMeshForShape(shape: Jolt.Shape): THREE.BufferGeometry { - // Get triangle data - const scale = new Raw.module.Vec3(1, 1, 1); - const triContext = new Raw.module.ShapeGetTriangles( - shape, - Raw.module.AABox.prototype.sBiggest(), - shape.GetCenterOfMass(), - Raw.module.Quat.prototype.sIdentity(), - scale - ); - Raw.module.destroy(scale); - - // Get a view on the triangle data (does not make a copy) - const vertices = new Float32Array( - Raw.module.HEAPF32.buffer, - triContext.GetVerticesData(), - triContext.GetVerticesSize() / Float32Array.BYTES_PER_ELEMENT - ); - - // Now move the triangle data to a buffer and clone it so that we can free the memory from the C++ heap (which could be limited in size) - const buffer = new THREE.BufferAttribute(vertices, 3).clone(); - Raw.module.destroy(triContext); - - // Create a three mesh - const geometry = new THREE.BufferGeometry(); - geometry.setAttribute('position', buffer); - geometry.computeVertexNormals(); - - return geometry; + // Get triangle data + const scale = new Raw.module.Vec3(1, 1, 1); + const triContext = new Raw.module.ShapeGetTriangles( + shape, + Raw.module.AABox.prototype.sBiggest(), + shape.GetCenterOfMass(), + Raw.module.Quat.prototype.sIdentity(), + scale + ); + Raw.module.destroy(scale); + + // Get a view on the triangle data (does not make a copy) + const vertices = new Float32Array( + Raw.module.HEAPF32.buffer, + triContext.GetVerticesData(), + triContext.GetVerticesSize() / Float32Array.BYTES_PER_ELEMENT + ); + + // Now move the triangle data to a buffer and clone it so that we can free the memory from the C++ heap (which could be limited in size) + const buffer = new THREE.BufferAttribute(vertices, 3).clone(); + Raw.module.destroy(triContext); + + // Create a three mesh + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute('position', buffer); + geometry.computeVertexNormals(); + + return geometry; } diff --git a/packages/react-three-jolt/src/systems/vehicles/VehicleManagerFourWheel.ts b/packages/react-three-jolt/src/systems/vehicles/VehicleManagerFourWheel.ts index fe4afe7..44533d8 100644 --- a/packages/react-three-jolt/src/systems/vehicles/VehicleManagerFourWheel.ts +++ b/packages/react-three-jolt/src/systems/vehicles/VehicleManagerFourWheel.ts @@ -6,9 +6,9 @@ import { vec3, quat } from '../../utils'; */ import { PhysicsSystem } from '../physics-system'; import { - VehicleFourWheelSettings, - // WheelState, - //createWheelSettings + VehicleFourWheelSettings + // WheelState, + //createWheelSettings } from './wheels'; import { VehicleManager } from './VehicleManager'; /* @@ -18,10 +18,7 @@ const BL_WHEEL = 2; const BR_WHEEL = 3; */ export class VehicleFourWheelManager extends VehicleManager { - constructor( - physicsSystem: PhysicsSystem, - settings: VehicleFourWheelSettings - ) { - super(physicsSystem, settings); - } + constructor(physicsSystem: PhysicsSystem, settings: VehicleFourWheelSettings) { + super(physicsSystem, settings); + } } diff --git a/packages/react-three-jolt/src/systems/vehicles/VehicleManagerTwoWheels.ts b/packages/react-three-jolt/src/systems/vehicles/VehicleManagerTwoWheels.ts index 46bbb14..dbc9231 100644 --- a/packages/react-three-jolt/src/systems/vehicles/VehicleManagerTwoWheels.ts +++ b/packages/react-three-jolt/src/systems/vehicles/VehicleManagerTwoWheels.ts @@ -5,9 +5,9 @@ import { vec3, quat } from '../../utils'; import { PhysicsSystem } from '../physics-system'; import { Layer } from '../../constants'; import { - VehicleFourWheelSettings, - WheelState, - //createWheelSettings + VehicleFourWheelSettings, + WheelState + //createWheelSettings } from './wheels'; import { VehicleManager } from './VehicleManager'; @@ -20,244 +20,228 @@ const BR_WHEEL = 3; //TODO Fix this type interface VehicleTwoWheelSettings extends VehicleFourWheelSettings { - backWheelRadius: number; - backWheelWidth: number; - backWheelPosZ: number; - backWheelSuspensionMinLength: number; - backWheelSuspensionMaxLength: number; - backSuspensionFreq: number; - backBrakeTorque: number; - - frontWheelRadius: number; - frontWheelWidth: number; - frontWheelPosZ: number; - frontSuspensionMinLength: number; - frontSuspensionMaxLength: number; - frontSuspensionFreq: number; - frontBrakeTorque: number; - - steerSpeed: number; - casterAngle: number; - maxPitchRollAngle: number; + backWheelRadius: number; + backWheelWidth: number; + backWheelPosZ: number; + backWheelSuspensionMinLength: number; + backWheelSuspensionMaxLength: number; + backSuspensionFreq: number; + backBrakeTorque: number; + + frontWheelRadius: number; + frontWheelWidth: number; + frontWheelPosZ: number; + frontSuspensionMinLength: number; + frontSuspensionMaxLength: number; + frontSuspensionFreq: number; + frontBrakeTorque: number; + + steerSpeed: number; + casterAngle: number; + maxPitchRollAngle: number; } export class VehicleManagerTwoWheels extends VehicleManager { - currentRight = 0; - settings: VehicleTwoWheelSettings; - constructor(physicsSystem: PhysicsSystem, settings: VehicleTwoWheelSettings) { - super(physicsSystem, settings); - // TODO: is this necessary? - this.settings = settings; - } - - // this createBody is different for motorcycles - createBody(): Jolt.Body { - // from jolt example - const motorcycleShapeSettings = - new Raw.module.OffsetCenterOfMassShapeSettings( - new Raw.module.Vec3(0, -this.settings.vehicleHeight! / 2, 0), - new Raw.module.BoxShapeSettings( - new Raw.module.Vec3( - this.settings.vehicleWidth! / 2, - this.settings.vehicleHeight! / 2, - this.settings.vehicleLength! / 2 - ) - ) - ); - const motorcycleShape = motorcycleShapeSettings.Create().Get(); - const motorcycleBodySettings = new Raw.module.BodyCreationSettings( - motorcycleShape, - new Raw.module.RVec3(...this.settings.bodyPosition), - Raw.module.Quat.prototype.sRotation( - new Raw.module.Vec3(0, 1, 0), - Math.PI - ), - Raw.module.EMotionType_Dynamic, - Layer.MOVING - ); - motorcycleBodySettings.mOverrideMassProperties = - Raw.module.EOverrideMassProperties_CalculateInertia; - motorcycleBodySettings.mMassPropertiesOverride.mMass = - this.settings.vehicleMass! | 250; - const motorcycleBody = this.physicsSystem.bodyInterface.CreateBody( - motorcycleBodySettings - ); - // DONT FORGET TO ADD TO THE SIMULATION - this.physicsSystem.bodyInterface.AddBody( - motorcycleBody.GetID(), - Raw.module.EActivation_Activate - ); - this.carBody = motorcycleBody; - const debugMesh = new THREE.Mesh( - new THREE.BoxGeometry( - this.settings.vehicleWidth!, - this.settings.vehicleHeight!, - this.settings.vehicleLength! - ), - new THREE.MeshStandardMaterial({ color: '#EAF0CE' }) - ); - this.threeObject.add(debugMesh); - - return motorcycleBody; - } - // create the primary Jolt items and generate the wheels - createConstraint() { - const vehicle = new Raw.module.VehicleConstraintSettings(); - vehicle.mMaxPitchRollAngle = this.settings.maxPitchRollAngle!; - vehicle.mWheels.clear(); - - // motorcycle makes the wheels declaritively. have to figure out the wheelState - // TODO rewrite these to use the createWheelSettings() - const front = new Raw.module.WheelSettingsWV(); - const frontPosition = new THREE.Vector3( - 0.0, - (-0.9 * this.settings.vehicleHeight!) / 2, - //@ts-ignore - this.settings.wheels.front.posZ - ); - front.mPosition = vec3.jolt(frontPosition); - //@ts-ignore - front.mMaxSteerAngle = this.settings.wheels.front.maxSteerAngle; - front.mSuspensionDirection = new Raw.module.Vec3( - 0, - -1, - Math.tan(this.settings.casterAngle) - ).Normalized(); - front.mSteeringAxis = new Raw.module.Vec3( - 0, - 1, - -Math.tan(this.settings.casterAngle) - ).Normalized(); - - if (this.settings.wheels.radius) - front.mRadius = this.settings.wheels.radius; - if (this.settings.wheels.width) front.mWidth = this.settings.wheels.width; - if (this.settings.wheels.suspensionMinLength) - front.mSuspensionMinLength = this.settings.wheels.suspensionMinLength; - if (this.settings.wheels.suspensionMaxLength) - front.mSuspensionMaxLength = this.settings.wheels.suspensionMaxLength; - front.mSuspensionSpring.mFrequency = - //@ts-ignore - this.settings.wheels.front.suspensionFreq; - //@ts-ignore - front.mMaxBrakeTorque = this.settings.wheels.front.brakeTorque; - - vehicle.mWheels.push_back(front); - - const back = new Raw.module.WheelSettingsWV(); - back.mPosition = new Raw.module.Vec3( - 0.0, - (-0.9 * this.settings.vehicleHeight!) / 2, - //@ts-ignore - this.settings.wheels.back.posZ - ); - back.mMaxSteerAngle = 0.0; - if (this.settings.wheels.radius) back.mRadius = this.settings.wheels.radius; - if (this.settings.wheels.width) back.mWidth = this.settings.wheels.width; - if (this.settings.wheels.suspensionMinLength) - back.mSuspensionMinLength = this.settings.wheels.suspensionMinLength; - if (this.settings.wheels.suspensionMaxLength) - back.mSuspensionMaxLength = this.settings.wheels.suspensionMaxLength; - back.mSuspensionSpring.mFrequency = - //@ts-ignore - this.settings.wheels.back.suspensionFreq; - //@ts-ignore - back.mMaxBrakeTorque = this.settings.wheels.back.brakeTorque; - - vehicle.mWheels.push_back(back); - - // create the controller - const controllerSettings = new Raw.module.MotorcycleControllerSettings(); - controllerSettings.mEngine.mMaxTorque = 150.0; - controllerSettings.mEngine.mMinRPM = 1000.0; - controllerSettings.mEngine.mMaxRPM = 10000.0; - controllerSettings.mTransmission.mShiftDownRPM = 2000.0; - controllerSettings.mTransmission.mShiftUpRPM = 8000.0; - controllerSettings.mTransmission.mClutchStrength = 2.0; - vehicle.mController = controllerSettings; - - controllerSettings.mDifferentials.clear(); - const differential = new Raw.module.VehicleDifferentialSettings(); - differential.mLeftWheel = -1; - differential.mRightWheel = 1; - differential.mDifferentialRatio = (1.93 * 40.0) / 16.0; - controllerSettings.mDifferentials.push_back(differential); - - this.constraint = new Raw.module.VehicleConstraint(this.carBody, vehicle); - - const tester = new Raw.module.VehicleCollisionTesterCastCylinder( - Layer.MOVING, - 1 - ); - this.constraint.SetVehicleCollisionTester(tester); - - console.log('wheel count', vehicle.mWheels.size()); - // now we have the constraint we can set the wheelStates - const frontState = new WheelState(this.constraint, 0); - this.wheels.set('front', frontState); - this.threeObject.add(frontState.threeObject); - //now the back - const backState = new WheelState(this.constraint, 1); - this.wheels.set('back', backState); - this.threeObject.add(backState.threeObject); - - // add the constraint to the physics system - this.physicsSystem.physicsSystem.AddConstraint(this.constraint); - // SUPER IMPORTANT WEIRD LOOP LISTENER - this.physicsSystem.physicsSystem.AddStepListener( - new Raw.module.VehicleConstraintStepListener(this.constraint) - ); - this.controller = Raw.module.castObject( - this.constraint.GetController(), - Raw.module.MotorcycleController - ); - } - - // run the physics step - prePhysicsUpdate(deltaTime: number): void { - let forward = this.moveDirection.y; - let right = this.moveDirection.x; - let brake = 0.0, - handBrake = 0.0; - - if (this.previousForward * forward < 0.0) { - const rotation = quat.joltToThree( - this.carBody.GetRotation().Conjugated() - ); - const linearV = vec3.three(this.carBody.GetLinearVelocity()); - const velocity = linearV.applyQuaternion(rotation).z; - if ( - (forward > 0.0 && velocity < -0.1) || - (forward < 0.0 && velocity > 0.1) - ) { - // Brake while we've not stopped yet - forward = 0.0; - brake = 1.0; - } else { - // When we've come to a stop, accept the new direction - this.previousForward = forward; - } + currentRight = 0; + settings: VehicleTwoWheelSettings; + constructor(physicsSystem: PhysicsSystem, settings: VehicleTwoWheelSettings) { + super(physicsSystem, settings); + // TODO: is this necessary? + this.settings = settings; } - if (this.handBrake) { - forward = 0.0; - handBrake = 1.0; + // this createBody is different for motorcycles + createBody(): Jolt.Body { + // from jolt example + const motorcycleShapeSettings = new Raw.module.OffsetCenterOfMassShapeSettings( + new Raw.module.Vec3(0, -this.settings.vehicleHeight! / 2, 0), + new Raw.module.BoxShapeSettings( + new Raw.module.Vec3( + this.settings.vehicleWidth! / 2, + this.settings.vehicleHeight! / 2, + this.settings.vehicleLength! / 2 + ) + ) + ); + const motorcycleShape = motorcycleShapeSettings.Create().Get(); + const motorcycleBodySettings = new Raw.module.BodyCreationSettings( + motorcycleShape, + new Raw.module.RVec3(...this.settings.bodyPosition), + Raw.module.Quat.prototype.sRotation(new Raw.module.Vec3(0, 1, 0), Math.PI), + Raw.module.EMotionType_Dynamic, + Layer.MOVING + ); + motorcycleBodySettings.mOverrideMassProperties = + Raw.module.EOverrideMassProperties_CalculateInertia; + motorcycleBodySettings.mMassPropertiesOverride.mMass = this.settings.vehicleMass! | 250; + const motorcycleBody = this.physicsSystem.bodyInterface.CreateBody(motorcycleBodySettings); + // DONT FORGET TO ADD TO THE SIMULATION + this.physicsSystem.bodyInterface.AddBody( + motorcycleBody.GetID(), + Raw.module.EActivation_Activate + ); + this.carBody = motorcycleBody; + const debugMesh = new THREE.Mesh( + new THREE.BoxGeometry( + this.settings.vehicleWidth!, + this.settings.vehicleHeight!, + this.settings.vehicleLength! + ), + new THREE.MeshStandardMaterial({ color: '#EAF0CE' }) + ); + this.threeObject.add(debugMesh); + + return motorcycleBody; + } + // create the primary Jolt items and generate the wheels + createConstraint() { + const vehicle = new Raw.module.VehicleConstraintSettings(); + vehicle.mMaxPitchRollAngle = this.settings.maxPitchRollAngle!; + vehicle.mWheels.clear(); + + // motorcycle makes the wheels declaritively. have to figure out the wheelState + // TODO rewrite these to use the createWheelSettings() + const front = new Raw.module.WheelSettingsWV(); + const frontPosition = new THREE.Vector3( + 0.0, + (-0.9 * this.settings.vehicleHeight!) / 2, + //@ts-ignore + this.settings.wheels.front.posZ + ); + front.mPosition = vec3.jolt(frontPosition); + //@ts-ignore + front.mMaxSteerAngle = this.settings.wheels.front.maxSteerAngle; + front.mSuspensionDirection = new Raw.module.Vec3( + 0, + -1, + Math.tan(this.settings.casterAngle) + ).Normalized(); + front.mSteeringAxis = new Raw.module.Vec3( + 0, + 1, + -Math.tan(this.settings.casterAngle) + ).Normalized(); + + if (this.settings.wheels.radius) front.mRadius = this.settings.wheels.radius; + if (this.settings.wheels.width) front.mWidth = this.settings.wheels.width; + if (this.settings.wheels.suspensionMinLength) + front.mSuspensionMinLength = this.settings.wheels.suspensionMinLength; + if (this.settings.wheels.suspensionMaxLength) + front.mSuspensionMaxLength = this.settings.wheels.suspensionMaxLength; + front.mSuspensionSpring.mFrequency = + //@ts-ignore + this.settings.wheels.front.suspensionFreq; + //@ts-ignore + front.mMaxBrakeTorque = this.settings.wheels.front.brakeTorque; + + vehicle.mWheels.push_back(front); + + const back = new Raw.module.WheelSettingsWV(); + back.mPosition = new Raw.module.Vec3( + 0.0, + (-0.9 * this.settings.vehicleHeight!) / 2, + //@ts-ignore + this.settings.wheels.back.posZ + ); + back.mMaxSteerAngle = 0.0; + if (this.settings.wheels.radius) back.mRadius = this.settings.wheels.radius; + if (this.settings.wheels.width) back.mWidth = this.settings.wheels.width; + if (this.settings.wheels.suspensionMinLength) + back.mSuspensionMinLength = this.settings.wheels.suspensionMinLength; + if (this.settings.wheels.suspensionMaxLength) + back.mSuspensionMaxLength = this.settings.wheels.suspensionMaxLength; + back.mSuspensionSpring.mFrequency = + //@ts-ignore + this.settings.wheels.back.suspensionFreq; + //@ts-ignore + back.mMaxBrakeTorque = this.settings.wheels.back.brakeTorque; + + vehicle.mWheels.push_back(back); + + // create the controller + const controllerSettings = new Raw.module.MotorcycleControllerSettings(); + controllerSettings.mEngine.mMaxTorque = 150.0; + controllerSettings.mEngine.mMinRPM = 1000.0; + controllerSettings.mEngine.mMaxRPM = 10000.0; + controllerSettings.mTransmission.mShiftDownRPM = 2000.0; + controllerSettings.mTransmission.mShiftUpRPM = 8000.0; + controllerSettings.mTransmission.mClutchStrength = 2.0; + vehicle.mController = controllerSettings; + + controllerSettings.mDifferentials.clear(); + const differential = new Raw.module.VehicleDifferentialSettings(); + differential.mLeftWheel = -1; + differential.mRightWheel = 1; + differential.mDifferentialRatio = (1.93 * 40.0) / 16.0; + controllerSettings.mDifferentials.push_back(differential); + + this.constraint = new Raw.module.VehicleConstraint(this.carBody, vehicle); + + const tester = new Raw.module.VehicleCollisionTesterCastCylinder(Layer.MOVING, 1); + this.constraint.SetVehicleCollisionTester(tester); + + console.log('wheel count', vehicle.mWheels.size()); + // now we have the constraint we can set the wheelStates + const frontState = new WheelState(this.constraint, 0); + this.wheels.set('front', frontState); + this.threeObject.add(frontState.threeObject); + //now the back + const backState = new WheelState(this.constraint, 1); + this.wheels.set('back', backState); + this.threeObject.add(backState.threeObject); + + // add the constraint to the physics system + this.physicsSystem.physicsSystem.AddConstraint(this.constraint); + // SUPER IMPORTANT WEIRD LOOP LISTENER + this.physicsSystem.physicsSystem.AddStepListener( + new Raw.module.VehicleConstraintStepListener(this.constraint) + ); + this.controller = Raw.module.castObject( + this.constraint.GetController(), + Raw.module.MotorcycleController + ); } - if (right > this.currentRight) - this.currentRight = Math.min( - this.currentRight + this.settings.steerSpeed * deltaTime, - right - ); - else if (right < this.currentRight) - this.currentRight = Math.max( - this.currentRight - this.settings.steerSpeed * deltaTime, - right - ); - right = this.currentRight; - - this.controller.SetDriverInput(forward, right, brake, handBrake); - if (right != 0.0 || forward != 0.0 || brake != 0.0 || handBrake != 0.0) - this.physicsSystem.bodyInterface.ActivateBody(this.carBody.GetID()); - } + // run the physics step + prePhysicsUpdate(deltaTime: number): void { + let forward = this.moveDirection.y; + let right = this.moveDirection.x; + let brake = 0.0, + handBrake = 0.0; + + if (this.previousForward * forward < 0.0) { + const rotation = quat.joltToThree(this.carBody.GetRotation().Conjugated()); + const linearV = vec3.three(this.carBody.GetLinearVelocity()); + const velocity = linearV.applyQuaternion(rotation).z; + if ((forward > 0.0 && velocity < -0.1) || (forward < 0.0 && velocity > 0.1)) { + // Brake while we've not stopped yet + forward = 0.0; + brake = 1.0; + } else { + // When we've come to a stop, accept the new direction + this.previousForward = forward; + } + } + + if (this.handBrake) { + forward = 0.0; + handBrake = 1.0; + } + + if (right > this.currentRight) + this.currentRight = Math.min( + this.currentRight + this.settings.steerSpeed * deltaTime, + right + ); + else if (right < this.currentRight) + this.currentRight = Math.max( + this.currentRight - this.settings.steerSpeed * deltaTime, + right + ); + right = this.currentRight; + + this.controller.SetDriverInput(forward, right, brake, handBrake); + if (right != 0.0 || forward != 0.0 || brake != 0.0 || handBrake != 0.0) + this.physicsSystem.bodyInterface.ActivateBody(this.carBody.GetID()); + } } diff --git a/packages/react-three-jolt/src/systems/vehicles/vehicle-system.ts b/packages/react-three-jolt/src/systems/vehicles/vehicle-system.ts index 604d3bd..dcd786c 100644 --- a/packages/react-three-jolt/src/systems/vehicles/vehicle-system.ts +++ b/packages/react-three-jolt/src/systems/vehicles/vehicle-system.ts @@ -10,150 +10,150 @@ import { VehicleFourWheelManager } from './VehicleManagerFourWheel'; import { VehicleManagerTwoWheels } from './VehicleManagerTwoWheels'; export class VehicleSystem { - private physicsSystem; + private physicsSystem; - // default car settings - defaultVehicleSettings = { - type: 'fourWheel', - bodyPosition: [0, 4, 0], - castType: 'cylinder', + // default car settings + defaultVehicleSettings = { + type: 'fourWheel', + bodyPosition: [0, 4, 0], + castType: 'cylinder', - vehicleLength: 4.0, - vehicleWidth: 1.8, - vehicleHeight: 0.4, - fourWheelDrive: true, - frontBackLimitedSlipRatio: 1.4, - leftRightLimitedSlipRatio: 1.4, - antiRollbar: true, + vehicleLength: 4.0, + vehicleWidth: 1.8, + vehicleHeight: 0.4, + fourWheelDrive: true, + frontBackLimitedSlipRatio: 1.4, + leftRightLimitedSlipRatio: 1.4, + antiRollbar: true, - vehicleMass: 1500.0, - maxEngineTorque: 500.0, - clutchStrength: 10.0, + vehicleMass: 1500.0, + maxEngineTorque: 500.0, + clutchStrength: 10.0, - //ds additional settings - splitEngineTorqueFront: 0.5, - splitEngineTorqueRear: 0.5, - // wheel settings - wheels: { - width: 0.3, - radius: 0.5, - wheelOffsetHorizontal: 1.4, - wheelOffsetVertical: 0.18, - suspensionMinLength: 0.3, - suspensionMaxLength: 0.5, - maxSteerAngle: THREE.MathUtils.degToRad(30), - fl: { - maxHandBrakeTorque: 0, - }, - fr: { - maxHandBrakeTorque: 0, - }, - bl: { - maxSteerAngle: 0, - }, - br: { - maxSteerAngle: 0, - }, - }, - }; + //ds additional settings + splitEngineTorqueFront: 0.5, + splitEngineTorqueRear: 0.5, + // wheel settings + wheels: { + width: 0.3, + radius: 0.5, + wheelOffsetHorizontal: 1.4, + wheelOffsetVertical: 0.18, + suspensionMinLength: 0.3, + suspensionMaxLength: 0.5, + maxSteerAngle: THREE.MathUtils.degToRad(30), + fl: { + maxHandBrakeTorque: 0 + }, + fr: { + maxHandBrakeTorque: 0 + }, + bl: { + maxSteerAngle: 0 + }, + br: { + maxSteerAngle: 0 + } + } + }; - defaultVehicleSettingsTwoWheels = { - type: 'twoWheel', - steerSpeed: 4, - casterAngle: THREE.MathUtils.degToRad(30), - maxPitchRollAngle: THREE.MathUtils.degToRad(60), - vehicleLength: 0.8, - vehicleWidth: 0.4, - vehicleHeight: 0.6, - vehicleMass: 250, - wheels: { - radius: 0.31, - width: 0.05, + defaultVehicleSettingsTwoWheels = { + type: 'twoWheel', + steerSpeed: 4, + casterAngle: THREE.MathUtils.degToRad(30), + maxPitchRollAngle: THREE.MathUtils.degToRad(60), + vehicleLength: 0.8, + vehicleWidth: 0.4, + vehicleHeight: 0.6, + vehicleMass: 250, + wheels: { + radius: 0.31, + width: 0.05, - suspensionMinLength: 0.3, - suspensionMaxLength: 0.5, - front: { - suspensionFreq: 1.5, - brakeTorque: 500.0, - posZ: 0.75, - maxSteerAngle: THREE.MathUtils.degToRad(30), - }, - back: { - suspensionFreq: 2.0, - brakeTorque: 250.0, - posZ: -0.75, - maxSteerAngle: 0.0, - }, - }, - }; + suspensionMinLength: 0.3, + suspensionMaxLength: 0.5, + front: { + suspensionFreq: 1.5, + brakeTorque: 500.0, + posZ: 0.75, + maxSteerAngle: THREE.MathUtils.degToRad(30) + }, + back: { + suspensionFreq: 2.0, + brakeTorque: 250.0, + posZ: -0.75, + maxSteerAngle: 0.0 + } + } + }; - vehicles = new Map(); + vehicles = new Map(); - constructor(physicSystem: PhysicsSystem) { - this.physicsSystem = physicSystem; - this.attachToLoop(); - } + constructor(physicSystem: PhysicsSystem) { + this.physicsSystem = physicSystem; + this.attachToLoop(); + } - //creates a new settings object by taking the default and input - createVehicleSettings(settings: any) { - const base = { ...this.defaultVehicleSettings, ...settings }; - if (base.type === 'twoWheel') { - return mergeSettings(base, this.defaultVehicleSettingsTwoWheels); + //creates a new settings object by taking the default and input + createVehicleSettings(settings: any) { + const base = { ...this.defaultVehicleSettings, ...settings }; + if (base.type === 'twoWheel') { + return mergeSettings(base, this.defaultVehicleSettingsTwoWheels); + } + return base; } - return base; - } - addVehicle(name: string, settings?: any) { - settings = this.createVehicleSettings(settings); - let vehicle; - console.log('adding vehicle', name, settings.type); - switch (settings.type) { - case 'twoWheel': - vehicle = new VehicleManagerTwoWheels(this.physicsSystem, settings); - break; - default: - vehicle = new VehicleFourWheelManager(this.physicsSystem, settings); - break; + addVehicle(name: string, settings?: any) { + settings = this.createVehicleSettings(settings); + let vehicle; + console.log('adding vehicle', name, settings.type); + switch (settings.type) { + case 'twoWheel': + vehicle = new VehicleManagerTwoWheels(this.physicsSystem, settings); + break; + default: + vehicle = new VehicleFourWheelManager(this.physicsSystem, settings); + break; + } + //console.log('adding vehicle', name, vehicle); + this.vehicles.set(name, vehicle); + return vehicle; } - //console.log('adding vehicle', name, vehicle); - this.vehicles.set(name, vehicle); - return vehicle; - } - //* Control vehicles by name ======================== - getVehicle(name: string) { - return this.vehicles.get(name); - } - setPosition(name: string, position: THREE.Vector3) { - const vehicle = this.getVehicle(name); - if (!vehicle) return; - vehicle.setPosition(position); - } + //* Control vehicles by name ======================== + getVehicle(name: string) { + return this.vehicles.get(name); + } + setPosition(name: string, position: THREE.Vector3) { + const vehicle = this.getVehicle(name); + if (!vehicle) return; + vehicle.setPosition(position); + } - //* Physics Loop ==================================== + //* Physics Loop ==================================== - private attachToLoop() { - this.physicsSystem.addPreStepListener((deltaTime: number) => - this.prePhysicsUpdate(deltaTime) - ); - this.physicsSystem.addPostStepListener((deltaTime: number) => - this.postPhysicsUpdate(deltaTime) - ); - } + private attachToLoop() { + this.physicsSystem.addPreStepListener((deltaTime: number) => + this.prePhysicsUpdate(deltaTime) + ); + this.physicsSystem.addPostStepListener((deltaTime: number) => + this.postPhysicsUpdate(deltaTime) + ); + } - prePhysicsUpdate(deltaTime: number) { - this.vehicles.forEach((vehicle) => { - vehicle.prePhysicsUpdate(deltaTime); - }); - } - postPhysicsUpdate(deltaTime: number) { - this.vehicles.forEach((vehicle) => { - vehicle.postPhysicsUpdate(deltaTime); - }); - } + prePhysicsUpdate(deltaTime: number) { + this.vehicles.forEach((vehicle) => { + vehicle.prePhysicsUpdate(deltaTime); + }); + } + postPhysicsUpdate(deltaTime: number) { + this.vehicles.forEach((vehicle) => { + vehicle.postPhysicsUpdate(deltaTime); + }); + } } //utils // take two settings objects, clone them, and then merge them function mergeSettings(defaultSettings: any, settings: any) { - return { ...defaultSettings, ...settings }; + return { ...defaultSettings, ...settings }; } diff --git a/packages/react-three-jolt/src/systems/vehicles/wheels.ts b/packages/react-three-jolt/src/systems/vehicles/wheels.ts index a4acb80..3d473e2 100644 --- a/packages/react-three-jolt/src/systems/vehicles/wheels.ts +++ b/packages/react-three-jolt/src/systems/vehicles/wheels.ts @@ -5,190 +5,182 @@ import { vec3, quat, joltPropName } from '../../utils'; //* Types ==================================== export type VehicleFourWheelSettings = { - type?: string; - bodyPosition: Vector; - castType: string; - wheelRadius?: number; - wheelWidth?: number; - vehicleLength?: number; - vehicleWidth?: number; - vehicleHeight?: number; - fourWheelDrive?: boolean; - frontBackLimitedSlipRatio?: number; - leftRightLimitedSlipRatio?: number; - antiRollbar?: boolean; - vehicleMass?: number; - maxEngineTorque?: number; - clutchStrength?: number; - previousForward?: number; + type?: string; + bodyPosition: Vector; + castType: string; + wheelRadius?: number; + wheelWidth?: number; + vehicleLength?: number; + vehicleWidth?: number; + vehicleHeight?: number; + fourWheelDrive?: boolean; + frontBackLimitedSlipRatio?: number; + leftRightLimitedSlipRatio?: number; + antiRollbar?: boolean; + vehicleMass?: number; + maxEngineTorque?: number; + clutchStrength?: number; + previousForward?: number; - // DS additional Settings - splitEngineTorqueFront?: number; - splitEngineTorqueRear?: number; - //rollbar stiffness - frontRollBarStiffness?: number; - rearRollBarStiffness?: number; - wheels: WheelSettingsFourWheelOveride; + // DS additional Settings + splitEngineTorqueFront?: number; + splitEngineTorqueRear?: number; + //rollbar stiffness + frontRollBarStiffness?: number; + rearRollBarStiffness?: number; + wheels: WheelSettingsFourWheelOveride; }; export type Vector = [number, number, number]; // see https://jrouwe.github.io/JoltPhysics/class_wheel_settings_w_v.html export interface WheelSettings { - inertia?: number; - angularDamping?: number; - width?: number; - radius?: number; - //attachment point to the body in local space [0,0,0] - position?: Vector; - // where force is applied, best to be center of wheel [0,0,0] - suspensionForcePoint?: Vector; - // should point down [0, -1, 0] - suspensionDirection?: Vector; - //think like a bike suspension pointing towards the bike [0,1,0] - steeringAxis?: Vector; - // can be used to give camber - wheelUp?: Vector; - //can be used to give toe - wheelForward?: Vector; - suspensionMinLength?: number; //0.3 - suspensionMaxLength?: number; //0.5 - //gives the springs more bounce - suspensionPreloadLength?: number; //0.0 - //DONT USE THIS - enableSuspensionForcePoint?: boolean; - //springs - SuspensionSpring?: { frequency: number; damping: number }; + inertia?: number; + angularDamping?: number; + width?: number; + radius?: number; + //attachment point to the body in local space [0,0,0] + position?: Vector; + // where force is applied, best to be center of wheel [0,0,0] + suspensionForcePoint?: Vector; + // should point down [0, -1, 0] + suspensionDirection?: Vector; + //think like a bike suspension pointing towards the bike [0,1,0] + steeringAxis?: Vector; + // can be used to give camber + wheelUp?: Vector; + //can be used to give toe + wheelForward?: Vector; + suspensionMinLength?: number; //0.3 + suspensionMaxLength?: number; //0.5 + //gives the springs more bounce + suspensionPreloadLength?: number; //0.0 + //DONT USE THIS + enableSuspensionForcePoint?: boolean; + //springs + SuspensionSpring?: { frequency: number; damping: number }; } export interface WheelSettingsFourWheel extends WheelSettings { - // Four WHeel settings - maxSteerAngle?: number; //1.22 radians(70 degrees - //Longitudinal Friction amd Lateral Friction are in curves - // not messing with them for now - maxBrakeTorque?: number; // 1500 - maxHandBrakeTorque?: number; // 4000 + // Four WHeel settings + maxSteerAngle?: number; //1.22 radians(70 degrees + //Longitudinal Friction amd Lateral Friction are in curves + // not messing with them for now + maxBrakeTorque?: number; // 1500 + maxHandBrakeTorque?: number; // 4000 } interface WheelSettingsFourWheelOveride extends WheelSettingsFourWheel { - fl: WheelSettingsFourWheel; - fr: WheelSettingsFourWheel; - bl: WheelSettingsFourWheel; - br: WheelSettingsFourWheel; + fl: WheelSettingsFourWheel; + fr: WheelSettingsFourWheel; + bl: WheelSettingsFourWheel; + br: WheelSettingsFourWheel; } //* End Types ==================================== export class WheelState { - index: number; - constraint; - threeObject = new THREE.Object3D(); - //@ts-ignore ts bug, created in createDebugWheel - debugObject: THREE.Mesh; - // because jolt isnt ready we'll put these here - wheelRight = new Raw.module.Vec3(0, 1, 0); - wheelUp = new Raw.module.Vec3(1, 0, 0); - //true for now - //TODO change this to default to false - private isDebugging = true; - set debug(value) { - this.isDebugging = value; - if (value) this.debugObject.visible = true; - else this.debugObject.visible = false; - } - get debug() { - return this.isDebugging; - } - // Im not sure we need this but I'll leave it for now - wheelSettings; - joltWheel; - constructor(constraint: any, wheelIndex: number) { - this.constraint = constraint; - this.index = wheelIndex; - this.joltWheel = constraint.GetWheel(wheelIndex); - this.wheelSettings = this.joltWheel.GetSettings(); - this.createDebugWheel(); - } - createDebugWheel() { - const geometry = new THREE.CylinderGeometry( - this.wheelSettings.mRadius, - this.wheelSettings.mRadius, - this.wheelSettings.mWidth, - 20, - 1 - ); - const mesh = new THREE.Mesh(geometry, getWheelMaterial()); - this.debugObject = mesh; - this.threeObject.add(mesh); - return mesh; - } - add(object: THREE.Object3D) { - this.threeObject.add(object); - } - // set the wheel position and rotation - updateLocalTransform() { - if (!this.threeObject) return; - const transform = this.constraint.GetWheelLocalTransform( - this.index, - this.wheelRight, - this.wheelUp - ); + index: number; + constraint; + threeObject = new THREE.Object3D(); + //@ts-ignore ts bug, created in createDebugWheel + debugObject: THREE.Mesh; + // because jolt isnt ready we'll put these here + wheelRight = new Raw.module.Vec3(0, 1, 0); + wheelUp = new Raw.module.Vec3(1, 0, 0); + //true for now + //TODO change this to default to false + private isDebugging = true; + set debug(value) { + this.isDebugging = value; + if (value) this.debugObject.visible = true; + else this.debugObject.visible = false; + } + get debug() { + return this.isDebugging; + } + // Im not sure we need this but I'll leave it for now + wheelSettings; + joltWheel; + constructor(constraint: any, wheelIndex: number) { + this.constraint = constraint; + this.index = wheelIndex; + this.joltWheel = constraint.GetWheel(wheelIndex); + this.wheelSettings = this.joltWheel.GetSettings(); + this.createDebugWheel(); + } + createDebugWheel() { + const geometry = new THREE.CylinderGeometry( + this.wheelSettings.mRadius, + this.wheelSettings.mRadius, + this.wheelSettings.mWidth, + 20, + 1 + ); + const mesh = new THREE.Mesh(geometry, getWheelMaterial()); + this.debugObject = mesh; + this.threeObject.add(mesh); + return mesh; + } + add(object: THREE.Object3D) { + this.threeObject.add(object); + } + // set the wheel position and rotation + updateLocalTransform() { + if (!this.threeObject) return; + const transform = this.constraint.GetWheelLocalTransform( + this.index, + this.wheelRight, + this.wheelUp + ); - this.threeObject.position.copy(vec3.three(transform.GetTranslation())); - this.threeObject.quaternion.copy( - quat.joltToThree(transform.GetRotation().GetQuaternion()) - ); - } + this.threeObject.position.copy(vec3.three(transform.GetTranslation())); + this.threeObject.quaternion.copy(quat.joltToThree(transform.GetRotation().GetQuaternion())); + } } // create a wheel from input settinsg -export function createWheelSettings( - baseSettings: any, - corner?: any, - type = 'wv' -) { - let wheel: any; - switch (type) { - case 'tv': - break; - default: - wheel = new Raw.module.WheelSettingsWV(); - break; - } - // we need the width from setitings - const halfVehicleWidth = baseSettings.vehicleWidth / 2; - //strip out optional settings - const { fl, fr, bl, br, ...defaultWheelSettings } = baseSettings.wheels; - const isFront = corner === 'fl' || corner === 'fr'; - const isLeft = corner === 'fl' || corner === 'bl'; - // remerge based on corner - const wheelSettings = { - ...defaultWheelSettings, - ...baseSettings.wheels[corner], - }; - // set the position based on corner - wheel.mPosition = new Raw.module.Vec3( - isLeft ? halfVehicleWidth : -halfVehicleWidth, - -wheelSettings.wheelOffsetVertical, - isFront - ? wheelSettings.wheelOffsetHorizontal - : -wheelSettings.wheelOffsetHorizontal - ); - // loop over the settings and set them on the wheel with the jolt prop name - // some settings don't exist. hopefully jolt ignores them - Object.keys(wheelSettings).forEach((key) => { - //@ts-ignore - wheel[joltPropName(key)] = wheelSettings[key]; - }); - return wheel; +export function createWheelSettings(baseSettings: any, corner?: any, type = 'wv') { + let wheel: any; + switch (type) { + case 'tv': + break; + default: + wheel = new Raw.module.WheelSettingsWV(); + break; + } + // we need the width from setitings + const halfVehicleWidth = baseSettings.vehicleWidth / 2; + //strip out optional settings + const { fl, fr, bl, br, ...defaultWheelSettings } = baseSettings.wheels; + const isFront = corner === 'fl' || corner === 'fr'; + const isLeft = corner === 'fl' || corner === 'bl'; + // remerge based on corner + const wheelSettings = { + ...defaultWheelSettings, + ...baseSettings.wheels[corner] + }; + // set the position based on corner + wheel.mPosition = new Raw.module.Vec3( + isLeft ? halfVehicleWidth : -halfVehicleWidth, + -wheelSettings.wheelOffsetVertical, + isFront ? wheelSettings.wheelOffsetHorizontal : -wheelSettings.wheelOffsetHorizontal + ); + // loop over the settings and set them on the wheel with the jolt prop name + // some settings don't exist. hopefully jolt ignores them + Object.keys(wheelSettings).forEach((key) => { + //@ts-ignore + wheel[joltPropName(key)] = wheelSettings[key]; + }); + return wheel; } //creates basic crashtest style wheel texture function getWheelMaterial() { - // Create material for wheel - const texLoader = new THREE.TextureLoader(); - const texture = texLoader.load( - 'data:image/gif;base64,R0lGODdhAgACAIABAAAAAP///ywAAAAAAgACAAACA0QCBQA7' - ); - texture.wrapS = texture.wrapT = THREE.RepeatWrapping; - texture.offset.set(0, 0); - texture.repeat.set(1, 1); - texture.magFilter = THREE.NearestFilter; - const wheelMaterial = new THREE.MeshPhongMaterial({ color: 0x666666 }); - wheelMaterial.map = texture; - return wheelMaterial; + // Create material for wheel + const texLoader = new THREE.TextureLoader(); + const texture = texLoader.load( + 'data:image/gif;base64,R0lGODdhAgACAIABAAAAAP///ywAAAAAAgACAAACA0QCBQA7' + ); + texture.wrapS = texture.wrapT = THREE.RepeatWrapping; + texture.offset.set(0, 0); + texture.repeat.set(1, 1); + texture.magFilter = THREE.NearestFilter; + const wheelMaterial = new THREE.MeshPhongMaterial({ color: 0x666666 }); + wheelMaterial.map = texture; + return wheelMaterial; } diff --git a/packages/react-three-jolt/src/tmp.ts b/packages/react-three-jolt/src/tmp.ts index e6ce61f..001fc1e 100644 --- a/packages/react-three-jolt/src/tmp.ts +++ b/packages/react-three-jolt/src/tmp.ts @@ -1,4 +1,4 @@ -import { Euler, Matrix4, Object3D, Quaternion, Vector3 } from "three"; +import { Euler, Matrix4, Object3D, Quaternion, Vector3 } from 'three'; export const _quaternion = new Quaternion(); export const _euler = new Euler(); @@ -7,4 +7,4 @@ export const _object3d = new Object3D(); export const _matrix4 = new Matrix4(); export const _position = new Vector3(); export const _rotation = new Quaternion(); -export const _scale = new Vector3(); \ No newline at end of file +export const _scale = new Vector3(); diff --git a/packages/react-three-jolt/src/useCommand/Command.ts b/packages/react-three-jolt/src/useCommand/Command.ts index 8a7c98e..7ecf005 100644 --- a/packages/react-three-jolt/src/useCommand/Command.ts +++ b/packages/react-three-jolt/src/useCommand/Command.ts @@ -1,142 +1,136 @@ import { CommandCallback } from './Commander'; export class Command { - label: string; - // TODO: move this type - value: string | number | boolean | { x: number; y: number } = 0; - downListeners: CommandCallback[] = []; - upListeners: CommandCallback[] = []; - keys: string[] = []; - buttons: number[] = []; - axis: number[] = []; + label: string; + // TODO: move this type + value: string | number | boolean | { x: number; y: number } = 0; + downListeners: CommandCallback[] = []; + upListeners: CommandCallback[] = []; + keys: string[] = []; + buttons: number[] = []; + axis: number[] = []; - // Options ----------------------------------------- - //throttle rate - threshold: number = 100; - deadzone: number = 0.05; + // Options ----------------------------------------- + //throttle rate + threshold: number = 100; + deadzone: number = 0.05; - // if this is a variable rate command - isVariable: boolean = false; - rate: number = 1; - max: number = 1; - min: number = -1; + // if this is a variable rate command + isVariable: boolean = false; + rate: number = 1; + max: number = 1; + min: number = -1; - // State Properties -------------------------------- - active = true; - startTime: number = 0; - duration: number = 0; - isInitial: boolean = false; - running = false; + // State Properties -------------------------------- + active = true; + startTime: number = 0; + duration: number = 0; + isInitial: boolean = false; + running = false; - constructor(label: string) { - this.label = label; - } - handleDown( - event: KeyboardEvent | MouseEvent, - value?: number | string | boolean | { x: number; y: number } - ) { - const now = Date.now(); - let duration; - // check if theres a duration and if its above the threshold (Throttling) - if ( - !this.active || - (this.startTime && - now - (this.startTime + this.duration) <= this.threshold) - ) - return false; - - // Because keydown events fire as long as the key is held down - // we need to detect if its the first or ongoing - if (this.running) this.isInitial = false; - else { - this.isInitial = true; - this.running = true; - this.startTime = now; - // we do the duration here because we don't need it on initial; - duration = this.updateDuration(); + constructor(label: string) { + this.label = label; } + handleDown( + event: KeyboardEvent | MouseEvent, + value?: number | string | boolean | { x: number; y: number } + ) { + const now = Date.now(); + let duration; + // check if theres a duration and if its above the threshold (Throttling) + if ( + !this.active || + (this.startTime && now - (this.startTime + this.duration) <= this.threshold) + ) + return false; + + // Because keydown events fire as long as the key is held down + // we need to detect if its the first or ongoing + if (this.running) this.isInitial = false; + else { + this.isInitial = true; + this.running = true; + this.startTime = now; + // we do the duration here because we don't need it on initial; + duration = this.updateDuration(); + } - // initalize the value to this minimum - // TODO: this looks dirty - if (this.isVariable) - this.value = value - ? //@ts-ignore - value >= this.min - ? value - : this.min - : this.min; - else this.value = value || this.rate; + // initalize the value to this minimum + // TODO: this looks dirty + if (this.isVariable) + this.value = value + ? //@ts-ignore + value >= this.min + ? value + : this.min + : this.min; + else this.value = value || this.rate; - const info = { - event, - label: this.label, - method: event.type, - value: this.value, - command: this, - isInitial: this.isInitial, - startTime: this.startTime, - duration, - }; - //@ts-ignore - this.downListeners.forEach((listener) => listener(info)); - // TODO Fix typescript to allow early returns - return false; - } - handleUp(event: MouseEvent | KeyboardEvent, value?: number | string) { - // check if we already stopped due to throttling - if (!this.running) return false; - // reset the running value - this.running = false; - this.value = value || 0; - const info = { - event, - method: event.type, - label: this.label, - value: this.value, - command: this, - isInitial: this.isInitial, // not needed? - startTime: this.startTime, - duration: this.updateDuration(), - }; - this.upListeners.forEach((listener) => listener(info)); - return false; - } - // if the value needs to change because of a tick - handleUpdate() { - this.isInitial = false; + const info = { + event, + label: this.label, + method: event.type, + value: this.value, + command: this, + isInitial: this.isInitial, + startTime: this.startTime, + duration + }; + //@ts-ignore + this.downListeners.forEach((listener) => listener(info)); + // TODO Fix typescript to allow early returns + return false; + } + handleUp(event: MouseEvent | KeyboardEvent, value?: number | string) { + // check if we already stopped due to throttling + if (!this.running) return false; + // reset the running value + this.running = false; + this.value = value || 0; + const info = { + event, + method: event.type, + label: this.label, + value: this.value, + command: this, + isInitial: this.isInitial, // not needed? + startTime: this.startTime, + duration: this.updateDuration() + }; + this.upListeners.forEach((listener) => listener(info)); + return false; + } + // if the value needs to change because of a tick + handleUpdate() { + this.isInitial = false; - if (typeof this.value == 'number') { - const newVal = this.value + this.rate; - this.value = - newVal >= this.min - ? newVal <= this.max - ? newVal - : this.max - : this.min; + if (typeof this.value == 'number') { + const newVal = this.value + this.rate; + this.value = newVal >= this.min ? (newVal <= this.max ? newVal : this.max) : this.min; + } + const info = { + method: 'update', + value: this.value, + command: this, + isInitial: this.isInitial, + startTime: this.startTime, + duration: this.updateDuration() + }; + //@ts-ignore + this.downListeners.forEach((listener) => listener(info)); } - const info = { - method: 'update', - value: this.value, - command: this, - isInitial: this.isInitial, - startTime: this.startTime, - duration: this.updateDuration(), - }; - //@ts-ignore - this.downListeners.forEach((listener) => listener(info)); - } - // takes in an options object and applies to this - setOptions(options: any) { - if (!options) return; - // TODO: should this go into an options object and not direct on the class? - Object.keys(options).forEach((key: string) => { - //@ts-ignore + // takes in an options object and applies to this + setOptions(options: any) { + if (!options) return; + // TODO: should this go into an options object and not direct on the class? + Object.keys(options).forEach((key: string) => { + //@ts-ignore - this[options[key]] = options[key]; - }); - } - private updateDuration() { - this.duration = Date.now() - this.startTime; - return this.duration; - } + this[options[key]] = options[key]; + }); + } + private updateDuration() { + this.duration = Date.now() - this.startTime; + return this.duration; + } } diff --git a/packages/react-three-jolt/src/useCommand/Commander.ts b/packages/react-three-jolt/src/useCommand/Commander.ts index fe6c8ac..1bd9432 100644 --- a/packages/react-three-jolt/src/useCommand/Commander.ts +++ b/packages/react-three-jolt/src/useCommand/Commander.ts @@ -7,228 +7,213 @@ import { VectorCommand } from './VectorCommand'; // im not sure yet if I'll include other libraries export type CommandCallback = (info: { - command: Command; - label: string; - method: string; - value: string | number | boolean; + command: Command; + label: string; + method: string; + value: string | number | boolean; - isInitial: boolean; + isInitial: boolean; - event?: KeyboardEvent | MouseEvent; - duration?: number; + event?: KeyboardEvent | MouseEvent; + duration?: number; }) => void; export class Commander { - commands: Map = new Map(); - // listeners for state changes - stateListeners = []; - // state object - state = {}; - // state flags - isDirty = false; - paused = false; - debug = false; - - gamepadListener: GamepadListener = new GamepadListener(); - - constructor() { - console.log('gamepad listeners attaching'); - // add gamepad listeners - this.gamepadListener.on('gamepad:connected', (event: any) => { - if (this.debug) console.log('gamepad connected', event); - }); - //gamepad - this.gamepadListener.on('gamepad:button', this.onButtonChange); - this.gamepadListener.on('gamepad:axis', this.onAxisChange); - this.gamepadListener.start(); - - // keyboard - window.addEventListener('keydown', this.keyEventListener); - window.addEventListener('keyup', this.keyEventListener); - - // mouse - window.addEventListener('mousedown', this.mouseEventListener); - window.addEventListener('mouseup', this.mouseEventListener); - } - - // Primary Listeners ========================== - // Gamepad Axis Events --- - onAxisChange = (event: any) => { - const { axis, value } = event.detail; - this.commands.forEach((command) => { - if (command.axis.includes(axis)) { - this.isDirty = true; - command.handleDown(event, value); - } - }); - if (this.isDirty) this.emitChange(); - }; - // Gamepad Button Events --- - // buttons act just like keys, just a little different when released - onButtonChange = (event: any) => { - const { button, value, pressed } = event.detail; - // TODO: Check if this is still needed. - /* On my xbox controller, the trigger button will send + commands: Map = new Map(); + // listeners for state changes + stateListeners = []; + // state object + state = {}; + // state flags + isDirty = false; + paused = false; + debug = false; + + gamepadListener: GamepadListener = new GamepadListener(); + + constructor() { + console.log('gamepad listeners attaching'); + // add gamepad listeners + this.gamepadListener.on('gamepad:connected', (event: any) => { + if (this.debug) console.log('gamepad connected', event); + }); + //gamepad + this.gamepadListener.on('gamepad:button', this.onButtonChange); + this.gamepadListener.on('gamepad:axis', this.onAxisChange); + this.gamepadListener.start(); + + // keyboard + window.addEventListener('keydown', this.keyEventListener); + window.addEventListener('keyup', this.keyEventListener); + + // mouse + window.addEventListener('mousedown', this.mouseEventListener); + window.addEventListener('mouseup', this.mouseEventListener); + } + + // Primary Listeners ========================== + // Gamepad Axis Events --- + onAxisChange = (event: any) => { + const { axis, value } = event.detail; + this.commands.forEach((command) => { + if (command.axis.includes(axis)) { + this.isDirty = true; + command.handleDown(event, value); + } + }); + if (this.isDirty) this.emitChange(); + }; + // Gamepad Button Events --- + // buttons act just like keys, just a little different when released + onButtonChange = (event: any) => { + const { button, value, pressed } = event.detail; + // TODO: Check if this is still needed. + /* On my xbox controller, the trigger button will send a value for less that 0.12 but not mark the trigger as pressed I initially thought it was deadzone, but that's not the case. the vanilla gamepad object shows the button as not pressed for now we will check if it's not pressed but has a value to pass it as a still down event */ - this.commands.forEach((command) => { - if (command.buttons.includes(button)) { - this.isDirty = true; - if (pressed || (!pressed && value !== 0)) - command.handleDown(event, value); - else command.handleUp(event); - } - }); - if (this.isDirty) this.emitChange(); - }; - // Keyboard Events --- - keyEventListener = (event: KeyboardEvent) => { - this.commands.forEach((command) => { - if (command.keys.includes(event.key)) { - this.isDirty = true; - if (event.type === 'keydown') command.handleDown(event); - else command.handleUp(event); - } - }); - if (this.isDirty) this.emitChange(); - }; - // Mouse Events --- - mouseEventListener = (event: MouseEvent) => { - const key = 'Mouse' + event.button; - this.commands.forEach((command) => { - if (command.keys.includes(key)) { - this.isDirty = true; - if (event.type === 'mousedown') command.handleDown(event); - else command.handleUp(event); - } - }); - if (this.isDirty) this.emitChange(); - }; - - // Commands ======================================== - - addCommand = ( - commandString: string, - // TODO: move this to a type - options?: { keys?: string[]; buttons?: string[]; asVector?: boolean } - ) => { - let { keys, buttons, asVector, ...rest } = options || {}; - const command = asVector - ? new VectorCommand(commandString, this, rest) - : new Command(commandString); - // check if the command is in our common list and pull the keys/buttons - //@ts-ignore - if (commonCommands[commandString]) { - //@ts-ignore - keys = keys || commonCommands[commandString].keys; - //@ts-ignore - buttons = buttons || commonCommands[commandString].buttons; - } - // if no keys or buttons are passed, default to the commandString - command.keys = keys || [commandString]; - //@ts-ignore - command.buttons = buttons || []; - if (this.debug) - console.log( - 'Adding command', - commandString, - command.keys, - command.buttons - ); - this.commands.set(commandString, command); - return this.getCommand(commandString); - }; - - getCommand = (commandString: string) => { - return this.commands.get(commandString); - }; - - // Listeners ======================================== - - // add listeners to a command - addListener = ( - commandString: string, - callback: CommandCallback, - asUp?: boolean - ) => { - const command = this.getCommand(commandString); - if (command) { - if (!asUp) command.downListeners.push(callback); - else command.upListeners.push(callback); - } - }; - // TODO: move this to a patern where the add returns the remove - // remove listener - removeListener = (commandString: string, callback: CommandCallback) => { - const command = this.getCommand(commandString); - if (command) { - command.downListeners = command.downListeners.filter( - (listener) => listener !== callback - ); - command.upListeners = command.upListeners.filter( - (listener) => listener !== callback - ); + this.commands.forEach((command) => { + if (command.buttons.includes(button)) { + this.isDirty = true; + if (pressed || (!pressed && value !== 0)) command.handleDown(event, value); + else command.handleUp(event); + } + }); + if (this.isDirty) this.emitChange(); + }; + // Keyboard Events --- + keyEventListener = (event: KeyboardEvent) => { + this.commands.forEach((command) => { + if (command.keys.includes(event.key)) { + this.isDirty = true; + if (event.type === 'keydown') command.handleDown(event); + else command.handleUp(event); + } + }); + if (this.isDirty) this.emitChange(); + }; + // Mouse Events --- + mouseEventListener = (event: MouseEvent) => { + const key = 'Mouse' + event.button; + this.commands.forEach((command) => { + if (command.keys.includes(key)) { + this.isDirty = true; + if (event.type === 'mousedown') command.handleDown(event); + else command.handleUp(event); + } + }); + if (this.isDirty) this.emitChange(); + }; + + // Commands ======================================== + + addCommand = ( + commandString: string, + // TODO: move this to a type + options?: { keys?: string[]; buttons?: string[]; asVector?: boolean } + ) => { + let { keys, buttons, asVector, ...rest } = options || {}; + const command = asVector + ? new VectorCommand(commandString, this, rest) + : new Command(commandString); + // check if the command is in our common list and pull the keys/buttons + //@ts-ignore + if (commonCommands[commandString]) { + //@ts-ignore + keys = keys || commonCommands[commandString].keys; + //@ts-ignore + buttons = buttons || commonCommands[commandString].buttons; + } + // if no keys or buttons are passed, default to the commandString + command.keys = keys || [commandString]; + //@ts-ignore + command.buttons = buttons || []; + if (this.debug) console.log('Adding command', commandString, command.keys, command.buttons); + this.commands.set(commandString, command); + return this.getCommand(commandString); + }; + + getCommand = (commandString: string) => { + return this.commands.get(commandString); + }; + + // Listeners ======================================== + + // add listeners to a command + addListener = (commandString: string, callback: CommandCallback, asUp?: boolean) => { + const command = this.getCommand(commandString); + if (command) { + if (!asUp) command.downListeners.push(callback); + else command.upListeners.push(callback); + } + }; + // TODO: move this to a patern where the add returns the remove + // remove listener + removeListener = (commandString: string, callback: CommandCallback) => { + const command = this.getCommand(commandString); + if (command) { + command.downListeners = command.downListeners.filter( + (listener) => listener !== callback + ); + command.upListeners = command.upListeners.filter((listener) => listener !== callback); + } + }; + + // we have a lot of options here. active, starteDate, running, etc + // for now just update all active commands + updateState() { + // bail if paused + if (this.paused) return; + this.commands.forEach((command) => { + if (command.active) { + //@ts-ignore + this.state[command.label] = command.value; + } + }); } - }; - - // we have a lot of options here. active, starteDate, running, etc - // for now just update all active commands - updateState() { - // bail if paused - if (this.paused) return; - this.commands.forEach((command) => { - if (command.active) { + + // return the state as a snapshot + getSnapsot = () => { + // if this isn't dirty, return the state directly + if (this.isDirty) { + //update the state values + this.updateState(); + // clone the state to create an imutable object. + const clone = Object.assign({}, this.state); + //set the state to the new object so it passes matching tests + this.state = clone; + this.isDirty = false; + } + return this.state; + }; + + // add a stateListener + subscribe = (callback: any) => { + if (this.debug) console.log('adding state listener'); //@ts-ignore - this.state[command.label] = command.value; - } - }); - } - - // return the state as a snapshot - getSnapsot = () => { - // if this isn't dirty, return the state directly - if (this.isDirty) { - //update the state values - this.updateState(); - // clone the state to create an imutable object. - const clone = Object.assign({}, this.state); - //set the state to the new object so it passes matching tests - this.state = clone; - this.isDirty = false; + this.stateListeners.push(callback); + return this.unsubscribe.bind(this, callback); + }; + // remove a stateListener + unsubscribe = (callback: any) => { + if (this.debug) console.log('removing state listener'); + this.stateListeners = this.stateListeners.filter((listener) => listener !== callback); + }; + // fire the subscription listener + emitChange() { + // dont emit if paused + if (this.paused) return; + + this.stateListeners.forEach((listener: any) => listener(this.state)); } - return this.state; - }; - - // add a stateListener - subscribe = (callback: any) => { - if (this.debug) console.log('adding state listener'); - //@ts-ignore - this.stateListeners.push(callback); - return this.unsubscribe.bind(this, callback); - }; - // remove a stateListener - unsubscribe = (callback: any) => { - if (this.debug) console.log('removing state listener'); - this.stateListeners = this.stateListeners.filter( - (listener) => listener !== callback - ); - }; - // fire the subscription listener - emitChange() { - // dont emit if paused - if (this.paused) return; - - this.stateListeners.forEach((listener: any) => listener(this.state)); - } - - destroy = () => { - window.removeEventListener('keydown', this.keyEventListener); - window.removeEventListener('keyup', this.keyEventListener); - window.removeEventListener('mousedown', this.mouseEventListener); - window.removeEventListener('mouseup', this.mouseEventListener); - this.gamepadListener.stop(); - }; + + destroy = () => { + window.removeEventListener('keydown', this.keyEventListener); + window.removeEventListener('keyup', this.keyEventListener); + window.removeEventListener('mousedown', this.mouseEventListener); + window.removeEventListener('mouseup', this.mouseEventListener); + this.gamepadListener.stop(); + }; } diff --git a/packages/react-three-jolt/src/useCommand/VectorCommand.ts b/packages/react-three-jolt/src/useCommand/VectorCommand.ts index abd1172..1823660 100644 --- a/packages/react-three-jolt/src/useCommand/VectorCommand.ts +++ b/packages/react-three-jolt/src/useCommand/VectorCommand.ts @@ -3,158 +3,157 @@ import { Command } from './Command'; import { VectorPreset, vectorPresets } from './commonCommands'; type VectorOptions = { - preset?: string; - bindings?: VectorPreset; - axis?: number[]; - inverted?: { x?: boolean; y?: boolean }; + preset?: string; + bindings?: VectorPreset; + axis?: number[]; + inverted?: { x?: boolean; y?: boolean }; }; // an vector command is like move or look // the value set is a {x, y} object and accepts up to 4 inputs export class VectorCommand extends Command { - // hold a handle to the parent commander - commander: Commander; - // for a mouse, the min and max wont match by axis - mouseRange = { x: { min: -1, max: 1 }, y: { min: -1, max: 1 } }; - // key and button bindings. default is move - bindings = Object.assign({}, vectorPresets.move); + // hold a handle to the parent commander + commander: Commander; + // for a mouse, the min and max wont match by axis + mouseRange = { x: { min: -1, max: 1 }, y: { min: -1, max: 1 } }; + // key and button bindings. default is move + bindings = Object.assign({}, vectorPresets.move); - // for now, bind all 4 axis - axis: number[] = [0, 1, 2, 3]; - inverted = { x: false, y: false }; - value = { x: 0, y: 0 }; + // for now, bind all 4 axis + axis: number[] = [0, 1, 2, 3]; + inverted = { x: false, y: false }; + value = { x: 0, y: 0 }; - // how many keys are down - // theres a bug when you lift it fires the end command even if other move keys are down - keysDown = {}; - activeKeys = new Map(); + // how many keys are down + // theres a bug when you lift it fires the end command even if other move keys are down + keysDown = {}; + activeKeys = new Map(); - constructor(label: string, commander: Commander, options?: VectorOptions) { - super(label); - this.commander = commander; + constructor(label: string, commander: Commander, options?: VectorOptions) { + super(label); + this.commander = commander; - // TODO: Do we need options in the constructor? - // binding options - // set the bindings based on this label - //@ts-ignore - this.bindings = vectorPresets[this.label]; - if (options) { - //@ts-ignore - if (options.preset && vectorPresets[options.preset]) + // TODO: Do we need options in the constructor? + // binding options + // set the bindings based on this label //@ts-ignore - this.bindings = vectorPresets[options.preset]; - if (options.bindings) Object.assign(this.bindings, options.bindings); - if (options.axis) this.axis = options.axis; - if (options.inverted) Object.assign(this.inverted, options.inverted); - } + this.bindings = vectorPresets[this.label]; + if (options) { + //@ts-ignore + if (options.preset && vectorPresets[options.preset]) + //@ts-ignore + this.bindings = vectorPresets[options.preset]; + if (options.bindings) Object.assign(this.bindings, options.bindings); + if (options.axis) this.axis = options.axis; + if (options.inverted) Object.assign(this.inverted, options.inverted); + } - // create our own bindings for the four directions - this.setupEventBindings(); - } - // Setup bindings - setupEventBindings() { - // loop over key bindings - Object.keys(this.bindings).forEach((direction) => { - //@ts-ignore loop over the keys - const commandArgs = this.bindings[direction]; - // if it's axis just add the value to this axis - if (direction == 'axis') { - this.axis = commandArgs; - } else { - this.commander.addCommand(this.label + direction, { - keys: commandArgs.keys, - buttons: commandArgs.buttons, + // create our own bindings for the four directions + this.setupEventBindings(); + } + // Setup bindings + setupEventBindings() { + // loop over key bindings + Object.keys(this.bindings).forEach((direction) => { + //@ts-ignore loop over the keys + const commandArgs = this.bindings[direction]; + // if it's axis just add the value to this axis + if (direction == 'axis') { + this.axis = commandArgs; + } else { + this.commander.addCommand(this.label + direction, { + keys: commandArgs.keys, + buttons: commandArgs.buttons + }); + this.commander.addListener( + this.label + direction, + this.handleInputStart.bind(this) + // This might be better + // this.handleDown.bind(this), + ); + this.commander.addListener( + this.label + direction, + this.handleInputEnd.bind(this), + true + ); + } }); - this.commander.addListener( - this.label + direction, - this.handleInputStart.bind(this) - // This might be better - // this.handleDown.bind(this), - ); - this.commander.addListener( - this.label + direction, - this.handleInputEnd.bind(this), - true - ); - } - }); - } - // TODO: Should I merge this and inputStart? this is mostly for axis - // we have to catch the handleDown event to be able to map it to vector - //@ts-ignore - handleDown(event: any, value?: any) { - // Right now only the axis is calling this as our own listeners moved to input - const { axis } = event.detail; - // in a gamepad only axis 1 & 3 are for forward/backwards - const direction = axis == 1 || axis == 3 ? 'forward' : 'right'; - //console.log('VectorCommand handleDown', event, value); - this.setVectorFromDirection(direction, 1, value); - //this.keysDown[event.event.key] = true; - super.handleDown(event, this.value); - } - - handleInputStart(event: any) { - // remove this label from the label property of the string - const direction = event.label.replace(this.label, ''); - // get the orientation from the binding + } + // TODO: Should I merge this and inputStart? this is mostly for axis + // we have to catch the handleDown event to be able to map it to vector //@ts-ignore - const orientation = this.bindings[direction].orientation || 1; - // set the value of the vector - this.setVectorFromDirection(direction, orientation, event.value); - //console.log('VectorCommand handleInputStart', this.value, event); - this.activeKeys.set(direction, event.value); - this.processActiveKeys(); - super.handleDown(event, this.value); - } - handleInputEnd(event: any) { - // TODO: Should this be a single function so its not replicated in start? - // remove this label from the label property of the string - const direction = event.label.replace(this.label, ''); - //@ts-ignore get the orientation from the binding - const orientation = this.bindings[direction].orientation || 1; - // set the value of the vector - this.setVectorFromDirection(direction, orientation, event.value); - //console.log('VectorCommand handleInputEnd', this.value, event); - //remove the key from the active keys - this.activeKeys.delete(direction); - this.processActiveKeys(); + handleDown(event: any, value?: any) { + // Right now only the axis is calling this as our own listeners moved to input + const { axis } = event.detail; + // in a gamepad only axis 1 & 3 are for forward/backwards + const direction = axis == 1 || axis == 3 ? 'forward' : 'right'; + //console.log('VectorCommand handleDown', event, value); + this.setVectorFromDirection(direction, 1, value); + //this.keysDown[event.event.key] = true; + super.handleDown(event, this.value); + } - //when we lift the key we need to tell the system of the new value - // which if there are keys down is different than what we expect - //@ts-ignore - if (!this.activeKeys.size) super.handleUp(event, this.value); - //otherwise fire the down event again - else super.handleDown(event, this.value); - } - //loop over the active keys and set the value - processActiveKeys() { - // set everything to 0 - this.value = { x: 0, y: 0 }; - this.activeKeys.forEach((value, direction) => { - //@ts-ignore - const orientation = this.bindings[direction].orientation || 1; - this.setVectorFromDirection(direction, orientation, value, true); - }); - return this.value; - } + handleInputStart(event: any) { + // remove this label from the label property of the string + const direction = event.label.replace(this.label, ''); + // get the orientation from the binding + //@ts-ignore + const orientation = this.bindings[direction].orientation || 1; + // set the value of the vector + this.setVectorFromDirection(direction, orientation, event.value); + //console.log('VectorCommand handleInputStart', this.value, event); + this.activeKeys.set(direction, event.value); + this.processActiveKeys(); + super.handleDown(event, this.value); + } + handleInputEnd(event: any) { + // TODO: Should this be a single function so its not replicated in start? + // remove this label from the label property of the string + const direction = event.label.replace(this.label, ''); + //@ts-ignore get the orientation from the binding + const orientation = this.bindings[direction].orientation || 1; + // set the value of the vector + this.setVectorFromDirection(direction, orientation, event.value); + //console.log('VectorCommand handleInputEnd', this.value, event); + //remove the key from the active keys + this.activeKeys.delete(direction); + this.processActiveKeys(); - // set the value of the vector based on the string direction - setVectorFromDirection( - direction: string, - orientation: number, - value: number, - addative: boolean = false - ) { - const targetProp = - direction == 'forward' || direction == 'backward' ? 'y' : 'x'; - // drop the value to 0 if its below/above the deadzone - if (Math.abs(value) <= this.deadzone) value = 0; - // handle orientation and if it's inverted - const current = addative ? this.value[targetProp] || 0 : 0; - this.value[targetProp] = - current + value * orientation * (this.inverted[targetProp] ? -1 : 1); - return this.value; - } - // this is me thinking about mapping look to commands - setMouseRange() {} + //when we lift the key we need to tell the system of the new value + // which if there are keys down is different than what we expect + //@ts-ignore + if (!this.activeKeys.size) super.handleUp(event, this.value); + //otherwise fire the down event again + else super.handleDown(event, this.value); + } + //loop over the active keys and set the value + processActiveKeys() { + // set everything to 0 + this.value = { x: 0, y: 0 }; + this.activeKeys.forEach((value, direction) => { + //@ts-ignore + const orientation = this.bindings[direction].orientation || 1; + this.setVectorFromDirection(direction, orientation, value, true); + }); + return this.value; + } + + // set the value of the vector based on the string direction + setVectorFromDirection( + direction: string, + orientation: number, + value: number, + addative: boolean = false + ) { + const targetProp = direction == 'forward' || direction == 'backward' ? 'y' : 'x'; + // drop the value to 0 if its below/above the deadzone + if (Math.abs(value) <= this.deadzone) value = 0; + // handle orientation and if it's inverted + const current = addative ? this.value[targetProp] || 0 : 0; + this.value[targetProp] = + current + value * orientation * (this.inverted[targetProp] ? -1 : 1); + return this.value; + } + // this is me thinking about mapping look to commands + setMouseRange() {} } diff --git a/packages/react-three-jolt/src/useCommand/commonCommands.ts b/packages/react-three-jolt/src/useCommand/commonCommands.ts index b929535..0316b33 100644 --- a/packages/react-three-jolt/src/useCommand/commonCommands.ts +++ b/packages/react-three-jolt/src/useCommand/commonCommands.ts @@ -3,41 +3,41 @@ export const commonCommands = { moveForward: { keys: ['w', 'W'], - buttons: [12], + buttons: [12] }, moveBackward: { keys: ['s', 'S'], - buttons: [13], + buttons: [13] }, moveLeft: { keys: ['a', 'A'], - buttons: [14], + buttons: [14] }, moveRight: { keys: ['d', 'D'], - buttons: [15], + buttons: [15] }, jump: { keys: [' '], - buttons: [0], + buttons: [0] }, crouch: { keys: ['Control', 'C', 'c'], - buttons: [1], + buttons: [1] }, run: { keys: ['Shift'], - buttons: [10], + buttons: [10] }, fire: { keys: ['Mouse0'], - buttons: [7], + buttons: [7] }, // special version of fire for asteroid type games (Space) spaceFire: { keys: [' ', 'Space', 'Mouse0'], - buttons: [7], - }, + buttons: [7] + } }; export type VectorPreset = { @@ -65,68 +65,68 @@ export const vectorPresets = { forward: { keys: ['w', 'W', 'ArrowUp'], buttons: [12], - orientation: -1, + orientation: -1 }, backward: { keys: ['s', 'S', 'ArrowDown'], buttons: [13], - orientation: 1, + orientation: 1 }, left: { keys: ['a', 'A', 'ArrowLeft'], buttons: [14], - orientation: -1, + orientation: -1 }, right: { keys: ['d', 'D', 'ArrowRight'], buttons: [15], - orientation: 1, + orientation: 1 }, - axis: [0, 1], + axis: [0, 1] }, look: { up: { keys: ['ArrowUp'], buttons: [12], - orientation: -1, + orientation: -1 }, down: { keys: ['ArrowDown'], buttons: [13], - orientation: 1, + orientation: 1 }, left: { keys: ['ArrowLeft'], buttons: [14], - orientation: -1, + orientation: -1 }, right: { keys: ['ArrowRight'], buttons: [15], - orientation: 1, + orientation: 1 }, - axis: [2, 3], + axis: [2, 3] }, race: { forward: { keys: ['w', 'ArrowUp'], buttons: [7], - orientation: -1, + orientation: -1 }, backward: { keys: ['s', 'ArrowDown'], buttons: [6], - orientation: 1, + orientation: 1 }, left: { keys: ['a', 'ArrowLeft'], buttons: [14], - orientation: -1, + orientation: -1 }, right: { keys: ['d', 'ArrowRight'], buttons: [15], - orientation: -1, - }, - }, + orientation: -1 + } + } }; diff --git a/packages/react-three-jolt/src/useCommand/index.ts b/packages/react-three-jolt/src/useCommand/index.ts index 8fa920a..169c237 100644 --- a/packages/react-three-jolt/src/useCommand/index.ts +++ b/packages/react-three-jolt/src/useCommand/index.ts @@ -13,75 +13,71 @@ let commander: Commander; // returns a state of the commander's commands export function useCommandState() { - const commander = useCommander(); - const commandState = useSyncExternalStore( - commander.subscribe, - commander.getSnapsot - ); - return commandState; + const commander = useCommander(); + const commandState = useSyncExternalStore(commander.subscribe, commander.getSnapsot); + return commandState; } // hook to load the commander class and also initialize the commander export const useCommander = () => { - if (!commander) commander = new Commander(); - return commander; + if (!commander) commander = new Commander(); + return commander; }; // actual primary hook export function useCommand( - commandString: string, - onStart?: (info: CommandCallback) => void, - onEnd?: (info: CommandCallback) => void, - options?: any + commandString: string, + onStart?: (info: CommandCallback) => void, + onEnd?: (info: CommandCallback) => void, + options?: any ) { - const commander = useCommander(); - let command = - commander.getCommand(commandString) || - commander.addCommand(commandString, options); + const commander = useCommander(); + let command = + commander.getCommand(commandString) || commander.addCommand(commandString, options); - // attach the listeners in a useEffect and the return will remove them - useEffect(() => { - //@ts-ignore - if (onStart) commander.addListener(commandString, onStart); - //@ts-ignore - if (onEnd) commander.addListener(commandString, onEnd, true); + // attach the listeners in a useEffect and the return will remove them + useEffect(() => { + //@ts-ignore + if (onStart) commander.addListener(commandString, onStart); + //@ts-ignore + if (onEnd) commander.addListener(commandString, onEnd, true); - // remove the listeners when destroyed - return () => { - //@ts-ignore - if (onStart) commander.removeListener(commandString, onStart); - //@ts-ignore - if (onEnd) commander.removeListener(commandString, onEnd); - }; - }, [commandString, onStart, onEnd]); - return command; + // remove the listeners when destroyed + return () => { + //@ts-ignore + if (onStart) commander.removeListener(commandString, onStart); + //@ts-ignore + if (onEnd) commander.removeListener(commandString, onEnd); + }; + }, [commandString, onStart, onEnd]); + return command; } export function useGamepadForCameraControls( - commandString: string, - controls: CameraControls, - options?: any + commandString: string, + controls: CameraControls, + options?: any ) { - // lets be 100% the command exists - const command = useCommand(commandString); + // lets be 100% the command exists + const command = useCommand(commandString); - // assign the options to the root command - useEffect(() => command!.setOptions(options), [options]); + // assign the options to the root command + useEffect(() => command!.setOptions(options), [options]); - // bind the state - const commandState = useCommandState(); - //@ts-ignore we do this verbose version incase you dont want 'look' - const targetCommand = commandState[commandString]; - // sensitivity (scalar) - const sensitivity = options?.sensitivity || 0.03; - // pass the other options to the command + // bind the state + const commandState = useCommandState(); + //@ts-ignore we do this verbose version incase you dont want 'look' + const targetCommand = commandState[commandString]; + // sensitivity (scalar) + const sensitivity = options?.sensitivity || 0.03; + // pass the other options to the command - // do the rotation - function rotate(rotation: Vec2) { - controls.rotate(rotation.x * sensitivity, rotation.y * sensitivity); - } + // do the rotation + function rotate(rotation: Vec2) { + controls.rotate(rotation.x * sensitivity, rotation.y * sensitivity); + } - // loop - useFrame(() => { - if (targetCommand) rotate(targetCommand); - }); + // loop + useFrame(() => { + if (targetCommand) rotate(targetCommand); + }); } diff --git a/packages/react-three-jolt/src/utils/heightmap.ts b/packages/react-three-jolt/src/utils/heightmap.ts index cafaf16..ac1a8d6 100644 --- a/packages/react-three-jolt/src/utils/heightmap.ts +++ b/packages/react-three-jolt/src/utils/heightmap.ts @@ -25,9 +25,7 @@ export function generateHeight(width: number, height: number): Uint8Array { for (let i = 0; i < size; i++) { const x = i % width, y = ~~(i / width); - data[i] += Math.abs( - perlin.noise(x / quality, y / quality, z) * quality * 1.75, - ); + data[i] += Math.abs(perlin.noise(x / quality, y / quality, z) * quality * 1.75); } quality *= 5; diff --git a/packages/react-three-jolt/src/utils/psrddnoise3.ts b/packages/react-three-jolt/src/utils/psrddnoise3.ts index 7b35a7e..253b3fe 100644 --- a/packages/react-three-jolt/src/utils/psrddnoise3.ts +++ b/packages/react-three-jolt/src/utils/psrddnoise3.ts @@ -25,23 +25,21 @@ export function psrddnoise( alpha: number, gradient: Vec3, dg: Vec3, - dg2: Vec3, + dg2: Vec3 ) { const M: Mat3 = [ [0.0, 1.0, 1.0], [1.0, 0.0, 1.0], - [1.0, 1.0, 0.0], + [1.0, 1.0, 0.0] ]; const Mi: Mat3 = [ [-0.5, 0.5, 0.5], [0.5, -0.5, 0.5], - [0.5, 0.5, -0.5], + [0.5, 0.5, -0.5] ]; // these maps are nuts - const uvw = M.map((row) => - row.reduce((acc, val, index) => acc + val * x[index], 0), - ); + const uvw = M.map((row) => row.reduce((acc, val, index) => acc + val * x[index], 0)); let i0 = uvw.map(Math.floor); const f0 = uvw.map((val) => val - Math.floor(val)); @@ -57,18 +55,10 @@ export function psrddnoise( let i2 = i0.map((val, index) => val + o2[index]); let i3 = i0.map((val) => val + 1); - const v0 = Mi.map((row) => - row.reduce((acc, val, index) => acc + val * i0[index], 0), - ); - const v1 = Mi.map((row) => - row.reduce((acc, val, index) => acc + val * i1[index], 0), - ); - const v2 = Mi.map((row) => - row.reduce((acc, val, index) => acc + val * i2[index], 0), - ); - const v3 = Mi.map((row) => - row.reduce((acc, val, index) => acc + val * i3[index], 0), - ); + const v0 = Mi.map((row) => row.reduce((acc, val, index) => acc + val * i0[index], 0)); + const v1 = Mi.map((row) => row.reduce((acc, val, index) => acc + val * i1[index], 0)); + const v2 = Mi.map((row) => row.reduce((acc, val, index) => acc + val * i2[index], 0)); + const v3 = Mi.map((row) => row.reduce((acc, val, index) => acc + val * i3[index], 0)); const x0 = x.map((val, index) => val - v0[index]); const x1 = x.map((val, index) => val - v1[index]); @@ -80,25 +70,14 @@ export function psrddnoise( const vy = [v0[1], v1[1], v2[1], v3[1]]; const vz = [v0[2], v1[2], v2[2], v3[2]]; - if (period[0] > 0) - vx.forEach((val, index) => (vx[index] = val % period[0])); - if (period[1] > 0) - vy.forEach((val, index) => (vy[index] = val % period[1])); - if (period[2] > 0) - vz.forEach((val, index) => (vz[index] = val % period[2])); - - i0 = M.map((row) => - row.reduce((acc, val, index) => acc + val * vx[index], 0), - ); - i1 = M.map((row) => - row.reduce((acc, val, index) => acc + val * vy[index], 0), - ); - i2 = M.map((row) => - row.reduce((acc, val, index) => acc + val * vz[index], 0), - ); - i3 = M.map((row) => - row.reduce((acc, val, index) => acc + val * vz[index], 0), - ); + if (period[0] > 0) vx.forEach((val, index) => (vx[index] = val % period[0])); + if (period[1] > 0) vy.forEach((val, index) => (vy[index] = val % period[1])); + if (period[2] > 0) vz.forEach((val, index) => (vz[index] = val % period[2])); + + i0 = M.map((row) => row.reduce((acc, val, index) => acc + val * vx[index], 0)); + i1 = M.map((row) => row.reduce((acc, val, index) => acc + val * vy[index], 0)); + i2 = M.map((row) => row.reduce((acc, val, index) => acc + val * vz[index], 0)); + i3 = M.map((row) => row.reduce((acc, val, index) => acc + val * vz[index], 0)); i0.forEach((val, index) => (i0[index] = Math.floor(val + 0.5))); i1.forEach((val, index) => (i1[index] = Math.floor(val + 0.5))); @@ -107,11 +86,9 @@ export function psrddnoise( } const hash = permute( - permute( - permute([i0[2], i1[2], i2[2], i3[2]]).map( - (val, index) => val + i0[index], - ), - ).map((val, index) => val + i0[index]), + permute(permute([i0[2], i1[2], i2[2], i3[2]]).map((val, index) => val + i0[index])).map( + (val, index) => val + i0[index] + ) ); const theta = hash.map((val) => val * 3.883222077); @@ -132,18 +109,10 @@ export function psrddnoise( const py = St.map((val) => val * sz_prime[0]); const pz = sz_prime; - const Ctp = St.map( - (val, index) => val * Sp[index] - Ct[index] * Cp[index], - ); - const qx = Ctp.map( - (val, index) => val * St[index] + Sp[index] * sz[index], - ); - const qy = Ctp.map( - (val, index) => val * -Ct[index] + Cp[index] * sz[index], - ); - const qz = py.map( - (val, index) => -(val * Cp[index] + px[index] * Sp[index]), - ); + const Ctp = St.map((val, index) => val * Sp[index] - Ct[index] * Cp[index]); + const qx = Ctp.map((val, index) => val * St[index] + Sp[index] * sz[index]); + const qy = Ctp.map((val, index) => val * -Ct[index] + Cp[index] * sz[index]); + const qz = py.map((val, index) => -(val * Cp[index] + px[index] * Sp[index])); const Sa = Math.sin(alpha); const Ca = Math.cos(alpha); @@ -166,7 +135,7 @@ export function psrddnoise( 0.5 - x0.reduce((acc, val) => acc + val * val, 0), 0.5 - x1.reduce((acc, val) => acc + val * val, 0), 0.5 - x2.reduce((acc, val) => acc + val * val, 0), - 0.5 - x3.reduce((acc, val) => acc + val * val, 0), + 0.5 - x3.reduce((acc, val) => acc + val * val, 0) ]; const w2 = w.map((val) => val * val); @@ -176,49 +145,25 @@ export function psrddnoise( const n = dot(w3, gdotx); - const dw = w2 - .map((val) => -6.0 * val) - .map((val, index) => val * gdotx[index]); - const dn0 = g0 - .map((val) => w3[0] * val) - .map((val, index) => val + dw[0] * x0[index]); - const dn1 = g1 - .map((val) => w3[1] * val) - .map((val, index) => val + dw[1] * x1[index]); - const dn2 = g2 - .map((val) => w3[2] * val) - .map((val, index) => val + dw[2] * x2[index]); - const dn3 = g3 - .map((val) => w3[3] * val) - .map((val, index) => val + dw[3] * x3[index]); - gradient = dn0.map( - (val, index) => 39.5 * (val + dn1[index] + dn2[index] + dn3[index]), - ); + const dw = w2.map((val) => -6.0 * val).map((val, index) => val * gdotx[index]); + const dn0 = g0.map((val) => w3[0] * val).map((val, index) => val + dw[0] * x0[index]); + const dn1 = g1.map((val) => w3[1] * val).map((val, index) => val + dw[1] * x1[index]); + const dn2 = g2.map((val) => w3[2] * val).map((val, index) => val + dw[2] * x2[index]); + const dn3 = g3.map((val) => w3[3] * val).map((val, index) => val + dw[3] * x3[index]); + gradient = dn0.map((val, index) => 39.5 * (val + dn1[index] + dn2[index] + dn3[index])); const dw2 = w.map((val) => 24.0 * val * gdotx); - const dga0 = - dw2[0] * x0[0] * x0[0] - 6.0 * w2[0] * (gdotx[0] + 2.0 * g0[0] * x0[0]); - const dga1 = - dw2[1] * x1[0] * x1[0] - 6.0 * w2[1] * (gdotx[1] + 2.0 * g1[0] * x1[0]); - const dga2 = - dw2[2] * x2[0] * x2[0] - 6.0 * w2[2] * (gdotx[2] + 2.0 * g2[0] * x2[0]); - const dga3 = - dw2[3] * x3[0] * x3[0] - 6.0 * w2[3] * (gdotx[3] + 2.0 * g3[0] * x3[0]); - dg = dga0.map( - (val, index) => 35.0 * (val + dga1[index] + dga2[index] + dga3[index]), - ); - - const dgb0 = - dw2[0] * x0[0] * x0[1] - 6.0 * w2[0] * (g0[0] * x0[1] + g0[1] * x0[0]); - const dgb1 = - dw2[1] * x1[0] * x1[1] - 6.0 * w2[1] * (g1[0] * x1[1] + g1[1] * x1[0]); - const dgb2 = - dw2[2] * x2[0] * x2[1] - 6.0 * w2[2] * (g2[0] * x2[1] + g2[1] * x2[0]); - const dgb3 = - dw2[3] * x3[0] * x3[1] - 6.0 * w2[3] * (g3[0] * x3[1] + g3[1] * x3[0]); - dg2 = dgb0.map( - (val, index) => 39.5 * (val + dgb1[index] + dgb2[index] + dgb3[index]), - ); + const dga0 = dw2[0] * x0[0] * x0[0] - 6.0 * w2[0] * (gdotx[0] + 2.0 * g0[0] * x0[0]); + const dga1 = dw2[1] * x1[0] * x1[0] - 6.0 * w2[1] * (gdotx[1] + 2.0 * g1[0] * x1[0]); + const dga2 = dw2[2] * x2[0] * x2[0] - 6.0 * w2[2] * (gdotx[2] + 2.0 * g2[0] * x2[0]); + const dga3 = dw2[3] * x3[0] * x3[0] - 6.0 * w2[3] * (gdotx[3] + 2.0 * g3[0] * x3[0]); + dg = dga0.map((val, index) => 35.0 * (val + dga1[index] + dga2[index] + dga3[index])); + + const dgb0 = dw2[0] * x0[0] * x0[1] - 6.0 * w2[0] * (g0[0] * x0[1] + g0[1] * x0[0]); + const dgb1 = dw2[1] * x1[0] * x1[1] - 6.0 * w2[1] * (g1[0] * x1[1] + g1[1] * x1[0]); + const dgb2 = dw2[2] * x2[0] * x2[1] - 6.0 * w2[2] * (g2[0] * x2[1] + g2[1] * x2[0]); + const dgb3 = dw2[3] * x3[0] * x3[1] - 6.0 * w2[3] * (g3[0] * x3[1] + g3[1] * x3[0]); + dg2 = dgb0.map((val, index) => 39.5 * (val + dgb1[index] + dgb2[index] + dgb3[index])); return 39.5 * n; } diff --git a/packages/react-three-jolt/tsconfig.json b/packages/react-three-jolt/tsconfig.json index b5d2437..7fd4e37 100644 --- a/packages/react-three-jolt/tsconfig.json +++ b/packages/react-three-jolt/tsconfig.json @@ -1,27 +1,27 @@ { - "compilerOptions": { - "target": "ES2022", - "useDefineForClassFields": true, - "module": "ES2022", - "lib": ["ES2022", "DOM"], - "moduleResolution": "Bundler", - "strict": true, - "sourceMap": true, - "resolveJsonModule": true, - "isolatedModules": true, - "esModuleInterop": true, - "noEmit": true, - "declaration": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "skipLibCheck": true, - "typeRoots": ["./types"], - "baseUrl": "./", - "rootDir": "./src", - "allowSyntheticDefaultImports": true, - "outDir": "./dist", - "jsx": "react" - }, - "files": ["./src/index.ts"] + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ES2022", + "lib": ["ES2022", "DOM"], + "moduleResolution": "Bundler", + "strict": true, + "sourceMap": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": true, + "declaration": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "skipLibCheck": true, + "typeRoots": ["./types"], + "baseUrl": "./", + "rootDir": "./src", + "allowSyntheticDefaultImports": true, + "outDir": "./dist", + "jsx": "react" + }, + "files": ["./src/index.ts"] } diff --git a/packages/react-three-jolt/types/gamepad.js.d.ts b/packages/react-three-jolt/types/gamepad.js.d.ts index 0b0b64b..1500fd1 100644 --- a/packages/react-three-jolt/types/gamepad.js.d.ts +++ b/packages/react-three-jolt/types/gamepad.js.d.ts @@ -1,4 +1,4 @@ -declare module "gamepad.js" { +declare module 'gamepad.js' { export interface Gamepad { axes: number[]; buttons: GamepadButton[]; @@ -25,11 +25,7 @@ declare module "gamepad.js" { } export default class GamepadHandler { - constructor( - index: number, - gamepad: Gamepad, - config?: GamepadHandlerOptions - ); + constructor(index: number, gamepad: Gamepad, config?: GamepadHandlerOptions); static resolveOptions(config: GamepadHandlerOptions): { axis: GamepadHandlerOptions; button: GamepadHandlerOptions; @@ -43,19 +39,10 @@ declare module "gamepad.js" { private setButtonValue(index: number, value: number): void; private resolveAxisValue(index: number): number; private resolveButtonValue(index: number): number; - on(event: "axis", listener: (event: GamepadHandlerEvent) => void): void; - on( - event: "button", - listener: (event: GamepadHandlerEvent) => void - ): void; - off( - event: "axis", - listener: (event: GamepadHandlerEvent) => void - ): void; - off( - event: "button", - listener: (event: GamepadHandlerEvent) => void - ): void; + on(event: 'axis', listener: (event: GamepadHandlerEvent) => void): void; + on(event: 'button', listener: (event: GamepadHandlerEvent) => void): void; + off(event: 'axis', listener: (event: GamepadHandlerEvent) => void): void; + off(event: 'button', listener: (event: GamepadHandlerEvent) => void): void; start(): void; stop(): void; } diff --git a/packages/react-three-jolt/vitest.config.ts b/packages/react-three-jolt/vitest.config.ts index 12e5d47..88457c4 100644 --- a/packages/react-three-jolt/vitest.config.ts +++ b/packages/react-three-jolt/vitest.config.ts @@ -4,6 +4,6 @@ import { defineConfig } from 'vite'; export default defineConfig({ test: { environment: 'happy-dom', - setupFiles: ['./test/setup.ts'], + setupFiles: ['./test/setup.ts'] } });