|
|
|
@@ -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();
|
|
|
|
|