/* ---------- Workspace Data Model ---------- */
/* workspace schema:
workspace = {
objects: [
{ id, name, attributes:[{key,value}], lines:[ {id, name, attributes:[{key,value}], items:[{x,y,w,h,row,col,badge,thumbDataURL}]} ], activeLineId }
],
activeObjectId
}
*/
const workspace = {
objects: [],
activeObjectId: null
};
/* ---------- DOM Elements ---------- */
let objectsBar, objectPills, addObjectBtn, renameObjectBtn, removeObjectBtn;
let linesBar, linePills, addLineBtn, renameLineBtn, removeLineBtn, clearLineBtn;
let tankStrip, tankCount, workspaceGridEl;
/* ---------- Active Getters ---------- */
function activeObject() {
return workspace.objects.find(o => o.id === workspace.activeObjectId) || null;
}
function activeLine() {
const o = activeObject();
if(!o) return null;
return o.lines.find(l => l.id === o.activeLineId) || null;
}
/* ---------- Object Management ---------- */
function renderObjects() {
try {
objectPills.innerHTML = '';
for (const o of workspace.objects) {
const b = document.createElement('button');
b.className = 'obj-pill' + (o.id === workspace.activeObjectId ? ' active' : '');
b.textContent = o.name;
b.onclick = () => {
workspace.activeObjectId = o.id;
if(!o.activeLineId && o.lines[0]) o.activeLineId = o.lines[0].id;
renderObjects();
renderLines();
renderActiveTank();
};
objectPills.appendChild(b);
}
} catch (error) {
alert(`Error rendering objects: ${error.message}`);
}
}
function addObject() {
try {
const idx = workspace.objects.length + 1;
const obj = {
id: window.CoreAPI.rid(),
name: `Object ${idx}`,
attributes: [],
lines: [{
id: window.CoreAPI.rid(),
name: 'Line 1',
attributes: [],
items: []
}],
activeLineId: null
};
obj.activeLineId = obj.lines[0].id;
workspace.objects.push(obj);
workspace.activeObjectId = obj.id;
renderObjects();
renderLines();
renderActiveTank();
} catch (error) {
alert(`Error adding object: ${error.message}`);
}
}
function renameObject() {
try {
const o = activeObject();
if(!o) return;
const name = prompt('Rename object:', o.name);
if (name && name.trim()) {
o.name = name.trim();
renderObjects();
}
} catch (error) {
alert(`Error renaming object: ${error.message}`);
}
}
function removeObject() {
try {
if (workspace.objects.length === 1) {
alert('At least one object is required.');
return;
}
const idx = workspace.objects.findIndex(o => o.id === workspace.activeObjectId);
if (idx >= 0) {
workspace.objects.splice(idx, 1);
const next = workspace.objects[Math.max(0, idx - 1)];
workspace.activeObjectId = next.id;
renderObjects();
renderLines();
renderActiveTank();
}
} catch (error) {
alert(`Error removing object: ${error.message}`);
}
}
/* ---------- Line Management ---------- */
function renderLines() {
try {
const o = activeObject();
linePills.innerHTML = '';
if (!o) return;
for (const l of o.lines) {
const b = document.createElement('button');
b.className = 'line-pill' + (l.id === o.activeLineId ? ' active' : '');
b.textContent = l.name;
b.onclick = () => {
o.activeLineId = l.id;
renderLines();
renderActiveTank();
};
linePills.appendChild(b);
}
} catch (error) {
alert(`Error rendering lines: ${error.message}`);
}
}
function addLine() {
try {
const o = activeObject();
if(!o) return;
const idx = o.lines.length + 1;
const line = {
id: window.CoreAPI.rid(),
name: `Line ${idx}`,
attributes: [
{ key: 'url', value: '' }
],
items: []
};
o.lines.push(line);
o.activeLineId = line.id;
renderLines();
renderActiveTank();
} catch (error) {
alert(`Error adding line: ${error.message}`);
}
}
function renameLine() {
try {
const o = activeObject();
if(!o) return;
const l = activeLine();
if(!l) return;
const name = prompt('Rename line:', l.name);
if (name && name.trim()) {
l.name = name.trim();
renderLines();
}
} catch (error) {
alert(`Error renaming line: ${error.message}`);
}
}
function removeLine() {
try {
const o = activeObject();
if(!o) return;
if (o.lines.length === 1) {
alert('At least one line is required.');
return;
}
const idx = o.lines.findIndex(l => l.id === o.activeLineId);
if (idx >= 0) {
o.lines.splice(idx, 1);
const next = o.lines[Math.max(0, idx - 1)];
o.activeLineId = next.id;
renderLines();
renderActiveTank();
}
} catch (error) {
alert(`Error removing line: ${error.message}`);
}
}
function clearLine() {
try {
const l = activeLine();
if(!l) return;
l.items = [];
renderActiveTank();
} catch (error) {
alert(`Error clearing line: ${error.message}`);
}
}
/* ---------- Tank Rendering ---------- */
function renderActiveTank() {
try {
const l = activeLine();
tankStrip.innerHTML = '';
if (!l) {
tankCount.textContent = '0 items';
return;
}
l.items.forEach((t, i) => {
const item = document.createElement('div');
item.className = 'tank-item';
item.title = `${t.badge} (${t.w}×${t.h}) @ (${t.x},${t.y})`;
const img = document.createElement('img');
img.src = t.thumbDataURL;
const badge = document.createElement('div');
badge.className = 'badge';
badge.textContent = t.badge;
const remove = document.createElement('button');
remove.className = 'remove';
remove.textContent = '×';
remove.title = 'Remove';
remove.onclick = () => {
l.items.splice(i, 1);
renderActiveTank();
};
item.appendChild(img);
item.appendChild(badge);
item.appendChild(remove);
tankStrip.appendChild(item);
});
tankCount.textContent = `${l.items.length} item${l.items.length === 1 ? '' : 's'}`;
} catch (error) {
alert(`Error rendering tank: ${error.message}`);
}
}
/* ---------- Tile Capture ---------- */
function initTileCapture() {
try {
workspaceGridEl.addEventListener('click', (e) => {
try {
const tile = e.target.closest('.tile');
if (!tile) return;
if (!window.CoreAPI.imgEl) {
alert('Load an image first from the gallery');
return;
}
e.stopPropagation(); // avoid panning
const x = parseInt(tile.dataset.x, 10);
const y = parseInt(tile.dataset.y, 10);
const w = parseInt(tile.dataset.w, 10);
const h = parseInt(tile.dataset.h, 10);
let row = parseInt(tile.dataset.row, 10);
let col = parseInt(tile.dataset.col, 10);
if (window.CoreAPI.ONE_BASED) {
row += 1;
col += 1;
}
const badgeText = `r${row}c${col}`;
// make thumbnail
const cv = document.createElement('canvas');
cv.width = w;
cv.height = h;
const cctx = cv.getContext('2d');
cctx.imageSmoothingEnabled = false;
cctx.drawImage(window.CoreAPI.imgEl, x, y, w, h, 0, 0, w, h);
const dataURL = cv.toDataURL('image/png');
const l = activeLine();
if(!l) {
alert('No active line selected');
return;
}
// Get current image URL from Core API (we'll need this for the url attribute)
const currentImageUrl = window.CoreAPI.imgEl?.src || '';
// Check if line already has tiles from a different image
if (l.items.length > 0) {
const lineUrlAttr = l.attributes.find(attr => attr.key === 'url');
const lineImageUrl = lineUrlAttr ? lineUrlAttr.value : '';
if (lineImageUrl && lineImageUrl !== currentImageUrl) {
alert(`This line already contains tiles from a different image.\nCreate a new line for tiles from this image.`);
return;
}
}
// Add the tile to the line
l.items.push({
x, y, w, h, row, col,
badge: badgeText,
thumbDataURL: dataURL
});
// If this is the first tile in the line, set the url attribute
if (l.items.length === 1 && currentImageUrl) {
let urlAttr = l.attributes.find(attr => attr.key === 'url');
if (urlAttr) {
urlAttr.value = currentImageUrl;
} else {
l.attributes.push({ key: 'url', value: currentImageUrl });
}
}
renderActiveTank();
} catch (error) {
alert(`Error capturing tile: ${error.message}`);
}
});
alert('Workspace: Tile capture initialized');
} catch (error) {
alert(`Failed to initialize tile capture: ${error.message}`);
throw error;
}
}
/* ---------- Event Handlers ---------- */
function initWorkspaceEvents() {
try {
alert('Workspace: Setting up event handlers...');
addObjectBtn.addEventListener('click', addObject);
renameObjectBtn.addEventListener('click', renameObject);
removeObjectBtn.addEventListener('click', removeObject);
addLineBtn.addEventListener('click', addLine);
renameLineBtn.addEventListener('click', renameLine);
removeLineBtn.addEventListener('click', removeLine);
clearLineBtn.addEventListener('click', clearLine);
alert('Workspace: Event handlers initialized');
} catch (error) {
alert(`Failed to initialize workspace events: ${error.message}`);
throw error;
}
}
/* ---------- Initialization ---------- */
function initializeWorkspace() {
try {
// Get DOM elements
objectsBar = document.getElementById('objectsBar');
objectPills = document.getElementById('objectPills');
addObjectBtn = document.getElementById('addObjectBtn');
renameObjectBtn = document.getElementById('renameObjectBtn');
removeObjectBtn = document.getElementById('removeObjectBtn');
linesBar = document.getElementById('linesBar');
linePills = document.getElementById('linePills');
addLineBtn = document.getElementById('addLineBtn');
renameLineBtn = document.getElementById('renameLineBtn');
removeLineBtn = document.getElementById('removeLineBtn');
clearLineBtn = document.getElementById('clearLineBtn');
tankStrip = document.getElementById('tankStrip');
tankCount = document.getElementById('tankCount');
workspaceGridEl = document.getElementById('grid');
// Check required DOM elements
if (!objectsBar || !tankStrip || !workspaceGridEl) {
throw new Error('Required DOM elements not found');
}
// Check CoreAPI dependency
if (!window.CoreAPI || typeof window.CoreAPI.rid !== 'function') {
throw new Error('CoreAPI not available');
}
// Create initial object
const initialObject = {
id: window.CoreAPI.rid(),
name: 'Object 1',
attributes: [],
lines: [{
id: window.CoreAPI.rid(),
name: 'Line 1',
attributes: [
{ key: 'url', value: '' }
],
items: []
}],
activeLineId: null
};
initialObject.activeLineId = initialObject.lines[0].id;
workspace.objects.push(initialObject);
workspace.activeObjectId = initialObject.id;
initWorkspaceEvents();
initTileCapture();
renderObjects();
renderLines();
renderActiveTank();
} catch (error) {
alert(`Workspace module failed to initialize: ${error.message}`);
throw error;
}
}
// Export API for other modules
window.WorkspaceAPI = {
workspace,
activeObject,
activeLine,
renderObjects,
renderLines,
renderActiveTank
};