From 22f7b3c4a9656a1be112c214709dfac1c44656bd Mon Sep 17 00:00:00 2001 From: Oli Passey Date: Sat, 21 Feb 2026 17:33:52 +0000 Subject: [PATCH] storage --- README.md | 3 ++ app.js | 117 ++++++++++++++++++++++++++++++++++++++++++++++++----- index.html | 5 +++ styles.css | 7 ++++ 4 files changed, 123 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 009a34d..74723da 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ A lightweight, browser-based tool to build a tile library and paint reusable pat - Support rectangular tiles (multi-cell) - Pin a floor tile row at the bottom - Fill any row with a chosen tile +- Auto-save tile library in the browser +- Import/export the tile library as JSON - Export JSON for handing off to a tiler - Export PNG snapshots - Import JSON to continue a saved pattern @@ -57,4 +59,5 @@ Open `index.html` directly in your browser, or use the optional local server scr - Shift-click or right click to erase a tile. - Enable “Floor row” to lock the bottom row to a chosen tile. - Use “Fill row” to quickly paint a full row with a tile. +- Tile library changes auto-save to your browser storage. - If you use image tiles, keep images square for best results. diff --git a/app.js b/app.js index b0670ec..ef3aef3 100644 --- a/app.js +++ b/app.js @@ -7,6 +7,9 @@ const tileImageInput = document.getElementById("tile-image"); const tileList = document.getElementById("tile-list"); const activeTileName = document.getElementById("active-tile-name"); const activeTileSwatch = document.getElementById("active-tile-swatch"); +const exportLibraryButton = document.getElementById("export-library"); +const importLibraryButton = document.getElementById("import-library"); +const importLibraryFile = document.getElementById("import-library-file"); const gridRowsInput = document.getElementById("grid-rows"); const gridColsInput = document.getElementById("grid-cols"); @@ -36,6 +39,7 @@ let floorEnabled = false; let floorTileId = null; const placements = new Map(); const imageCache = new Map(); +const STORAGE_KEY = "tile-library-v1"; const config = { rows: Number(gridRowsInput.value), @@ -59,6 +63,16 @@ const createPlacementId = () => ? crypto.randomUUID() : `placement-${Date.now()}-${Math.random().toString(16).slice(2)}`; +const normalizeTiles = (items) => + items.map((tile) => ({ + id: tile.id || createId(), + name: tile.name || "Untitled", + color: tile.color || "#999999", + width: clampNumber(Number(tile.width ?? 1), 1, 12), + height: clampNumber(Number(tile.height ?? 1), 1, 12), + imageData: tile.imageData || null, + })); + const applyFloorRow = () => { if (!floorEnabled || !floorTileId) { return; @@ -194,7 +208,13 @@ const renderTiles = () => { deleteButton.addEventListener("click", () => { tiles = tiles.filter((item) => item.id !== tile.id); imageCache.delete(tile.id); - grid = grid.map((row) => row.map((cell) => (cell === tile.id ? null : cell))); + const placementsToRemove = []; + placements.forEach((placement, placementId) => { + if (placement.tileId === tile.id) { + placementsToRemove.push(placementId); + } + }); + placementsToRemove.forEach((placementId) => clearPlacement(placementId)); if (floorTileId === tile.id) { floorTileId = null; } @@ -204,6 +224,7 @@ const renderTiles = () => { renderTiles(); renderCanvas(); syncPatternPreview(); + saveLibraryToStorage(); }); actions.append(selectButton, deleteButton); @@ -268,6 +289,55 @@ const drawGridLines = () => { } }; +const saveLibraryToStorage = () => { + const payload = { + version: 1, + tiles, + }; + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(payload)); + } catch (error) { + } +}; + +const loadLibraryFromPayload = (payload) => { + if (!payload || !Array.isArray(payload.tiles)) { + return false; + } + tiles = normalizeTiles(payload.tiles); + imageCache.clear(); + tiles.forEach((tile) => buildTileImage(tile)); + const validIds = new Set(tiles.map((tile) => tile.id)); + const placementsToRemove = []; + placements.forEach((placement, placementId) => { + if (!validIds.has(placement.tileId)) { + placementsToRemove.push(placementId); + } + }); + placementsToRemove.forEach((placementId) => clearPlacement(placementId)); + if (floorTileId && !validIds.has(floorTileId)) { + floorTileId = null; + } + setActiveTile(tiles[0]?.id ?? null); + renderTiles(); + renderFloorOptions(); + renderCanvas(); + syncPatternPreview(); + return true; +}; + +const loadLibraryFromStorage = () => { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) { + return; + } + const payload = JSON.parse(raw); + loadLibraryFromPayload(payload); + } catch (error) { + } +}; + const renderCanvas = () => { resizeCanvas(); drawGridBackground(); @@ -522,6 +592,7 @@ tileForm.addEventListener("submit", async (event) => { renderTiles(); renderCanvas(); syncPatternPreview(); + saveLibraryToStorage(); }); const buildPattern = () => ({ @@ -674,14 +745,7 @@ importJsonButton.addEventListener("click", () => { floorEnabledInput.checked = floorEnabled; fillRowInput.value = "1"; - tiles = payload.tiles.map((tile) => ({ - id: tile.id || createId(), - name: tile.name || "Untitled", - color: tile.color || "#999999", - width: clampNumber(Number(tile.width ?? 1), 1, 12), - height: clampNumber(Number(tile.height ?? 1), 1, 12), - imageData: tile.imageData || null, - })); + tiles = normalizeTiles(payload.tiles); imageCache.clear(); tiles.forEach((tile) => buildTileImage(tile)); @@ -716,12 +780,47 @@ importJsonButton.addEventListener("click", () => { renderFloorOptions(); renderCanvas(); syncPatternPreview(); + saveLibraryToStorage(); } catch (error) { alert("Could not import JSON. Please check the format."); } }); +exportLibraryButton.addEventListener("click", () => { + const payload = { + version: 1, + tiles, + }; + downloadFile(JSON.stringify(payload, null, 2), "tile-library.json", "application/json"); +}); + +importLibraryButton.addEventListener("click", () => { + importLibraryFile.click(); +}); + +importLibraryFile.addEventListener("change", async (event) => { + const file = event.target.files?.[0]; + if (!file) { + return; + } + try { + const text = await file.text(); + const payload = JSON.parse(text); + const loaded = loadLibraryFromPayload(payload); + if (loaded) { + saveLibraryToStorage(); + } else { + alert("Invalid library JSON."); + } + } catch (error) { + alert("Could not import library JSON."); + } finally { + importLibraryFile.value = ""; + } +}); + initGrid(); +loadLibraryFromStorage(); renderTiles(); renderCanvas(); syncPatternPreview(); diff --git a/index.html b/index.html index b7bd5e6..44bbffa 100644 --- a/index.html +++ b/index.html @@ -22,6 +22,11 @@

Tile Library

+
+ + + +