Merge branch 'camera-overhaul' into camera-overhaul-update

This commit is contained in:
Youwen Wu 2024-02-27 19:39:13 -08:00 committed by GitHub
commit b5fa64d40f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 803 additions and 290 deletions

View file

@ -29,20 +29,23 @@ If you would like to contribute to Jankboard 2, there's only a few simple steps
- If you don't have access to a development environment that supports running standalone executables (eg. Github Codespaces), you can try running `npm run dev` instead of `npm run tauri dev`, which will open a development server at `localhost:5173` with the frontend running in the web. However, this may break at any time as critical functionality is more directly attached to the Rust backend.
- If for some reason you need to install and use the Python backend while we are migrating to Rust, run `poetry install --no-root` in the root directory of the project to install dependencies. You can start the server with `poetry run flask --app app/server.py run --host localhost --port 1280` (it must be running at port `1280` for the frontend to detect it).
## Current progress
## Current progress and improvements over (original) Jankboard
- Basic UI layout complete
- Media player working with a few small issues
- App system working smoothly
- Camera feed likely working
- Frontend syncs basic telemetry data with robot through the same Socket-IO code that powered Jankboard v1
- Notification service installed, with Toast and audio capability
- Layout, toasts/notifications, music player, and app system ported.
- Toast and audio cue system is much more robust
- Transitions added almost everywhere to make things smoother
- Tauri app created successfully, currently still using Flask backend
- Visualization vastly improved with Threlte (Three.js) powered 3D robot simulation
- Robot model ported successfully via massive optimization through polygon decimation
- Added settings app with options to disable certain features and developer tools for testing
## TODO
- Camera cutout overlay
- Overhaul audio player system
- Robot visualization (3D, in Threlte).
- Overhaul backend
- Overhaul visualization (especially camera)
- Overhaul backend in Rust
- Further integrate telemetry (like GPWS, collision warning, etc)
- Finish re-creating / adding various voice alerts and sequences
- Create dynamic voice prompt system to support new languages very easily
- Add dynamic voice prompt fallback to support incremental voice prompt migration

152
client/package-lock.json generated
View file

