├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── .vscode └── settings.json ├── LICENSE ├── example ├── .gitignore ├── index.html ├── package-lock.json ├── package.json ├── src │ ├── App.tsx │ ├── demos │ │ ├── three-dimensional │ │ │ ├── ConstrainedGlobalRotation3D.tsx │ │ │ ├── ConstrainedLocalRotation3D.tsx │ │ │ ├── basic │ │ │ │ └── Basic.tsx │ │ │ ├── components │ │ │ │ ├── Base.tsx │ │ │ │ ├── JointTransforms.tsx │ │ │ │ ├── Link.tsx │ │ │ │ ├── Logger.tsx │ │ │ │ └── Target.tsx │ │ │ ├── moving-base │ │ │ │ ├── MovingBase.tsx │ │ │ │ └── arm2.gltf │ │ │ ├── moving-ends │ │ │ │ ├── MovingEnds.tsx │ │ │ │ ├── arm2.gltf │ │ │ │ └── useCircularMotion.tsx │ │ │ ├── skinned-mesh │ │ │ │ ├── SkinnedMesh.tsx │ │ │ │ └── arm2.gltf │ │ │ ├── three-js │ │ │ │ └── ThreeJS.tsx │ │ │ └── web-xr │ │ │ │ ├── WebXR.tsx │ │ │ │ └── arm2.gltf │ │ └── two-dimensional │ │ │ ├── ConstrainedGlobalRotation2D.tsx │ │ │ ├── ConstrainedLocalRotation2D.tsx │ │ │ ├── TwoDimension.tsx │ │ │ └── components │ │ │ ├── Base.tsx │ │ │ ├── JointTransforms.tsx │ │ │ ├── Link.tsx │ │ │ ├── Logger.tsx │ │ │ └── Target.tsx │ ├── hooks │ │ └── useAnimationFrame.tsx │ ├── index.css │ ├── index.tsx │ └── types.d.ts ├── tsconfig.json └── vite.config.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── readme.md ├── src ├── Range.ts ├── Solve2D.ts ├── Solve3D.ts ├── SolveOptions.ts ├── index.ts └── math │ ├── MathUtils.ts │ ├── Quaternion.ts │ ├── QuaternionO.ts │ ├── V2.ts │ ├── V2O.ts │ ├── V3.ts │ └── V3O.ts ├── tests ├── QuaternionO.test.ts ├── Solve2D.test.ts ├── Solve3D.test.ts ├── V3O.test.ts ├── setupTests.ts └── tsconfig.json └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | example -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | es6: true, 6 | node: true, 7 | }, 8 | parser: '@typescript-eslint/parser', 9 | plugins: ['@typescript-eslint', 'prettier'], 10 | extends: ['prettier', 'plugin:prettier/recommended'], 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | 7 | # build artifacts 8 | dist -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | tabWidth: 2, 6 | printWidth: 120, 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib", 3 | "cSpell.words": [ 4 | "FABRIK" 5 | ] 6 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jae Perris 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Inverse Kinematics examples 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.0.0", 4 | "homepage": "https://tmf-code.github.io/inverse-kinematics", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "serve": "vite preview", 9 | "predeploy": "npm run build", 10 | "deploy": "gh-pages -d dist" 11 | }, 12 | "dependencies": { 13 | "@react-three/drei": "^7.0.6", 14 | "@react-three/fiber": "7.0.4", 15 | "@react-three/xr": "^3.1.2", 16 | "@types/react-router-dom": "^5.1.8", 17 | "gh-pages": "^3.2.3", 18 | "inverse-kinematics": "file:..", 19 | "leva": "^0.9.13", 20 | "react": "^17.0.2", 21 | "react-dom": "^17.0.2", 22 | "react-router-dom": "^5.2.0", 23 | "three": "^0.130.1", 24 | "three-stdlib": "^2.3.1" 25 | }, 26 | "devDependencies": { 27 | "@types/react": "^17.0.14", 28 | "@types/react-dom": "^17.0.9", 29 | "@types/three": "^0.130.0", 30 | "@vitejs/plugin-react-refresh": "^1.3.5", 31 | "typescript": "^4.3.5", 32 | "vite": "^2.4.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react' 2 | import { HashRouter, Link, Route, useLocation } from 'react-router-dom' 3 | import SkinnedMeshExample from './demos/three-dimensional/skinned-mesh/SkinnedMesh' 4 | import Basic from './demos/three-dimensional/basic/Basic' 5 | import ThreeJS from './demos/three-dimensional/three-js/ThreeJS' 6 | import TwoDimension from './demos/two-dimensional/TwoDimension' 7 | import ConstrainedLocalRotation2D from './demos/two-dimensional/ConstrainedLocalRotation2D' 8 | import ConstrainedLocalRotation3D from './demos/three-dimensional/ConstrainedLocalRotation3D' 9 | import ConstrainedGlobalRotation2D from './demos/two-dimensional/ConstrainedGlobalRotation2D' 10 | import ConstrainedGlobalRotation3D from './demos/three-dimensional/ConstrainedGlobalRotation3D' 11 | import WebXRExample from './demos/three-dimensional/web-xr/WebXR' 12 | import MovingBaseExample from './demos/three-dimensional/moving-base/MovingBase' 13 | import MovingEndsExample from './demos/three-dimensional/moving-ends/MovingEnds' 14 | 15 | function App() { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | ) 67 | } 68 | 69 | function Menu() { 70 | const location = useLocation() 71 | const pathname = location.pathname 72 | return ( 73 |
74 |

Inverse kinematics examples

75 | Github page 76 |

2d

77 |
    78 |
  • 79 | 2D basic 80 |
  • 81 |
  • 82 | 2D basic - CCD 83 |
  • 84 |
  • 85 | Constrained local rotation 86 |
  • 87 |
  • 88 | Constrained global rotation 89 |
  • 90 |
91 |

3d

92 |
    93 |
  • 94 | 3D basic 95 |
  • 96 |
  • 97 | 3D CCD 98 |
  • 99 |
  • 100 | Three.js 101 |
  • 102 |
  • 103 | Three js Skinned Mesh 104 |
  • 105 |
  • 106 | Web XR 107 |
  • 108 |
  • 109 | Moving base 110 |
  • 111 |
  • 112 | Moving ends 113 |
  • 114 |
  • 115 | Constrained local rotation 116 |
  • 117 |
  • 118 | Constrained global rotation 119 |
  • 120 |
