/* ---------- Constants & State ---------- */
const ONE_BASED = false; // set true for r1c1 style labels
// Settings defaults
const defaults = { tileWidth:16, tileHeight:16, xOffset:0, yOffset:0, Hspacing:0, Vspacing:0 };
let settings = {...defaults};
// Image & viewport state
let imgEl = null, imgW = 0, imgH = 0;
let scale = 1, tx = 0, ty = 0;
const MIN_SCALE = 0.25, MAX_SCALE = 16;
// Touch/pointer tracking
const pts = new Map();
let lastPan = null, pinchRef = null;
/* ---------- DOM Elements ---------- */
let settingsBtn, imagesBtn, objectViewBtn, resetBtn, settingsPanel, imagesPanel;
let imgLayer, gridEl, meta, stage, viewport;
/* ---------- Utility Functions ---------- */
function rid() {
return Math.random().toString(36).slice(2,10) + Math.random().toString(36).slice(2,10);
}
function dist(a, b) {
const dx = a.x - b.x, dy = a.y - b.y;
return Math.hypot(dx, dy);
}
function screenToWorld(x, y) {
const r = stage.getBoundingClientRect();
const sx = x - r.left, sy = y - r.top;
return { wx:(sx - tx)/scale, wy:(sy - ty)/scale };
}
/* ---------- Panel Management ---------- */
function closeAllPanels() {
settingsPanel.classList.remove('open');
imagesPanel.classList.remove('open');
}
function togglePanel(p) {
const isOpen = p.classList.contains('open');
closeAllPanels();
if(!isOpen) p.classList.add('open');
}
/* ---------- Settings Management ---------- */
const stepperInputs = {};
function initSteppers() {
try {
alert('Core: Setting up steppers...');
document.querySelectorAll('.stepper').forEach(st => {
const key = st.dataset.key;
const input = st.querySelector('input');
const buttons = st.querySelectorAll('button');
if (!key) throw new Error(`Stepper missing data-key attribute`);
if (!input) throw new Error(`Stepper for ${key} missing input element`);
if (buttons.length < 2) throw new Error(`Stepper for ${key} missing buttons`);
const [minus, plus] = buttons;
stepperInputs[key] = input;
input.value = settings[key];
const stepSize = (key === 'tileWidth' || key === 'tileHeight') ? 8 : 1;
const commit = () => {
try {
let v = parseInt(input.value) || 0;
if (key === 'tileWidth' || key === 'tileHeight') {
v = Math.max(1, Math.round(v/8)*8);
} else {
v = Math.max(0, v);
}
settings[key] = v;
input.value = v;
drawGrid();
} catch (err) {
alert(`Error updating ${key}: ${err.message}`);
}
};
minus.addEventListener('click', () => {
try {
input.value = (parseInt(input.value) || 0) - stepSize;
commit();
} catch (err) {
alert(`Error in minus button for ${key}: ${err.message}`);
}
});
plus.addEventListener('click', () => {
try {
input.value = (parseInt(input.value) || 0) + stepSize;
commit();
} catch (err) {
alert(`Error in plus button for ${key}: ${err.message}`);
}
});
input.addEventListener('change', commit);
});
alert('Core: Steppers initialized successfully');
} catch (error) {
alert(`Failed to initialize settings controls: ${error.message}`);
throw error;
}
}
function setTileSize(n) {
if (!Number.isFinite(n) || n <= 0) return;
settings.tileWidth = settings.tileHeight = n;
if (stepperInputs.tileWidth) stepperInputs.tileWidth.value = n;
if (stepperInputs.tileHeight) stepperInputs.tileHeight.value = n;
drawGrid();
}
/* ---------- Image Management ---------- */
function setImage(src) {
const img = new Image();
img.decoding = 'async';
img.onload = () => {
imgW = img.naturalWidth;
imgH = img.naturalHeight;
imgLayer.innerHTML = '';
img.style.position = 'absolute';
img.style.left = '0px';
img.style.top = '0px';
img.style.width = imgW + 'px';
img.style.height = imgH + 'px';
imgLayer.appendChild(img);
gridEl.style.width = imgW + 'px';
gridEl.style.height = imgH + 'px';
meta.textContent = `Image: ${imgW}×${imgH} — tap tiles to add to the selected line`;
// Set the imgEl variable after image loads
imgEl = img;
drawGrid();
centerView();
};
img.onerror = () => {
alert(`Failed to load image: ${src}`);
};
img.src = src;
}
/* ---------- Grid Rendering ---------- */
function drawGrid() {
if (!imgW || !imgH) {
gridEl.innerHTML = '';
return;
}
const tw = settings.tileWidth | 0, th = settings.tileHeight | 0;
const ox = settings.xOffset | 0, oy = settings.yOffset | 0;
const hs = settings.Hspacing | 0, vs = settings.Vspacing | 0;
gridEl.innerHTML = '';
let row = 0;
for (let y = oy; y + th <= imgH; y += th + vs, row++) {
let col = 0;
for (let x = ox; x + tw <= imgW; x += tw + hs, col++) {
const d = document.createElement('div');
d.className = 'tile';
d.style.left = x + 'px';
d.style.top = y + 'px';
d.style.width = tw + 'px';
d.style.height = th + 'px';
d.dataset.x = x;
d.dataset.y = y;
d.dataset.w = tw;
d.dataset.h = th;
d.dataset.row = row;
d.dataset.col = col;
gridEl.appendChild(d);
}
}
}
/* ---------- Viewport Controls ---------- */
function applyTransform() {
viewport.style.transform = `translate(${tx}px, ${ty}px) scale(${scale})`;
}
function centerView() {
if(!imgW || !imgH) return;
const r = stage.getBoundingClientRect();
scale = Math.min(Math.max(1, Math.min(r.width/imgW, r.height/imgH)), 2);
tx = (r.width - imgW*scale)/2;
ty = (r.height - imgH*scale)/2;
applyTransform();
}
function initViewportEvents() {
try {
alert('Core: Setting up viewport events...');
function endPointer(e) {
try {
stage.releasePointerCapture(e.pointerId);
} catch {}
pts.delete(e.pointerId);
if(pts.size < 2) pinchRef = null;
if(pts.size === 0) lastPan = null;
}
stage.addEventListener('pointerdown', e => {
stage.setPointerCapture(e.pointerId);
pts.set(e.pointerId, {x:e.clientX, y:e.clientY});
if (pts.size === 1) {
lastPan = {x:e.clientX, y:e.clientY};
} else if (pts.size === 2) {
const [a,b] = Array.from(pts.values());
pinchRef = {
d0: dist(a,b),
m0: {x:(a.x+b.x)/2, y:(a.y+b.y)/2},
s0: scale
};
}
});
stage.addEventListener('pointermove', e => {
if(!pts.has(e.pointerId)) return;
pts.set(e.pointerId, {x:e.clientX, y:e.clientY});
if (pts.size === 1 && lastPan) {
const p = pts.get(e.pointerId);
tx += p.x - lastPan.x;
ty += p.y - lastPan.y;
lastPan = {x:p.x, y:p.y};
applyTransform();
} else if (pts.size === 2 && pinchRef) {
const [a,b] = Array.from(pts.values());
const d1 = dist(a,b);
if(pinchRef.d0 > 0) {
const world = screenToWorld(pinchRef.m0.x, pinchRef.m0.y);
let newScale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, pinchRef.s0 * (d1/pinchRef.d0)));
const r = stage.getBoundingClientRect();
const sx = pinchRef.m0.x - r.left;
const sy = pinchRef.m0.y - r.top;
tx = sx - world.wx * newScale;
ty = sy - world.wy * newScale;
scale = newScale;
applyTransform();
}
}
});
stage.addEventListener('pointerup', endPointer);
stage.addEventListener('pointercancel', endPointer);
stage.addEventListener('pointerleave', e => {
if(pts.has(e.pointerId)) endPointer(e);
});
// Double tap to center
let lastTap = 0;
stage.addEventListener('pointerdown', e => {
const now = performance.now();
if(now - lastTap < 300) centerView();
lastTap = now;
}, {capture:true});
// Mouse wheel zoom
stage.addEventListener('wheel', e => {
if(!imgW || !imgH) return;
e.preventDefault();
const k = Math.exp(-e.deltaY * 0.0015);
const r = stage.getBoundingClientRect();
const sx = e.clientX - r.left;
const sy = e.clientY - r.top;
const world = screenToWorld(e.clientX, e.clientY);
let newScale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, scale * k));
tx = sx - world.wx * newScale;
ty = sy - world.wy * newScale;
scale = newScale;
applyTransform();
}, {passive:false});
alert('Core: Viewport events initialized');
} catch (error) {
alert(`Failed to initialize viewport events: ${error.message}`);
throw error;
}
}
/* ---------- Event Handlers ---------- */
function initPanelEvents() {
try {
alert('Core: Setting up panel events...');
settingsBtn.addEventListener('click', () => togglePanel(settingsPanel));
imagesBtn.addEventListener('click', () => {
togglePanel(imagesPanel);
if (!imagesPanel.dataset.inited) {
imagesPanel.dataset.inited = '1';
if (window.openSub) window.openSub('');
}
});
resetBtn.addEventListener('click', centerView);
alert('Core: Panel events initialized');
} catch (error) {
alert(`Failed to initialize panel events: ${error.message}`);
throw error;
}
}
/* ---------- Initialization ---------- */
function initializeCore() {
try {
// Get DOM elements
settingsBtn = document.getElementById('settingsBtn');
imagesBtn = document.getElementById('imagesBtn');
objectViewBtn = document.getElementById('objectViewBtn');
resetBtn = document.getElementById('resetBtn');
settingsPanel = document.getElementById('settingsPanel');
imagesPanel = document.getElementById('imagesPanel');
imgLayer = document.getElementById('imgLayer');
gridEl = document.getElementById('grid');
meta = document.getElementById('meta');
stage = document.getElementById('stage');
viewport = document.getElementById('viewport');
if (!settingsBtn || !stage) {
throw new Error('Required DOM elements not found');
}
initSteppers();
initViewportEvents();
initPanelEvents();
} catch (error) {
alert(`Core module failed to initialize: ${error.message}`);
throw error;
}
}
// Export functions for other modules
window.CoreAPI = {
setImage,
setTileSize,
drawGrid,
centerView,
rid,
ONE_BASED,
get imgEl() { return imgEl; },
get imgW() { return imgW; },
get imgH() { return imgH; },
settings
};