@ -11,12 +11,14 @@
"@fontsource/roboto": "^5.0.8",
"@threlte/core": "^7.1.0",
"@threlte/extras": "^8.8.0",
"camera-controls": "^2.8.3",
"howler": "^2.2.4",
"material-icons": "^1.13.12",
"material-symbols": "^0.15.0",
"overlayscrollbars-svelte": "^0.5.3",
"socket.io-client": "^4.7.4",
"svelte-french-toast": "^1.2.0",
"svelte-tweakpane-ui": "^1.2.1",
"three": "^0.161.0"
},
"devDependencies": {
@ -1035,6 +1037,11 @@
"integrity": "sha512-BRbo1fOtyVbhfLyuCWw6wAWp+U8UQle+ZXu84MYYWzYSEB28dyfnRBIE99eoG+qdAC0po6L2ScIEivcT07UaMA==",
"dev": true
},
"node_modules/@tweakpane/core": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@tweakpane/core/-/core-2.0.3.tgz",
"integrity": "sha512-qHci4XA1Wngpwy8IzsLh5JEdscz8aDti/9YhyOaq01si+cgNDaZfwzTtXdn1+xTxSnCM+pW4Zb2/4eqn+K1ATw=="
},
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
@ -1300,6 +1307,14 @@
"node": ">= 6"
}
},
"node_modules/camera-controls": {
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-2.8.3.tgz",
"integrity": "sha512-zFjqUR6onLkG+z1A6vAWfzovxZxWVSvp6e5t3lfZgfgPZtX3n74aykNAUaoRbq8Y3tOxadHkDjbfGDOP9hFf2w==",
"peerDependencies": {
"three": ">=0.126.1"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001588",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001588.tgz",
@ -1592,9 +1607,7 @@
"node_modules/esm-env": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz",
"integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==",
"dev": true,
"peer": true
"integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA=="
},
"node_modules/estree-walker": {
"version": "3.0.3",
@ -1604,6 +1617,19 @@
"@types/estree": "^1.0.0"
}
},
"node_modules/fast-copy": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.1.tgz",
"integrity": "sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA=="
},
"node_modules/fast-equals": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz",
"integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/fast-glob": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
@ -2972,6 +2998,17 @@
"svelte": "^3.19.0 || ^4.0.0"
}
},
"node_modules/svelte-local-storage-store": {
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/svelte-local-storage-store/-/svelte-local-storage-store-0.6.4.tgz",
"integrity": "sha512-45WoY2vSGPQM1sIQJ9jTkPPj20hYeqm+af6mUGRFSPP5WglZf36YYoZqwmZZ8Dt/2SU8lem+BTA8/Z/8TkqNLg==",
"engines": {
"node": ">=0.14"
},
"peerDependencies": {
"svelte": "^3.48.0 || >4.0.0"
}
},
"node_modules/svelte-preprocess": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.3.tgz",
@ -3035,6 +3072,115 @@
}
}
},
"node_modules/svelte-tweakpane-ui": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/svelte-tweakpane-ui/-/svelte-tweakpane-ui-1.2.1.tgz",
"integrity": "sha512-F62AFvZiqhXa0E3HMpUdmdWWyus/C+nTBFrFd/vEhHP6dYOQ4EN9qwvjdybL90DZFTIwkZe3w4oSbEGn1muKkQ==",
"dependencies": {
"@0b5vr/tweakpane-plugin-profiler": "^0.4.1",
"@0b5vr/tweakpane-plugin-rotation": "^0.2.0",
"@kitschpatrol/tweakpane-image-plugin": "^2.0.0",
"@pangenerator/tweakpane-textarea-plugin": "^2.0.0",
"@tweakpane/core": "^2.0.3",
"@tweakpane/plugin-camerakit": "^0.3.0",
"@tweakpane/plugin-essentials": "^0.2.1",
"esm-env": "^1.0.0",
"fast-copy": "^3.0.1",
"fast-equals": "^5.0.1",
"nanoid": "^5.0.6",
"svelte-local-storage-store": "^0.6.4",
"tweakpane": "^4.0.3",
"tweakpane-plugin-waveform": "^1.0.0"
},
"engines": {
"node": ">=18.0.0",
"pnpm": ">=8.0.0"
},
"peerDependencies": {
"svelte": "^4.0.0"
}
},
"node_modules/svelte-tweakpane-ui/node_modules/@0b5vr/tweakpane-plugin-profiler": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@0b5vr/tweakpane-plugin-profiler/-/tweakpane-plugin-profiler-0.4.1.tgz",
"integrity": "sha512-jgkPbT24eQ7isj8F7/IsbdqrwvBoWBmwjqxdP35smD2D6xsx+9viR57SKBxi9PxTZDEayicmCzBk++0PTqRnBg==",
"peerDependencies": {
"tweakpane": "^4.0.0"
}
},
"node_modules/svelte-tweakpane-ui/node_modules/@0b5vr/tweakpane-plugin-rotation": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@0b5vr/tweakpane-plugin-rotation/-/tweakpane-plugin-rotation-0.2.0.tgz",
"integrity": "sha512-LK+84kNTusEepVwiKH6ib/Pd+5RxI3UC4rHxn5c14GO58QS49Hh0ft3hFXt/NDzYEST17Q9qg96BcpclhCzYYQ==",
"peerDependencies": {
"tweakpane": "^4.0.0"
}
},
"node_modules/svelte-tweakpane-ui/node_modules/@kitschpatrol/tweakpane-image-plugin": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@kitschpatrol/tweakpane-image-plugin/-/tweakpane-image-plugin-2.0.0.tgz",
"integrity": "sha512-BzEZqIhD/dM7AW0Ebv+309L4k8ZZJ5fC9Zks4sozVK3FwJooviE6JzaFAuB7k0M5oX45Wyn59tQXdHafgsP3YA==",
"peerDependencies": {
"tweakpane": "^4.0.0"
}
},
"node_modules/svelte-tweakpane-ui/node_modules/@pangenerator/tweakpane-textarea-plugin": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@pangenerator/tweakpane-textarea-plugin/-/tweakpane-textarea-plugin-2.0.0.tgz",
"integrity": "sha512-BERPuuyJYWvtJzXh4wtgYspza0ihigE2m4qs57ERKtWG59+lI2t/2TOXlwz7Xyx/QEIH25uO1g732YCljgKaUw==",
"peerDependencies": {
"tweakpane": "^4.0.0"
}
},
"node_modules/svelte-tweakpane-ui/node_modules/@tweakpane/plugin-camerakit": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@tweakpane/plugin-camerakit/-/plugin-camerakit-0.3.0.tgz",
"integrity": "sha512-6UwgwDKU+oaAgXJ2D/pOoIpEAZts0RyeLmVzBJGs+VVNqSfkiHzL0i5XD+XnmSL2PaLXBne0dlz0bYOrjmeELw==",
"peerDependencies": {
"tweakpane": "^4.0.0-beta.2"
}
},
"node_modules/svelte-tweakpane-ui/node_modules/@tweakpane/plugin-essentials": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@tweakpane/plugin-essentials/-/plugin-essentials-0.2.1.tgz",
"integrity": "sha512-VbFU1/uD+CJNFQdfLXUOLjeG5HyUZH97Ox9CxmyVetg1hqjVun3C83HAGFULyhKzl8tSgii8jr304r8QpdHwzQ==",
"peerDependencies": {
"tweakpane": "^4.0.0"
}
},
"node_modules/svelte-tweakpane-ui/node_modules/nanoid": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.6.tgz",
"integrity": "sha512-rRq0eMHoGZxlvaFOUdK1Ev83Bd1IgzzR+WJ3IbDJ7QOSdAxYjlurSPqFs9s4lJg29RT6nPwizFtJhQS6V5xgiA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": {
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^18 || >=20"
}
},
"node_modules/svelte-tweakpane-ui/node_modules/tweakpane": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/tweakpane/-/tweakpane-4.0.3.tgz",
"integrity": "sha512-BlcWOAe8oe4c+k9pmLBARGdWB6MVZMszayekkixQXTgkxTaYoTUpHpwVEp+3HkoamZkomodpbBf0CkguIHTgLg==",
"funding": {
"url": "https://github.com/sponsors/cocopon"
}
},
"node_modules/svelte-tweakpane-ui/node_modules/tweakpane-plugin-waveform": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/tweakpane-plugin-waveform/-/tweakpane-plugin-waveform-1.0.0.tgz",
"integrity": "sha512-fyTRe6Emt7YpgHC5iiTZgk6RHflNm5VIOAsl2+l3mm96+KE8I+7sNPeyADxKcfcQF23c7/R3La5WNhaHNyeJag==",
"peerDependencies": {
"tweakpane": "^4.0.0"
}
},
"node_modules/svelte-writable-derived": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/svelte-writable-derived/-/svelte-writable-derived-3.1.0.tgz",

