├── .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 |
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 |
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 |
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 | Distance |
27 | |
28 |
29 |
30 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 | Distance |
27 | |
28 |
29 |
30 |
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 | [](https://npmjs.com/package/inverse-kinematics)
4 | [](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 |
--------------------------------------------------------------------------------