diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2d98378 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{rs,gd}] +indent_size = 4 + +[*.gd] +indent_style = tab diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c55f038 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,60 @@ +name: CI + +on: + push: + branches: [master, v2] + pull_request: + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v7 + with: + node-version-file: .node-version + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm check + - run: pnpm build + - run: pnpm exec playwright install --with-deps chromium webkit + - run: pnpm test:e2e + - uses: actions/upload-artifact@v7 + if: failure() + with: + name: playwright-report + path: | + test-results/ + tests/fixtures/expected/*.actual.png + tests/fixtures/expected/*.diff.png + retention-days: 7 + + desktop: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v7 + - name: Linux build dependencies + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v7 + with: + node-version-file: .node-version + cache: pnpm + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + - uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri -> target + - run: pnpm install --frozen-lockfile + # the Rust build embeds ../dist + - run: pnpm build + - name: cargo fmt, clippy and test + working-directory: src-tauri + run: | + cargo fmt --check + cargo clippy --locked -- -D warnings + cargo test --locked diff --git a/.github/workflows/itch.yml b/.github/workflows/itch.yml new file mode 100644 index 0000000..f3aa4e0 --- /dev/null +++ b/.github/workflows/itch.yml @@ -0,0 +1,54 @@ +name: itch.io + +# Optional: pushes a published release and the web version to itch.io with butler. Inactive until the repository +# has the variable ITCH_GAME (e.g. `route1rodent/autotiler`) and the secret BUTLER_API_KEY (`butler login`, or +# https://itch.io/user/settings/api-keys). + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: Release tag to push, e.g. v2.0.0 + required: true + +jobs: + push: + if: vars.ITCH_GAME != '' + runs-on: ubuntu-latest + env: + TAG: ${{ github.event.release.tag_name || inputs.tag }} + ITCH_GAME: ${{ vars.ITCH_GAME }} + BUTLER_API_KEY: ${{ secrets.BUTLER_API_KEY }} + GH_TOKEN: ${{ github.token }} + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ env.TAG }} + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v7 + with: + node-version-file: .node-version + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm build + - name: Download the release bundles + run: | + mkdir -p itch/macos itch/windows itch/linux + gh release download "$TAG" --dir itch/macos --pattern '*_universal.dmg' + gh release download "$TAG" --dir itch/windows --pattern '*-setup.exe' + gh release download "$TAG" --dir itch/linux --pattern '*.AppImage' + - name: Install butler + run: | + curl -fsSL -o butler.zip https://broth.itch.zone/butler/linux-amd64/LATEST/archive/default + unzip -q butler.zip -d "$RUNNER_TEMP/butler" + chmod +x "$RUNNER_TEMP/butler/butler" + echo "$RUNNER_TEMP/butler" >> "$GITHUB_PATH" + - name: Push to itch.io + run: | + version="${TAG#v}" + butler push dist "$ITCH_GAME:html5" --userversion "$version" + butler push itch/macos "$ITCH_GAME:macos-universal" --userversion "$version" + butler push itch/windows "$ITCH_GAME:windows" --userversion "$version" + butler push itch/linux "$ITCH_GAME:linux-universal" --userversion "$version" diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..3599b21 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,44 @@ +name: Pages + +# Publishes the web version to GitHub Pages (repository settings → Pages → Source: GitHub Actions). + +on: + push: + branches: [master] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v7 + with: + node-version-file: .node-version + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm build + - uses: actions/configure-pages@v6 + - uses: actions/upload-pages-artifact@v5 + with: + path: dist + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..edee373 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,70 @@ +name: Release + +# Builds the desktop apps and attaches them to a *draft* GitHub Release named after the version in package.json. +# Push a tag (`git tag v2.0.0 && git push origin v2.0.0`) or run it by hand, then review and publish the draft. + +on: + push: + tags: ["v*"] + workflow_dispatch: + +permissions: + contents: write + +jobs: + desktop: + strategy: + fail-fast: false + matrix: + include: + - os: macos-latest + args: --target universal-apple-darwin + rust-targets: aarch64-apple-darwin,x86_64-apple-darwin + - os: windows-latest + args: "" + rust-targets: "" + # the oldest supported Ubuntu keeps the glibc requirement of the Linux bundles low + - os: ubuntu-22.04 + args: "" + rust-targets: "" + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v7 + - name: Linux build dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v7 + with: + node-version-file: .node-version + cache: pnpm + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.rust-targets }} + - uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri -> target + - run: pnpm install --frozen-lockfile + - uses: tauri-apps/tauri-action@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # macOS apps are ad-hoc signed (`signingIdentity: "-"`). With an Apple Developer account, add these + # secrets to sign and notarize instead: APPLE_CERTIFICATE, APPLE_CERTIFICATE_PASSWORD, + # APPLE_SIGNING_IDENTITY, APPLE_API_ISSUER, APPLE_API_KEY, APPLE_API_KEY_PATH. + with: + tagName: v__VERSION__ + releaseName: Autotiler v__VERSION__ + releaseBody: | + See [CHANGELOG.md](https://github.com/itsjavi/autotiler/blob/master/CHANGELOG.md) for what's new. + + - **macOS** (Apple Silicon and Intel, macOS 13+): `Autotiler_*_universal.dmg`. The app isn't notarized: + the first time, open it, then allow it in *System Settings → Privacy & Security → Open Anyway*. + - **Windows**: `Autotiler_*_x64-setup.exe` (or the `.msi`). + - **Linux**: `.AppImage`, `.deb` or `.rpm`. + - **No install**: the web version at https://itsjavi.github.io/autotiler/ + releaseDraft: true + prerelease: false + uploadUpdaterJson: false + args: ${{ matrix.args }} diff --git a/.gitignore b/.gitignore index f55c124..021ffbf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,27 +1,31 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - # dependencies node_modules -/.pnp -.pnp.js -# testing +# build output +/dist +/src-tauri/target +/src-tauri/gen + +# test output /coverage +/test-results +/playwright-report +/blob-report +/.vitest +/tests/fixtures/**/*.actual.png +/tests/fixtures/**/*.diff.png -# production -/build +# env +.env*.local -# misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local +# logs +*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -/dist -*~ +# OS / editors +.DS_Store Thumbs.db -*:encryptable \ No newline at end of file +*~ +*:encryptable + +# local research, notes and scratch files (not versioned) +/.local/ diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +24 diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 0000000..0554c8d --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,14 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "printWidth": 120, + "sortImports": true, + "sortTailwindcss": { "stylesheet": "./src/app/styles.css", "functions": ["cn"] }, + "ignorePatterns": [ + "dist/**", + "src-tauri/gen/**", + "src-tauri/target/**", + "tests/fixtures/**", + "public/**", + "**/__snapshots__/**" + ] +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..b135f6f --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,39 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["eslint", "typescript", "unicorn", "oxc", "react", "jsx-a11y", "import", "vitest"], + "categories": { "correctness": "error", "suspicious": "warn", "perf": "warn" }, + "env": { "browser": true, "es2024": true }, + "options": { "typeAware": true }, + "rules": { + "react/rules-of-hooks": "error", + "react/exhaustive-deps": "warn", + "react/react-in-jsx-scope": "off", + "import/no-unassigned-import": ["warn", { "allow": ["**/*.css"] }], + "typescript/no-floating-promises": "error", + "import/no-cycle": "error" + }, + "overrides": [ + { + "files": ["src/core/**"], + "rules": { + "no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": ["react", "react-dom", "react/*", "react-dom/*", "@tauri-apps/*", "node:*", "**/app/**"], + "message": "src/core must stay environment-free (no DOM, Node, React or Tauri)." + } + ] + } + ] + } + }, + { + "files": ["*.config.ts", "scripts/**", "tests/**", "src/**/*.test.ts"], + "env": { "node": true }, + "rules": { "no-restricted-imports": "off" } + } + ], + "ignorePatterns": ["dist/**", "src-tauri/**", "public/**", "**/__snapshots__/**"] +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..b3dd591 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "oxc.oxc-vscode", + "TypeScriptTeam.native-preview", + "rust-lang.rust-analyzer", + "tauri-apps.tauri-vscode" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..14a7484 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { "source.fixAll.oxc": "always" }, + "oxc.typeAware": true, + "js/ts.experimental.useTsgo": true, + "[rust]": { "editor.defaultFormatter": "rust-lang.rust-analyzer" } +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..be0d070 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,61 @@ +# Changelog + +## 2.0.0 (unreleased) + +A rewrite from scratch: a TypeScript engine with tests, a new UI, a small native desktop app and a web version. + +### Added + +- **Godot 4** export (#5): a TileSet with a terrain set, the peering bits of all 47 tiles and full-tile + collisions. It's verified by painting every terrain combination in headless Godot 4.7. An option forces nearest + filtering without touching the project settings. +- **Native Apple Silicon** support (#7): one universal macOS app for Apple Silicon and Intel. It's about 4 MB, + down from Electron's 100+ MB. +- **RPG Maker A2** templates as input, and conversion between the 13-tile and A2 templates. +- **Layouts:** Godot 3×3 minimal (12×4), GameMaker 47 (8×6) and a 16-tile **dual grid** for the TileMapDual + addon, next to v1's 11×5. +- **Exports:** Tiled tilesets with a Wang set, and a PNG with an optional JSON map of every tile's neighbours. +- A **test map** to paint with the tileset, fill randomly or fill with every combination. +- Overlays for the grid, quarters, peering bits and collisions, plus a tile inspector that also works from the + keyboard. +- Drag and drop, pasting from and copying to the clipboard, examples, blank colour-coded templates, keyboard + shortcuts and warnings for template problems (such as empty pieces). +- **Live updates:** export straight into a folder, re-read the template when it changes and optionally re-export. + When the folder is inside a Godot project, the app shows its `res://` path. +- Recent files and reopening the last file (desktop). +- A **web version** that needs no install. Chrome and Edge also get folder export and live updates. + +### Changed + +- The default layout is now Godot's 3×3 minimal (12×4), the one in the Godot docs and most tutorials. Pick + **Autotiler v1 (11×5)** to replace tilesets exported with v1. +- The tile size can be anything from 2 px (v1 needed multiples of 8), and it's detected from both dimensions. +- Godot 3 export is kept as _legacy_. +- The desktop app is built with Tauri 2 instead of Electron 9 and runs on macOS 13 or later, Windows 10 or later + and Linux. + +### Fixed + +- Transparent inner corners were filled in: the fill tile showed through their transparent pixels. +- Colours shifted in PNGs with colour-space chunks (`gAMA`, `cHRM`, `iCCP`), because v1 let the browser + colour-manage the pixels. v2 copies pixels exactly. +- Tiles in 1-tile-wide strips used some quarters from the wrong template tiles, which showed as notches at small + sizes (visible in v1's own 8 px example). +- The Godot 3 tile with all eight neighbours had no collision shape. +- Godot exports only worked when saved in the project root (hard-coded `res://` paths). Exports now work in any + folder. + +With the Autotiler v1 layout, v2 puts every pixel where v1 did, except where v1 was wrong (above). + +## 1.2.0 (2020-10-30) + +- Fixed paths on Windows; crash reporting. +- Later on `master`: a fix for out-of-bounds Godot autotile bitmasks (#4) and Electron 9.4. + +## 1.1.0 (2020-10-30) + +- Godot 3 export (a folder with the PNG, `.png.import` and `.tres`) and fixes for other tile sizes. + +## 1.0.1 (2020-08-07) + +- First public release: 47-tile blob tilesets from the 13-tile template. diff --git a/Makefile b/Makefile deleted file mode 100644 index 0244427..0000000 --- a/Makefile +++ /dev/null @@ -1,33 +0,0 @@ -default: build - -clean: - rm -rf dist/* - -start: - npm start - -build: - npm run dist - sleep 1 - rm -rf dist/autotiler-mac.app - cp -r dist/mac/Autotiler.app dist/autotiler-mac.app - -version-major: - npm version major - -version-minor: - npm version minor - -version-patch: - npm version patch - -version-fix: version-patch -version: version-minor - -release: build - node publish.js - -publish: release - -$(V).SILENT: -.PHONY: docs tests test build dist diff --git a/README.md b/README.md index ea32062..e8b3b55 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,137 @@ -# Autotiler +# Autotiler -Autotile tileset generator application with Godot export support. +Turn a small terrain template into a complete autotile tileset: the 47-tile blob set or a 16-tile dual grid, ready +for **Godot 4**, **Tiled** or any engine. Free, open source, and it runs offline. -Generates a full autotile tileset using a 15-tile image, including a -resource with configured collisions and bitmask ready to be used in Godot. +![Autotiler with an example template and its generated Godot 3×3 minimal tileset](docs/images/app.png) + +**Get it:** [desktop apps for macOS, Windows and Linux](https://github.com/itsjavi/autotiler/releases/latest) · +[web version](https://itsjavi.github.io/autotiler/) (nothing to install) · [itch.io](https://route1rodent.itch.io/autotiler) -![](img/preview.png) - ## Features -- Generates 42-tile Blob tilesets out of 15-tile Wang tilesets. -- Supports any tile size (multiples of 8) -- Godot Tileset resource export with configured region, bitmask, collisions and icon. +- **Two template formats in:** the Autotiler 13-tile template (5×3 tiles) and the RPG Maker A2 floor autotile + (2×3, as in VX Ace, MV and MZ). The template kind and tile size are detected from the image. Any tile size from + 2 px works (the dual grid needs an even size). +- **Four layouts out:** Godot 3×3 minimal (12×4, the layout from the Godot docs), Autotiler v1 (11×5), + GameMaker 47 (8×6) and a 16-tile dual grid (4×4, for the TileMapDual addon). +- **Exports:** + - **Godot 4:** a TileSet `.tres` with a terrain set, the peering bits of all 47 tiles and full-tile collisions. + The texture path is relative, so the files work in any folder of your project. + - **Tiled:** a `.tsx` tileset with a Wang set, for the terrain brush. + - **PNG:** optionally with a JSON map of each tile's neighbour mask, for custom engines. + - **Godot 3 (legacy):** an autotile with the 3×3 minimal bitmask. +- **Preview:** grid, quarter, peering-bit and collision overlays. An inspector shows each tile's neighbours and + bitmask (hover a tile, or focus the tileset and use the arrow keys). +- **Test map:** paint and erase with the tileset, or fill the map randomly or with every combination. Tiles are + picked the way Godot's terrain painter picks them. +- **Workflow:** open, drag and drop or paste the template. Copy the result back to the clipboard. The app also has + examples, blank colour-coded templates, and converts templates between 13-tile and RPG Maker A2. +- **Live updates:** in the desktop app (and in Chrome or Edge on the web) you can export straight into a folder. + Autotiler watches the template and re-exports when you save it, so an edit in Aseprite shows up in Godot. +- **Tested:** golden-image tests keep the output pixel-exact. The Godot 4 export is checked by painting every + terrain combination in headless Godot 4.7. + +## Templates + +Every output tile is assembled from four quarters. Each quarter shows one of five pieces, depending on its +neighbours. A template draws each piece at least once: + +| Autotiler 13-tile (5×3) | RPG Maker A2 (2×3) | +| ------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| ![Autotiler 13-tile template layout](docs/images/template-autotiler-13.png) | ![RPG Maker A2 template layout](docs/images/template-rpgmaker-a2.png) | +| A 3×3 island (outer corners, edges and fill) plus a 2×2 block whose centre holds the inner corners. The two bottom-right cells aren't used. | The top-left tile is RPG Maker's palette thumbnail and isn't used. The top-right tile holds the inner corners, and the 2×2 block below is the island. | + +Colours in the blank templates: 🟥 outer corner · 🟦 horizontal edge · 🟩 vertical edge · 🟨 inner corner · +⬜ fill. Download them from the app's **Blank template** menu, paint over them and open the result. The app dims +the cells it doesn't read, and it warns about empty pieces. + +## Using the tileset + +### Godot 4 + +1. Pick **Godot 4** and export into your project. In the desktop app, choose the folder once: the app shows its + `res://` path, and every later export is one click. +2. Select a TileMapLayer and load the `.tres` as its **Tile Set**. +3. In the **TileMap** panel, open **Terrains**, select the terrain and paint (Rect, Path or Connect mode). + +For crisp pixel art, set _Project Settings → Rendering → Textures → Canvas Textures → Default Texture Filter_ to +**Nearest**. Or turn on **Force nearest filtering** before exporting (Godot 4.2+). + +### Dual grid + +Pick the **Dual grid** layout and export a PNG for [TileMapDual](https://github.com/pablogila/TileMapDual). Its +tiles use TileMapDual's standard 4×4 order. The same layout exports to Tiled as a corner Wang set. + +### Tiled + +Add the exported `.tsx` to your map (_Map → Add External Tileset…_), then paint with the **Terrain Brush** and +the tileset's terrain set. + +### Other engines + +Export the **PNG** with **JSON tile map** turned on. For every tile, the JSON lists its position and its 8-bit +neighbour mask: N = 1, NE = 2, E = 4, SE = 8, S = 16, SW = 32, W = 64, NW = 128. A corner only counts when both +sides next to it are set. For the dual grid it lists the filled corners instead (TL = 1, TR = 2, BL = 4, BR = 8). + +### RPG Maker + +Open a 13-tile template, choose **Save as RPG Maker A2**, and paste the 2×3 block into an A2 slot of your +tileset. It works the other way too, if you want to take A2 art to Godot. -For roadmap and suggestions please [use this thread](https://github.com/itsjavi/autotiler/issues/1). +### Godot 3 +The **Godot 3** export writes the PNG, a `.png.import` with filtering off and an autotile `.tres` with the 3×3 +minimal bitmask and collisions. -## Usage +## Installing -### Basic instructions -Once you have the app installed and open, you only need to open a tileset with 13 tiles on the left side, arranged as shown in the screenshot (or as in the example images at the bottom). +- **macOS 13 or later** (one universal build for Apple Silicon and Intel). The app is ad-hoc signed but not + notarized, so macOS blocks it the first time. Open it once, then go to _System Settings → Privacy & Security_ and + click **Open Anyway**. On macOS 13 and 14 you can also right-click the app and choose **Open**. +- **Windows 10 and 11:** the `-setup.exe` or the `.msi`. Both need WebView2, which Windows 11 includes; the + installer adds it on Windows 10 when it's missing. +- **Linux:** the `.AppImage`, `.deb` or `.rpm`, with WebKitGTK 4.1. If the window stays blank on an NVIDIA GPU, + start the app with `WEBKIT_DISABLE_DMABUF_RENDERER=1`. +- **Web:** any current browser. Firefox and Safari download the files (several files come as a `.zip`), while + Chrome and Edge can also write into a folder and reload the template when it changes. -The tile size is detected automatically, even though you can manually edit it in the input. +## Development -Be sure that your image is made of 5 tiles wide and 3 tiles tall, regardless of the single tile size. +You need Node.js 24 or later and pnpm (the version is pinned in `package.json`; `corepack enable` picks it up). +The desktop app also needs Rust and the [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/) for +your OS. -After placing the image, the autotile tileset will be generated automatically and you can save the image using the save button. +```bash +pnpm install +pnpm dev # web app on http://localhost:5173 +pnpm desktop:dev # desktop app with hot reload +pnpm check # formatting, lint, type check and unit tests +pnpm test:e2e # Playwright smoke tests in Chromium and WebKit +pnpm desktop:build # native bundles in src-tauri/target/release/bundle +``` + +| Folder | What's in it | +| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `src/core` | The tileset engine: templates, layouts, generation and exporters. Plain TypeScript with no DOM, React or Node APIs (lint-enforced), so it runs in the browser, the desktop app and Node. | +| `src/app` | The React UI. `platform/` holds the web and desktop (Tauri) implementations of files, dialogs and settings. | +| `src-tauri` | The desktop shell (Tauri 2). | +| `tests` | Golden images, the headless Godot check and the Playwright tests. | +| `scripts` | `update-golden.ts` (after an intended output change, review the diff) and `screenshots.ts` (README images). | +The Godot check runs as part of `pnpm test` when a Godot 4 binary is found (`GODOT_BIN`, `/Applications/Godot.app` +or `godot` on the `PATH`). Otherwise it's skipped. -### Exporting to Godot -To export the tileset to Godot, you need to save it preferably on your Godot project folder. This will create a folder with the PNG, a .tres and a .import file. Once exported, you can open Godot and it will recognize it automatically. +**Releasing:** bump the version in `package.json` (and `src-tauri/Cargo.toml`), add it to `CHANGELOG.md`, then +push a `vX.Y.Z` tag. The release workflow builds every platform into a draft GitHub Release; publishing the draft +also pushes the builds to itch.io, once the `ITCH_GAME` variable and `BUTLER_API_KEY` secret are set. The web +version deploys from `master` to GitHub Pages. -Once opened in Godot, you can move the assets to other folders, but the first time it needs to be in the Godot project folder root. +## Feedback +Ideas and requests are welcome in the [roadmap thread](https://github.com/itsjavi/autotiler/issues/1) and in +[issues](https://github.com/itsjavi/autotiler/issues). ## License -This project is licensed under the [MIT license](LICENSE). +[MIT](LICENSE) diff --git a/app.js b/app.js deleted file mode 100644 index 66d509b..0000000 --- a/app.js +++ /dev/null @@ -1,434 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const dialog = require('electron').remote.dialog; -const util = require('util'); -const writeFile = util.promisify(fs.writeFile); -const pkg = require('./package.json'); -const open = require('open') - -const unhandled = require('electron-unhandled'); -unhandled({ - showDialog: true, - reportButton: error => { - open(pkg.homepage); - } -}); - -var $f = function () { - document.getElementById('app-version').innerText = 'v' + pkg.version; - const resourcesDir = path.join(__dirname, 'resources', 'templates') - const inputTileCountX = 5 - const tileCountX = 11 - const tileCountY = 5 - var inputFileName = 'demo.png'; - var tileSize = 16; - var scale = 1; - var imageLoader = document.getElementById('uploader-input'); - // var imageDownloader = document.getElementById('downloader-input'); - var $img = document.getElementById('uploader-img'); - var $imgOut = document.getElementById('img-output-generated'); - var $btnSaveImage = document.getElementById('btn-save-image'); - var $btnGodotExport = document.getElementById('btn-export-godot'); - var $tileSizeInput = document.getElementById('tile_size'); - imageLoader.addEventListener('change', handleImage, false); - // imageDownloader.addEventListener('click', downloadImage, false); - document.getElementById('uploader-pic').addEventListener('click', function (e) { - imageLoader.click(); - }, false); - $btnSaveImage.addEventListener('click', saveCanvasImage, false); - $btnGodotExport.addEventListener('click', saveGodotExport, false); - $tileSizeInput.addEventListener('change', function (e) { - tileSize = this.value; - console.log("New tile size is: " + tileSize); - generateCanvasImg(); - }, false); - - var canvas = document.getElementById("composer-img-canvas"); - var ctx = canvas.getContext("2d"); - ctx.imageSmoothingEnabled = false; - ctx.scale(scale, scale); - $img.addEventListener("load", function (e) { - generateCanvasImg(); - }, false); - - function drawCell(sx, sy, dx, dy) { - ctx.drawImage( - $img, - sx * tileSize, sy * tileSize, tileSize, tileSize, - dx * tileSize, dy * tileSize, tileSize, tileSize - ); - ctx.save(); - } - - function drawCellSlice(size, sx, sy, dx, dy) { - ctx.drawImage( - $img, - sx * tileSize, sy * tileSize, size, size, - dx * tileSize, dy * tileSize, size, size - ); - ctx.save(); - } - - function handleImage(e) { - $img.setAttribute("src", ""); - if (e.target.files.length > 0) { - var reader = new FileReader(); - reader.onload = function (event) { - $img.setAttribute('src', event.target.result); - detectTileSize(); - } - inputFileName = e.target.files[0].name; - document.title = 'Autotiler - ' + inputFileName; - reader.readAsDataURL(e.target.files[0]); - $btnSaveImage.disabled = false; - $btnGodotExport.disabled = false; - } - } - - function detectTileSize() { - var tmpIm = new Image(); - tmpIm.src = $img.src; - tmpIm.onload = function (e) { - // if (newTileSize === tileSize) { - // return; - // } - $tileSizeInput.value = Math.floor(tmpIm.width / inputTileCountX); - $tileSizeInput.dispatchEvent(new Event('change')) - $img.dispatchEvent(new Event('loaded')) - } - } - - function downloadImage(e) { - var newIm = new Image(); - newIm.src = createOutputCanvas().toDataURL(); - newIm.id = 'img-output-generated-img'; - // document.body.appendChild(canvas2); - $imgOut.innerHTML = ''; - $imgOut.appendChild(newIm); - // document.body.app = ('' + - // '

⟸ Back'); - $imgOut.addEventListener("click", saveCanvasImage) - } - - /** - * @returns {HTMLCanvasElement} - */ - function createOutputCanvas() { - var canvas2 = document.createElement("canvas"); - var ctx2 = canvas2.getContext("2d"); - ctx2.imageSmoothingEnabled = false; - // ctx2.width = ctx2.width / scale; - // ctx2.height = ctx2.height / scale; - ctx2.width = canvas.width / scale; - ctx2.height = canvas.height / scale; - canvas2.width = canvas.width / scale; - canvas2.height = canvas.height / scale; - ctx2.scale(1 / scale, 1 / scale); - ctx2.drawImage(canvas, 0, 0); - - return canvas2; - } - - function generateCanvasImg() { - canvas.width = (tileSize * tileCountX) - canvas.height = (tileSize * tileCountY) - ctx.width = canvas.width - ctx.height = canvas.height - - ctx.clearRect(0, 0, canvas.width, canvas.height); - // ctx.width = (88 * (tileSize / 8)) * scale; - // ctx.height = (40 * (tileSize / 8)) * scale; - - - drawCell(0, 0, 0, 0); - drawCell(1, 0, 1, 0); - drawCell(2, 0, 2, 0); - - drawCell(0, 1, 0, 1); - drawCell(1, 1, 1, 1); - drawCell(2, 1, 2, 1); - - drawCell(0, 2, 0, 2); - drawCell(1, 2, 1, 2); - drawCell(2, 2, 2, 2); - - - drawCellSlice(tileSize / 2, 0, 0, 0, 3); - drawCellSlice(tileSize / 2, 1, 0, 0.5, 3); - drawCellSlice(tileSize / 2, 1, 0, 1, 3); - drawCellSlice(tileSize / 2, 1, 0, 1.5, 3); - drawCellSlice(tileSize / 2, 1, 0, 2, 3); - drawCellSlice(tileSize / 2, 2.5, 0, 2.5, 3); - - drawCellSlice(tileSize / 2, 0, 2.5, 0, 3.5); - drawCellSlice(tileSize / 2, 1, 2.5, 0.5, 3.5); - drawCellSlice(tileSize / 2, 1, 2.5, 1, 3.5); - drawCellSlice(tileSize / 2, 1, 2.5, 1.5, 3.5); - drawCellSlice(tileSize / 2, 1, 2.5, 2, 3.5); - drawCellSlice(tileSize / 2, 2.5, 2.5, 2.5, 3.5); - - - drawCellSlice(tileSize / 2, 0, 0, 3, 3); - drawCellSlice(tileSize / 2, 2.5, 0, 3.5, 3); - drawCellSlice(tileSize / 2, 0, 2.5, 3, 3.5); - drawCellSlice(tileSize / 2, 2.5, 2.5, 3.5, 3.5); - - - drawCellSlice(tileSize / 2, 0, 0, 3, 0); - drawCellSlice(tileSize / 2, 2.5, 0, 3.5, 0); - - - drawCellSlice(tileSize / 2, 0, 0.5, 3, 0.5); - drawCellSlice(tileSize / 2, 2.5, 0.5, 3.5, 0.5); - drawCellSlice(tileSize / 2, 0, 0.5, 3, 1); - drawCellSlice(tileSize / 2, 2.5, 0.5, 3.5, 1); - drawCellSlice(tileSize / 2, 0, 0.5, 3, 1.5); - drawCellSlice(tileSize / 2, 2.5, 0.5, 3.5, 1.5); - drawCellSlice(tileSize / 2, 0, 0.5, 3, 2); - drawCellSlice(tileSize / 2, 2.5, 0.5, 3.5, 2); - drawCellSlice(tileSize / 2, 0, 2.5, 3, 2.5); - drawCellSlice(tileSize / 2, 2.5, 2.5, 3.5, 2.5); - - /// - - drawCell(0, 0, 4, 0); - drawCell(1, 0, 5, 0); - drawCell(1, 0, 6, 0); - drawCell(2, 0, 7, 0); - - drawCell(0, 1, 4, 1); - drawCell(1, 1, 5, 1); - drawCell(1, 1, 6, 1); - drawCell(2, 1, 7, 1); - - drawCell(0, 1, 4, 2); - drawCell(1, 1, 5, 2); - drawCell(1, 1, 6, 2); - drawCell(2, 1, 7, 2); - - drawCell(0, 2, 4, 3); - drawCell(1, 2, 5, 3); - drawCell(1, 2, 6, 3); - drawCell(2, 2, 7, 3); - - /// - - - drawCell(0, 1, 4, 4); - drawCell(1, 1, 5, 4); - drawCell(1, 1, 6, 4); - drawCell(2, 1, 7, 4); - - drawCell(1, 1, 8, 4); - drawCell(1, 2, 8, 3); - drawCell(1, 1, 8, 2); - drawCell(1, 1, 8, 1); - drawCell(1, 0, 8, 0); - - /// - - drawCell(1, 1, 9, 0); - drawCell(1, 1, 9, 1); - drawCell(1, 1, 9, 2); - drawCell(1, 1, 9, 3); - drawCell(1, 1, 10, 2); - drawCell(1, 1, 10, 3); - - /// corners/holes - drawCellSlice(tileSize / 2, 3.5, 0.5, 4.5, 0.5); - drawCellSlice(tileSize / 2, 3.5, 0.5, 4.5, 1.5); - drawCellSlice(tileSize / 2, 3.5, 0.5, 4.5, 4.5); - drawCellSlice(tileSize / 2, 3.5, 0.5, 5.5, 0.5); - drawCellSlice(tileSize / 2, 3.5, 0.5, 5.5, 1.5); - drawCellSlice(tileSize / 2, 3.5, 0.5, 5.5, 4.5); - drawCellSlice(tileSize / 2, 3.5, 0.5, 8.5, 0.5); - drawCellSlice(tileSize / 2, 3.5, 0.5, 8.5, 1.5); - drawCellSlice(tileSize / 2, 3.5, 0.5, 8.5, 4.5); - drawCellSlice(tileSize / 2, 3.5, 0.5, 9.5, 1.5); - drawCellSlice(tileSize / 2, 3.5, 0.5, 9.5, 3.5); - drawCellSlice(tileSize / 2, 3.5, 0.5, 10.5, 2.5); - drawCellSlice(tileSize / 2, 3.5, 0.5, 10.5, 3.5); - - - drawCellSlice(tileSize / 2, 4, 0.5, 6, 0.5); - drawCellSlice(tileSize / 2, 4, 0.5, 6, 1.5); - drawCellSlice(tileSize / 2, 4, 0.5, 6, 4.5); - drawCellSlice(tileSize / 2, 4, 0.5, 7, 0.5); - drawCellSlice(tileSize / 2, 4, 0.5, 7, 1.5); - drawCellSlice(tileSize / 2, 4, 0.5, 7, 4.5); - drawCellSlice(tileSize / 2, 4, 0.5, 8, 4.5); - drawCellSlice(tileSize / 2, 4, 0.5, 8, 1.5); - drawCellSlice(tileSize / 2, 4, 0.5, 8, 0.5); - drawCellSlice(tileSize / 2, 4, 0.5, 9, 0.5); - drawCellSlice(tileSize / 2, 4, 0.5, 9, 2.5); - drawCellSlice(tileSize / 2, 4, 0.5, 9, 3.5); - drawCellSlice(tileSize / 2, 4, 0.5, 10, 3.5); - - - /// - drawCellSlice(tileSize / 2, 3.5, 1, 4.5, 2); - drawCellSlice(tileSize / 2, 3.5, 1, 5.5, 2); - drawCellSlice(tileSize / 2, 3.5, 1, 4.5, 3); - drawCellSlice(tileSize / 2, 3.5, 1, 5.5, 3); - drawCellSlice(tileSize / 2, 3.5, 1, 4.5, 4); - drawCellSlice(tileSize / 2, 3.5, 1, 5.5, 4); - drawCellSlice(tileSize / 2, 3.5, 1, 8.5, 4); - drawCellSlice(tileSize / 2, 3.5, 1, 8.5, 3); - drawCellSlice(tileSize / 2, 3.5, 1, 8.5, 2); - drawCellSlice(tileSize / 2, 3.5, 1, 9.5, 0); - drawCellSlice(tileSize / 2, 3.5, 1, 9.5, 2); - drawCellSlice(tileSize / 2, 3.5, 1, 10.5, 2); - drawCellSlice(tileSize / 2, 3.5, 1, 10.5, 3); - - drawCellSlice(tileSize / 2, 4, 1, 6, 2); - drawCellSlice(tileSize / 2, 4, 1, 7, 2); - drawCellSlice(tileSize / 2, 4, 1, 6, 3); - drawCellSlice(tileSize / 2, 4, 1, 7, 3); - drawCellSlice(tileSize / 2, 4, 1, 6, 4); - drawCellSlice(tileSize / 2, 4, 1, 7, 4); - - drawCellSlice(tileSize / 2, 4, 1, 8, 2); - drawCellSlice(tileSize / 2, 4, 1, 8, 3); - drawCellSlice(tileSize / 2, 4, 1, 8, 4); - drawCellSlice(tileSize / 2, 4, 1, 9, 1); - drawCellSlice(tileSize / 2, 4, 1, 9, 2); - drawCellSlice(tileSize / 2, 4, 1, 9, 3); - drawCellSlice(tileSize / 2, 4, 1, 10, 2); - - ctx.save(); - // downloadImage(); - } - - function saveGodotExport() { - dialog.showSaveDialog( - {title: 'Export as Godot 3.x resource', defaultPath: inputFileName.replace(/\.([^.]+)/, '-autotile.$1')}, - ).then(file => { - // Stating whether dialog operation was cancelled or not. - if (!file.canceled) { - console.log(file.filePath.toString()); - saveGodotResources(file.filePath.toString()); - } else { - console.log("save canceled"); - } - }); - } - - function saveCanvasImage() { - dialog.showSaveDialog( - {title: 'Save generated image', defaultPath: inputFileName.replace(/\.([^.]+)/, '-autotile.$1')}, - ).then(file => { - // Stating whether dialog operation was cancelled or not. - if (!file.canceled) { - console.log(file.filePath.toString()); - saveImg(file.filePath.toString()); - } else { - console.log("save canceled"); - } - }); - } - - function readBlobAsUint8Array(blob) { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onloadend = () => { - return new Uint8Array(reader.result); - }; - reader.onerror = reject; - reader.readAsArrayBuffer(blob); - }); - } - - /** - * - * @param {HTMLCanvasElement} canv - * @returns {Promise} - */ - function getCanvasAsBlob(canv) { - return new Promise((resolve) => { - canv.toBlob(resolve, "image/png"); - }); - } - - async function saveCanvasAsPng(filename, canv) { - const blob = await getCanvasAsBlob(canv); - const data = await readBlobAsUint8Array(blob); - await writeFile(filename, data); - } - - function getImageBinaryData() { - const dataUrl = createOutputCanvas().toDataURL(); - return dataUrl.substring('data:image/png;base64,'.length); - } - - function saveImg(filename) { - const imgBase64 = getImageBinaryData(); - fs.writeFileSync(filename, imgBase64, 'base64'); - } - - function getFileExtension(filename) { - let ext = filename.split('.').pop() - if (filename === ext) { - return '' - } - return '.' + ext - } - - function replaceTemplateVars(template, varsObj) { - let contents = template - const keys = Object.keys(varsObj) - for (const i in keys) { - const key = keys[i] - const value = varsObj[key] - contents = contents.replace(new RegExp('\\$' + key, 'g'), value) - } - - return contents - } - - function getGodotExportFullDir(filename) { - return path.join(path.dirname(filename), getGodotExportBasename(filename)) - } - - function getGodotExportBasename(filename) { - return path.join(path.basename(filename, getFileExtension(filename))) - } - - function generateGodotResources(filename, tilesize) { - const baseFilename = path.basename(filename) - const baseFilenameNoExt = getGodotExportBasename(filename) - let importFileData = fs.readFileSync(path.join(resourcesDir, 'tileset.png.import'), "utf8") - let tresFileData = fs.readFileSync(path.join(resourcesDir, 'tileset.tres'), "utf8") - const tplVars = { - TS: tilesize, - IMGW: tilesize * tileCountX, - IMGH: tilesize * tileCountY, - IMGFILEPATH: (baseFilenameNoExt + '/' + baseFilename), - IMGFILE: baseFilename - } - - importFileData = replaceTemplateVars(importFileData, tplVars) - tresFileData = replaceTemplateVars(tresFileData, tplVars) - - return [ - {file: baseFilename + '.import', content: importFileData}, - {file: baseFilenameNoExt + '.tres', content: tresFileData}, - ] - } - - function saveGodotResources(filename) { - const destDir = getGodotExportFullDir(filename) - - const godotResources = generateGodotResources(filename, tileSize) - - if (!fs.existsSync(destDir)) { - fs.mkdirSync(destDir) - console.log(destDir) - } - - saveImg(path.join(destDir, path.basename(filename))) - godotResources.forEach(function (res) { - fs.writeFileSync(path.join(destDir, res.file), res.content); - }) - } -} -document.addEventListener("DOMContentLoaded", $f); diff --git a/docs/images/app.png b/docs/images/app.png new file mode 100644 index 0000000..8069f6d Binary files /dev/null and b/docs/images/app.png differ diff --git a/img/Icon-512x512.png b/docs/images/icon-512.png similarity index 100% rename from img/Icon-512x512.png rename to docs/images/icon-512.png diff --git a/docs/images/template-autotiler-13.png b/docs/images/template-autotiler-13.png new file mode 100644 index 0000000..a808fc5 Binary files /dev/null and b/docs/images/template-autotiler-13.png differ diff --git a/docs/images/template-rpgmaker-a2.png b/docs/images/template-rpgmaker-a2.png new file mode 100644 index 0000000..6cc4fd6 Binary files /dev/null and b/docs/images/template-rpgmaker-a2.png differ diff --git a/docs/images/test-map.png b/docs/images/test-map.png new file mode 100644 index 0000000..86462dc Binary files /dev/null and b/docs/images/test-map.png differ diff --git a/img/arrow.png b/img/arrow.png deleted file mode 100644 index d32bcaf..0000000 Binary files a/img/arrow.png and /dev/null differ diff --git a/img/demo-empty.png b/img/demo-empty.png deleted file mode 100644 index 1b9a6f1..0000000 Binary files a/img/demo-empty.png and /dev/null differ diff --git a/img/demos/godot-collisions.mp4 b/img/demos/godot-collisions.mp4 deleted file mode 100644 index 4eed263..0000000 Binary files a/img/demos/godot-collisions.mp4 and /dev/null differ diff --git a/img/demos/godot-tres.mp4 b/img/demos/godot-tres.mp4 deleted file mode 100644 index 231a127..0000000 Binary files a/img/demos/godot-tres.mp4 and /dev/null differ diff --git a/img/icon.png b/img/icon.png deleted file mode 100644 index a1d23f4..0000000 Binary files a/img/icon.png and /dev/null differ diff --git a/img/preview-sm.png b/img/preview-sm.png deleted file mode 100644 index 51e03ce..0000000 Binary files a/img/preview-sm.png and /dev/null differ diff --git a/img/preview.png b/img/preview.png deleted file mode 100644 index f138cca..0000000 Binary files a/img/preview.png and /dev/null differ diff --git a/index.html b/index.html index 0946da5..ae9c776 100644 --- a/index.html +++ b/index.html @@ -1,55 +1,18 @@ - + - - - + + + + + + Autotiler - - - - - - - -
-
- Tile Size: -
-
-
-
-
Input File (Wang)
-
- - source - - -
-
-
-
-
Preview (Blob)
-
- - - - -
- - -
-
-
-
-
- - - + + +
+ + diff --git a/main.js b/main.js deleted file mode 100644 index 1d5b0b7..0000000 --- a/main.js +++ /dev/null @@ -1,70 +0,0 @@ -// Modules to control application life and create native browser window -const {app, BrowserWindow, shell} = require('electron') -const path = require('path') -const open = require('open') - -function createWindow() { - // Create the browser window. - const mainWindow = new BrowserWindow({ - darkTheme: true, - width: 920, - height: 560, - useContentSize: true, - webPreferences: { - preload: path.join(__dirname, 'app.js'), - //nodeIntegration: true - }, - icon: path.join(__dirname, '/img/Icon-512x512.png') - }) - - mainWindow.setResizable(false) - mainWindow.setFullScreenable(false) - - // and load the index.html of the app. - mainWindow.loadFile('index.html') - - // Open the DevTools. - // mainWindow.webContents.openDevTools() - - // Remove menu bar - mainWindow.removeMenu() -} - -// This method will be called when Electron has finished -// initialization and is ready to create browser windows. -// Some APIs can only be used after this event occurs. -app.whenReady().then(() => { - createWindow() - - app.on('activate', function () { - // On macOS it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (BrowserWindow.getAllWindows().length === 0) createWindow() - }) -}) - -// Quit when all windows are closed, except on macOS. There, it's common -// for applications and their menu bar to stay active until the user quits -// explicitly with Cmd + Q. -app.on('window-all-closed', function () { - //if (process.platform !== 'darwin') - app.quit() -}) - -// In this file you can include the rest of your app's specific main process -// code. You can also put them in separate files and require them here. - - -// Open links in default browser -app.on('web-contents-created', (e, contents) => { - contents.on('new-window', (e, url) => { - e.preventDefault(); - open(url); - }); - contents.on('will-navigate', (e, url) => { - if (url !== contents.getURL()) { - e.preventDefault(); - open(url); - } - }); -}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index e540b0d..0000000 --- a/package-lock.json +++ /dev/null @@ -1,2249 +0,0 @@ -{ - "name": "autotiler", - "version": "1.2.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "7zip-bin": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.0.3.tgz", - "integrity": "sha512-GLyWIFBbGvpKPGo55JyRZAo4lVbnBiD52cKlw/0Vt+wnmKvWJkpZvsjVoaIolyBXDeAQKSicRtqFNPem9w0WYA==", - "dev": true - }, - "@develar/schema-utils": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", - "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", - "dev": true, - "requires": { - "ajv": "^6.12.0", - "ajv-keywords": "^3.4.1" - } - }, - "@electron/get": { - "version": "1.12.4", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-1.12.4.tgz", - "integrity": "sha512-6nr9DbJPUR9Xujw6zD3y+rS95TyItEVM0NVjt1EehY2vUWfIgPiIPVHxCvaTS0xr2B+DRxovYVKbuOWqC35kjg==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "global-agent": "^2.0.2", - "global-tunnel-ng": "^2.7.1", - "got": "^9.6.0", - "progress": "^2.0.3", - "semver": "^6.2.0", - "sumchecker": "^3.0.1" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@sindresorhus/is": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", - "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", - "dev": true - }, - "@szmarczak/http-timer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", - "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", - "dev": true, - "requires": { - "defer-to-connect": "^1.0.1" - } - }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true - }, - "@types/debug": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", - "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==", - "dev": true - }, - "@types/fs-extra": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.1.tgz", - "integrity": "sha512-B42Sxuaz09MhC3DDeW5kubRcQ5by4iuVQ0cRRWM2lggLzAa/KVom0Aft/208NgMvNQQZ86s5rVcqDdn/SH0/mg==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/node": { - "version": "12.12.53", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.53.tgz", - "integrity": "sha512-51MYTDTyCziHb70wtGNFRwB4l+5JNvdqzFSkbDvpbftEgVUBEE+T5f7pROhWMp/fxp07oNIEQZd5bbfAH22ohQ==", - "dev": true - }, - "@types/yargs": { - "version": "15.0.5", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz", - "integrity": "sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "@types/yargs-parser": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz", - "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==", - "dev": true - }, - "ajv": { - "version": "6.12.3", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz", - "integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true - }, - "ansi-align": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", - "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==", - "dev": true, - "requires": { - "string-width": "^3.0.0" - }, - "dependencies": { - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - } - } - }, - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "app-builder-bin": { - "version": "3.5.9", - "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-3.5.9.tgz", - "integrity": "sha512-NSjtqZ3x2kYiDp3Qezsgukx/AUzKPr3Xgf9by4cYt05ILWGAptepeeu0Uv+7MO+41o6ujhLixTou8979JGg2Kg==", - "dev": true - }, - "app-builder-lib": { - "version": "22.8.0", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-22.8.0.tgz", - "integrity": "sha512-RGaIRjCUrqkmh6QOGsyekQPEOaVynHfmeh8JZuyUymFYUOFdzBbPamkA2nhBVBTkkgfjRHsxK7LhedFKPzvWEQ==", - "dev": true, - "requires": { - "7zip-bin": "~5.0.3", - "@develar/schema-utils": "~2.6.5", - "async-exit-hook": "^2.0.1", - "bluebird-lst": "^1.0.9", - "builder-util": "22.8.0", - "builder-util-runtime": "8.7.2", - "chromium-pickle-js": "^0.2.0", - "debug": "^4.1.1", - "ejs": "^3.1.3", - "electron-publish": "22.8.0", - "fs-extra": "^9.0.1", - "hosted-git-info": "^3.0.5", - "is-ci": "^2.0.0", - "isbinaryfile": "^4.0.6", - "js-yaml": "^3.14.0", - "lazy-val": "^1.0.4", - "minimatch": "^3.0.4", - "normalize-package-data": "^2.5.0", - "read-config-file": "6.0.0", - "sanitize-filename": "^1.6.3", - "semver": "^7.3.2", - "temp-file": "^3.3.7" - }, - "dependencies": { - "fs-extra": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", - "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", - "dev": true, - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^1.0.0" - } - }, - "jsonfile": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", - "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^1.0.0" - } - }, - "universalify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", - "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==", - "dev": true - } - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - }, - "dependencies": { - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - } - } - }, - "async": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", - "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=", - "dev": true - }, - "async-exit-hook": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", - "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", - "dev": true - }, - "at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true - }, - "bluebird-lst": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz", - "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==", - "dev": true, - "requires": { - "bluebird": "^3.5.5" - } - }, - "boolean": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.0.2.tgz", - "integrity": "sha512-RwywHlpCRc3/Wh81MiCKun4ydaIFyW5Ea6JbL6sRCVx5q5irDw7pMXBUFYF/jArQ6YrG36q0kpovc9P/Kd3I4g==", - "dev": true, - "optional": true - }, - "boxen": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", - "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", - "dev": true, - "requires": { - "ansi-align": "^3.0.0", - "camelcase": "^5.3.1", - "chalk": "^3.0.0", - "cli-boxes": "^2.2.0", - "string-width": "^4.1.0", - "term-size": "^2.1.0", - "type-fest": "^0.8.1", - "widest-line": "^3.1.0" - }, - "dependencies": { - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - } - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", - "dev": true - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true - }, - "builder-util": { - "version": "22.8.0", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-22.8.0.tgz", - "integrity": "sha512-H80P1JzVy3TGpi63x81epQDK24XalL034+jAZlrPb5IhLtYmnNNdxCCAVJvg3VjSISd73Y71O+uhqCxWpqbPHw==", - "dev": true, - "requires": { - "7zip-bin": "~5.0.3", - "@types/debug": "^4.1.5", - "@types/fs-extra": "^9.0.1", - "app-builder-bin": "3.5.9", - "bluebird-lst": "^1.0.9", - "builder-util-runtime": "8.7.2", - "chalk": "^4.1.0", - "debug": "^4.1.1", - "fs-extra": "^9.0.1", - "is-ci": "^2.0.0", - "js-yaml": "^3.14.0", - "source-map-support": "^0.5.19", - "stat-mode": "^1.0.0", - "temp-file": "^3.3.7" - }, - "dependencies": { - "fs-extra": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", - "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", - "dev": true, - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^1.0.0" - } - }, - "jsonfile": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", - "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^1.0.0" - } - }, - "universalify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", - "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==", - "dev": true - } - } - }, - "builder-util-runtime": { - "version": "8.7.2", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-8.7.2.tgz", - "integrity": "sha512-xBqv+8bg6cfnzAQK1k3OGpfaHg+QkPgIgpEkXNhouZ0WiUkyZCftuRc2LYzQrLucFywpa14Xbc6+hTbpq83yRA==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "sax": "^1.2.4" - } - }, - "cacheable-request": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", - "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", - "dev": true, - "requires": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^3.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^4.1.0", - "responselike": "^1.0.2" - }, - "dependencies": { - "get-stream": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", - "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true - } - } - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "chromium-pickle-js": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", - "integrity": "sha1-BKEGZywYsIWrd02YPfo+oTjyIgU=", - "dev": true - }, - "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - }, - "clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" - }, - "cli-boxes": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.0.tgz", - "integrity": "sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w==", - "dev": true - }, - "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - } - } - }, - "clone-response": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", - "dev": true, - "requires": { - "mimic-response": "^1.0.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "config-chain": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", - "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", - "dev": true, - "optional": true, - "requires": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - } - }, - "configstore": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", - "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", - "dev": true, - "requires": { - "dot-prop": "^5.2.0", - "graceful-fs": "^4.1.2", - "make-dir": "^3.0.0", - "unique-string": "^2.0.0", - "write-file-atomic": "^3.0.0", - "xdg-basedir": "^4.0.0" - } - }, - "core-js": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.9.1.tgz", - "integrity": "sha512-gSjRvzkxQc1zjM/5paAmL4idJBFzuJoo+jDjF1tStYFMV2ERfD02HhahhCGXUyHxQRG4yFKVSdO6g62eoRMcDg==", - "dev": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "crypto-random-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", - "dev": true - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", - "dev": true, - "requires": { - "mimic-response": "^1.0.0" - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true - }, - "defer-to-connect": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", - "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", - "dev": true - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "optional": true, - "requires": { - "object-keys": "^1.0.12" - } - }, - "detect-node": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", - "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", - "dev": true, - "optional": true - }, - "dmg-builder": { - "version": "22.8.0", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-22.8.0.tgz", - "integrity": "sha512-orePWjcrl97SYLA8F/6UUtbXJSoZCYu5KOP1lVqD4LOomr8bjGDyEVYZmZYcg5WqKmXucdmO6OpqgzH/aRMMuA==", - "dev": true, - "requires": { - "app-builder-lib": "22.8.0", - "builder-util": "22.8.0", - "fs-extra": "^9.0.1", - "iconv-lite": "^0.6.2", - "js-yaml": "^3.14.0", - "sanitize-filename": "^1.6.3" - }, - "dependencies": { - "fs-extra": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", - "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", - "dev": true, - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^1.0.0" - } - }, - "jsonfile": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", - "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^1.0.0" - } - }, - "universalify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", - "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==", - "dev": true - } - } - }, - "dot-prop": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", - "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", - "dev": true, - "requires": { - "is-obj": "^2.0.0" - } - }, - "dotenv": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", - "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", - "dev": true - }, - "dotenv-expand": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", - "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", - "dev": true - }, - "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", - "dev": true - }, - "ejs": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.3.tgz", - "integrity": "sha512-wmtrUGyfSC23GC/B1SMv2ogAUgbQEtDmTIhfqielrG5ExIM9TP4UoYdi90jLF1aTcsWCJNEO0UrgKzP0y3nTSg==", - "dev": true, - "requires": { - "jake": "^10.6.1" - } - }, - "electron": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-9.4.0.tgz", - "integrity": "sha512-hOC4q0jkb+UDYZRy8vrZ1IANnq+jznZnbkD62OEo06nU+hIbp2IrwDRBNuSLmQ3cwZMVir0WSIA1qEVK0PkzGA==", - "dev": true, - "requires": { - "@electron/get": "^1.0.1", - "@types/node": "^12.0.12", - "extract-zip": "^1.0.3" - } - }, - "electron-builder": { - "version": "22.8.0", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-22.8.0.tgz", - "integrity": "sha512-dUv4F3srJouqxhWivtKqSoQP4Df6vYgjooGdzms+iYMTFi9f0b4LlEbr7kgsPvte8zAglee7VOGOODkCRJDkUQ==", - "dev": true, - "requires": { - "@types/yargs": "^15.0.5", - "app-builder-lib": "22.8.0", - "bluebird-lst": "^1.0.9", - "builder-util": "22.8.0", - "builder-util-runtime": "8.7.2", - "chalk": "^4.1.0", - "dmg-builder": "22.8.0", - "fs-extra": "^9.0.1", - "is-ci": "^2.0.0", - "lazy-val": "^1.0.4", - "read-config-file": "6.0.0", - "sanitize-filename": "^1.6.3", - "update-notifier": "^4.1.0", - "yargs": "^15.3.1" - }, - "dependencies": { - "fs-extra": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", - "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", - "dev": true, - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^1.0.0" - } - }, - "jsonfile": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", - "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^1.0.0" - } - }, - "universalify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", - "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==", - "dev": true - } - } - }, - "electron-is-dev": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-1.2.0.tgz", - "integrity": "sha512-R1oD5gMBPS7PVU8gJwH6CtT0e6VSoD0+SzSnYpNm+dBkcijgA+K7VAMHDfnRq/lkKPZArpzplTW6jfiMYosdzw==" - }, - "electron-publish": { - "version": "22.8.0", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-22.8.0.tgz", - "integrity": "sha512-uM0Zdi9hUqqGOrPj478v7toTvV1Kgto1w11rIiI168batiXAJvNLD8VZRfehOrZT0ibUyZlw8FtxoGCrjyHUOw==", - "dev": true, - "requires": { - "@types/fs-extra": "^9.0.1", - "bluebird-lst": "^1.0.9", - "builder-util": "22.8.0", - "builder-util-runtime": "8.7.2", - "chalk": "^4.1.0", - "fs-extra": "^9.0.1", - "lazy-val": "^1.0.4", - "mime": "^2.4.6" - }, - "dependencies": { - "fs-extra": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", - "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", - "dev": true, - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^1.0.0" - } - }, - "jsonfile": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", - "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^1.0.0" - } - }, - "universalify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", - "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==", - "dev": true - } - } - }, - "electron-unhandled": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/electron-unhandled/-/electron-unhandled-3.0.2.tgz", - "integrity": "sha512-IIqXnM5eNgV7k5sDA/GZ39ygJbpfF3WTArNGQ1TB4AI6ajQuuVztA0M6Mq9uEpmTh5gz4nR+YsTNWYsHLoM5rw==", - "requires": { - "clean-stack": "^2.1.0", - "electron-is-dev": "^1.0.1", - "ensure-error": "^2.0.0", - "lodash.debounce": "^4.0.8" - } - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", - "dev": true, - "optional": true - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "ensure-error": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ensure-error/-/ensure-error-2.1.0.tgz", - "integrity": "sha512-+BMSJHw9gxiJAAp2ZR1E0TNcL09dD3lOvkl7WVm4+Y6xnes/pMetP/TzCHiDduh8ihNDjbGfuYxl7l4PA1xZ8A==" - }, - "env-paths": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz", - "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==", - "dev": true - }, - "es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true, - "optional": true - }, - "escape-goat": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", - "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", - "dev": true - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "optional": true - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "extract-zip": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", - "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", - "dev": true, - "requires": { - "concat-stream": "^1.6.2", - "debug": "^2.6.9", - "mkdirp": "^0.5.4", - "yauzl": "^2.10.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "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 - }, - "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 - }, - "fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", - "dev": true, - "requires": { - "pend": "~1.2.0" - } - }, - "filelist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.1.tgz", - "integrity": "sha512-8zSK6Nu0DQIC08mUC46sWGXi+q3GGpKydAG36k+JDba6VRpkevvOWUW5a/PhShij4+vHT9M+ghgG7eM+a9JDUQ==", - "dev": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "global-agent": { - "version": "2.1.12", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-2.1.12.tgz", - "integrity": "sha512-caAljRMS/qcDo69X9BfkgrihGUgGx44Fb4QQToNQjsiWh+YlQ66uqYVAdA8Olqit+5Ng0nkz09je3ZzANMZcjg==", - "dev": true, - "optional": true, - "requires": { - "boolean": "^3.0.1", - "core-js": "^3.6.5", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" - } - }, - "global-dirs": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.0.1.tgz", - "integrity": "sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A==", - "dev": true, - "requires": { - "ini": "^1.3.5" - } - }, - "global-tunnel-ng": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/global-tunnel-ng/-/global-tunnel-ng-2.7.1.tgz", - "integrity": "sha512-4s+DyciWBV0eK148wqXxcmVAbFVPqtc3sEtUE/GTQfuU80rySLcMhUmHKSHI7/LDj8q0gDYI1lIhRRB7ieRAqg==", - "dev": true, - "optional": true, - "requires": { - "encodeurl": "^1.0.2", - "lodash": "^4.17.10", - "npm-conf": "^1.1.3", - "tunnel": "^0.0.6" - } - }, - "globalthis": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.2.tgz", - "integrity": "sha512-ZQnSFO1la8P7auIOQECnm0sSuoMeaSq0EEdXMBFF2QJO4uNcwbyhSgG3MruWNbFTqCLmxVwGOl7LZ9kASvHdeQ==", - "dev": true, - "optional": true, - "requires": { - "define-properties": "^1.1.3" - } - }, - "got": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", - "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", - "dev": true, - "requires": { - "@sindresorhus/is": "^0.14.0", - "@szmarczak/http-timer": "^1.1.2", - "cacheable-request": "^6.0.0", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^4.1.0", - "lowercase-keys": "^1.0.1", - "mimic-response": "^1.0.1", - "p-cancelable": "^1.0.0", - "to-readable-stream": "^1.0.0", - "url-parse-lax": "^3.0.0" - } - }, - "graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", - "dev": true - }, - "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 - }, - "has-yarn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", - "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", - "dev": true - }, - "hosted-git-info": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.5.tgz", - "integrity": "sha512-i4dpK6xj9BIpVOTboXIlKG9+8HMKggcrMX7WA24xZtKwX0TPelq/rbaS5rCKeNX8sJXZJGdSxpnEGtta+wismQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", - "dev": true - }, - "iconv-lite": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", - "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - }, - "import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", - "dev": true - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true - }, - "is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "dev": true, - "requires": { - "ci-info": "^2.0.0" - } - }, - "is-docker": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz", - "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "is-installed-globally": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", - "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", - "dev": true, - "requires": { - "global-dirs": "^2.0.1", - "is-path-inside": "^3.0.1" - } - }, - "is-npm": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", - "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==", - "dev": true - }, - "is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true - }, - "is-path-inside": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz", - "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==", - "dev": true - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "requires": { - "is-docker": "^2.0.0" - } - }, - "is-yarn-global": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", - "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isbinaryfile": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.6.tgz", - "integrity": "sha512-ORrEy+SNVqUhrCaal4hA4fBzhggQQ+BaLntyPOdoEiwlKZW9BZiJXjg3RMiruE4tPEI3pyVPpySHQF/dKWperg==", - "dev": true - }, - "jake": { - "version": "10.8.2", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.2.tgz", - "integrity": "sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A==", - "dev": true, - "requires": { - "async": "0.9.x", - "chalk": "^2.4.2", - "filelist": "^1.0.1", - "minimatch": "^3.0.4" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "js-yaml": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", - "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", - "dev": true - }, - "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 - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true, - "optional": true - }, - "json5": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", - "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "keyv": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", - "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", - "dev": true, - "requires": { - "json-buffer": "3.0.0" - } - }, - "latest-version": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", - "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", - "dev": true, - "requires": { - "package-json": "^6.3.0" - } - }, - "lazy-val": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.4.tgz", - "integrity": "sha512-u93kb2fPbIrfzBuLjZE+w+fJbUUMhNDXxNmMfaqNgpfQf1CO5ZSe2LfsnBqVAk7i/2NF48OSoRj+Xe2VT+lE8Q==", - "dev": true - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, - "optional": true - }, - "lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" - }, - "lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", - "dev": true - }, - "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, - "requires": { - "yallist": "^4.0.0" - } - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", - "dev": true, - "optional": true, - "requires": { - "escape-string-regexp": "^4.0.0" - } - }, - "mime": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", - "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", - "dev": true - }, - "mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "hosted-git-info": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "normalize-url": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", - "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", - "dev": true - }, - "npm-conf": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", - "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", - "dev": true, - "optional": true, - "requires": { - "config-chain": "^1.1.11", - "pify": "^3.0.0" - } - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "open": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/open/-/open-7.1.0.tgz", - "integrity": "sha512-lLPI5KgOwEYCDKXf4np7y1PBEkj7HYIyP2DY8mVDRnx0VIIu6bNrRB0R66TuO7Mack6EnTNLm4uvcl1UoklTpA==", - "requires": { - "is-docker": "^2.0.0", - "is-wsl": "^2.1.1" - } - }, - "p-cancelable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", - "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", - "dev": true - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "package-json": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", - "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", - "dev": true, - "requires": { - "got": "^9.6.0", - "registry-auth-token": "^4.0.0", - "registry-url": "^5.0.0", - "semver": "^6.2.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true - }, - "pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", - "dev": true - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "optional": true - }, - "prepend-http": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, - "proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", - "dev": true, - "optional": true - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "pupa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.0.1.tgz", - "integrity": "sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA==", - "dev": true, - "requires": { - "escape-goat": "^2.0.0" - } - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "read-config-file": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.0.0.tgz", - "integrity": "sha512-PHjROSdpceKUmqS06wqwP92VrM46PZSTubmNIMJ5DrMwg1OgenSTSEHIkCa6TiOJ+y/J0xnG1fFwG3M+Oi1aNA==", - "dev": true, - "requires": { - "dotenv": "^8.2.0", - "dotenv-expand": "^5.1.0", - "js-yaml": "^3.13.1", - "json5": "^2.1.2", - "lazy-val": "^1.0.4" - } - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "registry-auth-token": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.0.tgz", - "integrity": "sha512-P+lWzPrsgfN+UEpDS3U8AQKg/UjZX6mQSJueZj3EK+vNESoqBSpBUD3gmu4sF9lOsjXWjF11dQKUqemf3veq1w==", - "dev": true, - "requires": { - "rc": "^1.2.8" - } - }, - "registry-url": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", - "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", - "dev": true, - "requires": { - "rc": "^1.2.8" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, - "resolve": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", - "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - }, - "responselike": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", - "dev": true, - "requires": { - "lowercase-keys": "^1.0.0" - } - }, - "roarr": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", - "dev": true, - "optional": true, - "requires": { - "boolean": "^3.0.1", - "detect-node": "^2.0.4", - "globalthis": "^1.0.1", - "json-stringify-safe": "^5.0.1", - "semver-compare": "^1.0.0", - "sprintf-js": "^1.1.2" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "sanitize-filename": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", - "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", - "dev": true, - "requires": { - "truncate-utf8-bytes": "^1.0.0" - } - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true - }, - "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", - "dev": true - }, - "semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", - "dev": true, - "optional": true - }, - "semver-diff": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", - "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", - "dev": true, - "requires": { - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", - "dev": true, - "optional": true, - "requires": { - "type-fest": "^0.13.1" - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", - "dev": true - }, - "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 - }, - "source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", - "dev": true - }, - "sprintf-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", - "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", - "dev": true, - "optional": true - }, - "stat-mode": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", - "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", - "dev": true - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - } - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, - "sumchecker": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", - "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", - "dev": true, - "requires": { - "debug": "^4.1.0" - } - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "temp-file": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.3.7.tgz", - "integrity": "sha512-9tBJKt7GZAQt/Rg0QzVWA8Am8c1EFl+CAv04/aBVqlx5oyfQ508sFIABshQ0xbZu6mBrFLWIUXO/bbLYghW70g==", - "dev": true, - "requires": { - "async-exit-hook": "^2.0.1", - "fs-extra": "^8.1.0" - } - }, - "term-size": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz", - "integrity": "sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==", - "dev": true - }, - "to-readable-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", - "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", - "dev": true - }, - "truncate-utf8-bytes": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", - "integrity": "sha1-QFkjkJWS1W94pYGENLC3hInKXys=", - "dev": true, - "requires": { - "utf8-byte-length": "^1.0.1" - } - }, - "tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", - "dev": true, - "optional": true - }, - "type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true, - "optional": true - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true - }, - "typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "requires": { - "is-typedarray": "^1.0.0" - } - }, - "unique-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", - "dev": true, - "requires": { - "crypto-random-string": "^2.0.0" - } - }, - "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 - }, - "update-notifier": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.0.tgz", - "integrity": "sha512-w3doE1qtI0/ZmgeoDoARmI5fjDoT93IfKgEGqm26dGUOh8oNpaSTsGNdYRN/SjOuo10jcJGwkEL3mroKzktkew==", - "dev": true, - "requires": { - "boxen": "^4.2.0", - "chalk": "^3.0.0", - "configstore": "^5.0.1", - "has-yarn": "^2.1.0", - "import-lazy": "^2.1.0", - "is-ci": "^2.0.0", - "is-installed-globally": "^0.3.1", - "is-npm": "^4.0.0", - "is-yarn-global": "^0.3.0", - "latest-version": "^5.0.0", - "pupa": "^2.0.1", - "semver-diff": "^3.1.1", - "xdg-basedir": "^4.0.0" - }, - "dependencies": { - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - } - } - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "url-parse-lax": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", - "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", - "dev": true, - "requires": { - "prepend-http": "^2.0.0" - } - }, - "utf8-byte-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", - "integrity": "sha1-9F8VDExm7uloGGUFq5P8u4rWv2E=", - "dev": true - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, - "widest-line": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", - "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", - "dev": true, - "requires": { - "string-width": "^4.0.0" - } - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "xdg-basedir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", - "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", - "dev": true - }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dev": true, - "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - } - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, - "yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", - "dev": true, - "requires": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - } - } -} diff --git a/package.json b/package.json index f472dcd..3cd453c 100644 --- a/package.json +++ b/package.json @@ -1,47 +1,64 @@ { "name": "autotiler", - "version": "1.2.0", - "main": "main.js", + "version": "2.0.0", "private": true, + "description": "Turn a small terrain template into a 47-tile blob tileset, with Godot 4, Tiled and dual-grid export.", + "homepage": "https://route1rodent.itch.io/autotiler", + "license": "MIT", + "author": "route1rodent ", + "repository": "github:itsjavi/autotiler", + "type": "module", "scripts": { - "start": "electron .", - "dist": "electron-builder -mwl", - "postinstall": "electron-builder install-app-deps" + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "typecheck": "tsc -b", + "lint": "oxlint", + "lint:fix": "oxlint --fix", + "fmt": "oxfmt", + "fmt:check": "oxfmt --check", + "test": "vitest run", + "test:watch": "vitest", + "test:e2e": "playwright test", + "golden:update": "node scripts/update-golden.ts", + "check": "pnpm fmt:check && pnpm lint && pnpm typecheck && pnpm test", + "tauri": "tauri", + "desktop:dev": "tauri dev", + "desktop:build": "tauri build" }, - "author": "route1rodent ", - "homepage": "https://route1rodent.itch.io/autotiler", - "butler": { - "project": "route1rodent/autotiler" + "dependencies": { + "@base-ui/react": "^1.8.0", + "@tauri-apps/api": "^2.11.1", + "@tauri-apps/plugin-dialog": "~2.7.3", + "@tauri-apps/plugin-fs": "~2.5.2", + "@tauri-apps/plugin-opener": "~2.5.5", + "@tauri-apps/plugin-store": "~2.4.5", + "@tauri-apps/plugin-window-state": "~2.4.1", + "fast-png": "^8.0.0", + "fflate": "^0.8.3", + "lucide-react": "^1.48.0", + "react": "^19.3.0", + "react-dom": "^19.3.0", + "zustand": "^5.0.15" }, "devDependencies": { - "electron": "^9.4.0", - "electron-builder": "^22.8.0" + "@playwright/test": "^1.63.0", + "@tailwindcss/vite": "^4.3.3", + "@tauri-apps/cli": "^2.11.5", + "@types/node": "^24.12.0", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@vitejs/plugin-react": "^6.1.1", + "oxfmt": "~0.70.0", + "oxlint": "^1.85.0", + "oxlint-tsgolint": "^7.0.2003", + "tailwindcss": "^4.3.3", + "typescript": "^7.0.2", + "vite": "^8.3.1", + "vitest": "^5.0.1" }, - "dependencies": { - "electron-unhandled": "^3.0.2", - "open": "^7.1.0" + "engines": { + "node": ">=24.12" }, - "build": { - "appId": "com.route1rodent.autotiler", - "productName": "Autotiler", - "copyright": "Copyright © 2020, ${author}", - "artifactName": "${name}-${os}.${ext}", - "directories": { - "buildResources": "img" - }, - "files": [ - "**/*", - "img/*" - ], - "mac": { - "category": "public.app-category.graphics-design", - "target": "dmg" - }, - "win": { - "target": "portable" - }, - "linux": { - "target": "AppImage" - } - } + "packageManager": "pnpm@12.6.0" } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..dc325ce --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from "@playwright/test"; + +const ci = !!process.env.CI; + +export default defineConfig({ + testDir: "./tests/e2e", + fullyParallel: true, + retries: ci ? 1 : 0, + timeout: 30_000, + expect: { timeout: 5_000 }, + reporter: ci ? "github" : "list", + use: { baseURL: "http://127.0.0.1:4173", trace: "on-first-retry", acceptDownloads: true }, + webServer: { + // `exec` so Playwright stops the preview server itself (a pnpm wrapper doesn't forward the signal), and IPv4 + // explicitly: "localhost" may resolve to ::1 for Vite but 127.0.0.1 for the readiness probe + command: "pnpm build && exec node_modules/.bin/vite preview --host 127.0.0.1 --port 4173 --strictPort", + url: "http://127.0.0.1:4173", + reuseExistingServer: !ci, + timeout: 120_000, + }, + projects: [ + // locally reuse the installed Google Chrome; CI installs Playwright's Chromium + { + name: "chromium", + use: { + ...devices["Desktop Chrome"], + viewport: { width: 1440, height: 900 }, + ...(ci ? {} : { channel: "chrome" }), + }, + }, + // WebKit is what the macOS and Linux desktop builds render with + { name: "webkit", use: { ...devices["Desktop Safari"], viewport: { width: 1440, height: 900 } } }, + ], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..49bfcf1 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2344 @@ +--- +lockfileVersion: '9.0' + +importers: + + .: + configDependencies: {} + packageManagerDependencies: + pnpm: + specifier: 12.6.0 + version: 12.6.0 + +packages: + + '@pnpm/exe.android-arm64@12.6.0': + resolution: {integrity: sha512-kviIHft9h02q+7N2In7tSL9T/HRUSGwYmU74YPtsWU5ee7vgqPyC0JT6RRX6miW+NxX1CvHgQxg69qq1qTLJYQ==} + cpu: [arm64] + os: [android] + + '@pnpm/exe.android-x64@12.6.0': + resolution: {integrity: sha512-CT8aJKLq2mtZFE71pr4E5Z2xHL8uGnRGr+NclDqsXlVF4SVcQ2QAs14mWi0C8gjT4hbmi+Q0lJRRIkMAWfjjng==} + cpu: [x64] + os: [android] + + '@pnpm/exe.darwin-arm64@12.6.0': + resolution: {integrity: sha512-rafpVkjzugKBMxSvGQd5wK1x5eOTkD/+lcJIFHeQhvd+BJ8kDisOOvwhQDrpGd4vd2Fx+hhW0P2Ptt638OlhFg==} + cpu: [arm64] + os: [darwin] + + '@pnpm/exe.darwin-x64@12.6.0': + resolution: {integrity: sha512-72Jpuv1m24gI8zUNcaylYpBWEhBzz1VPhnDIU2GYM2eRP+KsOqVLwV78i+JrihI39zY2NNXmaN4eAKX1inpaDQ==} + cpu: [x64] + os: [darwin] + + '@pnpm/exe.freebsd-x64@12.6.0': + resolution: {integrity: sha512-LON4QgNy1w/XF4uySZIyC/5hEB0oGdelXsY9tSKqstER+2j2X2oHz67skD11RRd/HWFRtMr64shQZ4I9ZoZ0zg==} + cpu: [x64] + os: [freebsd] + + '@pnpm/exe.linux-arm64-musl@12.6.0': + resolution: {integrity: sha512-BdDpX+DeaMUc5x6WNcE6FkmFy36NwrBbBvwsZU737Zt3u1fTY+iKFPchjEQNCM4rT6q6xvF2oN3ZwExGsJCkxQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-arm64@12.6.0': + resolution: {integrity: sha512-8h2sNoIhHDpHDYqZZCh9PXr6krqW10btb0GHVGuw710muDHlFAHMtUG2ogmaHge6zJG1xgY4Y7mwX5g/LWvrYg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-ppc64@12.6.0': + resolution: {integrity: sha512-ld6xgcEhFCFrsVsJhbmdmR714PnsEspO/Ij8w0/D7OSiz2hYGu3fZd5tKRxOI5m6H6Gl0/RRFAPncTHbsgXVjQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-riscv64@12.6.0': + resolution: {integrity: sha512-HIbImydoC3J8NFt+8MXfhTCf5rO6NjCMyinSAHac0VmV8uCHw+kbQkyAzxV84rzz9ZOzmBOnj0b1N461UIYGsw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-s390x@12.6.0': + resolution: {integrity: sha512-DjSqT7+BZ/lWcDHwNMuzsRVfyxUimh+D54Xw4tSWbdVVQhtWtNMUyI93LVnwCmsoEpar2gC3SFeAgywx37svKQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-x64-musl@12.6.0': + resolution: {integrity: sha512-31lKeGPmRE6xfV6I3VjaEGVFRei14pl/zgoFDK4w+UkbJqOuNl2Ht3O1GuqNIg3Gl14qTEb4kZRAYFHXifDx9A==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-x64@12.6.0': + resolution: {integrity: sha512-qFWBneHJAJ73W4whtbaFOL1M/7DBC6ILHXuxc7ZPtEhfPuT1zeZiGrmKHoMAfJA+mcm6xhOFljqVTUS+00Jabw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.win32-arm64@12.6.0': + resolution: {integrity: sha512-OhfefXEEykZlslSUhR8PPRe6MVPV3Oink/dVfIecE5velpc7j80cZYsHlaDrtRDcm3TPwQHkaRW1SJ1efX9oFg==} + cpu: [arm64] + os: [win32] + + '@pnpm/exe.win32-x64@12.6.0': + resolution: {integrity: sha512-L2tuyrD2+Imgxs3VK/ST/2L3xD6vDP0ktJQxM0TaGw7TbAQEMq+RMc8iK6Ew7Id579uzjJe8jSfJID2ZGsOHCA==} + cpu: [x64] + os: [win32] + + pnpm@12.6.0: + resolution: {integrity: sha512-PvaPlRyxEawgS0paFvCy3fDaVqluBBPoHYVdnwtV75JnFHCQKOHNAMQFwsX7e56OxNxGd3yAXQNzwvL/AP0g7A==} + engines: {node: '>=18.*'} + hasBin: true + +snapshots: + + '@pnpm/exe.android-arm64@12.6.0': + optional: true + + '@pnpm/exe.android-x64@12.6.0': + optional: true + + '@pnpm/exe.darwin-arm64@12.6.0': + optional: true + + '@pnpm/exe.darwin-x64@12.6.0': + optional: true + + '@pnpm/exe.freebsd-x64@12.6.0': + optional: true + + '@pnpm/exe.linux-arm64-musl@12.6.0': + optional: true + + '@pnpm/exe.linux-arm64@12.6.0': + optional: true + + '@pnpm/exe.linux-ppc64@12.6.0': + optional: true + + '@pnpm/exe.linux-riscv64@12.6.0': + optional: true + + '@pnpm/exe.linux-s390x@12.6.0': + optional: true + + '@pnpm/exe.linux-x64-musl@12.6.0': + optional: true + + '@pnpm/exe.linux-x64@12.6.0': + optional: true + + '@pnpm/exe.win32-arm64@12.6.0': + optional: true + + '@pnpm/exe.win32-x64@12.6.0': + optional: true + + pnpm@12.6.0: + optionalDependencies: + '@pnpm/exe.android-arm64': 12.6.0 + '@pnpm/exe.android-x64': 12.6.0 + '@pnpm/exe.darwin-arm64': 12.6.0 + '@pnpm/exe.darwin-x64': 12.6.0 + '@pnpm/exe.freebsd-x64': 12.6.0 + '@pnpm/exe.linux-arm64': 12.6.0 + '@pnpm/exe.linux-arm64-musl': 12.6.0 + '@pnpm/exe.linux-ppc64': 12.6.0 + '@pnpm/exe.linux-riscv64': 12.6.0 + '@pnpm/exe.linux-s390x': 12.6.0 + '@pnpm/exe.linux-x64': 12.6.0 + '@pnpm/exe.linux-x64-musl': 12.6.0 + '@pnpm/exe.win32-arm64': 12.6.0 + '@pnpm/exe.win32-x64': 12.6.0 + +--- +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@base-ui/react': + specifier: ^1.8.0 + version: 1.8.0(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@tauri-apps/api': + specifier: ^2.11.1 + version: 2.11.1 + '@tauri-apps/plugin-dialog': + specifier: ~2.7.3 + version: 2.7.3 + '@tauri-apps/plugin-fs': + specifier: ~2.5.2 + version: 2.5.2 + '@tauri-apps/plugin-opener': + specifier: ~2.5.5 + version: 2.5.5 + '@tauri-apps/plugin-store': + specifier: ~2.4.5 + version: 2.4.5 + '@tauri-apps/plugin-window-state': + specifier: ~2.4.1 + version: 2.4.1 + fast-png: + specifier: ^8.0.0 + version: 8.0.0 + fflate: + specifier: ^0.8.3 + version: 0.8.3 + lucide-react: + specifier: ^1.48.0 + version: 1.48.0(react@19.3.0) + react: + specifier: ^19.3.0 + version: 19.3.0 + react-dom: + specifier: ^19.3.0 + version: 19.3.0(react@19.3.0) + zustand: + specifier: ^5.0.15 + version: 5.0.15(@types/react@19.3.0)(react@19.3.0)(use-sync-external-store@1.7.0(react@19.3.0)) + devDependencies: + '@playwright/test': + specifier: ^1.63.0 + version: 1.63.0 + '@tailwindcss/vite': + specifier: ^4.3.3 + version: 4.3.3(vite@8.3.1(@types/node@24.13.6)(jiti@2.7.0)) + '@tauri-apps/cli': + specifier: ^2.11.5 + version: 2.11.5 + '@types/node': + specifier: ^24.12.0 + version: 24.13.6 + '@types/react': + specifier: ^19.3.0 + version: 19.3.0 + '@types/react-dom': + specifier: ^19.3.0 + version: 19.3.0(@types/react@19.3.0) + '@vitejs/plugin-react': + specifier: ^6.1.1 + version: 6.1.1(vite@8.3.1(@types/node@24.13.6)(jiti@2.7.0)) + oxfmt: + specifier: ~0.70.0 + version: 0.70.0 + oxlint: + specifier: ^1.85.0 + version: 1.85.0(oxlint-tsgolint@7.0.2003) + oxlint-tsgolint: + specifier: ^7.0.2003 + version: 7.0.2003 + tailwindcss: + specifier: ^4.3.3 + version: 4.3.3 + typescript: + specifier: ^7.0.2 + version: 7.0.2 + vite: + specifier: ^8.3.1 + version: 8.3.1(@types/node@24.13.6)(jiti@2.7.0) + vitest: + specifier: ^5.0.1 + version: 5.0.1(@types/node@24.13.6)(vite@8.3.1(@types/node@24.13.6)(jiti@2.7.0)) + +packages: + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@base-ui/react@1.8.0': + resolution: {integrity: sha512-P0/1sxo6SBVZOklKMIedvTWqw2s2IQzi9x5bIVsXu980cuSOD4NeuRSs+/L7LZQfDkZP/uRZyGPyfFl/B1oH+Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@date-fns/tz': ^1.2.0 + '@types/react': ^17 || ^18 || ^19 + date-fns: ^4.0.0 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@date-fns/tz': + optional: true + '@types/react': + optional: true + date-fns: + optional: true + + '@base-ui/utils@0.4.0': + resolution: {integrity: sha512-bO9fz25kKtPf+aZVyfQrC0PDmJdmVni31W2hCS5/Owb+inwdIL3XU26pCPRPlt4LSxZrBgLwubXQXQlKaFEZzw==} + peerDependencies: + '@types/react': ^17 || ^18 || ^19 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} + + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} + + '@floating-ui/react-dom@2.1.9': + resolution: {integrity: sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@oxc-project/types@0.151.0': + resolution: {integrity: sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==} + + '@oxfmt/binding-android-arm-eabi@0.70.0': + resolution: {integrity: sha512-Xd7YO4/T2axEj6FTLcj4Why3mTBqFMg+x24xtorT4Lb2+1g82090GH0a/4U1m0pABGYiix2bq1pqkYrmV3f0Sw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxfmt/binding-android-arm64@0.70.0': + resolution: {integrity: sha512-x9rlMYyKXdgKdYyUJzGsK1ZV8P4di/J32ipzcS6Jet6p9r9UAh28neXIMtdlSaJJycdi61Z4YkcLKLpk8ueFjg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxfmt/binding-darwin-arm64@0.70.0': + resolution: {integrity: sha512-IUTUPvrBVYy7POh4stXzRdz4IVC/1QSaviCWoyenSlOhGu0X9j5K07vCTM9biLjAA2Zs31l0Rj5vvRpj9n95wA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxfmt/binding-darwin-x64@0.70.0': + resolution: {integrity: sha512-vw745q870oTd6J517O24asoX4/E+eK0nxYIFoedSLqgJ+nI5En7+ZS82iZSHZ69zevQrnOXiyHP01dA+t8xD8w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxfmt/binding-freebsd-x64@0.70.0': + resolution: {integrity: sha512-NO14EgSM9dFkcg+MfGPxvsKqXYs9LKaxPrOKXpv1R0rLokGGFDcCq6dBMq18dE4wlpFOovX0UZY2uh1P30O7QA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxfmt/binding-linux-arm-gnueabihf@0.70.0': + resolution: {integrity: sha512-139OEhHarj9CYoJ/i9gXlPv4KLBGtLj2toseOWYFf09QwlhklaZk+wW3aOvlqoeZtuayvkoSNVWra7WJ21s3VQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.70.0': + resolution: {integrity: sha512-GEh2PY3IWTE0M24eNhTduountANSbWyDmMnzFSQE/nGg/bjPugbUgiGuFu+xdqcQSd/HKSwH80/F2yVVD48yhA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm64-gnu@0.70.0': + resolution: {integrity: sha512-En5i+UJmZSPxuSf47F2Hl5YOzKB0bicQLnGQkeTCMQ35cWLtbrSwACJKfiLqRZrk05DwSnsJkhBRaM3OURtIaA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-arm64-musl@0.70.0': + resolution: {integrity: sha512-WWOoV5W9Im3flVwOVrWn/2DUlOF8v5vcCip+kcNuaMpulRCh6nzzt1Su2vcL2F908YJIXNV3HvegbBHuyLwKHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-ppc64-gnu@0.70.0': + resolution: {integrity: sha512-YUouneIqW+5n7aE8xx/zeZ6/utr/KH7oykcGoFyd8Uz8uh591T1oKlnoWA3BsRq/ZR42oY1w4MUYvS/0e/MQOA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-gnu@0.70.0': + resolution: {integrity: sha512-iEnMf21S5aGVa4hViDGY8sAQ/AHyCu2JPyrQF8P06wtHhSkD1YJBeT4m/KiGewgf7+a5XCYSCRIPcRQa1xwEoQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-musl@0.70.0': + resolution: {integrity: sha512-91Sdniaj20fQzyMeCxMDzTP4c9s4RB8dGQ308xHhDR0n6U7+1Xq7N9klE7mfXq8iV3lRmIGSXi5X23Hn/0XX/g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-s390x-gnu@0.70.0': + resolution: {integrity: sha512-uUV30M6E+2TKKGMaKiwfeL4RZrviHXlUxsrYJ/jFBb+1EZy+pnFT+hF73eeWdzh5NqPOAnw0iiMAIqjqiLZPFg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-gnu@0.70.0': + resolution: {integrity: sha512-ivMcX6kNDPhqtbOaBt/ItFlLlTlXNHLgRuNmxP6Na6UuYXRT10llpJcAPbGeRgjjb3Qzv4jwPp3fB0hui50WNQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-musl@0.70.0': + resolution: {integrity: sha512-w+S+fERxYmlZSyZlJK/U292FjyBoH8cCEj21/tYJX6atX5kNSn+HDkhlFQKT2zcMwUW0uAtUL/bOrlwJZwqRdA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-openharmony-arm64@0.70.0': + resolution: {integrity: sha512-Zlom1Xkx257R8bk4ZI4zJsrGno2opknz1+5v5baka3nn4FPyvNSdh8JUL4CdN1S1OWRMvJ9UJQ+RfIqGGCUEfA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxfmt/binding-win32-arm64-msvc@0.70.0': + resolution: {integrity: sha512-FQgPW5R17vzt7cgrJ8eG/dqX00o2xHsqFeLfw4xzA9FRHpN/DjFo9YDonvIIXGxiEuS9F/jZPnGO+KHNKuCo4Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxfmt/binding-win32-ia32-msvc@0.70.0': + resolution: {integrity: sha512-ZfZublNhZ+XBndMiXhkiLlPE+XyGRDa0CweeTL6t1fZypfCh1LTg7e5CvnOeTBunq15MskOcRempumSPGAaCQA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxfmt/binding-win32-x64-msvc@0.70.0': + resolution: {integrity: sha512-HlIZEn+WzLQL0DszNzldiRl/DPRCX5R0Vkt6qeUPR1YHwy52hZZo4x6HoTOVmKRP2wUiwPGtKsihNY/f8KRaBg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxlint-tsgolint/darwin-arm64@7.0.2003': + resolution: {integrity: sha512-TgV33rXr6ueXBwvc+0nssUkTBSXHJxv77I8p4RCjDjCnvexHtmoPiudVRDfj3S3puMDdeQdHW6jdUgUPy3nr/w==} + cpu: [arm64] + os: [darwin] + + '@oxlint-tsgolint/darwin-x64@7.0.2003': + resolution: {integrity: sha512-hY3FMAjIaPDdK3FNxyRXRfHPOFeoBn6dmnLyKZgQ2IbTTD1adXhl6PfCIqHhCPvurigv7nf7mYuWZZ19MmJzmg==} + cpu: [x64] + os: [darwin] + + '@oxlint-tsgolint/linux-arm64@7.0.2003': + resolution: {integrity: sha512-eAET4JpyfBbg8SO0K74o4R55tEjVC292pIyxGOw2XGf8x/HrPNyxwQ478jMYOM54FIVOHc8sJ6VRHxluy6lucw==} + cpu: [arm64] + os: [linux] + + '@oxlint-tsgolint/linux-x64@7.0.2003': + resolution: {integrity: sha512-GXdyO/XyqDJ3s/llR/oOktLsYNjZtWSQBy0JMc+/0gsNPvTcseKPhn9c6KcFyWmnr6H4vljYALbujonqSzzEGw==} + cpu: [x64] + os: [linux] + + '@oxlint-tsgolint/win32-arm64@7.0.2003': + resolution: {integrity: sha512-TWauXnPfet0VgpmrstoAK58eJ4gbuwPUuUTQmYQAzE/RG1CpnxXtrKUJULf6lyerPE4KN3gM+DflIA1hOH+38Q==} + cpu: [arm64] + os: [win32] + + '@oxlint-tsgolint/win32-x64@7.0.2003': + resolution: {integrity: sha512-DoRmfe7j8VqNlukp8liRVW45GQDhzRccNenjD/pdzelgtffW47pCMd1xbJLkPaPbKnTwID3onn3VZlL7JbebEA==} + cpu: [x64] + os: [win32] + + '@oxlint/binding-android-arm-eabi@1.85.0': + resolution: {integrity: sha512-q2KO/Zso9UT+OMn0NF9ywn4E4t0MI3yxiDhNyhsQ7DyQJrC4FhFE4TXOi4bktFnOWXTMds8qZSbpv2XwRaNOBg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.85.0': + resolution: {integrity: sha512-SxLN3ALjoT9NNdvpjEevGeHvfzTAFrF0NBYB5tzK7/GtCKMze3j1e/m/X2ozqGj2U9hfGG/dg/OG8vpVK4PiDA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.85.0': + resolution: {integrity: sha512-Y/Sup/J4f0f9UGsSd/xyCNTeWL+gepO63GBdEDAfue9nBsnk9zMmnIXx1O6b1V8C90vB5nucYNZ0pbMXAp8zJA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.85.0': + resolution: {integrity: sha512-ApOSNC04ynpDTwvBD+//0wyfODRSbEzvRoKpX8teffmc27z8AockwSNeMXGJXn5KP85eahDgR/2llICWLkzcnw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.85.0': + resolution: {integrity: sha512-bNrVrCOA/kHky3Tu79IXWXe5bhIgLXfUuUEDHlAGOHUk96MkvDZ1ecaQF19rwstrnaqfP1o9nBTqzIr9+ZHkUg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.85.0': + resolution: {integrity: sha512-NUrzOJ1s/EqsVvfn2L/1D8Wro2LPIZUbihL8kOJLh5fEdGEN3rdOGUYq3HwnUIL8sjpoP+4N6RaGrgmMJnaMPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.85.0': + resolution: {integrity: sha512-UJXrAT3E/RWkEqXLIs2ehETja1qfgkPb+5gwLIIS+o/6cf+grHvoOXTa5997a/YNQfcJS0DRBTOfZt95cvOI1g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.85.0': + resolution: {integrity: sha512-lK40QLjI0HxigO7CjDDshEtfYIeiYS0020v5BHFPqN4uuQBQxd2K9LNom2dW15o9F1937quSCRVp4ZsVhdbYdg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-arm64-musl@1.85.0': + resolution: {integrity: sha512-c2zbdBwGKreHXwRx3gWBuFGJxLhxgsg6YlZ+3H+RgRusU/UEV9jNwJ3HGYK+nRo0LvBa7mt6Kj86xoVotUo8cw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-ppc64-gnu@1.85.0': + resolution: {integrity: sha512-tlt/Hy8lZ97/lCPmCgw/B3k/mwh+BzaIPbPkldZEly7TwLmx0xe2CQcaW2g/rR0dOgS9JNGCZsMEqLhUNMGvaw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-gnu@1.85.0': + resolution: {integrity: sha512-3tNR9Xey82X0zKuY1d8hJ6Rc9gwRDurmqGLnQZa5xqOXy8/YyiqFXjAtugkKLY82obOlpK1eSiDRlgcNPuxtIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-musl@1.85.0': + resolution: {integrity: sha512-wbGRd5PqCcjkJFHhZuZ2OBSUQY9czlQsoA/cQQB9JK/L9mC5MQgGoKAh+xd8QjA5V+0D3j+Qd1lAWn1I8zlelA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-s390x-gnu@1.85.0': + resolution: {integrity: sha512-3Sn0kSrE4DPZCWV/8o+n4x3aFZxI9ulMnkYlwCbJ8eUVkwRK2IerohE/A/z3SNbCwoPFOCJmGE5Avrq0rrvdvQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-gnu@1.85.0': + resolution: {integrity: sha512-JY2pxxYfB62bAGfejljVCqc44etItehPuAyaeSAdMuEMtwNA00ggMnS66lC1oIhos6oOXUkuU6mZ9bpFh3BqWg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-musl@1.85.0': + resolution: {integrity: sha512-5k74vZ6qJBjBHEOlBk9B/iv68Yu0F1Afw/vvT2ar6OGCqEeXLaSjXz2n/IPCbhLG22UoKoYEJTzpYraRdcp6PA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxlint/binding-openharmony-arm64@1.85.0': + resolution: {integrity: sha512-GbAl5qt5TCkPLXTaIISZJnugrcBhra6rodcXc9jYt620UtdsTt71NlNmJmm0frxzFpd54x/G+MkitEJA8I/BoA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.85.0': + resolution: {integrity: sha512-kjmws5MK0et2swk4ND85D7NVQyDHw162i6whtZDLUA/lo6FQyBZDcmMRCMcVZcNrAhIaftVb00x9ChGDOjjNJA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.85.0': + resolution: {integrity: sha512-eSsIJx9n4yxvOqYTZyPEMyEXRmE60XH7xGAU7i0Qbsn1lf6Za3CWJ9aRd82oSFKXaxhp+sA6/yMJVRIpLpna6A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.85.0': + resolution: {integrity: sha512-pBebIPUpKKhWrhSMWhy8TdAZBewiXnfxmaAGxhzxM1068GagqFaTwgKlU6e+UyJ2sPR+VoHouhXuGJkQjsrDvA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@playwright/test@1.63.0': + resolution: {integrity: sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==} + engines: {node: '>=20'} + hasBin: true + + '@rolldown/binding-android-arm-eabi@1.2.11': + resolution: {integrity: sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.11': + resolution: {integrity: sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.11': + resolution: {integrity: sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.11': + resolution: {integrity: sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.11': + resolution: {integrity: sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.11': + resolution: {integrity: sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.11': + resolution: {integrity: sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.11': + resolution: {integrity: sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.11': + resolution: {integrity: sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.11': + resolution: {integrity: sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.11': + resolution: {integrity: sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.11': + resolution: {integrity: sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.11': + resolution: {integrity: sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.11': + resolution: {integrity: sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.11': + resolution: {integrity: sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} + + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.3': + resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@tauri-apps/api@2.11.1': + resolution: {integrity: sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==} + + '@tauri-apps/cli-darwin-arm64@2.11.5': + resolution: {integrity: sha512-tggOiVOohjIHdiElbjeBfB41s6cmfsq+ZQ0PX0fEtCdsDdZ9cCi54e5gNK7tdPbR6JbdDe7RA4kbQdyyBUUk/Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tauri-apps/cli-darwin-x64@2.11.5': + resolution: {integrity: sha512-SrO+KCbqvG1IvVPdlzQX4GEGZRQDISrmDY9bGcW68DLVFBCC06H+Loz6oyUZETUxsSMR6B4oDfvbQVgD4hxsqQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.5': + resolution: {integrity: sha512-qLrOaCa8hLD5QVPQRjAgOXvhlTFKXmTliVF773GgvADYcWZSdmncnF5zBFAkdu1uQK8KfQbDlBUwqQYZqBfBRQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tauri-apps/cli-linux-arm64-gnu@2.11.5': + resolution: {integrity: sha512-Tq32xpQjiQEdIrvwDq7lwHhXemn070CXWdbCvr9rdi9rsv9NFTbQ6izVSIfNE+59QaerxuwtUtZ5IxDNWaoYBw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-arm64-musl@2.11.5': + resolution: {integrity: sha512-+OdWKj5Aq+C8HXypQ4t926V8cpVnc0ZcJIdhTXWjVAJHkaNuo8EuQR6KRZKHWIbJJgKdMPCDosvwzIFaBBqHKQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-linux-riscv64-gnu@2.11.5': + resolution: {integrity: sha512-Q0V2hjpllyDW8dWQZSsITuQDfcY9BpBZuhXkMa2T5AFLZAn+FJPXQSyvnxWNVxRowdQ59Xn0QVtGm6HEHNUEvw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-gnu@2.11.5': + resolution: {integrity: sha512-Xz1s87gFjZJZMQKAhLa77YeiCJM6yVog3f9OxDgVd/KxDIoB1dA5V2xOnMM+SRkOGk9juwIBqesVAjV0kUB0DA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-musl@2.11.5': + resolution: {integrity: sha512-6A88wqAZXZHgMaBrGiy/Bpa4GgUnftXM7uquKOeOXCuK78/XdBxEECicK/1oHgrT179XBFOaA6lQEHysjRmGig==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-win32-arm64-msvc@2.11.5': + resolution: {integrity: sha512-sRGgF/ObRfLcS3PtUdyTbJtkGq3E+PNZTknvAURwMNUtHw9DU/BF6S5si43y9lS5yEO9bWJCA+GGYvnn6cphQw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tauri-apps/cli-win32-ia32-msvc@2.11.5': + resolution: {integrity: sha512-bHOv/yxqfKpI7ioIFQi5wOfZ7jkbPzJD/UhXY52I/hrmE3qNh71cohIPyqZB5GQn3LZeJWdCUOqmIg/w6XX+Wg==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@tauri-apps/cli-win32-x64-msvc@2.11.5': + resolution: {integrity: sha512-vaaOmavqdCeS7oMib2banBZemgrC5v54tSkwoBV+bxrXQZhQZrOwE2EO2i4x+CHNtvQUM/6bSs3iymZMAsycWg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tauri-apps/cli@2.11.5': + resolution: {integrity: sha512-YbeJ6tctNoo40purO4u3Eeo3RVqmsjJr0NoKQRu+4pvO4gMLzkjta7x0k96SxSLmx+rQ+8um1ThOlw+HLpgFug==} + engines: {node: '>= 10'} + hasBin: true + + '@tauri-apps/plugin-dialog@2.7.3': + resolution: {integrity: sha512-CRgE+7TP4tvq9MjBU6f04NLTFIqVMLKHk3hAqlhil00ngK9ACTrXPH3oHpKMProxILodd3YjBoKbMwSI4IEcfA==} + + '@tauri-apps/plugin-fs@2.5.2': + resolution: {integrity: sha512-XXvMSnFiob+G1H+YHCDf+bzWVumseQuEIhzpbOJzevUfL4k0U+sTApZWJHpoLiamggnVPJm5dJ5sDsniRyWxlg==} + + '@tauri-apps/plugin-opener@2.5.5': + resolution: {integrity: sha512-xvzGai5aQds8j8R8RsUK/lW6pGG50YgOYIPLzvkqmkwAj7dfySOD7sGtejRzvVdMmv1EQfKVEFh1MvmDp8QR0g==} + + '@tauri-apps/plugin-store@2.4.5': + resolution: {integrity: sha512-bVD9riKzf+WKYm1L8uXD1CMDTC/eaFS2Cbjcgq+39ahChtVGnyDCbzug3HpjgpZMw78yoAVMqHOAEjqJ/kD9YQ==} + + '@tauri-apps/plugin-window-state@2.4.1': + resolution: {integrity: sha512-OuvdrzyY8Q5Dbzpj+GcrnV1iCeoZbcFdzMjanZMMcAEUNy/6PH5pxZPXpaZLOR7whlzXiuzx0L9EKZbH7zpdRw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@24.13.6': + resolution: {integrity: sha512-SGrw/h3KPFshy3OE6ZL53LMBG5vGQQ8/gIpiqz/kRZhPJ7HgwCEs8LBuNtWLa8dvGZVpSF7+Bf+c11HUrCb/yg==} + + '@types/react-dom@19.3.0': + resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} + peerDependencies: + '@types/react': ^19.3.0 + + '@types/react@19.3.0': + resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} + + '@typescript/typescript-aix-ppc64@7.0.2': + resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [aix] + + '@typescript/typescript-darwin-arm64@7.0.2': + resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [darwin] + + '@typescript/typescript-darwin-x64@7.0.2': + resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [darwin] + + '@typescript/typescript-freebsd-arm64@7.0.2': + resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [freebsd] + + '@typescript/typescript-freebsd-x64@7.0.2': + resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [freebsd] + + '@typescript/typescript-linux-arm64@7.0.2': + resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [linux] + + '@typescript/typescript-linux-arm@7.0.2': + resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} + engines: {node: '>=16.20.0'} + cpu: [arm] + os: [linux] + + '@typescript/typescript-linux-loong64@7.0.2': + resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} + engines: {node: '>=16.20.0'} + cpu: [loong64] + os: [linux] + + '@typescript/typescript-linux-mips64el@7.0.2': + resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} + engines: {node: '>=16.20.0'} + cpu: [mips64el] + os: [linux] + + '@typescript/typescript-linux-ppc64@7.0.2': + resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [linux] + + '@typescript/typescript-linux-riscv64@7.0.2': + resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} + engines: {node: '>=16.20.0'} + cpu: [riscv64] + os: [linux] + + '@typescript/typescript-linux-s390x@7.0.2': + resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} + engines: {node: '>=16.20.0'} + cpu: [s390x] + os: [linux] + + '@typescript/typescript-linux-x64@7.0.2': + resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [linux] + + '@typescript/typescript-netbsd-arm64@7.0.2': + resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [netbsd] + + '@typescript/typescript-netbsd-x64@7.0.2': + resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [netbsd] + + '@typescript/typescript-openbsd-arm64@7.0.2': + resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [openbsd] + + '@typescript/typescript-openbsd-x64@7.0.2': + resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [openbsd] + + '@typescript/typescript-sunos-x64@7.0.2': + resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [sunos] + + '@typescript/typescript-win32-arm64@7.0.2': + resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [win32] + + '@typescript/typescript-win32-x64@7.0.2': + resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [win32] + + '@vitejs/plugin-react@6.1.1': + resolution: {integrity: sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + oxc-transform-react: ^0.145.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + oxc-transform-react: + optional: true + + '@vitest/mocker@5.0.1': + resolution: {integrity: sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/spy@5.0.1': + resolution: {integrity: sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + enhanced-resolve@5.25.1: + resolution: {integrity: sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w==} + engines: {node: '>=10.13.0'} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-png@8.0.0: + resolution: {integrity: sha512-gCysNasJ8KEMgfdYIKd/wTDo6ENK1PWT0RJO7O+0pgmuHPw2O6tA1WvdxFRJoLf9V8yFYpG0FA1YgI8X97OhJA==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + iobuffer@6.0.1: + resolution: {integrity: sha512-SZWYkWNfjIXIBYSDpXDYIgshqtbOPsi4lviawAEceR1Kqk+sHDlcQjWrzNQsii80AyBY0q5c8HCTNjqo74ul+Q==} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + lucide-react@1.48.0: + resolution: {integrity: sha512-R0CIKY/fXiC6y9xRBADgsK+VW2p/pcTJOMhLZf1T+uG+vJUvyUR02nGOgmIIC7MPkiNK4Ox8QDnbqF8rJYWPZQ==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magic-string@1.4.2: + resolution: {integrity: sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==} + + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + obug@2.2.1: + resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} + engines: {node: '>=12.20.0'} + + oxfmt@0.70.0: + resolution: {integrity: sha512-IsHxZ4y0wQLLMhnrJblBJgZsLDzfULrJnAw5j/QqsTlMa/m3AqsbToi+W71uhBGaqlqq/PbbjvHc09TJwdv3Tw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + svelte: ^5.0.0 + vite-plus: '*' + peerDependenciesMeta: + svelte: + optional: true + vite-plus: + optional: true + + oxlint-tsgolint@7.0.2003: + resolution: {integrity: sha512-VnK4zlqgmgq/7ZcjzCk/WpN8kKFsYGcB85Io9qT3wL4K8Un3RKmJkp793crNETieMusPMKOA4a+Mw2wbteI6TQ==} + hasBin: true + + oxlint@1.85.0: + resolution: {integrity: sha512-bc26s97nuvPj1ViyPsqmKecVkUWFMEdtayO8MaQ6oiLfs1pj94cQlZZhrh4BPNlr9HQosjhIlwgZKsfcwmcNgg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=7.0.2001' + vite-plus: '*' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + vite-plus: + optional: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + playwright-core@1.63.0: + resolution: {integrity: sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.63.0: + resolution: {integrity: sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==} + engines: {node: '>=20'} + hasBin: true + + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + + react-dom@19.3.0: + resolution: {integrity: sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==} + peerDependencies: + react: ^19.3.0 + + react@19.3.0: + resolution: {integrity: sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==} + engines: {node: '>=0.10.0'} + + reselect@5.3.0: + resolution: {integrity: sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg==} + + rolldown@1.2.11: + resolution: {integrity: sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + scheduler@0.28.0: + resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + tinybench@6.1.4: + resolution: {integrity: sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==} + engines: {node: '>=20.0.0'} + + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@2.1.2: + resolution: {integrity: sha512-9YodfrxS9g9IbFr/KOjE5bAeJ0p61n3bW6mqvy0jtoeKd1kTW1Cxm0oulm6KX2lyM9Gl6WIe8nEbY7LWv5ZJww==} + engines: {node: ^20.0.0 || >=22.0.0} + + typescript@7.0.2: + resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} + engines: {node: '>=16.20.0'} + hasBin: true + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + use-sync-external-store@1.7.0: + resolution: {integrity: sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + vite@8.3.1: + resolution: {integrity: sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.7.1 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@5.0.1: + resolution: {integrity: sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==} + engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 5.0.1 + '@vitest/browser-preview': 5.0.1 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + '@vitest/coverage-istanbul': 5.0.1 + '@vitest/coverage-v8': 5.0.1 + '@vitest/ui': 5.0.1 + happy-dom: '*' + jsdom: '*' + vite: ^6.4.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + zustand@5.0.15: + resolution: {integrity: sha512-MpSEjRiBkA9crSYeOUH32rJC7SVqAbm0Fqcqge/bUi2PPoLcBWKOsG+C8mevmpr8TwXHBVkChbbJiyvkE+i/3A==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + +snapshots: + + '@babel/runtime@7.29.7': {} + + '@base-ui/react@1.8.0(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + dependencies: + '@babel/runtime': 7.29.7 + '@base-ui/utils': 0.4.0(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@floating-ui/react-dom': 2.1.9(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@floating-ui/utils': 0.2.12 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + use-sync-external-store: 1.7.0(react@19.3.0) + optionalDependencies: + '@types/react': 19.3.0 + + '@base-ui/utils@0.4.0(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + dependencies: + '@babel/runtime': 7.29.7 + '@floating-ui/utils': 0.2.12 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + reselect: 5.3.0 + use-sync-external-store: 1.7.0(react@19.3.0) + optionalDependencies: + '@types/react': 19.3.0 + + '@floating-ui/core@1.8.0': + dependencies: + '@floating-ui/utils': 0.2.12 + + '@floating-ui/dom@1.8.0': + dependencies: + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 + + '@floating-ui/react-dom@2.1.9(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + dependencies: + '@floating-ui/dom': 1.8.0 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + + '@floating-ui/utils@0.2.12': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@oxc-project/types@0.151.0': {} + + '@oxfmt/binding-android-arm-eabi@0.70.0': + optional: true + + '@oxfmt/binding-android-arm64@0.70.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.70.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.70.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.70.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.70.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.70.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.70.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.70.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.70.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.70.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.70.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.70.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.70.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.70.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.70.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.70.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.70.0': + optional: true + + '@oxfmt/binding-win32-x64-msvc@0.70.0': + optional: true + + '@oxlint-tsgolint/darwin-arm64@7.0.2003': + optional: true + + '@oxlint-tsgolint/darwin-x64@7.0.2003': + optional: true + + '@oxlint-tsgolint/linux-arm64@7.0.2003': + optional: true + + '@oxlint-tsgolint/linux-x64@7.0.2003': + optional: true + + '@oxlint-tsgolint/win32-arm64@7.0.2003': + optional: true + + '@oxlint-tsgolint/win32-x64@7.0.2003': + optional: true + + '@oxlint/binding-android-arm-eabi@1.85.0': + optional: true + + '@oxlint/binding-android-arm64@1.85.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.85.0': + optional: true + + '@oxlint/binding-darwin-x64@1.85.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.85.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.85.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.85.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.85.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.85.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.85.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.85.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.85.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.85.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.85.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.85.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.85.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.85.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.85.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.85.0': + optional: true + + '@playwright/test@1.63.0': + dependencies: + playwright: 1.63.0 + + '@rolldown/binding-android-arm-eabi@1.2.11': + optional: true + + '@rolldown/binding-android-arm64@1.2.11': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.11': + optional: true + + '@rolldown/binding-darwin-x64@1.2.11': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.11': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.11': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.11': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.11': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.11': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.11': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.11': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.11': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.11': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.11': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.11': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@tailwindcss/node@4.3.3': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.25.1 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.3 + + '@tailwindcss/oxide-android-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide@4.3.3': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-x64': 4.3.3 + '@tailwindcss/oxide-freebsd-x64': 4.3.3 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-x64-musl': 4.3.3 + '@tailwindcss/oxide-wasm32-wasi': 4.3.3 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 + + '@tailwindcss/vite@4.3.3(vite@8.3.1(@types/node@24.13.6)(jiti@2.7.0))': + dependencies: + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + tailwindcss: 4.3.3 + vite: 8.3.1(@types/node@24.13.6)(jiti@2.7.0) + + '@tauri-apps/api@2.11.1': {} + + '@tauri-apps/cli-darwin-arm64@2.11.5': + optional: true + + '@tauri-apps/cli-darwin-x64@2.11.5': + optional: true + + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.5': + optional: true + + '@tauri-apps/cli-linux-arm64-gnu@2.11.5': + optional: true + + '@tauri-apps/cli-linux-arm64-musl@2.11.5': + optional: true + + '@tauri-apps/cli-linux-riscv64-gnu@2.11.5': + optional: true + + '@tauri-apps/cli-linux-x64-gnu@2.11.5': + optional: true + + '@tauri-apps/cli-linux-x64-musl@2.11.5': + optional: true + + '@tauri-apps/cli-win32-arm64-msvc@2.11.5': + optional: true + + '@tauri-apps/cli-win32-ia32-msvc@2.11.5': + optional: true + + '@tauri-apps/cli-win32-x64-msvc@2.11.5': + optional: true + + '@tauri-apps/cli@2.11.5': + optionalDependencies: + '@tauri-apps/cli-darwin-arm64': 2.11.5 + '@tauri-apps/cli-darwin-x64': 2.11.5 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.11.5 + '@tauri-apps/cli-linux-arm64-gnu': 2.11.5 + '@tauri-apps/cli-linux-arm64-musl': 2.11.5 + '@tauri-apps/cli-linux-riscv64-gnu': 2.11.5 + '@tauri-apps/cli-linux-x64-gnu': 2.11.5 + '@tauri-apps/cli-linux-x64-musl': 2.11.5 + '@tauri-apps/cli-win32-arm64-msvc': 2.11.5 + '@tauri-apps/cli-win32-ia32-msvc': 2.11.5 + '@tauri-apps/cli-win32-x64-msvc': 2.11.5 + + '@tauri-apps/plugin-dialog@2.7.3': + dependencies: + '@tauri-apps/api': 2.11.1 + + '@tauri-apps/plugin-fs@2.5.2': + dependencies: + '@tauri-apps/api': 2.11.1 + + '@tauri-apps/plugin-opener@2.5.5': + dependencies: + '@tauri-apps/api': 2.11.1 + + '@tauri-apps/plugin-store@2.4.5': + dependencies: + '@tauri-apps/api': 2.11.1 + + '@tauri-apps/plugin-window-state@2.4.1': + dependencies: + '@tauri-apps/api': 2.11.1 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/node@24.13.6': + dependencies: + undici-types: 7.18.2 + + '@types/react-dom@19.3.0(@types/react@19.3.0)': + dependencies: + '@types/react': 19.3.0 + + '@types/react@19.3.0': + dependencies: + csstype: 3.2.3 + + '@typescript/typescript-aix-ppc64@7.0.2': + optional: true + + '@typescript/typescript-darwin-arm64@7.0.2': + optional: true + + '@typescript/typescript-darwin-x64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-x64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm@7.0.2': + optional: true + + '@typescript/typescript-linux-loong64@7.0.2': + optional: true + + '@typescript/typescript-linux-mips64el@7.0.2': + optional: true + + '@typescript/typescript-linux-ppc64@7.0.2': + optional: true + + '@typescript/typescript-linux-riscv64@7.0.2': + optional: true + + '@typescript/typescript-linux-s390x@7.0.2': + optional: true + + '@typescript/typescript-linux-x64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-sunos-x64@7.0.2': + optional: true + + '@typescript/typescript-win32-arm64@7.0.2': + optional: true + + '@typescript/typescript-win32-x64@7.0.2': + optional: true + + '@vitejs/plugin-react@6.1.1(vite@8.3.1(@types/node@24.13.6)(jiti@2.7.0))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.3.1(@types/node@24.13.6)(jiti@2.7.0) + + '@vitest/mocker@5.0.1(vite@8.3.1(@types/node@24.13.6)(jiti@2.7.0))': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.1 + estree-walker: 3.0.3 + magic-string: 1.4.2 + optionalDependencies: + vite: 8.3.1(@types/node@24.13.6)(jiti@2.7.0) + + '@vitest/spy@5.0.1': {} + + assertion-error@2.0.1: {} + + chai@6.2.2: {} + + csstype@3.2.3: {} + + detect-libc@2.1.2: {} + + enhanced-resolve@5.25.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + es-module-lexer@2.3.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + + fast-png@8.0.0: + dependencies: + fflate: 0.8.3 + iobuffer: 6.0.1 + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fflate@0.8.3: {} + + fsevents@2.3.3: + optional: true + + graceful-fs@4.2.11: {} + + iobuffer@6.0.1: {} + + jiti@2.7.0: {} + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + lucide-react@1.48.0(react@19.3.0): + dependencies: + react: 19.3.0 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + magic-string@1.4.2: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + nanoid@3.3.19: {} + + obug@2.2.1: {} + + oxfmt@0.70.0: + dependencies: + tinypool: 2.1.2 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.70.0 + '@oxfmt/binding-android-arm64': 0.70.0 + '@oxfmt/binding-darwin-arm64': 0.70.0 + '@oxfmt/binding-darwin-x64': 0.70.0 + '@oxfmt/binding-freebsd-x64': 0.70.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.70.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.70.0 + '@oxfmt/binding-linux-arm64-gnu': 0.70.0 + '@oxfmt/binding-linux-arm64-musl': 0.70.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.70.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.70.0 + '@oxfmt/binding-linux-riscv64-musl': 0.70.0 + '@oxfmt/binding-linux-s390x-gnu': 0.70.0 + '@oxfmt/binding-linux-x64-gnu': 0.70.0 + '@oxfmt/binding-linux-x64-musl': 0.70.0 + '@oxfmt/binding-openharmony-arm64': 0.70.0 + '@oxfmt/binding-win32-arm64-msvc': 0.70.0 + '@oxfmt/binding-win32-ia32-msvc': 0.70.0 + '@oxfmt/binding-win32-x64-msvc': 0.70.0 + + oxlint-tsgolint@7.0.2003: + optionalDependencies: + '@oxlint-tsgolint/darwin-arm64': 7.0.2003 + '@oxlint-tsgolint/darwin-x64': 7.0.2003 + '@oxlint-tsgolint/linux-arm64': 7.0.2003 + '@oxlint-tsgolint/linux-x64': 7.0.2003 + '@oxlint-tsgolint/win32-arm64': 7.0.2003 + '@oxlint-tsgolint/win32-x64': 7.0.2003 + + oxlint@1.85.0(oxlint-tsgolint@7.0.2003): + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.85.0 + '@oxlint/binding-android-arm64': 1.85.0 + '@oxlint/binding-darwin-arm64': 1.85.0 + '@oxlint/binding-darwin-x64': 1.85.0 + '@oxlint/binding-freebsd-x64': 1.85.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.85.0 + '@oxlint/binding-linux-arm-musleabihf': 1.85.0 + '@oxlint/binding-linux-arm64-gnu': 1.85.0 + '@oxlint/binding-linux-arm64-musl': 1.85.0 + '@oxlint/binding-linux-ppc64-gnu': 1.85.0 + '@oxlint/binding-linux-riscv64-gnu': 1.85.0 + '@oxlint/binding-linux-riscv64-musl': 1.85.0 + '@oxlint/binding-linux-s390x-gnu': 1.85.0 + '@oxlint/binding-linux-x64-gnu': 1.85.0 + '@oxlint/binding-linux-x64-musl': 1.85.0 + '@oxlint/binding-openharmony-arm64': 1.85.0 + '@oxlint/binding-win32-arm64-msvc': 1.85.0 + '@oxlint/binding-win32-ia32-msvc': 1.85.0 + '@oxlint/binding-win32-x64-msvc': 1.85.0 + oxlint-tsgolint: 7.0.2003 + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + playwright-core@1.63.0: {} + + playwright@1.63.0: + dependencies: + playwright-core: 1.63.0 + + postcss@8.5.28: + dependencies: + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + react-dom@19.3.0(react@19.3.0): + dependencies: + react: 19.3.0 + scheduler: 0.28.0 + + react@19.3.0: {} + + reselect@5.3.0: {} + + rolldown@1.2.11: + dependencies: + '@oxc-project/types': 0.151.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.11 + '@rolldown/binding-android-arm64': 1.2.11 + '@rolldown/binding-darwin-arm64': 1.2.11 + '@rolldown/binding-darwin-x64': 1.2.11 + '@rolldown/binding-freebsd-x64': 1.2.11 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.11 + '@rolldown/binding-linux-arm64-gnu': 1.2.11 + '@rolldown/binding-linux-arm64-musl': 1.2.11 + '@rolldown/binding-linux-ppc64-gnu': 1.2.11 + '@rolldown/binding-linux-s390x-gnu': 1.2.11 + '@rolldown/binding-linux-x64-gnu': 1.2.11 + '@rolldown/binding-linux-x64-musl': 1.2.11 + '@rolldown/binding-openharmony-arm64': 1.2.11 + '@rolldown/binding-win32-arm64-msvc': 1.2.11 + '@rolldown/binding-win32-x64-msvc': 1.2.11 + + scheduler@0.28.0: {} + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@4.2.0: {} + + tailwindcss@4.3.3: {} + + tapable@2.3.3: {} + + tinybench@6.1.4: {} + + tinyexec@1.3.0: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinypool@2.1.2: {} + + typescript@7.0.2: + optionalDependencies: + '@typescript/typescript-aix-ppc64': 7.0.2 + '@typescript/typescript-darwin-arm64': 7.0.2 + '@typescript/typescript-darwin-x64': 7.0.2 + '@typescript/typescript-freebsd-arm64': 7.0.2 + '@typescript/typescript-freebsd-x64': 7.0.2 + '@typescript/typescript-linux-arm': 7.0.2 + '@typescript/typescript-linux-arm64': 7.0.2 + '@typescript/typescript-linux-loong64': 7.0.2 + '@typescript/typescript-linux-mips64el': 7.0.2 + '@typescript/typescript-linux-ppc64': 7.0.2 + '@typescript/typescript-linux-riscv64': 7.0.2 + '@typescript/typescript-linux-s390x': 7.0.2 + '@typescript/typescript-linux-x64': 7.0.2 + '@typescript/typescript-netbsd-arm64': 7.0.2 + '@typescript/typescript-netbsd-x64': 7.0.2 + '@typescript/typescript-openbsd-arm64': 7.0.2 + '@typescript/typescript-openbsd-x64': 7.0.2 + '@typescript/typescript-sunos-x64': 7.0.2 + '@typescript/typescript-win32-arm64': 7.0.2 + '@typescript/typescript-win32-x64': 7.0.2 + + undici-types@7.18.2: {} + + use-sync-external-store@1.7.0(react@19.3.0): + dependencies: + react: 19.3.0 + + vite@8.3.1(@types/node@24.13.6)(jiti@2.7.0): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.28 + rolldown: 1.2.11 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.13.6 + fsevents: 2.3.3 + jiti: 2.7.0 + + vitest@5.0.1(@types/node@24.13.6)(vite@8.3.1(@types/node@24.13.6)(jiti@2.7.0)): + dependencies: + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.1(vite@8.3.1(@types/node@24.13.6)(jiti@2.7.0)) + chai: 6.2.2 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 1.4.2 + obug: 2.2.1 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 6.1.4 + tinyexec: 1.3.0 + tinyglobby: 0.2.17 + vite: 8.3.1(@types/node@24.13.6)(jiti@2.7.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.13.6 + transitivePeerDependencies: + - msw + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + zustand@5.0.15(@types/react@19.3.0)(react@19.3.0)(use-sync-external-store@1.7.0(react@19.3.0)): + optionalDependencies: + '@types/react': 19.3.0 + react: 19.3.0 + use-sync-external-store: 1.7.0(react@19.3.0) diff --git a/img/demo.png b/public/examples/autotiler13-16px.png similarity index 100% rename from img/demo.png rename to public/examples/autotiler13-16px.png diff --git a/public/examples/autotiler13-32px.png b/public/examples/autotiler13-32px.png new file mode 100644 index 0000000..14cd440 Binary files /dev/null and b/public/examples/autotiler13-32px.png differ diff --git a/img/demo-8px.png b/public/examples/autotiler13-8px.png similarity index 100% rename from img/demo-8px.png rename to public/examples/autotiler13-8px.png diff --git a/public/examples/rpgmaker-a2-16px.png b/public/examples/rpgmaker-a2-16px.png new file mode 100644 index 0000000..a6e337e Binary files /dev/null and b/public/examples/rpgmaker-a2-16px.png differ diff --git a/img/favicon.png b/public/favicon.png similarity index 100% rename from img/favicon.png rename to public/favicon.png diff --git a/publish.js b/publish.js deleted file mode 100644 index fa20955..0000000 --- a/publish.js +++ /dev/null @@ -1,21 +0,0 @@ -const pkg = require('./package.json'); -const {exec} = require("child_process"); -const distDir = __dirname + '/dist'; - -const execHandler = (error, stdout, stderr) => { - if (error) { - console.log(`error: ${error.message}`); - process.exit(1); - return; - } - if (stderr) { - console.log(`stderr: ${stderr}`); - return; - } - console.log(`stdout: ${stdout}`); -}; - -//exec(`butler push ${distDir}/autotiler-mac.dmg ${pkg.butler.project}:macos-universal --userversion ${pkg.version}`, execHandler); -exec(`butler push ${distDir}/autotiler-mac.app ${pkg.butler.project}:macos-universal --userversion ${pkg.version}`, execHandler); -exec(`butler push ${distDir}/autotiler-win.exe ${pkg.butler.project}:windows --userversion ${pkg.version}`, execHandler); -exec(`butler push ${distDir}/autotiler-linux.AppImage ${pkg.butler.project}:linux-universal --userversion ${pkg.version}`, execHandler); diff --git a/renderer.js b/renderer.js deleted file mode 100644 index bab6b4f..0000000 --- a/renderer.js +++ /dev/null @@ -1,6 +0,0 @@ -// This file is required by the index.html file and will -// be executed in the renderer process for that window. -// No Node.js APIs are available in this process because -// `nodeIntegration` is turned off. Use `app.js` to -// selectively enable features needed in the rendering -// process. diff --git a/resources/demo-output-resorted.png b/resources/demo-output-resorted.png deleted file mode 100644 index 5cd3a51..0000000 Binary files a/resources/demo-output-resorted.png and /dev/null differ diff --git a/resources/demo-output.png b/resources/demo-output.png deleted file mode 100644 index 64c2cb2..0000000 Binary files a/resources/demo-output.png and /dev/null differ diff --git a/resources/guide-bitmask-2.png b/resources/guide-bitmask-2.png deleted file mode 100644 index 13b117b..0000000 Binary files a/resources/guide-bitmask-2.png and /dev/null differ diff --git a/resources/guide-bitmask-3.png b/resources/guide-bitmask-3.png deleted file mode 100644 index edb164a..0000000 Binary files a/resources/guide-bitmask-3.png and /dev/null differ diff --git a/resources/guide-bitmask.png b/resources/guide-bitmask.png deleted file mode 100644 index 9d92b02..0000000 Binary files a/resources/guide-bitmask.png and /dev/null differ diff --git a/resources/guide-collisions.png b/resources/guide-collisions.png deleted file mode 100644 index 2099586..0000000 Binary files a/resources/guide-collisions.png and /dev/null differ diff --git a/resources/templates/tileset.png b/resources/templates/tileset.png deleted file mode 100644 index 9eb286d..0000000 Binary files a/resources/templates/tileset.png and /dev/null differ diff --git a/resources/templates/tileset.png.import b/resources/templates/tileset.png.import deleted file mode 100644 index a039fe2..0000000 --- a/resources/templates/tileset.png.import +++ /dev/null @@ -1,34 +0,0 @@ -[remap] - -importer="texture" -type="StreamTexture" -path="res://.import/$IMGFILE-.stex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://$IMGFILE" -dest_files=[ "res://.import/$IMGFILE-.stex" ] - -[params] - -compress/mode=0 -compress/lossy_quality=0.7 -compress/hdr_mode=0 -compress/bptc_ldr=0 -compress/normal_map=0 -flags/repeat=0 -flags/filter=false -flags/mipmaps=false -flags/anisotropic=false -flags/srgb=2 -process/fix_alpha_border=true -process/premult_alpha=false -process/HDR_as_SRGB=false -process/invert_color=false -stream=false -size_limit=0 -detect_3d=true -svg/scale=1.0 diff --git a/scripts/screenshots.ts b/scripts/screenshots.ts new file mode 100644 index 0000000..a690273 --- /dev/null +++ b/scripts/screenshots.ts @@ -0,0 +1,47 @@ +// Regenerates the README images in docs/images: screenshots of the running app and enlarged blank templates. +// Usage: `pnpm dev` (or `pnpm build && pnpm preview`), then `node scripts/screenshots.ts [url]`. +import { writeFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; + +import { chromium } from "@playwright/test"; + +import { createImage, encodePng, renderGuide, TEMPLATES, type RgbaImage } from "../src/core/index.ts"; + +const url = process.argv[2] ?? "http://localhost:5173/"; +const out = (name: string) => fileURLToPath(new URL(`../docs/images/${name}`, import.meta.url)); + +/** Nearest-neighbour enlargement, for crisp pixels on GitHub (which drops image-rendering styles). */ +function enlarge(img: RgbaImage, k: number): RgbaImage { + const big = createImage(img.width * k, img.height * k); + for (let y = 0; y < big.height; y++) { + for (let x = 0; x < big.width; x++) { + const from = (Math.floor(y / k) * img.width + Math.floor(x / k)) * 4; + big.data.set(img.data.subarray(from, from + 4), (y * big.width + x) * 4); + } + } + return big; +} + +for (const template of TEMPLATES) { + writeFileSync(out(`template-${template.id}.png`), encodePng(enlarge(renderGuide(template, 16), 4))); +} + +const browser = await chromium.launch(); +const page = await browser.newPage({ + viewport: { width: 1440, height: 860 }, + deviceScaleFactor: 2, + colorScheme: "dark", +}); +await page.goto(url); +await page.getByRole("button", { name: /Beveled block, 16 px Autotiler/ }).click(); +const tileset = page.getByRole("img", { name: /Generated tileset/ }); +await tileset.waitFor(); +const box = await tileset.boundingBox(); +if (!box) throw new Error("the tileset isn't visible"); +await page.mouse.move(box.x + box.width * (4.5 / 12), box.y + box.height * (1.5 / 4)); // hover a tile: inspector +await page.screenshot({ path: out("app.png") }); + +await page.getByRole("tab", { name: "Test map" }).click(); +await page.getByRole("img", { name: /Test map/ }).waitFor(); +await page.getByRole("main").screenshot({ path: out("test-map.png") }); +await browser.close(); diff --git a/scripts/update-golden.ts b/scripts/update-golden.ts new file mode 100644 index 0000000..bd03506 --- /dev/null +++ b/scripts/update-golden.ts @@ -0,0 +1,24 @@ +// Regenerates golden images from the current generator. Run it only for intentional changes and review the diff. +// pnpm golden:update # rewrite every tests/fixtures/expected/..png +// pnpm golden:update demo-16.gamemaker-47 # also create new cases +import { readdirSync } from "node:fs"; + +import { generate, getLayout, getTemplate, isLayoutId } from "../src/core/index.ts"; +import { fixturesDir, parseInputName, readFixture, writeFixture } from "../tests/helpers.ts"; + +const existing = readdirSync(`${fixturesDir}expected`) + .filter((f) => f.endsWith(".png") && !f.endsWith(".actual.png") && !f.endsWith(".diff.png")) + .map((f) => f.replace(/\.png$/, "")); + +for (const key of new Set([...existing, ...process.argv.slice(2)])) { + const [input, layoutId] = key.split("."); + if (!isLayoutId(layoutId)) throw new Error(`unknown layout in ${key}`); + const { templateId, tileSize } = parseInputName(`${input}.png`); + const tileset = generate(readFixture(`inputs/${input}.png`), { + template: getTemplate(templateId), + tileSize, + layout: getLayout(layoutId), + }); + writeFixture(`expected/${key}.png`, tileset.image); + console.log(`wrote expected/${key}.png`); +} diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore new file mode 100644 index 0000000..502406b --- /dev/null +++ b/src-tauri/.gitignore @@ -0,0 +1,4 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +/gen/schemas diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock new file mode 100644 index 0000000..2ca9ebb --- /dev/null +++ b/src-tauri/Cargo.lock @@ -0,0 +1,5331 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "autotiler" +version = "2.0.0" +dependencies = [ + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-fs", + "tauri-plugin-opener", + "tauri-plugin-persisted-scope", + "tauri-plugin-store", + "tauri-plugin-window-state", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.2", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbad30e4b4c14a39e3cc8aed085a12a327257c316619c93581e017bc52be591" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.21", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.2", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 3.0.6", +] + +[[package]] +name = "darling_macro" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" +dependencies = [ + "darling_core", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.21", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.6+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "file-id" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fc6a637b6dc58414714eddd9170ff187ecb0933d4c7024d1abbd23a3cc26e9" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "inotify" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cc00ea907cab49550b7da656f80ebb97be1b997d931fbcd28d39734e17ce592" +dependencies = [ + "bitflags 2.13.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" +dependencies = [ + "libc", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jiff" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link 0.2.1", +] + +[[package]] +name = "jiff-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" +dependencies = [ + "defmt", + "log", +] + +[[package]] +name = "jiff-static" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.2", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "kqueue" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d763e5b24120b4ddf50de6c92308156765aabfbbccebf401da7cff2d70a41ea" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags 2.13.2", + "libc", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.21", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.2", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.13.2", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-debouncer-full" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375bd3a138be7bfeff3480e4a623df4cbfb55b79df617c055cd810ba466fa078" +dependencies = [ + "file-id", + "log", + "notify", + "notify-types", + "walkdir", +] + +[[package]] +name = "notify-types" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags 2.13.2", + "serde", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.2", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "open" +version = "5.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa576c76302b7b808eecc68061e67336c47833ef9d22caa74dda10fa9675eebc" +dependencies = [ + "dunce", + "is-wsl", + "libc", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plist" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896bade328c13f7042a297ea5ac5b0951f6cf989dea5f32c2fd98da398195cb" +dependencies = [ + "base64 0.23.1", + "indexmap 2.14.2", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.15+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_users" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +dependencies = [ + "libredox", + "thiserror 2.0.21", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.119", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.2", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "935177bb8c0cd8ca1a4e6d1a2ac8988bea69cab4f9d3a31311e012ad27868ea4" +dependencies = [ + "base64 0.23.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.2", + "jiff", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d607aa01a3cb0ad757d6fd216136910db3c97b102fe686585689615a02dbcdc" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.2", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa5bacdb9bbad5954af3d1bd6cf6ae9192cab1b2e270f4a07f904610b9e85f4" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.21", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.119", + "tauri-utils", + "thiserror 2.0.21", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61854a36651aa48381e5e209f69a01273b77f3f9f91f0c430b1b98d33bd47229" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.21", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de22eef34fd78c0da050e748710edd50bf127e651d02ea1b2bfada1523cc5c51" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "notify", + "notify-debouncer-full", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.21", + "toml 1.1.6+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d60366174b745b4ef5824b8bbc1c457fd08f0ce101ff643c0a49181a9f4e91" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.21", + "url", + "windows", + "zbus", +] + +[[package]] +name = "tauri-plugin-persisted-scope" +version = "2.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3455c9b9d86158aa9acf596b2385fd99f3963e16fce0f5a613020b4388a36770" +dependencies = [ + "aho-corasick", + "bincode", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin-fs", + "thiserror 2.0.21", +] + +[[package]] +name = "tauri-plugin-store" +version = "2.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2c7c771d3ce796b48c6e52b2fac38087d479a3c58e0424d3850cc21db6b4a69" +dependencies = [ + "dunce", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.21", + "tokio", + "tracing", +] + +[[package]] +name = "tauri-plugin-window-state" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704" +dependencies = [ + "bitflags 2.13.2", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.21", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.21", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.21", + "toml 1.1.6+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.6+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.2", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" +dependencies = [ + "indexmap 2.14.2", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.2", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.2", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.15+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" +dependencies = [ + "indexmap 2.14.2", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.2", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "045979e3f037cd18ad1cb2a419dfda133c5c29c9f3453370079f2255d46c257e" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.21", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.21", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.21", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.4", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 3.0.6", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" +dependencies = [ + "serde", + "winnow 1.0.4", + "zvariant", +] + +[[package]] +name = "zcheapstr" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" +dependencies = [ + "serde", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zvariant" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.4", + "zcheapstr", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 3.0.6", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 3.0.6", + "winnow 1.0.4", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..5c6ed1a --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "autotiler" +version = "2.0.0" +description = "Autotile tileset generator with Godot 4, Tiled and dual-grid export" +authors = ["route1rodent "] +license = "MIT" +repository = "https://github.com/itsjavi/autotiler" +edition = "2021" +rust-version = "1.77.2" + +[lib] +name = "autotiler_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2.6", features = [] } + +[dependencies] +# >= 2.11.6: IPC security fix (GHSA-w28w-mhc8-qvjv). Plugin crates must match the npm packages' major.minor. +tauri = { version = "2.11.6", features = [] } +tauri-plugin-dialog = "2.7" +tauri-plugin-fs = { version = "2.5", features = ["watch"] } +tauri-plugin-opener = "2.5" +tauri-plugin-persisted-scope = "2.3" +tauri-plugin-store = "2.4" +tauri-plugin-window-state = "2.4" +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[profile.release] +codegen-units = 1 +lto = true +opt-level = "s" +panic = "abort" +strip = true diff --git a/src-tauri/app-icon.png b/src-tauri/app-icon.png new file mode 100644 index 0000000..a2a2d27 Binary files /dev/null and b/src-tauri/app-icon.png differ diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100644 index 0000000..ccd2b91 --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,22 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "What the Autotiler window may do. No static file paths: a file or folder becomes accessible only after the user picks or drops it (persisted-scope keeps those grants across restarts).", + "windows": ["main"], + "permissions": [ + "core:default", + "dialog:allow-open", + "dialog:allow-save", + "fs:allow-read-file", + "fs:allow-write-file", + "fs:allow-write-text-file", + "fs:allow-mkdir", + "fs:allow-exists", + "fs:allow-watch", + "fs:allow-unwatch", + "store:default", + "window-state:default", + "opener:default", + "opener:allow-reveal-item-in-dir" + ] +} diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 0000000..0e9be65 Binary files /dev/null and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000..e14a20d Binary files /dev/null and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png new file mode 100644 index 0000000..2e9423a Binary files /dev/null and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/64x64.png b/src-tauri/icons/64x64.png new file mode 100644 index 0000000..881ec33 Binary files /dev/null and b/src-tauri/icons/64x64.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000..74ac970 Binary files /dev/null and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000..0542a3e Binary files /dev/null and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000..cf872e0 Binary files /dev/null and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000..3b220e1 Binary files /dev/null and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000..7a8a5e4 Binary files /dev/null and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000..f0537d2 Binary files /dev/null and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000..fd5ce7b Binary files /dev/null and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000..965a35e Binary files /dev/null and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000..340e4a9 Binary files /dev/null and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000..62bb76d Binary files /dev/null and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns new file mode 100644 index 0000000..7d1571d Binary files /dev/null and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico new file mode 100644 index 0000000..e1be63b Binary files /dev/null and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png new file mode 100644 index 0000000..4a2a267 Binary files /dev/null and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..bab141f --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,64 @@ +use std::path::Path; + +/// The `res://` path of `dir` when it lies inside a Godot project (a folder above it holds `project.godot`). +/// +/// A command because the webview's fs scope only covers folders the user picked, not their parents. +#[tauri::command] +fn godot_res_path(dir: String) -> Option { + let dir = Path::new(&dir); + let root = dir + .ancestors() + .find(|a| a.join("project.godot").is_file())?; + let rel = dir + .strip_prefix(root) + .ok()? + .to_string_lossy() + .replace('\\', "/"); + Some(if rel.is_empty() { + "res://".to_string() + } else { + format!("res://{rel}/") + }) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_window_state::Builder::new().build()) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_fs::init()) + // after fs: restores the file/folder grants from dialogs and drops, so saved folders keep working + .plugin(tauri_plugin_persisted_scope::init()) + .plugin(tauri_plugin_store::Builder::new().build()) + .plugin(tauri_plugin_opener::init()) + .invoke_handler(tauri::generate_handler![godot_res_path]) + .run(tauri::generate_context!()) + .expect("error while running Autotiler"); +} + +#[cfg(test)] +mod tests { + use super::godot_res_path; + use std::fs; + + #[test] + fn finds_the_res_path_of_a_folder_inside_a_godot_project() { + let root = std::env::temp_dir().join(format!("autotiler-res-{}", std::process::id())); + let tiles = root.join("assets").join("tiles"); + fs::create_dir_all(&tiles).unwrap(); + fs::write(root.join("project.godot"), "config_version=5\n").unwrap(); + assert_eq!( + godot_res_path(tiles.to_string_lossy().into()), + Some("res://assets/tiles/".into()) + ); + assert_eq!( + godot_res_path(root.to_string_lossy().into()), + Some("res://".into()) + ); + fs::remove_dir_all(&root).unwrap(); + assert_eq!( + godot_res_path(std::env::temp_dir().to_string_lossy().into()), + None + ); + } +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..2a5ed3e --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,6 @@ +// Prevents an extra console window on Windows in release builds. Do not remove. +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + autotiler_lib::run(); +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..0498281 --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,43 @@ +{ + "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", + "productName": "Autotiler", + "version": "../package.json", + "identifier": "com.route1rodent.autotiler", + "build": { + "frontendDist": "../dist", + "devUrl": "http://localhost:5173", + "beforeDevCommand": "pnpm dev", + "beforeBuildCommand": "pnpm build" + }, + "app": { + "windows": [ + { + "label": "main", + "title": "Autotiler", + "width": 1320, + "height": 840, + "minWidth": 1024, + "minHeight": 640, + "theme": "Dark", + "backgroundColor": "#1c202a", + "dragDropEnabled": true + } + ], + "security": { + "csp": "default-src 'self'; img-src 'self' data: blob:; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self' ipc: http://ipc.localhost" + } + }, + "bundle": { + "active": true, + "targets": "all", + "category": "GraphicsAndDesign", + "copyright": "© 2020-2026 route1rodent", + "shortDescription": "Autotile tileset generator", + "longDescription": "Turn a small terrain template into a 47-tile blob or 16-tile dual-grid tileset, with Godot 4, Tiled and PNG export.", + "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"], + "macOS": { + "minimumSystemVersion": "13.0", + "signingIdentity": "-" + } + } +} diff --git a/src/app/App.tsx b/src/app/App.tsx new file mode 100644 index 0000000..7b14f17 --- /dev/null +++ b/src/app/App.tsx @@ -0,0 +1,239 @@ +import { Dialog } from "@base-ui/react/dialog"; +import { Tabs } from "@base-ui/react/tabs"; +import { Tooltip } from "@base-ui/react/tooltip"; +import { Upload, X } from "lucide-react"; +import { useEffect, useState } from "react"; + +import { Toaster } from "./components/toasts.tsx"; +import { Kbd } from "./components/ui.tsx"; +import { copyPng, exportNow, loadSource, openFile } from "./features/actions.ts"; +import { ExportPanel } from "./features/export/ExportPanel.tsx"; +import { Header } from "./features/Header.tsx"; +import { SourcePanel } from "./features/source/SourcePanel.tsx"; +import { TestMapView } from "./features/test-map/TestMapView.tsx"; +import { TilesetView } from "./features/tileset/TilesetView.tsx"; +import { cn } from "./lib/cn.ts"; +import { platform } from "./platform/index.ts"; +import { useApp, type Overlays, type View } from "./state/store.ts"; + +const isTyping = (t: EventTarget | null) => + t instanceof HTMLElement && (t.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(t.tagName)); + +/** Drag & drop anywhere on the window (HTML5 on the web, native events on desktop). */ +function useFileDrop(): boolean { + const [over, setOver] = useState(false); + useEffect(() => { + const p = platform(); + if (p.subscribeNativeDrop) { + let unsubscribe: (() => void) | undefined; + let cancelled = false; + void p + .subscribeNativeDrop((file) => loadSource(file), setOver) + .then((u) => { + if (cancelled) u(); + else unsubscribe = u; + }); + return () => { + cancelled = true; + unsubscribe?.(); + }; + } + let depth = 0; + const hasFiles = (e: DragEvent) => e.dataTransfer?.types.includes("Files") ?? false; + const enter = (e: DragEvent) => { + if (!hasFiles(e)) return; + depth++; + setOver(true); + }; + const leave = () => { + depth = Math.max(0, depth - 1); + if (depth === 0) setOver(false); + }; + const overFn = (e: DragEvent) => { + if (hasFiles(e)) e.preventDefault(); + }; + const drop = (e: DragEvent) => { + if (!e.dataTransfer || !hasFiles(e)) return; + e.preventDefault(); + depth = 0; + setOver(false); + void p.fileFromDataTransfer(e.dataTransfer).then((file) => file && loadSource(file)); + }; + window.addEventListener("dragenter", enter); + window.addEventListener("dragleave", leave); + window.addEventListener("dragover", overFn); + window.addEventListener("drop", drop); + return () => { + window.removeEventListener("dragenter", enter); + window.removeEventListener("dragleave", leave); + window.removeEventListener("dragover", overFn); + window.removeEventListener("drop", drop); + }; + }, []); + return over; +} + +/** Paste a PNG from the clipboard (e.g. copied in Aseprite). */ +function usePaste(): void { + useEffect(() => { + const onPaste = (e: ClipboardEvent) => { + if (isTyping(e.target)) return; + const item = [...(e.clipboardData?.items ?? [])].find((i) => i.type === "image/png"); + const file = item?.getAsFile(); + if (!file) return; + e.preventDefault(); + void file.arrayBuffer().then((buf) => loadSource({ name: "pasted.png", bytes: new Uint8Array(buf) })); + }; + window.addEventListener("paste", onPaste); + return () => window.removeEventListener("paste", onPaste); + }, []); +} + +const OVERLAY_KEYS: Record = { g: "grid", q: "quarters", b: "bits", c: "collision" }; + +function useHotkeys(showShortcuts: () => void): void { + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + const mod = e.metaKey || e.ctrlKey; + const k = e.key.toLowerCase(); + if (mod && k === "o") { + e.preventDefault(); + void openFile(); + } else if (mod && k === "e") { + e.preventDefault(); + void exportNow(); + } else if (mod && e.shiftKey && k === "c") { + e.preventDefault(); + void copyPng(); + } else if (!mod && !e.altKey && !isTyping(e.target)) { + const s = useApp.getState(); + if (k === "1" || k === "2") s.setView(k === "1" ? "tileset" : "map"); + else if (OVERLAY_KEYS[k]) s.setOverlay(OVERLAY_KEYS[k], !s.settings.overlays[OVERLAY_KEYS[k]]); + else if (k === "?") showShortcuts(); + else if (k === "+" || k === "=" || k === "-" || k === "0") { + const key = s.view === "tileset" ? "zoom" : "mapZoom"; + const current = s.settings[key]; + if (k === "0") s.setSettings({ [key]: "fit" }); + else { + const base = current === "fit" ? 4 : current; + s.setSettings({ [key]: Math.min(16, Math.max(1, base + (k === "-" ? -1 : 1))) }); + } + } + } + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [showShortcuts]); +} + +/** Re-reads the source when it changes on disk; optionally re-exports. */ +function useLiveReload(): void { + const file = useApp((s) => s.source?.file); + useEffect(() => { + if (!file) return undefined; + return platform().watch(file, (next) => loadSource(next, "reload")) ?? undefined; + }, [file]); + + const revision = useApp((s) => s.source?.revision); + const reason = useApp((s) => s.source?.reason); + useEffect(() => { + const { settings, outputFolder } = useApp.getState(); + // `revision` changes on every reload, so saving the same file twice re-exports twice + if (revision !== undefined && reason === "reload" && settings.autoExport && outputFolder) + void exportNow({ quiet: true }); + }, [revision, reason]); +} + +const SHORTCUTS: ReadonlyArray<[string, string]> = [ + ["⌘/Ctrl O", "Open an image"], + ["⌘/Ctrl V", "Paste an image"], + ["⌘/Ctrl E", "Export"], + ["⌘/Ctrl ⇧ C", "Copy the tileset PNG"], + ["1 / 2", "Tileset / test map"], + ["G · Q · B · C", "Grid · quarters · bits · collision overlays"], + ["+ / − / 0", "Zoom in / out / fit"], +]; + +export function App() { + const [shortcutsOpen, setShortcutsOpen] = useState(false); + const dragging = useFileDrop(); + usePaste(); + useHotkeys(() => setShortcutsOpen(true)); + useLiveReload(); + const view = useApp((s) => s.view); + const setView = useApp((s) => s.setView); + + return ( + +
+
setShortcutsOpen(true)} /> +
+ + } + value={view} + onValueChange={(v: View) => setView(v)} + className="flex min-h-[28rem] min-w-0 flex-col bg-app" + > + + {(["tileset", "map"] as const).map((v) => ( + + {v === "tileset" ? "Tileset" : "Test map"} + + ))} + + + + + + + + + +
+
+ +
+
+ + Drop the template PNG +
+
+ + + + + +
+ Keyboard shortcuts + + + +
+
+ {SHORTCUTS.map(([keys, what]) => ( +
+
+ {keys} +
+
{what}
+
+ ))} +
+
+
+
+ +
+ ); +} diff --git a/src/app/components/PixelCanvas.tsx b/src/app/components/PixelCanvas.tsx new file mode 100644 index 0000000..615e110 --- /dev/null +++ b/src/app/components/PixelCanvas.tsx @@ -0,0 +1,149 @@ +// Crisp pixel-art display. Every image pixel covers a whole number of *device* pixels, so art stays sharp even at +// fractional display scaling (125 %, 150 % …). Pixels are only drawn here — never read back — so the canvas can't +// alter them. +import { + useEffect, + useMemo, + useRef, + useState, + type CanvasHTMLAttributes, + type PointerEvent as ReactPointerEvent, + type RefObject, +} from "react"; + +import type { RgbaImage } from "../../core/index.ts"; +import { cn } from "../lib/cn.ts"; + +export interface OverlayContext { + readonly ctx: CanvasRenderingContext2D; + /** device pixels per image pixel */ + readonly scale: number; + /** device pixels per CSS pixel */ + readonly dpr: number; +} + +export interface CanvasPointer { + /** image pixel coordinates (fractional) */ + readonly x: number; + readonly y: number; + readonly buttons: number; + readonly altKey: boolean; +} + +type Props = Pick< + CanvasHTMLAttributes, + "tabIndex" | "onKeyDown" | "onFocus" | "onBlur" | "aria-describedby" +> & { + image: RgbaImage; + /** CSS pixels per image pixel (integer); the backing store rounds it to whole device pixels */ + zoom: number; + overlay?: (o: OverlayContext) => void; + onPointer?: (p: CanvasPointer | null, type: "down" | "move" | "up" | "leave") => void; + className?: string; + label: string; +}; + +function useDevicePixelRatio(): number { + const [dpr, setDpr] = useState(() => window.devicePixelRatio || 1); + useEffect(() => { + // fires on browser zoom and when the window moves to a display with another scale + const onResize = () => setDpr(window.devicePixelRatio || 1); + window.addEventListener("resize", onResize); + return () => window.removeEventListener("resize", onResize); + }, []); + return dpr; +} + +export function PixelCanvas({ image, zoom, overlay, onPointer, className, label, ...rest }: Props) { + const ref = useRef(null); + const dpr = useDevicePixelRatio(); + const scale = Math.max(1, Math.round(zoom * dpr)); + + // the image at 1:1, reused by every redraw + const bitmap = useMemo(() => { + const c = document.createElement("canvas"); + c.width = image.width; + c.height = image.height; + const ctx = c.getContext("2d"); + if (ctx && image.width > 0 && image.height > 0) { + ctx.putImageData(new ImageData(new Uint8ClampedArray(image.data), image.width, image.height), 0, 0); + } + return c; + }, [image]); + + useEffect(() => { + const canvas = ref.current; + const ctx = canvas?.getContext("2d"); + if (!canvas || !ctx) return; + canvas.width = image.width * scale; + canvas.height = image.height * scale; + ctx.imageSmoothingEnabled = false; + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height); + overlay?.({ ctx, scale, dpr }); + }, [bitmap, image, scale, dpr, overlay]); + + const toPointer = (e: ReactPointerEvent): CanvasPointer => { + const rect = e.currentTarget.getBoundingClientRect(); + return { + x: ((e.clientX - rect.left) / rect.width) * image.width, + y: ((e.clientY - rect.top) / rect.height) * image.height, + buttons: e.buttons, + altKey: e.altKey, + }; + }; + + return ( + equivalent; role + label name it + role="img" + aria-label={label} + className={cn("checker block [image-rendering:pixelated]", className)} + style={{ width: (image.width * scale) / dpr, height: (image.height * scale) / dpr }} + onPointerDown={ + onPointer && + ((e) => { + e.currentTarget.setPointerCapture(e.pointerId); + onPointer(toPointer(e), "down"); + }) + } + onPointerMove={onPointer && ((e) => onPointer(toPointer(e), "move"))} + onPointerUp={onPointer && ((e) => onPointer(toPointer(e), "up"))} + onPointerLeave={onPointer && (() => onPointer(null, "leave"))} + onContextMenu={onPointer && ((e) => e.preventDefault())} + /> + ); +} + +/** + * Largest zoom (CSS px per image px) at which an image fits a box, in whole *device* pixels per image pixel — so on a + * 2× display the fit can be 1.5×, 2.5× … and still be crisp. Never below one device pixel per image pixel. + */ +export function fitZoom( + image: { width: number; height: number }, + box: { width: number; height: number }, + max = 16, + dpr = typeof window === "undefined" ? 1 : window.devicePixelRatio || 1, +): number { + if (!image.width || !image.height || box.width <= 0 || box.height <= 0) return 1; + const k = Math.floor(Math.min((box.width * dpr) / image.width, (box.height * dpr) / image.height)); + return Math.max(1, Math.min(Math.round(max * dpr), k)) / dpr; +} + +/** Tracks an element's content size. */ +export function useElementSize(ref: RefObject): { width: number; height: number } { + const [size, setSize] = useState({ width: 0, height: 0 }); + useEffect(() => { + const el = ref.current; + if (!el) return undefined; + const ro = new ResizeObserver(([entry]) => { + const { width, height } = entry.contentRect; + setSize((s) => (s.width === width && s.height === height ? s : { width, height })); + }); + ro.observe(el); + return () => ro.disconnect(); + }, [ref]); + return size; +} diff --git a/src/app/components/toasts.tsx b/src/app/components/toasts.tsx new file mode 100644 index 0000000..0041bd9 --- /dev/null +++ b/src/app/components/toasts.tsx @@ -0,0 +1,51 @@ +import { Toast } from "@base-ui/react/toast"; +import { X } from "lucide-react"; + +export const toastManager = Toast.createToastManager(); + +export function notify( + title: string, + description?: string, + options: { type?: "success" | "error" | "info"; action?: { label: string; onClick: () => void } } = {}, +): void { + const type = options.type ?? "info"; + toastManager.add({ + title, + description, + type, + timeout: type === "error" ? 9000 : 4500, + actionProps: options.action ? { children: options.action.label, onClick: options.action.onClick } : undefined, + }); +} + +function ToastList() { + const { toasts } = Toast.useToastManager(); + return toasts.map((toast) => ( + + +
+ + + +
+ + + +
+
+ )); +} + +export function Toaster() { + return ( + + + + + + ); +} diff --git a/src/app/components/ui.tsx b/src/app/components/ui.tsx new file mode 100644 index 0000000..55447b8 --- /dev/null +++ b/src/app/components/ui.tsx @@ -0,0 +1,345 @@ +// Small styled wrappers around Base UI primitives (behaviour and accessibility come from Base UI). +import { Menu } from "@base-ui/react/menu"; +import { Select } from "@base-ui/react/select"; +import { Switch } from "@base-ui/react/switch"; +import { Toggle } from "@base-ui/react/toggle"; +import { ToggleGroup } from "@base-ui/react/toggle-group"; +import { Tooltip } from "@base-ui/react/tooltip"; +import { Check, ChevronDown } from "lucide-react"; +import { + createContext, + use, + useId, + useMemo, + type ButtonHTMLAttributes, + type InputHTMLAttributes, + type ReactElement, + type ReactNode, +} from "react"; + +import { cn } from "../lib/cn.ts"; + +const focus = "outline-none focus-visible:ring-2 focus-visible:ring-accent/70 focus-visible:ring-offset-0"; + +type ButtonProps = ButtonHTMLAttributes & { + variant?: "primary" | "secondary" | "ghost"; + size?: "sm" | "md"; +}; + +export function Button({ variant = "secondary", size = "md", className, type = "button", ...props }: ButtonProps) { + return ( + + + + Examples + + } + items={EXAMPLES.map((e) => ({ + kind: "item", + key: e.file, + label: e.label, + hint: e.hint, + onSelect: () => void openExample(e), + }))} + /> + + Blank template + + } + items={[ + { kind: "heading", key: "h13", text: "Autotiler 13-tile (5×3)" }, + ...GUIDE_SIZES.map((ts) => ({ + kind: "item" as const, + key: `13-${ts}`, + label: `${ts} px tiles`, + hint: `${ts * 5}×${ts * 3} PNG, colour-coded slots`, + onSelect: () => void saveGuide("autotiler-13", ts), + })), + { kind: "separator", key: "sep" }, + { kind: "heading", key: "ha2", text: "RPG Maker A2 (2×3)" }, + ...GUIDE_SIZES.map((ts) => ({ + kind: "item" as const, + key: `a2-${ts}`, + label: `${ts} px tiles`, + hint: `${ts * 2}×${ts * 3} PNG, colour-coded slots`, + onSelect: () => void saveGuide("rpgmaker-a2", ts), + })), + ]} + /> + +
+ + + + platform().openUrl("https://github.com/itsjavi/autotiler")} + > + + +
+ + ); +} + +function RecentMenu() { + const recent = useApp((s) => s.recent); + const clearRecent = useApp((s) => s.clearRecent); + if (!platform().openPath || recent.length === 0) return null; + const items: MenuEntry[] = [ + ...recent.map((path) => ({ + kind: "item" as const, + key: path, + label: fileNameOf(path), + hint: {path}, + onSelect: () => void openRecent(path), + })), + { kind: "separator", key: "sep" }, + { kind: "item", key: "clear", label: "Clear the list", onSelect: clearRecent }, + ]; + return ( + + Recent + + } + items={items} + /> + ); +} diff --git a/src/app/features/actions.ts b/src/app/features/actions.ts new file mode 100644 index 0000000..7e7e8d0 --- /dev/null +++ b/src/app/features/actions.ts @@ -0,0 +1,181 @@ +// App-level actions shared by buttons, keyboard shortcuts, drag & drop, paste and live reload. +import { + autotiler13, + encodePng, + getTemplate, + renderGuide, + repackTemplate, + rpgmakerA2, + type ExportFile, + type TemplateId, +} from "../../core/index.ts"; +import { notify } from "../components/toasts.tsx"; +import { platform, type SourceFile } from "../platform/index.ts"; +import { baseName, tilesetOf } from "../state/derived.ts"; +import { useApp } from "../state/store.ts"; +import { buildExport, pngOf, targetSupports } from "./export/build.ts"; + +const messageOf = (err: unknown) => (err instanceof Error ? err.message : String(err)); + +export interface Example { + readonly file: string; + readonly label: string; + readonly hint: string; +} + +export const EXAMPLES: readonly Example[] = [ + { file: "autotiler13-16px.png", label: "Beveled block, 16 px", hint: "Autotiler 13-tile template" }, + { file: "autotiler13-8px.png", label: "Beveled block, 8 px", hint: "Autotiler 13-tile template" }, + { file: "autotiler13-32px.png", label: "Beveled block, 32 px", hint: "Autotiler 13-tile template" }, + { file: "rpgmaker-a2-16px.png", label: "Beveled block, 16 px", hint: "RPG Maker A2 template" }, +]; + +export function loadSource(file: SourceFile, reason: "open" | "reload" = "open"): void { + const ok = useApp.getState().loadFile(file, reason); + if (!ok) notify("Couldn't open the image", useApp.getState().loadError ?? undefined, { type: "error" }); +} + +export async function openFile(): Promise { + try { + const file = await platform().openImage(); + if (file) loadSource(file); + } catch (err) { + notify("Couldn't open the file", messageOf(err), { type: "error" }); + } +} + +/** The file name part of a path (either separator). */ +export const fileNameOf = (path: string): string => path.split(/[\\/]/).pop() ?? path; + +export async function openRecent(path: string): Promise { + const open = platform().openPath; + if (!open) return; + try { + loadSource(await open(path)); + } catch (err) { + useApp.setState((s) => ({ recent: s.recent.filter((r) => r !== path) })); + notify(`Couldn't open ${fileNameOf(path)}`, messageOf(err), { type: "error" }); + } +} + +/** Reopens the file of the last session (desktop); silently skipped when it moved or was deleted. */ +export async function restoreLastFile(path: string | null): Promise { + const open = platform().openPath; + if (!path || !open || useApp.getState().source) return; + try { + const file = await open(path); + if (!useApp.getState().source) useApp.getState().loadFile(file); + } catch { + // gone or no longer readable: start empty + } +} + +export async function openExample(example: Example): Promise { + try { + const res = await fetch(`./examples/${example.file}`); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + useApp.getState().setSettings({ templateId: "auto", tileSize: null }); + loadSource({ name: example.file, bytes: new Uint8Array(await res.arrayBuffer()) }); + } catch (err) { + notify("Couldn't load the example", messageOf(err), { type: "error" }); + } +} + +export async function exportNow(options: { quiet?: boolean } = {}): Promise { + const { source, settings, outputFolder } = useApp.getState(); + if (!source) { + notify("Nothing to export yet", "Open a template image first.", { type: "error" }); + return false; + } + const { tileset, error } = tilesetOf(source, settings); + if (!tileset) { + notify("Can't export", error ?? undefined, { type: "error" }); + return false; + } + if (!targetSupports(settings.target, tileset.layout)) { + notify("Pick another format", "Godot TileSets need a 47-tile layout; use PNG or Tiled for the dual grid.", { + type: "error", + }); + return false; + } + const bundle = buildExport(source, tileset, settings); + try { + const result = await platform().save(bundle.files, bundle.zipName, outputFolder); + if (!result) return false; // dialog cancelled + if (result.folder) useApp.getState().setOutputFolder(result.folder); + const folderPath = result.folder?.path ?? outputFolder?.path; + const files = bundle.files.map((f) => f.path).join(", "); + const showTip = settings.target === "godot4" && !settings.forceNearest && !settings.filterTipShown; + if (showTip) useApp.getState().setSettings({ filterTipShown: true }); + const tip = showTip + ? " For crisp pixels in Godot: Project Settings → Rendering → Textures → Canvas Textures → Default Texture Filter = Nearest (or turn on “Force nearest filtering”)." + : ""; + const p = platform(); + const where = folderPath; + notify( + options.quiet + ? `Re-exported ${bundle.name}` + : result.kind === "written" + ? `Exported to ${result.where}` + : `Downloaded ${result.where}`, + options.quiet ? files : `${files}.${tip}`, + { + type: "success", + action: + p.reveal && where + ? { label: "Show in folder", onClick: () => void p.reveal?.(`${where}/${bundle.files[0].path}`) } + : undefined, + }, + ); + return true; + } catch (err) { + notify("Export failed", messageOf(err), { type: "error" }); + return false; + } +} + +export async function copyPng(): Promise { + const { source, settings } = useApp.getState(); + const tileset = source ? tilesetOf(source, settings).tileset : null; + if (!tileset) return; + try { + await platform().copyPng(pngOf(tileset)); + notify("Copied the tileset image", "Paste it into Aseprite or any image editor.", { type: "success" }); + } catch (err) { + notify("Couldn't copy the image", messageOf(err), { type: "error" }); + } +} + +/** Saves one image through a save dialog (desktop) or as a download (web). */ +async function saveImage(file: ExportFile, what: string): Promise { + try { + const result = await platform().save([file], file.path, null); + if (result?.kind === "written") notify(`Saved the ${what}`, result.where, { type: "success" }); + } catch (err) { + notify(`Couldn't save the ${what}`, messageOf(err), { type: "error" }); + } +} + +export async function saveGuide(templateId: TemplateId, tileSize: number): Promise { + const png = encodePng(renderGuide(getTemplate(templateId), tileSize)); + await saveImage({ path: `${templateId}-guide-${tileSize}px.png`, data: png }, "blank template"); +} + +/** The other template kind: 13-tile ⇄ RPG Maker A2. */ +export const counterpartOf = (id: TemplateId) => (id === "rpgmaker-a2" ? autotiler13 : rpgmakerA2); + +/** Saves the source rearranged into the other template layout (e.g. to reuse the art in RPG Maker). */ +export async function saveConverted(): Promise { + const { source, settings } = useApp.getState(); + const analysis = source ? tilesetOf(source, settings).analysis : null; + if (!source || !analysis?.template || !analysis.tileSize) return; + const to = counterpartOf(analysis.template.id); + const png = encodePng(repackTemplate(source.image, analysis.template, to, analysis.tileSize)); + const suffix = to.id === "rpgmaker-a2" ? "a2" : "13tile"; + await saveImage({ path: `${baseName(source)}-${suffix}.png`, data: png }, `${to.name} template`); +} + +export async function chooseOutputFolder(): Promise { + const folder = await platform().pickOutputFolder(); + if (folder) useApp.getState().setOutputFolder(folder); +} diff --git a/src/app/features/export/ExportPanel.tsx b/src/app/features/export/ExportPanel.tsx new file mode 100644 index 0000000..a33c4f7 --- /dev/null +++ b/src/app/features/export/ExportPanel.tsx @@ -0,0 +1,206 @@ +import { Copy, FileDown, FolderInput, FolderOpen, X } from "lucide-react"; + +import { getLayout, LAYOUTS, toHex } from "../../../core/index.ts"; +import { Button, Field, IconButton, Segmented, SelectInput, SwitchInput, TextInput } from "../../components/ui.tsx"; +import { platform } from "../../platform/index.ts"; +import { baseName, exportNameOf, terrainColorOf, useTileset } from "../../state/derived.ts"; +import { useApp } from "../../state/store.ts"; +import { chooseOutputFolder, copyPng, exportNow } from "../actions.ts"; +import { buildExport, TARGETS, targetSupports } from "./build.ts"; + +export function ExportPanel() { + const settings = useApp((s) => s.settings); + const source = useApp((s) => s.source); + const outputFolder = useApp((s) => s.outputFolder); + const godotPath = useApp((s) => s.godotPath); + const setSettings = useApp((s) => s.setSettings); + const result = useTileset(); + const tileset = result?.tileset ?? null; + const layout = getLayout(settings.layoutId); + const supported = targetSupports(settings.target, layout); + const p = platform(); + const bundle = source && tileset && supported ? buildExport(source, tileset, settings) : null; + const autoColor = toHex(terrainColorOf(source, tileset, { ...settings, terrainColor: null })); + const usesTerrain = settings.target === "godot4" || settings.target === "tiled"; + const canAutoExport = p.canWatch && !!outputFolder; + const blocked = !source + ? "Open a template image to export its tileset." + : !tileset + ? "Fix the template first (see the source panel)." + : !supported + ? "This format needs a 47-tile layout." + : null; + + return ( + + ); +} + +function DestinationField() { + const outputFolder = useApp((s) => s.outputFolder); + const godotPath = useApp((s) => s.godotPath); + const setOutputFolder = useApp((s) => s.setOutputFolder); + const p = platform(); + if (!p.canPickFolder) { + return

Files are downloaded (several files come as a .zip).

; + } + return ( + +
+ + {outputFolder ? ( + setOutputFolder(null)}> + + + ) : null} +
+
+ ); +} diff --git a/src/app/features/export/build.ts b/src/app/features/export/build.ts new file mode 100644 index 0000000..c8cd5b4 --- /dev/null +++ b/src/app/features/export/build.ts @@ -0,0 +1,61 @@ +import { + encodePng, + exportGodot3, + exportGodot4, + exportPng, + exportTiled, + type ExportFile, + type Layout, + type TargetId, + type Tileset, +} from "../../../core/index.ts"; +import { exportNameOf, terrainColorOf, terrainNameOf } from "../../state/derived.ts"; +import type { Settings, Source } from "../../state/store.ts"; + +export const TARGETS: ReadonlyArray<{ id: TargetId; label: string; hint: string }> = [ + { id: "godot4", label: "Godot 4", hint: "TileSet with a terrain set, peering bits and collisions" }, + { id: "tiled", label: "Tiled", hint: "Tileset (.tsx) with a Wang set" }, + { id: "png", label: "PNG", hint: "Just the image, optionally with a JSON tile map" }, + { id: "godot3", label: "Godot 3", hint: "Legacy autotile (3x3 minimal bitmask)" }, +]; + +export function targetSupports(target: TargetId, layout: Layout): boolean { + return layout.kind === "blob" || target === "png" || target === "tiled"; +} + +export interface ExportBundle { + readonly files: ExportFile[]; + readonly zipName: string; + readonly name: string; +} + +export function buildExport(source: Source, tileset: Tileset, settings: Settings): ExportBundle { + const name = exportNameOf(source, settings); + const terrainName = terrainNameOf(source, settings); + const terrainColor = terrainColorOf(source, tileset, settings); + let files: ExportFile[]; + switch (settings.target) { + case "godot4": + files = exportGodot4(tileset, { + name, + terrainName, + terrainColor, + collision: settings.collision, + forceNearest: settings.forceNearest, + }); + break; + case "godot3": + files = exportGodot3(tileset, { name, collision: settings.collision }); + break; + case "tiled": + files = exportTiled(tileset, { name, terrainName, terrainColor }); + break; + default: + files = exportPng(tileset, { name, sidecar: settings.sidecar }); + } + return { files, zipName: `${name}.zip`, name }; +} + +export function pngOf(tileset: Tileset): Uint8Array { + return encodePng(tileset.image); +} diff --git a/src/app/features/source/SourcePanel.tsx b/src/app/features/source/SourcePanel.tsx new file mode 100644 index 0000000..23ef6e0 --- /dev/null +++ b/src/app/features/source/SourcePanel.tsx @@ -0,0 +1,295 @@ +import { AlertTriangle, ArrowLeftRight, CircleX, Eye, FolderOpen, Info, X } from "lucide-react"; +import { useCallback, useRef, useState } from "react"; + +import { PIECE_COLORS, PIECE_KINDS, PIECE_NAMES, quarterRect, type Issue, type Template } from "../../../core/index.ts"; +import { fitZoom, PixelCanvas, useElementSize, type OverlayContext } from "../../components/PixelCanvas.tsx"; +import { Button, Field, IconButton, SelectInput, TextInput, Tip } from "../../components/ui.tsx"; +import { cn } from "../../lib/cn.ts"; +import { platform } from "../../platform/index.ts"; +import { useTileset } from "../../state/derived.ts"; +import { useApp, type Source } from "../../state/store.ts"; +import { counterpartOf, EXAMPLES, fileNameOf, openExample, openFile, openRecent, saveConverted } from "../actions.ts"; + +const TEMPLATE_OPTIONS = [ + { value: "auto", label: "Detect automatically" }, + { value: "autotiler-13", label: "Autotiler 13-tile (5×3)", hint: "3×3 island + inner-corner block" }, + { value: "rpgmaker-a2", label: "RPG Maker A2 (2×3)", hint: "VX Ace / MV / MZ floor autotile" }, +] as const; + +export function SourcePanel() { + const source = useApp((s) => s.source); + const loadError = useApp((s) => s.loadError); + return ( + + ); +} + +function EmptySource() { + return ( + <> +

Source

+
+

Drop a template PNG here, paste it, or open a file.

+ +

+ Autotiler 13-tile: 5×3 tiles — a 3×3 island plus a 2×2 block + whose centre has the inner corners. +
+ RPG Maker A2: 2×3 tiles. +

+
+ +
+

Try an example

+ {EXAMPLES.map((e) => ( + void openExample(e)} /> + ))} +
+ + ); +} + +function RecentFiles() { + const recent = useApp((s) => s.recent); + if (!platform().openPath || recent.length === 0) return null; + return ( +
+

Recent

+ {recent.slice(0, 5).map((path) => ( + void openRecent(path)} + /> + ))} +
+ ); +} + +function ListButton({ + title, + detail, + tooltip, + onClick, +}: { + title: string; + detail: string; + tooltip?: string; + onClick: () => void; +}) { + return ( + + ); +} + +function LoadedSource({ source }: { source: Source }) { + const result = useTileset(); + const templateId = useApp((s) => s.settings.templateId); + const tileSize = useApp((s) => s.settings.tileSize); + const setSettings = useApp((s) => s.setSettings); + const closeSource = useApp((s) => s.closeSource); + const analysis = result?.analysis; + const template = analysis?.template ?? null; + const watching = platform().canWatch && source.file.ref !== undefined; + + return ( + <> +
+

Source

+ + + +
+ +
+ + {source.file.name} + + + {source.image.width}×{source.image.height} px + {template && analysis?.tileSize + ? ` · ${template.columns}×${template.rows} tiles of ${analysis.tileSize} px` + : ""} + + {watching ? ( + + Watching for changes — save in your editor to update + + ) : null} +
+ + setSettings({ templateId: v, tileSize: null })} + /> + + + { + const n = Number.parseInt(e.target.value, 10); + setSettings({ tileSize: Number.isFinite(n) && n >= 2 ? n : null }); + }} + /> + + {template && result?.tileset ? ( + + + + ) : null} + {analysis && analysis.issues.length > 0 ? ( +
    + {analysis.issues.map((issue) => ( + + ))} +
+ ) : null} + + + ); +} + +function IssueRow({ issue }: { issue: Issue }) { + const Icon = issue.level === "error" ? CircleX : issue.level === "warning" ? AlertTriangle : Info; + return ( +
  • + + {issue.message} +
  • + ); +} + +function PieceLegend() { + return ( +
    + How templates work +

    + Every tile is built from four quarters. A quarter shows one of five pieces depending on its neighbours; the + template provides each piece once (or a few times, for context): +

    +
      + {PIECE_KINDS.map((k) => ( +
    • + + {PIECE_NAMES[k]} +
    • + ))} +
    +

    The colours match the downloadable blank templates.

    +
    + ); +} + +function SourcePreview({ + source, + template, + tileSize, +}: { + source: Source; + template: Template | null; + tileSize: number | null; +}) { + const box = useRef(null); + const size = useElementSize(box); + const hover = useApp((s) => s.hover); + const result = useTileset(); + const [slotLabel, setSlotLabel] = useState(null); + const zoom = fitZoom(source.image, { width: size.width || 256, height: 240 }, 8); + + const overlay = useCallback( + ({ ctx, scale }: OverlayContext) => { + if (!template || !tileSize) return; + const t = tileSize * scale; + // dim the cells generation never reads + for (const slot of template.slots) { + if (slot.used) continue; + ctx.fillStyle = "rgba(20, 24, 34, 0.72)"; + ctx.fillRect(slot.tx * t, slot.ty * t, t, t); + } + ctx.fillStyle = "rgba(255, 255, 255, 0.28)"; + for (let x = 1; x < template.columns; x++) ctx.fillRect(x * t, 0, 1, template.rows * t); + for (let y = 1; y < template.rows; y++) ctx.fillRect(0, y * t, template.columns * t, 1); + // quarters used by the hovered output tile + const cell = hover && result?.tileset?.cells.find((c) => c.x === hover.x && c.y === hover.y); + if (cell) { + ctx.lineWidth = Math.max(2, Math.round(scale / 2)); + ctx.strokeStyle = "#fcc751"; + for (const ref of cell.quarters) { + if (!ref) continue; + const q = quarterRect(tileSize, ref.corner); + ctx.strokeRect(ref.tx * t + q.x * scale + 1, ref.ty * t + q.y * scale + 1, q.w * scale - 2, q.h * scale - 2); + } + } + }, + [template, tileSize, hover, result], + ); + + return ( +
    +
    + { + if (!p || !template || !tileSize) return setSlotLabel(null); + const slot = template.slots.find( + (s) => s.tx === Math.floor(p.x / tileSize) && s.ty === Math.floor(p.y / tileSize), + ); + setSlotLabel(slot ? `${slot.label}${slot.used ? "" : " — not used"}` : null); + }} + /> +
    + + {slotLabel ?? (hover ? "Highlighted: quarters used by the hovered tile" : "")} + +
    + ); +} diff --git a/src/app/features/test-map/TestMapView.tsx b/src/app/features/test-map/TestMapView.tsx new file mode 100644 index 0000000..7582927 --- /dev/null +++ b/src/app/features/test-map/TestMapView.tsx @@ -0,0 +1,112 @@ +import { Dices, Eraser, LayoutGrid, Paintbrush, PaintBucket } from "lucide-react"; +import { useCallback, useMemo, useRef, useState } from "react"; + +import { allBlobCombinations, allDualCombinations, randomMap, renderTestMap, setCell } from "../../../core/index.ts"; +import { fitZoom, PixelCanvas, useElementSize, type OverlayContext } from "../../components/PixelCanvas.tsx"; +import { Button, Kbd } from "../../components/ui.tsx"; +import { useTileset } from "../../state/derived.ts"; +import { emptyMap, MAP_HEIGHT, MAP_WIDTH, useApp } from "../../state/store.ts"; +import { ZoomControls } from "../tileset/TilesetView.tsx"; + +export function TestMapView() { + const result = useTileset(); + const map = useApp((s) => s.map); + const setMap = useApp((s) => s.setMap); + const zoomSetting = useApp((s) => s.settings.mapZoom); + const setSettings = useApp((s) => s.setSettings); + const box = useRef(null); + const size = useElementSize(box); + const [seed, setSeed] = useState(5); + const painting = useRef(null); + const tileset = result?.tileset ?? null; + + const image = useMemo(() => (tileset ? renderTestMap(tileset, map) : null), [tileset, map]); + const fit = image ? fitZoom(image, { width: size.width - 2, height: size.height - 2 }, 8) : 1; + const zoom = zoomSetting === "fit" ? fit : zoomSetting; + + const overlay = useCallback( + ({ ctx, scale }: OverlayContext) => { + if (!tileset) return; + const t = tileset.tileSize * scale; + ctx.fillStyle = "rgba(255, 255, 255, 0.07)"; + for (let x = 1; x < map.width; x++) ctx.fillRect(x * t, 0, 1, map.height * t); + for (let y = 1; y < map.height; y++) ctx.fillRect(0, y * t, map.width * t, 1); + }, + [tileset, map.width, map.height], + ); + + if (!tileset || !image) { + return ( +
    + {result?.error ?? "Open a template to paint with its tileset."} +
    + ); + } + + const dual = tileset.layout.kind === "dual"; + const combos = () => setMap(dual ? allDualCombinations() : allBlobCombinations(8)); + + return ( +
    +
    + + + + + + drag to paint · right-drag or Alt to erase + +
    + setSettings({ mapZoom: v })} /> +
    +
    +
    +
    + { + if (type === "up" || type === "leave" || !p) { + painting.current = null; + return; + } + if (type === "down") painting.current = !(p.buttons & 2 || p.altKey); + if (painting.current === null || !p.buttons) return; + const x = Math.floor(p.x / tileset.tileSize); + const y = Math.floor(p.y / tileset.tileSize); + const next = setCell(useApp.getState().map, x, y, painting.current); + if (next !== useApp.getState().map) setMap(next); + }} + /> +
    +
    +
    + {dual + ? "Dual grid: tiles are drawn half a tile off the painted cells, like TileMapDual does." + : "Painted like Godot's terrain painting picks tiles (verified against Godot 4.7)."} +
    +
    + ); +} diff --git a/src/app/features/tileset/TilesetView.tsx b/src/app/features/tileset/TilesetView.tsx new file mode 100644 index 0000000..7c3528b --- /dev/null +++ b/src/app/features/tileset/TilesetView.tsx @@ -0,0 +1,295 @@ +import { Grid3x3, Maximize, Minus, Plus, Shapes, SquareDashed, Waypoints } from "lucide-react"; +import { useCallback, useId, useRef, type KeyboardEvent } from "react"; + +import { + DIRECTIONS, + directionsOf, + godot3Bitmask, + godot4PeeringBits, + type Tileset, + type TilesetCell, +} from "../../../core/index.ts"; +import { fitZoom, PixelCanvas, useElementSize, type OverlayContext } from "../../components/PixelCanvas.tsx"; +import { IconButton, Kbd, Tip } from "../../components/ui.tsx"; +import { cn } from "../../lib/cn.ts"; +import { useTileset } from "../../state/derived.ts"; +import { useApp, type Overlays } from "../../state/store.ts"; + +export function ZoomControls({ + value, + fit, + onChange, +}: { + value: number | "fit"; + fit: number; + onChange: (v: number | "fit") => void; +}) { + const current = value === "fit" ? fit : value; + const label = Number.isInteger(current) ? `${current}×` : `${current.toFixed(1)}×`; + return ( +
    + onChange(Math.max(1, Math.ceil(current) - 1))} + disabled={current <= 1} + > + + + {label} + onChange(Math.min(16, Math.floor(current) + 1))} + disabled={current >= 16} + > + + + onChange("fit")} className={cn(value === "fit" && "text-accent")}> + + +
    + ); +} + +function OverlayToggle({ + id, + label, + keyHint, + children, +}: { + id: keyof Overlays; + label: string; + keyHint: string; + children: React.ReactNode; +}) { + const on = useApp((s) => s.settings.overlays[id]); + const setOverlay = useApp((s) => s.setOverlay); + return ( + + {label} {keyHint} + + } + > + + + ); +} + +function drawOverlays( + ctx: CanvasRenderingContext2D, + scale: number, + tileset: Tileset, + overlays: Overlays, + collision: boolean, + hover: { x: number; y: number } | null, +) { + const ts = tileset.tileSize; + const t = ts * scale; + const { columns, rows } = tileset.layout; + const W = columns * t; + const H = rows * t; + if (overlays.quarters) { + ctx.fillStyle = "rgba(255, 255, 255, 0.14)"; + const half = Math.floor(ts / 2) * scale; + for (let x = 0; x < columns; x++) ctx.fillRect(x * t + half, 0, 1, H); + for (let y = 0; y < rows; y++) ctx.fillRect(0, y * t + half, W, 1); + } + if (overlays.grid) { + ctx.fillStyle = "rgba(255, 255, 255, 0.30)"; + for (let x = 1; x < columns; x++) ctx.fillRect(x * t, 0, 1, H); + for (let y = 1; y < rows; y++) ctx.fillRect(0, y * t, W, 1); + } + if (overlays.bits) { + // small markers on a 3×3 grid, like Godot's terrain peering-bit editor + const third = t / 3; + const m = Math.max(2, Math.round(t / 7)); + for (const cell of tileset.cells) { + const ox = cell.x * t; + const oy = cell.y * t; + const square = (col: number, row: number) => { + const x = Math.round(ox + col * third + (third - m) / 2); + const y = Math.round(oy + row * third + (third - m) / 2); + ctx.fillStyle = "rgba(13, 20, 34, 0.85)"; + ctx.fillRect(x - 1, y - 1, m + 2, m + 2); + ctx.fillStyle = "#699ce8"; + ctx.fillRect(x, y, m, m); + }; + if (cell.mask !== null) { + square(1, 1); + for (const d of DIRECTIONS) if (cell.mask & d.bit) square(d.dx + 1, d.dy + 1); + } else if (cell.corners !== null) { + if (cell.corners & 1) square(0, 0); + if (cell.corners & 2) square(2, 0); + if (cell.corners & 4) square(0, 2); + if (cell.corners & 8) square(2, 2); + } + } + } + if (overlays.collision && collision && tileset.layout.kind === "blob") { + ctx.strokeStyle = "rgba(80, 220, 255, 0.85)"; + ctx.lineWidth = 1; + for (const cell of tileset.cells) ctx.strokeRect(cell.x * t + 1.5, cell.y * t + 1.5, t - 3, t - 3); + } + if (hover) { + ctx.strokeStyle = "#fcc751"; + ctx.lineWidth = Math.max(2, Math.round(scale / 2)); + ctx.strokeRect(hover.x * t + 1, hover.y * t + 1, t - 2, t - 2); + } +} + +function describe(cell: TilesetCell | undefined): string { + if (!cell) return ""; + if (cell.mask !== null) { + const dirs = directionsOf(cell.mask); + return `Tile ${cell.x},${cell.y} · neighbours: ${dirs.length ? dirs.join(" ") : "none (isolated)"} · Godot 3 bitmask ${godot3Bitmask(cell.mask)} · Godot 4 bits: ${godot4PeeringBits(cell.mask).length}`; + } + const c = cell.corners ?? 0; + const names = [c & 1 && "TL", c & 2 && "TR", c & 4 && "BL", c & 8 && "BR"].filter(Boolean); + return `Dual tile ${cell.x},${cell.y} · filled corners: ${names.length ? names.join(" ") : "none"}`; +} + +const ARROWS: Record = { + ArrowLeft: [-1, 0], + ArrowRight: [1, 0], + ArrowUp: [0, -1], + ArrowDown: [0, 1], +}; + +/** The next tile from `from` in direction (dx, dy), skipping the layout's empty cells. */ +function step(tileset: Tileset, from: { x: number; y: number }, dx: number, dy: number) { + const { columns, rows } = tileset.layout; + for (let x = from.x + dx, y = from.y + dy; x >= 0 && y >= 0 && x < columns && y < rows; x += dx, y += dy) { + if (tileset.cells.some((c) => c.x === x && c.y === y)) return { x, y }; + } + return null; +} + +const firstCell = (tileset: Tileset) => + tileset.cells.reduce((a, c) => (c.y < a.y || (c.y === a.y && c.x < a.x) ? c : a), tileset.cells[0]); + +export function TilesetView() { + const result = useTileset(); + const zoomSetting = useApp((s) => s.settings.zoom); + const overlays = useApp((s) => s.settings.overlays); + const collision = useApp((s) => s.settings.collision); + const hover = useApp((s) => s.hover); + const setHover = useApp((s) => s.setHover); + const setSettings = useApp((s) => s.setSettings); + const box = useRef(null); + const size = useElementSize(box); + const statusId = useId(); + const tileset = result?.tileset ?? null; + + const overlay = useCallback( + ({ ctx, scale }: OverlayContext) => { + if (tileset) drawOverlays(ctx, scale, tileset, overlays, collision, hover); + }, + [tileset, overlays, collision, hover], + ); + + // keyboard inspection: arrow keys walk the tiles, the status bar (a live region) describes the current one + const onKeyDown = (e: KeyboardEvent) => { + const d = ARROWS[e.key]; + if (!d || !tileset) return; + e.preventDefault(); + const next = hover ? step(tileset, hover, d[0], d[1]) : firstCell(tileset); + if (next) setHover({ x: next.x, y: next.y }); + }; + + const fit = tileset ? fitZoom(tileset.image, { width: size.width - 2, height: size.height - 2 }) : 1; + const zoom = zoomSetting === "fit" ? fit : zoomSetting; + const hovered = tileset && hover ? tileset.cells.find((c) => c.x === hover.x && c.y === hover.y) : undefined; + + return ( +
    +
    + + Grid + + + Quarters + + + Bits + + + Collision + +
    + {tileset ? setSettings({ zoom: v })} /> : null} +
    +
    +
    + {tileset ? ( +
    + { + if (!useApp.getState().hover) { + const first = firstCell(tileset); + setHover({ x: first.x, y: first.y }); + } + }} + onBlur={() => setHover(null)} + image={tileset.image} + zoom={zoom} + overlay={overlay} + onPointer={(p) => { + if (!p) return setHover(null); + const x = Math.floor(p.x / tileset.tileSize); + const y = Math.floor(p.y / tileset.tileSize); + const cell = tileset.cells.find((c) => c.x === x && c.y === y); + const next = cell ? { x, y } : null; + if (next?.x !== hover?.x || next?.y !== hover?.y) setHover(next); + }} + /> +
    + ) : ( + + )} +
    +
    + + {hovered + ? describe(hovered) + : tileset + ? `${tileset.cells.length} tiles · ${tileset.layout.name} · ${tileset.tileSize} px — hover a tile (or focus the image and use the arrow keys) to inspect it` + : ""} + +
    +
    + ); +} + +function EmptyState({ error }: { error: string | null }) { + return error ? ( +
    +

    The tileset can't be generated yet.

    +

    {error}

    +
    + ) : ( +
    +

    Turn a small template into a full autotile tileset

    +

    + Open or drop a PNG in the 13-tile Autotiler or RPG Maker A2 layout. You get the 47-tile blob set (or a 16-tile + dual grid) with Godot 4 terrains, Tiled Wang sets or plain PNG — and a test map to try it. +

    +
    + ); +} diff --git a/src/app/lib/cn.ts b/src/app/lib/cn.ts new file mode 100644 index 0000000..bc02555 --- /dev/null +++ b/src/app/lib/cn.ts @@ -0,0 +1,4 @@ +/** Joins class names, skipping falsy values. */ +export function cn(...classes: Array): string { + return classes.filter(Boolean).join(" "); +} diff --git a/src/app/main.tsx b/src/app/main.tsx new file mode 100644 index 0000000..fe6bd8d --- /dev/null +++ b/src/app/main.tsx @@ -0,0 +1,28 @@ +import { Toast } from "@base-ui/react/toast"; +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; + +import { App } from "./App.tsx"; +import { toastManager } from "./components/toasts.tsx"; +import { restoreLastFile } from "./features/actions.ts"; +import { initPlatform } from "./platform/index.ts"; +import { hydrate } from "./state/store.ts"; + +import "./styles.css"; + +async function start() { + await initPlatform(); + const { lastFile } = await hydrate(); + const root = document.getElementById("root"); + if (!root) throw new Error("#root missing"); + createRoot(root).render( + + + + + , + ); + await restoreLastFile(lastFile); +} + +void start(); diff --git a/src/app/platform/fs-access.d.ts b/src/app/platform/fs-access.d.ts new file mode 100644 index 0000000..97e61eb --- /dev/null +++ b/src/app/platform/fs-access.d.ts @@ -0,0 +1,30 @@ +// File System Access API bits that aren't in TypeScript's DOM lib (Chromium only). +interface FilePickerAcceptType { + description?: string; + accept: Record; +} + +interface OpenFilePickerOptions { + multiple?: boolean; + excludeAcceptAllOption?: boolean; + types?: FilePickerAcceptType[]; +} + +interface DirectoryPickerOptions { + id?: string; + mode?: "read" | "readwrite"; +} + +interface Window { + showOpenFilePicker?: (options?: OpenFilePickerOptions) => Promise; + showDirectoryPicker?: (options?: DirectoryPickerOptions) => Promise; +} + +interface DataTransferItem { + getAsFileSystemHandle?: () => Promise; +} + +interface FileSystemHandle { + queryPermission?: (descriptor?: { mode?: "read" | "readwrite" }) => Promise; + requestPermission?: (descriptor?: { mode?: "read" | "readwrite" }) => Promise; +} diff --git a/src/app/platform/index.ts b/src/app/platform/index.ts new file mode 100644 index 0000000..114b1c8 --- /dev/null +++ b/src/app/platform/index.ts @@ -0,0 +1,18 @@ +import { isTauri } from "@tauri-apps/api/core"; + +import type { Platform } from "./types.ts"; +import { webPlatform } from "./web.ts"; + +export type { OutputFolder, Platform, SaveResult, SourceFile } from "./types.ts"; + +let current: Platform = webPlatform; + +export function platform(): Platform { + return current; +} + +/** Swaps in the desktop implementation when running inside Tauri (loaded lazily so the web build stays lean). */ +export async function initPlatform(): Promise { + current = isTauri() ? (await import("./tauri.ts")).tauriPlatform : webPlatform; + return current; +} diff --git a/src/app/platform/tauri.ts b/src/app/platform/tauri.ts new file mode 100644 index 0000000..569c727 --- /dev/null +++ b/src/app/platform/tauri.ts @@ -0,0 +1,120 @@ +// Desktop platform (Tauri 2): native dialogs, drag & drop with real paths, file watching, direct writes into a +// folder, the store plugin for settings. Loaded lazily, only inside the desktop app. +import { invoke } from "@tauri-apps/api/core"; +import { basename, join } from "@tauri-apps/api/path"; +import { getCurrentWebview } from "@tauri-apps/api/webview"; +import { open, save } from "@tauri-apps/plugin-dialog"; +import { readFile, watch, writeFile, writeTextFile } from "@tauri-apps/plugin-fs"; +import { openUrl, revealItemInDir } from "@tauri-apps/plugin-opener"; +import { load, type Store } from "@tauri-apps/plugin-store"; + +import type { ExportFile } from "../../core/index.ts"; +import type { OutputFolder, Platform, SaveResult, SourceFile } from "./types.ts"; + +const PNG_FILTER = [{ name: "PNG image", extensions: ["png"] }]; + +async function readSource(path: string): Promise { + return { name: await basename(path), bytes: await readFile(path), ref: path, location: path }; +} + +async function write(path: string, data: ExportFile["data"]): Promise { + if (typeof data === "string") await writeTextFile(path, data); + else await writeFile(path, data); +} + +let storePromise: Promise | undefined; +const settingsStore = () => (storePromise ??= load("settings.json", { autoSave: false, defaults: {} })); + +async function pickFolder(title: string): Promise { + const dir = await open({ directory: true, recursive: true, title }); + return typeof dir === "string" ? { label: await basename(dir), path: dir, ref: dir } : null; +} + +export const tauriPlatform: Platform = { + kind: "desktop", + canWatch: true, + canPickFolder: true, + + openImage: async () => { + const path = await open({ multiple: false, directory: false, filters: PNG_FILTER, title: "Open a template image" }); + return typeof path === "string" ? readSource(path) : null; + }, + + openPath: readSource, + + fileFromDataTransfer: async (dt) => { + const file = dt.files[0]; + return file ? { name: file.name, bytes: new Uint8Array(await file.arrayBuffer()) } : null; + }, + + subscribeNativeDrop: (onFile, onHover) => + getCurrentWebview().onDragDropEvent((event) => { + const p = event.payload; + if (p.type === "enter" || p.type === "over") onHover(true); + else if (p.type === "leave") onHover(false); + else { + onHover(false); + const path = p.paths.find((x) => x.toLowerCase().endsWith(".png")) ?? p.paths[0]; + if (path) void readSource(path).then(onFile, () => undefined); + } + }), + + watch: (file, onChange) => { + const path = file.ref; + if (typeof path !== "string") return null; + let stop: (() => void) | undefined; + let cancelled = false; + // debounced; editors often write in several steps. Unreadable intermediate states are skipped. + void watch(path, () => void readSource(path).then(onChange, () => undefined), { delayMs: 200 }).then( + (unwatch) => { + if (cancelled) unwatch(); + else stop = unwatch; + }, + () => undefined, + ); + return () => { + cancelled = true; + stop?.(); + }; + }, + + pickOutputFolder: () => pickFolder("Export folder"), + + save: async (files, _zipName, folder): Promise => { + if (!folder && files.length === 1) { + const ext = files[0].path.split(".").pop() ?? ""; + const target = await save({ + defaultPath: files[0].path, + filters: [{ name: ext.toUpperCase(), extensions: [ext] }], + }); + if (!target) return null; + await write(target, files[0].data); + return { kind: "written", where: target }; + } + const dest = folder ?? (await pickFolder("Export into folder")); + if (!dest?.path) return null; + const dir = dest.path; + await Promise.all(files.map(async (f) => write(await join(dir, f.path), f.data))); + return { kind: "written", where: dest.label, folder: folder ? undefined : dest }; + }, + + godotPathOf: async (folder) => (folder.path ? invoke("godot_res_path", { dir: folder.path }) : null), + + reveal: (path) => revealItemInDir(path), + + copyPng: async (png) => { + await navigator.clipboard.write([ + new ClipboardItem({ "image/png": new Blob([new Uint8Array(png)], { type: "image/png" }) }), + ]); + }, + + openUrl: (url) => void openUrl(url), + + loadState: async () => (await settingsStore()).get("state"), + + saveState: async (state) => { + const store = await settingsStore(); + await store.set("state", state); + await store.save(); + }, +}; diff --git a/src/app/platform/types.ts b/src/app/platform/types.ts new file mode 100644 index 0000000..c364e8a --- /dev/null +++ b/src/app/platform/types.ts @@ -0,0 +1,52 @@ +import type { ExportFile } from "../../core/index.ts"; + +export interface SourceFile { + readonly name: string; + readonly bytes: Uint8Array; + /** platform reference used to re-read and watch the file (web: FileSystemFileHandle, desktop: path) */ + readonly ref?: unknown; + /** human readable location, when known (desktop: absolute path) */ + readonly location?: string; +} + +export interface OutputFolder { + /** what to show in the UI */ + readonly label: string; + /** platform reference (web: FileSystemDirectoryHandle, desktop: path) */ + readonly ref: unknown; + /** absolute path, when the platform knows it */ + readonly path?: string; +} + +export interface SaveResult { + readonly kind: "downloaded" | "written"; + /** file or folder the user will find the result in */ + readonly where: string; + /** a folder the user picked during this save, worth remembering for the next export */ + readonly folder?: OutputFolder; +} + +export interface Platform { + readonly kind: "web" | "desktop"; + /** can re-read the opened file when it changes on disk */ + readonly canWatch: boolean; + /** can write straight into a folder (otherwise files are downloaded) */ + readonly canPickFolder: boolean; + openImage: () => Promise; + /** re-opens a file by absolute path (desktop: recent files, restoring the last session) */ + openPath?: (path: string) => Promise; + fileFromDataTransfer: (dt: DataTransfer) => Promise; + /** native file drops (desktop); the web uses HTML5 drag and drop instead */ + subscribeNativeDrop?: (onFile: (file: SourceFile) => void, onHover: (over: boolean) => void) => Promise<() => void>; + watch: (file: SourceFile, onChange: (file: SourceFile) => void) => (() => void) | null; + pickOutputFolder: () => Promise; + /** null when the user cancelled a dialog */ + save: (files: readonly ExportFile[], zipName: string, folder: OutputFolder | null) => Promise; + /** res:// path of `folder` when it is inside a Godot project */ + godotPathOf: (folder: OutputFolder) => Promise; + reveal?: (path: string) => Promise; + copyPng: (png: Uint8Array) => Promise; + openUrl: (url: string) => void; + loadState: () => Promise; + saveState: (state: unknown) => Promise; +} diff --git a/src/app/platform/web.ts b/src/app/platform/web.ts new file mode 100644 index 0000000..dbc014f --- /dev/null +++ b/src/app/platform/web.ts @@ -0,0 +1,179 @@ +// Browser platform: /drag & drop/paste, downloads (zip for multi-file exports), and — in Chromium, +// when not inside a cross-origin iframe like itch.io's — File System Access for watching the source and writing +// straight into a folder. +import { zipFiles, type ExportFile } from "../../core/index.ts"; +import type { OutputFolder, Platform, SaveResult, SourceFile } from "./types.ts"; + +const STORAGE_KEY = "autotiler:v2"; +const topLevel = typeof window !== "undefined" && window.self === window.top; +const hasOpenPicker = topLevel && typeof window.showOpenFilePicker === "function"; +const hasDirPicker = topLevel && typeof window.showDirectoryPicker === "function"; + +const PNG_TYPES: FilePickerAcceptType[] = [{ description: "PNG images", accept: { "image/png": [".png"] } }]; + +async function fromFile(file: File, ref?: FileSystemFileHandle): Promise { + return { name: file.name, bytes: new Uint8Array(await file.arrayBuffer()), ref }; +} + +function isFileHandle(h: unknown): h is FileSystemFileHandle { + return typeof FileSystemFileHandle !== "undefined" && h instanceof FileSystemFileHandle; +} + +function isDirHandle(h: unknown): h is FileSystemDirectoryHandle { + return typeof FileSystemDirectoryHandle !== "undefined" && h instanceof FileSystemDirectoryHandle; +} + +function pickWithInput(): Promise { + return new Promise((resolve) => { + const input = document.createElement("input"); + input.type = "file"; + input.accept = "image/png"; + input.addEventListener("change", () => { + const file = input.files?.[0]; + resolve(file ? fromFile(file) : null); + }); + input.addEventListener("cancel", () => resolve(null)); + input.click(); + }); +} + +export function toBlob(data: Uint8Array | string, type: string): Blob { + return new Blob([typeof data === "string" ? data : new Uint8Array(data)], { type }); +} + +export function download(name: string, blob: Blob): void { + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = name; + document.body.append(a); + a.click(); + a.remove(); + setTimeout(() => URL.revokeObjectURL(url), 10_000); +} + +const mime = (path: string) => + path.endsWith(".png") ? "image/png" : path.endsWith(".json") ? "application/json" : "text/plain"; + +export const webPlatform: Platform = { + kind: "web", + canWatch: hasOpenPicker, + canPickFolder: hasDirPicker, + + async openImage() { + if (!hasOpenPicker || !window.showOpenFilePicker) return pickWithInput(); + try { + const [handle] = await window.showOpenFilePicker({ types: PNG_TYPES, excludeAcceptAllOption: false }); + return handle ? fromFile(await handle.getFile(), handle) : null; + } catch (err) { + if (err instanceof DOMException && err.name === "AbortError") return null; + return pickWithInput(); + } + }, + + async fileFromDataTransfer(dt) { + const item = [...dt.items].find((i) => i.kind === "file"); + if (item?.getAsFileSystemHandle && hasOpenPicker) { + try { + const handle = await item.getAsFileSystemHandle(); + if (isFileHandle(handle)) return fromFile(await handle.getFile(), handle); + } catch { + // fall back to the plain File below + } + } + const file = dt.files[0]; + return file ? fromFile(file) : null; + }, + + watch(file, onChange) { + const handle = file.ref; + if (!isFileHandle(handle)) return null; + let last = -1; + let stopped = false; + let timer: ReturnType | undefined; + const tick = async () => { + try { + const f = await handle.getFile(); + if (last !== -1 && f.lastModified !== last) onChange(await fromFile(f, handle)); + last = f.lastModified; + } catch { + // file moved or permission revoked: keep the last good version + } + if (!stopped) timer = setTimeout(tick, 1000); + }; + timer = setTimeout(tick, 0); + return () => { + stopped = true; + clearTimeout(timer); + }; + }, + + async pickOutputFolder() { + if (!window.showDirectoryPicker) return null; + try { + const dir = await window.showDirectoryPicker({ id: "autotiler-export", mode: "readwrite" }); + return { label: dir.name, ref: dir }; + } catch { + return null; + } + }, + + async save(files: readonly ExportFile[], zipName: string, folder: OutputFolder | null): Promise { + const dir = folder?.ref; + if (isDirHandle(dir)) { + if (dir.requestPermission && (await dir.requestPermission({ mode: "readwrite" })) !== "granted") { + throw new Error("Permission to write into the folder was denied."); + } + await Promise.all( + files.map(async (f) => { + const writable = await (await dir.getFileHandle(f.path, { create: true })).createWritable(); + await writable.write(toBlob(f.data, mime(f.path))); + await writable.close(); + }), + ); + return { kind: "written", where: dir.name }; + } + if (files.length === 1) { + download(files[0].path, toBlob(files[0].data, mime(files[0].path))); + return { kind: "downloaded", where: files[0].path }; + } + download(zipName, toBlob(zipFiles(files), "application/zip")); + return { kind: "downloaded", where: zipName }; + }, + + async godotPathOf(folder) { + const dir = folder.ref; + if (!isDirHandle(dir)) return null; + try { + await dir.getFileHandle("project.godot"); + return "res://"; + } catch { + return null; + } + }, + + async copyPng(png) { + await navigator.clipboard.write([new ClipboardItem({ "image/png": toBlob(png, "image/png") })]); + }, + + openUrl(url) { + window.open(url, "_blank", "noopener,noreferrer"); + }, + + async loadState() { + try { + const raw = localStorage.getItem(STORAGE_KEY); + return raw ? JSON.parse(raw) : null; + } catch { + return null; + } + }, + + async saveState(state) { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); + } catch { + // private mode / storage full: settings just won't persist + } + }, +}; diff --git a/src/app/state/derived.ts b/src/app/state/derived.ts new file mode 100644 index 0000000..6e9a549 --- /dev/null +++ b/src/app/state/derived.ts @@ -0,0 +1,104 @@ +// Values derived from the store. Generation is fast (~1 ms) but canvases re-render often, so results are memoized +// on the identity of their inputs. +import { + analyzeSource, + generate, + getLayout, + sanitizeName, + terrainColor, + type Rgb, + type SourceAnalysis, + type Tileset, +} from "../../core/index.ts"; +import { useApp, type Settings, type Source } from "./store.ts"; + +let lastAnalysis: { + source: Source; + templateId: Settings["templateId"]; + tileSize: number | null; + value: SourceAnalysis; +} | null = null; + +export function analysisOf( + source: Source, + templateId: Settings["templateId"], + tileSize: number | null, +): SourceAnalysis { + if ( + lastAnalysis && + lastAnalysis.source === source && + lastAnalysis.templateId === templateId && + lastAnalysis.tileSize === tileSize + ) { + return lastAnalysis.value; + } + const value = analyzeSource(source.image, templateId === "auto" ? undefined : templateId, tileSize ?? undefined); + lastAnalysis = { source, templateId, tileSize, value }; + return value; +} + +export interface TilesetResult { + readonly analysis: SourceAnalysis; + readonly tileset: Tileset | null; + readonly error: string | null; +} + +let lastTileset: { analysis: SourceAnalysis; layoutId: Settings["layoutId"]; value: TilesetResult } | null = null; + +export function tilesetOf( + source: Source, + settings: Pick, +): TilesetResult { + const analysis = analysisOf(source, settings.templateId, settings.tileSize); + if (lastTileset && lastTileset.analysis === analysis && lastTileset.layoutId === settings.layoutId) + return lastTileset.value; + let value: TilesetResult; + const blocking = analysis.issues.find((i) => i.level === "error"); + if (blocking || !analysis.template || !analysis.tileSize) { + value = { analysis, tileset: null, error: blocking?.message ?? "The image can't be used as a template." }; + } else { + try { + const tileset = generate(source.image, { + template: analysis.template, + tileSize: analysis.tileSize, + layout: getLayout(settings.layoutId), + }); + value = { analysis, tileset, error: null }; + } catch (err) { + value = { analysis, tileset: null, error: err instanceof Error ? err.message : String(err) }; + } + } + lastTileset = { analysis, layoutId: settings.layoutId, value }; + return value; +} + +export function useTileset(): TilesetResult | null { + const source = useApp((s) => s.source); + const templateId = useApp((s) => s.settings.templateId); + const tileSize = useApp((s) => s.settings.tileSize); + const layoutId = useApp((s) => s.settings.layoutId); + return source ? tilesetOf(source, { templateId, tileSize, layoutId }) : null; +} + +export function baseName(source: Source | null): string { + return sanitizeName(source?.file.name ?? "tileset"); +} + +export function exportNameOf(source: Source | null, settings: Settings): string { + return sanitizeName(settings.fileName || `${baseName(source)}-autotile`); +} + +export function terrainNameOf(source: Source | null, settings: Settings): string { + return settings.terrainName.trim() || baseName(source); +} + +export function hexToRgb(hex: string): Rgb | null { + const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex); + return m ? [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16)] : null; +} + +export function terrainColorOf(source: Source | null, tileset: Tileset | null, settings: Settings): Rgb { + const manual = settings.terrainColor ? hexToRgb(settings.terrainColor) : null; + if (manual) return manual; + return source && tileset ? terrainColor(tileset, source.image) : [90, 154, 60]; +} diff --git a/src/app/state/store.ts b/src/app/state/store.ts new file mode 100644 index 0000000..bf63d4f --- /dev/null +++ b/src/app/state/store.ts @@ -0,0 +1,233 @@ +import { create } from "zustand"; + +import { + createTestMap, + decodePng, + DEFAULT_LAYOUT_ID, + isLayoutId, + randomMap, + type LayoutId, + type RgbaImage, + type TargetId, + type TemplateId, + type TestMap, +} from "../../core/index.ts"; +import { platform, type OutputFolder, type SourceFile } from "../platform/index.ts"; + +export interface Source { + readonly file: SourceFile; + readonly image: RgbaImage; + /** increases on every (re)load, so live reloads trigger auto-export */ + readonly revision: number; + /** "reload" when the watched file changed on disk */ + readonly reason: "open" | "reload"; +} + +export interface Overlays { + grid: boolean; + quarters: boolean; + bits: boolean; + collision: boolean; +} + +export interface Settings { + templateId: TemplateId | "auto"; + /** null = detect from the image */ + tileSize: number | null; + layoutId: LayoutId; + target: TargetId; + /** "" = derive from the file name */ + terrainName: string; + /** "#rrggbb", null = average colour of the fill */ + terrainColor: string | null; + collision: boolean; + forceNearest: boolean; + sidecar: boolean; + /** "" = "-autotile" */ + fileName: string; + autoExport: boolean; + zoom: number | "fit"; + mapZoom: number | "fit"; + overlays: Overlays; + /** the Godot texture-filter tip is shown once, after the first Godot 4 export */ + filterTipShown: boolean; +} + +export const DEFAULT_SETTINGS: Settings = { + templateId: "auto", + tileSize: null, + layoutId: DEFAULT_LAYOUT_ID, + target: "godot4", + terrainName: "", + terrainColor: null, + collision: true, + forceNearest: false, + sidecar: false, + fileName: "", + autoExport: false, + zoom: "fit", + mapZoom: "fit", + overlays: { grid: true, quarters: false, bits: false, collision: false }, + filterTipShown: false, +}; + +export const MAP_WIDTH = 32; +export const MAP_HEIGHT = 20; + +export type View = "tileset" | "map"; + +interface AppState { + hydrated: boolean; + source: Source | null; + loadError: string | null; + settings: Settings; + outputFolder: OutputFolder | null; + /** res:// path of the output folder when it is inside a Godot project */ + godotPath: string | null; + view: View; + /** hovered tileset cell (atlas coordinates) */ + hover: { x: number; y: number } | null; + map: TestMap; + /** absolute paths of recently opened files, newest first (desktop only) */ + recent: string[]; + + loadFile: (file: SourceFile, reason?: "open" | "reload") => boolean; + closeSource: () => void; + setSettings: (patch: Partial) => void; + setOverlay: (key: keyof Overlays, on: boolean) => void; + setOutputFolder: (folder: OutputFolder | null) => void; + setView: (view: View) => void; + setHover: (cell: { x: number; y: number } | null) => void; + setMap: (map: TestMap) => void; + clearRecent: () => void; +} + +let revision = 0; + +export const useApp = create()((set, get) => ({ + hydrated: false, + source: null, + loadError: null, + settings: DEFAULT_SETTINGS, + outputFolder: null, + godotPath: null, + view: "tileset", + hover: null, + map: randomMap(MAP_WIDTH, MAP_HEIGHT, 4, 0.52), + recent: [], + + loadFile: (file, reason = "open") => { + try { + const image = decodePng(file.bytes); + const path = file.location; + set({ + source: { file, image, revision: ++revision, reason }, + loadError: null, + hover: reason === "open" ? null : get().hover, + ...(path && reason === "open" ? { recent: [path, ...get().recent.filter((r) => r !== path)].slice(0, 8) } : {}), + }); + return true; + } catch (err) { + set({ loadError: `${file.name}: ${err instanceof Error ? err.message : String(err)}` }); + return false; + } + }, + closeSource: () => set({ source: null, loadError: null, hover: null }), + setSettings: (patch) => set((s) => ({ settings: { ...s.settings, ...patch } })), + setOverlay: (key, on) => + set((s) => ({ settings: { ...s.settings, overlays: { ...s.settings.overlays, [key]: on } } })), + setOutputFolder: (folder) => { + set({ outputFolder: folder, godotPath: null }); + if (folder) { + void platform() + .godotPathOf(folder) + .then((godotPath) => { + if (get().outputFolder === folder) set({ godotPath }); + }); + } + }, + setView: (view) => set({ view }), + setHover: (hover) => set({ hover }), + setMap: (map) => set({ map }), + clearRecent: () => set({ recent: [] }), +})); + +// ---- persistence (settings, recent files, desktop output folder, test map) ---- + +interface Persisted { + settings?: Partial; + recent?: string[]; + outputFolder?: { label: string; path: string } | null; + map?: { width: number; height: number; cells: string }; + /** the file that was open when the app quit (desktop), reopened on the next launch */ + lastFile?: string | null; +} + +function isRecord(v: unknown): v is Record { + return typeof v === "object" && v !== null; +} + +/** Restores the persisted state; returns the file to reopen, if any. */ +export async function hydrate(): Promise<{ lastFile: string | null }> { + const raw: unknown = await platform().loadState(); + const data: Persisted = isRecord(raw) ? raw : {}; + const settings: Settings = { ...DEFAULT_SETTINGS, ...(isRecord(data.settings) ? data.settings : {}) }; + settings.overlays = { ...DEFAULT_SETTINGS.overlays, ...(isRecord(settings.overlays) ? settings.overlays : {}) }; + if (!isLayoutId(settings.layoutId)) settings.layoutId = DEFAULT_LAYOUT_ID; + const map = data.map; + const restored = + map && + Number.isInteger(map.width) && + Number.isInteger(map.height) && + map.width > 0 && + map.height > 0 && + map.width <= 128 && + map.height <= 128 && + typeof map.cells === "string" && + map.cells.length === map.width * map.height + ? { width: map.width, height: map.height, cells: Uint8Array.from(map.cells, (c) => (c === "1" ? 1 : 0)) } + : null; + useApp.setState({ + hydrated: true, + settings, + recent: Array.isArray(data.recent) ? data.recent.filter((r) => typeof r === "string") : [], + ...(restored ? { map: restored } : {}), + }); + const folder = data.outputFolder; + if (platform().kind === "desktop" && folder?.path) { + useApp.getState().setOutputFolder({ label: folder.label, path: folder.path, ref: folder.path }); + } + return { lastFile: typeof data.lastFile === "string" && platform().openPath ? data.lastFile : null }; +} + +let saveTimer: ReturnType | undefined; +useApp.subscribe((state, prev) => { + if (!state.hydrated) return; + if ( + state.settings === prev.settings && + state.recent === prev.recent && + state.outputFolder === prev.outputFolder && + state.map === prev.map && + state.source?.file.location === prev.source?.file.location + ) { + return; + } + clearTimeout(saveTimer); + saveTimer = setTimeout(() => { + const s = useApp.getState(); + const persisted: Persisted = { + settings: s.settings, + recent: s.recent, + outputFolder: s.outputFolder?.path ? { label: s.outputFolder.label, path: s.outputFolder.path } : null, + map: { + width: s.map.width, + height: s.map.height, + cells: Array.from(s.map.cells, (c) => (c ? "1" : "0")).join(""), + }, + lastFile: s.source?.file.location ?? null, + }; + void platform().saveState(persisted); + }, 300); +}); + +export const emptyMap = () => createTestMap(MAP_WIDTH, MAP_HEIGHT); diff --git a/src/app/styles.css b/src/app/styles.css new file mode 100644 index 0000000..944390a --- /dev/null +++ b/src/app/styles.css @@ -0,0 +1,67 @@ +@import "tailwindcss"; + +/* Compact dark theme in the spirit of the Godot editor (and v1's palette). */ +@theme { + --color-app: #1c202a; + --color-panel: #232937; + --color-raised: #2f3648; + --color-field: #1a1e28; + --color-line: #363e52; + --color-line-strong: #4a5470; + --color-ink: #d6d9e0; + --color-muted: #9aa3b5; /* ≥ 4.5:1 on every surface, down to raised */ + --color-accent: #699ce8; + --color-accent-ink: #0d1422; + --color-warn: #fcc751; + --color-danger: #ef6b73; + --color-ok: #7fc97a; + --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; +} + +@layer base { + :root { + color-scheme: dark; + } + html, + body, + #root { + height: 100%; + } + body { + margin: 0; + background: var(--color-app); + color: var(--color-ink); + font-family: var(--font-sans); + -webkit-font-smoothing: antialiased; + } + input[type="number"] { + appearance: textfield; + } + input[type="number"]::-webkit-inner-spin-button { + opacity: 0.5; + } + input[type="color"]::-webkit-color-swatch-wrapper { + padding: 0; + } + input[type="color"]::-webkit-color-swatch { + border: none; + border-radius: 4px; + } +} + +@layer components { + /* transparency checkerboard behind pixel canvases */ + .checker { + background-color: #2b3040; + background-image: + linear-gradient(45deg, #353b4e 25%, transparent 25%), linear-gradient(-45deg, #353b4e 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, #353b4e 75%), linear-gradient(-45deg, transparent 75%, #353b4e 75%); + background-size: 16px 16px; + background-position: + 0 0, + 0 8px, + 8px -8px, + -8px 0; + } +} diff --git a/src/core/blob/blob.test.ts b/src/core/blob/blob.test.ts new file mode 100644 index 0000000..7d0fac7 --- /dev/null +++ b/src/core/blob/blob.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, test } from "vitest"; + +import { autotilerV1 } from "../layouts/blob.ts"; +import { dualQuadrant } from "./dual.ts"; +import { godot3Bitmask, godot4PeeringBits } from "./godot.ts"; +import { BLOB47, canonical, E, isCanonical, maskOf, N, NE, NW, S, SE, SW, W } from "./mask.ts"; +import { CORNERS, pieceKind, quarterRect } from "./quarters.ts"; + +describe("masks", () => { + test("256 masks collapse into the 47 blob configurations", () => { + expect(BLOB47).toHaveLength(47); + for (let m = 0; m < 256; m++) expect(BLOB47).toContain(canonical(m)); + for (const m of BLOB47) expect(isCanonical(m)).toBe(true); + }); + + test("a corner only counts when both adjacent sides do", () => { + expect(canonical(NE)).toBe(0); + expect(canonical(N | NE)).toBe(N); + expect(canonical(N | E | NE)).toBe(N | E | NE); + expect(canonical(0xff)).toBe(0xff); + }); + + test("maskOf reads neighbours clockwise from north", () => { + const filled = new Set(["0,-1", "1,0", "1,-1", "-1,1"]); + expect(maskOf((dx, dy) => filled.has(`${dx},${dy}`))).toBe(N | NE | E); + }); +}); + +describe("quarter pieces", () => { + test("piece kinds", () => { + expect(pieceKind(0, "TL")).toBe("O"); + expect(pieceKind(E, "TR")).toBe("H"); + expect(pieceKind(S, "BR")).toBe("V"); + expect(pieceKind(S | E, "BR")).toBe("I"); + expect(pieceKind(S | E | SE, "BR")).toBe("F"); + expect(pieceKind(S | E | SE, "TL")).toBe("O"); + }); + + test("the 47 configurations use all 20 pieces", () => { + const seen = new Set(BLOB47.flatMap((m) => CORNERS.map((c) => `${pieceKind(m, c)}@${c}`))); + expect(seen.size).toBe(20); + }); + + test("odd tile sizes split unevenly but consistently", () => { + expect(quarterRect(16, "BR")).toEqual({ x: 8, y: 8, w: 8, h: 8 }); + expect(quarterRect(15, "TL")).toEqual({ x: 0, y: 0, w: 7, h: 7 }); + expect(quarterRect(15, "BR")).toEqual({ x: 7, y: 7, w: 8, h: 8 }); + }); +}); + +describe("engine bits", () => { + // bitmask_flags of Autotiler v1.2.0 (resources/templates/tileset.tres), verified against the tiles' art + // prettier-ignore + const V1: Record = { + "0,0": 432, "0,1": 438, "0,2": 54, "0,3": 48, "1,0": 504, "1,1": 511, "1,2": 63, "1,3": 56, + "2,0": 216, "2,1": 219, "2,2": 27, "2,3": 24, "3,0": 144, "3,1": 146, "3,2": 18, "3,3": 16, + "4,0": 176, "4,1": 182, "4,2": 434, "4,3": 50, "4,4": 178, "5,0": 248, "5,1": 255, "5,2": 507, + "5,3": 59, "5,4": 251, "6,0": 440, "6,1": 447, "6,2": 510, "6,3": 62, "6,4": 446, "7,0": 152, + "7,1": 155, "7,2": 218, "7,3": 26, "7,4": 154, "8,0": 184, "8,1": 191, "8,2": 506, "8,3": 58, + "8,4": 186, "9,0": 443, "9,1": 254, "9,2": 442, "9,3": 190, "10,2": 250, "10,3": 187, + }; + + test("Godot 3 bitmasks match v1's template for its 11×5 layout", () => { + const derived = Object.fromEntries(autotilerV1.cells.map((c) => [`${c.x},${c.y}`, godot3Bitmask(c.mask)])); + expect(derived).toEqual(V1); + }); + + test("Godot 4 peering bits follow CellNeighbor order", () => { + expect(godot4PeeringBits(0)).toEqual([]); + expect(godot4PeeringBits(N | E | S | W | NE | SE | SW | NW)).toEqual([ + "right_side", + "bottom_right_corner", + "bottom_side", + "bottom_left_corner", + "left_side", + "top_left_corner", + "top_side", + "top_right_corner", + ]); + expect(godot4PeeringBits(S | E)).toEqual(["right_side", "bottom_side"]); + }); +}); + +describe("dual grid", () => { + test("a dual quadrant is the opposite quarter of the world cell at that corner", () => { + // only the TL world cell filled: display-TL shows that cell's BR quarter as an outer corner + expect(dualQuadrant(1, "TL")).toEqual({ filled: true, quarter: "BR", kind: "O" }); + expect(dualQuadrant(1, "TR").filled).toBe(false); + // all filled: every quadrant is fill + for (const q of CORNERS) expect(dualQuadrant(15, q).kind).toBe("F"); + // TL+TR+BL: display-TL is the TL cell's BR quarter with both sides filled but not the diagonal → inner corner + expect(dualQuadrant(7, "TL")).toEqual({ filled: true, quarter: "BR", kind: "I" }); + }); + + test("dual quadrants agree with blob pieces of the same world configuration", () => { + // world cells a(TL) b(TR) c(BL) d(BR): the BR quarter of `a` must be the blob piece of a's neighbourhood + for (let corners = 0; corners < 16; corners++) { + const q = dualQuadrant(corners, "TL"); + if (!q.filled) continue; + const mask = canonical((corners & 2 ? E : 0) | (corners & 4 ? S : 0) | (corners & 8 ? SE : 0)); + expect(q.kind).toBe(pieceKind(mask, "BR")); + } + }); +}); diff --git a/src/core/blob/dual.ts b/src/core/blob/dual.ts new file mode 100644 index 0000000..3b5a13c --- /dev/null +++ b/src/core/blob/dual.ts @@ -0,0 +1,37 @@ +// Dual grid: terrain lives on the world grid and tiles are drawn on a grid offset by half a tile, so each display +// tile only depends on its four world corners (16 tiles). Each display quadrant is one quarter piece of the world +// cell sitting at that corner: display-TL is the BR quarter of the TL cell, and so on. +import { type Corner, kindFrom, type PieceKind } from "./quarters.ts"; + +export const DUAL_TL = 1; +export const DUAL_TR = 2; +export const DUAL_BL = 4; +export const DUAL_BR = 8; + +/** Filled world corners of a dual tile: TL=1, TR=2, BL=4, BR=8. */ +export type DualCorners = number; + +export interface DualQuadrant { + /** whether the world cell providing this quadrant is terrain (otherwise the quadrant stays empty) */ + readonly filled: boolean; + /** which quarter of that world cell is shown here */ + readonly quarter: Corner; + readonly kind: PieceKind; +} + +// For each display quadrant: [cell bit, cell quarter, vertical neighbour bit, horizontal bit, diagonal bit] +const TABLE: Record = { + TL: [DUAL_TL, "BR", DUAL_BL, DUAL_TR, DUAL_BR], + TR: [DUAL_TR, "BL", DUAL_BR, DUAL_TL, DUAL_BL], + BL: [DUAL_BL, "TR", DUAL_TL, DUAL_BR, DUAL_TR], + BR: [DUAL_BR, "TL", DUAL_TR, DUAL_BL, DUAL_TL], +}; + +export function dualQuadrant(corners: DualCorners, quadrant: Corner): DualQuadrant { + const [cell, quarter, v, h, d] = TABLE[quadrant]; + return { + filled: (corners & cell) !== 0, + quarter, + kind: kindFrom((corners & v) !== 0, (corners & h) !== 0, (corners & d) !== 0), + }; +} diff --git a/src/core/blob/godot.ts b/src/core/blob/godot.ts new file mode 100644 index 0000000..d6d20f8 --- /dev/null +++ b/src/core/blob/godot.ts @@ -0,0 +1,36 @@ +import { E, type Mask, N, NE, NW, S, SE, SW, W } from "./mask.ts"; + +/** Godot 3 autotile bitmask (3x3 minimal): TL=1 T=2 TR=4 L=8 C=16 R=32 BL=64 B=128 BR=256. */ +export function godot3Bitmask(mask: Mask): number { + let b = 16; + if (mask & NW) b |= 1; + if (mask & N) b |= 2; + if (mask & NE) b |= 4; + if (mask & W) b |= 8; + if (mask & E) b |= 32; + if (mask & SW) b |= 64; + if (mask & S) b |= 128; + if (mask & SE) b |= 256; + return b; +} + +/** Godot 4 terrain peering bits for square tiles, in the order Godot writes them (TileSet.CellNeighbor). */ +export const GODOT4_PEERING_BITS: ReadonlyArray = [ + [E, "right_side"], + [SE, "bottom_right_corner"], + [S, "bottom_side"], + [SW, "bottom_left_corner"], + [W, "left_side"], + [NW, "top_left_corner"], + [N, "top_side"], + [NE, "top_right_corner"], +]; + +/** + * Peering bits set to the terrain for a canonical mask. A side bit is set when that neighbour is terrain; a corner + * bit only when both adjacent sides and the corner are terrain (canonical masks already guarantee that). Every + * other bit stays unset (-1), which Godot reads as "no terrain" — there is no "don't care". + */ +export function godot4PeeringBits(mask: Mask): string[] { + return GODOT4_PEERING_BITS.filter(([bit]) => mask & bit).map(([, name]) => name); +} diff --git a/src/core/blob/mask.ts b/src/core/blob/mask.ts new file mode 100644 index 0000000..9ce2d6d --- /dev/null +++ b/src/core/blob/mask.ts @@ -0,0 +1,65 @@ +// Neighbour masks, clockwise from north (the "cr31" blob convention). The same bit order drives every exporter: +// Tiled's wangid lists top, top-right, right, …, top-left, and Godot 4's peering bits run clockwise from the right. + +export const N = 1; +export const NE = 2; +export const E = 4; +export const SE = 8; +export const S = 16; +export const SW = 32; +export const W = 64; +export const NW = 128; + +/** An 8-bit neighbour mask. Blob masks are always canonical (see `canonical`). */ +export type Mask = number; + +export type Direction = "N" | "NE" | "E" | "SE" | "S" | "SW" | "W" | "NW"; + +export const DIRECTIONS: ReadonlyArray<{ + readonly name: Direction; + readonly bit: number; + readonly dx: number; + readonly dy: number; +}> = [ + { name: "N", bit: N, dx: 0, dy: -1 }, + { name: "NE", bit: NE, dx: 1, dy: -1 }, + { name: "E", bit: E, dx: 1, dy: 0 }, + { name: "SE", bit: SE, dx: 1, dy: 1 }, + { name: "S", bit: S, dx: 0, dy: 1 }, + { name: "SW", bit: SW, dx: -1, dy: 1 }, + { name: "W", bit: W, dx: -1, dy: 0 }, + { name: "NW", bit: NW, dx: -1, dy: -1 }, +]; + +/** + * Drops every diagonal whose two adjacent sides are not both set: a corner only shows (as fill or as an inner + * corner) when the cell continues on both sides. The 256 masks collapse into the 47 blob configurations. + */ +export function canonical(mask: number): Mask { + let m = mask & 0xff; + if (!(m & N && m & E)) m &= ~NE; + if (!(m & S && m & E)) m &= ~SE; + if (!(m & S && m & W)) m &= ~SW; + if (!(m & N && m & W)) m &= ~NW; + return m; +} + +export function isCanonical(mask: number): boolean { + return canonical(mask) === mask; +} + +/** The 47 canonical masks, ascending. */ +export const BLOB47: readonly Mask[] = [...new Set(Array.from({ length: 256 }, (_, m) => canonical(m)))].toSorted( + (a, b) => a - b, +); + +/** Canonical mask of a cell, given a predicate telling whether the neighbour at (dx, dy) is terrain. */ +export function maskOf(isTerrain: (dx: number, dy: number) => boolean): Mask { + let m = 0; + for (const d of DIRECTIONS) if (isTerrain(d.dx, d.dy)) m |= d.bit; + return canonical(m); +} + +export function directionsOf(mask: Mask): Direction[] { + return DIRECTIONS.filter((d) => mask & d.bit).map((d) => d.name); +} diff --git a/src/core/blob/quarters.ts b/src/core/blob/quarters.ts new file mode 100644 index 0000000..0bdcc99 --- /dev/null +++ b/src/core/blob/quarters.ts @@ -0,0 +1,62 @@ +// Every autotile is made of four quarter tiles. A quarter at corner c of a terrain cell looks like one of five +// pieces depending on its vertical side (v), horizontal side (h) and diagonal (d) neighbours: +// O outer corner (!v !h) · H horizontal edge (!v h) · V vertical edge (v !h) · I inner corner (v h !d) · F fill +// 5 kinds × 4 corners = the 20 pieces every template in the wild is a packing of. +import { E, type Mask, N, NE, NW, S, SE, SW, W } from "./mask.ts"; + +export type Corner = "TL" | "TR" | "BL" | "BR"; +export const CORNERS: readonly Corner[] = ["TL", "TR", "BL", "BR"]; + +export type PieceKind = "O" | "H" | "V" | "I" | "F"; +export const PIECE_KINDS: readonly PieceKind[] = ["O", "H", "V", "I", "F"]; + +export const PIECE_NAMES: Record = { + O: "outer corner", + H: "horizontal edge", + V: "vertical edge", + I: "inner corner", + F: "fill", +}; + +export const CORNER_NEIGHBORS: Record< + Corner, + { readonly vertical: number; readonly horizontal: number; readonly diagonal: number } +> = { + TL: { vertical: N, horizontal: W, diagonal: NW }, + TR: { vertical: N, horizontal: E, diagonal: NE }, + BL: { vertical: S, horizontal: W, diagonal: SW }, + BR: { vertical: S, horizontal: E, diagonal: SE }, +}; + +export function isLeft(c: Corner): boolean { + return c === "TL" || c === "BL"; +} + +export function isTop(c: Corner): boolean { + return c === "TL" || c === "TR"; +} + +export function kindFrom(v: boolean, h: boolean, d: boolean): PieceKind { + if (!v && !h) return "O"; + if (!v) return "H"; + if (!h) return "V"; + return d ? "F" : "I"; +} + +/** Which piece quarter `corner` of a cell with neighbour mask `mask` needs. */ +export function pieceKind(mask: Mask, corner: Corner): PieceKind { + const n = CORNER_NEIGHBORS[corner]; + return kindFrom((mask & n.vertical) !== 0, (mask & n.horizontal) !== 0, (mask & n.diagonal) !== 0); +} + +/** + * Pixel rect of a quarter inside a tile. Odd tile sizes split unevenly: the left/top quarters get floor(ts/2), + * the right/bottom ones the rest. Pieces are always sampled at their own corner, so sizes always match. + */ +export function quarterRect(tileSize: number, corner: Corner): { x: number; y: number; w: number; h: number } { + const a = Math.floor(tileSize / 2); + const b = tileSize - a; + const left = isLeft(corner); + const top = isTop(corner); + return { x: left ? 0 : a, y: top ? 0 : a, w: left ? a : b, h: top ? a : b }; +} diff --git a/src/core/detect.test.ts b/src/core/detect.test.ts new file mode 100644 index 0000000..404f483 --- /dev/null +++ b/src/core/detect.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, test } from "vitest"; + +import { analyzeSource, detectTemplate } from "./detect.ts"; +import { copyRect, createImage } from "./image/rgba.ts"; + +const solid = (w: number, h: number) => { + const img = createImage(w, h); + img.data.fill(255); + return img; +}; + +describe("detectTemplate", () => { + test("picks the template from the aspect ratio", () => { + expect(detectTemplate(80, 48)?.id).toBe("autotiler-13"); + expect(detectTemplate(40, 24)?.id).toBe("autotiler-13"); + expect(detectTemplate(32, 48)?.id).toBe("rpgmaker-a2"); + expect(detectTemplate(96, 144)?.id).toBe("rpgmaker-a2"); + expect(detectTemplate(81, 48)?.id).toBe("autotiler-13"); // close enough to explain the fix + expect(detectTemplate(100, 20)).toBeNull(); + }); +}); + +describe("analyzeSource", () => { + test("detects the tile size", () => { + expect(analyzeSource(solid(80, 48))).toMatchObject({ tileSize: 16, issues: [] }); + expect(analyzeSource(solid(64, 96))).toMatchObject({ tileSize: 32, issues: [] }); + }); + + test("explains how to fix a wrong size", () => { + const r = analyzeSource(solid(81, 48)); + expect(r.tileSize).toBeNull(); + expect(r.issues[0]).toMatchObject({ level: "error", code: "size-mismatch" }); + expect(r.issues[0].message).toContain("crop it to 80×48"); + }); + + test("warns about odd tile sizes and empty slots", () => { + const img = solid(75, 45); + // clear the island's centre tile (15 px tiles) + copyRect(createImage(15, 15), 0, 0, 15, 15, img, 15, 15); + const r = analyzeSource(img); + expect(r.tileSize).toBe(15); + expect(r.issues.map((i) => i.code)).toEqual(["odd-tile-size", "empty-slot"]); + expect(r.issues[1].message).toContain("centre"); + }); + + test("manual tile size and template override detection", () => { + const r = analyzeSource(solid(90, 50), "autotiler-13", 16); + expect(r).toMatchObject({ tileSize: 16 }); + expect(r.issues.map((i) => i.code)).toEqual(["oversized"]); + expect(analyzeSource(solid(80, 48), "autotiler-13", 17).issues[0].code).toBe("size-mismatch"); + }); + + test("unknown sizes are an error", () => { + expect(analyzeSource(solid(100, 20)).issues[0].code).toBe("unknown-size"); + }); +}); diff --git a/src/core/detect.ts b/src/core/detect.ts new file mode 100644 index 0000000..98a63c6 --- /dev/null +++ b/src/core/detect.ts @@ -0,0 +1,117 @@ +import { CORNERS, quarterRect } from "./blob/quarters.ts"; +import { isRectTransparent, type RgbaImage } from "./image/rgba.ts"; +import { TEMPLATES } from "./templates/index.ts"; +import type { Template, TemplateId } from "./templates/types.ts"; + +export type IssueLevel = "error" | "warning" | "info"; + +export interface Issue { + readonly level: IssueLevel; + readonly code: "unknown-size" | "size-mismatch" | "tile-too-small" | "odd-tile-size" | "empty-slot" | "oversized"; + readonly message: string; +} + +export interface SourceAnalysis { + readonly template: Template | null; + readonly tileSize: number | null; + readonly issues: readonly Issue[]; +} + +/** Guesses the template from the image's aspect ratio (5×3 vs 2×3 tiles). */ +export function detectTemplate(width: number, height: number): Template | null { + if (width <= 0 || height <= 0) return null; + const exact = TEMPLATES.find((t) => width * t.rows === height * t.columns); + if (exact) return exact; + // not an exact ratio: pick the closest one so we can explain how to fix the image + let best: Template | null = null; + let bestErr = Number.POSITIVE_INFINITY; + for (const t of TEMPLATES) { + const err = Math.abs(width / height - t.columns / t.rows); + if (err < bestErr) [best, bestErr] = [t, err]; + } + return bestErr < 0.2 ? best : null; +} + +/** + * Works out template and tile size for a source image and reports problems. `templateId` / `tileSize` override the + * detection when given. + */ +export function analyzeSource(img: RgbaImage, templateId?: TemplateId, tileSize?: number): SourceAnalysis { + const issues: Issue[] = []; + const template = templateId + ? (TEMPLATES.find((t) => t.id === templateId) ?? null) + : detectTemplate(img.width, img.height); + if (!template) { + issues.push({ + level: "error", + code: "unknown-size", + message: `Can't tell the template from a ${img.width}×${img.height} image. Use 5×3 tiles (Autotiler 13-tile) or 2×3 tiles (RPG Maker A2).`, + }); + return { template: null, tileSize: null, issues }; + } + + let ts = tileSize ?? null; + if (ts === null) { + const tw = img.width / template.columns; + const th = img.height / template.rows; + if (Number.isInteger(tw) && tw === th) { + ts = tw; + } else { + const guess = Math.floor(Math.min(tw, th)); + issues.push({ + level: "error", + code: "size-mismatch", + message: + `A ${template.name} image must be ${template.columns}×${template.rows} tiles, but ${img.width}×${img.height} isn't.` + + (guess >= 2 + ? ` For ${guess} px tiles, crop it to ${guess * template.columns}×${guess * template.rows} or set the tile size.` + : ""), + }); + return { template, tileSize: null, issues }; + } + } + + if (ts < 2) { + issues.push({ level: "error", code: "tile-too-small", message: `Tile size must be at least 2 px (got ${ts}).` }); + return { template, tileSize: null, issues }; + } + if (img.width < template.columns * ts || img.height < template.rows * ts) { + issues.push({ + level: "error", + code: "size-mismatch", + message: `At ${ts} px a ${template.name} image needs ${template.columns * ts}×${template.rows * ts} px; this one is ${img.width}×${img.height}.`, + }); + return { template, tileSize: null, issues }; + } + if (img.width > template.columns * ts || img.height > template.rows * ts) { + issues.push({ + level: "info", + code: "oversized", + message: `Only the top-left ${template.columns * ts}×${template.rows * ts} px are used.`, + }); + } + if (ts % 2 !== 0) { + issues.push({ + level: "warning", + code: "odd-tile-size", + message: `${ts} px is odd: quarters split unevenly, and the dual-grid layout needs an even size.`, + }); + } + + for (const slot of template.slots) { + if (!slot.used) continue; + const quarters = CORNERS.filter((c) => template.pieceAt(slot.tx, slot.ty, c) !== null); + const empty = quarters.every((c) => { + const r = quarterRect(ts, c); + return isRectTransparent(img, slot.tx * ts + r.x, slot.ty * ts + r.y, r.w, r.h); + }); + if (quarters.length > 0 && empty) { + issues.push({ + level: "warning", + code: "empty-slot", + message: `The ${slot.label} slot (column ${slot.tx + 1}, row ${slot.ty + 1}) is empty.`, + }); + } + } + return { template, tileSize: ts, issues }; +} diff --git a/src/core/export/__snapshots__/grass.blob.tsx.xml b/src/core/export/__snapshots__/grass.blob.tsx.xml new file mode 100644 index 0000000..ad18975 --- /dev/null +++ b/src/core/export/__snapshots__/grass.blob.tsx.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/core/export/__snapshots__/grass.dual.tsx.xml b/src/core/export/__snapshots__/grass.dual.tsx.xml new file mode 100644 index 0000000..1578096 --- /dev/null +++ b/src/core/export/__snapshots__/grass.dual.tsx.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/templates/tileset.tres b/src/core/export/__snapshots__/grass.godot3.tres similarity index 71% rename from resources/templates/tileset.tres rename to src/core/export/__snapshots__/grass.godot3.tres index 194d3fd..e79de94 100644 --- a/resources/templates/tileset.tres +++ b/src/core/export/__snapshots__/grass.godot3.tres @@ -1,202 +1,159 @@ -[gd_resource type="TileSet" load_steps=48 format=2] +[gd_resource type="TileSet" load_steps=49 format=2] -[ext_resource path="res://$IMGFILEPATH" type="Texture" id=1] +[ext_resource path="grass.png" type="Texture" id=1] [sub_resource type="ConvexPolygonShape2D" id=1] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=2] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=3] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=4] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=5] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=6] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=7] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=8] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=9] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=10] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=11] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=12] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=13] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=14] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=15] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=16] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=17] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=18] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=19] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=20] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=21] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=22] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=23] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=24] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=25] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=26] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=27] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=28] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=29] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=30] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=31] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=32] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=33] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=34] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=35] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=36] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=37] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=38] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=39] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=40] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=41] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=42] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=43] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=44] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=45] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [sub_resource type="ConvexPolygonShape2D" id=46] -points = PoolVector2Array( 0, 0, $TS, 0, $TS, $TS, 0, $TS ) +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) + +[sub_resource type="ConvexPolygonShape2D" id=47] +points = PoolVector2Array( 0, 0, 16, 0, 16, 16, 0, 16 ) [resource] -1/name = "$IMGFILE 1" +1/name = "grass" 1/texture = ExtResource( 1 ) 1/tex_offset = Vector2( 0, 0 ) 1/modulate = Color( 1, 1, 1, 1 ) -1/region = Rect2( 0, 0, $IMGW, $IMGH ) +1/region = Rect2( 0, 0, 176, 80 ) 1/tile_mode = 1 1/autotile/bitmask_mode = 1 -1/autotile/bitmask_flags = [ Vector2( 0, 0 ), 432, -Vector2( 0, 1 ), 438, -Vector2( 0, 2 ), 54, -Vector2( 0, 3 ), 48, -Vector2( 1, 0 ), 504, -Vector2( 1, 1 ), 511, -Vector2( 1, 2 ), 63, -Vector2( 1, 3 ), 56, -Vector2( 2, 0 ), 216, -Vector2( 2, 1 ), 219, -Vector2( 2, 2 ), 27, -Vector2( 2, 3 ), 24, -Vector2( 3, 0 ), 144, -Vector2( 3, 1 ), 146, -Vector2( 3, 2 ), 18, -Vector2( 3, 3 ), 16, -Vector2( 4, 0 ), 176, -Vector2( 4, 1 ), 182, -Vector2( 4, 2 ), 434, -Vector2( 4, 3 ), 50, -Vector2( 4, 4 ), 178, -Vector2( 5, 0 ), 248, -Vector2( 5, 1 ), 255, -Vector2( 5, 2 ), 507, -Vector2( 5, 3 ), 59, -Vector2( 5, 4 ), 251, -Vector2( 6, 0 ), 440, -Vector2( 6, 1 ), 447, -Vector2( 6, 2 ), 510, -Vector2( 6, 3 ), 62, -Vector2( 6, 4 ), 446, -Vector2( 7, 0 ), 152, -Vector2( 7, 1 ), 155, -Vector2( 7, 2 ), 218, -Vector2( 7, 3 ), 26, -Vector2( 7, 4 ), 154, -Vector2( 8, 0 ), 184, -Vector2( 8, 1 ), 191, -Vector2( 8, 2 ), 506, -Vector2( 8, 3 ), 58, -Vector2( 8, 4 ), 186, -Vector2( 9, 0 ), 443, -Vector2( 9, 1 ), 254, -Vector2( 9, 2 ), 442, -Vector2( 9, 3 ), 190, -Vector2( 10, 2 ), 250, -Vector2( 10, 3 ), 187] +1/autotile/bitmask_flags = [ Vector2( 0, 0 ), 432, Vector2( 0, 1 ), 438, Vector2( 0, 2 ), 54, Vector2( 0, 3 ), 48, Vector2( 1, 0 ), 504, Vector2( 1, 1 ), 511, Vector2( 1, 2 ), 63, Vector2( 1, 3 ), 56, Vector2( 2, 0 ), 216, Vector2( 2, 1 ), 219, Vector2( 2, 2 ), 27, Vector2( 2, 3 ), 24, Vector2( 3, 0 ), 144, Vector2( 3, 1 ), 146, Vector2( 3, 2 ), 18, Vector2( 3, 3 ), 16, Vector2( 4, 0 ), 176, Vector2( 4, 1 ), 182, Vector2( 4, 2 ), 434, Vector2( 4, 3 ), 50, Vector2( 4, 4 ), 178, Vector2( 5, 0 ), 248, Vector2( 5, 1 ), 255, Vector2( 5, 2 ), 507, Vector2( 5, 3 ), 59, Vector2( 5, 4 ), 251, Vector2( 6, 0 ), 440, Vector2( 6, 1 ), 447, Vector2( 6, 2 ), 510, Vector2( 6, 3 ), 62, Vector2( 6, 4 ), 446, Vector2( 7, 0 ), 152, Vector2( 7, 1 ), 155, Vector2( 7, 2 ), 218, Vector2( 7, 3 ), 26, Vector2( 7, 4 ), 154, Vector2( 8, 0 ), 184, Vector2( 8, 1 ), 191, Vector2( 8, 2 ), 506, Vector2( 8, 3 ), 58, Vector2( 8, 4 ), 186, Vector2( 9, 0 ), 443, Vector2( 9, 1 ), 254, Vector2( 9, 2 ), 442, Vector2( 9, 3 ), 190, Vector2( 10, 2 ), 250, Vector2( 10, 3 ), 187 ] 1/autotile/icon_coordinate = Vector2( 3, 3 ) -1/autotile/tile_size = Vector2( $TS, $TS ) +1/autotile/tile_size = Vector2( 16, 16 ) 1/autotile/spacing = 0 1/autotile/occluder_map = [ ] 1/autotile/navpoly_map = [ ] @@ -210,13 +167,13 @@ Vector2( 10, 3 ), 187] 1/shape_one_way = false 1/shape_one_way_margin = 1.0 1/shapes = [ { -"autotile_coord": Vector2( 0, 1 ), +"autotile_coord": Vector2( 0, 0 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 1 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 2, 1 ), +"autotile_coord": Vector2( 0, 1 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 2 ), @@ -228,139 +185,139 @@ Vector2( 10, 3 ), 187] "shape": SubResource( 3 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 1, 2 ), +"autotile_coord": Vector2( 0, 3 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 4 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 2, 2 ), +"autotile_coord": Vector2( 1, 0 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 5 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 3, 2 ), +"autotile_coord": Vector2( 1, 1 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 6 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 3, 1 ), +"autotile_coord": Vector2( 1, 2 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 7 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 0, 3 ), +"autotile_coord": Vector2( 1, 3 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 8 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 1, 3 ), +"autotile_coord": Vector2( 2, 0 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 9 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 2, 3 ), +"autotile_coord": Vector2( 2, 1 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 10 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 3, 3 ), +"autotile_coord": Vector2( 2, 2 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 11 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 0, 0 ), +"autotile_coord": Vector2( 2, 3 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 12 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 1, 0 ), +"autotile_coord": Vector2( 3, 0 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 13 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 2, 0 ), +"autotile_coord": Vector2( 3, 1 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 14 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 3, 0 ), +"autotile_coord": Vector2( 3, 2 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 15 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 4, 0 ), +"autotile_coord": Vector2( 3, 3 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 16 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 7, 0 ), +"autotile_coord": Vector2( 4, 0 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 17 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 4, 3 ), +"autotile_coord": Vector2( 4, 1 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 18 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 7, 3 ), +"autotile_coord": Vector2( 4, 2 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 19 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 5, 3 ), +"autotile_coord": Vector2( 4, 3 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 20 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 6, 3 ), +"autotile_coord": Vector2( 4, 4 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 21 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 4, 2 ), +"autotile_coord": Vector2( 5, 0 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 22 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 4, 1 ), +"autotile_coord": Vector2( 5, 1 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 23 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 7, 1 ), +"autotile_coord": Vector2( 5, 2 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 24 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 7, 2 ), +"autotile_coord": Vector2( 5, 3 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 25 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 5, 0 ), +"autotile_coord": Vector2( 5, 4 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 26 ), @@ -372,61 +329,61 @@ Vector2( 10, 3 ), 187] "shape": SubResource( 27 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 5, 1 ), +"autotile_coord": Vector2( 6, 1 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 28 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 6, 1 ), +"autotile_coord": Vector2( 6, 2 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 29 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 5, 2 ), +"autotile_coord": Vector2( 6, 3 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 30 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 6, 2 ), +"autotile_coord": Vector2( 6, 4 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 31 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 9, 3 ), +"autotile_coord": Vector2( 7, 0 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 32 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 10, 3 ), +"autotile_coord": Vector2( 7, 1 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 33 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 8, 3 ), +"autotile_coord": Vector2( 7, 2 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 34 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 8, 2 ), +"autotile_coord": Vector2( 7, 3 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 35 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 8, 0 ), +"autotile_coord": Vector2( 7, 4 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 36 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 9, 0 ), +"autotile_coord": Vector2( 8, 0 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 37 ), @@ -438,52 +395,58 @@ Vector2( 10, 3 ), 187] "shape": SubResource( 38 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 9, 1 ), +"autotile_coord": Vector2( 8, 2 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 39 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 9, 2 ), +"autotile_coord": Vector2( 8, 3 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 40 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 10, 2 ), +"autotile_coord": Vector2( 8, 4 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 41 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 8, 4 ), +"autotile_coord": Vector2( 9, 0 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 42 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 7, 4 ), +"autotile_coord": Vector2( 9, 1 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 43 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 6, 4 ), +"autotile_coord": Vector2( 9, 2 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 44 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 5, 4 ), +"autotile_coord": Vector2( 9, 3 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 45 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) }, { -"autotile_coord": Vector2( 4, 4 ), +"autotile_coord": Vector2( 10, 2 ), "one_way": false, "one_way_margin": 1.0, "shape": SubResource( 46 ), "shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) +}, { +"autotile_coord": Vector2( 10, 3 ), +"one_way": false, +"one_way_margin": 1.0, +"shape": SubResource( 47 ), +"shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 ) } ] 1/z_index = 0 diff --git a/src/core/export/__snapshots__/grass.godot4.tres b/src/core/export/__snapshots__/grass.godot4.tres new file mode 100644 index 0000000..9827267 --- /dev/null +++ b/src/core/export/__snapshots__/grass.godot4.tres @@ -0,0 +1,391 @@ +[gd_resource type="TileSet" format=3] + +[ext_resource type="Texture2D" path="grass.png" id="1_texture"] + +[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_terrain"] +texture = ExtResource("1_texture") +texture_region_size = Vector2i(16, 16) +0:0/0 = 0 +0:0/0/terrain_set = 0 +0:0/0/terrain = 0 +0:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +0:0/0/terrains_peering_bit/bottom_side = 0 +1:0/0 = 0 +1:0/0/terrain_set = 0 +1:0/0/terrain = 0 +1:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +1:0/0/terrains_peering_bit/right_side = 0 +1:0/0/terrains_peering_bit/bottom_side = 0 +2:0/0 = 0 +2:0/0/terrain_set = 0 +2:0/0/terrain = 0 +2:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +2:0/0/terrains_peering_bit/right_side = 0 +2:0/0/terrains_peering_bit/bottom_side = 0 +2:0/0/terrains_peering_bit/left_side = 0 +3:0/0 = 0 +3:0/0/terrain_set = 0 +3:0/0/terrain = 0 +3:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +3:0/0/terrains_peering_bit/bottom_side = 0 +3:0/0/terrains_peering_bit/left_side = 0 +4:0/0 = 0 +4:0/0/terrain_set = 0 +4:0/0/terrain = 0 +4:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +4:0/0/terrains_peering_bit/right_side = 0 +4:0/0/terrains_peering_bit/bottom_side = 0 +4:0/0/terrains_peering_bit/left_side = 0 +4:0/0/terrains_peering_bit/top_left_corner = 0 +4:0/0/terrains_peering_bit/top_side = 0 +5:0/0 = 0 +5:0/0/terrain_set = 0 +5:0/0/terrain = 0 +5:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +5:0/0/terrains_peering_bit/right_side = 0 +5:0/0/terrains_peering_bit/bottom_right_corner = 0 +5:0/0/terrains_peering_bit/bottom_side = 0 +5:0/0/terrains_peering_bit/left_side = 0 +6:0/0 = 0 +6:0/0/terrain_set = 0 +6:0/0/terrain = 0 +6:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +6:0/0/terrains_peering_bit/right_side = 0 +6:0/0/terrains_peering_bit/bottom_side = 0 +6:0/0/terrains_peering_bit/bottom_left_corner = 0 +6:0/0/terrains_peering_bit/left_side = 0 +7:0/0 = 0 +7:0/0/terrain_set = 0 +7:0/0/terrain = 0 +7:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +7:0/0/terrains_peering_bit/right_side = 0 +7:0/0/terrains_peering_bit/bottom_side = 0 +7:0/0/terrains_peering_bit/left_side = 0 +7:0/0/terrains_peering_bit/top_side = 0 +7:0/0/terrains_peering_bit/top_right_corner = 0 +8:0/0 = 0 +8:0/0/terrain_set = 0 +8:0/0/terrain = 0 +8:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +8:0/0/terrains_peering_bit/right_side = 0 +8:0/0/terrains_peering_bit/bottom_right_corner = 0 +8:0/0/terrains_peering_bit/bottom_side = 0 +9:0/0 = 0 +9:0/0/terrain_set = 0 +9:0/0/terrain = 0 +9:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +9:0/0/terrains_peering_bit/right_side = 0 +9:0/0/terrains_peering_bit/bottom_right_corner = 0 +9:0/0/terrains_peering_bit/bottom_side = 0 +9:0/0/terrains_peering_bit/bottom_left_corner = 0 +9:0/0/terrains_peering_bit/left_side = 0 +9:0/0/terrains_peering_bit/top_side = 0 +10:0/0 = 0 +10:0/0/terrain_set = 0 +10:0/0/terrain = 0 +10:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +10:0/0/terrains_peering_bit/right_side = 0 +10:0/0/terrains_peering_bit/bottom_right_corner = 0 +10:0/0/terrains_peering_bit/bottom_side = 0 +10:0/0/terrains_peering_bit/bottom_left_corner = 0 +10:0/0/terrains_peering_bit/left_side = 0 +11:0/0 = 0 +11:0/0/terrain_set = 0 +11:0/0/terrain = 0 +11:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +11:0/0/terrains_peering_bit/bottom_side = 0 +11:0/0/terrains_peering_bit/bottom_left_corner = 0 +11:0/0/terrains_peering_bit/left_side = 0 +0:1/0 = 0 +0:1/0/terrain_set = 0 +0:1/0/terrain = 0 +0:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +0:1/0/terrains_peering_bit/bottom_side = 0 +0:1/0/terrains_peering_bit/top_side = 0 +1:1/0 = 0 +1:1/0/terrain_set = 0 +1:1/0/terrain = 0 +1:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +1:1/0/terrains_peering_bit/right_side = 0 +1:1/0/terrains_peering_bit/bottom_side = 0 +1:1/0/terrains_peering_bit/top_side = 0 +2:1/0 = 0 +2:1/0/terrain_set = 0 +2:1/0/terrain = 0 +2:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +2:1/0/terrains_peering_bit/right_side = 0 +2:1/0/terrains_peering_bit/bottom_side = 0 +2:1/0/terrains_peering_bit/left_side = 0 +2:1/0/terrains_peering_bit/top_side = 0 +3:1/0 = 0 +3:1/0/terrain_set = 0 +3:1/0/terrain = 0 +3:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +3:1/0/terrains_peering_bit/bottom_side = 0 +3:1/0/terrains_peering_bit/left_side = 0 +3:1/0/terrains_peering_bit/top_side = 0 +4:1/0 = 0 +4:1/0/terrain_set = 0 +4:1/0/terrain = 0 +4:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +4:1/0/terrains_peering_bit/right_side = 0 +4:1/0/terrains_peering_bit/bottom_right_corner = 0 +4:1/0/terrains_peering_bit/bottom_side = 0 +4:1/0/terrains_peering_bit/top_side = 0 +5:1/0 = 0 +5:1/0/terrain_set = 0 +5:1/0/terrain = 0 +5:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +5:1/0/terrains_peering_bit/right_side = 0 +5:1/0/terrains_peering_bit/bottom_right_corner = 0 +5:1/0/terrains_peering_bit/bottom_side = 0 +5:1/0/terrains_peering_bit/bottom_left_corner = 0 +5:1/0/terrains_peering_bit/left_side = 0 +5:1/0/terrains_peering_bit/top_side = 0 +5:1/0/terrains_peering_bit/top_right_corner = 0 +6:1/0 = 0 +6:1/0/terrain_set = 0 +6:1/0/terrain = 0 +6:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +6:1/0/terrains_peering_bit/right_side = 0 +6:1/0/terrains_peering_bit/bottom_right_corner = 0 +6:1/0/terrains_peering_bit/bottom_side = 0 +6:1/0/terrains_peering_bit/bottom_left_corner = 0 +6:1/0/terrains_peering_bit/left_side = 0 +6:1/0/terrains_peering_bit/top_left_corner = 0 +6:1/0/terrains_peering_bit/top_side = 0 +7:1/0 = 0 +7:1/0/terrain_set = 0 +7:1/0/terrain = 0 +7:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +7:1/0/terrains_peering_bit/bottom_side = 0 +7:1/0/terrains_peering_bit/bottom_left_corner = 0 +7:1/0/terrains_peering_bit/left_side = 0 +7:1/0/terrains_peering_bit/top_side = 0 +8:1/0 = 0 +8:1/0/terrain_set = 0 +8:1/0/terrain = 0 +8:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +8:1/0/terrains_peering_bit/right_side = 0 +8:1/0/terrains_peering_bit/bottom_right_corner = 0 +8:1/0/terrains_peering_bit/bottom_side = 0 +8:1/0/terrains_peering_bit/top_side = 0 +8:1/0/terrains_peering_bit/top_right_corner = 0 +9:1/0 = 0 +9:1/0/terrain_set = 0 +9:1/0/terrain = 0 +9:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +9:1/0/terrains_peering_bit/right_side = 0 +9:1/0/terrains_peering_bit/bottom_side = 0 +9:1/0/terrains_peering_bit/bottom_left_corner = 0 +9:1/0/terrains_peering_bit/left_side = 0 +9:1/0/terrains_peering_bit/top_side = 0 +9:1/0/terrains_peering_bit/top_right_corner = 0 +11:1/0 = 0 +11:1/0/terrain_set = 0 +11:1/0/terrain = 0 +11:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +11:1/0/terrains_peering_bit/right_side = 0 +11:1/0/terrains_peering_bit/bottom_side = 0 +11:1/0/terrains_peering_bit/bottom_left_corner = 0 +11:1/0/terrains_peering_bit/left_side = 0 +11:1/0/terrains_peering_bit/top_left_corner = 0 +11:1/0/terrains_peering_bit/top_side = 0 +0:2/0 = 0 +0:2/0/terrain_set = 0 +0:2/0/terrain = 0 +0:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +0:2/0/terrains_peering_bit/top_side = 0 +1:2/0 = 0 +1:2/0/terrain_set = 0 +1:2/0/terrain = 0 +1:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +1:2/0/terrains_peering_bit/right_side = 0 +1:2/0/terrains_peering_bit/top_side = 0 +2:2/0 = 0 +2:2/0/terrain_set = 0 +2:2/0/terrain = 0 +2:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +2:2/0/terrains_peering_bit/right_side = 0 +2:2/0/terrains_peering_bit/left_side = 0 +2:2/0/terrains_peering_bit/top_side = 0 +3:2/0 = 0 +3:2/0/terrain_set = 0 +3:2/0/terrain = 0 +3:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +3:2/0/terrains_peering_bit/left_side = 0 +3:2/0/terrains_peering_bit/top_side = 0 +4:2/0 = 0 +4:2/0/terrain_set = 0 +4:2/0/terrain = 0 +4:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +4:2/0/terrains_peering_bit/right_side = 0 +4:2/0/terrains_peering_bit/bottom_side = 0 +4:2/0/terrains_peering_bit/top_side = 0 +4:2/0/terrains_peering_bit/top_right_corner = 0 +5:2/0 = 0 +5:2/0/terrain_set = 0 +5:2/0/terrain = 0 +5:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +5:2/0/terrains_peering_bit/right_side = 0 +5:2/0/terrains_peering_bit/bottom_right_corner = 0 +5:2/0/terrains_peering_bit/bottom_side = 0 +5:2/0/terrains_peering_bit/left_side = 0 +5:2/0/terrains_peering_bit/top_left_corner = 0 +5:2/0/terrains_peering_bit/top_side = 0 +5:2/0/terrains_peering_bit/top_right_corner = 0 +6:2/0 = 0 +6:2/0/terrain_set = 0 +6:2/0/terrain = 0 +6:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +6:2/0/terrains_peering_bit/right_side = 0 +6:2/0/terrains_peering_bit/bottom_side = 0 +6:2/0/terrains_peering_bit/bottom_left_corner = 0 +6:2/0/terrains_peering_bit/left_side = 0 +6:2/0/terrains_peering_bit/top_left_corner = 0 +6:2/0/terrains_peering_bit/top_side = 0 +6:2/0/terrains_peering_bit/top_right_corner = 0 +7:2/0 = 0 +7:2/0/terrain_set = 0 +7:2/0/terrain = 0 +7:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +7:2/0/terrains_peering_bit/bottom_side = 0 +7:2/0/terrains_peering_bit/left_side = 0 +7:2/0/terrains_peering_bit/top_left_corner = 0 +7:2/0/terrains_peering_bit/top_side = 0 +8:2/0 = 0 +8:2/0/terrain_set = 0 +8:2/0/terrain = 0 +8:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +8:2/0/terrains_peering_bit/right_side = 0 +8:2/0/terrains_peering_bit/bottom_right_corner = 0 +8:2/0/terrains_peering_bit/bottom_side = 0 +8:2/0/terrains_peering_bit/left_side = 0 +8:2/0/terrains_peering_bit/top_side = 0 +8:2/0/terrains_peering_bit/top_right_corner = 0 +9:2/0 = 0 +9:2/0/terrain_set = 0 +9:2/0/terrain = 0 +9:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +9:2/0/terrains_peering_bit/right_side = 0 +9:2/0/terrains_peering_bit/bottom_right_corner = 0 +9:2/0/terrains_peering_bit/bottom_side = 0 +9:2/0/terrains_peering_bit/bottom_left_corner = 0 +9:2/0/terrains_peering_bit/left_side = 0 +9:2/0/terrains_peering_bit/top_left_corner = 0 +9:2/0/terrains_peering_bit/top_side = 0 +9:2/0/terrains_peering_bit/top_right_corner = 0 +10:2/0 = 0 +10:2/0/terrain_set = 0 +10:2/0/terrain = 0 +10:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +10:2/0/terrains_peering_bit/right_side = 0 +10:2/0/terrains_peering_bit/bottom_right_corner = 0 +10:2/0/terrains_peering_bit/bottom_side = 0 +10:2/0/terrains_peering_bit/left_side = 0 +10:2/0/terrains_peering_bit/top_left_corner = 0 +10:2/0/terrains_peering_bit/top_side = 0 +11:2/0 = 0 +11:2/0/terrain_set = 0 +11:2/0/terrain = 0 +11:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +11:2/0/terrains_peering_bit/bottom_side = 0 +11:2/0/terrains_peering_bit/bottom_left_corner = 0 +11:2/0/terrains_peering_bit/left_side = 0 +11:2/0/terrains_peering_bit/top_left_corner = 0 +11:2/0/terrains_peering_bit/top_side = 0 +0:3/0 = 0 +0:3/0/terrain_set = 0 +0:3/0/terrain = 0 +0:3/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +1:3/0 = 0 +1:3/0/terrain_set = 0 +1:3/0/terrain = 0 +1:3/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +1:3/0/terrains_peering_bit/right_side = 0 +2:3/0 = 0 +2:3/0/terrain_set = 0 +2:3/0/terrain = 0 +2:3/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +2:3/0/terrains_peering_bit/right_side = 0 +2:3/0/terrains_peering_bit/left_side = 0 +3:3/0 = 0 +3:3/0/terrain_set = 0 +3:3/0/terrain = 0 +3:3/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +3:3/0/terrains_peering_bit/left_side = 0 +4:3/0 = 0 +4:3/0/terrain_set = 0 +4:3/0/terrain = 0 +4:3/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +4:3/0/terrains_peering_bit/right_side = 0 +4:3/0/terrains_peering_bit/bottom_side = 0 +4:3/0/terrains_peering_bit/bottom_left_corner = 0 +4:3/0/terrains_peering_bit/left_side = 0 +4:3/0/terrains_peering_bit/top_side = 0 +5:3/0 = 0 +5:3/0/terrain_set = 0 +5:3/0/terrain = 0 +5:3/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +5:3/0/terrains_peering_bit/right_side = 0 +5:3/0/terrains_peering_bit/left_side = 0 +5:3/0/terrains_peering_bit/top_side = 0 +5:3/0/terrains_peering_bit/top_right_corner = 0 +6:3/0 = 0 +6:3/0/terrain_set = 0 +6:3/0/terrain = 0 +6:3/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +6:3/0/terrains_peering_bit/right_side = 0 +6:3/0/terrains_peering_bit/left_side = 0 +6:3/0/terrains_peering_bit/top_left_corner = 0 +6:3/0/terrains_peering_bit/top_side = 0 +7:3/0 = 0 +7:3/0/terrain_set = 0 +7:3/0/terrain = 0 +7:3/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +7:3/0/terrains_peering_bit/right_side = 0 +7:3/0/terrains_peering_bit/bottom_right_corner = 0 +7:3/0/terrains_peering_bit/bottom_side = 0 +7:3/0/terrains_peering_bit/left_side = 0 +7:3/0/terrains_peering_bit/top_side = 0 +8:3/0 = 0 +8:3/0/terrain_set = 0 +8:3/0/terrain = 0 +8:3/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +8:3/0/terrains_peering_bit/right_side = 0 +8:3/0/terrains_peering_bit/top_side = 0 +8:3/0/terrains_peering_bit/top_right_corner = 0 +9:3/0 = 0 +9:3/0/terrain_set = 0 +9:3/0/terrain = 0 +9:3/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +9:3/0/terrains_peering_bit/right_side = 0 +9:3/0/terrains_peering_bit/left_side = 0 +9:3/0/terrains_peering_bit/top_left_corner = 0 +9:3/0/terrains_peering_bit/top_side = 0 +9:3/0/terrains_peering_bit/top_right_corner = 0 +10:3/0 = 0 +10:3/0/terrain_set = 0 +10:3/0/terrain = 0 +10:3/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +10:3/0/terrains_peering_bit/right_side = 0 +10:3/0/terrains_peering_bit/bottom_side = 0 +10:3/0/terrains_peering_bit/left_side = 0 +10:3/0/terrains_peering_bit/top_left_corner = 0 +10:3/0/terrains_peering_bit/top_side = 0 +10:3/0/terrains_peering_bit/top_right_corner = 0 +11:3/0 = 0 +11:3/0/terrain_set = 0 +11:3/0/terrain = 0 +11:3/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +11:3/0/terrains_peering_bit/left_side = 0 +11:3/0/terrains_peering_bit/top_left_corner = 0 +11:3/0/terrains_peering_bit/top_side = 0 + +[resource] +tile_size = Vector2i(16, 16) +physics_layer_0/collision_layer = 1 +terrain_set_0/mode = 0 +terrain_set_0/terrain_0/name = "Grass" +terrain_set_0/terrain_0/color = Color(0.3529, 0.6039, 0.2353, 1) +sources/0 = SubResource("TileSetAtlasSource_terrain") diff --git a/src/core/export/common.ts b/src/core/export/common.ts new file mode 100644 index 0000000..008df9f --- /dev/null +++ b/src/core/export/common.ts @@ -0,0 +1,46 @@ +import { CORNERS, quarterRect } from "../blob/quarters.ts"; +import type { Tileset } from "../generate.ts"; +import type { Rgb } from "./types.ts"; + +/** A safe base file name: keeps letters/digits (any script), space, dot, dash and underscore. */ +export function sanitizeName(name: string, fallback = "tileset"): string { + const cleaned = name + .replace(/\.[a-z0-9]+$/i, "") + .replace(/[^\p{L}\p{N} ._-]+/gu, "_") + .replace(/^[\s.]+|[\s.]+$/g, ""); + return /[\p{L}\p{N}]/u.test(cleaned) ? cleaned : fallback; +} + +/** Average colour of the opaque pixels of the template's fill pieces — a good default terrain colour. */ +export function terrainColor(tileset: Tileset, source: { data: Uint8ClampedArray; width: number }): Rgb { + const ts = tileset.tileSize; + let r = 0; + let g = 0; + let b = 0; + let n = 0; + for (const corner of CORNERS) { + const ref = tileset.template.pieceSource("F", corner); + const q = quarterRect(ts, corner); + for (let y = 0; y < q.h; y++) { + for (let x = 0; x < q.w; x++) { + const i = ((ref.ty * ts + q.y + y) * source.width + ref.tx * ts + q.x + x) * 4; + if (source.data[i + 3] < 128) continue; + r += source.data[i]; + g += source.data[i + 1]; + b += source.data[i + 2]; + n++; + } + } + } + if (!n) return [90, 154, 60]; + return [Math.round(r / n), Math.round(g / n), Math.round(b / n)]; +} + +export function toHex([r, g, b]: Rgb): string { + return `#${[r, g, b].map((v) => v.toString(16).padStart(2, "0")).join("")}`; +} + +/** Shortest decimal (max 4 places) for text resource files. */ +export function num(n: number): string { + return String(Number(n.toFixed(4))); +} diff --git a/src/core/export/export.test.ts b/src/core/export/export.test.ts new file mode 100644 index 0000000..cae6c14 --- /dev/null +++ b/src/core/export/export.test.ts @@ -0,0 +1,151 @@ +import { unzipSync } from "fflate"; +import { describe, expect, test } from "vitest"; + +import { readFixture } from "../../../tests/helpers.ts"; +import { godot3Bitmask, godot4PeeringBits } from "../blob/godot.ts"; +import { generate } from "../generate.ts"; +import { decodePng } from "../image/png.ts"; +import { autotilerV1, dual16, godot12x4 } from "../layouts/index.ts"; +import { autotiler13 } from "../templates/index.ts"; +import { sanitizeName, terrainColor } from "./common.ts"; +import { exportGodot3, godot3Tres } from "./godot3.ts"; +import { exportGodot4, godot4Tres, type Godot4Options } from "./godot4.ts"; +import { exportPng, sidecarJson } from "./png.ts"; +import { exportTiled, tiledTsx } from "./tiled.ts"; +import { zipFiles } from "./zip.ts"; + +const source = readFixture("inputs/demo-16.png"); +const blob = generate(source, { template: autotiler13, tileSize: 16, layout: godot12x4 }); +const dual = generate(source, { template: autotiler13, tileSize: 16, layout: dual16 }); +const opts: Godot4Options = { + name: "grass", + terrainName: "Grass", + terrainColor: [90, 154, 60], + collision: true, + forceNearest: false, +}; + +describe("godot 4", () => { + const tres = godot4Tres(blob, opts); + + test("snapshot", async () => { + await expect(tres).toMatchFileSnapshot("./__snapshots__/grass.godot4.tres"); + }); + + test("sources/0 is the last line of [resource], after layers and terrain sets", () => { + const resource = tres.slice(tres.indexOf("[resource]")).trim().split("\n"); + expect(resource.at(-1)).toBe('sources/0 = SubResource("TileSetAtlasSource_terrain")'); + expect(resource.findIndex((l) => l.startsWith("terrain_set_0/"))).toBeLessThan(resource.length - 1); + expect(resource.findIndex((l) => l.startsWith("physics_layer_0/"))).toBeLessThan(resource.length - 1); + }); + + test("every tile declares terrain_set, terrain and the peering bits of its mask, in order", () => { + for (const cell of blob.cells) { + const id = `${cell.x}:${cell.y}/0`; + const lines = tres.split("\n").filter((l) => l.startsWith(`${id}/`) || l === `${id} = 0`); + expect(lines[0]).toBe(`${id} = 0`); + expect(lines[1]).toBe(`${id}/terrain_set = 0`); + expect(lines[2]).toBe(`${id}/terrain = 0`); + const bits = lines + .filter((l) => l.includes("terrains_peering_bit/")) + .map((l) => l.split("/").at(-1)?.split(" ")[0]); + expect(bits).toEqual(godot4PeeringBits(cell.mask ?? 0)); + } + expect(tres).not.toContain("#"); + }); + + test("relative texture path, sizes always written, polygons centred", () => { + expect(tres).toContain('[ext_resource type="Texture2D" path="grass.png" id="1_texture"]'); + expect(tres).toContain("texture_region_size = Vector2i(16, 16)"); + expect(tres).toContain("tile_size = Vector2i(16, 16)"); + expect(tres).toContain("PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)"); + expect(tres).toContain("terrain_set_0/terrain_0/color = Color(0.3529, 0.6039, 0.2353, 1)"); + }); + + test("no collision and force-nearest variants", () => { + const t = godot4Tres(blob, { ...opts, collision: false, forceNearest: true }); + expect(t).not.toContain("physics_layer_0"); + expect(t).toContain('[sub_resource type="CanvasTexture" id="CanvasTexture_nearest"]'); + expect(t).toContain('texture = SubResource("CanvasTexture_nearest")'); + }); + + test("files and dual-grid rejection", () => { + expect(exportGodot4(blob, opts).map((f) => f.path)).toEqual(["grass.png", "grass.tres"]); + expect(() => godot4Tres(dual, opts)).toThrow("blob layout"); + }); +}); + +describe("godot 3", () => { + test("snapshot, all 47 collisions, v1-compatible tile id", async () => { + const v1 = generate(source, { template: autotiler13, tileSize: 16, layout: autotilerV1 }); + const tres = godot3Tres(v1, { name: "grass", collision: true }); + await expect(tres).toMatchFileSnapshot("./__snapshots__/grass.godot3.tres"); + expect(tres.match(/ConvexPolygonShape2D/g)).toHaveLength(47); + expect(tres).toContain("load_steps=49"); + expect(tres).toContain("1/autotile/icon_coordinate = Vector2( 3, 3 )"); + for (const c of v1.cells) expect(tres).toContain(`Vector2( ${c.x}, ${c.y} ), ${godot3Bitmask(c.mask ?? 0)}`); + expect(exportGodot3(v1, { name: "grass", collision: true }).map((f) => f.path)).toEqual([ + "grass.png", + "grass.png.import", + "grass.tres", + ]); + }); +}); + +describe("tiled", () => { + test("mixed Wang set for blob layouts, never an all-zero wangid", async () => { + const tsx = tiledTsx(blob, { name: "grass", terrainName: "Grass", terrainColor: [90, 154, 60] }); + await expect(tsx).toMatchFileSnapshot("./__snapshots__/grass.blob.tsx.xml"); + expect(tsx).toContain('type="mixed"'); + expect(tsx).toContain('tilecount="48" columns="12"'); + const ids = [...tsx.matchAll(/wangid="([^"]+)"/g)].map((m) => m[1]); + expect(ids).toHaveLength(47); + expect(ids).toContain("2,2,2,2,2,2,2,2"); // the isolated tile survives + expect(ids).not.toContain("0,0,0,0,0,0,0,0"); + }); + + test("corner Wang set for the dual grid", async () => { + const tsx = tiledTsx(dual, { name: "grass", terrainName: "Grass", terrainColor: [90, 154, 60] }); + await expect(tsx).toMatchFileSnapshot("./__snapshots__/grass.dual.tsx.xml"); + expect(tsx).toContain('type="corner"'); + expect(tsx).toContain('wangid="0,2,0,2,0,2,0,2"'); // the empty dual tile + expect(exportTiled(dual, { name: "grass", terrainName: "", terrainColor: [0, 0, 0] })).toHaveLength(2); + }); +}); + +describe("png + json", () => { + test("sidecar describes every tile", () => { + const doc = JSON.parse(sidecarJson(blob)); + expect(doc).toMatchObject({ + format: "autotiler", + kind: "blob47", + layout: "godot-12x4", + tileSize: 16, + columns: 12, + rows: 4, + }); + expect(doc.tiles).toHaveLength(47); + expect(JSON.parse(sidecarJson(dual)).tiles).toHaveLength(16); + }); + + test("the exported PNG decodes back to the generated pixels", () => { + const [png] = exportPng(blob, { name: "grass", sidecar: false }); + if (typeof png.data === "string") throw new Error("PNG data should be binary"); + expect(decodePng(png.data).data).toEqual(blob.image.data); + }); +}); + +test("zip round trip", () => { + const files = exportGodot4(blob, opts); + const zip = unzipSync(zipFiles(files)); + expect(Object.keys(zip)).toEqual(["grass.png", "grass.tres"]); + expect(new TextDecoder().decode(zip["grass.tres"])).toBe(files[1].data); + expect(zipFiles(files)).toEqual(zipFiles(files)); // deterministic +}); + +test("helpers", () => { + expect(sanitizeName("my grass/tiles?.png")).toBe("my grass_tiles_"); + expect(sanitizeName("forêt-01.png")).toBe("forêt-01"); + expect(sanitizeName("../..")).toBe("tileset"); + expect(terrainColor(blob, source)).toEqual([252, 199, 81]); +}); diff --git a/src/core/export/godot3.ts b/src/core/export/godot3.ts new file mode 100644 index 0000000..9f19097 --- /dev/null +++ b/src/core/export/godot3.ts @@ -0,0 +1,118 @@ +// Godot 3 (legacy) TileSet: one autotile in "3x3 minimal" bitmask mode with full-square collisions, plus a +// .png.import with nearest filtering. Generated (not string-templated like v1); tile id 1 as in v1, so tilemaps +// painted with a v1 export keep working. Unlike v1, every tile — including the all-neighbours one — gets a collision. +import { godot3Bitmask } from "../blob/godot.ts"; +import type { Tileset } from "../generate.ts"; +import { encodePng } from "../image/png.ts"; +import { sanitizeName } from "./common.ts"; +import type { ExportFile } from "./types.ts"; + +export interface Godot3Options { + readonly name: string; + readonly collision: boolean; +} + +const TILE = "1/"; + +export function godot3Tres(tileset: Tileset, o: Godot3Options): string { + if (tileset.layout.kind !== "blob") throw new Error("Godot 3 export needs a 47-tile blob layout"); + const name = sanitizeName(o.name); + const ts = tileset.tileSize; + const { columns, rows } = tileset.layout; + const cells = tileset.cells.toSorted((a, b) => a.x - b.x || a.y - b.y); // v1 listed tiles column by column + const shaped = o.collision ? cells : []; + const isolated = cells.find((c) => c.mask === 0) ?? cells[0]; + + const L: string[] = []; + L.push(`[gd_resource type="TileSet" load_steps=${shaped.length + 2} format=2]`, ""); + L.push(`[ext_resource path=${JSON.stringify(`${name}.png`)} type="Texture" id=1]`, ""); + shaped.forEach((_, i) => { + L.push(`[sub_resource type="ConvexPolygonShape2D" id=${i + 1}]`); + L.push(`points = PoolVector2Array( 0, 0, ${ts}, 0, ${ts}, ${ts}, 0, ${ts} )`, ""); + }); + L.push("[resource]"); + L.push(`${TILE}name = ${JSON.stringify(name)}`); + L.push(`${TILE}texture = ExtResource( 1 )`); + L.push(`${TILE}tex_offset = Vector2( 0, 0 )`); + L.push(`${TILE}modulate = Color( 1, 1, 1, 1 )`); + L.push(`${TILE}region = Rect2( 0, 0, ${columns * ts}, ${rows * ts} )`); + L.push(`${TILE}tile_mode = 1`); + L.push(`${TILE}autotile/bitmask_mode = 1`); + L.push( + `${TILE}autotile/bitmask_flags = [ ` + + cells.map((c) => `Vector2( ${c.x}, ${c.y} ), ${godot3Bitmask(c.mask ?? 0)}`).join(", ") + + " ]", + ); + L.push(`${TILE}autotile/icon_coordinate = Vector2( ${isolated.x}, ${isolated.y} )`); + L.push(`${TILE}autotile/tile_size = Vector2( ${ts}, ${ts} )`); + L.push(`${TILE}autotile/spacing = 0`); + L.push(`${TILE}autotile/occluder_map = [ ]`); + L.push(`${TILE}autotile/navpoly_map = [ ]`); + L.push(`${TILE}autotile/priority_map = [ ]`); + L.push(`${TILE}autotile/z_index_map = [ ]`); + L.push(`${TILE}occluder_offset = Vector2( 0, 0 )`); + L.push(`${TILE}navigation_offset = Vector2( 0, 0 )`); + L.push(`${TILE}shape_offset = Vector2( 0, 0 )`); + L.push(`${TILE}shape_transform = Transform2D( 1, 0, 0, 1, 0, 0 )`); + if (shaped.length) L.push(`${TILE}shape = SubResource( 1 )`); + L.push(`${TILE}shape_one_way = false`); + L.push(`${TILE}shape_one_way_margin = 1.0`); + const shapes = shaped.map( + (c, i) => + `{\n"autotile_coord": Vector2( ${c.x}, ${c.y} ),\n"one_way": false,\n"one_way_margin": 1.0,\n"shape": SubResource( ${i + 1} ),\n"shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 )\n}`, + ); + L.push(`${TILE}shapes = [ ${shapes.join(", ")} ]`); + L.push(`${TILE}z_index = 0`, ""); + return L.join("\n"); +} + +/** Import settings for crisp pixel art (Godot 3 filters textures by default). Godot rewrites the paths on import. */ +export function godot3Import(name: string): string { + const file = `${sanitizeName(name)}.png`; + return [ + "[remap]", + "", + 'importer="texture"', + 'type="StreamTexture"', + `path="res://.import/${file}-.stex"`, + "metadata={", + '"vram_texture": false', + "}", + "", + "[deps]", + "", + `source_file="res://${file}"`, + `dest_files=[ "res://.import/${file}-.stex" ]`, + "", + "[params]", + "", + "compress/mode=0", + "compress/lossy_quality=0.7", + "compress/hdr_mode=0", + "compress/bptc_ldr=0", + "compress/normal_map=0", + "flags/repeat=0", + "flags/filter=false", + "flags/mipmaps=false", + "flags/anisotropic=false", + "flags/srgb=2", + "process/fix_alpha_border=true", + "process/premult_alpha=false", + "process/HDR_as_SRGB=false", + "process/invert_color=false", + "stream=false", + "size_limit=0", + "detect_3d=true", + "svg/scale=1.0", + "", + ].join("\n"); +} + +export function exportGodot3(tileset: Tileset, o: Godot3Options): ExportFile[] { + const name = sanitizeName(o.name); + return [ + { path: `${name}.png`, data: encodePng(tileset.image) }, + { path: `${name}.png.import`, data: godot3Import(name) }, + { path: `${name}.tres`, data: godot3Tres(tileset, o) }, + ]; +} diff --git a/src/core/export/godot4.ts b/src/core/export/godot4.ts new file mode 100644 index 0000000..c6fc037 --- /dev/null +++ b/src/core/export/godot4.ts @@ -0,0 +1,77 @@ +// Godot 4 TileSet (text resource, format=3): one atlas source, one terrain set in MATCH_CORNERS_AND_SIDES mode with +// the 47 blob configurations as peering bits, optional full-square collisions. Verified by loading and painting +// the exported files in headless Godot 4.7.2 (tests/godot). format=3 loads in every Godot 4.x release. +// +// Invariants (breaking them loads "fine" but silently corrupts the TileSet): +// - in [resource], every physics_layer_* / terrain_set_* line comes before sources/0 (Godot applies properties in +// file order; attaching the source first drops every polygon and peering bit) +// - per tile: terrain_set before terrain before terrains_peering_bit/* +// - unset peering bits mean "no terrain"; there is no "don't care" +// - never write "#" comments ("#" starts a colour in .tres files) +import { godot4PeeringBits } from "../blob/godot.ts"; +import type { Tileset } from "../generate.ts"; +import { encodePng } from "../image/png.ts"; +import { num, sanitizeName } from "./common.ts"; +import type { ExportFile, Rgb } from "./types.ts"; + +export interface Godot4Options { + /** base file name: writes .png and .tres side by side */ + readonly name: string; + readonly terrainName: string; + readonly terrainColor: Rgb; + /** full-square collision polygon on physics layer 0 for every tile */ + readonly collision: boolean; + /** wrap the texture in a CanvasTexture with nearest filtering (Godot 4.2+), so pixel art stays crisp */ + readonly forceNearest: boolean; +} + +const q = (s: string) => JSON.stringify(s); + +export function godot4Tres(tileset: Tileset, o: Godot4Options): string { + if (tileset.layout.kind !== "blob") throw new Error("Godot 4 export needs a 47-tile blob layout"); + const name = sanitizeName(o.name); + const ts = tileset.tileSize; + const h = num(ts / 2); + const mh = num(-ts / 2); + const L: string[] = []; + + L.push('[gd_resource type="TileSet" format=3]', ""); + // relative to the .tres, so the pair can live anywhere inside a Godot project + L.push(`[ext_resource type="Texture2D" path=${q(`${name}.png`)} id="1_texture"]`, ""); + if (o.forceNearest) { + L.push('[sub_resource type="CanvasTexture" id="CanvasTexture_nearest"]'); + L.push('diffuse_texture = ExtResource("1_texture")', "texture_filter = 1", ""); + } + L.push('[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_terrain"]'); + L.push(o.forceNearest ? 'texture = SubResource("CanvasTexture_nearest")' : 'texture = ExtResource("1_texture")'); + L.push(`texture_region_size = Vector2i(${ts}, ${ts})`); + + const cells = tileset.cells.toSorted((a, b) => a.y - b.y || a.x - b.x); + for (const cell of cells) { + const id = `${cell.x}:${cell.y}/0`; + L.push(`${id} = 0`, `${id}/terrain_set = 0`, `${id}/terrain = 0`); + if (o.collision) { + L.push( + `${id}/physics_layer_0/polygon_0/points = PackedVector2Array(${mh}, ${mh}, ${h}, ${mh}, ${h}, ${h}, ${mh}, ${h})`, + ); + } + for (const bit of godot4PeeringBits(cell.mask ?? 0)) L.push(`${id}/terrains_peering_bit/${bit} = 0`); + } + + const [r, g, b] = o.terrainColor; + L.push("", "[resource]", `tile_size = Vector2i(${ts}, ${ts})`); + if (o.collision) L.push("physics_layer_0/collision_layer = 1"); + L.push("terrain_set_0/mode = 0"); + L.push(`terrain_set_0/terrain_0/name = ${q(o.terrainName || name)}`); + L.push(`terrain_set_0/terrain_0/color = Color(${num(r / 255)}, ${num(g / 255)}, ${num(b / 255)}, 1)`); + L.push('sources/0 = SubResource("TileSetAtlasSource_terrain")', ""); + return L.join("\n"); +} + +export function exportGodot4(tileset: Tileset, o: Godot4Options): ExportFile[] { + const name = sanitizeName(o.name); + return [ + { path: `${name}.png`, data: encodePng(tileset.image) }, + { path: `${name}.tres`, data: godot4Tres(tileset, o) }, + ]; +} diff --git a/src/core/export/index.ts b/src/core/export/index.ts new file mode 100644 index 0000000..41025fb --- /dev/null +++ b/src/core/export/index.ts @@ -0,0 +1,7 @@ +export { sanitizeName, terrainColor, toHex } from "./common.ts"; +export { exportGodot3, godot3Import, godot3Tres, type Godot3Options } from "./godot3.ts"; +export { exportGodot4, godot4Tres, type Godot4Options } from "./godot4.ts"; +export { exportPng, sidecarJson, type PngOptions } from "./png.ts"; +export { exportTiled, tiledTsx, type TiledOptions } from "./tiled.ts"; +export type { ExportFile, Rgb, TargetId } from "./types.ts"; +export { zipFiles } from "./zip.ts"; diff --git a/src/core/export/png.ts b/src/core/export/png.ts new file mode 100644 index 0000000..81ac36c --- /dev/null +++ b/src/core/export/png.ts @@ -0,0 +1,50 @@ +import type { Tileset } from "../generate.ts"; +import { encodePng } from "../image/png.ts"; +import { sanitizeName } from "./common.ts"; +import type { ExportFile } from "./types.ts"; + +export interface PngOptions { + readonly name: string; + /** also write .autotile.json describing every tile */ + readonly sidecar: boolean; +} + +/** Engine-agnostic description of the atlas (bit order matches Tiled's wangid and Godot 4's peering bits). */ +export function sidecarJson(tileset: Tileset): string { + const { layout, tileSize } = tileset; + const tiles = tileset.cells + .toSorted((a, b) => a.y - b.y || a.x - b.x) + .map((c) => (layout.kind === "blob" ? { x: c.x, y: c.y, mask: c.mask } : { x: c.x, y: c.y, corners: c.corners })); + const doc = + layout.kind === "blob" + ? { + format: "autotiler", + version: 1, + kind: "blob47", + layout: layout.id, + tileSize, + columns: layout.columns, + rows: layout.rows, + bits: { N: 1, NE: 2, E: 4, SE: 8, S: 16, SW: 32, W: 64, NW: 128 }, + tiles, + } + : { + format: "autotiler", + version: 1, + kind: "dual16", + layout: layout.id, + tileSize, + columns: layout.columns, + rows: layout.rows, + bits: { TL: 1, TR: 2, BL: 4, BR: 8 }, + tiles, + }; + return `${JSON.stringify(doc, null, 2)}\n`; +} + +export function exportPng(tileset: Tileset, o: PngOptions): ExportFile[] { + const name = sanitizeName(o.name); + const files: ExportFile[] = [{ path: `${name}.png`, data: encodePng(tileset.image) }]; + if (o.sidecar) files.push({ path: `${name}.autotile.json`, data: sidecarJson(tileset) }); + return files; +} diff --git a/src/core/export/tiled.ts b/src/core/export/tiled.ts new file mode 100644 index 0000000..dad4fa9 --- /dev/null +++ b/src/core/export/tiled.ts @@ -0,0 +1,52 @@ +// Tiled tileset (.tsx) with a Wang set: "mixed" (corners + edges) for blob layouts, "corner" for the dual grid. +// Two colours are used — terrain (1) and "empty" (2) — because Tiled silently drops an all-zero wangid, which would +// lose the isolated tile. +import { DIRECTIONS } from "../blob/mask.ts"; +import type { Tileset } from "../generate.ts"; +import { encodePng } from "../image/png.ts"; +import { sanitizeName, toHex } from "./common.ts"; +import type { ExportFile, Rgb } from "./types.ts"; + +export interface TiledOptions { + readonly name: string; + readonly terrainName: string; + readonly terrainColor: Rgb; +} + +const v = (on: boolean) => (on ? 1 : 2); +const xml = (s: string) => s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); + +export function tiledTsx(tileset: Tileset, o: TiledOptions): string { + const name = sanitizeName(o.name); + const ts = tileset.tileSize; + const { columns, rows, kind } = tileset.layout; + const terrain = o.terrainName || name; + const cells = tileset.cells.toSorted((a, b) => a.y - b.y || a.x - b.x); + const wangid = (cell: (typeof cells)[number]): string => { + if (kind === "blob") return DIRECTIONS.map((d) => v(((cell.mask ?? 0) & d.bit) !== 0)).join(","); + // corner sets only use the corner slots: top-right, bottom-right, bottom-left, top-left + const c = cell.corners ?? 0; + return [0, v((c & 2) !== 0), 0, v((c & 8) !== 0), 0, v((c & 4) !== 0), 0, v((c & 1) !== 0)].join(","); + }; + const L: string[] = []; + L.push(''); + L.push( + ``, + ); + L.push(` `); + L.push(" "); + L.push(` `); + L.push(` `); + L.push(' '); + for (const cell of cells) L.push(` `); + L.push(" ", " ", "", ""); + return L.join("\n"); +} + +export function exportTiled(tileset: Tileset, o: TiledOptions): ExportFile[] { + const name = sanitizeName(o.name); + return [ + { path: `${name}.png`, data: encodePng(tileset.image) }, + { path: `${name}.tsx`, data: tiledTsx(tileset, o) }, + ]; +} diff --git a/src/core/export/types.ts b/src/core/export/types.ts new file mode 100644 index 0000000..00df092 --- /dev/null +++ b/src/core/export/types.ts @@ -0,0 +1,10 @@ +export interface ExportFile { + /** file name relative to the output folder */ + readonly path: string; + readonly data: Uint8Array | string; +} + +export type TargetId = "godot4" | "godot3" | "tiled" | "png"; + +/** RGB in 0..255 */ +export type Rgb = readonly [number, number, number]; diff --git a/src/core/export/zip.ts b/src/core/export/zip.ts new file mode 100644 index 0000000..8b55212 --- /dev/null +++ b/src/core/export/zip.ts @@ -0,0 +1,16 @@ +import { strToU8, zipSync, type Zippable } from "fflate"; + +import type { ExportFile } from "./types.ts"; + +// fixed timestamp so the same export always produces the same bytes +const MTIME = new Date(Date.UTC(2026, 0, 1)); + +export function zipFiles(files: readonly ExportFile[]): Uint8Array { + const entries: Zippable = {}; + for (const f of files) { + const data = typeof f.data === "string" ? strToU8(f.data) : f.data; + // PNGs are already deflated + entries[f.path] = [data, { level: f.path.endsWith(".png") ? 0 : 6, mtime: MTIME }]; + } + return zipSync(entries); +} diff --git a/src/core/generate.ts b/src/core/generate.ts new file mode 100644 index 0000000..7d1cf60 --- /dev/null +++ b/src/core/generate.ts @@ -0,0 +1,73 @@ +import { dualQuadrant } from "./blob/dual.ts"; +import type { Mask } from "./blob/mask.ts"; +import { CORNERS, quarterRect } from "./blob/quarters.ts"; +import { copyRect, createImage, type RgbaImage } from "./image/rgba.ts"; +import type { Layout } from "./layouts/types.ts"; +import type { QuarterRef, Template } from "./templates/types.ts"; + +export interface TilesetCell { + readonly x: number; + readonly y: number; + /** blob layouts: canonical neighbour mask */ + readonly mask: Mask | null; + /** dual layouts: filled world corners (TL=1 TR=2 BL=4 BR=8) */ + readonly corners: number | null; + /** source quarter used for each display quadrant (TL, TR, BL, BR); null = left empty */ + readonly quarters: readonly (QuarterRef | null)[]; +} + +export interface Tileset { + readonly image: RgbaImage; + readonly tileSize: number; + readonly template: Template; + readonly layout: Layout; + readonly cells: readonly TilesetCell[]; +} + +export interface GenerateOptions { + readonly template: Template; + readonly tileSize: number; + readonly layout: Layout; +} + +/** Composes the output atlas. Every output quadrant is copied exactly once from one source quarter — no blending. */ +export function generate(source: RgbaImage, { template, tileSize: ts, layout }: GenerateOptions): Tileset { + if (!Number.isInteger(ts) || ts < 2) throw new RangeError(`invalid tile size ${ts}`); + if (source.width < template.columns * ts || source.height < template.rows * ts) { + throw new RangeError( + `a ${template.columns}×${template.rows}-tile template at ${ts} px needs ${template.columns * ts}×${template.rows * ts} px, got ${source.width}×${source.height}`, + ); + } + if (layout.kind === "dual" && ts % 2 !== 0) throw new RangeError("dual-grid tilesets need an even tile size"); + + const image = createImage(layout.columns * ts, layout.rows * ts); + const place = (ref: QuarterRef, cellX: number, cellY: number, quadrant: (typeof CORNERS)[number]) => { + const s = quarterRect(ts, ref.corner); + const d = quarterRect(ts, quadrant); + copyRect(source, ref.tx * ts + s.x, ref.ty * ts + s.y, s.w, s.h, image, cellX * ts + d.x, cellY * ts + d.y); + }; + + let cells: TilesetCell[]; + if (layout.kind === "blob") { + cells = layout.cells.map(({ x, y, mask }) => { + const quarters = CORNERS.map((corner) => { + const ref = template.quarterFor(mask, corner); + place(ref, x, y, corner); + return ref; + }); + return { x, y, mask, corners: null, quarters }; + }); + } else { + cells = layout.cells.map(({ x, y, corners }) => { + const quarters = CORNERS.map((quadrant) => { + const q = dualQuadrant(corners, quadrant); + if (!q.filled) return null; + const ref = template.pieceSource(q.kind, q.quarter); + place(ref, x, y, quadrant); + return ref; + }); + return { x, y, mask: null, corners, quarters }; + }); + } + return { image, tileSize: ts, template, layout, cells }; +} diff --git a/src/core/guide.ts b/src/core/guide.ts new file mode 100644 index 0000000..e3ac336 --- /dev/null +++ b/src/core/guide.ts @@ -0,0 +1,42 @@ +// A blank template for artists: every quarter the generator reads is filled with the colour of its piece kind +// (shaded by corner), unused cells stay transparent. The app shows the legend. +import { CORNERS, type Corner, type PieceKind, quarterRect } from "./blob/quarters.ts"; +import { createImage, type RgbaImage } from "./image/rgba.ts"; +import type { Template } from "./templates/types.ts"; + +export const PIECE_COLORS: Record = { + O: [224, 108, 117], + H: [97, 175, 239], + V: [152, 195, 121], + I: [229, 192, 123], + F: [150, 158, 172], +}; + +const SHADE: Record = { TL: 1.12, TR: 1.04, BL: 0.96, BR: 0.88 }; + +export function renderGuide(template: Template, tileSize: number): RgbaImage { + const img = createImage(template.columns * tileSize, template.rows * tileSize); + for (let ty = 0; ty < template.rows; ty++) { + for (let tx = 0; tx < template.columns; tx++) { + for (const corner of CORNERS) { + const kind = template.pieceAt(tx, ty, corner); + if (!kind) continue; + const [r, g, b] = PIECE_COLORS[kind].map((v) => Math.min(255, Math.round(v * SHADE[corner]))); + const q = quarterRect(tileSize, corner); + for (let y = 0; y < q.h; y++) { + for (let x = 0; x < q.w; x++) { + const px = tx * tileSize + q.x + x; + const py = ty * tileSize + q.y + y; + // darker 1 px tile border (only when tiles are big enough to spare it) + const edge = + tileSize >= 16 && + (px % tileSize === 0 || py % tileSize === 0 || (px + 1) % tileSize === 0 || (py + 1) % tileSize === 0); + const k = edge ? 0.7 : 1; + img.data.set([Math.round(r * k), Math.round(g * k), Math.round(b * k), 255], (py * img.width + px) * 4); + } + } + } + } + } + return img; +} diff --git a/src/core/image/png.test.ts b/src/core/image/png.test.ts new file mode 100644 index 0000000..8e1a11b --- /dev/null +++ b/src/core/image/png.test.ts @@ -0,0 +1,77 @@ +import { readFileSync } from "node:fs"; + +import { encode } from "fast-png"; +import { describe, expect, test } from "vitest"; + +import { decodePng, encodePng, isPng } from "./png.ts"; +import { createImage, diffImages } from "./rgba.ts"; + +const demo = () => readFileSync(new URL("../../../tests/fixtures/inputs/demo-16.png", import.meta.url)); +/** Inserts a chunk right after IHDR (8-byte signature + 25-byte IHDR chunk). */ +function withChunk(png: Uint8Array, type: string, data: number[]): Uint8Array { + const body = Array.from(type, (c) => c.charCodeAt(0)).concat(data); + let crc = ~0; + for (const b of body) { + crc ^= b; + for (let k = 0; k < 8; k++) crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1)); + } + crc = ~crc >>> 0; + const len = data.length; + const chunk = [len >>> 24, (len >>> 16) & 255, (len >>> 8) & 255, len & 255, ...body]; + chunk.push(crc >>> 24, (crc >>> 16) & 255, (crc >>> 8) & 255, crc & 255); + return new Uint8Array([...png.subarray(0, 33), ...chunk, ...png.subarray(33)]); +} + +const colours = (data: Uint8ClampedArray) => { + const set = new Set(); + for (let i = 0; i < data.length; i += 4) set.add(`${data[i]},${data[i + 1]},${data[i + 2]},${data[i + 3]}`); + return set; +}; + +describe("decodePng", () => { + test("palette + tRNS + gAMA/cHRM PNG decodes to the raw palette (no colour management)", () => { + const img = decodePng(demo()); + expect([img.width, img.height]).toEqual([80, 48]); + const c = colours(img.data); + expect(c.size).toBe(7); + expect(c).toContain("93,22,16,255"); // v1 (Chromium canvas) turned this into 93,14,7 + expect(c).toContain("0,0,0,0"); + }); + + test("greyscale 1/2/4/8/16-bit, with and without tRNS", () => { + const gray4 = encode({ width: 4, height: 1, data: new Uint8Array([0x0f, 0xf0]), depth: 4, channels: 1 }); + expect([...decodePng(gray4).data]).toEqual([0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 255]); + const gray16 = encode({ width: 1, height: 1, data: new Uint16Array([0x8000]), depth: 16, channels: 1 }); + expect([...decodePng(gray16).data]).toEqual([128, 128, 128, 255]); + // fast-png only writes tRNS for palette images, so add the chunk by hand: transparent colour = 4,5,6. + // (fast-png's decoder also rejects an RGB tRNS on images with fewer than 3 pixels, hence 4×1.) + const rgb8 = withChunk( + encode({ + width: 4, + height: 1, + data: new Uint8Array([1, 2, 3, 4, 5, 6, 4, 5, 6, 7, 8, 9]), + depth: 8, + channels: 3, + }), + "tRNS", + [0, 4, 0, 5, 0, 6], + ); + expect([...decodePng(rgb8).data]).toEqual([1, 2, 3, 255, 4, 5, 6, 0, 4, 5, 6, 0, 7, 8, 9, 255]); + const ga = encode({ width: 1, height: 1, data: new Uint8Array([200, 100]), depth: 8, channels: 2 }); + expect([...decodePng(ga).data]).toEqual([200, 200, 200, 100]); + }); + + test("rejects non-PNG data", () => { + expect(isPng(new Uint8Array([1, 2, 3]))).toBe(false); + expect(() => decodePng(new Uint8Array([1, 2, 3]))).toThrow("not a PNG file"); + }); +}); + +test("encode → decode round trip is lossless, including semi-transparent pixels", () => { + const img = createImage(3, 2); + img.data.set([255, 0, 0, 255, 10, 20, 30, 1, 0, 0, 0, 0, 7, 8, 9, 128, 255, 255, 255, 255, 1, 2, 3, 254]); + const back = decodePng(encodePng(img)); + expect(diffImages(img, back).count).toBe(0); + expect([...back.data]).toEqual([...img.data]); + expect(decodePng(encodePng(decodePng(demo()))).data).toEqual(decodePng(demo()).data); +}); diff --git a/src/core/image/png.ts b/src/core/image/png.ts new file mode 100644 index 0000000..0d032cf --- /dev/null +++ b/src/core/image/png.ts @@ -0,0 +1,74 @@ +// PNG <-> RgbaImage with a pure-JS codec. Colour chunks (gAMA, cHRM, iCCP, sRGB) are ignored on purpose: game +// engines use the raw pixel values, and colour-managing them is what shifted palettes in v1. +import { convertIndexedToRgb, decode, encode } from "fast-png"; + +import { createImage, type RgbaImage } from "./rgba.ts"; + +const SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; + +export function isPng(bytes: Uint8Array): boolean { + return SIGNATURE.every((b, i) => bytes[i] === b); +} + +export function decodePng(bytes: Uint8Array): RgbaImage { + if (!isPng(bytes)) throw new Error("not a PNG file"); + const png = decode(bytes); + const { width, height } = png; + const out = createImage(width, height); + const px = width * height; + + if (png.palette) { + const rgb = convertIndexedToRgb(png); + const ch = rgb.length / px; + for (let i = 0; i < px; i++) { + out.data[i * 4] = rgb[i * ch]; + out.data[i * 4 + 1] = rgb[i * ch + 1]; + out.data[i * 4 + 2] = rgb[i * ch + 2]; + out.data[i * 4 + 3] = ch === 4 ? rgb[i * ch + 3] : 255; + } + return out; + } + + const ch = png.channels; + const depth = png.depth; + const max = (1 << depth) - 1; + const rowBytes = Math.ceil((width * ch * depth) / 8); + // raw sample value (at the image's bit depth) of channel c of pixel i + const sample = (i: number, c: number): number => { + if (depth >= 8) return png.data[i * ch + c]; + const x = i % width; + const y = (i - x) / width; + const bit = (x * ch + c) * depth; + const byte = png.data[y * rowBytes + (bit >> 3)]; + return (byte >> (8 - depth - (bit & 7))) & max; + }; + const to8 = (v: number): number => (depth === 8 ? v : depth === 16 ? v >> 8 : Math.round((v * 255) / max)); + // tRNS for greyscale/truecolour images is a single "transparent colour" in raw sample units + const key = png.transparency && png.transparency.length > 0 ? png.transparency : null; + + for (let i = 0; i < px; i++) { + const o = i * 4; + if (ch >= 3) { + const r = sample(i, 0); + const g = sample(i, 1); + const b = sample(i, 2); + out.data[o] = to8(r); + out.data[o + 1] = to8(g); + out.data[o + 2] = to8(b); + out.data[o + 3] = ch === 4 ? to8(sample(i, 3)) : key && key[0] === r && key[1] === g && key[2] === b ? 0 : 255; + } else { + const v = sample(i, 0); + const g8 = to8(v); + out.data[o] = g8; + out.data[o + 1] = g8; + out.data[o + 2] = g8; + out.data[o + 3] = ch === 2 ? to8(sample(i, 1)) : key && key[0] === v ? 0 : 255; + } + } + return out; +} + +/** RGBA8 PNG without colour chunks. */ +export function encodePng(img: RgbaImage): Uint8Array { + return encode({ width: img.width, height: img.height, data: img.data, depth: 8, channels: 4 }); +} diff --git a/src/core/image/rgba.ts b/src/core/image/rgba.ts new file mode 100644 index 0000000..be63b57 --- /dev/null +++ b/src/core/image/rgba.ts @@ -0,0 +1,100 @@ +/** An 8-bit RGBA image with straight (non-premultiplied) alpha. */ +export interface RgbaImage { + readonly width: number; + readonly height: number; + readonly data: Uint8ClampedArray; +} + +export function createImage(width: number, height: number): RgbaImage { + if (!Number.isInteger(width) || !Number.isInteger(height) || width < 0 || height < 0) { + throw new RangeError(`invalid image size ${width}x${height}`); + } + return { width, height, data: new Uint8ClampedArray(width * height * 4) }; +} + +/** Copies a w×h block, replacing the destination pixels (alpha included). Never blends. */ +export function copyRect( + src: RgbaImage, + sx: number, + sy: number, + w: number, + h: number, + dst: RgbaImage, + dx: number, + dy: number, +): void { + if (sx < 0 || sy < 0 || sx + w > src.width || sy + h > src.height) { + throw new RangeError(`source rect ${sx},${sy} ${w}x${h} is outside the ${src.width}x${src.height} image`); + } + if (dx < 0 || dy < 0 || dx + w > dst.width || dy + h > dst.height) { + throw new RangeError(`destination rect ${dx},${dy} ${w}x${h} is outside the ${dst.width}x${dst.height} image`); + } + const rowBytes = w * 4; + for (let y = 0; y < h; y++) { + const si = ((sy + y) * src.width + sx) * 4; + const di = ((dy + y) * dst.width + dx) * 4; + dst.data.set(src.data.subarray(si, si + rowBytes), di); + } +} + +/** Like copyRect, but silently clips the parts that fall outside the destination. */ +export function copyRectClipped( + src: RgbaImage, + sx: number, + sy: number, + w: number, + h: number, + dst: RgbaImage, + dx: number, + dy: number, +): void { + const x0 = Math.max(0, -dx); + const y0 = Math.max(0, -dy); + const x1 = Math.min(w, dst.width - dx); + const y1 = Math.min(h, dst.height - dy); + if (x1 <= x0 || y1 <= y0) return; + copyRect(src, sx + x0, sy + y0, x1 - x0, y1 - y0, dst, dx + x0, dy + y0); +} + +/** True when every pixel of the rect is fully transparent. */ +export function isRectTransparent(img: RgbaImage, x: number, y: number, w: number, h: number): boolean { + for (let yy = y; yy < y + h; yy++) { + for (let xx = x; xx < x + w; xx++) { + if (img.data[(yy * img.width + xx) * 4 + 3] !== 0) return false; + } + } + return true; +} + +export interface ImageDiff { + /** false when the sizes differ (count and image are then meaningless) */ + readonly sameSize: boolean; + /** number of differing pixels */ + readonly count: number; + /** a faded copy of `a` with differing pixels in magenta */ + readonly image: RgbaImage; +} + +/** Pixel-exact comparison. Fully transparent pixels compare equal whatever their RGB values. */ +export function diffImages(a: RgbaImage, b: RgbaImage): ImageDiff { + if (a.width !== b.width || a.height !== b.height) { + return { sameSize: false, count: Math.max(a.width * a.height, b.width * b.height), image: createImage(0, 0) }; + } + const image = createImage(a.width, a.height); + let count = 0; + for (let i = 0; i < a.data.length; i += 4) { + const aa = a.data[i + 3]; + const ba = b.data[i + 3]; + const same = + (aa === 0 && ba === 0) || + (aa === ba && a.data[i] === b.data[i] && a.data[i + 1] === b.data[i + 1] && a.data[i + 2] === b.data[i + 2]); + if (same) { + const v = Math.round((a.data[i] + a.data[i + 1] + a.data[i + 2]) / 3); + image.data.set([v, v, v, Math.min(aa, 64)], i); + } else { + count++; + image.data.set([255, 0, 255, 255], i); + } + } + return { sameSize: true, count, image }; +} diff --git a/src/core/index.ts b/src/core/index.ts new file mode 100644 index 0000000..072e72c --- /dev/null +++ b/src/core/index.ts @@ -0,0 +1,50 @@ +// Public API of the environment-free core: runs unchanged in the browser, the Tauri webview and Node. +export { DUAL_BL, DUAL_BR, DUAL_TL, DUAL_TR, dualQuadrant, type DualCorners } from "./blob/dual.ts"; +export { GODOT4_PEERING_BITS, godot3Bitmask, godot4PeeringBits } from "./blob/godot.ts"; +export { + BLOB47, + canonical, + DIRECTIONS, + directionsOf, + isCanonical, + maskOf, + type Direction, + type Mask, +} from "./blob/mask.ts"; +export { + CORNERS, + PIECE_KINDS, + PIECE_NAMES, + pieceKind, + quarterRect, + type Corner, + type PieceKind, +} from "./blob/quarters.ts"; +export { analyzeSource, detectTemplate, type Issue, type IssueLevel, type SourceAnalysis } from "./detect.ts"; +export * from "./export/index.ts"; +export { generate, type GenerateOptions, type Tileset, type TilesetCell } from "./generate.ts"; +export { PIECE_COLORS, renderGuide } from "./guide.ts"; +export { decodePng, encodePng, isPng } from "./image/png.ts"; +export { + copyRect, + copyRectClipped, + createImage, + diffImages, + isRectTransparent, + type ImageDiff, + type RgbaImage, +} from "./image/rgba.ts"; +export * from "./layouts/index.ts"; +export * from "./templates/index.ts"; +export { + allBlobCombinations, + allDualCombinations, + blobMaskAt, + createTestMap, + dualCornersAt, + isTerrain, + randomMap, + renderTestMap, + setCell, + type TestMap, +} from "./test-map.ts"; diff --git a/src/core/layouts/blob.ts b/src/core/layouts/blob.ts new file mode 100644 index 0000000..a56ecdc --- /dev/null +++ b/src/core/layouts/blob.ts @@ -0,0 +1,61 @@ +// 47-tile blob layouts as (column, row) grids of canonical masks (N=1 NE=2 E=4 SE=8 S=16 SW=32 W=64 NW=128). +// null = empty cell. Every layout is verified in Godot 4.7.2 (terrain painting picks the expected tile). +import type { BlobCell, BlobLayout } from "./types.ts"; + +function cellsFrom(grid: ReadonlyArray>): BlobCell[] { + return grid.flatMap((row, y) => row.flatMap((mask, x) => (mask === null ? [] : [{ x, y, mask }]))); +} + +/** Godot 3 docs `autotile_template_3x3_minimal.png` order — the most copied blob layout. */ +export const godot12x4: BlobLayout = { + kind: "blob", + id: "godot-12x4", + name: "Godot 3×3 minimal (12×4)", + description: "The standard blob template from the Godot docs, used by most tools and tutorials.", + columns: 12, + rows: 4, + cells: cellsFrom([ + [16, 20, 84, 80, 213, 92, 116, 87, 28, 125, 124, 112], + [17, 21, 85, 81, 29, 127, 253, 113, 31, 119, null, 245], + [1, 5, 69, 65, 23, 223, 247, 209, 95, 255, 221, 241], + [0, 4, 68, 64, 117, 71, 197, 93, 7, 199, 215, 193], + ]), +}; + +/** Autotiler v1's own arrangement (tile ids stay compatible with v1 exports). */ +export const autotilerV1: BlobLayout = { + kind: "blob", + id: "autotiler-v1", + name: "Autotiler v1 (11×5)", + description: "The layout Autotiler 1.x generated. Pick it to replace a tileset exported with v1.", + columns: 11, + rows: 5, + cells: cellsFrom([ + [28, 124, 112, 16, 20, 116, 92, 80, 84, 221, null], + [31, 255, 241, 17, 23, 247, 223, 209, 215, 119, null], + [7, 199, 193, 1, 29, 253, 127, 113, 125, 93, 117], + [4, 68, 64, 0, 5, 197, 71, 65, 69, 87, 213], + [null, null, null, null, 21, 245, 95, 81, 85, null, null], + ]), +}; + +/** + * GameMaker's 47-tile auto-tile template order (= RPG Maker's internal shape order), 8 columns. GameMaker reserves + * tile index 0 as "no tile", so the first cell stays empty and template entry k sits at index k+1. + */ +export const gamemaker47: BlobLayout = { + kind: "blob", + id: "gamemaker-47", + name: "GameMaker 47 (8×6)", + description: "GameMaker's 47-tile auto-tile template order; the first cell is left empty for GameMaker's tile 0.", + columns: 8, + rows: 6, + cells: cellsFrom([ + [null, 255, 127, 253, 125, 247, 119, 245], + [117, 223, 95, 221, 93, 215, 87, 213], + [85, 31, 29, 23, 21, 124, 116, 92], + [84, 241, 209, 113, 81, 199, 71, 197], + [69, 17, 68, 28, 20, 112, 80, 193], + [65, 7, 5, 16, 4, 1, 64, 0], + ]), +}; diff --git a/src/core/layouts/dual.ts b/src/core/layouts/dual.ts new file mode 100644 index 0000000..e9d6e89 --- /dev/null +++ b/src/core/layouts/dual.ts @@ -0,0 +1,20 @@ +import type { DualCell, DualLayout } from "./types.ts"; + +// TileMapDual "Standard" / Godot 3 docs `autotile_template_2x2.png` order. Values are the filled world corners +// (TL=1 TR=2 BL=4 BR=8); the sheet reads as one connected picture. +const GRID = [ + [4, 10, 13, 12], + [9, 14, 15, 7], + [2, 3, 11, 5], + [0, 8, 6, 1], +]; + +export const dual16: DualLayout = { + kind: "dual", + id: "dual-16", + name: "Dual grid (4×4)", + description: "16 corner-based tiles for dual-grid tilemaps (TileMapDual addon, Tiled corner sets).", + columns: 4, + rows: 4, + cells: GRID.flatMap((row, y) => row.map((corners, x): DualCell => ({ x, y, corners }))), +}; diff --git a/src/core/layouts/index.ts b/src/core/layouts/index.ts new file mode 100644 index 0000000..90b4603 --- /dev/null +++ b/src/core/layouts/index.ts @@ -0,0 +1,20 @@ +import { autotilerV1, gamemaker47, godot12x4 } from "./blob.ts"; +import { dual16 } from "./dual.ts"; +import type { Layout, LayoutId } from "./types.ts"; + +export type { BlobCell, BlobLayout, BlobLayoutId, DualCell, DualLayout, Layout, LayoutId } from "./types.ts"; +export { autotilerV1, dual16, gamemaker47, godot12x4 }; + +export const LAYOUTS: readonly Layout[] = [godot12x4, autotilerV1, gamemaker47, dual16]; + +export const DEFAULT_LAYOUT_ID: LayoutId = "godot-12x4"; + +export function isLayoutId(id: string): id is LayoutId { + return LAYOUTS.some((l) => l.id === id); +} + +export function getLayout(id: LayoutId): Layout { + const layout = LAYOUTS.find((l) => l.id === id); + if (!layout) throw new Error(`unknown layout ${id}`); + return layout; +} diff --git a/src/core/layouts/layouts.test.ts b/src/core/layouts/layouts.test.ts new file mode 100644 index 0000000..02320a2 --- /dev/null +++ b/src/core/layouts/layouts.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, test } from "vitest"; + +import { BLOB47 } from "../blob/mask.ts"; +import { DEFAULT_LAYOUT_ID, dual16, getLayout, LAYOUTS } from "./index.ts"; + +describe.each(LAYOUTS.filter((l) => l.kind === "blob").map((l) => [l.id, l] as const))("%s", (_, layout) => { + test("holds each of the 47 canonical masks exactly once, inside the grid", () => { + const masks = layout.cells.map((c) => c.mask).toSorted((a, b) => a - b); + expect(masks).toEqual([...BLOB47]); + const positions = new Set(layout.cells.map((c) => `${c.x},${c.y}`)); + expect(positions.size).toBe(47); + for (const c of layout.cells) { + expect(c.x).toBeLessThan(layout.columns); + expect(c.y).toBeLessThan(layout.rows); + } + }); +}); + +test("dual-16 holds each corner combination once", () => { + expect(dual16.cells.map((c) => c.corners).toSorted((a, b) => a - b)).toEqual([...Array(16).keys()]); +}); + +test("GameMaker layout keeps tile index 0 empty", () => { + const gm = getLayout("gamemaker-47"); + expect(gm.cells.some((c) => c.x === 0 && c.y === 0)).toBe(false); +}); + +test("the default layout is the Godot docs 12×4 template", () => { + expect(getLayout(DEFAULT_LAYOUT_ID).id).toBe("godot-12x4"); +}); diff --git a/src/core/layouts/types.ts b/src/core/layouts/types.ts new file mode 100644 index 0000000..0b555eb --- /dev/null +++ b/src/core/layouts/types.ts @@ -0,0 +1,41 @@ +import type { DualCorners } from "../blob/dual.ts"; +import type { Mask } from "../blob/mask.ts"; + +export type BlobLayoutId = "godot-12x4" | "autotiler-v1" | "gamemaker-47"; +export type LayoutId = BlobLayoutId | "dual-16"; + +export interface BlobCell { + readonly x: number; + readonly y: number; + readonly mask: Mask; +} + +export interface DualCell { + readonly x: number; + readonly y: number; + readonly corners: DualCorners; +} + +interface LayoutInfo { + readonly name: string; + readonly description: string; + /** size in tiles */ + readonly columns: number; + readonly rows: number; +} + +/** A 47-tile blob layout: where each canonical mask lives in the atlas. */ +export interface BlobLayout extends LayoutInfo { + readonly kind: "blob"; + readonly id: BlobLayoutId; + readonly cells: readonly BlobCell[]; +} + +/** A 16-tile dual-grid (Wang 2-corner) layout. */ +export interface DualLayout extends LayoutInfo { + readonly kind: "dual"; + readonly id: "dual-16"; + readonly cells: readonly DualCell[]; +} + +export type Layout = BlobLayout | DualLayout; diff --git a/src/core/templates/autotiler13.ts b/src/core/templates/autotiler13.ts new file mode 100644 index 0000000..b3caf70 --- /dev/null +++ b/src/core/templates/autotiler13.ts @@ -0,0 +1,75 @@ +// Autotiler v1's template: 5×3 tiles. +// x: 0 1 2 3 4 +// y0 [TL] [T ] [TR] [i ] [i ] a 3×3 island (outer corners, edges, centre) at x0-2, +// y1 [L ] [C ] [R ] [i ] [i ] a 2×2 block at x3-4,y0-1 whose four centre quarters are the inner corners, +// y2 [BL] [B ] [BR] [--] [--] and two spare cells. +import { E, type Mask, N, S, W } from "../blob/mask.ts"; +import { type Corner, isLeft, isTop, type PieceKind, pieceKind } from "../blob/quarters.ts"; +import type { QuarterRef, Template, TemplateSlot } from "./types.ts"; + +const INNER: Record = { + TL: { tx: 4, ty: 1, corner: "TL" }, + TR: { tx: 3, ty: 1, corner: "TR" }, + BL: { tx: 4, ty: 0, corner: "BL" }, + BR: { tx: 3, ty: 0, corner: "BR" }, +}; + +const BOX_LABELS = [ + ["top-left corner", "top edge", "top-right corner"], + ["left edge", "centre", "right edge"], + ["bottom-left corner", "bottom edge", "bottom-right corner"], +]; + +const SLOTS: TemplateSlot[] = [ + ...BOX_LABELS.flatMap((row, ty) => row.map((label, tx) => ({ tx, ty, label, used: true }))), + { tx: 3, ty: 0, label: "inner corners", used: true }, + { tx: 4, ty: 0, label: "inner corners", used: true }, + { tx: 3, ty: 1, label: "inner corners", used: true }, + { tx: 4, ty: 1, label: "inner corners", used: true }, + { tx: 3, ty: 2, label: "spare", used: false }, + { tx: 4, ty: 2, label: "spare", used: false }, +]; + +export const autotiler13: Template = { + id: "autotiler-13", + name: "Autotiler 13-tile (5×3)", + description: "A 3×3 island plus a 2×2 inner-corner block — the Autotiler v1 template.", + columns: 5, + rows: 3, + + // "Matching base tile" rule: take the quarter from the island tile whose sides match the cell best, so each side + // configuration keeps exactly what the artist drew. Reproduces v1 except its 12 mismatched strip quarters. + quarterFor(mask: Mask, corner: Corner): QuarterRef { + if (pieceKind(mask, corner) === "I") return INNER[corner]; + const l = (mask & W) !== 0; + const r = (mask & E) !== 0; + const t = (mask & N) !== 0; + const b = (mask & S) !== 0; + const tx = isLeft(corner) ? (l ? (r ? 1 : 2) : 0) : r ? (l ? 1 : 0) : 2; + const ty = isTop(corner) ? (t ? (b ? 1 : 2) : 0) : b ? (t ? 1 : 0) : 2; + return { tx, ty, corner }; + }, + + pieceSource(kind: PieceKind, corner: Corner): QuarterRef { + if (kind === "I") return INNER[corner]; + const x = isLeft(corner) ? 0 : 2; + const y = isTop(corner) ? 0 : 2; + const [tx, ty] = kind === "O" ? [x, y] : kind === "H" ? [1, y] : kind === "V" ? [x, 1] : [1, 1]; + return { tx, ty, corner }; + }, + + pieceAt(tx: number, ty: number, corner: Corner): PieceKind | null { + if (tx <= 2 && ty <= 2) { + // inside the island a quarter's outward neighbours exist iff they stay inside the 3×3 box + const hx = tx + (isLeft(corner) ? -1 : 1); + const vy = ty + (isTop(corner) ? -1 : 1); + const h = hx >= 0 && hx <= 2; + const v = vy >= 0 && vy <= 2; + return h && v ? "F" : h ? "H" : v ? "V" : "O"; + } + const inner = INNER[corner]; + return inner.tx === tx && inner.ty === ty ? "I" : null; + }, + + slots: SLOTS, +}; diff --git a/src/core/templates/index.ts b/src/core/templates/index.ts new file mode 100644 index 0000000..d20ecf4 --- /dev/null +++ b/src/core/templates/index.ts @@ -0,0 +1,15 @@ +import { autotiler13 } from "./autotiler13.ts"; +import { rpgmakerA2 } from "./rpgmaker-a2.ts"; +import type { Template, TemplateId } from "./types.ts"; + +export { repackTemplate } from "./repack.ts"; +export type { QuarterRef, Template, TemplateId, TemplateSlot } from "./types.ts"; +export { autotiler13, rpgmakerA2 }; + +export const TEMPLATES: readonly Template[] = [autotiler13, rpgmakerA2]; + +export function getTemplate(id: TemplateId): Template { + const template = TEMPLATES.find((t) => t.id === id); + if (!template) throw new Error(`unknown template ${id}`); + return template; +} diff --git a/src/core/templates/repack.ts b/src/core/templates/repack.ts new file mode 100644 index 0000000..ea6970a --- /dev/null +++ b/src/core/templates/repack.ts @@ -0,0 +1,39 @@ +// Converts a template image between layouts (e.g. Autotiler 13-tile → RPG Maker A2) by moving each of the 20 +// pieces to where the target template expects it. +import { CORNERS, quarterRect } from "../blob/quarters.ts"; +import { copyRect, createImage, type RgbaImage } from "../image/rgba.ts"; +import type { Template } from "./types.ts"; + +export function repackTemplate(src: RgbaImage, from: Template, to: Template, tileSize: number): RgbaImage { + const out = createImage(to.columns * tileSize, to.rows * tileSize); + const copyPiece = ( + kind: Parameters[0], + corner: (typeof CORNERS)[number], + tx: number, + ty: number, + ) => { + const ref = from.pieceSource(kind, corner); + const q = quarterRect(tileSize, corner); + copyRect( + src, + ref.tx * tileSize + q.x, + ref.ty * tileSize + q.y, + q.w, + q.h, + out, + tx * tileSize + q.x, + ty * tileSize + q.y, + ); + }; + for (let ty = 0; ty < to.rows; ty++) { + for (let tx = 0; tx < to.columns; tx++) { + for (const corner of CORNERS) { + const kind = to.pieceAt(tx, ty, corner); + if (kind) copyPiece(kind, corner, tx, ty); + } + } + } + // RPG Maker shows the A2 thumbnail tile in its palette: use the isolated tile there + if (to.id === "rpgmaker-a2") for (const corner of CORNERS) copyPiece("O", corner, 0, 0); + return out; +} diff --git a/src/core/templates/rpgmaker-a2.ts b/src/core/templates/rpgmaker-a2.ts new file mode 100644 index 0000000..bd89df3 --- /dev/null +++ b/src/core/templates/rpgmaker-a2.ts @@ -0,0 +1,74 @@ +// RPG Maker VX/VX Ace/MV/MZ "A2" floor autotile: 2×3 tiles = a 4×6 grid of quarters. Every piece exists exactly +// once, so there is nothing to choose. Map checked against RPG Maker MV's Tilemap.FLOOR_AUTOTILE_TABLE, which +// decodes to exactly the 47 canonical masks with this layout. +// qy\qx 0 1 2 3 +// 0 thumb thumb I@TL I@TR tile (0,0): palette thumbnail, never used in maps +// 1 thumb thumb I@BL I@BR tile (1,0): inner corners +// 2 O@TL H@TR H@TL O@TR tiles (0,1)-(1,2): a 2×2 island +// 3 V@BL F@BR F@BL V@BR +// 4 V@TL F@TR F@TL V@TR +// 5 O@BL H@BR H@BL O@BR +import type { Mask } from "../blob/mask.ts"; +import { type Corner, type PieceKind, pieceKind } from "../blob/quarters.ts"; +import type { QuarterRef, Template, TemplateSlot } from "./types.ts"; + +type Piece = readonly [PieceKind, Corner]; +// prettier-ignore +const GRID: ReadonlyArray> = [ + [null, null, ["I", "TL"], ["I", "TR"]], + [null, null, ["I", "BL"], ["I", "BR"]], + [["O", "TL"], ["H", "TR"], ["H", "TL"], ["O", "TR"]], + [["V", "BL"], ["F", "BR"], ["F", "BL"], ["V", "BR"]], + [["V", "TL"], ["F", "TR"], ["F", "TL"], ["V", "TR"]], + [["O", "BL"], ["H", "BR"], ["H", "BL"], ["O", "BR"]], +]; + +const cornerOf = (qx: number, qy: number): Corner => `${qy % 2 === 0 ? "T" : "B"}${qx % 2 === 0 ? "L" : "R"}` as Corner; + +const SOURCES = new Map(); +GRID.forEach((row, qy) => + row.forEach((piece, qx) => { + if (!piece) return; + const [kind, at] = piece; + const corner = cornerOf(qx, qy); + if (at !== corner) throw new Error(`A2 table error: ${kind}@${at} sits at ${corner}`); + SOURCES.set(`${kind}@${corner}`, { tx: qx >> 1, ty: qy >> 1, corner }); + }), +); + +function pieceSource(kind: PieceKind, corner: Corner): QuarterRef { + const ref = SOURCES.get(`${kind}@${corner}`); + if (!ref) throw new Error(`A2 has no ${kind}@${corner}`); + return ref; +} + +const SLOTS: TemplateSlot[] = [ + { tx: 0, ty: 0, label: "palette thumbnail (ignored)", used: false }, + { tx: 1, ty: 0, label: "inner corners", used: true }, + { tx: 0, ty: 1, label: "island top-left", used: true }, + { tx: 1, ty: 1, label: "island top-right", used: true }, + { tx: 0, ty: 2, label: "island bottom-left", used: true }, + { tx: 1, ty: 2, label: "island bottom-right", used: true }, +]; + +export const rpgmakerA2: Template = { + id: "rpgmaker-a2", + name: "RPG Maker A2 (2×3)", + description: "RPG Maker VX Ace / MV / MZ floor autotile: palette tile, inner corners and a 2×2 island.", + columns: 2, + rows: 3, + + quarterFor(mask: Mask, corner: Corner): QuarterRef { + return pieceSource(pieceKind(mask, corner), corner); + }, + + pieceSource, + + pieceAt(tx: number, ty: number, corner: Corner): PieceKind | null { + const qx = tx * 2 + (corner.endsWith("R") ? 1 : 0); + const qy = ty * 2 + (corner.startsWith("B") ? 1 : 0); + return GRID[qy]?.[qx]?.[0] ?? null; + }, + + slots: SLOTS, +}; diff --git a/src/core/templates/templates.test.ts b/src/core/templates/templates.test.ts new file mode 100644 index 0000000..8b59b07 --- /dev/null +++ b/src/core/templates/templates.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, test } from "vitest"; + +import { readFixture } from "../../../tests/helpers.ts"; +import { BLOB47 } from "../blob/mask.ts"; +import { CORNERS, PIECE_KINDS, pieceKind } from "../blob/quarters.ts"; +import { generate } from "../generate.ts"; +import { diffImages } from "../image/rgba.ts"; +import { godot12x4 } from "../layouts/index.ts"; +import { autotiler13, repackTemplate, rpgmakerA2, TEMPLATES } from "./index.ts"; + +describe.each(TEMPLATES.map((t) => [t.id, t] as const))("%s", (_, template) => { + test("every blob quarter is sampled from a quarter holding the right piece at the same corner", () => { + for (const mask of BLOB47) { + for (const corner of CORNERS) { + const ref = template.quarterFor(mask, corner); + expect(ref.corner).toBe(corner); + expect(ref.tx).toBeLessThan(template.columns); + expect(ref.ty).toBeLessThan(template.rows); + expect(template.pieceAt(ref.tx, ref.ty, ref.corner)).toBe(pieceKind(mask, corner)); + } + } + }); + + test("context-free piece sources (dual grid) hold the requested piece", () => { + for (const kind of PIECE_KINDS) { + for (const corner of CORNERS) { + const ref = template.pieceSource(kind, corner); + expect(ref.corner).toBe(corner); + expect(template.pieceAt(ref.tx, ref.ty, corner)).toBe(kind); + } + } + }); + + test("slots cover the whole grid", () => { + expect(template.slots).toHaveLength(template.columns * template.rows); + }); +}); + +describe("autotiler-13", () => { + test("1-tile strips take their edges from the matching edge tiles (v1 used the corner tiles)", () => { + // N-S strip middle (mask N|S): left/right edges of the island's middle row + expect(autotiler13.quarterFor(17, "TL")).toEqual({ tx: 0, ty: 1, corner: "TL" }); + expect(autotiler13.quarterFor(17, "BR")).toEqual({ tx: 2, ty: 1, corner: "BR" }); + // E-W strip middle (mask E|W): top/bottom edges of the island's middle column + expect(autotiler13.quarterFor(68, "TR")).toEqual({ tx: 1, ty: 0, corner: "TR" }); + expect(autotiler13.quarterFor(68, "BL")).toEqual({ tx: 1, ty: 2, corner: "BL" }); + }); + + test("island tiles are copied whole when the cell matches them", () => { + // mask E|S|SE is the island's top-left tile: all four quarters come from tile (0,0) + expect(CORNERS.map((c) => autotiler13.quarterFor(4 | 16 | 8, c))).toEqual( + CORNERS.map((corner) => ({ tx: 0, ty: 0, corner })), + ); + }); +}); + +describe("rpgmaker-a2", () => { + test("the palette thumbnail is never read", () => { + for (const corner of CORNERS) expect(rpgmakerA2.pieceAt(0, 0, corner)).toBeNull(); + }); +}); + +describe("repackTemplate", () => { + test("13-tile → A2 matches the research A2 fixture and generates the same tileset", () => { + const t13 = readFixture("inputs/holes-16.png"); + const a2 = repackTemplate(t13, autotiler13, rpgmakerA2, 16); + expect(diffImages(a2, readFixture("inputs/a2-holes-16.png")).count).toBe(0); + const fromA2 = generate(a2, { template: rpgmakerA2, tileSize: 16, layout: godot12x4 }).image; + const from13 = generate(t13, { template: autotiler13, tileSize: 16, layout: godot12x4 }).image; + expect(diffImages(fromA2, from13).count).toBe(0); + }); + + test("A2 → 13-tile → A2 round-trips", () => { + const a2 = readFixture("inputs/a2-demo-16.png"); + const back = repackTemplate(repackTemplate(a2, rpgmakerA2, autotiler13, 16), autotiler13, rpgmakerA2, 16); + expect(diffImages(back, a2).count).toBe(0); + }); +}); diff --git a/src/core/templates/types.ts b/src/core/templates/types.ts new file mode 100644 index 0000000..1bbea78 --- /dev/null +++ b/src/core/templates/types.ts @@ -0,0 +1,35 @@ +import type { Mask } from "../blob/mask.ts"; +import type { Corner, PieceKind } from "../blob/quarters.ts"; + +export type TemplateId = "autotiler-13" | "rpgmaker-a2"; + +/** A quarter of a template tile. `corner` is always the same corner the piece is used at. */ +export interface QuarterRef { + readonly tx: number; + readonly ty: number; + readonly corner: Corner; +} + +export interface TemplateSlot { + readonly tx: number; + readonly ty: number; + readonly label: string; + /** false for cells generation never reads (RPG Maker's palette thumbnail, v1's two spare cells) */ + readonly used: boolean; +} + +export interface Template { + readonly id: TemplateId; + readonly name: string; + readonly description: string; + /** size in tiles */ + readonly columns: number; + readonly rows: number; + /** Where to copy quarter `corner` of a blob cell with neighbour mask `mask` from; may use the cell's context. */ + quarterFor(mask: Mask, corner: Corner): QuarterRef; + /** Context-free source of a piece — what the dual grid uses, since a dual tile only sees a 2×2 window. */ + pieceSource(kind: PieceKind, corner: Corner): QuarterRef; + /** The piece a template quarter holds, or null when generation never reads that quarter. */ + pieceAt(tx: number, ty: number, corner: Corner): PieceKind | null; + readonly slots: readonly TemplateSlot[]; +} diff --git a/src/core/test-map.test.ts b/src/core/test-map.test.ts new file mode 100644 index 0000000..545ab16 --- /dev/null +++ b/src/core/test-map.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, test } from "vitest"; + +import { readFixture } from "../../tests/helpers.ts"; +import { BLOB47 } from "./blob/mask.ts"; +import { generate } from "./generate.ts"; +import { diffImages } from "./image/rgba.ts"; +import { dual16, godot12x4 } from "./layouts/index.ts"; +import { autotiler13 } from "./templates/index.ts"; +import { + allBlobCombinations, + allDualCombinations, + blobMaskAt, + dualCornersAt, + randomMap, + renderTestMap, +} from "./test-map.ts"; + +describe("test map", () => { + test("the combinations pattern contains all 47 blob configurations", () => { + const map = allBlobCombinations(); + const seen = new Set(); + for (let y = 0; y < map.height; y++) { + for (let x = 0; x < map.width; x++) if (map.cells[y * map.width + x]) seen.add(blobMaskAt(map, x, y)); + } + expect([...seen].toSorted((a, b) => a - b)).toEqual([...BLOB47]); + }); + + test("the dual pattern contains all 16 corner combinations", () => { + const map = allDualCombinations(); + const seen = new Set(); + for (let y = 0; y <= map.height; y++) for (let x = 0; x <= map.width; x++) seen.add(dualCornersAt(map, x, y)); + expect(seen.size).toBe(16); + }); + + test("random maps are deterministic per seed", () => { + expect(randomMap(20, 10, 7).cells).toEqual(randomMap(20, 10, 7).cells); + expect(randomMap(20, 10, 7).cells).not.toEqual(randomMap(20, 10, 8).cells); + }); + + test("a dual-16 rendering is pixel-identical to the blob-47 rendering for context-free art", () => { + const source = readFixture("inputs/holes-16.png"); + const map = randomMap(24, 16, 3); + const blob = renderTestMap(generate(source, { template: autotiler13, tileSize: 16, layout: godot12x4 }), map); + const dual = renderTestMap(generate(source, { template: autotiler13, tileSize: 16, layout: dual16 }), map); + expect(diffImages(blob, dual).count).toBe(0); + }); +}); diff --git a/src/core/test-map.ts b/src/core/test-map.ts new file mode 100644 index 0000000..fb39230 --- /dev/null +++ b/src/core/test-map.ts @@ -0,0 +1,137 @@ +// A small terrain map to try a tileset: paint cells, render them with the generated tiles exactly as Godot's +// terrain painting (blob) or a dual-grid renderer would. +import { BLOB47, maskOf, type Mask } from "./blob/mask.ts"; +import type { Tileset } from "./generate.ts"; +import { copyRect, copyRectClipped, createImage, type RgbaImage } from "./image/rgba.ts"; + +export interface TestMap { + readonly width: number; + readonly height: number; + /** row-major, 1 = terrain */ + readonly cells: Uint8Array; +} + +export function createTestMap(width: number, height: number): TestMap { + return { width, height, cells: new Uint8Array(width * height) }; +} + +export function isTerrain(map: TestMap, x: number, y: number): boolean { + return x >= 0 && y >= 0 && x < map.width && y < map.height && map.cells[y * map.width + x] === 1; +} + +export function setCell(map: TestMap, x: number, y: number, terrain: boolean): TestMap { + if (x < 0 || y < 0 || x >= map.width || y >= map.height || isTerrain(map, x, y) === terrain) return map; + const cells = map.cells.slice(); + cells[y * map.width + x] = terrain ? 1 : 0; + return { ...map, cells }; +} + +export function blobMaskAt(map: TestMap, x: number, y: number): Mask { + return maskOf((dx, dy) => isTerrain(map, x + dx, y + dy)); +} + +/** Filled corners (TL=1 TR=2 BL=4 BR=8) of the display tile whose centre is the world grid point (X, Y). */ +export function dualCornersAt(map: TestMap, X: number, Y: number): number { + return ( + (isTerrain(map, X - 1, Y - 1) ? 1 : 0) | + (isTerrain(map, X, Y - 1) ? 2 : 0) | + (isTerrain(map, X - 1, Y) ? 4 : 0) | + (isTerrain(map, X, Y) ? 8 : 0) + ); +} + +/** Renders the map with the tileset; empty cells stay transparent. */ +export function renderTestMap(tileset: Tileset, map: TestMap): RgbaImage { + const ts = tileset.tileSize; + const out = createImage(map.width * ts, map.height * ts); + if (tileset.layout.kind === "blob") { + const byMask = new Map(tileset.layout.cells.map((c) => [c.mask, c])); + for (let y = 0; y < map.height; y++) { + for (let x = 0; x < map.width; x++) { + if (!isTerrain(map, x, y)) continue; + const cell = byMask.get(blobMaskAt(map, x, y)); + if (cell) copyRect(tileset.image, cell.x * ts, cell.y * ts, ts, ts, out, x * ts, y * ts); + } + } + } else { + const byCorners = new Map(tileset.layout.cells.map((c) => [c.corners, c])); + const half = ts / 2; + for (let Y = 0; Y <= map.height; Y++) { + for (let X = 0; X <= map.width; X++) { + const corners = dualCornersAt(map, X, Y); + const cell = corners ? byCorners.get(corners) : undefined; + if (cell) copyRectClipped(tileset.image, cell.x * ts, cell.y * ts, ts, ts, out, X * ts - half, Y * ts - half); + } + } + } + return out; +} + +/** A map containing every blob configuration once: each of the 47 masks stamped as a 3×3 neighbourhood. */ +export function allBlobCombinations(columns = 8): TestMap { + const rows = Math.ceil(BLOB47.length / columns); + let map = createTestMap(columns * 4 + 1, rows * 4 + 1); + const stamp = (cx: number, cy: number, mask: Mask) => { + map = setCell(map, cx, cy, true); + for (const [bit, dx, dy] of [ + [1, 0, -1], + [2, 1, -1], + [4, 1, 0], + [8, 1, 1], + [16, 0, 1], + [32, -1, 1], + [64, -1, 0], + [128, -1, -1], + ] as const) { + if (mask & bit) map = setCell(map, cx + dx, cy + dy, true); + } + }; + BLOB47.forEach((mask, i) => stamp((i % columns) * 4 + 2, Math.floor(i / columns) * 4 + 2, mask)); + return map; +} + +/** A map containing every dual-grid corner combination: 2×2 stamps. */ +export function allDualCombinations(): TestMap { + let map = createTestMap(4 * 3 + 1, 4 * 3 + 1); + for (let corners = 0; corners < 16; corners++) { + const x = (corners % 4) * 3 + 1; + const y = Math.floor(corners / 4) * 3 + 1; + if (corners & 1) map = setCell(map, x, y, true); + if (corners & 2) map = setCell(map, x + 1, y, true); + if (corners & 4) map = setCell(map, x, y + 1, true); + if (corners & 8) map = setCell(map, x + 1, y + 1, true); + } + return map; +} + +/** Blobby random terrain (seeded noise smoothed by a few cellular-automaton passes). */ +export function randomMap(width: number, height: number, seed = 1, density = 0.5): TestMap { + let state = seed >>> 0 || 1; + const rand = () => { + // mulberry32 + state = (state + 0x6d2b79f5) >>> 0; + let t = state; + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; + let cells = new Uint8Array(width * height).map(() => (rand() < density ? 1 : 0)); + for (let pass = 0; pass < 3; pass++) { + const next = new Uint8Array(cells.length); + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + let n = 0; + for (let dy = -1; dy <= 1; dy++) { + for (let dx = -1; dx <= 1; dx++) { + const xx = x + dx; + const yy = y + dy; + if (xx >= 0 && yy >= 0 && xx < width && yy < height) n += cells[yy * width + xx]; + } + } + next[y * width + x] = n >= 5 ? 1 : 0; + } + } + cells = next; + } + return { width, height, cells }; +} diff --git a/styles.css b/styles.css deleted file mode 100644 index cb69b56..0000000 --- a/styles.css +++ /dev/null @@ -1,217 +0,0 @@ -* { - position: relative; - box-sizing: border-box; -} - -html { - font-family: ".AppleSystemUIFont", Avenir, "Helvetica Neue", sans-serif; - font-size: 62.5%; - background: #333B4F; -} - -html, body { - margin: 0; - padding: 0; - color: #CCCED3; - overflow: hidden; -} - -body { - /*border: 1px solid #191D27;*/ - /*background: #333B4F;*/ - padding: 20px; - user-select: none; -} - -main { - padding: 0 10px; -} - -h1, h2 { - margin-top: 0; - padding: 0; -} - -header, -footer { - padding: 20px 10px; -} - -header { - padding-top: 10px; -} - -header p { - margin-top: 40px; -} - -hr { - border-color: #191D27; - margin: 15px 0; -} - -footer { - padding: 20px 0; - font-size: 1.3rem; - text-align: center; -} - -footer i { - font-style: normal; - margin: 0 10px; -} - -a, a:hover, a:visited { - color: #699ce8; -} - -body { - padding: 2rem; - font-size: 1.6rem; -} - -h1 img, -h2 img { - height: 32px; - vertical-align: bottom; - margin-right: 4px; -} - -figure { - display: inline-block; - margin-left: 0; - margin-right: 0; - vertical-align: top; - font-weight: bold; -} - -.big-arrow { - width: 140px; - display: block; - top: 25%; -} - -#uploader-input { - cursor: pointer; -} - -img, canvas { - image-rendering: -moz-crisp-edges; - image-rendering: -webkit-crisp-edges; - image-rendering: pixelated; - image-rendering: crisp-edges; -} - -#composer-pic, -#uploader-pic { - display: block; -} - -.checkerboard-bg { - background-image: linear-gradient(45deg, rgba(0, 0, 0, 0.1) 25%, transparent 25%), linear-gradient(-45deg, rgba(0, 0, 0, 0.1) 25%, transparent 25%), - linear-gradient(45deg, transparent 75%, rgba(0, 0, 0, 0.1) 75%), linear-gradient(-45deg, transparent 75%, rgba(0, 0, 0, 0.1) 75%); - background-size: 64px 64px; - background-position: 0 0, 0 32px, 32px -32px, -32px 0px; -} - -.with-grid:after { - content: ""; - position: absolute; - top: 0; - left: 0; - z-index: 1; - width: 100%; - height: 100%; - background-color: transparent; - background-image: linear-gradient(0deg, transparent 24%, rgba(255, 0, 255, 0.3) 25%, rgba(255, 0, 255, 0.3) 26%, - transparent 27%, transparent 74%, rgba(255, 0, 255, 0.3) 75%, rgba(255, 0, 255, 0.3) 76%, transparent 77%, transparent), - linear-gradient(90deg, transparent 24%, rgba(255, 0, 255, 0.3) 25%, rgba(255, 0, 255, 0.3) 26%, transparent 27%, - transparent 74%, rgba(255, 0, 255, 0.3) 75%, rgba(255, 0, 255, 0.3) 76%, transparent 77%, transparent); - background-size: 128px 128px; - background-position: center; -} - -.flex { - display: flex; - align-items: stretch; - align-content: center; -} - -.flex > * { - /*background: #699ce8;*/ - vertical-align: middle; -} - -.flex > figure { - background: #4f5667; - /*border: 1px dashed #CCCED3;*/ - padding: 2rem; - /*border-radius: 1rem;*/ - box-shadow: 5px 5px 0 #191D27; -} - -figure picture { - display: inline-block; - margin: 30px 0; - border: 1px inset #000; - min-height: 160px; - overflow: hidden; -} - -figure picture img, -figure picture canvas { - margin: 0; - display: inline-block; -} - -#uploader-img { - min-height: 96px; - max-width: 240px; -} - -#uploader-pic { - cursor: pointer; -} - -#composer-img-canvas { - width: 352px; -} - -input, button { - background: #262C3B; - color: #CCCED3; - padding: 6px 9px; - border: 1px solid #111; - min-height: 32px; - text-align: center; - vertical-align: center; -} - -input { - user-select: auto; -} - -input:focus, button:focus { - border-color: #CCCED3; - outline: none; -} - -input:active, button:active { - border-color: #fcc751; -} - -button:disabled, button:active:disabled { - color: #4f5667; - border: 1px solid #111; -} - -button { - cursor: pointer; -} - -input[type=file] { - display: block; - width: 100%; - margin-right: 10px; - padding: 5px 9px; -} diff --git a/tests/e2e/app.spec.ts b/tests/e2e/app.spec.ts new file mode 100644 index 0000000..a879265 --- /dev/null +++ b/tests/e2e/app.spec.ts @@ -0,0 +1,71 @@ +// Smoke tests of the web build: the UI wires the core correctly and exports the exact golden pixels. +import { readFileSync } from "node:fs"; + +import { expect, test, type Page } from "@playwright/test"; +import { unzipSync } from "fflate"; + +import { decodePng, diffImages } from "../../src/core/index.ts"; + +const golden = decodePng(readFileSync(new URL("../fixtures/expected/demo-16.godot-12x4.png", import.meta.url))); + +async function openExample(page: Page) { + await page.goto("/"); + await page.getByRole("button", { name: /Beveled block, 16 px Autotiler/ }).click(); + await expect(page.getByRole("img", { name: /Generated tileset, 47 tiles/ })).toBeVisible(); + await expect(page.getByText("80×48 px · 5×3 tiles of 16 px")).toBeVisible(); +} + +async function download(page: Page, button: RegExp): Promise { + const pending = page.waitForEvent("download"); + await page.getByRole("button", { name: button }).click(); + const file = await pending; + const path = await file.path(); + return readFileSync(path); +} + +test("an example exports the golden PNG", async ({ page }) => { + await openExample(page); + await page.getByRole("button", { name: "PNG", exact: true }).click(); + const png = await download(page, /^Download$/); + const diff = diffImages(decodePng(new Uint8Array(png)), golden); + expect(diff.sameSize).toBe(true); + expect(diff.count).toBe(0); +}); + +test("Godot 4 export downloads a zip with the PNG and a terrain TileSet", async ({ page }) => { + await openExample(page); + const zip = unzipSync(new Uint8Array(await download(page, /^Download$/))); + expect(Object.keys(zip).toSorted()).toEqual(["autotiler13-16px-autotile.png", "autotiler13-16px-autotile.tres"]); + const tres = new TextDecoder().decode(zip["autotiler13-16px-autotile.tres"]); + expect(tres).toContain('path="autotiler13-16px-autotile.png"'); + expect(tres).toContain("terrain_set_0/mode = 0"); + expect(tres.match(/\/terrain = 0$/gm)).toHaveLength(47); + expect(diffImages(decodePng(zip["autotiler13-16px-autotile.png"]), golden).count).toBe(0); +}); + +test("the dual grid only offers formats that support it, and the test map paints", async ({ page }) => { + await openExample(page); + await page.getByRole("combobox", { name: "Layout" }).click(); + await page.getByRole("option", { name: /Dual grid/ }).click(); + await expect(page.getByRole("button", { name: "Godot 4" })).toBeDisabled(); + await expect(page.getByRole("button", { name: "PNG", exact: true })).toHaveAttribute("aria-pressed", "true"); + await page.getByRole("tab", { name: "Test map" }).click(); + await page.getByRole("button", { name: "Clear" }).click(); + const map = page.getByRole("img", { name: /Test map/ }); + const box = await map.boundingBox(); + if (!box) throw new Error("no test map"); + await page.mouse.move(box.x + box.width * 0.3, box.y + box.height * 0.5); + await page.mouse.down(); + await page.mouse.move(box.x + box.width * 0.6, box.y + box.height * 0.5, { steps: 8 }); + await page.mouse.up(); + // the map is persisted (debounced), so poll the saved state + await expect + .poll(() => + page.evaluate(() => { + const raw = localStorage.getItem("autotiler:v2"); + const cells: unknown = raw ? JSON.parse(raw).map?.cells : ""; + return typeof cells === "string" ? cells.split("").filter((c) => c === "1").length : 0; + }), + ) + .toBeGreaterThan(3); +}); diff --git a/tests/fixtures/expected/a2-demo-16.autotiler-v1.png b/tests/fixtures/expected/a2-demo-16.autotiler-v1.png new file mode 100644 index 0000000..15cd23e Binary files /dev/null and b/tests/fixtures/expected/a2-demo-16.autotiler-v1.png differ diff --git a/tests/fixtures/expected/a2-demo-16.gamemaker-47.png b/tests/fixtures/expected/a2-demo-16.gamemaker-47.png new file mode 100644 index 0000000..2630343 Binary files /dev/null and b/tests/fixtures/expected/a2-demo-16.gamemaker-47.png differ diff --git a/tests/fixtures/expected/a2-demo-16.godot-12x4.png b/tests/fixtures/expected/a2-demo-16.godot-12x4.png new file mode 100644 index 0000000..ffb9b88 Binary files /dev/null and b/tests/fixtures/expected/a2-demo-16.godot-12x4.png differ diff --git a/tests/fixtures/expected/a2-holes-16.autotiler-v1.png b/tests/fixtures/expected/a2-holes-16.autotiler-v1.png new file mode 100644 index 0000000..7c3c265 Binary files /dev/null and b/tests/fixtures/expected/a2-holes-16.autotiler-v1.png differ diff --git a/tests/fixtures/expected/a2-holes-16.godot-12x4.png b/tests/fixtures/expected/a2-holes-16.godot-12x4.png new file mode 100644 index 0000000..94275a9 Binary files /dev/null and b/tests/fixtures/expected/a2-holes-16.godot-12x4.png differ diff --git a/tests/fixtures/expected/demo-16.autotiler-v1.png b/tests/fixtures/expected/demo-16.autotiler-v1.png new file mode 100644 index 0000000..15cd23e Binary files /dev/null and b/tests/fixtures/expected/demo-16.autotiler-v1.png differ diff --git a/tests/fixtures/expected/demo-16.dual-16.png b/tests/fixtures/expected/demo-16.dual-16.png new file mode 100644 index 0000000..58a7449 Binary files /dev/null and b/tests/fixtures/expected/demo-16.dual-16.png differ diff --git a/tests/fixtures/expected/demo-16.gamemaker-47.png b/tests/fixtures/expected/demo-16.gamemaker-47.png new file mode 100644 index 0000000..2630343 Binary files /dev/null and b/tests/fixtures/expected/demo-16.gamemaker-47.png differ diff --git a/tests/fixtures/expected/demo-16.godot-12x4.png b/tests/fixtures/expected/demo-16.godot-12x4.png new file mode 100644 index 0000000..ffb9b88 Binary files /dev/null and b/tests/fixtures/expected/demo-16.godot-12x4.png differ diff --git a/tests/fixtures/expected/demo-24.autotiler-v1.png b/tests/fixtures/expected/demo-24.autotiler-v1.png new file mode 100644 index 0000000..7f5f6c7 Binary files /dev/null and b/tests/fixtures/expected/demo-24.autotiler-v1.png differ diff --git a/tests/fixtures/expected/demo-24.godot-12x4.png b/tests/fixtures/expected/demo-24.godot-12x4.png new file mode 100644 index 0000000..683ceb7 Binary files /dev/null and b/tests/fixtures/expected/demo-24.godot-12x4.png differ diff --git a/tests/fixtures/expected/demo-32.autotiler-v1.png b/tests/fixtures/expected/demo-32.autotiler-v1.png new file mode 100644 index 0000000..67182e0 Binary files /dev/null and b/tests/fixtures/expected/demo-32.autotiler-v1.png differ diff --git a/tests/fixtures/expected/demo-32.dual-16.png b/tests/fixtures/expected/demo-32.dual-16.png new file mode 100644 index 0000000..b8e10c5 Binary files /dev/null and b/tests/fixtures/expected/demo-32.dual-16.png differ diff --git a/tests/fixtures/expected/demo-32.godot-12x4.png b/tests/fixtures/expected/demo-32.godot-12x4.png new file mode 100644 index 0000000..a4f8035 Binary files /dev/null and b/tests/fixtures/expected/demo-32.godot-12x4.png differ diff --git a/tests/fixtures/expected/demo-8.autotiler-v1.png b/tests/fixtures/expected/demo-8.autotiler-v1.png new file mode 100644 index 0000000..ed99120 Binary files /dev/null and b/tests/fixtures/expected/demo-8.autotiler-v1.png differ diff --git a/tests/fixtures/expected/demo-8.dual-16.png b/tests/fixtures/expected/demo-8.dual-16.png new file mode 100644 index 0000000..61e9119 Binary files /dev/null and b/tests/fixtures/expected/demo-8.dual-16.png differ diff --git a/tests/fixtures/expected/demo-8.godot-12x4.png b/tests/fixtures/expected/demo-8.godot-12x4.png new file mode 100644 index 0000000..0428f8c Binary files /dev/null and b/tests/fixtures/expected/demo-8.godot-12x4.png differ diff --git a/tests/fixtures/expected/holes-16.autotiler-v1.png b/tests/fixtures/expected/holes-16.autotiler-v1.png new file mode 100644 index 0000000..7c3c265 Binary files /dev/null and b/tests/fixtures/expected/holes-16.autotiler-v1.png differ diff --git a/tests/fixtures/expected/holes-16.dual-16.png b/tests/fixtures/expected/holes-16.dual-16.png new file mode 100644 index 0000000..c4f618b Binary files /dev/null and b/tests/fixtures/expected/holes-16.dual-16.png differ diff --git a/tests/fixtures/expected/holes-16.godot-12x4.png b/tests/fixtures/expected/holes-16.godot-12x4.png new file mode 100644 index 0000000..94275a9 Binary files /dev/null and b/tests/fixtures/expected/holes-16.godot-12x4.png differ diff --git a/tests/fixtures/expected/labels-16.autotiler-v1.png b/tests/fixtures/expected/labels-16.autotiler-v1.png new file mode 100644 index 0000000..989c4b6 Binary files /dev/null and b/tests/fixtures/expected/labels-16.autotiler-v1.png differ diff --git a/tests/fixtures/expected/labels-16.godot-12x4.png b/tests/fixtures/expected/labels-16.godot-12x4.png new file mode 100644 index 0000000..5432569 Binary files /dev/null and b/tests/fixtures/expected/labels-16.godot-12x4.png differ diff --git a/tests/fixtures/inputs/a2-demo-16.png b/tests/fixtures/inputs/a2-demo-16.png new file mode 100644 index 0000000..a6e337e Binary files /dev/null and b/tests/fixtures/inputs/a2-demo-16.png differ diff --git a/tests/fixtures/inputs/a2-holes-16.png b/tests/fixtures/inputs/a2-holes-16.png new file mode 100644 index 0000000..f741b14 Binary files /dev/null and b/tests/fixtures/inputs/a2-holes-16.png differ diff --git a/tests/fixtures/inputs/demo-16.png b/tests/fixtures/inputs/demo-16.png new file mode 100644 index 0000000..ede723b Binary files /dev/null and b/tests/fixtures/inputs/demo-16.png differ diff --git a/tests/fixtures/inputs/demo-24.png b/tests/fixtures/inputs/demo-24.png new file mode 100644 index 0000000..a2b9422 Binary files /dev/null and b/tests/fixtures/inputs/demo-24.png differ diff --git a/tests/fixtures/inputs/demo-32.png b/tests/fixtures/inputs/demo-32.png new file mode 100644 index 0000000..14cd440 Binary files /dev/null and b/tests/fixtures/inputs/demo-32.png differ diff --git a/tests/fixtures/inputs/demo-8.png b/tests/fixtures/inputs/demo-8.png new file mode 100644 index 0000000..b374b60 Binary files /dev/null and b/tests/fixtures/inputs/demo-8.png differ diff --git a/tests/fixtures/inputs/holes-16.png b/tests/fixtures/inputs/holes-16.png new file mode 100644 index 0000000..276724f Binary files /dev/null and b/tests/fixtures/inputs/holes-16.png differ diff --git a/tests/fixtures/inputs/labels-16.png b/tests/fixtures/inputs/labels-16.png new file mode 100644 index 0000000..d3ed5cd Binary files /dev/null and b/tests/fixtures/inputs/labels-16.png differ diff --git a/tests/godot/godot.test.ts b/tests/godot/godot.test.ts new file mode 100644 index 0000000..e531eb9 --- /dev/null +++ b/tests/godot/godot.test.ts @@ -0,0 +1,159 @@ +// End-to-end check against a real Godot 4 (headless): export TileSets, let Godot import them, paint a mask with +// TileMapLayer.set_cells_terrain_connect and compare every chosen tile with the tile whose neighbour mask matches. +// Skipped unless a Godot 4 binary is found (GODOT_BIN, /Applications/Godot.app, or `godot`/`godot4` on PATH). +import { execFileSync } from "node:child_process"; +import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { delimiter, join } from "node:path"; + +import { afterAll, beforeAll, describe, expect, test } from "vitest"; + +import { exportGodot4, generate, getLayout, getTemplate, type LayoutId, maskOf } from "../../src/core/index.ts"; +import { readFixture } from "../helpers.ts"; + +function findGodot4(): string | null { + const candidates = [ + process.env.GODOT_BIN, + "/Applications/Godot.app/Contents/MacOS/Godot", + ...(process.env.PATH ?? "").split(delimiter).flatMap((d) => [join(d, "godot"), join(d, "godot4")]), + ].filter((c): c is string => !!c && existsSync(c)); + for (const bin of candidates) { + try { + if (execFileSync(bin, ["--headless", "--version"], { encoding: "utf8" }).trim().startsWith("4.")) return bin; + } catch { + // not a runnable Godot 4 + } + } + return null; +} + +// islands, strips, rings, holes, T-junctions, diagonal-only contacts and a checkerboard (141 terrain cells) +const MASK = [ + "....................", + ".##.###########.....", + ".##.#.........#.#.#.", + "....#.#####.#.#.....", + ".#..#.#...#.#.#.#.#.", + "....#.#.#.#.#.#.#...", + ".##.#.#...#...#.#.#.", + ".#..#.#####.###.....", + "....#.......#.##.##.", + ".#.##########.#..##.", + "..#.#.......#.......", + ".#..#.#.#.#.#.###.#.", + "....###.#.###.#.#.#.", + "..........#...###.#.", + "..#####...........#.", + "..#####..#.#.#......", + "..##.##...#.#.......", + "..#####..#.#.#......", + "..#####.............", + "....................", +]; + +const CASES: Array<{ + id: string; + input: string; + tileSize: number; + layout: LayoutId; + collision: boolean; + forceNearest: boolean; +}> = [ + { id: "godot12x4_16", input: "demo-16", tileSize: 16, layout: "godot-12x4", collision: true, forceNearest: false }, + { id: "v1_8", input: "demo-8", tileSize: 8, layout: "autotiler-v1", collision: true, forceNearest: false }, + { id: "gamemaker_32", input: "demo-32", tileSize: 32, layout: "gamemaker-47", collision: true, forceNearest: false }, + { + id: "nearest_nocollision", + input: "holes-16", + tileSize: 16, + layout: "godot-12x4", + collision: false, + forceNearest: true, + }, +]; + +const terrain = (x: number, y: number) => MASK[y]?.[x] === "#"; + +interface GodotResult { + info: { tiles: number; tile_size: number[]; physics_layers: number; terrain_mode: number }; + chosen: Record; + extra_cells: unknown[]; +} + +const godot = findGodot4(); + +describe.skipIf(!godot)("Godot 4 terrain painting", () => { + let project = ""; + let home = ""; + const run = (args: string[]) => + execFileSync(godot!, ["--headless", "--path", project, ...args], { + encoding: "utf8", + env: { ...process.env, HOME: home }, // keep Godot's editor settings/caches out of the user's home + stdio: ["ignore", "pipe", "pipe"], + }); + + beforeAll(() => { + project = mkdtempSync(join(tmpdir(), "autotiler-godot-")); + home = join(project, ".home"); + mkdirSync(home); + cpSync(new URL("./project/", import.meta.url), project, { recursive: true }); + writeFileSync(join(project, "mask.txt"), `${MASK.join("\n")}\n`); + for (const c of CASES) { + const tileset = generate(readFixture(`inputs/${c.input}.png`), { + template: getTemplate("autotiler-13"), + tileSize: c.tileSize, + layout: getLayout(c.layout), + }); + const dir = join(project, "terrain", c.id); + mkdirSync(dir, { recursive: true }); + const files = exportGodot4(tileset, { + name: c.id, + terrainName: c.id, + terrainColor: [90, 154, 60], + collision: c.collision, + forceNearest: c.forceNearest, + }); + for (const f of files) writeFileSync(join(dir, f.path), f.data); + } + mkdirSync(join(project, "out")); + run(["--import"]); + }, 180_000); + + afterAll(() => { + if (project) rmSync(project, { recursive: true, force: true }); + }); + + test.each(CASES)( + "$id: every painted cell gets the tile of its neighbour mask", + (c) => { + const log = run([ + "--script", + "res://validate.gd", + "--", + `res://terrain/${c.id}/${c.id}.tres`, + "res://mask.txt", + `res://out/${c.id}.json`, + ]); + expect(log).not.toMatch(/ERROR|SCRIPT ERROR/); + const result: GodotResult = JSON.parse(readFileSync(join(project, "out", `${c.id}.json`), "utf8")); + expect(result.info).toMatchObject({ + tiles: 47, + tile_size: [c.tileSize, c.tileSize], + terrain_mode: 0, + physics_layers: c.collision ? 1 : 0, + }); + + const layout = getLayout(c.layout); + if (layout.kind !== "blob") throw new Error("blob layouts only"); + const at = new Map(layout.cells.map((cell) => [cell.mask, `${cell.x},${cell.y}`])); + const mismatches = Object.entries(result.chosen).filter(([key, got]) => { + const [x, y] = key.split(",").map(Number); + return at.get(maskOf((dx, dy) => terrain(x + dx, y + dy))) !== got.join(","); + }); + expect(Object.keys(result.chosen)).toHaveLength(141); + expect(mismatches).toEqual([]); + expect(result.extra_cells).toEqual([]); + }, + 60_000, + ); +}); diff --git a/tests/godot/project/project.godot b/tests/godot/project/project.godot new file mode 100644 index 0000000..517b06e --- /dev/null +++ b/tests/godot/project/project.godot @@ -0,0 +1,10 @@ +; Throwaway project used by tests/godot/godot.test.ts (copied to a temp dir per run). +config_version=5 + +[application] + +config/name="autotiler-godot-check" + +[rendering] + +textures/canvas_textures/default_texture_filter=0 diff --git a/tests/godot/project/validate.gd b/tests/godot/project/validate.gd new file mode 100644 index 0000000..3d4662a --- /dev/null +++ b/tests/godot/project/validate.gd @@ -0,0 +1,64 @@ +# Loads an exported TileSet, paints a mask with terrain auto-connect and dumps what Godot chose. +# godot --headless --path . --script res://validate.gd -- +extends SceneTree + +func _init() -> void: + var args := OS.get_cmdline_user_args() + var tres_path: String = args[0] + var mask_path: String = args[1] + var out_path: String = args[2] + + var ts: TileSet = load(tres_path) + if ts == null: + push_error("could not load " + tres_path) + quit(2) + return + var src := ts.get_source(ts.get_source_id(0)) as TileSetAtlasSource + var info := { + "tile_size": [ts.tile_size.x, ts.tile_size.y], + "sources": ts.get_source_count(), + "terrain_sets": ts.get_terrain_sets_count(), + "terrain_mode": ts.get_terrain_set_mode(0), + "terrain_name": ts.get_terrain_name(0, 0), + "physics_layers": ts.get_physics_layers_count(), + "texture": src.texture.resource_path if src.texture else "", + "texture_size": [src.texture.get_width(), src.texture.get_height()] if src.texture else [], + "region_size": [src.texture_region_size.x, src.texture_region_size.y], + "tiles": src.get_tiles_count(), + } + # collision polygon of the first tile, to check the coordinate convention survived the round trip + var first := src.get_tile_id(0) + var td := src.get_tile_data(first, 0) + info["first_tile"] = [first.x, first.y] + info["first_tile_polygons"] = td.get_collision_polygons_count(0) if ts.get_physics_layers_count() > 0 else 0 + if ts.get_physics_layers_count() > 0 and td.get_collision_polygons_count(0) > 0: + info["first_tile_polygon"] = Array(td.get_collision_polygon_points(0, 0)) + + var text := FileAccess.get_file_as_string(mask_path) + var cells: Array[Vector2i] = [] + var lines := text.strip_edges().split("\n") + for y in lines.size(): + for x in lines[y].length(): + if lines[y][x] == "#": + cells.append(Vector2i(x, y)) + + var layer := TileMapLayer.new() + layer.tile_set = ts + root.add_child(layer) + layer.set_cells_terrain_connect(cells, 0, 0) + + var chosen := {} + for c in cells: + var ac := layer.get_cell_atlas_coords(c) + chosen["%d,%d" % [c.x, c.y]] = [ac.x, ac.y] + # any tiles placed outside the mask? + var extra := [] + for c in layer.get_used_cells(): + if not cells.has(c): + extra.append([c.x, c.y]) + + var f := FileAccess.open(out_path, FileAccess.WRITE) + f.store_string(JSON.stringify({"info": info, "chosen": chosen, "extra_cells": extra}, " ")) + f.close() + print("OK tiles=%d cells=%d" % [info["tiles"], cells.size()]) + quit(0) diff --git a/tests/golden.test.ts b/tests/golden.test.ts new file mode 100644 index 0000000..dfda386 --- /dev/null +++ b/tests/golden.test.ts @@ -0,0 +1,42 @@ +// Golden images: every fixture input × layout must reproduce tests/fixtures/expected/..png exactly. +// The expected images come from the research baseline (v1's drawing code run verbatim in Chromium, then fixed for +// v1's transparency, colour and strip bugs). On failure, .actual.png and .diff.png are written next to it. +// To accept an intentional change: pnpm golden:update +import { existsSync, readdirSync } from "node:fs"; + +import { describe, expect, test } from "vitest"; + +import { diffImages, generate, getLayout, getTemplate, isLayoutId } from "../src/core/index.ts"; +import { fixturesDir, parseInputName, readFixture, writeFixture } from "./helpers.ts"; + +const cases = readdirSync(`${fixturesDir}expected`) + .filter((f) => f.endsWith(".png") && !f.endsWith(".actual.png") && !f.endsWith(".diff.png")) + .map((f) => { + const [input, layoutId] = f.replace(/\.png$/, "").split("."); + if (!isLayoutId(layoutId)) throw new Error(`unknown layout in fixture name ${f}`); + return { file: f, input, layoutId }; + }); + +describe("golden images", () => { + test("covers all research fixtures", () => { + expect(cases.length).toBeGreaterThanOrEqual(20); + }); + + test.each(cases)("$input → $layoutId", ({ file, input, layoutId }) => { + expect(existsSync(`${fixturesDir}inputs/${input}.png`)).toBe(true); + const { templateId, tileSize } = parseInputName(`${input}.png`); + const actual = generate(readFixture(`inputs/${input}.png`), { + template: getTemplate(templateId), + tileSize, + layout: getLayout(layoutId), + }).image; + const expected = readFixture(`expected/${file}`); + const diff = diffImages(actual, expected); + if (!diff.sameSize || diff.count > 0) { + writeFixture(`expected/${file.replace(/\.png$/, ".actual.png")}`, actual); + if (diff.sameSize) writeFixture(`expected/${file.replace(/\.png$/, ".diff.png")}`, diff.image); + } + expect([actual.width, actual.height]).toEqual([expected.width, expected.height]); + expect(diff.count).toBe(0); + }); +}); diff --git a/tests/helpers.ts b/tests/helpers.ts new file mode 100644 index 0000000..1e02b2b --- /dev/null +++ b/tests/helpers.ts @@ -0,0 +1,28 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; + +import { decodePng, encodePng, type RgbaImage } from "../src/core/index.ts"; + +export const fixturesDir = fileURLToPath(new URL("./fixtures/", import.meta.url)); + +export function readFixture(path: string): RgbaImage { + return decodePng(readFileSync(`${fixturesDir}${path}`)); +} + +export function writeFixture(path: string, img: RgbaImage): void { + writeFileSync(`${fixturesDir}${path}`, encodePng(img)); +} + +/** Fixture inputs are named `-.png`; `a2-` prefix = RPG Maker A2 template. */ +export function parseInputName(file: string): { + name: string; + templateId: "autotiler-13" | "rpgmaker-a2"; + tileSize: number; +} { + const name = file.replace(/\.png$/, ""); + return { + name, + templateId: name.startsWith("a2-") ? "rpgmaker-a2" : "autotiler-13", + tileSize: Number(name.split("-").pop()), + }; +} diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..b0b730f --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/app.tsbuildinfo", + "lib": ["ES2023", "DOM"], + "types": ["vite/client"], + "jsx": "react-jsx" + }, + "include": ["src"], + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "**/__snapshots__/**"] +} diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..60ff5c1 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es2023", + "lib": ["ES2023"], + "module": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "types": [], + "strict": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "erasableSyntaxOnly": true, + "skipLibCheck": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + } +} diff --git a/tsconfig.core.json b/tsconfig.core.json new file mode 100644 index 0000000..aafe4ff --- /dev/null +++ b/tsconfig.core.json @@ -0,0 +1,12 @@ +{ + // Portability guard: the core must run unchanged in the browser, the Tauri webview, Node (tests) and a future + // CLI. No DOM lib and no @types/node here, and nodenext forces explicit .ts import extensions. + "extends": "./tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/core.tsbuildinfo", + "module": "nodenext", + "moduleResolution": "nodenext" + }, + "include": ["src/core"], + "exclude": ["src/core/**/*.test.ts", "**/__snapshots__/**"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b3fac50 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,8 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.core.json" }, + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..8611212 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + // Config files, scripts and all tests (unit, golden, Godot, e2e). + "extends": "./tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/node.tsbuildinfo", + "lib": ["ES2023", "DOM"], + "types": ["node"] + }, + "include": ["*.config.ts", "scripts", "tests", "src/**/*.test.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..a8af879 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,24 @@ +import tailwindcss from "@tailwindcss/vite"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +// Set by `tauri dev` when developing on a device (mobile); unused on desktop. +const host = process.env.TAURI_DEV_HOST; + +export default defineConfig({ + plugins: [react(), tailwindcss()], + // Relative asset URLs: the same build works inside Tauri, on GitHub Pages sub-paths and on itch.io. + base: "./", + clearScreen: false, + server: { + port: 5173, + strictPort: true, + host: host || false, + hmr: host ? { protocol: "ws", host, port: 1421 } : undefined, + watch: { ignored: ["**/src-tauri/**"] }, + }, + envPrefix: ["VITE_", "TAURI_ENV_*"], + // one screen, one chunk: ~540 kB minified / ~170 kB gzipped (React + Base UI + the core) is fine for a desktop + // app and a static site, so don't warn about it + build: { chunkSizeWarningLimit: 700 }, +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..0c7471e --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/**/*.test.ts", "tests/**/*.test.ts"], + environment: "node", + }, +});