View file

@ -31,12 +31,14 @@
"@fontsource/roboto": "^5.0.8",
"@threlte/core": "^7.1.0",
"@threlte/extras": "^8.8.0",
"camera-controls": "^2.8.3",
"howler": "^2.2.4",
"material-icons": "^1.13.12",
"material-symbols": "^0.15.0",
"overlayscrollbars-svelte": "^0.5.3",
"socket.io-client": "^4.7.4",
"svelte-french-toast": "^1.2.0",
"svelte-tweakpane-ui": "^1.2.1",
"three": "^0.161.0"
}
}

View file

@ -0,0 +1,106 @@
<script context="module" lang="ts">
let installed = false
</script>
<script lang="ts">
import {
T,
forwardEventHandlers,
useTask,
useParent,
useThrelte,
} from '@threlte/core'
import type {
CameraControlsEvents,
CameraControlsProps,
CameraControlsSlots,
} from './CameraControls.svelte'
type $$Props = CameraControlsProps
type $$Events = CameraControlsEvents
type $$Slots = CameraControlsSlots
import CameraControls from 'camera-controls'
import {
Box3,
Matrix4,
Quaternion,
Raycaster,
Sphere,
Spherical,
Vector2,
Vector3,
Vector4,
type PerspectiveCamera,
} from 'three'
import { DEG2RAD } from 'three/src/math/MathUtils'
const subsetOfTHREE = {
Vector2,
Vector3,
Vector4,
Quaternion,
Matrix4,
Spherical,
Box3,
Sphere,
Raycaster,
}
if (!installed) {
CameraControls.install({ THREE: subsetOfTHREE })
installed = true
}
const parent = useParent()
if (!$parent) {
throw new Error('CameraControls must be a child of a ThreeJS camera')
}
const { renderer, invalidate } = useThrelte()
export let autoRotate = false
export let autoRotateSpeed = 1
export const ref = new CameraControls(
$parent as PerspectiveCamera,
renderer?.domElement
)
const getControls = () => ref
let disableAutoRotate = false
useTask(
delta => {
if (autoRotate && !disableAutoRotate) {
getControls().azimuthAngle += 4 * delta * DEG2RAD * autoRotateSpeed
}
const updated = getControls().update(delta)
if (updated) invalidate()
},
{
autoInvalidate: false,
}
)
const forwardingComponent = forwardEventHandlers()
</script>
<T
is={ref}
on:controlstart={e => {
disableAutoRotate = true
}}
on:zoom={e => {
console.log('zoomstart', e)
}}
on:controlend={() => {
disableAutoRotate = false
}}
{...$$restProps}
bind:this={$forwardingComponent}
>
<slot {ref} />
</T>

