Merge pull request #19 from runeharlyk/testing

🪖 Major refactor of controller app
This commit is contained in:
Rune Harlyk
2024-02-23 13:57:14 +01:00
committed by GitHub
50 changed files with 2261 additions and 1437 deletions
+5 -2
View File
@@ -1,9 +1,12 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"> <meta
name="viewport"
content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"
/>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
+4 -1
View File
@@ -9,6 +9,7 @@
"build": "cross-env FOR_EMBEDDED=true vite build", "build": "cross-env FOR_EMBEDDED=true vite build",
"build:web": "cross-env FOR_EMBEDDED=false vite build --mode WEB", "build:web": "cross-env FOR_EMBEDDED=false vite build --mode WEB",
"preview": "vite preview", "preview": "vite preview",
"test": "vitest",
"check": "svelte-check --tsconfig ./tsconfig.json", "check": "svelte-check --tsconfig ./tsconfig.json",
"format": "prettier --plugin-search-dir . --write ." "format": "prettier --plugin-search-dir . --write ."
}, },
@@ -32,10 +33,12 @@
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vite": "^5.0.12", "vite": "^5.0.12",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"vite-plugin-singlefile": "^1.0.0" "vite-plugin-singlefile": "^1.0.0",
"vitest": "^1.3.1"
}, },
"dependencies": { "dependencies": {
"nipplejs": "^0.10.1", "nipplejs": "^0.10.1",
"prettier-plugin-svelte": "^3.2.1",
"svelte-routing": "^2.11.0", "svelte-routing": "^2.11.0",
"three": "^0.160.1", "three": "^0.160.1",
"urdf-loader": "^0.12.1", "urdf-loader": "^0.12.1",
+316 -21
View File
@@ -8,6 +8,9 @@ dependencies:
nipplejs: nipplejs:
specifier: ^0.10.1 specifier: ^0.10.1
version: 0.10.1 version: 0.10.1
prettier-plugin-svelte:
specifier: ^3.2.1
version: 3.2.1(prettier@3.2.4)(svelte@4.2.9)
svelte-routing: svelte-routing:
specifier: ^2.11.0 specifier: ^2.11.0
version: 2.11.0 version: 2.11.0
@@ -85,6 +88,9 @@ devDependencies:
vite-plugin-singlefile: vite-plugin-singlefile:
specifier: ^1.0.0 specifier: ^1.0.0
version: 1.0.0(rollup@4.9.6)(vite@5.0.12) version: 1.0.0(rollup@4.9.6)(vite@5.0.12)
vitest:
specifier: ^1.3.1
version: 1.3.1
packages: packages:
@@ -104,7 +110,6 @@ packages:
dependencies: dependencies:
'@jridgewell/gen-mapping': 0.3.3 '@jridgewell/gen-mapping': 0.3.3
'@jridgewell/trace-mapping': 0.3.18 '@jridgewell/trace-mapping': 0.3.18
dev: true
/@esbuild/aix-ppc64@0.19.12: /@esbuild/aix-ppc64@0.19.12:
resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==}
@@ -370,6 +375,13 @@ packages:
resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==}
dev: true dev: true
/@jest/schemas@29.6.3:
resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
'@sinclair/typebox': 0.27.8
dev: true
/@jridgewell/gen-mapping@0.3.3: /@jridgewell/gen-mapping@0.3.3:
resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
@@ -377,32 +389,26 @@ packages:
'@jridgewell/set-array': 1.1.2 '@jridgewell/set-array': 1.1.2
'@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/sourcemap-codec': 1.4.15
'@jridgewell/trace-mapping': 0.3.18 '@jridgewell/trace-mapping': 0.3.18
dev: true
/@jridgewell/resolve-uri@3.1.0: /@jridgewell/resolve-uri@3.1.0:
resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
dev: true
/@jridgewell/set-array@1.1.2: /@jridgewell/set-array@1.1.2:
resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
dev: true
/@jridgewell/sourcemap-codec@1.4.14: /@jridgewell/sourcemap-codec@1.4.14:
resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==}
dev: true
/@jridgewell/sourcemap-codec@1.4.15: /@jridgewell/sourcemap-codec@1.4.15:
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
dev: true
/@jridgewell/trace-mapping@0.3.18: /@jridgewell/trace-mapping@0.3.18:
resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==}
dependencies: dependencies:
'@jridgewell/resolve-uri': 3.1.0 '@jridgewell/resolve-uri': 3.1.0
'@jridgewell/sourcemap-codec': 1.4.14 '@jridgewell/sourcemap-codec': 1.4.14
dev: true
/@nodelib/fs.scandir@2.1.5: /@nodelib/fs.scandir@2.1.5:
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
@@ -529,6 +535,10 @@ packages:
dev: true dev: true
optional: true optional: true
/@sinclair/typebox@0.27.8:
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
dev: true
/@steeze-ui/heroicons@2.2.3: /@steeze-ui/heroicons@2.2.3:
resolution: {integrity: sha512-bv4YK375U9TDS50SmJ5VdFy2UmMhgKbJCCbo6IUU3qs2luEj2VX6sQoDy96j8AIKttxMd62ih1j0lZj7CTjIfA==} resolution: {integrity: sha512-bv4YK375U9TDS50SmJ5VdFy2UmMhgKbJCCbo6IUU3qs2luEj2VX6sQoDy96j8AIKttxMd62ih1j0lZj7CTjIfA==}
dev: true dev: true
@@ -575,7 +585,6 @@ packages:
/@types/estree@1.0.5: /@types/estree@1.0.5:
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
dev: true
/@types/json-schema@7.0.12: /@types/json-schema@7.0.12:
resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==}
@@ -742,6 +751,45 @@ packages:
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
dev: true dev: true
/@vitest/expect@1.3.1:
resolution: {integrity: sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==}
dependencies:
'@vitest/spy': 1.3.1
'@vitest/utils': 1.3.1
chai: 4.4.1
dev: true
/@vitest/runner@1.3.1:
resolution: {integrity: sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==}
dependencies:
'@vitest/utils': 1.3.1
p-limit: 5.0.0
pathe: 1.1.2
dev: true
/@vitest/snapshot@1.3.1:
resolution: {integrity: sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==}
dependencies:
magic-string: 0.30.5
pathe: 1.1.2
pretty-format: 29.7.0
dev: true
/@vitest/spy@1.3.1:
resolution: {integrity: sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==}
dependencies:
tinyspy: 2.2.1
dev: true
/@vitest/utils@1.3.1:
resolution: {integrity: sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==}
dependencies:
diff-sequences: 29.6.3
estree-walker: 3.0.3
loupe: 2.3.7
pretty-format: 29.7.0
dev: true
/acorn-jsx@5.3.2(acorn@8.11.3): /acorn-jsx@5.3.2(acorn@8.11.3):
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies: peerDependencies:
@@ -750,11 +798,15 @@ packages:
acorn: 8.11.3 acorn: 8.11.3
dev: true dev: true
/acorn-walk@8.3.2:
resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==}
engines: {node: '>=0.4.0'}
dev: true
/acorn@8.11.3: /acorn@8.11.3:
resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
hasBin: true hasBin: true
dev: true
/ajv@6.12.6: /ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@@ -787,6 +839,11 @@ packages:
dependencies: dependencies:
color-convert: 2.0.1 color-convert: 2.0.1
/ansi-styles@5.2.0:
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
engines: {node: '>=10'}
dev: true
/ansi-styles@6.2.1: /ansi-styles@6.2.1:
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -816,13 +873,16 @@ packages:
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
dependencies: dependencies:
dequal: 2.0.3 dequal: 2.0.3
dev: true
/array-union@2.1.0: /array-union@2.1.0:
resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/assertion-error@1.1.0:
resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
dev: true
/autoprefixer@10.4.17(postcss@8.4.33): /autoprefixer@10.4.17(postcss@8.4.33):
resolution: {integrity: sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==} resolution: {integrity: sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
@@ -843,7 +903,6 @@ packages:
resolution: {integrity: sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==} resolution: {integrity: sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==}
dependencies: dependencies:
dequal: 2.0.3 dequal: 2.0.3
dev: true
/balanced-match@1.0.2: /balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@@ -889,6 +948,11 @@ packages:
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
dev: true dev: true
/cac@6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'}
dev: true
/callsites@3.1.0: /callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -903,6 +967,19 @@ packages:
resolution: {integrity: sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==} resolution: {integrity: sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==}
dev: true dev: true
/chai@4.4.1:
resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==}
engines: {node: '>=4'}
dependencies:
assertion-error: 1.1.0
check-error: 1.0.3
deep-eql: 4.1.3
get-func-name: 2.0.2
loupe: 2.3.7
pathval: 1.1.1
type-detect: 4.0.8
dev: true
/chalk@4.1.2: /chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -916,6 +993,12 @@ packages:
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
dev: true dev: true
/check-error@1.0.3:
resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==}
dependencies:
get-func-name: 2.0.2
dev: true
/chokidar@3.5.3: /chokidar@3.5.3:
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
engines: {node: '>= 8.10.0'} engines: {node: '>= 8.10.0'}
@@ -963,7 +1046,6 @@ packages:
acorn: 8.11.3 acorn: 8.11.3
estree-walker: 3.0.3 estree-walker: 3.0.3
periscopic: 3.1.0 periscopic: 3.1.0
dev: true
/color-convert@2.0.1: /color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
@@ -1015,7 +1097,6 @@ packages:
dependencies: dependencies:
mdn-data: 2.0.30 mdn-data: 2.0.30
source-map-js: 1.0.2 source-map-js: 1.0.2
dev: true
/cssesc@3.0.0: /cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
@@ -1035,6 +1116,13 @@ packages:
ms: 2.1.2 ms: 2.1.2
dev: true dev: true
/deep-eql@4.1.3:
resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==}
engines: {node: '>=6'}
dependencies:
type-detect: 4.0.8
dev: true
/deep-is@0.1.4: /deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
dev: true dev: true
@@ -1047,7 +1135,6 @@ packages:
/dequal@2.0.3: /dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true
/detect-indent@6.1.0: /detect-indent@6.1.0:
resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
@@ -1058,6 +1145,11 @@ packages:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
dev: true dev: true
/diff-sequences@29.6.3:
resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dev: true
/dir-glob@3.0.1: /dir-glob@3.0.1:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1224,7 +1316,6 @@ packages:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
dependencies: dependencies:
'@types/estree': 1.0.5 '@types/estree': 1.0.5
dev: true
/esutils@2.0.3: /esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
@@ -1361,6 +1452,10 @@ packages:
engines: {node: '>=18'} engines: {node: '>=18'}
dev: true dev: true
/get-func-name@2.0.2:
resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==}
dev: true
/get-stream@8.0.1: /get-stream@8.0.1:
resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
engines: {node: '>=16'} engines: {node: '>=16'}
@@ -1537,7 +1632,6 @@ packages:
resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==} resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==}
dependencies: dependencies:
'@types/estree': 1.0.5 '@types/estree': 1.0.5
dev: true
/is-stream@3.0.0: /is-stream@3.0.0:
resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
@@ -1553,6 +1647,10 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/js-tokens@8.0.3:
resolution: {integrity: sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==}
dev: true
/js-yaml@4.1.0: /js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true hasBin: true
@@ -1572,6 +1670,10 @@ packages:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
dev: true dev: true
/jsonc-parser@3.2.1:
resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==}
dev: true
/jsonfile@6.1.0: /jsonfile@6.1.0:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
dependencies: dependencies:
@@ -1644,9 +1746,16 @@ packages:
wrap-ansi: 9.0.0 wrap-ansi: 9.0.0
dev: true dev: true
/local-pkg@0.5.0:
resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==}
engines: {node: '>=14'}
dependencies:
mlly: 1.6.0
pkg-types: 1.0.3
dev: true
/locate-character@3.0.0: /locate-character@3.0.0:
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
dev: true
/locate-path@6.0.0: /locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
@@ -1670,6 +1779,12 @@ packages:
wrap-ansi: 9.0.0 wrap-ansi: 9.0.0
dev: true dev: true
/loupe@2.3.7:
resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==}
dependencies:
get-func-name: 2.0.2
dev: true
/lru-cache@6.0.0: /lru-cache@6.0.0:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -1682,11 +1797,9 @@ packages:
engines: {node: '>=12'} engines: {node: '>=12'}
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/sourcemap-codec': 1.4.15
dev: true
/mdn-data@2.0.30: /mdn-data@2.0.30:
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
dev: true
/merge-stream@2.0.0: /merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
@@ -1748,6 +1861,15 @@ packages:
minimist: 1.2.8 minimist: 1.2.8
dev: true dev: true
/mlly@1.6.0:
resolution: {integrity: sha512-YOvg9hfYQmnaB56Yb+KrJE2u0Yzz5zR+sLejEvF4fzwzV1Al6hkf2vyHTwqCRyv0hCi9rVCqVoXpyYevQIRwLQ==}
dependencies:
acorn: 8.11.3
pathe: 1.1.2
pkg-types: 1.0.3
ufo: 1.4.0
dev: true
/mri@1.2.0: /mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -1849,6 +1971,13 @@ packages:
yocto-queue: 0.1.0 yocto-queue: 0.1.0
dev: true dev: true
/p-limit@5.0.0:
resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==}
engines: {node: '>=18'}
dependencies:
yocto-queue: 1.0.0
dev: true
/p-locate@5.0.0: /p-locate@5.0.0:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -1892,13 +2021,20 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/pathe@1.1.2:
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
dev: true
/pathval@1.1.1:
resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
dev: true
/periscopic@3.1.0: /periscopic@3.1.0:
resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==}
dependencies: dependencies:
'@types/estree': 1.0.5 '@types/estree': 1.0.5
estree-walker: 3.0.3 estree-walker: 3.0.3
is-reference: 3.0.2 is-reference: 3.0.2
dev: true
/picocolors@1.0.0: /picocolors@1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
@@ -1925,6 +2061,14 @@ packages:
engines: {node: '>= 6'} engines: {node: '>= 6'}
dev: true dev: true
/pkg-types@1.0.3:
resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==}
dependencies:
jsonc-parser: 3.2.1
mlly: 1.6.0
pathe: 1.1.2
dev: true
/postcss-import@15.1.0(postcss@8.4.33): /postcss-import@15.1.0(postcss@8.4.33):
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@@ -2000,10 +2144,28 @@ packages:
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
dev: true dev: true
/prettier-plugin-svelte@3.2.1(prettier@3.2.4)(svelte@4.2.9):
resolution: {integrity: sha512-ENAPbIxASf2R79IZwgkG5sBdeNA9kLRlXVvKKmTXh79zWTy0KKoT86XO2pHrTitUPINd+iXWy12MRmgzKGVckA==}
peerDependencies:
prettier: ^3.0.0
svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0
dependencies:
prettier: 3.2.4
svelte: 4.2.9
dev: false
/prettier@3.2.4: /prettier@3.2.4:
resolution: {integrity: sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==} resolution: {integrity: sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==}
engines: {node: '>=14'} engines: {node: '>=14'}
hasBin: true hasBin: true
/pretty-format@29.7.0:
resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
'@jest/schemas': 29.6.3
ansi-styles: 5.2.0
react-is: 18.2.0
dev: true dev: true
/punycode@2.3.1: /punycode@2.3.1:
@@ -2015,6 +2177,10 @@ packages:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
dev: true dev: true
/react-is@18.2.0:
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
dev: true
/read-cache@1.0.0: /read-cache@1.0.0:
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
dependencies: dependencies:
@@ -2143,6 +2309,10 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
dev: true
/signal-exit@3.0.7: /signal-exit@3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
dev: true dev: true
@@ -2186,6 +2356,13 @@ packages:
/source-map-js@1.0.2: /source-map-js@1.0.2:
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
/stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
dev: true
/std-env@3.7.0:
resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==}
dev: true dev: true
/string-argv@0.3.2: /string-argv@0.3.2:
@@ -2241,6 +2418,12 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/strip-literal@2.0.0:
resolution: {integrity: sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==}
dependencies:
js-tokens: 8.0.3
dev: true
/sucrase@3.33.0: /sucrase@3.33.0:
resolution: {integrity: sha512-ARGC7vbufOHfpvyGcZZXFaXCMZ9A4fffOGC5ucOW7+WHDGlAe8LJdf3Jts1sWhDeiI1RSWrKy5Hodl+JWGdW2A==} resolution: {integrity: sha512-ARGC7vbufOHfpvyGcZZXFaXCMZ9A4fffOGC5ucOW7+WHDGlAe8LJdf3Jts1sWhDeiI1RSWrKy5Hodl+JWGdW2A==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -2380,7 +2563,6 @@ packages:
locate-character: 3.0.0 locate-character: 3.0.0
magic-string: 0.30.5 magic-string: 0.30.5
periscopic: 3.1.0 periscopic: 3.1.0
dev: true
/tailwindcss@3.4.1: /tailwindcss@3.4.1:
resolution: {integrity: sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==} resolution: {integrity: sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==}
@@ -2434,6 +2616,20 @@ packages:
resolution: {integrity: sha512-Bgl2wPJypDOZ1stAxwfWAcJ0WQf7QzlptsxkjYiURPz+n5k4RBDLsq+6f9Y75TYxn6aHLcWz+JNmwTOXWrQTBQ==} resolution: {integrity: sha512-Bgl2wPJypDOZ1stAxwfWAcJ0WQf7QzlptsxkjYiURPz+n5k4RBDLsq+6f9Y75TYxn6aHLcWz+JNmwTOXWrQTBQ==}
dev: false dev: false
/tinybench@2.6.0:
resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==}
dev: true
/tinypool@0.8.2:
resolution: {integrity: sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==}
engines: {node: '>=14.0.0'}
dev: true
/tinyspy@2.2.1:
resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==}
engines: {node: '>=14.0.0'}
dev: true
/to-regex-range@5.0.1: /to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'} engines: {node: '>=8.0'}
@@ -2465,6 +2661,11 @@ packages:
prelude-ls: 1.2.1 prelude-ls: 1.2.1
dev: true dev: true
/type-detect@4.0.8:
resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
engines: {node: '>=4'}
dev: true
/type-fest@0.20.2: /type-fest@0.20.2:
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -2481,6 +2682,10 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/ufo@1.4.0:
resolution: {integrity: sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==}
dev: true
/universalify@2.0.0: /universalify@2.0.0:
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
@@ -2519,6 +2724,27 @@ packages:
resolution: {integrity: sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==} resolution: {integrity: sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==}
dev: false dev: false
/vite-node@1.3.1:
resolution: {integrity: sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
dependencies:
cac: 6.7.14
debug: 4.3.4
pathe: 1.1.2
picocolors: 1.0.0
vite: 5.0.12
transitivePeerDependencies:
- '@types/node'
- less
- lightningcss
- sass
- stylus
- sugarss
- supports-color
- terser
dev: true
/vite-plugin-compression@0.5.1(vite@5.0.12): /vite-plugin-compression@0.5.1(vite@5.0.12):
resolution: {integrity: sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==} resolution: {integrity: sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==}
peerDependencies: peerDependencies:
@@ -2590,6 +2816,61 @@ packages:
vite: 5.0.12 vite: 5.0.12
dev: true dev: true
/vitest@1.3.1:
resolution: {integrity: sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@types/node': ^18.0.0 || >=20.0.0
'@vitest/browser': 1.3.1
'@vitest/ui': 1.3.1
happy-dom: '*'
jsdom: '*'
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
'@types/node':
optional: true
'@vitest/browser':
optional: true
'@vitest/ui':
optional: true
happy-dom:
optional: true
jsdom:
optional: true
dependencies:
'@vitest/expect': 1.3.1
'@vitest/runner': 1.3.1
'@vitest/snapshot': 1.3.1
'@vitest/spy': 1.3.1
'@vitest/utils': 1.3.1
acorn-walk: 8.3.2
chai: 4.4.1
debug: 4.3.4
execa: 8.0.1
local-pkg: 0.5.0
magic-string: 0.30.5
pathe: 1.1.2
picocolors: 1.0.0
std-env: 3.7.0
strip-literal: 2.0.0
tinybench: 2.6.0
tinypool: 0.8.2
vite: 5.0.12
vite-node: 1.3.1
why-is-node-running: 2.2.2
transitivePeerDependencies:
- less
- lightningcss
- sass
- stylus
- sugarss
- supports-color
- terser
dev: true
/which@2.0.2: /which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -2598,6 +2879,15 @@ packages:
isexe: 2.0.0 isexe: 2.0.0
dev: true dev: true
/why-is-node-running@2.2.2:
resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==}
engines: {node: '>=8'}
hasBin: true
dependencies:
siginfo: 2.0.0
stackback: 0.0.2
dev: true
/wrap-ansi@7.0.0: /wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -2668,3 +2958,8 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
dev: true dev: true
/yocto-queue@1.0.0:
resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}
engines: {node: '>=12.20'}
dev: true
+23 -23
View File
@@ -2,40 +2,40 @@
import { Router, Route } from 'svelte-routing'; import { Router, Route } from 'svelte-routing';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import TopBar from './components/TopBar.svelte'; import TopBar from './components/TopBar.svelte';
import { connect } from './lib/socket'; import socketService from '$lib/services/socket-service';
import Controller from './routes/Controller.svelte'; import Controller from './routes/Controller.svelte';
import FileCache from './lib/cache'; import { fileService } from '$lib/services';
import { socketLocation } from './lib/location';
import Settings from './routes/Settings.svelte'; import Settings from './routes/Settings.svelte';
import { jointNames, model } from './lib/store'; import { jointNames, model, outControllerData } from '$lib/store';
import { loadModelAsync } from './lib/modelLoader'; import { loadModelAsync } from '$lib/utilities';
import { socketLocation } from '$lib/utilities';
import type { Result } from '$lib/utilities/result';
export let url = window.location.pathname export let url = window.location.pathname;
onMount(async () => { onMount(async () => {
connect(socketLocation); socketService.connect(socketLocation);
registerFetchIntercept() socketService.addPublisher(outControllerData);
const [urdf, JOINT_NAME] = await loadModelAsync('/spot_micro.urdf.xacro') registerFetchIntercept();
jointNames.set(JOINT_NAME) const modelRes = await loadModelAsync('/spot_micro.urdf.xacro');
model.set(urdf)
if (modelRes.isOk()) {
const [urdf, JOINT_NAME] = modelRes.inner;
jointNames.set(JOINT_NAME);
model.set(urdf);
} else {
console.error(modelRes.inner, { exception: modelRes.exception });
}
}); });
const registerFetchIntercept = () => { const registerFetchIntercept = () => {
const { fetch: originalFetch } = window; const { fetch: originalFetch } = window;
window.fetch = async (...args) => { window.fetch = async (...args) => {
const [resource, config] = args; const [resource, config] = args;
await FileCache.openDatabase(); let file: Result<Uint8Array | undefined, string>;
let file: BodyInit | Uint8Array | undefined | null; file = await fileService.getFile(resource.toString());
try { return file.isOk() ? new Response(file.inner) : originalFetch(resource, config);
file = await FileCache.getFile(resource.toString()); };
} catch (error) {
console.log(error);
}
return file
? new Response(file)
: originalFetch(resource, config)
}; };
}
</script> </script>
<Router {url}> <Router {url}>
+27 -16
View File
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import nipplejs from 'nipplejs'; import nipplejs from 'nipplejs';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { throttler } from '$lib/throttle'; import { throttler, toInt8 } from '$lib/utilities';
import { socket } from '$lib/socket'; import socketService from '$lib/services/socket-service';
import { emulateModel, input, outControllerData } from '$lib/store'; import { emulateModel, input, outControllerData } from '$lib/store';
let throttle = new throttler(); let throttle = new throttler();
@@ -13,7 +13,7 @@
let mode = 'rest'; // 'rest' | 'stand' | 'stand+' | 'walk' let mode = 'rest'; // 'rest' | 'stand' | 'stand+' | 'walk'
let data = new Uint8Array(6); let data = new Int8Array(6);
onMount(() => { onMount(() => {
left = nipplejs.create({ left = nipplejs.create({
@@ -25,12 +25,18 @@
}); });
left.on('move', (evt, data) => { left.on('move', (evt, data) => {
input.update(o => {o.left = data.vector; return o;}) input.update((o) => {
o.left = data.vector;
return o;
});
throttle.throttle(updateData, throttle_timing); throttle.throttle(updateData, throttle_timing);
}); });
left.on('end', (evt, data) => { left.on('end', (evt, data) => {
input.update(o => {o.left = { x: 0, y: 0 }; return o;}) input.update((o) => {
o.left = { x: 0, y: 0 };
return o;
});
throttle.throttle(updateData, throttle_timing); throttle.throttle(updateData, throttle_timing);
}); });
@@ -43,30 +49,35 @@
}); });
right.on('move', (evt, data) => { right.on('move', (evt, data) => {
input.update(o => {o.right = data.vector; return o;}) input.update((o) => {
o.right = data.vector;
return o;
});
throttle.throttle(updateData, throttle_timing); throttle.throttle(updateData, throttle_timing);
}); });
right.on('end', (evt, data) => { right.on('end', (evt, data) => {
input.update(o => {o.right = { x: 0, y: 0 }; return o;}) input.update((o) => {
o.right = { x: 0, y: 0 };
return o;
});
throttle.throttle(updateData, throttle_timing); throttle.throttle(updateData, throttle_timing);
}); });
}); });
const updateData = () => { const updateData = () => {
data[0] = 0; data[0] = 0;
data[1] = $input.left.x * 127 + 128; data[1] = toInt8($input.left.x, -1, 1);
data[2] = $input.left.y * 127 + 128; data[2] = toInt8($input.left.y, -1, 1);
data[3] = $input.right.x * 127 + 128; data[3] = toInt8($input.right.x, -1, 1);
data[4] = $input.right.y * 127 + 128; data[4] = toInt8($input.right.y, -1, 1);
data[5] = $input.height; data[5] = toInt8($input.height, 0, 100);
data[6] = $input.speed; data[6] = toInt8($input.speed, 0, 100);
outControllerData.set(data) outControllerData.set(data);
if(!$emulateModel) $socket.send(data); if (!$emulateModel) socketService.send(data);
}; };
</script> </script>
<div class="absolute top-0 left-0 w-screen h-screen"> <div class="absolute top-0 left-0 w-screen h-screen">
+22 -14
View File
@@ -1,29 +1,29 @@
<script lang="ts"> <script lang="ts">
import { isConnected, status, socket } from '$lib/socket'; import socketService from '$lib/services/socket-service';
import { Icon, Bars3, XMark, Power, Battery100, Signal, SignalSlash } from 'svelte-hero-icons'; import { Icon, Bars3, XMark, Power, Battery100, Signal, SignalSlash } from 'svelte-hero-icons';
import { emulateModel } from '$lib/store'; import { emulateModel } from '$lib/store';
import { Link, useLocation } from 'svelte-routing' import { Link, useLocation } from 'svelte-routing';
const views = ["Virtual environment", "Robot camera"] const views = ['Virtual environment', 'Robot camera'];
const modes = ["Drive", "Choreography"] const modes = ['Drive', 'Choreography'];
const location = useLocation() const location = useLocation();
let selected_view = views[0]; let selected_view = views[0];
let selected_modes = modes[0]; let selected_modes = modes[0];
let settingOpen = window.location.pathname.includes('/settings') let settingOpen = window.location.pathname.includes('/settings');
let isConnected = socketService.isConnected;
$: emulateModel.set(selected_view === views[0]) $: emulateModel.set(selected_view === views[0]);
$: settingOpen = $location.pathname.includes('/settings') $: settingOpen = $location.pathname.includes('/settings');
const stop = () => { const stop = () => {
if ($isConnected) { if ($isConnected) {
$socket.send(JSON.stringify({type:"system/stop"})) socketService.send(JSON.stringify({ type: 'system/stop' }));
}
} }
};
</script> </script>
<div class="topbar absolute left-0 top-0 w-full z-10 flex justify-between bg-zinc-800"> <div class="topbar absolute left-0 top-0 w-full z-10 flex justify-between bg-zinc-800">
<div class="flex gap-2 p-2"> <div class="flex gap-2 p-2">
{#if settingOpen} {#if settingOpen}
@@ -35,13 +35,19 @@
<Icon src={Bars3} size="32" /> <Icon src={Bars3} size="32" />
</Link> </Link>
{/if} {/if}
<select bind:value={selected_modes} class="rounded-md outline outline-2 text-zinc-200 outline-zinc-600 bg-zinc-800"> <select
bind:value={selected_modes}
class="rounded-md outline outline-2 text-zinc-200 outline-zinc-600 bg-zinc-800"
>
{#each modes as mode} {#each modes as mode}
<option>{mode}</option> <option>{mode}</option>
{/each} {/each}
</select> </select>
<select bind:value={selected_view} class="rounded-md outline outline-2 text-zinc-200 outline-zinc-600 bg-zinc-800"> <select
bind:value={selected_view}
class="rounded-md outline outline-2 text-zinc-200 outline-zinc-600 bg-zinc-800"
>
{#each views as view} {#each views as view}
<option>{view}</option> <option>{view}</option>
{/each} {/each}
@@ -53,7 +59,9 @@
<Icon src={Power} size="24" /> <Icon src={Power} size="24" />
</button> </button>
<button class="action_button"><Icon src={Battery100} size="24" /></button> <button class="action_button"><Icon src={Battery100} size="24" /></button>
<button class="action_button"><Icon src={$isConnected ? Signal : SignalSlash} size="24" /></button> <button class="action_button"
><Icon src={$isConnected ? Signal : SignalSlash} size="24" /></button
>
</div> </div>
<div> <div>
<button class="h-full w-20 bg-red-600 text-white" on:click={stop}>STOP</button> <button class="h-full w-20 bg-red-600 text-white" on:click={stop}>STOP</button>
+88 -65
View File
@@ -1,35 +1,45 @@
<script lang="ts"> <script lang="ts">
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { CanvasTexture, CircleGeometry, Mesh, MeshBasicMaterial } from 'three'; import { CanvasTexture, CircleGeometry, Mesh, MeshBasicMaterial } from 'three';
import {socket, angles, mpu } from '$lib/socket' import socketService from '$lib/services/socket-service';
import { lerp } from '$lib/utils';
import uzip from 'uzip'; import uzip from 'uzip';
import { model, outControllerData } from '$lib/store'; import { model } from '$lib/store';
import { ForwardKinematics } from '$lib/kinematic'; import { ForwardKinematics } from '$lib/kinematic';
import location from '$lib/location'; import { location } from '$lib/utilities';
import FileCache from '$lib/cache'; import { fileService } from '$lib/services';
import { servoAngles, mpu } from '$lib/stores';
import SceneBuilder from '$lib/sceneBuilder'; import SceneBuilder from '$lib/sceneBuilder';
import { lerp, degToRad } from 'three/src/math/MathUtils';
let sceneManager:SceneBuilder let sceneManager: SceneBuilder;
let canvas: HTMLCanvasElement, streamCanvas: HTMLCanvasElement, stream: HTMLImageElement let canvas: HTMLCanvasElement, streamCanvas: HTMLCanvasElement, stream: HTMLImageElement;
let context: CanvasRenderingContext2D, texture: CanvasTexture let context: CanvasRenderingContext2D, texture: CanvasTexture;
let modelAngles:number[] | Int16Array = new Array(12).fill(0) let modelAngles: number[] | Int16Array = new Array(12).fill(0);
let modelTargetAngles:number[] | Int16Array = new Array(12).fill(0) let modelTargetAngles: number[] | Int16Array = new Array(12).fill(0);
let modelBodyAngles:EulerAngle = { omega: 0, phi: 0, psi: 0 } let modelBodyAngles: EulerAngle = { omega: 0, phi: 0, psi: 0 };
let modelTargeBodyAngles:EulerAngle = { omega: 0, phi: 0, psi: 0 } let modelTargeBodyAngles: EulerAngle = { omega: 0, phi: 0, psi: 0 };
const videoStream = `//${location}/api/stream`; const videoStream = `//${location}/api/stream`;
let showModel = true, showStream = false let showModel = true,
showStream = false;
const servoNames = [ const servoNames = [
"front_left_shoulder", "front_left_leg", "front_left_foot", 'front_left_shoulder',
"front_right_shoulder", "front_right_leg", "front_right_foot", 'front_left_leg',
"rear_left_shoulder", "rear_left_leg", "rear_left_foot", 'front_left_foot',
"rear_right_shoulder", "rear_right_leg", "rear_right_foot" 'front_right_shoulder',
] 'front_right_leg',
'front_right_foot',
'rear_left_shoulder',
'rear_left_leg',
'rear_left_foot',
'rear_right_shoulder',
'rear_right_leg',
'rear_right_foot'
];
interface EulerAngle { interface EulerAngle {
omega: number; omega: number;
@@ -37,40 +47,36 @@ interface EulerAngle {
psi: number; psi: number;
} }
const degToRad = (val:number) => val * (Math.PI / 180)
onMount(async () => { onMount(async () => {
await cacheModelFiles() await cacheModelFiles();
await createScene() await createScene();
outControllerData.subscribe(data => {
$socket.send(JSON.stringify({
type: "kinematic/bodystate",
angles:[0, (data[1]-128)/3, (data[2]-128) / 4],
position:[(data[4]-128)/2, data[5], (data[3]-128)/2]}))
})
}); });
onDestroy(() => { onDestroy(() => {
canvas.remove() canvas.remove();
}) });
const cacheModelFiles = async () => { const cacheModelFiles = async () => {
let data = await fetch("/stl.zip").then(data => data.arrayBuffer()) let data = await fetch('/stl.zip').then((data) => data.arrayBuffer());
var files = uzip.parse(data); var files = uzip.parse(data);
await FileCache.openDatabase()
for (const [path, data] of Object.entries(files) as [path: string, data: Uint8Array][]) { for (const [path, data] of Object.entries(files) as [path: string, data: Uint8Array][]) {
const url = new URL(path, window.location.href) const url = new URL(path, window.location.href);
FileCache.saveFile(url.toString(), data) fileService.saveFile(url.toString(), data);
}
} }
};
const updateAngles = (name: string, angle: number) => { const updateAngles = (name: string, angle: number) => {
modelTargetAngles[servoNames.indexOf(name)] = angle * (180/Math.PI) modelTargetAngles[servoNames.indexOf(name)] = angle * (180 / Math.PI);
$socket.send(JSON.stringify({type:"kinematic/angle", angle:angle * (180/Math.PI), id:servoNames.indexOf(name)})) socketService.send(
} JSON.stringify({
type: 'kinematic/angle',
angle: angle * (180 / Math.PI),
id: servoNames.indexOf(name)
})
);
};
const createScene = async () => { const createScene = async () => {
sceneManager = new SceneBuilder() sceneManager = new SceneBuilder()
@@ -88,51 +94,68 @@ const createScene = async () => {
.addDragControl(updateAngles) .addDragControl(updateAngles)
.handleResize() .handleResize()
.addRenderCb(render) .addRenderCb(render)
.startRenderLoop() .startRenderLoop();
addVideoStream() addVideoStream();
} };
const addVideoStream = () => { const addVideoStream = () => {
context = streamCanvas.getContext("2d"); context = streamCanvas.getContext('2d');
texture = new CanvasTexture(stream); texture = new CanvasTexture(stream);
const liveStream = new Mesh( new CircleGeometry(35, 32), new MeshBasicMaterial({ map: texture })) const liveStream = new Mesh(
liveStream.position.z = -50 new CircleGeometry(35, 32),
liveStream.visible = showStream new MeshBasicMaterial({ map: texture })
sceneManager.scene.add(liveStream) );
} liveStream.position.z = -50;
liveStream.visible = showStream;
sceneManager.scene.add(liveStream);
};
const handleVideoStream = () => { const handleVideoStream = () => {
if(!showStream) return if (!showStream) return;
context.drawImage(stream, 0, 0) context.drawImage(stream, 0, 0);
texture.needsUpdate = true; texture.needsUpdate = true;
} };
const render = () => { const render = () => {
const robot = sceneManager.model const robot = sceneManager.model;
if(!robot) return if (!robot) return;
const forwardKinematics = new ForwardKinematics() const forwardKinematics = new ForwardKinematics();
const points = forwardKinematics.calculateFootpoints(modelAngles.map(ang => degToRad(ang)) as number[]) const points = forwardKinematics.calculateFootpoints(
robot.position.y = Math.max(...points.map(coord => coord[0] / 100)) - 2.7 modelAngles.map((ang) => degToRad(ang)) as number[]
robot.rotation.z = lerp(robot.rotation.z, degToRad($mpu.heading + 90), 0.1) );
modelTargetAngles = $angles robot.position.y = Math.max(...points.map((coord) => coord[0] / 100)) - 2.7;
robot.rotation.z = lerp(robot.rotation.z, degToRad($mpu.heading + 90), 0.1);
modelTargetAngles = $servoAngles;
handleVideoStream() handleVideoStream();
for (let i = 0; i < servoNames.length; i++) { for (let i = 0; i < servoNames.length; i++) {
modelAngles[i] = lerp(robot.joints[servoNames[i]].angle * (180/Math.PI), modelTargetAngles[i], 0.1) modelAngles[i] = lerp(
robot.joints[servoNames[i]].angle * (180 / Math.PI),
modelTargetAngles[i],
0.1
);
robot.joints[servoNames[i]].setJointValue(degToRad(modelAngles[i])); robot.joints[servoNames[i]].setJointValue(degToRad(modelAngles[i]));
} }
modelBodyAngles.omega = lerp(robot.rotation.x * (180/Math.PI), modelTargeBodyAngles.omega - 90, 0.1) modelBodyAngles.omega = lerp(
modelBodyAngles.phi = lerp(robot.rotation.y * (180/Math.PI), modelTargeBodyAngles.phi, 0.1) robot.rotation.x * (180 / Math.PI),
modelBodyAngles.psi = lerp(robot.rotation.z * (180/Math.PI), modelTargeBodyAngles.psi + 90, 0.1) modelTargeBodyAngles.omega - 90,
} 0.1
);
modelBodyAngles.phi = lerp(robot.rotation.y * (180 / Math.PI), modelTargeBodyAngles.phi, 0.1);
modelBodyAngles.psi = lerp(
robot.rotation.z * (180 / Math.PI),
modelTargeBodyAngles.psi + 90,
0.1
);
};
</script> </script>
<svelte:window on:resize={sceneManager.handleResize}></svelte:window> <svelte:window on:resize={sceneManager.handleResize} />
{#if showStream} {#if showStream}
<img <img
+1 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import location from '$lib/location'; import { location } from '$lib/utilities';
let videoStream = `//${location}/api/stream`; let videoStream = `//${location}/api/stream`;
+26 -13
View File
@@ -13,7 +13,7 @@
let servos: any[] = []; let servos: any[] = [];
onMount(() => { onMount(() => {
jointNames.subscribe(data => { jointNames.subscribe((data) => {
servos = data.map((name: string, i: number) => { servos = data.map((name: string, i: number) => {
return { return {
id: i, id: i,
@@ -23,9 +23,8 @@
pwmFor180: 0 pwmFor180: 0
}; };
}); });
}) });
}) });
let selectedServo: number | null = null; let selectedServo: number | null = null;
@@ -34,11 +33,12 @@
} }
const formatServo = (servo: Servo) => { const formatServo = (servo: Servo) => {
const string = servo.name const string = servo.name;
const name = string.charAt(0).toUpperCase() + string.split('_').join(' ').slice(1); const name = string.charAt(0).toUpperCase() + string.split('_').join(' ').slice(1);
return `${servo.id} ${name}` return `${servo.id} ${name}`;
} };
</script> </script>
<div> <div>
<div class="servo-selector"> <div class="servo-selector">
<label for="servo-select">Select Servo:</label> <label for="servo-select">Select Servo:</label>
@@ -53,19 +53,32 @@
<div class="mt-5"> <div class="mt-5">
<h2>Servo {formatServo(servos[selectedServo])} Calibration</h2> <h2>Servo {formatServo(servos[selectedServo])} Calibration</h2>
<label for="minPWM">Min PWM:</label> <label for="minPWM">Min PWM:</label>
<input type="number" id="minPWM" class="bg-zinc-800" <input
type="number"
id="minPWM"
class="bg-zinc-800"
value={servos[selectedServo].minPWM} value={servos[selectedServo].minPWM}
on:blur={(event) => updateServoValue(selectedServo, 'minPWM', Number(event.target.value))} /> on:blur={(event) => updateServoValue(selectedServo, 'minPWM', Number(event.target.value))}
/>
<label for="maxPWM">Max PWM:</label> <label for="maxPWM">Max PWM:</label>
<input type="number" id="maxPWM" class="bg-zinc-800" <input
type="number"
id="maxPWM"
class="bg-zinc-800"
value={servos[selectedServo].maxPWM} value={servos[selectedServo].maxPWM}
on:blur={(event) => updateServoValue(selectedServo, 'maxPWM', Number(event.target.value))} /> on:blur={(event) => updateServoValue(selectedServo, 'maxPWM', Number(event.target.value))}
/>
<label for="pwmFor180">PWM for 180°:</label> <label for="pwmFor180">PWM for 180°:</label>
<input type="number" id="pwmFor180" class="bg-zinc-800" <input
type="number"
id="pwmFor180"
class="bg-zinc-800"
value={servos[selectedServo].pwmFor180} value={servos[selectedServo].pwmFor180}
on:blur={(event) => updateServoValue(selectedServo, 'pwmFor180', Number(event.target.value))} /> on:blur={(event) =>
updateServoValue(selectedServo, 'pwmFor180', Number(event.target.value))}
/>
</div> </div>
{/if} {/if}
</div> </div>
@@ -1,14 +1,16 @@
<script lang="ts"> <script lang="ts">
import { socket, isConnected, settings } from "../../lib/socket"; import socketService from '$lib/services/socket-service';
import { onMount } from 'svelte' import { onMount } from 'svelte';
let isConnected = socketService.isConnected;
let settings = socketService.settings;
onMount(() => { onMount(() => {
if ($isConnected) { if ($isConnected) {
const message = JSON.stringify({type: 'system/settings'}) const message = JSON.stringify({ type: 'system/settings' });
$socket.send(message) socketService.send(message);
} }
}) });
</script> </script>
<div class="w-full h-full"> <div class="w-full h-full">
+12 -8
View File
@@ -1,22 +1,26 @@
<script lang="ts"> <script lang="ts">
import { socket, isConnected, systemInfo } from "../../lib/socket"; import { onMount } from 'svelte';
import { onMount } from 'svelte' import { humanFileSize } from '$lib/utilities';
import { humanFileSize } from "../../lib/utils"; import socketService from '$lib/services/socket-service';
let isConnected = socketService.isConnected;
let settings = socketService.settings;
let systemInfo = socketService.systemInfo;
onMount(() => { onMount(() => {
if ($isConnected) { if ($isConnected) {
const message = JSON.stringify({type: 'system/info'}) const message = JSON.stringify({ type: 'system/info' });
$socket.send(message) socketService.send(message);
} }
}) });
</script> </script>
<div class="w-full h-full"> <div class="w-full h-full">
<div class="w-1/3"> <div class="w-1/3">
{#each Object.entries($systemInfo ?? {}) as entry} {#each Object.entries($systemInfo ?? {}) as entry}
<div class="flex gap-8"> <div class="flex gap-8">
<div class="w-32">{entry[0]}:</div> <div class="w-32">{entry[0]}:</div>
{#if entry[0].includes("Size") || entry[0].includes("Free") || entry[0].includes("Min")} {#if entry[0].includes('Size') || entry[0].includes('Free') || entry[0].includes('Min')}
<div>{humanFileSize(entry[1])}</div> <div>{humanFileSize(entry[1])}</div>
{:else} {:else}
<div>{entry[1]}</div> <div>{entry[1]}</div>
+8 -6
View File
@@ -1,14 +1,16 @@
<script lang="ts"> <script lang="ts">
import { socket, isConnected, log } from "../../lib/socket"; import socketService from '$lib/services/socket-service';
import { onMount } from 'svelte' import { onMount } from 'svelte';
let isConnected = socketService.isConnected;
let log = socketService.log;
onMount(() => { onMount(() => {
if ($isConnected) { if ($isConnected) {
const message = JSON.stringify({type: 'system/logs'}) const message = JSON.stringify({ type: 'system/logs' });
$socket.send(message) socketService.send(message);
} }
}) });
</script> </script>
<div class="w-full h-full"> <div class="w-full h-full">
-102
View File
@@ -1,102 +0,0 @@
class FileCache {
private request: IDBOpenDBRequest;
private db: IDBDatabase | null = null;
private store: IDBObjectStore | null = null;
dbName = 'fileStorageDB';
dbVersion = 1;
storeName = 'files';
constructor() {
this.request = indexedDB.open(this.dbName, this.dbVersion);
this.request.onerror = (event) => {
console.error("An error occurred with IndexedDB", event);
};
this.request.onupgradeneeded = () => {
this.db = this.request.result;
this.store = this.db.createObjectStore(this.storeName);
};
this.request.onsuccess = () => {
this.db = this.request.result;
const transaction = this.db.transaction(this.storeName, "readwrite");
this.store = transaction.objectStore(this.storeName);
}
}
public isOpen(): boolean {
return this.db !== null;
}
public async saveFile(key:string, file: Uint8Array): Promise<string | IDBValidKey> {
return new Promise((resolve, reject) => {
if(!this.db) {
reject("Database not open")
return;
}
const transaction = this.db.transaction(this.storeName, "readwrite");
const store = transaction.objectStore(this.storeName);
const request = store.put(file, key);
if(!request) {
reject("Request not created")
return
}
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
public async getFile(key:string): Promise<Uint8Array | null> {
return new Promise((resolve, reject) => {
if(!key) {
reject("Key was not defined")
return;
}
if(!this.db) {
reject("Database not open")
return;
}
const transaction = this.db.transaction(this.storeName, "readwrite");
const store = transaction.objectStore(this.storeName);
const request = store.get(key);
if(!request) {
reject("Request not created")
return
}
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
public async openDatabase(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.dbVersion);
request.onerror = (event) => {
reject('Error opening database');
};
request.onsuccess = (event) => {
const db = event.target?.result;
this.db = db;
resolve(db);
};
request.onupgradeneeded = (event) => {
this.db = event.target?.result;
this.db?.createObjectStore('files', { autoIncrement: true });
};
});
}
}
export default new FileCache();
+55 -43
View File
@@ -17,26 +17,33 @@ export default class Kinematic {
this.W = 75; this.W = 75;
} }
bodyIK(omega: number, phi: number, psi: number, xm: number, ym: number, zm: number): number[][][] { bodyIK(
omega: number,
phi: number,
psi: number,
xm: number,
ym: number,
zm: number
): number[][][] {
const { cos, sin } = Math; const { cos, sin } = Math;
const Rx: number[][] = [ const Rx: number[][] = [
[1, 0, 0, 0], [1, 0, 0, 0],
[0, cos(omega), -sin(omega), 0], [0, cos(omega), -sin(omega), 0],
[0, sin(omega), cos(omega), 0], [0, sin(omega), cos(omega), 0],
[0, 0, 0, 1], [0, 0, 0, 1]
]; ];
const Ry: number[][] = [ const Ry: number[][] = [
[cos(phi), 0, sin(phi), 0], [cos(phi), 0, sin(phi), 0],
[0, 1, 0, 0], [0, 1, 0, 0],
[-sin(phi), 0, cos(phi), 0], [-sin(phi), 0, cos(phi), 0],
[0, 0, 0, 1], [0, 0, 0, 1]
]; ];
const Rz: number[][] = [ const Rz: number[][] = [
[cos(psi), -sin(psi), 0, 0], [cos(psi), -sin(psi), 0, 0],
[sin(psi), cos(psi), 0, 0], [sin(psi), cos(psi), 0, 0],
[0, 0, 1, 0], [0, 0, 1, 0],
[0, 0, 0, 1], [0, 0, 0, 1]
]; ];
const Rxyz: number[][] = this.matrixMultiply(this.matrixMultiply(Rx, Ry), Rz); const Rxyz: number[][] = this.matrixMultiply(this.matrixMultiply(Rx, Ry), Rz);
@@ -44,7 +51,7 @@ export default class Kinematic {
[0, 0, 0, xm], [0, 0, 0, xm],
[0, 0, 0, ym], [0, 0, 0, ym],
[0, 0, 0, zm], [0, 0, 0, zm],
[0, 0, 0, 0], [0, 0, 0, 0]
]; ];
const Tm: number[][] = this.matrixAdd(T, Rxyz); const Tm: number[][] = this.matrixAdd(T, Rxyz);
@@ -58,26 +65,26 @@ export default class Kinematic {
[cHp, 0, sHp, L / 2], [cHp, 0, sHp, L / 2],
[0, 1, 0, 0], [0, 1, 0, 0],
[-sHp, 0, cHp, W / 2], [-sHp, 0, cHp, W / 2],
[0, 0, 0, 1], [0, 0, 0, 1]
]), ]),
this.matrixMultiply(Tm, [ this.matrixMultiply(Tm, [
[cHp, 0, sHp, L / 2], [cHp, 0, sHp, L / 2],
[0, 1, 0, 0], [0, 1, 0, 0],
[-sHp, 0, cHp, -W / 2], [-sHp, 0, cHp, -W / 2],
[0, 0, 0, 1], [0, 0, 0, 1]
]), ]),
this.matrixMultiply(Tm, [ this.matrixMultiply(Tm, [
[cHp, 0, sHp, -L / 2], [cHp, 0, sHp, -L / 2],
[0, 1, 0, 0], [0, 1, 0, 0],
[-sHp, 0, cHp, W / 2], [-sHp, 0, cHp, W / 2],
[0, 0, 0, 1], [0, 0, 0, 1]
]), ]),
this.matrixMultiply(Tm, [ this.matrixMultiply(Tm, [
[cHp, 0, sHp, -L / 2], [cHp, 0, sHp, -L / 2],
[0, 1, 0, 0], [0, 1, 0, 0],
[-sHp, 0, cHp, -W / 2], [-sHp, 0, cHp, -W / 2],
[0, 0, 0, 1], [0, 0, 0, 1]
]), ])
]; ];
} }
@@ -90,22 +97,22 @@ export default class Kinematic {
try { try {
F = sqrt(x ** 2 + y ** 2 - l1 ** 2); F = sqrt(x ** 2 + y ** 2 - l1 ** 2);
if(isNaN(F)) throw new Error("F is NaN") if (isNaN(F)) throw new Error('F is NaN');
} catch (error) { } catch (error) {
//console.log(error) //console.log(error)
F = l1 F = l1;
} }
const G = F - l2; const G = F - l2;
const H = sqrt(G ** 2 + z ** 2); const H = sqrt(G ** 2 + z ** 2);
const theta1 = -atan2(y, x) - atan2(F, -l1); const theta1 = -atan2(y, x) - atan2(F, -l1);
const D = (H ** 2 - l3 ** 2 - l4 ** 2) / (2 * l3 * l4); const D = (H ** 2 - l3 ** 2 - l4 ** 2) / (2 * l3 * l4);
let theta3: number let theta3: number;
try { try {
theta3 = acos(D); theta3 = acos(D);
if(isNaN(theta3)) throw new Error("theta3 is NaN") if (isNaN(theta3)) throw new Error('theta3 is NaN');
} catch (error) { } catch (error) {
theta3 = 0 theta3 = 0;
} }
const theta2 = atan2(z, G) - atan2(l4 * sin(theta3), l3 + l4 * cos(theta3)); const theta2 = atan2(z, G) - atan2(l4 * sin(theta3), l3 + l4 * cos(theta3));
@@ -140,7 +147,7 @@ export default class Kinematic {
const vectorLength = vector.length; const vectorLength = vector.length;
if (cols !== vectorLength) { if (cols !== vectorLength) {
throw new Error("Matrix and vector dimensions do not match for multiplication."); throw new Error('Matrix and vector dimensions do not match for multiplication.');
} }
const result = []; const result = [];
@@ -179,32 +186,30 @@ export default class Kinematic {
const theta23 = theta2 + theta3; const theta23 = theta2 + theta3;
const T0: number[] = [0, 0, 0, 1]; const T0: number[] = [0, 0, 0, 1];
const T1: number[] = this.vectorAdd( const T1: number[] = this.vectorAdd(T0, [
T0, -this.l1 * Math.cos(theta1),
[-this.l1 * Math.cos(theta1), this.l1 * Math.sin(theta1), 0, 0] this.l1 * Math.sin(theta1),
); 0,
const T2: number[] = this.vectorAdd( 0
T1, ]);
[-this.l2 * Math.sin(theta1), -this.l2 * Math.cos(theta1), 0, 0] const T2: number[] = this.vectorAdd(T1, [
); -this.l2 * Math.sin(theta1),
const T3: number[] = this.vectorAdd( -this.l2 * Math.cos(theta1),
T2, 0,
[ 0
]);
const T3: number[] = this.vectorAdd(T2, [
-this.l3 * Math.sin(theta1) * Math.cos(theta2), -this.l3 * Math.sin(theta1) * Math.cos(theta2),
-this.l3 * Math.cos(theta1) * Math.cos(theta2), -this.l3 * Math.cos(theta1) * Math.cos(theta2),
this.l3 * Math.sin(theta2), this.l3 * Math.sin(theta2),
0, 0
] ]);
); const T4: number[] = this.vectorAdd(T3, [
const T4: number[] = this.vectorAdd(
T3,
[
-this.l4 * Math.sin(theta1) * Math.cos(theta23), -this.l4 * Math.sin(theta1) * Math.cos(theta23),
-this.l4 * Math.cos(theta1) * Math.cos(theta23), -this.l4 * Math.cos(theta1) * Math.cos(theta23),
this.l4 * Math.sin(theta23), this.l4 * Math.sin(theta23),
0, 0
] ]);
);
return [T0, T1, T2, T3, T4]; return [T0, T1, T2, T3, T4];
} }
@@ -219,14 +224,14 @@ export default class Kinematic {
[-1, 0, 0, 0], [-1, 0, 0, 0],
[0, 1, 0, 0], [0, 1, 0, 0],
[0, 0, 1, 0], [0, 0, 1, 0],
[0, 0, 0, 1], [0, 0, 0, 1]
]; ];
return [ return [
this.legIK(this.multiplyVector(this.matrixInverse(Tlf), Lp[0])), this.legIK(this.multiplyVector(this.matrixInverse(Tlf), Lp[0])),
this.legIK(this.multiplyVector(Ix, this.multiplyVector(this.matrixInverse(Trf), Lp[1]))), this.legIK(this.multiplyVector(Ix, this.multiplyVector(this.matrixInverse(Trf), Lp[1]))),
this.legIK(this.multiplyVector(this.matrixInverse(Tlb), Lp[2])), this.legIK(this.multiplyVector(this.matrixInverse(Tlb), Lp[2])),
this.legIK(this.multiplyVector(Ix, this.multiplyVector(this.matrixInverse(Trb), Lp[3]))), this.legIK(this.multiplyVector(Ix, this.multiplyVector(this.matrixInverse(Trb), Lp[3])))
]; ];
} }
@@ -255,7 +260,7 @@ export default class Kinematic {
private determinant(matrix: number[][]): number { private determinant(matrix: number[][]): number {
if (matrix.length !== matrix[0].length) { if (matrix.length !== matrix[0].length) {
throw new Error("The matrix is not square."); throw new Error('The matrix is not square.');
} }
if (matrix.length === 2) { if (matrix.length === 2) {
@@ -288,7 +293,7 @@ export default class Kinematic {
private adjugate(matrix: number[][]): number[][] { private adjugate(matrix: number[][]): number[][] {
if (matrix.length !== matrix[0].length) { if (matrix.length !== matrix[0].length) {
throw new Error("The matrix is not square."); throw new Error('The matrix is not square.');
} }
const adjugate: number[][] = []; const adjugate: number[][] = [];
@@ -341,7 +346,6 @@ export default class Kinematic {
} }
} }
export class ForwardKinematics { export class ForwardKinematics {
private l1: number; private l1: number;
private l2: number; private l2: number;
@@ -358,8 +362,16 @@ export class ForwardKinematics {
public calculateFootpoint(theta1: number, theta2: number, theta3: number): number[] { public calculateFootpoint(theta1: number, theta2: number, theta3: number): number[] {
const { cos, sin } = Math; const { cos, sin } = Math;
const x = this.l1 * cos(theta1) + this.l2 * cos(theta1) + this.l3 * cos(theta1 + theta2) + this.l4 * cos(theta1 + theta2 + theta3); const x =
const y = this.l1 * sin(theta1) + this.l2 * sin(theta1) + this.l3 * sin(theta1 + theta2) + this.l4 * sin(theta1 + theta2 + theta3); this.l1 * cos(theta1) +
this.l2 * cos(theta1) +
this.l3 * cos(theta1 + theta2) +
this.l4 * cos(theta1 + theta2 + theta3);
const y =
this.l1 * sin(theta1) +
this.l2 * sin(theta1) +
this.l3 * sin(theta1 + theta2) +
this.l4 * sin(theta1 + theta2 + theta3);
const z = 0; const z = 0;
return [x, y, z]; return [x, y, z];
-8
View File
@@ -1,8 +0,0 @@
const forWeb = import.meta.env.MODE === "WEB"
const mock = import.meta.env.MODE === "MOCK"
const location = mock ? `${window.location.hostname}:2096` : "leika.local"
export const socketLocation = forWeb ? `wss://${window.location.hostname}:2096` : `ws://${location}`
export default location;
-31
View File
@@ -1,31 +0,0 @@
import { LoaderUtils } from "three";
import URDFLoader, { type URDFRobot } from "urdf-loader"
import { XacroLoader } from "xacro-parser"
export const loadModelAsync = async (url:string):Promise<[URDFRobot, string[]]> => {
return new Promise((resolve, reject) => {
const xacroLoader = new XacroLoader();
xacroLoader.load(url, async (xml) => {
const urdfLoader = new URDFLoader();
urdfLoader.workingPath = LoaderUtils.extractUrlBase(url);
try {
const model = urdfLoader.parse(xml);
model.rotation.x = -Math.PI / 2;
model.rotation.z = Math.PI / 2;
model.traverse(c => c.castShadow = true);
model.updateMatrixWorld(true);
model.scale.setScalar(10);
const joints = Object.entries(model.joints)
.filter(joint => joint[1]._jointType !== 'fixed')
.map(joint => joint[0])
resolve([model, joints]);
} catch (error) {
reject(error);
}
}, (error) => reject(error));
});
}
+13
View File
@@ -0,0 +1,13 @@
export type angles = number[] | Int16Array;
type AnglesData = {
type: 'angles';
data: angles;
};
type LogData = {
type: 'log';
data: string;
};
export type WebSocketJsonMsg = AnglesData | LogData;
+120 -95
View File
@@ -1,4 +1,5 @@
import { Mesh, import {
Mesh,
PerspectiveCamera, PerspectiveCamera,
PlaneGeometry, PlaneGeometry,
Scene, Scene,
@@ -19,81 +20,81 @@ import { Mesh,
MeshPhongMaterial, MeshPhongMaterial,
EquirectangularReflectionMapping, EquirectangularReflectionMapping,
ACESFilmicToneMapping, ACESFilmicToneMapping,
MathUtils, MathUtils
} from "three"; } from 'three';
import { Sky } from 'three/addons/objects/Sky.js'; import { Sky } from 'three/addons/objects/Sky.js';
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { type URDFMimicJoint } from "urdf-loader"; import { type URDFJoint, type URDFMimicJoint, type URDFRobot } from 'urdf-loader';
import { PointerURDFDragControls } from 'urdf-loader/src/URDFDragControls' import { PointerURDFDragControls } from 'urdf-loader/src/URDFDragControls';
export const addScene = () => new Scene() export const addScene = () => new Scene();
interface position { interface position {
x?: number, x?: number;
y?: number, y?: number;
z?: number z?: number;
} }
interface light { interface light {
color?: ColorRepresentation, color?: ColorRepresentation;
intensity?: number intensity?: number;
} }
interface gridOptions { interface gridOptions {
divisions?: number, divisions?: number;
size?: number, size?: number;
} }
interface arrowOptions { interface arrowOptions {
origin:position, origin: position;
direction:position, direction: position;
length?:number, length?: number;
color?:ColorRepresentation color?: ColorRepresentation;
} }
type directionalLight = position & light type directionalLight = position & light;
type gridHelperOptions = gridOptions & position type gridHelperOptions = gridOptions & position;
function calculateCurrentSunElevation() { function calculateCurrentSunElevation() {
let now = new Date(); let now = new Date();
let decimalTime = now.getHours() + now.getMinutes() / 60; let decimalTime = now.getHours() + now.getMinutes() / 60;
let normalizedTime = ((decimalTime - 6) % 12) / 6 - 1; let normalizedTime = (decimalTime % 12) / 6 - 1;
return 10 * Math.sin(normalizedTime * Math.PI); return 10 * Math.sin(normalizedTime * Math.PI);
} }
export default class SceneBuilder { export default class SceneBuilder {
public scene: Scene public scene: Scene;
public camera: PerspectiveCamera public camera: PerspectiveCamera;
public ground: Mesh public ground: Mesh;
public renderer:WebGLRenderer public renderer: WebGLRenderer;
public controls:OrbitControls public controls: OrbitControls;
public callback:Function public callback: Function;
public gridHelper: GridHelper; public gridHelper: GridHelper;
public model: Object3D<Event> public model: URDFRobot;
public liveStreamTexture: CanvasTexture public liveStreamTexture: CanvasTexture;
private fog:FogExp2 private fog: FogExp2;
private isLoaded:boolean = false private isLoaded: boolean = false;
highlightMaterial: any; highlightMaterial: any;
constructor() { constructor() {
this.scene = new Scene() this.scene = new Scene();
if (this.scene.environment?.mapping) { if (this.scene.environment?.mapping) {
this.scene.environment.mapping = EquirectangularReflectionMapping; this.scene.environment.mapping = EquirectangularReflectionMapping;
} }
return this return this;
} }
public addRenderer = (parameters?: WebGLRendererParameters) => { public addRenderer = (parameters?: WebGLRendererParameters) => {
this.renderer = new WebGLRenderer(parameters); this.renderer = new WebGLRenderer(parameters);
this.renderer.outputColorSpace = "srgb"; this.renderer.outputColorSpace = 'srgb';
this.renderer.shadowMap.enabled = true; this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = PCFSoftShadowMap; this.renderer.shadowMap.type = PCFSoftShadowMap;
this.renderer.toneMapping = ACESFilmicToneMapping; this.renderer.toneMapping = ACESFilmicToneMapping;
this.renderer.toneMappingExposure = 0.85; this.renderer.toneMappingExposure = 0.85;
document.body.appendChild(this.renderer.domElement); document.body.appendChild(this.renderer.domElement);
return this return this;
} };
public addSky = () => { public addSky = () => {
const sky = new Sky(); const sky = new Sky();
@@ -120,15 +121,15 @@ export default class SceneBuilder {
sun.setFromSphericalCoords(1, phi, theta); sun.setFromSphericalCoords(1, phi, theta);
uniforms['sunPosition'].value.copy(sun); uniforms['sunPosition'].value.copy(sun);
return this return this;
} };
public addPerspectiveCamera = (options: position) => { public addPerspectiveCamera = (options: position) => {
this.camera = new PerspectiveCamera(); this.camera = new PerspectiveCamera();
this.camera.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0); this.camera.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0);
this.scene.add(this.camera); this.scene.add(this.camera);
return this return this;
} };
public addGroundPlane = (options: position) => { public addGroundPlane = (options: position) => {
this.ground = new Mesh(new PlaneGeometry(), new ShadowMaterial({ side: 2 })); this.ground = new Mesh(new PlaneGeometry(), new ShadowMaterial({ side: 2 }));
@@ -137,22 +138,22 @@ export default class SceneBuilder {
this.ground.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0); this.ground.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0);
this.ground.receiveShadow = true; this.ground.receiveShadow = true;
this.scene.add(this.ground); this.scene.add(this.ground);
return this return this;
} };
public addOrbitControls = (minDistance: number, maxDistance: number) => { public addOrbitControls = (minDistance: number, maxDistance: number) => {
this.controls = new OrbitControls(this.camera, this.renderer.domElement); this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.minDistance = minDistance; this.controls.minDistance = minDistance;
this.controls.maxDistance = maxDistance; this.controls.maxDistance = maxDistance;
this.controls.update(); this.controls.update();
return this return this;
} };
public addAmbientLight = (options: light) => { public addAmbientLight = (options: light) => {
const ambientLight = new AmbientLight(options.color, options.intensity); const ambientLight = new AmbientLight(options.color, options.intensity);
this.scene.add(ambientLight); this.scene.add(ambientLight);
return this return this;
} };
public addDirectionalLight = (options: directionalLight) => { public addDirectionalLight = (options: directionalLight) => {
const directionalLight = new DirectionalLight(options.color, options.intensity); const directionalLight = new DirectionalLight(options.color, options.intensity);
@@ -161,10 +162,10 @@ export default class SceneBuilder {
directionalLight.shadow.mapSize.width = 1024; directionalLight.shadow.mapSize.width = 1024;
directionalLight.shadow.mapSize.height = 1024; directionalLight.shadow.mapSize.height = 1024;
directionalLight.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0); directionalLight.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0);
directionalLight.shadow.radius = 5 directionalLight.shadow.radius = 5;
this.scene.add(directionalLight); this.scene.add(directionalLight);
return this return this;
} };
public addGridHelper = (options: gridHelperOptions) => { public addGridHelper = (options: gridHelperOptions) => {
this.gridHelper = new GridHelper(options.size, options.divisions); this.gridHelper = new GridHelper(options.size, options.divisions);
@@ -173,55 +174,68 @@ export default class SceneBuilder {
this.gridHelper.material.depthWrite = false; this.gridHelper.material.depthWrite = false;
this.gridHelper.material.transparent = true; this.gridHelper.material.transparent = true;
this.scene.add(this.gridHelper); this.scene.add(this.gridHelper);
return this return this;
} };
public addFogExp2 = (color: ColorRepresentation, density?: number) => { public addFogExp2 = (color: ColorRepresentation, density?: number) => {
this.scene.fog = new FogExp2(color, density); this.scene.fog = new FogExp2(color, density);
return this return this;
} };
public handleResize = () => { public handleResize = () => {
this.renderer.setSize(window.innerWidth, window.innerHeight); this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setPixelRatio(window.devicePixelRatio); this.renderer.setPixelRatio(window.devicePixelRatio);
this.camera.aspect = window.innerWidth / window.innerHeight; this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix(); this.camera.updateProjectionMatrix();
return this return this;
} };
public addRenderCb = (callback: Function) => { public addRenderCb = (callback: Function) => {
this.callback = callback this.callback = callback;
return this return this;
} };
public startRenderLoop = () => { public startRenderLoop = () => {
this.renderer.setAnimationLoop(() => { this.renderer.setAnimationLoop(() => {
this.renderer.render(this.scene, this.camera); this.renderer.render(this.scene, this.camera);
this.handleRobotShadow() this.handleRobotShadow();
if(this.callback) this.callback() if (this.callback) this.callback();
if(!this.liveStreamTexture) return if (!this.liveStreamTexture) return;
}); });
return this return this;
} };
public addArrowHelper = (options?: arrowOptions) => { public addArrowHelper = (options?: arrowOptions) => {
const dir = new Vector3(options?.direction.x ?? 0, options?.direction.y ?? 0, options?.direction.z ?? 0); const dir = new Vector3(
const origin = new Vector3(options?.origin.x ?? 0, options?.origin.y ?? 0, options?.origin.z ?? 0); options?.direction.x ?? 0,
const arrowHelper = new ArrowHelper( dir, origin, options?.length ?? 1.5, options?.color ?? 0xff0000 ); options?.direction.y ?? 0,
options?.direction.z ?? 0
);
const origin = new Vector3(
options?.origin.x ?? 0,
options?.origin.y ?? 0,
options?.origin.z ?? 0
);
const arrowHelper = new ArrowHelper(
dir,
origin,
options?.length ?? 1.5,
options?.color ?? 0xff0000
);
this.scene.add(arrowHelper); this.scene.add(arrowHelper);
return this return this;
} };
private setJointValue(jointName: string, angle: number) { private setJointValue(jointName: string, angle: number) {
if (!this.model) return; if (!this.model) return;
if (!this.model.joints[jointName]) return; if (!this.model.joints[jointName]) return;
this.model.joints[jointName].setJointValue(angle) this.model.joints[jointName].setJointValue(angle);
} }
isJoint = j => j.isURDFJoint && j.jointType !== 'fixed'; isJoint = (j: URDFJoint) => j.isURDFJoint && j.jointType !== 'fixed';
highlightLinkGeometry = (m: URDFMimicJoint, revert: boolean, material: MeshPhongMaterial) => { highlightLinkGeometry = (m: URDFMimicJoint, revert: boolean, material: MeshPhongMaterial) => {
const traverse = c => { const traverse = (c: any) => {
if (c.type === 'Mesh') { if (c.type === 'Mesh') {
if (revert) { if (revert) {
c.material = c.__origMaterial; c.material = c.__origMaterial;
@@ -245,49 +259,60 @@ export default class SceneBuilder {
}; };
public addModel = (model: any) => { public addModel = (model: any) => {
this.model = model this.model = model;
this.scene.add(model) this.scene.add(model);
return this return this;
} };
public addDragControl = (updateAngle: any) => { public addDragControl = (updateAngle: any) => {
const highlightColor = '#FFFFFF' const highlightColor = '#FFFFFF';
const highlightMaterial = const highlightMaterial = new MeshPhongMaterial({
new MeshPhongMaterial({
shininess: 10, shininess: 10,
color: highlightColor, color: highlightColor,
emissive: highlightColor, emissive: highlightColor,
emissiveIntensity: 0.25, emissiveIntensity: 0.25
}); });
const dragControls = new PointerURDFDragControls(this.scene, this.camera, this.renderer.domElement); const dragControls = new PointerURDFDragControls(
this.scene,
this.camera,
this.renderer.domElement
);
dragControls.updateJoint = (joint: URDFMimicJoint, angle: number) => { dragControls.updateJoint = (joint: URDFMimicJoint, angle: number) => {
this.setJointValue(joint.name, angle); this.setJointValue(joint.name, angle);
updateAngle(joint.name, angle) updateAngle(joint.name, angle);
}; };
dragControls.onDragStart = () => this.controls.enabled = false; dragControls.onDragStart = () => (this.controls.enabled = false);
dragControls.onDragEnd = () => this.controls.enabled = true; dragControls.onDragEnd = () => (this.controls.enabled = true);
dragControls.onHover = (joint:URDFMimicJoint) => this.highlightLinkGeometry(joint, false, highlightMaterial); dragControls.onHover = (joint: URDFMimicJoint) =>
dragControls.onUnhover = (joint:URDFMimicJoint) => this.highlightLinkGeometry(joint, true, highlightMaterial); this.highlightLinkGeometry(joint, false, highlightMaterial);
dragControls.onUnhover = (joint: URDFMimicJoint) =>
this.highlightLinkGeometry(joint, true, highlightMaterial);
this.renderer.domElement.addEventListener('touchstart', (data) => dragControls._mouseDown(data.touches[0])); this.renderer.domElement.addEventListener('touchstart', (data) =>
this.renderer.domElement.addEventListener('touchmove', (data) => dragControls._mouseMove(data.touches[0])) dragControls._mouseDown(data.touches[0])
this.renderer.domElement.addEventListener('touchup', (data) => dragControls._mouseUp(data.touches[0])); );
return this this.renderer.domElement.addEventListener('touchmove', (data) =>
} dragControls._mouseMove(data.touches[0])
);
this.renderer.domElement.addEventListener('touchend', (data) =>
dragControls._mouseUp(data.touches[0])
);
return this;
};
public toggleFog = () => { public toggleFog = () => {
this.scene.fog = this.scene.fog ? null : this.fog; this.scene.fog = this.scene.fog ? null : this.fog;
} };
private handleRobotShadow = () => { private handleRobotShadow = () => {
if(this.isLoaded) return if (this.isLoaded) return;
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
this.model?.traverse(c => c.castShadow = true); this.model?.traverse((c) => (c.castShadow = true));
}, 10); }, 10);
setTimeout(() => { setTimeout(() => {
clearInterval(intervalId) clearInterval(intervalId);
}, 1000); }, 1000);
this.isLoaded = true; this.isLoaded = true;
} };
} }
+71
View File
@@ -0,0 +1,71 @@
import { Result } from '$lib/utilities/result';
class FileService {
private dbName = 'fileStorageDB';
private dbVersion = 1;
private storeName = 'files';
private dbPromise: Promise<Result<IDBDatabase, string>>;
constructor() {
this.dbPromise = this.openDatabase();
}
private async openDatabase(): Promise<Result<IDBDatabase, string>> {
return new Promise((resolve) => {
const request = indexedDB.open(this.dbName, this.dbVersion);
request.onerror = () => resolve(Result.err('Error opening database'));
request.onsuccess = () => resolve(Result.ok(request.result));
request.onupgradeneeded = (event) => {
const db = request.result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName);
}
};
});
}
private async getStore(mode: IDBTransactionMode): Promise<Result<IDBObjectStore, string>> {
const dbResult = await this.dbPromise;
if (dbResult.isErr()) {
return Result.err('Database not initialized properly');
}
const db = dbResult.inner;
const transaction = db.transaction(this.storeName, mode);
return Result.ok(transaction.objectStore(this.storeName));
}
public async saveFile(key: string, file: Uint8Array): Promise<Result<IDBValidKey, string>> {
const storeResult = await this.getStore('readwrite');
if (storeResult.isErr()) {
return Result.err('Failed to access object store for writing');
}
const store = storeResult.inner;
return new Promise((resolve) => {
const request = store.put(file, key);
request.onsuccess = () => resolve(Result.ok(request.result));
request.onerror = () => resolve(Result.err('Failed to save file'));
});
}
public async getFile(key: string): Promise<Result<Uint8Array | undefined, string>> {
const storeResult = await this.getStore('readonly');
if (storeResult.isErr()) {
return Result.err('Failed to access object store for reading');
}
const store = storeResult.inner;
return new Promise((resolve) => {
const request = store.get(key);
request.onsuccess = () =>
resolve(request.result ? Result.ok(request.result) : Result.err('File content not found'));
request.onerror = () => resolve(Result.err('Failed to retrieve file'));
});
}
}
export default new FileService();
+3
View File
@@ -0,0 +1,3 @@
export { default as fileService } from './file-service';
export { default as socketService } from './socket-service';
export { default as resultService } from './result-service';
+19
View File
@@ -0,0 +1,19 @@
import { errorLogs, latestErrorLog } from '$lib/stores';
import type { Result } from '$lib/utilities';
class ResultService {
public handleResult(result: Result<unknown, string>, tag?: string) {
if (result.isErr()) {
const errorLogEntry = { tag, message: result.inner, exception: result.exception };
latestErrorLog.set(errorLogEntry);
errorLogs.update((entries) => {
entries.push(errorLogEntry);
return entries;
});
}
return result;
}
}
export default new ResultService();
+91
View File
@@ -0,0 +1,91 @@
import { isConnected, socketData } from '$lib/stores';
import { Result, Ok } from '$lib/utilities';
import { resultService } from '$lib/services';
import { type WebSocketJsonMsg } from '$lib/models';
import type { Writable } from 'svelte/store';
type WebsocketOutData = string | ArrayBufferLike | Blob | ArrayBufferView;
// TODO
/**
* MOVE THE store to a store.ts file
*
* Make an object on the class that encapsulate all the stores
*
* Make the handle message function look up the type and set the value, to simplify the code
*/
class SocketService {
private socket!: WebSocket;
constructor() {}
public connect(url: string): void {
this.socket = new WebSocket(url);
this.socket.binaryType = 'arraybuffer';
this.socket.onopen = () => this.handleConnected();
this.socket.onclose = () => this.handleDisconnected();
this.socket.onmessage = (event: MessageEvent) =>
resultService.handleResult(this.handleMessage(event), 'SocketService');
this.socket.onerror = (error: Event) => console.log(error);
}
public send(data: WebsocketOutData): Result<void, string> {
if (this.socket.readyState === WebSocket.OPEN) {
this.socket.send(data);
return Ok.void();
}
return Result.err('The connection is not open');
}
public addPublisher(store: Writable<WebsocketOutData>, type?: string) {
store.subscribe((data) => this.send(type ? JSON.stringify({ type, data }) : data));
}
private handleConnected(): void {
isConnected.set(true);
}
private handleDisconnected(): void {
isConnected.set(false);
}
private getJsonFromMessage(msg: string): Result<WebSocketJsonMsg, string> {
try {
return Result.ok(JSON.parse(msg) as WebSocketJsonMsg);
} catch (error) {
return Result.err('Failed to parse socket message', error);
}
}
private handleBufferMessage(buffer: ArrayBuffer): Result<void, string> {
console.log(buffer);
return Ok.void();
}
private handleMessage(event: MessageEvent): Result<void, string> {
if (event.data instanceof ArrayBuffer) {
return this.handleBufferMessage(event.data);
}
let msgRes = this.getJsonFromMessage(event.data);
if (msgRes.isErr()) {
return msgRes;
}
const msg = msgRes.inner;
if (msg.type === 'log') {
socketData.logs.update((entries) => {
entries.push(msg.data);
return entries;
});
return Ok.void();
} else if (msg.data && msg.type in socketData) {
socketData[msg.type].set(msg.data);
return Ok.void();
}
return Result.err(`Got invalid msg: ${JSON.stringify(msg)}`);
}
}
export default new SocketService();
-92
View File
@@ -1,92 +0,0 @@
import { writable, type Writable } from 'svelte/store';
export type WebSocketStatus = 'OPEN' | 'CONNECTING' | 'CLOSED'
export const isConnected = writable(false)
export const angles = writable(new Int16Array(12).fill(0))
export const log = writable([])
export const battery = writable({})
export const mpu = writable({heading:0})
export const distances = writable({})
export const settings = writable({})
export const systemInfo = writable({} as number)
export const dataBuffer = writable(new Float32Array(13))
export const servoBuffer:Writable<Int16Array|number[]> = writable(new Int16Array(12))
export const data = writable();
export const status:Writable<WebSocketStatus> = writable('CLOSED')
export const socket:Writable<WebSocket> = writable()
export const connect = (url:string) => {
status.set('CONNECTING')
let _socket = new WebSocket(url);
_socket.binaryType = "arraybuffer";
_socket.onopen = _connected;
_socket.onclose = _disconnected;
_socket.onmessage = _message;
socket.set(_socket)
servoBuffer.subscribe(data => {
if(_socket.readyState !== 1) return
const buffer = []
buffer[0] = 1
buffer.push(...data)
_socket.send(new Int16Array(buffer))
})
}
const _connected = () => {
status.set('OPEN')
isConnected.set(true)
}
const _disconnected = () => {
status.set('CLOSED')
isConnected.set(false)
}
const _message = (event:any) => {
if (event.data instanceof ArrayBuffer) {
let buffer = new Int8Array(event.data);
if(buffer.length === 44) {
dataBuffer.set(new Float32Array(buffer.buffer) )
}
} else {
let data = event.data
try {
data = JSON.parse(event.data)
} catch (error) {
console.warn(error)
}
switch (data.type) {
case "angles":
angles.set(data.angles)
break
case "logs":
log.set(data.logs)
break
case "log":
log.update(entries => {entries.push(data.log); return entries})
break
case "settings":
settings.set(data.settings)
case "info":
systemInfo.set(data.info)
break
case "mpu":
mpu.set(data.mpu)
break
case "distances":
distances.set(data.distances)
break
case "battery":
battery.set(data.battery)
break
}
}
}
+10 -5
View File
@@ -1,12 +1,17 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import { persistentStore } from './utils'; import { persistentStore } from '$lib/utilities';
export const emulateModel = writable(true); export const emulateModel = writable(true);
export const input = writable({left:{x:0, y:0}, right:{x:0, y:0}, height:70, speed:0}); export const input = writable({
left: { x: 0, y: 0 },
right: { x: 0, y: 0 },
height: 70,
speed: 0
});
export const outControllerData = writable(new Uint8Array([0, 128, 128, 128, 128, 70, 0])); export const outControllerData = writable(new Int8Array([0, 0, 0, 0, 0, 70, 0]));
export const jointNames = persistentStore("joint_names", []) export const jointNames = persistentStore('joint_names', []);
export const model = writable() export const model = writable();
+2
View File
@@ -0,0 +1,2 @@
export * from './socket-store';
export * from './logging-store';
+11
View File
@@ -0,0 +1,11 @@
import { writable, type Writable } from 'svelte/store';
export interface errorLog {
message: unknown;
tag?: string;
exception?: unknown;
}
export const latestErrorLog: Writable<errorLog> = writable();
export const errorLogs: Writable<errorLog[]> = writable([]);
+31
View File
@@ -0,0 +1,31 @@
import { writable, type Writable } from 'svelte/store';
import { type angles } from '$lib/models';
export const isConnected = writable(false);
export const servoAngles: Writable<angles> = writable(new Int16Array(12).fill(0));
export const logs = writable([] as string[]);
export const battery = writable({});
export const mpu = writable({ heading: 0 });
export const distances = writable({});
export const settings = writable({});
export const systemInfo = writable({} as number);
export interface socketDataCollection {
angles: Writable<angles>;
logs: Writable<string[]>;
battery: Writable<unknown>;
mpu: Writable<unknown>;
distances: Writable<unknown>;
settings: Writable<unknown>;
systemInfo: Writable<unknown>;
}
export const socketData = {
angles: servoAngles,
logs,
battery,
mpu,
distances,
settings,
systemInfo
};
-15
View File
@@ -1,15 +0,0 @@
export class throttler {
private _throttlePause: boolean;
constructor() {
this._throttlePause = false;
}
throttle = (callback:Function, time:number) => {
if (this._throttlePause) return;
this._throttlePause = true;
setTimeout(() => {
callback();
this._throttlePause = false;
}, time);
};
}
+15
View File
@@ -0,0 +1,15 @@
export class throttler {
private _throttlePause: boolean;
constructor() {
this._throttlePause = false;
}
throttle = (callback: Function, time: number) => {
if (this._throttlePause) return;
this._throttlePause = true;
setTimeout(() => {
callback();
this._throttlePause = false;
}, time);
};
}
+7
View File
@@ -0,0 +1,7 @@
export * from './result';
export * from './string-utilities';
export * from './svelte-utilities';
export * from './math-utilities';
export * from './buffer-utilities';
export * from './model-utilities';
export * from './location-utilities';
@@ -0,0 +1,8 @@
const forWeb = import.meta.env.MODE === 'WEB';
const mock = import.meta.env.MODE === 'MOCK';
export const location = mock ? `${window.location.hostname}:2096` : 'leika.local';
export const socketLocation = forWeb
? `wss://${window.location.hostname}:2096`
: `ws://${location}`;
+11
View File
@@ -0,0 +1,11 @@
export const toUint8 = (number: number, min: number, max: number) => {
number = Math.max(min, Math.min(max, number));
let scaled = ((number - min) / (max - min)) * 255;
return Math.round(scaled) & 0xff;
};
export const toInt8 = (number: number, min: number, max: number) => {
number = Math.max(min, Math.min(max, number));
let scaled = ((number - min) / (max - min)) * 255 - 128;
return Math.max(-128, Math.min(127, Math.round(scaled))) | 0;
};
+38
View File
@@ -0,0 +1,38 @@
import { LoaderUtils } from 'three';
import URDFLoader, { type URDFRobot } from 'urdf-loader';
import { XacroLoader } from 'xacro-parser';
import { Result } from '$lib/utilities';
export const loadModelAsync = async (
url: string
): Promise<Result<[URDFRobot, string[]], string>> => {
return new Promise((resolve, reject) => {
const xacroLoader = new XacroLoader();
xacroLoader.load(
url,
async (xml) => {
const urdfLoader = new URDFLoader();
urdfLoader.workingPath = LoaderUtils.extractUrlBase(url);
try {
const model = urdfLoader.parse(xml);
model.rotation.x = -Math.PI / 2;
model.rotation.z = Math.PI / 2;
model.traverse((c) => (c.castShadow = true));
model.updateMatrixWorld(true);
model.scale.setScalar(10);
const joints = Object.entries(model.joints)
.filter((joint) => joint[1].jointType !== 'fixed')
.map((joint) => joint[0]);
resolve(Result.ok([model, joints]));
} catch (error) {
resolve(Result.err('Failed to load model', error));
}
},
(error) => reject(error)
);
});
};
+42
View File
@@ -0,0 +1,42 @@
export class Err<T, U> {
#inner: T;
#exception?: U;
constructor(inner: T, exception?: U) {
this.#inner = inner;
this.#exception = exception;
}
get inner(): T {
return this.#inner;
}
get exception(): U | undefined {
return this.#exception;
}
/**
* Type guard for `Ok`
* @returns `true` if `Ok`; `false` if `Err`
*/
isOk(): false {
return false;
}
/**
* Type guard for `Err`
* @returns `true` if `Err`; `false` if `Ok`
*/
isErr(): this is Err<T, U> {
return true;
}
/**
* Create an `Err`
* @param inner
* @returns `Err(inner)`
*/
static new<E, F>(inner: E, exception: F): Err<E, F> {
return new Err<E, F>(inner, exception);
}
}
+3
View File
@@ -0,0 +1,3 @@
export * from './err';
export * from './ok';
export * from './result';
+44
View File
@@ -0,0 +1,44 @@
export class Ok<T> {
#inner: T;
constructor(inner: T) {
this.#inner = inner;
}
get inner(): T {
return this.#inner;
}
/**
* Type guard for `Ok`
* @returns `true` if `Ok`; `false` if `Err`
*/
isOk(): this is Ok<T> {
return true;
}
/**
* Type guard for `Err`
* @returns `true` if `Err`; `false` if `Ok`
*/
isErr(): false {
return false;
}
/**
* Create an `Ok`
* @param inner
* @returns `Ok(inner)`
*/
static new<T>(inner: T): Ok<T> {
return new Ok<T>(inner);
}
/**
* Create an empty `Ok`
* @returns `Ok(void)`
*/
static void(): Ok<void> {
return new Ok(undefined);
}
}
+20
View File
@@ -0,0 +1,20 @@
import { Err } from './err';
import { Ok } from './ok';
export type Result<T = unknown, E = unknown, F = unknown> = Ok<T> | Err<E, F>;
export namespace Result {
/**
* @returns `Ok<T>`
*/
export function ok<T = unknown>(value: T) {
return Ok.new(value);
}
/**
* @returns `Err<E, F>`
*/
export function err<E = unknown, F = unknown>(error: E, exception?: F) {
return Err.new(error, exception);
}
}
@@ -0,0 +1,5 @@
export const humanFileSize = (size: number): string => {
const units = ['B', 'kB', 'MB', 'GB', 'TB'];
var i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
return Number((size / Math.pow(1024, i)).toFixed(2)) * 1 + units[i];
};
+13
View File
@@ -0,0 +1,13 @@
import { writable } from 'svelte/store';
export const persistentStore = (key: string, initialValue: any) => {
const savedValue = JSON.parse(localStorage.getItem(key) as string);
const data = savedValue !== null ? savedValue : initialValue;
const store = writable(data);
store.subscribe((value) => {
localStorage.setItem(key, JSON.stringify(value));
});
return store;
};
-22
View File
@@ -1,22 +0,0 @@
import { writable } from 'svelte/store';
export const humanFileSize = (size:number):string => {
var i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
return Number((size / Math.pow(1024, i)).toFixed(2)) * 1 + ['B', 'kB', 'MB', 'GB', 'TB'][i];
}
export const lerp = (start: number, end: number, amt: number) => {
return (1 - amt) * start + amt * end;
};
export const persistentStore = (key:string, initialValue:any) => {
const savedValue = JSON.parse(localStorage.getItem(key) as string);
const data = savedValue !== null ? savedValue : initialValue;
const store = writable(data);
store.subscribe(value => {
localStorage.setItem(key, JSON.stringify(value));
});
return store;
}
+12 -3
View File
@@ -3,10 +3,19 @@
import Info from '../components/settings/Info.svelte'; import Info from '../components/settings/Info.svelte';
import Log from '../components/settings/Log.svelte'; import Log from '../components/settings/Log.svelte';
import Configuration from '../components/settings/Configuration.svelte'; import Configuration from '../components/settings/Configuration.svelte';
import { Icon, Wifi, CommandLine, InformationCircle, BookOpen, AdjustmentsVertical, Cog6Tooth, Newspaper } from 'svelte-hero-icons'; import {
Icon,
Wifi,
CommandLine,
InformationCircle,
BookOpen,
AdjustmentsVertical,
Cog6Tooth,
Newspaper
} from 'svelte-hero-icons';
import Calibration from '../components/settings/Calibration.svelte'; import Calibration from '../components/settings/Calibration.svelte';
export const page = "" export const page = '';
const menu = [ const menu = [
{ {
@@ -32,7 +41,7 @@
path: '/settings', path: '/settings',
icon: Cog6Tooth, icon: Cog6Tooth,
component: Configuration component: Configuration
}, }
]; ];
</script> </script>
+7 -7
View File
@@ -4,20 +4,20 @@ export default {
theme: { theme: {
extend: { extend: {
colors: { colors: {
'primary': '#6200EE', primary: '#6200EE',
'primary-variant': '#3700B3', 'primary-variant': '#3700B3',
'secondary': '#3700B3', secondary: '#3700B3',
'secondary-variant': '#3700B3', 'secondary-variant': '#3700B3',
'background': '#1e1e1e', background: '#1e1e1e',
'surface': '#2c2c2c', surface: '#2c2c2c',
'error': '#B00020', error: '#B00020',
'on-primary': '#FFFFFF', 'on-primary': '#FFFFFF',
'on-secondary': '#FFFFFF', 'on-secondary': '#FFFFFF',
'on-background': '#FFFFFF', 'on-background': '#FFFFFF',
'on-surface': '#FFFFFF', 'on-surface': '#FFFFFF',
'on-error': '#FFFFFF', 'on-error': '#FFFFFF'
}
} }
},
}, },
plugins: [] plugins: []
}; };
+28
View File
@@ -0,0 +1,28 @@
import { describe, it, expect } from 'vitest';
import { humanFileSize } from '../../src/lib/utilities';
describe('humanFileSize', () => {
it('returns "0B" for 0 bytes', () => {
expect(humanFileSize(0)).toBe('0B');
});
it('returns the size in bytes correctly', () => {
expect(humanFileSize(500)).toBe('500B');
});
it('returns the size in kB correctly', () => {
expect(humanFileSize(1024)).toBe('1kB');
});
it('returns the size in MB correctly', () => {
expect(humanFileSize(1048576)).toBe('1MB'); // 1024 * 1024
});
it('returns the size in GB correctly', () => {
expect(humanFileSize(1073741824)).toBe('1GB'); // 1024 * 1024 * 1024
});
it('rounds to 2 decimal places correctly', () => {
expect(humanFileSize(1536)).toBe('1.5kB'); // 1024 + 512
});
});
+46
View File
@@ -0,0 +1,46 @@
import { describe, it, expect } from 'vitest';
import { toUint8, toInt8 } from '../../src/lib/utilities';
describe('toUint8', () => {
it('min interval value should get 0', () => {
expect(toUint8(-1, -1, 1)).toBe(0);
});
it('middle interval value should get 128', () => {
expect(toUint8(0, -1, 1)).toBe(128);
});
it('max interval value should get 255', () => {
expect(toUint8(1, -1, 1)).toBe(255);
});
it('min value should be clamped', () => {
expect(toUint8(-2, -1, 1)).toBe(0);
});
it('max value should be clamped', () => {
expect(toUint8(2, -1, 1)).toBe(255);
});
});
describe('toInt8', () => {
it('min interval value should get -128', () => {
expect(toInt8(-1, -1, 1)).toBe(-128);
});
it('middle interval value should get 0', () => {
expect(toInt8(0, -1, 1)).toBe(0);
});
it('max interval value should get 127', () => {
expect(toInt8(1, -1, 1)).toBe(127);
});
it('min value should be clamped', () => {
expect(toInt8(-2, -1, 1)).toBe(-128);
});
it('max value should be clamped', () => {
expect(toInt8(2, -1, 1)).toBe(127);
});
});
+39
View File
@@ -0,0 +1,39 @@
import { describe, it, expect } from 'vitest';
import { Result } from '../../src/lib/utilities';
describe('Result', () => {
it('should create a success result correctly', () => {
const successValue = 'Success value';
const result = Result.ok(successValue);
expect(result.isOk()).toBe(true);
expect(result.isErr()).toBe(false);
expect(result.inner).toBe(successValue);
});
it('should create an error result correctly', () => {
const errorMessage = 'Error message';
const result = Result.err(errorMessage);
expect(result.isOk()).toBe(false);
expect(result.isErr()).toBe(true);
expect(result.inner).toBe(errorMessage);
});
it('should type guard success and error results correctly', () => {
const successResult = Result.ok(123);
const errorResult = Result.err('Error');
if (successResult.isOk()) {
expect(typeof successResult.inner).toBe('number');
} else {
throw new Error('Expected successResult to be ok');
}
if (errorResult.isErr()) {
expect(typeof errorResult.inner).toBe('string');
} else {
throw new Error('Expected errorResult to be fail');
}
});
});
+46
View File
@@ -0,0 +1,46 @@
import { throttler } from '../../src/lib/utilities';
import { describe, it, expect, beforeEach, afterEach, test, vitest } from 'vitest';
describe('throttler', () => {
let throttleInstance: throttler;
let callback;
beforeEach(() => {
vitest.useFakeTimers();
throttleInstance = new throttler();
callback = vitest.fn();
});
afterEach(() => {
vitest.useRealTimers();
});
it('should call the callback function after the specified time', () => {
throttleInstance.throttle(callback, 1000);
expect(callback).not.toHaveBeenCalled();
vitest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalledTimes(1);
});
it('should not call the callback function if throttle is called again within the timeout period', () => {
throttleInstance.throttle(callback, 1000);
throttleInstance.throttle(callback, 1000);
vitest.advanceTimersByTime(500);
expect(callback).not.toHaveBeenCalled();
vitest.advanceTimersByTime(500);
expect(callback).toHaveBeenCalledTimes(1);
});
it('should allow the callback to be called again after the timeout period', () => {
throttleInstance.throttle(callback, 1000);
vitest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalledTimes(1);
throttleInstance.throttle(callback, 1000);
vitest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalledTimes(2);
});
});
-1
View File
@@ -21,7 +21,6 @@
"$components/*": ["./src/components/*"], "$components/*": ["./src/components/*"],
"$stores/*": ["./src/stores/*"] "$stores/*": ["./src/stores/*"]
} }
}, },
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }]
+12 -10
View File
@@ -2,24 +2,26 @@ import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte'; import { svelte } from '@sveltejs/vite-plugin-svelte';
import { viteSingleFile } from 'vite-plugin-singlefile'; import { viteSingleFile } from 'vite-plugin-singlefile';
import viteCompression from 'vite-plugin-compression'; import viteCompression from 'vite-plugin-compression';
import path from 'path' import path from 'path';
const forEmbedded = process.env.FOR_EMBEDDED == 'true' const forEmbedded = process.env.FOR_EMBEDDED == 'true';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [svelte(), plugins: [
...(forEmbedded ? [ viteSingleFile(), viteCompression({deleteOriginFile: true})]: [])], svelte(),
...(forEmbedded ? [viteSingleFile(), viteCompression({ deleteOriginFile: true })] : [])
],
build: { build: {
outDir: forEmbedded ? '../data' : './build', outDir: forEmbedded ? '../data' : './build',
emptyOutDir: true emptyOutDir: true
}, },
resolve: { resolve: {
alias: { alias: {
'$lib': path.resolve('./src/lib/'), $lib: path.resolve('./src/lib/'),
'$components': path.resolve('./src/components'), $components: path.resolve('./src/components'),
'$utils': path.resolve('./src/utils'), $utils: path.resolve('./src/utils'),
'$stores': path.resolve('./src/stores'), $stores: path.resolve('./src/stores')
}, }
}, }
}); });
+88 -16
View File
@@ -5,7 +5,7 @@ import { WebSocketServer } from "ws";
const app = express(); const app = express();
const kinematic = new Kinematic(); const kinematic = new Kinematic();
const wss = new WebSocketServer({ port: 8080 }); const wss = new WebSocketServer({ port: 2096 });
app.use(cors()); app.use(cors());
app.use(express.json()); app.use(express.json());
@@ -89,6 +89,7 @@ const model = {
rssi: 100, rssi: 100,
}, },
running: true, running: true,
mode: "stand",
}; };
const settings = { const settings = {
@@ -197,12 +198,56 @@ const updateAngles = (angles) => {
return model.servos.angles; return model.servos.angles;
}; };
wss.on("connection", (ws) => { const bufferToController = (buffer) => {
const clientState = createNewClientState(); return {
ws.clientState = clientState; stop: buffer[0],
ws.on("error", console.error); lx: buffer[1],
ly: buffer[2],
rx: buffer[3],
ry: buffer[4],
h: buffer[5],
s: buffer[6],
};
};
ws.on("message", (message) => { const unpackMessageBuffer = (data) => {
return {
angles: [0, data.rx / 4, data.ry / 4],
position: [data.ly / 2, 70, data.lx / 2],
};
};
const updateStanding = (ws, controller) => {
if (!ws.clientState.model.running) return;
const data = unpackMessageBuffer(controller);
ws.send(
JSON.stringify({
type: "angles",
data: updateBodyState(ws.clientState.model, data.angles, data.position),
})
);
};
const handelController = (ws, buffer) => {
const controllerData = bufferToController(new Int8Array(buffer));
if (controllerData.stop) {
ws.clientState.model.running = false;
ws.clientState.logs.push("[2024-02-05 19:10:00] [Warning] STOPPING SERVOS");
ws.send(JSON.stringify({ type: "log", data: ws.clientState.logs.last() }));
return;
}
if (ws.clientState.model.mode === "stand") {
updateStanding(ws, controllerData);
}
};
const handleBufferMessage = (ws, buffer) => {
if (buffer.length === 6) {
handelController(ws, buffer);
}
};
const handleJsonMessage = (ws, message) => {
let data = message; let data = message;
try { try {
data = JSON.parse(message); data = JSON.parse(message);
@@ -217,10 +262,10 @@ wss.on("connection", (ws) => {
unsubscribeClientFromCategory(ws, data.category); unsubscribeClientFromCategory(ws, data.category);
break; break;
case "sensor/battery": case "sensor/battery":
ws.send({ type: "battery", battery: JSON.stringify(updateBattery()) }); ws.send({ type: "battery", data: JSON.stringify(updateBattery()) });
break; break;
case "sensor/mpu": case "sensor/mpu":
ws.send({ type: "battery", mpu: JSON.stringify(updateMpu()) }); ws.send({ type: "battery", data: JSON.stringify(updateMpu()) });
break; break;
case "sensor/distances": case "sensor/distances":
ws.send(JSON.stringify(updateDistances())); ws.send(JSON.stringify(updateDistances()));
@@ -234,7 +279,7 @@ wss.on("connection", (ws) => {
ws.send( ws.send(
JSON.stringify({ JSON.stringify({
type: "angles", type: "angles",
angles: ws.clientState.model.servos.angles, data: ws.clientState.model.servos.angles,
}) })
); );
} else { } else {
@@ -247,7 +292,7 @@ wss.on("connection", (ws) => {
ws.send( ws.send(
JSON.stringify({ JSON.stringify({
type: "angles", type: "angles",
angles: ws.clientState.model.servos.angles, data: ws.clientState.model.servos.angles,
}) })
); );
} else { } else {
@@ -259,7 +304,11 @@ wss.on("connection", (ws) => {
ws.send( ws.send(
JSON.stringify({ JSON.stringify({
type: "angles", type: "angles",
angles: updateBodyState(ws.clientState.model, data.angles, data.position), data: updateBodyState(
ws.clientState.model,
data.angles,
data.position
),
}) })
); );
} else { } else {
@@ -267,10 +316,10 @@ wss.on("connection", (ws) => {
} }
break; break;
case "system/logs": case "system/logs":
ws.send(JSON.stringify({ type: "logs", logs:ws.clientState.logs })); ws.send(JSON.stringify({ type: "logs", data: ws.clientState.logs }));
break; break;
case "system/info": case "system/info":
ws.send(JSON.stringify({ type: "info", info: updateSystem() })); ws.send(JSON.stringify({ type: "info", data: updateSystem() }));
break; break;
case "system/settings": case "system/settings":
if (data.settings) { if (data.settings) {
@@ -279,17 +328,40 @@ wss.on("connection", (ws) => {
); );
ws.send(JSON.stringify(ws.clientState.settings)); ws.send(JSON.stringify(ws.clientState.settings));
} else { } else {
ws.send(JSON.stringify({type:"settings", settings: ws.clientState.settings})); ws.send(
JSON.stringify({
type: "settings",
settings: ws.clientState.settings,
})
);
} }
break; break;
case "system/stop": case "system/stop":
ws.clientState.model.running = false; ws.clientState.model.running = false;
ws.clientState.logs.push("[2024-02-05 19:10:00] [Warning] STOPPING SERVOS") ws.clientState.logs.push(
ws.send(JSON.stringify({type:"log", log:ws.clientState.logs.last()})); "[2024-02-05 19:10:00] [Warning] STOPPING SERVOS"
);
ws.send(
JSON.stringify({ type: "log", data: ws.clientState.logs.last() })
);
break; break;
default: default:
ws.send(JSON.stringify({ error: "Unknown request type" })); ws.send(JSON.stringify({ error: "Unknown request type" }));
} }
};
wss.on("connection", (ws) => {
const clientState = createNewClientState();
ws.clientState = clientState;
ws.on("error", console.error);
ws.on("message", (message) => {
if (typeof message === "object") {
handleBufferMessage(ws, message);
return;
}
handleJsonMessage(ws, message);
}); });
ws.on("close", () => { ws.on("close", () => {