📏 Formats app code

This commit is contained in:
Rune Harlyk
2024-02-23 09:16:20 +01:00
parent 2e1d99b1df
commit 22b54261f0
34 changed files with 1525 additions and 1400 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>
+50 -49
View File
@@ -1,51 +1,52 @@
{ {
"name": "app", "name": "app",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"dev:mock": "vite --mode MOCK", "dev:mock": "vite --mode MOCK",
"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", "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 ."
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.0.2", "@sveltejs/vite-plugin-svelte": "^3.0.2",
"@tsconfig/svelte": "^5.0.2", "@tsconfig/svelte": "^5.0.2",
"@types/three": "^0.160.0", "@types/three": "^0.160.0",
"@typescript-eslint/eslint-plugin": "^6.20.0", "@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0", "@typescript-eslint/parser": "^6.20.0",
"autoprefixer": "^10.4.17", "autoprefixer": "^10.4.17",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"husky": "^9.0.7", "husky": "^9.0.7",
"lint-staged": "^15.2.0", "lint-staged": "^15.2.0",
"postcss": "^8.4.33", "postcss": "^8.4.33",
"prettier": "3.2.4", "prettier": "3.2.4",
"svelte": "^4.2.9", "svelte": "^4.2.9",
"svelte-check": "^3.6.3", "svelte-check": "^3.6.3",
"svelte-hero-icons": "^5.0.0", "svelte-hero-icons": "^5.0.0",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"tslib": "^2.6.2", "tslib": "^2.6.2",
"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" "vitest": "^1.3.1"
}, },
"dependencies": { "dependencies": {
"nipplejs": "^0.10.1", "nipplejs": "^0.10.1",
"svelte-routing": "^2.11.0", "prettier-plugin-svelte": "^3.2.1",
"three": "^0.160.1", "svelte-routing": "^2.11.0",
"urdf-loader": "^0.12.1", "three": "^0.160.1",
"uzip": "^0.20201231.0", "urdf-loader": "^0.12.1",
"xacro-parser": "^0.3.9" "uzip": "^0.20201231.0",
}, "xacro-parser": "^0.3.9"
"lint-staged": { },
"*.js": "eslint --cache --fix", "lint-staged": {
"*.{js,css,md,ts,svelte}": "prettier --write" "*.js": "eslint --cache --fix",
} "*.{js,css,md,ts,svelte}": "prettier --write"
}
} }
+13 -23
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
@@ -107,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==}
@@ -387,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==}
@@ -589,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==}
@@ -812,7 +807,6 @@ packages:
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==}
@@ -879,7 +873,6 @@ 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==}
@@ -910,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==}
@@ -1054,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==}
@@ -1106,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==}
@@ -1145,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==}
@@ -1327,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==}
@@ -1644,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==}
@@ -1769,7 +1756,6 @@ packages:
/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==}
@@ -1811,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==}
@@ -2051,7 +2035,6 @@ packages:
'@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==}
@@ -2161,11 +2144,20 @@ 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
dev: true
/pretty-format@29.7.0: /pretty-format@29.7.0:
resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
@@ -2364,7 +2356,6 @@ 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'}
dev: true
/stackback@0.0.2: /stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
@@ -2572,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==}
+22 -24
View File
@@ -4,39 +4,37 @@
import TopBar from './components/TopBar.svelte'; import TopBar from './components/TopBar.svelte';
import socketService from '$lib/services/socket-service'; import socketService from '$lib/services/socket-service';
import Controller from './routes/Controller.svelte'; import Controller from './routes/Controller.svelte';
import fileService from '$lib/services/file-service'; import { fileService } from '$lib/services';
import Settings from './routes/Settings.svelte'; import Settings from './routes/Settings.svelte';
import { jointNames, model } from '$lib/store'; import { jointNames, model } from '$lib/store';
import { loadModelAsync } from '$lib/utilities'; import { loadModelAsync } from '$lib/utilities';
import { socketLocation } from '$lib/utilities'; import { socketLocation } from '$lib/utilities';
import type { Result } from '$lib/utilities/result'; import type { Result } from '$lib/utilities/result';
export let url = window.location.pathname export let url = window.location.pathname;
onMount(async () => { onMount(async () => {
socketService.connect(socketLocation); socketService.connect(socketLocation);
registerFetchIntercept() registerFetchIntercept();
const modelRes = await loadModelAsync('/spot_micro.urdf.xacro') const modelRes = await loadModelAsync('/spot_micro.urdf.xacro');
if (modelRes.isOk()) { if (modelRes.isOk()) {
const [urdf, JOINT_NAME] = modelRes.inner const [urdf, JOINT_NAME] = modelRes.inner;
jointNames.set(JOINT_NAME) jointNames.set(JOINT_NAME);
model.set(urdf) model.set(urdf);
} else { } else {
console.error(modelRes.inner, {"exception": modelRes.exception}) 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;
let file: Result<Uint8Array | undefined, string>; let file: Result<Uint8Array | undefined, string>;
file = await fileService.getFile(resource.toString()); file = await fileService.getFile(resource.toString());
return file.isOk() return file.isOk() ? new Response(file.inner) : originalFetch(resource, config);
? new Response(file.inner) };
: originalFetch(resource, config) };
};
}
</script> </script>
<Router {url}> <Router {url}>
+18 -7
View File
@@ -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,12 +49,18 @@
}); });
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);
}); });
}); });
@@ -62,11 +74,10 @@
data[5] = $input.height; data[5] = $input.height;
data[6] = $input.speed; data[6] = $input.speed;
outControllerData.set(data) outControllerData.set(data);
if(!$emulateModel) socketService.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">
+66 -59
View File
@@ -2,76 +2,83 @@
import socketService from '$lib/services/socket-service'; 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 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) {
socketService.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}
<Link to="/"> <Link to="/">
<Icon src={XMark} size="32" /> <Icon src={XMark} size="32" />
</Link> </Link>
{:else} {:else}
<Link to="/settings"> <Link to="/settings">
<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
{#each modes as mode} bind:value={selected_modes}
<option>{mode}</option> class="rounded-md outline outline-2 text-zinc-200 outline-zinc-600 bg-zinc-800"
{/each} >
</select> {#each modes as mode}
<option>{mode}</option>
<select bind:value={selected_view} class="rounded-md outline outline-2 text-zinc-200 outline-zinc-600 bg-zinc-800"> {/each}
{#each views as view} </select>
<option>{view}</option>
{/each}
</select>
</div>
<div class="flex gap-2 p-2"> <select
<button class="action_button bg-zinc-600"> bind:value={selected_view}
<Icon src={Power} size="24" /> class="rounded-md outline outline-2 text-zinc-200 outline-zinc-600 bg-zinc-800"
</button> >
<button class="action_button"><Icon src={Battery100} size="24" /></button> {#each views as view}
<button class="action_button"><Icon src={$isConnected ? Signal : SignalSlash} size="24" /></button> <option>{view}</option>
</div> {/each}
<div> </select>
<button class="h-full w-20 bg-red-600 text-white" on:click={stop}>STOP</button> </div>
</div>
<div class="flex gap-2 p-2">
<button class="action_button bg-zinc-600">
<Icon src={Power} 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
>
</div>
<div>
<button class="h-full w-20 bg-red-600 text-white" on:click={stop}>STOP</button>
</div>
</div> </div>
<style> <style>
.topbar { .topbar {
height: 50px; height: 50px;
} }
.action_button { .action_button {
border-radius: 4px; border-radius: 4px;
width: 34px; width: 34px;
height: 34px; height: 34px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
outline: 1px solid #52525b; outline: 1px solid #52525b;
} }
</style> </style>
+156 -121
View File
@@ -1,149 +1,184 @@
<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 socketService from '$lib/services/socket-service'; import socketService from '$lib/services/socket-service';
import { lerp } from '$lib/utilities'; import { lerp } from '$lib/utilities';
import uzip from 'uzip'; import uzip from 'uzip';
import { model, outControllerData } from '$lib/store'; import { model, outControllerData } from '$lib/store';
import { ForwardKinematics } from '$lib/kinematic'; import { ForwardKinematics } from '$lib/kinematic';
import { location } from '$lib/utilities'; import { location } from '$lib/utilities';
import FileService from '$lib/services/file-service'; import { fileService } from '$lib/services';
import SceneBuilder from '$lib/sceneBuilder'; import SceneBuilder from '$lib/sceneBuilder';
const angles = socketService.angles const angles = socketService.angles;
const mpu = socketService.mpu const mpu = socketService.mpu;
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;
phi: number; phi: number;
psi: number; psi: number;
} }
const degToRad = (val:number) => val * (Math.PI / 180) const degToRad = (val: number) => val * (Math.PI / 180);
onMount(async () => { onMount(async () => {
await cacheModelFiles() await cacheModelFiles();
await createScene() await createScene();
outControllerData.subscribe(data => { outControllerData.subscribe((data) => {
socketService.send(JSON.stringify({ socketService.send(
type: "kinematic/bodystate", JSON.stringify({
angles:[0, (data[1]-128)/3, (data[2]-128) / 4], type: 'kinematic/bodystate',
position:[(data[4]-128)/2, data[5], (data[3]-128)/2]})) 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);
for(const [path, data] of Object.entries(files) as [path:string, data:Uint8Array][]){
const url = new URL(path, window.location.href)
FileService.saveFile(url.toString(), data)
}
}
const updateAngles = (name:string, angle:number) => { var files = uzip.parse(data);
modelTargetAngles[servoNames.indexOf(name)] = angle * (180/Math.PI)
socketService.send(JSON.stringify({type:"kinematic/angle", angle:angle * (180/Math.PI), id:servoNames.indexOf(name)}))
}
const createScene = async () => { for (const [path, data] of Object.entries(files) as [path: string, data: Uint8Array][]) {
sceneManager = new SceneBuilder() const url = new URL(path, window.location.href);
.addRenderer({ antialias: true, canvas: canvas, alpha: true}) fileService.saveFile(url.toString(), data);
.addPerspectiveCamera({x:-0.5, y:0.5, z:1}) }
.addOrbitControls(10, 30) };
.addSky()
.addGroundPlane({x:0, y:-2, z:0})
.addGridHelper({size:250, divisions:125, y:-2})
.addAmbientLight({color:0xffffff, intensity:0.7})
.addDirectionalLight({x:10, y:100, z:10, color:0xffffff, intensity:1})
.addArrowHelper({origin:{x:0, y:0, z:0}, direction:{x:0, y:-2, z:0}})
.addFogExp2(0xcccccc, 0.015)
.addModel($model)
.addDragControl(updateAngles)
.handleResize()
.addRenderCb(render)
.startRenderLoop()
addVideoStream() const updateAngles = (name: string, angle: number) => {
} modelTargetAngles[servoNames.indexOf(name)] = angle * (180 / Math.PI);
socketService.send(
JSON.stringify({
type: 'kinematic/angle',
angle: angle * (180 / Math.PI),
id: servoNames.indexOf(name)
})
);
};
const addVideoStream = () => { const createScene = async () => {
context = streamCanvas.getContext("2d"); sceneManager = new SceneBuilder()
texture = new CanvasTexture( stream ); .addRenderer({ antialias: true, canvas: canvas, alpha: true })
const liveStream = new Mesh( new CircleGeometry(35, 32), new MeshBasicMaterial({ map: texture })) .addPerspectiveCamera({ x: -0.5, y: 0.5, z: 1 })
liveStream.position.z = -50 .addOrbitControls(10, 30)
liveStream.visible = showStream .addSky()
sceneManager.scene.add(liveStream) .addGroundPlane({ x: 0, y: -2, z: 0 })
} .addGridHelper({ size: 250, divisions: 125, y: -2 })
.addAmbientLight({ color: 0xffffff, intensity: 0.7 })
.addDirectionalLight({ x: 10, y: 100, z: 10, color: 0xffffff, intensity: 1 })
.addArrowHelper({ origin: { x: 0, y: 0, z: 0 }, direction: { x: 0, y: -2, z: 0 } })
.addFogExp2(0xcccccc, 0.015)
.addModel($model)
.addDragControl(updateAngles)
.handleResize()
.addRenderCb(render)
.startRenderLoop();
const handleVideoStream = () => { addVideoStream();
if(!showStream) return };
context.drawImage(stream, 0, 0)
texture.needsUpdate = true;
}
const render = () => { const addVideoStream = () => {
const robot = sceneManager.model context = streamCanvas.getContext('2d');
if(!robot) return texture = new CanvasTexture(stream);
const liveStream = new Mesh(
new CircleGeometry(35, 32),
new MeshBasicMaterial({ map: texture })
);
liveStream.position.z = -50;
liveStream.visible = showStream;
sceneManager.scene.add(liveStream);
};
const forwardKinematics = new ForwardKinematics() const handleVideoStream = () => {
if (!showStream) return;
context.drawImage(stream, 0, 0);
texture.needsUpdate = true;
};
const points = forwardKinematics.calculateFootpoints(modelAngles.map(ang => degToRad(ang)) as number[]) const render = () => {
robot.position.y = Math.max(...points.map(coord => coord[0] / 100)) - 2.7 const robot = sceneManager.model;
robot.rotation.z = lerp(robot.rotation.z, degToRad($mpu.heading + 90), 0.1) if (!robot) return;
modelTargetAngles = $angles
handleVideoStream()
for (let i = 0; i < servoNames.length; i++) { const forwardKinematics = new ForwardKinematics();
modelAngles[i] = lerp(robot.joints[servoNames[i]].angle * (180/Math.PI), modelTargetAngles[i], 0.1)
robot.joints[servoNames[i]].setJointValue(degToRad(modelAngles[i]));
}
modelBodyAngles.omega = lerp(robot.rotation.x * (180/Math.PI), modelTargeBodyAngles.omega - 90, 0.1) const points = forwardKinematics.calculateFootpoints(
modelBodyAngles.phi = lerp(robot.rotation.y * (180/Math.PI), modelTargeBodyAngles.phi, 0.1) modelAngles.map((ang) => degToRad(ang)) as number[]
modelBodyAngles.psi = lerp(robot.rotation.z * (180/Math.PI), modelTargeBodyAngles.psi + 90, 0.1) );
} 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 = $angles;
handleVideoStream();
for (let i = 0; i < servoNames.length; i++) {
modelAngles[i] = lerp(
robot.joints[servoNames[i]].angle * (180 / Math.PI),
modelTargetAngles[i],
0.1
);
robot.joints[servoNames[i]].setJointValue(degToRad(modelAngles[i]));
}
modelBodyAngles.omega = lerp(
robot.rotation.x * (180 / Math.PI),
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
bind:this={stream} bind:this={stream}
src={videoStream} src={videoStream}
class="hidden" class="hidden"
alt="Live stream is down" alt="Live stream is down"
crossorigin="anonymous" crossorigin="anonymous"
/> />
{/if} {/if}
<canvas bind:this={streamCanvas} class="hidden"></canvas> <canvas bind:this={streamCanvas} class="hidden"></canvas>
<canvas bind:this={canvas} class="absolute"></canvas> <canvas bind:this={canvas} class="absolute"></canvas>
+59 -46
View File
@@ -2,43 +2,43 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { jointNames } from '../../lib/store'; import { jointNames } from '../../lib/store';
type Servo = { type Servo = {
id: number; id: number;
name: string; name: string;
minPWM: number; minPWM: number;
maxPWM: number; maxPWM: number;
pwmFor180: number; pwmFor180: number;
}; };
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,
name, name,
minPWM: 0, minPWM: 0,
maxPWM: 0, maxPWM: 0,
pwmFor180: 0 pwmFor180: 0
}; };
}); });
}) });
}) });
let selectedServo: number | null = null; let selectedServo: number | null = null;
function updateServoValue(index: number, field: keyof Servo, value: number): void { function updateServoValue(index: number, field: keyof Servo, value: number): void {
servos[index] = { ...servos[index], [field]: value }; servos[index] = { ...servos[index], [field]: value };
} }
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>
@@ -49,23 +49,36 @@
</select> </select>
</div> </div>
{#if selectedServo !== null} {#if selectedServo !== null}
<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
value={servos[selectedServo].minPWM} type="number"
on:blur={(event) => updateServoValue(selectedServo, 'minPWM', Number(event.target.value))} /> id="minPWM"
class="bg-zinc-800"
value={servos[selectedServo].minPWM}
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
value={servos[selectedServo].maxPWM} type="number"
on:blur={(event) => updateServoValue(selectedServo, 'maxPWM', Number(event.target.value))} /> id="maxPWM"
class="bg-zinc-800"
value={servos[selectedServo].maxPWM}
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
value={servos[selectedServo].pwmFor180} type="number"
on:blur={(event) => updateServoValue(selectedServo, 'pwmFor180', Number(event.target.value))} /> id="pwmFor180"
</div> class="bg-zinc-800"
{/if} value={servos[selectedServo].pwmFor180}
on:blur={(event) =>
updateServoValue(selectedServo, 'pwmFor180', Number(event.target.value))}
/>
</div>
{/if}
</div> </div>
@@ -1,26 +1,25 @@
<script lang="ts"> <script lang="ts">
import socketService from "$lib/services/socket-service"; import socketService from '$lib/services/socket-service';
import { onMount } from 'svelte' import { onMount } from 'svelte';
let isConnected = socketService.isConnected
let settings = socketService.settings
onMount(() => { let isConnected = socketService.isConnected;
if ($isConnected) { let settings = socketService.settings;
const message = JSON.stringify({type: 'system/settings'})
socketService.send(message)
}
})
onMount(() => {
if ($isConnected) {
const message = JSON.stringify({ type: 'system/settings' });
socketService.send(message);
}
});
</script> </script>
<div class="w-full h-full"> <div class="w-full h-full">
<div> <div>
{#each Object.entries($settings) as entry} {#each Object.entries($settings) 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>
<div>{entry[1]}</div> <div>{entry[1]}</div>
</div> </div>
{/each} {/each}
</div> </div>
</div> </div>
+25 -25
View File
@@ -1,31 +1,31 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte' import { onMount } from 'svelte';
import { humanFileSize } from "$lib/utilities"; import { humanFileSize } from '$lib/utilities';
import socketService from '$lib/services/socket-service'; import socketService from '$lib/services/socket-service';
let isConnected = socketService.isConnected let isConnected = socketService.isConnected;
let settings = socketService.settings let settings = socketService.settings;
let systemInfo = socketService.systemInfo let systemInfo = socketService.systemInfo;
onMount(() => {
if ($isConnected) {
const message = JSON.stringify({type: 'system/info'})
socketService.send(message)
}
})
onMount(() => {
if ($isConnected) {
const message = JSON.stringify({ type: 'system/info' });
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>
{/if} {/if}
</div> </div>
{/each} {/each}
</div> </div>
</div> </div>
+14 -15
View File
@@ -1,21 +1,20 @@
<script lang="ts"> <script lang="ts">
import socketService from "$lib/services/socket-service"; import socketService from '$lib/services/socket-service';
import { onMount } from 'svelte' import { onMount } from 'svelte';
let isConnected = socketService.isConnected let isConnected = socketService.isConnected;
let log = socketService.log let log = socketService.log;
onMount(() => {
if ($isConnected) {
const message = JSON.stringify({type: 'system/logs'})
socketService.send(message)
}
})
onMount(() => {
if ($isConnected) {
const message = JSON.stringify({ type: 'system/logs' });
socketService.send(message);
}
});
</script> </script>
<div class="w-full h-full"> <div class="w-full h-full">
{#each $log as entry} {#each $log as entry}
<div>{entry}</div> <div>{entry}</div>
{/each} {/each}
</div> </div>
+383 -371
View File
@@ -1,381 +1,393 @@
export default class Kinematic { export default class Kinematic {
private l1: number; private l1: number;
private l2: number; private l2: number;
private l3: number; private l3: number;
private l4: number; private l4: number;
private L: number;
private W: number;
constructor() {
this.l1 = 50;
this.l2 = 20;
this.l3 = 120;
this.l4 = 155;
this.L = 140;
this.W = 75;
}
bodyIK(omega: number, phi: number, psi: number, xm: number, ym: number, zm: number): number[][][] {
const { cos, sin } = Math;
const Rx: number[][] = [
[1, 0, 0, 0],
[0, cos(omega), -sin(omega), 0],
[0, sin(omega), cos(omega), 0],
[0, 0, 0, 1],
];
const Ry: number[][] = [
[cos(phi), 0, sin(phi), 0],
[0, 1, 0, 0],
[-sin(phi), 0, cos(phi), 0],
[0, 0, 0, 1],
];
const Rz: number[][] = [
[cos(psi), -sin(psi), 0, 0],
[sin(psi), cos(psi), 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1],
];
const Rxyz: number[][] = this.matrixMultiply(this.matrixMultiply(Rx, Ry), Rz);
const T: number[][] = [
[0, 0, 0, xm],
[0, 0, 0, ym],
[0, 0, 0, zm],
[0, 0, 0, 0],
];
const Tm: number[][] = this.matrixAdd(T, Rxyz);
const sHp = sin(Math.PI / 2);
const cHp = cos(Math.PI / 2);
const L = this.L;
const W = this.W;
return [
this.matrixMultiply(Tm, [
[cHp, 0, sHp, L / 2],
[0, 1, 0, 0],
[-sHp, 0, cHp, W / 2],
[0, 0, 0, 1],
]),
this.matrixMultiply(Tm, [
[cHp, 0, sHp, L / 2],
[0, 1, 0, 0],
[-sHp, 0, cHp, -W / 2],
[0, 0, 0, 1],
]),
this.matrixMultiply(Tm, [
[cHp, 0, sHp, -L / 2],
[0, 1, 0, 0],
[-sHp, 0, cHp, W / 2],
[0, 0, 0, 1],
]),
this.matrixMultiply(Tm, [
[cHp, 0, sHp, -L / 2],
[0, 1, 0, 0],
[-sHp, 0, cHp, -W / 2],
[0, 0, 0, 1],
]),
];
}
private legIK(point: number[]): number[] {
const [x, y, z] = point;
const { atan2, cos, sin, sqrt, acos } = Math;
const { l1, l2, l3, l4 } = this;
let F;
try { private L: number;
F = sqrt(x ** 2 + y ** 2 - l1 ** 2); private W: number;
if(isNaN(F)) throw new Error("F is NaN")
} catch (error) {
//console.log(error)
F = l1
}
const G = F - l2;
const H = sqrt(G ** 2 + z ** 2);
const theta1 = -atan2(y, x) - atan2(F, -l1); constructor() {
const D = (H ** 2 - l3 ** 2 - l4 ** 2) / (2 * l3 * l4); this.l1 = 50;
let theta3: number this.l2 = 20;
try { this.l3 = 120;
theta3 = acos(D); this.l4 = 155;
if(isNaN(theta3)) throw new Error("theta3 is NaN")
} catch (error) {
theta3 = 0
}
const theta2 = atan2(z, G) - atan2(l4 * sin(theta3), l3 + l4 * cos(theta3));
return [theta1, theta2, theta3]; this.L = 140;
} this.W = 75;
}
matrixMultiply(a: number[][], b: number[][]): number[][] {
const result: number[][] = [];
for (let i = 0; i < a.length; i++) {
const row: number[] = [];
for (let j = 0; j < b[0].length; j++) {
let sum = 0;
for (let k = 0; k < a[i].length; k++) {
sum += a[i][k] * b[k][j];
}
row.push(sum);
}
result.push(row);
}
return result;
}
multiplyVector(matrix: number[][], vector: number[]): number[] { bodyIK(
const rows = matrix.length; omega: number,
const cols = matrix[0].length; phi: number,
const vectorLength = vector.length; psi: number,
xm: number,
if (cols !== vectorLength) { ym: number,
throw new Error("Matrix and vector dimensions do not match for multiplication."); zm: number
} ): number[][][] {
const { cos, sin } = Math;
const result = [];
for (let i = 0; i < rows; i++) {
let sum = 0;
for (let j = 0; j < cols; j++) {
sum += matrix[i][j] * vector[j];
}
result.push(sum);
}
return result;
}
private matrixAdd(a: number[][], b: number[][]): number[][] {
const result: number[][] = [];
for (let i = 0; i < a.length; i++) {
const row: number[] = [];
for (let j = 0; j < a[i].length; j++) {
row.push(a[i][j] + b[i][j]);
}
result.push(row);
}
return result;
}
public calcLegPoints(angles: number[]): number[][] {
const [theta1, theta2, theta3] = angles;
const theta23 = theta2 + theta3;
const T0: number[] = [0, 0, 0, 1];
const T1: number[] = this.vectorAdd(
T0,
[-this.l1 * Math.cos(theta1), this.l1 * Math.sin(theta1), 0, 0]
);
const T2: number[] = this.vectorAdd(
T1,
[-this.l2 * Math.sin(theta1), -this.l2 * Math.cos(theta1), 0, 0]
);
const T3: number[] = this.vectorAdd(
T2,
[
-this.l3 * Math.sin(theta1) * Math.cos(theta2),
-this.l3 * Math.cos(theta1) * Math.cos(theta2),
this.l3 * Math.sin(theta2),
0,
]
);
const T4: number[] = this.vectorAdd(
T3,
[
-this.l4 * Math.sin(theta1) * Math.cos(theta23),
-this.l4 * Math.cos(theta1) * Math.cos(theta23),
this.l4 * Math.sin(theta23),
0,
]
);
return [T0, T1, T2, T3, T4];
}
public calcIK(Lp: number[][], angles: number[], center: number[]): number[][] {
const [omega, phi, psi] = angles;
const [xm, ym, zm] = center;
const [Tlf, Trf, Tlb, Trb] = this.bodyIK(omega, phi, psi, xm, ym, zm);
const Ix: number[][] = [
[-1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1],
];
return [ const Rx: number[][] = [
this.legIK(this.multiplyVector(this.matrixInverse(Tlf), Lp[0])), [1, 0, 0, 0],
this.legIK(this.multiplyVector(Ix, this.multiplyVector(this.matrixInverse(Trf), Lp[1]))), [0, cos(omega), -sin(omega), 0],
this.legIK(this.multiplyVector(this.matrixInverse(Tlb), Lp[2])), [0, sin(omega), cos(omega), 0],
this.legIK(this.multiplyVector(Ix, this.multiplyVector(this.matrixInverse(Trb), Lp[3]))), [0, 0, 0, 1]
]; ];
} const Ry: number[][] = [
[cos(phi), 0, sin(phi), 0],
private vectorAdd(a: number[], b: number[]): number[] { [0, 1, 0, 0],
return a.map((val, index) => val + b[index]); [-sin(phi), 0, cos(phi), 0],
} [0, 0, 0, 1]
];
private matrixInverse(matrix: number[][]): number[][] { const Rz: number[][] = [
const det = this.determinant(matrix); [cos(psi), -sin(psi), 0, 0],
const adjugate = this.adjugate(matrix); [sin(psi), cos(psi), 0, 0],
const scalar = 1 / det; [0, 0, 1, 0],
const inverse: number[][] = []; [0, 0, 0, 1]
];
for (let i = 0; i < matrix.length; i++) { const Rxyz: number[][] = this.matrixMultiply(this.matrixMultiply(Rx, Ry), Rz);
const row: number[] = [];
for (let j = 0; j < matrix[i].length; j++) {
row.push(adjugate[i][j] * scalar);
}
inverse.push(row);
}
return inverse;
}
private determinant(matrix: number[][]): number {
if (matrix.length !== matrix[0].length) {
throw new Error("The matrix is not square.");
}
if (matrix.length === 2) {
return matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0];
}
let det = 0;
for (let i = 0; i < matrix.length; i++) {
const sign = i % 2 === 0 ? 1 : -1;
const subMatrix: number[][] = [];
for (let j = 1; j < matrix.length; j++) {
const row: number[] = [];
for (let k = 0; k < matrix.length; k++) {
if (k !== i) {
row.push(matrix[j][k]);
}
}
subMatrix.push(row);
}
det += sign * matrix[0][i] * this.determinant(subMatrix);
}
return det;
}
private adjugate(matrix: number[][]): number[][] {
if (matrix.length !== matrix[0].length) {
throw new Error("The matrix is not square.");
}
const adjugate: number[][] = [];
for (let i = 0; i < matrix.length; i++) {
const row: number[] = [];
for (let j = 0; j < matrix[i].length; j++) {
const sign = (i + j) % 2 === 0 ? 1 : -1;
const subMatrix: number[][] = [];
for (let k = 0; k < matrix.length; k++) {
if (k !== i) {
const subRow: number[] = [];
for (let l = 0; l < matrix.length; l++) {
if (l !== j) {
subRow.push(matrix[k][l]);
}
}
subMatrix.push(subRow);
}
}
const cofactor = sign * this.determinant(subMatrix);
row.push(cofactor);
}
adjugate.push(row);
}
return this.transpose(adjugate);
}
private transpose(matrix: number[][]): number[][] {
const transposed: number[][] = [];
for (let i = 0; i < matrix.length; i++) {
const row: number[] = [];
for (let j = 0; j < matrix[i].length; j++) {
row.push(matrix[j][i]);
}
transposed.push(row);
}
return transposed;
}
}
const T: number[][] = [
[0, 0, 0, xm],
[0, 0, 0, ym],
[0, 0, 0, zm],
[0, 0, 0, 0]
];
const Tm: number[][] = this.matrixAdd(T, Rxyz);
const sHp = sin(Math.PI / 2);
const cHp = cos(Math.PI / 2);
const L = this.L;
const W = this.W;
return [
this.matrixMultiply(Tm, [
[cHp, 0, sHp, L / 2],
[0, 1, 0, 0],
[-sHp, 0, cHp, W / 2],
[0, 0, 0, 1]
]),
this.matrixMultiply(Tm, [
[cHp, 0, sHp, L / 2],
[0, 1, 0, 0],
[-sHp, 0, cHp, -W / 2],
[0, 0, 0, 1]
]),
this.matrixMultiply(Tm, [
[cHp, 0, sHp, -L / 2],
[0, 1, 0, 0],
[-sHp, 0, cHp, W / 2],
[0, 0, 0, 1]
]),
this.matrixMultiply(Tm, [
[cHp, 0, sHp, -L / 2],
[0, 1, 0, 0],
[-sHp, 0, cHp, -W / 2],
[0, 0, 0, 1]
])
];
}
private legIK(point: number[]): number[] {
const [x, y, z] = point;
const { atan2, cos, sin, sqrt, acos } = Math;
const { l1, l2, l3, l4 } = this;
let F;
try {
F = sqrt(x ** 2 + y ** 2 - l1 ** 2);
if (isNaN(F)) throw new Error('F is NaN');
} catch (error) {
//console.log(error)
F = l1;
}
const G = F - l2;
const H = sqrt(G ** 2 + z ** 2);
const theta1 = -atan2(y, x) - atan2(F, -l1);
const D = (H ** 2 - l3 ** 2 - l4 ** 2) / (2 * l3 * l4);
let theta3: number;
try {
theta3 = acos(D);
if (isNaN(theta3)) throw new Error('theta3 is NaN');
} catch (error) {
theta3 = 0;
}
const theta2 = atan2(z, G) - atan2(l4 * sin(theta3), l3 + l4 * cos(theta3));
return [theta1, theta2, theta3];
}
matrixMultiply(a: number[][], b: number[][]): number[][] {
const result: number[][] = [];
for (let i = 0; i < a.length; i++) {
const row: number[] = [];
for (let j = 0; j < b[0].length; j++) {
let sum = 0;
for (let k = 0; k < a[i].length; k++) {
sum += a[i][k] * b[k][j];
}
row.push(sum);
}
result.push(row);
}
return result;
}
multiplyVector(matrix: number[][], vector: number[]): number[] {
const rows = matrix.length;
const cols = matrix[0].length;
const vectorLength = vector.length;
if (cols !== vectorLength) {
throw new Error('Matrix and vector dimensions do not match for multiplication.');
}
const result = [];
for (let i = 0; i < rows; i++) {
let sum = 0;
for (let j = 0; j < cols; j++) {
sum += matrix[i][j] * vector[j];
}
result.push(sum);
}
return result;
}
private matrixAdd(a: number[][], b: number[][]): number[][] {
const result: number[][] = [];
for (let i = 0; i < a.length; i++) {
const row: number[] = [];
for (let j = 0; j < a[i].length; j++) {
row.push(a[i][j] + b[i][j]);
}
result.push(row);
}
return result;
}
public calcLegPoints(angles: number[]): number[][] {
const [theta1, theta2, theta3] = angles;
const theta23 = theta2 + theta3;
const T0: number[] = [0, 0, 0, 1];
const T1: number[] = this.vectorAdd(T0, [
-this.l1 * Math.cos(theta1),
this.l1 * Math.sin(theta1),
0,
0
]);
const T2: number[] = this.vectorAdd(T1, [
-this.l2 * Math.sin(theta1),
-this.l2 * Math.cos(theta1),
0,
0
]);
const T3: number[] = this.vectorAdd(T2, [
-this.l3 * Math.sin(theta1) * Math.cos(theta2),
-this.l3 * Math.cos(theta1) * Math.cos(theta2),
this.l3 * Math.sin(theta2),
0
]);
const T4: number[] = this.vectorAdd(T3, [
-this.l4 * Math.sin(theta1) * Math.cos(theta23),
-this.l4 * Math.cos(theta1) * Math.cos(theta23),
this.l4 * Math.sin(theta23),
0
]);
return [T0, T1, T2, T3, T4];
}
public calcIK(Lp: number[][], angles: number[], center: number[]): number[][] {
const [omega, phi, psi] = angles;
const [xm, ym, zm] = center;
const [Tlf, Trf, Tlb, Trb] = this.bodyIK(omega, phi, psi, xm, ym, zm);
const Ix: number[][] = [
[-1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
];
return [
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(this.matrixInverse(Tlb), Lp[2])),
this.legIK(this.multiplyVector(Ix, this.multiplyVector(this.matrixInverse(Trb), Lp[3])))
];
}
private vectorAdd(a: number[], b: number[]): number[] {
return a.map((val, index) => val + b[index]);
}
private matrixInverse(matrix: number[][]): number[][] {
const det = this.determinant(matrix);
const adjugate = this.adjugate(matrix);
const scalar = 1 / det;
const inverse: number[][] = [];
for (let i = 0; i < matrix.length; i++) {
const row: number[] = [];
for (let j = 0; j < matrix[i].length; j++) {
row.push(adjugate[i][j] * scalar);
}
inverse.push(row);
}
return inverse;
}
private determinant(matrix: number[][]): number {
if (matrix.length !== matrix[0].length) {
throw new Error('The matrix is not square.');
}
if (matrix.length === 2) {
return matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0];
}
let det = 0;
for (let i = 0; i < matrix.length; i++) {
const sign = i % 2 === 0 ? 1 : -1;
const subMatrix: number[][] = [];
for (let j = 1; j < matrix.length; j++) {
const row: number[] = [];
for (let k = 0; k < matrix.length; k++) {
if (k !== i) {
row.push(matrix[j][k]);
}
}
subMatrix.push(row);
}
det += sign * matrix[0][i] * this.determinant(subMatrix);
}
return det;
}
private adjugate(matrix: number[][]): number[][] {
if (matrix.length !== matrix[0].length) {
throw new Error('The matrix is not square.');
}
const adjugate: number[][] = [];
for (let i = 0; i < matrix.length; i++) {
const row: number[] = [];
for (let j = 0; j < matrix[i].length; j++) {
const sign = (i + j) % 2 === 0 ? 1 : -1;
const subMatrix: number[][] = [];
for (let k = 0; k < matrix.length; k++) {
if (k !== i) {
const subRow: number[] = [];
for (let l = 0; l < matrix.length; l++) {
if (l !== j) {
subRow.push(matrix[k][l]);
}
}
subMatrix.push(subRow);
}
}
const cofactor = sign * this.determinant(subMatrix);
row.push(cofactor);
}
adjugate.push(row);
}
return this.transpose(adjugate);
}
private transpose(matrix: number[][]): number[][] {
const transposed: number[][] = [];
for (let i = 0; i < matrix.length; i++) {
const row: number[] = [];
for (let j = 0; j < matrix[i].length; j++) {
row.push(matrix[j][i]);
}
transposed.push(row);
}
return transposed;
}
}
export class ForwardKinematics { export class ForwardKinematics {
private l1: number; private l1: number;
private l2: number; private l2: number;
private l3: number; private l3: number;
private l4: number; private l4: number;
constructor() {
this.l1 = 50;
this.l2 = 20;
this.l3 = 120;
this.l4 = 155;
}
public calculateFootpoint(theta1: number, theta2: number, theta3: number): number[] {
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 y = this.l1 * sin(theta1) + this.l2 * sin(theta1) + this.l3 * sin(theta1 + theta2) + this.l4 * sin(theta1 + theta2 + theta3);
const z = 0;
return [x, y, z];
}
public calculateFootpoints(angles: number[]): number[][] { constructor() {
const footpoints: number[][] = []; this.l1 = 50;
this.l2 = 20;
for (let i = 0; i < angles.length; i += 3) { this.l3 = 120;
const theta1 = angles[i]; this.l4 = 155;
const theta2 = angles[i + 1]; }
const theta3 = angles[i + 2];
const footpoint = this.calculateFootpoint(theta1, theta2, theta3); public calculateFootpoint(theta1: number, theta2: number, theta3: number): number[] {
footpoints.push(footpoint); const { cos, sin } = Math;
}
const x =
return footpoints; 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;
return [x, y, z];
}
public calculateFootpoints(angles: number[]): number[][] {
const footpoints: number[][] = [];
for (let i = 0; i < angles.length; i += 3) {
const theta1 = angles[i];
const theta2 = angles[i + 1];
const theta3 = angles[i + 2];
const footpoint = this.calculateFootpoint(theta1, theta2, theta3);
footpoints.push(footpoint);
}
return footpoints;
}
}
+273 -248
View File
@@ -1,293 +1,318 @@
import { Mesh, import {
PerspectiveCamera, Mesh,
PlaneGeometry, PerspectiveCamera,
Scene, PlaneGeometry,
ShadowMaterial, Scene,
WebGLRenderer, ShadowMaterial,
AmbientLight, WebGLRenderer,
DirectionalLight, AmbientLight,
PCFSoftShadowMap, DirectionalLight,
GridHelper, PCFSoftShadowMap,
ArrowHelper, GridHelper,
Vector3, ArrowHelper,
LoaderUtils, Vector3,
Object3D, LoaderUtils,
FogExp2, Object3D,
CanvasTexture, FogExp2,
type ColorRepresentation, CanvasTexture,
type WebGLRendererParameters, type ColorRepresentation,
MeshPhongMaterial, type WebGLRendererParameters,
EquirectangularReflectionMapping, MeshPhongMaterial,
ACESFilmicToneMapping, EquirectangularReflectionMapping,
MathUtils, ACESFilmicToneMapping,
} from "three"; MathUtils
} 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 URDFMimicJoint } 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: Object3D<Event>;
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();
sky.scale.setScalar(450000); sky.scale.setScalar(450000);
this.scene.add(sky); this.scene.add(sky);
const effectController = { const effectController = {
turbidity: 10, turbidity: 10,
rayleigh: 3, rayleigh: 3,
mieCoefficient: 0.005, mieCoefficient: 0.005,
mieDirectionalG: 0.7, mieDirectionalG: 0.7,
elevation: calculateCurrentSunElevation(), elevation: calculateCurrentSunElevation(),
azimuth: 180, azimuth: 180,
exposure: this.renderer.toneMappingExposure exposure: this.renderer.toneMappingExposure
}; };
const uniforms = sky.material.uniforms; const uniforms = sky.material.uniforms;
uniforms['turbidity'].value = effectController.turbidity; uniforms['turbidity'].value = effectController.turbidity;
uniforms['rayleigh'].value = effectController.rayleigh; uniforms['rayleigh'].value = effectController.rayleigh;
uniforms['mieCoefficient'].value = effectController.mieCoefficient; uniforms['mieCoefficient'].value = effectController.mieCoefficient;
uniforms['mieDirectionalG'].value = effectController.mieDirectionalG; uniforms['mieDirectionalG'].value = effectController.mieDirectionalG;
this.renderer.toneMappingExposure = 0.5; this.renderer.toneMappingExposure = 0.5;
const phi = MathUtils.degToRad( 90 - effectController.elevation ); const phi = MathUtils.degToRad(90 - effectController.elevation);
const theta = MathUtils.degToRad( effectController.azimuth ); const theta = MathUtils.degToRad(effectController.azimuth);
const sun = new Vector3(); const sun = new Vector3();
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 }));
this.ground.rotation.x = -Math.PI / 2; this.ground.rotation.x = -Math.PI / 2;
this.ground.scale.setScalar(30); this.ground.scale.setScalar(30);
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);
directionalLight.castShadow = true; directionalLight.castShadow = true;
directionalLight.shadow.mapSize.setScalar(2048); directionalLight.shadow.mapSize.setScalar(2048);
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);
this.gridHelper.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0); this.gridHelper.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0);
this.gridHelper.material.opacity = 0.2; this.gridHelper.material.opacity = 0.2;
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,
this.scene.add( arrowHelper ); options?.direction.z ?? 0
return this );
} 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);
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) => 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) => {
if (c.type === 'Mesh') { if (c.type === 'Mesh') {
if (revert) { if (revert) {
c.material = c.__origMaterial; c.material = c.__origMaterial;
delete c.__origMaterial; delete c.__origMaterial;
} else { } else {
c.__origMaterial = c.material; c.__origMaterial = c.material;
c.material = material; c.material = material;
} }
} }
if (c === m || !this.isJoint(c)) { if (c === m || !this.isJoint(c)) {
for (let i = 0; i < c.children.length; i++) { for (let i = 0; i < c.children.length; i++) {
const child = c.children[i]; const child = c.children[i];
if (!child.isURDFCollider) { if (!child.isURDFCollider) {
traverse(c.children[i]); traverse(c.children[i]);
} }
} }
} }
}; };
traverse(m); traverse(m);
}; };
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);
dragControls.updateJoint = (joint:URDFMimicJoint, angle:number) => {
this.setJointValue(joint.name, angle);
updateAngle(joint.name, angle)
};
dragControls.onDragStart = () => this.controls.enabled = false;
dragControls.onDragEnd = () => this.controls.enabled = true;
dragControls.onHover = (joint:URDFMimicJoint) => 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])); const dragControls = new PointerURDFDragControls(
this.renderer.domElement.addEventListener('touchmove', (data) => dragControls._mouseMove(data.touches[0])) this.scene,
this.renderer.domElement.addEventListener('touchup', (data) => dragControls._mouseUp(data.touches[0])); this.camera,
return this this.renderer.domElement
} );
dragControls.updateJoint = (joint: URDFMimicJoint, angle: number) => {
this.setJointValue(joint.name, angle);
updateAngle(joint.name, angle);
};
dragControls.onDragStart = () => (this.controls.enabled = false);
dragControls.onDragEnd = () => (this.controls.enabled = true);
dragControls.onHover = (joint: URDFMimicJoint) =>
this.highlightLinkGeometry(joint, false, highlightMaterial);
dragControls.onUnhover = (joint: URDFMimicJoint) =>
this.highlightLinkGeometry(joint, true, highlightMaterial);
public toggleFog = () => { this.renderer.domElement.addEventListener('touchstart', (data) =>
this.scene.fog = this.scene.fog ? null : this.fog; dragControls._mouseDown(data.touches[0])
} );
this.renderer.domElement.addEventListener('touchmove', (data) =>
dragControls._mouseMove(data.touches[0])
);
this.renderer.domElement.addEventListener('touchup', (data) =>
dragControls._mouseUp(data.touches[0])
);
return this;
};
private handleRobotShadow = () => { public toggleFog = () => {
if(this.isLoaded) return this.scene.fog = this.scene.fog ? null : this.fog;
const intervalId = setInterval(() => { };
this.model?.traverse(c => c.castShadow = true);
}, 10); private handleRobotShadow = () => {
setTimeout(() => { if (this.isLoaded) return;
clearInterval(intervalId) const intervalId = setInterval(() => {
}, 1000); this.model?.traverse((c) => (c.castShadow = true));
this.isLoaded = true; }, 10);
} setTimeout(() => {
} clearInterval(intervalId);
}, 1000);
this.isLoaded = true;
};
}
+56 -57
View File
@@ -1,72 +1,71 @@
import { Result } from '$lib/utilities/result'; import { Result } from '$lib/utilities/result';
class FileService { class FileService {
private dbName = 'fileStorageDB'; private dbName = 'fileStorageDB';
private dbVersion = 1; private dbVersion = 1;
private storeName = 'files'; private storeName = 'files';
private dbPromise: Promise<Result<IDBDatabase, string>>; private dbPromise: Promise<Result<IDBDatabase, string>>;
constructor() { constructor() {
this.dbPromise = this.openDatabase(); this.dbPromise = this.openDatabase();
} }
private async openDatabase(): Promise<Result<IDBDatabase, string>> { private async openDatabase(): Promise<Result<IDBDatabase, string>> {
return new Promise((resolve) => { return new Promise((resolve) => {
const request = indexedDB.open(this.dbName, this.dbVersion); const request = indexedDB.open(this.dbName, this.dbVersion);
request.onerror = () => resolve(Result.err("Error opening database")); request.onerror = () => resolve(Result.err('Error opening database'));
request.onsuccess = () => resolve(Result.ok(request.result)); request.onsuccess = () => resolve(Result.ok(request.result));
request.onupgradeneeded = (event) => { request.onupgradeneeded = (event) => {
const db = request.result; const db = request.result;
if (!db.objectStoreNames.contains(this.storeName)) { if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName); db.createObjectStore(this.storeName);
} }
}; };
}); });
} }
private async getStore(mode: IDBTransactionMode): Promise<Result<IDBObjectStore, string>> { private async getStore(mode: IDBTransactionMode): Promise<Result<IDBObjectStore, string>> {
const dbResult = await this.dbPromise; const dbResult = await this.dbPromise;
if (dbResult.isErr()) { if (dbResult.isErr()) {
return Result.err("Database not initialized properly"); return Result.err('Database not initialized properly');
} }
const db = dbResult.inner; const db = dbResult.inner;
const transaction = db.transaction(this.storeName, mode); const transaction = db.transaction(this.storeName, mode);
return Result.ok(transaction.objectStore(this.storeName)); return Result.ok(transaction.objectStore(this.storeName));
} }
public async saveFile(key: string, file: Uint8Array): Promise<Result<IDBValidKey, string>> { public async saveFile(key: string, file: Uint8Array): Promise<Result<IDBValidKey, string>> {
const storeResult = await this.getStore("readwrite"); const storeResult = await this.getStore('readwrite');
if (storeResult.isErr()) { if (storeResult.isErr()) {
return Result.err("Failed to access object store for writing"); return Result.err('Failed to access object store for writing');
} }
const store = storeResult.inner; const store = storeResult.inner;
return new Promise((resolve) => { return new Promise((resolve) => {
const request = store.put(file, key); const request = store.put(file, key);
request.onsuccess = () => resolve(Result.ok(request.result)); request.onsuccess = () => resolve(Result.ok(request.result));
request.onerror = () => resolve(Result.err("Failed to save file")); request.onerror = () => resolve(Result.err('Failed to save file'));
}); });
} }
public async getFile(key: string): Promise<Result<Uint8Array | undefined, string>> { public async getFile(key: string): Promise<Result<Uint8Array | undefined, string>> {
const storeResult = await this.getStore("readonly"); const storeResult = await this.getStore('readonly');
if (storeResult.isErr()) { if (storeResult.isErr()) {
return Result.err("Failed to access object store for reading"); return Result.err('Failed to access object store for reading');
} }
const store = storeResult.inner; const store = storeResult.inner;
return new Promise((resolve) => { return new Promise((resolve) => {
const request = store.get(key); const request = store.get(key);
request.onsuccess = () => request.onsuccess = () =>
resolve(request.result ? Result.ok(request.result) : Result.err("File content not found")) resolve(request.result ? Result.ok(request.result) : Result.err('File content not found'));
request.onerror = () => request.onerror = () => resolve(Result.err('Failed to retrieve file'));
resolve(Result.err("Failed to retrieve file")); });
}); }
}
} }
export default new FileService(); export default new FileService();
+2 -2
View File
@@ -1,2 +1,2 @@
export * from './file-service' export { default as fileService } from './file-service';
export * from './socket-service' export { default as socketService } from './socket-service';
+87 -76
View File
@@ -1,89 +1,100 @@
import { Result, Ok } from '$lib/utilities'; import { Result, Ok } from '$lib/utilities';
import { writable, type Writable } from 'svelte/store'; import { writable, type Writable } from 'svelte/store';
type WebsocketData = string | ArrayBufferLike | Blob | ArrayBufferView type WebsocketData = 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 { class SocketService {
public isConnected = writable(false);
public angles = writable(new Int16Array(12).fill(0));
public log = writable([] as string[]);
public battery = writable({});
public mpu = writable({ heading: 0 });
public distances = writable({});
public settings = writable({});
public systemInfo = writable({} as number);
public dataBuffer = writable(new Float32Array(13));
public servoBuffer: Writable<Int16Array | number[]> = writable(new Int16Array(12));
public data = writable();
private socket!: WebSocket;
public isConnected = writable(false); constructor() {}
public angles = writable(new Int16Array(12).fill(0));
public log = writable([] as string[]);
public battery = writable({});
public mpu = writable({ heading: 0 });
public distances = writable({});
public settings = writable({});
public systemInfo = writable({} as number);
public dataBuffer = writable(new Float32Array(13));
public servoBuffer: Writable<Int16Array | number[]> = writable(new Int16Array(12));
public data = writable();
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: unknown) => this.handleMessage(event);
}
public connect(url: string): void { public send(data: WebsocketData): Result<void, string> {
this.socket = new WebSocket(url); if (this.socket.readyState === WebSocket.OPEN) {
this.socket.binaryType = "arraybuffer"; this.socket.send(data);
this.socket.onopen = () => this.handleConnected(); return Ok.void();
this.socket.onclose = () => this.handleDisconnected(); }
this.socket.onmessage = (event: unknown) => this.handleMessage(event); return Result.err('The connection is not open');
} }
public send(data: WebsocketData): Result<void, string> { private handleConnected(): void {
if (this.socket.readyState === WebSocket.OPEN){ this.isConnected.set(true);
this.socket.send(data) }
return Ok.void()
}
return Result.err("The connection is not open")
}
private handleConnected(): void { private handleDisconnected(): void {
this.isConnected.set(true); this.isConnected.set(false);
} }
private handleDisconnected(): void { private handleMessage(event: any): void {
this.isConnected.set(false); if (event.data instanceof ArrayBuffer) {
} let buffer = new Int8Array(event.data);
if (buffer.length === 44) {
private handleMessage(event: any): void { this.dataBuffer.set(new Float32Array(buffer.buffer));
if (event.data instanceof ArrayBuffer) { }
let buffer = new Int8Array(event.data); } else {
if (buffer.length === 44) { let data = event.data;
this.dataBuffer.set(new Float32Array(buffer.buffer)); try {
} data = JSON.parse(event.data);
} else { } catch (error) {
let data = event.data; console.warn(error);
try { }
data = JSON.parse(event.data); switch (data.type) {
} catch (error) { case 'angles':
console.warn(error); this.angles.set(data.angles);
} break;
switch (data.type) { case 'logs':
case "angles": this.log.set(data.logs);
this.angles.set(data.angles); break;
break; case 'log':
case "logs": this.log.update((entries) => {
this.log.set(data.logs); entries.push(data.log);
break; return entries;
case "log": });
this.log.update(entries => { entries.push(data.log); return entries; }); break;
break; case 'settings':
case "settings": this.settings.set(data.settings);
this.settings.set(data.settings); case 'info':
case "info": this.systemInfo.set(data.info);
this.systemInfo.set(data.info); break;
break; case 'mpu':
case "mpu": this.mpu.set(data.mpu);
this.mpu.set(data.mpu); break;
break; case 'distances':
case "distances": this.distances.set(data.distances);
this.distances.set(data.distances); break;
break; case 'battery':
case "battery": this.battery.set(data.battery);
this.battery.set(data.battery); break;
break; }
} }
} }
}
} }
export default new SocketService(); export default new SocketService();
+8 -3
View File
@@ -3,10 +3,15 @@ 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 Uint8Array([0, 128, 128, 128, 128, 70, 0]));
export const jointNames = persistentStore("joint_names", []) export const jointNames = persistentStore('joint_names', []);
export const model = writable() export const model = writable();
+13 -14
View File
@@ -1,16 +1,15 @@
export class throttler { export class throttler {
private _throttlePause: boolean; private _throttlePause: boolean;
constructor() { constructor() {
this._throttlePause = false; this._throttlePause = false;
} }
throttle = (callback:Function, time:number) => { throttle = (callback: Function, time: number) => {
if (this._throttlePause) return; if (this._throttlePause) return;
this._throttlePause = true;
setTimeout(() => {
callback();
this._throttlePause = false;
}, time);
};
}
this._throttlePause = true;
setTimeout(() => {
callback();
this._throttlePause = false;
}, time);
};
}
+7 -7
View File
@@ -1,7 +1,7 @@
export * from './result' export * from './result';
export * from './string-utilities' export * from './string-utilities';
export * from './svelte-utilities' export * from './svelte-utilities';
export * from './math-utilities' export * from './math-utilities';
export * from './buffer-utilities' export * from './buffer-utilities';
export * from './model-utilities' export * from './model-utilities';
export * from './location-utilities' export * from './location-utilities';
+6 -4
View File
@@ -1,6 +1,8 @@
const forWeb = import.meta.env.MODE === "WEB" const forWeb = import.meta.env.MODE === 'WEB';
const mock = import.meta.env.MODE === "MOCK" const mock = import.meta.env.MODE === 'MOCK';
export const location = mock ? `${window.location.hostname}:2096` : "leika.local" export const location = mock ? `${window.location.hostname}:2096` : 'leika.local';
export const socketLocation = forWeb ? `wss://${window.location.hostname}:2096` : `ws://${location}` export const socketLocation = forWeb
? `wss://${window.location.hostname}:2096`
: `ws://${location}`;
+1 -1
View File
@@ -1,3 +1,3 @@
export const lerp = (start: number, end: number, amt: number) => { export const lerp = (start: number, end: number, amt: number) => {
return (1 - amt) * start + amt * end; return (1 - amt) * start + amt * end;
}; };
+36 -30
View File
@@ -1,32 +1,38 @@
import { LoaderUtils } from "three"; import { LoaderUtils } from 'three';
import URDFLoader, { type URDFRobot } from "urdf-loader" import URDFLoader, { type URDFRobot } from 'urdf-loader';
import { XacroLoader } from "xacro-parser" import { XacroLoader } from 'xacro-parser';
import { Result } from "$lib/utilities"; import { Result } from '$lib/utilities';
export const loadModelAsync = async (url:string):Promise<Result<[URDFRobot, string[]], string>> => { export const loadModelAsync = async (
return new Promise((resolve, reject) => { url: string
const xacroLoader = new XacroLoader(); ): Promise<Result<[URDFRobot, string[]], string>> => {
return new Promise((resolve, reject) => {
xacroLoader.load(url, async (xml) => { const xacroLoader = new XacroLoader();
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])); xacroLoader.load(
} catch (error) { url,
resolve(Result.err("Failed to load model", error)); async (xml) => {
} const urdfLoader = new URDFLoader();
}, (error) => reject(error));
}); 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)
);
});
};
+34 -34
View File
@@ -1,42 +1,42 @@
export class Err<T, U> { export class Err<T, U> {
#inner: T #inner: T;
#exception?: U #exception?: U;
constructor(inner: T, exception?: U) { constructor(inner: T, exception?: U) {
this.#inner = inner this.#inner = inner;
this.#exception = exception this.#exception = exception;
} }
get inner(): T { get inner(): T {
return this.#inner return this.#inner;
} }
get exception(): U | undefined { get exception(): U | undefined {
return this.#exception; return this.#exception;
} }
/** /**
* Type guard for `Ok` * Type guard for `Ok`
* @returns `true` if `Ok`; `false` if `Err` * @returns `true` if `Ok`; `false` if `Err`
*/ */
isOk(): false { isOk(): false {
return false return false;
} }
/** /**
* Type guard for `Err` * Type guard for `Err`
* @returns `true` if `Err`; `false` if `Ok` * @returns `true` if `Err`; `false` if `Ok`
*/ */
isErr(): this is Err<T, U> { isErr(): this is Err<T, U> {
return true return true;
} }
/** /**
* Create an `Err` * Create an `Err`
* @param inner * @param inner
* @returns `Err(inner)` * @returns `Err(inner)`
*/ */
static new<E, F>(inner: E, exception: F): Err<E, F> { static new<E, F>(inner: E, exception: F): Err<E, F> {
return new Err<E, F>(inner, exception) return new Err<E, F>(inner, exception);
} }
} }
+3 -3
View File
@@ -1,3 +1,3 @@
export * from './err' export * from './err';
export * from './ok' export * from './ok';
export * from './result' export * from './result';
+36 -36
View File
@@ -1,44 +1,44 @@
export class Ok<T> { export class Ok<T> {
#inner: T #inner: T;
constructor(inner: T) { constructor(inner: T) {
this.#inner = inner this.#inner = inner;
} }
get inner(): T { get inner(): T {
return this.#inner return this.#inner;
} }
/** /**
* Type guard for `Ok` * Type guard for `Ok`
* @returns `true` if `Ok`; `false` if `Err` * @returns `true` if `Ok`; `false` if `Err`
*/ */
isOk(): this is Ok<T> { isOk(): this is Ok<T> {
return true return true;
} }
/** /**
* Type guard for `Err` * Type guard for `Err`
* @returns `true` if `Err`; `false` if `Ok` * @returns `true` if `Err`; `false` if `Ok`
*/ */
isErr(): false { isErr(): false {
return false return false;
} }
/** /**
* Create an `Ok` * Create an `Ok`
* @param inner * @param inner
* @returns `Ok(inner)` * @returns `Ok(inner)`
*/ */
static new<T>(inner: T): Ok<T> { static new<T>(inner: T): Ok<T> {
return new Ok<T>(inner) return new Ok<T>(inner);
} }
/** /**
* Create an empty `Ok` * Create an empty `Ok`
* @returns `Ok(void)` * @returns `Ok(void)`
*/ */
static void(): Ok<void> { static void(): Ok<void> {
return new Ok(undefined) return new Ok(undefined);
} }
} }
+15 -15
View File
@@ -1,20 +1,20 @@
import { Err } from './err' import { Err } from './err';
import { Ok } from './ok' import { Ok } from './ok';
export type Result<T = unknown, E = unknown, F = unknown> = Ok<T> | Err<E, F> export type Result<T = unknown, E = unknown, F = unknown> = Ok<T> | Err<E, F>;
export namespace Result { export namespace Result {
/** /**
* @returns `Ok<T>` * @returns `Ok<T>`
*/ */
export function ok<T = unknown>(value: T) { export function ok<T = unknown>(value: T) {
return Ok.new(value) return Ok.new(value);
} }
/** /**
* @returns `Err<E, F>` * @returns `Err<E, F>`
*/ */
export function err<E = unknown, F = unknown>(error: E, exception?: F) { export function err<E = unknown, F = unknown>(error: E, exception?: F) {
return Err.new(error, exception) return Err.new(error, exception);
} }
} }
+5 -5
View File
@@ -1,5 +1,5 @@
export const humanFileSize = (size:number):string => { export const humanFileSize = (size: number): string => {
const units = ['B', 'kB', 'MB', 'GB', 'TB'] const units = ['B', 'kB', 'MB', 'GB', 'TB'];
var i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); 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]; return Number((size / Math.pow(1024, i)).toFixed(2)) * 1 + units[i];
} };
+12 -12
View File
@@ -1,13 +1,13 @@
import { writable } from "svelte/store"; import { writable } from 'svelte/store';
export const persistentStore = (key:string, initialValue:any) => { export const persistentStore = (key: string, initialValue: any) => {
const savedValue = JSON.parse(localStorage.getItem(key) as string); const savedValue = JSON.parse(localStorage.getItem(key) as string);
const data = savedValue !== null ? savedValue : initialValue; const data = savedValue !== null ? savedValue : initialValue;
const store = writable(data); const store = writable(data);
store.subscribe(value => { store.subscribe((value) => {
localStorage.setItem(key, JSON.stringify(value)); localStorage.setItem(key, JSON.stringify(value));
}); });
return store; return store;
} };
+6 -6
View File
@@ -1,15 +1,15 @@
<script lang="ts"> <script lang="ts">
import Stream from '$components/Views/Stream.svelte'; import Stream from '$components/Views/Stream.svelte';
import Model from '$components/Views/Model.svelte'; import Model from '$components/Views/Model.svelte';
import Controls from '$components/Controls.svelte'; import Controls from '$components/Controls.svelte';
import { emulateModel } from '$lib/store'; import { emulateModel } from '$lib/store';
</script> </script>
<div class="flex justify-center items-center w-full h-full"> <div class="flex justify-center items-center w-full h-full">
{#if $emulateModel} {#if $emulateModel}
<Model /> <Model />
{:else} {:else}
<Stream /> <Stream />
{/if} {/if}
<Controls /> <Controls />
</div> </div>
+33 -24
View File
@@ -3,54 +3,63 @@
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 = [
{ {
title: 'Calibration', title: 'Calibration',
path: '/calibration', path: '/calibration',
icon: AdjustmentsVertical, icon: AdjustmentsVertical,
component: Calibration component: Calibration
}, },
{ {
title: 'System info', title: 'System info',
path: '/info', path: '/info',
icon: InformationCircle, icon: InformationCircle,
component: Info component: Info
}, },
{ {
title: 'Log', title: 'Log',
path: '/log', path: '/log',
icon: BookOpen, icon: BookOpen,
component: Log component: Log
}, },
{ {
title: 'Settings', title: 'Settings',
path: '/settings', path: '/settings',
icon: Cog6Tooth, icon: Cog6Tooth,
component: Configuration component: Configuration
}, }
]; ];
</script> </script>
<div class="pt-14 flex h-full"> <div class="pt-14 flex h-full">
<nav class="w-1/6 flex flex-col"> <nav class="w-1/6 flex flex-col">
{#each menu as link} {#each menu as link}
<Link to={'/settings'+link.path}> <Link to={'/settings' + link.path}>
<div class="px-4 py-2 flex gap-2 items-center"> <div class="px-4 py-2 flex gap-2 items-center">
<Icon src={link.icon} size="24" />{link.title} <Icon src={link.icon} size="24" />{link.title}
</div> </div>
</Link> </Link>
{/each} {/each}
</nav> </nav>
<main class="w-full h-full"> <main class="w-full h-full">
<Router> <Router>
{#each menu as link} {#each menu as link}
<Route path={link.path} component={link.component}></Route> <Route path={link.path} component={link.component}></Route>
{/each} {/each}
</Router> </Router>
</main> </main>
</div> </div>
+16 -16
View File
@@ -2,22 +2,22 @@
export default { export default {
content: ['./src/**/*.{html,js,ts,svelte}'], content: ['./src/**/*.{html,js,ts,svelte}'],
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: []
}; };
+29 -29
View File
@@ -2,38 +2,38 @@ import { describe, it, expect } from 'vitest';
import { Result } from '../src/lib/utilities'; import { Result } from '../src/lib/utilities';
describe('Result', () => { describe('Result', () => {
it('should create a success result correctly', () => { it('should create a success result correctly', () => {
const successValue = "Success value"; const successValue = 'Success value';
const result = Result.ok(successValue); const result = Result.ok(successValue);
expect(result.isOk()).toBe(true); expect(result.isOk()).toBe(true);
expect(result.isErr()).toBe(false); expect(result.isErr()).toBe(false);
expect(result.inner).toBe(successValue); expect(result.inner).toBe(successValue);
}); });
it('should create an error result correctly', () => { it('should create an error result correctly', () => {
const errorMessage = "Error message"; const errorMessage = 'Error message';
const result = Result.err(errorMessage); const result = Result.err(errorMessage);
expect(result.isOk()).toBe(false); expect(result.isOk()).toBe(false);
expect(result.isErr()).toBe(true); expect(result.isErr()).toBe(true);
expect(result.inner).toBe(errorMessage); expect(result.inner).toBe(errorMessage);
}); });
it('should type guard success and error results correctly', () => { it('should type guard success and error results correctly', () => {
const successResult = Result.ok(123); const successResult = Result.ok(123);
const errorResult = Result.err("Error"); const errorResult = Result.err('Error');
if (successResult.isOk()) { if (successResult.isOk()) {
expect(typeof successResult.inner).toBe('number'); expect(typeof successResult.inner).toBe('number');
} else { } else {
throw new Error('Expected successResult to be ok'); throw new Error('Expected successResult to be ok');
} }
if (errorResult.isErr()) { if (errorResult.isErr()) {
expect(typeof errorResult.inner).toBe('string'); expect(typeof errorResult.inner).toBe('string');
} else { } else {
throw new Error('Expected errorResult to be fail'); throw new Error('Expected errorResult to be fail');
} }
}); });
}); });
+2 -3
View File
@@ -5,7 +5,7 @@
"useDefineForClassFields": true, "useDefineForClassFields": true,
"module": "ESNext", "module": "ESNext",
"resolveJsonModule": true, "resolveJsonModule": true,
"moduleResolution": "Node", "moduleResolution": "Node",
/** /**
* Typecheck JS in `.svelte` and `.js` files by default. * Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS. * Disable checkJs if you'd like to use dynamic types in JS.
@@ -15,13 +15,12 @@
"allowJs": true, "allowJs": true,
"checkJs": true, "checkJs": true,
"isolatedModules": true, "isolatedModules": true,
"paths": { "paths": {
"$lib/*": ["./src/lib/*"], "$lib/*": ["./src/lib/*"],
"$utils/*": ["./src/utils/*"], "$utils/*": ["./src/utils/*"],
"$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" }]
+15 -13
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')
}, }
}, }
}); });