View file

@ -0,0 +1,16 @@
import type { Events, Props, Slots } from '@threlte/core'
import CC from 'camera-controls'
import type { SvelteComponent } from 'svelte'
export type CameraControlsProps = Props<CC> & {
autoRotate?: boolean
autoRotateSpeed?: number
}
export type CameraControlsEvents = Events<CC>
export type CameraControlsSlots = Slots<CC>
export default class CameraControls extends SvelteComponent<
CameraControlsProps,
CameraControlsEvents,
CameraControlsSlots
> {}

View file

@ -0,0 +1,122 @@
<script lang="ts">
import { T, useTask } from "@threlte/core";
import { Grid } from "@threlte/extras";
import CameraControls from "./CameraControls.svelte";
import { cameraControls, mesh } from "./utils/cameraStore";
import { Vector3 } from "three";
import { onMount } from "svelte";
import RobotDecimated from "../models/RobotDecimated.svelte";
function vectorFromObject() {
let ideal: Vector3 = new Vector3();
$cameraControls.getPosition(ideal, true);
ideal.applyQuaternion($mesh.quaternion);
ideal.add(
new Vector3($mesh.position.x, $mesh.position.y, $mesh.position.z)
);
return ideal;
}
const follow = (delta: number) => {
// the object's position is bound to the prop
if (!$mesh || !$cameraControls) return;
// typescript HACKS! never do this! How does this work? who knows!
const robotPosition = vectorFromObject();
const horizontalOffsetDistance = 12; // 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, -2.8, 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($mesh.quaternion);
// Calculate the trailing vector's position
const trailingVector = robotPosition.clone().sub(rotatedOffsetVector);
$cameraControls.setLookAt(
trailingVector.x,
trailingVector.y,
trailingVector.z,
$mesh.position.x,
$mesh.position.y,
$mesh.position.z,
true
);
};
useTask((delta) => {
// follow(delta)
// $cameraControls.moveTo(
// $mesh.position.x,
// $mesh.position.y,
// $mesh.position.z,
// true
// )
});
onMount(() => {
setTimeout(() => {
// $cameraControls.setLookAt(
// $mesh.position.x,
// $mesh.position.y,
// $mesh.position.z,
// $mesh.position.x,
// $mesh.position.y,
// $mesh.position.z,
// true
// )
$cameraControls.setLookAt(
30,
20,
-40,
$mesh.position.x,
$mesh.position.y,
$mesh.position.z,
true
);
}, 8000);
});
</script>
<T.PerspectiveCamera
makeDefault
position={[10, 10, 10]}
on:create={({ ref }) => {
ref.lookAt(0, 1, 0);
}}
>
<CameraControls
on:create={({ ref }) => {
$cameraControls = ref;
}}
/>
</T.PerspectiveCamera>
<T.DirectionalLight position={[3, 10, 7]} />
<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

