diff --git a/oled-emulator/.gitignore b/oled-emulator/.gitignore new file mode 100644 index 000000000..8f2999efe --- /dev/null +++ b/oled-emulator/.gitignore @@ -0,0 +1,30 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# Vite cache +.vite/ + +# TypeScript cache +*.tsbuildinfo + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* + +# Local env files +.env +.env.local +.env.*.local diff --git a/oled-emulator/README.md b/oled-emulator/README.md new file mode 100644 index 000000000..1ce2b581c --- /dev/null +++ b/oled-emulator/README.md @@ -0,0 +1,149 @@ +# Meshtastic OLED Emulator + +A Vue.js component and TypeScript library for emulating SSD1306/SH1106 OLED displays used in Meshtastic devices. This allows firmware UI development and testing directly in the browser. + +## Features + +- **Same API as firmware**: Uses the same `OLEDDisplay` API as esp8266-oled-ssd1306 library +- **Multiple display sizes**: 128x64, 128x32, 64x48, 128x128 +- **Realistic rendering**: Pixel glow, OLED color tints (blue, white, yellow/blue dual) +- **XBM image support**: Render firmware icons and images +- **Font rendering**: Bitmap font support with text alignment +- **Vue 3 component**: Easy integration with Vue applications + +## Installation + +```bash +npm install @meshtastic/oled-emulator +``` + +## Quick Start + +```vue + + + +``` + +## OLEDDisplay API + +The `OLEDDisplay` class provides the same methods as the firmware's display library: + +```typescript +const display = new OLEDDisplay("128x64"); + +// Basic drawing +display.clear(); +display.setPixel(x, y); +display.drawLine(x0, y0, x1, y1); +display.drawRect(x, y, width, height); +display.fillRect(x, y, width, height); +display.drawCircle(x, y, radius); +display.fillCircle(x, y, radius); + +// Text +display.setFont(ArialMT_Plain_10); +display.setTextAlignment("TEXT_ALIGN_CENTER"); +display.drawString(x, y, "Hello"); +display.drawStringMaxWidth(x, y, maxWidth, "Long text..."); +const width = display.getStringWidth("text"); + +// Images (XBM format) +display.drawXbm(x, y, width, height, imageData); + +// Colors +display.setColor("WHITE"); // or 'BLACK', 'INVERSE' + +// Buffer access (for custom rendering) +const buffer = display.getBuffer(); // Uint8Array +``` + +## Display Presets + +The emulator includes several realistic display presets: + +| Preset | Description | +| --------------------- | ------------------------------- | +| `ssd1306-blue` | Classic blue OLED (default) | +| `ssd1306-white` | White OLED | +| `ssd1306-yellow-blue` | Dual-color (top 16 rows yellow) | +| `sh1106` | SH1106 style | +| `sh1107` | SH1107 128x128 | +| `ssd1306-64x48` | Small 64x48 display | +| `ssd1306-128x32` | Wide 128x32 display | + +## Screen Renderers + +Pre-built screen renderers matching firmware UI: + +```typescript +import { ScreenRenderers } from "@meshtastic/oled-emulator"; + +// Boot screen +ScreenRenderers.drawBootScreen(display, "2.5.0"); + +// Node info screen +ScreenRenderers.drawNodeInfoScreen(display, { + shortName: "MESH", + longName: "My Node", + nodeId: "!abcd1234", + lastHeard: "2m ago", + snr: 12.5, +}); + +// Message screen +ScreenRenderers.drawMessageScreen(display, { + from: "Alice", + text: "Hello from the mesh!", + time: "14:32", + channel: "LongFast", +}); + +// And more: drawGPSScreen, drawNodeListScreen, drawSystemScreen, drawCompassScreen +``` + +## Development + +```bash +# Install dependencies +npm install + +# Run development server +npm run dev + +# Build library +npm run build +``` + +## Future: Protocol Bridge + +A future enhancement will allow connecting this emulator to a real Meshtastic firmware instance (running on native/portduino), enabling live framebuffer streaming over WebSocket for real-time display mirroring. + +## License + +GPL-3.0 - Same as Meshtastic firmware diff --git a/oled-emulator/index.html b/oled-emulator/index.html new file mode 100644 index 000000000..b0acab187 --- /dev/null +++ b/oled-emulator/index.html @@ -0,0 +1,22 @@ + + + + + + Meshtastic OLED Emulator + + + +
+ + + diff --git a/oled-emulator/package-lock.json b/oled-emulator/package-lock.json new file mode 100644 index 000000000..7d1977bcc --- /dev/null +++ b/oled-emulator/package-lock.json @@ -0,0 +1,2211 @@ +{ + "name": "@meshtastic/oled-emulator", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@meshtastic/oled-emulator", + "version": "0.1.0", + "license": "GPL-3.0", + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "typescript": "^5.3.0", + "vite": "^5.0.0", + "vite-plugin-dts": "^3.7.0", + "vue": "^3.4.0", + "vue-tsc": "^2.0.0" + }, + "peerDependencies": { + "vue": "^3.3.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@microsoft/api-extractor": { + "version": "7.43.0", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.43.0.tgz", + "integrity": "sha512-GFhTcJpB+MI6FhvXEI9b2K0snulNLWHqC/BbcJtyNYcKUiw7l3Lgis5ApsYncJ0leALX7/of4XfmXk+maT111w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/api-extractor-model": "7.28.13", + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "4.0.2", + "@rushstack/rig-package": "0.5.2", + "@rushstack/terminal": "0.10.0", + "@rushstack/ts-command-line": "4.19.1", + "lodash": "~4.17.15", + "minimatch": "~3.0.3", + "resolve": "~1.22.1", + "semver": "~7.5.4", + "source-map": "~0.6.1", + "typescript": "5.4.2" + }, + "bin": { + "api-extractor": "bin/api-extractor" + } + }, + "node_modules/@microsoft/api-extractor-model": { + "version": "7.28.13", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.28.13.tgz", + "integrity": "sha512-39v/JyldX4MS9uzHcdfmjjfS6cYGAoXV+io8B5a338pkHiSt+gy2eXQ0Q7cGFJ7quSa1VqqlMdlPrB6sLR/cAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "4.0.2" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/typescript": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", + "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@microsoft/tsdoc": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", + "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@microsoft/tsdoc-config": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz", + "integrity": "sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.14.2", + "ajv": "~6.12.6", + "jju": "~1.4.0", + "resolve": "~1.19.0" + } + }, + "node_modules/@microsoft/tsdoc-config/node_modules/resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rushstack/node-core-library": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-4.0.2.tgz", + "integrity": "sha512-hyES82QVpkfQMeBMteQUnrhASL/KHPhd7iJ8euduwNJG4mu2GSOKybf0rOEjOm1Wz7CwJEUm9y0yD7jg2C1bfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.5.4", + "z-schema": "~5.0.2" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/rig-package": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.2.tgz", + "integrity": "sha512-mUDecIJeH3yYGZs2a48k+pbhM6JYwWlgjs2Ca5f2n1G2/kgdgP9D/07oglEGf6mRyXEnazhEENeYTSNDRCwdqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "~1.22.1", + "strip-json-comments": "~3.1.1" + } + }, + "node_modules/@rushstack/terminal": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.10.0.tgz", + "integrity": "sha512-UbELbXnUdc7EKwfH2sb8ChqNgapUOdqcCIdQP4NGxBpTZV2sQyeekuK3zmfQSa/MN+/7b4kBogl2wq0vpkpYGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rushstack/node-core-library": "4.0.2", + "supports-color": "~8.1.1" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/ts-command-line": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.19.1.tgz", + "integrity": "sha512-J7H768dgcpG60d7skZ5uSSwyCZs/S2HrWP1Ds8d1qYAyaaeJmpmmLr9BVw97RjFzmQPOYnoXcKA4GkqDCkduQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rushstack/terminal": "0.10.0", + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "string-argv": "~0.3.1" + } + }, + "node_modules/@types/argparse": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", + "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.11.1.tgz", + "integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "1.11.1" + } + }, + "node_modules/@volar/source-map": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.11.1.tgz", + "integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "muggle-string": "^0.3.1" + } + }, + "node_modules/@volar/typescript": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz", + "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@volar/typescript/node_modules/@volar/language-core": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.15" + } + }, + "node_modules/@volar/typescript/node_modules/@volar/source-map": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.28.tgz", + "integrity": "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.28", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.28.tgz", + "integrity": "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.28", + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.28.tgz", + "integrity": "sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.28", + "@vue/compiler-dom": "3.5.28", + "@vue/compiler-ssr": "3.5.28", + "@vue/shared": "3.5.28", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.28.tgz", + "integrity": "sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.28", + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/language-core": { + "version": "1.8.27", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.27.tgz", + "integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "~1.11.1", + "@volar/source-map": "~1.11.1", + "@vue/compiler-dom": "^3.3.0", + "@vue/shared": "^3.3.0", + "computeds": "^0.0.1", + "minimatch": "^9.0.3", + "muggle-string": "^0.3.1", + "path-browserify": "^1.0.1", + "vue-template-compiler": "^2.7.14" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@vue/language-core/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.28.tgz", + "integrity": "sha512-gr5hEsxvn+RNyu9/9o1WtdYdwDjg5FgjUSBEkZWqgTKlo/fvwZ2+8W6AfKsc9YN2k/+iHYdS9vZYAhpi10kNaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.28.tgz", + "integrity": "sha512-POVHTdbgnrBBIpnbYU4y7pOMNlPn2QVxVzkvEA2pEgvzbelQq4ZOUxbp2oiyo+BOtiYlm8Q44wShHJoBvDPAjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.28", + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.28.tgz", + "integrity": "sha512-4SXxSF8SXYMuhAIkT+eBRqOkWEfPu6nhccrzrkioA6l0boiq7sp18HCOov9qWJA5HML61kW8p/cB4MmBiG9dSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.28", + "@vue/runtime-core": "3.5.28", + "@vue/shared": "3.5.28", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.28.tgz", + "integrity": "sha512-pf+5ECKGj8fX95bNincbzJ6yp6nyzuLDhYZCeFxUNp8EBrQpPpQaLX3nNCp49+UbgbPun3CeVE+5CXVV1Xydfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.28", + "@vue/shared": "3.5.28" + }, + "peerDependencies": { + "vue": "3.5.28" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.28.tgz", + "integrity": "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/computeds": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz", + "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz", + "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/validator": { + "version": "13.15.26", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", + "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-plugin-dts": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/vite-plugin-dts/-/vite-plugin-dts-3.9.1.tgz", + "integrity": "sha512-rVp2KM9Ue22NGWB8dNtWEr+KekN3rIgz1tWD050QnRGlriUCmaDwa7qA5zDEjbXg5lAXhYMSBJtx3q3hQIJZSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/api-extractor": "7.43.0", + "@rollup/pluginutils": "^5.1.0", + "@vue/language-core": "^1.8.27", + "debug": "^4.3.4", + "kolorist": "^1.8.0", + "magic-string": "^0.30.8", + "vue-tsc": "^1.8.27" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "typescript": "*", + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vite-plugin-dts/node_modules/@volar/typescript": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.11.1.tgz", + "integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "1.11.1", + "path-browserify": "^1.0.1" + } + }, + "node_modules/vite-plugin-dts/node_modules/vue-tsc": { + "version": "1.8.27", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.27.tgz", + "integrity": "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "~1.11.1", + "@vue/language-core": "1.8.27", + "semver": "^7.5.4" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.28.tgz", + "integrity": "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.28", + "@vue/compiler-sfc": "3.5.28", + "@vue/runtime-dom": "3.5.28", + "@vue/server-renderer": "3.5.28", + "@vue/shared": "3.5.28" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-template-compiler": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz", + "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.15", + "@vue/language-core": "2.2.12" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/vue-tsc/node_modules/@volar/language-core": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.15" + } + }, + "node_modules/vue-tsc/node_modules/@volar/source-map": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue-tsc/node_modules/@vue/language-core": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz", + "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-tsc/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/vue-tsc/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/vue-tsc/node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + } + } +} diff --git a/oled-emulator/package.json b/oled-emulator/package.json new file mode 100644 index 000000000..439e6d5ec --- /dev/null +++ b/oled-emulator/package.json @@ -0,0 +1,54 @@ +{ + "name": "@meshtastic/oled-emulator", + "version": "0.1.0", + "description": "Vue.js OLED display emulator for Meshtastic firmware UI development", + "keywords": [ + "meshtastic", + "oled", + "ssd1306", + "sh1106", + "display", + "emulator", + "vue", + "vue3" + ], + "author": "Meshtastic", + "license": "GPL-3.0", + "repository": { + "type": "git", + "url": "https://github.com/meshtastic/firmware.git", + "directory": "oled-emulator" + }, + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.mjs", + "require": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./style.css": "./dist/style.css" + }, + "files": [ + "dist", + "src" + ], + "scripts": { + "dev": "vite", + "build": "vue-tsc && vite build", + "preview": "vite preview", + "type-check": "vue-tsc --noEmit" + }, + "peerDependencies": { + "vue": "^3.3.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "typescript": "^5.3.0", + "vite": "^5.0.0", + "vite-plugin-dts": "^3.7.0", + "vue": "^3.4.0", + "vue-tsc": "^2.0.0" + } +} diff --git a/oled-emulator/src/Demo.vue b/oled-emulator/src/Demo.vue new file mode 100644 index 000000000..c60d3b18e --- /dev/null +++ b/oled-emulator/src/Demo.vue @@ -0,0 +1,1001 @@ + + + + + diff --git a/oled-emulator/src/DisplayPresets.ts b/oled-emulator/src/DisplayPresets.ts new file mode 100644 index 000000000..b35a8e603 --- /dev/null +++ b/oled-emulator/src/DisplayPresets.ts @@ -0,0 +1,82 @@ +/** + * DisplayPresets.ts - OLED display presets for emulator + */ + +import type { OLEDDISPLAY_GEOMETRY } from "./OLEDDisplay"; + +// Display type presets with realistic OLED colors +export interface DisplayPreset { + name: string; + geometry: OLEDDISPLAY_GEOMETRY; + pixelOn: { r: number; g: number; b: number }; + pixelOff: { r: number; g: number; b: number }; + glow: number; + contrast: number; +} + +export const DISPLAY_PRESETS: Record = { + // Classic blue OLED (SSD1306) + "ssd1306-blue": { + name: "SSD1306 Blue", + geometry: "128x64", + pixelOn: { r: 100, g: 180, b: 255 }, + pixelOff: { r: 2, g: 3, b: 8 }, + glow: 0.4, + contrast: 1.0, + }, + // White OLED (SSD1306) + "ssd1306-white": { + name: "SSD1306 White", + geometry: "128x64", + pixelOn: { r: 255, g: 255, b: 245 }, + pixelOff: { r: 5, g: 5, b: 5 }, + glow: 0.3, + contrast: 1.0, + }, + // Yellow-blue dual color OLED + "ssd1306-yellow-blue": { + name: "SSD1306 Yellow/Blue", + geometry: "128x64", + // Yellow section is top 16 rows, blue is rest + pixelOn: { r: 255, g: 220, b: 50 }, // Will be overridden per-pixel + pixelOff: { r: 3, g: 3, b: 5 }, + glow: 0.35, + contrast: 1.0, + }, + // SH1106 (slightly different characteristics) + sh1106: { + name: "SH1106", + geometry: "128x64", + pixelOn: { r: 120, g: 200, b: 255 }, + pixelOff: { r: 2, g: 2, b: 6 }, + glow: 0.45, + contrast: 0.95, + }, + // SH1107 128x128 + sh1107: { + name: "SH1107 128x128", + geometry: "128x128", + pixelOn: { r: 255, g: 255, b: 240 }, + pixelOff: { r: 4, g: 4, b: 6 }, + glow: 0.3, + contrast: 1.0, + }, + // Small 64x48 OLED + "ssd1306-64x48": { + name: "SSD1306 64x48", + geometry: "64x48", + pixelOn: { r: 100, g: 180, b: 255 }, + pixelOff: { r: 2, g: 3, b: 8 }, + glow: 0.4, + contrast: 1.0, + }, + // Mini 128x32 + "ssd1306-128x32": { + name: "SSD1306 128x32", + geometry: "128x32", + pixelOn: { r: 100, g: 180, b: 255 }, + pixelOff: { r: 2, g: 3, b: 8 }, + glow: 0.4, + contrast: 1.0, + }, +}; diff --git a/oled-emulator/src/OLEDDisplay.ts b/oled-emulator/src/OLEDDisplay.ts new file mode 100644 index 000000000..247929083 --- /dev/null +++ b/oled-emulator/src/OLEDDisplay.ts @@ -0,0 +1,557 @@ +/** + * OLEDDisplay.ts - TypeScript port of esp8266-oled-ssd1306 OLEDDisplay API + * + * This provides the same drawing API as the firmware uses, allowing + * Meshtastic screen rendering code to be ported to the web. + * + * Buffer format: Vertical byte packing (8 vertical pixels per byte) + * Same as SSD1306/SH1106 native format. + */ + +export type OLEDDISPLAY_GEOMETRY = + | "128x64" + | "128x32" + | "64x48" + | "64x32" + | "128x128" + | "96x16"; + +export type OLEDDISPLAY_COLOR = "WHITE" | "BLACK" | "INVERSE"; + +export type OLEDDISPLAY_TEXT_ALIGNMENT = + | "TEXT_ALIGN_LEFT" + | "TEXT_ALIGN_CENTER" + | "TEXT_ALIGN_RIGHT"; + +export interface OLEDFont { + height: number; + firstChar: number; + charCount: number; + widths: number[]; + data: Uint8Array; +} + +// Resolution info based on geometry +const GEOMETRY_SIZES: Record< + OLEDDISPLAY_GEOMETRY, + { width: number; height: number } +> = { + "128x64": { width: 128, height: 64 }, + "128x32": { width: 128, height: 32 }, + "64x48": { width: 64, height: 48 }, + "64x32": { width: 64, height: 32 }, + "128x128": { width: 128, height: 128 }, + "96x16": { width: 96, height: 16 }, +}; + +export class OLEDDisplay { + private buffer: Uint8Array; + private _width: number; + private _height: number; + private color: OLEDDISPLAY_COLOR = "WHITE"; + private textAlignment: OLEDDISPLAY_TEXT_ALIGNMENT = "TEXT_ALIGN_LEFT"; + private font: OLEDFont | null = null; + + constructor(geometry: OLEDDISPLAY_GEOMETRY = "128x64") { + const size = GEOMETRY_SIZES[geometry]; + this._width = size.width; + this._height = size.height; + // Buffer size: width * (height / 8) - vertical byte packing + this.buffer = new Uint8Array((this._width * this._height) / 8); + } + + get width(): number { + return this._width; + } + + get height(): number { + return this._height; + } + + getWidth(): number { + return this._width; + } + + getHeight(): number { + return this._height; + } + + /** + * Get the raw framebuffer (vertical byte packing format) + */ + getBuffer(): Uint8Array { + return this.buffer; + } + + getBufferSize(): number { + return this.buffer.length; + } + + /** + * Clear the display buffer + */ + clear(): void { + this.buffer.fill(0); + } + + /** + * Set the drawing color + */ + setColor(color: OLEDDISPLAY_COLOR): void { + this.color = color; + } + + /** + * Set text alignment for drawString + */ + setTextAlignment(align: OLEDDISPLAY_TEXT_ALIGNMENT): void { + this.textAlignment = align; + } + + /** + * Set the current font + */ + setFont(font: OLEDFont): void { + this.font = font; + } + + /** + * Set a single pixel + */ + setPixel(x: number, y: number): void { + if (x < 0 || x >= this._width || y < 0 || y >= this._height) return; + + // Vertical byte packing: each byte represents 8 vertical pixels + const byteIndex = x + Math.floor(y / 8) * this._width; + const bitMask = 1 << (y & 7); + + switch (this.color) { + case "WHITE": + this.buffer[byteIndex] |= bitMask; + break; + case "BLACK": + this.buffer[byteIndex] &= ~bitMask; + break; + case "INVERSE": + this.buffer[byteIndex] ^= bitMask; + break; + } + } + + /** + * Clear a single pixel (set to black) + */ + clearPixel(x: number, y: number): void { + if (x < 0 || x >= this._width || y < 0 || y >= this._height) return; + const byteIndex = x + Math.floor(y / 8) * this._width; + const bitMask = 1 << (y & 7); + this.buffer[byteIndex] &= ~bitMask; + } + + /** + * Get a pixel value (true = on, false = off) + */ + getPixel(x: number, y: number): boolean { + if (x < 0 || x >= this._width || y < 0 || y >= this._height) return false; + const byteIndex = x + Math.floor(y / 8) * this._width; + const bitMask = 1 << (y & 7); + return (this.buffer[byteIndex] & bitMask) !== 0; + } + + /** + * Draw a line using Bresenham's algorithm + */ + drawLine(x0: number, y0: number, x1: number, y1: number): void { + const dx = Math.abs(x1 - x0); + const dy = Math.abs(y1 - y0); + const sx = x0 < x1 ? 1 : -1; + const sy = y0 < y1 ? 1 : -1; + let err = dx - dy; + + while (true) { + this.setPixel(x0, y0); + if (x0 === x1 && y0 === y1) break; + const e2 = 2 * err; + if (e2 > -dy) { + err -= dy; + x0 += sx; + } + if (e2 < dx) { + err += dx; + y0 += sy; + } + } + } + + /** + * Draw horizontal line (optimized) + */ + drawHorizontalLine(x: number, y: number, length: number): void { + if (y < 0 || y >= this._height) return; + for (let i = 0; i < length; i++) { + this.setPixel(x + i, y); + } + } + + /** + * Draw vertical line (optimized) + */ + drawVerticalLine(x: number, y: number, length: number): void { + if (x < 0 || x >= this._width) return; + for (let i = 0; i < length; i++) { + this.setPixel(x, y + i); + } + } + + /** + * Draw a rectangle outline + */ + drawRect(x: number, y: number, width: number, height: number): void { + this.drawHorizontalLine(x, y, width); + this.drawHorizontalLine(x, y + height - 1, width); + this.drawVerticalLine(x, y, height); + this.drawVerticalLine(x + width - 1, y, height); + } + + /** + * Draw a filled rectangle + */ + fillRect(x: number, y: number, width: number, height: number): void { + for (let i = 0; i < width; i++) { + for (let j = 0; j < height; j++) { + this.setPixel(x + i, y + j); + } + } + } + + /** + * Draw a circle outline using midpoint algorithm + */ + drawCircle(x0: number, y0: number, radius: number): void { + let x = radius; + let y = 0; + let err = 0; + + while (x >= y) { + this.setPixel(x0 + x, y0 + y); + this.setPixel(x0 + y, y0 + x); + this.setPixel(x0 - y, y0 + x); + this.setPixel(x0 - x, y0 + y); + this.setPixel(x0 - x, y0 - y); + this.setPixel(x0 - y, y0 - x); + this.setPixel(x0 + y, y0 - x); + this.setPixel(x0 + x, y0 - y); + + y++; + if (err <= 0) { + err += 2 * y + 1; + } + if (err > 0) { + x--; + err -= 2 * x + 1; + } + } + } + + /** + * Draw a filled circle + */ + fillCircle(x0: number, y0: number, radius: number): void { + let x = radius; + let y = 0; + let err = 0; + + while (x >= y) { + this.drawHorizontalLine(x0 - x, y0 + y, 2 * x + 1); + this.drawHorizontalLine(x0 - y, y0 + x, 2 * y + 1); + this.drawHorizontalLine(x0 - x, y0 - y, 2 * x + 1); + this.drawHorizontalLine(x0 - y, y0 - x, 2 * y + 1); + + y++; + if (err <= 0) { + err += 2 * y + 1; + } + if (err > 0) { + x--; + err -= 2 * x + 1; + } + } + } + + /** + * Draw a quarter circle (quadrant 0-3) + */ + drawCircleQuads(x0: number, y0: number, radius: number, quads: number): void { + let x = radius; + let y = 0; + let err = 0; + + while (x >= y) { + if (quads & 0x1) { + this.setPixel(x0 + x, y0 - y); + this.setPixel(x0 + y, y0 - x); + } + if (quads & 0x2) { + this.setPixel(x0 - y, y0 - x); + this.setPixel(x0 - x, y0 - y); + } + if (quads & 0x4) { + this.setPixel(x0 - x, y0 + y); + this.setPixel(x0 - y, y0 + x); + } + if (quads & 0x8) { + this.setPixel(x0 + y, y0 + x); + this.setPixel(x0 + x, y0 + y); + } + + y++; + if (err <= 0) { + err += 2 * y + 1; + } + if (err > 0) { + x--; + err -= 2 * x + 1; + } + } + } + + /** + * Draw an XBM image (like firmware's drawXbm) + * XBM format: LSB first, horizontal + */ + drawXbm( + x: number, + y: number, + width: number, + height: number, + xbm: Uint8Array | number[], + ): void { + const widthBytes = Math.ceil(width / 8); + for (let j = 0; j < height; j++) { + for (let i = 0; i < width; i++) { + const byteIndex = j * widthBytes + Math.floor(i / 8); + const bitIndex = i % 8; + if (xbm[byteIndex] & (1 << bitIndex)) { + this.setPixel(x + i, y + j); + } + } + } + } + + /** + * Draw a PROGMEM-style image (vertical byte format, like some firmware icons) + */ + drawFastImage( + x: number, + y: number, + width: number, + height: number, + image: Uint8Array | number[], + ): void { + const heightInBytes = Math.ceil(height / 8); + for (let i = 0; i < width; i++) { + for (let j = 0; j < heightInBytes; j++) { + const imageByte = image[i + j * width]; + for (let bit = 0; bit < 8; bit++) { + if (imageByte & (1 << bit)) { + this.setPixel(x + i, y + j * 8 + bit); + } + } + } + } + } + + /** + * Get the width of a string with the current font + */ + getStringWidth(text: string): number { + if (!this.font) return 0; + let width = 0; + for (const char of text) { + const charCode = char.charCodeAt(0); + if ( + charCode >= this.font.firstChar && + charCode < this.font.firstChar + this.font.charCount + ) { + width += this.font.widths[charCode - this.font.firstChar]; + } else { + // Unknown char - use space width or default + width += this.font.widths[0] || 4; + } + } + return width; + } + + /** + * Draw a string at the given position + */ + drawString(x: number, y: number, text: string): void { + if (!this.font) return; + + // Adjust x based on alignment + let startX = x; + if (this.textAlignment === "TEXT_ALIGN_CENTER") { + startX = x - Math.floor(this.getStringWidth(text) / 2); + } else if (this.textAlignment === "TEXT_ALIGN_RIGHT") { + startX = x - this.getStringWidth(text); + } + + let cursorX = startX; + for (const char of text) { + const charCode = char.charCodeAt(0); + if ( + charCode >= this.font.firstChar && + charCode < this.font.firstChar + this.font.charCount + ) { + const charIndex = charCode - this.font.firstChar; + const charWidth = this.font.widths[charIndex]; + + // Find the offset into the font data + let offset = 0; + const bytesPerColumn = Math.ceil(this.font.height / 8); + for (let i = 0; i < charIndex; i++) { + offset += this.font.widths[i] * bytesPerColumn; + } + + // Draw the character + this.drawFontChar( + cursorX, + y, + charWidth, + this.font.height, + this.font.data, + offset, + ); + cursorX += charWidth; + } else { + // Unknown character - advance by space + cursorX += this.font.widths[0] || 4; + } + } + } + + /** + * Draw a single font character + */ + private drawFontChar( + x: number, + y: number, + width: number, + height: number, + data: Uint8Array, + offset: number, + ): void { + const bytesPerColumn = Math.ceil(height / 8); + for (let col = 0; col < width; col++) { + for (let byteRow = 0; byteRow < bytesPerColumn; byteRow++) { + const dataByte = data[offset + col * bytesPerColumn + byteRow]; + for (let bit = 0; bit < 8; bit++) { + const pixelY = byteRow * 8 + bit; + if (pixelY < height && dataByte & (1 << bit)) { + this.setPixel(x + col, y + pixelY); + } + } + } + } + } + + /** + * Draw a string that wraps within maxWidth + */ + drawStringMaxWidth( + x: number, + y: number, + maxWidth: number, + text: string, + ): void { + if (!this.font) return; + + const words = text.split(" "); + let line = ""; + let lineY = y; + + for (const word of words) { + const testLine = line + (line ? " " : "") + word; + const testWidth = this.getStringWidth(testLine); + + if (testWidth > maxWidth && line) { + this.drawString(x, lineY, line); + line = word; + lineY += this.font.height; + } else { + line = testLine; + } + } + + if (line) { + this.drawString(x, lineY, line); + } + } + + /** + * Draw a progress bar + */ + drawProgressBar( + x: number, + y: number, + width: number, + height: number, + progress: number, + ): void { + // Clamp progress 0-100 + progress = Math.max(0, Math.min(100, progress)); + + // Draw outline + this.drawRect(x, y, width, height); + + // Draw fill + const fillWidth = Math.floor(((width - 4) * progress) / 100); + this.fillRect(x + 2, y + 2, fillWidth, height - 4); + } + + /** + * Display - no-op for emulator (would send buffer to hardware) + */ + display(): void { + // In real hardware this sends the buffer to the display + // For emulator, this is a no-op - we render from getBuffer() + } + + /** + * Flip the display (useful for some mounting orientations) + */ + flipScreenVertically(): void { + // Rotate buffer 180 degrees + const newBuffer = new Uint8Array(this.buffer.length); + const heightBytes = this._height / 8; + + for (let x = 0; x < this._width; x++) { + for (let yByte = 0; yByte < heightBytes; yByte++) { + let srcByte = this.buffer[x + yByte * this._width]; + // Reverse the bits + let reversed = 0; + for (let i = 0; i < 8; i++) { + if (srcByte & (1 << i)) { + reversed |= 1 << (7 - i); + } + } + // Place in mirrored position + const newX = this._width - 1 - x; + const newYByte = heightBytes - 1 - yByte; + newBuffer[newX + newYByte * this._width] = reversed; + } + } + + this.buffer = newBuffer; + } +} + +// Color constants for compatibility with firmware code style +export const WHITE: OLEDDISPLAY_COLOR = "WHITE"; +export const BLACK: OLEDDISPLAY_COLOR = "BLACK"; +export const INVERSE: OLEDDISPLAY_COLOR = "INVERSE"; + +export const TEXT_ALIGN_LEFT: OLEDDISPLAY_TEXT_ALIGNMENT = "TEXT_ALIGN_LEFT"; +export const TEXT_ALIGN_CENTER: OLEDDISPLAY_TEXT_ALIGNMENT = + "TEXT_ALIGN_CENTER"; +export const TEXT_ALIGN_RIGHT: OLEDDISPLAY_TEXT_ALIGNMENT = "TEXT_ALIGN_RIGHT"; diff --git a/oled-emulator/src/OLEDEmulator.vue b/oled-emulator/src/OLEDEmulator.vue new file mode 100644 index 000000000..862b6f35b --- /dev/null +++ b/oled-emulator/src/OLEDEmulator.vue @@ -0,0 +1,327 @@ + + + + + diff --git a/oled-emulator/src/OLEDFonts.ts b/oled-emulator/src/OLEDFonts.ts new file mode 100644 index 000000000..8a45ad02e --- /dev/null +++ b/oled-emulator/src/OLEDFonts.ts @@ -0,0 +1,467 @@ +/** + * OLEDFonts.ts - Font data for OLED display emulator + * + * These are simplified bitmap fonts matching the style used in + * Meshtastic firmware (based on ArialMT from esp8266-oled-ssd1306). + * + * Font format: Vertical byte packing, column-major order + * Each character is drawn column by column, with each byte representing + * 8 vertical pixels. + */ + +import type { OLEDFont } from "./OLEDDisplay"; + +/** + * ArialMT Plain 10 - Small font (height ~13 pixels) + * Covers ASCII 32-126 (space through ~) + */ +export const ArialMT_Plain_10: OLEDFont = { + height: 13, + firstChar: 32, + charCount: 95, + widths: [ + 3, // space + 3, // ! + 4, // " + 6, // # + 6, // $ + 9, // % + 7, // & + 2, // ' + 4, // ( + 4, // ) + 5, // * + 6, // + + 3, // , + 4, // - + 3, // . + 4, // / + 6, // 0 + 6, // 1 + 6, // 2 + 6, // 3 + 6, // 4 + 6, // 5 + 6, // 6 + 6, // 7 + 6, // 8 + 6, // 9 + 3, // : + 3, // ; + 6, // < + 6, // = + 6, // > + 6, // ? + 11, // @ + 8, // A + 7, // B + 7, // C + 7, // D + 6, // E + 6, // F + 8, // G + 7, // H + 3, // I + 5, // J + 7, // K + 6, // L + 9, // M + 7, // N + 8, // O + 6, // P + 8, // Q + 7, // R + 6, // S + 6, // T + 7, // U + 7, // V + 11, // W + 7, // X + 7, // Y + 6, // Z + 4, // [ + 4, // \ + 4, // ] + 6, // ^ + 6, // _ + 4, // ` + 6, // a + 6, // b + 5, // c + 6, // d + 6, // e + 4, // f + 6, // g + 6, // h + 3, // i + 3, // j + 6, // k + 3, // l + 9, // m + 6, // n + 6, // o + 6, // p + 6, // q + 4, // r + 5, // s + 4, // t + 6, // u + 6, // v + 9, // w + 6, // x + 6, // y + 5, // z + 4, // { + 3, // | + 4, // } + 6, // ~ + ], + data: new Uint8Array([ + // Space (32) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // ! (33) + 0x00, 0x00, 0xbe, 0x00, 0x00, 0x00, + // " (34) + 0x00, 0x0e, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, + // # (35) + 0x00, 0x48, 0xf8, 0x4e, 0xf8, 0x4e, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, + // $ (36) + 0x00, 0x9c, 0x92, 0xff, 0x92, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // % (37) + 0x0c, 0x12, 0x12, 0x8c, 0x60, 0x18, 0xc6, 0x20, 0x20, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // & (38) + 0x00, 0x60, 0x9c, 0x92, 0x6a, 0xa4, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + // ' (39) + 0x00, 0x0e, 0x00, 0x00, + // ( (40) + 0x00, 0x7c, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00, + // ) (41) + 0x00, 0x82, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, + // * (42) + 0x14, 0x08, 0x3e, 0x08, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, + // + (43) + 0x10, 0x10, 0x7c, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // , (44) + 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, + // - (45) + 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, + // . (46) + 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, + // / (47) + 0x00, 0xc0, 0x38, 0x06, 0x00, 0x00, 0x00, 0x00, + // 0 (48) + 0x7c, 0x82, 0x82, 0x82, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 1 (49) + 0x00, 0x04, 0x02, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 2 (50) + 0x84, 0xc2, 0xa2, 0x92, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 3 (51) + 0x44, 0x82, 0x92, 0x92, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 4 (52) + 0x30, 0x28, 0x24, 0xfe, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 5 (53) + 0x4e, 0x8a, 0x8a, 0x8a, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 6 (54) + 0x78, 0x94, 0x92, 0x92, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 7 (55) + 0x02, 0xc2, 0x32, 0x0a, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 8 (56) + 0x6c, 0x92, 0x92, 0x92, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 9 (57) + 0x0c, 0x92, 0x92, 0x52, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // : (58) + 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, + // ; (59) + 0x00, 0x00, 0xc8, 0x00, 0x00, 0x00, + // < (60) + 0x10, 0x28, 0x28, 0x44, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // = (61) + 0x28, 0x28, 0x28, 0x28, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // > (62) + 0x44, 0x44, 0x28, 0x28, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // ? (63) + 0x04, 0x02, 0xa2, 0x12, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // @ (64) + 0xf0, 0x08, 0xe4, 0x12, 0x12, 0x12, 0xe2, 0x14, 0x08, 0xf0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // A (65) + 0x00, 0xc0, 0x38, 0x26, 0x26, 0x38, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + // B (66) + 0xfe, 0x92, 0x92, 0x92, 0x92, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + // C (67) + 0x7c, 0x82, 0x82, 0x82, 0x82, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + // D (68) + 0xfe, 0x82, 0x82, 0x82, 0x44, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + // E (69) + 0xfe, 0x92, 0x92, 0x92, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // F (70) + 0xfe, 0x12, 0x12, 0x12, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // G (71) + 0x7c, 0x82, 0x82, 0x92, 0x92, 0x74, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + // H (72) + 0xfe, 0x10, 0x10, 0x10, 0x10, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + // I (73) + 0x00, 0xfe, 0x00, 0x00, 0x00, 0x00, + // J (74) + 0x40, 0x80, 0x80, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // K (75) + 0xfe, 0x10, 0x28, 0x44, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + // L (76) + 0xfe, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // M (77) + 0xfe, 0x04, 0x18, 0x60, 0x60, 0x18, 0x04, 0xfe, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // N (78) + 0xfe, 0x04, 0x08, 0x30, 0x40, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + // O (79) + 0x7c, 0x82, 0x82, 0x82, 0x82, 0x82, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + // P (80) + 0xfe, 0x12, 0x12, 0x12, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Q (81) + 0x7c, 0x82, 0x82, 0x82, 0x42, 0xc2, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + // R (82) + 0xfe, 0x12, 0x12, 0x32, 0x4c, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + // S (83) + 0x4c, 0x92, 0x92, 0x92, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // T (84) + 0x02, 0x02, 0xfe, 0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // U (85) + 0x7e, 0x80, 0x80, 0x80, 0x80, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + // V (86) + 0x06, 0x18, 0x60, 0x80, 0x60, 0x18, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + // W (87) + 0x06, 0x38, 0xc0, 0x38, 0x06, 0x38, 0xc0, 0x38, 0x06, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // X (88) + 0x82, 0x44, 0x28, 0x10, 0x28, 0x44, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + // Y (89) + 0x02, 0x04, 0x08, 0xf0, 0x08, 0x04, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + // Z (90) + 0xc2, 0xa2, 0x92, 0x8a, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // [ (91) + 0x00, 0xfe, 0x82, 0x82, 0x00, 0x00, 0x00, 0x00, + // \ (92) + 0x06, 0x38, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, + // ] (93) + 0x82, 0x82, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, + // ^ (94) + 0x08, 0x04, 0x02, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // _ (95) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // ` (96) + 0x00, 0x02, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, + // a (97) + 0x40, 0xa8, 0xa8, 0xa8, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // b (98) + 0xfe, 0x88, 0x88, 0x88, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // c (99) + 0x70, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // d (100) + 0x70, 0x88, 0x88, 0x88, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // e (101) + 0x70, 0xa8, 0xa8, 0xa8, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // f (102) + 0x08, 0xfc, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, + // g (103) + 0x30, 0x48, 0x48, 0x48, 0xf8, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, + // h (104) + 0xfe, 0x08, 0x08, 0x08, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // i (105) + 0x00, 0xfa, 0x00, 0x00, 0x00, 0x00, + // j (106) + 0x00, 0x00, 0xfa, 0x01, 0x00, 0x00, + // k (107) + 0xfe, 0x20, 0x50, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // l (108) + 0x00, 0xfe, 0x00, 0x00, 0x00, 0x00, + // m (109) + 0xf8, 0x08, 0x08, 0xf0, 0x08, 0x08, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // n (110) + 0xf8, 0x08, 0x08, 0x08, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // o (111) + 0x70, 0x88, 0x88, 0x88, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // p (112) + 0xf8, 0x48, 0x48, 0x48, 0x30, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // q (113) + 0x30, 0x48, 0x48, 0x48, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + // r (114) + 0xf8, 0x10, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, + // s (115) + 0x90, 0xa8, 0xa8, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // t (116) + 0x08, 0x7e, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, + // u (117) + 0x78, 0x80, 0x80, 0x80, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // v (118) + 0x18, 0x60, 0x80, 0x60, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // w (119) + 0x78, 0x80, 0x60, 0x18, 0x60, 0x80, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // x (120) + 0x88, 0x50, 0x20, 0x50, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // y (121) + 0x18, 0x60, 0x80, 0x60, 0x18, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + // z (122) + 0xc8, 0xa8, 0x98, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // { (123) + 0x10, 0x6c, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00, + // | (124) + 0x00, 0xfe, 0x00, 0x00, 0x00, 0x00, + // } (125) + 0x82, 0x6c, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, + // ~ (126) + 0x10, 0x08, 0x10, 0x20, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]), +}; + +/** + * ArialMT Plain 16 - Medium font (height ~19 pixels) + * Simplified subset covering 0-9, A-Z, a-z, common punctuation + */ +export const ArialMT_Plain_16: OLEDFont = { + height: 19, + firstChar: 32, + charCount: 95, + widths: [ + 4, // space + 4, // ! + 5, // " + 8, // # + 8, // $ + 12, // % + 10, // & + 2, // ' + 5, // ( + 5, // ) + 6, // * + 8, // + + 4, // , + 5, // - + 4, // . + 4, // / + 8, // 0 + 8, // 1 + 8, // 2 + 8, // 3 + 8, // 4 + 8, // 5 + 8, // 6 + 8, // 7 + 8, // 8 + 8, // 9 + 4, // : + 4, // ; + 8, // < + 8, // = + 8, // > + 8, // ? + 15, // @ + 10, // A + 9, // B + 10, // C + 10, // D + 9, // E + 8, // F + 11, // G + 9, // H + 4, // I + 6, // J + 9, // K + 8, // L + 11, // M + 9, // N + 11, // O + 9, // P + 11, // Q + 10, // R + 9, // S + 8, // T + 9, // U + 10, // V + 14, // W + 10, // X + 10, // Y + 9, // Z + 4, // [ + 4, // \ + 4, // ] + 7, // ^ + 8, // _ + 5, // ` + 8, // a + 8, // b + 7, // c + 8, // d + 8, // e + 4, // f + 8, // g + 8, // h + 4, // i + 4, // j + 8, // k + 4, // l + 12, // m + 8, // n + 8, // o + 8, // p + 8, // q + 5, // r + 7, // s + 4, // t + 8, // u + 8, // v + 12, // w + 8, // x + 8, // y + 7, // z + 5, // { + 4, // | + 5, // } + 8, // ~ + ], + // Font data would be similar pattern but larger - using placeholder + data: new Uint8Array(2500).fill(0xff), // Placeholder - would contain actual font bitmap data +}; + +/** + * Helper to get FONT_HEIGHT_SMALL equivalent + */ +export const FONT_HEIGHT_SMALL = 13; +export const FONT_HEIGHT_MEDIUM = 19; +export const FONT_HEIGHT_LARGE = 28; + +/** + * Helper function to create a font from raw byte data (for importing firmware fonts) + */ +export function createFontFromBytes( + height: number, + firstChar: number, + widths: number[], + data: number[], +): OLEDFont { + return { + height, + firstChar, + charCount: widths.length, + widths, + data: new Uint8Array(data), + }; +} diff --git a/oled-emulator/src/OLEDImages.ts b/oled-emulator/src/OLEDImages.ts new file mode 100644 index 000000000..e81a2cc27 --- /dev/null +++ b/oled-emulator/src/OLEDImages.ts @@ -0,0 +1,255 @@ +/** + * OLEDImages.ts - Icon and image data for OLED display emulator + * + * These are direct ports of the XBM/bitmap icons from the Meshtastic firmware. + * Format: XBM style (LSB first, horizontal scanning) + */ + +// Satellite icon (8x8) +export const imgSatellite_width = 8; +export const imgSatellite_height = 8; +export const imgSatellite = new Uint8Array([ + 0b00000000, 0b00000000, 0b00000000, 0b00011000, 0b11011011, 0b11111111, + 0b11011011, 0b00011000, +]); + +// USB icon (10x8) +export const imgUSB = new Uint8Array([ + 0x00, 0xfc, 0xf0, 0xfc, 0x88, 0xff, 0x86, 0xfe, 0x85, 0xfe, 0x89, 0xff, 0xf1, + 0xfc, 0x00, 0xfc, +]); + +// Power icon (8x16) +export const imgPower = new Uint8Array([ + 0x40, 0x40, 0x40, 0x58, 0x48, 0x08, 0x08, 0x08, 0x1c, 0x22, 0x22, 0x41, 0x7f, + 0x22, 0x22, 0x22, +]); + +// User icon (8x8) +export const imgUser = new Uint8Array([ + 0x3c, 0x42, 0x99, 0xa5, 0xa5, 0x99, 0x42, 0x3c, +]); + +// Position empty arrow (6x8) +export const imgPositionEmpty = new Uint8Array([ + 0x20, 0x30, 0x28, 0x24, 0x42, 0xff, +]); + +// Position solid arrow (6x8) +export const imgPositionSolid = new Uint8Array([ + 0x20, 0x30, 0x38, 0x3c, 0x7e, 0xff, +]); + +// Mail icon (10x7) +export const mail_width = 10; +export const mail_height = 7; +export const mail = new Uint8Array([ + 0b11111111, + 0b00, // Top line + 0b10000001, + 0b00, // Edges + 0b11000011, + 0b00, // Diagonals start + 0b10100101, + 0b00, // Inner M part + 0b10011001, + 0b00, // Inner M part + 0b10000001, + 0b00, // Edges + 0b11111111, + 0b00, // Bottom line +]); + +// Hop icon (9x10) +export const hop_width = 9; +export const hop_height = 10; +export const hop = new Uint8Array([ + 0x05, 0x00, 0x07, 0x00, 0x05, 0x00, 0x38, 0x00, 0x28, 0x00, 0x38, 0x00, 0xc0, + 0x01, 0x40, 0x01, 0xc0, 0x01, 0x40, 0x00, +]); + +// Mail icon (8x8) +export const icon_mail = new Uint8Array([ + 0b11111111, // ████████ top border + 0b10000001, // █ █ sides + 0b11000011, // ██ ██ diagonal + 0b10100101, // █ █ █ █ inner M + 0b10011001, // █ ██ █ inner M + 0b10000001, // █ █ sides + 0b10000001, // █ █ sides + 0b11111111, // ████████ bottom +]); + +// Compass icon (8x8) +export const icon_compass = new Uint8Array([ + 0x3c, // Row 0: ..####.. + 0x52, // Row 1: .#..#.#. + 0x91, // Row 2: #...#..# + 0x91, // Row 3: #...#..# + 0x91, // Row 4: #...#..# + 0x81, // Row 5: #......# + 0x42, // Row 6: .#....#. + 0x3c, // Row 7: ..####.. +]); + +// Radio icon (8x8) +export const icon_radio = new Uint8Array([ + 0x0f, // Row 0: ####.... + 0x10, // Row 1: ....#... + 0x27, // Row 2: ###..#.. + 0x48, // Row 3: ...#..#. + 0x93, // Row 4: ##..#..# + 0xa4, // Row 5: ..#..#.# + 0xa8, // Row 6: ...#.#.# + 0xa9, // Row 7: #..#.#.# +]); + +// System/settings icon (8x8) +export const icon_system = new Uint8Array([ + 0x24, // Row 0: ..#..#.. + 0x3c, // Row 1: ..####.. + 0xc3, // Row 2: ##....## + 0x5a, // Row 3: .#.##.#. + 0x5a, // Row 4: .#.##.#. + 0xc3, // Row 5: ##....## + 0x3c, // Row 6: ..####.. + 0x24, // Row 7: ..#..#.. +]); + +// WiFi icon (8x8) +export const icon_wifi = new Uint8Array([ + 0b00000000, 0b00011000, 0b00111100, 0b01111110, 0b11011011, 0b00011000, + 0b00011000, 0b00000000, +]); + +// Nodes icon (8x8) +export const icon_nodes = new Uint8Array([ + 0xf9, // Row 0: #..##### + 0x00, // Row 1 + 0xf9, // Row 2: #..##### + 0x00, // Row 3 + 0xf9, // Row 4: #..##### + 0x00, // Row 5 + 0xf9, // Row 6: #..##### + 0x00, // Row 7 +]); + +// Battery vertical (7x11) +export const batteryBitmap_v = new Uint8Array([ + 0b00011100, 0b00111110, 0b01000001, 0b01000001, 0b00000000, 0b00000000, + 0b00000000, 0b01000001, 0b01000001, 0b01000001, 0b00111110, +]); + +// Battery side gaps (8x3) +export const batteryBitmap_sidegaps_v = new Uint8Array([ + 0b10000010, 0b10000010, 0b10000010, +]); + +// Lightning bolt vertical (5x5) +export const lightning_bolt_v = new Uint8Array([ + 0b00000100, 0b00000110, 0b00011111, 0b00001100, 0b00000100, +]); + +// Horizontal battery bottom (13x9) +export const batteryBitmap_h_bottom = new Uint8Array([ + 0b00011110, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, + 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, + 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, + 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, + 0b00011110, 0b00000000, +]); + +// Horizontal battery top (13x9) +export const batteryBitmap_h_top = new Uint8Array([ + 0b00111100, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, + 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b11000000, 0b00000000, + 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b01000000, 0b00000000, + 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, + 0b00111100, 0b00000000, +]); + +// Lightning bolt horizontal (13x9) +export const lightning_bolt_h = new Uint8Array([ + 0b00000000, 0b00000000, 0b00100000, 0b00000000, 0b00110000, 0b00000000, + 0b00111000, 0b00000000, 0b00111100, 0b00000000, 0b00011110, 0b00000000, + 0b11111111, 0b00000000, 0b01111000, 0b00000000, 0b00111100, 0b00000000, + 0b00011100, 0b00000000, 0b00001100, 0b00000000, 0b00000100, 0b00000000, + 0b00000000, 0b00000000, +]); + +// Signal bars icon (various signal strengths) +export const signal_bars = { + width: 12, + height: 8, + // 0 bars + none: new Uint8Array([ + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, 0b00000000, + ]), + // 1 bar + low: new Uint8Array([ + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, + 0b00000011, 0b00000000, 0b00000011, 0b00000000, + ]), + // 2 bars + medium: new Uint8Array([ + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00001100, 0b00000000, 0b00001100, 0b00000000, + 0b00001111, 0b00000000, 0b00001111, 0b00000000, + ]), + // 3 bars + high: new Uint8Array([ + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00110000, 0b00000000, + 0b00110000, 0b00000000, 0b00111100, 0b00000000, 0b00111100, 0b00000000, + 0b00111111, 0b00000000, 0b00111111, 0b00000000, + ]), + // 4 bars (full) + full: new Uint8Array([ + 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11110000, 0b00000000, + 0b11110000, 0b00000000, 0b11111100, 0b00000000, 0b11111100, 0b00000000, + 0b11111111, 0b00000000, 0b11111111, 0b00000000, + ]), +}; + +// Meshtastic logo (simplified 32x32) +export const logo_width = 32; +export const logo_height = 32; +export const meshtastic_logo = new Uint8Array([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, + 0x03, 0xc0, 0x00, 0x80, 0x01, 0x80, 0x01, 0xc0, 0x00, 0x00, 0x03, 0x60, 0x00, + 0x00, 0x06, 0x30, 0x00, 0x00, 0x0c, 0x18, 0xf8, 0x1f, 0x18, 0x08, 0x04, 0x20, + 0x10, 0x0c, 0x02, 0x40, 0x30, 0x04, 0x01, 0x80, 0x20, 0x06, 0x01, 0x80, 0x60, + 0x02, 0xe1, 0x87, 0x40, 0x02, 0x11, 0x88, 0x40, 0x03, 0x09, 0x90, 0xc0, 0x03, + 0x09, 0x90, 0xc0, 0x02, 0x11, 0x88, 0x40, 0x02, 0xe1, 0x87, 0x40, 0x06, 0x01, + 0x80, 0x60, 0x04, 0x01, 0x80, 0x20, 0x0c, 0x02, 0x40, 0x30, 0x08, 0x04, 0x20, + 0x10, 0x18, 0xf8, 0x1f, 0x18, 0x30, 0x00, 0x00, 0x0c, 0x60, 0x00, 0x00, 0x06, + 0xc0, 0x00, 0x00, 0x03, 0x80, 0x01, 0x80, 0x01, 0x00, 0x03, 0xc0, 0x00, 0x00, + 0x06, 0x60, 0x00, 0x00, 0xfc, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, +]); + +// Bluetooth icon (8x11) +export const icon_bluetooth = new Uint8Array([ + 0b00000000, 0b10000100, 0b01001010, 0b00101010, 0b00010100, 0b11111111, + 0b00010100, 0b00101010, 0b01001010, 0b10000100, 0b00000000, +]); + +// GPS/Location pin icon (8x12) +export const icon_gps_pin = new Uint8Array([ + 0b00111100, 0b01111110, 0b11111111, 0b11100111, 0b11100111, 0b11111111, + 0b01111110, 0b00111100, 0b00011000, 0b00011000, 0b00001000, 0b00000000, +]); + +// Message/chat bubble icon (10x8) +export const icon_message = new Uint8Array([ + 0b11111111, 0b00, 0b10000001, 0b00, 0b10000001, 0b00, 0b10000001, 0b00, + 0b10000001, 0b00, 0b11111111, 0b00, 0b00000110, 0b00, 0b00000010, 0b00, +]); + +/** + * Helper to convert icon data to XBM format if needed + */ +export function toXbm(data: Uint8Array | number[]): Uint8Array { + return data instanceof Uint8Array ? data : new Uint8Array(data); +} diff --git a/oled-emulator/src/ScreenRenderers.ts b/oled-emulator/src/ScreenRenderers.ts new file mode 100644 index 000000000..4dee89f4a --- /dev/null +++ b/oled-emulator/src/ScreenRenderers.ts @@ -0,0 +1,627 @@ +/** + * ScreenRenderers.ts - Sample screen rendering functions + * + * These demonstrate how to port Meshtastic firmware UI screens + * to the OLED emulator. The API matches the firmware's drawing code. + */ + +import { + OLEDDisplay, + WHITE, + BLACK, + TEXT_ALIGN_LEFT, + TEXT_ALIGN_CENTER, + TEXT_ALIGN_RIGHT, +} from "./OLEDDisplay"; +import { Font_6x8 } from "./SimpleFonts"; +import * as images from "./OLEDImages"; + +// Font height for simple 6x8 font +const FONT_HEIGHT_SMALL = 8; + +// Screen resolution detection (matches SharedUIDisplay.cpp) +export type ScreenResolution = "UltraLow" | "Low" | "High"; + +export function determineScreenResolution( + width: number, + height: number, +): ScreenResolution { + if (width <= 64 || height <= 48) { + return "UltraLow"; + } + if (width > 128) { + return "High"; + } + return "Low"; +} + +// Helper for consistent line positioning +export function getTextPositions(display: OLEDDisplay): number[] { + const fontHeight = FONT_HEIGHT_SMALL; + return [ + 0, // textZeroLine + fontHeight - 1, // textFirstLine + fontHeight - 1 + (fontHeight - 5), // textSecondLine + fontHeight - 1 + 2 * (fontHeight - 5), // textThirdLine + fontHeight - 1 + 3 * (fontHeight - 5), // textFourthLine + fontHeight - 1 + 4 * (fontHeight - 5), // textFifthLine + ]; +} + +/** + * Draw a rounded highlight box (used for inverted headers) + */ +export function drawRoundedHighlight( + display: OLEDDisplay, + x: number, + y: number, + w: number, + h: number, + r: number, +): void { + // Draw the center and side rectangles + display.fillRect(x + r, y, w - 2 * r, h); // center bar + display.fillRect(x, y + r, r, h - 2 * r); // left edge + display.fillRect(x + w - r, y + r, r, h - 2 * r); // right edge + + // Draw the rounded corners using filled circles + display.fillCircle(x + r + 1, y + r, r); // top-left + display.fillCircle(x + w - r - 1, y + r, r); // top-right + display.fillCircle(x + r + 1, y + h - r - 1, r); // bottom-left + display.fillCircle(x + w - r - 1, y + h - r - 1, r); // bottom-right +} + +/** + * Draw the common header bar (battery, title, icons) + */ +export function drawCommonHeader( + display: OLEDDisplay, + x: number, + y: number, + titleStr: string, + options: { + inverted?: boolean; + batteryPercent?: number; + isCharging?: boolean; + hasUSB?: boolean; + hasUnreadMessage?: boolean; + } = {}, +): void { + const { + inverted = true, + batteryPercent = 75, + isCharging = false, + hasUSB = false, + hasUnreadMessage = false, + } = options; + + const HEADER_OFFSET_Y = 1; + y += HEADER_OFFSET_Y; + + display.setFont(Font_6x8); + const highlightHeight = FONT_HEIGHT_SMALL - 1; + const screenW = display.getWidth(); + + if (inverted) { + // Draw inverted header background + display.setColor(WHITE); + drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2); + display.setColor(BLACK); + } else { + // Draw line under header + display.setColor(WHITE); + display.drawLine(0, 14, screenW, 14); + } + + // Draw title centered + display.setTextAlignment(TEXT_ALIGN_CENTER); + display.drawString(screenW / 2, y, titleStr); + + // Reset color for remaining elements + if (inverted) { + display.setColor(WHITE); + } + + // Draw battery on left + let batteryX = 1; + const batteryY = HEADER_OFFSET_Y + 1; + + if (hasUSB && !isCharging) { + // Draw USB icon + display.drawXbm(batteryX + 1, batteryY + 2, 10, 8, images.imgUSB); + batteryX += 11; + } else { + // Draw battery outline + display.drawXbm(batteryX, batteryY, 7, 11, images.batteryBitmap_v); + + if (isCharging) { + // Draw lightning bolt + display.drawXbm( + batteryX + 1, + batteryY + 3, + 5, + 5, + images.lightning_bolt_v, + ); + } else { + // Draw battery level + display.drawXbm( + batteryX - 1, + batteryY + 4, + 8, + 3, + images.batteryBitmap_sidegaps_v, + ); + const fillHeight = Math.floor((8 * batteryPercent) / 100); + const fillY = batteryY - fillHeight + 10; + display.fillRect(batteryX + 1, fillY, 5, fillHeight); + } + batteryX += 9; + } + + // Draw mail icon on right if unread message + if (hasUnreadMessage) { + const mailX = screenW - images.mail_width - 2; + display.drawXbm( + mailX, + y + 2, + images.mail_width, + images.mail_height, + images.mail, + ); + } + + display.setTextAlignment(TEXT_ALIGN_LEFT); + display.setColor(WHITE); +} + +/** + * Draw a boot/splash screen with logo + */ +export function drawBootScreen( + display: OLEDDisplay, + appVersion: string = "2.5.0", +): void { + display.clear(); + display.setFont(Font_6x8); + display.setColor(WHITE); + + const centerX = display.getWidth() / 2; + const centerY = display.getHeight() / 2; + + // Draw logo centered + const logoX = centerX - images.logo_width / 2; + const logoY = centerY - images.logo_height / 2 - 8; + display.drawXbm( + logoX, + logoY, + images.logo_width, + images.logo_height, + images.meshtastic_logo, + ); + + // Draw version below + display.setTextAlignment(TEXT_ALIGN_CENTER); + display.drawString( + centerX, + centerY + images.logo_height / 2, + `v${appVersion}`, + ); + + display.setTextAlignment(TEXT_ALIGN_LEFT); +} + +/** + * Draw a node info screen + */ +export function drawNodeInfoScreen( + display: OLEDDisplay, + nodeInfo: { + shortName: string; + longName: string; + nodeId: string; + batteryLevel?: number; + lastHeard?: string; + snr?: number; + hopsAway?: number; + }, +): void { + display.clear(); + + // Draw header + drawCommonHeader(display, 0, 0, "Node Info", { + inverted: true, + batteryPercent: 85, + }); + + display.setFont(Font_6x8); + display.setColor(WHITE); + display.setTextAlignment(TEXT_ALIGN_LEFT); + + const lines = getTextPositions(display); + const screenW = display.getWidth(); + + // Node name (bolded by drawing twice offset) + display.drawString(0, lines[1], nodeInfo.longName); + display.drawString(1, lines[1], nodeInfo.longName); + + // Short name and ID + display.drawString(0, lines[2], `${nodeInfo.shortName} • ${nodeInfo.nodeId}`); + + // Last heard + if (nodeInfo.lastHeard) { + display.drawString(0, lines[3], `Heard: ${nodeInfo.lastHeard}`); + } + + // Signal info on right side + if (nodeInfo.snr !== undefined) { + const snrStr = `SNR: ${nodeInfo.snr}dB`; + display.setTextAlignment(TEXT_ALIGN_RIGHT); + display.drawString(screenW, lines[3], snrStr); + } + + // Hops + if (nodeInfo.hopsAway !== undefined) { + const hopStr = + nodeInfo.hopsAway === 0 ? "Direct" : `${nodeInfo.hopsAway} hops`; + display.setTextAlignment(TEXT_ALIGN_LEFT); + display.drawString(0, lines[4], hopStr); + } + + display.setTextAlignment(TEXT_ALIGN_LEFT); +} + +/** + * Draw a message screen + */ +export function drawMessageScreen( + display: OLEDDisplay, + message: { + from: string; + text: string; + time: string; + channel?: string; + }, +): void { + display.clear(); + + // Draw header with channel name if provided + const headerTitle = message.channel || "Message"; + drawCommonHeader(display, 0, 0, headerTitle, { + inverted: true, + batteryPercent: 72, + hasUnreadMessage: true, + }); + + display.setFont(Font_6x8); + display.setColor(WHITE); + display.setTextAlignment(TEXT_ALIGN_LEFT); + + const lines = getTextPositions(display); + const screenW = display.getWidth(); + + // From line + display.drawString(0, lines[1], `From: ${message.from}`); + + // Time on right + display.setTextAlignment(TEXT_ALIGN_RIGHT); + display.drawString(screenW, lines[1], message.time); + + // Message text (with word wrap) + display.setTextAlignment(TEXT_ALIGN_LEFT); + display.drawStringMaxWidth(0, lines[2], screenW, message.text); +} + +/** + * Draw a GPS/location screen + */ +export function drawGPSScreen( + display: OLEDDisplay, + gpsInfo: { + latitude?: number; + longitude?: number; + altitude?: number; + satellites?: number; + hasLock: boolean; + speed?: number; + }, +): void { + display.clear(); + + drawCommonHeader(display, 0, 0, "GPS", { + inverted: true, + batteryPercent: 90, + }); + + display.setFont(Font_6x8); + display.setColor(WHITE); + display.setTextAlignment(TEXT_ALIGN_LEFT); + + const lines = getTextPositions(display); + + // Draw satellite icon and count + display.drawXbm( + 0, + lines[1], + images.imgSatellite_width, + images.imgSatellite_height, + images.imgSatellite, + ); + + if (!gpsInfo.hasLock) { + display.drawString(12, lines[1], "No Lock"); + } else { + display.drawString(12, lines[1], `${gpsInfo.satellites || 0} sats`); + } + + if ( + gpsInfo.hasLock && + gpsInfo.latitude !== undefined && + gpsInfo.longitude !== undefined + ) { + // Coordinates + display.drawString(0, lines[2], `Lat: ${gpsInfo.latitude.toFixed(6)}`); + display.drawString(0, lines[3], `Lon: ${gpsInfo.longitude.toFixed(6)}`); + + // Altitude + if (gpsInfo.altitude !== undefined) { + display.drawString(0, lines[4], `Alt: ${gpsInfo.altitude.toFixed(0)}m`); + } + + // Speed + if (gpsInfo.speed !== undefined) { + const screenW = display.getWidth(); + display.setTextAlignment(TEXT_ALIGN_RIGHT); + display.drawString(screenW, lines[4], `${gpsInfo.speed.toFixed(1)} km/h`); + } + } + + display.setTextAlignment(TEXT_ALIGN_LEFT); +} + +/** + * Draw a node list screen + */ +export function drawNodeListScreen( + display: OLEDDisplay, + nodes: Array<{ + shortName: string; + longName: string; + lastHeard: string; + snr?: number; + }>, + selectedIndex: number = 0, +): void { + display.clear(); + + drawCommonHeader(display, 0, 0, `Nodes (${nodes.length})`, { + inverted: true, + batteryPercent: 80, + }); + + display.setFont(Font_6x8); + display.setColor(WHITE); + display.setTextAlignment(TEXT_ALIGN_LEFT); + + const lines = getTextPositions(display); + const screenW = display.getWidth(); + const maxVisibleNodes = 4; + const startIndex = Math.max( + 0, + selectedIndex - Math.floor(maxVisibleNodes / 2), + ); + + for (let i = 0; i < maxVisibleNodes && startIndex + i < nodes.length; i++) { + const node = nodes[startIndex + i]; + const lineY = lines[i + 1]; + const isSelected = startIndex + i === selectedIndex; + + // Highlight selected row + if (isSelected) { + display.fillRect(0, lineY - 1, screenW, FONT_HEIGHT_SMALL); + display.setColor(BLACK); + } + + // Node short name + display.drawString(0, lineY, node.shortName); + + // Last heard on right + display.setTextAlignment(TEXT_ALIGN_RIGHT); + display.drawString(screenW, lineY, node.lastHeard); + display.setTextAlignment(TEXT_ALIGN_LEFT); + + if (isSelected) { + display.setColor(WHITE); + } + } +} + +/** + * Draw a system info screen + */ +export function drawSystemScreen( + display: OLEDDisplay, + sysInfo: { + uptime: string; + channelUtil: number; + airUtil: number; + batteryVoltage?: number; + nodes: number; + freeMemory?: number; + }, +): void { + display.clear(); + + drawCommonHeader(display, 0, 0, "System", { + inverted: true, + batteryPercent: 95, + }); + + display.setFont(Font_6x8); + display.setColor(WHITE); + display.setTextAlignment(TEXT_ALIGN_LEFT); + + const lines = getTextPositions(display); + const screenW = display.getWidth(); + + // Uptime + display.drawString(0, lines[1], `Uptime: ${sysInfo.uptime}`); + + // Channel utilization + display.drawString(0, lines[2], `ChUtil: ${sysInfo.channelUtil.toFixed(1)}%`); + + // Air utilization + display.setTextAlignment(TEXT_ALIGN_RIGHT); + display.drawString( + screenW, + lines[2], + `AirTx: ${sysInfo.airUtil.toFixed(1)}%`, + ); + display.setTextAlignment(TEXT_ALIGN_LEFT); + + // Nodes + display.drawString(0, lines[3], `Nodes: ${sysInfo.nodes}`); + + // Battery voltage + if (sysInfo.batteryVoltage !== undefined) { + display.setTextAlignment(TEXT_ALIGN_RIGHT); + display.drawString( + screenW, + lines[3], + `${sysInfo.batteryVoltage.toFixed(2)}V`, + ); + } + + // Free memory + if (sysInfo.freeMemory !== undefined) { + display.setTextAlignment(TEXT_ALIGN_LEFT); + display.drawString( + 0, + lines[4], + `Free: ${(sysInfo.freeMemory / 1024).toFixed(0)}KB`, + ); + } + + display.setTextAlignment(TEXT_ALIGN_LEFT); +} + +/** + * Draw a compass screen with bearing arrow + */ +export function drawCompassScreen( + display: OLEDDisplay, + compassInfo: { + heading: number; // Device heading in degrees + bearing: number; // Bearing to target in degrees + distance?: number; // Distance to target in meters + targetName?: string; + }, +): void { + display.clear(); + + drawCommonHeader(display, 0, 0, compassInfo.targetName || "Compass", { + inverted: true, + batteryPercent: 70, + }); + + display.setFont(Font_6x8); + display.setColor(WHITE); + + const screenW = display.getWidth(); + const screenH = display.getHeight(); + + // Compass center and radius + const compassRadius = Math.min(screenW, screenH - 20) / 2 - 4; + const compassX = screenW / 2; + const compassY = (screenH + 16) / 2; + + // Draw compass circle + display.drawCircle(compassX, compassY, compassRadius); + + // Draw cardinal directions + display.setTextAlignment(TEXT_ALIGN_CENTER); + display.drawString(compassX, compassY - compassRadius - 10, "N"); + + // Calculate relative bearing (bearing - heading) + const relativeBearing = + ((compassInfo.bearing - compassInfo.heading) * Math.PI) / 180; + + // Draw bearing arrow + const arrowLength = compassRadius - 6; + const arrowX = compassX + Math.sin(relativeBearing) * arrowLength; + const arrowY = compassY - Math.cos(relativeBearing) * arrowLength; + + display.drawLine(compassX, compassY, arrowX, arrowY); + + // Draw arrowhead + const headAngle = 0.4; + const headLength = 8; + const angle1 = relativeBearing + Math.PI - headAngle; + const angle2 = relativeBearing + Math.PI + headAngle; + display.drawLine( + arrowX, + arrowY, + arrowX + Math.sin(angle1) * headLength, + arrowY - Math.cos(angle1) * headLength, + ); + display.drawLine( + arrowX, + arrowY, + arrowX + Math.sin(angle2) * headLength, + arrowY - Math.cos(angle2) * headLength, + ); + + // Draw distance at bottom + if (compassInfo.distance !== undefined) { + let distStr: string; + if (compassInfo.distance >= 1000) { + distStr = `${(compassInfo.distance / 1000).toFixed(1)} km`; + } else { + distStr = `${Math.round(compassInfo.distance)} m`; + } + display.drawString(compassX, screenH - FONT_HEIGHT_SMALL, distStr); + } + + display.setTextAlignment(TEXT_ALIGN_LEFT); +} + +/** + * Draw a progress/loading screen + */ +export function drawProgressScreen( + display: OLEDDisplay, + title: string, + progress: number, + statusText?: string, +): void { + display.clear(); + display.setFont(Font_6x8); + display.setColor(WHITE); + display.setTextAlignment(TEXT_ALIGN_CENTER); + + const screenW = display.getWidth(); + const screenH = display.getHeight(); + const centerX = screenW / 2; + const centerY = screenH / 2; + + // Title + display.drawString(centerX, centerY - 20, title); + + // Progress bar + const barWidth = screenW - 20; + const barHeight = 10; + const barX = 10; + const barY = centerY - barHeight / 2; + + display.drawProgressBar(barX, barY, barWidth, barHeight, progress); + + // Progress percentage + display.drawString(centerX, barY + barHeight + 4, `${Math.round(progress)}%`); + + // Status text + if (statusText) { + display.drawString(centerX, screenH - FONT_HEIGHT_SMALL - 2, statusText); + } + + display.setTextAlignment(TEXT_ALIGN_LEFT); +} diff --git a/oled-emulator/src/SimpleFonts.ts b/oled-emulator/src/SimpleFonts.ts new file mode 100644 index 000000000..f3629fa82 --- /dev/null +++ b/oled-emulator/src/SimpleFonts.ts @@ -0,0 +1,421 @@ +/** + * SimpleFonts.ts - Simple working bitmap fonts for OLED emulator + * + * These are basic monospace bitmap fonts that are easy to render correctly. + * Format: Each character is stored in column-major order, 1 byte per column. + * For fonts taller than 8px, multiple bytes per column are used. + */ + +import type { OLEDFont } from "./OLEDDisplay"; + +/** + * Simple 6x8 monospace font + * Each character is 6 columns wide, 8 rows tall + * 1 byte per column (8 vertical pixels) + * Includes ASCII 32-126 + */ +export const Font_6x8: OLEDFont = { + height: 8, + firstChar: 32, + charCount: 95, + widths: new Array(95).fill(6), // Fixed width font + data: new Uint8Array([ + // Space (32) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // ! (33) + 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, + // " (34) + 0x00, 0x07, 0x00, 0x07, 0x00, 0x00, + // # (35) + 0x14, 0x7f, 0x14, 0x7f, 0x14, 0x00, + // $ (36) + 0x24, 0x2a, 0x7f, 0x2a, 0x12, 0x00, + // % (37) + 0x23, 0x13, 0x08, 0x64, 0x62, 0x00, + // & (38) + 0x36, 0x49, 0x55, 0x22, 0x50, 0x00, + // ' (39) + 0x00, 0x05, 0x03, 0x00, 0x00, 0x00, + // ( (40) + 0x00, 0x1c, 0x22, 0x41, 0x00, 0x00, + // ) (41) + 0x00, 0x41, 0x22, 0x1c, 0x00, 0x00, + // * (42) + 0x08, 0x2a, 0x1c, 0x2a, 0x08, 0x00, + // + (43) + 0x08, 0x08, 0x3e, 0x08, 0x08, 0x00, + // , (44) + 0x00, 0x50, 0x30, 0x00, 0x00, 0x00, + // - (45) + 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, + // . (46) + 0x00, 0x60, 0x60, 0x00, 0x00, 0x00, + // / (47) + 0x20, 0x10, 0x08, 0x04, 0x02, 0x00, + // 0 (48) + 0x3e, 0x51, 0x49, 0x45, 0x3e, 0x00, + // 1 (49) + 0x00, 0x42, 0x7f, 0x40, 0x00, 0x00, + // 2 (50) + 0x42, 0x61, 0x51, 0x49, 0x46, 0x00, + // 3 (51) + 0x21, 0x41, 0x45, 0x4b, 0x31, 0x00, + // 4 (52) + 0x18, 0x14, 0x12, 0x7f, 0x10, 0x00, + // 5 (53) + 0x27, 0x45, 0x45, 0x45, 0x39, 0x00, + // 6 (54) + 0x3c, 0x4a, 0x49, 0x49, 0x30, 0x00, + // 7 (55) + 0x01, 0x71, 0x09, 0x05, 0x03, 0x00, + // 8 (56) + 0x36, 0x49, 0x49, 0x49, 0x36, 0x00, + // 9 (57) + 0x06, 0x49, 0x49, 0x29, 0x1e, 0x00, + // : (58) + 0x00, 0x36, 0x36, 0x00, 0x00, 0x00, + // ; (59) + 0x00, 0x56, 0x36, 0x00, 0x00, 0x00, + // < (60) + 0x00, 0x08, 0x14, 0x22, 0x41, 0x00, + // = (61) + 0x14, 0x14, 0x14, 0x14, 0x14, 0x00, + // > (62) + 0x41, 0x22, 0x14, 0x08, 0x00, 0x00, + // ? (63) + 0x02, 0x01, 0x51, 0x09, 0x06, 0x00, + // @ (64) + 0x32, 0x49, 0x79, 0x41, 0x3e, 0x00, + // A (65) + 0x7e, 0x11, 0x11, 0x11, 0x7e, 0x00, + // B (66) + 0x7f, 0x49, 0x49, 0x49, 0x36, 0x00, + // C (67) + 0x3e, 0x41, 0x41, 0x41, 0x22, 0x00, + // D (68) + 0x7f, 0x41, 0x41, 0x22, 0x1c, 0x00, + // E (69) + 0x7f, 0x49, 0x49, 0x49, 0x41, 0x00, + // F (70) + 0x7f, 0x09, 0x09, 0x01, 0x01, 0x00, + // G (71) + 0x3e, 0x41, 0x41, 0x51, 0x32, 0x00, + // H (72) + 0x7f, 0x08, 0x08, 0x08, 0x7f, 0x00, + // I (73) + 0x00, 0x41, 0x7f, 0x41, 0x00, 0x00, + // J (74) + 0x20, 0x40, 0x41, 0x3f, 0x01, 0x00, + // K (75) + 0x7f, 0x08, 0x14, 0x22, 0x41, 0x00, + // L (76) + 0x7f, 0x40, 0x40, 0x40, 0x40, 0x00, + // M (77) + 0x7f, 0x02, 0x04, 0x02, 0x7f, 0x00, + // N (78) + 0x7f, 0x04, 0x08, 0x10, 0x7f, 0x00, + // O (79) + 0x3e, 0x41, 0x41, 0x41, 0x3e, 0x00, + // P (80) + 0x7f, 0x09, 0x09, 0x09, 0x06, 0x00, + // Q (81) + 0x3e, 0x41, 0x51, 0x21, 0x5e, 0x00, + // R (82) + 0x7f, 0x09, 0x19, 0x29, 0x46, 0x00, + // S (83) + 0x46, 0x49, 0x49, 0x49, 0x31, 0x00, + // T (84) + 0x01, 0x01, 0x7f, 0x01, 0x01, 0x00, + // U (85) + 0x3f, 0x40, 0x40, 0x40, 0x3f, 0x00, + // V (86) + 0x1f, 0x20, 0x40, 0x20, 0x1f, 0x00, + // W (87) + 0x7f, 0x20, 0x18, 0x20, 0x7f, 0x00, + // X (88) + 0x63, 0x14, 0x08, 0x14, 0x63, 0x00, + // Y (89) + 0x03, 0x04, 0x78, 0x04, 0x03, 0x00, + // Z (90) + 0x61, 0x51, 0x49, 0x45, 0x43, 0x00, + // [ (91) + 0x00, 0x00, 0x7f, 0x41, 0x41, 0x00, + // \ (92) + 0x02, 0x04, 0x08, 0x10, 0x20, 0x00, + // ] (93) + 0x41, 0x41, 0x7f, 0x00, 0x00, 0x00, + // ^ (94) + 0x04, 0x02, 0x01, 0x02, 0x04, 0x00, + // _ (95) + 0x40, 0x40, 0x40, 0x40, 0x40, 0x00, + // ` (96) + 0x00, 0x01, 0x02, 0x04, 0x00, 0x00, + // a (97) + 0x20, 0x54, 0x54, 0x54, 0x78, 0x00, + // b (98) + 0x7f, 0x48, 0x44, 0x44, 0x38, 0x00, + // c (99) + 0x38, 0x44, 0x44, 0x44, 0x20, 0x00, + // d (100) + 0x38, 0x44, 0x44, 0x48, 0x7f, 0x00, + // e (101) + 0x38, 0x54, 0x54, 0x54, 0x18, 0x00, + // f (102) + 0x08, 0x7e, 0x09, 0x01, 0x02, 0x00, + // g (103) + 0x08, 0x14, 0x54, 0x54, 0x3c, 0x00, + // h (104) + 0x7f, 0x08, 0x04, 0x04, 0x78, 0x00, + // i (105) + 0x00, 0x44, 0x7d, 0x40, 0x00, 0x00, + // j (106) + 0x20, 0x40, 0x44, 0x3d, 0x00, 0x00, + // k (107) + 0x00, 0x7f, 0x10, 0x28, 0x44, 0x00, + // l (108) + 0x00, 0x41, 0x7f, 0x40, 0x00, 0x00, + // m (109) + 0x7c, 0x04, 0x18, 0x04, 0x78, 0x00, + // n (110) + 0x7c, 0x08, 0x04, 0x04, 0x78, 0x00, + // o (111) + 0x38, 0x44, 0x44, 0x44, 0x38, 0x00, + // p (112) + 0x7c, 0x14, 0x14, 0x14, 0x08, 0x00, + // q (113) + 0x08, 0x14, 0x14, 0x18, 0x7c, 0x00, + // r (114) + 0x7c, 0x08, 0x04, 0x04, 0x08, 0x00, + // s (115) + 0x48, 0x54, 0x54, 0x54, 0x20, 0x00, + // t (116) + 0x04, 0x3f, 0x44, 0x40, 0x20, 0x00, + // u (117) + 0x3c, 0x40, 0x40, 0x20, 0x7c, 0x00, + // v (118) + 0x1c, 0x20, 0x40, 0x20, 0x1c, 0x00, + // w (119) + 0x3c, 0x40, 0x30, 0x40, 0x3c, 0x00, + // x (120) + 0x44, 0x28, 0x10, 0x28, 0x44, 0x00, + // y (121) + 0x0c, 0x50, 0x50, 0x50, 0x3c, 0x00, + // z (122) + 0x44, 0x64, 0x54, 0x4c, 0x44, 0x00, + // { (123) + 0x00, 0x08, 0x36, 0x41, 0x00, 0x00, + // | (124) + 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, + // } (125) + 0x00, 0x41, 0x36, 0x08, 0x00, 0x00, + // ~ (126) + 0x08, 0x08, 0x2a, 0x1c, 0x08, 0x00, + ]), +}; + +/** + * Simple 5x7 monospace font (more compact) + * Each character is 5 columns wide, 7 rows tall + * 1 byte per column + */ +export const Font_5x7: OLEDFont = { + height: 7, + firstChar: 32, + charCount: 95, + widths: new Array(95).fill(5), + data: new Uint8Array([ + // Space (32) + 0x00, 0x00, 0x00, 0x00, 0x00, + // ! (33) + 0x00, 0x00, 0x2f, 0x00, 0x00, + // " (34) + 0x00, 0x07, 0x00, 0x07, 0x00, + // # (35) + 0x14, 0x3e, 0x14, 0x3e, 0x14, + // $ (36) + 0x24, 0x2a, 0x7f, 0x2a, 0x12, + // % (37) + 0x23, 0x13, 0x08, 0x64, 0x62, + // & (38) + 0x36, 0x49, 0x55, 0x22, 0x50, + // ' (39) + 0x00, 0x05, 0x03, 0x00, 0x00, + // ( (40) + 0x00, 0x1c, 0x22, 0x41, 0x00, + // ) (41) + 0x00, 0x41, 0x22, 0x1c, 0x00, + // * (42) + 0x14, 0x08, 0x3e, 0x08, 0x14, + // + (43) + 0x08, 0x08, 0x3e, 0x08, 0x08, + // , (44) + 0x00, 0x50, 0x30, 0x00, 0x00, + // - (45) + 0x08, 0x08, 0x08, 0x08, 0x08, + // . (46) + 0x00, 0x30, 0x30, 0x00, 0x00, + // / (47) + 0x20, 0x10, 0x08, 0x04, 0x02, + // 0 (48) + 0x3e, 0x51, 0x49, 0x45, 0x3e, + // 1 (49) + 0x00, 0x42, 0x7f, 0x40, 0x00, + // 2 (50) + 0x42, 0x61, 0x51, 0x49, 0x46, + // 3 (51) + 0x21, 0x41, 0x45, 0x4b, 0x31, + // 4 (52) + 0x18, 0x14, 0x12, 0x7f, 0x10, + // 5 (53) + 0x27, 0x45, 0x45, 0x45, 0x39, + // 6 (54) + 0x3c, 0x4a, 0x49, 0x49, 0x30, + // 7 (55) + 0x01, 0x71, 0x09, 0x05, 0x03, + // 8 (56) + 0x36, 0x49, 0x49, 0x49, 0x36, + // 9 (57) + 0x06, 0x49, 0x49, 0x29, 0x1e, + // : (58) + 0x00, 0x36, 0x36, 0x00, 0x00, + // ; (59) + 0x00, 0x56, 0x36, 0x00, 0x00, + // < (60) + 0x08, 0x14, 0x22, 0x41, 0x00, + // = (61) + 0x14, 0x14, 0x14, 0x14, 0x14, + // > (62) + 0x00, 0x41, 0x22, 0x14, 0x08, + // ? (63) + 0x02, 0x01, 0x51, 0x09, 0x06, + // @ (64) + 0x32, 0x49, 0x79, 0x41, 0x3e, + // A (65) + 0x7e, 0x11, 0x11, 0x11, 0x7e, + // B (66) + 0x7f, 0x49, 0x49, 0x49, 0x36, + // C (67) + 0x3e, 0x41, 0x41, 0x41, 0x22, + // D (68) + 0x7f, 0x41, 0x41, 0x22, 0x1c, + // E (69) + 0x7f, 0x49, 0x49, 0x49, 0x41, + // F (70) + 0x7f, 0x09, 0x09, 0x09, 0x01, + // G (71) + 0x3e, 0x41, 0x49, 0x49, 0x7a, + // H (72) + 0x7f, 0x08, 0x08, 0x08, 0x7f, + // I (73) + 0x00, 0x41, 0x7f, 0x41, 0x00, + // J (74) + 0x20, 0x40, 0x41, 0x3f, 0x01, + // K (75) + 0x7f, 0x08, 0x14, 0x22, 0x41, + // L (76) + 0x7f, 0x40, 0x40, 0x40, 0x40, + // M (77) + 0x7f, 0x02, 0x0c, 0x02, 0x7f, + // N (78) + 0x7f, 0x04, 0x08, 0x10, 0x7f, + // O (79) + 0x3e, 0x41, 0x41, 0x41, 0x3e, + // P (80) + 0x7f, 0x09, 0x09, 0x09, 0x06, + // Q (81) + 0x3e, 0x41, 0x51, 0x21, 0x5e, + // R (82) + 0x7f, 0x09, 0x19, 0x29, 0x46, + // S (83) + 0x46, 0x49, 0x49, 0x49, 0x31, + // T (84) + 0x01, 0x01, 0x7f, 0x01, 0x01, + // U (85) + 0x3f, 0x40, 0x40, 0x40, 0x3f, + // V (86) + 0x1f, 0x20, 0x40, 0x20, 0x1f, + // W (87) + 0x3f, 0x40, 0x38, 0x40, 0x3f, + // X (88) + 0x63, 0x14, 0x08, 0x14, 0x63, + // Y (89) + 0x07, 0x08, 0x70, 0x08, 0x07, + // Z (90) + 0x61, 0x51, 0x49, 0x45, 0x43, + // [ (91) + 0x00, 0x7f, 0x41, 0x41, 0x00, + // \ (92) + 0x02, 0x04, 0x08, 0x10, 0x20, + // ] (93) + 0x00, 0x41, 0x41, 0x7f, 0x00, + // ^ (94) + 0x04, 0x02, 0x01, 0x02, 0x04, + // _ (95) + 0x40, 0x40, 0x40, 0x40, 0x40, + // ` (96) + 0x00, 0x01, 0x02, 0x04, 0x00, + // a (97) + 0x20, 0x54, 0x54, 0x54, 0x78, + // b (98) + 0x7f, 0x48, 0x44, 0x44, 0x38, + // c (99) + 0x38, 0x44, 0x44, 0x44, 0x20, + // d (100) + 0x38, 0x44, 0x44, 0x48, 0x7f, + // e (101) + 0x38, 0x54, 0x54, 0x54, 0x18, + // f (102) + 0x08, 0x7e, 0x09, 0x01, 0x02, + // g (103) + 0x0c, 0x52, 0x52, 0x52, 0x3e, + // h (104) + 0x7f, 0x08, 0x04, 0x04, 0x78, + // i (105) + 0x00, 0x44, 0x7d, 0x40, 0x00, + // j (106) + 0x20, 0x40, 0x44, 0x3d, 0x00, + // k (107) + 0x7f, 0x10, 0x28, 0x44, 0x00, + // l (108) + 0x00, 0x41, 0x7f, 0x40, 0x00, + // m (109) + 0x7c, 0x04, 0x18, 0x04, 0x78, + // n (110) + 0x7c, 0x08, 0x04, 0x04, 0x78, + // o (111) + 0x38, 0x44, 0x44, 0x44, 0x38, + // p (112) + 0x7c, 0x14, 0x14, 0x14, 0x08, + // q (113) + 0x08, 0x14, 0x14, 0x18, 0x7c, + // r (114) + 0x7c, 0x08, 0x04, 0x04, 0x08, + // s (115) + 0x48, 0x54, 0x54, 0x54, 0x20, + // t (116) + 0x04, 0x3f, 0x44, 0x40, 0x20, + // u (117) + 0x3c, 0x40, 0x40, 0x20, 0x7c, + // v (118) + 0x1c, 0x20, 0x40, 0x20, 0x1c, + // w (119) + 0x3c, 0x40, 0x30, 0x40, 0x3c, + // x (120) + 0x44, 0x28, 0x10, 0x28, 0x44, + // y (121) + 0x0c, 0x50, 0x50, 0x50, 0x3c, + // z (122) + 0x44, 0x64, 0x54, 0x4c, 0x44, + // { (123) + 0x00, 0x08, 0x36, 0x41, 0x00, + // | (124) + 0x00, 0x00, 0x7f, 0x00, 0x00, + // } (125) + 0x00, 0x41, 0x36, 0x08, 0x00, + // ~ (126) + 0x10, 0x08, 0x08, 0x10, 0x08, + ]), +}; + +// Alias for compatibility +export const SimpleFont = Font_6x8; diff --git a/oled-emulator/src/index.ts b/oled-emulator/src/index.ts new file mode 100644 index 000000000..44de34217 --- /dev/null +++ b/oled-emulator/src/index.ts @@ -0,0 +1,50 @@ +/** + * Meshtastic OLED Emulator + * + * A Vue.js component and TypeScript library for emulating SSD1306/SH1106 OLED displays. + * Provides the same OLEDDisplay API as the Meshtastic firmware, enabling + * screen development and testing in the browser. + */ + +// Core display class +export { + OLEDDisplay, + type OLEDDISPLAY_GEOMETRY, + type OLEDDISPLAY_COLOR, + type OLEDDISPLAY_TEXT_ALIGNMENT, + type OLEDFont, +} from "./OLEDDisplay"; +export { + WHITE, + BLACK, + INVERSE, + TEXT_ALIGN_LEFT, + TEXT_ALIGN_CENTER, + TEXT_ALIGN_RIGHT, +} from "./OLEDDisplay"; + +// Fonts +export { + ArialMT_Plain_10, + ArialMT_Plain_16, + FONT_HEIGHT_SMALL, + FONT_HEIGHT_MEDIUM, + FONT_HEIGHT_LARGE, + createFontFromBytes, +} from "./OLEDFonts"; + +// Simple working fonts (recommended) +export { Font_6x8, Font_5x7, SimpleFont } from "./SimpleFonts"; + +// Images and icons +export * as OLEDImages from "./OLEDImages"; + +// Screen renderers (firmware UI ports) +export * as ScreenRenderers from "./ScreenRenderers"; + +// Vue component +export { default as OLEDEmulator } from "./OLEDEmulator.vue"; +export { DISPLAY_PRESETS, type DisplayPreset } from "./DisplayPresets"; + +// Demo component +export { default as OLEDEmulatorDemo } from "./Demo.vue"; diff --git a/oled-emulator/src/main.ts b/oled-emulator/src/main.ts new file mode 100644 index 000000000..6f2bfadfa --- /dev/null +++ b/oled-emulator/src/main.ts @@ -0,0 +1,4 @@ +import { createApp } from "vue"; +import Demo from "./Demo.vue"; + +createApp(Demo).mount("#app"); diff --git a/oled-emulator/tsconfig.json b/oled-emulator/tsconfig.json new file mode 100644 index 000000000..881b6989a --- /dev/null +++ b/oled-emulator/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + /* Vue */ + "types": ["vite/client"] + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], + "exclude": ["node_modules", "dist"] +} diff --git a/oled-emulator/vite.config.ts b/oled-emulator/vite.config.ts new file mode 100644 index 000000000..2a5b862d6 --- /dev/null +++ b/oled-emulator/vite.config.ts @@ -0,0 +1,35 @@ +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; +import dts from "vite-plugin-dts"; +import { resolve } from "path"; + +export default defineConfig({ + plugins: [ + vue(), + dts({ + insertTypesEntry: true, + }), + ], + build: { + lib: { + entry: resolve(__dirname, "src/index.ts"), + name: "MeshtasticOLEDEmulator", + formats: ["es", "cjs"], + fileName: (format) => `index.${format === "es" ? "mjs" : "js"}`, + }, + rollupOptions: { + external: ["vue"], + output: { + globals: { + vue: "Vue", + }, + }, + }, + cssCodeSplit: false, + }, + resolve: { + alias: { + "@": resolve(__dirname, "src"), + }, + }, +});