first commit

This commit is contained in:
cpu
2026-09-04 15:48:02 +02:00
commit 64b4982385
10 changed files with 2394 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules/
ui/
dist/
+149
View File
@@ -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!
+242
View File
@@ -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 SDKs 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 packages `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 plugins `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 repos `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 repos `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 plugins 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.
+28
View File
@@ -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"]
}
}
+178
View File
@@ -0,0 +1,178 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vision Alignment</title>
</head>
<body>
<div id="app">
<header class="header">
<h1>Vision Alignment</h1>
<p class="subtitle">
Three-point fiducial registration · camera offset · software skew compensation
</p>
</header>
<section class="controls">
<label class="field">
<span>Camera</span>
<select id="camera-select">
<option value="">— select after starting —</option>
</select>
</label>
<div class="btn-row">
<button type="button" id="btn-start" class="btn primary">Start camera</button>
<button type="button" id="btn-stop" class="btn" disabled>Stop</button>
</div>
<p id="status" class="status" role="status">Camera idle</p>
</section>
<section class="viewport-wrap">
<div class="camera-viewport" id="viewport">
<video id="cam" autoplay playsinline muted></video>
<div class="crosshair" aria-hidden="true"><span class="ring"></span></div>
<div class="placeholder" id="placeholder">
Start the camera, jog so the cross-hair is on a fiducial, then mark points.
</div>
</div>
</section>
<section class="controls jog-section">
<h2 class="section-title">Jog</h2>
<div class="jog-layout">
<div class="jog-xy" aria-label="XY jog">
<button type="button" class="btn jog-btn" data-jog="y+" title="Y+">Y+</button>
<button type="button" class="btn jog-btn" data-jog="x-" title="X-">X-</button>
<button type="button" class="btn jog-btn jog-stop" data-jog="stop" title="Stop jog"></button>
<button type="button" class="btn jog-btn" data-jog="x+" title="X+">X+</button>
<button type="button" class="btn jog-btn" data-jog="y-" title="Y-">Y-</button>
</div>
<div class="jog-z" aria-label="Z jog">
<button type="button" class="btn jog-btn" data-jog="z+" title="Z+">Z+</button>
<button type="button" class="btn jog-btn" data-jog="z-" title="Z-">Z-</button>
</div>
<div class="jog-settings">
<div class="jog-dro">
<div class="dro-row"><span>MPos</span> <code id="jog-mpos">X — Y — Z —</code></div>
<div class="dro-row"><span>WPos</span> <code id="jog-wpos">X — Y — Z —</code></div>
</div>
<div class="jog-selects">
<label class="field compact">
<span>Step</span>
<select id="jog-step">
<option value="0.1">0.1</option>
<option value="1" selected>1</option>
<option value="5">5</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
</select>
</label>
<label class="field compact">
<span>Feed</span>
<select id="jog-feed">
<option value="100">100</option>
<option value="500" selected>500</option>
<option value="1000">1000</option>
<option value="2000">2000</option>
<option value="3000">3000</option>
</select>
</label>
</div>
<label class="field checkbox">
<input type="checkbox" id="jog-continuous" />
<span>Hold = continuous</span>
</label>
</div>
</div>
<p class="hint">Uses Grbl <code>$J=</code> jogging. Hold buttons for continuous when enabled; release or ⏹ to cancel.</p>
</section>
<section class="controls">
<h2 class="section-title">Camera-to-spindle offset</h2>
<p class="hint">
Distance from <strong>camera centre</strong> to <strong>spindle centre</strong>
(positive X = spindle is to the +X of the camera).
</p>
<div class="row-2">
<label class="field">
<span>Offset X</span>
<input type="number" id="offset-x" step="0.001" value="0" />
</label>
<label class="field">
<span>Offset Y</span>
<input type="number" id="offset-y" step="0.001" value="0" />
</label>
</div>
</section>
<section class="controls">
<h2 class="section-title">Fiducial points</h2>
<p class="pos-line">
Live: <code id="live-pos">X — Y —</code>
<span id="conn-badge" class="badge"></span>
</p>
<div class="points-grid">
<div class="point-card">
<strong>Point 1 · Origin</strong>
<code id="pt1-display">not set</code>
<button type="button" id="btn-mark-1" class="btn primary small-btn">Mark Point 1</button>
</div>
<div class="point-card">
<strong>Point 2 · Axis</strong>
<code id="pt2-display">not set</code>
<button type="button" id="btn-mark-2" class="btn primary small-btn">Mark Point 2</button>
</div>
<div class="point-card">
<strong>Point 3 · Verify</strong>
<code id="pt3-display">not set</code>
<button type="button" id="btn-mark-3" class="btn primary small-btn">Mark Point 3</button>
</div>
</div>
<div class="btn-row">
<button type="button" id="btn-clear-points" class="btn">Clear points</button>
</div>
</section>
<section class="controls">
<h2 class="section-title">Alignment result</h2>
<pre id="align-result" class="result-box">Mark points 1 and 2 (3 optional) then Align.</pre>
<div class="btn-row">
<button type="button" id="btn-align" class="btn primary" disabled>
Align &amp; Zero Workspace
</button>
<button type="button" id="btn-rotate-gcode" class="btn" disabled>
Rotate loaded G-code
</button>
</div>
<p class="hint">
<strong>Align &amp; Zero</strong> computes θ from points 1→2, applies camera offset,
and issues <code>G10 L20</code> so spindle WCS origin matches fiducial 1.
<strong>Rotate loaded G-code</strong> rewrites X/Y/I/J by −θ and reloads the job
(FluidNC has no G68).
</p>
</section>
<section class="options">
<label class="field inline">
<span>Cross-hair colour</span>
<input type="color" id="crosshair-color" value="#00ff66" />
</label>
<label class="field inline">
<span>Thickness</span>
<input type="range" id="crosshair-thickness" min="1" max="4" step="1" value="2" />
</label>
</section>
<section class="debug-panel">
<div class="debug-header">
<strong>Debug log</strong>
<button type="button" id="btn-clear-log" class="btn small">Clear</button>
</div>
<pre id="debug-log" class="debug-log" aria-live="polite"></pre>
</section>
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+17
View File
@@ -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"
}
}
+694
View File
@@ -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 = '<option value="">No cameras found</option>';
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 P1P3: ${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(() => {});
+508
View File
@@ -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; }
+11
View File
@@ -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,
},
});
+563
View File
@@ -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"