commit 64b498238558e0aac51e1d49c7d0bcee8b3867d0 Author: cpu Date: Fri Sep 4 15:48:02 2026 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..662b0b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +ui/ +dist/ + diff --git a/Development.md b/Development.md new file mode 100644 index 0000000..05cbbd0 --- /dev/null +++ b/Development.md @@ -0,0 +1,149 @@ +# Developing and Simulating on Linux + +When developing custom CNC plugins—especially those manipulating Work Coordinate Systems (WCS)—testing on physical hardware can result in dangerous and costly crashes. + +Fortunately, FluidNC provides a native PC port that can run directly on Linux. By pairing your local instance of gSender Edge with this virtual controller, you can rapidly test UI changes, G-code injection, and coordinate transformations in a 100% safe environment. + +## Prerequisites + +Ensure your Linux environment has the required build tools and dependencies. You will need Git, Python, `socat` (for the virtual serial cable), and `picocom` (for testing). To compile gSender locally, you will also need the C++ build chain, curl, and some specific UI libraries. + +```bash +sudo apt-get update +sudo apt-get install git python3 python3-venv socat picocom build-essential libudev-dev libgtk-3-dev curl +``` + +--- + +## Step 1: Set Up PlatformIO and Compile FluidNC + +FluidNC uses PlatformIO for its build system. Before compiling, you need to create a Python virtual environment and install the PlatformIO CLI. + +```bash +# Create and activate a Python virtual environment +python3 -m venv pio_env +source pio_env/bin/activate + +# Install PlatformIO +pip install platformio +``` + +With PlatformIO active, clone the FluidNC repository and compile the native Linux 64-bit simulator. + +```bash +git clone https://github.com/bdring/FluidNC.git +cd FluidNC +pio run -e linux_x86_64 +``` +*(This will download the necessary toolchains and output the compiled program to `./.pio/build/linux_x86_64/program`)* + +## Step 2: Initialize Virtual Filesystem & Configuration + +The Linux simulator expects a local folder named `native_localfs` to act as flash storage. Create the folder and copy in the standard test-drive configuration: + +```bash +# From the root of the FluidNC directory: +mkdir native_localfs +cp ./FluidNC/data/config.yaml ./native_localfs/ +``` + +--- + +## Step 3: Connect gSender (Choose Method A or B) + +You can connect gSender to the simulator using either a Virtual Serial Port or a local Network Socket (Ethernet). The Ethernet method is faster, but the Serial method (see **Method B**) mimics physical hardware more closely. + +### Method A: The Ethernet Shortcut (Recommended) + +FluidNC natively simulates the ESP32's network stack by opening a Telnet server on port 23. + +1. Grant the simulator permission to bind to privileged network ports: + ```bash + sudo setcap 'cap_net_bind_service=+ep' ./.pio/build/linux_x86_64/program + ``` +2. Run the simulator normally: + ```bash + ./.pio/build/linux_x86_64/program + ``` +3. In gSender, select the **Ethernet** connection option and connect to `127.0.0.1` on port `23`. +4. **WAKE THE GUI:** Because Telnet connections do not trigger a hardware reset, FluidNC will not automatically send its welcome string, leaving gSender's UI disabled. **Open the Console tab in gSender, type `?`, and press Enter.** The controller will respond, and the UI will unlock into the `Idle` state. + +--- + +### Method B: Virtual Serial Port (socat) + +If you need to test standard serial behavior, use `socat` to create a virtual null-modem cable. We name the gSender side `ttyCNC` to trick gSender's strict hardware filters into listing the port. + +1. Open **Terminal A** and create the virtual cable: + ```bash + sudo socat -d -d \ + pty,link=/dev/ttyCNC,raw,echo=0,b115200,mode=666 \ + pty,link=/dev/ttyVIRT,raw,echo=0,b115200,mode=666 + ``` +2. Open **Terminal B** and run the simulator, piping its input/output directly into the virtual cable: + ```bash + ./.pio/build/linux_x86_64/program < /dev/ttyVIRT > /dev/ttyVIRT + ``` + *(Note: This terminal will appear to freeze. This is correct! All output is being routed into the virtual cable.)* +3. Add **ports** section into `~/.sender_rc`. It should look like this: +```json +{ + "ports": [ + { + "path": "/dev/ttyCNC", + "manufacturer": "Virtual CNC", + "vendorId": "0403", + "productId": "6001" + } + ], + "events": {}, + // other definitions +} +``` +4. Open **gSender**, select **Serial**, choose `ttyCNC` from the Ports list, (make sure the baudrate is set to `115200` in the **Config->Basic->Baud** rate), and click **Connect**. + +--- + +## Step 4: Test Your Plugin + +Your fully functional virtual CNC machine is now running! + +--- + +## Step 5: Run gSender for Development + +If you're actively developing custom CNC plugins or modifying gSender's UI, it is highly recommended to run gSender from its source code. This enables hot-reloading for faster iteration. The following instructions are adapted from the official [Sienci Compile gSender guide](https://resources.sienci.com/view/gs-compile/). + +### 1. Install Node.js and Yarn +gSender is a Node.js/Electron application. We will use Node Version Manager (NVM) to install the necessary Node.js version. + +```bash +# Install Node Version Manager (NVM) +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash +source "$HOME/.nvm/nvm.sh" + +# Install Node.js (v24.x is currently recommended) +nvm install 24 + +# Enable Yarn package manager +corepack enable yarn +``` + +### 2. Clone gSender and Install Dependencies +```bash +# Clone the gSender repository +git clone https://github.com/Sienci-Labs/gsender.git +cd gsender + +# Install all required packages (this may take some time to compile native modules) +yarn install +``` + +### 3. Launch Development Mode +To start gSender in development mode with hot-reloading enabled: +```bash +yarn dev +``` +*(This launches the local development server. You can open gSender in your browser at `http://localhost:8000`, or via the automatically launched Electron window. Any changes you make to the source code will automatically reload.)* + +Once running, you can connect this development instance of gSender directly to your FluidNC simulator using either the Ethernet or Virtual Serial method outlined in Step 3! diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..9332144 --- /dev/null +++ b/Readme.md @@ -0,0 +1,242 @@ +# gSender Vision Alignment Plugin (FluidNC) + +## Overview +The **Vision Alignment Plugin** is a custom extension for gSender (v1.7.0-Edge-1 and later) running on **Linux**. It interfaces with FluidNC-based CNC controllers and uses a live camera feed with a digital cross-hair overlay for highly accurate stock alignment. + +By guiding the user to locate three known fiducial markers on their stock, the plugin mathematically determines the exact X/Y workspace origin and the angular deviation of the material. Since FluidNC does not natively support hardware-level coordinate rotation (`G68`), the plugin automatically sets the work zero and **transforms the loaded G-code in software**—applying a rotation matrix to compensate for the skewed stock—completely eliminating the need for perfect physical stock alignment. + +## Features +* **Live Camera Feed:** Streams real-time video directly within the gSender interface using any standard USB webcam or microscope camera. +* **Software Cross-Hair Overlay:** Provides a customizable, digital center-target for high-precision visual probing. +* **Three-Point Fiducial Registration:** + * Captures machine coordinates at three distinct points on the stock. + * Point 1 establishes the reference origin. + * Points 2 and 3 establish the true X/Y axes vectors and verify scale/skew. +* **Software-Side Skew Compensation:** Calculates the exact rotational angle (θ) of the stock. The plugin then parses your loaded G-code and applies a rotational matrix (adjusting all `X`, `Y`, `I`, and `J` values) to dynamically match the skew of your stock before sending the job to the controller. +* **Automated Work Zeroing:** Automatically calculates and applies camera-to-spindle offsets and issues `G10 L20` commands to establish the new Work Coordinate System (WCS). + +## Prerequisites +* **gSender:** v1.7.0-Edge-1 or later on Linux (plugin system required). +* **Controller:** A CNC machine running **FluidNC** firmware (or any Grbl / grblHAL-based firmware). +* **Hardware:** A rigidly mounted camera (e.g., attached to the spindle or Z-axis carriage) with calibrated X/Y offsets. +* **Development Environment:** Node.js 18+ and yarn (or npm). + +## Installation (Linux end users) + +gSender loads plugins from: + +```text +~/.config/gSender/plugins/ +``` + +(The exact path is also shown under **Tools → Plugins** inside the app.) + +1. Obtain a **built** plugin folder (one that already contains `gsender-plugin.json` **and** a `ui/` directory with `index.html`). +2. Copy that folder into `~/.config/gSender/plugins/`, **or** use **Tools → Plugins → Import** and select the folder. +3. Restart gSender. The plugin appears on the **Tools** page. + +> Plugins with a `com.sienci.*` id are labelled “Sienci official”; all others appear as “Community”. + +## Usage Guide +1. Open the **Tools** page and launch **Vision Alignment**. +2. Select your camera device from the dropdown to initialize the feed. +3. **Calibrate Camera Offset (if not already done):** Enter the physical X and Y distance between your spindle center and camera center. +4. **Point 1 (Origin):** Jog the machine until the cross-hair is perfectly centered on your first fiducial. Click **"Mark Point 1"**. +5. **Point 2 (Axis Reference):** Jog to the second fiducial (defines your primary axis line) and click **"Mark Point 2"**. +6. **Point 3 (Verification):** Jog to the third fiducial and click **"Mark Point 3"**. +7. Click **"Align & Zero Workspace"**. + * The plugin processes the affine transformation math. + * It zeros the workspace to the calculated origin point. + * It applies a rotation matrix to the loaded G-code so the toolpath matches the rotational skew of your stock. + +--- + +## Plugin architecture (gSender 1.7+ on Linux) + +gSender plugins are self-contained SPA folders. Each plugin **must** contain: + +* `gsender-plugin.json` – manifest +* `ui/` – **built** frontend (produced by Vite; not present until you build) + +### Manifest (`gsender-plugin.json`) + +| Field | Required | Description | +|--------------------|----------|-------------| +| `id` | yes | Unique reverse-DNS id (e.g. `com.yourname.vision-alignment`). | +| `name` | yes | Display name shown on the Tools page. | +| `version` | yes | Semver string. | +| `description` | no | Short blurb on the plugin card. | +| `engine` | no | Compatible gSender version range (e.g. `>=1.7.0`). | +| `ui.entry` | yes | Path to the built entry HTML (usually `ui/index.html`). | +| `ui.contributions` | no | Array of `{ slot, route, label }` – most plugins use `"slot": "tools-page"`. | +| `capabilities` | no | Bridge permissions the plugin is allowed to use. | + +Example skeleton: + +```json +{ + "id": "com.yourname.vision-alignment", + "name": "Vision Alignment", + "description": "Camera-based stock alignment and software skew compensation for FluidNC.", + "version": "0.1.0", + "engine": ">=1.7.0", + "ui": { + "entry": "ui/index.html", + "contributions": [ + { + "slot": "tools-page", + "route": "vision-alignment", + "label": "Vision Alignment" + } + ] + }, + "capabilities": { + "requestTypes": ["gcode:load:to:visualizer"], + "topics": ["workspace"], + "allowedFunctions": ["gcode", "machine", "useWorkspaceState"] + } +} +``` + +The bridge **denies** any call the plugin was not granted. + +### Official examples & SDK + +Official examples live in the gSender repo under [`plugins/`](https://github.com/Sienci-Labs/gsender/tree/v1.7.0-Edge-1/plugins): + +| Folder | Stack | Demonstrates | +|-------------------|--------------------------------|--------------| +| `example-hello/` | Plain JS + Vite | Bridge client, subscriptions | +| `react-ts-app/` | React + TypeScript + Vite | React hooks | +| `example-viewer/` | Plain JS + Vite | Embedded G-code preview | +| `basic-cam/` | React + TypeScript + Vite + Tailwind | Full reference CAM plugin | + +SDK package: `@sienci/gsender-plugin-sdk` (source lives in `packages/plugin-sdk`). + +| Import | Purpose | +|--------|---------| +| `@sienci/gsender-plugin-sdk` | Framework-agnostic bridge | +| `@sienci/gsender-plugin-sdk/react` | React hooks | +| `@sienci/gsender-plugin-sdk/viewer` | G-code viewer (`@sienci/gviewer`) | +| `@sienci/gsender-plugin-sdk/vite` | Vite plugin for correct externalisation & import map | + +Always build plugins with the SDK’s Vite helper: + +```ts +// vite.config.ts +import gsenderPlugin from "@sienci/gsender-plugin-sdk/vite"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [react(), gsenderPlugin()], + base: "./", + build: { outDir: "ui", emptyOutDir: true }, +}); +``` + +Dark mode is applied by gSender as the class `html.dark` on the plugin iframe—use class-based dark styles (or Tailwind `dark:` with class strategy), not `prefers-color-scheme`. + +--- + +## Building plugins (Linux development) + +### Important: build the SDK first + +The example plugins depend on the local SDK via `file:../../packages/plugin-sdk`. That package’s `package.json` points at `./dist/…`, which **does not exist until you build it**. If you skip this step you will see: + +```text +Failed to resolve entry for package "@sienci/gsender-plugin-sdk" +``` + +### Step-by-step + +From the **gSender repo root** (`~/proj/gsender` or similar): + +```bash +# 1. Build the plugin SDK (creates packages/plugin-sdk/dist/) +cd packages/plugin-sdk +yarn install +yarn build + +# 2. Build the example plugin(s) you need +cd ../../plugins/example-hello +yarn install +yarn build # creates ui/index.html + +# Repeat for other examples if desired: +# cd ../react-ts-app && yarn install && yarn build +# cd ../example-viewer && yarn install && yarn build +# cd ../basic-cam && yarn install && yarn build +``` + +Or build everything in one go: + +```bash +cd ~/proj/gsender + +(cd packages/plugin-sdk && yarn install && yarn build) + +for d in plugins/example-hello plugins/react-ts-app plugins/example-viewer plugins/basic-cam; do + echo "=== Building $d ===" + (cd "$d" && yarn install && yarn build) +done +``` + +After a successful build you should have: + +```text +plugins/example-hello/ui/index.html +``` + +### “UI entry not found: ui/index.html” + +This red message in **Tools → Plugins** means the plugin folder exists but has not been built (no `ui/` directory). Run `yarn build` inside the plugin folder (after the SDK is built) and refresh/restart gSender. + +### Watch mode while developing + +```bash +cd plugins/example-hello # or your own plugin +yarn build -- --watch +``` + +gSender (when started with `yarn dev` / `NODE_ENV=development`) watches each plugin’s `ui/` directory and reloads the iframe automatically. + +### Starting from an official template + +1. Copy the closest example (`example-hello`, `react-ts-app`, `example-viewer`, or `basic-cam`). +2. Edit `gsender-plugin.json` – change `id`, `name`, `description`, `route`, and `label`. +3. Rename the folder if desired (the manifest `id` is what matters). +4. Ensure the SDK is built (`packages/plugin-sdk` → `yarn build`). +5. `yarn install && yarn build` inside the new plugin folder. +6. Place the folder in `~/.config/gSender/plugins/` **or** keep it under the gSender repo’s `plugins/` for local dev. + +### Local development inside the gSender source tree + +When gSender runs in development (`NODE_ENV=development`, e.g. `yarn dev` or `yarn electron:hot`): + +* Plugins are loaded from **both** the repo’s `plugins/` folder **and** `~/.config/gSender/plugins/`. +* Repo plugins take precedence when ids collide. +* Extra search paths: `GSENDER_PLUGINS_DIRS` (colon-separated on Linux). +* Adding a brand-new plugin folder requires a one-time server restart; subsequent edits hot-reload once `ui/` is present. + +--- + +## Development & Testing Workflow + +### Leveraging the FluidNC PC Simulation Feature + +When developing a CNC plugin that manipulates Work Coordinate Systems and modifies G-code on the fly, testing on a physical machine carries a high risk of accidental crashes. **Because you are developing on Linux, the FluidNC PC Simulation feature is highly recommended.** + +FluidNC provides a **PC port/simulation mode** that compiles and runs the firmware as a native executable, completely bypassing the need for an ESP32 microcontroller or physical steppers. + +#### Why it is critical for this plugin’s development +1. **Zero Hardware Risk:** Safely test the trigonometry for software-side G-code transformation (linear moves and arcs) and workspace zeroing (`G10 L20`) without risk of plunging a real endmill into the spoilboard. +2. **Rapid Iteration:** Run gSender Edge and the FluidNC simulator side-by-side on the same Linux machine—no workshop computer or hardware cabling required for UI/math work. +3. **Toolpath Verification:** The simulator behaves identically to real FluidNC firmware. Send the dynamically rotated G-code from the plugin and query the controller (`?` or `$#`) to confirm the virtual machine executes the skewed toolpath correctly. + +#### Setup Guide for the Simulator +See the [Development](Development.md) guide. + +## License +MIT License. See [LICENSE](LICENSE) for more information. diff --git a/gsender-plugin.json b/gsender-plugin.json new file mode 100644 index 0000000..3780945 --- /dev/null +++ b/gsender-plugin.json @@ -0,0 +1,28 @@ +{ + "id": "com.example.vision-alignment", + "name": "Vision Alignment", + "description": "Camera-based stock alignment with three-point registration and software skew compensation for FluidNC/Grbl.", + "version": "0.2.1", + "engine": ">=1.7.0", + "ui": { + "entry": "ui/index.html", + "contributions": [ + { + "slot": "tools-page", + "route": "vision-alignment", + "label": "Vision Alignment" + } + ] + }, + "capabilities": { + "requestTypes": [ + "gcode:load:to:visualizer", + "machine:command", + "machine:get:context", + "workspace:get:state", + "redux:get:state" + ], + "topics": ["workspace", "redux"], + "allowedFunctions": ["gcode", "machine", "useWorkspaceState", "redux"] + } +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..70ed258 --- /dev/null +++ b/index.html @@ -0,0 +1,178 @@ + + + + + + Vision Alignment + + +
+
+