121 |
122 | ) 123 | } 124 | 125 | export default App 126 | -------------------------------------------------------------------------------- /example/src/demos/three-dimensional/ConstrainedGlobalRotation3D.tsx: -------------------------------------------------------------------------------- 1 | import { Canvas } from '@react-three/fiber' 2 | import { MathUtils, QuaternionO, Solve3D, V3 } from 'inverse-kinematics' 3 | import React, { useEffect, useState } from 'react' 4 | import { useAnimationFrame } from '../../hooks/useAnimationFrame' 5 | import { Base } from './components/Base' 6 | import { JointTransforms } from './components/JointTransforms' 7 | import { Logger } from './components/Logger' 8 | import { Target } from './components/Target' 9 | import { useControls } from 'leva' 10 | import { OrbitControls } from '@react-three/drei' 11 | 12 | const base: Solve3D.JointTransform = { position: [0, 0, 0], rotation: QuaternionO.zeroRotation() } 13 | 14 | export default function ConstrainedGlobalRotation3D() { 15 | const [target, setTarget] = useState([500, 50, 0] as V3) 16 | const [links, setLinks] = useState([]) 17 | 18 | const { linkCount, linkLength, endEffectorRotation } = useControls({ 19 | linkCount: { value: 4, min: 0, max: 50, step: 1 }, 20 | linkLength: { value: 1, min: 0.1, max: 2, step: 0.1 }, 21 | endEffectorRotation: { value: 0, min: -180, max: 180, step: 5 }, 22 | }) 23 | 24 | useEffect(() => { 25 | setLinks(makeLinks(linkCount, linkLength, endEffectorRotation)) 26 | }, [linkCount, linkLength, endEffectorRotation]) 27 | 28 | useAnimationFrame(60, () => { 29 | const knownRangeOfMovement = linkCount * linkLength 30 | 31 | function learningRate(errorDistance: number): number { 32 | const relativeDistanceToTarget = MathUtils.clamp(errorDistance / knownRangeOfMovement, 0, 1) 33 | const cutoff = 0.5 34 | 35 | if (relativeDistanceToTarget > cutoff) { 36 | return 10e-3 37 | } 38 | 39 | // result is between 0 and 1 40 | const remainingDistance = relativeDistanceToTarget / 0.02 41 | const minimumLearningRate = 10e-4 42 | 43 | return (minimumLearningRate + remainingDistance * 10e-4) / knownRangeOfMovement 44 | } 45 | 46 | const result = Solve3D.solve(links, base, target, { 47 | method: 'FABRIK', 48 | learningRate, 49 | acceptedError: 0.1, 50 | }).links 51 | 52 | links.forEach((_, index) => { 53 | links[index] = result[index]! 54 | }) 55 | }) 56 | 57 | return ( 58 |
59 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
75 | ) 76 | } 77 | 78 | const makeLinks = (linkCount: number, linkLength: number, endEffectorRotation: number): Solve3D.Link[] => 79 | Array.from({ length: linkCount }).map((_, index) => { 80 | if (index === linkCount - 1) { 81 | return { 82 | position: [linkLength, 0, 0], 83 | constraints: { 84 | value: QuaternionO.fromEulerAngles([0, 0, (endEffectorRotation * Math.PI) / 180]), 85 | type: 'global', 86 | }, 87 | rotation: QuaternionO.zeroRotation(), 88 | } 89 | } 90 | return { 91 | position: [linkLength, 0, 0], 92 | rotation: QuaternionO.zeroRotation(), 93 | } 94 | }) 95 | -------------------------------------------------------------------------------- /example/src/demos/three-dimensional/ConstrainedLocalRotation3D.tsx: -------------------------------------------------------------------------------- 1 | import { Canvas } from '@react-three/fiber' 2 | import { MathUtils, QuaternionO, Solve3D, V2, V3 } from 'inverse-kinematics' 3 | import React, { useEffect, useRef, useState } from 'react' 4 | import { useAnimationFrame } from '../../hooks/useAnimationFrame' 5 | import { Base } from './components/Base' 6 | import { JointTransforms } from './components/JointTransforms' 7 | import { Logger } from './components/Logger' 8 | import { Target } from './components/Target' 9 | import { useControls } from 'leva' 10 | import { OrbitControls } from '@react-three/drei' 11 | 12 | const base: Solve3D.JointTransform = { position: [0, 0, 0], rotation: QuaternionO.zeroRotation() } 13 | 14 | export default function ConstrainedLocalRotation3D() { 15 | const [target, setTarget] = useState([500, 50, 0] as V3) 16 | const [links, setLinks] = useState([]) 17 | 18 | const { linkCount, linkLength, endEffectorRotation } = useControls({ 19 | linkCount: { value: 4, min: 0, max: 50, step: 1 }, 20 | linkLength: { value: 1, min: 0.1, max: 2, step: 0.1 }, 21 | endEffectorRotation: { value: 0, min: -180, max: 180, step: 5 }, 22 | }) 23 | 24 | useEffect(() => { 25 | setLinks(makeLinks(linkCount, linkLength, endEffectorRotation)) 26 | }, [linkCount, linkLength, endEffectorRotation]) 27 | 28 | useAnimationFrame(60, () => { 29 | const knownRangeOfMovement = linkCount * linkLength 30 | 31 | function learningRate(errorDistance: number): number { 32 | const relativeDistanceToTarget = MathUtils.clamp(errorDistance / knownRangeOfMovement, 0, 1) 33 | const cutoff = 0.5 34 | 35 | if (relativeDistanceToTarget > cutoff) { 36 | return 10e-3 37 | } 38 | 39 | // result is between 0 and 1 40 | const remainingDistance = relativeDistanceToTarget / 0.02 41 | const minimumLearningRate = 10e-4 42 | 43 | return (minimumLearningRate + remainingDistance * 10e-4) / knownRangeOfMovement 44 | } 45 | 46 | const result = Solve3D.solve(links, base, target, { 47 | method: 'FABRIK', 48 | learningRate, 49 | acceptedError: 0.1, 50 | }).links 51 | 52 | links.forEach((_, index) => { 53 | links[index] = result[index]! 54 | }) 55 | }) 56 | 57 | return ( 58 |
59 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
75 | ) 76 | } 77 | 78 | const makeLinks = (linkCount: number, linkLength: number, endEffectorRotation: number): Solve3D.Link[] => 79 | Array.from({ length: linkCount }).map((_, index) => { 80 | if (index === linkCount - 1) { 81 | return { 82 | position: [linkLength, 0, 0], 83 | constraints: { 84 | value: QuaternionO.fromEulerAngles([0, 0, (endEffectorRotation * Math.PI) / 180]), 85 | type: 'local', 86 | }, 87 | rotation: QuaternionO.zeroRotation(), 88 | } 89 | } 90 | return { 91 | position: [linkLength, 0, 0], 92 | rotation: QuaternionO.zeroRotation(), 93 | } 94 | }) 95 | -------------------------------------------------------------------------------- /example/src/demos/three-dimensional/basic/Basic.tsx: -------------------------------------------------------------------------------- 1 | import { OrbitControls } from '@react-three/drei' 2 | import { Canvas } from '@react-three/fiber' 3 | import { MathUtils, QuaternionO, Solve3D, V3 } from 'inverse-kinematics' 4 | import { useControls } from 'leva' 5 | import React, { useEffect, useState } from 'react' 6 | import { useAnimationFrame } from '../../../hooks/useAnimationFrame' 7 | import { Base } from '../components/Base' 8 | import { JointTransforms } from '../components/JointTransforms' 9 | import { Logger } from '../components/Logger' 10 | import { Target } from '../components/Target' 11 | 12 | const base: Solve3D.JointTransform = { position: [0, 0, 0], rotation: QuaternionO.zeroRotation() } 13 | 14 | export default function Basic({ method }: { method: 'CCD' | 'FABRIK' }) { 15 | const [target, setTarget] = useState([500, 50, 0] as V3) 16 | const [links, setLinks] = useState([]) 17 | 18 | const { linkCount, linkLength, linkMinAngle, linkMaxAngle } = useControls({ 19 | linkCount: { value: 4, min: 0, max: 50, step: 1 }, 20 | linkLength: { value: 1, min: 0.1, max: 2, step: 0.1 }, 21 | linkMinAngle: { value: -90, min: -360, max: 0, step: 10 }, 22 | linkMaxAngle: { value: 90, min: 0, max: 360, step: 10 }, 23 | }) 24 | 25 | useEffect(() => { 26 | setLinks(makeLinks(linkCount, linkLength, linkMinAngle, linkMaxAngle)) 27 | }, [linkCount, linkLength, linkMinAngle, linkMaxAngle]) 28 | 29 | useAnimationFrame(60, () => { 30 | const knownRangeOfMovement = linkCount * linkLength 31 | 32 | function learningRate(errorDistance: number): number { 33 | const relativeDistanceToTarget = MathUtils.clamp(errorDistance / knownRangeOfMovement, 0, 1) 34 | const cutoff = 0.5 35 | 36 | if (relativeDistanceToTarget > cutoff) { 37 | return 10e-3 38 | } 39 | 40 | // result is between 0 and 1 41 | const remainingDistance = relativeDistanceToTarget / 0.02 42 | const minimumLearningRate = 10e-4 43 | 44 | return (minimumLearningRate + remainingDistance * 10e-4) / knownRangeOfMovement 45 | } 46 | 47 | const result = Solve3D.solve(links, base, target, { 48 | method, 49 | learningRate: method === 'FABRIK' ? learningRate : 1, 50 | acceptedError: 0, 51 | }).links 52 | 53 | links.forEach((_, index) => { 54 | links[index] = result[index]! 55 | }) 56 | }) 57 | 58 | return ( 59 |
60 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 |
78 | ) 79 | } 80 | 81 | const makeLinks = (linkCount: number, linkLength: number, linkMinAngle: number, linkMaxAngle: number): Solve3D.Link[] => 82 | Array.from({ length: linkCount }).map(() => ({ 83 | position: [linkLength, 0, 0], 84 | rotation: QuaternionO.zeroRotation(), 85 | // constraints: { 86 | // pitch: { min: (linkMinAngle * Math.PI) / 180, max: (linkMaxAngle * Math.PI) / 180 }, 87 | // yaw: { min: (linkMinAngle * Math.PI) / 180, max: (linkMaxAngle * Math.PI) / 180 }, 88 | // roll: { min: (linkMinAngle * Math.PI) / 180, max: (linkMaxAngle * Math.PI) / 180 }, 89 | // }, 90 | })) 91 | -------------------------------------------------------------------------------- /example/src/demos/three-dimensional/components/Base.tsx: -------------------------------------------------------------------------------- 1 | import { useFrame } from '@react-three/fiber' 2 | import { QuaternionO, Solve3D } from 'inverse-kinematics' 3 | import React, { useMemo, useRef } from 'react' 4 | import { BoxBufferGeometry, Mesh, MeshNormalMaterial } from 'three' 5 | import { Link, LinkProps } from './Link' 6 | 7 | export const Base = ({ base: base, links }: { links: Solve3D.Link[]; base: Solve3D.JointTransform }) => { 8 | const ref = useRef>() 9 | const chain = useMemo(() => makeChain(links), [links]) 10 | 11 | useFrame(() => { 12 | if (!ref.current) return 13 | ref.current.position.set(...base.position) 14 | ref.current.quaternion.set(...base.rotation) 15 | 16 | let depth = 0 17 | let child = chain 18 | 19 | while (child !== undefined && links[depth] !== undefined) { 20 | child.link.rotation = links[depth]!.rotation ?? QuaternionO.zeroRotation() 21 | depth++ 22 | child = child.child 23 | } 24 | }) 25 | 26 | return ( 27 | 28 | 29 | 30 | {chain && } 31 | 32 | ) 33 | } 34 | 35 | function makeChain(links: Solve3D.Link[]): LinkProps | undefined { 36 | let chain: LinkProps | undefined 37 | for (let index = links.length - 1; index >= 0; index--) { 38 | const link: LinkProps = { 39 | link: { ...links[index]!, rotation: links[index]!.rotation ?? QuaternionO.zeroRotation() }, 40 | } 41 | 42 | // Is first element 43 | if (chain === undefined) { 44 | chain = link 45 | continue 46 | } 47 | 48 | chain = { link: link.link, child: chain } 49 | } 50 | 51 | return chain 52 | } 53 | -------------------------------------------------------------------------------- /example/src/demos/three-dimensional/components/JointTransforms.tsx: -------------------------------------------------------------------------------- 1 | import { useFrame } from '@react-three/fiber' 2 | import { Solve3D } from 'inverse-kinematics' 3 | import React, { useMemo, useRef } from 'react' 4 | import { Group } from 'three' 5 | 6 | export const JointTransforms = ({ links, base }: { links: Solve3D.Link[]; base: Solve3D.JointTransform }) => { 7 | const ref = useRef() 8 | 9 | useFrame(() => { 10 | if (ref.current === undefined) return 11 | 12 | const { transforms } = Solve3D.getJointTransforms(links, base) 13 | for (let index = 0; index < ref.current.children.length; index++) { 14 | const child = ref.current.children[index]! 15 | const jointPosition = transforms[index]?.position 16 | if (jointPosition === undefined) { 17 | throw new Error(`No corresponding child position for index ${index}`) 18 | } 19 | child.position.set(...jointPosition) 20 | } 21 | }) 22 | 23 | const jointTransforms = useMemo( 24 | () => 25 | Array.from({ length: links.length + 1 }).map((_, index) => { 26 | return ( 27 | 28 | 29 | 30 | 31 | ) 32 | }), 33 | [links], 34 | ) 35 | return {jointTransforms} 36 | } 37 | -------------------------------------------------------------------------------- /example/src/demos/three-dimensional/components/Link.tsx: -------------------------------------------------------------------------------- 1 | import { useFrame } from '@react-three/fiber' 2 | import { Quaternion, V3 } from 'inverse-kinematics' 3 | import React, { useMemo, useRef } from 'react' 4 | import { 5 | BoxBufferGeometry, 6 | BufferGeometry, 7 | Color, 8 | Group, 9 | Line, 10 | LineBasicMaterial, 11 | Mesh, 12 | MeshNormalMaterial, 13 | Vector3, 14 | } from 'three' 15 | 16 | export interface LinkProps { 17 | link: { rotation: Quaternion; position: V3 } 18 | child?: LinkProps 19 | } 20 | 21 | export const Link = ({ link, child }: LinkProps) => { 22 | const rotationRef = useRef() 23 | const translationRef = useRef>() 24 | 25 | useFrame(() => { 26 | if (!rotationRef.current) return 27 | if (!translationRef.current) return 28 | rotationRef.current.quaternion.set(...link.rotation) 29 | translationRef.current.position.set(...link.position) 30 | }) 31 | 32 | const line: Line = useMemo(() => { 33 | const points = [new Vector3(), new Vector3(...link.position)] 34 | const geometry = new BufferGeometry().setFromPoints(points) 35 | const material = new LineBasicMaterial({ color: new Color('#8B008B') }) 36 | 37 | return new Line(geometry, material) 38 | }, [link]) 39 | 40 | return ( 41 | 42 | 43 | 44 | 45 | {child && } 46 | 47 | 48 | 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /example/src/demos/three-dimensional/components/Logger.tsx: -------------------------------------------------------------------------------- 1 | import { Solve3D, V3 } from 'inverse-kinematics' 2 | import React, { useRef } from 'react' 3 | import { useAnimationFrame } from '../../../hooks/useAnimationFrame' 4 | 5 | export const Logger = ({ 6 | target, 7 | links, 8 | base: base, 9 | }: { 10 | target: V3 11 | links: Solve3D.Link[] 12 | base: Solve3D.JointTransform 13 | }) => { 14 | const distanceRef = useRef(null) 15 | 16 | useAnimationFrame(1, () => { 17 | if (!distanceRef.current) return 18 | distanceRef.current.innerText = Solve3D.getErrorDistance(links, base, target).toFixed(3) 19 | }) 20 | 21 | return ( 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
Distance
31 |
32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /example/src/demos/three-dimensional/components/Target.tsx: -------------------------------------------------------------------------------- 1 | import { useThree } from '@react-three/fiber' 2 | import { V3, V3O } from 'inverse-kinematics' 3 | import React, { useEffect } from 'react' 4 | import { Vector3 } from 'three' 5 | 6 | export const Target = ({ position, setPosition }: { position: V3; setPosition: (position: V3) => void }) => { 7 | const { camera } = useThree() 8 | useEffect(() => { 9 | const onClick = (event: MouseEvent) => { 10 | const vec = new Vector3() 11 | const clickPosition = new Vector3() 12 | vec.set((event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 + 1, 0.5) 13 | vec.unproject(camera) 14 | vec.sub(camera.position).normalize() 15 | const distance = -camera.position.z / vec.z 16 | clickPosition.copy(camera.position).add(vec.multiplyScalar(distance)) 17 | setPosition(V3O.fromVector3(clickPosition)) 18 | } 19 | window.addEventListener('click', onClick) 20 | return () => { 21 | window.removeEventListener('click', onClick) 22 | } 23 | }, []) 24 | return ( 25 | 26 | 27 | 28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /example/src/demos/three-dimensional/moving-base/MovingBase.tsx: -------------------------------------------------------------------------------- 1 | import { OrbitControls, useGLTF } from '@react-three/drei' 2 | import { Canvas, useFrame } from '@react-three/fiber' 3 | import { MathUtils, QuaternionO, Solve3D, V3, V3O } from 'inverse-kinematics' 4 | import React, { Suspense, useRef, useState } from 'react' 5 | import * as THREE from 'three' 6 | import { Bone, MeshNormalMaterial, Vector3 } from 'three' 7 | import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader' 8 | import { Base } from '../components/Base' 9 | import { JointTransforms } from '../components/JointTransforms' 10 | import { Target } from '../components/Target' 11 | import { useCircularMotion } from '../moving-ends/useCircularMotion' 12 | import modelSrc from './arm2.gltf?url' 13 | 14 | type GLTFResult = GLTF & { 15 | nodes: { 16 | Cylinder: THREE.SkinnedMesh 17 | shoulder: THREE.Bone 18 | } 19 | } 20 | 21 | function MovingBaseExample() { 22 | return ( 23 | 32 | 33 | 34 | 35 | 36 | ) 37 | } 38 | 39 | function Scene() { 40 | const { nodes } = useGLTF(modelSrc) as GLTFResult 41 | const modelRef = useRef() 42 | 43 | const [base] = useState({ position: V3O.zero(), rotation: QuaternionO.zeroRotation() }) 44 | const [debugLinks, setLinks] = useState([]) 45 | const [target, setTarget] = useState([0, 0, 0]) 46 | 47 | useFrame(() => { 48 | // Get base position 49 | const firstBone = modelRef.current?.children.find((child) => (child as Bone).isBone) as Bone | undefined 50 | if (!firstBone) return 51 | 52 | const basePosition = V3O.fromVector3(firstBone.getWorldPosition(new Vector3())) 53 | const baseTransform: Solve3D.JointTransform = { 54 | position: basePosition, 55 | rotation: QuaternionO.zeroRotation(), 56 | } 57 | 58 | base.position = baseTransform.position 59 | base.rotation = baseTransform.rotation 60 | 61 | const modelScale = V3O.fromVector3(modelRef.current!.getWorldScale(new Vector3())) 62 | const bones: Bone[] = getBones(firstBone) 63 | 64 | const links: Solve3D.Link[] = bones.map((bone, index, array) => { 65 | const nextBone = array[index + 1] 66 | const rotation = QuaternionO.fromObject(bone.quaternion) 67 | 68 | return { 69 | position: V3O.multiply(V3O.fromVector3(nextBone?.position ?? bone.position), modelScale), 70 | rotation: rotation, 71 | constraints: { 72 | roll: { min: -Math.PI / 4, max: Math.PI / 4 }, 73 | pitch: { min: -Math.PI / 4, max: Math.PI / 4 }, 74 | yaw: { min: -Math.PI / 4, max: Math.PI / 4 }, 75 | }, 76 | } 77 | }) 78 | 79 | const knownRangeOfMovement = links.reduce((acc, cur) => acc + V3O.euclideanLength(cur.position), 0) 80 | 81 | const results = Solve3D.solve(links, baseTransform, target, { 82 | method: 'FABRIK', 83 | learningRate: learningRate(knownRangeOfMovement), 84 | acceptedError: knownRangeOfMovement / 100, 85 | }).links 86 | 87 | results.forEach((link, index, array) => { 88 | const bone = bones[index]! 89 | bone.quaternion.set(...link.rotation) 90 | }) 91 | 92 | setLinks(results) 93 | }) 94 | 95 | useCircularMotion(modelRef, 5, 1 / 3) 96 | 97 | return ( 98 | <> 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 112 | 113 | 114 | 115 | 116 | ) 117 | } 118 | 119 | export default MovingBaseExample 120 | 121 | function getBones(firstBone: THREE.Bone) { 122 | let currentBone = firstBone 123 | const bones: Bone[] = [firstBone] 124 | while (currentBone.children[0] !== undefined) { 125 | bones.push(currentBone.children[0] as Bone) 126 | currentBone = currentBone.children[0] as Bone 127 | } 128 | return bones 129 | } 130 | 131 | useGLTF.preload(modelSrc) 132 | 133 | function learningRate(totalLength: number): (errorDistance: number) => number { 134 | return (errorDistance: number) => { 135 | const relativeDistanceToTarget = MathUtils.clamp(errorDistance / totalLength, 0, 1) 136 | const cutoff = 0.1 137 | 138 | if (relativeDistanceToTarget > cutoff) { 139 | return 10e-3 140 | } 141 | 142 | const remainingDistance = relativeDistanceToTarget / 0.02 143 | const minimumLearningRate = 10e-4 144 | 145 | return minimumLearningRate + remainingDistance * 10e-4 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /example/src/demos/three-dimensional/moving-ends/MovingEnds.tsx: -------------------------------------------------------------------------------- 1 | import { OrbitControls, Sphere, useGLTF } from '@react-three/drei' 2 | import { Canvas, useFrame } from '@react-three/fiber' 3 | import { MathUtils, QuaternionO, Solve3D, V3, V3O } from 'inverse-kinematics' 4 | import React, { Suspense, useRef, useState } from 'react' 5 | import * as THREE from 'three' 6 | import { Bone, MeshNormalMaterial, Object3D, Vector3 } from 'three' 7 | import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader' 8 | import { Base } from '../components/Base' 9 | import { JointTransforms } from '../components/JointTransforms' 10 | import { Target } from '../components/Target' 11 | import modelSrc from './arm2.gltf?url' 12 | import { useCircularMotion } from './useCircularMotion' 13 | 14 | type GLTFResult = GLTF & { 15 | nodes: { 16 | Cylinder: THREE.SkinnedMesh 17 | shoulder: THREE.Bone 18 | } 19 | } 20 | 21 | function MovingEndsExample() { 22 | return ( 23 | 32 | 33 | 34 | 35 | 36 | ) 37 | } 38 | 39 | function Scene() { 40 | const { nodes } = useGLTF(modelSrc) as GLTFResult 41 | const modelRef = useRef() 42 | 43 | const [base] = useState({ position: V3O.zero(), rotation: QuaternionO.zeroRotation() }) 44 | const [debugLinks, setLinks] = useState([]) 45 | 46 | const targetRef = useRef(null) 47 | useFrame(() => { 48 | // Get base position 49 | if (!targetRef.current) return 50 | const firstBone = modelRef.current?.children.find((child) => (child as Bone).isBone) as Bone | undefined 51 | if (!firstBone) return 52 | 53 | const basePosition = V3O.fromVector3(firstBone.getWorldPosition(new Vector3())) 54 | const baseTransform: Solve3D.JointTransform = { 55 | position: basePosition, 56 | rotation: QuaternionO.zeroRotation(), 57 | } 58 | 59 | base.position = baseTransform.position 60 | base.rotation = baseTransform.rotation 61 | 62 | const modelScale = V3O.fromVector3(modelRef.current!.getWorldScale(new Vector3())) 63 | const bones: Bone[] = getBones(firstBone) 64 | 65 | const links: Solve3D.Link[] = bones.map((bone, index, array) => { 66 | const nextBone = array[index + 1] 67 | const rotation = QuaternionO.fromObject(bone.quaternion) 68 | 69 | return { 70 | position: V3O.multiply(V3O.fromVector3(nextBone?.position ?? bone.position), modelScale), 71 | rotation: rotation, 72 | constraints: { 73 | roll: { min: -Math.PI / 4, max: Math.PI / 4 }, 74 | pitch: { min: -Math.PI / 4, max: Math.PI / 4 }, 75 | yaw: { min: -Math.PI / 4, max: Math.PI / 4 }, 76 | }, 77 | } 78 | }) 79 | 80 | const knownRangeOfMovement = links.reduce((acc, cur) => acc + V3O.euclideanLength(cur.position), 0) 81 | 82 | const target = V3O.fromVector3(targetRef.current.getWorldPosition(new Vector3())) 83 | 84 | const results = Solve3D.solve(links, baseTransform, target, { 85 | learningRate: learningRate(knownRangeOfMovement), 86 | acceptedError: knownRangeOfMovement / 100, 87 | method: 'FABRIK', 88 | }).links 89 | 90 | results.forEach((link, index) => { 91 | const bone = bones[index]! 92 | bone.quaternion.set(...link.rotation) 93 | }) 94 | 95 | setLinks(results) 96 | }) 97 | 98 | useCircularMotion(modelRef, 5, 1 / 3) 99 | 100 | useCircularMotion(targetRef, 8, 1 / 2) 101 | return ( 102 | <> 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 122 | 123 | 124 | 125 | 126 | ) 127 | } 128 | 129 | export default MovingEndsExample 130 | 131 | function getBones(firstBone: THREE.Bone) { 132 | let currentBone = firstBone 133 | const bones: Bone[] = [firstBone] 134 | while (currentBone.children[0] !== undefined) { 135 | bones.push(currentBone.children[0] as Bone) 136 | currentBone = currentBone.children[0] as Bone 137 | } 138 | return bones 139 | } 140 | 141 | useGLTF.preload(modelSrc) 142 | 143 | function learningRate(totalLength: number): (errorDistance: number) => number { 144 | return (errorDistance: number) => { 145 | const relativeDistanceToTarget = MathUtils.clamp(errorDistance / totalLength, 0, 1) 146 | const cutoff = 0.1 147 | 148 | if (relativeDistanceToTarget > cutoff) { 149 | return 10e-4 150 | } 151 | 152 | const remainingDistance = relativeDistanceToTarget / 0.02 153 | const minimumLearningRate = 10e-5 154 | 155 | return minimumLearningRate + remainingDistance * 10e-5 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /example/src/demos/three-dimensional/moving-ends/useCircularMotion.tsx: -------------------------------------------------------------------------------- 1 | import { useFrame } from '@react-three/fiber' 2 | import { V2O } from 'inverse-kinematics' 3 | import React, { useRef } from 'react' 4 | import { Object3D } from 'three' 5 | 6 | export function useCircularMotion( 7 | ref: React.MutableRefObject, 8 | radius: number, 9 | rate: number, 10 | ) { 11 | const angle = useRef(0) 12 | useFrame((_context, deltaTime) => { 13 | if (!ref.current) return 14 | const position = V2O.fromPolar(radius, angle.current) 15 | ref.current.position.x = position[0] 16 | ref.current.position.z = position[1] 17 | 18 | angle.current += deltaTime * rate 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /example/src/demos/three-dimensional/skinned-mesh/SkinnedMesh.tsx: -------------------------------------------------------------------------------- 1 | import { GizmoHelper, GizmoViewport, OrbitControls, useGLTF } from '@react-three/drei' 2 | import { Canvas, useFrame } from '@react-three/fiber' 3 | import { MathUtils, QuaternionO, Solve3D, V3O } from 'inverse-kinematics' 4 | import React, { Suspense, useRef, useState } from 'react' 5 | import { Bone, Group, MeshNormalMaterial, Vector3 } from 'three' 6 | import { OrbitControls as ThreeOrbitControls } from 'three-stdlib' 7 | import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader' 8 | import { Base } from '../components/Base' 9 | import { JointTransforms } from '../components/JointTransforms' 10 | import { Target } from '../components/Target' 11 | import modelSrc from './arm2.gltf?url' 12 | 13 | function SkinnedMeshExample() { 14 | return ( 15 | 24 | 25 | 26 | 27 | 28 | ) 29 | } 30 | 31 | function Scene() { 32 | const { nodes } = useGLTF(modelSrc) as GLTFResult 33 | const modelRef = useRef() 34 | 35 | const [target, setTarget] = useState(V3O.zero()) 36 | const [base] = useState({ position: V3O.zero(), rotation: QuaternionO.zeroRotation() }) 37 | const [debugLinks, setLinks] = useState([]) 38 | 39 | const controlsRef = useRef(null) 40 | 41 | useFrame(() => { 42 | const firstBone = modelRef.current?.children.find((child) => (child as Bone).isBone) as Bone | undefined 43 | if (!firstBone) return 44 | 45 | const basePosition = V3O.fromVector3(firstBone.position) 46 | const baseTransform: Solve3D.JointTransform = { 47 | position: basePosition, 48 | rotation: QuaternionO.zeroRotation(), 49 | } 50 | 51 | base.position = baseTransform.position 52 | base.rotation = baseTransform.rotation 53 | 54 | const bones: Bone[] = getBones(firstBone) 55 | 56 | const links: Solve3D.Link[] = bones.map((bone, index, array) => { 57 | const nextBone = array[index + 1] 58 | const rotation = QuaternionO.fromObject(bone.quaternion) 59 | 60 | return { 61 | position: V3O.fromVector3(nextBone?.position ?? bone.position), 62 | rotation: rotation, 63 | } 64 | }) 65 | 66 | const knownRangeOfMovement = links.reduce((acc, cur) => acc + V3O.euclideanLength(cur.position), 0) 67 | function learningRate(errorDistance: number): number { 68 | const relativeDistanceToTarget = MathUtils.clamp(errorDistance / knownRangeOfMovement, 0, 1) 69 | const cutoff = 0.1 70 | 71 | if (relativeDistanceToTarget > cutoff) { 72 | return 10e-4 73 | } 74 | 75 | const remainingDistance = relativeDistanceToTarget / 0.02 76 | const minimumLearningRate = 10e-5 77 | 78 | return minimumLearningRate + remainingDistance * 10e-5 79 | } 80 | 81 | const results = Solve3D.solve(links, baseTransform, target, { 82 | learningRate, 83 | acceptedError: knownRangeOfMovement / 1000, 84 | method: 'FABRIK', 85 | }).links 86 | 87 | results.forEach((link, index, array) => { 88 | const bone = bones[index]! 89 | bone.quaternion.set(...link.rotation) 90 | }) 91 | 92 | setLinks(results) 93 | }) 94 | 95 | return ( 96 | <> 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 109 | 110 | 111 | 112 | controlsRef?.current?.target as Vector3} 116 | onUpdate={() => controlsRef.current?.update!()} 117 | > 118 | 119 | 120 | 121 | ) 122 | } 123 | 124 | export default SkinnedMeshExample 125 | 126 | function getBones(firstBone: Bone) { 127 | let currentBone = firstBone 128 | const bones: Bone[] = [firstBone] 129 | while (currentBone.children[0] !== undefined) { 130 | bones.push(currentBone.children[0] as Bone) 131 | currentBone = currentBone.children[0] as Bone 132 | } 133 | return bones 134 | } 135 | 136 | type GLTFResult = GLTF & { 137 | nodes: { 138 | Cylinder: THREE.SkinnedMesh 139 | shoulder: THREE.Bone 140 | } 141 | } 142 | 143 | useGLTF.preload(modelSrc) 144 | -------------------------------------------------------------------------------- /example/src/demos/three-dimensional/three-js/ThreeJS.tsx: -------------------------------------------------------------------------------- 1 | import { OrbitControls } from '@react-three/drei' 2 | import { Canvas } from '@react-three/fiber' 3 | import { MathUtils, QuaternionO, Solve3D, V3, V3O } from 'inverse-kinematics' 4 | import React, { useEffect, useRef, useState } from 'react' 5 | import { Object3D, Quaternion, Vector3 } from 'three' 6 | import { useAnimationFrame } from '../../../hooks/useAnimationFrame' 7 | import { Base } from '../components/Base' 8 | import { JointTransforms } from '../components/JointTransforms' 9 | import { Logger } from '../components/Logger' 10 | import { Target } from '../components/Target' 11 | 12 | function ThreeJS() { 13 | const [target, setTarget] = useState([500, 50, 0] as V3) 14 | const [links, setLinks] = useState([]) 15 | const [base, setBase] = useState({ 16 | position: [0, 0, 0], 17 | rotation: QuaternionO.zeroRotation(), 18 | }) 19 | const flattennedHierarchy = useRef(undefined) 20 | 21 | const parent = useRef() 22 | 23 | useEffect(() => { 24 | if (parent.current === undefined) return 25 | if (flattennedHierarchy.current !== undefined) return 26 | 27 | const hierarchy: Object3D[] = [] 28 | 29 | const recurseAndAdd = (current: Object3D) => { 30 | hierarchy.push(current) 31 | if (current.children[0]) { 32 | recurseAndAdd(current.children[0]) 33 | } 34 | } 35 | 36 | if (parent.current.children[0]) { 37 | recurseAndAdd(parent.current.children[0]) 38 | } 39 | 40 | const links: Solve3D.Link[] = hierarchy.map((object) => ({ 41 | rotation: QuaternionO.fromObject(object.quaternion), 42 | position: V3O.fromVector3(object.position), 43 | })) 44 | 45 | setLinks(links) 46 | setBase({ 47 | position: V3O.fromVector3(parent.current.getWorldPosition(new Vector3())), 48 | rotation: QuaternionO.fromObject(parent.current.getWorldQuaternion(new Quaternion())), 49 | }) 50 | flattennedHierarchy.current = hierarchy 51 | }) 52 | 53 | useAnimationFrame(60, () => { 54 | if (!flattennedHierarchy.current) return 55 | const knownRangeOfMovement = links.reduce((acc, cur) => acc + V3O.euclideanLength(cur.position), 0) 56 | function learningRate(errorDistance: number): number { 57 | const relativeDistanceToTarget = MathUtils.clamp(errorDistance / knownRangeOfMovement, 0, 1) 58 | const cutoff = 0.1 59 | 60 | if (relativeDistanceToTarget > cutoff) { 61 | return 10e-3 62 | } 63 | 64 | // result is between 0 and 1 65 | const remainingDistance = relativeDistanceToTarget / 0.02 66 | const minimumLearningRate = 10e-4 67 | 68 | return minimumLearningRate + remainingDistance * 10e-4 69 | } 70 | 71 | const results = Solve3D.solve(links, base, target, { 72 | learningRate, 73 | acceptedError: knownRangeOfMovement / 1000, 74 | method: 'FABRIK', 75 | }).links 76 | 77 | links.forEach((_, index) => { 78 | const result = results[index]! 79 | links[index] = result 80 | const q = result.rotation! 81 | 82 | const object = flattennedHierarchy.current![index]! 83 | const length = object?.position.length() 84 | const position = V3O.rotate([length, 0, 0], q) 85 | object.position.set(...position) 86 | object.quaternion.set(...q) 87 | }) 88 | }) 89 | 90 | return ( 91 |
92 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 |
123 | ) 124 | } 125 | 126 | export default ThreeJS 127 | -------------------------------------------------------------------------------- /example/src/demos/three-dimensional/web-xr/WebXR.tsx: -------------------------------------------------------------------------------- 1 | import { useGLTF } from '@react-three/drei' 2 | import { useFrame } from '@react-three/fiber' 3 | import { DefaultXRControllers, useController, VRCanvas } from '@react-three/xr' 4 | import { MathUtils, QuaternionO, Solve3D, V3O } from 'inverse-kinematics' 5 | import React, { Suspense, useRef, useState } from 'react' 6 | import * as THREE from 'three' 7 | import { Bone, MeshNormalMaterial, Vector3 } from 'three' 8 | import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader' 9 | import { Base } from '../components/Base' 10 | import { JointTransforms } from '../components/JointTransforms' 11 | import modelSrc from './arm2.gltf?url' 12 | 13 | type GLTFResult = GLTF & { 14 | nodes: { 15 | Cylinder: THREE.SkinnedMesh 16 | shoulder: THREE.Bone 17 | } 18 | } 19 | 20 | function WebXRExample() { 21 | return ( 22 | 31 | 32 | 33 | ) 34 | } 35 | 36 | function Scene() { 37 | const { nodes } = useGLTF(modelSrc) as GLTFResult 38 | const modelRef = useRef() 39 | 40 | const [base] = useState({ position: V3O.zero(), rotation: QuaternionO.zeroRotation() }) 41 | const [debugLinks, setLinks] = useState([]) 42 | 43 | const controller = useController('left') 44 | 45 | useFrame(() => { 46 | // Get base position 47 | const firstBone = modelRef.current?.children.find((child) => (child as Bone).isBone) as Bone | undefined 48 | if (!firstBone) return 49 | 50 | const basePosition = V3O.fromVector3(firstBone.getWorldPosition(new Vector3())) 51 | const baseTransform: Solve3D.JointTransform = { 52 | position: basePosition, 53 | rotation: QuaternionO.zeroRotation(), 54 | } 55 | 56 | base.position = baseTransform.position 57 | base.rotation = baseTransform.rotation 58 | 59 | const modelScale = V3O.fromVector3(modelRef.current!.getWorldScale(new Vector3())) 60 | const bones: Bone[] = getBones(firstBone) 61 | 62 | const links: Solve3D.Link[] = bones.map((bone, index, array) => { 63 | const nextBone = array[index + 1] 64 | const rotation = QuaternionO.fromObject(bone.quaternion) 65 | 66 | return { 67 | position: V3O.multiply(V3O.fromVector3(nextBone?.position ?? bone.position), modelScale), 68 | rotation: rotation, 69 | } 70 | }) 71 | 72 | const knownRangeOfMovement = links.reduce((acc, cur) => acc + V3O.euclideanLength(cur.position), 0) 73 | 74 | if (controller) { 75 | const target = V3O.fromVector3(controller?.controller.getWorldPosition(new Vector3())) 76 | 77 | const results = Solve3D.solve(links, baseTransform, target, { 78 | learningRate: learningRate(knownRangeOfMovement), 79 | acceptedError: knownRangeOfMovement / 1000, 80 | method: 'FABRIK', 81 | }).links 82 | 83 | results.forEach((link, index, array) => { 84 | const bone = bones[index]! 85 | bone.quaternion.set(...link.rotation) 86 | }) 87 | 88 | setLinks(results) 89 | } 90 | }) 91 | 92 | return ( 93 | <> 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 107 | 108 | 109 | 110 | 111 | 112 | ) 113 | } 114 | 115 | export default WebXRExample 116 | 117 | function getBones(firstBone: THREE.Bone) { 118 | let currentBone = firstBone 119 | const bones: Bone[] = [firstBone] 120 | while (currentBone.children[0] !== undefined) { 121 | bones.push(currentBone.children[0] as Bone) 122 | currentBone = currentBone.children[0] as Bone 123 | } 124 | return bones 125 | } 126 | 127 | useGLTF.preload(modelSrc) 128 | 129 | function learningRate(totalLength: number): (errorDistance: number) => number { 130 | return (errorDistance: number) => { 131 | const relativeDistanceToTarget = MathUtils.clamp(errorDistance / totalLength, 0, 1) 132 | const cutoff = 0.1 133 | 134 | if (relativeDistanceToTarget > cutoff) { 135 | return 10e-4 136 | } 137 | 138 | const remainingDistance = relativeDistanceToTarget / 0.02 139 | const minimumLearningRate = 10e-5 140 | 141 | return minimumLearningRate + remainingDistance * 10e-5 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /example/src/demos/three-dimensional/web-xr/arm2.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset": { 3 | "generator": "Khronos glTF Blender I/O v1.6.16", 4 | "version": "2.0" 5 | }, 6 | "scene": 0, 7 | "scenes": [ 8 | { 9 | "name": "Scene", 10 | "nodes": [6] 11 | } 12 | ], 13 | "nodes": [ 14 | { 15 | "name": "end-effector", 16 | "translation": [0, 4, 0] 17 | }, 18 | { 19 | "children": [0], 20 | "name": "wrist", 21 | "translation": [0, 4, 0] 22 | }, 23 | { 24 | "children": [1], 25 | "name": "forearm", 26 | "translation": [0, 4, 0] 27 | }, 28 | { 29 | "children": [2], 30 | "name": "huberus", 31 | "translation": [0, 4, 0] 32 | }, 33 | { 34 | "children": [3], 35 | "name": "shoulder", 36 | "translation": [0, -10, 0] 37 | }, 38 | { 39 | "mesh": 0, 40 | "name": "Cylinder", 41 | "skin": 0 42 | }, 43 | { 44 | "children": [5, 4], 45 | "name": "Armature" 46 | } 47 | ], 48 | "meshes": [ 49 | { 50 | "name": "Cylinder", 51 | "primitives": [ 52 | { 53 | "attributes": { 54 | "POSITION": 0, 55 | "NORMAL": 1, 56 | "TEXCOORD_0": 2, 57 | "JOINTS_0": 3, 58 | "WEIGHTS_0": 4 59 | }, 60 | "indices": 5 61 | } 62 | ] 63 | } 64 | ], 65 | "skins": [ 66 | { 67 | "inverseBindMatrices": 6, 68 | "joints": [4, 3, 2, 1, 0], 69 | "name": "Armature" 70 | } 71 | ], 72 | "accessors": [ 73 | { 74 | "bufferView": 0, 75 | "componentType": 5126, 76 | "count": 660, 77 | "max": [1, 10, 1], 78 | "min": [-1, -10, -1], 79 | "type": "VEC3" 80 | }, 81 | { 82 | "bufferView": 1, 83 | "componentType": 5126, 84 | "count": 660, 85 | "type": "VEC3" 86 | }, 87 | { 88 | "bufferView": 2, 89 | "componentType": 5126, 90 | "count": 660, 91 | "type": "VEC2" 92 | }, 93 | { 94 | "bufferView": 3, 95 | "componentType": 5121, 96 | "count": 660, 97 | "type": "VEC4" 98 | }, 99 | { 100 | "bufferView": 4, 101 | "componentType": 5126, 102 | "count": 660, 103 | "type": "VEC4" 104 | }, 105 | { 106 | "bufferView": 5, 107 | "componentType": 5123, 108 | "count": 1140, 109 | "type": "SCALAR" 110 | }, 111 | { 112 | "bufferView": 6, 113 | "componentType": 5126, 114 | "count": 5, 115 | "type": "MAT4" 116 | } 117 | ], 118 | "bufferViews": [ 119 | { 120 | "buffer": 0, 121 | "byteLength": 7920, 122 | "byteOffset": 0 123 | }, 124 | { 125 | "buffer": 0, 126 | "byteLength": 7920, 127 | "byteOffset": 7920 128 | }, 129 | { 130 | "buffer": 0, 131 | "byteLength": 5280, 132 | "byteOffset": 15840 133 | }, 134 | { 135 | "buffer": 0, 136 | "byteLength": 2640, 137 | "byteOffset": 21120 138 | }, 139 | { 140 | "buffer": 0, 141 | "byteLength": 10560, 142 | "byteOffset": 23760 143 | }, 144 | { 145 | "buffer": 0, 146 | "byteLength": 2280, 147 | "byteOffset": 34320 148 | }, 149 | { 150 | "buffer": 0, 151 | "byteLength": 320, 152 | "byteOffset": 36600 153 | } 154 | ], 155 | "buffers": [ 156 | { 157 | "byteLength": 36920, 158 | "uri": "data:application/octet-stream;base64,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vhR7Pzze+j/ExUe+vhR7P+OQAsDExUe+vhR7P+OQAsDExUe+vhR7P+OQAsDExUe+vhR7P3JIwcDExUe+vhR7P3JIwcDExUe+vhR7P3JIwcDExUe+AACAP4+3vkAuvTszAACAP4+3vkAuvTszAACAP4+3vkAuvTszAACAP4+3vkAuvTszAACAPzze+j8uvTszAACAPzze+j8uvTszAACAPzze+j8uvTszAACAP+OQAsAuvTszAACAP+OQAsAuvTszAACAP+OQAsAuvTszAACAP3JIwcAuvTszAACAP3JIwcAuvTszAACAP3JIwcAuvTszvhR7P4+3vkDCxUc+vhR7P4+3vkDCxUc+vhR7P4+3vkDCxUc+vhR7P4+3vkDCxUc+vhR7Pzze+j/CxUc+vhR7Pzze+j/CxUc+vhR7P+OQAsDCxUc+vhR7P+OQAsDCxUc+vhR7P3JIwcDCxUc+vhR7P3JIwcDCxUc+vhR7P3JIwcDCxUc+vhR7P3JIwcDCxUc+X4NsP4+3vkAU78M+X4NsP4+3vkAU78M+X4NsP4+3vkAU78M+X4NsP4+3vkAU78M+X4NsPzze+j8U78M+X4NsPzze+j8U78M+X4NsPzze+j8U78M+X4NsP+OQAsAU78M+X4NsP+OQAsAU78M+X4NsP3JIwcAU78M+X4NsP3JIwcAU78M+X4NsP3JIwcAU78M+X4NsP3JIwcAU78M+MttUP4+3vkDZOQ4/MttUP4+3vkDZOQ4/MttUP4+3vkDZOQ4/MttUP4+3vkDZOQ4/MttUPzze+j/ZOQ4/MttUPzze+j/ZOQ4/MttUPzze+j/ZOQ4/MttUPzze+j/ZOQ4/MttUP+OQAsDZOQ4/MttUP+OQAsDZOQ4/MttUP+OQAsDZOQ4/MttUP3JIwcDZOQ4/MttUP3JIwcDZOQ4/MttUP3JIwcDZOQ4/MttUP3JIwcDZOQ4/8wQ1P4+3vkDzBDU/8wQ1P4+3vkDzBDU/8wQ1P4+3vkDzBDU/8wQ1P4+3vkDzBDU/8wQ1Pzze+j/zBDU/8wQ1Pzze+j/zBDU/8wQ1Pzze+j/zBDU/8wQ1Pzze+j/zBDU/8wQ1P+OQAsDzBDU/8wQ1P+OQAsDzBDU/8wQ1P+OQAsDzBDU/8wQ1P+OQAsDzBDU/8wQ1P3JIwcDzBDU/8wQ1P3JIwcDzBDU/8wQ1P3JIwcDzBDU/8wQ1P3JIwcDzBDU/2TkOP4+3vkAy21Q/2TkOP4+3vkAy21Q/2TkOP4+3vkAy21Q/2TkOP4+3vkAy21Q/2TkOPzze+j8y21Q/2TkOPzze+j8y21Q/2TkOPzze+j8y21Q/2TkOPzze+j8y21Q/2TkOP+OQAsAy21Q/2TkOP+OQAsAy21Q/2TkOP+OQAsAy21Q/2TkOP+OQAsAy21Q/2TkOP3JIwcAy21Q/2TkOP3JIwcAy21Q/2TkOP3JIwcAy21Q/2TkOP3JIwcAy21Q/F+/DPo+3vkBeg2w/F+/DPo+3vkBeg2w/F+/DPo+3vkBeg2w/F+/DPjze+j9eg2w/F+/DPjze+j9eg2w/F+/DPjze+j9eg2w/F+/DPuOQAsBeg2w/F+/DPuOQAsBeg2w/F+/DPuOQAsBeg2w/F+/DPuOQAsBeg2w/F+/DPnJIwcBeg2w/F+/DPnJIwcBeg2w/F+/DPnJIwcBeg2w/F+/DPnJIwcBeg2w/wcVHPo+3vkC/FHs/wcVHPo+3vkC/FHs/wcVHPo+3vkC/FHs/wcVHPjze+j+/FHs/wcVHPjze+j+/FHs/wcVHPjze+j+/FHs/wcVHPuOQAsC/FHs/wcVHPuOQAsC/FHs/wcVHPuOQAsC/FHs/wcVHPuOQAsC/FHs/wcVHPnJIwcC/FHs/wcVHPnJIwcC/FHs/wcVHPnJIwcC/FHs/wcVHPnJIwcC/FHs/Lr27s4+3vkAAAIA/Lr27s4+3vkAAAIA/Lr27s4+3vkAAAIA/Lr27s4+3vkAAAIA/Lr27szze+j8AAIA/Lr27szze+j8AAIA/Lr27szze+j8AAIA/Lr27szze+j8AAIA/Lr27s+OQAsAAAIA/Lr27s+OQAsAAAIA/Lr27s+OQAsAAAIA/Lr27s+OQAsAAAIA/Lr27s3JIwcAAAIA/Lr27s3JIwcAAAIA/Lr27s3JIwcAAAIA/Lr27s3JIwcAAAIA/vcVHvo+3vkC/FHs/vcVHvo+3vkC/FHs/vcVHvo+3vkC/FHs/vcVHvo+3vkC/FHs/vcVHvjze+j+/FHs/vcVHvjze+j+/FHs/vcVHvjze+j+/FHs/vcVHvjze+j+/FHs/vcVHvuOQAsC/FHs/vcVHvuOQAsC/FHs/vcVHvuOQAsC/FHs/vcVHvnJIwcC/FHs/vcVHvnJIwcC/FHs/vcVHvnJIwcC/FHs/vcVHvnJIwcC/FHs/Fe/Dvo+3vkBeg2w/Fe/Dvo+3vkBeg2w/Fe/Dvo+3vkBeg2w/Fe/Dvo+3vkBeg2w/Fe/Dvjze+j9eg2w/Fe/Dvjze+j9eg2w/Fe/Dvjze+j9eg2w/Fe/Dvjze+j9eg2w/Fe/DvuOQAsBeg2w/Fe/DvuOQAsBeg2w/Fe/DvuOQAsBeg2w/Fe/DvnJIwcBeg2w/Fe/DvnJIwcBeg2w/Fe/DvnJIwcBeg2w/Fe/DvnJIwcBeg2w/2zkOv4+3vkAw21Q/2zkOv4+3vkAw21Q/2zkOv4+3vkAw21Q/2zkOv4+3vkAw21Q/2zkOvzze+j8w21Q/2zkOvzze+j8w21Q/2zkOvzze+j8w21Q/2zkOvzze+j8w21Q/2zkOv+OQAsAw21Q/2zkOv+OQAsAw21Q/2zkOv+OQAsAw21Q/2zkOv+OQAsAw21Q/2zkOv3JIwcAw21Q/2zkOv3JIwcAw21Q/2zkOv3JIwcAw21Q/2zkOv3JIwcAw21Q/8gQ1v4+3vkD0BDU/8gQ1v4+3vkD0BDU/8gQ1v4+3vkD0BDU/8gQ1v4+3vkD0BDU/8gQ1vzze+j/0BDU/8gQ1vzze+j/0BDU/8gQ1vzze+j/0BDU/8gQ1vzze+j/0BDU/8gQ1v+OQAsD0BDU/8gQ1v+OQAsD0BDU/8gQ1v+OQAsD0BDU/8gQ1v+OQAsD0BDU/8gQ1v3JIwcD0BDU/8gQ1v3JIwcD0BDU/8gQ1v3JIwcD0BDU/8gQ1v3JIwcD0BDU/L9tUv4+3vkDdOQ4/L9tUv4+3vkDdOQ4/L9tUv4+3vkDdOQ4/L9tUv4+3vkDdOQ4/L9tUvzze+j/dOQ4/L9tUvzze+j/dOQ4/L9tUvzze+j/dOQ4/L9tUvzze+j/dOQ4/L9tUv+OQAsDdOQ4/L9tUv+OQAsDdOQ4/L9tUv+OQAsDdOQ4/L9tUv+OQAsDdOQ4/L9tUv3JIwcDdOQ4/L9tUv3JIwcDdOQ4/L9tUv3JIwcDdOQ4/L9tUv3JIwcDdOQ4/XoNsv4+3vkAa78M+XoNsv4+3vkAa78M+XoNsv4+3vkAa78M+XoNsv4+3vkAa78M+XoNsvzze+j8a78M+XoNsvzze+j8a78M+XoNsvzze+j8a78M+XoNsvzze+j8a78M+XoNsv+OQAsAa78M+XoNsv+OQAsAa78M+XoNsv+OQAsAa78M+XoNsv+OQAsAa78M+XoNsv3JIwcAa78M+XoNsv3JIwcAa78M+XoNsv3JIwcAa78M+XoNsv3JIwcAa78M+vhR7v4+3vkDGxUc+vhR7v4+3vkDGxUc+vhR7v4+3vkDGxUc+vhR7v4+3vkDGxUc+vhR7vzze+j/GxUc+vhR7vzze+j/GxUc+vhR7vzze+j/GxUc+vhR7vzze+j/GxUc+vhR7v+OQAsDGxUc+vhR7v+OQAsDGxUc+vhR7v+OQAsDGxUc+vhR7v+OQAsDGxUc+vhR7v3JIwcDGxUc+vhR7v3JIwcDGxUc+vhR7v3JIwcDGxUc+AACAv4+3vkAu3kyyAACAv4+3vkAu3kyyAACAv4+3vkAu3kyyAACAv4+3vkAu3kyyAACAvzze+j8u3kyyAACAvzze+j8u3kyyAACAvzze+j8u3kyyAACAvzze+j8u3kyyAACAv+OQAsAu3kyyAACAv+OQAsAu3kyyAACAv+OQAsAu3kyyAACAv+OQAsAu3kyyAACAv3JIwcAu3kyyAACAv3JIwcAu3kyyvhR7v4+3vkDIxUe+vhR7v4+3vkDIxUe+vhR7v4+3vkDIxUe+vhR7v4+3vkDIxUe+vhR7vzze+j/IxUe+vhR7vzze+j/IxUe+vhR7vzze+j/IxUe+vhR7vzze+j/IxUe+vhR7v+OQAsDIxUe+vhR7v+OQAsDIxUe+vhR7v+OQAsDIxUe+vhR7v+OQAsDIxUe+vhR7v3JIwcDIxUe+vhR7v3JIwcDIxUe+vhR7v3JIwcDIxUe+XYNsv4+3vkAb78O+XYNsv4+3vkAb78O+XYNsv4+3vkAb78O+XYNsv4+3vkAb78O+XYNsvzze+j8b78O+XYNsvzze+j8b78O+XYNsvzze+j8b78O+XYNsv+OQAsAb78O+XYNsv+OQAsAb78O+XYNsv+OQAsAb78O+XYNsv3JIwcAb78O+XYNsv3JIwcAb78O+XYNsv3JIwcAb78O+XYNsv3JIwcAb78O+M9tUv4+3vkDXOQ6/M9tUv4+3vkDXOQ6/M9tUv4+3vkDXOQ6/M9tUv4+3vkDXOQ6/M9tUvzze+j/XOQ6/M9tUvzze+j/XOQ6/M9tUvzze+j/XOQ6/M9tUv+OQAsDXOQ6/M9tUv+OQAsDXOQ6/M9tUv3JIwcDXOQ6/M9tUv3JIwcDXOQ6/M9tUv3JIwcDXOQ6/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AAAAAAAAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAQAAAAEAAAACAAAAAgAAAAIAAAACAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAACAAAAAgAAAAIAAAABAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAADAAAAAwAAAAIAAAACAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAADAAAAAwAAAAIAAAACAAAAAgAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAwAAAAMAAAADAAAAAgAAAAIAAAACAAAAAgAAAAEAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAMAAAADAAAAAwAAAAIAAAACAAAAAgAAAAIAAAABAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAADAAAAAwAAAAMAAAACAAAAAgAAAAIAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAMAAAADAAAAAwAAAAIAAAACAAAAAgAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAADAAAAAwAAAAMAAAACAAAAAgAAAAIAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAAAwAAAAMAAAADAAAAAgAAAAIAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAwAAAAMAAAADAAAAAgAAAAIAAAACAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAMAAAADAAAAAwAAAAIAAAACAAAAAgAAAAIAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAMAAAADAAAAAwAAAAIAAAACAAAAAgAAAAIAAAABAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAADAAAAAwAAAAMAAAACAAAAAgAAAAIAAAACAAAAAQAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAwAAAAMAAAACAAAAAgAAAAIAAAABAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAADAAAAAwAAAAIAAAACAAAAAgAAAAEAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAMAAAADAAAAAwAAAAIAAAACAAAAAgAAAAIAAAABAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAADAAAAAwAAAAMAAAACAAAAAgAAAAIAAAACAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAADAAAAAwAAAAMAAAACAAAAAgAAAAIAAAACAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAADAAAAAwAAAAMAAAACAAAAAgAAAAIAAAACAAAAAQAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAwAAAAMAAAADAAAAAgAAAAIAAAACAAAAAgAAAAEAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAMAAAADAAAAAwAAAAIAAAACAAAAAgAAAAIAAAABAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAADAAAAAwAAAAMAAAACAAAAAgAAAAIAAAACAAAAAQAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAwAAAAMAAAADAAAAAgAAAAIAAAACAAAAAgAAAAEAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAAAwAAAAMAAAADAAAAAgAAAAIAAAACAAAAAgAAAAEAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAADAAAAAwAAAAMAAAACAAAAAgAAAAIAAAACAAAAAQAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAADAAAAAwAAAAMAAAACAAAAAgAAAAIAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAMAAAADAAAAAwAAAAIAAAACAAAAAgAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAADAAAAAwAAAAMAAAACAAAAAgAAAAIAAAACAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAMAAAADAAAAAwAAAAIAAAACAAAAAgAAAAIAAAABAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAMAAAADAAAAAwAAAAIAAAACAAAAAgAAAAEAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAMAAAADAAAAAwAAAAIAAAACAAAAAgAAAAEAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAzwAFAAoAzwAKANEA0gALABAA0gAQAN8A4QARABYA4QAWAOsA7QAXABwA7QAcAPkA+gAdACIA+gAiAAgBCgEjACgACgEoABgBGwEpAC4AGwEuACcBKQEvADUAKQE1ADUBNAE0ADsANAE7AEIBQAE6AEEAQAFBAE4BTQFAAEcATQFHAFwBWQFGAE0AWQFNAGoBaAFMAFMAaAFTAHoBeQFSAFkAeQFZAIoBiAFYAF8AiAFfAJgBlgFeAGUAlgFlAKYBpQFjAGoApQFqALcBtQFpAHAAtQFwAMYBwwFvAHYAwwF2ANQB0gF1AHwA0gF8AOQB4gF7AIIA4gGCAPQB8wGBAIgA8wGIAAUCAgKHAI4AAgKOABQCEgKNAJQAEgKUACICIQKTAJkAIQKZAC8CMQKaAJ8AMQKfAD4CQQKgAKUAQQKlAE0CTgKmAKsATgKrAFkCXAKsALEAXAKxAGgCaQKyALcAaQK3AHYCDwAJAAQABAC/ALkAuQCzAK0ArQCnAKEAoQCbAJUAlQCPAIkAiQCDAH0AfQB3AHEAcQBrAGQAZABdAFcAVwBRAEsASwBFAD8APwA5ADMAMwAtACcAJwAhABsAGwAVAA8ADwAEALkAuQCtAKEAoQCVAIkAiQB9AHEAcQBkAFcAVwBLAD8APwAzACcAJwAbAA8ADwC5AKEAoQCJAHEAcQBXAD8APwAnAA8ADwChAHEAcQA/AA8AeAK4AL0AeAK9AIUCiAK+AAMAiAIDAM0AvAABAAYABgAMABIAEgAYAB4AHgAkACoAKgAwADYANgA8AEIAQgBIAE4ATgBUAFoAWgBhAGgAaABuAHQAdAB6AIAAgACGAIwAjACSAJgAmACeAKQApACqALAAsAC2ALwAvAAGABIAEgAeACoAKgA2AEIAQgBOAFoAWgBoAHQAdACAAIwAjACYAKQApACwALwAvAASACoAKgBCAFoAWgB0AIwAjACkALwAvAAqAFoAWgCMALwAuwCTAsEAuwDBAAAAkgKOAsQAkgLEAMAAjwKLAskAjwLJAMUAigKHAswAigLMAMgAtQCEApECtQCRAroAgwJ/AowCgwKMApACgAJ8AokCgAKJAo0CfAJ5AoYCfAKGAokCrwB0AoECrwCBArQAdQJyAn4CdQJ+AoICcQJtAnoCcQJ6An0CbgJqAncCbgJ3AnsCqQBmAnMCqQBzAq4AZgJiAm8CZgJvAnMCYwJgAmwCYwJsAnACXwJbAmcCXwJnAmsCowBXAmQCowBkAqgAWAJUAmECWAJhAmUCVAJSAl4CVAJeAmECUQJPAloCUQJaAl0CnQBLAlYCnQBWAqIASgJHAlMCSgJTAlUCRwJEAlACRwJQAlMCRAJAAkwCRAJMAlAClwA8AkgClwBIApwAPQI5AkUCPQJFAkkCOgI2AkMCOgJDAkYCNQIyAj8CNQI/AkICkAAtAjsCkAA7ApYALQIrAjgCLQI4AjsCKQIlAjMCKQIzAjcCJwIjAjACJwIwAjQCigAeAi4CigAuApEAHgIbAiwCHgIsAi4CGgIWAiYCGgImAioCFwITAiQCFwIkAigChAAOAh8ChAAfAosADwILAh0CDwIdAiACCgIGAhgCCgIYAhwCBwIDAhUCBwIVAhkCfgD/ARECfgARAoUA/gH6AQwC/gEMAhAC+wH3AQkC+wEJAg0C9gHyAQQC9gEEAggCeADuAQACeAAAAn8A7wHrAf0B7wH9AQEC6gHmAfgB6gH4AfwB5wHjAfUB5wH1AfkBcgDeAfABcgDwAXkA3wHbAe0B3wHtAfEB2gHWAegB2gHoAewB1wHTAeUB1wHlAekBbADOAeABbADgAXMAzwHMAd0BzwHdAeEBywHHAdgBywHYAdwByAHEAdUByAHVAdkBZgDAAdEBZgDRAW0AvwG8Ac0BvwHNAdABvAG5AcoBvAHKAc0BuAG0AcUBuAHFAckBYACxAcIBYADCAWcAsAGsAb0BsAG9AcEBrQGpAbsBrQG7Ab4BqAGkAbYBqAG2AboBWwCgAbIBWwCyAWIAoQGdAa8BoQGvAbMBnAGZAaoBnAGqAa4BmgGXAacBmgGnAasBVQCTAaMBVQCjAVwAkgGOAZ4BkgGeAaIBjwGLAZsBjwGbAZ8BiwGIAZgBiwGYAZsBTwCFAZUBTwCVAVYAhAGAAZABhAGQAZQBgQF9AY0BgQGNAZEBfAF4AYkBfAGJAYwBSQB0AYYBSQCGAVAAdQFwAYIBdQGCAYcBcQFtAX8BcQF/AYMBbAFpAXsBbAF7AX4BQwBkAXYBQwB2AUoAZQFiAXMBZQFzAXcBYQFdAW4BYQFuAXIBXgFaAWsBXgFrAW8BPQBWAWcBPQBnAUQAVQFTAWMBVQFjAWYBUwFQAV8BUwFfAWMBUQFMAVsBUQFbAWABNwBIAVcBNwBXAT4ASQFGAVQBSQFUAVgBRgFEAVIBRgFSAVQBRAFBAU8BRAFPAVIBMgA+AUoBMgBKATgAPwE8AUcBPwFHAUsBPAE5AUUBPAFFAUcBOQE2AUMBOQFDAUUBLAAyAT0BLAA9ATEAMgEuAToBMgE6AT0BLwEsATgBLwE4ATsBKwEoATMBKwEzATcBJgAlATEBJgAxASsAJAEhAS0BJAEtATABIQEeASoBIQEqAS0BHgEaASYBHgEmASoBIAAXASMBIAAjASUAFgESAR8BFgEfASIBEwEPAR0BEwEdASABDgELARkBDgEZARwBGgAGARQBGgAUAR8ABwEDAREBBwERARUBAgH+AAwBAgEMARAB/wD7AAkB/wAJAQ0BFAD3AAUBFAAFARkA9gDyAAAB9gAAAQQB8wDwAP0A8wD9AAEB7wDsAPgA7wD4APwADgDpAPQADgD0ABMA6gDmAPEA6gDxAPUA5gDjAO4A5gDuAPEA4wDhAOsA4wDrAO4ACADdAOcACADnAA0A3gDaAOUA3gDlAOgA2QDWAOIA2QDiAOQA1gDTAOAA1gDgAOIAAgDDANwAAgDcAAcAwgDGANcAwgDXANsAxwDLANUAxwDVANgAygDOANAAygDQANQAAACAPwAAAIAAAAAAAAAAgAAAAIAAAIA/AAAAgAAAAAAAAAAAAAAAgAAAgD8AAACAAAAAgAAAIEEAAACAAACAPwAAgD8AAACAAAAAAAAAAIAAAACAAACAPwAAAIAAAAAAAAAAAAAAAIAAAIA/AAAAgAAAAIAAAMBAAAAAgAAAgD8AAIA/AAAAgAAAAAAAAACAAAAAgAAAgD8AAACAAAAAAAAAAAAAAACAAACAPwAAAIAAAACAAQAAQAAAAIAAAIA/AACAPwAAAIAAAAAAAAAAgAAAAIAAAIA/AAAAgAAAAAAAAAAAAAAAgAAAgD8AAACAAAAAgP7//78AAACAAACAPwAAgD8AAACAAAAAAAAAAIAAAACAAACAPwAAAIAAAAAAAAAAAAAAAIAAAIA/AAAAgAAAAIAAAMDAAAAAgAAAgD8=" 159 | } 160 | ] 161 | } 162 | -------------------------------------------------------------------------------- /example/src/demos/two-dimensional/ConstrainedGlobalRotation2D.tsx: -------------------------------------------------------------------------------- 1 | import { Canvas } from '@react-three/fiber' 2 | import { MathUtils, Solve2D, V2 } from 'inverse-kinematics' 3 | import { useControls } from 'leva' 4 | import React, { useEffect, useState } from 'react' 5 | import { useAnimationFrame } from '../../hooks/useAnimationFrame' 6 | import { Base } from './components/Base' 7 | import { JointTransforms } from './components/JointTransforms' 8 | import { Logger } from './components/Logger' 9 | import { Target } from './components/Target' 10 | 11 | const base: Solve2D.JointTransform = { position: [0, 0], rotation: 0 } 12 | 13 | export default function ConstrainedGlobalRotation2D() { 14 | const [target, setTarget] = useState([500, 50] as V2) 15 | const [links, setLinks] = useState([]) 16 | 17 | const { linkCount, linkLength, endEffectorRotation } = useControls({ 18 | linkCount: { value: 4, min: 0, max: 50, step: 1 }, 19 | linkLength: { value: 200, min: 1, max: 200, step: 10 }, 20 | endEffectorRotation: { value: 0, min: -180, max: 180, step: 15 }, 21 | }) 22 | 23 | useEffect(() => { 24 | setLinks(makeLinks(linkCount, linkLength, endEffectorRotation)) 25 | }, [linkCount, linkLength, endEffectorRotation]) 26 | 27 | useAnimationFrame(60, () => { 28 | const knownRangeOfMovement = linkCount * linkLength 29 | 30 | function learningRate(errorDistance: number): number { 31 | const relativeDistanceToTarget = MathUtils.clamp(errorDistance / knownRangeOfMovement, 0, 1) 32 | const cutoff = 0.1 33 | 34 | if (relativeDistanceToTarget > cutoff) { 35 | return 10e-6 36 | } 37 | 38 | // result is between 0 and 1 39 | const remainingDistance = relativeDistanceToTarget / 0.02 40 | const minimumLearningRate = 10e-7 41 | 42 | return minimumLearningRate + remainingDistance * 10e-7 43 | } 44 | 45 | const result = Solve2D.solve(links, base, target, { 46 | method: 'FABRIK', 47 | learningRate, 48 | acceptedError: 10, 49 | }).links 50 | 51 | links.forEach((_, index) => { 52 | links[index] = result[index]! 53 | }) 54 | }) 55 | 56 | return ( 57 |
59 | setTarget([clientX - window.innerWidth / 2, -clientY + window.innerHeight / 2]) 60 | } 61 | > 62 | 73 | 74 | 75 | 76 | 77 | 78 |
79 | ) 80 | } 81 | 82 | const makeLinks = (linkCount: number, linkLength: number, endEffectorRotation: number): Solve2D.Link[] => 83 | Array.from({ length: linkCount }).map((_, index) => { 84 | if (index === linkCount - 1) { 85 | return { 86 | position: [linkLength, 0], 87 | constraints: { value: (endEffectorRotation * Math.PI) / 180, type: 'global' }, 88 | rotation: 0, 89 | } 90 | } 91 | return Solve2D.buildLink([linkLength, 0]) 92 | }) 93 | -------------------------------------------------------------------------------- /example/src/demos/two-dimensional/ConstrainedLocalRotation2D.tsx: -------------------------------------------------------------------------------- 1 | import { Canvas } from '@react-three/fiber' 2 | import { MathUtils, Solve2D, V2 } from 'inverse-kinematics' 3 | import { useControls } from 'leva' 4 | import React, { useEffect, useState } from 'react' 5 | import { useAnimationFrame } from '../../hooks/useAnimationFrame' 6 | import { Base } from './components/Base' 7 | import { JointTransforms } from './components/JointTransforms' 8 | import { Logger } from './components/Logger' 9 | import { Target } from './components/Target' 10 | 11 | const base: Solve2D.JointTransform = { position: [0, 0], rotation: 0 } 12 | 13 | export default function ConstrainedLocalRotation2D() { 14 | const [target, setTarget] = useState([500, 50] as V2) 15 | const [links, setLinks] = useState([]) 16 | 17 | const { linkCount, linkLength, endEffectorRotation } = useControls({ 18 | linkCount: { value: 4, min: 0, max: 50, step: 1 }, 19 | linkLength: { value: 200, min: 1, max: 200, step: 10 }, 20 | endEffectorRotation: { value: 0, min: -180, max: 180, step: 5 }, 21 | }) 22 | 23 | useEffect(() => { 24 | setLinks(makeLinks(linkCount, linkLength, endEffectorRotation)) 25 | }, [linkCount, linkLength, endEffectorRotation]) 26 | 27 | useAnimationFrame(60, () => { 28 | const knownRangeOfMovement = linkCount * linkLength 29 | 30 | function learningRate(errorDistance: number): number { 31 | const relativeDistanceToTarget = MathUtils.clamp(errorDistance / knownRangeOfMovement, 0, 1) 32 | const cutoff = 0.1 33 | 34 | if (relativeDistanceToTarget > cutoff) { 35 | return 10e-6 36 | } 37 | 38 | // result is between 0 and 1 39 | const remainingDistance = relativeDistanceToTarget / 0.02 40 | const minimumLearningRate = 10e-7 41 | 42 | return minimumLearningRate + remainingDistance * 10e-7 43 | } 44 | 45 | const result = Solve2D.solve(links, base, target, { 46 | method: 'FABRIK', 47 | learningRate, 48 | acceptedError: 10, 49 | }).links 50 | 51 | links.forEach((_, index) => { 52 | links[index] = result[index]! 53 | }) 54 | }) 55 | 56 | return ( 57 |
59 | setTarget([clientX - window.innerWidth / 2, -clientY + window.innerHeight / 2]) 60 | } 61 | > 62 | 73 | 74 | 75 | 76 | 77 | 78 |
79 | ) 80 | } 81 | 82 | const makeLinks = (linkCount: number, linkLength: number, endEffectorRotation: number): Solve2D.Link[] => 83 | Array.from({ length: linkCount }).map((_, index) => { 84 | if (index === linkCount - 1) { 85 | return { 86 | position: [linkLength, 0], 87 | constraints: { value: (endEffectorRotation * Math.PI) / 180, type: 'local' }, 88 | rotation: 0, 89 | } 90 | } 91 | return { 92 | position: [linkLength, 0], 93 | rotation: 0, 94 | } 95 | }) 96 | -------------------------------------------------------------------------------- /example/src/demos/two-dimensional/TwoDimension.tsx: -------------------------------------------------------------------------------- 1 | import { Canvas } from '@react-three/fiber' 2 | import { MathUtils, Solve2D, V2 } from 'inverse-kinematics' 3 | import React, { useEffect, useRef, useState } from 'react' 4 | import { useAnimationFrame } from '../../hooks/useAnimationFrame' 5 | import { Base } from './components/Base' 6 | import { JointTransforms } from './components/JointTransforms' 7 | import { Logger } from './components/Logger' 8 | import { Target } from './components/Target' 9 | import { useControls } from 'leva' 10 | 11 | const base: Solve2D.JointTransform = { position: [0, 0], rotation: 0 } 12 | 13 | export default function TwoDimension({ method }: { method: 'CCD' | 'FABRIK' }) { 14 | const [target, setTarget] = useState([500, 50] as V2) 15 | const [links, setLinks] = useState([]) 16 | 17 | const { linkCount, linkLength, linkMinAngle, linkMaxAngle } = useControls({ 18 | linkCount: { value: 1, min: 0, max: 50, step: 1 }, 19 | linkLength: { value: 200, min: 1, max: 200, step: 10 }, 20 | linkMinAngle: { value: -90, min: -360, max: 0, step: 10 }, 21 | linkMaxAngle: { value: 90, min: 0, max: 360, step: 10 }, 22 | }) 23 | 24 | useEffect(() => { 25 | setLinks(makeLinks(linkCount, linkLength, linkMinAngle, linkMaxAngle)) 26 | }, [linkCount, linkLength, linkMinAngle, linkMaxAngle]) 27 | 28 | useAnimationFrame(60, () => { 29 | const knownRangeOfMovement = linkCount * linkLength 30 | 31 | function learningRate(errorDistance: number): number { 32 | const relativeDistanceToTarget = MathUtils.clamp(errorDistance / knownRangeOfMovement, 0, 1) 33 | const cutoff = 0.1 34 | 35 | if (relativeDistanceToTarget > cutoff) { 36 | return 10e-6 37 | } 38 | 39 | // result is between 0 and 1 40 | const remainingDistance = relativeDistanceToTarget / 0.02 41 | const minimumLearningRate = 10e-7 42 | 43 | return minimumLearningRate + remainingDistance * 10e-7 44 | } 45 | 46 | const result = Solve2D.solve(links, base, target, { 47 | learningRate: method === 'FABRIK' ? learningRate : 1, 48 | acceptedError: 10, 49 | method, 50 | }).links 51 | 52 | links.forEach((_, index) => { 53 | links[index] = result[index]! 54 | }) 55 | }) 56 | 57 | return ( 58 |
60 | setTarget([clientX - window.innerWidth / 2, -clientY + window.innerHeight / 2]) 61 | } 62 | > 63 | 74 | 75 | 76 | 77 | 78 | 79 |
80 | ) 81 | } 82 | 83 | const makeLinks = (linkCount: number, linkLength: number, linkMinAngle: number, linkMaxAngle: number): Solve2D.Link[] => 84 | Array.from({ length: linkCount }).map(() => ({ 85 | position: [linkLength, 0], 86 | constraints: { min: (linkMinAngle * Math.PI) / 180, max: (linkMaxAngle * Math.PI) / 180 }, 87 | rotation: 0, 88 | })) 89 | -------------------------------------------------------------------------------- /example/src/demos/two-dimensional/components/Base.tsx: -------------------------------------------------------------------------------- 1 | import { useFrame } from '@react-three/fiber' 2 | import { Solve2D, V2 } from 'inverse-kinematics' 3 | import React, { useMemo, useRef } from 'react' 4 | import { BoxBufferGeometry, Mesh, MeshNormalMaterial } from 'three' 5 | import { Link, LinkProps } from './Link' 6 | 7 | export const Base = ({ base: base, links }: { links: Solve2D.Link[]; base: Solve2D.JointTransform }) => { 8 | const ref = useRef>() 9 | const chain = useMemo(() => makeChain(links), [links]) 10 | 11 | useFrame(() => { 12 | if (!ref.current) return 13 | ref.current.position.set(...base.position, 0) 14 | ref.current.rotation.set(0, 0, base.rotation) 15 | 16 | let depth = 0 17 | let child = chain 18 | 19 | while (child !== undefined && links[depth] !== undefined) { 20 | child.link.rotation = links[depth]!.rotation ?? 0 21 | depth++ 22 | child = child.child 23 | } 24 | }) 25 | 26 | return ( 27 | 28 | 29 | 30 | {chain && } 31 | 32 | ) 33 | } 34 | 35 | function makeChain(links: Solve2D.Link[]): LinkProps | undefined { 36 | let chain: LinkProps | undefined 37 | for (let index = links.length - 1; index >= 0; index--) { 38 | const link: LinkProps = { link: { ...links[index]!, rotation: links[index]!.rotation ?? 0 } } 39 | 40 | // Is first element 41 | if (chain === undefined) { 42 | chain = link 43 | continue 44 | } 45 | 46 | chain = { link: link.link, child: chain } 47 | } 48 | 49 | return chain 50 | } 51 | -------------------------------------------------------------------------------- /example/src/demos/two-dimensional/components/JointTransforms.tsx: -------------------------------------------------------------------------------- 1 | import { useFrame } from '@react-three/fiber' 2 | import { Solve2D } from 'inverse-kinematics' 3 | import React, { useMemo, useRef } from 'react' 4 | import { Group } from 'three' 5 | 6 | export const JointTransforms = ({ links, base }: { links: Solve2D.Link[]; base: Solve2D.JointTransform }) => { 7 | const ref = useRef() 8 | 9 | useFrame(() => { 10 | if (ref.current === undefined) return 11 | 12 | const { transforms } = Solve2D.getJointTransforms(links, base) 13 | for (let index = 0; index < ref.current.children.length; index++) { 14 | const child = ref.current.children[index]! 15 | const jointPosition = transforms[index]?.position 16 | if (jointPosition === undefined) { 17 | throw new Error(`No corresponding child position for index ${index}`) 18 | } 19 | child.position.set(...jointPosition, 100) 20 | } 21 | }) 22 | 23 | const jointPositions = useMemo( 24 | () => 25 | Array.from({ length: links.length + 1 }).map((_, index) => { 26 | return ( 27 | 28 | 29 | 30 | 31 | ) 32 | }), 33 | [links], 34 | ) 35 | return {jointPositions} 36 | } 37 | -------------------------------------------------------------------------------- /example/src/demos/two-dimensional/components/Link.tsx: -------------------------------------------------------------------------------- 1 | import { useFrame } from '@react-three/fiber' 2 | import React, { useMemo, useRef } from 'react' 3 | import { 4 | BoxBufferGeometry, 5 | BufferGeometry, 6 | Color, 7 | Group, 8 | Line, 9 | LineBasicMaterial, 10 | Mesh, 11 | MeshNormalMaterial, 12 | Vector3, 13 | } from 'three' 14 | import { V2 } from 'inverse-kinematics' 15 | 16 | export interface LinkProps { 17 | link: { rotation: number; position: V2 } 18 | child?: LinkProps 19 | } 20 | 21 | export const Link = ({ link, child }: LinkProps) => { 22 | const rotationRef = useRef() 23 | const translationRef = useRef>() 24 | 25 | useFrame(() => { 26 | if (!rotationRef.current) return 27 | if (!translationRef.current) return 28 | rotationRef.current.rotation.set(0, 0, link.rotation) 29 | translationRef.current.position.set(...link.position, 0) 30 | }) 31 | 32 | const line: Line = useMemo(() => { 33 | const points = [new Vector3(), new Vector3(...link.position, 0)] 34 | const geometry = new BufferGeometry().setFromPoints(points) 35 | const material = new LineBasicMaterial({ color: new Color('#8B008B') }) 36 | 37 | return new Line(geometry, material) 38 | }, [link]) 39 | 40 | return ( 41 | 42 | 43 | 44 | 45 | {child && } 46 | 47 | 48 | 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /example/src/demos/two-dimensional/components/Logger.tsx: -------------------------------------------------------------------------------- 1 | import { Solve2D, V2 } from 'inverse-kinematics' 2 | import React, { useRef } from 'react' 3 | import { useAnimationFrame } from '../../../hooks/useAnimationFrame' 4 | 5 | export const Logger = ({ 6 | target, 7 | links, 8 | base: base, 9 | }: { 10 | target: V2 11 | links: Solve2D.Link[] 12 | base: Solve2D.JointTransform 13 | }) => { 14 | const distanceRef = useRef(null) 15 | 16 | useAnimationFrame(1, () => { 17 | if (!distanceRef.current) return 18 | distanceRef.current.innerText = Solve2D.getErrorDistance(links, base, target).toFixed(3) 19 | }) 20 | 21 | return ( 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
Distance
31 |
32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /example/src/demos/two-dimensional/components/Target.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { V2 } from 'inverse-kinematics' 3 | 4 | export const Target = ({ position }: { position: V2 }) => { 5 | return ( 6 | 7 | 8 | 9 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /example/src/hooks/useAnimationFrame.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react' 2 | 3 | export const useAnimationFrame = (frameRate: number, callback: (deltaTime: number) => void | Promise) => { 4 | const requestRef = useRef() 5 | const previousTimeRef = useRef() 6 | 7 | const animate = useCallback( 8 | async (time: number) => { 9 | if (previousTimeRef.current === undefined) { 10 | previousTimeRef.current = time 11 | } 12 | const deltaTime = time - previousTimeRef.current 13 | if (deltaTime > 1000 / frameRate) { 14 | await callback(deltaTime) 15 | previousTimeRef.current = time 16 | } 17 | requestRef.current = requestAnimationFrame(animate) 18 | }, 19 | [callback, frameRate], 20 | ) 21 | 22 | useEffect(() => { 23 | requestRef.current = requestAnimationFrame(animate) 24 | return () => { 25 | requestRef.current && cancelAnimationFrame(requestRef.current) 26 | } 27 | }, [animate]) // Make sure the effect runs only once 28 | } 29 | -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | body, 2 | html { 3 | width: 100%; 4 | height: 100%; 5 | border: 0; 6 | margin: 0; 7 | pad: 0; 8 | font-family: Arial, Helvetica, sans-serif; 9 | } 10 | 11 | .highlighted { 12 | font-weight: bold; 13 | } 14 | -------------------------------------------------------------------------------- /example/src/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.css' 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import App from './App' 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root'), 11 | ) 12 | -------------------------------------------------------------------------------- /example/src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.fbx' { 2 | const src: string 3 | export default src 4 | } 5 | 6 | declare module '*.gltf' { 7 | const src: string 8 | export default src 9 | } 10 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "types": ["vite/client"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noUncheckedIndexedAccess": true, 13 | "module": "ESNext", 14 | "moduleResolution": "Node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react" 19 | }, 20 | "include": ["./src"] 21 | } 22 | -------------------------------------------------------------------------------- /example/vite.config.ts: -------------------------------------------------------------------------------- 1 | import reactRefresh from '@vitejs/plugin-react-refresh' 2 | import { defineConfig } from 'vite' 3 | 4 | export default defineConfig({ 5 | base: 'https://tmf-code.github.io/inverse-kinematics/', 6 | plugins: [reactRefresh()], 7 | server: { 8 | host: true, 9 | https: true, 10 | fs: { 11 | allow: ['..'], 12 | }, 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | testPathIgnorePatterns: ['/node_modules/', './example/', './dist/'], 5 | watchPathIgnorePatterns: ['/node_modules/', './example/', './dist/'], 6 | setupFilesAfterEnv: ['./tests/setupTests.ts'], 7 | globals: { 8 | 'ts-jest': { 9 | tsconfig: './tests/tsconfig.json', 10 | }, 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inverse-kinematics", 3 | "version": "0.1.3", 4 | "author": "Jae Perris & Alexandra Barancova", 5 | "description": "Inverse kinematics for 2D and 3D applications", 6 | "main": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "keywords": [ 9 | "2D", 10 | "inverse kinematics", 11 | "forward kinematics", 12 | "ik", 13 | "3D" 14 | ], 15 | "files": [ 16 | "dist" 17 | ], 18 | "scripts": { 19 | "test": "jest", 20 | "test:watch": "jest --watch", 21 | "build": "tsc", 22 | "typecheck": "tsc --noEmit && cd example && tsc --noEmit", 23 | "prepublish": "npm run typecheck && npm run build && npm test && npm run eslint", 24 | "eslint": "eslint --fix src/**/*.ts" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/tmf-code/inverse-kinematics.git" 29 | }, 30 | "license": "MIT", 31 | "homepage": "https://github.com/tmf-code/inverse-kinematics#readme", 32 | "devDependencies": { 33 | "@types/jest": "^26.0.24", 34 | "@typescript-eslint/eslint-plugin": "^4.28.2", 35 | "@typescript-eslint/parser": "^4.28.2", 36 | "eslint": "^7.30.0", 37 | "eslint-config-prettier": "^8.3.0", 38 | "eslint-plugin-prettier": "^3.4.0", 39 | "prettier": "^2.3.2", 40 | "ts-jest": "^27.0.3", 41 | "typescript": "^4.3.5" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Inverse Kinematics 2 | 3 | [![Version](https://img.shields.io/npm/v/inverse-kinematics)](https://npmjs.com/package/inverse-kinematics) 4 | [![Downloads](https://img.shields.io/npm/dt/inverse-kinematics.svg)](https://npmjs.com/package/inverse-kinematics) 5 | 6 | A typescript/javascript library for calculating inverse kinematics. Supports 2D and 3D applications. 7 | 8 | ## Install 9 | 10 | ```bash 11 | npm install inverse-kinematics 12 | ``` 13 | 14 | ## Quickstart 2D 15 | 16 | https://codesandbox.io/s/quickstart-2d-ob7yw?file=/src/index.ts 17 | 18 | ```ts 19 | import { V2, Solve2D } from 'inverse-kinematics' 20 | 21 | // Create a list of 'links' 22 | // Three links, of 50 units long, all pointing in the same direction 23 | let links: Solve2D.Link[] = [ 24 | { position: [50, 0], rotation: 0 }, 25 | { position: [50, 0], rotation: 0 }, 26 | { position: [50, 0], rotation: 0 }, 27 | ] 28 | 29 | // Define the base of the links 30 | const base: Solve2D.JointTransform = { position: [0, 0], rotation: 0 } 31 | 32 | // Define a target for the 'end effector' or the tip of the last link to move to 33 | const target: V2 = [50, 50] 34 | 35 | // Iterate until the error is within acceptable range 36 | const acceptedError = 10 37 | function loop() { 38 | const result = Solve2D.solve(links, base, target) 39 | const error = result.getErrorDistance() 40 | links = result.links 41 | if (error < acceptedError) return 42 | setTimeout(loop, 100) 43 | console.log(error.toFixed(0)) 44 | } 45 | loop() 46 | ``` 47 | 48 | ## Quickstart 3D 49 | 50 | https://codesandbox.io/s/quickstart-3d-25xy6?file=/src/index.ts 51 | 52 | ```ts 53 | import { V3, Solve3D, QuaternionO } from 'inverse-kinematics' 54 | 55 | // Create a list of 'links' 56 | // Three links, of 50 units long, all pointing in the same direction 57 | let links: Solve3D.Link[] = [ 58 | { position: [50, 0, 0], rotation: QuaternionO.zeroRotation() }, 59 | { position: [50, 0, 0], rotation: QuaternionO.zeroRotation() }, 60 | { position: [50, 0, 0], rotation: QuaternionO.zeroRotation() }, 61 | ] 62 | 63 | // Define the base of the links 64 | const base: Solve3D.JointTransform = { 65 | position: [0, 0, 0], 66 | rotation: QuaternionO.zeroRotation(), 67 | } 68 | 69 | // Define a target for the 'end effector' or the tip of the last link to move to 70 | const target: V3 = [50, 50, 50] 71 | 72 | // Iterate until the error is within acceptable range 73 | const acceptedError = 10 74 | function loop() { 75 | const result = Solve3D.solve(links, base, target) 76 | const error = result.getErrorDistance() 77 | links = result.links 78 | if (error < acceptedError) return 79 | setTimeout(loop, 100) 80 | console.log(error.toFixed(0)) 81 | } 82 | loop() 83 | ``` 84 | 85 | ## Examples 86 | 87 | Check out https://tmf-code.github.io/inverse-kinematics or find them in the folder /example 88 | 89 | ## Terminology 90 | 91 | ### Base 92 | 93 | The starting point of the link chain 94 | 95 | ### Link 96 | 97 | A `Link` can be thought of as a connecting bar, that extends from it's joint, to the joint of the next link in the chain. 98 | 99 | ### Joint 100 | 101 | Occurs at the tip of the preceding link, and at the base of the following link. We've chosen to consider ownership of the joint to the following link. So that itself can be considered a `Base` to the remaining links. 102 | 103 | ### Visulization of terminology 104 | 105 | You could visualize a link chain like so: 106 | 107 | ``` 108 | Base 109 | -> rotate(link_1.rotation) [joint_1] -> translate(link_1.position) 110 | -> rotate(link_2.rotation) [joint_2] -> translate(link_2.position) 111 | ``` 112 | 113 | ## Constraints 114 | 115 | There are a number of ways in which you can limit the movement of a joint, from the default ball and socket configuration. For both 3d and 2d you can supply either: 116 | 117 | - A single value per axis, this specifies half of the rotational range either direction from the direction vector of the previous link 118 | - A range with values `min` and `max`. 119 | - An exact rotation in the local coordinate system 120 | - An exact rotation in the bases coordinate system 121 | 122 | ### 2D 123 | 124 | ```ts 125 | interface Link { 126 | /** 127 | * The rotation at the base of the link 128 | */ 129 | rotation: number 130 | 131 | /** 132 | * undefined: No constraint 133 | * 134 | * Range: minimum angle, maximum angle (radians), positive is anticlockwise from previous Link's direction vector 135 | * 136 | * ExactRotation: Either a global, or local rotation which the Link is locked to 137 | */ 138 | constraints?: Constraints 139 | position: V2 140 | } 141 | 142 | type Constraints = number | Range | ExactRotation 143 | 144 | interface ExactRotation { 145 | value: number 146 | /** 147 | * 'local': Relative to previous links direction vector 148 | * 149 | * 'global': Relative to the baseJoints world transform 150 | */ 151 | type: 'global' | 'local' 152 | } 153 | ``` 154 | 155 | ### 3D 156 | 157 | ```ts 158 | interface Link { 159 | /** 160 | * The rotation at the base of the link 161 | */ 162 | rotation: Quaternion 163 | 164 | /** 165 | * undefined: No constraint 166 | * 167 | * {pitch, yaw, roll}: Range | Number 168 | * 169 | * Range: minimum angle, maximum angle (radians), positive is anticlockwise from previous Link's direction vector 170 | * 171 | * number: the range of rotation (radian) about the previous links direction vector. A rotation of 90 deg would be 45 deg either direction 172 | * 173 | * ExactRotation: Either a global, or local rotation which the Link is locked to 174 | */ 175 | constraints?: Constraints 176 | position: V3 177 | } 178 | 179 | type Constraints = EulerConstraint | ExactRotation 180 | 181 | interface EulerConstraint { 182 | /** 183 | * Rotation about X 184 | */ 185 | pitch?: number | Range 186 | /** 187 | * Rotation about Y 188 | */ 189 | yaw?: number | Range 190 | /** 191 | * Rotation about Z 192 | */ 193 | roll?: number | Range 194 | } 195 | 196 | interface ExactRotation { 197 | value: Quaternion 198 | type: 'global' | 'local' 199 | } 200 | ``` 201 | 202 | ## Tuning & Algorithm 203 | 204 | Currently this package supports gradient descent. Soon it will also support a CCD approach. 205 | 206 | The algorithm is quite simple. You can find it in `src/Solve2D.ts`, or `src/Solve3D.ts`. Available parameters to tune with are: 207 | 208 | ```ts 209 | interface SolveOptions { 210 | /** 211 | * Angle gap taken to calculate the gradient of the error function 212 | * Usually the default here will do. 213 | * @default 0.00001 214 | */ 215 | deltaAngle?: number 216 | /** 217 | * Sets the 'speed' at which the algorithm converges on the target. 218 | * Larger values will cause oscillations, or vibrations about the target 219 | * Lower values may move too slowly. You should tune this manually 220 | * 221 | * Can either be a constant, or a function that returns a learning rate 222 | * @default 0.0001 223 | */ 224 | learningRate?: number | ((errorDistance: number) => number) 225 | /** 226 | * Useful if there is oscillations or vibration around the target 227 | * @default 0 228 | */ 229 | acceptedError?: number 230 | } 231 | ``` 232 | 233 | For good results manually tune the accepted error and the learning rate. 234 | 235 | The learning rate can either be a constant or a function. An example learning rate function could be 236 | 237 | ```ts 238 | const knownRangeOfMovement = 200 239 | function learningRate(errorDistance: number): number { 240 | const relativeDistanceToTarget = clamp(errorDistance / knownRangeOfMovement, 0, 1) 241 | const cutoff = 0.02 242 | 243 | if (relativeDistanceToTarget > cutoff) { 244 | return 10e-4 245 | } 246 | 247 | // result is between 0 and 1 248 | const remainingDistance = relativeDistanceToTarget / 0.02 249 | const minimumLearningRate = 10e-5 250 | 251 | return minimumLearningRate + remainingDistance * minimumLearningRate 252 | } 253 | ``` 254 | -------------------------------------------------------------------------------- /src/Range.ts: -------------------------------------------------------------------------------- 1 | export interface Range { 2 | min: number 3 | max: number 4 | } 5 | -------------------------------------------------------------------------------- /src/Solve2D.ts: -------------------------------------------------------------------------------- 1 | import { V2, V2O } from '.' 2 | import { clamp } from './math/MathUtils' 3 | import { Range } from './Range' 4 | import { 5 | defaultCCDOptions, 6 | defaultFABRIKOptions, 7 | SolveCCDOptions, 8 | SolveFABRIKOptions, 9 | SolveOptions, 10 | } from './SolveOptions' 11 | 12 | export interface Link { 13 | /** 14 | * The rotation at the base of the link 15 | */ 16 | rotation: number 17 | 18 | /** 19 | * undefined: No constraint 20 | * 21 | * Range: minimum angle, maximum angle (radians), positive is anticlockwise from previous Link's direction vector 22 | * 23 | * ExactRotation: Either a global, or local rotation which the Link is locked to 24 | */ 25 | constraints?: Constraints 26 | position: V2 27 | } 28 | 29 | type Constraints = number | Range | ExactRotation 30 | 31 | interface ExactRotation { 32 | value: number 33 | /** 34 | * 'local': Relative to previous links direction vector 35 | * 36 | * 'global': Relative to the baseJoints world transform 37 | */ 38 | type: 'global' | 'local' 39 | } 40 | 41 | export interface SolveResult { 42 | /** 43 | * Copy of the structure of input links 44 | * With the possibility of their rotation being changed 45 | */ 46 | links: Link[] 47 | /** 48 | * Returns the error distance after the solve step 49 | */ 50 | getErrorDistance: () => number 51 | /** 52 | * true if the solve terminates early due to the end effector being close to the target. 53 | * undefined if solve has adjusted the rotations in links 54 | * 55 | * undefined is used here as we don't rerun error checking after the angle adjustment, thus it cannot be known true or false. 56 | * This is done to improve performance 57 | */ 58 | isWithinAcceptedError: true | undefined 59 | } 60 | 61 | /** 62 | * Changes joint angle to minimize distance of end effector to target 63 | * 64 | * If given no options, runs in FABRIK mode 65 | */ 66 | export function solve( 67 | links: Link[], 68 | baseJoint: JointTransform, 69 | target: V2, 70 | options: SolveOptions = { method: 'FABRIK' }, 71 | ): SolveResult { 72 | switch (options.method) { 73 | case 'FABRIK': 74 | return solveFABRIK(links, baseJoint, target, { 75 | method: 'FABRIK', 76 | acceptedError: options.acceptedError ?? defaultFABRIKOptions.acceptedError, 77 | deltaAngle: options.deltaAngle ?? defaultFABRIKOptions.deltaAngle, 78 | learningRate: options.learningRate ?? defaultFABRIKOptions.learningRate, 79 | }) 80 | 81 | case 'CCD': 82 | return solveCCD(links, baseJoint, target, { 83 | method: 'CCD', 84 | acceptedError: options.acceptedError ?? defaultCCDOptions.acceptedError, 85 | learningRate: options.learningRate ?? defaultCCDOptions.learningRate, 86 | }) 87 | } 88 | } 89 | 90 | function solveFABRIK( 91 | links: Link[], 92 | baseJoint: JointTransform, 93 | target: V2, 94 | { learningRate, deltaAngle, acceptedError }: Required, 95 | ): SolveResult { 96 | const { transforms: joints, effectorPosition } = getJointTransforms(links, baseJoint) 97 | const error = V2O.euclideanDistance(target, effectorPosition) 98 | if (error < acceptedError) 99 | return { links: links.map(copyLink), isWithinAcceptedError: true, getErrorDistance: () => error } 100 | 101 | if (joints.length !== links.length + 1) { 102 | throw new Error( 103 | `Joint transforms should have the same length as links + 1. Got ${joints.length}, expected ${links.length}`, 104 | ) 105 | } 106 | 107 | const withAngleStep = links.map(({ rotation = 0, position, constraints }, index) => { 108 | const linkWithAngleDelta = { 109 | position, 110 | rotation: rotation + deltaAngle, 111 | } 112 | 113 | // Get remaining links from this links joint 114 | const projectedLinks: Link[] = [linkWithAngleDelta, ...links.slice(index + 1)] 115 | 116 | // Get gradient from small change in joint angle 117 | const joint = joints[index]! 118 | const projectedError = getErrorDistance(projectedLinks, joint, target) 119 | const gradient = (projectedError - error) / deltaAngle 120 | 121 | // Get resultant angle step which minimizes error 122 | const angleStep = -gradient * (typeof learningRate === 'function' ? learningRate(projectedError) : learningRate) 123 | 124 | return { rotation: rotation + angleStep, position, constraints } 125 | }) 126 | 127 | const adjustedJoints = getJointTransforms(withAngleStep, baseJoint).transforms 128 | const withConstraints = applyConstraints(withAngleStep, adjustedJoints) 129 | 130 | return { 131 | links: withConstraints, 132 | getErrorDistance: () => getErrorDistance(withConstraints, baseJoint, target), 133 | isWithinAcceptedError: undefined, 134 | } 135 | } 136 | 137 | function solveCCD( 138 | links: Link[], 139 | baseJoint: JointTransform, 140 | target: V2, 141 | { acceptedError, learningRate }: Required, 142 | ): SolveResult { 143 | // 1. From base to tip, point projection from joint to effector at target 144 | let adjustedLinks: Link[] = [...links.map(copyLink)] 145 | 146 | for (let index = adjustedLinks.length - 1; index >= 0; index--) { 147 | const joints = getJointTransforms(adjustedLinks, baseJoint) 148 | const effectorPosition = joints.effectorPosition 149 | const error = V2O.euclideanDistance(target, effectorPosition) 150 | 151 | if (error < acceptedError) break 152 | 153 | const link = adjustedLinks[index]! 154 | const { rotation, position, constraints } = link 155 | const joint = joints.transforms[index]! 156 | 157 | const directionToTarget = V2O.angle(V2O.subtract(target, joint.position)) 158 | const directionToEffector = V2O.angle(V2O.subtract(effectorPosition, joint.position)) 159 | const angleBetween = directionToEffector - directionToTarget 160 | 161 | const angleStep = -angleBetween * (typeof learningRate === 'function' ? learningRate(error) : learningRate) 162 | const withAngleStep = { rotation: rotation + angleStep, position, constraints } 163 | adjustedLinks[index] = withAngleStep 164 | 165 | const adjustedJoints = getJointTransforms(adjustedLinks, baseJoint) 166 | const withConstraint = applyConstraint(withAngleStep, adjustedJoints.transforms[index + 1]!) 167 | adjustedLinks[index] = withConstraint 168 | } 169 | 170 | const adjustedJoints = getJointTransforms(adjustedLinks, baseJoint).transforms 171 | const withConstraints = applyConstraints(adjustedLinks, adjustedJoints) 172 | 173 | return { 174 | links: withConstraints, 175 | getErrorDistance: () => getErrorDistance(withConstraints, baseJoint, target), 176 | isWithinAcceptedError: undefined, 177 | } 178 | } 179 | function applyConstraint({ rotation, position, constraints }: Link, joint: JointTransform): Link { 180 | if (constraints === undefined) return { position, rotation } 181 | 182 | if (typeof constraints === 'number') { 183 | const halfConstraint = constraints / 2 184 | const clampedRotation = clamp(rotation, -halfConstraint, halfConstraint) 185 | return { position, rotation: clampedRotation, constraints: constraints } 186 | } 187 | 188 | if (isExactRotation(constraints)) { 189 | if (constraints.type === 'global') { 190 | const targetRotation = constraints.value 191 | const currentRotation = joint.rotation 192 | const deltaRotation = targetRotation - currentRotation 193 | 194 | return { position, rotation: rotation + deltaRotation, constraints: constraints } 195 | } else { 196 | return { position, rotation: constraints.value, constraints: constraints } 197 | } 198 | } else { 199 | const clampedRotation = clamp(rotation, constraints.min, constraints.max) 200 | return { position, rotation: clampedRotation, constraints } 201 | } 202 | } 203 | 204 | function applyConstraints( 205 | links: { rotation: number; position: V2; constraints?: Constraints }[], 206 | joints: JointTransform[], 207 | ) { 208 | return links.map((link, index) => applyConstraint(link, joints[index + 1]!)) 209 | } 210 | 211 | export interface JointTransform { 212 | position: V2 213 | rotation: number 214 | } 215 | 216 | /** 217 | * Distance from end effector to the target 218 | */ 219 | export function getErrorDistance(links: Link[], base: JointTransform, target: V2): number { 220 | const effectorPosition = getEndEffectorPosition(links, base) 221 | return V2O.euclideanDistance(target, effectorPosition) 222 | } 223 | 224 | /** 225 | * Absolute position of the end effector (last links tip) 226 | */ 227 | export function getEndEffectorPosition(links: Link[], joint: JointTransform): V2 { 228 | return getJointTransforms(links, joint).effectorPosition 229 | } 230 | 231 | /** 232 | * Returns the absolute position and rotation of each link 233 | */ 234 | export function getJointTransforms( 235 | links: Link[], 236 | joint: JointTransform, 237 | ): { 238 | transforms: JointTransform[] 239 | effectorPosition: V2 240 | effectorRotation: number 241 | } { 242 | const transforms = [joint] 243 | 244 | for (let index = 0; index < links.length; index++) { 245 | const currentLink = links[index]! 246 | const parentTransform = transforms[index]! 247 | 248 | const absoluteRotation = (currentLink.rotation ?? 0) + parentTransform.rotation 249 | const relativePosition = V2O.rotate(currentLink.position, absoluteRotation) 250 | const absolutePosition = V2O.add(relativePosition, parentTransform.position) 251 | transforms.push({ position: absolutePosition, rotation: absoluteRotation }) 252 | } 253 | 254 | const effectorPosition = transforms[transforms.length - 1]!.position 255 | const effectorRotation = transforms[transforms.length - 1]!.rotation 256 | 257 | return { transforms, effectorPosition, effectorRotation } 258 | } 259 | 260 | export function buildLink(position: V2, rotation = 0, constraint?: number | Range | ExactRotation): Link { 261 | return { 262 | position, 263 | rotation, 264 | constraints: constraint, 265 | } 266 | } 267 | 268 | function copyLink({ rotation, position, constraints: constraint }: Link): Link { 269 | return { rotation, position: [...position], constraints: constraint === undefined ? undefined : constraint } 270 | } 271 | 272 | function isExactRotation(rotation: number | Range | ExactRotation): rotation is ExactRotation { 273 | return (rotation as ExactRotation).value !== undefined 274 | } 275 | -------------------------------------------------------------------------------- /src/Solve3D.ts: -------------------------------------------------------------------------------- 1 | import { QuaternionO, SolveOptions, V3O } from '.' 2 | import { Quaternion } from './math/Quaternion' 3 | import { V3 } from './math/V3' 4 | import { Range } from './Range' 5 | import { defaultCCDOptions, defaultFABRIKOptions, SolveCCDOptions, SolveFABRIKOptions } from './SolveOptions' 6 | 7 | export interface Link { 8 | /** 9 | * The rotation at the base of the link 10 | */ 11 | rotation: Quaternion 12 | 13 | /** 14 | * undefined: No constraint 15 | * 16 | * {pitch, yaw, roll}: Range | Number 17 | * 18 | * Range: minimum angle, maximum angle (radians), positive is anticlockwise from previous Link's direction vector 19 | * 20 | * number: the range of rotation (radian) about the previous links direction vector. A rotation of 90 deg would be 45 deg either direction 21 | * 22 | * ExactRotation: Either a global, or local rotation which the Link is locked to 23 | */ 24 | constraints?: Constraints 25 | position: V3 26 | } 27 | 28 | type Constraints = EulerConstraint | ExactRotation 29 | 30 | interface EulerConstraint { 31 | /** 32 | * Rotation about X 33 | */ 34 | pitch?: number | Range 35 | /** 36 | * Rotation about Y 37 | */ 38 | yaw?: number | Range 39 | /** 40 | * Rotation about Z 41 | */ 42 | roll?: number | Range 43 | } 44 | 45 | interface ExactRotation { 46 | value: Quaternion 47 | /** 48 | * 'local': Relative to previous links direction vector 49 | * 50 | * 'global': Relative to the baseJoints world transform 51 | */ 52 | type: 'global' | 'local' 53 | } 54 | 55 | export interface SolveResult { 56 | /** 57 | * Copy of the structure of input links 58 | * With the possibility of their rotation being changed 59 | */ 60 | links: Link[] 61 | /** 62 | * Returns the error distance after the solve step 63 | */ 64 | getErrorDistance: () => number 65 | /** 66 | * true if the solve terminates early due to the end effector being close to the target. 67 | * undefined if solve has adjusted the rotations in links 68 | * 69 | * undefined is used here as we don't rerun error checking after the angle adjustment, thus it cannot be known true or false. 70 | * This is done to improve performance 71 | */ 72 | isWithinAcceptedError: true | undefined 73 | } 74 | 75 | /** 76 | * Changes joint angle to minimize distance of end effector to target 77 | * 78 | * If given no options, runs in FABRIK mode 79 | */ 80 | export function solve( 81 | links: Link[], 82 | baseJoint: JointTransform, 83 | target: V3, 84 | options: SolveOptions = { method: 'FABRIK' }, 85 | ): SolveResult { 86 | switch (options.method) { 87 | case 'FABRIK': 88 | return solveFABRIK(links, baseJoint, target, { 89 | method: 'FABRIK', 90 | acceptedError: options.acceptedError ?? defaultFABRIKOptions.acceptedError, 91 | deltaAngle: options.deltaAngle ?? defaultFABRIKOptions.deltaAngle, 92 | learningRate: options.learningRate ?? defaultFABRIKOptions.learningRate, 93 | }) 94 | 95 | case 'CCD': 96 | return solveCCD(links, baseJoint, target, { 97 | method: 'CCD', 98 | acceptedError: options.acceptedError ?? defaultCCDOptions.acceptedError, 99 | learningRate: options.learningRate ?? defaultCCDOptions.learningRate, 100 | }) 101 | } 102 | } 103 | 104 | function solveFABRIK( 105 | links: Link[], 106 | baseJoint: JointTransform, 107 | target: V3, 108 | { deltaAngle, learningRate, acceptedError }: Required, 109 | ): SolveResult { 110 | const { transforms: joints, effectorPosition } = getJointTransforms(links, baseJoint) 111 | const error = V3O.euclideanDistance(target, effectorPosition) 112 | 113 | if (error < acceptedError) 114 | return { links: links.map(copyLink), isWithinAcceptedError: true, getErrorDistance: () => error } 115 | 116 | if (joints.length !== links.length + 1) { 117 | throw new Error( 118 | `Joint transforms should have the same length as links + 1. Got ${joints.length}, expected ${links.length}`, 119 | ) 120 | } 121 | 122 | const withAngleStep: Link[] = links.map( 123 | ({ position, rotation = QuaternionO.zeroRotation(), constraints }, linkIndex) => { 124 | // For each, calculate partial derivative, sum to give full numerical derivative 125 | const angleStep: V3 = V3O.fromArray( 126 | [0, 0, 0].map((_, v3Index) => { 127 | const eulerAngle = [0, 0, 0] 128 | eulerAngle[v3Index] = deltaAngle 129 | const linkWithAngleDelta = { 130 | position, 131 | rotation: QuaternionO.multiply(rotation, QuaternionO.fromEulerAngles(V3O.fromArray(eulerAngle))), 132 | } 133 | 134 | // Get remaining links from this links joint 135 | const projectedLinks: Link[] = [linkWithAngleDelta, ...links.slice(linkIndex + 1)] 136 | 137 | // Get gradient from small change in joint angle 138 | const joint = joints[linkIndex]! 139 | const projectedError = getErrorDistance(projectedLinks, joint, target) 140 | const gradient = (projectedError - error) / deltaAngle 141 | 142 | // Get resultant angle step which minimizes error 143 | const angleStep = 144 | -gradient * (typeof learningRate === 'function' ? learningRate(projectedError) : learningRate) 145 | 146 | return angleStep 147 | }), 148 | ) 149 | 150 | const steppedRotation = QuaternionO.multiply(rotation, QuaternionO.fromEulerAngles(angleStep)) 151 | 152 | return { position, rotation: steppedRotation, constraints } 153 | }, 154 | ) 155 | 156 | const adjustedJoints = getJointTransforms(withAngleStep, baseJoint).transforms 157 | const withConstraints = applyConstraints(withAngleStep, adjustedJoints) 158 | 159 | return { 160 | links: withConstraints, 161 | getErrorDistance: () => getErrorDistance(withConstraints, baseJoint, target), 162 | isWithinAcceptedError: undefined, 163 | } 164 | } 165 | 166 | function solveCCD( 167 | links: Link[], 168 | baseJoint: JointTransform, 169 | target: V3, 170 | { learningRate, acceptedError }: Required, 171 | ): SolveResult { 172 | // 1. From base to tip, point projection from joint to effector at target 173 | let adjustedLinks: Link[] = [...links.map(copyLink)] 174 | 175 | for (let index = adjustedLinks.length - 1; index >= 0; index--) { 176 | const joints = getJointTransforms(adjustedLinks, baseJoint) 177 | const effectorPosition = joints.effectorPosition 178 | const error = V3O.euclideanDistance(target, effectorPosition) 179 | 180 | if (error < acceptedError) break 181 | 182 | const link = adjustedLinks[index]! 183 | const { rotation, position, constraints } = link 184 | const joint = joints.transforms[index]! 185 | 186 | /** 187 | * Following http://rodolphe-vaillant.fr/?e=114 188 | * 189 | * We found that if we didn't convert the world coordinate system here to local 190 | * that it would give very unstable solutions. It seems that others have struggled 191 | * with the same thing. 192 | * 193 | * https://github.com/zalo/zalo.github.io/blob/fb1b899ce9825b1123b0ebd2bfdce2459566e6db/assets/js/IK/IKExample.js#L67 194 | */ 195 | const inverseRotation = QuaternionO.inverse(joint.rotation) 196 | const rotatedTarget = V3O.rotate(target, inverseRotation) 197 | const rotatedEffector = V3O.rotate(effectorPosition, inverseRotation) 198 | const rotatedJoint = V3O.rotate(joint.position, inverseRotation) 199 | const directionToTarget = V3O.subtract(rotatedTarget, rotatedJoint) 200 | const directionToEffector = V3O.subtract(rotatedEffector, rotatedJoint) 201 | 202 | const angleBetween = QuaternionO.rotationFromTo(directionToEffector, directionToTarget) 203 | 204 | const angleStep: Quaternion = QuaternionO.slerp( 205 | QuaternionO.zeroRotation(), 206 | angleBetween, 207 | typeof learningRate === 'function' ? learningRate(error) : learningRate, 208 | ) 209 | const withAngleStep = { rotation: QuaternionO.multiply(rotation, angleStep), position, constraints } 210 | adjustedLinks[index] = withAngleStep 211 | 212 | const adjustedJoints = getJointTransforms(adjustedLinks, baseJoint) 213 | const withConstraint = applyConstraint(withAngleStep, adjustedJoints.transforms[index + 1]!) 214 | adjustedLinks[index] = withConstraint 215 | } 216 | 217 | return { 218 | links: adjustedLinks, 219 | getErrorDistance: () => getErrorDistance(adjustedLinks, baseJoint, target), 220 | isWithinAcceptedError: undefined, 221 | } 222 | } 223 | 224 | export interface JointTransform { 225 | position: V3 226 | rotation: Quaternion 227 | } 228 | 229 | function applyConstraint({ position, rotation, constraints }: Link, joint: JointTransform): Link { 230 | if (constraints === undefined) return { position: position, rotation } 231 | 232 | if (isExactRotation(constraints)) { 233 | if (constraints.type === 'global') { 234 | const targetRotation = constraints.value 235 | const currentRotation = joint.rotation 236 | const adjustedRotation = QuaternionO.multiply( 237 | QuaternionO.multiply(rotation, QuaternionO.inverse(currentRotation)), 238 | targetRotation, 239 | ) 240 | 241 | return { position, rotation: adjustedRotation, constraints } 242 | } else { 243 | return { position, rotation: constraints.value, constraints } 244 | } 245 | } 246 | 247 | const { pitch, yaw, roll } = constraints 248 | 249 | let pitchMin: number 250 | let pitchMax: number 251 | if (typeof pitch === 'number') { 252 | pitchMin = -pitch / 2 253 | pitchMax = pitch / 2 254 | } else if (pitch === undefined) { 255 | pitchMin = -Infinity 256 | pitchMax = Infinity 257 | } else { 258 | pitchMin = pitch.min 259 | pitchMax = pitch.max 260 | } 261 | 262 | let yawMin: number 263 | let yawMax: number 264 | if (typeof yaw === 'number') { 265 | yawMin = -yaw / 2 266 | yawMax = yaw / 2 267 | } else if (yaw === undefined) { 268 | yawMin = -Infinity 269 | yawMax = Infinity 270 | } else { 271 | yawMin = yaw.min 272 | yawMax = yaw.max 273 | } 274 | 275 | let rollMin: number 276 | let rollMax: number 277 | if (typeof roll === 'number') { 278 | rollMin = -roll / 2 279 | rollMax = roll / 2 280 | } else if (roll === undefined) { 281 | rollMin = -Infinity 282 | rollMax = Infinity 283 | } else { 284 | rollMin = roll.min 285 | rollMax = roll.max 286 | } 287 | 288 | const lowerBound: V3 = [pitchMin, yawMin, rollMin] 289 | const upperBound: V3 = [pitchMax, yawMax, rollMax] 290 | const clampedRotation = QuaternionO.clamp(rotation, lowerBound, upperBound) 291 | return { position: position, rotation: clampedRotation, constraints: copyConstraints(constraints) } 292 | } 293 | 294 | function applyConstraints(links: Link[], joints: JointTransform[]): Link[] { 295 | return links.map((link, index) => applyConstraint(link, joints[index + 1]!)) 296 | } 297 | 298 | /** 299 | * Distance from end effector to the target 300 | */ 301 | export function getErrorDistance(links: Link[], base: JointTransform, target: V3): number { 302 | const effectorPosition = getEndEffectorPosition(links, base) 303 | return V3O.euclideanDistance(target, effectorPosition) 304 | } 305 | 306 | /** 307 | * Absolute position of the end effector (last links tip) 308 | */ 309 | export function getEndEffectorPosition(links: Link[], joint: JointTransform): V3 { 310 | return getJointTransforms(links, joint).effectorPosition 311 | } 312 | 313 | /** 314 | * Returns the absolute position and rotation of each link 315 | */ 316 | export function getJointTransforms( 317 | links: Link[], 318 | joint: JointTransform, 319 | ): { 320 | transforms: JointTransform[] 321 | effectorPosition: V3 322 | } { 323 | const transforms = [{ ...joint }] 324 | 325 | for (let index = 0; index < links.length; index++) { 326 | const currentLink = links[index]! 327 | const parentTransform = transforms[index]! 328 | 329 | const absoluteRotation = QuaternionO.multiply( 330 | parentTransform.rotation, 331 | currentLink.rotation ?? QuaternionO.zeroRotation(), 332 | ) 333 | const relativePosition = V3O.rotate(currentLink.position, absoluteRotation) 334 | const absolutePosition = V3O.add(relativePosition, parentTransform.position) 335 | transforms.push({ position: absolutePosition, rotation: absoluteRotation }) 336 | } 337 | 338 | const effectorPosition = transforms[transforms.length - 1]!.position 339 | 340 | return { transforms, effectorPosition } 341 | } 342 | 343 | export function buildLink(position: V3, rotation = QuaternionO.zeroRotation(), constraints?: Constraints): Link { 344 | return { 345 | position, 346 | rotation, 347 | constraints, 348 | } 349 | } 350 | 351 | function copyLink({ rotation, position, constraints }: Link): Link { 352 | return { 353 | rotation, 354 | position: [...position], 355 | constraints: constraints === undefined ? undefined : copyConstraints(constraints), 356 | } 357 | } 358 | 359 | function copyConstraints(constraints: Constraints): Constraints { 360 | const result: Constraints = {} 361 | 362 | if (isExactRotation(constraints)) { 363 | return { type: constraints.type, value: [...constraints.value] } 364 | } 365 | const { pitch, yaw, roll } = constraints 366 | 367 | if (typeof pitch === 'number') { 368 | result.pitch = pitch 369 | } else if (pitch !== undefined) { 370 | result.pitch = { ...pitch! } 371 | } 372 | 373 | if (typeof yaw === 'number') { 374 | result.yaw = yaw 375 | } else if (yaw !== undefined) { 376 | result.yaw = { ...yaw! } 377 | } 378 | 379 | if (typeof roll === 'number') { 380 | result.roll = roll 381 | } else if (roll !== undefined) { 382 | result.roll = { ...roll! } 383 | } 384 | 385 | return result 386 | } 387 | 388 | function isExactRotation(rotation: EulerConstraint | ExactRotation): rotation is ExactRotation { 389 | return (rotation as ExactRotation).value !== undefined 390 | } 391 | -------------------------------------------------------------------------------- /src/SolveOptions.ts: -------------------------------------------------------------------------------- 1 | export interface SolveFABRIKOptions { 2 | method: 'FABRIK' 3 | /** 4 | * Angle gap taken to calculate the gradient of the error function 5 | * Usually the default here will do. 6 | * @default 0.00001 7 | */ 8 | deltaAngle?: number 9 | /** 10 | * Sets the 'speed' at which the algorithm converges on the target. 11 | * Larger values will cause oscillations, or vibrations about the target 12 | * Lower values may move too slowly. You should tune this manually 13 | * 14 | * Can either be a constant, or a function that returns a learning rate 15 | * @default 0.0001 16 | */ 17 | learningRate?: number | ((errorDistance: number) => number) 18 | /** 19 | * Useful if there is oscillations or vibration around the target 20 | * @default 0 21 | */ 22 | acceptedError?: number 23 | } 24 | 25 | export const defaultFABRIKOptions: Required = { 26 | method: 'FABRIK', 27 | deltaAngle: 0.0001, 28 | learningRate: 0.001, 29 | acceptedError: 0, 30 | } 31 | 32 | export interface SolveCCDOptions { 33 | method: 'CCD' 34 | /** 35 | * Sets the 'speed' at which the algorithm converges on the target. 36 | * Larger values will cause oscillations, or vibrations about the target 37 | * Lower values may move too slowly. You should tune this manually 38 | * 39 | * Can either be a constant, or a function that returns a learning rate 40 | * @default 1.0 41 | */ 42 | learningRate?: number | ((errorDistance: number) => number) 43 | /** 44 | * Useful if there is oscillations or vibration around the target 45 | * @default 0 46 | */ 47 | acceptedError?: number 48 | } 49 | 50 | export const defaultCCDOptions: Required = { 51 | method: 'CCD', 52 | learningRate: 1, 53 | acceptedError: 0, 54 | } 55 | 56 | export type SolveOptions = SolveFABRIKOptions | SolveCCDOptions 57 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * as MathUtils from './math/MathUtils' 2 | export * from './math/Quaternion' 3 | export * as QuaternionO from './math/QuaternionO' 4 | export * from './math/V2' 5 | export * as V2O from './math/V2O' 6 | export * from './math/V3' 7 | export * as V3O from './math/V3O' 8 | export * as Solve2D from './Solve2D' 9 | export * as Solve3D from './Solve3D' 10 | export * from './SolveOptions' 11 | export * from './Range' 12 | -------------------------------------------------------------------------------- /src/math/MathUtils.ts: -------------------------------------------------------------------------------- 1 | export function lerp(from: number, to: number, amount: number): number { 2 | amount = amount < 0 ? 0 : amount 3 | amount = amount > 1 ? 1 : amount 4 | return from + (to - from) * amount 5 | } 6 | 7 | export const clamp = (value: number, min: number, max: number): number => Math.min(Math.max(min, value), max) 8 | 9 | export function lerpTheta(from: number, to: number, amount: number, circleAt: number = Math.PI * 2) { 10 | const removeLoops = (distance: number) => clamp(distance - Math.floor(distance / circleAt) * circleAt, 0, circleAt) 11 | 12 | const distance = to - from 13 | const unloopedDistance = removeLoops(distance) 14 | const isLeft = unloopedDistance > Math.PI 15 | const offset = isLeft ? unloopedDistance - Math.PI * 2 : unloopedDistance 16 | return lerp(from, from + offset, amount) 17 | } 18 | 19 | export const valuesAreWithinDistance = (valueA: number, valueB: number, delta: number) => { 20 | const highest = Math.max(valueA, valueB) 21 | const lowest = Math.min(valueA, valueB) 22 | 23 | return highest - delta < lowest 24 | } 25 | 26 | export const rotationsAreWithinAngle = (rotationA: number, rotationB: number, angle: number) => { 27 | const normalisedA = rotationA % (Math.PI * 2) 28 | const normalisedB = rotationB % (Math.PI * 2) 29 | 30 | return valuesAreWithinDistance(normalisedA, normalisedB, angle) 31 | } 32 | -------------------------------------------------------------------------------- /src/math/Quaternion.ts: -------------------------------------------------------------------------------- 1 | export type Quaternion = readonly [real: number, x: number, y: number, z: number] 2 | -------------------------------------------------------------------------------- /src/math/QuaternionO.ts: -------------------------------------------------------------------------------- 1 | import * as V3O from './V3O' 2 | import { Quaternion } from './Quaternion' 3 | import { V3 } from './V3' 4 | import * as MathUtils from './MathUtils' 5 | import { QuaternionO } from 'src' 6 | 7 | export const multiply = (a: Quaternion, b: Quaternion): Quaternion => { 8 | const qax = a[0] 9 | const qay = a[1] 10 | const qaz = a[2] 11 | const qaw = a[3] 12 | const qbx = b[0] 13 | const qby = b[1] 14 | const qbz = b[2] 15 | const qbw = b[3] 16 | 17 | return [ 18 | qax * qbw + qaw * qbx + qay * qbz - qaz * qby, 19 | qay * qbw + qaw * qby + qaz * qbx - qax * qbz, 20 | qaz * qbw + qaw * qbz + qax * qby - qay * qbx, 21 | qaw * qbw - qax * qbx - qay * qby - qaz * qbz, 22 | ] 23 | } 24 | 25 | export const fromEulerAngles = ([x, y, z]: V3): Quaternion => { 26 | const cos = Math.cos 27 | const sin = Math.sin 28 | 29 | const c1 = cos(x / 2) 30 | const c2 = cos(y / 2) 31 | const c3 = cos(z / 2) 32 | 33 | const s1 = sin(x / 2) 34 | const s2 = sin(y / 2) 35 | const s3 = sin(z / 2) 36 | 37 | return [ 38 | s1 * c2 * c3 + c1 * s2 * s3, 39 | c1 * s2 * c3 - s1 * c2 * s3, 40 | c1 * c2 * s3 + s1 * s2 * c3, 41 | c1 * c2 * c3 - s1 * s2 * s3, 42 | ] 43 | } 44 | 45 | export const slerp = (from: Quaternion, to: Quaternion, amount: number): Quaternion => { 46 | // Calculate angle between them. 47 | const cosHalfTheta = from[0] * to[0] + from[1] * to[1] + from[2] * to[2] + from[3] * to[3] 48 | // Are parallel in either direction. from = to || from = -to 49 | if (Math.abs(cosHalfTheta) >= 1.0) { 50 | return from 51 | } 52 | 53 | const halfTheta = Math.acos(cosHalfTheta) 54 | const sinHalfTheta = Math.sqrt(1.0 - cosHalfTheta * cosHalfTheta) 55 | // if theta = 180 degrees then result is not fully defined 56 | // we could rotate around any axis normal to qa or qb 57 | if (Math.abs(sinHalfTheta) < 0.001) { 58 | return [ 59 | from[0] * 0.5 + to[0] * 0.5, 60 | from[1] * 0.5 + to[1] * 0.5, 61 | from[2] * 0.5 + to[2] * 0.5, 62 | from[3] * 0.5 + to[3] * 0.5, 63 | ] 64 | } 65 | 66 | const ratioA = Math.sin((1 - amount) * halfTheta) / sinHalfTheta 67 | const ratioB = Math.sin(amount * halfTheta) / sinHalfTheta 68 | //calculate Quaternion. 69 | return [ 70 | from[0] * ratioA + to[0] * ratioB, 71 | from[1] * ratioA + to[1] * ratioB, 72 | from[2] * ratioA + to[2] * ratioB, 73 | from[3] * ratioA + to[3] * ratioB, 74 | ] 75 | } 76 | 77 | export const conjugate = (quaternion: Quaternion): Quaternion => { 78 | return [-quaternion[0], -quaternion[1], -quaternion[2], quaternion[3]] 79 | } 80 | 81 | export const inverse = (quaternion: Quaternion): Quaternion => { 82 | const conj = conjugate(quaternion) 83 | const mag = magnitude(quaternion) 84 | 85 | return [conj[0] / mag, conj[1] / mag, conj[2] / mag, conj[3] / mag] 86 | } 87 | 88 | export const magnitude = (quaternion: Quaternion): number => Math.hypot(...quaternion) 89 | 90 | export const zeroRotation = (): Quaternion => [0, 0, 0, 1] 91 | 92 | export const normalize = (quaternion: Quaternion): Quaternion => { 93 | const length = Math.hypot(...quaternion) 94 | if (length === 0) return zeroRotation() 95 | return [quaternion[0] / length, quaternion[1] / length, quaternion[2] / length, quaternion[3] / length] 96 | } 97 | 98 | export const clamp = (quaternion: Quaternion, lowerBound: V3, upperBound: V3): Quaternion => { 99 | const rotationAxis = [quaternion[0], quaternion[1], quaternion[2]] 100 | const w = quaternion[3] 101 | 102 | const [x, y, z] = V3O.fromArray( 103 | rotationAxis.map((component, index) => { 104 | const angle = 2 * Math.atan(component / w) 105 | 106 | const lower = lowerBound[index]! 107 | const upper = upperBound[index]! 108 | if (lower > upper) 109 | throw new Error( 110 | `Lower bound should be less than upper bound for component ${index}. Lower: ${lower}, upper: ${upper}`, 111 | ) 112 | const clampedAngle = MathUtils.clamp(angle, lower, upper) 113 | return Math.tan(0.5 * clampedAngle) 114 | }), 115 | ) 116 | return normalize([x, y, z, 1]) 117 | } 118 | 119 | export const fromUnitDirectionVector = (vector: V3): Quaternion => { 120 | return rotationFromTo([1, 0, 0], vector) 121 | } 122 | 123 | export const rotationFromTo = (a: V3, b: V3): Quaternion => { 124 | const aNormalised = V3O.normalise(a) 125 | const bNormalised = V3O.normalise(b) 126 | const dot = V3O.dotProduct(aNormalised, bNormalised) 127 | 128 | const isParallel = dot >= 1 129 | if (isParallel) { 130 | // a, b are parallel 131 | return zeroRotation() 132 | } 133 | 134 | const isAntiParallel = dot < -1 + Number.EPSILON 135 | if (isAntiParallel) { 136 | let axis = V3O.crossProduct([1, 0, 0], aNormalised) 137 | const aPointsForward = V3O.sqrEuclideanLength(axis) === 0 138 | 139 | if (aPointsForward) { 140 | axis = V3O.crossProduct([0, 1, 0], aNormalised) 141 | } 142 | 143 | axis = V3O.normalise(axis) 144 | 145 | return fromAxisAngle(axis, Math.PI) 146 | } 147 | 148 | const q: Quaternion = [...V3O.crossProduct(aNormalised, bNormalised), 1 + dot] 149 | return normalize(q) 150 | } 151 | 152 | export const fromAxisAngle = (axis: V3, angle: number): Quaternion => { 153 | const halfAngle = angle / 2 154 | return [...V3O.scale(axis, Math.sin(halfAngle)), Math.cos(halfAngle)] 155 | } 156 | 157 | export const fromObject = (object: { w: number; x: number; y: number; z: number }): Quaternion => [ 158 | object.x, 159 | object.y, 160 | object.z, 161 | object.w, 162 | ] 163 | -------------------------------------------------------------------------------- /src/math/V2.ts: -------------------------------------------------------------------------------- 1 | export type V2 = readonly [x: number, y: number] 2 | -------------------------------------------------------------------------------- /src/math/V2O.ts: -------------------------------------------------------------------------------- 1 | import * as MathUtils from './MathUtils' 2 | import { V2 } from './V2' 3 | 4 | const VECTOR_LENGTH = 2 5 | 6 | export const combine = (a: V2, b: V2, operation: (aElement: number, bElement: number) => number): V2 => { 7 | const result = new Array(VECTOR_LENGTH) 8 | 9 | for (let index = 0; index < VECTOR_LENGTH; index++) { 10 | const aElement = a[index] 11 | const bElement = b[index] 12 | 13 | result[index] = operation(aElement!, bElement!) 14 | } 15 | 16 | return fromArray(result) 17 | } 18 | 19 | export const angle = ([x, y]: V2): number => Math.atan2(y, x) 20 | export const add = (a: V2, b: V2): V2 => combine(a, b, (a, b) => a + b) 21 | export const map = (vector: V2, callback: (element: number, elementIndex: number, vector: V2) => T): [T, T] => { 22 | return vector.map((value, index, array) => callback(value, index, array as V2)) as [T, T] 23 | } 24 | export const maxAbs = (vector: V2, max: number): V2 => { 25 | const isClipped = max * max < sqrEuclideanLength(vector) 26 | if (!isClipped) return [...vector] 27 | 28 | const normalised = normalise(vector) 29 | const maxVector = scale(normalised, max) 30 | return maxVector 31 | } 32 | export const maxElement = (a: V2): number => Math.max(...a) 33 | export const abs = (a: V2): V2 => [Math.abs(a[0]), Math.abs(a[1])] 34 | export const subtract = (base: V2, subtraction: V2): V2 => combine(base, subtraction, (a, b) => a - b) 35 | export const multiply = (base: V2, multiplier: V2): V2 => combine(base, multiplier, (a, b) => a * b) 36 | export const dot = (a: V2, b: V2): number => a[0] * b[0] + a[1] * b[1] 37 | export const lerp = (from: V2, to: V2, amount: number): V2 => 38 | combine(from, to, (fromElement, toElement) => MathUtils.lerp(fromElement, toElement, amount)) 39 | 40 | export const clamp = (value: V2, min: V2, max: V2): V2 => [ 41 | MathUtils.clamp(value[0], min[0], max[0]), 42 | MathUtils.clamp(value[1], min[1], max[1]), 43 | ] 44 | export const tangent = (vector: V2): V2 => [-vector[1], vector[0]] 45 | export const scale = (base: V2, factor: number): V2 => fromArray(base.map((element) => element * factor)) 46 | export const divideScalar = (base: V2, divisor: number): V2 => fromArray(base.map((element) => element / divisor)) 47 | export const divide = (base: V2, divisor: V2): V2 => combine(base, divisor, (a, b) => a / b) 48 | 49 | export const normalise = (vector: V2): V2 => { 50 | const length = euclideanLength(vector) 51 | if (length === 0) { 52 | return zero() 53 | } 54 | return scale(vector, 1 / length) 55 | } 56 | 57 | export const sqrEuclideanLength = (vector: V2): number => vector[0] ** 2 + vector[1] ** 2 58 | 59 | export const euclideanLength = (vector: V2): number => sqrEuclideanLength(vector) ** 0.5 60 | 61 | export const sqrEuclideanDistance = (a: V2, b: V2): number => { 62 | const distance = subtract(a, b) 63 | return sqrEuclideanLength(distance) 64 | } 65 | 66 | export const euclideanDistance = (a: V2, b: V2): number => { 67 | const distance = subtract(a, b) 68 | return euclideanLength(distance) 69 | } 70 | 71 | export const rotate = ([x, y]: V2, angleRadians: number): V2 => { 72 | const cos = Math.cos(angleRadians) 73 | const sin = Math.sin(angleRadians) 74 | 75 | return [x * cos - y * sin, x * sin + y * cos] 76 | } 77 | 78 | export const zero = (): V2 => [0, 0] 79 | export const flipX = (vector: V2): V2 => [-vector[0], vector[1]] 80 | export const flipY = (vector: V2): V2 => [vector[0], -vector[1]] 81 | export const flipAxes = (vector: V2): V2 => [-vector[0], -vector[1]] 82 | 83 | export const pickX = (vector: V2): V2 => [vector[0], 0] 84 | export const pickY = (vector: V2): V2 => [0, vector[1]] 85 | 86 | export const fromArray = (array: number[]): V2 => { 87 | if (array.length !== VECTOR_LENGTH) 88 | throw new Error(`Cannot create V2 from ${array}, length is ${array.length}. Length should be ${VECTOR_LENGTH}`) 89 | return array as unknown as V2 90 | } 91 | 92 | export const valueEquality = (a: V2, b: V2): boolean => a[0] === b[0] && a[1] === b[1] 93 | 94 | export const fromVector2 = ({ x, y }: { x: number; y: number }): V2 => [x, y] 95 | 96 | export const average = (...vectors: [V2, ...V2[]]): V2 => { 97 | let sum: V2 = [0, 0] 98 | vectors.forEach((vector) => { 99 | sum = add(sum, vector) 100 | }) 101 | 102 | return divideScalar(sum, vectors.length) 103 | } 104 | 105 | export const fromPolar = (radius: number, angle: number) => rotate([radius, 0], angle) 106 | -------------------------------------------------------------------------------- /src/math/V3.ts: -------------------------------------------------------------------------------- 1 | export type V3 = readonly [x: number, y: number, z: number] 2 | -------------------------------------------------------------------------------- /src/math/V3O.ts: -------------------------------------------------------------------------------- 1 | import * as MathUtils from './MathUtils' 2 | import { Quaternion } from './Quaternion' 3 | import * as QuaternionO from './QuaternionO' 4 | import { V2 } from './V2' 5 | import { V3 } from './V3' 6 | 7 | const VECTOR_LENGTH = 3 8 | 9 | export const add = (a: V3, b: V3): V3 => [a[0] + b[0], a[1] + b[1], a[2] + b[2]] 10 | export const sum = (vectors: V3[]): V3 => { 11 | const result = [0, 0, 0] 12 | 13 | for (let vectorIndex = 0; vectorIndex < vectors.length; vectorIndex++) { 14 | const vector = vectors[vectorIndex]! 15 | result[0] += vector[0] 16 | result[1] += vector[1] 17 | result[2] += vector[2] 18 | } 19 | 20 | return fromArray(result) 21 | } 22 | 23 | /** 24 | * @returns a - b 25 | */ 26 | export const subtract = (base: V3, subtraction: V3): V3 => [ 27 | base[0] - subtraction[0], 28 | base[1] - subtraction[1], 29 | base[2] - subtraction[2], 30 | ] 31 | 32 | /** 33 | * @returns element-wise multiplication of base and multiplier 34 | */ 35 | export const multiply = (base: V3, multiplier: V3): V3 => [ 36 | base[0] * multiplier[0], 37 | base[1] * multiplier[1], 38 | base[2] * multiplier[2], 39 | ] 40 | 41 | /** 42 | * @returns a / b 43 | */ 44 | export const divide = (base: V3, divisor: V3): V3 => [base[0] / divisor[0], base[1] / divisor[1], base[2] / divisor[2]] 45 | 46 | export const scale = (base: V3, factor: number): V3 => [base[0] * factor, base[1] * factor, base[2] * factor] 47 | 48 | export const sign = (vector: V3): V3 => [Math.sign(vector[0]), Math.sign(vector[1]), Math.sign(vector[2])] 49 | 50 | export const normalise = (vector: V3): V3 => { 51 | const length = euclideanLength(vector) 52 | if (length === 0) { 53 | return zero() 54 | } 55 | return scale(vector, 1 / length) 56 | } 57 | 58 | export const extractXY = (vector: V3): V2 => [vector[0], vector[1]] 59 | export const extractXZ = (vector: V3): V2 => [vector[0], vector[2]] 60 | export const extractYZ = (vector: V3): V2 => [vector[1], vector[2]] 61 | export const sqrEuclideanLength = (vector: V3): number => manhattanLength(multiply(vector, vector)) 62 | export const manhattanLength = (vector: V3): number => vector[0] + vector[1] + vector[2] 63 | 64 | export const dotProduct = (a: V3, b: V3): number => { 65 | const product = multiply(a, b) 66 | return manhattanLength(product) 67 | } 68 | 69 | export const crossProduct = (a: V3, b: V3): V3 => { 70 | const [ax, ay, az] = a 71 | const [bx, by, bz] = b 72 | 73 | const x = ay * bz - az * by 74 | const y = az * bx - ax * bz 75 | const z = ax * by - ay * bx 76 | 77 | return [x, y, z] 78 | } 79 | 80 | export const euclideanLength = (vector: V3): number => sqrEuclideanLength(vector) ** 0.5 81 | 82 | export const sqrEuclideanDistance = (a: V3, b: V3): number => { 83 | const distance = subtract(a, b) 84 | return sqrEuclideanLength(distance) 85 | } 86 | 87 | export const euclideanDistance = (a: V3, b: V3): number => { 88 | const distance = subtract(a, b) 89 | return euclideanLength(distance) 90 | } 91 | 92 | export const lerp = (from: V3, to: V3, amount: number): V3 => [ 93 | MathUtils.lerp(from[0], to[0], amount), 94 | MathUtils.lerp(from[1], to[1], amount), 95 | MathUtils.lerp(from[2], to[2], amount), 96 | ] 97 | 98 | export const lerpTheta = (from: V3, to: V3, amount: number): V3 => [ 99 | MathUtils.lerpTheta(from[0], to[0], amount, Math.PI * 2), 100 | MathUtils.lerpTheta(from[1], to[1], amount, Math.PI * 2), 101 | MathUtils.lerpTheta(from[2], to[2], amount, Math.PI * 2), 102 | ] 103 | 104 | export const up = (): V3 => [0, 1, 0] 105 | export const down = (): V3 => [0, -1, 0] 106 | export const right = (): V3 => [1, 0, 0] 107 | export const left = (): V3 => [-1, 0, 0] 108 | export const forwards = (): V3 => [0, 0, 1] 109 | export const back = (): V3 => [0, 0, -1] 110 | export const zero = (): V3 => [0, 0, 0] 111 | export const copy = (vector: V3): V3 => [vector[0], vector[1], vector[2]] 112 | export const flipX = (vector: V3): V3 => [-vector[0], vector[1], vector[2]] 113 | export const flipY = (vector: V3): V3 => [vector[0], -vector[1], vector[2]] 114 | export const flipZ = (vector: V3): V3 => [vector[0], vector[1], -vector[2]] 115 | export const clamp = (vector: V3, min: V3, max: V3): V3 => [ 116 | MathUtils.clamp(vector[0], min[0], max[0]), 117 | MathUtils.clamp(vector[1], min[1], max[1]), 118 | MathUtils.clamp(vector[2], min[2], max[2]), 119 | ] 120 | 121 | export const random = (): V3 => [Math.random(), Math.random(), Math.random()] 122 | export const randomRange = (min: number, max: number): V3 => add(scale(random(), max - min), [min, min, min]) 123 | export const fromArray = (array: number[]): V3 => { 124 | if (array.length !== VECTOR_LENGTH) 125 | throw new Error(`Cannot create V3 from ${array}, length is ${array.length}. Length should be ${VECTOR_LENGTH}`) 126 | return array as unknown as V3 127 | } 128 | 129 | export const fromVector3 = (vector: { x: number; y: number; z: number }): V3 => [vector.x, vector.y, vector.z] 130 | export const rotate = (vector: V3, rotation: Quaternion): V3 => { 131 | const conjugate = QuaternionO.conjugate(rotation) 132 | const intermediate = QuaternionO.multiply(rotation, [...vector, 0]) 133 | const result = QuaternionO.multiply(intermediate, conjugate) 134 | return [result[0], result[1], result[2]] 135 | } 136 | 137 | export const fromPolar = (radius: number, angle: Quaternion): V3 => rotate([radius, 0, 0], angle) 138 | 139 | export const toPolar = (vector: V3): [radius: number, angle: Quaternion] => { 140 | const radius = euclideanLength(vector) 141 | const angle = QuaternionO.fromUnitDirectionVector(normalise(vector)) 142 | 143 | return [radius, angle] 144 | } 145 | -------------------------------------------------------------------------------- /tests/QuaternionO.test.ts: -------------------------------------------------------------------------------- 1 | import { QuaternionO, Quaternion, V3 } from '../src' 2 | 3 | const identity = QuaternionO.zeroRotation() 4 | const oneOnRootTwo = 1 / Math.pow(2, 0.5) 5 | 6 | describe('Quaternion Operations', () => { 7 | it('Multiplies two identity quaternions correctly', () => { 8 | const a: Quaternion = identity 9 | const b: Quaternion = identity 10 | 11 | expect(QuaternionO.multiply(a, b)).toEqual(identity) 12 | }) 13 | 14 | it('Multiplies with identity and has no effect', () => { 15 | const a: Quaternion = identity 16 | const b: Quaternion = [0, oneOnRootTwo, 0, oneOnRootTwo] 17 | 18 | expect(QuaternionO.multiply(a, b)).toEqual(b) 19 | }) 20 | 21 | it('Rotates by 90 degree rotations twice to form a 180 degree rotation', () => { 22 | const a: Quaternion = [oneOnRootTwo, 0, 0, oneOnRootTwo] 23 | const b: Quaternion = [oneOnRootTwo, 0, 0, oneOnRootTwo] 24 | 25 | expect(QuaternionO.multiply(a, b)).toBeCloseToQuaternion([1, 0, 0, 0]) 26 | }) 27 | 28 | it('Rotates by 90 degree rotations twice to form a 180 degree rotation, in each axis', () => { 29 | for (let axis = 0; axis < 3; axis++) { 30 | const test: Quaternion = [ 31 | ...Array.from({ length: 3 }).map((_, index) => (index === axis ? oneOnRootTwo : 0)), 32 | oneOnRootTwo, 33 | ] as unknown as Quaternion 34 | 35 | const expected = [ 36 | ...Array.from({ length: 3 }).map((_, index) => (index === axis ? 1 : 0)), 37 | 0, 38 | ] as unknown as Quaternion 39 | 40 | expect(QuaternionO.multiply(test, test)).toBeCloseToQuaternion(expected) 41 | } 42 | }) 43 | 44 | it('Can create from Euler Angles', () => { 45 | const angles: V3 = [Math.PI / 2, Math.PI / 4, Math.PI / 6] 46 | const expected: Quaternion = [0.701, 0.092, 0.43, 0.56] 47 | 48 | expect(QuaternionO.fromEulerAngles(angles)).toBeCloseToQuaternion(expected) 49 | }) 50 | 51 | it('Creates conjugate', () => { 52 | const input: Quaternion = [0, oneOnRootTwo, 0, oneOnRootTwo] 53 | const expected: Quaternion = [-0, -oneOnRootTwo, -0, oneOnRootTwo] 54 | 55 | expect(QuaternionO.conjugate(input)).toBeCloseToQuaternion(expected) 56 | }) 57 | 58 | it('Can clamp to zero', () => { 59 | const input: Quaternion = [oneOnRootTwo, 0, 0, oneOnRootTwo] 60 | const lowerBound: V3 = [0, 0, 0] 61 | const upperBound: V3 = [0, 0, 0] 62 | 63 | const expected: Quaternion = [0, 0, 0, 1] 64 | 65 | expect(QuaternionO.clamp(input, lowerBound, upperBound)).toBeCloseToQuaternion(expected) 66 | }) 67 | 68 | it('Can clamp to upperBound', () => { 69 | const input: Quaternion = [oneOnRootTwo, 0, 0, oneOnRootTwo] 70 | const lowerBound: V3 = [0, 0, 0] 71 | const upperBound: V3 = [Math.PI / 4, 0, 0] 72 | 73 | const expected: Quaternion = [0.3826834, 0, 0, 0.9238795] 74 | 75 | expect(QuaternionO.clamp(input, lowerBound, upperBound)).toBeCloseToQuaternion(expected) 76 | }) 77 | 78 | it('Can clamp to lowerBound', () => { 79 | const input: Quaternion = [-oneOnRootTwo, 0, 0, oneOnRootTwo] 80 | const lowerBound: V3 = [-Math.PI / 4, 0, 0] 81 | const upperBound: V3 = [0, 0, 0] 82 | 83 | const expected: Quaternion = [-0.3826834, 0, 0, 0.9238795] 84 | 85 | expect(QuaternionO.clamp(input, lowerBound, upperBound)).toBeCloseToQuaternion(expected) 86 | }) 87 | 88 | it('Ignores clamping inside range -Infinity - +Infinity', () => { 89 | const input: Quaternion = [-oneOnRootTwo, 0, 0, oneOnRootTwo] 90 | const lowerBound: V3 = [-Infinity, 0, 0] 91 | const upperBound: V3 = [Infinity, 0, 0] 92 | 93 | expect(QuaternionO.clamp(input, lowerBound, upperBound)).toBeCloseToQuaternion(input) 94 | }) 95 | 96 | it('Throws if clamp lower bound > upper bound', () => { 97 | const input: Quaternion = [0, 0, 0, 1] 98 | const lowerBound: V3 = [Math.PI / 2, 0, 0] 99 | const upperBound: V3 = [0, 0, 0] 100 | 101 | expect(() => QuaternionO.clamp(input, lowerBound, upperBound)).toThrow() 102 | }) 103 | 104 | it('Calculates magnitude', () => { 105 | { 106 | const input: Quaternion = [0, 0, 0, 1] 107 | const magnitude = QuaternionO.magnitude(input) 108 | 109 | expect(magnitude).toBe(1) 110 | } 111 | 112 | { 113 | const input: Quaternion = [0, 0, 0, 0] 114 | const magnitude = QuaternionO.magnitude(input) 115 | 116 | expect(magnitude).toBe(0) 117 | } 118 | }) 119 | 120 | it('Calculates inverse', () => { 121 | const input: Quaternion = QuaternionO.fromEulerAngles([Math.PI, 0, 0]) 122 | const expected: Quaternion = QuaternionO.fromEulerAngles([-Math.PI, 0, 0]) 123 | 124 | expect(QuaternionO.inverse(input)).toBeCloseToQuaternion(expected) 125 | }) 126 | 127 | it('Creates from unit direction vectors', () => { 128 | { 129 | const input: V3 = [0, 1, 0] 130 | const expected: Quaternion = [0, 0, oneOnRootTwo, oneOnRootTwo] 131 | 132 | expect(QuaternionO.fromUnitDirectionVector(input)).toBeCloseToQuaternion(expected) 133 | } 134 | { 135 | const input: V3 = [0, -1, 0] 136 | const expected: Quaternion = [0, 0, -oneOnRootTwo, oneOnRootTwo] 137 | 138 | expect(QuaternionO.fromUnitDirectionVector(input)).toBeCloseToQuaternion(expected) 139 | } 140 | { 141 | const input: V3 = [1, 0, 0] 142 | const expected: Quaternion = [0, 0, 0, 1] 143 | 144 | expect(QuaternionO.fromUnitDirectionVector(input)).toBeCloseToQuaternion(expected) 145 | } 146 | }) 147 | 148 | it('Creates from axis angle', () => { 149 | const axis: V3 = [0, 1, 0] 150 | const angle = Math.PI / 2 151 | 152 | const expected: Quaternion = [0, oneOnRootTwo, 0, oneOnRootTwo] 153 | 154 | expect(QuaternionO.fromAxisAngle(axis, angle)).toBeCloseToQuaternion(expected) 155 | }) 156 | 157 | it('Creates from rotation from two vectors', () => { 158 | { 159 | const a: V3 = [0, 0, -1] 160 | const b: V3 = [-1, 0, 0] 161 | 162 | const expected: Quaternion = [0, oneOnRootTwo, 0, oneOnRootTwo] 163 | 164 | expect(QuaternionO.rotationFromTo(a, b)).toBeCloseToQuaternion(expected) 165 | } 166 | { 167 | const a: V3 = [1, 0, 0] 168 | const b: V3 = [0, 4, 0] 169 | 170 | const expected: Quaternion = [0, 0, oneOnRootTwo, oneOnRootTwo] 171 | 172 | expect(QuaternionO.rotationFromTo(a, b)).toBeCloseToQuaternion(expected) 173 | } 174 | }) 175 | }) 176 | -------------------------------------------------------------------------------- /tests/Solve2D.test.ts: -------------------------------------------------------------------------------- 1 | import { SolveOptions, V2, V2O } from '../src' 2 | import { 3 | buildLink, 4 | getErrorDistance, 5 | getJointTransforms, 6 | JointTransform, 7 | Link, 8 | solve, 9 | SolveResult, 10 | } from '../src/Solve2D' 11 | 12 | describe('getJointTransforms', () => { 13 | it('Returns base in empty chain', () => { 14 | const links: Link[] = [] 15 | const pivotTransform = { position: [0, 0] as V2, rotation: 0 } 16 | const endEffectorPosition = getJointTransforms(links, pivotTransform).effectorPosition 17 | 18 | expect(endEffectorPosition).toEqual([0, 0]) 19 | }) 20 | 21 | it('Returns end effector position', () => { 22 | const links: Link[] = [{ rotation: 0, position: [50, 0] }] 23 | const pivotTransform = { position: [0, 0] as V2, rotation: 0 } 24 | const endEffectorPosition = getJointTransforms(links, pivotTransform).effectorPosition 25 | 26 | expect(endEffectorPosition).toEqual([50, 0]) 27 | }) 28 | 29 | it('Respects base rotation', () => { 30 | const links: Link[] = [buildLink([50, 0])] 31 | const pivotTransform = { position: [0, 0] as V2, rotation: Math.PI / 2 } 32 | const endEffectorPosition = getJointTransforms(links, pivotTransform).effectorPosition 33 | 34 | expect(endEffectorPosition[1]).toBeCloseTo(50) 35 | }) 36 | 37 | it('Returns end effector position after long chain', () => { 38 | const links: Link[] = [ 39 | { rotation: 0, position: [50, 0] }, 40 | { rotation: 0, position: [50, 0] }, 41 | { rotation: 0, position: [50, 0] }, 42 | { rotation: 0, position: [50, 0] }, 43 | ] 44 | const pivotTransform = { position: [0, 0] as V2, rotation: 0 } 45 | const endEffectorPosition = getJointTransforms(links, pivotTransform).effectorPosition 46 | 47 | expect(endEffectorPosition).toEqual([200, 0]) 48 | }) 49 | 50 | it('Returns end effector position after bend', () => { 51 | const links: Link[] = [{ rotation: Math.PI / 2, position: [50, 0] }] 52 | const pivotTransform = { position: [0, 0] as V2, rotation: 0 } 53 | const endEffectorPosition = getJointTransforms(links, pivotTransform).effectorPosition 54 | 55 | expect(endEffectorPosition[0]).toBeCloseTo(0) 56 | expect(endEffectorPosition[1]).toBeCloseTo(50) 57 | }) 58 | 59 | it('Returns end effector position chain with bends', () => { 60 | const links: Link[] = [ 61 | { rotation: 0, position: [50, 0] }, 62 | { rotation: 0, position: [50, 0] }, 63 | { rotation: Math.PI / 2, position: [50, 0] }, 64 | { rotation: -Math.PI / 2, position: [50, 0] }, 65 | ] 66 | const pivotTransform = { position: [0, 0] as V2, rotation: 0 } 67 | const endEffectorPosition = getJointTransforms(links, pivotTransform).effectorPosition 68 | 69 | expect(endEffectorPosition[0]).toBeCloseTo(150) 70 | expect(endEffectorPosition[1]).toBeCloseTo(50) 71 | }) 72 | 73 | it('Returns absolute transforms for empty chain', () => { 74 | const links: Link[] = [] 75 | const pivotTransform = { position: [0, 0] as V2, rotation: 0 } 76 | const transforms = getJointTransforms(links, pivotTransform).transforms 77 | 78 | expect(transforms.length).toBe(1) 79 | expect(transforms[0]).toStrictEqual({ position: [0, 0], rotation: 0 }) 80 | }) 81 | 82 | it('Returns absolute transforms for chain', () => { 83 | const links: Link[] = [ 84 | { rotation: 0, position: [50, 0] }, 85 | { rotation: 0, position: [50, 0] }, 86 | ] 87 | const pivotTransform = { position: [50, 0] as V2, rotation: 0 } 88 | const transforms = getJointTransforms(links, pivotTransform).transforms 89 | 90 | expect(transforms.length).toBe(3) 91 | expect(transforms).toStrictEqual([ 92 | { position: [50, 0], rotation: 0 }, 93 | { position: [100, 0], rotation: 0 }, 94 | { position: [150, 0], rotation: 0 }, 95 | ]) 96 | }) 97 | 98 | it('Returns absolute transforms for chain with bends', () => { 99 | const links: Link[] = [ 100 | { rotation: 0, position: [50, 0] }, 101 | { rotation: 0, position: [50, 0] }, 102 | { rotation: Math.PI / 2, position: [50, 0] }, 103 | { rotation: -Math.PI / 2, position: [50, 0] }, 104 | ] 105 | const pivotTransform = { position: [50, 0] as V2, rotation: 0 } 106 | const transforms = getJointTransforms(links, pivotTransform).transforms 107 | 108 | expect(transforms.length).toBe(5) 109 | expect(transforms).toStrictEqual([ 110 | { position: [50, 0], rotation: 0 }, 111 | { position: [100, 0], rotation: 0 }, 112 | { position: [150, 0], rotation: 0 }, 113 | { position: [150, 50], rotation: Math.PI / 2 }, 114 | { position: [200, 50], rotation: 0 }, 115 | ]) 116 | }) 117 | }) 118 | 119 | describe('solve FABRIK', () => { 120 | it('Runs with empty links array', () => { 121 | const links: Link[] = [] 122 | const linksCopy = cloneDeep(links) 123 | solve(links, { position: [0, 0], rotation: 0 }, [0, 0]) 124 | 125 | expect(links).toStrictEqual(linksCopy) 126 | }) 127 | 128 | it('Reduces distance to target each time it is called', () => { 129 | const links: Link[] = [{ rotation: 0, position: [50, 0] }] 130 | const target: V2 = [0, 50] 131 | 132 | const base: JointTransform = { position: [0, 0], rotation: 0 } 133 | 134 | solveAndCheckDidImprove(links, base, target, 3) 135 | }) 136 | 137 | it('Reduces distance to target each time it is called with complex chain', () => { 138 | const links: Link[] = [ 139 | { rotation: 0, position: [50, 0] }, 140 | { rotation: 0, position: [50, 0] }, 141 | { rotation: 0, position: [50, 0] }, 142 | { rotation: 0, position: [50, 0] }, 143 | ] 144 | const target: V2 = [0, 50] 145 | 146 | const base: JointTransform = { position: [0, 0], rotation: 0 } 147 | 148 | solveAndCheckDidImprove(links, base, target, 3) 149 | }) 150 | 151 | it('Respects no rotation unary constraint', () => { 152 | const links: Link[] = [{ rotation: 0, position: [50, 0], constraints: 0 }] 153 | const target: V2 = [0, 50] 154 | const base: JointTransform = { position: [0, 0], rotation: 0 } 155 | 156 | solveAndCheckDidNotImprove(links, base, target, 3) 157 | }) 158 | 159 | it('Respects unary constraint', () => { 160 | let links: Link[] = [{ rotation: 0, position: [1, 0], constraints: Math.PI / 2 }] 161 | const target: V2 = [0, 1] 162 | const base: JointTransform = { position: [0, 0], rotation: 0 } 163 | 164 | let error: number 165 | let lastError = getErrorDistance(links, base, target) 166 | while (true) { 167 | const result = solve(links, base, target, { learningRate: 10e-2, method: 'FABRIK' }) 168 | links = result.links 169 | error = result.getErrorDistance() 170 | 171 | const errorDifference = lastError - error 172 | const didNotImprove = errorDifference <= 0 173 | if (didNotImprove) break 174 | 175 | lastError = error 176 | } 177 | 178 | const expectedError = V2O.euclideanDistance(target, [0.7071, 0.7071]) 179 | expect(error).toBeCloseTo(expectedError) 180 | 181 | const jointTransforms = getJointTransforms(links, base) 182 | expect(jointTransforms.transforms[1]?.rotation).toBeCloseTo(Math.PI / 4) 183 | }) 184 | 185 | it('Respects no rotation binary constraint', () => { 186 | const links: Link[] = [{ rotation: 0, position: [50, 0], constraints: { min: 0, max: 0 } }] 187 | const target: V2 = [0, 50] 188 | const base: JointTransform = { position: [0, 0], rotation: 0 } 189 | 190 | solveAndCheckDidNotImprove(links, base, target, 3) 191 | }) 192 | 193 | it('Respects binary constraint', () => { 194 | let links: Link[] = [{ rotation: 0, position: [1, 0], constraints: { min: -Math.PI / 4, max: Math.PI / 4 } }] 195 | const target: V2 = [0, 1] 196 | const base: JointTransform = { position: [0, 0], rotation: 0 } 197 | 198 | let error: number 199 | let lastError = getErrorDistance(links, base, target) 200 | while (true) { 201 | const result = solve(links, base, target, { learningRate: 10e-2, method: 'FABRIK' }) 202 | links = result.links 203 | error = result.getErrorDistance() 204 | 205 | const errorDifference = lastError - error 206 | const didNotImprove = errorDifference <= 0 207 | if (didNotImprove) break 208 | 209 | lastError = error 210 | } 211 | const expectedError = V2O.euclideanDistance(target, [0.7071, 0.7071]) 212 | expect(error).toBeCloseTo(expectedError) 213 | 214 | const jointTransforms = getJointTransforms(links, base) 215 | expect(jointTransforms.transforms[1]?.rotation).toBeCloseTo(Math.PI / 4) 216 | }) 217 | 218 | it('Respects exact local constraint', () => { 219 | let links: Link[] = [{ rotation: 0, position: [1, 0], constraints: { value: Math.PI / 4, type: 'local' } }] 220 | const target: V2 = [0, 1] 221 | const base: JointTransform = { position: [0, 0], rotation: 0 } 222 | const result = solve(links, base, target, { learningRate: 0, method: 'FABRIK' }) 223 | links = result.links 224 | 225 | const jointTransforms = getJointTransforms(links, base) 226 | expect(jointTransforms.transforms[1]?.rotation).toBeCloseTo(Math.PI / 4) 227 | }) 228 | 229 | it('Respects exact global constraint', () => { 230 | let links: Link[] = [ 231 | { rotation: 0, position: [1, 0] }, 232 | { rotation: 0, position: [1, 0] }, 233 | { rotation: 0, position: [1, 0], constraints: { value: Math.PI / 4, type: 'global' } }, 234 | ] 235 | const target: V2 = [0, 1] 236 | const base: JointTransform = { position: [0, 0], rotation: 0 } 237 | const result = solve(links, base, target, { learningRate: 0, method: 'FABRIK' }) 238 | links = result.links 239 | 240 | const jointTransforms = getJointTransforms(links, base) 241 | 242 | expect(jointTransforms.transforms[3]?.rotation).toBe(Math.PI / 4) 243 | }) 244 | }) 245 | 246 | describe('solve CCD', () => { 247 | it('Runs with empty links array', () => { 248 | const links: Link[] = [] 249 | const linksCopy = cloneDeep(links) 250 | solve(links, { position: [0, 0], rotation: 0 }, [0, 0], { method: 'CCD' }) 251 | 252 | expect(links).toStrictEqual(linksCopy) 253 | }) 254 | 255 | it('Reduces distance to target each time it is called', () => { 256 | const links: Link[] = [{ rotation: 0, position: [50, 0] }] 257 | const target: V2 = [0, 50] 258 | 259 | const base: JointTransform = { position: [0, 0], rotation: 0 } 260 | 261 | solveAndCheckDidImprove(links, base, target, 1, 'CCD') 262 | }) 263 | 264 | it('Reduces distance to target each time it is called with complex chain', () => { 265 | const links: Link[] = [ 266 | { rotation: 0, position: [50, 0] }, 267 | { rotation: 0, position: [50, 0] }, 268 | { rotation: 0, position: [50, 0] }, 269 | { rotation: 0, position: [50, 0] }, 270 | ] 271 | const target: V2 = [0, 50] 272 | 273 | const base: JointTransform = { position: [0, 0], rotation: 0 } 274 | 275 | solveAndCheckDidImprove(links, base, target, 3, 'CCD') 276 | }) 277 | 278 | it('Respects no rotation unary constraint', () => { 279 | const links: Link[] = [{ rotation: 0, position: [50, 0], constraints: 0 }] 280 | const target: V2 = [0, 50] 281 | const base: JointTransform = { position: [0, 0], rotation: 0 } 282 | 283 | solveAndCheckDidNotImprove(links, base, target, 3, 'CCD') 284 | }) 285 | 286 | it('Respects unary constraint', () => { 287 | let links: Link[] = [{ rotation: 0, position: [1, 0], constraints: Math.PI / 2 }] 288 | const target: V2 = [0, 1] 289 | const base: JointTransform = { position: [0, 0], rotation: 0 } 290 | 291 | let error: number 292 | let lastError = getErrorDistance(links, base, target) 293 | while (true) { 294 | const result = solve(links, base, target, { learningRate: 10e-2, method: 'CCD' }) 295 | links = result.links 296 | error = result.getErrorDistance() 297 | 298 | const errorDifference = lastError - error 299 | const didNotImprove = errorDifference <= 0 300 | if (didNotImprove) break 301 | 302 | lastError = error 303 | } 304 | 305 | const expectedError = V2O.euclideanDistance(target, [0.7071, 0.7071]) 306 | expect(error).toBeCloseTo(expectedError) 307 | 308 | const jointTransforms = getJointTransforms(links, base) 309 | expect(jointTransforms.transforms[1]?.rotation).toBeCloseTo(Math.PI / 4) 310 | }) 311 | 312 | it('Respects no rotation binary constraint', () => { 313 | const links: Link[] = [{ rotation: 0, position: [50, 0], constraints: { min: 0, max: 0 } }] 314 | const target: V2 = [0, 50] 315 | const base: JointTransform = { position: [0, 0], rotation: 0 } 316 | 317 | solveAndCheckDidNotImprove(links, base, target, 3, 'CCD') 318 | }) 319 | 320 | it('Respects binary constraint', () => { 321 | let links: Link[] = [{ rotation: 0, position: [1, 0], constraints: { min: -Math.PI / 4, max: Math.PI / 4 } }] 322 | const target: V2 = [0, 1] 323 | const base: JointTransform = { position: [0, 0], rotation: 0 } 324 | 325 | let error: number 326 | let lastError = getErrorDistance(links, base, target) 327 | while (true) { 328 | const result = solve(links, base, target, { learningRate: 10e-2, method: 'CCD' }) 329 | links = result.links 330 | error = result.getErrorDistance() 331 | 332 | const errorDifference = lastError - error 333 | const didNotImprove = errorDifference <= 0 334 | if (didNotImprove) break 335 | 336 | lastError = error 337 | } 338 | const expectedError = V2O.euclideanDistance(target, [0.7071, 0.7071]) 339 | expect(error).toBeCloseTo(expectedError) 340 | 341 | const jointTransforms = getJointTransforms(links, base) 342 | expect(jointTransforms.transforms[1]?.rotation).toBeCloseTo(Math.PI / 4) 343 | }) 344 | 345 | it('Respects exact local constraint', () => { 346 | let links: Link[] = [{ rotation: 0, position: [1, 0], constraints: { value: Math.PI / 4, type: 'local' } }] 347 | const target: V2 = [0, 1] 348 | const base: JointTransform = { position: [0, 0], rotation: 0 } 349 | const result = solve(links, base, target, { learningRate: 0, method: 'CCD' }) 350 | links = result.links 351 | 352 | const jointTransforms = getJointTransforms(links, base) 353 | expect(jointTransforms.transforms[1]?.rotation).toBeCloseTo(Math.PI / 4) 354 | }) 355 | 356 | it('Respects exact global constraint', () => { 357 | let links: Link[] = [ 358 | { rotation: 0, position: [1, 0] }, 359 | { rotation: 0, position: [1, 0] }, 360 | { rotation: 0, position: [1, 0], constraints: { value: Math.PI / 4, type: 'global' } }, 361 | ] 362 | const target: V2 = [0, 1] 363 | const base: JointTransform = { position: [0, 0], rotation: 0 } 364 | const result = solve(links, base, target, { learningRate: 0, method: 'CCD' }) 365 | links = result.links 366 | 367 | const jointTransforms = getJointTransforms(links, base) 368 | 369 | expect(jointTransforms.transforms[3]?.rotation).toBe(Math.PI / 4) 370 | }) 371 | }) 372 | 373 | function cloneDeep(object: T): T { 374 | return JSON.parse(JSON.stringify(object)) 375 | } 376 | 377 | function solveAndCheckDidImprove( 378 | links: Link[], 379 | base: JointTransform, 380 | target: V2, 381 | times: number, 382 | method: 'CCD' | 'FABRIK' = 'FABRIK', 383 | ) { 384 | const options: SolveOptions = { 385 | acceptedError: 0, 386 | method, 387 | } 388 | 389 | let solveResult: undefined | SolveResult 390 | 391 | for (let index = 0; index < times; index++) { 392 | const linksThisIteration = solveResult?.links ?? links 393 | const errorBefore = getErrorDistance(linksThisIteration, base, target) 394 | solveResult = solve(linksThisIteration, base, target, { ...options, method }) 395 | const errorAfter = solveResult.getErrorDistance() 396 | expect(errorBefore).toBeGreaterThan(errorAfter) 397 | } 398 | } 399 | 400 | function solveAndCheckDidNotImprove( 401 | links: Link[], 402 | base: JointTransform, 403 | target: V2, 404 | times: number, 405 | method: 'CCD' | 'FABRIK' = 'FABRIK', 406 | ) { 407 | const options: SolveOptions = { 408 | acceptedError: 0, 409 | method, 410 | } 411 | 412 | let solveResult: undefined | SolveResult 413 | 414 | for (let index = 0; index < times; index++) { 415 | const linksThisIteration = solveResult?.links ?? links 416 | const errorBefore = getErrorDistance(linksThisIteration, base, target) 417 | solveResult = solve(linksThisIteration, base, target, { ...options, method }) 418 | const errorAfter = solveResult.getErrorDistance() 419 | expect(errorBefore).not.toBeGreaterThan(errorAfter) 420 | } 421 | } 422 | -------------------------------------------------------------------------------- /tests/Solve3D.test.ts: -------------------------------------------------------------------------------- 1 | import { Quaternion, QuaternionO, Solve3D, SolveOptions, V3, V3O } from '../src' 2 | import { Link, getJointTransforms, getErrorDistance, solve, JointTransform, SolveResult } from '../src/Solve3D' 3 | 4 | describe('forwardPass', () => { 5 | it('Returns base in empty chain', () => { 6 | const links: Link[] = [] 7 | const pivotTransform = { position: [0, 0, 0] as V3, rotation: QuaternionO.zeroRotation() } 8 | const endEffectorPosition = getJointTransforms(links, pivotTransform).effectorPosition 9 | 10 | expect(endEffectorPosition).toEqual([0, 0, 0]) 11 | }) 12 | 13 | it('Returns end effector position', () => { 14 | const links: Link[] = [{ rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }] 15 | const pivotTransform = { position: [0, 0, 0] as V3, rotation: QuaternionO.zeroRotation() } 16 | const endEffectorPosition = getJointTransforms(links, pivotTransform).effectorPosition 17 | 18 | expect(endEffectorPosition).toEqual([50, 0, 0]) 19 | }) 20 | 21 | it('Respects base rotation', () => { 22 | const links: Link[] = [{ rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }] 23 | const pivotTransform = { position: [0, 0, 0] as V3, rotation: QuaternionO.fromEulerAngles([0, 0, Math.PI / 2]) } 24 | const endEffectorPosition = getJointTransforms(links, pivotTransform).effectorPosition 25 | 26 | expect(endEffectorPosition[1]).toBeCloseTo(50) 27 | }) 28 | 29 | it('Returns end effector position after long chain', () => { 30 | const links: Link[] = [ 31 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }, 32 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }, 33 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }, 34 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }, 35 | ] 36 | const pivotTransform = { position: [0, 0, 0] as V3, rotation: QuaternionO.zeroRotation() } 37 | const endEffectorPosition = getJointTransforms(links, pivotTransform).effectorPosition 38 | 39 | expect(endEffectorPosition).toEqual([200, 0, 0]) 40 | }) 41 | 42 | it('Returns end effector position after bend', () => { 43 | const links: Link[] = [{ rotation: QuaternionO.fromEulerAngles([0, 0, Math.PI / 2]), position: [50, 0, 0] }] 44 | const pivotTransform = { position: [0, 0, 0] as V3, rotation: QuaternionO.zeroRotation() } 45 | const endEffectorPosition = getJointTransforms(links, pivotTransform).effectorPosition 46 | 47 | expect(endEffectorPosition[0]).toBeCloseTo(0) 48 | expect(endEffectorPosition[1]).toBeCloseTo(50) 49 | }) 50 | 51 | it('Returns end effector position chain with bends', () => { 52 | const links: Link[] = [ 53 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }, 54 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }, 55 | { rotation: QuaternionO.fromEulerAngles([0, 0, Math.PI / 2]), position: [50, 0, 0] }, 56 | { rotation: QuaternionO.fromEulerAngles([0, 0, -Math.PI / 2]), position: [50, 0, 0] }, 57 | ] 58 | const pivotTransform = { position: [0, 0, 0] as V3, rotation: QuaternionO.zeroRotation() } 59 | const endEffectorPosition = getJointTransforms(links, pivotTransform).effectorPosition 60 | 61 | expect(endEffectorPosition[0]).toBeCloseTo(150) 62 | expect(endEffectorPosition[1]).toBeCloseTo(50) 63 | }) 64 | 65 | it('Returns absolute transforms for empty chain', () => { 66 | const links: Link[] = [] 67 | const pivotTransform = { position: [0, 0, 0] as V3, rotation: QuaternionO.zeroRotation() } 68 | const transforms = getJointTransforms(links, pivotTransform).transforms 69 | 70 | expect(transforms.length).toBe(1) 71 | expect(transforms[0]).toStrictEqual({ position: [0, 0, 0], rotation: QuaternionO.zeroRotation() }) 72 | }) 73 | 74 | it('Returns absolute transforms for chain', () => { 75 | const links: Link[] = [ 76 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }, 77 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }, 78 | ] 79 | const pivotTransform = { position: [50, 0, 0] as V3, rotation: QuaternionO.zeroRotation() } 80 | const transforms = getJointTransforms(links, pivotTransform).transforms 81 | 82 | expect(transforms.length).toBe(3) 83 | expect(transforms).toStrictEqual([ 84 | { position: [50, 0, 0], rotation: QuaternionO.zeroRotation() }, 85 | { position: [100, 0, 0], rotation: QuaternionO.zeroRotation() }, 86 | { position: [150, 0, 0], rotation: QuaternionO.zeroRotation() }, 87 | ]) 88 | }) 89 | 90 | it('Returns absolute transforms for chain with bends', () => { 91 | const links: Link[] = [ 92 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }, 93 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }, 94 | { rotation: QuaternionO.fromEulerAngles([0, 0, Math.PI / 2]), position: [50, 0, 0] }, 95 | { rotation: QuaternionO.fromEulerAngles([0, 0, -Math.PI / 2]), position: [50, 0, 0] }, 96 | ] 97 | const pivotTransform = { position: [50, 0, 0] as V3, rotation: QuaternionO.zeroRotation() } 98 | const transforms = getJointTransforms(links, pivotTransform).transforms 99 | 100 | expect(transforms.length).toBe(5) 101 | expect(transforms).toStrictEqual([ 102 | { position: [50, 0, 0], rotation: QuaternionO.zeroRotation() }, 103 | { position: [100, 0, 0], rotation: QuaternionO.zeroRotation() }, 104 | { position: [150, 0, 0], rotation: QuaternionO.zeroRotation() }, 105 | { position: [150, 50, 0], rotation: QuaternionO.fromEulerAngles([0, 0, Math.PI / 2]) }, 106 | { position: [200, 50, 0], rotation: QuaternionO.zeroRotation() }, 107 | ]) 108 | }) 109 | 110 | it('Respects exact local constraint', () => { 111 | let links: Link[] = [ 112 | { 113 | rotation: QuaternionO.zeroRotation(), 114 | position: [1, 0, 0], 115 | constraints: { value: QuaternionO.fromEulerAngles([Math.PI / 4, 0, 0]), type: 'local' }, 116 | }, 117 | ] 118 | const target: V3 = [0, 1, 0] 119 | const base: JointTransform = { position: [0, 0, 0], rotation: QuaternionO.zeroRotation() } 120 | const result = solve(links, base, target, { learningRate: 0, method: 'FABRIK' }) 121 | links = result.links 122 | 123 | const jointTransforms = getJointTransforms(links, base) 124 | expect(jointTransforms.transforms[1]?.rotation).toBeCloseToQuaternion( 125 | QuaternionO.fromEulerAngles([Math.PI / 4, 0, 0]), 126 | ) 127 | }) 128 | 129 | it('Respects exact global constraint', () => { 130 | let links: Link[] = [ 131 | { rotation: QuaternionO.zeroRotation(), position: [1, 0, 0] }, 132 | { rotation: QuaternionO.zeroRotation(), position: [1, 0, 0] }, 133 | { 134 | rotation: QuaternionO.zeroRotation(), 135 | position: [1, 0, 0], 136 | constraints: { value: QuaternionO.fromEulerAngles([Math.PI / 2, 0, 0]), type: 'global' }, 137 | }, 138 | ] 139 | const target: V3 = [0, 1, 0] 140 | const base: JointTransform = { position: [0, 0, 0], rotation: QuaternionO.zeroRotation() } 141 | const result = solve(links, base, target, { learningRate: 0, method: 'FABRIK' }) 142 | links = result.links 143 | 144 | const jointTransforms = getJointTransforms(links, base) 145 | 146 | expect(jointTransforms.transforms[3]?.rotation).toBeCloseToQuaternion( 147 | QuaternionO.fromEulerAngles([Math.PI / 2, 0, 0]), 148 | ) 149 | }) 150 | }) 151 | 152 | describe('solve FABRIK', () => { 153 | it('Runs with empty links array', () => { 154 | const links: Link[] = [] 155 | const linksCopy = cloneDeep(links) 156 | solve(links, { position: [0, 0, 0], rotation: QuaternionO.zeroRotation() }, [0, 0, 0]) 157 | 158 | expect(links).toStrictEqual(linksCopy) 159 | }) 160 | 161 | it('Reduces distance to target each time it is called', () => { 162 | const links: Link[] = [{ rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }] 163 | const target: V3 = [0, 50, 0] 164 | 165 | const base: JointTransform = { position: [0, 0, 0], rotation: QuaternionO.zeroRotation() } 166 | 167 | solveAndCheckDidImprove(links, base, target, 3, 'FABRIK') 168 | }) 169 | 170 | it('Reduces distance to target each time it is called with complex chain', () => { 171 | const links: Link[] = [ 172 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }, 173 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }, 174 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }, 175 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }, 176 | ] 177 | const target: V3 = [0, 50, 0] 178 | 179 | const base: JointTransform = { position: [0, 0, 0], rotation: QuaternionO.zeroRotation() } 180 | 181 | solveAndCheckDidImprove(links, base, target, 3, 'FABRIK') 182 | }) 183 | 184 | it('Should not improve if output of previous step is not used as input to following step', () => { 185 | const links: Link[] = [ 186 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }, 187 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }, 188 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }, 189 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }, 190 | ] 191 | const target: V3 = [0, 50, 0] 192 | 193 | const base: JointTransform = { position: [0, 0, 0], rotation: QuaternionO.zeroRotation() } 194 | 195 | const options: SolveOptions = { 196 | acceptedError: 0, 197 | method: 'FABRIK', 198 | } 199 | 200 | for (let index = 0; index < 3; index++) { 201 | const errorBefore = getErrorDistance(links, base, target) 202 | solve(links, base, target, options) 203 | const errorAfter = Solve3D.getErrorDistance(links, base, target) 204 | expect(errorBefore).toEqual(errorAfter) 205 | } 206 | }) 207 | 208 | it('Respects no rotation unary constraint', () => { 209 | const links: Link[] = [ 210 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0], constraints: { yaw: 0, roll: 0, pitch: 0 } }, 211 | ] 212 | const target: V3 = [0, 50, 0] 213 | const base: JointTransform = { position: [0, 0, 0], rotation: QuaternionO.zeroRotation() } 214 | 215 | solveAndCheckDidNotImprove(links, base, target, 3, 'FABRIK') 216 | }) 217 | 218 | it('Respects no rotation binary constraint', () => { 219 | const links: Link[] = [ 220 | { 221 | rotation: QuaternionO.zeroRotation(), 222 | position: [50, 0, 0], 223 | constraints: { yaw: { min: 0, max: 0 }, roll: { min: 0, max: 0 }, pitch: { min: 0, max: 0 } }, 224 | }, 225 | ] 226 | const target: V3 = [0, 50, 0] 227 | const base: JointTransform = { position: [0, 0, 0], rotation: QuaternionO.zeroRotation() } 228 | 229 | solveAndCheckDidNotImprove(links, base, target, 3, 'FABRIK') 230 | }) 231 | 232 | it('Respects binary constraint', () => { 233 | let links: Link[] = [ 234 | { 235 | rotation: QuaternionO.zeroRotation(), 236 | position: [1, 0, 0], 237 | constraints: { 238 | yaw: 0, 239 | // Roll about z, causes x points vector to rotate to point up at y 240 | roll: { min: -Math.PI / 4, max: Math.PI / 4 }, 241 | pitch: 0, 242 | }, 243 | }, 244 | ] 245 | const target: V3 = [0, 1, 0] 246 | const base: JointTransform = { position: [0, 0, 0], rotation: QuaternionO.zeroRotation() } 247 | 248 | let error: number 249 | let lastError = getErrorDistance(links, base, target) 250 | while (true) { 251 | const result = solve(links, base, target, { learningRate: 10e-3, method: 'FABRIK' }) 252 | links = result.links 253 | error = result.getErrorDistance() 254 | 255 | const errorDifference = lastError - error 256 | const didNotImprove = errorDifference <= 0 257 | if (didNotImprove) break 258 | 259 | lastError = error 260 | } 261 | 262 | // Length 1, pointing 45 degrees on way from x to y 263 | const expectedError = V3O.euclideanDistance(target, [0.7071, 0.7071, 0]) 264 | expect(error).toBeCloseTo(expectedError) 265 | 266 | const jointTransforms = getJointTransforms(links, base) 267 | 268 | // Quaternion from euler rotation about z 45 degrees 269 | expect(jointTransforms.transforms[1]?.rotation).toBeCloseToQuaternion([0, 0, 0.3826834, 0.9238795]) 270 | }) 271 | }) 272 | 273 | describe('solve CCD', () => { 274 | it('Runs with empty links array', () => { 275 | const links: Link[] = [] 276 | const linksCopy = cloneDeep(links) 277 | solve(links, { position: [0, 0, 0], rotation: QuaternionO.zeroRotation() }, [0, 0, 0], { method: 'CCD' }) 278 | 279 | expect(links).toStrictEqual(linksCopy) 280 | }) 281 | 282 | it('Reduces distance to target each time it is called', () => { 283 | const links: Link[] = [{ rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }] 284 | const target: V3 = [0, 50, 0] 285 | 286 | const base: JointTransform = { position: [0, 0, 0], rotation: QuaternionO.zeroRotation() } 287 | 288 | solveAndCheckDidImprove(links, base, target, 1, 'CCD') 289 | }) 290 | 291 | it('Reduces distance to target each time it is called with complex chain', () => { 292 | const links: Link[] = [ 293 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }, 294 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }, 295 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }, 296 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }, 297 | ] 298 | const target: V3 = [0, 50, 0] 299 | 300 | const base: JointTransform = { position: [0, 0, 0], rotation: QuaternionO.zeroRotation() } 301 | 302 | solveAndCheckDidImprove(links, base, target, 1, 'CCD') 303 | }) 304 | 305 | it('Should not improve if output of previous step is not used as input to following step', () => { 306 | const links: Link[] = [ 307 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }, 308 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }, 309 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }, 310 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0] }, 311 | ] 312 | const target: V3 = [0, 50, 0] 313 | 314 | const base: JointTransform = { position: [0, 0, 0], rotation: QuaternionO.zeroRotation() } 315 | 316 | const options: SolveOptions = { 317 | acceptedError: 0, 318 | method: 'CCD', 319 | } 320 | 321 | for (let index = 0; index < 3; index++) { 322 | const errorBefore = getErrorDistance(links, base, target) 323 | solve(links, base, target, options) 324 | const errorAfter = Solve3D.getErrorDistance(links, base, target) 325 | expect(errorBefore).toEqual(errorAfter) 326 | } 327 | }) 328 | 329 | it('Respects no rotation unary constraint', () => { 330 | const links: Link[] = [ 331 | { rotation: QuaternionO.zeroRotation(), position: [50, 0, 0], constraints: { yaw: 0, roll: 0, pitch: 0 } }, 332 | ] 333 | const target: V3 = [0, 50, 0] 334 | const base: JointTransform = { position: [0, 0, 0], rotation: QuaternionO.zeroRotation() } 335 | 336 | solveAndCheckDidNotImprove(links, base, target, 3, 'CCD') 337 | }) 338 | 339 | it('Respects no rotation binary constraint', () => { 340 | const links: Link[] = [ 341 | { 342 | rotation: QuaternionO.zeroRotation(), 343 | position: [50, 0, 0], 344 | constraints: { yaw: { min: 0, max: 0 }, roll: { min: 0, max: 0 }, pitch: { min: 0, max: 0 } }, 345 | }, 346 | ] 347 | const target: V3 = [0, 50, 0] 348 | const base: JointTransform = { position: [0, 0, 0], rotation: QuaternionO.zeroRotation() } 349 | 350 | solveAndCheckDidNotImprove(links, base, target, 3, 'CCD') 351 | }) 352 | 353 | it('Respects binary constraint', () => { 354 | let links: Link[] = [ 355 | { 356 | rotation: QuaternionO.zeroRotation(), 357 | position: [1, 0, 0], 358 | constraints: { 359 | yaw: 0, 360 | // Roll about z, causes x points vector to rotate to point up at y 361 | roll: { min: -Math.PI / 4, max: Math.PI / 4 }, 362 | pitch: 0, 363 | }, 364 | }, 365 | ] 366 | const target: V3 = [0, 1, 0] 367 | const base: JointTransform = { position: [0, 0, 0], rotation: QuaternionO.zeroRotation() } 368 | 369 | let error: number 370 | let lastError = getErrorDistance(links, base, target) 371 | while (true) { 372 | const result = solve(links, base, target, { learningRate: 10e-3, method: 'CCD' }) 373 | links = result.links 374 | error = result.getErrorDistance() 375 | 376 | const errorDifference = lastError - error 377 | const didNotImprove = errorDifference <= 0 378 | if (didNotImprove) break 379 | 380 | lastError = error 381 | } 382 | 383 | // Length 1, pointing 45 degrees on way from x to y 384 | const expectedError = V3O.euclideanDistance(target, [0.7071, 0.7071, 0]) 385 | expect(error).toBeCloseTo(expectedError) 386 | 387 | const jointTransforms = getJointTransforms(links, base) 388 | 389 | // Quaternion from euler rotation about z 45 degrees 390 | expect(jointTransforms.transforms[1]?.rotation).toBeCloseToQuaternion([0, 0, 0.3826834, 0.9238795]) 391 | }) 392 | }) 393 | 394 | function cloneDeep(object: T): T { 395 | return JSON.parse(JSON.stringify(object)) 396 | } 397 | 398 | function solveAndCheckDidImprove( 399 | links: Link[], 400 | base: JointTransform, 401 | target: V3, 402 | times: number, 403 | method: 'CCD' | 'FABRIK', 404 | ) { 405 | const options: SolveOptions = { 406 | acceptedError: 0, 407 | method, 408 | } 409 | 410 | let solveResult: undefined | SolveResult 411 | 412 | for (let index = 0; index < times; index++) { 413 | const linksThisIteration = solveResult?.links ?? links 414 | const errorBefore = getErrorDistance(linksThisIteration, base, target) 415 | solveResult = solve(linksThisIteration, base, target, options) 416 | const errorAfter = solveResult.getErrorDistance() 417 | expect(errorBefore).toBeGreaterThan(errorAfter) 418 | } 419 | } 420 | 421 | function solveAndCheckDidNotImprove( 422 | links: Link[], 423 | base: JointTransform, 424 | target: V3, 425 | times: number, 426 | method: 'CCD' | 'FABRIK', 427 | ) { 428 | const options: SolveOptions = { 429 | acceptedError: 0, 430 | method, 431 | } 432 | 433 | let solveResult: undefined | SolveResult 434 | 435 | for (let index = 0; index < times; index++) { 436 | const linksThisIteration = solveResult?.links ?? links 437 | const errorBefore = getErrorDistance(linksThisIteration, base, target) 438 | solveResult = solve(linksThisIteration, base, target, options) 439 | const errorAfter = solveResult.getErrorDistance() 440 | expect(errorBefore).not.toBeGreaterThan(errorAfter) 441 | } 442 | } 443 | 444 | declare global { 445 | namespace jest { 446 | interface Matchers { 447 | toBeCloseToQuaternion(expected: Quaternion): R 448 | } 449 | } 450 | } 451 | 452 | expect.extend({ 453 | toBeCloseToQuaternion(received: Quaternion, expected: Quaternion, precision = 2) { 454 | let pass = true 455 | received.forEach((_, index) => { 456 | const receivedComponent = received[index]! 457 | const expectedComponent = expected[index]! 458 | let expectedDiff = 0 459 | let receivedDiff = 0 460 | 461 | if (pass === false) return 462 | 463 | if (receivedComponent === Infinity && expectedComponent === Infinity) { 464 | return 465 | } else if (receivedComponent === -Infinity && expectedComponent === -Infinity) { 466 | return 467 | } else { 468 | expectedDiff = Math.pow(10, -precision) / 2 469 | receivedDiff = Math.abs(expectedComponent - receivedComponent) 470 | pass = receivedDiff < expectedDiff 471 | } 472 | }) 473 | 474 | if (pass) { 475 | return { 476 | message: () => 477 | `expected ${received.map((number) => number.toPrecision(precision))} not to be close to ${expected.map( 478 | (number) => number.toPrecision(precision), 479 | )}`, 480 | pass: true, 481 | } 482 | } else { 483 | return { 484 | message: () => 485 | `expected ${received.map((number) => number.toPrecision(precision))} to be close to ${expected.map( 486 | (number) => number.toPrecision(precision), 487 | )}`, 488 | pass: false, 489 | } 490 | } 491 | }, 492 | }) 493 | -------------------------------------------------------------------------------- /tests/V3O.test.ts: -------------------------------------------------------------------------------- 1 | import { Quaternion, V3, V3O } from '../src' 2 | 3 | const oneOnRootTwo = 1 / Math.pow(2, 0.5) 4 | 5 | describe('Vector3 Operations', () => { 6 | it('Can create vector from polar coordinates', () => { 7 | const radius = 1 8 | 9 | const angle: Quaternion = [0, oneOnRootTwo, 0, oneOnRootTwo] 10 | expect(V3O.fromPolar(radius, angle)).toBeCloseToV3([0, 0, -1]) 11 | }) 12 | 13 | it('Can create polar from vector coordinates', () => { 14 | const input: V3 = [0, 10, 0] 15 | const expected = [10, [0, 0, oneOnRootTwo, oneOnRootTwo] as Quaternion] as const 16 | 17 | expect(V3O.toPolar(input)).toStrictEqual(expected) 18 | }) 19 | 20 | it('Can chain toPolar and fromPolar without error', () => { 21 | const input: V3 = [1, 1, 1] 22 | 23 | expect(V3O.fromPolar(...V3O.toPolar(input))).toBeCloseToV3(input) 24 | }) 25 | 26 | it('Can chain fromPolar and toPolar without error', () => { 27 | const input = [1, [0, 0, 1, 0] as Quaternion] as const 28 | const expected: V3 = [-1, 0, 0] 29 | 30 | expect(V3O.fromPolar(...V3O.toPolar(V3O.fromPolar(...input)))).toBeCloseToV3(expected) 31 | }) 32 | 33 | it('Can create vector from polar coordinates, in each axis', () => { 34 | // X 35 | { 36 | const radius = 1 37 | const angle: Quaternion = [0, 0, 0, 1] 38 | expect(V3O.fromPolar(radius, angle)).toBeCloseToV3([1, 0, 0]) 39 | } 40 | 41 | // Y 42 | { 43 | const radius = 1 44 | const angle: Quaternion = [0, 0, oneOnRootTwo, oneOnRootTwo] 45 | expect(V3O.fromPolar(radius, angle)).toBeCloseToV3([0, 1, 0]) 46 | } 47 | 48 | // Z 49 | { 50 | const radius = 1 51 | const angle: Quaternion = [0, oneOnRootTwo, 0, oneOnRootTwo] 52 | expect(V3O.fromPolar(radius, angle)).toBeCloseToV3([0, 0, -1]) 53 | } 54 | 55 | // -X 56 | // Just rolls on own axis so no movement 57 | { 58 | const radius = 1 59 | const angle: Quaternion = [1, 0, 0, 0] 60 | expect(V3O.fromPolar(radius, angle)).toBeCloseToV3([1, 0, 0]) 61 | } 62 | 63 | // -Y 64 | { 65 | const radius = 1 66 | const angle: Quaternion = [0, 1, 0, 0] 67 | expect(V3O.fromPolar(radius, angle)).toBeCloseToV3([-1, 0, 0]) 68 | } 69 | 70 | // -Z 71 | { 72 | const radius = 1 73 | const angle: Quaternion = [0, 0, 1, 0] 74 | expect(V3O.fromPolar(radius, angle)).toBeCloseToV3([-1, 0, 0]) 75 | } 76 | }) 77 | 78 | it('Can rotate a vector', () => { 79 | const vector: V3 = [1, 0, 0] 80 | const rotation: Quaternion = [0, oneOnRootTwo, 0, oneOnRootTwo] 81 | 82 | const expected: V3 = [0, 0, -1] 83 | 84 | expect(V3O.rotate(vector, rotation)).toBeCloseToV3(expected) 85 | }) 86 | 87 | it('Can rotate a vector in each axis', () => { 88 | // vector in x direction, rotating 90 degrees about y, points in z 89 | { 90 | const vector: V3 = [1, 0, 0] 91 | const rotation: Quaternion = [0, oneOnRootTwo, 0, oneOnRootTwo] 92 | const expected: V3 = [0, 0, -1] 93 | expect(V3O.rotate(vector, rotation)).toBeCloseToV3(expected) 94 | } 95 | 96 | // vector in x direction, rotating 90 degrees about z, points in y 97 | { 98 | const vector: V3 = [1, 0, 0] 99 | const rotation: Quaternion = [0, 0, oneOnRootTwo, oneOnRootTwo] 100 | const expected: V3 = [0, 1, 0] 101 | expect(V3O.rotate(vector, rotation)).toBeCloseToV3(expected) 102 | } 103 | 104 | // vector in y direction, rotating 90 degrees about z, points in -x 105 | { 106 | const vector: V3 = [0, 1, 0] 107 | const rotation: Quaternion = [0, 0, oneOnRootTwo, oneOnRootTwo] 108 | const expected: V3 = [-1, 0, 0] 109 | expect(V3O.rotate(vector, rotation)).toBeCloseToV3(expected) 110 | } 111 | 112 | // vector in z direction, rotating 90 degrees about x, points in y 113 | { 114 | const vector: V3 = [0, 0, 1] 115 | const rotation: Quaternion = [oneOnRootTwo, 0, 0, oneOnRootTwo] 116 | const expected: V3 = [0, -1, 0] 117 | expect(V3O.rotate(vector, rotation)).toBeCloseToV3(expected) 118 | } 119 | }) 120 | }) 121 | -------------------------------------------------------------------------------- /tests/setupTests.ts: -------------------------------------------------------------------------------- 1 | import { Quaternion, V3 } from 'src' 2 | declare global { 3 | namespace jest { 4 | interface Matchers { 5 | toBeCloseToV3(expected: V3): R 6 | toBeCloseToQuaternion(expected: Quaternion): R 7 | } 8 | } 9 | } 10 | 11 | expect.extend({ 12 | toBeCloseToV3(received: V3, expected: V3, precision = 2) { 13 | let pass = true 14 | received.forEach((_, index) => { 15 | const receivedComponent = received[index]! 16 | const expectedComponent = expected[index]! 17 | let expectedDiff = 0 18 | let receivedDiff = 0 19 | 20 | if (pass === false) return 21 | 22 | if (receivedComponent === Infinity && expectedComponent === Infinity) { 23 | return 24 | } else if (receivedComponent === -Infinity && expectedComponent === -Infinity) { 25 | return 26 | } else { 27 | expectedDiff = Math.pow(10, -precision) / 2 28 | receivedDiff = Math.abs(expectedComponent - receivedComponent) 29 | pass = receivedDiff < expectedDiff 30 | } 31 | }) 32 | 33 | if (pass) { 34 | return { 35 | message: () => 36 | `expected ${toPrecision(received, precision)} not to be close to ${toPrecision(expected, precision)}`, 37 | pass: true, 38 | } 39 | } else { 40 | return { 41 | message: () => 42 | `expected ${toPrecision(received, precision)} to be close to ${toPrecision(expected, precision)}`, 43 | 44 | pass: false, 45 | } 46 | } 47 | }, 48 | }) 49 | 50 | expect.extend({ 51 | toBeCloseToQuaternion(received: Quaternion, expected: Quaternion, precision = 2) { 52 | let pass = true 53 | received.forEach((_, index) => { 54 | const receivedComponent = received[index]! 55 | const expectedComponent = expected[index]! 56 | let expectedDiff = 0 57 | let receivedDiff = 0 58 | 59 | if (pass === false) return 60 | 61 | if (receivedComponent === Infinity && expectedComponent === Infinity) { 62 | return 63 | } else if (receivedComponent === -Infinity && expectedComponent === -Infinity) { 64 | return 65 | } else { 66 | expectedDiff = Math.pow(10, -precision) / 2 67 | receivedDiff = Math.abs(expectedComponent - receivedComponent) 68 | pass = receivedDiff < expectedDiff 69 | } 70 | }) 71 | 72 | if (pass) { 73 | return { 74 | message: () => 75 | `expected ${toPrecision(received, precision)} not to be close to ${toPrecision(expected, precision)}`, 76 | pass: true, 77 | } 78 | } else { 79 | return { 80 | message: () => 81 | `expected ${toPrecision(received, precision)} to be close to ${toPrecision(expected, precision)}`, 82 | pass: false, 83 | } 84 | } 85 | }, 86 | }) 87 | 88 | function toPrecision(array: readonly number[], precision: number): string[] { 89 | return array.map((component) => component.toPrecision(precision)) 90 | } 91 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "typeRoots": [".", "../node_modules/@types"] 6 | }, 7 | "include": ["."] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "ES2020", 5 | "moduleResolution": "node", 6 | "baseUrl": "./", 7 | "outDir": "dist", 8 | "esModuleInterop": true, 9 | "pretty": true, 10 | "strict": true, 11 | "skipLibCheck": true, 12 | "declaration": true, 13 | "noUncheckedIndexedAccess": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitOverride": true, 17 | "forceConsistentCasingInFileNames": true 18 | }, 19 | "include": ["src"], 20 | "exclude": ["node_modules", "dist"] 21 | } 22 | --------------------------------------------------------------------------------