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.
+
+
+**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)
-
-
## 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) |
+| ------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
+|  |  |
+| 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 = ('' +
- // '