Vision Alignment

+

+ Three-point fiducial registration · camera offset · software skew compensation +

+
+ +
+ +
+ + +
+

Camera idle

+
+ +
+
+ + +
+ Start the camera, jog so the cross-hair is on a fiducial, then mark points. +
+
+
+ +
+

Jog

+
+
+ + + + + +
+
+ + +
+
+
+
MPos X — Y — Z —
+
WPos X — Y — Z —
+
+
+ + +
+ +
+
+

Uses Grbl $J= jogging. Hold buttons for continuous when enabled; release or ⏹ to cancel.

+
+ +
+

Camera-to-spindle offset

+

+ Distance from camera centre to spindle centre + (positive X = spindle is to the +X of the camera). +

+
+ + +
+
+ +
+

Fiducial points

+

+ Live: X — Y — + +

+
+
+ Point 1 · Origin + not set + +
+
+ Point 2 · Axis + not set + +
+
+ Point 3 · Verify + not set + +
+
+
+ +
+
+ +
+

Alignment result

+
Mark points 1 and 2 (3 optional) then Align.
+
+ + +
+

+ Align & Zero computes θ from points 1→2, applies camera offset, + and issues G10 L20 so spindle WCS origin matches fiducial 1. + Rotate loaded G-code rewrites X/Y/I/J by −θ and reloads the job + (FluidNC has no G68). +

