Compare commits
10 Commits
f2ccb6b840
...
02526f340a
| Author | SHA1 | Date | |
|---|---|---|---|
|
02526f340a
|
|||
| d7da674267 | |||
| 9029739657 | |||
| 27eadaeca4 | |||
| 7bd76c1d4a | |||
| 62a656cfda | |||
| 6e3a97da5f | |||
| 724a423ce8 | |||
|
2ae20cdac1
|
|||
|
81fb8beece
|
@@ -31,7 +31,7 @@ RUN cp /psn-py/psn$(python3-config --extension-suffix) /output
|
|||||||
|
|
||||||
FROM alpine:3.20 AS final
|
FROM alpine:3.20 AS final
|
||||||
|
|
||||||
RUN apk add --no-cache python3 py3-aiohttp
|
RUN apk add --no-cache python3 py3-aiohttp py3-python-osc
|
||||||
|
|
||||||
|
|
||||||
COPY --from=backend /output /backend/psn.so
|
COPY --from=backend /output /backend/psn.so
|
||||||
|
|||||||
@@ -3,17 +3,62 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
|
import weakref
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
import psn
|
import psn
|
||||||
from aiohttp import web
|
from pythonosc.osc_server import AsyncIOOSCUDPServer
|
||||||
|
from pythonosc.dispatcher import Dispatcher
|
||||||
|
from aiohttp import web, WSCloseCode
|
||||||
|
|
||||||
PSN_DEFAULT_UDP_PORT = 56565
|
PSN_DEFAULT_UDP_PORT = 56565
|
||||||
PSN_DEFAULT_UDP_MCAST_ADDRESS = "236.10.10.10"
|
PSN_DEFAULT_UDP_MCAST_ADDRESS = "236.10.10.10"
|
||||||
PORT = 8000
|
WEB_SERVER_PORT = 8000
|
||||||
IP = "0.0.0.0"
|
IP = "0.0.0.0"
|
||||||
|
OSC_SERVER_PORT = 6969
|
||||||
NUM_TRACKERS = 3
|
NUM_TRACKERS = 3
|
||||||
|
|
||||||
|
class SceneDimensions:
|
||||||
|
x_min: float
|
||||||
|
x_max: float
|
||||||
|
y_min: float
|
||||||
|
y_max: float
|
||||||
|
z_min: float
|
||||||
|
z_max: float
|
||||||
|
|
||||||
|
dimension_name: str
|
||||||
|
|
||||||
|
dimension_map = {
|
||||||
|
"scene_only": (-13 / 2, 13 / 2, 0, 6.3 , 0, 4),
|
||||||
|
"full_arena": (-13 / 2, 13 / 2, -9.7, 6.3 , 0, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.set_scene_only_dimensions()
|
||||||
|
|
||||||
|
def _set_new_dimensions(self, x_min, x_max, y_min, y_max, z_min, z_max):
|
||||||
|
self.x_min = x_min
|
||||||
|
self.x_max = x_max
|
||||||
|
self.y_min = y_min
|
||||||
|
self.y_max = y_max
|
||||||
|
self.z_min = z_min
|
||||||
|
self.z_max = z_max
|
||||||
|
|
||||||
|
def set_scene_only_dimensions(self):
|
||||||
|
self._set_new_dimensions(*SceneDimensions.dimension_map["scene_only"])
|
||||||
|
self.dimension_name = "scene_only"
|
||||||
|
|
||||||
|
def set_full_arena_dimensions(self):
|
||||||
|
self._set_new_dimensions(*SceneDimensions.dimension_map["full_arena"])
|
||||||
|
self.dimension_name = "full_arena"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
START_POSITION_INTERNAL = (0.5, 0.5, 2)
|
||||||
|
|
||||||
|
def map_range(value: float, in_min: float, in_max: float, out_min: float, out_max: float) -> float:
|
||||||
|
return (value - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
|
||||||
|
|
||||||
|
|
||||||
# Internal state is a list of TrackerData objects
|
# Internal state is a list of TrackerData objects
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -21,23 +66,33 @@ class TrackerData:
|
|||||||
id: int
|
id: int
|
||||||
x: float
|
x: float
|
||||||
y: float
|
y: float
|
||||||
|
z: float
|
||||||
|
|
||||||
# Convert from internal coordinates to actual scene coordinates
|
@staticmethod
|
||||||
# Internal representation is float values from 0 to 1 for x and y
|
def internal_to_scene_coords_3d(x: float, y: float, z: float, dim: SceneDimensions) -> tuple[float, float, float]:
|
||||||
# Thus we need to know the max size and aspect ratio of the scene
|
"""
|
||||||
# x is the width of the scene, y is the depth, z is the height
|
Convert internal coordinates to scene coordinates
|
||||||
def pic_to_scene_coords(x, y):
|
|
||||||
scene_width = 8.0
|
|
||||||
scene_depth = 6.3
|
|
||||||
tracker_height = 0.8 + 1.5 # Scene height + height of person
|
|
||||||
|
|
||||||
x_val = x * scene_width - (scene_width / 2)
|
Internal coordinates are in the range [0, 1] for x and y with (0,0) at the top left
|
||||||
y_val = y * scene_depth
|
Scene coordinates are in the range [X_MIN, X_MAX] for x and [Y_MIN, YMAX] for y
|
||||||
return x_val, y_val, tracker_height
|
"""
|
||||||
|
x_val = map_range(x, 0, 1, dim.x_min, dim.x_max)
|
||||||
|
y_val = map_range(y, 0, 1, dim.y_max, dim.y_min) # Invert y axis
|
||||||
|
z_val = z
|
||||||
|
|
||||||
def to_tracker(self):
|
return x_val, y_val, z_val
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def scene_to_internal_coords_3d(x: float, y: float, z: float, dim: SceneDimensions) -> tuple[float, float, float]:
|
||||||
|
x_val = map_range(x, dim.x_min, dim.x_max, 0, 1)
|
||||||
|
y_val = map_range(y, dim.y_max, dim.y_min, 0, 1) # Invert y axis
|
||||||
|
z_val = z
|
||||||
|
|
||||||
|
return x_val, y_val, z_val
|
||||||
|
|
||||||
|
def to_tracker(self, dim: SceneDimensions) -> psn.Tracker:
|
||||||
tracker = psn.Tracker(self.id, f"Tracker {self.id}")
|
tracker = psn.Tracker(self.id, f"Tracker {self.id}")
|
||||||
x, y, z = self.pic_to_scene_coords(self.x, self.y)
|
x, y, z = TrackerData.internal_to_scene_coords_3d(self.x, self.y, self.z, dim)
|
||||||
tracker.set_pos(psn.Float3(x, y, z))
|
tracker.set_pos(psn.Float3(x, y, z))
|
||||||
return tracker
|
return tracker
|
||||||
|
|
||||||
@@ -62,12 +117,19 @@ def get_elapsed_time_ms():
|
|||||||
return get_time_ms() - START_TIME
|
return get_time_ms() - START_TIME
|
||||||
|
|
||||||
|
|
||||||
async def update_all_clients(app: web.Application, ws: web.WebSocketResponse = None):
|
async def update_all_other_clients(app: web.Application, ws: web.WebSocketResponse = None):
|
||||||
for ws_send in app["ws_clients"]:
|
for ws_send in app["ws_clients"]:
|
||||||
if ws_send == ws:
|
if ws_send == ws:
|
||||||
continue
|
continue
|
||||||
await ws_send.send_str(trackers_to_json(app))
|
await ws_send.send_str(trackers_to_json(app))
|
||||||
|
|
||||||
|
async def update_all_clients(app: web.Application):
|
||||||
|
for ws in app["ws_clients"]:
|
||||||
|
await ws.send_str(trackers_to_json(app))
|
||||||
|
|
||||||
|
async def update_all_clients_bg(app: web.Application):
|
||||||
|
for ws in app["ws_clients"]:
|
||||||
|
await ws.send_str(json.dumps({"refresh": True}))
|
||||||
|
|
||||||
async def handle_websocket(request):
|
async def handle_websocket(request):
|
||||||
ws = web.WebSocketResponse()
|
ws = web.WebSocketResponse()
|
||||||
@@ -83,34 +145,64 @@ async def handle_websocket(request):
|
|||||||
async for msg in ws:
|
async for msg in ws:
|
||||||
if msg.type == web.WSMsgType.TEXT:
|
if msg.type == web.WSMsgType.TEXT:
|
||||||
# Each message is a single tracker object
|
# Each message is a single tracker object
|
||||||
|
logging.debug(f"Received ws update: {msg.data}")
|
||||||
update_tracker(msg.data, request.app)
|
update_tracker(msg.data, request.app)
|
||||||
await update_all_clients(request.app, ws)
|
await update_all_other_clients(request.app, ws)
|
||||||
|
|
||||||
elif msg.type == web.WSMsgType.ERROR:
|
elif msg.type == web.WSMsgType.ERROR:
|
||||||
logging.error("ws connection closed with exception %s" % ws.exception())
|
logging.error("ws connection closed with exception %s" % ws.exception())
|
||||||
print("ws connection closed with exception %s" % ws.exception())
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Websocket exception: {e}")
|
logging.error(f"Websocket exception: {e}")
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
logging.debug("Websocket connection closing")
|
logging.debug("Websocket connection closing")
|
||||||
await ws.close()
|
request.app["ws_clients"].discard(ws)
|
||||||
|
|
||||||
request.app["ws_clients"].remove(ws)
|
|
||||||
|
|
||||||
return ws
|
return ws
|
||||||
|
|
||||||
|
async def on_shutdown(app):
|
||||||
|
for ws in set(app["ws_clients"]):
|
||||||
|
await ws.close(code=WSCloseCode.GOING_AWAY, message="Server shutdown")
|
||||||
|
|
||||||
|
|
||||||
async def handle_root(request):
|
async def handle_root(request):
|
||||||
return web.FileResponse("./static/index.html")
|
return web.FileResponse("./static/index.html")
|
||||||
|
|
||||||
|
async def handle_background_image(request):
|
||||||
|
if request.app["scene_dimensions"].dimension_name == "full_arena":
|
||||||
|
return web.FileResponse("./static/scene_and_crowd.png")
|
||||||
|
elif request.app["scene_dimensions"].dimension_name == "scene_only":
|
||||||
|
return web.FileResponse("./static/scene_only.png")
|
||||||
|
return web.Response(text="Incorrect server scende dimension state", status=500)
|
||||||
|
|
||||||
|
async def handle_set_mode(request):
|
||||||
|
try:
|
||||||
|
data = request.data = await request.json()
|
||||||
|
mode = data["mode"]
|
||||||
|
if mode == "full_arena":
|
||||||
|
request.app["scene_dimensions"].set_full_arena_dimensions()
|
||||||
|
elif mode == "scene_only":
|
||||||
|
request.app["scene_dimensions"].set_scene_only_dimensions()
|
||||||
|
else:
|
||||||
|
return web.Response(text="Invalid mode", status=400)
|
||||||
|
|
||||||
|
await update_all_clients_bg(request.app)
|
||||||
|
|
||||||
|
return web.Response(text="OK")
|
||||||
|
except Exception as e:
|
||||||
|
return web.Response(text=f"Error: {e}", status=400)
|
||||||
|
|
||||||
|
async def handlet_get_mode(request):
|
||||||
|
return web.json_response({"mode": request.app["scene_dimensions"].dimension_name})
|
||||||
|
|
||||||
|
|
||||||
async def broadcast_psn_data(app):
|
async def broadcast_psn_data(app):
|
||||||
encoder = psn.Encoder("Server 1")
|
encoder = psn.Encoder("Server 1")
|
||||||
while True:
|
while True:
|
||||||
trackers = {}
|
trackers = {}
|
||||||
for tracker_data in app["trackers"].values():
|
for tracker_data in app["trackers"].values():
|
||||||
trackers[tracker_data.id] = tracker_data.to_tracker()
|
trackers[tracker_data.id] = tracker_data.to_tracker(app["scene_dimensions"])
|
||||||
packets = encoder.encode_data(trackers, get_elapsed_time_ms())
|
packets = encoder.encode_data(trackers, get_elapsed_time_ms())
|
||||||
for packet in packets:
|
for packet in packets:
|
||||||
app["sock"].sendto(
|
app["sock"].sendto(
|
||||||
@@ -126,21 +218,49 @@ async def background_tasks(app: web.Application):
|
|||||||
await app["broadcast_psn_data"]
|
await app["broadcast_psn_data"]
|
||||||
|
|
||||||
|
|
||||||
|
def osc_tracker_updater(address, fixed_args, *args) -> None:
|
||||||
|
app = fixed_args[0]
|
||||||
|
tracker_id = int(address.split("/")[-1])
|
||||||
|
x, y, z = TrackerData.scene_to_internal_coords_3d(*args, app["scene_dimensions"])
|
||||||
|
logging.debug(f"OSC received: id: {tracker_id} at {x}, {y}, {z}")
|
||||||
|
tracker = TrackerData(tracker_id, x, y, z)
|
||||||
|
app["trackers"][tracker_id] = tracker
|
||||||
|
|
||||||
|
asyncio.ensure_future(update_all_clients(app))
|
||||||
|
|
||||||
|
|
||||||
|
async def receive_osc_data(app):
|
||||||
|
dispatcher = Dispatcher()
|
||||||
|
dispatcher.map("/Tracker*", osc_tracker_updater, app)
|
||||||
|
server = AsyncIOOSCUDPServer((IP, OSC_SERVER_PORT), dispatcher, asyncio.get_event_loop())
|
||||||
|
transport, protocol = await server.create_serve_endpoint()
|
||||||
|
app["osc_transport"] = transport
|
||||||
|
yield
|
||||||
|
await transport.close()
|
||||||
|
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_get("/", handle_root)
|
app.router.add_get("/", handle_root)
|
||||||
app.router.add_get("/ws", handle_websocket)
|
app.router.add_get("/ws", handle_websocket)
|
||||||
|
app.router.add_get("/background_image", handle_background_image)
|
||||||
|
app.router.add_post("/mode", handle_set_mode)
|
||||||
|
app.router.add_get("/mode", handlet_get_mode)
|
||||||
app.router.add_static("/", "./static")
|
app.router.add_static("/", "./static")
|
||||||
|
|
||||||
# Setup app state
|
# Setup app state
|
||||||
app["ws_clients"] = set()
|
# All app state needs to be mutable as changing state variables directly while running is not supported
|
||||||
|
app["ws_clients"] = weakref.WeakSet()
|
||||||
app["trackers"] = {}
|
app["trackers"] = {}
|
||||||
app["sock"] = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
app["sock"] = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
app["scene_dimensions"] = SceneDimensions()
|
||||||
|
|
||||||
for i in range(NUM_TRACKERS):
|
for i in range(NUM_TRACKERS):
|
||||||
app["trackers"][i] = TrackerData(i, 0, 0)
|
app["trackers"][i] = TrackerData(i, *START_POSITION_INTERNAL)
|
||||||
|
|
||||||
|
app.on_shutdown.append(on_shutdown)
|
||||||
app.cleanup_ctx.append(background_tasks)
|
app.cleanup_ctx.append(background_tasks)
|
||||||
|
app.cleanup_ctx.append(receive_osc_data)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
@@ -148,4 +268,4 @@ def create_app():
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
app = create_app()
|
app = create_app()
|
||||||
web.run_app(app, host=IP, port=PORT)
|
web.run_app(app, host=IP, port=WEB_SERVER_PORT)
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 138 KiB |
@@ -1,3 +1,7 @@
|
|||||||
{
|
{
|
||||||
"plugins": ["prettier-plugin-astro","prettier-plugin-svelte","prettier-plugin-tailwindcss"]
|
"plugins": [
|
||||||
|
"prettier-plugin-astro",
|
||||||
|
"prettier-plugin-svelte",
|
||||||
|
"prettier-plugin-tailwindcss"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
// @ts-check
|
// @ts-check
|
||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from "astro/config";
|
||||||
|
|
||||||
import svelte from '@astrojs/svelte';
|
import svelte from "@astrojs/svelte";
|
||||||
|
|
||||||
import tailwind from '@astrojs/tailwind';
|
import tailwind from "@astrojs/tailwind";
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
integrations: [svelte(), tailwind()]
|
integrations: [svelte(), tailwind()],
|
||||||
});
|
});
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"build": "astro check && astro build",
|
"build": "astro check && astro build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
|
"format": "prettier --write .",
|
||||||
"astro": "astro"
|
"astro": "astro"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
4495
frontend/pnpm-lock.yaml
generated
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -1,9 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
|
||||||
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
|
||||||
<style>
|
|
||||||
path { fill: #000; }
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
path { fill: #FFF; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 749 B |
BIN
frontend/public/scene_and_crowd.png
Normal file
|
After Width: | Height: | Size: 199 KiB |
|
Before Width: | Height: | Size: 138 KiB |
BIN
frontend/public/scene_only.png
Normal file
|
After Width: | Height: | Size: 484 KiB |
@@ -2,30 +2,45 @@
|
|||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import type { TrackerData } from "../utils/types";
|
import type { TrackerData } from "../utils/types";
|
||||||
import Drag from "./Drag.svelte";
|
import Drag from "./Drag.svelte";
|
||||||
|
import Slider from "./Slider.svelte";
|
||||||
|
|
||||||
let ws: WebSocket | null = $state(null);
|
let ws: WebSocket | null = $state(null);
|
||||||
let image: HTMLImageElement | null = $state(null);
|
let image: HTMLImageElement | null = $state(null);
|
||||||
|
|
||||||
let trackers: TrackerData[] = $state([
|
let trackers: TrackerData[] = $state([
|
||||||
{ id: 1, x: 0, y: 0 },
|
{ id: 1, x: 0, y: 0, z: 0 },
|
||||||
{ id: 2, x: 0, y: 0 },
|
{ id: 2, x: 0, y: 0, z: 0 },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let width = $state(0);
|
let width = $state(0);
|
||||||
let height = $state(0);
|
let height = $state(0);
|
||||||
|
let selected = $state(0);
|
||||||
|
|
||||||
let debounce: number | null = $state(null);
|
let debounce: number | null = $state(null);
|
||||||
|
|
||||||
const connect = () => {
|
const connect = () => {
|
||||||
|
if (ws) {
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
|
||||||
ws = new WebSocket("/ws");
|
ws = new WebSocket("/ws");
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
for (const tracker of data) {
|
console.log(data)
|
||||||
tracker.x *= width;
|
|
||||||
tracker.y *= height;
|
if (data.refresh && image) {
|
||||||
|
image.src = `/background_image?${Math.random()}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
for (const tracker of data) {
|
||||||
|
tracker.x *= width;
|
||||||
|
tracker.y *= height;
|
||||||
|
}
|
||||||
|
trackers = data;
|
||||||
}
|
}
|
||||||
trackers = data;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,24 +66,31 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window onresize={resize} />
|
<svelte:window onresize={resize} />
|
||||||
<div class="relative">
|
<div class="flex h-screen items-center justify-center w-screen">
|
||||||
<img
|
<div class="w-20 mr-auto"></div>
|
||||||
src="/scene_drawing.png"
|
<div class="relative">
|
||||||
alt=""
|
<img
|
||||||
bind:this={image}
|
src="/background_image?342038402"
|
||||||
class="max-w-screen max-h-screen"
|
alt=""
|
||||||
onload={resize}
|
bind:this={image}
|
||||||
/>
|
class="max-w-screen max-h-screen"
|
||||||
<div class="absolute inset-0 border border-red-500">
|
onload={resize}
|
||||||
{#each trackers as tracker}
|
/>
|
||||||
<Drag
|
<div class="absolute inset-0 border border-red-500">
|
||||||
bind:id={tracker.id}
|
{#each trackers as tracker}
|
||||||
bind:x={tracker.x}
|
<Drag
|
||||||
bind:y={tracker.y}
|
bind:id={tracker.id}
|
||||||
bind:width
|
bind:x={tracker.x}
|
||||||
bind:height
|
bind:y={tracker.y}
|
||||||
{ws}
|
bind:z={tracker.z}
|
||||||
/>
|
bind:width
|
||||||
{/each}
|
bind:height
|
||||||
|
bind:selected
|
||||||
|
{ws}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Slider bind:trackers {ws} bind:height bind:width bind:selected />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,18 +3,22 @@
|
|||||||
id: number;
|
id: number;
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
z: number;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
ws: WebSocket | null;
|
ws: WebSocket | null;
|
||||||
|
selected: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
let {
|
let {
|
||||||
id = $bindable(),
|
id = $bindable(),
|
||||||
x = $bindable(),
|
x = $bindable(),
|
||||||
y = $bindable(),
|
y = $bindable(),
|
||||||
|
z = $bindable(),
|
||||||
ws = $bindable(),
|
ws = $bindable(),
|
||||||
width = $bindable(),
|
width = $bindable(),
|
||||||
height = $bindable(),
|
height = $bindable(),
|
||||||
|
selected = $bindable(),
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let element: HTMLElement;
|
let element: HTMLElement;
|
||||||
@@ -33,6 +37,7 @@
|
|||||||
|
|
||||||
function onPointerDown(e: PointerEvent) {
|
function onPointerDown(e: PointerEvent) {
|
||||||
element!.setPointerCapture(e.pointerId);
|
element!.setPointerCapture(e.pointerId);
|
||||||
|
selected = id;
|
||||||
capturedPointerId = e.pointerId;
|
capturedPointerId = e.pointerId;
|
||||||
}
|
}
|
||||||
function onPointerUp(e: PointerEvent) {
|
function onPointerUp(e: PointerEvent) {
|
||||||
@@ -58,6 +63,7 @@
|
|||||||
id: id,
|
id: id,
|
||||||
x: x_send,
|
x: x_send,
|
||||||
y: y_send,
|
y: y_send,
|
||||||
|
z: z,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -69,7 +75,8 @@
|
|||||||
onpointerup={onPointerUp}
|
onpointerup={onPointerUp}
|
||||||
onpointermove={onPointerMove}
|
onpointermove={onPointerMove}
|
||||||
style={`transform: translate(${vis_x}px, ${vis_y}px)`}
|
style={`transform: translate(${vis_x}px, ${vis_y}px)`}
|
||||||
class="absolute flex h-32 w-32 touch-none select-none items-center justify-center rounded-full border-red-400 bg-red-400"
|
class={`absolute flex h-24 w-24 touch-none select-none items-center justify-center rounded-full
|
||||||
|
${selected === id ? 'bg-green-400 border-green-400' : 'bg-red-400 border-red-400'}`}
|
||||||
>
|
>
|
||||||
Tracker {id}
|
Tracker {id}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
10
frontend/src/components/Modal.svelte
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
type Props = {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<dialog>
|
||||||
|
<div></div>
|
||||||
|
</dialog>
|
||||||
108
frontend/src/components/Slider.svelte
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { TrackerData } from "$lib/utils/types";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import Modal from "./Modal.svelte";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
selected: number;
|
||||||
|
trackers: TrackerData[];
|
||||||
|
ws: WebSocket | null;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
let {
|
||||||
|
selected = $bindable(),
|
||||||
|
trackers = $bindable(),
|
||||||
|
ws,
|
||||||
|
width = $bindable(),
|
||||||
|
height = $bindable(),
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
let arena_state: "full_arena" | "scene_only" = $state("scene_only");
|
||||||
|
|
||||||
|
const onchange = () => {
|
||||||
|
const x_send = trackers[selected].x / width;
|
||||||
|
const y_send = trackers[selected].y / height;
|
||||||
|
ws?.send(
|
||||||
|
JSON.stringify({
|
||||||
|
id: trackers[selected].id,
|
||||||
|
x: x_send,
|
||||||
|
y: y_send,
|
||||||
|
z: trackers[selected].z,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type arenaResponse = {
|
||||||
|
mode: "full_arena" | "scene_only";
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttonAction = async () => {
|
||||||
|
await fetch("/mode")
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data: arenaResponse) => {
|
||||||
|
arena_state = data.mode;
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetch("/mode", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
mode: arena_state === "full_arena" ? "scene_only" : "full_arena",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let z_viz = $derived(trackers[selected].z.toFixed(2));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal />
|
||||||
|
<div class="slider-container ml-auto">
|
||||||
|
<button class="bg-red-400 rounded-md p-2 mb-24"
|
||||||
|
onclick={buttonAction}>Change mode</button>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="z"
|
||||||
|
min="0"
|
||||||
|
max="4"
|
||||||
|
step="0.01"
|
||||||
|
oninput={onchange}
|
||||||
|
bind:value={trackers[selected].z}
|
||||||
|
/>
|
||||||
|
<output for="z">{z_viz} m</output>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.slider-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
writing-mode: vertical-lr;
|
||||||
|
direction: rtl;
|
||||||
|
appearance: none;
|
||||||
|
width: 120px;
|
||||||
|
height: 50%;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* WebKit Browsers (Chrome, Safari, Edge) */
|
||||||
|
input::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 50px; /* Larger size */
|
||||||
|
height: 50px;
|
||||||
|
background: red; /* Customize color */
|
||||||
|
margin-left: -22px; /* Center the thumb */
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::-webkit-slider-runnable-track {
|
||||||
|
background: lightgray; /* Customize track */
|
||||||
|
width: 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,13 +5,13 @@ import Container from "../components/Container.svelte";
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
<meta name="generator" content={Astro.generator} />
|
<meta name="generator" content={Astro.generator} />
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
|
||||||
/>
|
/>
|
||||||
<title>Astro</title>
|
<title>Followspot</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="flex h-dvh w-screen items-center justify-center overflow-hidden">
|
<body class="flex h-dvh w-screen items-center justify-center overflow-hidden">
|
||||||
<Container client:load />
|
<Container client:load />
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ export interface TrackerData {
|
|||||||
id: number;
|
id: number;
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
z: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { vitePreprocess } from '@astrojs/svelte';
|
import { vitePreprocess } from "@astrojs/svelte";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
preprocess: vitePreprocess(),
|
preprocess: vitePreprocess(),
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
export default {
|
export default {
|
||||||
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -7,11 +7,6 @@
|
|||||||
"$lib/*": ["./src/*"]
|
"$lib/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [".astro/types.d.ts", "**/*"],
|
||||||
".astro/types.d.ts",
|
"exclude": ["dist"]
|
||||||
"**/*"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"dist"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||