@ -0,0 +1,7 @@
import type CameraControls from "camera-controls";
import { writable } from "svelte/store";
import type { Mesh, Object3DEventMap } from "three";
import type { Group } from "three/examples/jsm/libs/tween.module.js";
export const cameraControls = writable<CameraControls>();
export const mesh = writable<Mesh>();

View file

@ -0,0 +1,20 @@
import { useThrelteUserContext } from '@threlte/core'
import { writable, type Writable } from 'svelte/store'
import type { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
type ControlsContext = {
orbitControls: Writable<OrbitControls | undefined>
}
/**
* ### `useControlsContext`
*
* This hook is used to register the `OrbitControls` instance with the
* `ControlsContext`. We're using this context to enable and disable the
* controls when the user is interacting with the TransformControls.
*/
export const useControlsContext = (): ControlsContext => {
return useThrelteUserContext<ControlsContext>('threlte-controls', {
orbitControls: writable<OrbitControls | undefined>(undefined),
})
}

View file

@ -1,23 +1,23 @@
<script lang="ts">
import { T, useTask } from '@threlte/core'
import { ContactShadows, Float, Grid, OrbitControls } from '@threlte/extras'
import Robot from './models/RobotDecimated.svelte'
import Controls from './Controls.svelte'
import { T, useTask } from "@threlte/core";
import { ContactShadows, Float, Grid, OrbitControls } from "@threlte/extras";
import Controls from "./Controls.svelte";
import {
Vector3,
type Camera,
type Group,
type Object3D,
type Object3DEventMap,
} 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'
} from "../../stores/telemetryStore";
import { get } from "svelte/store";
import { Vector2 } from "three";
import { SmoothMotionController } from "./smoothMotionController";
import { onMount } from "svelte";
/* 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
@ -32,91 +32,91 @@
is the most esoteric and jank code ever written.
*/
let shouldOrbit = true
let shouldOrbit = true;
// 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 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
// Proportional control factor
const kP = 2 // Adjust this value based on responsiveness and stability needs
const kP = 2; // Adjust this value based on responsiveness and stability needs
// Sync robot orientation with target rotation
let targetRot = 0
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
let rot = 0; // (initial) rotation in radians
let angularVelocity = 0;
const updateRotation = (delta: number) => {
let angleDifference = targetRot - rot
let angleDifference = targetRot - rot;
// Normalize angle difference to the range [-π, π]
angleDifference = ((angleDifference + Math.PI) % (2 * Math.PI)) - Math.PI
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))
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
desiredVelocity = 0;
}
// Adjust angular velocity towards desired velocity
angularVelocity = desiredVelocity
angularVelocity = desiredVelocity;
// Update rotation
rot += angularVelocity * delta
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
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
rot = targetRot;
angularVelocity = 0;
}
}
};
let robotPos: Vector3 = new Vector3(0, 0, 0)
let robotPos: Vector3 = new Vector3(0, 0, 0);
const robotPosition = new Vector2(0, 0) // Initial position
const initialVelocity = { x: 0, y: 0 } // Initial velocity
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)
const controller = new SmoothMotionController(robotPosition, initialVelocity);
onMount(() => {
telemetryReadonlyStore.subscribe(value => {
targetRot = (value['orientation'] * Math.PI) / 180 // convert deg to rad
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'
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()
robotPos = new Vector3(0, 0, 0);
controller.reset();
}
})
})
});
});
useTask(delta => {
useTask((delta) => {
if (!shouldOrbit) {
updateRotation(delta)
updateRotation(delta);
controller.update(delta)
robotPos.x = controller.getPosition().x
robotPos.z = controller.getPosition().y
controller.update(delta);
robotPos.x = controller.getPosition().x;
robotPos.z = controller.getPosition().y;
}
})
});
let capsule: Group<Object3DEventMap>
let capRef: Group<Object3DEventMap>
let capsule: Group<Object3DEventMap>;
let capRef: Group<Object3DEventMap>;
$: if (capsule) {
capRef = capsule
capRef = capsule;
}
</script>
@ -145,11 +145,12 @@
<ContactShadows scale={10} blur={2} far={2.5} opacity={0.5} />
<Robot
position.y={1}
<!-- <Hornet
position.y={2}
position.z={robotPos.z}
position.x={robotPos.x}
scale={[5, 5, 5]}
bind:ref={capsule}
rotation.y={rot}
/>
/> -->