+
+ +
+ + +
+ +
+
+ Debug log + +
+

+      
+
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..0c7ab05 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "vision-camera", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@sienci/gsender-plugin-sdk": "file:../../packages/plugin-sdk" + }, + "devDependencies": { + "vite": "^6.0.0" + } +} diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..1ea3867 --- /dev/null +++ b/src/main.js @@ -0,0 +1,694 @@ +import "./style.css"; +import { + gsender, + subscribeWorkspaceState, + subscribeSelector, +} from "@sienci/gsender-plugin-sdk"; + +// ---- DOM ---- +const videoEl = document.getElementById("cam"); +const selectEl = document.getElementById("camera-select"); +const btnStart = document.getElementById("btn-start"); +const btnStop = document.getElementById("btn-stop"); +const statusEl = document.getElementById("status"); +const viewportEl = document.getElementById("viewport"); +const colorInput = document.getElementById("crosshair-color"); +const thicknessInput = document.getElementById("crosshair-thickness"); +const debugLogEl = document.getElementById("debug-log"); +const btnClearLog = document.getElementById("btn-clear-log"); +const livePosEl = document.getElementById("live-pos"); +const connBadge = document.getElementById("conn-badge"); +const offsetXEl = document.getElementById("offset-x"); +const offsetYEl = document.getElementById("offset-y"); +const ptDisplay = [ + document.getElementById("pt1-display"), + document.getElementById("pt2-display"), + document.getElementById("pt3-display"), +]; +const btnMark = [ + document.getElementById("btn-mark-1"), + document.getElementById("btn-mark-2"), + document.getElementById("btn-mark-3"), +]; +const btnClearPoints = document.getElementById("btn-clear-points"); +const btnAlign = document.getElementById("btn-align"); +const btnRotateGcode = document.getElementById("btn-rotate-gcode"); +const alignResultEl = document.getElementById("align-result"); + +// ---- State ---- +let currentStream = null; +let livePos = { x: null, y: null, z: null }; +/** @type {({x:number,y:number,z:number}|null)[]} */ +const points = [null, null, null]; +let lastAlign = null; + +const logLines = []; +const MAX_LOG = 250; + +function log(msg, data) { + const ts = new Date().toISOString().slice(11, 23); + let line = `[${ts}] ${msg}`; + if (data !== undefined) { + try { + line += "\n" + (typeof data === "string" ? data : JSON.stringify(data, null, 2)); + } catch { + line += `\n${String(data)}`; + } + } + logLines.push(line); + while (logLines.length > MAX_LOG) logLines.shift(); + if (debugLogEl) { + debugLogEl.textContent = logLines.join("\n\n"); + debugLogEl.scrollTop = debugLogEl.scrollHeight; + } + console.log(`[vision-align] ${msg}`, data !== undefined ? data : ""); +} + +function setStatus(text, kind = "") { + statusEl.textContent = text; + statusEl.className = "status" + (kind ? ` ${kind}` : ""); +} + +function fmt(n, digits = 3) { + if (n == null || Number.isNaN(n)) return "—"; + return Number(n).toFixed(digits); +} + +function applyCrosshairStyle() { + document.documentElement.style.setProperty("--crosshair", colorInput.value); + document.documentElement.style.setProperty( + "--crosshair-thickness", + `${thicknessInput.value}px`, + ); + try { + localStorage.setItem("va-crosshair-color", colorInput.value); + localStorage.setItem("va-crosshair-thickness", thicknessInput.value); + } catch { /* ignore */ } +} + +function loadPrefs() { + try { + const c = localStorage.getItem("va-crosshair-color"); + const t = localStorage.getItem("va-crosshair-thickness"); + const ox = localStorage.getItem("va-offset-x"); + const oy = localStorage.getItem("va-offset-y"); + if (c) colorInput.value = c; + if (t) thicknessInput.value = t; + if (ox != null) offsetXEl.value = ox; + if (oy != null) offsetYEl.value = oy; + } catch { /* ignore */ } + applyCrosshairStyle(); +} + +function saveOffsets() { + try { + localStorage.setItem("va-offset-x", offsetXEl.value); + localStorage.setItem("va-offset-y", offsetYEl.value); + } catch { /* ignore */ } +} + +async function listCameras() { + if (!navigator.mediaDevices?.enumerateDevices) return []; + const devices = await navigator.mediaDevices.enumerateDevices(); + return devices.filter((d) => d.kind === "videoinput"); +} + +async function refreshCameraList(preferredId) { + const cams = await listCameras(); + selectEl.innerHTML = ""; + if (!cams.length) { + selectEl.innerHTML = ''; + return; + } + for (const cam of cams) { + const opt = document.createElement("option"); + opt.value = cam.deviceId; + opt.textContent = cam.label || `Camera ${cam.deviceId.slice(0, 8)}…`; + selectEl.appendChild(opt); + } + if (preferredId && cams.some((c) => c.deviceId === preferredId)) { + selectEl.value = preferredId; + } +} + +function stopCamera() { + if (currentStream) { + currentStream.getTracks().forEach((t) => t.stop()); + currentStream = null; + } + videoEl.srcObject = null; + videoEl.removeAttribute("data-active"); + viewportEl.removeAttribute("data-streaming"); + btnStart.disabled = false; + btnStop.disabled = true; + setStatus("Camera stopped"); +} + +async function startCamera(deviceId) { + stopCamera(); + if (!navigator.mediaDevices?.getUserMedia) { + setStatus("getUserMedia not available", "error"); + return; + } + const constraints = { + audio: false, + video: deviceId + ? { deviceId: { exact: deviceId }, width: { ideal: 1280 }, height: { ideal: 720 } } + : { width: { ideal: 1280 }, height: { ideal: 720 } }, + }; + setStatus("Requesting camera…"); + try { + const stream = await navigator.mediaDevices.getUserMedia(constraints); + currentStream = stream; + videoEl.srcObject = stream; + await videoEl.play(); + videoEl.setAttribute("data-active", "true"); + viewportEl.setAttribute("data-streaming", "true"); + btnStart.disabled = true; + btnStop.disabled = false; + const track = stream.getVideoTracks()[0]; + const settings = track?.getSettings?.() ?? {}; + setStatus( + `Live — ${track?.label || "Camera"}${settings.width ? ` (${settings.width}×${settings.height})` : ""}`, + "live", + ); + const id = settings.deviceId || deviceId; + await refreshCameraList(id); + if (id) { + try { localStorage.setItem("va-device-id", id); } catch { /* ignore */ } + } + } catch (err) { + const name = err?.name || "Error"; + log("getUserMedia FAILED", { name, message: err?.message }); + setStatus(`${name}: ${err?.message || err}`, "error"); + btnStart.disabled = false; + btnStop.disabled = true; + } +} + +function parseAxis(obj) { + if (!obj || typeof obj !== "object") return null; + const x = Number(obj.x ?? obj.X); + const y = Number(obj.y ?? obj.Y); + const z = Number(obj.z ?? obj.Z); + if (Number.isFinite(x) && Number.isFinite(y)) { + return { x, y, z: Number.isFinite(z) ? z : 0 }; + } + return null; +} + +/** Prefer machine position for fiducial marks (WPos is often 0,0,0 after zeroing). */ +function extractXY(ctx) { + if (!ctx || typeof ctx !== "object") return null; + const mpos = + parseAxis(ctx.position) || + parseAxis(ctx.mpos) || + parseAxis(ctx.machinePosition) || + parseAxis(ctx.machine) || + parseAxis(ctx.MPos); + if (mpos) return mpos; + const wpos = + parseAxis(ctx.workPosition) || + parseAxis(ctx.wpos) || + parseAxis(ctx.work) || + parseAxis(ctx.WPos); + if (wpos) return wpos; + return parseAxis(ctx); +} + +function extractBoth(ctx) { + if (!ctx || typeof ctx !== "object") return { mpos: null, wpos: null }; + const mpos = + parseAxis(ctx.position) || + parseAxis(ctx.mpos) || + parseAxis(ctx.machinePosition) || + parseAxis(ctx.machine) || + parseAxis(ctx.MPos); + const wpos = + parseAxis(ctx.workPosition) || + parseAxis(ctx.wpos) || + parseAxis(ctx.work) || + parseAxis(ctx.WPos); + return { mpos, wpos }; +} + +async function readCurrentPosition() { + try { + const ctx = await gsender.machine.getContext(); + log("machine.getContext", ctx); + const p = extractXY(ctx); + if (p) return p; + } catch (e) { + log("machine.getContext failed", e?.message || String(e)); + } + try { + const ws = await gsender.workspace.getState(); + log("workspace.getState", ws); + const p = extractXY(ws); + if (p) return p; + } catch (e) { + log("workspace.getState failed", e?.message || String(e)); + } + if (livePos.x != null && livePos.y != null) { + return { ...livePos }; + } + throw new Error("Could not read machine position from bridge"); +} + +function updatePointUI() { + for (let i = 0; i < 3; i++) { + const p = points[i]; + ptDisplay[i].textContent = p + ? `X${fmt(p.x)} Y${fmt(p.y)} Z${fmt(p.z)}` + : "not set"; + } + const ready = !!(points[0] && points[1]); + btnAlign.disabled = !ready; + const canRotate = !!lastAlign; + btnRotateGcode.disabled = !canRotate; + log("updatePointUI", { + ready, + canRotate, + lastAlignThetaDeg: lastAlign ? lastAlign.thetaDeg : null, + btnRotateDisabled: btnRotateGcode.disabled, + }); +} + +async function markPoint(index) { + try { + const p = await readCurrentPosition(); + points[index] = p; + log(`Marked point ${index + 1}`, p); + updatePointUI(); + setStatus(`Point ${index + 1} marked`, "live"); + } catch (e) { + log(`Mark point ${index + 1} failed`, e?.message || String(e)); + setStatus(String(e?.message || e), "error"); + } +} + +function clearPoints() { + points[0] = points[1] = points[2] = null; + lastAlign = null; + updatePointUI(); + alignResultEl.textContent = "Mark points 1 and 2 (3 optional) then Align."; + log("Points cleared"); +} + +function computeAlignment(p1, p2, p3, offsetX, offsetY) { + const dx = p2.x - p1.x; + const dy = p2.y - p1.y; + const dist12 = Math.hypot(dx, dy); + if (dist12 < 1e-6) { + throw new Error("Point 1 and 2 are too close"); + } + const theta = Math.atan2(dy, dx); + const thetaDeg = (theta * 180) / Math.PI; + const spindleAtP1 = { x: p1.x + offsetX, y: p1.y + offsetY }; + let skewNote = ""; + if (p3) { + const dx3 = p3.x - p1.x; + const dy3 = p3.y - p1.y; + const dist13 = Math.hypot(dx3, dy3); + const ang3 = Math.atan2(dy3, dx3); + const delta = ((ang3 - theta) * 180) / Math.PI; + let d = ((delta + 180) % 360) - 180; + if (d < -180) d += 360; + skewNote = `P1→P3 angle vs P1→P2: ${d.toFixed(2)}° (ideal ±90°). Dist P1–P3: ${fmt(dist13)}`; + } + return { theta, thetaDeg, dist12, spindleAtP1, offsetX, offsetY, skewNote }; +} + +async function alignAndZero() { + if (!points[0] || !points[1]) { + setStatus("Need Point 1 and Point 2", "error"); + return; + } + const offsetX = Number(offsetXEl.value) || 0; + const offsetY = Number(offsetYEl.value) || 0; + saveOffsets(); + + try { + const result = computeAlignment( + points[0], + points[1], + points[2], + offsetX, + offsetY, + ); + lastAlign = result; + btnRotateGcode.disabled = false; + log("lastAlign set — Rotate button should be enabled", { + thetaDeg: result.thetaDeg, + btnRotateDisabled: btnRotateGcode.disabled, + }); + + const text = [ + `Stock angle θ = ${result.thetaDeg.toFixed(4)}° (${result.theta.toFixed(6)} rad)`, + `P1→P2 distance = ${fmt(result.dist12)}`, + `Camera→spindle offset = X${fmt(offsetX)} Y${fmt(offsetY)}`, + `Spindle at fiducial 1 ≈ X${fmt(result.spindleAtP1.x)} Y${fmt(result.spindleAtP1.y)}`, + result.skewNote || "", + "", + "Issuing G10 L20 to set WCS origin at current spindle = fiducial 1…", + "(Jog so camera is on Point 1 before Align.)", + ] + .filter(Boolean) + .join("\n"); + alignResultEl.textContent = text; + log("Alignment computed", result); + + try { + await sendRaw("G10 L20 P1 X0 Y0"); + alignResultEl.textContent += + "\n\nSent: G10 L20 P1 X0 Y0 (current tool position → WCS origin).\n" + + "Ensure the camera was still on Point 1 (or jog there) before Align."; + setStatus("Align & zero done", "live"); + } catch (g10err) { + log("G10 zero failed (alignment math still kept)", g10err?.message || String(g10err)); + alignResultEl.textContent += + "\n\nG10 failed: " + (g10err?.message || g10err) + + "\nAngle is still available — use Rotate loaded G-code."; + setStatus("Aligned (G10 failed) — rotate still available", "error"); + } + btnRotateGcode.disabled = !lastAlign; + log("after align", { lastAlign: !!lastAlign, btnRotateDisabled: btnRotateGcode.disabled }); + } catch (e) { + log("Align failed", e?.message || String(e)); + setStatus(String(e?.message || e), "error"); + alignResultEl.textContent = `Error: ${e?.message || e}`; + btnRotateGcode.disabled = !lastAlign; + } +} + +function rotateGcodeText(gcode, thetaRad) { + const c = Math.cos(thetaRad); + const s = Math.sin(thetaRad); + const rot = (x, y) => ({ x: x * c - y * s, y: x * s + y * c }); + const wordRe = /([A-Za-z])\s*([-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?)/g; + return gcode.split(/\r?\n/).map((line) => { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith(";") || trimmed.startsWith("(")) return line; + let x, y, iVal, jVal; + let m; + wordRe.lastIndex = 0; + const words = []; + while ((m = wordRe.exec(trimmed)) !== null) { + words.push({ letter: m[1].toUpperCase(), value: parseFloat(m[2]) }); + } + if (!words.length) return line; + for (const w of words) { + if (w.letter === "X") x = w.value; + else if (w.letter === "Y") y = w.value; + else if (w.letter === "I") iVal = w.value; + else if (w.letter === "J") jVal = w.value; + } + if (x === undefined && y === undefined && iVal === undefined && jVal === undefined) { + return line; + } + const prefix = []; + for (const w of words) { + if ("XYIJ".includes(w.letter)) break; + prefix.push(`${w.letter}${w.value}`); + } + const geo = []; + if (x !== undefined || y !== undefined) { + const r = rot(x ?? 0, y ?? 0); + if (x !== undefined) geo.push(`X${roundG(r.x)}`); + if (y !== undefined) geo.push(`Y${roundG(r.y)}`); + } + if (iVal !== undefined || jVal !== undefined) { + const r = rot(iVal ?? 0, jVal ?? 0); + if (iVal !== undefined) geo.push(`I${roundG(r.x)}`); + if (jVal !== undefined) geo.push(`J${roundG(r.y)}`); + } + const suffix = []; + let seenGeo = false; + for (const w of words) { + if ("XYIJ".includes(w.letter)) { seenGeo = true; continue; } + if (!seenGeo) continue; + suffix.push(`${w.letter}${w.value}`); + } + return [...prefix, ...geo, ...suffix].join(" "); + }).join("\n"); +} + +function roundG(n) { + return String(Math.round(n * 1e6) / 1e6); +} + +async function rotateLoadedGcode() { + if (!lastAlign) { + setStatus("Run Align first", "error"); + log("rotate blocked: lastAlign is null"); + return; + } + log("Rotate clicked", { thetaDeg: lastAlign.thetaDeg }); + try { + let content = null; + let source = null; + + // 1) Redux (needs redux:get:state in manifest) + try { + const state = await gsender.redux.getState(); + log("redux keys", state && typeof state === "object" ? Object.keys(state) : state); + content = + state?.file?.content || + state?.file?.gcode || + state?.gcode?.content || + state?.visualizer?.gcode || + state?.controller?.gcode || + null; + if (content) source = "redux"; + + if (!content && state && typeof state === "object") { + const stack = [state]; + while (stack.length && !content) { + const cur = stack.pop(); + if (!cur || typeof cur !== "object") continue; + for (const [k, v] of Object.entries(cur)) { + if ( + typeof v === "string" && + v.length > 80 && + /G[0-3]\b/i.test(v) && + /[XY]/i.test(v) + ) { + content = v; + source = `redux nested (${k})`; + log(`Found gcode-like string via key ${k}, len=${v.length}`); + break; + } + if (v && typeof v === "object" && stack.length < 40) stack.push(v); + } + } + } + } catch (e) { + log("redux.getState failed", e?.message || String(e)); + } + + // 2) Workspace fallback + if (!content) { + try { + const ws = await gsender.workspace.getState(); + log("workspace keys", ws && typeof ws === "object" ? Object.keys(ws) : ws); + content = + ws?.file?.content || + ws?.gcode || + ws?.content || + null; + if (content) source = "workspace"; + } catch (e) { + log("workspace.getState failed", e?.message || String(e)); + } + } + + if (!content || typeof content !== "string") { + setStatus("No loaded G-code found in host state", "error"); + alignResultEl.textContent += + "\n\nCould not find loaded G-code. Ensure a file is loaded in gSender, " + + "manifest includes redux:get:state, and gSender was restarted."; + log("rotate: no gcode content found"); + return; + } + + log("rotating gcode", { source, chars: content.length, thetaDeg: lastAlign.thetaDeg }); + const rotated = rotateGcodeText(content, lastAlign.theta); + await gsender.gcode.loadToVisualizer(rotated, "vision-aligned.nc"); + log("Rotated G-code loaded", { chars: rotated.length, thetaDeg: lastAlign.thetaDeg }); + alignResultEl.textContent += + `\n\nRotated G-code by θ=${lastAlign.thetaDeg.toFixed(4)}° (source=${source}) and loaded as vision-aligned.nc.`; + setStatus("Rotated G-code loaded into visualizer", "live"); + } catch (e) { + log("Rotate G-code failed", e?.message || String(e)); + setStatus(String(e?.message || e), "error"); + } +} + +btnStart.addEventListener("click", () => { + let preferred = selectEl.value; + if (!preferred) { + try { preferred = localStorage.getItem("va-device-id") || ""; } catch { /* ignore */ } + } + startCamera(preferred || undefined); +}); +btnStop.addEventListener("click", () => stopCamera()); +selectEl.addEventListener("change", () => { + if (currentStream && selectEl.value) startCamera(selectEl.value); +}); +colorInput.addEventListener("input", applyCrosshairStyle); +thicknessInput.addEventListener("input", applyCrosshairStyle); +offsetXEl.addEventListener("change", saveOffsets); +offsetYEl.addEventListener("change", saveOffsets); +btnMark[0].addEventListener("click", () => markPoint(0)); +btnMark[1].addEventListener("click", () => markPoint(1)); +btnMark[2].addEventListener("click", () => markPoint(2)); +btnClearPoints.addEventListener("click", () => clearPoints()); +btnAlign.addEventListener("click", () => alignAndZero()); +btnRotateGcode.addEventListener("click", () => rotateLoadedGcode()); +btnClearLog.addEventListener("click", () => { + logLines.length = 0; + if (debugLogEl) debugLogEl.textContent = ""; +}); +window.addEventListener("pagehide", () => stopCamera()); + +const jogMposEl = document.getElementById("jog-mpos"); +const jogWposEl = document.getElementById("jog-wpos"); + +function updateDro(mpos, wpos) { + if (mpos) { + livePos = mpos; + const t = `X ${fmt(mpos.x)} Y ${fmt(mpos.y)} Z ${fmt(mpos.z)}`; + livePosEl.textContent = t; + if (jogMposEl) jogMposEl.textContent = t; + } + if (wpos && jogWposEl) { + jogWposEl.textContent = `X ${fmt(wpos.x)} Y ${fmt(wpos.y)} Z ${fmt(wpos.z)}`; + } +} + +async function refreshContextDro() { + try { + const ctx = await gsender.machine.getContext(); + const { mpos, wpos } = extractBoth(ctx); + updateDro(mpos, wpos); + } catch (e) { /* ignore */ } +} + +try { + subscribeWorkspaceState((ws) => { + const { mpos, wpos } = extractBoth(ws); + updateDro(mpos || extractXY(ws), wpos); + }); +} catch (e) { + log("subscribeWorkspaceState failed", e?.message); +} + +setInterval(() => { refreshContextDro(); }, 400); + +try { + subscribeSelector( + (s) => s?.connection?.isConnected ?? s?.controller?.connected ?? null, + (connected) => { + if (connected === null) { connBadge.textContent = "pos?"; return; } + connBadge.textContent = connected ? "connected" : "disconnected"; + connBadge.classList.toggle("ok", !!connected); + }, + ); +} catch (e) { + log("subscribeSelector failed", e?.message); +} + +const jogStepEl = document.getElementById("jog-step"); +const jogFeedEl = document.getElementById("jog-feed"); +const jogContinuousEl = document.getElementById("jog-continuous"); +let jogActive = false; + +async function sendRaw(line) { + // Matches gSender main UI: socket.command(port, "gcode", line) + await gsender.machine.command("gcode", line); + log("sent gcode", line); +} + +async function jogCancel() { + jogActive = false; + try { + await gsender.machine.command("jogCancel"); + log("sent jogCancel"); + return; + } catch (e) { + log("jogCancel name failed", e?.message || String(e)); + } + try { + await sendRaw(String.fromCharCode(0x85)); + } catch (e) { + log("0x85 cancel failed", e?.message || String(e)); + } +} + +async function jogStart(direction, continuous) { + const step = continuous ? 250 : Number(jogStepEl.value) || 1; + const feed = Number(jogFeedEl.value) || 500; + let axis = ""; + let sign = 1; + if (direction === "x+") { axis = "X"; sign = 1; } + else if (direction === "x-") { axis = "X"; sign = -1; } + else if (direction === "y+") { axis = "Y"; sign = 1; } + else if (direction === "y-") { axis = "Y"; sign = -1; } + else if (direction === "z+") { axis = "Z"; sign = 1; } + else if (direction === "z-") { axis = "Z"; sign = -1; } + else return; + + const dist = sign * step; + // Exact format used by gSender UI: + // $J=G21 G91 X-5 F3000 + const cmd = `$J=G21 G91 ${axis}${dist} F${feed}`; + jogActive = true; + try { + await sendRaw(cmd); + setStatus(`Jog ${direction} ${continuous ? "(continuous)" : `${step} mm`}`, "live"); + } catch (e) { + jogActive = false; + log("jog failed", e?.message || String(e)); + setStatus(`Jog failed: ${e?.message || e}`, "error"); + } +} + +function wireJogButtons() { + document.querySelectorAll("[data-jog]").forEach((btn) => { + const dir = btn.getAttribute("data-jog"); + if (dir === "stop") { + btn.addEventListener("pointerdown", (e) => { + e.preventDefault(); + jogCancel(); + setStatus("Jog cancelled"); + }); + return; + } + const onDown = async (e) => { + e.preventDefault(); + btn.setPointerCapture?.(e.pointerId); + await jogStart(dir, !!jogContinuousEl?.checked); + }; + const onUp = (e) => { + e.preventDefault(); + if (jogContinuousEl?.checked && jogActive) jogCancel(); + }; + btn.addEventListener("pointerdown", onDown); + btn.addEventListener("pointerup", onUp); + btn.addEventListener("pointercancel", onUp); + btn.addEventListener("pointerleave", (e) => { + if (jogContinuousEl?.checked && jogActive && e.buttons === 0) jogCancel(); + }); + }); +} + +wireJogButtons(); +loadPrefs(); +updatePointUI(); +log("Vision Alignment plugin boot", { + href: location.href, + isSecureContext: window.isSecureContext, + inIframe: window !== window.top, +}); +setStatus("Camera idle — start camera, mark 3 points, Align"); +refreshCameraList().catch(() => {}); diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..77bea5f --- /dev/null +++ b/src/style.css @@ -0,0 +1,508 @@ +/* Base — gSender applies html.dark for dark mode */ +:root { + --bg: #f4f4f5; + --panel: #ffffff; + --text: #18181b; + --muted: #71717a; + --border: #e4e4e7; + --accent: #2563eb; + --accent-hover: #1d4ed8; + --danger: #dc2626; + --crosshair: #00ff66; + --crosshair-thickness: 2px; + --radius: 8px; + font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; +} + +html.dark { + --bg: #09090b; + --panel: #18181b; + --text: #fafafa; + --muted: #a1a1aa; + --border: #27272a; + --accent: #3b82f6; + --accent-hover: #60a5fa; +} + +* { box-sizing: border-box; } + +body { + margin: 0; + background: var(--bg); + color: var(--text); + min-height: 100vh; +} + +#app { + max-width: 720px; + margin: 0 auto; + padding: 1rem 1.25rem 2rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.header h1 { + margin: 0; + font-size: 1.35rem; + font-weight: 650; +} + +.subtitle { + margin: 0.25rem 0 0; + color: var(--muted); + font-size: 0.9rem; +} + +.controls, +.options { + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 0.85rem 1rem; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.field { + display: flex; + flex-direction: column; + gap: 0.35rem; + font-size: 0.85rem; +} + +.field.inline { + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.field span { + color: var(--muted); + font-weight: 500; +} + +select, +input[type="color"] { + font: inherit; + color: var(--text); + background: var(--bg); + border: 1px solid var(--border); + border-radius: 6px; + padding: 0.45rem 0.6rem; +} + +select { width: 100%; } + +input[type="color"] { + width: 3rem; + height: 2rem; + padding: 2px; + cursor: pointer; +} + +input[type="range"] { width: 8rem; } + +.btn-row { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.btn { + font: inherit; + font-weight: 560; + padding: 0.5rem 1rem; + border-radius: 6px; + border: 1px solid var(--border); + background: var(--bg); + color: var(--text); + cursor: pointer; +} + +.btn:hover:not(:disabled) { border-color: var(--muted); } +.btn:disabled { opacity: 0.45; cursor: not-allowed; } + +.btn.primary { + background: var(--accent); + border-color: var(--accent); + color: #fff; +} + +.btn.primary:hover:not(:disabled) { + background: var(--accent-hover); + border-color: var(--accent-hover); +} + +.status { + margin: 0; + font-size: 0.8rem; + color: var(--muted); +} + +.status.error { color: var(--danger); } +.status.live { color: #16a34a; } +html.dark .status.live { color: #4ade80; } + +.viewport-wrap { + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 0.75rem; +} + +.camera-viewport { + position: relative; + width: 100%; + aspect-ratio: 4 / 3; + background: #0a0a0a; + border-radius: 6px; + overflow: hidden; +} + +.camera-viewport video { + width: 100%; + height: 100%; + object-fit: contain; + display: block; + background: #000; +} + +.camera-viewport video:not([data-active="true"]) { opacity: 0; } + +.placeholder { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 1.5rem; + text-align: center; + color: #a1a1aa; + font-size: 0.9rem; + pointer-events: none; +} + +.camera-viewport[data-streaming="true"] .placeholder { display: none; } + +.crosshair { + pointer-events: none; + position: absolute; + inset: 0; + --c: var(--crosshair); + --t: var(--crosshair-thickness); +} + +.crosshair::before, +.crosshair::after { + content: ""; + position: absolute; + background: var(--c); + opacity: 0.92; +} + +.crosshair::before { + left: 0; + right: 0; + top: 50%; + height: var(--t); + transform: translateY(-50%); + background: linear-gradient( + to right, + var(--c) 0%, + var(--c) calc(50% - 18px), + transparent calc(50% - 18px), + transparent calc(50% + 18px), + var(--c) calc(50% + 18px), + var(--c) 100% + ); +} + +.crosshair::after { + top: 0; + bottom: 0; + left: 50%; + width: var(--t); + transform: translateX(-50%); + background: linear-gradient( + to bottom, + var(--c) 0%, + var(--c) calc(50% - 18px), + transparent calc(50% - 18px), + transparent calc(50% + 18px), + var(--c) calc(50% + 18px), + var(--c) 100% + ); +} + +.crosshair .ring { + position: absolute; + left: 50%; + top: 50%; + width: 28px; + height: 28px; + margin: -14px 0 0 -14px; + border: calc(var(--t) + 0.5px) solid var(--c); + border-radius: 50%; + box-sizing: border-box; + opacity: 0.95; +} + +.debug-panel { + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 0.75rem 1rem 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.debug-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + font-size: 0.9rem; +} + +.btn.small { + padding: 0.25rem 0.6rem; + font-size: 0.8rem; +} + +.debug-log { + margin: 0; + max-height: 280px; + overflow: auto; + padding: 0.65rem 0.75rem; + font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; + font-size: 0.72rem; + line-height: 1.4; + background: #0a0a0a; + color: #d4d4d8; + border-radius: 6px; + white-space: pre-wrap; + word-break: break-word; +} + +html.dark .debug-log { + background: #000; + color: #e4e4e7; +} + +.section-title { + margin: 0; + font-size: 0.95rem; + font-weight: 650; +} + +.hint { + margin: 0; + font-size: 0.8rem; + color: var(--muted); + line-height: 1.4; +} + +.hint code { font-size: 0.78rem; } + +.row-2 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.75rem; +} + +.row-2 input[type="number"] { + font: inherit; + color: var(--text); + background: var(--bg); + border: 1px solid var(--border); + border-radius: 6px; + padding: 0.45rem 0.6rem; + width: 100%; +} + +.pos-line { + margin: 0; + font-size: 0.85rem; + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; +} + +.pos-line code { font-size: 0.85rem; } + +.badge { + font-size: 0.7rem; + padding: 0.15rem 0.45rem; + border-radius: 999px; + background: var(--border); + color: var(--muted); +} + +.badge.ok { + background: #166534; + color: #bbf7d0; +} + +.points-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.5rem; +} + +@media (max-width: 640px) { + .points-grid { grid-template-columns: 1fr; } +} + +.point-card { + background: var(--bg); + border: 1px solid var(--border); + border-radius: 6px; + padding: 0.6rem; + display: flex; + flex-direction: column; + gap: 0.4rem; + font-size: 0.8rem; +} + +.point-card code { + font-size: 0.75rem; + word-break: break-all; + color: var(--muted); +} + +.small-btn { + padding: 0.35rem 0.5rem; + font-size: 0.8rem; +} + +.result-box { + margin: 0; + padding: 0.65rem 0.75rem; + font-family: ui-monospace, Menlo, Consolas, monospace; + font-size: 0.75rem; + line-height: 1.45; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 6px; + white-space: pre-wrap; + max-height: 200px; + overflow: auto; +} + +/* ---- Jog pad ---- */ + +.jog-layout { + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: flex-start; +} + +.jog-xy { + display: grid; + grid-template-columns: repeat(3, 2.6rem); + grid-template-rows: repeat(3, 2.6rem); + gap: 0.35rem; +} + +.jog-xy .jog-btn[data-jog="y+"] { grid-column: 2; grid-row: 1; } +.jog-xy .jog-btn[data-jog="x-"] { grid-column: 1; grid-row: 2; } +.jog-xy .jog-btn[data-jog="stop"] { grid-column: 2; grid-row: 2; } +.jog-xy .jog-btn[data-jog="x+"] { grid-column: 3; grid-row: 2; } +.jog-xy .jog-btn[data-jog="y-"] { grid-column: 2; grid-row: 3; } + +.jog-z { + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.jog-z .jog-btn { + min-width: 2.6rem; + min-height: 2.6rem; +} + +.jog-btn { + min-width: 2.6rem; + min-height: 2.6rem; + padding: 0; + font-weight: 700; + font-size: 0.85rem; + user-select: none; + touch-action: none; +} + +.jog-btn.jog-stop { + font-size: 0.9rem; + background: #450a0a; + border-color: #7f1d1d; + color: #fecaca; +} + +.jog-btn:active:not(:disabled) { + transform: scale(0.96); + background: var(--accent); + border-color: var(--accent); + color: #fff; +} + +.jog-settings { + flex: 1; + min-width: 8rem; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.field.checkbox { + flex-direction: row; + align-items: center; + gap: 0.5rem; +} + +.field.checkbox input { width: auto; } + +.jog-dro { + font-size: 0.8rem; + display: flex; + flex-direction: column; + gap: 0.25rem; + margin-bottom: 0.25rem; +} + +.dro-row { + display: flex; + gap: 0.5rem; + align-items: baseline; +} + +.dro-row span { + color: var(--muted); + font-weight: 500; + min-width: 2.6rem; +} + +.dro-row code { font-size: 0.8rem; } + +.jog-selects { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.field.compact { + flex: 0 0 auto; + min-width: 0; +} + +.field.compact select { + width: 5.5rem; + min-width: 5.5rem; +} + +.jog-settings select { width: 5.5rem; } diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..e6ba8f7 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,11 @@ +import gsenderPlugin from "@sienci/gsender-plugin-sdk/vite"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [gsenderPlugin()], + base: "./", + build: { + outDir: "ui", + emptyOutDir: true, + }, +}); diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..e2b603c --- /dev/null +++ b/yarn.lock @@ -0,0 +1,563 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@esbuild/aix-ppc64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz#80fcbe36130e58b7670511e888b8e88a259ed76c" + integrity sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA== + +"@esbuild/aix-ppc64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz#bf6e10303bcf2e7c686975fa52f937ec2728d8bc" + integrity sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ== + +"@esbuild/android-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz#8aa4965f8d0a7982dc21734bf6601323a66da752" + integrity sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg== + +"@esbuild/android-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz#0c6246bc8d2c4d172aac2db3fb1190d72bd65504" + integrity sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A== + +"@esbuild/android-arm@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz#300712101f7f50f1d2627a162e6e09b109b6767a" + integrity sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg== + +"@esbuild/android-arm@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.28.2.tgz#2d84ece6a4e2684d92be26ee13d42757d831c381" + integrity sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg== + +"@esbuild/android-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz#87dfb27161202bdc958ef48bb61b09c758faee16" + integrity sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg== + +"@esbuild/android-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.28.2.tgz#fc38d4d6358d8dc1cf53f09f7589fe436eb64801" + integrity sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q== + +"@esbuild/darwin-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz#79197898ec1ff745d21c071e1c7cc3c802f0c1fd" + integrity sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg== + +"@esbuild/darwin-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz#f83afeeac1d7dac01c7a2fd012b3e451a0591fcc" + integrity sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw== + +"@esbuild/darwin-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz#146400a8562133f45c4d2eadcf37ddd09718079e" + integrity sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA== + +"@esbuild/darwin-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz#510147c055a795588dbbe14fd6b1b8ad0a2f30de" + integrity sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw== + +"@esbuild/freebsd-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz#1c5f9ba7206e158fd2b24c59fa2d2c8bb47ca0fe" + integrity sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg== + +"@esbuild/freebsd-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz#093b9200ecf0b115ba4e5e248a7485c9c5f8bd5e" + integrity sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw== + +"@esbuild/freebsd-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz#ea631f4a36beaac4b9279fa0fcc6ca29eaeeb2b3" + integrity sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ== + +"@esbuild/freebsd-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz#0be22b6df925d213e841ea87123af5df80b0faf7" + integrity sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg== + +"@esbuild/linux-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz#e1066bce58394f1b1141deec8557a5f0a22f5977" + integrity sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ== + +"@esbuild/linux-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz#1bdbc651cda9ba9995c53ed9c71ceaa65094762d" + integrity sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug== + +"@esbuild/linux-arm@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz#452cd66b20932d08bdc53a8b61c0e30baf4348b9" + integrity sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw== + +"@esbuild/linux-arm@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz#beb12ad72b84f72d28488cc1b8ee9f7eb141d753" + integrity sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w== + +"@esbuild/linux-ia32@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz#b24f8acc45bcf54192c7f2f3be1b53e6551eafe0" + integrity sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA== + +"@esbuild/linux-ia32@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz#b81f9d55529b45c206a46a138214b1aa6879696b" + integrity sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ== + +"@esbuild/linux-loong64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz#f9cfffa7fc8322571fbc4c8b3268caf15bd81ad0" + integrity sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng== + +"@esbuild/linux-loong64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz#598667241a04c99b76ed6ef940ac50038c419f98" + integrity sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ== + +"@esbuild/linux-mips64el@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz#575a14bd74644ffab891adc7d7e60d275296f2cd" + integrity sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw== + +"@esbuild/linux-mips64el@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz#1c51eb9cea903f53d97b5af3b1841db70f5596ca" + integrity sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA== + +"@esbuild/linux-ppc64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz#75b99c70a95fbd5f7739d7692befe60601591869" + integrity sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA== + +"@esbuild/linux-ppc64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz#63dd61f17ceb31a81227f413feac8a71bc2c51f2" + integrity sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ== + +"@esbuild/linux-riscv64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz#2e3259440321a44e79ddf7535c325057da875cd6" + integrity sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w== + +"@esbuild/linux-riscv64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz#3763b08fde5cf25ab1facb8e7752edfe45fbfc27" + integrity sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA== + +"@esbuild/linux-s390x@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz#17676cabbfe5928da5b2a0d6df5d58cd08db2663" + integrity sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg== + +"@esbuild/linux-s390x@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz#1a137ff293a82906eb3176385bd7e8e0e5cfb7cb" + integrity sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg== + +"@esbuild/linux-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz#0583775685ca82066d04c3507f09524d3cd7a306" + integrity sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw== + +"@esbuild/linux-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz#268b36211c146ca54f8fe12c578a8d6ef8979485" + integrity sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ== + +"@esbuild/netbsd-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz#f04c4049cb2e252fe96b16fed90f70746b13f4a4" + integrity sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg== + +"@esbuild/netbsd-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz#22571ad951d62bb6accc82d8d1fad5c8c1ac0ba1" + integrity sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw== + +"@esbuild/netbsd-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz#77da0d0a0d826d7c921eea3d40292548b258a076" + integrity sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ== + +"@esbuild/netbsd-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz#42fcc57297eb0a0ca3f5fc475291f4c1a3f7c0de" + integrity sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw== + +"@esbuild/openbsd-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz#6296f5867aedef28a81b22ab2009c786a952dccd" + integrity sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A== + +"@esbuild/openbsd-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz#9eb32af104ac3dacf4edca01f596664aab0c73ef" + integrity sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ== + +"@esbuild/openbsd-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz#f8d23303360e27b16cf065b23bbff43c14142679" + integrity sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw== + +"@esbuild/openbsd-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz#febed2402d6088225e91f20fb4ce2522ad0a4efd" + integrity sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw== + +"@esbuild/openharmony-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz#49e0b768744a3924be0d7fd97dd6ce9b2923d88d" + integrity sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg== + +"@esbuild/openharmony-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz#85641c3d466428bfbccea5f21c26836663fef5ce" + integrity sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q== + +"@esbuild/sunos-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz#a6ed7d6778d67e528c81fb165b23f4911b9b13d6" + integrity sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w== + +"@esbuild/sunos-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz#a736f9d8962481045fc4c3e54f5479f22c870fb4" + integrity sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g== + +"@esbuild/win32-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz#9ac14c378e1b653af17d08e7d3ce34caef587323" + integrity sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg== + +"@esbuild/win32-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz#ee5ab40fad186201b652a33f8a5eb149e9e42532" + integrity sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ== + +"@esbuild/win32-ia32@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz#918942dcbbb35cc14fca39afb91b5e6a3d127267" + integrity sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ== + +"@esbuild/win32-ia32@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz#c40d28a6d99a127da6711f2afd74b11cb63b06a7" + integrity sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA== + +"@esbuild/win32-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz#9bdad8176be7811ad148d1f8772359041f46c6c5" + integrity sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA== + +"@esbuild/win32-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz#b21affb804cc167c133d95f45b3a1dc1323b9a87" + integrity sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g== + +"@napi-rs/lzma-linux-x64-gnu@1.5.1": + version "1.5.1" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz#e57d4306966078662038094fb38eb9146dc3aea9" + integrity sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ== + +"@rollup/rollup-android-arm-eabi@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz#d03ba6ea54f9ec80688d153763cd325a2d2a5af6" + integrity sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ== + +"@rollup/rollup-android-arm64@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz#db5e36aa8a955b4b5e0b024d671230edc5cdc191" + integrity sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ== + +"@rollup/rollup-darwin-arm64@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz#1ed1c43922e7b9b5d020ef65d8402e3c81edc86e" + integrity sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q== + +"@rollup/rollup-darwin-x64@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz#0a86e782bf7a546e74f531e395e24fdb45c83527" + integrity sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg== + +"@rollup/rollup-freebsd-arm64@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz#82fa51c540185b5063c8b3c63aac79b15f2801ad" + integrity sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw== + +"@rollup/rollup-freebsd-x64@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz#df1d73b567fd62e0cf21c9b57dff7f44bfd69638" + integrity sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q== + +"@rollup/rollup-linux-arm-gnueabihf@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz#a585ba0418027a5b567693db3982e5e57544a4c7" + integrity sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw== + +"@rollup/rollup-linux-arm-musleabihf@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz#1326f0db22b690a92efd8eaa06e92268dc7d1bb6" + integrity sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw== + +"@rollup/rollup-linux-arm64-gnu@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz#9491939f7cc43a5b26a877417faeafe69bac79ac" + integrity sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg== + +"@rollup/rollup-linux-arm64-musl@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz#a53d93dac32acc671324af1153930ab5a04d8639" + integrity sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw== + +"@rollup/rollup-linux-loong64-gnu@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz#db6e06173efc870be49a2df692d0c0607417a679" + integrity sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ== + +"@rollup/rollup-linux-loong64-musl@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz#a60734a3de407bcf4bfe44d0b64222c0b9fb30bc" + integrity sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA== + +"@rollup/rollup-linux-ppc64-gnu@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz#28a67e15d7ba8630ef044980a39626e0627d6e73" + integrity sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA== + +"@rollup/rollup-linux-ppc64-musl@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz#77bb553a514942af54070756763dbb44c9c6cb2b" + integrity sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA== + +"@rollup/rollup-linux-riscv64-gnu@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz#ef9f31b917e3b310eac5b86d3b6626df7e543e3d" + integrity sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w== + +"@rollup/rollup-linux-riscv64-musl@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz#68cf61a2fc02171d1fa62568b73c1d942f82e80c" + integrity sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ== + +"@rollup/rollup-linux-s390x-gnu@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz#92d393ca47da0d03d1c1cffb3d7340f26c3a53d2" + integrity sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A== + +"@rollup/rollup-linux-x64-gnu@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz#f6e5c5c51f96ae298617fa26da54675acd60e3bc" + integrity sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w== + +"@rollup/rollup-linux-x64-musl@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz#227a949c481909c781d8a39c280f75553cdd16bc" + integrity sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow== + +"@rollup/rollup-openbsd-x64@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz#f57241ebdb73d3bc236e7b1252988408b2139c13" + integrity sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA== + +"@rollup/rollup-openharmony-arm64@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz#3a9ffe5af71e8316dd2716b57dd64287a8c1fa0b" + integrity sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw== + +"@rollup/rollup-win32-arm64-msvc@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz#7d4a40396ae79ebc1e3636c1d566c7d1838b800c" + integrity sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg== + +"@rollup/rollup-win32-ia32-msvc@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz#f234ab80141da45ebe85727f714eb20de2e72c2a" + integrity sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg== + +"@rollup/rollup-win32-x64-gnu@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz#5d5a664c23c8ff0526b9abd703b50ffe09703c2a" + integrity sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg== + +"@rollup/rollup-win32-x64-msvc@4.63.1": + version "4.63.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz#cd19d691330cbd52ebb13620acb6cf7140b95e80" + integrity sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w== + +"@sienci/gsender-plugin-sdk@file:../../packages/plugin-sdk": + version "0.2.1" + dependencies: + esbuild "^0.28.2" + +"@types/estree@1.0.9": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.9.tgz#cf3f0e876d7bee15a93ab925b82bf570a3904a24" + integrity sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg== + +esbuild@^0.25.0: + version "0.25.12" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.12.tgz#97a1d041f4ab00c2fce2f838d2b9969a2d2a97a5" + integrity sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg== + optionalDependencies: + "@esbuild/aix-ppc64" "0.25.12" + "@esbuild/android-arm" "0.25.12" + "@esbuild/android-arm64" "0.25.12" + "@esbuild/android-x64" "0.25.12" + "@esbuild/darwin-arm64" "0.25.12" + "@esbuild/darwin-x64" "0.25.12" + "@esbuild/freebsd-arm64" "0.25.12" + "@esbuild/freebsd-x64" "0.25.12" + "@esbuild/linux-arm" "0.25.12" + "@esbuild/linux-arm64" "0.25.12" + "@esbuild/linux-ia32" "0.25.12" + "@esbuild/linux-loong64" "0.25.12" + "@esbuild/linux-mips64el" "0.25.12" + "@esbuild/linux-ppc64" "0.25.12" + "@esbuild/linux-riscv64" "0.25.12" + "@esbuild/linux-s390x" "0.25.12" + "@esbuild/linux-x64" "0.25.12" + "@esbuild/netbsd-arm64" "0.25.12" + "@esbuild/netbsd-x64" "0.25.12" + "@esbuild/openbsd-arm64" "0.25.12" + "@esbuild/openbsd-x64" "0.25.12" + "@esbuild/openharmony-arm64" "0.25.12" + "@esbuild/sunos-x64" "0.25.12" + "@esbuild/win32-arm64" "0.25.12" + "@esbuild/win32-ia32" "0.25.12" + "@esbuild/win32-x64" "0.25.12" + +esbuild@^0.28.2: + version "0.28.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.28.2.tgz#0f43bd1bad955b72d24e2261e3abe5957ccf0816" + integrity sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA== + optionalDependencies: + "@esbuild/aix-ppc64" "0.28.2" + "@esbuild/android-arm" "0.28.2" + "@esbuild/android-arm64" "0.28.2" + "@esbuild/android-x64" "0.28.2" + "@esbuild/darwin-arm64" "0.28.2" + "@esbuild/darwin-x64" "0.28.2" + "@esbuild/freebsd-arm64" "0.28.2" + "@esbuild/freebsd-x64" "0.28.2" + "@esbuild/linux-arm" "0.28.2" + "@esbuild/linux-arm64" "0.28.2" + "@esbuild/linux-ia32" "0.28.2" + "@esbuild/linux-loong64" "0.28.2" + "@esbuild/linux-mips64el" "0.28.2" + "@esbuild/linux-ppc64" "0.28.2" + "@esbuild/linux-riscv64" "0.28.2" + "@esbuild/linux-s390x" "0.28.2" + "@esbuild/linux-x64" "0.28.2" + "@esbuild/netbsd-arm64" "0.28.2" + "@esbuild/netbsd-x64" "0.28.2" + "@esbuild/openbsd-arm64" "0.28.2" + "@esbuild/openbsd-x64" "0.28.2" + "@esbuild/openharmony-arm64" "0.28.2" + "@esbuild/sunos-x64" "0.28.2" + "@esbuild/win32-arm64" "0.28.2" + "@esbuild/win32-ia32" "0.28.2" + "@esbuild/win32-x64" "0.28.2" + +fdir@^6.4.4, fdir@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" + integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== + +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +nanoid@^3.3.18: + version "3.3.18" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.18.tgz#f66a2de1199ffde0fcf21c8a5f13106b1c081913" + integrity sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w== + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^4.0.2, picomatch@^4.0.4: + version "4.0.7" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.7.tgz#6313360034ccb36b3dc61ecbdff78121f90fe21f" + integrity sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA== + +postcss@^8.5.3: + version "8.5.28" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.28.tgz#da4563a99a06e62d6c1cd1acae363224bcaed6e9" + integrity sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A== + dependencies: + nanoid "^3.3.18" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +rollup@^4.34.9: + version "4.63.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.63.1.tgz#a9b96d5b2558d034babb12ad8b67a043bc870ac4" + integrity sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg== + dependencies: + "@types/estree" "1.0.9" + optionalDependencies: + "@napi-rs/lzma-linux-x64-gnu" "1.5.1" + "@rollup/rollup-android-arm-eabi" "4.63.1" + "@rollup/rollup-android-arm64" "4.63.1" + "@rollup/rollup-darwin-arm64" "4.63.1" + "@rollup/rollup-darwin-x64" "4.63.1" + "@rollup/rollup-freebsd-arm64" "4.63.1" + "@rollup/rollup-freebsd-x64" "4.63.1" + "@rollup/rollup-linux-arm-gnueabihf" "4.63.1" + "@rollup/rollup-linux-arm-musleabihf" "4.63.1" + "@rollup/rollup-linux-arm64-gnu" "4.63.1" + "@rollup/rollup-linux-arm64-musl" "4.63.1" + "@rollup/rollup-linux-loong64-gnu" "4.63.1" + "@rollup/rollup-linux-loong64-musl" "4.63.1" + "@rollup/rollup-linux-ppc64-gnu" "4.63.1" + "@rollup/rollup-linux-ppc64-musl" "4.63.1" + "@rollup/rollup-linux-riscv64-gnu" "4.63.1" + "@rollup/rollup-linux-riscv64-musl" "4.63.1" + "@rollup/rollup-linux-s390x-gnu" "4.63.1" + "@rollup/rollup-linux-x64-gnu" "4.63.1" + "@rollup/rollup-linux-x64-musl" "4.63.1" + "@rollup/rollup-openbsd-x64" "4.63.1" + "@rollup/rollup-openharmony-arm64" "4.63.1" + "@rollup/rollup-win32-arm64-msvc" "4.63.1" + "@rollup/rollup-win32-ia32-msvc" "4.63.1" + "@rollup/rollup-win32-x64-gnu" "4.63.1" + "@rollup/rollup-win32-x64-msvc" "4.63.1" + fsevents "~2.3.2" + +source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +tinyglobby@^0.2.13: + version "0.2.17" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.17.tgz#562a9a6c9eb2b3b123d39719f9af5bb44fcd7631" + integrity sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g== + dependencies: + fdir "^6.5.0" + picomatch "^4.0.4" + +vite@^6.0.0: + version "6.4.3" + resolved "https://registry.yarnpkg.com/vite/-/vite-6.4.3.tgz#85a164db7ce706f2a776812efa2b340f1721858e" + integrity sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A== + dependencies: + esbuild "^0.25.0" + fdir "^6.4.4" + picomatch "^4.0.2" + postcss "^8.5.3" + rollup "^4.34.9" + tinyglobby "^0.2.13" + optionalDependencies: + fsevents "~2.3.3"