feat: vastly improve visualization; add more sequences; improve error handling in rust; improve UI; update settings;

This commit is contained in:
Youwen Wu 2024-02-29 23:49:20 -08:00
parent eb41340ee0
commit 05c52d4d4a
18 changed files with 516 additions and 827 deletions

143
client/package-lock.json generated
View file

@ -19,7 +19,6 @@
"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": {
@ -1052,11 +1051,6 @@
"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",
@ -1622,7 +1616,9 @@
"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=="
"integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==",
"dev": true,
"peer": true
},
"node_modules/estree-walker": {
"version": "3.0.3",
@ -1632,19 +1628,6 @@
"@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",
@ -3013,17 +2996,6 @@
"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",
@ -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": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/svelte-writable-derived/-/svelte-writable-derived-3.1.0.tgz",

View file

@ -39,7 +39,6 @@
"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"
}
}

View file

@ -9,6 +9,8 @@ const NTABLE_IP: (u8, u8, u8, u8) = (10, 12, 80, 2);
const NTABLE_PORT: u16 = 5810;
pub async fn subscribe_topics(app_handle: AppHandle) {
loop {
// I hope this doesn't lead to a catastrophic infinite loop failure
let client = loop {
match Client::try_new_w_config(
SocketAddrV4::new(
@ -69,4 +71,5 @@ pub async fn subscribe_topics(app_handle: AppHandle) {
app_handle
.emit_all("telemetry_status", "disconnected")
.expect("Failed to emit telemetry_disconnected event");
}
}

View file

@ -1,4 +1,5 @@
<script lang="ts">
import { onDestroy } from 'svelte'
import {
cameraControls,
cameraState,
@ -11,21 +12,15 @@
setStationaryTelemetry,
} from './telemetrySimulators'
let cameraMode = 'orbit'
let value: typeof $cameraState.mode = $cameraState.mode
const changeCamera = () => {
if (cameraMode === 'follow-direction') {
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)
}
$: {
cameraState.set('mode', value)
}
const unsubscribe = cameraState.subscribe(state => {
if (value !== state.mode) value = state.mode
})
onDestroy(unsubscribe)
</script>
<AppContainer
@ -69,7 +64,11 @@
<button class="button" on:click={simulateMotion}>
Simulate random motion
</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>
<style lang="postcss">

View file

@ -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)."
>Voice Prompt Language</SettingsSelector
>
<SettingsToggle
setting="sentry"
tooltip="Sentry mode protects the robot and operator from foreign threats."
>Sentry Mode</SettingsToggle
>
<button
class="mt-10 px-4 py-2 bg-amber-600 hover:brightness-75 text-medium rounded-lg w-min"
on:click={resetSettings}>Reset</button

View file

@ -20,7 +20,7 @@
>
<div class="text-lg font-medium">SPEED<br />LIMIT</div>
<div class="text-2xl font-bold transition">
{speedLimit}
{speedLimit.toFixed(1)}
</div>
</div>
</div>

View file

@ -1,6 +1,40 @@
<script lang="ts">
import {
shiftedInAutoSequence,
shiftedInDriveSequence,
shiftedInLowSequence,
shiftedInNeutralSequence,
shiftedInParkSequence,
shiftedInReverseSequence,
} from '../../Sequences/sequences'
export let selectedGear: Gear
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>
<div class="flex justify-center w-full transition">

View file

@ -7,28 +7,39 @@
Displays the drive mode
-->
<script lang="ts">
import { fade } from 'svelte/transition'
import {
modeChillSequence,
modeCruiseSequence,
modeLudicrousSequence,
} from '../../Sequences/sequences'
export let selectedMode: Mode
export let placeholder: boolean
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) {
modeText = 'DISCONNECTED'
} else {
switch (selectedMode) {
case 'chill':
modeText = 'CHILL'
break
case 'cruise':
modeText = 'CRUISE'
break
case 'ludicrous':
modeText = 'LUDICROUS'
break
}
setModeText(selectedMode)
}
}
</script>

View file

@ -33,6 +33,7 @@
Vector4,
type PerspectiveCamera,
} from 'three'
// @ts-expect-error
import { DEG2RAD } from 'three/src/math/MathUtils'
import { cameraState } from './utils/cameraStore'
@ -61,7 +62,7 @@
const { renderer, invalidate } = useThrelte()
export let autoRotate = false
let autoRotate = true
export let autoRotateSpeed = 1
export const ref = new CameraControls(
@ -73,7 +74,7 @@
useTask(
delta => {
if (autoRotate && !$cameraState.userControlled) {
if (autoRotate && $cameraState.mode === 'orbit') {
getControls().azimuthAngle += 4 * delta * DEG2RAD * autoRotateSpeed
}
const updated = getControls().update(delta)
@ -90,13 +91,10 @@
<T
is={ref}
on:controlstart={e => {
cameraState.set('userControlled', true)
}}
on:zoom={e => {
console.log('zoomstart', e)
autoRotate = false
}}
on:controlend={() => {
cameraState.set('userControlled', false)
autoRotate = true
}}
{...$$restProps}
bind:this={$forwardingComponent}

View file

@ -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
/>

View file

@ -14,12 +14,10 @@ type CameraMode =
interface CameraState {
mode: CameraMode
userControlled: boolean
}
const { set, update, subscribe } = writable<CameraState>({
mode: 'orbit',
userControlled: false,
})
const createCameraState = () => {
@ -28,7 +26,7 @@ const createCameraState = () => {
subscribe,
set: (prop: keyof CameraState, val: any) =>
update(state => ({ ...state, [prop]: val })),
reset: () => set({ mode: 'orbit', userControlled: false }),
reset: () => set({ mode: 'orbit' }),
}
}

View file

@ -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>

View file

@ -1,155 +1,134 @@
<script lang="ts">
import { T, useTask } from '@threlte/core'
import { ContactShadows, Float, Grid, OrbitControls } from '@threlte/extras'
import Controls from './Controls.svelte'
import { Grid } from '@threlte/extras'
import CameraControls from './CameraControls/CameraControls.svelte'
import {
Vector3,
type Camera,
type Group,
type Object3D,
type Object3DEventMap,
} from 'three'
import {
telemetryReadonlyStore,
telemetryStore,
} from '../../stores/telemetryStore'
import { get } from 'svelte/store'
import { Vector2 } from 'three'
import { SmoothMotionController } from './smoothMotionController'
cameraControls,
mesh,
cameraState,
} from './CameraControls/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'
/* This is the root scene where the robot visualization is built.
It renders an infinite grid (it's not actually infinite, but we shouldn't run out
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.
*/
const SPEED_MULTIPLIER = 4
const axis = new Vector3(0, 1, 0)
let shouldOrbit = true
const follow = (delta: number) => {
// the object's position is bound to the prop
if (!$mesh || !$cameraControls) return
// CONSTANTS
const maxAngularVelocity = 2 // Max angular velocity, in radians per second
const stoppingThreshold = 0.005 // Threshold in radians for when to consider the rotation close enough to stop
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)
// Proportional control factor
const kP = 2 // Adjust this value based on responsiveness and stability needs
const followDirection = () => {
const angle = Math.atan2(
$telemetryReadonlyStore['chassis-y-speed'],
$telemetryReadonlyStore['chassis-x-speed']
)
// Sync robot orientation with target rotation
let targetRot = 0
// Updates rotation to match target with PID controller (intended to be invoked in useTask)
let rot = 0 // (initial) rotation in radians
let angularVelocity = 0
const updateRotation = (delta: number) => {
let angleDifference = targetRot - rot
// Normalize angle difference to the range [-π, π]
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
$cameraControls.setLookAt(
offsetPosition.x - 13 * Math.sin(angle),
offsetPosition.y + 4,
offsetPosition.z - 13 * Math.cos(angle),
offsetPosition.x,
offsetPosition.y,
offsetPosition.z,
true
)
$cameraControls.zoomTo(1.1, true)
}
// Adjust angular velocity towards desired velocity
angularVelocity = desiredVelocity
// Update rotation
rot += angularVelocity * delta
// Normalize rot to the range [0, 2π]
if (rot < 0) rot += 2 * Math.PI
else if (rot > 2 * Math.PI) rot -= 2 * Math.PI
// Snap to the target rotation to prevent tiny oscillations if close enough
if (Math.abs(angleDifference) < stoppingThreshold) {
rot = targetRot
angularVelocity = 0
const followFacing = () => {
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)
}
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()
const orbit = () => {
$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
)
}
switch ($cameraState.mode) {
case 'follow-direction':
followDirection()
break
case 'follow-facing':
followFacing()
break
case 'orbit':
orbit()
break
default:
orbit()
break
}
}
})
})
useTask(delta => {
if (!shouldOrbit) {
updateRotation(delta)
/* TODO: standardize a scale (meters : grid lengths) so we can have
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)
robotPos.x = controller.getPosition().x
robotPos.z = controller.getPosition().y
}
// run the follow function
follow(delta)
})
let capsule: Group<Object3DEventMap>
let capRef: Group<Object3DEventMap>
$: if (capsule) {
capRef = capsule
}
onMount(() => {})
</script>
<T.PerspectiveCamera makeDefault position={[0, 8, -20]} fov={30} on:create>
<OrbitControls
autoRotateSpeed={1.5}
target.y={1.5}
autoRotate
enableDamping
<T.PerspectiveCamera makeDefault position={[12, 10, 12]}>
<CameraControls
on:create={({ ref }) => {
$cameraControls = ref
}}
autoRotateSpeed={3}
/>
<Controls {shouldOrbit} bind:object={capRef} rotateSpeed={angularVelocity} />
</T.PerspectiveCamera>
<T.DirectionalLight intensity={0.8} position.x={5} position.y={10} />
<T.AmbientLight intensity={0.2} />
<T.DirectionalLight position={[3, 10, 7]} />
<T.AmbientLight color={'#f0f0f0'} intensity={0.1} />
<Grid
position.y={1}
cellColor="#ffffff"
sectionColor="#ffffff"
sectionThickness={0}
fadeDistance={100}
cellSize={6}
infiniteGrid
<RobotDecimated
scale={[10, 10, 10]}
position.y={0}
on:create={({ ref }) => {
// @ts-expect-error
$mesh = ref
}}
/>
<ContactShadows scale={10} blur={2} far={2.5} opacity={0.5} />
<!-- <Hornet
position.y={2}
position.z={robotPos.z}
position.x={robotPos.x}
scale={[5, 5, 5]}
bind:ref={capsule}
rotation.y={rot}
/> -->
<Grid
sectionColor={'#ff3e00'}
sectionThickness={1}
fadeDistance={100}
cellSize={6}
sectionSize={24}
cellColor={'#cccccc'}
infiniteGrid
/>

View file

@ -1,16 +1,9 @@
<script lang="ts">
import { Canvas } from '@threlte/core'
// @ts-expect-error
import { DEG2RAD } from 'three/src/math/MathUtils'
import { Pane, Button, Separator } from 'svelte-tweakpane-ui'
import { cameraControls, mesh } from './CameraControls/utils/cameraStore'
import Scene from './CameraControls/Scene.svelte'
import { cameraControls } from './CameraControls/utils/cameraStore'
import Scene from './Scene.svelte'
import { onMount } from 'svelte'
import {
Vector3,
type OrthographicCamera,
type PerspectiveCamera,
} from 'three'
import { type OrthographicCamera, type PerspectiveCamera } from 'three'
let camera: PerspectiveCamera | OrthographicCamera

View file

@ -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 }
}
}

View file

@ -14,51 +14,52 @@ Sequences should be either event-driven or periodic. In the case of periodic
sequences, invoke them in the periodicSequence function
*/
import { Notifications } from "../Notifications/notifications";
import { sequenceStore } from "../stores/sequenceStore";
import { settingsStore } from "../stores/settingsStore";
import { get } from "svelte/store";
import getVoicePath from "../utils/getVoicePath";
import { tick } from "svelte";
import { Notifications } from '../Notifications/notifications'
import { sequenceStore } from '../stores/sequenceStore'
import { settingsStore } from '../stores/settingsStore'
import { get } from 'svelte/store'
import getVoicePath from '../utils/getVoicePath'
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
// state is synced and no weird side effects occur
export const initializationSequence = async () => {
await tick();
Notifications.info("Jankboard initialized!", {
await tick()
Notifications.info('Jankboard initialized!', {
withAudio: true,
src: getVoicePath("jankboard-initialized"),
src: getVoicePath('jankboard-initialized'),
onComplete: () => {
if (get(settingsStore).goWoke) {
sequenceStore.update("initializationComplete", true);
periodicSequence();
return;
sequenceStore.update('initializationComplete', true)
periodicSequence()
return
}
Notifications.success("LittenOS is online", {
Notifications.success('LittenOS is online', {
withAudio: true,
src: getVoicePath("littenos-is-online"),
src: getVoicePath('littenos-is-online'),
onComplete: () => {
Notifications.warn("Breaching Monte Vista codebase", {
Notifications.warn('Breaching Monte Vista codebase', {
withAudio: true,
src: getVoicePath("breaching-monte-vista"),
src: getVoicePath('breaching-monte-vista'),
onComplete: () => {
Notifications.playAudio(
getVoicePath("hello-virtual-assistant"),
getVoicePath('hello-virtual-assistant'),
() => {
sequenceStore.update("initializationComplete", true);
periodicSequence();
sequenceStore.update('initializationComplete', true)
periodicSequence()
}
);
)
},
});
})
},
});
})
},
});
};
})
}
let counter = 1;
let counter = 1
/**
* Special sequence that plays invokes itself periodically, started automatically
* at the end of the initializationSequence
@ -68,7 +69,7 @@ let counter = 1;
* @return void
*/
const periodicSequence = async () => {
await tick();
await tick()
/**
* Returns either true or false based on the provided probability
@ -78,11 +79,11 @@ const periodicSequence = async () => {
*/
const chance = (probability: number) => {
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.
@ -91,141 +92,141 @@ const periodicSequence = async () => {
* @param callback - the function to call
*/
const every = (seconds: number, callback: () => void) => {
if (counter % seconds === 0) callback();
};
if (counter % seconds === 0) callback()
}
// add your periodic sequences here
every(15, () => {
if (chance(0.2)) breaching1323Sequence();
else if (chance(0.2)) breaching254Sequence();
});
if (chance(0.2)) breaching1323Sequence()
else if (chance(0.2)) breaching254Sequence()
})
every(25, () => {
if (chance(0.05)) bullyingRohanSequence();
else if (chance(0.1)) bypassCoprocessorRestrictionsSequence();
});
if (chance(0.05)) bullyingRohanSequence()
else if (chance(0.1)) bypassCoprocessorRestrictionsSequence()
})
// Dont touch
counter++;
setTimeout(periodicSequence, 1000);
};
counter++
setTimeout(periodicSequence, 1000)
}
export const criticalFailureIminentSequence = async () => {
await tick();
Notifications.error("Critical robot failure imminent", {
await tick()
Notifications.error('Critical robot failure imminent', {
withAudio: true,
src: getVoicePath("critical-robot-failure"),
});
};
src: getVoicePath('critical-robot-failure'),
})
}
export const collisionDetectedSequence = async () => {
await tick();
Notifications.error("Collision detected", {
await tick()
Notifications.error('Collision detected', {
withAudio: true,
src: getVoicePath("collision-detected"),
});
};
src: getVoicePath('collision-detected'),
})
}
export const collisionImminentSequence = async () => {
await tick();
Notifications.error("Collision imminent", {
await tick()
Notifications.error('Collision imminent', {
withAudio: true,
src: getVoicePath("collision-imminent"),
});
};
src: getVoicePath('collision-imminent'),
})
}
export const cruiseControlEngagedSequence = async () => {
if (get(settingsStore).disableAnnoyances) return;
await tick();
Notifications.success("Cruise control engaged", {
if (get(settingsStore).disableAnnoyances) return
await tick()
Notifications.success('Cruise control engaged', {
withAudio: true,
src: getVoicePath("cruise-control-engaged"),
});
};
src: getVoicePath('cruise-control-engaged'),
})
}
export const retardSequence = async () => {
if (get(settingsStore).goWoke) return;
await tick();
Notifications.warn("Retard", {
if (get(settingsStore).goWoke) return
await tick()
Notifications.warn('Retard', {
withAudio: true,
src: getVoicePath("retard"),
});
};
src: getVoicePath('retard'),
})
}
const breaching254Sequence = async () => {
if (get(settingsStore).disableAnnoyances) return;
await tick();
Notifications.warn("Breaching 254 mainframe", {
if (get(settingsStore).disableAnnoyances) return
await tick()
Notifications.warn('Breaching 254 mainframe', {
withAudio: true,
src: getVoicePath("breaching-254-mainframe"),
});
};
src: getVoicePath('breaching-254-mainframe'),
})
}
const breaching1323Sequence = async () => {
if (get(settingsStore).disableAnnoyances) return;
await tick();
Notifications.warn("Breaching 1323 mainframe", {
if (get(settingsStore).disableAnnoyances) return
await tick()
Notifications.warn('Breaching 1323 mainframe', {
withAudio: true,
src: getVoicePath("breaching-1323-mainframe"),
});
};
src: getVoicePath('breaching-1323-mainframe'),
})
}
const bullyingRohanSequence = async () => {
if (get(settingsStore).disableAnnoyances) return;
await tick();
Notifications.info("Bullying Rohan", {
if (get(settingsStore).disableAnnoyances) return
await tick()
Notifications.info('Bullying Rohan', {
withAudio: true,
src: getVoicePath("bullying-rohan"),
});
};
src: getVoicePath('bullying-rohan'),
})
}
export const userErrorDetectedSequence = async () => {
await tick();
Notifications.error("User error detected", {
await tick()
Notifications.error('User error detected', {
withAudio: true,
src: getVoicePath("user-error-detected"),
});
};
src: getVoicePath('user-error-detected'),
})
}
// hacky way to prevent duplicate infotainment bootups
let infotainmentStarted = false;
let infotainmentStarted = false
export const infotainmentBootupSequence = async () => {
if (
get(sequenceStore).infotainmentStartedFirstTime ||
get(settingsStore).disableAnnoyances ||
infotainmentStarted
) {
return;
return
}
infotainmentStarted = true;
await tick();
infotainmentStarted = true
await tick()
const sequence = () => {
Notifications.info("Infotainment system buffering", {
Notifications.info('Infotainment system buffering', {
withAudio: true,
src: getVoicePath("infotainment-system-buffering"),
src: getVoicePath('infotainment-system-buffering'),
onComplete: () => {
Notifications.success("Infotainment system online", {
Notifications.success('Infotainment system online', {
withAudio: true,
src: getVoicePath("infotainment-system-online"),
src: getVoicePath('infotainment-system-online'),
onComplete: () => {
sequenceStore.update("infotainmentStartedFirstTime", true);
sequenceStore.update('infotainmentStartedFirstTime', true)
},
});
})
},
});
};
})
}
if (!get(sequenceStore).initializationComplete) {
const unsubscribe = sequenceStore.subscribe((data) => {
const unsubscribe = sequenceStore.subscribe(data => {
if (data.initializationComplete) {
sequence();
unsubscribe();
sequence()
unsubscribe()
}
});
})
} else {
sequence();
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) => {
if (!get(sequenceStore).infotainmentStartedFirstTime) {
const unsubscribe = sequenceStore.subscribe((data) => {
const unsubscribe = sequenceStore.subscribe(data => {
if (data.infotainmentStartedFirstTime) {
sequence();
unsubscribe();
sequence()
unsubscribe()
}
});
})
} else {
sequence();
sequence()
}
};
}
export const musicPlayerBootupSequence = async () => {
if (
get(sequenceStore).musicStartedFirstTime ||
get(settingsStore).disableAnnoyances
)
return;
return
await tick();
await tick()
sequenceStore.update("musicStartedFirstTime", true);
sequenceStore.update('musicStartedFirstTime', true)
waitForInfotainmentBootup(() => {
Notifications.info("Downloading copyrighted music...", {
Notifications.info('Downloading copyrighted music...', {
withAudio: true,
src: getVoicePath("downloading-copyrighted-music"),
});
});
};
src: getVoicePath('downloading-copyrighted-music'),
})
})
}
export const gbaEmulatorBootupSequence = async () => {
if (
get(sequenceStore).gbaEmulatorStartedFirstTime ||
get(settingsStore).disableAnnoyances
)
return;
return
await tick();
sequenceStore.update("gbaEmulatorStartedFirstTime", true);
await tick()
sequenceStore.update('gbaEmulatorStartedFirstTime', true)
waitForInfotainmentBootup(() => {
Notifications.info("Loading pirated Nintendo ROMs", {
Notifications.info('Loading pirated Nintendo ROMs', {
withAudio: true,
src: getVoicePath("loading-pirated-nintendo"),
});
});
};
src: getVoicePath('loading-pirated-nintendo'),
})
})
}
export const doomBootupSequence = async () => {
if (
get(sequenceStore).doomStartedFirstTime ||
get(settingsStore).disableAnnoyances
)
return;
return
await tick();
sequenceStore.update("doomStartedFirstTime", true);
await tick()
sequenceStore.update('doomStartedFirstTime', true)
waitForInfotainmentBootup(() => {
Notifications.success("Doom Engaged", {
Notifications.success('Doom Engaged', {
withAudio: true,
src: getVoicePath("doom-engaged"),
});
});
};
src: getVoicePath('doom-engaged'),
})
})
}
const bypassCoprocessorRestrictionsSequence = async () => {
if (get(settingsStore).disableAnnoyances) return;
await tick();
Notifications.warn("Bypassing coprocessor restrictions", {
if (
get(settingsStore).disableAnnoyances ||
get(sequenceStore).initializationComplete
)
return
await tick()
Notifications.warn('Bypassing coprocessor restrictions', {
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'))
}

View file

@ -10,6 +10,7 @@ export interface SettingsStoreData {
fastStartup: boolean
randomWeight: number
voiceLang: SupportedLanguage
sentry: boolean
}
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.)
randomWeight: 1, // the weight of random events (multiplied by the original probability)
voiceLang: 'en-US',
sentry: true,
}
const createSequenceStore = () => {