// Debug alert for mobile debugging
if (typeof debugAlert === 'function') debugAlert('tilepicker.js loaded');
// ===== Shared state from files.js =====
// window.selectedImage, window.selectedImageName, window.selectedTileSize
// Groups have no category now
let groups = [{ id: 'Group_1', url: null, tiles: [], name: 'Group 1' }];
let currentGroup = 0;
let nextUniqueId = 1; // start at 1 (0 = "no object")
// ===== Grid settings (toggleable panel) =====
const gridSettings = {
tileWidth: Math.max(1, parseInt(window.selectedTileSize || 32, 10)),
tileHeight: Math.max(1, parseInt(window.selectedTileSize || 32, 10)),
spacing: 0,
showPanel: false,
__initializedFromFolder: false,
__lastFolderSize: null
};
// ===== Bridge to tilemap.js =====
// Shape tilemap probably wants: [{ groupId, groupName, url, tiles:[{ id, x, y, w, h, url }] }]
function buildTileRects() {
return groups.map(g => ({
groupId: g.id,
groupName: g.name || '',
url: g.url || '',
tiles: g.tiles.map(t => ({
id: t.uniqueId,
x: t.sourceX,
y: t.sourceY,
w: t.width ?? t.size, // support old size-only tiles
h: t.height ?? t.size,
url: t.sourceUrl
}))
}));
}
// Notify any listeners (event + optional callback) and expose a helper global
function notifyUpdate() {
const rects = buildTileRects();
const detail = { rects, groups };
// CustomEvent
try {
window.dispatchEvent(new CustomEvent('tiles:updated', { detail }));
} catch (e) {
// no-op in older browsers
}
// Optional callback
if (typeof window.onTilesUpdated === 'function') {
try { window.onTilesUpdated(detail); } catch (e) { console.error(e); }
}
// Helper global for polling
window.Tilepicker = window.Tilepicker || {};
window.Tilepicker.getTileRects = () => buildTileRects();
window.Tilepicker.getGroups = () => groups;
window.Tilepicker.getCurrentGroupIndex = () => currentGroup;
}
/** Main entry point (called by files.js via openOverlay('tiles')) */
function openTilePickerOverlay() {
const overlayContent = document.getElementById('overlayContent');
const img = window.selectedImage;
const name = window.selectedImageName;
const tileSize = window.selectedTileSize; // optional hint; we already copied on load
if (img && (tileSize || gridSettings.tileWidth)) {
// If user picked a new size in Files view, sync it once when opening
if (tileSize && (!gridSettings.__initializedFromFolder || gridSettings.__lastFolderSize !== tileSize)) {
gridSettings.tileWidth = parseInt(tileSize, 10) || gridSettings.tileWidth;
gridSettings.tileHeight = parseInt(tileSize, 10) || gridSettings.tileHeight;
gridSettings.__initializedFromFolder = true;
gridSettings.__lastFolderSize = tileSize;
}
overlayContent.innerHTML = `
<h2>Tile Picker 🧩</h2>
<div id="groupTabs"></div>
<div id="groupControls"></div>
<div id="pickedImages"></div>
<div id="tileViewport">
<div id="tileContainer" style="position:relative; display:inline-block;">
<img id="tileImage" src="${img}" alt="${name}">
</div>
</div>
`;
initializeTilePicker();
} else {
overlayContent.innerHTML = `
<h2>Tile Picker 🧩</h2>
<p>Select an image and a numeric folder (tile size) first.</p>
`;
}
}
/** Initialize the tile picker functionality */
function initializeTilePicker() {
renderTabs();
renderGroupControls();
renderPicked();
setupTileGrid();
}
/** Build the grid overlay with width/height/spacing */
function setupTileGrid() {
const imgEl = document.getElementById('tileImage');
if (!imgEl) return;
const rebuild = () => {
const container = document.getElementById('tileContainer');
if (!container) return;
const w = imgEl.naturalWidth;
const h = imgEl.naturalHeight;
const tw = Math.max(1, parseInt(gridSettings.tileWidth, 10) || 1);
const th = Math.max(1, parseInt(gridSettings.tileHeight, 10) || 1);
const sp = Math.max(0, parseInt(gridSettings.spacing, 10) || 0);
// Dimensions
imgEl.style.width = w + "px";
imgEl.style.height = h + "px";
container.style.width = w + "px";
container.style.height = h + "px";
// Clear prior cells
container.querySelectorAll('.grid-cell').forEach(c => c.remove());
// Create cells; respect spacing (gaps between cells that don't consume image pixels)
let tileIndex = 0;
for (let y = 0; y + th <= h; y += th + sp) {
for (let x = 0; x + tw <= w; x += tw + sp) {
tileIndex++;
const cell = document.createElement('div');
cell.className = 'grid-cell';
cell.style.cssText = `
position: absolute;
left: ${x}px;
top: ${y}px;
width: ${tw}px;
height: ${th}px;
border: 2px solid rgba(102, 204, 255, 0.7);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.3);
color: white;
font-weight: bold;
font-size: 12px;
text-shadow: 1px 1px 2px black;
box-sizing: border-box;
`;
const label = document.createElement('span');
label.textContent = tileIndex;
cell.appendChild(label);
cell.addEventListener('mouseenter', () => {
cell.style.background = 'rgba(102, 204, 255, 0.4)';
cell.style.borderColor = '#6cf';
});
cell.addEventListener('mouseleave', () => {
cell.style.background = 'rgba(0, 0, 0, 0.3)';
cell.style.borderColor = 'rgba(102, 204, 255, 0.7)';
});
cell.onclick = () => pickTile(imgEl, x, y, tw, th, window.selectedImage);
container.appendChild(cell);
}
}
};
imgEl.onload = rebuild;
// If the image is cached, onload may not fire; force a tick
if (imgEl.complete && imgEl.naturalWidth) {
const src = imgEl.src;
imgEl.src = '';
imgEl.src = src;
}
}
/** Group controls (toggleable grid settings, rename, clear) */
function renderGroupControls() {
const controls = document.getElementById('groupControls');
if (!controls) return;
controls.innerHTML = '';
controls.style.cssText = `
margin-bottom:10px;padding:10px;background:#333;border-radius:6px;
display:flex;align-items:center;gap:10px;flex-wrap:wrap;
`;
// Toggle button
const toggleBtn = document.createElement('button');
toggleBtn.textContent = gridSettings.showPanel ? 'Hide grid settings ▲' : '⚙️ Grid settings ▼';
toggleBtn.style.cssText = `
background:#555;color:#fff;border:1px solid #777;border-radius:4px;
padding:6px 10px;cursor:pointer;font-size:12px;
`;
toggleBtn.onclick = () => {
gridSettings.showPanel = !gridSettings.showPanel;
renderGroupControls();
};
controls.appendChild(toggleBtn);
// Rename
const renameBtn = document.createElement('button');
renameBtn.textContent = 'Rename group';
renameBtn.style.cssText = `
background:#555;color:#fff;border:1px solid #777;border-radius:4px;
padding:6px 10px;cursor:pointer;font-size:12px;
`;
renameBtn.onclick = () => renameGroup(currentGroup);
controls.appendChild(renameBtn);
// Clear tiles
const clearBtn = document.createElement('button');
clearBtn.textContent = 'Clear tiles';
clearBtn.style.cssText = `
background:#d44;color:#fff;border:none;border-radius:4px;
padding:6px 10px;cursor:pointer;font-size:12px;
`;
clearBtn.onclick = () => {
if (!groups[currentGroup].tiles.length) return;
if (confirm('Clear all tiles from this group?')) clearCurrentGroup();
};
controls.appendChild(clearBtn);
// Grid settings panel (hidden unless toggled)
if (gridSettings.showPanel) {
const panel = document.createElement('div');
panel.style.cssText = `
width:100%;
display:flex;align-items:center;gap:10px;flex-wrap:wrap;
background:#2b2b2b;border:1px solid #444;border-radius:6px;
padding:8px 10px;margin-top:6px;
`;
const makeField = (label, key, min = 0) => {
const wrap = document.createElement('label');
wrap.style.cssText = 'color:#ccc;font-size:12px;display:flex;align-items:center;gap:6px;';
wrap.textContent = label;
const input = document.createElement('input');
input.type = 'number';
input.min = String(min);
input.value = String(gridSettings[key]);
input.style.cssText = `
width:80px;background:#555;color:#fff;border:1px solid #777;border-radius:4px;
padding:4px 6px;font-size:12px;
`;
input.onchange = () => {
const v = parseInt(input.value, 10);
if (!isNaN(v)) {
gridSettings[key] = Math.max(min, v);
setupTileGrid(); // live rebuild
}
};
wrap.appendChild(input);
return wrap;
};
panel.appendChild(makeField('Width', 'tileWidth', 1));
panel.appendChild(makeField('Height', 'tileHeight', 1));
panel.appendChild(makeField('Spacing', 'spacing', 0));
controls.appendChild(panel);
}
}
/** Group tabs (simple, no categories) */
function renderTabs() {
const tabBar = document.getElementById('groupTabs');
if (!tabBar) return;
tabBar.innerHTML = '';
tabBar.style.cssText = 'margin-bottom:10px;display:flex;gap:5px;align-items:center;flex-wrap:wrap;';
groups.forEach((g, idx) => {
const btn = document.createElement('button');
const name = g.name || `Group ${idx + 1}`;
btn.textContent = name;
btn.style.cssText = `
background:${idx === currentGroup ? '#6cf' : '#555'};
color:${idx === currentGroup ? '#000' : '#fff'};
border:2px solid #777;
padding:6px 12px;border-radius:4px;cursor:pointer;font-size:12px;
position:relative;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
`;
btn.onclick = () => { currentGroup = idx; renderTabs(); renderGroupControls(); renderPicked(); };
btn.ondblclick = (e) => { e.stopPropagation(); renameGroup(idx); };
if (g.tiles.length) {
const badge = document.createElement('span');
badge.textContent = g.tiles.length;
badge.style.cssText = `
position:absolute;top:-5px;right:-5px;background:#f44;color:#fff;border-radius:50%;
width:16px;height:16px;font-size:9px;display:flex;align-items:center;justify-content:center;
`;
btn.appendChild(badge);
}
tabBar.appendChild(btn);
});
// "+" add group
const addBtn = document.createElement('button');
addBtn.textContent = '+';
addBtn.style.cssText = `
background:#4a4;color:#fff;border:none;padding:6px 12px;border-radius:4px;
cursor:pointer;font-size:14px;font-weight:bold;
`;
addBtn.onclick = () => {
const name = prompt('Enter name for new group:', `Group ${groups.length + 1}`);
if (name && name.trim()) createGroup(name.trim());
};
addBtn.title = 'Create new group';
tabBar.appendChild(addBtn);
// "Remove group" (if >1 groups)
if (groups.length > 1) {
const removeBtn = document.createElement('button');
removeBtn.textContent = 'Remove';
removeBtn.style.cssText = `
background:#777;color:#fff;border:none;padding:6px 12px;border-radius:4px;
cursor:pointer;font-size:12px;
`;
removeBtn.onclick = () => {
if (confirm('Remove current group?')) removeGroup(currentGroup);
};
tabBar.appendChild(removeBtn);
}
}
/** Picked tiles panel */
function renderPicked() {
const container = document.getElementById('pickedImages');
if (!container) return;
container.innerHTML = '';
container.style.cssText = `
margin-bottom:15px;padding:10px;background:#2a2a2a;border-radius:6px;
min-height:80px;max-height:200px;overflow-y:auto;
`;
const group = groups[currentGroup];
if (!group.tiles.length) {
container.innerHTML = '<div style="color:#888;text-align:center;padding:20px;">No tiles picked yet. Click on the grid below to select tiles.</div>';
// broadcast even for empty (some UIs clear palette)
notifyUpdate();
return;
}
const wrap = document.createElement('div');
wrap.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;';
group.tiles.forEach((tile, idx) => {
const card = document.createElement('div');
card.style.cssText = `
position:relative;display:flex;flex-direction:column;align-items:center;
padding:5px;background:#333;border-radius:4px;border:2px solid #6cf;
`;
// Scale preview to fit within 64x64 while preserving aspect
const maxPreview = 64;
const scale = Math.min(maxPreview / tile.width, maxPreview / tile.height, 1);
const cw = Math.max(1, Math.round(tile.width * scale));
const ch = Math.max(1, Math.round(tile.height * scale));
const canvas = document.createElement('canvas');
canvas.width = cw;
canvas.height = ch;
canvas.style.cssText = 'border:1px solid #666;background:#000;';
const ctx = canvas.getContext('2d');
const temp = document.createElement('canvas');
temp.width = tile.width;
temp.height = tile.height;
const tctx = temp.getContext('2d');
tctx.putImageData(tile.data, 0, 0);
ctx.drawImage(temp, 0, 0, tile.width, tile.height, 0, 0, cw, ch);
const rm = document.createElement('button');
rm.textContent = '×';
rm.style.cssText = `
position:absolute;top:-5px;right:-5px;background:#f44;color:#fff;border:none;
border-radius:50%;width:20px;height:20px;cursor:pointer;font-size:12px;
display:flex;align-items:center;justify-content:center;
`;
rm.onclick = () => { group.tiles.splice(idx, 1); renderPicked(); notifyUpdate(); };
const id = document.createElement('span');
id.textContent = `ID ${tile.uniqueId} (${tile.width}×${tile.height})`;
id.style.cssText = 'font-size:10px;color:#ccc;margin-top:4px;text-align:center;';
card.appendChild(canvas);
card.appendChild(rm);
card.appendChild(id);
wrap.appendChild(card);
});
container.appendChild(wrap);
// Broadcast current state so tilemap can see fresh picks
notifyUpdate();
}
/** Extract a tile at (x,y) with width/height */
function pickTile(imgEl, x, y, width, height, url) {
const group = groups[currentGroup];
// Lock a group to a single spritesheet
if (group.url && group.url !== url) {
alert('This group already uses a different image. Create a new group for another sheet.');
return;
}
// Prevent duplicate picks of same cell
const duplicate = group.tiles.find(t => t.sourceX === x && t.sourceY === y && t.sourceUrl === url);
if (duplicate) {
alert(`This tile is already picked (ID ${duplicate.uniqueId})`);
return;
}
group.url = url;
if (!group.name) {
const base = (url.split('/').pop() || '').split('.')[0] || 'Sheet';
const w = Math.max(1, parseInt(gridSettings.tileWidth, 10) || width);
const h = Math.max(1, parseInt(gridSettings.tileHeight, 10) || height);
group.name = `${base}_${w}x${h}`;
renderTabs();
}
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(imgEl, x, y, width, height, 0, 0, width, height);
const data = ctx.getImageData(0, 0, width, height);
group.tiles.push({
width,
height,
data,
uniqueId: nextUniqueId++,
sourceX: x,
sourceY: y,
sourceUrl: url
});
renderPicked(); // also calls notifyUpdate()
}
/** Group helpers */
function createGroup(groupName) {
const id = groupName.replace(/[^a-zA-Z0-9]/g, '_');
groups.push({ id, url: null, tiles: [], name: groupName });
currentGroup = groups.length - 1;
renderTabs(); renderGroupControls(); renderPicked(); notifyUpdate();
}
function renameGroup(groupIndex) {
const group = groups[groupIndex];
const currentName = group.name || `Group ${groupIndex + 1}`;
const newName = prompt('Enter new group name:', currentName);
if (newName && newName.trim()) {
group.name = newName.trim();
group.id = newName.trim().replace(/[^a-zA-Z0-9]/g, '_');
renderTabs(); notifyUpdate();
}
}
function clearCurrentGroup() {
const group = groups[currentGroup];
group.tiles = [];
group.url = null; // keep name
renderTabs(); renderPicked(); notifyUpdate();
}
function removeGroup(idx) {
if (groups.length > 1 && idx >= 0 && idx < groups.length) {
groups.splice(idx, 1);
if (currentGroup >= groups.length) currentGroup = groups.length - 1;
renderTabs(); renderGroupControls(); renderPicked(); notifyUpdate();
}
}
// Optional external API
function getCurrentGroup() { return groups[currentGroup]; }
function getAllGroups() { return groups; }
function setCurrentGroup(idx) {
if (idx >= 0 && idx < groups.length) {
currentGroup = idx;
renderTabs(); renderGroupControls(); renderPicked(); notifyUpdate();
}
}
// Expose helpers immediately so tilemap.js can call them on load
window.Tilepicker = window.Tilepicker || {};
window.Tilepicker.getTileRects = () => buildTileRects();
window.Tilepicker.getGroups = () => groups;
window.Tilepicker.getCurrentGroupIndex = () => currentGroup;
// Debug alert for mobile debugging - success
if (typeof debugAlert === 'function') debugAlert('tilepicker.js loaded successfully');