feat: vastly improve visualization; add more sequences; improve error handling in rust; improve UI; update settings;
This commit is contained in:
parent
eb41340ee0
commit
05c52d4d4a
18 changed files with 516 additions and 827 deletions
143
client/package-lock.json
generated
143
client/package-lock.json
generated
|
@ -19,7 +19,6 @@
|
||||||
"overlayscrollbars-svelte": "^0.5.3",
|
"overlayscrollbars-svelte": "^0.5.3",
|
||||||
"socket.io-client": "^4.7.4",
|
"socket.io-client": "^4.7.4",
|
||||||
"svelte-french-toast": "^1.2.0",
|
"svelte-french-toast": "^1.2.0",
|
||||||
"svelte-tweakpane-ui": "^1.2.1",
|
|
||||||
"three": "^0.161.0"
|
"three": "^0.161.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -1052,11 +1051,6 @@
|
||||||
"integrity": "sha512-BRbo1fOtyVbhfLyuCWw6wAWp+U8UQle+ZXu84MYYWzYSEB28dyfnRBIE99eoG+qdAC0po6L2ScIEivcT07UaMA==",
|
"integrity": "sha512-BRbo1fOtyVbhfLyuCWw6wAWp+U8UQle+ZXu84MYYWzYSEB28dyfnRBIE99eoG+qdAC0po6L2ScIEivcT07UaMA==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@types/cookie": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||||
|
@ -1622,7 +1616,9 @@
|
||||||
"node_modules/esm-env": {
|
"node_modules/esm-env": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz",
|
||||||
"integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA=="
|
"integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==",
|
||||||
|
"dev": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/estree-walker": {
|
"node_modules/estree-walker": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
|
@ -1632,19 +1628,6 @@
|
||||||
"@types/estree": "^1.0.0"
|
"@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": {
|
"node_modules/fast-glob": {
|
||||||
"version": "3.3.2",
|
"version": "3.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
||||||
|
@ -3013,17 +2996,6 @@
|
||||||
"svelte": "^3.19.0 || ^4.0.0"
|
"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": {
|
"node_modules/svelte-preprocess": {
|
||||||
"version": "5.1.3",
|
"version": "5.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.3.tgz",
|
||||||
|
@ -3087,115 +3059,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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": {
|
"node_modules/svelte-writable-derived": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/svelte-writable-derived/-/svelte-writable-derived-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/svelte-writable-derived/-/svelte-writable-derived-3.1.0.tgz",
|
||||||
|
|
|
@ -39,7 +39,6 @@
|
||||||
"overlayscrollbars-svelte": "^0.5.3",
|
"overlayscrollbars-svelte": "^0.5.3",
|
||||||
"socket.io-client": "^4.7.4",
|
"socket.io-client": "^4.7.4",
|
||||||
"svelte-french-toast": "^1.2.0",
|
"svelte-french-toast": "^1.2.0",
|
||||||
"svelte-tweakpane-ui": "^1.2.1",
|
|
||||||
"three": "^0.161.0"
|
"three": "^0.161.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,64 +9,67 @@ const NTABLE_IP: (u8, u8, u8, u8) = (10, 12, 80, 2);
|
||||||
const NTABLE_PORT: u16 = 5810;
|
const NTABLE_PORT: u16 = 5810;
|
||||||
|
|
||||||
pub async fn subscribe_topics(app_handle: AppHandle) {
|
pub async fn subscribe_topics(app_handle: AppHandle) {
|
||||||
let client = loop {
|
loop {
|
||||||
match Client::try_new_w_config(
|
// I hope this doesn't lead to a catastrophic infinite loop failure
|
||||||
SocketAddrV4::new(
|
let client = loop {
|
||||||
Ipv4Addr::new(NTABLE_IP.0, NTABLE_IP.1, NTABLE_IP.2, NTABLE_IP.3),
|
match Client::try_new_w_config(
|
||||||
NTABLE_PORT,
|
SocketAddrV4::new(
|
||||||
),
|
Ipv4Addr::new(NTABLE_IP.0, NTABLE_IP.1, NTABLE_IP.2, NTABLE_IP.3),
|
||||||
Config {
|
NTABLE_PORT,
|
||||||
..Default::default()
|
),
|
||||||
},
|
Config {
|
||||||
)
|
..Default::default()
|
||||||
.await
|
},
|
||||||
{
|
)
|
||||||
Ok(client) => {
|
.await
|
||||||
println!("Client created");
|
{
|
||||||
app_handle
|
Ok(client) => {
|
||||||
.emit_all("telemetry_connected", "connected")
|
println!("Client created");
|
||||||
.expect("Failed to emit telemetry_status connected event");
|
app_handle
|
||||||
break client; // Exit the loop if the client is successfully created
|
.emit_all("telemetry_connected", "connected")
|
||||||
}
|
.expect("Failed to emit telemetry_status connected event");
|
||||||
Err(e) => {
|
break client; // Exit the loop if the client is successfully created
|
||||||
println!("Failed to create client: {}. Retrying in 3 seconds...", e);
|
}
|
||||||
app_handle
|
Err(e) => {
|
||||||
.emit_all("telemetry_status", "disconnected")
|
println!("Failed to create client: {}. Retrying in 3 seconds...", e);
|
||||||
.expect("Failed to emit telemetry_status disconnected event");
|
app_handle
|
||||||
|
.emit_all("telemetry_status", "disconnected")
|
||||||
|
.expect("Failed to emit telemetry_status disconnected event");
|
||||||
|
|
||||||
sleep(Duration::from_secs(3)).await; // Wait for 3 seconds before retrying
|
sleep(Duration::from_secs(3)).await; // Wait for 3 seconds before retrying
|
||||||
continue; // Continue the loop to retry
|
continue; // Continue the loop to retry
|
||||||
}
|
}
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
let mut subscription = client
|
let mut subscription = client
|
||||||
.subscribe_w_options(
|
.subscribe_w_options(
|
||||||
&["/SmartDashboard"],
|
&["/SmartDashboard"],
|
||||||
Some(SubscriptionOptions {
|
Some(SubscriptionOptions {
|
||||||
all: Some(true),
|
all: Some(true),
|
||||||
prefix: Some(true),
|
prefix: Some(true),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to subscribe");
|
.expect("Failed to subscribe");
|
||||||
while let Some(message) = subscription.next().await {
|
while let Some(message) = subscription.next().await {
|
||||||
let mut modified_message = message.clone();
|
let mut modified_message = message.clone();
|
||||||
|
|
||||||
if let Some(stripped) = modified_message.topic_name.strip_prefix("/SmartDashboard/") {
|
if let Some(stripped) = modified_message.topic_name.strip_prefix("/SmartDashboard/") {
|
||||||
modified_message.topic_name = stripped.to_string();
|
modified_message.topic_name = stripped.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let json_message = to_string(&modified_message).expect("Failed to serialize message");
|
||||||
|
app_handle
|
||||||
|
.emit_all("telemetry_data", json_message.clone())
|
||||||
|
.expect("Failed to send telemetry message");
|
||||||
|
|
||||||
|
println!("{}", json_message);
|
||||||
}
|
}
|
||||||
|
println!("disconnected");
|
||||||
let json_message = to_string(&modified_message).expect("Failed to serialize message");
|
|
||||||
app_handle
|
app_handle
|
||||||
.emit_all("telemetry_data", json_message.clone())
|
.emit_all("telemetry_status", "disconnected")
|
||||||
.expect("Failed to send telemetry message");
|
.expect("Failed to emit telemetry_disconnected event");
|
||||||
|
|
||||||
println!("{}", json_message);
|
|
||||||
}
|
}
|
||||||
println!("disconnected");
|
|
||||||
app_handle
|
|
||||||
.emit_all("telemetry_status", "disconnected")
|
|
||||||
.expect("Failed to emit telemetry_disconnected event");
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onDestroy } from 'svelte'
|
||||||
import {
|
import {
|
||||||
cameraControls,
|
cameraControls,
|
||||||
cameraState,
|
cameraState,
|
||||||
|
@ -11,21 +12,15 @@
|
||||||
setStationaryTelemetry,
|
setStationaryTelemetry,
|
||||||
} from './telemetrySimulators'
|
} from './telemetrySimulators'
|
||||||
|
|
||||||
let cameraMode = 'orbit'
|
let value: typeof $cameraState.mode = $cameraState.mode
|
||||||
|
|
||||||
const changeCamera = () => {
|
$: {
|
||||||
if (cameraMode === 'follow-direction') {
|
cameraState.set('mode', value)
|
||||||
cameraMode = 'orbit'
|
|
||||||
cameraState.set('mode', 'orbit')
|
|
||||||
cameraState.set('userControlled', false)
|
|
||||||
console.log($cameraState.mode)
|
|
||||||
} else {
|
|
||||||
cameraMode = 'follow-direction'
|
|
||||||
cameraState.set('mode', 'follow-direction')
|
|
||||||
cameraState.set('userControlled', true)
|
|
||||||
console.log($cameraState.mode)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const unsubscribe = cameraState.subscribe(state => {
|
||||||
|
if (value !== state.mode) value = state.mode
|
||||||
|
})
|
||||||
|
onDestroy(unsubscribe)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AppContainer
|
<AppContainer
|
||||||
|
@ -69,7 +64,11 @@
|
||||||
<button class="button" on:click={simulateMotion}>
|
<button class="button" on:click={simulateMotion}>
|
||||||
Simulate random motion
|
Simulate random motion
|
||||||
</button>
|
</button>
|
||||||
<button class="button" on:click={changeCamera}> Change camera mode </button>
|
<select bind:value class="bg-slate-300">
|
||||||
|
<option value="orbit">Orbit</option>
|
||||||
|
<option value="follow-facing">Follow Facing</option>
|
||||||
|
<option value="follow-direction">Follow Direction</option>
|
||||||
|
</select>
|
||||||
</AppContainer>
|
</AppContainer>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
|
|
|
@ -47,6 +47,11 @@
|
||||||
tooltip="Selects the language/locale used for Jankboard voice prompts. Does not affect application language (ie. Jankboard itself will always be in English)."
|
tooltip="Selects the language/locale used for Jankboard voice prompts. Does not affect application language (ie. Jankboard itself will always be in English)."
|
||||||
>Voice Prompt Language</SettingsSelector
|
>Voice Prompt Language</SettingsSelector
|
||||||
>
|
>
|
||||||
|
<SettingsToggle
|
||||||
|
setting="sentry"
|
||||||
|
tooltip="Sentry mode protects the robot and operator from foreign threats."
|
||||||
|
>Sentry Mode</SettingsToggle
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
class="mt-10 px-4 py-2 bg-amber-600 hover:brightness-75 text-medium rounded-lg w-min"
|
class="mt-10 px-4 py-2 bg-amber-600 hover:brightness-75 text-medium rounded-lg w-min"
|
||||||
on:click={resetSettings}>Reset</button
|
on:click={resetSettings}>Reset</button
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
>
|
>
|
||||||
<div class="text-lg font-medium">SPEED<br />LIMIT</div>
|
<div class="text-lg font-medium">SPEED<br />LIMIT</div>
|
||||||
<div class="text-2xl font-bold transition">
|
<div class="text-2xl font-bold transition">
|
||||||
{speedLimit}
|
{speedLimit.toFixed(1)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,40 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
shiftedInAutoSequence,
|
||||||
|
shiftedInDriveSequence,
|
||||||
|
shiftedInLowSequence,
|
||||||
|
shiftedInNeutralSequence,
|
||||||
|
shiftedInParkSequence,
|
||||||
|
shiftedInReverseSequence,
|
||||||
|
} from '../../Sequences/sequences'
|
||||||
|
|
||||||
export let selectedGear: Gear
|
export let selectedGear: Gear
|
||||||
export let placeholder: boolean
|
export let placeholder: boolean
|
||||||
|
|
||||||
|
const shift = (selectedGear: Gear) => {
|
||||||
|
switch (selectedGear) {
|
||||||
|
case 'park':
|
||||||
|
shiftedInParkSequence()
|
||||||
|
break
|
||||||
|
case 'reverse':
|
||||||
|
shiftedInReverseSequence()
|
||||||
|
break
|
||||||
|
case 'neutral':
|
||||||
|
shiftedInNeutralSequence()
|
||||||
|
break
|
||||||
|
case 'low':
|
||||||
|
shiftedInLowSequence()
|
||||||
|
break
|
||||||
|
case 'auto':
|
||||||
|
shiftedInAutoSequence()
|
||||||
|
break
|
||||||
|
case 'drive':
|
||||||
|
shiftedInDriveSequence()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: shift(selectedGear)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex justify-center w-full transition">
|
<div class="flex justify-center w-full transition">
|
||||||
|
|
|
@ -7,28 +7,39 @@
|
||||||
Displays the drive mode
|
Displays the drive mode
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { fade } from 'svelte/transition'
|
import {
|
||||||
|
modeChillSequence,
|
||||||
|
modeCruiseSequence,
|
||||||
|
modeLudicrousSequence,
|
||||||
|
} from '../../Sequences/sequences'
|
||||||
|
|
||||||
export let selectedMode: Mode
|
export let selectedMode: Mode
|
||||||
export let placeholder: boolean
|
export let placeholder: boolean
|
||||||
|
|
||||||
let modeText = ''
|
let modeText = ''
|
||||||
|
|
||||||
|
const setModeText = (selectedMode: Mode) => {
|
||||||
|
switch (selectedMode) {
|
||||||
|
case 'chill':
|
||||||
|
modeText = 'CHILL'
|
||||||
|
modeChillSequence()
|
||||||
|
break
|
||||||
|
case 'cruise':
|
||||||
|
modeText = 'CRUISE'
|
||||||
|
modeCruiseSequence()
|
||||||
|
break
|
||||||
|
case 'ludicrous':
|
||||||
|
modeText = 'LUDICROUS'
|
||||||
|
modeLudicrousSequence()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (placeholder) {
|
if (placeholder) {
|
||||||
modeText = 'DISCONNECTED'
|
modeText = 'DISCONNECTED'
|
||||||
} else {
|
} else {
|
||||||
switch (selectedMode) {
|
setModeText(selectedMode)
|
||||||
case 'chill':
|
|
||||||
modeText = 'CHILL'
|
|
||||||
break
|
|
||||||
case 'cruise':
|
|
||||||
modeText = 'CRUISE'
|
|
||||||
break
|
|
||||||
case 'ludicrous':
|
|
||||||
modeText = 'LUDICROUS'
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
Vector4,
|
Vector4,
|
||||||
type PerspectiveCamera,
|
type PerspectiveCamera,
|
||||||
} from 'three'
|
} from 'three'
|
||||||
|
// @ts-expect-error
|
||||||
import { DEG2RAD } from 'three/src/math/MathUtils'
|
import { DEG2RAD } from 'three/src/math/MathUtils'
|
||||||
import { cameraState } from './utils/cameraStore'
|
import { cameraState } from './utils/cameraStore'
|
||||||
|
|
||||||
|
@ -61,7 +62,7 @@
|
||||||
|
|
||||||
const { renderer, invalidate } = useThrelte()
|
const { renderer, invalidate } = useThrelte()
|
||||||
|
|
||||||
export let autoRotate = false
|
let autoRotate = true
|
||||||
export let autoRotateSpeed = 1
|
export let autoRotateSpeed = 1
|
||||||
|
|
||||||
export const ref = new CameraControls(
|
export const ref = new CameraControls(
|
||||||
|
@ -73,7 +74,7 @@
|
||||||
|
|
||||||
useTask(
|
useTask(
|
||||||
delta => {
|
delta => {
|
||||||
if (autoRotate && !$cameraState.userControlled) {
|
if (autoRotate && $cameraState.mode === 'orbit') {
|
||||||
getControls().azimuthAngle += 4 * delta * DEG2RAD * autoRotateSpeed
|
getControls().azimuthAngle += 4 * delta * DEG2RAD * autoRotateSpeed
|
||||||
}
|
}
|
||||||
const updated = getControls().update(delta)
|
const updated = getControls().update(delta)
|
||||||
|
@ -90,13 +91,10 @@
|
||||||
<T
|
<T
|
||||||
is={ref}
|
is={ref}
|
||||||
on:controlstart={e => {
|
on:controlstart={e => {
|
||||||
cameraState.set('userControlled', true)
|
autoRotate = false
|
||||||
}}
|
|
||||||
on:zoom={e => {
|
|
||||||
console.log('zoomstart', e)
|
|
||||||
}}
|
}}
|
||||||
on:controlend={() => {
|
on:controlend={() => {
|
||||||
cameraState.set('userControlled', false)
|
autoRotate = true
|
||||||
}}
|
}}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
bind:this={$forwardingComponent}
|
bind:this={$forwardingComponent}
|
||||||
|
|
|
@ -1,96 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { T, useTask } from '@threlte/core'
|
|
||||||
import { Grid } from '@threlte/extras'
|
|
||||||
import CameraControls from './CameraControls.svelte'
|
|
||||||
import { cameraControls, mesh, cameraState } from './utils/cameraStore'
|
|
||||||
import { Vector3 } from 'three'
|
|
||||||
import { onMount } from 'svelte'
|
|
||||||
import RobotDecimated from '../models/RobotDecimated.svelte'
|
|
||||||
import { telemetryReadonlyStore } from '../../../stores/telemetryStore'
|
|
||||||
import { DEG2RAD } from 'three/src/math/MathUtils.js'
|
|
||||||
|
|
||||||
const SPEED_MULTIPLIER = 4
|
|
||||||
const axis = new Vector3(0, 1, 0)
|
|
||||||
|
|
||||||
const follow = (delta: number) => {
|
|
||||||
// the object's position is bound to the prop
|
|
||||||
if (!$mesh || !$cameraControls) return
|
|
||||||
|
|
||||||
const offsetPosition = new Vector3()
|
|
||||||
offsetPosition.copy($mesh.position)
|
|
||||||
const offsetVector = new Vector3(2.5, 0, -2)
|
|
||||||
offsetVector.applyAxisAngle(axis, $mesh.rotation.y)
|
|
||||||
offsetPosition.add(offsetVector)
|
|
||||||
|
|
||||||
cameraState.set('mode', 'follow-direction')
|
|
||||||
if (($cameraState.mode = 'follow-facing')) {
|
|
||||||
$cameraControls.setLookAt(
|
|
||||||
offsetPosition.x + 15 * Math.sin($mesh.rotation.y),
|
|
||||||
offsetPosition.y + 8,
|
|
||||||
offsetPosition.z + 15 * Math.cos($mesh.rotation.y),
|
|
||||||
offsetPosition.x,
|
|
||||||
offsetPosition.y,
|
|
||||||
offsetPosition.z,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (($cameraState.mode = 'orbit')) {
|
|
||||||
$cameraControls.moveTo(
|
|
||||||
offsetPosition.x,
|
|
||||||
offsetPosition.y,
|
|
||||||
offsetPosition.z
|
|
||||||
// true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useTask(delta => {
|
|
||||||
$mesh.position.x +=
|
|
||||||
$telemetryReadonlyStore['chassis-y-speed'] * delta * SPEED_MULTIPLIER
|
|
||||||
$mesh.position.z +=
|
|
||||||
$telemetryReadonlyStore['chassis-x-speed'] * delta * SPEED_MULTIPLIER
|
|
||||||
$mesh.rotation.y = $telemetryReadonlyStore.orientation * DEG2RAD
|
|
||||||
follow(delta)
|
|
||||||
})
|
|
||||||
|
|
||||||
onMount(() => {})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<T.PerspectiveCamera
|
|
||||||
makeDefault
|
|
||||||
position={[12, 10, 12]}
|
|
||||||
on:create={({ ref }) => {
|
|
||||||
ref.lookAt(0, 1, 0)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CameraControls
|
|
||||||
on:create={({ ref }) => {
|
|
||||||
$cameraControls = ref
|
|
||||||
}}
|
|
||||||
autoRotate={$cameraState.mode === 'orbit'}
|
|
||||||
autoRotateSpeed={3}
|
|
||||||
/>
|
|
||||||
</T.PerspectiveCamera>
|
|
||||||
|
|
||||||
<T.DirectionalLight position={[3, 10, 7]} />
|
|
||||||
<T.AmbientLight color={'#f0f0f0'} intensity={0.1} />
|
|
||||||
|
|
||||||
<RobotDecimated
|
|
||||||
scale={[10, 10, 10]}
|
|
||||||
position.y={0}
|
|
||||||
on:create={({ ref }) => {
|
|
||||||
// @ts-expect-error
|
|
||||||
$mesh = ref
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Grid
|
|
||||||
sectionColor={'#ff3e00'}
|
|
||||||
sectionThickness={1}
|
|
||||||
fadeDistance={100}
|
|
||||||
cellSize={6}
|
|
||||||
sectionSize={24}
|
|
||||||
cellColor={'#cccccc'}
|
|
||||||
infiniteGrid
|
|
||||||
/>
|
|
|
@ -14,12 +14,10 @@ type CameraMode =
|
||||||
|
|
||||||
interface CameraState {
|
interface CameraState {
|
||||||
mode: CameraMode
|
mode: CameraMode
|
||||||
userControlled: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { set, update, subscribe } = writable<CameraState>({
|
const { set, update, subscribe } = writable<CameraState>({
|
||||||
mode: 'orbit',
|
mode: 'orbit',
|
||||||
userControlled: false,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const createCameraState = () => {
|
const createCameraState = () => {
|
||||||
|
@ -28,7 +26,7 @@ const createCameraState = () => {
|
||||||
subscribe,
|
subscribe,
|
||||||
set: (prop: keyof CameraState, val: any) =>
|
set: (prop: keyof CameraState, val: any) =>
|
||||||
update(state => ({ ...state, [prop]: val })),
|
update(state => ({ ...state, [prop]: val })),
|
||||||
reset: () => set({ mode: 'orbit', userControlled: false }),
|
reset: () => set({ mode: 'orbit' }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,165 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
|
||||||
import {
|
|
||||||
Camera,
|
|
||||||
Vector2,
|
|
||||||
Vector3,
|
|
||||||
Quaternion,
|
|
||||||
Object3D,
|
|
||||||
type Object3DEventMap,
|
|
||||||
Group,
|
|
||||||
} from 'three'
|
|
||||||
import { useThrelte, useParent, useTask } from '@threlte/core'
|
|
||||||
|
|
||||||
export let object: Group<Object3DEventMap>
|
|
||||||
export let rotateSpeed = 1.0
|
|
||||||
export let shouldOrbit: boolean
|
|
||||||
|
|
||||||
$: if (object) {
|
|
||||||
// console.log(object)
|
|
||||||
// object.position.y = 10
|
|
||||||
// // Calculate the direction vector towards (0, 0, 0)
|
|
||||||
// const target = new Vector3(0, 0, 0)
|
|
||||||
// const direction = target.clone().sub(object.position).normalize()
|
|
||||||
// // Extract the forward direction from the object's current rotation matrix
|
|
||||||
// const currentDirection = new Vector3(0, 1, 0)
|
|
||||||
// currentDirection.applyQuaternion(object.quaternion)
|
|
||||||
// // Calculate the axis and angle to rotate the object
|
|
||||||
// const rotationAxis = currentDirection.clone().cross(direction).normalize()
|
|
||||||
// const rotationAngle = Math.acos(currentDirection.dot(direction))
|
|
||||||
// // Rotate the object using rotateOnAxis()
|
|
||||||
// object.rotateOnAxis(rotationAxis, rotationAngle)
|
|
||||||
}
|
|
||||||
|
|
||||||
export let idealOffset = { x: -0.5, y: 2, z: -3 }
|
|
||||||
export let idealLookAt = { x: 0, y: 1, z: 5 }
|
|
||||||
|
|
||||||
const currentPosition = new Vector3()
|
|
||||||
const currentLookAt = new Vector3()
|
|
||||||
|
|
||||||
let isOrbiting = false
|
|
||||||
let pointerDown = false
|
|
||||||
|
|
||||||
const rotateStart = new Vector2()
|
|
||||||
const rotateEnd = new Vector2()
|
|
||||||
const rotateDelta = new Vector2()
|
|
||||||
|
|
||||||
const axis = new Vector3(0, 1, 0)
|
|
||||||
const rotationQuat = new Quaternion()
|
|
||||||
|
|
||||||
const { renderer, invalidate } = useThrelte()
|
|
||||||
|
|
||||||
const domElement = renderer.domElement
|
|
||||||
const camera = useParent()
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
|
|
||||||
const isCamera = (p: any): p is Camera => {
|
|
||||||
return p.isCamera
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isCamera($camera)) {
|
|
||||||
throw new Error(
|
|
||||||
'Parent missing: <PointerLockControls> need to be a child of a <Camera>'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is basically your update function
|
|
||||||
useTask(delta => {
|
|
||||||
// the object's position is bound to the prop
|
|
||||||
if (!object) return
|
|
||||||
|
|
||||||
// then we calculate our ideal's
|
|
||||||
const offset = vectorFromObject(idealOffset)
|
|
||||||
const lookAt = vectorFromObject(idealLookAt)
|
|
||||||
|
|
||||||
// camera is based on character so we rotation character first
|
|
||||||
// rotationQuat.setFromAxisAngle(axis, rotateSpeed * delta)
|
|
||||||
// object.quaternion.multiply(rotationQuat)
|
|
||||||
|
|
||||||
// and how far we should move towards them
|
|
||||||
const t = 1.0 - Math.pow(0.001, delta)
|
|
||||||
currentPosition.lerp(offset, t)
|
|
||||||
currentLookAt.lerp(lookAt, t)
|
|
||||||
|
|
||||||
// typescript HACKS! never do this! How does this work? who knows!
|
|
||||||
const robotPosition = vectorFromObject(
|
|
||||||
object as unknown as { x: number; y: number; z: number }
|
|
||||||
)
|
|
||||||
|
|
||||||
const horizontalOffsetDistance = 15 // Distance behind the leading vector
|
|
||||||
const direction = new Vector3(0, 0, 1) // Default forward direction in Three.js is negative z-axis, so behind is positive z-axis
|
|
||||||
const verticalOffset = new Vector3(0, -5, 0)
|
|
||||||
|
|
||||||
// Calculate the offset vector
|
|
||||||
const offsetVector = direction
|
|
||||||
.normalize()
|
|
||||||
.multiplyScalar(horizontalOffsetDistance)
|
|
||||||
.add(verticalOffset)
|
|
||||||
|
|
||||||
// If the leading object is rotating, apply its rotation to the offset vector
|
|
||||||
const rotatedOffsetVector = offsetVector.applyQuaternion(object.quaternion)
|
|
||||||
|
|
||||||
const leftOffset = -1.5
|
|
||||||
function shiftVectorLeft(
|
|
||||||
vector: Vector3,
|
|
||||||
amount: number,
|
|
||||||
upDirection = axis
|
|
||||||
): void {
|
|
||||||
// Calculate the left direction. Assuming 'up' is the global up direction or a custom up vector.
|
|
||||||
// This creates a vector pointing to the "left" of the original vector, in a 3D context.
|
|
||||||
let leftDirection = new Vector3()
|
|
||||||
.crossVectors(upDirection, vector)
|
|
||||||
.normalize()
|
|
||||||
|
|
||||||
// Scale the left direction by the desired amount
|
|
||||||
leftDirection.multiplyScalar(amount)
|
|
||||||
|
|
||||||
// Add the scaled left direction to the original vector, mutating it
|
|
||||||
vector.add(leftDirection)
|
|
||||||
}
|
|
||||||
|
|
||||||
shiftVectorLeft(rotatedOffsetVector, leftOffset)
|
|
||||||
|
|
||||||
// Calculate the trailing vector's position
|
|
||||||
const trailingVector = robotPosition.clone().sub(rotatedOffsetVector)
|
|
||||||
|
|
||||||
function shiftVectorLeftNonMutate(
|
|
||||||
vector: Vector3,
|
|
||||||
amount: number,
|
|
||||||
upDirection = axis
|
|
||||||
): Vector3 {
|
|
||||||
// Create a new vector to avoid mutating the original vector
|
|
||||||
let shiftedVector = vector.clone()
|
|
||||||
|
|
||||||
// Calculate the left direction. Assuming 'up' is the global up direction or a custom up vector.
|
|
||||||
// This creates a vector pointing to the "left" of the original vector, in a 3D context.
|
|
||||||
let leftDirection = new Vector3()
|
|
||||||
.crossVectors(upDirection, vector)
|
|
||||||
.normalize()
|
|
||||||
|
|
||||||
// Scale the left direction by the desired amount and add it to the original vector
|
|
||||||
leftDirection.multiplyScalar(amount)
|
|
||||||
shiftedVector.add(leftDirection)
|
|
||||||
|
|
||||||
return shiftedVector
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!shouldOrbit) {
|
|
||||||
// then finally set the camera, a bit behind the model
|
|
||||||
$camera!.position.copy(trailingVector)
|
|
||||||
// Rotate the offset around the Y-axis
|
|
||||||
$camera!.lookAt(shiftVectorLeftNonMutate(currentLookAt, -leftOffset))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function vectorFromObject(vec: { x: number; y: number; z: number }) {
|
|
||||||
const { x, y, z } = vec
|
|
||||||
const ideal = new Vector3(x, y, z)
|
|
||||||
ideal.applyQuaternion(object.quaternion)
|
|
||||||
ideal.add(
|
|
||||||
new Vector3(object.position.x, object.position.y, object.position.z)
|
|
||||||
)
|
|
||||||
return ideal
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,155 +1,134 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { T, useTask } from '@threlte/core'
|
import { T, useTask } from '@threlte/core'
|
||||||
import { ContactShadows, Float, Grid, OrbitControls } from '@threlte/extras'
|
import { Grid } from '@threlte/extras'
|
||||||
import Controls from './Controls.svelte'
|
import CameraControls from './CameraControls/CameraControls.svelte'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Vector3,
|
cameraControls,
|
||||||
type Camera,
|
mesh,
|
||||||
type Group,
|
cameraState,
|
||||||
type Object3D,
|
} from './CameraControls/utils/cameraStore'
|
||||||
type Object3DEventMap,
|
import { Vector3 } from 'three'
|
||||||
} from 'three'
|
|
||||||
import {
|
|
||||||
telemetryReadonlyStore,
|
|
||||||
telemetryStore,
|
|
||||||
} from '../../stores/telemetryStore'
|
|
||||||
import { get } from 'svelte/store'
|
|
||||||
import { Vector2 } from 'three'
|
|
||||||
import { SmoothMotionController } from './smoothMotionController'
|
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
|
import RobotDecimated from './models/RobotDecimated.svelte'
|
||||||
|
import { telemetryReadonlyStore } from '../../stores/telemetryStore'
|
||||||
|
import { DEG2RAD } from 'three/src/math/MathUtils.js'
|
||||||
|
|
||||||
/* This is the root scene where the robot visualization is built.
|
const SPEED_MULTIPLIER = 4
|
||||||
It renders an infinite grid (it's not actually infinite, but we shouldn't run out
|
const axis = new Vector3(0, 1, 0)
|
||||||
of space in realistic use), and a 3D model of the robot. The camera is locked
|
|
||||||
to the model, and the model is rotated to match the robot's orientation.
|
|
||||||
A PID controller is used to smoothly rotate the model to match the robot's
|
|
||||||
orientation and dampen out jittering. How does it work? Who knows!
|
|
||||||
75% percent of this was created while reading
|
|
||||||
https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation,
|
|
||||||
and rest was generated by AI!
|
|
||||||
The rest of this codebase is remarkably jank-free, but this visualization module
|
|
||||||
is the most esoteric and jank code ever written.
|
|
||||||
*/
|
|
||||||
|
|
||||||
let shouldOrbit = true
|
const follow = (delta: number) => {
|
||||||
|
// the object's position is bound to the prop
|
||||||
|
if (!$mesh || !$cameraControls) return
|
||||||
|
|
||||||
// CONSTANTS
|
const offsetPosition = new Vector3()
|
||||||
const maxAngularVelocity = 2 // Max angular velocity, in radians per second
|
offsetPosition.copy($mesh.position)
|
||||||
const stoppingThreshold = 0.005 // Threshold in radians for when to consider the rotation close enough to stop
|
const offsetVector = new Vector3(2.5, 0, -2)
|
||||||
|
offsetVector.applyAxisAngle(axis, $mesh.rotation.y)
|
||||||
|
offsetPosition.add(offsetVector)
|
||||||
|
|
||||||
// Proportional control factor
|
const followDirection = () => {
|
||||||
const kP = 2 // Adjust this value based on responsiveness and stability needs
|
const angle = Math.atan2(
|
||||||
|
$telemetryReadonlyStore['chassis-y-speed'],
|
||||||
|
$telemetryReadonlyStore['chassis-x-speed']
|
||||||
|
)
|
||||||
|
|
||||||
// Sync robot orientation with target rotation
|
$cameraControls.setLookAt(
|
||||||
let targetRot = 0
|
offsetPosition.x - 13 * Math.sin(angle),
|
||||||
|
offsetPosition.y + 4,
|
||||||
// Updates rotation to match target with PID controller (intended to be invoked in useTask)
|
offsetPosition.z - 13 * Math.cos(angle),
|
||||||
let rot = 0 // (initial) rotation in radians
|
offsetPosition.x,
|
||||||
let angularVelocity = 0
|
offsetPosition.y,
|
||||||
const updateRotation = (delta: number) => {
|
offsetPosition.z,
|
||||||
let angleDifference = targetRot - rot
|
true
|
||||||
|
)
|
||||||
// Normalize angle difference to the range [-π, π]
|
$cameraControls.zoomTo(1.1, true)
|
||||||
angleDifference = ((angleDifference + Math.PI) % (2 * Math.PI)) - Math.PI
|
|
||||||
|
|
||||||
// Calculate the desired angular velocity based on the angle difference
|
|
||||||
let desiredVelocity =
|
|
||||||
Math.sign(angleDifference) *
|
|
||||||
Math.min(maxAngularVelocity, Math.abs(kP * angleDifference))
|
|
||||||
|
|
||||||
// If the object is very close to the target, adjust the desired velocity to zero to prevent overshooting
|
|
||||||
if (Math.abs(angleDifference) < stoppingThreshold) {
|
|
||||||
desiredVelocity = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjust angular velocity towards desired velocity
|
const followFacing = () => {
|
||||||
angularVelocity = desiredVelocity
|
if ($cameraState.mode === 'follow-facing') {
|
||||||
|
$cameraControls.setLookAt(
|
||||||
|
offsetPosition.x + 13 * Math.sin($mesh.rotation.y),
|
||||||
|
offsetPosition.y + 5,
|
||||||
|
offsetPosition.z + 13 * Math.cos($mesh.rotation.y),
|
||||||
|
offsetPosition.x,
|
||||||
|
offsetPosition.y + 2,
|
||||||
|
offsetPosition.z,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
$cameraControls.zoomTo(1.2, true)
|
||||||
|
}
|
||||||
|
|
||||||
// Update rotation
|
const orbit = () => {
|
||||||
rot += angularVelocity * delta
|
$cameraControls.zoomTo(0.8, true)
|
||||||
|
$cameraControls.moveTo(
|
||||||
|
offsetPosition.x + 4 * Math.sin($mesh.rotation.y),
|
||||||
|
offsetPosition.y + 3,
|
||||||
|
offsetPosition.z * Math.cos($mesh.rotation.y),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Normalize rot to the range [0, 2π]
|
switch ($cameraState.mode) {
|
||||||
if (rot < 0) rot += 2 * Math.PI
|
case 'follow-direction':
|
||||||
else if (rot > 2 * Math.PI) rot -= 2 * Math.PI
|
followDirection()
|
||||||
|
break
|
||||||
// Snap to the target rotation to prevent tiny oscillations if close enough
|
case 'follow-facing':
|
||||||
if (Math.abs(angleDifference) < stoppingThreshold) {
|
followFacing()
|
||||||
rot = targetRot
|
break
|
||||||
angularVelocity = 0
|
case 'orbit':
|
||||||
|
orbit()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
orbit()
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let robotPos: Vector3 = new Vector3(0, 0, 0)
|
|
||||||
|
|
||||||
const robotPosition = new Vector2(0, 0) // Initial position
|
|
||||||
const initialVelocity = { x: 0, y: 0 } // Initial velocity
|
|
||||||
// The smooth motion controller utilizes a cubic hermite spline to interpolate between
|
|
||||||
// the current simulation velocity and the robot's actual velocity
|
|
||||||
const controller = new SmoothMotionController(robotPosition, initialVelocity)
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
telemetryReadonlyStore.subscribe(value => {
|
|
||||||
targetRot = (value['orientation'] * Math.PI) / 180 // convert deg to rad
|
|
||||||
controller.setTargetVelocity({
|
|
||||||
x: value['chassis-x-speed'],
|
|
||||||
y: value['chassis-y-speed'],
|
|
||||||
})
|
|
||||||
// shouldOrbit = value.gear === "park" || value.gear === "-999";
|
|
||||||
if (shouldOrbit) {
|
|
||||||
robotPos = new Vector3(0, 0, 0)
|
|
||||||
controller.reset()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
useTask(delta => {
|
useTask(delta => {
|
||||||
if (!shouldOrbit) {
|
/* TODO: standardize a scale (meters : grid lengths) so we can have
|
||||||
updateRotation(delta)
|
accurate positioning of sensor detected objects */
|
||||||
|
// update position data for robot model
|
||||||
|
$mesh.position.x +=
|
||||||
|
$telemetryReadonlyStore['chassis-y-speed'] * delta * SPEED_MULTIPLIER
|
||||||
|
$mesh.position.z +=
|
||||||
|
$telemetryReadonlyStore['chassis-x-speed'] * delta * SPEED_MULTIPLIER
|
||||||
|
$mesh.rotation.y = $telemetryReadonlyStore.orientation * DEG2RAD
|
||||||
|
|
||||||
controller.update(delta)
|
// run the follow function
|
||||||
robotPos.x = controller.getPosition().x
|
follow(delta)
|
||||||
robotPos.z = controller.getPosition().y
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
let capsule: Group<Object3DEventMap>
|
onMount(() => {})
|
||||||
let capRef: Group<Object3DEventMap>
|
|
||||||
$: if (capsule) {
|
|
||||||
capRef = capsule
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<T.PerspectiveCamera makeDefault position={[0, 8, -20]} fov={30} on:create>
|
<T.PerspectiveCamera makeDefault position={[12, 10, 12]}>
|
||||||
<OrbitControls
|
<CameraControls
|
||||||
autoRotateSpeed={1.5}
|
on:create={({ ref }) => {
|
||||||
target.y={1.5}
|
$cameraControls = ref
|
||||||
autoRotate
|
}}
|
||||||
enableDamping
|
autoRotateSpeed={3}
|
||||||
/>
|
/>
|
||||||
<Controls {shouldOrbit} bind:object={capRef} rotateSpeed={angularVelocity} />
|
|
||||||
</T.PerspectiveCamera>
|
</T.PerspectiveCamera>
|
||||||
|
|
||||||
<T.DirectionalLight intensity={0.8} position.x={5} position.y={10} />
|
<T.DirectionalLight position={[3, 10, 7]} />
|
||||||
<T.AmbientLight intensity={0.2} />
|
<T.AmbientLight color={'#f0f0f0'} intensity={0.1} />
|
||||||
|
|
||||||
<Grid
|
<RobotDecimated
|
||||||
position.y={1}
|
scale={[10, 10, 10]}
|
||||||
cellColor="#ffffff"
|
position.y={0}
|
||||||
sectionColor="#ffffff"
|
on:create={({ ref }) => {
|
||||||
sectionThickness={0}
|
// @ts-expect-error
|
||||||
fadeDistance={100}
|
$mesh = ref
|
||||||
cellSize={6}
|
}}
|
||||||
infiniteGrid
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ContactShadows scale={10} blur={2} far={2.5} opacity={0.5} />
|
<Grid
|
||||||
|
sectionColor={'#ff3e00'}
|
||||||
<!-- <Hornet
|
sectionThickness={1}
|
||||||
position.y={2}
|
fadeDistance={100}
|
||||||
position.z={robotPos.z}
|
cellSize={6}
|
||||||
position.x={robotPos.x}
|
sectionSize={24}
|
||||||
scale={[5, 5, 5]}
|
cellColor={'#cccccc'}
|
||||||
bind:ref={capsule}
|
infiniteGrid
|
||||||
rotation.y={rot}
|
/>
|
||||||
/> -->
|
|
||||||
|
|
|
@ -1,16 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Canvas } from '@threlte/core'
|
import { Canvas } from '@threlte/core'
|
||||||
// @ts-expect-error
|
import { cameraControls } from './CameraControls/utils/cameraStore'
|
||||||
import { DEG2RAD } from 'three/src/math/MathUtils'
|
import Scene from './Scene.svelte'
|
||||||
import { Pane, Button, Separator } from 'svelte-tweakpane-ui'
|
|
||||||
import { cameraControls, mesh } from './CameraControls/utils/cameraStore'
|
|
||||||
import Scene from './CameraControls/Scene.svelte'
|
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import {
|
import { type OrthographicCamera, type PerspectiveCamera } from 'three'
|
||||||
Vector3,
|
|
||||||
type OrthographicCamera,
|
|
||||||
type PerspectiveCamera,
|
|
||||||
} from 'three'
|
|
||||||
|
|
||||||
let camera: PerspectiveCamera | OrthographicCamera
|
let camera: PerspectiveCamera | OrthographicCamera
|
||||||
|
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
import { Vector2 } from 'three'
|
|
||||||
|
|
||||||
interface Velocity {
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SmoothMotionController {
|
|
||||||
private currentPosition: Vector2
|
|
||||||
private currentVelocity: Vector2
|
|
||||||
private targetVelocity: Velocity
|
|
||||||
private dampingFactor: number
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
initialPosition: Vector2,
|
|
||||||
initialVelocity: Velocity,
|
|
||||||
dampingFactor: number = 0.1
|
|
||||||
) {
|
|
||||||
this.currentPosition = initialPosition
|
|
||||||
this.currentVelocity = new Vector2(initialVelocity.x, initialVelocity.y)
|
|
||||||
this.targetVelocity = { ...initialVelocity }
|
|
||||||
this.dampingFactor = dampingFactor
|
|
||||||
}
|
|
||||||
|
|
||||||
setTargetVelocity(velocity: Velocity) {
|
|
||||||
this.targetVelocity = velocity
|
|
||||||
}
|
|
||||||
|
|
||||||
update(delta: number) {
|
|
||||||
// Apply cubic interpolation to smoothly transition the current velocity towards the target velocity
|
|
||||||
this.currentVelocity.x +=
|
|
||||||
(this.targetVelocity.x - this.currentVelocity.x) *
|
|
||||||
this.dampingFactor *
|
|
||||||
delta
|
|
||||||
this.currentVelocity.y +=
|
|
||||||
(this.targetVelocity.y - this.currentVelocity.y) *
|
|
||||||
this.dampingFactor *
|
|
||||||
delta
|
|
||||||
|
|
||||||
// Update position based on the current velocity and the time delta
|
|
||||||
this.currentPosition.x += this.currentVelocity.x * delta * 3
|
|
||||||
this.currentPosition.y += this.currentVelocity.y * delta * 3
|
|
||||||
}
|
|
||||||
|
|
||||||
getPosition(): Vector2 {
|
|
||||||
return this.currentPosition
|
|
||||||
}
|
|
||||||
|
|
||||||
public reset() {
|
|
||||||
this.currentPosition = new Vector2(0, 0)
|
|
||||||
this.currentVelocity = new Vector2(0, 0)
|
|
||||||
this.targetVelocity = { x: 0, y: 0 }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -14,51 +14,52 @@ Sequences should be either event-driven or periodic. In the case of periodic
|
||||||
sequences, invoke them in the periodicSequence function
|
sequences, invoke them in the periodicSequence function
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Notifications } from "../Notifications/notifications";
|
import { Notifications } from '../Notifications/notifications'
|
||||||
import { sequenceStore } from "../stores/sequenceStore";
|
import { sequenceStore } from '../stores/sequenceStore'
|
||||||
import { settingsStore } from "../stores/settingsStore";
|
import { settingsStore } from '../stores/settingsStore'
|
||||||
import { get } from "svelte/store";
|
import { get } from 'svelte/store'
|
||||||
import getVoicePath from "../utils/getVoicePath";
|
import getVoicePath from '../utils/getVoicePath'
|
||||||
import { tick } from "svelte";
|
import { tick } from 'svelte'
|
||||||
|
import { cameraState } from '../Dashboard/Visualization/CameraControls/utils/cameraStore'
|
||||||
|
|
||||||
// await a "tick" (a svelte update frame) at the start of every sequence so that
|
// await a "tick" (a svelte update frame) at the start of every sequence so that
|
||||||
// state is synced and no weird side effects occur
|
// state is synced and no weird side effects occur
|
||||||
|
|
||||||
export const initializationSequence = async () => {
|
export const initializationSequence = async () => {
|
||||||
await tick();
|
await tick()
|
||||||
Notifications.info("Jankboard initialized!", {
|
Notifications.info('Jankboard initialized!', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath("jankboard-initialized"),
|
src: getVoicePath('jankboard-initialized'),
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
if (get(settingsStore).goWoke) {
|
if (get(settingsStore).goWoke) {
|
||||||
sequenceStore.update("initializationComplete", true);
|
sequenceStore.update('initializationComplete', true)
|
||||||
periodicSequence();
|
periodicSequence()
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
Notifications.success("LittenOS is online", {
|
Notifications.success('LittenOS is online', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath("littenos-is-online"),
|
src: getVoicePath('littenos-is-online'),
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
Notifications.warn("Breaching Monte Vista codebase", {
|
Notifications.warn('Breaching Monte Vista codebase', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath("breaching-monte-vista"),
|
src: getVoicePath('breaching-monte-vista'),
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
Notifications.playAudio(
|
Notifications.playAudio(
|
||||||
getVoicePath("hello-virtual-assistant"),
|
getVoicePath('hello-virtual-assistant'),
|
||||||
() => {
|
() => {
|
||||||
sequenceStore.update("initializationComplete", true);
|
sequenceStore.update('initializationComplete', true)
|
||||||
periodicSequence();
|
periodicSequence()
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
let counter = 1;
|
let counter = 1
|
||||||
/**
|
/**
|
||||||
* Special sequence that plays invokes itself periodically, started automatically
|
* Special sequence that plays invokes itself periodically, started automatically
|
||||||
* at the end of the initializationSequence
|
* at the end of the initializationSequence
|
||||||
|
@ -68,7 +69,7 @@ let counter = 1;
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
const periodicSequence = async () => {
|
const periodicSequence = async () => {
|
||||||
await tick();
|
await tick()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns either true or false based on the provided probability
|
* Returns either true or false based on the provided probability
|
||||||
|
@ -78,11 +79,11 @@ const periodicSequence = async () => {
|
||||||
*/
|
*/
|
||||||
const chance = (probability: number) => {
|
const chance = (probability: number) => {
|
||||||
if (probability < 0 || probability > 1) {
|
if (probability < 0 || probability > 1) {
|
||||||
throw new Error("Probability must be between 0 and 1");
|
throw new Error('Probability must be between 0 and 1')
|
||||||
}
|
}
|
||||||
|
|
||||||
return Math.random() < probability * get(settingsStore).randomWeight;
|
return Math.random() < probability * get(settingsStore).randomWeight
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls a callback function at regular intervals.
|
* Calls a callback function at regular intervals.
|
||||||
|
@ -91,141 +92,141 @@ const periodicSequence = async () => {
|
||||||
* @param callback - the function to call
|
* @param callback - the function to call
|
||||||
*/
|
*/
|
||||||
const every = (seconds: number, callback: () => void) => {
|
const every = (seconds: number, callback: () => void) => {
|
||||||
if (counter % seconds === 0) callback();
|
if (counter % seconds === 0) callback()
|
||||||
};
|
}
|
||||||
|
|
||||||
// add your periodic sequences here
|
// add your periodic sequences here
|
||||||
every(15, () => {
|
every(15, () => {
|
||||||
if (chance(0.2)) breaching1323Sequence();
|
if (chance(0.2)) breaching1323Sequence()
|
||||||
else if (chance(0.2)) breaching254Sequence();
|
else if (chance(0.2)) breaching254Sequence()
|
||||||
});
|
})
|
||||||
every(25, () => {
|
every(25, () => {
|
||||||
if (chance(0.05)) bullyingRohanSequence();
|
if (chance(0.05)) bullyingRohanSequence()
|
||||||
else if (chance(0.1)) bypassCoprocessorRestrictionsSequence();
|
else if (chance(0.1)) bypassCoprocessorRestrictionsSequence()
|
||||||
});
|
})
|
||||||
|
|
||||||
// Dont touch
|
// Dont touch
|
||||||
counter++;
|
counter++
|
||||||
setTimeout(periodicSequence, 1000);
|
setTimeout(periodicSequence, 1000)
|
||||||
};
|
}
|
||||||
export const criticalFailureIminentSequence = async () => {
|
export const criticalFailureIminentSequence = async () => {
|
||||||
await tick();
|
await tick()
|
||||||
Notifications.error("Critical robot failure imminent", {
|
Notifications.error('Critical robot failure imminent', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath("critical-robot-failure"),
|
src: getVoicePath('critical-robot-failure'),
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const collisionDetectedSequence = async () => {
|
export const collisionDetectedSequence = async () => {
|
||||||
await tick();
|
await tick()
|
||||||
Notifications.error("Collision detected", {
|
Notifications.error('Collision detected', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath("collision-detected"),
|
src: getVoicePath('collision-detected'),
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const collisionImminentSequence = async () => {
|
export const collisionImminentSequence = async () => {
|
||||||
await tick();
|
await tick()
|
||||||
Notifications.error("Collision imminent", {
|
Notifications.error('Collision imminent', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath("collision-imminent"),
|
src: getVoicePath('collision-imminent'),
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const cruiseControlEngagedSequence = async () => {
|
export const cruiseControlEngagedSequence = async () => {
|
||||||
if (get(settingsStore).disableAnnoyances) return;
|
if (get(settingsStore).disableAnnoyances) return
|
||||||
await tick();
|
await tick()
|
||||||
Notifications.success("Cruise control engaged", {
|
Notifications.success('Cruise control engaged', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath("cruise-control-engaged"),
|
src: getVoicePath('cruise-control-engaged'),
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const retardSequence = async () => {
|
export const retardSequence = async () => {
|
||||||
if (get(settingsStore).goWoke) return;
|
if (get(settingsStore).goWoke) return
|
||||||
await tick();
|
await tick()
|
||||||
Notifications.warn("Retard", {
|
Notifications.warn('Retard', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath("retard"),
|
src: getVoicePath('retard'),
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const breaching254Sequence = async () => {
|
const breaching254Sequence = async () => {
|
||||||
if (get(settingsStore).disableAnnoyances) return;
|
if (get(settingsStore).disableAnnoyances) return
|
||||||
await tick();
|
await tick()
|
||||||
Notifications.warn("Breaching 254 mainframe", {
|
Notifications.warn('Breaching 254 mainframe', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath("breaching-254-mainframe"),
|
src: getVoicePath('breaching-254-mainframe'),
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const breaching1323Sequence = async () => {
|
const breaching1323Sequence = async () => {
|
||||||
if (get(settingsStore).disableAnnoyances) return;
|
if (get(settingsStore).disableAnnoyances) return
|
||||||
await tick();
|
await tick()
|
||||||
Notifications.warn("Breaching 1323 mainframe", {
|
Notifications.warn('Breaching 1323 mainframe', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath("breaching-1323-mainframe"),
|
src: getVoicePath('breaching-1323-mainframe'),
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const bullyingRohanSequence = async () => {
|
const bullyingRohanSequence = async () => {
|
||||||
if (get(settingsStore).disableAnnoyances) return;
|
if (get(settingsStore).disableAnnoyances) return
|
||||||
await tick();
|
await tick()
|
||||||
Notifications.info("Bullying Rohan", {
|
Notifications.info('Bullying Rohan', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath("bullying-rohan"),
|
src: getVoicePath('bullying-rohan'),
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const userErrorDetectedSequence = async () => {
|
export const userErrorDetectedSequence = async () => {
|
||||||
await tick();
|
await tick()
|
||||||
Notifications.error("User error detected", {
|
Notifications.error('User error detected', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath("user-error-detected"),
|
src: getVoicePath('user-error-detected'),
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
// hacky way to prevent duplicate infotainment bootups
|
// hacky way to prevent duplicate infotainment bootups
|
||||||
let infotainmentStarted = false;
|
let infotainmentStarted = false
|
||||||
export const infotainmentBootupSequence = async () => {
|
export const infotainmentBootupSequence = async () => {
|
||||||
if (
|
if (
|
||||||
get(sequenceStore).infotainmentStartedFirstTime ||
|
get(sequenceStore).infotainmentStartedFirstTime ||
|
||||||
get(settingsStore).disableAnnoyances ||
|
get(settingsStore).disableAnnoyances ||
|
||||||
infotainmentStarted
|
infotainmentStarted
|
||||||
) {
|
) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
infotainmentStarted = true;
|
infotainmentStarted = true
|
||||||
await tick();
|
await tick()
|
||||||
|
|
||||||
const sequence = () => {
|
const sequence = () => {
|
||||||
Notifications.info("Infotainment system buffering", {
|
Notifications.info('Infotainment system buffering', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath("infotainment-system-buffering"),
|
src: getVoicePath('infotainment-system-buffering'),
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
Notifications.success("Infotainment system online", {
|
Notifications.success('Infotainment system online', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath("infotainment-system-online"),
|
src: getVoicePath('infotainment-system-online'),
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
sequenceStore.update("infotainmentStartedFirstTime", true);
|
sequenceStore.update('infotainmentStartedFirstTime', true)
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
if (!get(sequenceStore).initializationComplete) {
|
if (!get(sequenceStore).initializationComplete) {
|
||||||
const unsubscribe = sequenceStore.subscribe((data) => {
|
const unsubscribe = sequenceStore.subscribe(data => {
|
||||||
if (data.initializationComplete) {
|
if (data.initializationComplete) {
|
||||||
sequence();
|
sequence()
|
||||||
unsubscribe();
|
unsubscribe()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
sequence();
|
sequence()
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Waits for the infotainment system to boot up before executing the given sequence.
|
* Waits for the infotainment system to boot up before executing the given sequence.
|
||||||
|
@ -236,77 +237,196 @@ export const infotainmentBootupSequence = async () => {
|
||||||
*/
|
*/
|
||||||
const waitForInfotainmentBootup = (sequence: () => void) => {
|
const waitForInfotainmentBootup = (sequence: () => void) => {
|
||||||
if (!get(sequenceStore).infotainmentStartedFirstTime) {
|
if (!get(sequenceStore).infotainmentStartedFirstTime) {
|
||||||
const unsubscribe = sequenceStore.subscribe((data) => {
|
const unsubscribe = sequenceStore.subscribe(data => {
|
||||||
if (data.infotainmentStartedFirstTime) {
|
if (data.infotainmentStartedFirstTime) {
|
||||||
sequence();
|
sequence()
|
||||||
unsubscribe();
|
unsubscribe()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
sequence();
|
sequence()
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export const musicPlayerBootupSequence = async () => {
|
export const musicPlayerBootupSequence = async () => {
|
||||||
if (
|
if (
|
||||||
get(sequenceStore).musicStartedFirstTime ||
|
get(sequenceStore).musicStartedFirstTime ||
|
||||||
get(settingsStore).disableAnnoyances
|
get(settingsStore).disableAnnoyances
|
||||||
)
|
)
|
||||||
return;
|
return
|
||||||
|
|
||||||
await tick();
|
await tick()
|
||||||
|
|
||||||
sequenceStore.update("musicStartedFirstTime", true);
|
sequenceStore.update('musicStartedFirstTime', true)
|
||||||
|
|
||||||
waitForInfotainmentBootup(() => {
|
waitForInfotainmentBootup(() => {
|
||||||
Notifications.info("Downloading copyrighted music...", {
|
Notifications.info('Downloading copyrighted music...', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath("downloading-copyrighted-music"),
|
src: getVoicePath('downloading-copyrighted-music'),
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const gbaEmulatorBootupSequence = async () => {
|
export const gbaEmulatorBootupSequence = async () => {
|
||||||
if (
|
if (
|
||||||
get(sequenceStore).gbaEmulatorStartedFirstTime ||
|
get(sequenceStore).gbaEmulatorStartedFirstTime ||
|
||||||
get(settingsStore).disableAnnoyances
|
get(settingsStore).disableAnnoyances
|
||||||
)
|
)
|
||||||
return;
|
return
|
||||||
|
|
||||||
await tick();
|
await tick()
|
||||||
sequenceStore.update("gbaEmulatorStartedFirstTime", true);
|
sequenceStore.update('gbaEmulatorStartedFirstTime', true)
|
||||||
|
|
||||||
waitForInfotainmentBootup(() => {
|
waitForInfotainmentBootup(() => {
|
||||||
Notifications.info("Loading pirated Nintendo ROMs", {
|
Notifications.info('Loading pirated Nintendo ROMs', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath("loading-pirated-nintendo"),
|
src: getVoicePath('loading-pirated-nintendo'),
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const doomBootupSequence = async () => {
|
export const doomBootupSequence = async () => {
|
||||||
if (
|
if (
|
||||||
get(sequenceStore).doomStartedFirstTime ||
|
get(sequenceStore).doomStartedFirstTime ||
|
||||||
get(settingsStore).disableAnnoyances
|
get(settingsStore).disableAnnoyances
|
||||||
)
|
)
|
||||||
return;
|
return
|
||||||
|
|
||||||
await tick();
|
await tick()
|
||||||
sequenceStore.update("doomStartedFirstTime", true);
|
sequenceStore.update('doomStartedFirstTime', true)
|
||||||
|
|
||||||
waitForInfotainmentBootup(() => {
|
waitForInfotainmentBootup(() => {
|
||||||
Notifications.success("Doom Engaged", {
|
Notifications.success('Doom Engaged', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath("doom-engaged"),
|
src: getVoicePath('doom-engaged'),
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const bypassCoprocessorRestrictionsSequence = async () => {
|
const bypassCoprocessorRestrictionsSequence = async () => {
|
||||||
if (get(settingsStore).disableAnnoyances) return;
|
if (
|
||||||
await tick();
|
get(settingsStore).disableAnnoyances ||
|
||||||
Notifications.warn("Bypassing coprocessor restrictions", {
|
get(sequenceStore).initializationComplete
|
||||||
|
)
|
||||||
|
return
|
||||||
|
await tick()
|
||||||
|
Notifications.warn('Bypassing coprocessor restrictions', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath("bypassing-coprocessor-restrictions"),
|
src: getVoicePath('bypassing-coprocessor-restrictions'),
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export const shiftedInParkSequence = async () => {
|
||||||
|
await tick()
|
||||||
|
cameraState.set('mode', 'orbit')
|
||||||
|
|
||||||
|
if (
|
||||||
|
get(settingsStore).disableAnnoyances ||
|
||||||
|
!get(sequenceStore).initializationComplete
|
||||||
|
)
|
||||||
|
return
|
||||||
|
Notifications.playAudio(getVoicePath('parked-brakes-engaged'), () => {
|
||||||
|
if (!get(settingsStore).sentry) return
|
||||||
|
|
||||||
|
Notifications.playAudio(getVoicePath('sentry-mode-engaged'))
|
||||||
|
Notifications.warn('Sentry mode engaged. Threats will be neutralized')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shiftedInReverseSequence = async () => {
|
||||||
|
await tick()
|
||||||
|
|
||||||
|
cameraState.set('mode', 'follow-direction')
|
||||||
|
|
||||||
|
if (
|
||||||
|
get(settingsStore).disableAnnoyances ||
|
||||||
|
!get(sequenceStore).initializationComplete
|
||||||
|
)
|
||||||
|
return
|
||||||
|
Notifications.playAudio(getVoicePath('reverse'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shiftedInNeutralSequence = async () => {
|
||||||
|
await tick()
|
||||||
|
|
||||||
|
cameraState.set('mode', 'orbit')
|
||||||
|
|
||||||
|
if (
|
||||||
|
get(settingsStore).disableAnnoyances ||
|
||||||
|
!get(sequenceStore).initializationComplete
|
||||||
|
)
|
||||||
|
return
|
||||||
|
Notifications.playAudio(getVoicePath('neutral-brakes-engaged'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shiftedInLowSequence = async () => {
|
||||||
|
await tick()
|
||||||
|
|
||||||
|
cameraState.set('mode', 'follow-facing')
|
||||||
|
|
||||||
|
if (
|
||||||
|
get(settingsStore).disableAnnoyances ||
|
||||||
|
!get(sequenceStore).initializationComplete
|
||||||
|
)
|
||||||
|
return
|
||||||
|
Notifications.playAudio(getVoicePath('shifted-into-low'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shiftedInAutoSequence = async () => {
|
||||||
|
await tick()
|
||||||
|
|
||||||
|
cameraState.set('mode', 'follow-direction')
|
||||||
|
|
||||||
|
if (
|
||||||
|
get(settingsStore).disableAnnoyances ||
|
||||||
|
!get(sequenceStore).initializationComplete
|
||||||
|
)
|
||||||
|
return
|
||||||
|
Notifications.playAudio(getVoicePath('shifted-into-automatic'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shiftedInDriveSequence = async () => {
|
||||||
|
await tick()
|
||||||
|
|
||||||
|
cameraState.set('mode', 'follow-facing')
|
||||||
|
|
||||||
|
if (
|
||||||
|
get(settingsStore).disableAnnoyances ||
|
||||||
|
!get(sequenceStore).initializationComplete
|
||||||
|
)
|
||||||
|
return
|
||||||
|
Notifications.playAudio(getVoicePath('shifted-into-drive'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const modeChillSequence = async () => {
|
||||||
|
if (
|
||||||
|
get(settingsStore).disableAnnoyances ||
|
||||||
|
!get(sequenceStore).initializationComplete
|
||||||
|
)
|
||||||
|
return
|
||||||
|
await tick()
|
||||||
|
|
||||||
|
Notifications.playAudio(getVoicePath('set-acceleration-profile-chill'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const modeCruiseSequence = async () => {
|
||||||
|
if (
|
||||||
|
get(settingsStore).disableAnnoyances ||
|
||||||
|
!get(sequenceStore).initializationComplete
|
||||||
|
)
|
||||||
|
return
|
||||||
|
await tick()
|
||||||
|
|
||||||
|
Notifications.playAudio(getVoicePath('cruise-control-engaged'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const modeLudicrousSequence = async () => {
|
||||||
|
if (
|
||||||
|
get(settingsStore).disableAnnoyances ||
|
||||||
|
!get(sequenceStore).initializationComplete
|
||||||
|
)
|
||||||
|
return
|
||||||
|
await tick()
|
||||||
|
|
||||||
|
Notifications.playAudio(getVoicePath('set-acceleration-profile-ludicrous'))
|
||||||
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ export interface SettingsStoreData {
|
||||||
fastStartup: boolean
|
fastStartup: boolean
|
||||||
randomWeight: number
|
randomWeight: number
|
||||||
voiceLang: SupportedLanguage
|
voiceLang: SupportedLanguage
|
||||||
|
sentry: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaults: SettingsStoreData = {
|
export const defaults: SettingsStoreData = {
|
||||||
|
@ -18,6 +19,7 @@ export const defaults: SettingsStoreData = {
|
||||||
fastStartup: false, // skip the loading splash screen (for development purposes. Setting this from within the app has no effect.)
|
fastStartup: false, // skip the loading splash screen (for development purposes. Setting this from within the app has no effect.)
|
||||||
randomWeight: 1, // the weight of random events (multiplied by the original probability)
|
randomWeight: 1, // the weight of random events (multiplied by the original probability)
|
||||||
voiceLang: 'en-US',
|
voiceLang: 'en-US',
|
||||||
|
sentry: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
const createSequenceStore = () => {
|
const createSequenceStore = () => {
|
||||||
|
|
Loading…
Reference in a new issue