diff --git a/.prettierignore b/.prettierignore index 32db49eeb944e7e5b085dd79aafaeee756f24ed8..2b4c54a93e97af2ffd4d8d96e760f0731677067f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -10,3 +10,12 @@ src-python/ node_modules/ build/ .svelte-kit/ + +# External +external/ + +# Build +build/ + +# Package +packages/ \ No newline at end of file diff --git a/README.md b/README.md index bff7054b104ac4a32508d6fa4c34f430476356b3..36da8b1aa72e265b5c1a22a4d70aa3e2eadad722 100644 --- a/README.md +++ b/README.md @@ -15,14 +15,14 @@ app_port: 8000 pinned: true license: mit fullWidth: true -short_description: Web interface of the RobotHub platform +short_description: Web interface of the RobotHub platform --- # πŸ€– RobotHub Arena – Frontend RobotHub is an **open-source, end-to-end robotics stack** that combines real-time communication, 3-D visualisation, and modern AI policies to control both simulated and physical robots. -**This repository contains the *Frontend*** – a SvelteKit web application that runs completely in the browser (or inside Electron / Tauri). It talks to two backend micro-services that live in their own repositories: +**This repository contains the _Frontend_** – a SvelteKit web application that runs completely in the browser (or inside Electron / Tauri). It talks to two backend micro-services that live in their own repositories: 1. **[RobotHub Transport Server](https://github.com/julien-blanchon/RobotHub-TransportServer)** – WebSocket / WebRTC switch-board for video streams & robot joint messages. @@ -46,22 +46,22 @@ RobotHub is an **open-source, end-to-end robotics stack** that combines real-tim ## ✨ Key Features β€’ **Digital-Twin 3-D Scene** – inspect robots, cameras & AI compute blocks in real-time. -β€’ **Multi-Workspace Collaboration** – share a hash URL and others join the *same* WS rooms instantly. +β€’ **Multi-Workspace Collaboration** – share a hash URL and others join the _same_ WS rooms instantly. β€’ **Drag-&-Drop Add-ons** – spawn robots, cameras or AI models from the toolbar. β€’ **Transport-Agnostic** – control physical hardware over USB, or send/receive via WebRTC rooms. β€’ **Model Agnostic** – any policy exposed by the Inference Server can be used (ACT, Diffusion, …). -β€’ **Reactive Core** – built with *Svelte 5 runes* – state is automatically pushed into the UI. +β€’ **Reactive Core** – built with _Svelte 5 runes_ – state is automatically pushed into the UI. --- ## πŸ“‚ Repository Layout (short) -| Path | Purpose | -|-------------------------------|---------| -| `src/` | SvelteKit app (routes, components) | -| `src/lib/elements` | Runtime domain logic (robots, video, compute) | -| `external/RobotHub-*` | Git sub-modules for the backend services – used for generated clients & tests | -| `static/` | URDFs, STL meshes, textures, favicon | +| Path | Purpose | +| --------------------- | ----------------------------------------------------------------------------- | +| `src/` | SvelteKit app (routes, components) | +| `src/lib/elements` | Runtime domain logic (robots, video, compute) | +| `external/RobotHub-*` | Git sub-modules for the backend services – used for generated clients & tests | +| `static/` | URDFs, STL meshes, textures, favicon | A more in-depth component overview can be found in `/src/lib/components/**` – every major popup/modal has its own Svelte file. @@ -96,34 +96,34 @@ $ python launch_simple.py # β†’ http://localhost:8001 $ bun run dev -- --open # β†’ http://localhost:5173 (hash = workspace-id) ``` -The **workspace-id** in the URL hash ties all three services together. Share `http://localhost:5173/#` and a collaborator instantly joins the same set of rooms. +The **workspace-id** in the URL hash ties all three services together. Share `http://localhost:5173/#` and a collaborator instantly joins the same set of rooms. --- ## πŸ› οΈ Usage Walk-Through -1. **Open the web-app** β†’ a fresh *workspace* is created (☝ left corner shows 🌐 ID). -2. Click *Add Robot* β†’ spawns an SO-100 6-DoF arm (URDF). -3. Click *Add Sensor β†’ Camera* β†’ creates a virtual camera element. -4. Click *Add Model β†’ ACT* β†’ spawns a *Compute* block. -5. On the Compute block choose *Create Session* – select model path (`LaetusH/act_so101_beyond`) and cameras (`front`). +1. **Open the web-app** β†’ a fresh _workspace_ is created (☝ left corner shows 🌐 ID). +2. Click _Add Robot_ β†’ spawns an SO-100 6-DoF arm (URDF). +3. Click _Add Sensor β†’ Camera_ β†’ creates a virtual camera element. +4. Click _Add Model β†’ ACT_ β†’ spawns a _Compute_ block. +5. On the Compute block choose _Create Session_ – select model path (`LaetusH/act_so101_beyond`) and cameras (`front`). 6. Connect: - β€’ *Video Input* – local webcam β†’ `front` room. - β€’ *Robot Input* – robot β†’ *joint-input* room (producer). - β€’ *Robot Output* – robot ← AI predictions (consumer). -7. Press *Start Inference* – the model will predict the next joint trajectory every few frames. πŸŽ‰ + β€’ _Video Input_ – local webcam β†’ `front` room. + β€’ _Robot Input_ – robot β†’ _joint-input_ room (producer). + β€’ _Robot Output_ – robot ← AI predictions (consumer). +7. Press _Start Inference_ – the model will predict the next joint trajectory every few frames. πŸŽ‰ -All modals (`AISessionConnectionModal`, `RobotInputConnectionModal`, …) expose precisely what is happening under the hood: which room ID, whether you are *producer* or *consumer*, and the live status. +All modals (`AISessionConnectionModal`, `RobotInputConnectionModal`, …) expose precisely what is happening under the hood: which room ID, whether you are _producer_ or _consumer_, and the live status. --- ## 🧩 Package Relations -| Package | Role | Artifacts exposed to this repo | -|---------|------|--------------------------------| -| **Transport Server** | Low-latency switch-board (WS/WebRTC). Creates *rooms* for video & joint messages. | TypeScript & Python client libraries (imported from sub-module) | -| **Inference Server** | Loads checkpoints (ACT, Pi-0, …) and manages *sessions*. Each session automatically asks the Transport Server to create dedicated rooms. | Generated TS SDK (`@robothub/inference-server-client`) – auto-called from `RemoteComputeManager` | -| **Frontend (this repo)** | UI + 3-D scene. Manages *robots*, *videos* & *compute* blocks and connects them to the correct rooms. | – | +| Package | Role | Artifacts exposed to this repo | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | +| **Transport Server** | Low-latency switch-board (WS/WebRTC). Creates _rooms_ for video & joint messages. | TypeScript & Python client libraries (imported from sub-module) | +| **Inference Server** | Loads checkpoints (ACT, Pi-0, …) and manages _sessions_. Each session automatically asks the Transport Server to create dedicated rooms. | Generated TS SDK (`@robothub/inference-server-client`) – auto-called from `RemoteComputeManager` | +| **Frontend (this repo)** | UI + 3-D scene. Manages _robots_, _videos_ & _compute_ blocks and connects them to the correct rooms. | – | > Because the two backend repos are included as git sub-modules you can develop & debug the whole trio in one repo clone. @@ -135,7 +135,7 @@ All modals (`AISessionConnectionModal`, `RobotInputConnectionModal`, …) expose β€’ `RobotManager` – talks to Transport Server and USB drivers. β€’ `VideoManager` – handles local/remote camera streams and WebRTC. -Each element is a small class with `$state` fields which Svelte 5 picks up automatically. The modals listed below are *thin* UI shells around those classes: +Each element is a small class with `$state` fields which Svelte 5 picks up automatically. The modals listed below are _thin_ UI shells around those classes: ``` AISessionConnectionModal – create / start / stop AI sessions @@ -163,9 +163,9 @@ See `Dockerfile` for the full build – it also performs `bun test` & `bun run b ## πŸ§‘β€πŸ’» Contributing -PRs are welcome! The codebase is organised into **domain managers** (robot / video / compute) and **pure-UI** components. If you add a new feature, create a manager first so that business logic can be unit-tested without DOM. +PRs are welcome! The codebase is organised into **domain managers** (robot / video / compute) and **pure-UI** components. If you add a new feature, create a manager first so that business logic can be unit-tested without DOM. -1. `bun test` – unit tests. +1. `bun test` – unit tests. 2. `bun run typecheck` – strict TS config. Please run `bun format` before committing – ESLint + Prettier configs are included. @@ -176,7 +176,8 @@ Please run `bun format` before committing – ESLint + Prettier configs are incl Huge gratitude to [Tim Qian](https://github.com/timqian) ([X/Twitter](https://x.com/tim_qian)) and the [bambot project](https://bambot.org/) for open-sourcing **feetech.js** – the -delightful js driver that powers our USB communication layer. +delightful js driver that powers our USB communication layer. + --- ## πŸ“„ License @@ -187,34 +188,34 @@ MIT – see `LICENSE` in the root. RobotHub follows a **separation-of-concerns** design: -* **Transport Server** is the single source of truth for *real-time* data – video frames, joint values, heart-beats. Every participant (browser, Python script, robot firmware) only needs one WebSocket/WebRTC connection, no matter how many peers join later. -* **Inference Server** is stateless with regard to connectivity; it spins up / tears down *sessions* that rely on rooms in the Transport Server. This lets heavy AI models live on a GPU box while cameras and robots stay on the edge. -* **Frontend** stays 100 % in the browser – no secret keys or device drivers required – and simply wires together rooms that already exist. +- **Transport Server** is the single source of truth for _real-time_ data – video frames, joint values, heart-beats. Every participant (browser, Python script, robot firmware) only needs one WebSocket/WebRTC connection, no matter how many peers join later. +- **Inference Server** is stateless with regard to connectivity; it spins up / tears down _sessions_ that rely on rooms in the Transport Server. This lets heavy AI models live on a GPU box while cameras and robots stay on the edge. +- **Frontend** stays 100 % in the browser – no secret keys or device drivers required – and simply wires together rooms that already exist. > By decoupling the pipeline we can deploy each piece on separate hardware or even different clouds, swap alternative implementations (e.g. ROS bridge instead of WebRTC) and scale each micro-service independently. --- -## πŸ›° Transport Server – Real-Time Router +## πŸ›° Transport Server – Real-Time Router ``` Browser / Robot ⟷ 🌐 Transport Server ⟷ Other Browser / AI / HW ``` -* **Creates rooms** – `POST /robotics/workspaces/{ws}/rooms` or `POST /video/workspaces/{ws}/rooms`. -* **Manages roles** – every WebSocket identifies as *producer* (source) or *consumer* (sink). -* **Does zero processing** – it only forwards JSON (robotics) or WebRTC SDP/ICE (video). -* **Health-check** – `GET /api/health` returns a JSON heartbeat. +- **Creates rooms** – `POST /robotics/workspaces/{ws}/rooms` or `POST /video/workspaces/{ws}/rooms`. +- **Manages roles** – every WebSocket identifies as _producer_ (source) or _consumer_ (sink). +- **Does zero processing** – it only forwards JSON (robotics) or WebRTC SDP/ICE (video). +- **Health-check** – `GET /api/health` returns a JSON heartbeat. Why useful? -* You never expose robot hardware directly to the internet – it only speaks to the Transport Server. -* Multiple followers can subscribe to the *same* producer without extra bandwidth on the producer side (server fans out messages). -* Works across NAT thanks to WebRTC TURN support. +- You never expose robot hardware directly to the internet – it only speaks to the Transport Server. +- Multiple followers can subscribe to the _same_ producer without extra bandwidth on the producer side (server fans out messages). +- Works across NAT thanks to WebRTC TURN support. -## 🏒 Workspaces – Lightweight Multi-Tenant Isolation +## 🏒 Workspaces – Lightweight Multi-Tenant Isolation -A **workspace** is simply a UUID namespace in the Transport Server. Every room URL starts with: +A **workspace** is simply a UUID namespace in the Transport Server. Every room URL starts with: ``` /robotics/workspaces/{workspace_id}/rooms/{room_id} @@ -223,105 +224,110 @@ A **workspace** is simply a UUID namespace in the Transport Server. Every room Why bother? -1. **Privacy / Security** – clients in workspace *A* can neither list nor join rooms from workspace *B*. A workspace id is like a private password that keeps the rooms in the same workspace isolated from each other. +1. **Privacy / Security** – clients in workspace _A_ can neither list nor join rooms from workspace _B_. A workspace id is like a private password that keeps the rooms in the same workspace isolated from each other. 2. **Organisation** – keep each class, project or experiment separated without spinning up extra servers. -3. **Zero-config sharing** – the Frontend stores the workspace ID in the URL hash (e.g. `/#d742e85d-c9e9-4f7b-…`). Send that link to a teammate and they automatically connect to the *same* namespace – all existing video feeds, robot rooms and AI sessions become visible. +3. **Zero-config sharing** – the Frontend stores the workspace ID in the URL hash (e.g. `/#d742e85d-c9e9-4f7b-…`). Send that link to a teammate and they automatically connect to the _same_ namespace – all existing video feeds, robot rooms and AI sessions become visible. 4. **Stateless Scale-out** – Transport Server holds no global state; deleting a workspace removes all rooms in one call. Typical lifecycle: -* **Create** – Frontend generates `crypto.randomUUID()` if the hash is empty. Back-end rooms are lazily created when the first producer/consumer calls the REST API. -* **Share** – click the *#workspace* badge β†’ *Copy URL* (handled by `WorkspaceIdButton.svelte`) +- **Create** – Frontend generates `crypto.randomUUID()` if the hash is empty. Back-end rooms are lazily created when the first producer/consumer calls the REST API. +- **Share** – click the _#workspace_ badge β†’ _Copy URL_ (handled by `WorkspaceIdButton.svelte`) > Practical tip: Use one workspace per demo to prevent collisions, then recycle it afterwards. --- -## 🧠 Inference Server – Session Lifecycle +## 🧠 Inference Server – Session Lifecycle 1. **Create session** `POST /api/sessions` with JSON: ```jsonc { - "session_id": "pick_place_demo", - "policy_path": "LaetusH/act_so101_beyond", - "camera_names": ["front", "wrist"], - "transport_server_url": "http://localhost:8000", - "workspace_id": "" // optional + "session_id": "pick_place_demo", + "policy_path": "LaetusH/act_so101_beyond", + "camera_names": ["front", "wrist"], + "transport_server_url": "http://localhost:8000", + "workspace_id": "" // optional } ``` -2. **Receive response** +2. **Receive response** ```jsonc { - "workspace_id": "ws-uuid", - "camera_room_ids": { "front": "room-id-a", "wrist": "room-id-b" }, - "joint_input_room_id": "room-id-c", - "joint_output_room_id": "room-id-d" + "workspace_id": "ws-uuid", + "camera_room_ids": { "front": "room-id-a", "wrist": "room-id-b" }, + "joint_input_room_id": "room-id-c", + "joint_output_room_id": "room-id-d" } ``` 3. **Wire connections** - * Camera PC joins `front` / `wrist` rooms as **producer** (WebRTC). - * Robot joins `joint_input_room_id` as **producer** (joint states). - * Robot (or simulator) joins `joint_output_room_id` as **consumer** (commands). + - Camera PC joins `front` / `wrist` rooms as **producer** (WebRTC). + - Robot joins `joint_input_room_id` as **producer** (joint states). + - Robot (or simulator) joins `joint_output_room_id` as **consumer** (commands). 4. **Start inference** `POST /api/sessions/{id}/start` – server loads the model and begins publishing commands. -5. **Stop / delete** as needed. Stats & health are available via `GET /api/sessions`. +5. **Stop / delete** as needed. Stats & health are available via `GET /api/sessions`. -The Frontend automates steps 1-4 via the *AI Session* modal – you only click buttons. +The Frontend automates steps 1-4 via the _AI Session_ modal – you only click buttons. --- ## 🌐 Hosted Demo End-Points -| Service | URL | Status | -|---------|-----|--------| -| Transport Server | | Public & healthy | -| Inference Server | | `{"status":"healthy"}` | -| Frontend (read-only preview) | | latest `main` | +| Service | URL | Status | +| ---------------------------- | -------------------------------------------------------- | ---------------------- | +| Transport Server | | Public & healthy | +| Inference Server | | `{"status":"healthy"}` | +| Frontend (read-only preview) | | latest `main` | -Point the *Settings β†’ Server Configuration* panel to these URLs and you can play without any local backend. +Point the _Settings β†’ Server Configuration_ panel to these URLs and you can play without any local backend. --- ## 🎯 Main Use-Cases -Below are typical connection patterns you can set-up **entirely from the UI**. Each example lists the raw data-flow (β†’ = producer to consumer/AI) plus a video placeholder you can swap for a screen-capture. +Below are typical connection patterns you can set-up **entirely from the UI**. Each example lists the raw data-flow (β†’ = producer to consumer/AI) plus a video placeholder you can swap for a screen-capture. ### Direct Tele-Operation (Leader ➜ Follower) -*Leader PC* `USB` ➜ **Robot A** ➜ `Remote producer` β†’ **Transport room** β†’ `Remote consumer` ➜ **Robot B** (`USB`) + +_Leader PC_ `USB` ➜ **Robot A** ➜ `Remote producer` β†’ **Transport room** β†’ `Remote consumer` ➜ **Robot B** (`USB`) > One human moves Robot A, Robot B mirrors the motion in real-time. Works with any number of followers – just add more consumers to the same room. > -> πŸ“Ί *demo-teleop-1.mp4* +> πŸ“Ί _demo-teleop-1.mp4_ ### Web-UI Manual Control + **Browser sliders** (`ManualControlSheet`) β†’ `Remote producer` β†’ **Robot (USB)** > No physical master arm needed – drive joints from any device. > -> πŸ“Ί *demo-webui.mp4* +> πŸ“Ί _demo-webui.mp4_ ### AI Inference Loop + **Robot (USB)** ➜ `Remote producer` β†’ **joint-input room** **Camera PC** ➜ `Video producer` β†’ **camera room(s)** **Inference Server** (consumer) β†’ processes β†’ publishes to **joint-output room** β†’ `Remote consumer` ➜ **Robot** > Lets a low-power robot PC stream data while a beefy GPU node does the heavy lifting. > -> πŸ“Ί *demo-inference.mp4* +> πŸ“Ί _demo-inference.mp4_ ### Hybrid Classroom (Multi-Follower AI) -*Same as AI Inference Loop* with additional **Robot C, D…** subscribing to `joint_output_room_id` to run the same policy in parallel. + +_Same as AI Inference Loop_ with additional **Robot C, D…** subscribing to `joint_output_room_id` to run the same policy in parallel. > Useful for swarm behaviours or classroom demonstrations. > -> πŸ“Ί *demo-classroom.mp4* +> πŸ“Ί _demo-classroom.mp4_ ### Split Video / Robot Across Machines + **Laptop A** (near cameras) β†’ streams video β†’ Transport -**Laptop B** (near robot) β†’ joins joint rooms -**Browser** anywhere β†’ watches video consumer & sends manual overrides +**Laptop B** (near robot) β†’ joins joint rooms +**Browser** anywhere β†’ watches video consumer & sends manual overrides > Ideal when the camera PC stays close to sensors and you want minimal upstream bandwidth. > -> πŸ“Ί *demo-splitio.mp4* +> πŸ“Ί _demo-splitio.mp4_ diff --git a/src/app.css b/src/app.css index d64750f3e26eaf5f0b5c586a7185b0d30ca20670..d6d39ffe0cf34352bf3ecba4e97dd3b789f076d7 100644 --- a/src/app.css +++ b/src/app.css @@ -6,117 +6,117 @@ @custom-variant dark (&:is(.dark *)); :root { - --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.147 0.004 49.25); - --card: oklch(1 0 0); - --card-foreground: oklch(0.147 0.004 49.25); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.147 0.004 49.25); - --primary: oklch(0.216 0.006 56.043); - --primary-foreground: oklch(0.985 0.001 106.423); - --secondary: oklch(0.97 0.001 106.424); - --secondary-foreground: oklch(0.216 0.006 56.043); - --muted: oklch(0.97 0.001 106.424); - --muted-foreground: oklch(0.553 0.013 58.071); - --accent: oklch(0.97 0.001 106.424); - --accent-foreground: oklch(0.216 0.006 56.043); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.923 0.003 48.717); - --input: oklch(0.923 0.003 48.717); - --ring: oklch(0.709 0.01 56.259); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.985 0.001 106.423); - --sidebar-foreground: oklch(0.147 0.004 49.25); - --sidebar-primary: oklch(0.216 0.006 56.043); - --sidebar-primary-foreground: oklch(0.985 0.001 106.423); - --sidebar-accent: oklch(0.97 0.001 106.424); - --sidebar-accent-foreground: oklch(0.216 0.006 56.043); - --sidebar-border: oklch(0.923 0.003 48.717); - --sidebar-ring: oklch(0.709 0.01 56.259); + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.147 0.004 49.25); + --card: oklch(1 0 0); + --card-foreground: oklch(0.147 0.004 49.25); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.147 0.004 49.25); + --primary: oklch(0.216 0.006 56.043); + --primary-foreground: oklch(0.985 0.001 106.423); + --secondary: oklch(0.97 0.001 106.424); + --secondary-foreground: oklch(0.216 0.006 56.043); + --muted: oklch(0.97 0.001 106.424); + --muted-foreground: oklch(0.553 0.013 58.071); + --accent: oklch(0.97 0.001 106.424); + --accent-foreground: oklch(0.216 0.006 56.043); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.923 0.003 48.717); + --input: oklch(0.923 0.003 48.717); + --ring: oklch(0.709 0.01 56.259); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0.001 106.423); + --sidebar-foreground: oklch(0.147 0.004 49.25); + --sidebar-primary: oklch(0.216 0.006 56.043); + --sidebar-primary-foreground: oklch(0.985 0.001 106.423); + --sidebar-accent: oklch(0.97 0.001 106.424); + --sidebar-accent-foreground: oklch(0.216 0.006 56.043); + --sidebar-border: oklch(0.923 0.003 48.717); + --sidebar-ring: oklch(0.709 0.01 56.259); } .dark { - --background: oklch(0.147 0.004 49.25); - --foreground: oklch(0.985 0.001 106.423); - --card: oklch(0.216 0.006 56.043); - --card-foreground: oklch(0.985 0.001 106.423); - --popover: oklch(0.216 0.006 56.043); - --popover-foreground: oklch(0.985 0.001 106.423); - --primary: oklch(0.923 0.003 48.717); - --primary-foreground: oklch(0.216 0.006 56.043); - --secondary: oklch(0.268 0.007 34.298); - --secondary-foreground: oklch(0.985 0.001 106.423); - --muted: oklch(0.268 0.007 34.298); - --muted-foreground: oklch(0.709 0.01 56.259); - --accent: oklch(0.268 0.007 34.298); - --accent-foreground: oklch(0.985 0.001 106.423); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.553 0.013 58.071); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.216 0.006 56.043); - --sidebar-foreground: oklch(0.985 0.001 106.423); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0.001 106.423); - --sidebar-accent: oklch(0.268 0.007 34.298); - --sidebar-accent-foreground: oklch(0.985 0.001 106.423); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.553 0.013 58.071); + --background: oklch(0.147 0.004 49.25); + --foreground: oklch(0.985 0.001 106.423); + --card: oklch(0.216 0.006 56.043); + --card-foreground: oklch(0.985 0.001 106.423); + --popover: oklch(0.216 0.006 56.043); + --popover-foreground: oklch(0.985 0.001 106.423); + --primary: oklch(0.923 0.003 48.717); + --primary-foreground: oklch(0.216 0.006 56.043); + --secondary: oklch(0.268 0.007 34.298); + --secondary-foreground: oklch(0.985 0.001 106.423); + --muted: oklch(0.268 0.007 34.298); + --muted-foreground: oklch(0.709 0.01 56.259); + --accent: oklch(0.268 0.007 34.298); + --accent-foreground: oklch(0.985 0.001 106.423); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.553 0.013 58.071); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.216 0.006 56.043); + --sidebar-foreground: oklch(0.985 0.001 106.423); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0.001 106.423); + --sidebar-accent: oklch(0.268 0.007 34.298); + --sidebar-accent-foreground: oklch(0.985 0.001 106.423); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.553 0.013 58.071); } @theme inline { - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); } @layer base { - * { - @apply border-border outline-ring/50; - } - body { - @apply bg-background text-foreground; - } -} \ No newline at end of file + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/src/app.d.ts b/src/app.d.ts index 955d6f90b8308833a905b67f5a33e808a587fa1c..f0a0bec7ddca352a4d107df799b95c5e0615e00c 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,4 +1,4 @@ -import type { InteractivityProps } from '@threlte/extras' +import type { InteractivityProps } from "@threlte/extras"; // See https://svelte.dev/docs/kit/types#app.d.ts // for information about these interfaces diff --git a/src/lib/components/3d/elements/compute/ComputeGridItem.svelte b/src/lib/components/3d/elements/compute/ComputeGridItem.svelte index 486e36a95636fedbb9844a7fb4d47e9f39645bc9..2fa9e1c2ba2cdf74849680bd50e5b8779ffd78a5 100644 --- a/src/lib/components/3d/elements/compute/ComputeGridItem.svelte +++ b/src/lib/components/3d/elements/compute/ComputeGridItem.svelte @@ -23,7 +23,6 @@ event.stopPropagation(); isToggled = !isToggled; } - - + - + - + - + {/if} - \ No newline at end of file + diff --git a/src/lib/components/3d/elements/compute/GPU.svelte b/src/lib/components/3d/elements/compute/GPU.svelte index 413cca3dc716cb26c8a4e9ca99d1df4051265fb3..c93b214ba60616138f998bc370b0ce0c4fa70f72 100644 --- a/src/lib/components/3d/elements/compute/GPU.svelte +++ b/src/lib/components/3d/elements/compute/GPU.svelte @@ -15,7 +15,12 @@ } // Props with defaults - let { position = [0, 0, 0], rotation = [0, 0, 0], scale = [1, 1, 1], rotating = false }: Props = $props(); + let { + position = [0, 0, 0], + rotation = [0, 0, 0], + scale = [1, 1, 1], + rotating = false + }: Props = $props(); // Create the TV frame geometry (outer rounded rectangle) function createTVFrame( @@ -87,7 +92,7 @@ let fan_rotation = $state(0); let rotationPerSeconds = $state(1); // 1 rotation per second by default - + onMount(() => { const interval = setInterval(() => { // Calculate angle increment per frame for desired rotations per second @@ -95,24 +100,16 @@ const angleIncrement = (Math.PI * 2 * rotationPerSeconds) / 60; fan_rotation = fan_rotation + angleIncrement; } - }, 1000/60); // Run at ~60fps + }, 1000 / 60); // Run at ~60fps return () => { clearInterval(interval); }; }); - - - - - + + + diff --git a/src/lib/components/3d/elements/compute/GPUModel.svelte b/src/lib/components/3d/elements/compute/GPUModel.svelte index 1a05fe6f4efcdb3079ac1e49bdb5bd12ca7e1a5f..736f8254241704fff4e0bba60273edde74582ff5 100644 --- a/src/lib/components/3d/elements/compute/GPUModel.svelte +++ b/src/lib/components/3d/elements/compute/GPUModel.svelte @@ -8,193 +8,178 @@ Title: Nvidia GeForce RTX 3090 --> - - {#await gltf} - {@render fallback?.()} - {:then gltf} - - - - - - - - - - - - - - - - - - - - - - - - {:catch err} - {@render error?.({ error: err })} - {/await} + + {#await gltf} + {@render fallback?.()} + {:then gltf} + + + + + + + + + + + + + + + + + + + + + + + + {:catch err} + {@render error?.({ error: err })} + {/await} - {@render children?.({ ref })} + {@render children?.({ ref })} diff --git a/src/lib/components/3d/elements/compute/modal/AISessionConnectionModal.svelte b/src/lib/components/3d/elements/compute/modal/AISessionConnectionModal.svelte index 8356722f7d24f947bef642e7e61e115705e45c0d..acf87ccb2b85b0ec8b249708c52f9bd30fdb9924 100644 --- a/src/lib/components/3d/elements/compute/modal/AISessionConnectionModal.svelte +++ b/src/lib/components/3d/elements/compute/modal/AISessionConnectionModal.svelte @@ -380,9 +380,9 @@ placeholder="front, wrist, overhead" class="border-slate-300 bg-slate-50 text-slate-900 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-100" /> -

- Comma-separated camera names -

+

+ Comma-separated camera names +

