diff --git a/client/package-lock.json b/client/package-lock.json
index c3afd7d..218f778 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -11,12 +11,14 @@
"@fontsource/roboto": "^5.0.8",
"@threlte/core": "^7.1.0",
"@threlte/extras": "^8.8.0",
+ "camera-controls": "^2.8.3",
"howler": "^2.2.4",
"material-icons": "^1.13.12",
"material-symbols": "^0.15.0",
"overlayscrollbars-svelte": "^0.5.3",
"socket.io-client": "^4.7.4",
"svelte-french-toast": "^1.2.0",
+ "svelte-tweakpane-ui": "^1.2.1",
"three": "^0.161.0"
},
"devDependencies": {
@@ -1035,6 +1037,11 @@
"integrity": "sha512-BRbo1fOtyVbhfLyuCWw6wAWp+U8UQle+ZXu84MYYWzYSEB28dyfnRBIE99eoG+qdAC0po6L2ScIEivcT07UaMA==",
"dev": true
},
+ "node_modules/@tweakpane/core": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@tweakpane/core/-/core-2.0.3.tgz",
+ "integrity": "sha512-qHci4XA1Wngpwy8IzsLh5JEdscz8aDti/9YhyOaq01si+cgNDaZfwzTtXdn1+xTxSnCM+pW4Zb2/4eqn+K1ATw=="
+ },
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
@@ -1300,6 +1307,14 @@
"node": ">= 6"
}
},
+ "node_modules/camera-controls": {
+ "version": "2.8.3",
+ "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-2.8.3.tgz",
+ "integrity": "sha512-zFjqUR6onLkG+z1A6vAWfzovxZxWVSvp6e5t3lfZgfgPZtX3n74aykNAUaoRbq8Y3tOxadHkDjbfGDOP9hFf2w==",
+ "peerDependencies": {
+ "three": ">=0.126.1"
+ }
+ },
"node_modules/caniuse-lite": {
"version": "1.0.30001588",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001588.tgz",
@@ -1592,9 +1607,7 @@
"node_modules/esm-env": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz",
- "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==",
- "dev": true,
- "peer": true
+ "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA=="
},
"node_modules/estree-walker": {
"version": "3.0.3",
@@ -1604,6 +1617,19 @@
"@types/estree": "^1.0.0"
}
},
+ "node_modules/fast-copy": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.1.tgz",
+ "integrity": "sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA=="
+ },
+ "node_modules/fast-equals": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz",
+ "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/fast-glob": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
@@ -2972,6 +2998,17 @@
"svelte": "^3.19.0 || ^4.0.0"
}
},
+ "node_modules/svelte-local-storage-store": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/svelte-local-storage-store/-/svelte-local-storage-store-0.6.4.tgz",
+ "integrity": "sha512-45WoY2vSGPQM1sIQJ9jTkPPj20hYeqm+af6mUGRFSPP5WglZf36YYoZqwmZZ8Dt/2SU8lem+BTA8/Z/8TkqNLg==",
+ "engines": {
+ "node": ">=0.14"
+ },
+ "peerDependencies": {
+ "svelte": "^3.48.0 || >4.0.0"
+ }
+ },
"node_modules/svelte-preprocess": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.3.tgz",
@@ -3035,6 +3072,115 @@
}
}
},
+ "node_modules/svelte-tweakpane-ui": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/svelte-tweakpane-ui/-/svelte-tweakpane-ui-1.2.1.tgz",
+ "integrity": "sha512-F62AFvZiqhXa0E3HMpUdmdWWyus/C+nTBFrFd/vEhHP6dYOQ4EN9qwvjdybL90DZFTIwkZe3w4oSbEGn1muKkQ==",
+ "dependencies": {
+ "@0b5vr/tweakpane-plugin-profiler": "^0.4.1",
+ "@0b5vr/tweakpane-plugin-rotation": "^0.2.0",
+ "@kitschpatrol/tweakpane-image-plugin": "^2.0.0",
+ "@pangenerator/tweakpane-textarea-plugin": "^2.0.0",
+ "@tweakpane/core": "^2.0.3",
+ "@tweakpane/plugin-camerakit": "^0.3.0",
+ "@tweakpane/plugin-essentials": "^0.2.1",
+ "esm-env": "^1.0.0",
+ "fast-copy": "^3.0.1",
+ "fast-equals": "^5.0.1",
+ "nanoid": "^5.0.6",
+ "svelte-local-storage-store": "^0.6.4",
+ "tweakpane": "^4.0.3",
+ "tweakpane-plugin-waveform": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "pnpm": ">=8.0.0"
+ },
+ "peerDependencies": {
+ "svelte": "^4.0.0"
+ }
+ },
+ "node_modules/svelte-tweakpane-ui/node_modules/@0b5vr/tweakpane-plugin-profiler": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@0b5vr/tweakpane-plugin-profiler/-/tweakpane-plugin-profiler-0.4.1.tgz",
+ "integrity": "sha512-jgkPbT24eQ7isj8F7/IsbdqrwvBoWBmwjqxdP35smD2D6xsx+9viR57SKBxi9PxTZDEayicmCzBk++0PTqRnBg==",
+ "peerDependencies": {
+ "tweakpane": "^4.0.0"
+ }
+ },
+ "node_modules/svelte-tweakpane-ui/node_modules/@0b5vr/tweakpane-plugin-rotation": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/@0b5vr/tweakpane-plugin-rotation/-/tweakpane-plugin-rotation-0.2.0.tgz",
+ "integrity": "sha512-LK+84kNTusEepVwiKH6ib/Pd+5RxI3UC4rHxn5c14GO58QS49Hh0ft3hFXt/NDzYEST17Q9qg96BcpclhCzYYQ==",
+ "peerDependencies": {
+ "tweakpane": "^4.0.0"
+ }
+ },
+ "node_modules/svelte-tweakpane-ui/node_modules/@kitschpatrol/tweakpane-image-plugin": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@kitschpatrol/tweakpane-image-plugin/-/tweakpane-image-plugin-2.0.0.tgz",
+ "integrity": "sha512-BzEZqIhD/dM7AW0Ebv+309L4k8ZZJ5fC9Zks4sozVK3FwJooviE6JzaFAuB7k0M5oX45Wyn59tQXdHafgsP3YA==",
+ "peerDependencies": {
+ "tweakpane": "^4.0.0"
+ }
+ },
+ "node_modules/svelte-tweakpane-ui/node_modules/@pangenerator/tweakpane-textarea-plugin": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@pangenerator/tweakpane-textarea-plugin/-/tweakpane-textarea-plugin-2.0.0.tgz",
+ "integrity": "sha512-BERPuuyJYWvtJzXh4wtgYspza0ihigE2m4qs57ERKtWG59+lI2t/2TOXlwz7Xyx/QEIH25uO1g732YCljgKaUw==",
+ "peerDependencies": {
+ "tweakpane": "^4.0.0"
+ }
+ },
+ "node_modules/svelte-tweakpane-ui/node_modules/@tweakpane/plugin-camerakit": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@tweakpane/plugin-camerakit/-/plugin-camerakit-0.3.0.tgz",
+ "integrity": "sha512-6UwgwDKU+oaAgXJ2D/pOoIpEAZts0RyeLmVzBJGs+VVNqSfkiHzL0i5XD+XnmSL2PaLXBne0dlz0bYOrjmeELw==",
+ "peerDependencies": {
+ "tweakpane": "^4.0.0-beta.2"
+ }
+ },
+ "node_modules/svelte-tweakpane-ui/node_modules/@tweakpane/plugin-essentials": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/@tweakpane/plugin-essentials/-/plugin-essentials-0.2.1.tgz",
+ "integrity": "sha512-VbFU1/uD+CJNFQdfLXUOLjeG5HyUZH97Ox9CxmyVetg1hqjVun3C83HAGFULyhKzl8tSgii8jr304r8QpdHwzQ==",
+ "peerDependencies": {
+ "tweakpane": "^4.0.0"
+ }
+ },
+ "node_modules/svelte-tweakpane-ui/node_modules/nanoid": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.6.tgz",
+ "integrity": "sha512-rRq0eMHoGZxlvaFOUdK1Ev83Bd1IgzzR+WJ3IbDJ7QOSdAxYjlurSPqFs9s4lJg29RT6nPwizFtJhQS6V5xgiA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.js"
+ },
+ "engines": {
+ "node": "^18 || >=20"
+ }
+ },
+ "node_modules/svelte-tweakpane-ui/node_modules/tweakpane": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/tweakpane/-/tweakpane-4.0.3.tgz",
+ "integrity": "sha512-BlcWOAe8oe4c+k9pmLBARGdWB6MVZMszayekkixQXTgkxTaYoTUpHpwVEp+3HkoamZkomodpbBf0CkguIHTgLg==",
+ "funding": {
+ "url": "https://github.com/sponsors/cocopon"
+ }
+ },
+ "node_modules/svelte-tweakpane-ui/node_modules/tweakpane-plugin-waveform": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/tweakpane-plugin-waveform/-/tweakpane-plugin-waveform-1.0.0.tgz",
+ "integrity": "sha512-fyTRe6Emt7YpgHC5iiTZgk6RHflNm5VIOAsl2+l3mm96+KE8I+7sNPeyADxKcfcQF23c7/R3La5WNhaHNyeJag==",
+ "peerDependencies": {
+ "tweakpane": "^4.0.0"
+ }
+ },
"node_modules/svelte-writable-derived": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/svelte-writable-derived/-/svelte-writable-derived-3.1.0.tgz",
diff --git a/client/package.json b/client/package.json
index 634fbe2..6c81629 100644
--- a/client/package.json
+++ b/client/package.json
@@ -31,12 +31,14 @@
"@fontsource/roboto": "^5.0.8",
"@threlte/core": "^7.1.0",
"@threlte/extras": "^8.8.0",
+ "camera-controls": "^2.8.3",
"howler": "^2.2.4",
"material-icons": "^1.13.12",
"material-symbols": "^0.15.0",
"overlayscrollbars-svelte": "^0.5.3",
"socket.io-client": "^4.7.4",
"svelte-french-toast": "^1.2.0",
+ "svelte-tweakpane-ui": "^1.2.1",
"three": "^0.161.0"
}
}
diff --git a/client/src/lib/Dashboard/Visualization/CameraControls/CameraControls.svelte b/client/src/lib/Dashboard/Visualization/CameraControls/CameraControls.svelte
new file mode 100644
index 0000000..f6d3935
--- /dev/null
+++ b/client/src/lib/Dashboard/Visualization/CameraControls/CameraControls.svelte
@@ -0,0 +1,106 @@
+
+
+
+
+ {
+ disableAutoRotate = true
+ }}
+ on:zoom={e => {
+ console.log('zoomstart', e)
+ }}
+ on:controlend={() => {
+ disableAutoRotate = false
+ }}
+ {...$$restProps}
+ bind:this={$forwardingComponent}
+>
+
+
diff --git a/client/src/lib/Dashboard/Visualization/CameraControls/CameraControls.svelte.d.ts b/client/src/lib/Dashboard/Visualization/CameraControls/CameraControls.svelte.d.ts
new file mode 100644
index 0000000..6bc8f0e
--- /dev/null
+++ b/client/src/lib/Dashboard/Visualization/CameraControls/CameraControls.svelte.d.ts
@@ -0,0 +1,16 @@
+import type { Events, Props, Slots } from '@threlte/core'
+import CC from 'camera-controls'
+import type { SvelteComponent } from 'svelte'
+
+export type CameraControlsProps = Props & {
+ autoRotate?: boolean
+ autoRotateSpeed?: number
+}
+export type CameraControlsEvents = Events
+export type CameraControlsSlots = Slots
+
+export default class CameraControls extends SvelteComponent<
+ CameraControlsProps,
+ CameraControlsEvents,
+ CameraControlsSlots
+> {}
diff --git a/client/src/lib/Dashboard/Visualization/CameraControls/Scene.svelte b/client/src/lib/Dashboard/Visualization/CameraControls/Scene.svelte
new file mode 100644
index 0000000..f1e1df2
--- /dev/null
+++ b/client/src/lib/Dashboard/Visualization/CameraControls/Scene.svelte
@@ -0,0 +1,39 @@
+
+
+ {
+ ref.lookAt(0, 1, 0)
+ }}
+>
+ {
+ $cameraControls = ref
+ }}
+ autoRotate
+ />
+
+
+
+
+ {
+ $mesh = ref
+ }}
+/>
+
+
diff --git a/client/src/lib/Dashboard/Visualization/CameraControls/utils/cameraStore.ts b/client/src/lib/Dashboard/Visualization/CameraControls/utils/cameraStore.ts
new file mode 100644
index 0000000..8bb6f23
--- /dev/null
+++ b/client/src/lib/Dashboard/Visualization/CameraControls/utils/cameraStore.ts
@@ -0,0 +1,6 @@
+import type CameraControls from 'camera-controls'
+import { writable } from 'svelte/store'
+import Hornet from '../../models/Hornet.svelte'
+
+export const cameraControls = writable()
+export const mesh = writable()
diff --git a/client/src/lib/Dashboard/Visualization/CameraControls/utils/useControlsContext.ts b/client/src/lib/Dashboard/Visualization/CameraControls/utils/useControlsContext.ts
new file mode 100644
index 0000000..c8862b9
--- /dev/null
+++ b/client/src/lib/Dashboard/Visualization/CameraControls/utils/useControlsContext.ts
@@ -0,0 +1,20 @@
+import { useThrelteUserContext } from '@threlte/core'
+import { writable, type Writable } from 'svelte/store'
+import type { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
+
+type ControlsContext = {
+ orbitControls: Writable
+}
+
+/**
+ * ### `useControlsContext`
+ *
+ * This hook is used to register the `OrbitControls` instance with the
+ * `ControlsContext`. We're using this context to enable and disable the
+ * controls when the user is interacting with the TransformControls.
+ */
+export const useControlsContext = (): ControlsContext => {
+ return useThrelteUserContext('threlte-controls', {
+ orbitControls: writable(undefined),
+ })
+}
diff --git a/client/src/lib/Dashboard/Visualization/Visualization.svelte b/client/src/lib/Dashboard/Visualization/Visualization.svelte
index 4ae23fe..6d6ea20 100644
--- a/client/src/lib/Dashboard/Visualization/Visualization.svelte
+++ b/client/src/lib/Dashboard/Visualization/Visualization.svelte
@@ -1,6 +1,47 @@