View file

@ -1,6 +1,39 @@
<script lang="ts">
import { Canvas } from '@threlte/core'
import Scene from './Scene.svelte'
// @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 { onMount } from 'svelte'
import {
Vector3,
type OrthographicCamera,
type PerspectiveCamera,
} from 'three'
let camera: PerspectiveCamera | OrthographicCamera
$: if ($cameraControls) {
// @ts-expect-error
camera = $cameraControls._camera
}
onMount(() => {
// setTimeout(() => {
// // $cameraControls.moveTo(3, 5, 2, true)
// // $cameraControls.setOrbitPoint(20, 20, 20)
// $cameraControls.setLookAt(
// 3,
// 5,
// 2,
// $mesh.position.x,
// $mesh.position.y,
// $mesh.position.z,
// true
// )
// }, 8000)
})
</script>
<Canvas>

View file

@ -1,113 +1,147 @@
import { toast } from 'svelte-french-toast'
import type { ToastOptions } from 'svelte-french-toast'
import InfoIcon from './InfoIcon.svelte'
import { Howl } from 'howler'
import WarnIcon from './WarnIcon.svelte'
import { toast } from "svelte-french-toast";
import type { ToastOptions } from "svelte-french-toast";
import InfoIcon from "./InfoIcon.svelte";
import { Howl } from "howler";
import WarnIcon from "./WarnIcon.svelte";
interface NotificationOptions extends ToastOptions {
withAudio?: boolean
src?: string
onComplete?: () => void
withAudio?: boolean;
src?: string;
onComplete?: () => void;
}
// get colors from https://tailwindcss.com/docs/customizing-colors
export class Notifications {
private static readonly defaultDuration = 3000
private static readonly defaultDuration = 3000;
public static success(message: string, options?: NotificationOptions) {
if (options?.withAudio && !options.src)
throw new Error('No audio source provided')
throw new Error("No audio source provided");
const onComplete = () => {
if (options?.onComplete) options.onComplete()
}
if (options?.onComplete) options.onComplete();
};
const sendToast = (duration: number) => {
toast.success(message, {
style:
'padding: 25px; font-size: 1.5rem; background-color: #15803d; color: #fafafa; gap: 0.5rem; user-select: none; max-width: 70vw;',
"padding: 25px; font-size: 1.5rem; background-color: #15803d; color: #fafafa; gap: 0.5rem; user-select: none; max-width: 70vw;",
duration,
...options,
})
}
});
};
if (options?.withAudio && options?.src) {
let sound: Howl
let sound: Howl;
sound = new Howl({
src: [options.src],
preload: true,
autoplay: true,
onload: () => {
let duration = sound.duration() * 1000
sendToast(duration)
setTimeout(onComplete, duration)
let duration = sound.duration() * 1000;
sendToast(duration);
setTimeout(onComplete, duration);
},
})
});
} else {
sendToast(this.defaultDuration)
setTimeout(onComplete, this.defaultDuration)
sendToast(this.defaultDuration);
setTimeout(onComplete, this.defaultDuration);
}
}
public static error(message: string, options?: NotificationOptions) {
if (options?.withAudio && !options.src)
throw new Error("No audio source provided");
const onComplete = () => {
if (options?.onComplete) options.onComplete();
};
const sendToast = (duration: number) => {
toast.error(message, {
style:
"padding: 25px; font-size: 1.5rem; background-color: #dc2626; color: #fafafa; gap: 0.5rem; user-select: none; max-width: 70vw;",
duration,
...options,
});
};
if (options?.withAudio && options?.src) {
let sound: Howl;
sound = new Howl({
src: [options.src],
preload: true,
autoplay: true,
onload: () => {
let duration = sound.duration() * 1000;
sendToast(duration);
setTimeout(onComplete, duration);
},
});
} else {
sendToast(this.defaultDuration);
setTimeout(onComplete, this.defaultDuration);
}
}
public static info(message: string, options?: NotificationOptions) {
const onComplete = () => {
if (options?.onComplete) options.onComplete()
}
if (options?.onComplete) options.onComplete();
};
const sendToast = (duration: number) => {
toast(message, {
style:
'padding: 25px; font-size: 1.5rem; gap: 0.5rem; user-select: none; max-width-600px; max-width: 70vw;',
"padding: 25px; font-size: 1.5rem; gap: 0.5rem; user-select: none; max-width-600px; max-width: 70vw;",
icon: InfoIcon,
duration,
...options,
})
}
});
};
if (options?.withAudio && options?.src) {
let sound: Howl
let sound: Howl;
sound = new Howl({
src: [options.src],
preload: true,
autoplay: true,
onload: () => {
let duration = sound.duration() * 1000
sendToast(duration)
setTimeout(onComplete, duration)
let duration = sound.duration() * 1000;
sendToast(duration);
setTimeout(onComplete, duration);
},
})
});
} else {
sendToast(this.defaultDuration)
setTimeout(onComplete, this.defaultDuration)
sendToast(this.defaultDuration);
setTimeout(onComplete, this.defaultDuration);
}
}
public static warn(message: string, options?: NotificationOptions) {
const onComplete = () => {
if (options?.onComplete) options.onComplete()
}
if (options?.onComplete) options.onComplete();
};
const sendToast = (duration: number) => {
toast(message, {
style:
'padding: 25px; font-size: 1.5rem; background-color: #f59e0b; color: #fafafa; gap: 0.5rem; user-select: none; max-width: 70vw;',
"padding: 25px; font-size: 1.5rem; background-color: #f59e0b; color: #fafafa; gap: 0.5rem; user-select: none; max-width: 70vw;",
icon: WarnIcon,
duration,
...options,
})
}
});
};
if (options?.withAudio && options?.src) {
let sound: Howl
let sound: Howl;
sound = new Howl({
src: [options.src],
preload: true,
autoplay: true,
onload: () => {
let duration = sound.duration() * 1000
sendToast(duration)
setTimeout(onComplete, duration)
let duration = sound.duration() * 1000;
sendToast(duration);
setTimeout(onComplete, duration);
},
})
});
} else {
sendToast(this.defaultDuration)
setTimeout(onComplete, this.defaultDuration)
sendToast(this.defaultDuration);
setTimeout(onComplete, this.defaultDuration);
}
}
public static playAudio(src: string, onComplete: () => void = () => {}) {
@ -116,8 +150,8 @@ export class Notifications {
preload: true,
autoplay: true,
onload: () => {
setTimeout(onComplete, 1000 * sound.duration())
setTimeout(onComplete, 1000 * sound.duration());
},
})
});
}
}