{#if modelConfig.requiresLanguageInstruction} @@ -403,7 +403,9 @@ {/if} -
+
@@ -420,9 +422,9 @@ >
- Tip: This will create a new {modelConfig.label} inference session with dedicated rooms for camera - inputs, joint inputs, and joint outputs in the inference server communication - system. + Tip: This will create a new {modelConfig.label} inference session + with dedicated rooms for camera inputs, joint inputs, and joint outputs in the inference + server communication system.
@@ -450,8 +452,8 @@ class="rounded border border-slate-300 bg-slate-100/30 p-2 text-xs text-slate-600 dark:border-slate-700 dark:bg-slate-800/30 dark:text-slate-500" > - Inference Sessions require a trained {modelConfig.label} and create dedicated communication rooms for video - inputs, robot joint states, and control outputs in the inference server system. + Inference Sessions require a trained {modelConfig.label} and create dedicated communication rooms + for video inputs, robot joint states, and control outputs in the inference server system.
diff --git a/src/lib/components/3d/elements/compute/status/ComputeBoxUIKit.svelte b/src/lib/components/3d/elements/compute/status/ComputeBoxUIKit.svelte index 744f08c090de0c20e2aefae591a65aeb8937560f..72f865c899fc39a271a815decef5f229d4729acf 100644 --- a/src/lib/components/3d/elements/compute/status/ComputeBoxUIKit.svelte +++ b/src/lib/components/3d/elements/compute/status/ComputeBoxUIKit.svelte @@ -1,11 +1,7 @@ import type { RemoteCompute } from "$lib/elements/compute//RemoteCompute.svelte"; import { ICON } from "$lib/utils/icon"; - import { BaseStatusBox, StatusHeader, StatusContent, StatusIndicator, StatusButton } from "$lib/components/3d/ui"; + import { + BaseStatusBox, + StatusHeader, + StatusContent, + StatusIndicator, + StatusButton + } from "$lib/components/3d/ui"; interface Props { compute: RemoteCompute; @@ -13,7 +19,7 @@ const outputColor = "rgb(59, 130, 246)"; - {#if compute.hasSession && compute.outputConnections} - - - - - {/if} - \ No newline at end of file + diff --git a/src/lib/components/3d/elements/compute/status/VideoInputBoxUIKit.svelte b/src/lib/components/3d/elements/compute/status/VideoInputBoxUIKit.svelte index ec840d5efed4da17ba93903bdbb70cde89f1de39..3c7ba0235be432b125f369aa3c28f984c24997d2 100644 --- a/src/lib/components/3d/elements/compute/status/VideoInputBoxUIKit.svelte +++ b/src/lib/components/3d/elements/compute/status/VideoInputBoxUIKit.svelte @@ -1,6 +1,12 @@ - {#if compute.hasSession && compute.inputConnections} - - {:else} - - + - {/if} - \ No newline at end of file + diff --git a/src/lib/components/3d/elements/robot/RobotGridItem.svelte b/src/lib/components/3d/elements/robot/RobotGridItem.svelte index 2e0ab48d8d299dd7a90bdbf585ef0333a7af4b9d..aaaa74816c5a5f8dec128a144ad84d7eba495f5b 100644 --- a/src/lib/components/3d/elements/robot/RobotGridItem.svelte +++ b/src/lib/components/3d/elements/robot/RobotGridItem.svelte @@ -106,7 +106,6 @@ } const { onPointerEnter, onPointerLeave, hovering } = useCursor(); - let isToggled = $state(false); @@ -124,13 +123,17 @@ scale={[10, 10, 10]} rotation={[-Math.PI / 2, 0, 0]} > - { - event.stopPropagation(); - onPointerEnter(); - }} onpointerleave={(event) => { - event.stopPropagation(); - onPointerLeave(); - }} onclick={handleClick}> + { + event.stopPropagation(); + onPointerEnter(); + }} + onpointerleave={(event) => { + event.stopPropagation(); + onPointerLeave(); + }} + onclick={handleClick} + > {#if urdfRobotState} {#each getRootLinks(urdfRobotState) as link} - + {/if} @@ -174,7 +168,7 @@ renderOrder={999} frustumCulled={false} > - + {room.id}

-
+
{room.has_producer ? "πŸ“€ Has Output" : "πŸ“₯ No Output"} πŸ‘₯ {room.participants?.total || 0} users
-
- + - - -
- {#if modalType === 'consumer'} - - - {:else if modalType === 'producer'} - - - {:else} -

Manual control interface would go here

- {/if} -
- -
- {#if modalType !== 'manual'} - Note: USB connections will prompt for calibration if needed - {/if} -
- - -{/if} \ No newline at end of file +
+
+
+

+ {modalType === "consumer" + ? "Consumer Driver" + : modalType === "producer" + ? "Producer Drivers" + : "Manual Control"} +

+ +
+ +
+ {#if modalType === "consumer"} + + + {:else if modalType === "producer"} + + + {:else} +

Manual control interface would go here

+ {/if} +
+ +
+ {#if modalType !== "manual"} + Note: USB connections will prompt for calibration if needed + {/if} +
+
+
+{/if} diff --git a/src/lib/elements/robot/components/RobotItem.svelte b/src/lib/elements/robot/components/RobotItem.svelte index 7e63bf65ea158bf629bba40fe0738bf4389453ab..57b55733dcd00a08ef2e91d302831f84df474893 100644 --- a/src/lib/elements/robot/components/RobotItem.svelte +++ b/src/lib/elements/robot/components/RobotItem.svelte @@ -1,205 +1,217 @@ - {#if urdfRobotState} - - ) => { - event.stopPropagation(); - isSelected = true; - onInteract(robot, 'manual'); - }} - onpointerenter={(event: IntersectionEvent) => { - event.stopPropagation(); - onPointerEnter(); - isHovered = true; - }} - onpointerleave={(event: IntersectionEvent) => { - event.stopPropagation(); - onPointerLeave(); - isHovered = false; - }} - > - {#each getRootLinks(urdfRobotState) as link} - - {/each} - - {:else} - - isHovered = true} - onpointerleave={() => isHovered = false} - onclick={() => onInteract(robot, 'manual')} - > - - - - {/if} - - - {#if isHovered} - - -
-
{robot.id}
- - -
- - - - -
-
Robot
-
{robot.jointArray.length} joints
-
- - - -
- - -
- {isManualControl ? 'Manual Control' : 'External Control'} -
-
-
-
- {/if} -
\ No newline at end of file + {#if urdfRobotState} + + ) => { + event.stopPropagation(); + isSelected = true; + onInteract(robot, "manual"); + }} + onpointerenter={(event: IntersectionEvent) => { + event.stopPropagation(); + onPointerEnter(); + isHovered = true; + }} + onpointerleave={(event: IntersectionEvent) => { + event.stopPropagation(); + onPointerLeave(); + isHovered = false; + }} + > + {#each getRootLinks(urdfRobotState) as link} + + {/each} + + {:else} + + (isHovered = true)} + onpointerleave={() => (isHovered = false)} + onclick={() => onInteract(robot, "manual")} + > + + + + {/if} + + + {#if isHovered} + + +
+
{robot.id}
+ + +
+ + + + +
+
Robot
+
{robot.jointArray.length} joints
+
+ + + +
+ + +
+ {isManualControl ? "Manual Control" : "External Control"} +
+
+
+
+ {/if} +
diff --git a/src/lib/elements/robot/components/index.ts b/src/lib/elements/robot/components/index.ts index 126e4b8007e234fd294d1d4ddc412b8ec1057670..f8aee16d6aac714bda310627c6ee5f6aafede529 100644 --- a/src/lib/elements/robot/components/index.ts +++ b/src/lib/elements/robot/components/index.ts @@ -1,5 +1,5 @@ // Robot components exports -export { default as RobotItem } from './RobotItem.svelte'; -export { default as RobotGrid } from './RobotGrid.svelte'; -export { default as ConnectionPanel } from './ConnectionPanel.svelte'; -export { default as RobotControls } from './RobotControls.svelte'; \ No newline at end of file +export { default as RobotItem } from "./RobotItem.svelte"; +export { default as RobotGrid } from "./RobotGrid.svelte"; +export { default as ConnectionPanel } from "./ConnectionPanel.svelte"; +export { default as RobotControls } from "./RobotControls.svelte"; diff --git a/src/lib/elements/robot/config.ts b/src/lib/elements/robot/config.ts index 4a0b623d377215d6b9653cc04eb626348ef20eae..725363a98412d98a7c6b3d1ebcd7cdcfe40f6c54 100644 --- a/src/lib/elements/robot/config.ts +++ b/src/lib/elements/robot/config.ts @@ -2,54 +2,54 @@ // Single source of truth for all timing and communication parameters export const ROBOT_CONFIG = { - // USB Communication Settings - usb: { - baudRate: 1000000, - servoWriteDelay: 8, // ms between servo writes (optimized from 10ms) - maxRetries: 3, // max retry attempts for failed operations - retryDelay: 100, // ms between retries - connectionTimeout: 5000, // ms for connection timeout - readTimeout: 200, // ms for individual servo reads - }, + // USB Communication Settings + usb: { + baudRate: 1000000, + servoWriteDelay: 8, // ms between servo writes (optimized from 10ms) + maxRetries: 3, // max retry attempts for failed operations + retryDelay: 100, // ms between retries + connectionTimeout: 5000, // ms for connection timeout + readTimeout: 200 // ms for individual servo reads + }, - // Polling & Update Frequencies - polling: { - uiUpdateRate: 100, // ms (10Hz) - UI state updates - consumerPollingRate: 40, // ms (25Hz) - USB consumer polling (optimized from 50ms) - calibrationPollingRate: 16, // ms (60Hz) - calibration polling (needs to be fast) - errorBackoffRate: 200, // ms - delay after polling errors - maxPollingErrors: 5, // max consecutive errors before longer backoff - }, + // Polling & Update Frequencies + polling: { + uiUpdateRate: 100, // ms (10Hz) - UI state updates + consumerPollingRate: 40, // ms (25Hz) - USB consumer polling (optimized from 50ms) + calibrationPollingRate: 16, // ms (60Hz) - calibration polling (needs to be fast) + errorBackoffRate: 200, // ms - delay after polling errors + maxPollingErrors: 5 // max consecutive errors before longer backoff + }, - // Command Processing - commands: { - dedupWindow: 16, // ms - skip duplicate commands within this window - maxQueueSize: 50, // max pending commands before dropping old ones - batchSize: 6, // max servos to process in parallel batches - }, + // Command Processing + commands: { + dedupWindow: 16, // ms - skip duplicate commands within this window + maxQueueSize: 50, // max pending commands before dropping old ones + batchSize: 6 // max servos to process in parallel batches + }, - // Remote Connection Settings - remote: { - reconnectDelay: 2000, // ms between reconnection attempts - heartbeatInterval: 30000, // ms for connection health check - messageTimeout: 5000, // ms for message response timeout - }, + // Remote Connection Settings + remote: { + reconnectDelay: 2000, // ms between reconnection attempts + heartbeatInterval: 30000, // ms for connection health check + messageTimeout: 5000 // ms for message response timeout + }, - // Calibration Settings - calibration: { - minRangeThreshold: 500, // minimum servo range for valid calibration - progressUpdateRate: 100, // ms between progress updates - finalPositionTimeout: 2000, // ms timeout for reading final positions - }, + // Calibration Settings + calibration: { + minRangeThreshold: 500, // minimum servo range for valid calibration + progressUpdateRate: 100, // ms between progress updates + finalPositionTimeout: 2000 // ms timeout for reading final positions + }, - // Performance Tuning - performance: { - jointUpdateThreshold: 0.5, // min change to trigger joint update - uiUpdateThreshold: 0.1, // min change to trigger UI update - maxConcurrentReads: 3, // max concurrent servo reads - memoryCleanupInterval: 30000, // ms - periodic cleanup interval - } + // Performance Tuning + performance: { + jointUpdateThreshold: 0.5, // min change to trigger joint update + uiUpdateThreshold: 0.1, // min change to trigger UI update + maxConcurrentReads: 3, // max concurrent servo reads + memoryCleanupInterval: 30000 // ms - periodic cleanup interval + } } as const; // Type exports for better IntelliSense -export type RobotConfig = typeof ROBOT_CONFIG; \ No newline at end of file +export type RobotConfig = typeof ROBOT_CONFIG; diff --git a/src/lib/elements/robot/createRobot.svelte.ts b/src/lib/elements/robot/createRobot.svelte.ts index a1d26f3322c1725d21afd49bd82adeb79548a13c..0134f943f63307d918d4f7c55150934394130326 100644 --- a/src/lib/elements/robot/createRobot.svelte.ts +++ b/src/lib/elements/robot/createRobot.svelte.ts @@ -6,10 +6,10 @@ export async function createUrdfRobot(urdfConfig: RobotUrdfConfig): Promise void)[] = []; - private commandCallbacks: ((command: RobotCommand) => void)[] = []; - - private consumer: robotics.RoboticsConsumer | null = null; - private client: robotics.RoboticsClientCore | null = null; - private workspaceId: string | null = null; - - constructor(config: RemoteDriverConfig) { - this.config = config; - this.id = `remote-consumer-${config.robotId}-${Date.now()}`; - this.name = `Remote Consumer (${config.robotId})`; - } - - get status(): ConnectionStatus { - return this._status; - } - - async connect(joinExistingRoom = false): Promise { - try { - const serverUrl = this.config.url.replace(/^ws/, "http"); - console.log(`[RemoteConsumer] Connecting to ${serverUrl} for robot ${this.config.robotId} (join mode: ${joinExistingRoom})`); - - // Create core client for room management - this.client = new robotics.RoboticsClientCore(serverUrl); - - // Create consumer to receive commands - this.consumer = new robotics.RoboticsConsumer(serverUrl); - - // Set up event handlers - this.consumer.onConnected(() => { - console.log(`[RemoteConsumer] Connected to room ${this.config.robotId}`); - }); - - this.consumer.onDisconnected(() => { - console.log(`[RemoteConsumer] Disconnected from room ${this.config.robotId}`); - }); - - this.consumer.onError((error: string) => { - console.error(`[RemoteConsumer] Error:`, error); - this._status = { isConnected: false, error: `Consumer error: ${error}` }; - this.notifyStatusChange(); - }); - - // RECEIVE joint updates and forward as normalized commands - this.consumer.onJointUpdate((joints: JointData[]) => { - console.debug(`[RemoteConsumer] Received joint update:`, joints); - - const command: RobotCommand = { - timestamp: Date.now(), - joints: joints.map((joint: JointData) => ({ - name: joint.name, - value: joint.value, // Already normalized from server - })) - }; - this.notifyCommand(command); - }); - - // RECEIVE state sync - this.consumer.onStateSync((state: Record) => { - console.debug(`[RemoteConsumer] Received state sync:`, state); - - const joints = Object.entries(state).map(([name, value]) => ({ - name, - value: value as number - })); - - if (joints.length > 0) { - const command: RobotCommand = { - timestamp: Date.now(), - joints - }; - this.notifyCommand(command); - } - }); - - // Use workspace ID from config or default - this.workspaceId = this.config.workspaceId || 'default-workspace'; - - let roomData; - if (joinExistingRoom) { - // Join existing room (for Inference Session integration) - roomData = { workspaceId: this.workspaceId, roomId: this.config.robotId }; - console.log(`[RemoteConsumer] Joining existing room ${this.config.robotId} in workspace ${this.workspaceId}`); - } else { - // Create new room (for standalone operation) - roomData = await this.client.createRoom(this.workspaceId, this.config.robotId); - console.log(`[RemoteConsumer] Created new room ${roomData.roomId} in workspace ${roomData.workspaceId}`); - } - - const success = await this.consumer.connect(roomData.workspaceId, roomData.roomId, this.id); - - if (!success) { - throw new Error("Failed to connect consumer to room"); - } - - this._status = { isConnected: true, lastConnected: new Date() }; - this.notifyStatusChange(); - - console.log(`[RemoteConsumer] Connected successfully to workspace ${roomData.workspaceId}, room ${roomData.roomId}`); - } catch (error) { - console.error(`[RemoteConsumer] Connection failed:`, error); - this._status = { isConnected: false, error: `Connection failed: ${error}` }; - this.notifyStatusChange(); - throw error; - } - } - - async disconnect(): Promise { - console.log(`[RemoteConsumer] Disconnecting...`); - - if (this.consumer) { - await this.consumer.disconnect(); - this.consumer = null; - } - if (this.client) { - // Client doesn't need explicit disconnect - this.client = null; - } - - this.workspaceId = null; - this._status = { isConnected: false }; - this.notifyStatusChange(); - - console.log(`[RemoteConsumer] Disconnected`); - } - - // Event handlers - onStatusChange(callback: (status: ConnectionStatus) => void): () => void { - this.statusCallbacks.push(callback); - return () => { - const index = this.statusCallbacks.indexOf(callback); - if (index >= 0) { - this.statusCallbacks.splice(index, 1); - } - }; - } - - onCommand(callback: (command: RobotCommand) => void): () => void { - this.commandCallbacks.push(callback); - return () => { - const index = this.commandCallbacks.indexOf(callback); - if (index >= 0) { - this.commandCallbacks.splice(index, 1); - } - }; - } - - // Private methods - private notifyCommand(command: RobotCommand): void { - this.commandCallbacks.forEach(callback => { - try { - callback(command); - } catch (error) { - console.error('[RemoteConsumer] Error in command callback:', error); - } - }); - } - - private notifyStatusChange(): void { - this.statusCallbacks.forEach(callback => { - try { - callback(this._status); - } catch (error) { - console.error('[RemoteConsumer] Error in status callback:', error); - } - }); - } -} \ No newline at end of file + readonly id: string; + readonly name: string; + readonly config: RemoteDriverConfig; + + private _status: ConnectionStatus = { isConnected: false }; + private statusCallbacks: ((status: ConnectionStatus) => void)[] = []; + private commandCallbacks: ((command: RobotCommand) => void)[] = []; + + private consumer: robotics.RoboticsConsumer | null = null; + private client: robotics.RoboticsClientCore | null = null; + private workspaceId: string | null = null; + + constructor(config: RemoteDriverConfig) { + this.config = config; + this.id = `remote-consumer-${config.robotId}-${Date.now()}`; + this.name = `Remote Consumer (${config.robotId})`; + } + + get status(): ConnectionStatus { + return this._status; + } + + async connect(joinExistingRoom = false): Promise { + try { + const serverUrl = this.config.url.replace(/^ws/, "http"); + console.log( + `[RemoteConsumer] Connecting to ${serverUrl} for robot ${this.config.robotId} (join mode: ${joinExistingRoom})` + ); + + // Create core client for room management + this.client = new robotics.RoboticsClientCore(serverUrl); + + // Create consumer to receive commands + this.consumer = new robotics.RoboticsConsumer(serverUrl); + + // Set up event handlers + this.consumer.onConnected(() => { + console.log(`[RemoteConsumer] Connected to room ${this.config.robotId}`); + }); + + this.consumer.onDisconnected(() => { + console.log(`[RemoteConsumer] Disconnected from room ${this.config.robotId}`); + }); + + this.consumer.onError((error: string) => { + console.error(`[RemoteConsumer] Error:`, error); + this._status = { isConnected: false, error: `Consumer error: ${error}` }; + this.notifyStatusChange(); + }); + + // RECEIVE joint updates and forward as normalized commands + this.consumer.onJointUpdate((joints: JointData[]) => { + console.debug(`[RemoteConsumer] Received joint update:`, joints); + + const command: RobotCommand = { + timestamp: Date.now(), + joints: joints.map((joint: JointData) => ({ + name: joint.name, + value: joint.value // Already normalized from server + })) + }; + this.notifyCommand(command); + }); + + // RECEIVE state sync + this.consumer.onStateSync((state: Record) => { + console.debug(`[RemoteConsumer] Received state sync:`, state); + + const joints = Object.entries(state).map(([name, value]) => ({ + name, + value: value as number + })); + + if (joints.length > 0) { + const command: RobotCommand = { + timestamp: Date.now(), + joints + }; + this.notifyCommand(command); + } + }); + + // Use workspace ID from config or default + this.workspaceId = this.config.workspaceId || "default-workspace"; + + let roomData; + if (joinExistingRoom) { + // Join existing room (for Inference Session integration) + roomData = { workspaceId: this.workspaceId, roomId: this.config.robotId }; + console.log( + `[RemoteConsumer] Joining existing room ${this.config.robotId} in workspace ${this.workspaceId}` + ); + } else { + // Create new room (for standalone operation) + roomData = await this.client.createRoom(this.workspaceId, this.config.robotId); + console.log( + `[RemoteConsumer] Created new room ${roomData.roomId} in workspace ${roomData.workspaceId}` + ); + } + + const success = await this.consumer.connect(roomData.workspaceId, roomData.roomId, this.id); + + if (!success) { + throw new Error("Failed to connect consumer to room"); + } + + this._status = { isConnected: true, lastConnected: new Date() }; + this.notifyStatusChange(); + + console.log( + `[RemoteConsumer] Connected successfully to workspace ${roomData.workspaceId}, room ${roomData.roomId}` + ); + } catch (error) { + console.error(`[RemoteConsumer] Connection failed:`, error); + this._status = { isConnected: false, error: `Connection failed: ${error}` }; + this.notifyStatusChange(); + throw error; + } + } + + async disconnect(): Promise { + console.log(`[RemoteConsumer] Disconnecting...`); + + if (this.consumer) { + await this.consumer.disconnect(); + this.consumer = null; + } + if (this.client) { + // Client doesn't need explicit disconnect + this.client = null; + } + + this.workspaceId = null; + this._status = { isConnected: false }; + this.notifyStatusChange(); + + console.log(`[RemoteConsumer] Disconnected`); + } + + // Event handlers + onStatusChange(callback: (status: ConnectionStatus) => void): () => void { + this.statusCallbacks.push(callback); + return () => { + const index = this.statusCallbacks.indexOf(callback); + if (index >= 0) { + this.statusCallbacks.splice(index, 1); + } + }; + } + + onCommand(callback: (command: RobotCommand) => void): () => void { + this.commandCallbacks.push(callback); + return () => { + const index = this.commandCallbacks.indexOf(callback); + if (index >= 0) { + this.commandCallbacks.splice(index, 1); + } + }; + } + + // Private methods + private notifyCommand(command: RobotCommand): void { + this.commandCallbacks.forEach((callback) => { + try { + callback(command); + } catch (error) { + console.error("[RemoteConsumer] Error in command callback:", error); + } + }); + } + + private notifyStatusChange(): void { + this.statusCallbacks.forEach((callback) => { + try { + callback(this._status); + } catch (error) { + console.error("[RemoteConsumer] Error in status callback:", error); + } + }); + } +} diff --git a/src/lib/elements/robot/drivers/RemoteProducer.ts b/src/lib/elements/robot/drivers/RemoteProducer.ts index 48ab5426da9a24b3a0697da50a2ac8e11a7cd0b1..4738955a4b2e3192a29e920c527c171175660b8e 100644 --- a/src/lib/elements/robot/drivers/RemoteProducer.ts +++ b/src/lib/elements/robot/drivers/RemoteProducer.ts @@ -1,181 +1,193 @@ -import type { Producer, ConnectionStatus, RobotCommand, RemoteDriverConfig } from '../models.js'; +import type { Producer, ConnectionStatus, RobotCommand, RemoteDriverConfig } from "../models.js"; import { robotics } from "@robothub/transport-server-client"; import type { JointData } from "@robothub/transport-server-client/robotics"; export class RemoteProducer implements Producer { - readonly id: string; - readonly name: string; - readonly config: RemoteDriverConfig; - - private _status: ConnectionStatus = { isConnected: false }; - private statusCallbacks: ((status: ConnectionStatus) => void)[] = []; - - private producer: robotics.RoboticsProducer | null = null; - private client: robotics.RoboticsClientCore | null = null; - private workspaceId: string | null = null; - - // State update interval for producer mode - private stateUpdateInterval?: ReturnType; - private lastKnownState: Record = {}; - - constructor(config: RemoteDriverConfig) { - this.config = config; - this.id = `remote-producer-${config.robotId}-${Date.now()}`; - this.name = `Remote Producer (${config.robotId})`; - } - - get status(): ConnectionStatus { - return this._status; - } - - async connect(joinExistingRoom = false): Promise { - try { - const serverUrl = this.config.url.replace(/^ws/, "http"); - console.log(`[RemoteProducer] Connecting to ${serverUrl} for robot ${this.config.robotId} (join mode: ${joinExistingRoom})`); - - // Create core client for room management - this.client = new robotics.RoboticsClientCore(serverUrl); - - // Create producer to send commands - this.producer = new robotics.RoboticsProducer(serverUrl); - - // Set up event handlers - this.producer.onConnected(() => { - console.log(`[RemoteProducer] Connected to room ${this.config.robotId}`); - this.startStateUpdates(); - }); - - this.producer.onDisconnected(() => { - console.log(`[RemoteProducer] Disconnected from room ${this.config.robotId}`); - this.stopStateUpdates(); - }); - - this.producer.onError((error: string) => { - console.error(`[RemoteProducer] Error:`, error); - this._status = { isConnected: false, error: `Producer error: ${error}` }; - this.notifyStatusChange(); - this.stopStateUpdates(); - }); - - // Use workspace ID from config or default - this.workspaceId = this.config.workspaceId || 'default-workspace'; - - let roomData; - if (joinExistingRoom) { - // Join existing room (for Inference Session integration) - roomData = { workspaceId: this.workspaceId, roomId: this.config.robotId }; - console.log(`[RemoteProducer] Joining existing room ${this.config.robotId} in workspace ${this.workspaceId}`); - } else { - // Create new room (for standalone operation) - roomData = await this.client.createRoom(this.workspaceId, this.config.robotId); - console.log(`[RemoteProducer] Created new room ${roomData.roomId} in workspace ${roomData.workspaceId}`); - } - - const success = await this.producer.connect(roomData.workspaceId, roomData.roomId, this.id); - - if (!success) { - throw new Error("Failed to connect producer to room"); - } - - this._status = { isConnected: true, lastConnected: new Date() }; - this.notifyStatusChange(); - - console.log(`[RemoteProducer] Connected successfully to workspace ${roomData.workspaceId}, room ${roomData.roomId}`); - } catch (error) { - console.error(`[RemoteProducer] Connection failed:`, error); - this._status = { isConnected: false, error: `Connection failed: ${error}` }; - this.notifyStatusChange(); - throw error; - } - } - - async disconnect(): Promise { - console.log(`[RemoteProducer] Disconnecting...`); - - this.stopStateUpdates(); - - if (this.producer) { - await this.producer.disconnect(); - this.producer = null; - } - if (this.client) { - // Client doesn't need explicit disconnect - this.client = null; - } - - this.workspaceId = null; - this._status = { isConnected: false }; - this.notifyStatusChange(); - - console.log(`[RemoteProducer] Disconnected`); - } - - async sendCommand(command: RobotCommand): Promise { - if (!this._status.isConnected || !this.producer) { - throw new Error('Cannot send command: Remote producer not connected'); - } - - try { - console.debug(`[RemoteProducer] Sending command:`, command); - - // Update last known state for periodic updates - command.joints.forEach(joint => { - this.lastKnownState[joint.name] = joint.value; - }); - - // Send joint update with normalized values - const joints = command.joints.map(joint => ({ - name: joint.name, - value: joint.value // Already normalized - })); - - await this.producer.sendJointUpdate(joints); - console.debug(`[RemoteProducer] Sent joint update with ${joints.length} joints`); - } catch (error) { - console.error('[RemoteProducer] Failed to send command:', error); - throw error; - } - } - - // Event handlers - onStatusChange(callback: (status: ConnectionStatus) => void): () => void { - this.statusCallbacks.push(callback); - return () => { - const index = this.statusCallbacks.indexOf(callback); - if (index >= 0) { - this.statusCallbacks.splice(index, 1); - } - }; - } - - // Private methods - private startStateUpdates(): void { - // Send periodic state updates to keep remote server informed - this.stateUpdateInterval = setInterval(async () => { - if (this.producer && this._status.isConnected && Object.keys(this.lastKnownState).length > 0) { - try { - await this.producer.sendStateSync(this.lastKnownState); - } catch (error) { - console.error('[RemoteProducer] Failed to send state update:', error); - } - } - }, 100); // 10 Hz updates - } - - private stopStateUpdates(): void { - if (this.stateUpdateInterval) { - clearInterval(this.stateUpdateInterval); - this.stateUpdateInterval = undefined; - } - } - - private notifyStatusChange(): void { - this.statusCallbacks.forEach(callback => { - try { - callback(this._status); - } catch (error) { - console.error('[RemoteProducer] Error in status callback:', error); - } - }); - } -} \ No newline at end of file + readonly id: string; + readonly name: string; + readonly config: RemoteDriverConfig; + + private _status: ConnectionStatus = { isConnected: false }; + private statusCallbacks: ((status: ConnectionStatus) => void)[] = []; + + private producer: robotics.RoboticsProducer | null = null; + private client: robotics.RoboticsClientCore | null = null; + private workspaceId: string | null = null; + + // State update interval for producer mode + private stateUpdateInterval?: ReturnType; + private lastKnownState: Record = {}; + + constructor(config: RemoteDriverConfig) { + this.config = config; + this.id = `remote-producer-${config.robotId}-${Date.now()}`; + this.name = `Remote Producer (${config.robotId})`; + } + + get status(): ConnectionStatus { + return this._status; + } + + async connect(joinExistingRoom = false): Promise { + try { + const serverUrl = this.config.url.replace(/^ws/, "http"); + console.log( + `[RemoteProducer] Connecting to ${serverUrl} for robot ${this.config.robotId} (join mode: ${joinExistingRoom})` + ); + + // Create core client for room management + this.client = new robotics.RoboticsClientCore(serverUrl); + + // Create producer to send commands + this.producer = new robotics.RoboticsProducer(serverUrl); + + // Set up event handlers + this.producer.onConnected(() => { + console.log(`[RemoteProducer] Connected to room ${this.config.robotId}`); + this.startStateUpdates(); + }); + + this.producer.onDisconnected(() => { + console.log(`[RemoteProducer] Disconnected from room ${this.config.robotId}`); + this.stopStateUpdates(); + }); + + this.producer.onError((error: string) => { + console.error(`[RemoteProducer] Error:`, error); + this._status = { isConnected: false, error: `Producer error: ${error}` }; + this.notifyStatusChange(); + this.stopStateUpdates(); + }); + + // Use workspace ID from config or default + this.workspaceId = this.config.workspaceId || "default-workspace"; + + let roomData; + if (joinExistingRoom) { + // Join existing room (for Inference Session integration) + roomData = { workspaceId: this.workspaceId, roomId: this.config.robotId }; + console.log( + `[RemoteProducer] Joining existing room ${this.config.robotId} in workspace ${this.workspaceId}` + ); + } else { + // Create new room (for standalone operation) + roomData = await this.client.createRoom(this.workspaceId, this.config.robotId); + console.log( + `[RemoteProducer] Created new room ${roomData.roomId} in workspace ${roomData.workspaceId}` + ); + } + + const success = await this.producer.connect(roomData.workspaceId, roomData.roomId, this.id); + + if (!success) { + throw new Error("Failed to connect producer to room"); + } + + this._status = { isConnected: true, lastConnected: new Date() }; + this.notifyStatusChange(); + + console.log( + `[RemoteProducer] Connected successfully to workspace ${roomData.workspaceId}, room ${roomData.roomId}` + ); + } catch (error) { + console.error(`[RemoteProducer] Connection failed:`, error); + this._status = { isConnected: false, error: `Connection failed: ${error}` }; + this.notifyStatusChange(); + throw error; + } + } + + async disconnect(): Promise { + console.log(`[RemoteProducer] Disconnecting...`); + + this.stopStateUpdates(); + + if (this.producer) { + await this.producer.disconnect(); + this.producer = null; + } + if (this.client) { + // Client doesn't need explicit disconnect + this.client = null; + } + + this.workspaceId = null; + this._status = { isConnected: false }; + this.notifyStatusChange(); + + console.log(`[RemoteProducer] Disconnected`); + } + + async sendCommand(command: RobotCommand): Promise { + if (!this._status.isConnected || !this.producer) { + throw new Error("Cannot send command: Remote producer not connected"); + } + + try { + console.debug(`[RemoteProducer] Sending command:`, command); + + // Update last known state for periodic updates + command.joints.forEach((joint) => { + this.lastKnownState[joint.name] = joint.value; + }); + + // Send joint update with normalized values + const joints = command.joints.map((joint) => ({ + name: joint.name, + value: joint.value // Already normalized + })); + + await this.producer.sendJointUpdate(joints); + console.debug(`[RemoteProducer] Sent joint update with ${joints.length} joints`); + } catch (error) { + console.error("[RemoteProducer] Failed to send command:", error); + throw error; + } + } + + // Event handlers + onStatusChange(callback: (status: ConnectionStatus) => void): () => void { + this.statusCallbacks.push(callback); + return () => { + const index = this.statusCallbacks.indexOf(callback); + if (index >= 0) { + this.statusCallbacks.splice(index, 1); + } + }; + } + + // Private methods + private startStateUpdates(): void { + // Send periodic state updates to keep remote server informed + this.stateUpdateInterval = setInterval(async () => { + if ( + this.producer && + this._status.isConnected && + Object.keys(this.lastKnownState).length > 0 + ) { + try { + await this.producer.sendStateSync(this.lastKnownState); + } catch (error) { + console.error("[RemoteProducer] Failed to send state update:", error); + } + } + }, 100); // 10 Hz updates + } + + private stopStateUpdates(): void { + if (this.stateUpdateInterval) { + clearInterval(this.stateUpdateInterval); + this.stateUpdateInterval = undefined; + } + } + + private notifyStatusChange(): void { + this.statusCallbacks.forEach((callback) => { + try { + callback(this._status); + } catch (error) { + console.error("[RemoteProducer] Error in status callback:", error); + } + }); + } +} diff --git a/src/lib/elements/robot/drivers/USBConsumer.ts b/src/lib/elements/robot/drivers/USBConsumer.ts index 93ae2d57e3d26dea704e9806dc2e59893b3bc3f6..9873c42ba967cb00dcfbbfa6cd60f72162c64e3f 100644 --- a/src/lib/elements/robot/drivers/USBConsumer.ts +++ b/src/lib/elements/robot/drivers/USBConsumer.ts @@ -1,139 +1,138 @@ -import type { Consumer, RobotCommand } from '../models.js'; -import { USBServoDriver } from './USBServoDriver.js'; -import { ROBOT_CONFIG } from '../config.js'; +import type { Consumer, RobotCommand } from "../models.js"; +import { USBServoDriver } from "./USBServoDriver.js"; +import { ROBOT_CONFIG } from "../config.js"; export class USBConsumer extends USBServoDriver implements Consumer { - private commandCallbacks: ((command: RobotCommand) => void)[] = []; - private pollingInterval: ReturnType | null = null; - private lastPositions: Record = {}; - private errorCount = 0; - - constructor(config: any) { - super(config, 'Consumer'); - } - - async connect(): Promise { - // Connect to USB first (this triggers browser's device selection dialog) - await this.connectToUSB(); - - // Unlock servos for manual movement (consumer mode) - await this.unlockAllServos(); - - // Note: Calibration is checked when operations are actually needed - } - - async disconnect(): Promise { - await this.stopListening(); - await this.disconnectFromUSB(); - } - - async startListening(): Promise { - if (!this._status.isConnected || this.pollingInterval !== null) { - return; - } - - if (!this.isCalibrated) { - throw new Error('Cannot start listening: not calibrated'); - } - - console.log(`[${this.name}] Starting position listening...`); - this.errorCount = 0; - - this.pollingInterval = setInterval(async () => { - try { - await this.pollAndBroadcastPositions(); - this.errorCount = 0; - } catch (error) { - this.errorCount++; - console.warn(`[${this.name}] Polling error (${this.errorCount}):`, error); - - if (this.errorCount >= ROBOT_CONFIG.polling.maxPollingErrors) { - console.warn(`[${this.name}] Too many polling errors, slowing down...`); - await this.stopListening(); - setTimeout(() => this.startListening(), ROBOT_CONFIG.polling.errorBackoffRate); - } - } - }, ROBOT_CONFIG.polling.consumerPollingRate); - } - - async stopListening(): Promise { - if (this.pollingInterval !== null) { - clearInterval(this.pollingInterval); - this.pollingInterval = null; - console.log(`[${this.name}] Stopped position listening`); - } - } - - // Event handlers already in base class - - onCommand(callback: (command: RobotCommand) => void): () => void { - this.commandCallbacks.push(callback); - return () => { - const index = this.commandCallbacks.indexOf(callback); - if (index >= 0) { - this.commandCallbacks.splice(index, 1); - } - }; - } - - // Private methods - private async pollAndBroadcastPositions(): Promise { - if (!this.scsServoSDK || !this._status.isConnected) { - return; - } - - try { - // Read positions for all servos - const servoIds = Object.values(this.jointToServoMap); - const positions = await this.scsServoSDK.syncReadPositions(servoIds); - - const jointsWithChanges: { name: string; value: number }[] = []; - - // Check for position changes and convert to normalized values - Object.entries(this.jointToServoMap).forEach(([jointName, servoId]) => { - const currentPosition = positions.get(servoId); - const lastPosition = this.lastPositions[servoId]; - - if (currentPosition !== undefined && - (lastPosition === undefined || - Math.abs(currentPosition - lastPosition) > ROBOT_CONFIG.performance.jointUpdateThreshold)) { - - this.lastPositions[servoId] = currentPosition; - - // Convert to normalized value using calibration (required) - const normalizedValue = this.normalizeValue(currentPosition, jointName); - - jointsWithChanges.push({ - name: jointName, - value: normalizedValue - }); - } - }); - - // Broadcast changes if any - if (jointsWithChanges.length > 0) { - const command: RobotCommand = { - timestamp: Date.now(), - joints: jointsWithChanges - }; - - this.notifyCommand(command); - } - - } catch (error) { - throw error; // Re-throw for error handling in polling loop - } - } - - private notifyCommand(command: RobotCommand): void { - this.commandCallbacks.forEach(callback => { - try { - callback(command); - } catch (error) { - console.error(`[${this.name}] Error in command callback:`, error); - } - }); - } - - -} \ No newline at end of file + private commandCallbacks: ((command: RobotCommand) => void)[] = []; + private pollingInterval: ReturnType | null = null; + private lastPositions: Record = {}; + private errorCount = 0; + + constructor(config: any) { + super(config, "Consumer"); + } + + async connect(): Promise { + // Connect to USB first (this triggers browser's device selection dialog) + await this.connectToUSB(); + + // Unlock servos for manual movement (consumer mode) + await this.unlockAllServos(); + + // Note: Calibration is checked when operations are actually needed + } + + async disconnect(): Promise { + await this.stopListening(); + await this.disconnectFromUSB(); + } + + async startListening(): Promise { + if (!this._status.isConnected || this.pollingInterval !== null) { + return; + } + + if (!this.isCalibrated) { + throw new Error("Cannot start listening: not calibrated"); + } + + console.log(`[${this.name}] Starting position listening...`); + this.errorCount = 0; + + this.pollingInterval = setInterval(async () => { + try { + await this.pollAndBroadcastPositions(); + this.errorCount = 0; + } catch (error) { + this.errorCount++; + console.warn(`[${this.name}] Polling error (${this.errorCount}):`, error); + + if (this.errorCount >= ROBOT_CONFIG.polling.maxPollingErrors) { + console.warn(`[${this.name}] Too many polling errors, slowing down...`); + await this.stopListening(); + setTimeout(() => this.startListening(), ROBOT_CONFIG.polling.errorBackoffRate); + } + } + }, ROBOT_CONFIG.polling.consumerPollingRate); + } + + async stopListening(): Promise { + if (this.pollingInterval !== null) { + clearInterval(this.pollingInterval); + this.pollingInterval = null; + console.log(`[${this.name}] Stopped position listening`); + } + } + + // Event handlers already in base class + + onCommand(callback: (command: RobotCommand) => void): () => void { + this.commandCallbacks.push(callback); + return () => { + const index = this.commandCallbacks.indexOf(callback); + if (index >= 0) { + this.commandCallbacks.splice(index, 1); + } + }; + } + + // Private methods + private async pollAndBroadcastPositions(): Promise { + if (!this.scsServoSDK || !this._status.isConnected) { + return; + } + + try { + // Read positions for all servos + const servoIds = Object.values(this.jointToServoMap); + const positions = await this.scsServoSDK.syncReadPositions(servoIds); + + const jointsWithChanges: { name: string; value: number }[] = []; + + // Check for position changes and convert to normalized values + Object.entries(this.jointToServoMap).forEach(([jointName, servoId]) => { + const currentPosition = positions.get(servoId); + const lastPosition = this.lastPositions[servoId]; + + if ( + currentPosition !== undefined && + (lastPosition === undefined || + Math.abs(currentPosition - lastPosition) > + ROBOT_CONFIG.performance.jointUpdateThreshold) + ) { + this.lastPositions[servoId] = currentPosition; + + // Convert to normalized value using calibration (required) + const normalizedValue = this.normalizeValue(currentPosition, jointName); + + jointsWithChanges.push({ + name: jointName, + value: normalizedValue + }); + } + }); + + // Broadcast changes if any + if (jointsWithChanges.length > 0) { + const command: RobotCommand = { + timestamp: Date.now(), + joints: jointsWithChanges + }; + + this.notifyCommand(command); + } + } catch (error) { + throw error; // Re-throw for error handling in polling loop + } + } + + private notifyCommand(command: RobotCommand): void { + this.commandCallbacks.forEach((callback) => { + try { + callback(command); + } catch (error) { + console.error(`[${this.name}] Error in command callback:`, error); + } + }); + } +} diff --git a/src/lib/elements/robot/drivers/USBProducer.ts b/src/lib/elements/robot/drivers/USBProducer.ts index bc78f47673f4e32b7a741ffa3d4e93850fbc4592..04c7496fcdf7990147676cc4c7e50f104985c82f 100644 --- a/src/lib/elements/robot/drivers/USBProducer.ts +++ b/src/lib/elements/robot/drivers/USBProducer.ts @@ -1,122 +1,119 @@ -import type { Producer, RobotCommand } from '../models.js'; -import { ROBOT_CONFIG } from '../config.js'; -import { USBServoDriver } from './USBServoDriver.js'; +import type { Producer, RobotCommand } from "../models.js"; +import { ROBOT_CONFIG } from "../config.js"; +import { USBServoDriver } from "./USBServoDriver.js"; export class USBProducer extends USBServoDriver implements Producer { - private commandQueue: RobotCommand[] = []; - private isProcessingCommands = false; - - constructor(config: any) { - super(config, 'Producer'); - } - - async connect(): Promise { - // Connect to USB first (this triggers browser's device selection dialog) - await this.connectToUSB(); - - // Lock servos for software control (producer mode) - await this.lockAllServos(); - - // Note: Calibration is checked when operations are actually needed - } - - async disconnect(): Promise { - // Stop command processing - this.isProcessingCommands = false; - this.commandQueue = []; - - await this.disconnectFromUSB(); - } - - async sendCommand(command: RobotCommand): Promise { - if (!this._status.isConnected) { - throw new Error(`[${this.name}] Cannot send command: not connected`); - } - - if (!this.isCalibrated) { - throw new Error(`[${this.name}] Cannot send command: not calibrated`); - } - - // Add command to queue for processing - this.commandQueue.push(command); - - // Limit queue size to prevent memory issues - if (this.commandQueue.length > ROBOT_CONFIG.commands.maxQueueSize) { - this.commandQueue.shift(); // Remove oldest command - } - - // Start processing if not already running - if (!this.isProcessingCommands) { - this.processCommandQueue(); - } - } - - // Event handlers already in base class - - // Private methods - private async processCommandQueue(): Promise { - if (this.isProcessingCommands || !this._status.isConnected) { - return; - } - - this.isProcessingCommands = true; - - while (this.commandQueue.length > 0 && this._status.isConnected) { - const command = this.commandQueue.shift(); - if (command) { - try { - await this.executeCommand(command); - } catch (error) { - console.error(`[${this.name}] Command execution failed:`, error); - // Continue processing other commands - } - } - } - - this.isProcessingCommands = false; - } - - private async executeCommand(command: RobotCommand): Promise { - if (!this.scsServoSDK || !this._status.isConnected) { - return; - } - - try { - // Convert normalized values to servo positions using calibration (required) - const servoCommands = new Map(); - - command.joints.forEach(joint => { - const servoId = this.jointToServoMap[joint.name as keyof typeof this.jointToServoMap]; - if (servoId) { - const servoPosition = this.denormalizeValue(joint.value, joint.name); - - // Clamp to valid servo range - const clampedPosition = Math.max(0, Math.min(4095, Math.round(servoPosition))); - servoCommands.set(servoId, clampedPosition); - } - }); - - if (servoCommands.size > 0) { - // Use batch commands when possible for better performance - if (servoCommands.size > 1) { - await this.scsServoSDK.syncWritePositions(servoCommands); - } else { - // Single servo command - const entry = servoCommands.entries().next().value; - if (entry) { - const [servoId, position] = entry; - await this.scsServoSDK.writePosition(servoId, position); - } - } - - console.debug(`[${this.name}] Sent positions to ${servoCommands.size} servos`); - } - - } catch (error) { - console.error(`[${this.name}] Failed to execute command:`, error); - throw error; - } - } - - -} \ No newline at end of file + private commandQueue: RobotCommand[] = []; + private isProcessingCommands = false; + + constructor(config: any) { + super(config, "Producer"); + } + + async connect(): Promise { + // Connect to USB first (this triggers browser's device selection dialog) + await this.connectToUSB(); + + // Lock servos for software control (producer mode) + await this.lockAllServos(); + + // Note: Calibration is checked when operations are actually needed + } + + async disconnect(): Promise { + // Stop command processing + this.isProcessingCommands = false; + this.commandQueue = []; + + await this.disconnectFromUSB(); + } + + async sendCommand(command: RobotCommand): Promise { + if (!this._status.isConnected) { + throw new Error(`[${this.name}] Cannot send command: not connected`); + } + + if (!this.isCalibrated) { + throw new Error(`[${this.name}] Cannot send command: not calibrated`); + } + + // Add command to queue for processing + this.commandQueue.push(command); + + // Limit queue size to prevent memory issues + if (this.commandQueue.length > ROBOT_CONFIG.commands.maxQueueSize) { + this.commandQueue.shift(); // Remove oldest command + } + + // Start processing if not already running + if (!this.isProcessingCommands) { + this.processCommandQueue(); + } + } + + // Event handlers already in base class + + // Private methods + private async processCommandQueue(): Promise { + if (this.isProcessingCommands || !this._status.isConnected) { + return; + } + + this.isProcessingCommands = true; + + while (this.commandQueue.length > 0 && this._status.isConnected) { + const command = this.commandQueue.shift(); + if (command) { + try { + await this.executeCommand(command); + } catch (error) { + console.error(`[${this.name}] Command execution failed:`, error); + // Continue processing other commands + } + } + } + + this.isProcessingCommands = false; + } + + private async executeCommand(command: RobotCommand): Promise { + if (!this.scsServoSDK || !this._status.isConnected) { + return; + } + + try { + // Convert normalized values to servo positions using calibration (required) + const servoCommands = new Map(); + + command.joints.forEach((joint) => { + const servoId = this.jointToServoMap[joint.name as keyof typeof this.jointToServoMap]; + if (servoId) { + const servoPosition = this.denormalizeValue(joint.value, joint.name); + + // Clamp to valid servo range + const clampedPosition = Math.max(0, Math.min(4095, Math.round(servoPosition))); + servoCommands.set(servoId, clampedPosition); + } + }); + + if (servoCommands.size > 0) { + // Use batch commands when possible for better performance + if (servoCommands.size > 1) { + await this.scsServoSDK.syncWritePositions(servoCommands); + } else { + // Single servo command + const entry = servoCommands.entries().next().value; + if (entry) { + const [servoId, position] = entry; + await this.scsServoSDK.writePosition(servoId, position); + } + } + + console.debug(`[${this.name}] Sent positions to ${servoCommands.size} servos`); + } + } catch (error) { + console.error(`[${this.name}] Failed to execute command:`, error); + throw error; + } + } +} diff --git a/src/lib/elements/robot/drivers/USBServoDriver.ts b/src/lib/elements/robot/drivers/USBServoDriver.ts index a8debb9f3996151677252b91a58db2747cded9af..8cb62538cb289d9f9723b4538d4ed7520ce7c5c5 100644 --- a/src/lib/elements/robot/drivers/USBServoDriver.ts +++ b/src/lib/elements/robot/drivers/USBServoDriver.ts @@ -1,415 +1,439 @@ -import type { ConnectionStatus, USBDriverConfig } from '../models.js'; -import { CalibrationState } from '../calibration/CalibrationState.svelte.js'; -import { ScsServoSDK } from 'feetech.js'; -import { ROBOT_CONFIG } from '../config.js'; +import type { ConnectionStatus, USBDriverConfig } from "../models.js"; +import { CalibrationState } from "../calibration/CalibrationState.svelte.js"; +import { ScsServoSDK } from "feetech.js"; +import { ROBOT_CONFIG } from "../config.js"; export abstract class USBServoDriver { - readonly id: string; - readonly name: string; - readonly config: USBDriverConfig; - - protected _status: ConnectionStatus = { isConnected: false }; - protected statusCallbacks: ((status: ConnectionStatus) => void)[] = []; - - protected scsServoSDK: ScsServoSDK | null = null; - - // Calibration state - directly embedded - readonly calibrationState: CalibrationState; - - // Calibration polling - private calibrationPollingInterval: ReturnType | null = null; - - // Joint to servo ID mapping for SO-100 arm - protected readonly jointToServoMap = { - "Rotation": 1, - "Pitch": 2, - "Elbow": 3, - "Wrist_Pitch": 4, - "Wrist_Roll": 5, - "Jaw": 6 - }; - - constructor(config: USBDriverConfig, driverType: string) { - this.config = config; - this.id = `usb-${driverType}-${Date.now()}`; - this.name = `USB ${driverType}`; - this.calibrationState = new CalibrationState(); - } - - get status(): ConnectionStatus { - return this._status; - } - - get needsCalibration(): boolean { - return this.calibrationState.needsCalibration; - } - - get isCalibrating(): boolean { - return this.calibrationState.isCalibrating; - } - - get isCalibrated(): boolean { - return this.calibrationState.isCalibrated; - } - - // Type guard to check if a driver is a USB driver - static isUSBDriver(driver: any): driver is USBServoDriver { - return driver && typeof driver.calibrationState === 'object' && - typeof driver.needsCalibration === 'boolean' && - typeof driver.isCalibrated === 'boolean' && - typeof driver.startCalibration === 'function'; - } - - // Type-safe method to get calibration interface - getCalibrationInterface(): { - needsCalibration: boolean; - isCalibrating: boolean; - isCalibrated: boolean; - startCalibration: () => Promise; - completeCalibration: () => Promise>; - skipCalibration: () => void; - cancelCalibration: () => void; - onCalibrationCompleteWithPositions: (callback: (positions: Record) => void) => () => void; - } { - return { - needsCalibration: this.needsCalibration, - isCalibrating: this.isCalibrating, - isCalibrated: this.isCalibrated, - startCalibration: () => this.startCalibration(), - completeCalibration: () => this.completeCalibration(), - skipCalibration: () => this.skipCalibration(), - cancelCalibration: () => this.cancelCalibration(), - onCalibrationCompleteWithPositions: (callback) => this.onCalibrationCompleteWithPositions(callback) - }; - } - - // Abstract methods that subclasses must implement - abstract connect(): Promise; - abstract disconnect(): Promise; - - // Common connection logic - protected async connectToUSB(): Promise { - if (this._status.isConnected) { - console.log(`[${this.name}] Already connected`); - return; - } - - try { - console.log(`[${this.name}] Connecting...`); - - // Create a new SDK instance for this driver instead of using the singleton - // This allows multiple drivers to connect to different ports simultaneously - this.scsServoSDK = new ScsServoSDK(); - - await this.scsServoSDK.connect({ - baudRate: this.config.baudRate || ROBOT_CONFIG.usb.baudRate, - protocolEnd: 0 // STS/SMS protocol - }); - - this._status = { isConnected: true, lastConnected: new Date() }; - this.notifyStatusChange(); - - console.log(`[${this.name}] Connected successfully`); - - // Debug: Log SDK instance methods to identify the issue - console.log(`[${this.name}] SDK instance methods:`, Object.getOwnPropertyNames(this.scsServoSDK)); - console.log(`[${this.name}] SDK prototype methods:`, Object.getOwnPropertyNames(Object.getPrototypeOf(this.scsServoSDK))); - console.log(`[${this.name}] writeTorqueEnable available:`, typeof this.scsServoSDK.writeTorqueEnable); - console.log(`[${this.name}] syncReadPositions available:`, typeof this.scsServoSDK.syncReadPositions); - - } catch (error) { - console.error(`[${this.name}] Connection failed:`, error); - this._status = { isConnected: false, error: `Connection failed: ${error}` }; - this.notifyStatusChange(); - throw error; - } - } - - protected async disconnectFromUSB(): Promise { - if (this.scsServoSDK) { - try { - await this.unlockAllServos(); - await this.scsServoSDK.disconnect(); - } catch (error) { - console.warn(`[${this.name}] Error during disconnect:`, error); - } - this.scsServoSDK = null; - } - - this._status = { isConnected: false }; - this.notifyStatusChange(); - - console.log(`[${this.name}] Disconnected`); - } - - // Calibration methods - async startCalibration(): Promise { - if (!this._status.isConnected) { - await this.connectToUSB(); - } - - if (!this._status.isConnected) { - throw new Error('Cannot start calibration: not connected'); - } - - console.log(`[${this.name}] Starting calibration...`); - this.calibrationState.startCalibration(); - - // Unlock servos for manual movement during calibration - await this.unlockAllServos(); - - // Start polling positions during calibration - await this.startCalibrationPolling(); - } - - async completeCalibration(): Promise> { - if (!this.isCalibrating) { - throw new Error('Not currently calibrating'); - } - - // Stop polling - this.stopCalibrationPolling(); - - // Read final positions - const finalPositions = await this.readCurrentPositions(); - - // Complete calibration state - this.calibrationState.completeCalibration(); - - console.log(`[${this.name}] Calibration completed`); - return finalPositions; - } - - skipCalibration(): void { - // Stop polling if active - this.stopCalibrationPolling(); - this.calibrationState.skipCalibration(); - } - - async setPredefinedCalibration(): Promise { - // Stop polling if active - this.stopCalibrationPolling(); - this.skipCalibration(); - } - - // Cancel calibration - cancelCalibration(): void { - // Stop polling if active - this.stopCalibrationPolling(); - this.calibrationState.cancelCalibration(); - } - - // Start polling servo positions during calibration - private async startCalibrationPolling(): Promise { - if (this.calibrationPollingInterval !== null) { - return; // Already polling - } - - console.log(`[${this.name}] Starting calibration position polling...`); - - // Poll positions every 100ms during calibration - this.calibrationPollingInterval = setInterval(async () => { - if (!this.isCalibrating || !this._status.isConnected || !this.scsServoSDK) { - this.stopCalibrationPolling(); - return; - } - - try { - // Read positions for all servos - const servoIds = Object.values(this.jointToServoMap); - const positions = await this.scsServoSDK.syncReadPositions(servoIds); - - // Update calibration state with current positions - Object.entries(this.jointToServoMap).forEach(([jointName, servoId]) => { - const position = positions.get(servoId); - if (position !== undefined) { - this.calibrationState.updateCurrentValue(jointName, position); - console.debug(`[${this.name}] ${jointName} (servo ${servoId}): ${position}`); - } - }); - - } catch (error) { - console.warn(`[${this.name}] Calibration polling error:`, error); - // Continue polling despite errors - user might be moving servos rapidly - } - }, 100); // Poll every 100ms - } - - // Stop polling servo positions - private stopCalibrationPolling(): void { - if (this.calibrationPollingInterval !== null) { - clearInterval(this.calibrationPollingInterval); - this.calibrationPollingInterval = null; - console.log(`[${this.name}] Stopped calibration position polling`); - } - } - - // Servo position reading (for calibration) - async readCurrentPositions(): Promise> { - if (!this.scsServoSDK || !this._status.isConnected) { - throw new Error('Cannot read positions: not connected'); - } - - const positions: Record = {}; - - try { - const servoIds = Object.values(this.jointToServoMap); - const servoPositions = await this.scsServoSDK.syncReadPositions(servoIds); - - Object.entries(this.jointToServoMap).forEach(([jointName, servoId]) => { - const position = servoPositions.get(servoId); - if (position !== undefined) { - positions[jointName] = position; - // Update calibration state with current position - this.calibrationState.updateCurrentValue(jointName, position); - } - }); - - } catch (error) { - console.error(`[${this.name}] Error reading positions:`, error); - throw error; - } - - return positions; - } - - // Value conversion methods - normalizeValue(rawValue: number, jointName: string): number { - if (!this.isCalibrated) { - throw new Error('Cannot normalize value: not calibrated'); - } - return this.calibrationState.normalizeValue(rawValue, jointName); - } - - denormalizeValue(normalizedValue: number, jointName: string): number { - if (!this.isCalibrated) { - throw new Error('Cannot denormalize value: not calibrated'); - } - return this.calibrationState.denormalizeValue(normalizedValue, jointName); - } - - // Servo control methods - protected async lockAllServos(): Promise { - if (!this.scsServoSDK) return; - - try { - console.log(`[${this.name}] Locking all servos...`); - - const servoIds = Object.values(this.jointToServoMap); - - for (const servoId of servoIds) { - try { - // Check if writeTorqueEnable method exists before calling - if (typeof this.scsServoSDK.writeTorqueEnable !== 'function') { - console.warn(`[${this.name}] writeTorqueEnable method not available on SDK instance for servo ${servoId}`); - continue; - } - - await this.scsServoSDK.writeTorqueEnable(servoId, true); - await new Promise(resolve => setTimeout(resolve, ROBOT_CONFIG.usb.servoWriteDelay)); - } catch (error) { - console.warn(`[${this.name}] Failed to lock servo ${servoId}:`, error); - } - } - - console.log(`[${this.name}] All servos locked`); - - } catch (error) { - console.error(`[${this.name}] Error locking servos:`, error); - } - } - - protected async unlockAllServos(): Promise { - if (!this.scsServoSDK) return; - - try { - console.log(`[${this.name}] Unlocking all servos...`); - - const servoIds = Object.values(this.jointToServoMap); - - for (const servoId of servoIds) { - try { - // Check if writeTorqueEnable method exists before calling - if (typeof this.scsServoSDK.writeTorqueEnable !== 'function') { - console.warn(`[${this.name}] writeTorqueEnable method not available on SDK instance for servo ${servoId}`); - continue; - } - - await this.scsServoSDK.writeTorqueEnable(servoId, false); - await new Promise(resolve => setTimeout(resolve, ROBOT_CONFIG.usb.servoWriteDelay)); - } catch (error) { - console.warn(`[${this.name}] Failed to unlock servo ${servoId}:`, error); - } - } - - console.log(`[${this.name}] All servos unlocked`); - - } catch (error) { - console.error(`[${this.name}] Error unlocking servos:`, error); - } - } - - // Event handlers - onStatusChange(callback: (status: ConnectionStatus) => void): () => void { - this.statusCallbacks.push(callback); - return () => { - const index = this.statusCallbacks.indexOf(callback); - if (index >= 0) { - this.statusCallbacks.splice(index, 1); - } - }; - } - - protected notifyStatusChange(): void { - this.statusCallbacks.forEach(callback => { - try { - callback(this._status); - } catch (error) { - console.error(`[${this.name}] Error in status callback:`, error); - } - }); - } - - // Register callback for calibration completion with positions - onCalibrationCompleteWithPositions(callback: (positions: Record) => void): () => void { - return this.calibrationState.onCalibrationCompleteWithPositions(callback); - } - - // Sync robot joint positions using normalized values from calibration - syncRobotPositions(finalPositions: Record, updateRobotCallback?: (jointName: string, normalizedValue: number) => void): void { - if (!updateRobotCallback) return; - - console.log(`[${this.name}] πŸ”„ Syncing robot to final calibration positions...`); - - Object.entries(finalPositions).forEach(([jointName, rawPosition]) => { - try { - // Convert raw servo position to normalized value using calibration - const normalizedValue = this.normalizeValue(rawPosition, jointName); - - // Clamp to appropriate normalized range based on joint type - let clampedValue: number; - if (jointName.toLowerCase() === 'jaw' || jointName.toLowerCase() === 'gripper') { - clampedValue = Math.max(0, Math.min(100, normalizedValue)); - } else { - clampedValue = Math.max(-100, Math.min(100, normalizedValue)); - } - - console.log(`[${this.name}] ${jointName}: ${rawPosition} (raw) -> ${normalizedValue.toFixed(1)} -> ${clampedValue.toFixed(1)} (normalized)`); - - // Update robot joint through callback - updateRobotCallback(jointName, clampedValue); - } catch (error) { - console.warn(`[${this.name}] Failed to sync position for joint ${jointName}:`, error); - } - }); - - console.log(`[${this.name}] βœ… Robot synced to calibration positions`); - } - - // Cleanup - async destroy(): Promise { - this.stopCalibrationPolling(); - await this.disconnect(); - this.calibrationState.destroy(); - } -} \ No newline at end of file + readonly id: string; + readonly name: string; + readonly config: USBDriverConfig; + + protected _status: ConnectionStatus = { isConnected: false }; + protected statusCallbacks: ((status: ConnectionStatus) => void)[] = []; + + protected scsServoSDK: ScsServoSDK | null = null; + + // Calibration state - directly embedded + readonly calibrationState: CalibrationState; + + // Calibration polling + private calibrationPollingInterval: ReturnType | null = null; + + // Joint to servo ID mapping for SO-100 arm + protected readonly jointToServoMap = { + Rotation: 1, + Pitch: 2, + Elbow: 3, + Wrist_Pitch: 4, + Wrist_Roll: 5, + Jaw: 6 + }; + + constructor(config: USBDriverConfig, driverType: string) { + this.config = config; + this.id = `usb-${driverType}-${Date.now()}`; + this.name = `USB ${driverType}`; + this.calibrationState = new CalibrationState(); + } + + get status(): ConnectionStatus { + return this._status; + } + + get needsCalibration(): boolean { + return this.calibrationState.needsCalibration; + } + + get isCalibrating(): boolean { + return this.calibrationState.isCalibrating; + } + + get isCalibrated(): boolean { + return this.calibrationState.isCalibrated; + } + + // Type guard to check if a driver is a USB driver + static isUSBDriver(driver: any): driver is USBServoDriver { + return ( + driver && + typeof driver.calibrationState === "object" && + typeof driver.needsCalibration === "boolean" && + typeof driver.isCalibrated === "boolean" && + typeof driver.startCalibration === "function" + ); + } + + // Type-safe method to get calibration interface + getCalibrationInterface(): { + needsCalibration: boolean; + isCalibrating: boolean; + isCalibrated: boolean; + startCalibration: () => Promise; + completeCalibration: () => Promise>; + skipCalibration: () => void; + cancelCalibration: () => void; + onCalibrationCompleteWithPositions: ( + callback: (positions: Record) => void + ) => () => void; + } { + return { + needsCalibration: this.needsCalibration, + isCalibrating: this.isCalibrating, + isCalibrated: this.isCalibrated, + startCalibration: () => this.startCalibration(), + completeCalibration: () => this.completeCalibration(), + skipCalibration: () => this.skipCalibration(), + cancelCalibration: () => this.cancelCalibration(), + onCalibrationCompleteWithPositions: (callback) => + this.onCalibrationCompleteWithPositions(callback) + }; + } + + // Abstract methods that subclasses must implement + abstract connect(): Promise; + abstract disconnect(): Promise; + + // Common connection logic + protected async connectToUSB(): Promise { + if (this._status.isConnected) { + console.log(`[${this.name}] Already connected`); + return; + } + + try { + console.log(`[${this.name}] Connecting...`); + + // Create a new SDK instance for this driver instead of using the singleton + // This allows multiple drivers to connect to different ports simultaneously + this.scsServoSDK = new ScsServoSDK(); + + await this.scsServoSDK.connect({ + baudRate: this.config.baudRate || ROBOT_CONFIG.usb.baudRate, + protocolEnd: 0 // STS/SMS protocol + }); + + this._status = { isConnected: true, lastConnected: new Date() }; + this.notifyStatusChange(); + + console.log(`[${this.name}] Connected successfully`); + + // Debug: Log SDK instance methods to identify the issue + console.log( + `[${this.name}] SDK instance methods:`, + Object.getOwnPropertyNames(this.scsServoSDK) + ); + console.log( + `[${this.name}] SDK prototype methods:`, + Object.getOwnPropertyNames(Object.getPrototypeOf(this.scsServoSDK)) + ); + console.log( + `[${this.name}] writeTorqueEnable available:`, + typeof this.scsServoSDK.writeTorqueEnable + ); + console.log( + `[${this.name}] syncReadPositions available:`, + typeof this.scsServoSDK.syncReadPositions + ); + } catch (error) { + console.error(`[${this.name}] Connection failed:`, error); + this._status = { isConnected: false, error: `Connection failed: ${error}` }; + this.notifyStatusChange(); + throw error; + } + } + + protected async disconnectFromUSB(): Promise { + if (this.scsServoSDK) { + try { + await this.unlockAllServos(); + await this.scsServoSDK.disconnect(); + } catch (error) { + console.warn(`[${this.name}] Error during disconnect:`, error); + } + this.scsServoSDK = null; + } + + this._status = { isConnected: false }; + this.notifyStatusChange(); + + console.log(`[${this.name}] Disconnected`); + } + + // Calibration methods + async startCalibration(): Promise { + if (!this._status.isConnected) { + await this.connectToUSB(); + } + + if (!this._status.isConnected) { + throw new Error("Cannot start calibration: not connected"); + } + + console.log(`[${this.name}] Starting calibration...`); + this.calibrationState.startCalibration(); + + // Unlock servos for manual movement during calibration + await this.unlockAllServos(); + + // Start polling positions during calibration + await this.startCalibrationPolling(); + } + + async completeCalibration(): Promise> { + if (!this.isCalibrating) { + throw new Error("Not currently calibrating"); + } + + // Stop polling + this.stopCalibrationPolling(); + + // Read final positions + const finalPositions = await this.readCurrentPositions(); + + // Complete calibration state + this.calibrationState.completeCalibration(); + + console.log(`[${this.name}] Calibration completed`); + return finalPositions; + } + + skipCalibration(): void { + // Stop polling if active + this.stopCalibrationPolling(); + this.calibrationState.skipCalibration(); + } + + async setPredefinedCalibration(): Promise { + // Stop polling if active + this.stopCalibrationPolling(); + this.skipCalibration(); + } + + // Cancel calibration + cancelCalibration(): void { + // Stop polling if active + this.stopCalibrationPolling(); + this.calibrationState.cancelCalibration(); + } + + // Start polling servo positions during calibration + private async startCalibrationPolling(): Promise { + if (this.calibrationPollingInterval !== null) { + return; // Already polling + } + + console.log(`[${this.name}] Starting calibration position polling...`); + + // Poll positions every 100ms during calibration + this.calibrationPollingInterval = setInterval(async () => { + if (!this.isCalibrating || !this._status.isConnected || !this.scsServoSDK) { + this.stopCalibrationPolling(); + return; + } + + try { + // Read positions for all servos + const servoIds = Object.values(this.jointToServoMap); + const positions = await this.scsServoSDK.syncReadPositions(servoIds); + + // Update calibration state with current positions + Object.entries(this.jointToServoMap).forEach(([jointName, servoId]) => { + const position = positions.get(servoId); + if (position !== undefined) { + this.calibrationState.updateCurrentValue(jointName, position); + console.debug(`[${this.name}] ${jointName} (servo ${servoId}): ${position}`); + } + }); + } catch (error) { + console.warn(`[${this.name}] Calibration polling error:`, error); + // Continue polling despite errors - user might be moving servos rapidly + } + }, 100); // Poll every 100ms + } + + // Stop polling servo positions + private stopCalibrationPolling(): void { + if (this.calibrationPollingInterval !== null) { + clearInterval(this.calibrationPollingInterval); + this.calibrationPollingInterval = null; + console.log(`[${this.name}] Stopped calibration position polling`); + } + } + + // Servo position reading (for calibration) + async readCurrentPositions(): Promise> { + if (!this.scsServoSDK || !this._status.isConnected) { + throw new Error("Cannot read positions: not connected"); + } + + const positions: Record = {}; + + try { + const servoIds = Object.values(this.jointToServoMap); + const servoPositions = await this.scsServoSDK.syncReadPositions(servoIds); + + Object.entries(this.jointToServoMap).forEach(([jointName, servoId]) => { + const position = servoPositions.get(servoId); + if (position !== undefined) { + positions[jointName] = position; + // Update calibration state with current position + this.calibrationState.updateCurrentValue(jointName, position); + } + }); + } catch (error) { + console.error(`[${this.name}] Error reading positions:`, error); + throw error; + } + + return positions; + } + + // Value conversion methods + normalizeValue(rawValue: number, jointName: string): number { + if (!this.isCalibrated) { + throw new Error("Cannot normalize value: not calibrated"); + } + return this.calibrationState.normalizeValue(rawValue, jointName); + } + + denormalizeValue(normalizedValue: number, jointName: string): number { + if (!this.isCalibrated) { + throw new Error("Cannot denormalize value: not calibrated"); + } + return this.calibrationState.denormalizeValue(normalizedValue, jointName); + } + + // Servo control methods + protected async lockAllServos(): Promise { + if (!this.scsServoSDK) return; + + try { + console.log(`[${this.name}] Locking all servos...`); + + const servoIds = Object.values(this.jointToServoMap); + + for (const servoId of servoIds) { + try { + // Check if writeTorqueEnable method exists before calling + if (typeof this.scsServoSDK.writeTorqueEnable !== "function") { + console.warn( + `[${this.name}] writeTorqueEnable method not available on SDK instance for servo ${servoId}` + ); + continue; + } + + await this.scsServoSDK.writeTorqueEnable(servoId, true); + await new Promise((resolve) => setTimeout(resolve, ROBOT_CONFIG.usb.servoWriteDelay)); + } catch (error) { + console.warn(`[${this.name}] Failed to lock servo ${servoId}:`, error); + } + } + + console.log(`[${this.name}] All servos locked`); + } catch (error) { + console.error(`[${this.name}] Error locking servos:`, error); + } + } + + protected async unlockAllServos(): Promise { + if (!this.scsServoSDK) return; + + try { + console.log(`[${this.name}] Unlocking all servos...`); + + const servoIds = Object.values(this.jointToServoMap); + + for (const servoId of servoIds) { + try { + // Check if writeTorqueEnable method exists before calling + if (typeof this.scsServoSDK.writeTorqueEnable !== "function") { + console.warn( + `[${this.name}] writeTorqueEnable method not available on SDK instance for servo ${servoId}` + ); + continue; + } + + await this.scsServoSDK.writeTorqueEnable(servoId, false); + await new Promise((resolve) => setTimeout(resolve, ROBOT_CONFIG.usb.servoWriteDelay)); + } catch (error) { + console.warn(`[${this.name}] Failed to unlock servo ${servoId}:`, error); + } + } + + console.log(`[${this.name}] All servos unlocked`); + } catch (error) { + console.error(`[${this.name}] Error unlocking servos:`, error); + } + } + + // Event handlers + onStatusChange(callback: (status: ConnectionStatus) => void): () => void { + this.statusCallbacks.push(callback); + return () => { + const index = this.statusCallbacks.indexOf(callback); + if (index >= 0) { + this.statusCallbacks.splice(index, 1); + } + }; + } + + protected notifyStatusChange(): void { + this.statusCallbacks.forEach((callback) => { + try { + callback(this._status); + } catch (error) { + console.error(`[${this.name}] Error in status callback:`, error); + } + }); + } + + // Register callback for calibration completion with positions + onCalibrationCompleteWithPositions( + callback: (positions: Record) => void + ): () => void { + return this.calibrationState.onCalibrationCompleteWithPositions(callback); + } + + // Sync robot joint positions using normalized values from calibration + syncRobotPositions( + finalPositions: Record, + updateRobotCallback?: (jointName: string, normalizedValue: number) => void + ): void { + if (!updateRobotCallback) return; + + console.log(`[${this.name}] πŸ”„ Syncing robot to final calibration positions...`); + + Object.entries(finalPositions).forEach(([jointName, rawPosition]) => { + try { + // Convert raw servo position to normalized value using calibration + const normalizedValue = this.normalizeValue(rawPosition, jointName); + + // Clamp to appropriate normalized range based on joint type + let clampedValue: number; + if (jointName.toLowerCase() === "jaw" || jointName.toLowerCase() === "gripper") { + clampedValue = Math.max(0, Math.min(100, normalizedValue)); + } else { + clampedValue = Math.max(-100, Math.min(100, normalizedValue)); + } + + console.log( + `[${this.name}] ${jointName}: ${rawPosition} (raw) -> ${normalizedValue.toFixed(1)} -> ${clampedValue.toFixed(1)} (normalized)` + ); + + // Update robot joint through callback + updateRobotCallback(jointName, clampedValue); + } catch (error) { + console.warn(`[${this.name}] Failed to sync position for joint ${jointName}:`, error); + } + }); + + console.log(`[${this.name}] βœ… Robot synced to calibration positions`); + } + + // Cleanup + async destroy(): Promise { + this.stopCalibrationPolling(); + await this.disconnect(); + this.calibrationState.destroy(); + } +} diff --git a/src/lib/elements/robot/drivers/index.ts b/src/lib/elements/robot/drivers/index.ts index d2c3b2a859ef9880d7deab6eda802de2e11afac0..6332f104215c25452aed8dd409356ccd71562847 100644 --- a/src/lib/elements/robot/drivers/index.ts +++ b/src/lib/elements/robot/drivers/index.ts @@ -1,6 +1,6 @@ // Robot drivers exports -export { USBServoDriver } from './USBServoDriver.js'; -export { USBConsumer } from './USBConsumer.js'; -export { USBProducer } from './USBProducer.js'; -export { RemoteConsumer } from './RemoteConsumer.js'; -export { RemoteProducer } from './RemoteProducer.js'; \ No newline at end of file +export { USBServoDriver } from "./USBServoDriver.js"; +export { USBConsumer } from "./USBConsumer.js"; +export { USBProducer } from "./USBProducer.js"; +export { RemoteConsumer } from "./RemoteConsumer.js"; +export { RemoteProducer } from "./RemoteProducer.js"; diff --git a/src/lib/elements/robot/index.ts b/src/lib/elements/robot/index.ts index 041fe4450111a4b1e72b129eaccf704b965a3e06..e8990930503424aeae107121ce3a49107322bd53 100644 --- a/src/lib/elements/robot/index.ts +++ b/src/lib/elements/robot/index.ts @@ -2,18 +2,18 @@ // Everything you need in one place // Core robot classes -export { Robot } from './Robot.svelte.js'; -export { RobotManager } from './RobotManager.svelte.js'; +export { Robot } from "./Robot.svelte.js"; +export { RobotManager } from "./RobotManager.svelte.js"; // Robot models and types -export * from './models.js'; +export * from "./models.js"; // Robot drivers -export * from './drivers/index.js'; +export * from "./drivers/index.js"; // Robot calibration (avoid naming conflicts with models) -export { CalibrationState as CalibrationStateManager } from './calibration/CalibrationState.svelte.js'; -export { default as USBCalibrationPanel } from './calibration/USBCalibrationPanel.svelte'; +export { CalibrationState as CalibrationStateManager } from "./calibration/CalibrationState.svelte.js"; +export { default as USBCalibrationPanel } from "./calibration/USBCalibrationPanel.svelte"; // Robot components -export * from './components/index.js'; \ No newline at end of file +export * from "./components/index.js"; diff --git a/src/lib/elements/robot/models.ts b/src/lib/elements/robot/models.ts index aae8e3ddecda695fe9488f96fe46293db7a9b510..c385e33fd00e15f39bf1847a2f0ee8ff4884b199 100644 --- a/src/lib/elements/robot/models.ts +++ b/src/lib/elements/robot/models.ts @@ -1,72 +1,72 @@ // Core models with clean typing export interface JointState { - name: string; - value: number; // Normalized value (-100 to +100 for regular joints, 0-100 for grippers) - limits?: { lower: number; upper: number }; // URDF limits in radians - servoId?: number; // For hardware mapping + name: string; + value: number; // Normalized value (-100 to +100 for regular joints, 0-100 for grippers) + limits?: { lower: number; upper: number }; // URDF limits in radians + servoId?: number; // For hardware mapping } export interface JointCalibration { - isCalibrated: boolean; - minServoValue?: number; - maxServoValue?: number; + isCalibrated: boolean; + minServoValue?: number; + maxServoValue?: number; } export interface RobotCommand { - joints: { name: string; value: number }[]; - timestamp?: number; + joints: { name: string; value: number }[]; + timestamp?: number; } export interface ConnectionStatus { - isConnected: boolean; - error?: string; - lastConnected?: Date; + isConnected: boolean; + error?: string; + lastConnected?: Date; } export interface Position3D { - x: number; - y: number; - z: number; + x: number; + y: number; + z: number; } // Driver configurations export interface USBDriverConfig { - type: 'usb'; - baudRate?: number; + type: "usb"; + baudRate?: number; } export interface RemoteDriverConfig { - type: 'remote'; - url: string; - robotId: string; - workspaceId?: string; // Optional workspace ID for remote connections + type: "remote"; + url: string; + robotId: string; + workspaceId?: string; // Optional workspace ID for remote connections } // Driver base interface export interface Driver { - readonly id: string; - readonly name: string; - readonly status: ConnectionStatus; - - connect(): Promise; - disconnect(): Promise; - onStatusChange(callback: (status: ConnectionStatus) => void): () => void; + readonly id: string; + readonly name: string; + readonly status: ConnectionStatus; + + connect(): Promise; + disconnect(): Promise; + onStatusChange(callback: (status: ConnectionStatus) => void): () => void; } // Consumer interface (receives commands) - Robot can only have ONE export interface Consumer extends Driver { - onCommand(callback: (command: RobotCommand) => void): () => void; - startListening?(): Promise; - stopListening?(): Promise; + onCommand(callback: (command: RobotCommand) => void): () => void; + startListening?(): Promise; + stopListening?(): Promise; } // Producer interface (sends commands) - Robot can have MULTIPLE export interface Producer extends Driver { - sendCommand(command: RobotCommand): Promise; + sendCommand(command: RobotCommand): Promise; } // Calibration state for UI components export interface CalibrationState { - isCalibrating: boolean; - progress: number; -} \ No newline at end of file + isCalibrating: boolean; + progress: number; +} diff --git a/src/lib/elements/video/VideoManager.svelte.ts b/src/lib/elements/video/VideoManager.svelte.ts index 03a08376deea27ae8b836af214b6b89f75323164..7c52a74247a72be120bd92f1ff0a872bd94a6941 100644 --- a/src/lib/elements/video/VideoManager.svelte.ts +++ b/src/lib/elements/video/VideoManager.svelte.ts @@ -4,12 +4,12 @@ * Manages multiple video instances, each with their own streaming state */ -import { video as videoClient } from '@robothub/transport-server-client'; -import type { video as videoTypes } from '@robothub/transport-server-client'; +import { video as videoClient } from "@robothub/transport-server-client"; +import type { video as videoTypes } from "@robothub/transport-server-client"; import { generateName } from "$lib/utils/generateName"; -import type { Positionable, Position3D } from '$lib/types/positionable'; -import { positionManager } from '$lib/utils/positionManager'; -import { settings } from '$lib/runes/settings.svelte'; +import type { Positionable, Position3D } from "$lib/types/positionable"; +import { positionManager } from "$lib/utils/positionManager"; +import { settings } from "$lib/runes/settings.svelte"; /** * Individual video instance state @@ -17,32 +17,37 @@ import { settings } from '$lib/runes/settings.svelte'; export class VideoInstance implements Positionable { public id: string; public name: string; - + // Input state (what this video is viewing) input = $state({ - type: null as 'local-camera' | 'remote-stream' | null, + type: null as "local-camera" | "remote-stream" | null, stream: null as MediaStream | null, client: null as videoTypes.VideoConsumer | null, roomId: null as string | null, // Connection lifecycle state - connectionState: 'disconnected' as 'disconnected' | 'connecting' | 'connected' | 'prepared' | 'paused', + connectionState: "disconnected" as + | "disconnected" + | "connecting" + | "connected" + | "prepared" + | "paused", preparedRoomId: null as string | null, // Connection policy - determines if connection should persist or can be paused - connectionPolicy: 'persistent' as 'persistent' | 'lazy', + connectionPolicy: "persistent" as "persistent" | "lazy" }); // Output state (what this video is broadcasting) output = $state({ active: false, - type: null as 'recording' | 'remote-broadcast' | null, + type: null as "recording" | "remote-broadcast" | null, stream: null as MediaStream | null, client: null as videoTypes.VideoProducer | null, - roomId: null as string | null, + roomId: null as string | null }); // Position (reactive and bindable) position = $state({ x: 0, y: 0, z: 0 }); - + constructor(id: string, name?: string) { this.id = id; this.name = name || `Video ${id}`; @@ -66,7 +71,7 @@ export class VideoInstance implements Positionable { get canOutput(): boolean { // Can only output if input is local camera (not remote stream) - return this.input.type === 'local-camera' && this.input.stream !== null; + return this.input.type === "local-camera" && this.input.stream !== null; } get currentStream(): MediaStream | null { @@ -84,8 +89,9 @@ export class VideoInstance implements Positionable { const connectionState = this.input.connectionState; const preparedRoomId = this.input.preparedRoomId; const connectionPolicy = this.input.connectionPolicy; - const canActivate = (connectionState === 'prepared' || connectionState === 'paused') && preparedRoomId !== null; - const canPause = connectionState === 'connected' && connectionPolicy === 'lazy'; + const canActivate = + (connectionState === "prepared" || connectionState === "paused") && preparedRoomId !== null; + const canPause = connectionState === "connected" && connectionPolicy === "lazy"; return { id: this.id, @@ -99,7 +105,7 @@ export class VideoInstance implements Positionable { preparedRoomId, connectionPolicy, canActivate, - canPause, + canPause }; } } @@ -112,12 +118,12 @@ export interface VideoStatus { name: string; hasInput: boolean; hasOutput: boolean; - inputType: 'local-camera' | 'remote-stream' | null; + inputType: "local-camera" | "remote-stream" | null; outputRoomId: string | null; inputRoomId: string | null; - connectionState: 'disconnected' | 'connecting' | 'connected' | 'prepared' | 'paused'; + connectionState: "disconnected" | "connecting" | "connected" | "prepared" | "paused"; preparedRoomId: string | null; - connectionPolicy: 'persistent' | 'lazy'; + connectionPolicy: "persistent" | "lazy"; canActivate: boolean; canPause: boolean; } @@ -150,7 +156,7 @@ export class VideoManager { */ createVideo(id?: string, name?: string, position?: Position3D): VideoInstance { const videoId = id || generateName(); - + // Check if video already exists if (this._videos.find((v) => v.id === videoId)) { throw new Error(`Video with ID ${videoId} already exists`); @@ -165,7 +171,9 @@ export class VideoManager { // Add to reactive array this._videos.push(video); - console.log(`Created video ${videoId} at position (${video.position.x.toFixed(1)}, ${video.position.y.toFixed(1)}, ${video.position.z.toFixed(1)}). Total videos: ${this._videos.length}`); + console.log( + `Created video ${videoId} at position (${video.position.x.toFixed(1)}, ${video.position.y.toFixed(1)}, ${video.position.z.toFixed(1)}). Total videos: ${this._videos.length}` + ); return video; } @@ -214,7 +222,7 @@ export class VideoManager { this.rooms = rooms; return rooms; } catch (error) { - console.error('Failed to list rooms:', error); + console.error("Failed to list rooms:", error); this.rooms = []; return []; } finally { @@ -226,7 +234,10 @@ export class VideoManager { await this.listRooms(workspaceId); } - async createVideoRoom(workspaceId: string, roomId?: string): Promise<{ success: boolean; roomId?: string; error?: string }> { + async createVideoRoom( + workspaceId: string, + roomId?: string + ): Promise<{ success: boolean; roomId?: string; error?: string }> { try { const client = new videoClient.VideoClientCore(settings.transportServerUrl); const result = await client.createRoom(workspaceId, roomId); @@ -234,8 +245,8 @@ export class VideoManager { await this.refreshRooms(workspaceId); return { success: true, roomId: result.roomId }; } catch (error) { - console.error('Failed to create video room:', error); - return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; + console.error("Failed to create video room:", error); + return { success: false, error: error instanceof Error ? error.message : "Unknown error" }; } } @@ -246,22 +257,26 @@ export class VideoManager { /** * Start video output to an existing room */ - async startVideoOutputToRoom(workspaceId: string, videoId: string, roomId: string): Promise<{ success: boolean; error?: string }> { + async startVideoOutputToRoom( + workspaceId: string, + videoId: string, + roomId: string + ): Promise<{ success: boolean; error?: string }> { const video = this.getVideo(videoId); if (!video) { return { success: false, error: `Video ${videoId} not found` }; } if (!video.canOutput) { - return { success: false, error: 'Cannot output - input must be local camera' }; + return { success: false, error: "Cannot output - input must be local camera" }; } try { const producer = new videoClient.VideoProducer(settings.transportServerUrl); - const connected = await producer.connect(workspaceId, roomId, 'producer-id'); - + const connected = await producer.connect(workspaceId, roomId, "producer-id"); + if (!connected) { - throw new Error('Failed to connect to room'); + throw new Error("Failed to connect to room"); } // Start camera streaming - VideoProducer creates its own stream @@ -272,7 +287,7 @@ export class VideoManager { // Update output state video.output.active = true; - video.output.type = 'remote-broadcast'; + video.output.type = "remote-broadcast"; video.output.stream = video.input.stream; video.output.client = producer; video.output.roomId = roomId; @@ -288,26 +303,34 @@ export class VideoManager { /** * Create a new room and start video output as producer */ - async startVideoOutputAsProducer(workspaceId: string, videoId: string, roomId?: string): Promise<{ success: boolean; roomId?: string; error?: string }> { + async startVideoOutputAsProducer( + workspaceId: string, + videoId: string, + roomId?: string + ): Promise<{ success: boolean; roomId?: string; error?: string }> { try { // Create room first if roomId provided, otherwise generate one const finalRoomId = roomId || this.generateRoomId(videoId); const createResult = await this.createVideoRoom(workspaceId, finalRoomId); - + if (!createResult.success) { return createResult; } // Start output to the new room - const outputResult = await this.startVideoOutputToRoom(workspaceId, videoId, createResult.roomId!); - + const outputResult = await this.startVideoOutputToRoom( + workspaceId, + videoId, + createResult.roomId! + ); + if (!outputResult.success) { return { success: false, error: outputResult.error }; } return { success: true, roomId: createResult.roomId }; } catch (error) { - return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; + return { success: false, error: error instanceof Error ? error.message : "Unknown error" }; } } @@ -316,33 +339,47 @@ export class VideoManager { /** * Prepare a remote stream connection (stores roomId without connecting) */ - prepareRemoteStream(videoId: string, roomId: string, policy: 'persistent' | 'lazy' = 'lazy'): { success: boolean; error?: string } { + prepareRemoteStream( + videoId: string, + roomId: string, + policy: "persistent" | "lazy" = "lazy" + ): { success: boolean; error?: string } { const video = this.getVideo(videoId); if (!video) { return { success: false, error: `Video ${videoId} not found` }; } video.input.preparedRoomId = roomId; - video.input.connectionState = 'prepared'; + video.input.connectionState = "prepared"; video.input.connectionPolicy = policy; - console.log(`Prepared remote stream for video ${videoId}, roomId: ${roomId}, policy: ${policy}`); + console.log( + `Prepared remote stream for video ${videoId}, roomId: ${roomId}, policy: ${policy}` + ); return { success: true }; } /** * Activate a prepared or paused remote stream connection */ - async activateRemoteStream(videoId: string, workspaceId: string): Promise<{ success: boolean; error?: string }> { + async activateRemoteStream( + videoId: string, + workspaceId: string + ): Promise<{ success: boolean; error?: string }> { const video = this.getVideo(videoId); if (!video) { return { success: false, error: `Video ${videoId} not found` }; } if (!video.input.preparedRoomId) { - return { success: false, error: 'No prepared room ID to activate' }; + return { success: false, error: "No prepared room ID to activate" }; } - return await this.connectRemoteStream(workspaceId, videoId, video.input.preparedRoomId, video.input.connectionPolicy); + return await this.connectRemoteStream( + workspaceId, + videoId, + video.input.preparedRoomId, + video.input.connectionPolicy + ); } /** @@ -350,7 +387,7 @@ export class VideoManager { */ async pauseRemoteStream(videoId: string): Promise { const video = this.getVideo(videoId); - if (!video || video.input.type !== 'remote-stream') return; + if (!video || video.input.type !== "remote-stream") return; // Store the current roomId for later activation if (video.input.roomId && !video.input.preparedRoomId) { @@ -361,12 +398,12 @@ export class VideoManager { if (video.input.client) { video.input.client.disconnect(); } - + video.input.type = null; video.input.stream = null; video.input.client = null; video.input.roomId = null; - video.input.connectionState = 'paused'; + video.input.connectionState = "paused"; console.log(`Paused remote stream for video ${videoId}, can activate later`); } @@ -388,25 +425,30 @@ export class VideoManager { }); // Update input state atomically to prevent reactive loops - video.input.type = 'local-camera'; + video.input.type = "local-camera"; video.input.stream = stream; video.input.client = null; video.input.roomId = null; - video.input.connectionState = 'connected'; + video.input.connectionState = "connected"; video.input.preparedRoomId = null; - video.input.connectionPolicy = 'persistent'; + video.input.connectionPolicy = "persistent"; console.log(`Local camera connected to video ${videoId}`); return { success: true }; } catch (error) { console.error(`Failed to connect local camera to video ${videoId}:`, error); // Ensure clean state on error - video.input.connectionState = 'disconnected'; + video.input.connectionState = "disconnected"; return { success: false, error: error instanceof Error ? error.message : String(error) }; } } - async connectRemoteStream(workspaceId: string, videoId: string, roomId: string, policy: 'persistent' | 'lazy' = 'persistent'): Promise<{ success: boolean; error?: string }> { + async connectRemoteStream( + workspaceId: string, + videoId: string, + roomId: string, + policy: "persistent" | "lazy" = "persistent" + ): Promise<{ success: boolean; error?: string }> { const video = this.getVideo(videoId); if (!video) { return { success: false, error: `Video ${videoId} not found` }; @@ -417,29 +459,29 @@ export class VideoManager { await this.disconnectVideoInput(videoId); // Update connection state - video.input.connectionState = 'connecting'; + video.input.connectionState = "connecting"; const consumer = new videoClient.VideoConsumer(settings.transportServerUrl); - const connected = await consumer.connect(workspaceId, roomId, 'consumer-id'); - + const connected = await consumer.connect(workspaceId, roomId, "consumer-id"); + if (!connected) { - throw new Error('Failed to connect to remote stream'); + throw new Error("Failed to connect to remote stream"); } // Start receiving video await consumer.startReceiving(); // Set up stream receiving - consumer.on('streamReceived', (stream: MediaStream) => { + consumer.on("streamReceived", (stream: MediaStream) => { video.input.stream = stream; }); // Update input state - video.input.type = 'remote-stream'; + video.input.type = "remote-stream"; video.input.client = consumer; video.input.roomId = roomId; video.input.preparedRoomId = null; // Clear prepared since we're now connected - video.input.connectionState = 'connected'; + video.input.connectionState = "connected"; video.input.connectionPolicy = policy; console.log(`Remote stream connected to video ${videoId} with policy ${policy}`); @@ -461,9 +503,9 @@ export class VideoManager { try { // Stop local camera tracks if any - if (video.input.stream && video.input.type === 'local-camera') { + if (video.input.stream && video.input.type === "local-camera") { console.log(`Stopping ${video.input.stream.getTracks().length} camera tracks`); - video.input.stream.getTracks().forEach(track => { + video.input.stream.getTracks().forEach((track) => { console.log(`Stopping track: ${track.kind} (${track.label})`); track.stop(); }); @@ -480,9 +522,9 @@ export class VideoManager { video.input.stream = null; video.input.client = null; video.input.roomId = null; - video.input.connectionState = 'disconnected'; + video.input.connectionState = "disconnected"; video.input.preparedRoomId = null; - video.input.connectionPolicy = 'persistent'; + video.input.connectionPolicy = "persistent"; console.log(`Input successfully disconnected from video ${videoId}`); } catch (error) { @@ -492,34 +534,37 @@ export class VideoManager { video.input.stream = null; video.input.client = null; video.input.roomId = null; - video.input.connectionState = 'disconnected'; + video.input.connectionState = "disconnected"; video.input.preparedRoomId = null; - video.input.connectionPolicy = 'persistent'; + video.input.connectionPolicy = "persistent"; throw error; } } // ============= OUTPUT MANAGEMENT ============= - async startVideoOutput(workspaceId: string, videoId: string): Promise<{ success: boolean; error?: string; roomId?: string }> { + async startVideoOutput( + workspaceId: string, + videoId: string + ): Promise<{ success: boolean; error?: string; roomId?: string }> { const video = this.getVideo(videoId); if (!video) { return { success: false, error: `Video ${videoId} not found` }; } if (!video.canOutput) { - return { success: false, error: 'Cannot output - input must be local camera' }; + return { success: false, error: "Cannot output - input must be local camera" }; } try { const producer = new videoClient.VideoProducer(settings.transportServerUrl); - + // Create room const result = await producer.createRoom(workspaceId); - const connected = await producer.connect(result.workspaceId, result.roomId, 'producer-id'); - + const connected = await producer.connect(result.workspaceId, result.roomId, "producer-id"); + if (!connected) { - throw new Error('Failed to connect producer'); + throw new Error("Failed to connect producer"); } // Start camera with existing stream @@ -532,7 +577,7 @@ export class VideoManager { // Update output state video.output.active = true; - video.output.type = 'remote-broadcast'; + video.output.type = "remote-broadcast"; video.output.stream = video.input.stream; video.output.client = producer; video.output.roomId = result.roomId; @@ -580,4 +625,4 @@ export class VideoManager { } // Global video manager instance -export const videoManager = new VideoManager(); \ No newline at end of file +export const videoManager = new VideoManager(); diff --git a/src/lib/elements/video/videoConnection.svelte.ts b/src/lib/elements/video/videoConnection.svelte.ts index 66828bffc783707a2ec1b643e3c1b2239024be71..00409443dabf42aa58228f35c19d46b625e18cb6 100644 --- a/src/lib/elements/video/videoConnection.svelte.ts +++ b/src/lib/elements/video/videoConnection.svelte.ts @@ -3,48 +3,48 @@ * Clean and simple video producer/consumer management */ -import { video } from '@robothub/transport-server-client'; -import type { video as videoTypes } from '@robothub/transport-server-client'; -import { settings } from '$lib/runes/settings.svelte'; +import { video } from "@robothub/transport-server-client"; +import type { video as videoTypes } from "@robothub/transport-server-client"; +import { settings } from "$lib/runes/settings.svelte"; // Simple connection state using runes export class VideoConnectionState { - // Producer state - producer = $state({ - connected: false, - client: null as videoTypes.VideoProducer | null, - roomId: null as string | null, - stream: null as MediaStream | null, - }); - - // Consumer state - consumer = $state({ - connected: false, - client: null as videoTypes.VideoConsumer | null, - roomId: null as string | null, - stream: null as MediaStream | null, - }); - - // Room listing state - rooms = $state([]); - roomsLoading = $state(false); - - // Derived state - get hasProducer() { - return this.producer.connected; - } - - get hasConsumer() { - return this.consumer.connected; - } - - get isStreaming() { - return this.hasProducer && this.producer.stream !== null; - } - - get canConnectConsumer() { - return this.hasProducer && this.producer.roomId !== null; - } + // Producer state + producer = $state({ + connected: false, + client: null as videoTypes.VideoProducer | null, + roomId: null as string | null, + stream: null as MediaStream | null + }); + + // Consumer state + consumer = $state({ + connected: false, + client: null as videoTypes.VideoConsumer | null, + roomId: null as string | null, + stream: null as MediaStream | null + }); + + // Room listing state + rooms = $state([]); + roomsLoading = $state(false); + + // Derived state + get hasProducer() { + return this.producer.connected; + } + + get hasConsumer() { + return this.consumer.connected; + } + + get isStreaming() { + return this.hasProducer && this.producer.stream !== null; + } + + get canConnectConsumer() { + return this.hasProducer && this.producer.roomId !== null; + } } // Create global instance @@ -52,155 +52,159 @@ export const videoConnection = new VideoConnectionState(); // External action functions export const videoActions = { - - // Room management - async listRooms(workspaceId: string): Promise { - videoConnection.roomsLoading = true; - try { - const client = new video.VideoClientCore(settings.transportServerUrl); - const rooms = await client.listRooms(workspaceId); - videoConnection.rooms = rooms; - return rooms; - } catch (error) { - console.error('Failed to list rooms:', error); - videoConnection.rooms = []; - return []; - } finally { - videoConnection.roomsLoading = false; - } - }, - - async createRoom(workspaceId: string, roomId?: string): Promise { - try { - const client = new video.VideoClientCore(settings.transportServerUrl); - const result = await client.createRoom(workspaceId, roomId); - if (result) { - // Refresh room list - await this.listRooms(workspaceId); - return result.roomId; - } - return null; - } catch (error) { - console.error('Failed to create room:', error); - return null; - } - }, - - async deleteRoom(workspaceId: string, roomId: string): Promise { - try { - const client = new video.VideoClientCore(settings.transportServerUrl); - await client.deleteRoom(workspaceId, roomId); - // Refresh room list - await this.listRooms(workspaceId); - return true; - } catch (error) { - console.error('Failed to delete room:', error); - return false; - } - }, - - // Producer actions (simplified - only remote/local camera) - async connectProducer(workspaceId: string): Promise<{ success: boolean; error?: string; roomId?: string }> { - try { - const producer = new video.VideoProducer(settings.transportServerUrl); - - // Create or join room - const roomData = await producer.createRoom(workspaceId); - const connected = await producer.connect(roomData.workspaceId, roomData.roomId); - - if (!connected) { - throw new Error('Failed to connect producer'); - } - - // Start camera stream - const stream = await producer.startCamera({ - video: { width: 1280, height: 720 }, - audio: true - }); - - // Update state - videoConnection.producer.connected = true; - videoConnection.producer.client = producer; - videoConnection.producer.roomId = roomData.roomId; - videoConnection.producer.stream = stream; - - // Refresh room list - await this.listRooms(workspaceId); - - return { success: true, roomId: roomData.roomId }; - } catch (error) { - console.error('Failed to connect producer:', error); - return { success: false, error: error instanceof Error ? error.message : String(error) }; - } - }, - - async disconnectProducer(): Promise { - if (videoConnection.producer.client) { - videoConnection.producer.client.disconnect(); - } - if (videoConnection.producer.stream) { - videoConnection.producer.stream.getTracks().forEach(track => track.stop()); - } - - // Reset state - videoConnection.producer.connected = false; - videoConnection.producer.client = null; - videoConnection.producer.roomId = null; - videoConnection.producer.stream = null; - }, - - // Consumer actions (simplified - only remote consumer) - async connectConsumer(workspaceId: string, roomId: string): Promise<{ success: boolean; error?: string }> { - try { - const consumer = new video.VideoConsumer(settings.transportServerUrl); - const connected = await consumer.connect(workspaceId, roomId); - - if (!connected) { - throw new Error('Failed to connect consumer'); - } - - // Start receiving video - await consumer.startReceiving(); - - // Set up stream receiving - consumer.on('streamReceived', (stream: MediaStream) => { - videoConnection.consumer.stream = stream; - }); - - // Update state - videoConnection.consumer.connected = true; - videoConnection.consumer.client = consumer; - videoConnection.consumer.roomId = roomId; - - return { success: true }; - } catch (error) { - console.error('Failed to connect consumer:', error); - return { success: false, error: error instanceof Error ? error.message : String(error) }; - } - }, - - async disconnectConsumer(): Promise { - if (videoConnection.consumer.client) { - videoConnection.consumer.client.disconnect(); - } - - // Reset state - videoConnection.consumer.connected = false; - videoConnection.consumer.client = null; - videoConnection.consumer.roomId = null; - videoConnection.consumer.stream = null; - }, - - // Utility functions - async refreshRooms(workspaceId: string): Promise { - await this.listRooms(workspaceId); - }, - - getAvailableRooms(): videoTypes.RoomInfo[] { - return videoConnection.rooms.filter(room => room.participants.producer !== null); - }, - - getRoomById(roomId: string): videoTypes.RoomInfo | undefined { - return videoConnection.rooms.find(room => room.id === roomId); - } -}; \ No newline at end of file + // Room management + async listRooms(workspaceId: string): Promise { + videoConnection.roomsLoading = true; + try { + const client = new video.VideoClientCore(settings.transportServerUrl); + const rooms = await client.listRooms(workspaceId); + videoConnection.rooms = rooms; + return rooms; + } catch (error) { + console.error("Failed to list rooms:", error); + videoConnection.rooms = []; + return []; + } finally { + videoConnection.roomsLoading = false; + } + }, + + async createRoom(workspaceId: string, roomId?: string): Promise { + try { + const client = new video.VideoClientCore(settings.transportServerUrl); + const result = await client.createRoom(workspaceId, roomId); + if (result) { + // Refresh room list + await this.listRooms(workspaceId); + return result.roomId; + } + return null; + } catch (error) { + console.error("Failed to create room:", error); + return null; + } + }, + + async deleteRoom(workspaceId: string, roomId: string): Promise { + try { + const client = new video.VideoClientCore(settings.transportServerUrl); + await client.deleteRoom(workspaceId, roomId); + // Refresh room list + await this.listRooms(workspaceId); + return true; + } catch (error) { + console.error("Failed to delete room:", error); + return false; + } + }, + + // Producer actions (simplified - only remote/local camera) + async connectProducer( + workspaceId: string + ): Promise<{ success: boolean; error?: string; roomId?: string }> { + try { + const producer = new video.VideoProducer(settings.transportServerUrl); + + // Create or join room + const roomData = await producer.createRoom(workspaceId); + const connected = await producer.connect(roomData.workspaceId, roomData.roomId); + + if (!connected) { + throw new Error("Failed to connect producer"); + } + + // Start camera stream + const stream = await producer.startCamera({ + video: { width: 1280, height: 720 }, + audio: true + }); + + // Update state + videoConnection.producer.connected = true; + videoConnection.producer.client = producer; + videoConnection.producer.roomId = roomData.roomId; + videoConnection.producer.stream = stream; + + // Refresh room list + await this.listRooms(workspaceId); + + return { success: true, roomId: roomData.roomId }; + } catch (error) { + console.error("Failed to connect producer:", error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + }, + + async disconnectProducer(): Promise { + if (videoConnection.producer.client) { + videoConnection.producer.client.disconnect(); + } + if (videoConnection.producer.stream) { + videoConnection.producer.stream.getTracks().forEach((track) => track.stop()); + } + + // Reset state + videoConnection.producer.connected = false; + videoConnection.producer.client = null; + videoConnection.producer.roomId = null; + videoConnection.producer.stream = null; + }, + + // Consumer actions (simplified - only remote consumer) + async connectConsumer( + workspaceId: string, + roomId: string + ): Promise<{ success: boolean; error?: string }> { + try { + const consumer = new video.VideoConsumer(settings.transportServerUrl); + const connected = await consumer.connect(workspaceId, roomId); + + if (!connected) { + throw new Error("Failed to connect consumer"); + } + + // Start receiving video + await consumer.startReceiving(); + + // Set up stream receiving + consumer.on("streamReceived", (stream: MediaStream) => { + videoConnection.consumer.stream = stream; + }); + + // Update state + videoConnection.consumer.connected = true; + videoConnection.consumer.client = consumer; + videoConnection.consumer.roomId = roomId; + + return { success: true }; + } catch (error) { + console.error("Failed to connect consumer:", error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + }, + + async disconnectConsumer(): Promise { + if (videoConnection.consumer.client) { + videoConnection.consumer.client.disconnect(); + } + + // Reset state + videoConnection.consumer.connected = false; + videoConnection.consumer.client = null; + videoConnection.consumer.roomId = null; + videoConnection.consumer.stream = null; + }, + + // Utility functions + async refreshRooms(workspaceId: string): Promise { + await this.listRooms(workspaceId); + }, + + getAvailableRooms(): videoTypes.RoomInfo[] { + return videoConnection.rooms.filter((room) => room.participants.producer !== null); + }, + + getRoomById(roomId: string): videoTypes.RoomInfo | undefined { + return videoConnection.rooms.find((room) => room.id === roomId); + } +}; diff --git a/src/lib/elements/video/videoStreaming.svelte.ts b/src/lib/elements/video/videoStreaming.svelte.ts index 09fc8f32a801da38ec624995fcb8d601f94e5608..b302cdeaaf5c68ae52a98b05cfa8e1165454ea40 100644 --- a/src/lib/elements/video/videoStreaming.svelte.ts +++ b/src/lib/elements/video/videoStreaming.svelte.ts @@ -3,48 +3,48 @@ * Clean separation between input sources and output destinations */ -import { video } from '@robothub/transport-server-client'; -import type { video as videoTypes } from '@robothub/transport-server-client'; -import { settings } from '$lib/runes/settings.svelte'; +import { video } from "@robothub/transport-server-client"; +import type { video as videoTypes } from "@robothub/transport-server-client"; +import { settings } from "$lib/runes/settings.svelte"; // Input/Output state using runes export class VideoStreamingState { - // Input state (what you're viewing) - input = $state({ - type: null as 'local-camera' | 'remote-stream' | null, - stream: null as MediaStream | null, - client: null as videoTypes.VideoConsumer | null, - roomId: null as string | null, - }); - - // Output state (what you're broadcasting) - output = $state({ - active: false, - client: null as videoTypes.VideoProducer | null, - roomId: null as string | null, - }); - - // Room listing state - rooms = $state([]); - roomsLoading = $state(false); - - // Derived state - get hasInput() { - return this.input.type !== null && this.input.stream !== null; - } - - get hasOutput() { - return this.output.active; - } - - get canOutput() { - // Can only output if input is local camera (not remote stream) - return this.input.type === 'local-camera' && this.input.stream !== null; - } - - get currentStream() { - return this.input.stream; - } + // Input state (what you're viewing) + input = $state({ + type: null as "local-camera" | "remote-stream" | null, + stream: null as MediaStream | null, + client: null as videoTypes.VideoConsumer | null, + roomId: null as string | null + }); + + // Output state (what you're broadcasting) + output = $state({ + active: false, + client: null as videoTypes.VideoProducer | null, + roomId: null as string | null + }); + + // Room listing state + rooms = $state([]); + roomsLoading = $state(false); + + // Derived state + get hasInput() { + return this.input.type !== null && this.input.stream !== null; + } + + get hasOutput() { + return this.output.active; + } + + get canOutput() { + // Can only output if input is local camera (not remote stream) + return this.input.type === "local-camera" && this.input.stream !== null; + } + + get currentStream() { + return this.input.stream; + } } // Create global instance @@ -52,169 +52,177 @@ export const videoStreaming = new VideoStreamingState(); // External action functions export const videoActions = { - - // Room management - async listRooms(workspaceId: string): Promise { - videoStreaming.roomsLoading = true; - try { - const client = new video.VideoClientCore(settings.transportServerUrl); - const rooms = await client.listRooms(workspaceId); - videoStreaming.rooms = rooms; - return rooms; - } catch (error) { - console.error('Failed to list rooms:', error); - videoStreaming.rooms = []; - return []; - } finally { - videoStreaming.roomsLoading = false; - } - }, - - // Input actions - async connectLocalCamera(): Promise<{ success: boolean; error?: string }> { - try { - // Get local camera stream - no server connection needed for local viewing - const stream = await navigator.mediaDevices.getUserMedia({ - video: { width: 1280, height: 720 }, - audio: true - }); - - // First disconnect any existing input to avoid conflicts - await this.disconnectInput(); - - // Update input state - purely local, no server interaction - videoStreaming.input.type = 'local-camera'; - videoStreaming.input.stream = stream; - videoStreaming.input.client = null; - videoStreaming.input.roomId = null; - - console.log('Local camera connected (local viewing only)'); - return { success: true }; - } catch (error) { - console.error('Failed to connect local camera:', error); - return { success: false, error: error instanceof Error ? error.message : String(error) }; - } - }, - - async connectRemoteStream(workspaceId: string, roomId: string): Promise<{ success: boolean; error?: string }> { - try { - // First disconnect any existing input - await this.disconnectInput(); - - const consumer = new video.VideoConsumer(settings.transportServerUrl); - const connected = await consumer.connect(workspaceId, roomId, 'consumer-id'); - - if (!connected) { - throw new Error('Failed to connect to remote stream'); - } - - // Start receiving video - await consumer.startReceiving(); - - // Set up stream receiving - consumer.on('streamReceived', (stream: MediaStream) => { - videoStreaming.input.stream = stream; - }); - - // Update input state - videoStreaming.input.type = 'remote-stream'; - videoStreaming.input.client = consumer; - videoStreaming.input.roomId = roomId; - - console.log('Remote stream connected'); - return { success: true }; - } catch (error) { - console.error('Failed to connect remote stream:', error); - return { success: false, error: error instanceof Error ? error.message : String(error) }; - } - }, - - async disconnectInput(): Promise { - // Stop local camera tracks if any - if (videoStreaming.input.stream && videoStreaming.input.type === 'local-camera') { - videoStreaming.input.stream.getTracks().forEach(track => track.stop()); - } - - // Disconnect remote client if any - if (videoStreaming.input.client) { - videoStreaming.input.client.disconnect(); - } - - // Reset input state - videoStreaming.input.type = null; - videoStreaming.input.stream = null; - videoStreaming.input.client = null; - videoStreaming.input.roomId = null; - - console.log('Input disconnected'); - }, - - // Output actions - async startOutput(workspaceId: string): Promise<{ success: boolean; error?: string; roomId?: string }> { - if (!videoStreaming.canOutput) { - return { success: false, error: 'Cannot output - input must be local camera' }; - } - - try { - const producer = new video.VideoProducer(settings.transportServerUrl); - - // Create room - const roomData = await producer.createRoom(workspaceId); - const connected = await producer.connect(roomData.workspaceId, roomData.roomId, 'producer-id'); - - if (!connected) { - throw new Error('Failed to connect producer'); - } - - // Use the current input stream for output by starting camera with existing stream - if (videoStreaming.input.stream) { - // We need to use the producer's startCamera method properly - // For now, we'll start a new camera stream since we can't directly use existing stream - await producer.startCamera({ - video: { width: 1280, height: 720 }, - audio: true - }); - } - - // Update output state - videoStreaming.output.active = true; - videoStreaming.output.client = producer; - videoStreaming.output.roomId = roomData.roomId; - - // Refresh room list - await this.listRooms(workspaceId); - - console.log('Output started, room created:', roomData.roomId); - return { success: true, roomId: roomData.roomId }; - } catch (error) { - console.error('Failed to start output:', error); - return { success: false, error: error instanceof Error ? error.message : String(error) }; - } - }, - - async stopOutput(): Promise { - if (videoStreaming.output.client) { - videoStreaming.output.client.disconnect(); - } - - // Reset output state - videoStreaming.output.active = false; - videoStreaming.output.client = null; - videoStreaming.output.roomId = null; - - console.log('Output stopped'); - }, - - // Utility functions - async refreshRooms(workspaceId: string): Promise { - await this.listRooms(workspaceId); - }, - - getAvailableRooms(): videoTypes.RoomInfo[] { - return videoStreaming.rooms.filter(room => room.participants.producer !== null); - }, - - getRoomById(roomId: string): videoTypes.RoomInfo | undefined { - return videoStreaming.rooms.find(room => room.id === roomId); - } -}; \ No newline at end of file + // Room management + async listRooms(workspaceId: string): Promise { + videoStreaming.roomsLoading = true; + try { + const client = new video.VideoClientCore(settings.transportServerUrl); + const rooms = await client.listRooms(workspaceId); + videoStreaming.rooms = rooms; + return rooms; + } catch (error) { + console.error("Failed to list rooms:", error); + videoStreaming.rooms = []; + return []; + } finally { + videoStreaming.roomsLoading = false; + } + }, + + // Input actions + async connectLocalCamera(): Promise<{ success: boolean; error?: string }> { + try { + // Get local camera stream - no server connection needed for local viewing + const stream = await navigator.mediaDevices.getUserMedia({ + video: { width: 1280, height: 720 }, + audio: true + }); + + // First disconnect any existing input to avoid conflicts + await this.disconnectInput(); + + // Update input state - purely local, no server interaction + videoStreaming.input.type = "local-camera"; + videoStreaming.input.stream = stream; + videoStreaming.input.client = null; + videoStreaming.input.roomId = null; + + console.log("Local camera connected (local viewing only)"); + return { success: true }; + } catch (error) { + console.error("Failed to connect local camera:", error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + }, + + async connectRemoteStream( + workspaceId: string, + roomId: string + ): Promise<{ success: boolean; error?: string }> { + try { + // First disconnect any existing input + await this.disconnectInput(); + + const consumer = new video.VideoConsumer(settings.transportServerUrl); + const connected = await consumer.connect(workspaceId, roomId, "consumer-id"); + + if (!connected) { + throw new Error("Failed to connect to remote stream"); + } + + // Start receiving video + await consumer.startReceiving(); + + // Set up stream receiving + consumer.on("streamReceived", (stream: MediaStream) => { + videoStreaming.input.stream = stream; + }); + + // Update input state + videoStreaming.input.type = "remote-stream"; + videoStreaming.input.client = consumer; + videoStreaming.input.roomId = roomId; + + console.log("Remote stream connected"); + return { success: true }; + } catch (error) { + console.error("Failed to connect remote stream:", error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + }, + + async disconnectInput(): Promise { + // Stop local camera tracks if any + if (videoStreaming.input.stream && videoStreaming.input.type === "local-camera") { + videoStreaming.input.stream.getTracks().forEach((track) => track.stop()); + } + + // Disconnect remote client if any + if (videoStreaming.input.client) { + videoStreaming.input.client.disconnect(); + } + + // Reset input state + videoStreaming.input.type = null; + videoStreaming.input.stream = null; + videoStreaming.input.client = null; + videoStreaming.input.roomId = null; + + console.log("Input disconnected"); + }, + + // Output actions + async startOutput( + workspaceId: string + ): Promise<{ success: boolean; error?: string; roomId?: string }> { + if (!videoStreaming.canOutput) { + return { success: false, error: "Cannot output - input must be local camera" }; + } + + try { + const producer = new video.VideoProducer(settings.transportServerUrl); + + // Create room + const roomData = await producer.createRoom(workspaceId); + const connected = await producer.connect( + roomData.workspaceId, + roomData.roomId, + "producer-id" + ); + + if (!connected) { + throw new Error("Failed to connect producer"); + } + + // Use the current input stream for output by starting camera with existing stream + if (videoStreaming.input.stream) { + // We need to use the producer's startCamera method properly + // For now, we'll start a new camera stream since we can't directly use existing stream + await producer.startCamera({ + video: { width: 1280, height: 720 }, + audio: true + }); + } + + // Update output state + videoStreaming.output.active = true; + videoStreaming.output.client = producer; + videoStreaming.output.roomId = roomData.roomId; + + // Refresh room list + await this.listRooms(workspaceId); + + console.log("Output started, room created:", roomData.roomId); + return { success: true, roomId: roomData.roomId }; + } catch (error) { + console.error("Failed to start output:", error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + }, + + async stopOutput(): Promise { + if (videoStreaming.output.client) { + videoStreaming.output.client.disconnect(); + } + + // Reset output state + videoStreaming.output.active = false; + videoStreaming.output.client = null; + videoStreaming.output.roomId = null; + + console.log("Output stopped"); + }, + + // Utility functions + async refreshRooms(workspaceId: string): Promise { + await this.listRooms(workspaceId); + }, + + getAvailableRooms(): videoTypes.RoomInfo[] { + return videoStreaming.rooms.filter((room) => room.participants.producer !== null); + }, + + getRoomById(roomId: string): videoTypes.RoomInfo | undefined { + return videoStreaming.rooms.find((room) => room.id === roomId); + } +}; diff --git a/src/lib/runes/settings.svelte.ts b/src/lib/runes/settings.svelte.ts index d1641a393ccfdf0e8db6fcbaf95dbb7b88b19130..2791cd79e98f6d72b63863485811182da3932a3b 100644 --- a/src/lib/runes/settings.svelte.ts +++ b/src/lib/runes/settings.svelte.ts @@ -1,4 +1,4 @@ -import { env } from '$env/dynamic/public'; +import { env } from "$env/dynamic/public"; interface Settings { inferenceServerUrl: string; @@ -10,6 +10,8 @@ export const settings: Settings = $state({ // transportServerUrl: 'http://localhost:8000' // inferenceServerUrl: 'https://blanchon-robothub-inferenceserver.hf.space/api', // transportServerUrl: 'https://blanchon-robothub-transport-server.hf.space/api' - inferenceServerUrl: env.PUBLIC_INFERENCE_SERVER_URL ?? 'https://blanchon-robothub-inferenceserver.hf.space/api', - transportServerUrl: env.PUBLIC_TRANSPORT_SERVER_URL ?? 'https://blanchon-robothub-transportserver.hf.space/api' + inferenceServerUrl: + env.PUBLIC_INFERENCE_SERVER_URL ?? "https://blanchon-robothub-inferenceserver.hf.space/api", + transportServerUrl: + env.PUBLIC_TRANSPORT_SERVER_URL ?? "https://blanchon-robothub-transportserver.hf.space/api" }); diff --git a/src/lib/types/positionable.ts b/src/lib/types/positionable.ts index 8398a51c6e587a70ec795a56ab195806c74cf07a..aa40a5b066fd6b354cee0863f1a82aa1ef3c1786 100644 --- a/src/lib/types/positionable.ts +++ b/src/lib/types/positionable.ts @@ -1,9 +1,9 @@ -import type { Position3D } from '$lib/utils/positionUtils'; +import type { Position3D } from "$lib/utils/positionUtils"; export interface Positionable { - readonly id: string; - position: Position3D; - updatePosition(newPosition: Position3D): void; + readonly id: string; + position: Position3D; + updatePosition(newPosition: Position3D): void; } -export type { Position3D } from '$lib/utils/positionUtils'; \ No newline at end of file +export type { Position3D } from "$lib/utils/positionUtils"; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 3167bf89c14379d21c54ac1f77ef33223b8156ad..dda595710a3cb6f2ea61ea5beeebdd7557169ee6 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -12,7 +12,6 @@ export type WithoutChildren = T extends { children?: any } ? Omit = WithoutChildren>; export type WithElementRef = T & { ref?: U | null }; - // === Servo Position and Angle Conversion Functions === /** diff --git a/src/lib/utils/icon.ts b/src/lib/utils/icon.ts index c17b134847f7094ada631544ca62640c18cc1d07..fe6bb70d2fc667e49fd80321075a234d4742d463 100644 --- a/src/lib/utils/icon.ts +++ b/src/lib/utils/icon.ts @@ -8,68 +8,68 @@ interface Icon { // https://icon-sets.iconify.design export const ICON: Record = { - "icon-[material-symbols--upload]": { - svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTEgMTZWNy44NWwtMi42IDIuNkw3IDlsNS01bDUgNWwtMS40IDEuNDVsLTIuNi0yLjZWMTZ6bS01IDRxLS44MjUgMC0xLjQxMi0uNTg3VDQgMTh2LTNoMnYzaDEydi0zaDJ2M3EwIC44MjUtLjU4NyAxLjQxM1QxOCAyMHoiLz48L3N2Zz4=", - alt: "Upload" - }, - "icon-[material-symbols--download]": { - svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJtMTIgMTZsLTUtNWwxLjQtMS40NWwyLjYgMi42VjRoMnY4LjE1bDIuNi0yLjZMMTcgMTF6bS02IDRxLS44MjUgMC0xLjQxMi0uNTg3VDQgMTh2LTNoMnYzaDEydi0zaDJ2M3EwIC44MjUtLjU4NyAxLjQxM1QxOCAyMHoiLz48L3N2Zz4=", - alt: "Download" - }, - "icon-[ix--robotic-arm]": { - svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+PHBhdGggZmlsbD0iIzAwMCIgZD0ibTI4My4wMTYgNDguMzhsLTEzOS43MTMgODMuNTRhNTkuOSA1OS45IDAgMCAwLTE3LjU4NS0yLjYzMkM5MS42MzIgMTI5LjI4OCA2NCAxNTcuOTA0IDY0IDE5My4yMDRjMCAxNC41NDYgNC42OTIgMjcuOTU3IDEyLjU5NCAzOC42OTlhNjQgNjQgMCAwIDEtMi45MTgtNC4zMjVsNzUuNDEzIDEzNS4yOTVsLS4xMzcuNTU3YTg4LjMgODguMyAwIDAgMC0yLjQzOSAyMC42NzlsLjAxMyA0Mi42MDdsLTQxLjE1OC4wMDR2NDIuNjExaDI1Ny4yOTl2LTQyLjY2N2wtNTEuNTU5LjA1MmwtLjAxMi00Mi42MDdjMC0zNC4xMDYtMTkuMzQ2LTYzLjUzMy00Ny4yOTQtNzcuMTUzbC00LjQzOC0xLjk5OWwtNjAuMjE3LTEwOC4wMmwxMDUuMTExLTYyLjg1MWw0NS44NDEgODIuMzI1Yy0xLjkxOCA3LjA3Mi0xLjQ5NSAxNC44MTkgMS42MjEgMjEuODg0bDEuMzYzIDIuNzM5YzIuMDY5IDMuNzA5IDQuNzQ3IDYuODI4IDcuODI4IDkuMjk2bC4wMjggNDMuNjQ4bC0uMDM3LjE2NGwzOS43NDMgMTEuMDI5bDUuMzI1LTIwLjU4bC0yNC40NTctNi44MDJsLjAwMi0yMC44MThjNC42ODUtLjI2NSA5LjM4Mi0xLjY0NCAxMy43MjMtNC4yNGM0LjU5My0yLjc0NiA4LjI0Ny02LjUzMSAxMC44NDQtMTAuOTAzbDE4LjAxMyAxMC43NzlsLTYuNDg4IDI1LjE2MWwxOS44NzIgNS41MTRMNDQ4IDI0Mi4zNTdsLS4wNDYtLjAwNWwuMDM0LS4wNjdsLTM3Ljc0OS0yMi41OTFhMzIuNyAzMi43IDAgMCAwLTMuNzA2LTEwLjYxOWMtNC43OTQtOC41OTktMTIuODY0LTE0LjAxOC0yMS42MzUtMTUuNTQybC00Ny4xMy04NC41MTFjOC40NTEtMTMuMDcgOS41NjEtMzAuNTAzIDEuNDU0LTQ1LjA0NWMtMTEuMzYyLTIwLjM4LTM2LjUyNi0yNy4zNjQtNTYuMjA2LTE1LjU5N20tNTQuMjExIDI5My4xMThjMjEuNjkxIDAgMzkuNDYyIDE3LjM4MyA0MS4wMzIgMzkuNDMxbC4xMTMgMy4xOHY0Mi42MTFoLTgyLjI5MXYtNDIuNjExYzAtMjIuNDY0IDE2Ljc4NS00MC44NjcgMzguMDc1LTQyLjQ5NHptLTU4LjM5Ny0xMDQuMjFsMi4yNzgtMi42MTZsMzcuMDg3IDY2LjUwNmwuNzg1LS4xODhjLTEyLjUyNiAyLjkzNi0yMy45NjggOC44MzItMzMuNjA1IDE2LjkzOWwtMy41ODggMy4ybC0zNi4yOTQtNjUuMDg3bC0uNTYxLjEwNWMxMy4yMzItMi40MTcgMjUuMDEtOS4xOTkgMzMuODk4LTE4Ljg1OW0tNDQuNjktNjUuMzg5YzExLjM2MiAwIDIwLjU3MyA5LjUzOSAyMC41NzMgMjEuMzA1cy05LjIxMSAyMS4zMDYtMjAuNTczIDIxLjMwNnMtMjAuNTcyLTkuNTM5LTIwLjU3Mi0yMS4zMDZjMC0xMS43NjYgOS4yMS0yMS4zMDUgMjAuNTcyLTIxLjMwNW0xNzUuMzUtOTYuNzg0YzUuNjgxIDAgMTAuMjg2IDQuNzcgMTAuMjg2IDEwLjY1M3MtNC42MDUgMTAuNjUzLTEwLjI4NiAxMC42NTNzLTEwLjI4Ni00Ljc3LTEwLjI4Ni0xMC42NTNzNC42MDUtMTAuNjUzIDEwLjI4Ni0xMC42NTMiLz48L3N2Zz4=", - alt: "Robotic Arm" - }, - "icon-[mdi--video]": { - svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTcgMTAuNVY3YTEgMSAwIDAgMC0xLTFINGExIDEgMCAwIDAtMSAxdjEwYTEgMSAwIDAgMCAxIDFoMTJhMSAxIDAgMCAwIDEtMXYtMy41bDQgNHYtMTF6Ii8+PC9zdmc+", - alt: "Video" - }, - "icon-[mdi--video-off]" : { - svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMy4yNyAyTDIgMy4yN0w0LjczIDZINGExIDEgMCAwIDAtMSAxdjEwYTEgMSAwIDAgMCAxIDFoMTJjLjIgMCAuMzktLjA4LjU0LS4xOEwxOS43MyAyMUwyMSAxOS43M00yMSA2LjVsLTQgNFY3YTEgMSAwIDAgMC0xLTFIOS44MkwyMSAxNy4xOHoiLz48L3N2Zz4=", - alt: "Video off" - }, - "icon-[mdi--video-plus]" : { - svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTcgMTAuNVY3YTEgMSAwIDAgMC0xLTFINGExIDEgMCAwIDAtMSAxdjEwYTEgMSAwIDAgMCAxIDFoMTJhMSAxIDAgMCAwIDEtMXYtMy41bDQgNHYtMTF6TTE0IDEzaC0zdjNIOXYtM0g2di0yaDNWOGgydjNoM3oiLz48L3N2Zz4=", - alt: "Video Plus" - }, - "icon-[mdi--plus]": { - svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTkgMTNoLTZ2NmgtMnYtNkg1di0yaDZWNWgydjZoNnoiLz48L3N2Zz4=", - alt: "Plus" - }, - "icon-[material-symbols--lock]": { - svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNNiAyMnEtLjgyNSAwLTEuNDEyLS41ODdUNCAyMFYxMHEwLS44MjUuNTg4LTEuNDEyVDYgOGgxVjZxMC0yLjA3NSAxLjQ2My0zLjUzN1QxMiAxdDMuNTM4IDEuNDYzVDE3IDZ2MmgxcS44MjUgMCAxLjQxMy41ODhUMjAgMTB2MTBxMCAuODI1LS41ODcgMS40MTNUMTggMjJ6bTYtNXEuODI1IDAgMS40MTMtLjU4N1QxNCAxNXQtLjU4Ny0xLjQxMlQxMiAxM3QtMS40MTIuNTg4VDEwIDE1dC41ODggMS40MTNUMTIgMTdNOSA4aDZWNnEwLTEuMjUtLjg3NS0yLjEyNVQxMiAzdC0yLjEyNS44NzVUOSA2eiIvPjwvc3ZnPg==", - alt: "Lock" - }, - "icon-[solar--volume-knob-bold]": { - svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTEuMjUgNy4wNTZhNS4wMDEgNS4wMDEgMCAxIDAgMS41IDBWMTFhLjc1Ljc1IDAgMCAxLTEuNSAwek0xMyAzLjVhMSAxIDAgMSAxLTIgMGExIDEgMCAwIDEgMiAwbTcuNSA5LjVhMSAxIDAgMSAxIDAtMmExIDEgMCAwIDEgMCAybS0xNyAwYTEgMSAwIDEgMSAwLTJhMSAxIDAgMCAxIDAgMm0zLjE5Ny03LjcxOGExIDEgMCAxIDEtMS40MTQgMS40MTVhMSAxIDAgMCAxIDEuNDE0LTEuNDE1bTEyLjAyIDEyLjAyMWExIDEgMCAxIDEtMS40MTQgMS40MTVhMSAxIDAgMCAxIDEuNDE0LTEuNDE1bTAtMTAuNjA2YTEgMSAwIDEgMS0xLjQxNC0xLjQxNWExIDEgMCAwIDEgMS40MTQgMS40MTVNNi42OTcgMTguNzE4YTEgMSAwIDEgMS0xLjQxNC0xLjQxNWExIDEgMCAwIDEgMS40MTQgMS40MTUiLz48L3N2Zz4=", - alt: "Volume Knob" - }, - "icon-[mingcute--settings-2-fill]": { - svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Im0xMi41OTMgMjMuMjU4bC0uMDExLjAwMmwtLjA3MS4wMzVsLS4wMi4wMDRsLS4wMTQtLjAwNGwtLjA3MS0uMDM1cS0uMDE2LS4wMDUtLjAyNC4wMDVsLS4wMDQuMDFsLS4wMTcuNDI4bC4wMDUuMDJsLjAxLjAxM2wuMTA0LjA3NGwuMDE1LjAwNGwuMDEyLS4wMDRsLjEwNC0uMDc0bC4wMTItLjAxNmwuMDA0LS4wMTdsLS4wMTctLjQyN3EtLjAwNC0uMDE2LS4wMTctLjAxOG0uMjY1LS4xMTNsLS4wMTMuMDAybC0uMTg1LjA5M2wtLjAxLjAxbC0uMDAzLjAxMWwuMDE4LjQzbC4wMDUuMDEybC4wMDguMDA3bC4yMDEuMDkzcS4wMTkuMDA1LjAyOS0uMDA4bC4wMDQtLjAxNGwtLjAzNC0uNjE0cS0uMDA1LS4wMTgtLjAyLS4wMjJtLS43MTUuMDAyYS4wMi4wMiAwIDAgMC0uMDI3LjAwNmwtLjAwNi4wMTRsLS4wMzQuNjE0cS4wMDEuMDE4LjAxNy4wMjRsLjAxNS0uMDAybC4yMDEtLjA5M2wuMDEtLjAwOGwuMDA0LS4wMTFsLjAxNy0uNDNsLS4wMDMtLjAxMmwtLjAxLS4wMXoiLz48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTguNSA0YTEuNSAxLjUgMCAwIDAtMyAwdi41SDRhMS41IDEuNSAwIDEgMCAwIDNoMTEuNVY4YTEuNSAxLjUgMCAwIDAgMyAwdi0uNUgyMGExLjUgMS41IDAgMCAwIDAtM2gtMS41ek00IDEwLjVhMS41IDEuNSAwIDAgMCAwIDNoMS41di41YTEuNSAxLjUgMCAwIDAgMyAwdi0uNUgyMGExLjUgMS41IDAgMCAwIDAtM0g4LjVWMTBhMS41IDEuNSAwIDEgMC0zIDB2LjV6TTIuNSAxOEExLjUgMS41IDAgMCAxIDQgMTYuNWgxMS41VjE2YTEuNSAxLjUgMCAwIDEgMyAwdi41SDIwYTEuNSAxLjUgMCAwIDEgMCAzaC0xLjV2LjVhMS41IDEuNSAwIDAgMS0zIDB2LS41SDRBMS41IDEuNSAwIDAgMSAyLjUgMTgiLz48L2c+PC9zdmc+", - alt: "Settings" - }, - "icon-[iconamoon--lightning-1-duotone]": { - svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48ZyBmaWxsPSJub25lIj48cGF0aCBmaWxsPSIjMDAwIiBkPSJtNiAxNGw3LTEydjhoNWwtNyAxMnYtOHoiIG9wYWNpdHk9IjAuMTYiLz48cGF0aCBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMiIgZD0ibTYgMTRsNy0xMnY4aDVsLTcgMTJ2LTh6Ii8+PC9nPjwvc3ZnPg==", - alt: "Lightning" - }, - "icon-[formkit--arrowright]": { - svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSI5IiB2aWV3Qm94PSIwIDAgMTYgOSI+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTEyLjUgNWgtOWMtLjI4IDAtLjUtLjIyLS41LS41cy4yMi0uNS41LS41aDljLjI4IDAgLjUuMjIuNS41cy0uMjIuNS0uNS41Ii8+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTEwIDguNWEuNDcuNDcgMCAwIDEtLjM1LS4xNWMtLjItLjItLjItLjUxIDAtLjcxbDMuMTUtMy4xNWwtMy4xNS0zLjE1Yy0uMi0uMi0uMi0uNTEgMC0uNzFzLjUxLS4yLjcxIDBsMy41IDMuNWMuMi4yLjIuNTEgMCAuNzFsLTMuNSAzLjVjLS4xLjEtLjIzLjE1LS4zNS4xNVoiLz48L3N2Zz4=", - alt: "Arrow Right" - }, - "icon-[formkit--arrowup]": { - svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iLTMuNSAwIDE2IDE2Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNNC41IDE0Yy0uMjggMC0uNS0uMjItLjUtLjV2LTljMC0uMjguMjItLjUuNS0uNXMuNS4yMi41LjV2OWMwIC4yOC0uMjIuNS0uNS41Ii8+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTggNy41YS40Ny40NyAwIDAgMS0uMzUtLjE1TDQuNSA0LjJMMS4zNSA3LjM1Yy0uMi4yLS41MS4yLS43MSAwcy0uMi0uNTEgMC0uNzFsMy41LTMuNWMuMi0uMi41MS0uMi43MSAwbDMuNSAzLjVjLjIuMi4yLjUxIDAgLjcxYy0uMS4xLS4yMy4xNS0uMzUuMTUiLz48L3N2Zz4=", - alt: "Arrow up" - }, - "icon-[formkit--arrowdown]": { - svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iLTMuNSAwIDE2IDE2Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNNC41IDEzYy0uMjggMC0uNS0uMjItLjUtLjV2LTljMC0uMjguMjItLjUuNS0uNXMuNS4yMi41LjV2OWMwIC4yOC0uMjIuNS0uNS41Ii8+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTQuNSAxNGEuNDcuNDcgMCAwIDEtLjM1LS4xNWwtMy41LTMuNWMtLjItLjItLjItLjUxIDAtLjcxcy41MS0uMi43MSAwbDMuMTUgMy4xNWwzLjE1LTMuMTVjLjItLjIuNTEtLjIuNzEgMHMuMi41MSAwIC43MWwtMy41IDMuNWMtLjEuMS0uMjMuMTUtLjM1LjE1WiIvPjwvc3ZnPg==", - alt: "Arrow down" - }, - "icon-[formkit--arrowleft]": { - svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSI5IiB2aWV3Qm94PSIwIDAgMTYgOSI+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTEyLjUgNWgtOWMtLjI4IDAtLjUtLjIyLS41LS41cy4yMi0uNS41LS41aDljLjI4IDAgLjUuMjIuNS41cy0uMjIuNS0uNS41Ii8+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTYgOC41YS40Ny40NyAwIDAgMS0uMzUtLjE1bC0zLjUtMy41Yy0uMi0uMi0uMi0uNTEgMC0uNzFMNS42NS42NWMuMi0uMi41MS0uMi43MSAwcy4yLjUxIDAgLjcxTDMuMjEgNC41MWwzLjE1IDMuMTVjLjIuMi4yLjUxIDAgLjcxYy0uMS4xLS4yMy4xNS0uMzUuMTVaIi8+PC9zdmc+", - alt: "Arrow left" - }, - "icon-[mdi--brain]": { - svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMjEuMzMgMTIuOTFjLjA5IDEuNTUtLjYyIDMuMDQtMS44OSAzLjk1bC43NyAxLjQ5Yy4yMy40NS4yNi45OC4wNiAxLjQ1Yy0uMTkuNDctLjU4Ljg0LTEuMDYgMWwtLjc5LjI1YTEuNjkgMS42OSAwIDAgMS0xLjg2LS41NUwxNC40NCAxOGMtLjg5LS4xNS0xLjczLS41My0yLjQ0LTEuMWMtLjUuMTUtMSAuMjMtMS41LjIzYy0uODggMC0xLjc2LS4yNy0yLjUtLjc5Yy0uNTMuMTYtMS4wNy4yMy0xLjYyLjIyYy0uNzkuMDEtMS41Ny0uMTUtMi4zLS40NWE0LjEgNC4xIDAgMCAxLTIuNDMtMy42MWMtLjA4LS43Mi4wNC0xLjQ1LjM1LTIuMTFjLS4yOS0uNzUtLjMyLTEuNTctLjA3LTIuMzNDMi4zIDcuMTEgMyA2LjMyIDMuODcgNS44MmMuNTgtMS42OSAyLjIxLTIuODIgNC0yLjdjMS42LTEuNSA0LjA1LTEuNjYgNS44My0uMzdjLjQyLS4xMS44Ni0uMTcgMS4zLS4xN2MxLjM2LS4wMyAyLjY1LjU3IDMuNSAxLjY0YzIuMDQuNTMgMy41IDIuMzUgMy41OCA0LjQ3Yy4wNSAxLjExLS4yNSAyLjItLjg2IDMuMTNjLjA3LjM2LjExLjcyLjExIDEuMDltLTUtMS40MWMuNTcuMDcgMS4wMi41IDEuMDIgMS4wN2ExIDEgMCAwIDEtMSAxaC0uNjNjLS4zMi45LS44OCAxLjY5LTEuNjIgMi4yOWMuMjUuMDkuNTEuMTQuNzcuMjFjNS4xMy0uMDcgNC41My0zLjIgNC41My0zLjI1YTIuNTkgMi41OSAwIDAgMC0yLjY5LTIuNDlhMSAxIDAgMCAxLTEtMWExIDEgMCAwIDEgMS0xYzEuMjMuMDMgMi40MS40OSAzLjMzIDEuM2MuMDUtLjI5LjA4LS41OS4wOC0uODljLS4wNi0xLjI0LS42Mi0yLjMyLTIuODctMi41M2MtMS4yNS0yLjk2LTQuNC0xLjMyLTQuNC0uNGMtLjAzLjIzLjIxLjcyLjI1Ljc1YTEgMSAwIDAgMSAxIDFjMCAuNTUtLjQ1IDEtMSAxYy0uNTMtLjAyLTEuMDMtLjIyLTEuNDMtLjU2Yy0uNDguMzEtMS4wMy41LTEuNi41NmMtLjU3LjA1LTEuMDQtLjM1LTEuMDctLjlhLjk3Ljk3IDAgMCAxIC44OC0xLjFjLjE2LS4wMi45NC0uMTQuOTQtLjc3YzAtLjY2LjI1LTEuMjkuNjgtMS43OWMtLjkyLS4yNS0xLjkxLjA4LTIuOTEgMS4yOUM2Ljc1IDUgNiA1LjI1IDUuNDUgNy4yQzQuNSA3LjY3IDQgOCAzLjc4IDljMS4wOC0uMjIgMi4xOS0uMTMgMy4yMi4yNWMuNS4xOS43OC43NS41OSAxLjI5Yy0uMTkuNTItLjc3Ljc4LTEuMjkuNTljLS43My0uMzItMS41NS0uMzQtMi4zLS4wNmMtLjMyLjI3LS4zMi44My0uMzIgMS4yN2MwIC43NC4zNyAxLjQzIDEgMS44M2MuNTMuMjcgMS4xMi40MSAxLjcxLjRxLS4yMjUtLjM5LS4zOS0uODFhMS4wMzggMS4wMzggMCAwIDEgMS45Ni0uNjhjLjQgMS4xNCAxLjQyIDEuOTIgMi42MiAyLjA1YzEuMzctLjA3IDIuNTktLjg4IDMuMTktMi4xM2MuMjMtMS4zOCAxLjM0LTEuNSAyLjU2LTEuNW0yIDcuNDdsLS42Mi0xLjNsLS43MS4xNmwxIDEuMjV6bS00LjY1LTguNjFhMSAxIDAgMCAwLS45MS0xLjAzYy0uNzEtLjA0LTEuNC4yLTEuOTMuNjdjLS41Ny41OC0uODcgMS4zOC0uODQgMi4xOWExIDEgMCAwIDAgMSAxYy41NyAwIDEtLjQ1IDEtMWMwLS4yNy4wNy0uNTQuMjMtLjc2Yy4xMi0uMS4yNy0uMTUuNDMtLjE1Yy41NS4wMyAxLjAyLS4zOCAxLjAyLS45MiIvPjwvc3ZnPg==", - alt: "Brain" - }, -} + "icon-[material-symbols--upload]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTEgMTZWNy44NWwtMi42IDIuNkw3IDlsNS01bDUgNWwtMS40IDEuNDVsLTIuNi0yLjZWMTZ6bS01IDRxLS44MjUgMC0xLjQxMi0uNTg3VDQgMTh2LTNoMnYzaDEydi0zaDJ2M3EwIC44MjUtLjU4NyAxLjQxM1QxOCAyMHoiLz48L3N2Zz4=", + alt: "Upload" + }, + "icon-[material-symbols--download]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJtMTIgMTZsLTUtNWwxLjQtMS40NWwyLjYgMi42VjRoMnY4LjE1bDIuNi0yLjZMMTcgMTF6bS02IDRxLS44MjUgMC0xLjQxMi0uNTg3VDQgMTh2LTNoMnYzaDEydi0zaDJ2M3EwIC44MjUtLjU4NyAxLjQxM1QxOCAyMHoiLz48L3N2Zz4=", + alt: "Download" + }, + "icon-[ix--robotic-arm]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+PHBhdGggZmlsbD0iIzAwMCIgZD0ibTI4My4wMTYgNDguMzhsLTEzOS43MTMgODMuNTRhNTkuOSA1OS45IDAgMCAwLTE3LjU4NS0yLjYzMkM5MS42MzIgMTI5LjI4OCA2NCAxNTcuOTA0IDY0IDE5My4yMDRjMCAxNC41NDYgNC42OTIgMjcuOTU3IDEyLjU5NCAzOC42OTlhNjQgNjQgMCAwIDEtMi45MTgtNC4zMjVsNzUuNDEzIDEzNS4yOTVsLS4xMzcuNTU3YTg4LjMgODguMyAwIDAgMC0yLjQzOSAyMC42NzlsLjAxMyA0Mi42MDdsLTQxLjE1OC4wMDR2NDIuNjExaDI1Ny4yOTl2LTQyLjY2N2wtNTEuNTU5LjA1MmwtLjAxMi00Mi42MDdjMC0zNC4xMDYtMTkuMzQ2LTYzLjUzMy00Ny4yOTQtNzcuMTUzbC00LjQzOC0xLjk5OWwtNjAuMjE3LTEwOC4wMmwxMDUuMTExLTYyLjg1MWw0NS44NDEgODIuMzI1Yy0xLjkxOCA3LjA3Mi0xLjQ5NSAxNC44MTkgMS42MjEgMjEuODg0bDEuMzYzIDIuNzM5YzIuMDY5IDMuNzA5IDQuNzQ3IDYuODI4IDcuODI4IDkuMjk2bC4wMjggNDMuNjQ4bC0uMDM3LjE2NGwzOS43NDMgMTEuMDI5bDUuMzI1LTIwLjU4bC0yNC40NTctNi44MDJsLjAwMi0yMC44MThjNC42ODUtLjI2NSA5LjM4Mi0xLjY0NCAxMy43MjMtNC4yNGM0LjU5My0yLjc0NiA4LjI0Ny02LjUzMSAxMC44NDQtMTAuOTAzbDE4LjAxMyAxMC43NzlsLTYuNDg4IDI1LjE2MWwxOS44NzIgNS41MTRMNDQ4IDI0Mi4zNTdsLS4wNDYtLjAwNWwuMDM0LS4wNjdsLTM3Ljc0OS0yMi41OTFhMzIuNyAzMi43IDAgMCAwLTMuNzA2LTEwLjYxOWMtNC43OTQtOC41OTktMTIuODY0LTE0LjAxOC0yMS42MzUtMTUuNTQybC00Ny4xMy04NC41MTFjOC40NTEtMTMuMDcgOS41NjEtMzAuNTAzIDEuNDU0LTQ1LjA0NWMtMTEuMzYyLTIwLjM4LTM2LjUyNi0yNy4zNjQtNTYuMjA2LTE1LjU5N20tNTQuMjExIDI5My4xMThjMjEuNjkxIDAgMzkuNDYyIDE3LjM4MyA0MS4wMzIgMzkuNDMxbC4xMTMgMy4xOHY0Mi42MTFoLTgyLjI5MXYtNDIuNjExYzAtMjIuNDY0IDE2Ljc4NS00MC44NjcgMzguMDc1LTQyLjQ5NHptLTU4LjM5Ny0xMDQuMjFsMi4yNzgtMi42MTZsMzcuMDg3IDY2LjUwNmwuNzg1LS4xODhjLTEyLjUyNiAyLjkzNi0yMy45NjggOC44MzItMzMuNjA1IDE2LjkzOWwtMy41ODggMy4ybC0zNi4yOTQtNjUuMDg3bC0uNTYxLjEwNWMxMy4yMzItMi40MTcgMjUuMDEtOS4xOTkgMzMuODk4LTE4Ljg1OW0tNDQuNjktNjUuMzg5YzExLjM2MiAwIDIwLjU3MyA5LjUzOSAyMC41NzMgMjEuMzA1cy05LjIxMSAyMS4zMDYtMjAuNTczIDIxLjMwNnMtMjAuNTcyLTkuNTM5LTIwLjU3Mi0yMS4zMDZjMC0xMS43NjYgOS4yMS0yMS4zMDUgMjAuNTcyLTIxLjMwNW0xNzUuMzUtOTYuNzg0YzUuNjgxIDAgMTAuMjg2IDQuNzcgMTAuMjg2IDEwLjY1M3MtNC42MDUgMTAuNjUzLTEwLjI4NiAxMC42NTNzLTEwLjI4Ni00Ljc3LTEwLjI4Ni0xMC42NTNzNC42MDUtMTAuNjUzIDEwLjI4Ni0xMC42NTMiLz48L3N2Zz4=", + alt: "Robotic Arm" + }, + "icon-[mdi--video]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTcgMTAuNVY3YTEgMSAwIDAgMC0xLTFINGExIDEgMCAwIDAtMSAxdjEwYTEgMSAwIDAgMCAxIDFoMTJhMSAxIDAgMCAwIDEtMXYtMy41bDQgNHYtMTF6Ii8+PC9zdmc+", + alt: "Video" + }, + "icon-[mdi--video-off]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMy4yNyAyTDIgMy4yN0w0LjczIDZINGExIDEgMCAwIDAtMSAxdjEwYTEgMSAwIDAgMCAxIDFoMTJjLjIgMCAuMzktLjA4LjU0LS4xOEwxOS43MyAyMUwyMSAxOS43M00yMSA2LjVsLTQgNFY3YTEgMSAwIDAgMC0xLTFIOS44MkwyMSAxNy4xOHoiLz48L3N2Zz4=", + alt: "Video off" + }, + "icon-[mdi--video-plus]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTcgMTAuNVY3YTEgMSAwIDAgMC0xLTFINGExIDEgMCAwIDAtMSAxdjEwYTEgMSAwIDAgMCAxIDFoMTJhMSAxIDAgMCAwIDEtMXYtMy41bDQgNHYtMTF6TTE0IDEzaC0zdjNIOXYtM0g2di0yaDNWOGgydjNoM3oiLz48L3N2Zz4=", + alt: "Video Plus" + }, + "icon-[mdi--plus]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTkgMTNoLTZ2NmgtMnYtNkg1di0yaDZWNWgydjZoNnoiLz48L3N2Zz4=", + alt: "Plus" + }, + "icon-[material-symbols--lock]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNNiAyMnEtLjgyNSAwLTEuNDEyLS41ODdUNCAyMFYxMHEwLS44MjUuNTg4LTEuNDEyVDYgOGgxVjZxMC0yLjA3NSAxLjQ2My0zLjUzN1QxMiAxdDMuNTM4IDEuNDYzVDE3IDZ2MmgxcS44MjUgMCAxLjQxMy41ODhUMjAgMTB2MTBxMCAuODI1LS41ODcgMS40MTNUMTggMjJ6bTYtNXEuODI1IDAgMS40MTMtLjU4N1QxNCAxNXQtLjU4Ny0xLjQxMlQxMiAxM3QtMS40MTIuNTg4VDEwIDE1dC41ODggMS40MTNUMTIgMTdNOSA4aDZWNnEwLTEuMjUtLjg3NS0yLjEyNVQxMiAzdC0yLjEyNS44NzVUOSA2eiIvPjwvc3ZnPg==", + alt: "Lock" + }, + "icon-[solar--volume-knob-bold]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTEuMjUgNy4wNTZhNS4wMDEgNS4wMDEgMCAxIDAgMS41IDBWMTFhLjc1Ljc1IDAgMCAxLTEuNSAwek0xMyAzLjVhMSAxIDAgMSAxLTIgMGExIDEgMCAwIDEgMiAwbTcuNSA5LjVhMSAxIDAgMSAxIDAtMmExIDEgMCAwIDEgMCAybS0xNyAwYTEgMSAwIDEgMSAwLTJhMSAxIDAgMCAxIDAgMm0zLjE5Ny03LjcxOGExIDEgMCAxIDEtMS40MTQgMS40MTVhMSAxIDAgMCAxIDEuNDE0LTEuNDE1bTEyLjAyIDEyLjAyMWExIDEgMCAxIDEtMS40MTQgMS40MTVhMSAxIDAgMCAxIDEuNDE0LTEuNDE1bTAtMTAuNjA2YTEgMSAwIDEgMS0xLjQxNC0xLjQxNWExIDEgMCAwIDEgMS40MTQgMS40MTVNNi42OTcgMTguNzE4YTEgMSAwIDEgMS0xLjQxNC0xLjQxNWExIDEgMCAwIDEgMS40MTQgMS40MTUiLz48L3N2Zz4=", + alt: "Volume Knob" + }, + "icon-[mingcute--settings-2-fill]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Im0xMi41OTMgMjMuMjU4bC0uMDExLjAwMmwtLjA3MS4wMzVsLS4wMi4wMDRsLS4wMTQtLjAwNGwtLjA3MS0uMDM1cS0uMDE2LS4wMDUtLjAyNC4wMDVsLS4wMDQuMDFsLS4wMTcuNDI4bC4wMDUuMDJsLjAxLjAxM2wuMTA0LjA3NGwuMDE1LjAwNGwuMDEyLS4wMDRsLjEwNC0uMDc0bC4wMTItLjAxNmwuMDA0LS4wMTdsLS4wMTctLjQyN3EtLjAwNC0uMDE2LS4wMTctLjAxOG0uMjY1LS4xMTNsLS4wMTMuMDAybC0uMTg1LjA5M2wtLjAxLjAxbC0uMDAzLjAxMWwuMDE4LjQzbC4wMDUuMDEybC4wMDguMDA3bC4yMDEuMDkzcS4wMTkuMDA1LjAyOS0uMDA4bC4wMDQtLjAxNGwtLjAzNC0uNjE0cS0uMDA1LS4wMTgtLjAyLS4wMjJtLS43MTUuMDAyYS4wMi4wMiAwIDAgMC0uMDI3LjAwNmwtLjAwNi4wMTRsLS4wMzQuNjE0cS4wMDEuMDE4LjAxNy4wMjRsLjAxNS0uMDAybC4yMDEtLjA5M2wuMDEtLjAwOGwuMDA0LS4wMTFsLjAxNy0uNDNsLS4wMDMtLjAxMmwtLjAxLS4wMXoiLz48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTguNSA0YTEuNSAxLjUgMCAwIDAtMyAwdi41SDRhMS41IDEuNSAwIDEgMCAwIDNoMTEuNVY4YTEuNSAxLjUgMCAwIDAgMyAwdi0uNUgyMGExLjUgMS41IDAgMCAwIDAtM2gtMS41ek00IDEwLjVhMS41IDEuNSAwIDAgMCAwIDNoMS41di41YTEuNSAxLjUgMCAwIDAgMyAwdi0uNUgyMGExLjUgMS41IDAgMCAwIDAtM0g4LjVWMTBhMS41IDEuNSAwIDEgMC0zIDB2LjV6TTIuNSAxOEExLjUgMS41IDAgMCAxIDQgMTYuNWgxMS41VjE2YTEuNSAxLjUgMCAwIDEgMyAwdi41SDIwYTEuNSAxLjUgMCAwIDEgMCAzaC0xLjV2LjVhMS41IDEuNSAwIDAgMS0zIDB2LS41SDRBMS41IDEuNSAwIDAgMSAyLjUgMTgiLz48L2c+PC9zdmc+", + alt: "Settings" + }, + "icon-[iconamoon--lightning-1-duotone]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48ZyBmaWxsPSJub25lIj48cGF0aCBmaWxsPSIjMDAwIiBkPSJtNiAxNGw3LTEydjhoNWwtNyAxMnYtOHoiIG9wYWNpdHk9IjAuMTYiLz48cGF0aCBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMiIgZD0ibTYgMTRsNy0xMnY4aDVsLTcgMTJ2LTh6Ii8+PC9nPjwvc3ZnPg==", + alt: "Lightning" + }, + "icon-[formkit--arrowright]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSI5IiB2aWV3Qm94PSIwIDAgMTYgOSI+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTEyLjUgNWgtOWMtLjI4IDAtLjUtLjIyLS41LS41cy4yMi0uNS41LS41aDljLjI4IDAgLjUuMjIuNS41cy0uMjIuNS0uNS41Ii8+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTEwIDguNWEuNDcuNDcgMCAwIDEtLjM1LS4xNWMtLjItLjItLjItLjUxIDAtLjcxbDMuMTUtMy4xNWwtMy4xNS0zLjE1Yy0uMi0uMi0uMi0uNTEgMC0uNzFzLjUxLS4yLjcxIDBsMy41IDMuNWMuMi4yLjIuNTEgMCAuNzFsLTMuNSAzLjVjLS4xLjEtLjIzLjE1LS4zNS4xNVoiLz48L3N2Zz4=", + alt: "Arrow Right" + }, + "icon-[formkit--arrowup]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iLTMuNSAwIDE2IDE2Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNNC41IDE0Yy0uMjggMC0uNS0uMjItLjUtLjV2LTljMC0uMjguMjItLjUuNS0uNXMuNS4yMi41LjV2OWMwIC4yOC0uMjIuNS0uNS41Ii8+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTggNy41YS40Ny40NyAwIDAgMS0uMzUtLjE1TDQuNSA0LjJMMS4zNSA3LjM1Yy0uMi4yLS41MS4yLS43MSAwcy0uMi0uNTEgMC0uNzFsMy41LTMuNWMuMi0uMi41MS0uMi43MSAwbDMuNSAzLjVjLjIuMi4yLjUxIDAgLjcxYy0uMS4xLS4yMy4xNS0uMzUuMTUiLz48L3N2Zz4=", + alt: "Arrow up" + }, + "icon-[formkit--arrowdown]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iLTMuNSAwIDE2IDE2Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNNC41IDEzYy0uMjggMC0uNS0uMjItLjUtLjV2LTljMC0uMjguMjItLjUuNS0uNXMuNS4yMi41LjV2OWMwIC4yOC0uMjIuNS0uNS41Ii8+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTQuNSAxNGEuNDcuNDcgMCAwIDEtLjM1LS4xNWwtMy41LTMuNWMtLjItLjItLjItLjUxIDAtLjcxcy41MS0uMi43MSAwbDMuMTUgMy4xNWwzLjE1LTMuMTVjLjItLjIuNTEtLjIuNzEgMHMuMi41MSAwIC43MWwtMy41IDMuNWMtLjEuMS0uMjMuMTUtLjM1LjE1WiIvPjwvc3ZnPg==", + alt: "Arrow down" + }, + "icon-[formkit--arrowleft]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSI5IiB2aWV3Qm94PSIwIDAgMTYgOSI+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTEyLjUgNWgtOWMtLjI4IDAtLjUtLjIyLS41LS41cy4yMi0uNS41LS41aDljLjI4IDAgLjUuMjIuNS41cy0uMjIuNS0uNS41Ii8+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTYgOC41YS40Ny40NyAwIDAgMS0uMzUtLjE1bC0zLjUtMy41Yy0uMi0uMi0uMi0uNTEgMC0uNzFMNS42NS42NWMuMi0uMi41MS0uMi43MSAwcy4yLjUxIDAgLjcxTDMuMjEgNC41MWwzLjE1IDMuMTVjLjIuMi4yLjUxIDAgLjcxYy0uMS4xLS4yMy4xNS0uMzUuMTVaIi8+PC9zdmc+", + alt: "Arrow left" + }, + "icon-[mdi--brain]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMjEuMzMgMTIuOTFjLjA5IDEuNTUtLjYyIDMuMDQtMS44OSAzLjk1bC43NyAxLjQ5Yy4yMy40NS4yNi45OC4wNiAxLjQ1Yy0uMTkuNDctLjU4Ljg0LTEuMDYgMWwtLjc5LjI1YTEuNjkgMS42OSAwIDAgMS0xLjg2LS41NUwxNC40NCAxOGMtLjg5LS4xNS0xLjczLS41My0yLjQ0LTEuMWMtLjUuMTUtMSAuMjMtMS41LjIzYy0uODggMC0xLjc2LS4yNy0yLjUtLjc5Yy0uNTMuMTYtMS4wNy4yMy0xLjYyLjIyYy0uNzkuMDEtMS41Ny0uMTUtMi4zLS40NWE0LjEgNC4xIDAgMCAxLTIuNDMtMy42MWMtLjA4LS43Mi4wNC0xLjQ1LjM1LTIuMTFjLS4yOS0uNzUtLjMyLTEuNTctLjA3LTIuMzNDMi4zIDcuMTEgMyA2LjMyIDMuODcgNS44MmMuNTgtMS42OSAyLjIxLTIuODIgNC0yLjdjMS42LTEuNSA0LjA1LTEuNjYgNS44My0uMzdjLjQyLS4xMS44Ni0uMTcgMS4zLS4xN2MxLjM2LS4wMyAyLjY1LjU3IDMuNSAxLjY0YzIuMDQuNTMgMy41IDIuMzUgMy41OCA0LjQ3Yy4wNSAxLjExLS4yNSAyLjItLjg2IDMuMTNjLjA3LjM2LjExLjcyLjExIDEuMDltLTUtMS40MWMuNTcuMDcgMS4wMi41IDEuMDIgMS4wN2ExIDEgMCAwIDEtMSAxaC0uNjNjLS4zMi45LS44OCAxLjY5LTEuNjIgMi4yOWMuMjUuMDkuNTEuMTQuNzcuMjFjNS4xMy0uMDcgNC41My0zLjIgNC41My0zLjI1YTIuNTkgMi41OSAwIDAgMC0yLjY5LTIuNDlhMSAxIDAgMCAxLTEtMWExIDEgMCAwIDEgMS0xYzEuMjMuMDMgMi40MS40OSAzLjMzIDEuM2MuMDUtLjI5LjA4LS41OS4wOC0uODljLS4wNi0xLjI0LS42Mi0yLjMyLTIuODctMi41M2MtMS4yNS0yLjk2LTQuNC0xLjMyLTQuNC0uNGMtLjAzLjIzLjIxLjcyLjI1Ljc1YTEgMSAwIDAgMSAxIDFjMCAuNTUtLjQ1IDEtMSAxYy0uNTMtLjAyLTEuMDMtLjIyLTEuNDMtLjU2Yy0uNDguMzEtMS4wMy41LTEuNi41NmMtLjU3LjA1LTEuMDQtLjM1LTEuMDctLjlhLjk3Ljk3IDAgMCAxIC44OC0xLjFjLjE2LS4wMi45NC0uMTQuOTQtLjc3YzAtLjY2LjI1LTEuMjkuNjgtMS43OWMtLjkyLS4yNS0xLjkxLjA4LTIuOTEgMS4yOUM2Ljc1IDUgNiA1LjI1IDUuNDUgNy4yQzQuNSA3LjY3IDQgOCAzLjc4IDljMS4wOC0uMjIgMi4xOS0uMTMgMy4yMi4yNWMuNS4xOS43OC43NS41OSAxLjI5Yy0uMTkuNTItLjc3Ljc4LTEuMjkuNTljLS43My0uMzItMS41NS0uMzQtMi4zLS4wNmMtLjMyLjI3LS4zMi44My0uMzIgMS4yN2MwIC43NC4zNyAxLjQzIDEgMS44M2MuNTMuMjcgMS4xMi40MSAxLjcxLjRxLS4yMjUtLjM5LS4zOS0uODFhMS4wMzggMS4wMzggMCAwIDEgMS45Ni0uNjhjLjQgMS4xNCAxLjQyIDEuOTIgMi42MiAyLjA1YzEuMzctLjA3IDIuNTktLjg4IDMuMTktMi4xM2MuMjMtMS4zOCAxLjM0LTEuNSAyLjU2LTEuNW0yIDcuNDdsLS42Mi0xLjNsLS43MS4xNmwxIDEuMjV6bS00LjY1LTguNjFhMSAxIDAgMCAwLS45MS0xLjAzYy0uNzEtLjA0LTEuNC4yLTEuOTMuNjdjLS41Ny41OC0uODcgMS4zOC0uODQgMi4xOWExIDEgMCAwIDAgMSAxYy41NyAwIDEtLjQ1IDEtMWMwLS4yNy4wNy0uNTQuMjMtLjc2Yy4xMi0uMS4yNy0uMTUuNDMtLjE1Yy41NS4wMyAxLjAyLS4zOCAxLjAyLS45MiIvPjwvc3ZnPg==", + alt: "Brain" + } +}; diff --git a/src/lib/utils/positionManager.ts b/src/lib/utils/positionManager.ts index cd28b9af28b3d1eb5f17ff171f4129f104179a1b..c17caa6ef5aaaaf813386ec5817c1d2ef151f653 100644 --- a/src/lib/utils/positionManager.ts +++ b/src/lib/utils/positionManager.ts @@ -1,93 +1,95 @@ -import type { Position3D } from './positionUtils'; +import type { Position3D } from "./positionUtils"; /** * Spiral-based position manager * Assigns positions in a spiral pattern starting from center to avoid overlapping objects - * + * * Pattern: Center -> Right -> Up -> Left -> Down -> Right (outward spiral) * Example positions: (0,0) -> (1,0) -> (1,-1) -> (0,-1) -> (-1,-1) -> (-1,0) -> (-1,1) -> (0,1) -> (1,1) -> (2,1) ... */ export class PositionManager { - private static instance: PositionManager; - private gridSize = 5; // Distance between grid points - private spiralGenerator: Generator<{ x: number; z: number }, never, unknown>; + private static instance: PositionManager; + private gridSize = 5; // Distance between grid points + private spiralGenerator: Generator<{ x: number; z: number }, never, unknown>; - static getInstance(): PositionManager { - if (!PositionManager.instance) { - PositionManager.instance = new PositionManager(); - } - return PositionManager.instance; - } + static getInstance(): PositionManager { + if (!PositionManager.instance) { + PositionManager.instance = new PositionManager(); + } + return PositionManager.instance; + } - constructor() { - this.spiralGenerator = this.generateSpiralPositions(); - // Skip the center position since there's already an object there - this.spiralGenerator.next(); - } + constructor() { + this.spiralGenerator = this.generateSpiralPositions(); + // Skip the center position since there's already an object there + this.spiralGenerator.next(); + } - /** - * Get next available position in a spiral pattern - * Starts from center (0,0) and spirals outward - */ - getNextPosition(): Position3D { - const { value: coord } = this.spiralGenerator.next(); - - return { - x: coord.x * this.gridSize, - y: 0, - z: coord.z * this.gridSize - }; - } + /** + * Get next available position in a spiral pattern + * Starts from center (0,0) and spirals outward + */ + getNextPosition(): Position3D { + const { value: coord } = this.spiralGenerator.next(); - /** - * Generator function that yields spiral positions infinitely - * Uses a simple clockwise spiral starting from origin - */ - private *generateSpiralPositions(): Generator<{ x: number; z: number }, never, unknown> { - let x = 0, z = 0; - let dx = 1, dz = 0; // Start moving right - let steps = 1; - let stepCount = 0; - let changeDirection = 0; + return { + x: coord.x * this.gridSize, + y: 0, + z: coord.z * this.gridSize + }; + } - // Yield center position first - yield { x, z }; + /** + * Generator function that yields spiral positions infinitely + * Uses a simple clockwise spiral starting from origin + */ + private *generateSpiralPositions(): Generator<{ x: number; z: number }, never, unknown> { + let x = 0, + z = 0; + let dx = 1, + dz = 0; // Start moving right + let steps = 1; + let stepCount = 0; + let changeDirection = 0; - // Generate spiral positions infinitely - while (true) { - x += dx; - z += dz; - yield { x, z }; - - stepCount++; - - // Change direction when we've completed the required steps - if (stepCount === steps) { - stepCount = 0; - changeDirection++; - - // Rotate 90 degrees clockwise: (dx, dz) -> (dz, -dx) - const temp = dx; - dx = dz; - dz = -temp; - - // Increase step count after every two direction changes - if (changeDirection % 2 === 0) { - steps++; - } - } - } - } + // Yield center position first + yield { x, z }; - /** - * Reset position generator (useful for testing) - */ - reset(): void { - this.spiralGenerator = this.generateSpiralPositions(); - // Skip the center position since there's already an object there - this.spiralGenerator.next(); - } + // Generate spiral positions infinitely + while (true) { + x += dx; + z += dz; + yield { x, z }; + + stepCount++; + + // Change direction when we've completed the required steps + if (stepCount === steps) { + stepCount = 0; + changeDirection++; + + // Rotate 90 degrees clockwise: (dx, dz) -> (dz, -dx) + const temp = dx; + dx = dz; + dz = -temp; + + // Increase step count after every two direction changes + if (changeDirection % 2 === 0) { + steps++; + } + } + } + } + + /** + * Reset position generator (useful for testing) + */ + reset(): void { + this.spiralGenerator = this.generateSpiralPositions(); + // Skip the center position since there's already an object there + this.spiralGenerator.next(); + } } // Global instance -export const positionManager = PositionManager.getInstance(); \ No newline at end of file +export const positionManager = PositionManager.getInstance(); diff --git a/src/lib/utils/positionUtils.ts b/src/lib/utils/positionUtils.ts index deed782527d6b7eabae4e476ed690111f725a619..26c5d543bbd262decfb6fae24fbb4118ecad2be4 100644 --- a/src/lib/utils/positionUtils.ts +++ b/src/lib/utils/positionUtils.ts @@ -45,4 +45,4 @@ export function getDistance(pos1: Position3D, pos2: Position3D): number { const dy = pos2.y - pos1.y; const dz = pos2.z - pos1.z; return Math.sqrt(dx * dx + dy * dy + dz * dz); -} \ No newline at end of file +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index aa5e2b6f2a62e5b38d7c26b916c9599aa27d67c6..eed5279b8ec334fd858bfe1028a63d2896e4f85c 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -18,7 +18,8 @@ toastOptions={{ unstyled: true, classes: { - toast: "rounded-lg shadow-xl p-4 flex items-start gap-3 min-w-[300px] max-w-[450px] border bg-white border-slate-200 text-slate-900 dark:bg-slate-900 dark:border-slate-700 dark:text-slate-100", + toast: + "rounded-lg shadow-xl p-4 flex items-start gap-3 min-w-[300px] max-w-[450px] border bg-white border-slate-200 text-slate-900 dark:bg-slate-900 dark:border-slate-700 dark:text-slate-100", title: "font-medium text-sm leading-tight", description: "text-sm mt-1 leading-relaxed opacity-90", actionButton: @@ -27,12 +28,15 @@ "bg-slate-200 hover:bg-slate-300 text-slate-700 px-3 py-1.5 rounded-md text-xs font-medium transition-colors duration-200 dark:bg-slate-700 dark:hover:bg-slate-600 dark:text-slate-300", closeButton: "text-current opacity-70 hover:opacity-100 transition-opacity duration-200 p-1 rounded-md hover:bg-slate-100 dark:hover:bg-white/10", - success: "!bg-green-50 !border-green-200 !text-green-800 dark:!bg-green-950 dark:!border-green-700 dark:!text-green-200", - error: "!bg-red-50 !border-red-200 !text-red-800 dark:!bg-red-950 dark:!border-red-700 dark:!text-red-200", - warning: "!bg-yellow-50 !border-yellow-200 !text-yellow-800 dark:!bg-yellow-950 dark:!border-yellow-700 dark:!text-yellow-200", + success: + "!bg-green-50 !border-green-200 !text-green-800 dark:!bg-green-950 dark:!border-green-700 dark:!text-green-200", + error: + "!bg-red-50 !border-red-200 !text-red-800 dark:!bg-red-950 dark:!border-red-700 dark:!text-red-200", + warning: + "!bg-yellow-50 !border-yellow-200 !text-yellow-800 dark:!bg-yellow-950 dark:!border-yellow-700 dark:!text-yellow-200", info: "!bg-blue-50 !border-blue-200 !text-blue-800 dark:!bg-blue-950 dark:!border-blue-700 dark:!text-blue-200", - loading: "!bg-slate-50 !border-slate-200 !text-slate-800 dark:!bg-slate-900 dark:!border-slate-700 dark:!text-slate-200" + loading: + "!bg-slate-50 !border-slate-200 !text-slate-800 dark:!bg-slate-900 dark:!border-slate-700 dark:!text-slate-200" } }} /> - diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index d6b14f7cc1fd76bfcdf520969a914b8e04b03547..0c25f79bbb01b36fba2c3b3c5681381f1e3e0f7b 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -76,8 +76,6 @@ {:else}
-
- Loading -
+
Loading
{/if} diff --git a/static-server.js b/static-server.js index 11f75e9f350248a3b897a27d8ce3898d2188d459..b1b38cd571e5339ce667e990ff489b14a5663b13 100755 --- a/static-server.js +++ b/static-server.js @@ -8,99 +8,98 @@ const BUILD_DIR = "./build"; // MIME types for common web assets const MIME_TYPES = { - '.html': 'text/html', - '.css': 'text/css', - '.js': 'application/javascript', - '.json': 'application/json', - '.png': 'image/png', - '.jpg': 'image/jpeg', - '.jpeg': 'image/jpeg', - '.gif': 'image/gif', - '.svg': 'image/svg+xml', - '.ico': 'image/x-icon', - '.woff': 'font/woff', - '.woff2': 'font/woff2', - '.ttf': 'font/ttf', - '.eot': 'application/vnd.ms-fontobject', - '.webp': 'image/webp', - '.avif': 'image/avif', - '.mp4': 'video/mp4', - '.webm': 'video/webm' + ".html": "text/html", + ".css": "text/css", + ".js": "application/javascript", + ".json": "application/json", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".svg": "image/svg+xml", + ".ico": "image/x-icon", + ".woff": "font/woff", + ".woff2": "font/woff2", + ".ttf": "font/ttf", + ".eot": "application/vnd.ms-fontobject", + ".webp": "image/webp", + ".avif": "image/avif", + ".mp4": "video/mp4", + ".webm": "video/webm" }; function getMimeType(filename) { - const ext = filename.substring(filename.lastIndexOf('.')); - return MIME_TYPES[ext] || 'application/octet-stream'; + const ext = filename.substring(filename.lastIndexOf(".")); + return MIME_TYPES[ext] || "application/octet-stream"; } const server = Bun.serve({ - port: PORT, - hostname: "0.0.0.0", - - async fetch(req) { - const url = new URL(req.url); - let pathname = url.pathname; - - // Remove leading slash and default to index.html for root - if (pathname === '/') { - pathname = 'index.html'; - } else { - pathname = pathname.substring(1); // Remove leading slash - } - - try { - // Try to serve the requested file - const filePath = join(BUILD_DIR, pathname); - const file = Bun.file(filePath); - - if (await file.exists()) { - const mimeType = getMimeType(pathname); - const headers = { - 'Content-Type': mimeType, - }; - - // Set cache headers - if (pathname.includes('/_app/immutable/')) { - // Long-term cache for immutable assets - headers['Cache-Control'] = 'public, max-age=31536000, immutable'; - } else if (pathname.endsWith('.html')) { - // No cache for HTML files - headers['Cache-Control'] = 'public, max-age=0, must-revalidate'; - } else { - // Short cache for other assets - headers['Cache-Control'] = 'public, max-age=3600'; - } - - return new Response(file, { headers }); - } - - // If file not found and no extension, serve index.html for SPA routing - if (!pathname.includes('.')) { - const indexFile = Bun.file(join(BUILD_DIR, 'index.html')); - if (await indexFile.exists()) { - return new Response(indexFile, { - headers: { - 'Content-Type': 'text/html', - 'Cache-Control': 'public, max-age=0, must-revalidate' - } - }); - } - } - - return new Response('Not Found', { - status: 404, - headers: { 'Content-Type': 'text/plain' } - }); - - } catch (error) { - console.error('Server error:', error); - return new Response('Internal Server Error', { - status: 500, - headers: { 'Content-Type': 'text/plain' } - }); - } - } + port: PORT, + hostname: "0.0.0.0", + + async fetch(req) { + const url = new URL(req.url); + let pathname = url.pathname; + + // Remove leading slash and default to index.html for root + if (pathname === "/") { + pathname = "index.html"; + } else { + pathname = pathname.substring(1); // Remove leading slash + } + + try { + // Try to serve the requested file + const filePath = join(BUILD_DIR, pathname); + const file = Bun.file(filePath); + + if (await file.exists()) { + const mimeType = getMimeType(pathname); + const headers = { + "Content-Type": mimeType + }; + + // Set cache headers + if (pathname.includes("/_app/immutable/")) { + // Long-term cache for immutable assets + headers["Cache-Control"] = "public, max-age=31536000, immutable"; + } else if (pathname.endsWith(".html")) { + // No cache for HTML files + headers["Cache-Control"] = "public, max-age=0, must-revalidate"; + } else { + // Short cache for other assets + headers["Cache-Control"] = "public, max-age=3600"; + } + + return new Response(file, { headers }); + } + + // If file not found and no extension, serve index.html for SPA routing + if (!pathname.includes(".")) { + const indexFile = Bun.file(join(BUILD_DIR, "index.html")); + if (await indexFile.exists()) { + return new Response(indexFile, { + headers: { + "Content-Type": "text/html", + "Cache-Control": "public, max-age=0, must-revalidate" + } + }); + } + } + + return new Response("Not Found", { + status: 404, + headers: { "Content-Type": "text/plain" } + }); + } catch (error) { + console.error("Server error:", error); + return new Response("Internal Server Error", { + status: 500, + headers: { "Content-Type": "text/plain" } + }); + } + } }); console.log(`πŸš€ Static server running on http://localhost:${server.port}`); -console.log(`πŸ“ Serving files from: ${BUILD_DIR}`); \ No newline at end of file +console.log(`πŸ“ Serving files from: ${BUILD_DIR}`); diff --git a/static/gpu/scene.gltf b/static/gpu/scene.gltf index 8c51a65b308f2a7a52ea2055e7271d7951817554..18135a19bc9e7e0b7a1e76214c6a755a3dfc32c2 100644 --- a/static/gpu/scene.gltf +++ b/static/gpu/scene.gltf @@ -1,2494 +1,1620 @@ { - "accessors": [ - { - "bufferView": 2, - "componentType": 5126, - "count": 18466, - "max": [ - 224.7469940185547, - 25.571533203125, - 88.23554992675781 - ], - "min": [ - -224.74513244628906, - -35.34562683105469, - -88.20793914794922 - ], - "type": "VEC3" - }, - { - "bufferView": 2, - "byteOffset": 221592, - "componentType": 5126, - "count": 18466, - "max": [ - 1.0, - 1.0, - 1.0 - ], - "min": [ - -1.0, - -1.0, - -1.0 - ], - "type": "VEC3" - }, - { - "bufferView": 3, - "componentType": 5126, - "count": 18466, - "max": [ - 0.8829709887504578, - 1.0, - 1.0, - 1.0 - ], - "min": [ - -0.8874375224113464, - -1.0, - -1.0, - -1.0 - ], - "type": "VEC4" - }, - { - "bufferView": 1, - "componentType": 5126, - "count": 18466, - "max": [ - 0.9974566698074341, - 1.0000001192092896 - ], - "min": [ - 0.0, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 1, - "byteOffset": 147728, - "componentType": 5126, - "count": 18466, - "max": [ - 0.9974566698074341, - 1.0000001192092896 - ], - "min": [ - 0.0, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 1, - "byteOffset": 295456, - "componentType": 5126, - "count": 18466, - "max": [ - 0.9974566698074341, - 1.0000001192092896 - ], - "min": [ - 0.0, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 0, - "componentType": 5125, - "count": 80496, - "type": "SCALAR" - }, - { - "bufferView": 2, - "byteOffset": 443184, - "componentType": 5126, - "count": 14, - "max": [ - 115.76972961425781, - 0.0, - 100.0 - ], - "min": [ - -97.66373443603516, - 0.0, - -100.0 - ], - "type": "VEC3" - }, - { - "bufferView": 2, - "byteOffset": 443352, - "componentType": 5126, - "count": 14, - "max": [ - 0.0, - 1.0, - 0.0 - ], - "min": [ - 0.0, - 1.0, - 0.0 - ], - "type": "VEC3" - }, - { - "bufferView": 3, - "byteOffset": 295456, - "componentType": 5126, - "count": 14, - "max": [ - 0.0, - 0.0, - 1.0, - 1.0 - ], - "min": [ - -1.4227993005988537e-07, - 0.0, - 1.0, - 1.0 - ], - "type": "VEC4" - }, - { - "bufferView": 1, - "byteOffset": 443184, - "componentType": 5126, - "count": 14, - "max": [ - 0.5462319254875183, - 0.5829209089279175 - ], - "min": [ - 0.0, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 1, - "byteOffset": 443296, - "componentType": 5126, - "count": 14, - "max": [ - 0.5462319254875183, - 0.5829209089279175 - ], - "min": [ - 0.0, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 1, - "byteOffset": 443408, - "componentType": 5126, - "count": 14, - "max": [ - 0.5462319254875183, - 0.5829209089279175 - ], - "min": [ - 0.0, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 0, - "byteOffset": 321984, - "componentType": 5125, - "count": 36, - "type": "SCALAR" - }, - { - "bufferView": 2, - "byteOffset": 443520, - "componentType": 5126, - "count": 1792, - "max": [ - 99.35418701171875, - 3.8504819869995117, - 99.35418701171875 - ], - "min": [ - -99.35418701171875, - -36.769859313964844, - -99.35418701171875 - ], - "type": "VEC3" - }, - { - "bufferView": 2, - "byteOffset": 465024, - "componentType": 5126, - "count": 1792, - "max": [ - 1.0, - 0.9999999403953552, - 1.0 - ], - "min": [ - -1.0, - -0.9999999403953552, - -1.0 - ], - "type": "VEC3" - }, - { - "bufferView": 3, - "byteOffset": 295680, - "componentType": 5126, - "count": 1792, - "max": [ - 1.0, - 1.0, - 1.0, - 1.0 - ], - "min": [ - -1.0, - -1.0, - -1.0, - 1.0 - ], - "type": "VEC4" - }, - { - "bufferView": 1, - "byteOffset": 443520, - "componentType": 5126, - "count": 1792, - "max": [ - 0.0, - 0.0 - ], - "min": [ - 0.0, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 1, - "byteOffset": 457856, - "componentType": 5126, - "count": 1792, - "max": [ - 0.0, - 0.0 - ], - "min": [ - 0.0, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 1, - "byteOffset": 472192, - "componentType": 5126, - "count": 1792, - "max": [ - 0.0, - 0.0 - ], - "min": [ - 0.0, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 0, - "byteOffset": 322128, - "componentType": 5125, - "count": 9216, - "type": "SCALAR" - }, - { - "bufferView": 2, - "byteOffset": 486528, - "componentType": 5126, - "count": 5519, - "max": [ - 243.8433380126953, - 3.845768690109253, - 245.13909912109375 - ], - "min": [ - -245.8629150390625, - -81.39239501953125, - -244.58486938476563 - ], - "type": "VEC3" - }, - { - "bufferView": 2, - "byteOffset": 552756, - "componentType": 5126, - "count": 5519, - "max": [ - 0.9999997615814209, - 0.9999998807907104, - 0.9999997615814209 - ], - "min": [ - -0.9999997615814209, - -0.9999996423721313, - -0.9999997615814209 - ], - "type": "VEC3" - }, - { - "bufferView": 3, - "byteOffset": 324352, - "componentType": 5126, - "count": 5519, - "max": [ - 0.9960846900939941, - 0.9999998211860657, - 0.9974399209022522, - 1.0 - ], - "min": [ - -0.9998785257339478, - -0.9999998211860657, - -0.9908028244972229, - -1.0 - ], - "type": "VEC4" - }, - { - "bufferView": 1, - "byteOffset": 486528, - "componentType": 5126, - "count": 5519, - "max": [ - 0.8715417385101318, - 1.0 - ], - "min": [ - 0.0001247699256055057, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 1, - "byteOffset": 530680, - "componentType": 5126, - "count": 5519, - "max": [ - 0.8715417385101318, - 1.0 - ], - "min": [ - 0.0001247699256055057, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 1, - "byteOffset": 574832, - "componentType": 5126, - "count": 5519, - "max": [ - 0.8715417385101318, - 1.0 - ], - "min": [ - 0.0001247699256055057, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 0, - "byteOffset": 358992, - "componentType": 5125, - "count": 24528, - "type": "SCALAR" - }, - { - "bufferView": 2, - "byteOffset": 618984, - "componentType": 5126, - "count": 948, - "max": [ - 89.06101989746094, - 3.585411310195923, - 89.0611343383789 - ], - "min": [ - -89.06101989746094, - -81.14639282226563, - -89.06101989746094 - ], - "type": "VEC3" - }, - { - "bufferView": 2, - "byteOffset": 630360, - "componentType": 5126, - "count": 948, - "max": [ - 0.9999997615814209, - 1.0, - 0.9999997615814209 - ], - "min": [ - -0.9999997615814209, - -1.0, - -0.9999997615814209 - ], - "type": "VEC3" - }, - { - "bufferView": 1, - "byteOffset": 618984, - "componentType": 5126, - "count": 948, - "max": [ - 0.8170047998428345, - 0.5114779472351074 - ], - "min": [ - 0.7189478874206543, - 0.2939568758010864 - ], - "type": "VEC2" - }, - { - "bufferView": 0, - "byteOffset": 457104, - "componentType": 5125, - "count": 4224, - "type": "SCALAR" - }, - { - "bufferView": 2, - "byteOffset": 641736, - "componentType": 5126, - "count": 136, - "max": [ - 77.20637512207031, - -1.1121496754640248e-06, - 25.964923858642578 - ], - "min": [ - -77.20637512207031, - -10.26113510131836, - -53.971092224121094 - ], - "type": "VEC3" - }, - { - "bufferView": 2, - "byteOffset": 643368, - "componentType": 5126, - "count": 136, - "max": [ - 0.995922327041626, - 1.0, - 0.9986786842346191 - ], - "min": [ - -0.995922327041626, - -1.0, - -0.9876721501350403 - ], - "type": "VEC3" - }, - { - "bufferView": 3, - "byteOffset": 412656, - "componentType": 5126, - "count": 136, - "max": [ - 0.8069314956665039, - 0.6983179450035095, - 1.0, - 1.0 - ], - "min": [ - -0.5064710974693298, - -1.0, - -0.995922327041626, - 1.0 - ], - "type": "VEC4" - }, - { - "bufferView": 1, - "byteOffset": 626568, - "componentType": 5126, - "count": 136, - "max": [ - 0.9828680753707886, - 0.9973888397216797 - ], - "min": [ - 0.0, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 1, - "byteOffset": 627656, - "componentType": 5126, - "count": 136, - "max": [ - 0.9828680753707886, - 0.9973888397216797 - ], - "min": [ - 0.0, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 1, - "byteOffset": 628744, - "componentType": 5126, - "count": 136, - "max": [ - 0.9828680753707886, - 0.9973888397216797 - ], - "min": [ - 0.0, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 0, - "byteOffset": 474000, - "componentType": 5125, - "count": 348, - "type": "SCALAR" - }, - { - "bufferView": 2, - "byteOffset": 645000, - "componentType": 5126, - "count": 260, - "max": [ - 72.35018920898438, - -1.4828661960564204e-06, - 13.962870597839355 - ], - "min": [ - -82.06255340576172, - -10.26113510131836, - -65.97315216064453 - ], - "type": "VEC3" - }, - { - "bufferView": 2, - "byteOffset": 648120, - "componentType": 5126, - "count": 260, - "max": [ - 0.9959227442741394, - 1.0, - 0.9986786842346191 - ], - "min": [ - -0.9959227442741394, - -1.0, - -0.9876720309257507 - ], - "type": "VEC3" - }, - { - "bufferView": 3, - "byteOffset": 414832, - "componentType": 5126, - "count": 260, - "max": [ - 0.850715160369873, - 1.0, - 1.0, - 1.0 - ], - "min": [ - -0.5064702033996582, - -1.0, - -0.9959227442741394, - 1.0 - ], - "type": "VEC4" - }, - { - "bufferView": 1, - "byteOffset": 629832, - "componentType": 5126, - "count": 260, - "max": [ - 0.7645499110221863, - 1.0000001192092896 - ], - "min": [ - 0.028024811297655106, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 1, - "byteOffset": 631912, - "componentType": 5126, - "count": 260, - "max": [ - 0.7645499110221863, - 1.0000001192092896 - ], - "min": [ - 0.028024811297655106, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 1, - "byteOffset": 633992, - "componentType": 5126, - "count": 260, - "max": [ - 0.7645499110221863, - 1.0000001192092896 - ], - "min": [ - 0.028024811297655106, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 0, - "byteOffset": 475392, - "componentType": 5125, - "count": 708, - "type": "SCALAR" - }, - { - "bufferView": 2, - "byteOffset": 651240, - "componentType": 5126, - "count": 1792, - "max": [ - 99.35418701171875, - 3.8504819869995117, - 99.35418701171875 - ], - "min": [ - -99.35418701171875, - -36.769859313964844, - -99.35418701171875 - ], - "type": "VEC3" - }, - { - "bufferView": 2, - "byteOffset": 672744, - "componentType": 5126, - "count": 1792, - "max": [ - 1.0, - 0.9999999403953552, - 1.0 - ], - "min": [ - -1.0, - -0.9999999403953552, - -1.0 - ], - "type": "VEC3" - }, - { - "bufferView": 3, - "byteOffset": 418992, - "componentType": 5126, - "count": 1792, - "max": [ - 1.0, - 1.0, - 1.0, - 1.0 - ], - "min": [ - -1.0, - -1.0, - -1.0, - 1.0 - ], - "type": "VEC4" - }, - { - "bufferView": 1, - "byteOffset": 636072, - "componentType": 5126, - "count": 1792, - "max": [ - 0.0, - 0.0 - ], - "min": [ - 0.0, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 1, - "byteOffset": 650408, - "componentType": 5126, - "count": 1792, - "max": [ - 0.0, - 0.0 - ], - "min": [ - 0.0, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 1, - "byteOffset": 664744, - "componentType": 5126, - "count": 1792, - "max": [ - 0.0, - 0.0 - ], - "min": [ - 0.0, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 0, - "byteOffset": 478224, - "componentType": 5125, - "count": 9216, - "type": "SCALAR" - }, - { - "bufferView": 2, - "byteOffset": 694248, - "componentType": 5126, - "count": 568, - "max": [ - 100.02538299560547, - 0.0050859092734754086, - 100.0043716430664 - ], - "min": [ - -102.13546752929688, - -3.877331256866455, - -104.43467712402344 - ], - "type": "VEC3" - }, - { - "bufferView": 2, - "byteOffset": 701064, - "componentType": 5126, - "count": 568, - "max": [ - 0.9999963641166687, - 1.0, - 0.999996542930603 - ], - "min": [ - -0.999996542930603, - -1.0, - -0.9998822808265686 - ], - "type": "VEC3" - }, - { - "bufferView": 0, - "byteOffset": 515088, - "componentType": 5125, - "count": 912, - "type": "SCALAR" - }, - { - "bufferView": 2, - "byteOffset": 707880, - "componentType": 5126, - "count": 568, - "max": [ - 100.02538299560547, - 0.0050859092734754086, - 100.0043716430664 - ], - "min": [ - -102.13546752929688, - -3.877331256866455, - -104.43467712402344 - ], - "type": "VEC3" - }, - { - "bufferView": 2, - "byteOffset": 714696, - "componentType": 5126, - "count": 568, - "max": [ - 0.9999963641166687, - 1.0, - 0.999996542930603 - ], - "min": [ - -0.999996542930603, - -1.0, - -0.9998822808265686 - ], - "type": "VEC3" - }, - { - "bufferView": 0, - "byteOffset": 518736, - "componentType": 5125, - "count": 912, - "type": "SCALAR" - }, - { - "bufferView": 2, - "byteOffset": 721512, - "componentType": 5126, - "count": 92, - "max": [ - 115.76972961425781, - 0.7328627109527588, - 100.0 - ], - "min": [ - -97.66373443603516, - 0.0, - -100.0 - ], - "type": "VEC3" - }, - { - "bufferView": 2, - "byteOffset": 722616, - "componentType": 5126, - "count": 92, - "max": [ - 1.0, - 1.0, - 1.0 - ], - "min": [ - -1.0, - -1.0, - -1.0 - ], - "type": "VEC3" - }, - { - "bufferView": 1, - "byteOffset": 679080, - "componentType": 5126, - "count": 92, - "max": [ - 0.4384359121322632, - 0.9677327871322632 - ], - "min": [ - 0.24906408786773682, - 0.7783609628677368 - ], - "type": "VEC2" - }, - { - "bufferView": 0, - "byteOffset": 522384, - "componentType": 5125, - "count": 156, - "type": "SCALAR" - }, - { - "bufferView": 2, - "byteOffset": 723720, - "componentType": 5126, - "count": 246, - "max": [ - 135.4468536376953, - 1.6171379089355469, - 15.116771697998047 - ], - "min": [ - -8.459820747375488, - 0.0, - -9.43148136138916 - ], - "type": "VEC3" - }, - { - "bufferView": 2, - "byteOffset": 726672, - "componentType": 5126, - "count": 246, - "max": [ - 1.0, - 1.0, - 1.0 - ], - "min": [ - -1.0, - -1.0, - -1.0 - ], - "type": "VEC3" - }, - { - "bufferView": 1, - "byteOffset": 679816, - "componentType": 5126, - "count": 246, - "max": [ - 0.4994727373123169, - 0.8554167747497559 - ], - "min": [ - 0.2284148782491684, - 0.7796869277954102 - ], - "type": "VEC2" - }, - { - "bufferView": 0, - "byteOffset": 523008, - "componentType": 5125, - "count": 702, - "type": "SCALAR" - }, - { - "bufferView": 2, - "byteOffset": 729624, - "componentType": 5126, - "count": 440, - "max": [ - 1.1522057056427002, - 20.3384952545166, - 71.93663024902344 - ], - "min": [ - -16.551036834716797, - -20.3384952545166, - -98.71595764160156 - ], - "type": "VEC3" - }, - { - "bufferView": 2, - "byteOffset": 734904, - "componentType": 5126, - "count": 440, - "max": [ - 1.0, - 1.0, - 1.0 - ], - "min": [ - -1.0, - -1.0, - -1.0 - ], - "type": "VEC3" - }, - { - "bufferView": 3, - "byteOffset": 447664, - "componentType": 5126, - "count": 440, - "max": [ - 1.0, - 1.0, - 1.0, - 1.0 - ], - "min": [ - -1.0, - -1.0, - -1.0, - 1.0 - ], - "type": "VEC4" - }, - { - "bufferView": 1, - "byteOffset": 681784, - "componentType": 5126, - "count": 440, - "max": [ - 0.7879384160041809, - 0.9999999403953552 - ], - "min": [ - 0.0, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 1, - "byteOffset": 685304, - "componentType": 5126, - "count": 440, - "max": [ - 0.7879384160041809, - 0.9999999403953552 - ], - "min": [ - 0.0, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 1, - "byteOffset": 688824, - "componentType": 5126, - "count": 440, - "max": [ - 0.7879384160041809, - 0.9999999403953552 - ], - "min": [ - 0.0, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 0, - "byteOffset": 525816, - "componentType": 5125, - "count": 1116, - "type": "SCALAR" - }, - { - "bufferView": 2, - "byteOffset": 740184, - "componentType": 5126, - "count": 22938, - "max": [ - 92.23814392089844, - 35.8652458190918, - 79.53164672851563 - ], - "min": [ - -118.7790298461914, - -14.789424896240234, - -79.50471496582031 - ], - "type": "VEC3" - }, - { - "bufferView": 2, - "byteOffset": 1015440, - "componentType": 5126, - "count": 22938, - "max": [ - 1.0, - 1.0, - 0.7779618501663208 - ], - "min": [ - -1.0, - -1.0, - -0.7796278595924377 - ], - "type": "VEC3" - }, - { - "bufferView": 1, - "byteOffset": 692344, - "componentType": 5126, - "count": 22938, - "max": [ - 0.0, - 0.0 - ], - "min": [ - 0.0, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 1, - "byteOffset": 875848, - "componentType": 5126, - "count": 22938, - "max": [ - 0.0, - 0.0 - ], - "min": [ - 0.0, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 1, - "byteOffset": 1059352, - "componentType": 5126, - "count": 22938, - "max": [ - 0.0, - 0.0 - ], - "min": [ - 0.0, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 0, - "byteOffset": 530280, - "componentType": 5125, - "count": 68424, - "type": "SCALAR" - }, - { - "bufferView": 2, - "byteOffset": 1290696, - "componentType": 5126, - "count": 22938, - "max": [ - 92.23814392089844, - 35.8652458190918, - 79.53164672851563 - ], - "min": [ - -118.7790298461914, - -14.789424896240234, - -79.50471496582031 - ], - "type": "VEC3" - }, - { - "bufferView": 2, - "byteOffset": 1565952, - "componentType": 5126, - "count": 22938, - "max": [ - 1.0, - 1.0, - 0.7779618501663208 - ], - "min": [ - -1.0, - -1.0, - -0.7796278595924377 - ], - "type": "VEC3" - }, - { - "bufferView": 1, - "byteOffset": 1242856, - "componentType": 5126, - "count": 22938, - "max": [ - 0.0, - 0.0 - ], - "min": [ - 0.0, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 1, - "byteOffset": 1426360, - "componentType": 5126, - "count": 22938, - "max": [ - 0.0, - 0.0 - ], - "min": [ - 0.0, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 1, - "byteOffset": 1609864, - "componentType": 5126, - "count": 22938, - "max": [ - 0.0, - 0.0 - ], - "min": [ - 0.0, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 0, - "byteOffset": 803976, - "componentType": 5125, - "count": 68424, - "type": "SCALAR" - }, - { - "bufferView": 2, - "byteOffset": 1841208, - "componentType": 5126, - "count": 5977, - "max": [ - 243.8433380126953, - 3.845768690109253, - 245.13909912109375 - ], - "min": [ - -245.8629150390625, - -81.39239501953125, - -244.58486938476563 - ], - "type": "VEC3" - }, - { - "bufferView": 2, - "byteOffset": 1912932, - "componentType": 5126, - "count": 5977, - "max": [ - 0.9999997615814209, - 0.9999998807907104, - 0.9999997615814209 - ], - "min": [ - -0.9999997615814209, - -0.9999996423721313, - -0.9999997615814209 - ], - "type": "VEC3" - }, - { - "bufferView": 3, - "byteOffset": 454704, - "componentType": 5126, - "count": 5977, - "max": [ - 0.999821662902832, - 0.9999991655349731, - 0.9965219497680664, - 1.0 - ], - "min": [ - -0.9999917149543762, - -0.9999999403953552, - -0.9999608397483826, - 1.0 - ], - "type": "VEC4" - }, - { - "bufferView": 1, - "byteOffset": 1793368, - "componentType": 5126, - "count": 5977, - "max": [ - 1.0000001192092896, - 0.9204620122909546 - ], - "min": [ - 0.0, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 1, - "byteOffset": 1841184, - "componentType": 5126, - "count": 5977, - "max": [ - 1.0000001192092896, - 0.9204620122909546 - ], - "min": [ - 0.0, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 1, - "byteOffset": 1889000, - "componentType": 5126, - "count": 5977, - "max": [ - 1.0000001192092896, - 0.9204620122909546 - ], - "min": [ - 0.0, - 0.0 - ], - "type": "VEC2" - }, - { - "bufferView": 0, - "byteOffset": 1077672, - "componentType": 5125, - "count": 24528, - "type": "SCALAR" - }, - { - "bufferView": 2, - "byteOffset": 1984656, - "componentType": 5126, - "count": 948, - "max": [ - 89.06101989746094, - 3.585411310195923, - 89.0611343383789 - ], - "min": [ - -89.06101989746094, - -81.14639282226563, - -89.06101989746094 - ], - "type": "VEC3" - }, - { - "bufferView": 2, - "byteOffset": 1996032, - "componentType": 5126, - "count": 948, - "max": [ - 0.9999997615814209, - 1.0, - 0.9999997615814209 - ], - "min": [ - -0.9999997615814209, - -1.0, - -0.9999997615814209 - ], - "type": "VEC3" - }, - { - "bufferView": 1, - "byteOffset": 1936816, - "componentType": 5126, - "count": 948, - "max": [ - 0.8170047998428345, - 0.5114779472351074 - ], - "min": [ - 0.7189478874206543, - 0.2939568758010864 - ], - "type": "VEC2" - }, - { - "bufferView": 0, - "byteOffset": 1175784, - "componentType": 5125, - "count": 4224, - "type": "SCALAR" - } - ], - "asset": { - "extras": { - "author": "Cem GΓΌrbΓΌz (https://sketchfab.com/cemgurbuzz)", - "license": "CC-BY-NC-4.0 (http://creativecommons.org/licenses/by-nc/4.0/)", - "source": "https://sketchfab.com/3d-models/nvidia-geforce-rtx-3090-9b7cd73fefd5435f99f891567f5a9c2e", - "title": "Nvidia GeForce RTX 3090" - }, - "generator": "Sketchfab-12.68.0", - "version": "2.0" - }, - "bufferViews": [ - { - "buffer": 0, - "byteLength": 1192680, - "name": "floatBufferViews", - "target": 34963 - }, - { - "buffer": 0, - "byteLength": 1944400, - "byteOffset": 1192680, - "byteStride": 8, - "name": "floatBufferViews", - "target": 34962 - }, - { - "buffer": 0, - "byteLength": 2007408, - "byteOffset": 3137080, - "byteStride": 12, - "name": "floatBufferViews", - "target": 34962 - }, - { - "buffer": 0, - "byteLength": 550336, - "byteOffset": 5144488, - "byteStride": 16, - "name": "floatBufferViews", - "target": 34962 - } - ], - "buffers": [ - { - "byteLength": 5694824, - "uri": "scene.bin" - } - ], - "images": [ - { - "uri": "/textures/Metal_baseColor.png" - }, - { - "uri": "/textures/Metal_metallicRoughness.png" - }, - { - "uri": "/textures/Metal_normal.png" - }, - { - "uri": "/textures/Black_baseColor.png" - }, - { - "uri": "/textures/Black_metallicRoughness.png" - }, - { - "uri": "/textures/Black_normal.png" - }, - { - "uri": "/textures/Black_Fan_baseColor.png" - }, - { - "uri": "/textures/Black_Fan_metallicRoughness.png" - }, - { - "uri": "/textures/Black_Fan_normal.png" - }, - { - "uri": "/textures/Slot.1_baseColor.png" - }, - { - "uri": "/textures/Metal_S_baseColor.png" - }, - { - "uri": "/textures/Metal_S_metallicRoughness.png" - }, - { - "uri": "/textures/Metal_S_normal.png" - } - ], - "materials": [ - { - "doubleSided": true, - "name": "Metal", - "normalTexture": { - "index": 2 - }, - "pbrMetallicRoughness": { - "baseColorFactor": [ - 0.7374110015, - 0.7374110015, - 0.7374110015, - 1.0 - ], - "baseColorTexture": { - "index": 0 - }, - "metallicRoughnessTexture": { - "index": 1 - }, - "roughnessFactor": 0.2692376679 - } - }, - { - "doubleSided": true, - "name": "Black", - "normalTexture": { - "index": 5 - }, - "pbrMetallicRoughness": { - "baseColorFactor": [ - 0.792132559935307, - 0.792132559935307, - 0.792132559935307, - 1.0 - ], - "baseColorTexture": { - "index": 3 - }, - "metallicRoughnessTexture": { - "index": 4 - } - } - }, - { - "doubleSided": true, - "name": "Black_Fan", - "normalTexture": { - "index": 8 - }, - "pbrMetallicRoughness": { - "baseColorTexture": { - "index": 6 - }, - "metallicRoughnessTexture": { - "index": 7 - }, - "roughnessFactor": 0.3908411311554153 - } - }, - { - "doubleSided": true, - "name": "Slot.1", - "pbrMetallicRoughness": { - "baseColorTexture": { - "index": 9 - }, - "metallicFactor": 0.0, - "roughnessFactor": 0.7927045273 - } - }, - { - "doubleSided": true, - "name": "Metal_Black", - "pbrMetallicRoughness": { - "baseColorFactor": [ - 0.1717234471995979, - 0.14983924486362052, - 0.14983924486362052, - 1.0 - ], - "roughnessFactor": 0.6097273650353562 - } - }, - { - "doubleSided": true, - "name": "Black.001", - "pbrMetallicRoughness": { - "baseColorFactor": [ - 0.0330691, - 0.0330691, - 0.0330691, - 1.0 - ], - "metallicFactor": 0.0, - "roughnessFactor": 0.9918997578746929 - } - }, - { - "doubleSided": true, - "name": "Slot", - "pbrMetallicRoughness": { - "baseColorTexture": { - "index": 9 - }, - "metallicFactor": 0.0, - "roughnessFactor": 0.8040408206 - } - }, - { - "doubleSided": true, - "name": "Metal_S", - "normalTexture": { - "index": 12 - }, - "pbrMetallicRoughness": { - "baseColorTexture": { - "index": 10 - }, - "metallicRoughnessTexture": { - "index": 11 - } - } - } - ], - "meshes": [ - { - "name": "Metal Frame_Metal_0", - "primitives": [ - { - "attributes": { - "NORMAL": 1, - "POSITION": 0, - "TANGENT": 2, - "TEXCOORD_0": 3, - "TEXCOORD_1": 4, - "TEXCOORD_2": 5 - }, - "indices": 6, - "material": 0, - "mode": 4 - } - ] - }, - { - "name": "Front Cover_Black_0", - "primitives": [ - { - "attributes": { - "NORMAL": 8, - "POSITION": 7, - "TANGENT": 9, - "TEXCOORD_0": 10, - "TEXCOORD_1": 11, - "TEXCOORD_2": 12 - }, - "indices": 13, - "material": 1, - "mode": 4 - } - ] - }, - { - "name": "Fan Circle_Black Fan_0", - "primitives": [ - { - "attributes": { - "NORMAL": 15, - "POSITION": 14, - "TANGENT": 16, - "TEXCOORD_0": 17, - "TEXCOORD_1": 18, - "TEXCOORD_2": 19 - }, - "indices": 20, - "material": 2, - "mode": 4 - } - ] - }, - { - "name": "Fan F_Black Fan_0", - "primitives": [ - { - "attributes": { - "NORMAL": 22, - "POSITION": 21, - "TANGENT": 23, - "TEXCOORD_0": 24, - "TEXCOORD_1": 25, - "TEXCOORD_2": 26 - }, - "indices": 27, - "material": 2, - "mode": 4 - } - ] - }, - { - "name": "Fan F_Slot.1_0", - "primitives": [ - { - "attributes": { - "NORMAL": 29, - "POSITION": 28, - "TEXCOORD_0": 30 - }, - "indices": 31, - "material": 3, - "mode": 4 - } - ] - }, - { - "name": "Front Cover U_Black_0", - "primitives": [ - { - "attributes": { - "NORMAL": 33, - "POSITION": 32, - "TANGENT": 34, - "TEXCOORD_0": 35, - "TEXCOORD_1": 36, - "TEXCOORD_2": 37 - }, - "indices": 38, - "material": 1, - "mode": 4 - } - ] - }, - { - "name": "Front Cover T_Black_0", - "primitives": [ - { - "attributes": { - "NORMAL": 40, - "POSITION": 39, - "TANGENT": 41, - "TEXCOORD_0": 42, - "TEXCOORD_1": 43, - "TEXCOORD_2": 44 - }, - "indices": 45, - "material": 1, - "mode": 4 - } - ] - }, - { - "name": "Fan Circle B_Black Fan_0", - "primitives": [ - { - "attributes": { - "NORMAL": 47, - "POSITION": 46, - "TANGENT": 48, - "TEXCOORD_0": 49, - "TEXCOORD_1": 50, - "TEXCOORD_2": 51 - }, - "indices": 52, - "material": 2, - "mode": 4 - } - ] - }, - { - "name": "Grills U_Metal Black_0", - "primitives": [ - { - "attributes": { - "NORMAL": 54, - "POSITION": 53 - }, - "indices": 55, - "material": 4, - "mode": 4 - } - ] - }, - { - "name": "Grills T_Metal Black_0", - "primitives": [ - { - "attributes": { - "NORMAL": 57, - "POSITION": 56 - }, - "indices": 58, - "material": 4, - "mode": 4 - } - ] - }, - { - "name": "Plane.010_Black.001_0", - "primitives": [ - { - "attributes": { - "NORMAL": 60, - "POSITION": 59, - "TEXCOORD_0": 61 - }, - "indices": 62, - "material": 5, - "mode": 4 - } - ] - }, - { - "name": "Socket_Slot_0", - "primitives": [ - { - "attributes": { - "NORMAL": 64, - "POSITION": 63, - "TEXCOORD_0": 65 - }, - "indices": 66, - "material": 6, - "mode": 4 - } - ] - }, - { - "name": "Side Metal Part_Metal S_0", - "primitives": [ - { - "attributes": { - "NORMAL": 68, - "POSITION": 67, - "TANGENT": 69, - "TEXCOORD_0": 70, - "TEXCOORD_1": 71, - "TEXCOORD_2": 72 - }, - "indices": 73, - "material": 7, - "mode": 4 - } - ] - }, - { - "name": "Grills F.003_Metal Black_0", - "primitives": [ - { - "attributes": { - "NORMAL": 75, - "POSITION": 74, - "TEXCOORD_0": 76, - "TEXCOORD_1": 77, - "TEXCOORD_2": 78 - }, - "indices": 79, - "material": 4, - "mode": 4 - } - ] - }, - { - "name": "Grills F.002_Metal Black_0", - "primitives": [ - { - "attributes": { - "NORMAL": 81, - "POSITION": 80, - "TEXCOORD_0": 82, - "TEXCOORD_1": 83, - "TEXCOORD_2": 84 - }, - "indices": 85, - "material": 4, - "mode": 4 - } - ] - }, - { - "name": "Fan B_Black Fan_0", - "primitives": [ - { - "attributes": { - "NORMAL": 87, - "POSITION": 86, - "TANGENT": 88, - "TEXCOORD_0": 89, - "TEXCOORD_1": 90, - "TEXCOORD_2": 91 - }, - "indices": 92, - "material": 2, - "mode": 4 - } - ] - }, - { - "name": "Fan B_Slot.1_0", - "primitives": [ - { - "attributes": { - "NORMAL": 94, - "POSITION": 93, - "TEXCOORD_0": 95 - }, - "indices": 96, - "material": 3, - "mode": 4 - } - ] - } - ], - "nodes": [ - { - "children": [ - 1 - ], - "matrix": [ - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - 2.220446049250313e-16, - -1.0, - 0.0, - 0.0, - 1.0, - 2.220446049250313e-16, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0 - ], - "name": "Sketchfab_model" - }, - { - "children": [ - 2 - ], - "matrix": [ - 0.009999999776482582, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.009999999776482582, - 0.0, - 0.0, - -0.009999999776482582, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0 - ], - "name": "747a3f0779154e25a172cd94a8a85a59.fbx" - }, - { - "children": [ - 3, - 5, - 7, - 9, - 12, - 14, - 16, - 18, - 20, - 22, - 24, - 26, - 28, - 30, - 32 - ], - "name": "RootNode" - }, - { - "children": [ - 4 - ], - "matrix": [ - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - -1.6292067939183141e-07, - 0.9999999999999868, - 0.0, - 0.0, - -0.9999999999999868, - -1.6292067939183141e-07, - 0.0, - -0.0009551644325256348, - 88.30477905273438, - -8.472945213317871, - 1.0 - ], - "name": "Metal Frame" - }, - { - "mesh": 0, - "name": "Metal Frame_Metal_0" - }, - { - "children": [ - 6 - ], - "matrix": [ - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - -1.6292067939183141e-07, - 0.9999999999999868, - 0.0, - 0.0, - -0.8362743854522594, - -1.362463910358702e-07, - 0.0, - -122.30303192138672, - 89.6927261352539, - 12.111770629882813, - 1.0 - ], - "name": "Front Cover" - }, - { - "mesh": 1, - "name": "Front Cover_Black_0" - }, - { - "children": [ - 8 - ], - "matrix": [ - 0.7936016917228699, - 0.0, - 0.0, - 0.0, - 0.0, - -1.292941267819967e-07, - 0.7936016917228594, - 0.0, - 0.0, - -0.7936016917228594, - -1.292941267819967e-07, - 0.0, - 127.49998474121094, - 88.5121078491211, - 10.287901878356934, - 1.0 - ], - "name": "Fan Circle" - }, - { - "mesh": 2, - "name": "Fan Circle_Black Fan_0" - }, - { - "children": [ - 10, - 11 - ], - "matrix": [ - 0.30334164847183204, - 0.015296130773162211, - 1.9346649197391982e-09, - 0.0, - 2.3832869586734116e-09, - -8.567920238331605e-08, - 0.3037270605563997, - 0.0, - 0.015296130773162149, - -0.30334164847182, - -8.569050622925244e-08, - 0.0, - 127.49998474121094, - 88.5121078491211, - 10.287901878356934, - 1.0 - ], - "name": "Fan F" - }, - { - "mesh": 3, - "name": "Fan F_Black Fan_0" - }, - { - "mesh": 4, - "name": "Fan F_Slot.1_0" - }, - { - "children": [ - 13 - ], - "matrix": [ - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - -1.6292067939183141e-07, - 0.9999999999999868, - 0.0, - 0.0, - -0.9999999999999868, - -1.6292067939183141e-07, - 0.0, - 0.021076202392578125, - 26.082378387451172, - 14.08882999420166, - 1.0 - ], - "name": "Front Cover U" - }, - { - "mesh": 5, - "name": "Front Cover U_Black_0" - }, - { - "children": [ - 15 - ], - "matrix": [ - -0.9999999999999203, - -3.8941437731121037e-07, - 8.742276236262104e-08, - 0.0, - 8.742269891895826e-08, - 1.6292071369772287e-07, - 0.9999999999999828, - 0.0, - -3.894143915541825e-07, - 0.9999999999999108, - -1.6292067961387602e-07, - 0.0, - -4.7524333000183105, - 163.40081787109375, - 14.088836669921875, - 1.0 - ], - "name": "Front Cover T" - }, - { - "mesh": 6, - "name": "Front Cover T_Black_0" - }, - { - "children": [ - 17 - ], - "matrix": [ - -0.7936016917228065, - 6.937887475876209e-08, - 3.090399089678156e-07, - 0.0, - -3.0903990372985424e-07, - 5.99152878920572e-08, - -0.7936016917228074, - 0.0, - -6.937889809063118e-08, - -0.7936016917228645, - -5.991526075495118e-08, - 0.0, - -124.1536636352539, - 88.51213073730469, - -40.17750549316406, - 1.0 - ], - "name": "Fan Circle B" - }, - { - "mesh": 7, - "name": "Fan Circle B_Black Fan_0" - }, - { - "children": [ - 19 - ], - "matrix": [ - 0.38677331740211784, - -0.3867733343085177, - 6.07269882145477e-17, - 0.0, - -3.632659060470012e-07, - -3.632658903889941e-07, - 11.752899169921864, - 0.0, - -0.38677333430851735, - -0.38677331740211757, - -2.3909259764264718e-08, - 0.0, - -0.11941343545913696, - 3.1571507453918457, - 3.0867953300476074, - 1.0 - ], - "name": "Grills U" - }, - { - "mesh": 8, - "name": "Grills U_Metal Black_0" - }, - { - "children": [ - 21 - ], - "matrix": [ - -0.38677332816141635, - 0.3867733235491907, - 1.4909048089098407e-07, - 0.0, - 3.6191709386740606e-06, - -9.112484713068894e-07, - 11.752899169921283, - 0.0, - 0.38677332354918276, - 0.3867733281614427, - -8.911436685577966e-08, - 0.0, - 0.7981881499290466, - 174.49168395996094, - 3.0868005752563477, - 1.0 - ], - "name": "Grills T" - }, - { - "mesh": 9, - "name": "Grills T_Metal Black_0" - }, - { - "children": [ - 23 - ], - "matrix": [ - -0.9999999999999203, - -3.8941437731121037e-07, - 8.742276236262104e-08, - 0.0, - 8.742269891895826e-08, - 1.6292071369772287e-07, - 0.9999999999999828, - 0.0, - -3.2565728098324384e-07, - 0.8362743854521959, - -1.3624639122156042e-07, - 0.0, - 121.83759307861328, - 88.41606140136719, - -34.240440368652344, - 1.0 - ], - "name": "Plane.010" - }, - { - "mesh": 10, - "name": "Plane.010_Black.001_0" - }, - { - "children": [ - 25 - ], - "matrix": [ - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - -3.14346242379091e-07, - 1.929443478584264, - 0.0, - 0.0, - -0.9999999999999868, - -1.6292067939183141e-07, - 0.0, - -149.70736694335938, - 187.4652557373047, - -39.00934982299805, - 1.0 - ], - "name": "Socket" - }, - { - "mesh": 11, - "name": "Socket_Slot_0" - }, - { - "children": [ - 27 - ], - "matrix": [ - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - -1.6292067939183141e-07, - 0.9999999999999868, - 0.0, - 0.0, - -0.9999999999999868, - -1.6292067939183141e-07, - 0.0, - -225.87086486816406, - 118.08707427978516, - -12.542006492614746, - 1.0 - ], - "name": "Side Metal Part" - }, - { - "mesh": 12, - "name": "Side Metal Part_Metal S_0" - }, - { - "children": [ - 29 - ], - "matrix": [ - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - -1.6292067939183141e-07, - 0.9999999999999868, - 0.0, - 0.0, - -1.0234513282775743, - -1.667413857274569e-07, - 0.0, - 131.4942626953125, - 88.83919525146484, - -23.017173767089844, - 1.0 - ], - "name": "Grills F.003" - }, - { - "mesh": 13, - "name": "Grills F.003_Metal Black_0" - }, - { - "children": [ - 31 - ], - "matrix": [ - -0.9999999999999241, - 9.892427301959894e-15, - 3.8941437775530107e-07, - 0.0, - -3.7781312610095593e-07, - 7.32487015753169e-08, - -0.9702084660529326, - 0.0, - -4.0213853733746167e-14, - -1.0234513282775848, - -7.72684257400924e-08, - 0.0, - -128.1754608154297, - 88.83919525146484, - -4.171847343444824, - 1.0 - ], - "name": "Grills F.002" - }, - { - "mesh": 14, - "name": "Grills F.002_Metal Black_0" - }, - { - "children": [ - 33, - 34 - ], - "matrix": [ - -0.3033416743311482, - 0.01529620971837044, - 9.933227977876897e-08, - 0.0, - -1.003610534326315e-07, - -1.789911585542887e-08, - -0.3037270605563946, - 0.0, - -0.015296209718363723, - -0.30334167433116394, - 2.29307573058707e-08, - 0.0, - -123.89674377441406, - 88.5121078491211, - -37.82356262207031, - 1.0 - ], - "name": "Fan B" - }, - { - "mesh": 15, - "name": "Fan B_Black Fan_0" - }, - { - "mesh": 16, - "name": "Fan B_Slot.1_0" - } - ], - "samplers": [ - { - "magFilter": 9729, - "minFilter": 9987, - "wrapS": 10497, - "wrapT": 10497 - } - ], - "scene": 0, - "scenes": [ - { - "name": "Sketchfab_Scene", - "nodes": [ - 0 - ] - } - ], - "textures": [ - { - "sampler": 0, - "source": 0 - }, - { - "sampler": 0, - "source": 1 - }, - { - "sampler": 0, - "source": 2 - }, - { - "sampler": 0, - "source": 3 - }, - { - "sampler": 0, - "source": 4 - }, - { - "sampler": 0, - "source": 5 - }, - { - "sampler": 0, - "source": 6 - }, - { - "sampler": 0, - "source": 7 - }, - { - "sampler": 0, - "source": 8 - }, - { - "sampler": 0, - "source": 9 - }, - { - "sampler": 0, - "source": 10 - }, - { - "sampler": 0, - "source": 11 - }, - { - "sampler": 0, - "source": 12 - } - ] + "accessors": [ + { + "bufferView": 2, + "componentType": 5126, + "count": 18466, + "max": [224.7469940185547, 25.571533203125, 88.23554992675781], + "min": [-224.74513244628906, -35.34562683105469, -88.20793914794922], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 221592, + "componentType": 5126, + "count": 18466, + "max": [1.0, 1.0, 1.0], + "min": [-1.0, -1.0, -1.0], + "type": "VEC3" + }, + { + "bufferView": 3, + "componentType": 5126, + "count": 18466, + "max": [0.8829709887504578, 1.0, 1.0, 1.0], + "min": [-0.8874375224113464, -1.0, -1.0, -1.0], + "type": "VEC4" + }, + { + "bufferView": 1, + "componentType": 5126, + "count": 18466, + "max": [0.9974566698074341, 1.0000001192092896], + "min": [0.0, 0.0], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 147728, + "componentType": 5126, + "count": 18466, + "max": [0.9974566698074341, 1.0000001192092896], + "min": [0.0, 0.0], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 295456, + "componentType": 5126, + "count": 18466, + "max": [0.9974566698074341, 1.0000001192092896], + "min": [0.0, 0.0], + "type": "VEC2" + }, + { + "bufferView": 0, + "componentType": 5125, + "count": 80496, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 443184, + "componentType": 5126, + "count": 14, + "max": [115.76972961425781, 0.0, 100.0], + "min": [-97.66373443603516, 0.0, -100.0], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 443352, + "componentType": 5126, + "count": 14, + "max": [0.0, 1.0, 0.0], + "min": [0.0, 1.0, 0.0], + "type": "VEC3" + }, + { + "bufferView": 3, + "byteOffset": 295456, + "componentType": 5126, + "count": 14, + "max": [0.0, 0.0, 1.0, 1.0], + "min": [-1.4227993005988537e-7, 0.0, 1.0, 1.0], + "type": "VEC4" + }, + { + "bufferView": 1, + "byteOffset": 443184, + "componentType": 5126, + "count": 14, + "max": [0.5462319254875183, 0.5829209089279175], + "min": [0.0, 0.0], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 443296, + "componentType": 5126, + "count": 14, + "max": [0.5462319254875183, 0.5829209089279175], + "min": [0.0, 0.0], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 443408, + "componentType": 5126, + "count": 14, + "max": [0.5462319254875183, 0.5829209089279175], + "min": [0.0, 0.0], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 321984, + "componentType": 5125, + "count": 36, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 443520, + "componentType": 5126, + "count": 1792, + "max": [99.35418701171875, 3.8504819869995117, 99.35418701171875], + "min": [-99.35418701171875, -36.769859313964844, -99.35418701171875], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 465024, + "componentType": 5126, + "count": 1792, + "max": [1.0, 0.9999999403953552, 1.0], + "min": [-1.0, -0.9999999403953552, -1.0], + "type": "VEC3" + }, + { + "bufferView": 3, + "byteOffset": 295680, + "componentType": 5126, + "count": 1792, + "max": [1.0, 1.0, 1.0, 1.0], + "min": [-1.0, -1.0, -1.0, 1.0], + "type": "VEC4" + }, + { + "bufferView": 1, + "byteOffset": 443520, + "componentType": 5126, + "count": 1792, + "max": [0.0, 0.0], + "min": [0.0, 0.0], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 457856, + "componentType": 5126, + "count": 1792, + "max": [0.0, 0.0], + "min": [0.0, 0.0], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 472192, + "componentType": 5126, + "count": 1792, + "max": [0.0, 0.0], + "min": [0.0, 0.0], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 322128, + "componentType": 5125, + "count": 9216, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 486528, + "componentType": 5126, + "count": 5519, + "max": [243.8433380126953, 3.845768690109253, 245.13909912109375], + "min": [-245.8629150390625, -81.39239501953125, -244.58486938476563], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 552756, + "componentType": 5126, + "count": 5519, + "max": [0.9999997615814209, 0.9999998807907104, 0.9999997615814209], + "min": [-0.9999997615814209, -0.9999996423721313, -0.9999997615814209], + "type": "VEC3" + }, + { + "bufferView": 3, + "byteOffset": 324352, + "componentType": 5126, + "count": 5519, + "max": [0.9960846900939941, 0.9999998211860657, 0.9974399209022522, 1.0], + "min": [-0.9998785257339478, -0.9999998211860657, -0.9908028244972229, -1.0], + "type": "VEC4" + }, + { + "bufferView": 1, + "byteOffset": 486528, + "componentType": 5126, + "count": 5519, + "max": [0.8715417385101318, 1.0], + "min": [0.0001247699256055057, 0.0], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 530680, + "componentType": 5126, + "count": 5519, + "max": [0.8715417385101318, 1.0], + "min": [0.0001247699256055057, 0.0], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 574832, + "componentType": 5126, + "count": 5519, + "max": [0.8715417385101318, 1.0], + "min": [0.0001247699256055057, 0.0], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 358992, + "componentType": 5125, + "count": 24528, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 618984, + "componentType": 5126, + "count": 948, + "max": [89.06101989746094, 3.585411310195923, 89.0611343383789], + "min": [-89.06101989746094, -81.14639282226563, -89.06101989746094], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 630360, + "componentType": 5126, + "count": 948, + "max": [0.9999997615814209, 1.0, 0.9999997615814209], + "min": [-0.9999997615814209, -1.0, -0.9999997615814209], + "type": "VEC3" + }, + { + "bufferView": 1, + "byteOffset": 618984, + "componentType": 5126, + "count": 948, + "max": [0.8170047998428345, 0.5114779472351074], + "min": [0.7189478874206543, 0.2939568758010864], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 457104, + "componentType": 5125, + "count": 4224, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 641736, + "componentType": 5126, + "count": 136, + "max": [77.20637512207031, -1.1121496754640248e-6, 25.964923858642578], + "min": [-77.20637512207031, -10.26113510131836, -53.971092224121094], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 643368, + "componentType": 5126, + "count": 136, + "max": [0.995922327041626, 1.0, 0.9986786842346191], + "min": [-0.995922327041626, -1.0, -0.9876721501350403], + "type": "VEC3" + }, + { + "bufferView": 3, + "byteOffset": 412656, + "componentType": 5126, + "count": 136, + "max": [0.8069314956665039, 0.6983179450035095, 1.0, 1.0], + "min": [-0.5064710974693298, -1.0, -0.995922327041626, 1.0], + "type": "VEC4" + }, + { + "bufferView": 1, + "byteOffset": 626568, + "componentType": 5126, + "count": 136, + "max": [0.9828680753707886, 0.9973888397216797], + "min": [0.0, 0.0], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 627656, + "componentType": 5126, + "count": 136, + "max": [0.9828680753707886, 0.9973888397216797], + "min": [0.0, 0.0], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 628744, + "componentType": 5126, + "count": 136, + "max": [0.9828680753707886, 0.9973888397216797], + "min": [0.0, 0.0], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 474000, + "componentType": 5125, + "count": 348, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 645000, + "componentType": 5126, + "count": 260, + "max": [72.35018920898438, -1.4828661960564204e-6, 13.962870597839355], + "min": [-82.06255340576172, -10.26113510131836, -65.97315216064453], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 648120, + "componentType": 5126, + "count": 260, + "max": [0.9959227442741394, 1.0, 0.9986786842346191], + "min": [-0.9959227442741394, -1.0, -0.9876720309257507], + "type": "VEC3" + }, + { + "bufferView": 3, + "byteOffset": 414832, + "componentType": 5126, + "count": 260, + "max": [0.850715160369873, 1.0, 1.0, 1.0], + "min": [-0.5064702033996582, -1.0, -0.9959227442741394, 1.0], + "type": "VEC4" + }, + { + "bufferView": 1, + "byteOffset": 629832, + "componentType": 5126, + "count": 260, + "max": [0.7645499110221863, 1.0000001192092896], + "min": [0.028024811297655106, 0.0], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 631912, + "componentType": 5126, + "count": 260, + "max": [0.7645499110221863, 1.0000001192092896], + "min": [0.028024811297655106, 0.0], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 633992, + "componentType": 5126, + "count": 260, + "max": [0.7645499110221863, 1.0000001192092896], + "min": [0.028024811297655106, 0.0], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 475392, + "componentType": 5125, + "count": 708, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 651240, + "componentType": 5126, + "count": 1792, + "max": [99.35418701171875, 3.8504819869995117, 99.35418701171875], + "min": [-99.35418701171875, -36.769859313964844, -99.35418701171875], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 672744, + "componentType": 5126, + "count": 1792, + "max": [1.0, 0.9999999403953552, 1.0], + "min": [-1.0, -0.9999999403953552, -1.0], + "type": "VEC3" + }, + { + "bufferView": 3, + "byteOffset": 418992, + "componentType": 5126, + "count": 1792, + "max": [1.0, 1.0, 1.0, 1.0], + "min": [-1.0, -1.0, -1.0, 1.0], + "type": "VEC4" + }, + { + "bufferView": 1, + "byteOffset": 636072, + "componentType": 5126, + "count": 1792, + "max": [0.0, 0.0], + "min": [0.0, 0.0], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 650408, + "componentType": 5126, + "count": 1792, + "max": [0.0, 0.0], + "min": [0.0, 0.0], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 664744, + "componentType": 5126, + "count": 1792, + "max": [0.0, 0.0], + "min": [0.0, 0.0], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 478224, + "componentType": 5125, + "count": 9216, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 694248, + "componentType": 5126, + "count": 568, + "max": [100.02538299560547, 0.0050859092734754086, 100.0043716430664], + "min": [-102.13546752929688, -3.877331256866455, -104.43467712402344], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 701064, + "componentType": 5126, + "count": 568, + "max": [0.9999963641166687, 1.0, 0.999996542930603], + "min": [-0.999996542930603, -1.0, -0.9998822808265686], + "type": "VEC3" + }, + { + "bufferView": 0, + "byteOffset": 515088, + "componentType": 5125, + "count": 912, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 707880, + "componentType": 5126, + "count": 568, + "max": [100.02538299560547, 0.0050859092734754086, 100.0043716430664], + "min": [-102.13546752929688, -3.877331256866455, -104.43467712402344], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 714696, + "componentType": 5126, + "count": 568, + "max": [0.9999963641166687, 1.0, 0.999996542930603], + "min": [-0.999996542930603, -1.0, -0.9998822808265686], + "type": "VEC3" + }, + { + "bufferView": 0, + "byteOffset": 518736, + "componentType": 5125, + "count": 912, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 721512, + "componentType": 5126, + "count": 92, + "max": [115.76972961425781, 0.7328627109527588, 100.0], + "min": [-97.66373443603516, 0.0, -100.0], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 722616, + "componentType": 5126, + "count": 92, + "max": [1.0, 1.0, 1.0], + "min": [-1.0, -1.0, -1.0], + "type": "VEC3" + }, + { + "bufferView": 1, + "byteOffset": 679080, + "componentType": 5126, + "count": 92, + "max": [0.4384359121322632, 0.9677327871322632], + "min": [0.24906408786773682, 0.7783609628677368], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 522384, + "componentType": 5125, + "count": 156, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 723720, + "componentType": 5126, + "count": 246, + "max": [135.4468536376953, 1.6171379089355469, 15.116771697998047], + "min": [-8.459820747375488, 0.0, -9.43148136138916], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 726672, + "componentType": 5126, + "count": 246, + "max": [1.0, 1.0, 1.0], + "min": [-1.0, -1.0, -1.0], + "type": "VEC3" + }, + { + "bufferView": 1, + "byteOffset": 679816, + "componentType": 5126, + "count": 246, + "max": [0.4994727373123169, 0.8554167747497559], + "min": [0.2284148782491684, 0.7796869277954102], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 523008, + "componentType": 5125, + "count": 702, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 729624, + "componentType": 5126, + "count": 440, + "max": [1.1522057056427002, 20.3384952545166, 71.93663024902344], + "min": [-16.551036834716797, -20.3384952545166, -98.71595764160156], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 734904, + "componentType": 5126, + "count": 440, + "max": [1.0, 1.0, 1.0], + "min": [-1.0, -1.0, -1.0], + "type": "VEC3" + }, + { + "bufferView": 3, + "byteOffset": 447664, + "componentType": 5126, + "count": 440, + "max": [1.0, 1.0, 1.0, 1.0], + "min": [-1.0, -1.0, -1.0, 1.0], + "type": "VEC4" + }, + { + "bufferView": 1, + "byteOffset": 681784, + "componentType": 5126, + "count": 440, + "max": [0.7879384160041809, 0.9999999403953552], + "min": [0.0, 0.0], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 685304, + "componentType": 5126, + "count": 440, + "max": [0.7879384160041809, 0.9999999403953552], + "min": [0.0, 0.0], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 688824, + "componentType": 5126, + "count": 440, + "max": [0.7879384160041809, 0.9999999403953552], + "min": [0.0, 0.0], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 525816, + "componentType": 5125, + "count": 1116, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 740184, + "componentType": 5126, + "count": 22938, + "max": [92.23814392089844, 35.8652458190918, 79.53164672851563], + "min": [-118.7790298461914, -14.789424896240234, -79.50471496582031], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 1015440, + "componentType": 5126, + "count": 22938, + "max": [1.0, 1.0, 0.7779618501663208], + "min": [-1.0, -1.0, -0.7796278595924377], + "type": "VEC3" + }, + { + "bufferView": 1, + "byteOffset": 692344, + "componentType": 5126, + "count": 22938, + "max": [0.0, 0.0], + "min": [0.0, 0.0], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 875848, + "componentType": 5126, + "count": 22938, + "max": [0.0, 0.0], + "min": [0.0, 0.0], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 1059352, + "componentType": 5126, + "count": 22938, + "max": [0.0, 0.0], + "min": [0.0, 0.0], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 530280, + "componentType": 5125, + "count": 68424, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 1290696, + "componentType": 5126, + "count": 22938, + "max": [92.23814392089844, 35.8652458190918, 79.53164672851563], + "min": [-118.7790298461914, -14.789424896240234, -79.50471496582031], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 1565952, + "componentType": 5126, + "count": 22938, + "max": [1.0, 1.0, 0.7779618501663208], + "min": [-1.0, -1.0, -0.7796278595924377], + "type": "VEC3" + }, + { + "bufferView": 1, + "byteOffset": 1242856, + "componentType": 5126, + "count": 22938, + "max": [0.0, 0.0], + "min": [0.0, 0.0], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 1426360, + "componentType": 5126, + "count": 22938, + "max": [0.0, 0.0], + "min": [0.0, 0.0], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 1609864, + "componentType": 5126, + "count": 22938, + "max": [0.0, 0.0], + "min": [0.0, 0.0], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 803976, + "componentType": 5125, + "count": 68424, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 1841208, + "componentType": 5126, + "count": 5977, + "max": [243.8433380126953, 3.845768690109253, 245.13909912109375], + "min": [-245.8629150390625, -81.39239501953125, -244.58486938476563], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 1912932, + "componentType": 5126, + "count": 5977, + "max": [0.9999997615814209, 0.9999998807907104, 0.9999997615814209], + "min": [-0.9999997615814209, -0.9999996423721313, -0.9999997615814209], + "type": "VEC3" + }, + { + "bufferView": 3, + "byteOffset": 454704, + "componentType": 5126, + "count": 5977, + "max": [0.999821662902832, 0.9999991655349731, 0.9965219497680664, 1.0], + "min": [-0.9999917149543762, -0.9999999403953552, -0.9999608397483826, 1.0], + "type": "VEC4" + }, + { + "bufferView": 1, + "byteOffset": 1793368, + "componentType": 5126, + "count": 5977, + "max": [1.0000001192092896, 0.9204620122909546], + "min": [0.0, 0.0], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 1841184, + "componentType": 5126, + "count": 5977, + "max": [1.0000001192092896, 0.9204620122909546], + "min": [0.0, 0.0], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 1889000, + "componentType": 5126, + "count": 5977, + "max": [1.0000001192092896, 0.9204620122909546], + "min": [0.0, 0.0], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 1077672, + "componentType": 5125, + "count": 24528, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 1984656, + "componentType": 5126, + "count": 948, + "max": [89.06101989746094, 3.585411310195923, 89.0611343383789], + "min": [-89.06101989746094, -81.14639282226563, -89.06101989746094], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 1996032, + "componentType": 5126, + "count": 948, + "max": [0.9999997615814209, 1.0, 0.9999997615814209], + "min": [-0.9999997615814209, -1.0, -0.9999997615814209], + "type": "VEC3" + }, + { + "bufferView": 1, + "byteOffset": 1936816, + "componentType": 5126, + "count": 948, + "max": [0.8170047998428345, 0.5114779472351074], + "min": [0.7189478874206543, 0.2939568758010864], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 1175784, + "componentType": 5125, + "count": 4224, + "type": "SCALAR" + } + ], + "asset": { + "extras": { + "author": "Cem GΓΌrbΓΌz (https://sketchfab.com/cemgurbuzz)", + "license": "CC-BY-NC-4.0 (http://creativecommons.org/licenses/by-nc/4.0/)", + "source": "https://sketchfab.com/3d-models/nvidia-geforce-rtx-3090-9b7cd73fefd5435f99f891567f5a9c2e", + "title": "Nvidia GeForce RTX 3090" + }, + "generator": "Sketchfab-12.68.0", + "version": "2.0" + }, + "bufferViews": [ + { + "buffer": 0, + "byteLength": 1192680, + "name": "floatBufferViews", + "target": 34963 + }, + { + "buffer": 0, + "byteLength": 1944400, + "byteOffset": 1192680, + "byteStride": 8, + "name": "floatBufferViews", + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 2007408, + "byteOffset": 3137080, + "byteStride": 12, + "name": "floatBufferViews", + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 550336, + "byteOffset": 5144488, + "byteStride": 16, + "name": "floatBufferViews", + "target": 34962 + } + ], + "buffers": [ + { + "byteLength": 5694824, + "uri": "scene.bin" + } + ], + "images": [ + { + "uri": "/textures/Metal_baseColor.png" + }, + { + "uri": "/textures/Metal_metallicRoughness.png" + }, + { + "uri": "/textures/Metal_normal.png" + }, + { + "uri": "/textures/Black_baseColor.png" + }, + { + "uri": "/textures/Black_metallicRoughness.png" + }, + { + "uri": "/textures/Black_normal.png" + }, + { + "uri": "/textures/Black_Fan_baseColor.png" + }, + { + "uri": "/textures/Black_Fan_metallicRoughness.png" + }, + { + "uri": "/textures/Black_Fan_normal.png" + }, + { + "uri": "/textures/Slot.1_baseColor.png" + }, + { + "uri": "/textures/Metal_S_baseColor.png" + }, + { + "uri": "/textures/Metal_S_metallicRoughness.png" + }, + { + "uri": "/textures/Metal_S_normal.png" + } + ], + "materials": [ + { + "doubleSided": true, + "name": "Metal", + "normalTexture": { + "index": 2 + }, + "pbrMetallicRoughness": { + "baseColorFactor": [0.7374110015, 0.7374110015, 0.7374110015, 1.0], + "baseColorTexture": { + "index": 0 + }, + "metallicRoughnessTexture": { + "index": 1 + }, + "roughnessFactor": 0.2692376679 + } + }, + { + "doubleSided": true, + "name": "Black", + "normalTexture": { + "index": 5 + }, + "pbrMetallicRoughness": { + "baseColorFactor": [0.792132559935307, 0.792132559935307, 0.792132559935307, 1.0], + "baseColorTexture": { + "index": 3 + }, + "metallicRoughnessTexture": { + "index": 4 + } + } + }, + { + "doubleSided": true, + "name": "Black_Fan", + "normalTexture": { + "index": 8 + }, + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 6 + }, + "metallicRoughnessTexture": { + "index": 7 + }, + "roughnessFactor": 0.3908411311554153 + } + }, + { + "doubleSided": true, + "name": "Slot.1", + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 9 + }, + "metallicFactor": 0.0, + "roughnessFactor": 0.7927045273 + } + }, + { + "doubleSided": true, + "name": "Metal_Black", + "pbrMetallicRoughness": { + "baseColorFactor": [0.1717234471995979, 0.14983924486362052, 0.14983924486362052, 1.0], + "roughnessFactor": 0.6097273650353562 + } + }, + { + "doubleSided": true, + "name": "Black.001", + "pbrMetallicRoughness": { + "baseColorFactor": [0.0330691, 0.0330691, 0.0330691, 1.0], + "metallicFactor": 0.0, + "roughnessFactor": 0.9918997578746929 + } + }, + { + "doubleSided": true, + "name": "Slot", + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 9 + }, + "metallicFactor": 0.0, + "roughnessFactor": 0.8040408206 + } + }, + { + "doubleSided": true, + "name": "Metal_S", + "normalTexture": { + "index": 12 + }, + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 10 + }, + "metallicRoughnessTexture": { + "index": 11 + } + } + } + ], + "meshes": [ + { + "name": "Metal Frame_Metal_0", + "primitives": [ + { + "attributes": { + "NORMAL": 1, + "POSITION": 0, + "TANGENT": 2, + "TEXCOORD_0": 3, + "TEXCOORD_1": 4, + "TEXCOORD_2": 5 + }, + "indices": 6, + "material": 0, + "mode": 4 + } + ] + }, + { + "name": "Front Cover_Black_0", + "primitives": [ + { + "attributes": { + "NORMAL": 8, + "POSITION": 7, + "TANGENT": 9, + "TEXCOORD_0": 10, + "TEXCOORD_1": 11, + "TEXCOORD_2": 12 + }, + "indices": 13, + "material": 1, + "mode": 4 + } + ] + }, + { + "name": "Fan Circle_Black Fan_0", + "primitives": [ + { + "attributes": { + "NORMAL": 15, + "POSITION": 14, + "TANGENT": 16, + "TEXCOORD_0": 17, + "TEXCOORD_1": 18, + "TEXCOORD_2": 19 + }, + "indices": 20, + "material": 2, + "mode": 4 + } + ] + }, + { + "name": "Fan F_Black Fan_0", + "primitives": [ + { + "attributes": { + "NORMAL": 22, + "POSITION": 21, + "TANGENT": 23, + "TEXCOORD_0": 24, + "TEXCOORD_1": 25, + "TEXCOORD_2": 26 + }, + "indices": 27, + "material": 2, + "mode": 4 + } + ] + }, + { + "name": "Fan F_Slot.1_0", + "primitives": [ + { + "attributes": { + "NORMAL": 29, + "POSITION": 28, + "TEXCOORD_0": 30 + }, + "indices": 31, + "material": 3, + "mode": 4 + } + ] + }, + { + "name": "Front Cover U_Black_0", + "primitives": [ + { + "attributes": { + "NORMAL": 33, + "POSITION": 32, + "TANGENT": 34, + "TEXCOORD_0": 35, + "TEXCOORD_1": 36, + "TEXCOORD_2": 37 + }, + "indices": 38, + "material": 1, + "mode": 4 + } + ] + }, + { + "name": "Front Cover T_Black_0", + "primitives": [ + { + "attributes": { + "NORMAL": 40, + "POSITION": 39, + "TANGENT": 41, + "TEXCOORD_0": 42, + "TEXCOORD_1": 43, + "TEXCOORD_2": 44 + }, + "indices": 45, + "material": 1, + "mode": 4 + } + ] + }, + { + "name": "Fan Circle B_Black Fan_0", + "primitives": [ + { + "attributes": { + "NORMAL": 47, + "POSITION": 46, + "TANGENT": 48, + "TEXCOORD_0": 49, + "TEXCOORD_1": 50, + "TEXCOORD_2": 51 + }, + "indices": 52, + "material": 2, + "mode": 4 + } + ] + }, + { + "name": "Grills U_Metal Black_0", + "primitives": [ + { + "attributes": { + "NORMAL": 54, + "POSITION": 53 + }, + "indices": 55, + "material": 4, + "mode": 4 + } + ] + }, + { + "name": "Grills T_Metal Black_0", + "primitives": [ + { + "attributes": { + "NORMAL": 57, + "POSITION": 56 + }, + "indices": 58, + "material": 4, + "mode": 4 + } + ] + }, + { + "name": "Plane.010_Black.001_0", + "primitives": [ + { + "attributes": { + "NORMAL": 60, + "POSITION": 59, + "TEXCOORD_0": 61 + }, + "indices": 62, + "material": 5, + "mode": 4 + } + ] + }, + { + "name": "Socket_Slot_0", + "primitives": [ + { + "attributes": { + "NORMAL": 64, + "POSITION": 63, + "TEXCOORD_0": 65 + }, + "indices": 66, + "material": 6, + "mode": 4 + } + ] + }, + { + "name": "Side Metal Part_Metal S_0", + "primitives": [ + { + "attributes": { + "NORMAL": 68, + "POSITION": 67, + "TANGENT": 69, + "TEXCOORD_0": 70, + "TEXCOORD_1": 71, + "TEXCOORD_2": 72 + }, + "indices": 73, + "material": 7, + "mode": 4 + } + ] + }, + { + "name": "Grills F.003_Metal Black_0", + "primitives": [ + { + "attributes": { + "NORMAL": 75, + "POSITION": 74, + "TEXCOORD_0": 76, + "TEXCOORD_1": 77, + "TEXCOORD_2": 78 + }, + "indices": 79, + "material": 4, + "mode": 4 + } + ] + }, + { + "name": "Grills F.002_Metal Black_0", + "primitives": [ + { + "attributes": { + "NORMAL": 81, + "POSITION": 80, + "TEXCOORD_0": 82, + "TEXCOORD_1": 83, + "TEXCOORD_2": 84 + }, + "indices": 85, + "material": 4, + "mode": 4 + } + ] + }, + { + "name": "Fan B_Black Fan_0", + "primitives": [ + { + "attributes": { + "NORMAL": 87, + "POSITION": 86, + "TANGENT": 88, + "TEXCOORD_0": 89, + "TEXCOORD_1": 90, + "TEXCOORD_2": 91 + }, + "indices": 92, + "material": 2, + "mode": 4 + } + ] + }, + { + "name": "Fan B_Slot.1_0", + "primitives": [ + { + "attributes": { + "NORMAL": 94, + "POSITION": 93, + "TEXCOORD_0": 95 + }, + "indices": 96, + "material": 3, + "mode": 4 + } + ] + } + ], + "nodes": [ + { + "children": [1], + "matrix": [ + 1.0, 0.0, 0.0, 0.0, 0.0, 2.220446049250313e-16, -1.0, 0.0, 0.0, 1.0, 2.220446049250313e-16, + 0.0, 0.0, 0.0, 0.0, 1.0 + ], + "name": "Sketchfab_model" + }, + { + "children": [2], + "matrix": [ + 0.009999999776482582, 0.0, 0.0, 0.0, 0.0, 0.0, 0.009999999776482582, 0.0, 0.0, + -0.009999999776482582, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0 + ], + "name": "747a3f0779154e25a172cd94a8a85a59.fbx" + }, + { + "children": [3, 5, 7, 9, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32], + "name": "RootNode" + }, + { + "children": [4], + "matrix": [ + 1.0, 0.0, 0.0, 0.0, 0.0, -1.6292067939183141e-7, 0.9999999999999868, 0.0, 0.0, + -0.9999999999999868, -1.6292067939183141e-7, 0.0, -0.0009551644325256348, 88.30477905273438, + -8.472945213317871, 1.0 + ], + "name": "Metal Frame" + }, + { + "mesh": 0, + "name": "Metal Frame_Metal_0" + }, + { + "children": [6], + "matrix": [ + 1.0, 0.0, 0.0, 0.0, 0.0, -1.6292067939183141e-7, 0.9999999999999868, 0.0, 0.0, + -0.8362743854522594, -1.362463910358702e-7, 0.0, -122.30303192138672, 89.6927261352539, + 12.111770629882813, 1.0 + ], + "name": "Front Cover" + }, + { + "mesh": 1, + "name": "Front Cover_Black_0" + }, + { + "children": [8], + "matrix": [ + 0.7936016917228699, 0.0, 0.0, 0.0, 0.0, -1.292941267819967e-7, 0.7936016917228594, 0.0, 0.0, + -0.7936016917228594, -1.292941267819967e-7, 0.0, 127.49998474121094, 88.5121078491211, + 10.287901878356934, 1.0 + ], + "name": "Fan Circle" + }, + { + "mesh": 2, + "name": "Fan Circle_Black Fan_0" + }, + { + "children": [10, 11], + "matrix": [ + 0.30334164847183204, 0.015296130773162211, 1.9346649197391982e-9, 0.0, + 2.3832869586734116e-9, -8.567920238331605e-8, 0.3037270605563997, 0.0, 0.015296130773162149, + -0.30334164847182, -8.569050622925244e-8, 0.0, 127.49998474121094, 88.5121078491211, + 10.287901878356934, 1.0 + ], + "name": "Fan F" + }, + { + "mesh": 3, + "name": "Fan F_Black Fan_0" + }, + { + "mesh": 4, + "name": "Fan F_Slot.1_0" + }, + { + "children": [13], + "matrix": [ + 1.0, 0.0, 0.0, 0.0, 0.0, -1.6292067939183141e-7, 0.9999999999999868, 0.0, 0.0, + -0.9999999999999868, -1.6292067939183141e-7, 0.0, 0.021076202392578125, 26.082378387451172, + 14.08882999420166, 1.0 + ], + "name": "Front Cover U" + }, + { + "mesh": 5, + "name": "Front Cover U_Black_0" + }, + { + "children": [15], + "matrix": [ + -0.9999999999999203, -3.8941437731121037e-7, 8.742276236262104e-8, 0.0, + 8.742269891895826e-8, 1.6292071369772287e-7, 0.9999999999999828, 0.0, -3.894143915541825e-7, + 0.9999999999999108, -1.6292067961387602e-7, 0.0, -4.7524333000183105, 163.40081787109375, + 14.088836669921875, 1.0 + ], + "name": "Front Cover T" + }, + { + "mesh": 6, + "name": "Front Cover T_Black_0" + }, + { + "children": [17], + "matrix": [ + -0.7936016917228065, 6.937887475876209e-8, 3.090399089678156e-7, 0.0, + -3.0903990372985424e-7, 5.99152878920572e-8, -0.7936016917228074, 0.0, + -6.937889809063118e-8, -0.7936016917228645, -5.991526075495118e-8, 0.0, -124.1536636352539, + 88.51213073730469, -40.17750549316406, 1.0 + ], + "name": "Fan Circle B" + }, + { + "mesh": 7, + "name": "Fan Circle B_Black Fan_0" + }, + { + "children": [19], + "matrix": [ + 0.38677331740211784, -0.3867733343085177, 6.07269882145477e-17, 0.0, -3.632659060470012e-7, + -3.632658903889941e-7, 11.752899169921864, 0.0, -0.38677333430851735, -0.38677331740211757, + -2.3909259764264718e-8, 0.0, -0.11941343545913696, 3.1571507453918457, 3.0867953300476074, + 1.0 + ], + "name": "Grills U" + }, + { + "mesh": 8, + "name": "Grills U_Metal Black_0" + }, + { + "children": [21], + "matrix": [ + -0.38677332816141635, 0.3867733235491907, 1.4909048089098407e-7, 0.0, 3.6191709386740606e-6, + -9.112484713068894e-7, 11.752899169921283, 0.0, 0.38677332354918276, 0.3867733281614427, + -8.911436685577966e-8, 0.0, 0.7981881499290466, 174.49168395996094, 3.0868005752563477, 1.0 + ], + "name": "Grills T" + }, + { + "mesh": 9, + "name": "Grills T_Metal Black_0" + }, + { + "children": [23], + "matrix": [ + -0.9999999999999203, -3.8941437731121037e-7, 8.742276236262104e-8, 0.0, + 8.742269891895826e-8, 1.6292071369772287e-7, 0.9999999999999828, 0.0, + -3.2565728098324384e-7, 0.8362743854521959, -1.3624639122156042e-7, 0.0, 121.83759307861328, + 88.41606140136719, -34.240440368652344, 1.0 + ], + "name": "Plane.010" + }, + { + "mesh": 10, + "name": "Plane.010_Black.001_0" + }, + { + "children": [25], + "matrix": [ + 1.0, 0.0, 0.0, 0.0, 0.0, -3.14346242379091e-7, 1.929443478584264, 0.0, 0.0, + -0.9999999999999868, -1.6292067939183141e-7, 0.0, -149.70736694335938, 187.4652557373047, + -39.00934982299805, 1.0 + ], + "name": "Socket" + }, + { + "mesh": 11, + "name": "Socket_Slot_0" + }, + { + "children": [27], + "matrix": [ + 1.0, 0.0, 0.0, 0.0, 0.0, -1.6292067939183141e-7, 0.9999999999999868, 0.0, 0.0, + -0.9999999999999868, -1.6292067939183141e-7, 0.0, -225.87086486816406, 118.08707427978516, + -12.542006492614746, 1.0 + ], + "name": "Side Metal Part" + }, + { + "mesh": 12, + "name": "Side Metal Part_Metal S_0" + }, + { + "children": [29], + "matrix": [ + 1.0, 0.0, 0.0, 0.0, 0.0, -1.6292067939183141e-7, 0.9999999999999868, 0.0, 0.0, + -1.0234513282775743, -1.667413857274569e-7, 0.0, 131.4942626953125, 88.83919525146484, + -23.017173767089844, 1.0 + ], + "name": "Grills F.003" + }, + { + "mesh": 13, + "name": "Grills F.003_Metal Black_0" + }, + { + "children": [31], + "matrix": [ + -0.9999999999999241, 9.892427301959894e-15, 3.8941437775530107e-7, 0.0, + -3.7781312610095593e-7, 7.32487015753169e-8, -0.9702084660529326, 0.0, + -4.0213853733746167e-14, -1.0234513282775848, -7.72684257400924e-8, 0.0, -128.1754608154297, + 88.83919525146484, -4.171847343444824, 1.0 + ], + "name": "Grills F.002" + }, + { + "mesh": 14, + "name": "Grills F.002_Metal Black_0" + }, + { + "children": [33, 34], + "matrix": [ + -0.3033416743311482, 0.01529620971837044, 9.933227977876897e-8, 0.0, -1.003610534326315e-7, + -1.789911585542887e-8, -0.3037270605563946, 0.0, -0.015296209718363723, + -0.30334167433116394, 2.29307573058707e-8, 0.0, -123.89674377441406, 88.5121078491211, + -37.82356262207031, 1.0 + ], + "name": "Fan B" + }, + { + "mesh": 15, + "name": "Fan B_Black Fan_0" + }, + { + "mesh": 16, + "name": "Fan B_Slot.1_0" + } + ], + "samplers": [ + { + "magFilter": 9729, + "minFilter": 9987, + "wrapS": 10497, + "wrapT": 10497 + } + ], + "scene": 0, + "scenes": [ + { + "name": "Sketchfab_Scene", + "nodes": [0] + } + ], + "textures": [ + { + "sampler": 0, + "source": 0 + }, + { + "sampler": 0, + "source": 1 + }, + { + "sampler": 0, + "source": 2 + }, + { + "sampler": 0, + "source": 3 + }, + { + "sampler": 0, + "source": 4 + }, + { + "sampler": 0, + "source": 5 + }, + { + "sampler": 0, + "source": 6 + }, + { + "sampler": 0, + "source": 7 + }, + { + "sampler": 0, + "source": 8 + }, + { + "sampler": 0, + "source": 9 + }, + { + "sampler": 0, + "source": 10 + }, + { + "sampler": 0, + "source": 11 + }, + { + "sampler": 0, + "source": 12 + } + ] } diff --git a/svelte.config.js b/svelte.config.js index 94d9b73379903189b2e538783c0b8bc5d934623a..885519e5a692c28bd4560bf01916592e6b598c1f 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -13,9 +13,9 @@ const config = { // If your environment is not supported, or you settled on a specific environment, switch out the adapter. // See https://svelte.dev/docs/kit/adapters for more information about adapters. adapter: adapter({ - pages: 'build', - assets: 'build', - fallback: 'index.html', + pages: "build", + assets: "build", + fallback: "index.html", precompress: false, strict: true }), diff --git a/vite.config.ts b/vite.config.ts index 7969ff97b1ee9542363be627577f6d912a47a45b..9bee9f425c32d2c3210e885e799be7cd0bf58601 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,6 +3,6 @@ import { sveltekit } from "@sveltejs/kit/vite"; import { defineConfig } from "vite"; export default defineConfig({ - plugins: [tailwindcss(), sveltekit()], + plugins: [tailwindcss(), sveltekit()] // server: { fs: { allow: ["../backend/client/js", "packages/feetech.js"] } } });