View file

@ -14,47 +14,51 @@ 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";
// 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', 'en'),
})
setTimeout(() => {
if (get(settingsStore).goWoke) return
Notifications.success('LittenOS is online', {
withAudio: true,
src: getVoicePath('littenos-is-online', 'en'),
})
setTimeout(() => {
Notifications.warn('Breaching Monte Vista codebase', {
src: getVoicePath("jankboard-initialized"),
onComplete: () => {
if (get(settingsStore).goWoke) {
sequenceStore.update("initializationComplete", true);
periodicSequence();
return;
}
Notifications.success("LittenOS is online", {
withAudio: true,
src: getVoicePath('breaching-monte-vista', 'en'),
})
setTimeout(() => {
Notifications.playAudio(
getVoicePath('hello-virtual-assistant', 'en'),
() => {
sequenceStore.update('initializationComplete', true)
periodicSequence()
}
)
}, 3000)
}, 3000)
}, 3000)
}
src: getVoicePath("littenos-is-online"),
onComplete: () => {
Notifications.warn("Breaching Monte Vista codebase", {
withAudio: true,
src: getVoicePath("breaching-monte-vista"),
onComplete: () => {
Notifications.playAudio(
getVoicePath("hello-virtual-assistant"),
() => {
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
@ -64,7 +68,7 @@ let counter = 1
* @return void
*/
const periodicSequence = async () => {
await tick()
await tick();
/**
* Returns either true or false based on the provided probability
@ -74,11 +78,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.
@ -87,140 +91,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', 'en'),
})
}
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', 'en'),
})
}
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', 'en'),
})
}
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', 'en'),
})
}
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', 'en'),
})
}
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', 'en'),
})
}
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', 'en'),
})
}
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', 'en'),
})
}
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', 'en'),
})
}
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', 'en'),
})
setTimeout(() => {
Notifications.success('Infotainment system online', {
withAudio: true,
src: getVoicePath('infotainment-system-online', 'en'),
onComplete: () => {
sequenceStore.update('infotainmentStartedFirstTime', true)
},
})
}, 3000)
}
src: getVoicePath("infotainment-system-buffering"),
onComplete: () => {
Notifications.success("Infotainment system online", {
withAudio: true,
src: getVoicePath("infotainment-system-online"),
onComplete: () => {
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.
@ -231,77 +236,77 @@ 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', 'en'),
})
})
}
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', 'en'),
})
})
}
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', 'en'),
})
})
}
src: getVoicePath("doom-engaged"),
});
});
};
const bypassCoprocessorRestrictionsSequence = async () => {
if (get(settingsStore).disableAnnoyances) return
await tick()
Notifications.warn('Bypassing coprocessor restrictions', {
if (get(settingsStore).disableAnnoyances) return;
await tick();
Notifications.warn("Bypassing coprocessor restrictions", {
withAudio: true,
src: getVoicePath('bypassing-coprocessor-restrictions', 'en'),
})
}
src: getVoicePath("bypassing-coprocessor-restrictions"),
});
};

View file

@ -1,12 +1,15 @@
/* stores global app wide settings */
import { writable } from 'svelte/store'
import { writable } from "svelte/store";
type SupportedLanguage = "en" | "rus";
export interface SettingsStoreData {
disableAnnoyances: boolean
goWoke: boolean
fastStartup: boolean
randomWeight: number
disableAnnoyances: boolean;
goWoke: boolean;
fastStartup: boolean;
randomWeight: number;
voiceLang: SupportedLanguage;
}
export const defaults: SettingsStoreData = {
@ -14,25 +17,26 @@ export const defaults: SettingsStoreData = {
goWoke: false, // go woke (for showing parents or other officials where DEI has taken over), disables "offensive" sequences
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",
};
const createSequenceStore = () => {
const { subscribe, set, update } = writable<SettingsStoreData>(defaults)
const { subscribe, set, update } = writable<SettingsStoreData>(defaults);
return {
subscribe,
update: (
data: keyof SettingsStoreData,
newValue: SettingsStoreData[typeof data]
) => {
update(store => {
update((store) => {
// @ts-expect-error
store[data] = newValue
return store
})
store[data] = newValue;
return store;
});
},
reset: () => set(defaults),
set: (data: SettingsStoreData) => set(data),
}
}
};
};
export const settingsStore = createSequenceStore()
export const settingsStore = createSequenceStore();

View file

@ -1,3 +1,6 @@
import { get } from "svelte/store";
import { settingsStore } from "../stores/settingsStore";
/**
* Retrieves the voice audio path for the given audio file.
*
@ -5,7 +8,18 @@
* @param lang - the language of the audio
* @return the path of the audio file
*/
type SupportedLanguage = 'en' | 'rus'
export default function getVoicePath(audio: string, lang: SupportedLanguage) {
return `/static/voices/${lang}/${audio}.wav`
type SupportedLanguage = "en" | "rus";
let currentLang = "en";
settingsStore.subscribe((data) => {
currentLang = data.voiceLang;
});
export default function getVoicePath(audio: string, lang?: SupportedLanguage) {
console.log(get(settingsStore).voiceLang);
if (!lang) {
return `/static/voices/${get(settingsStore).voiceLang}/${audio}.wav`;
}
return `/static/voices/${lang}/${audio}.wav`;
}