📜
workspace.js
Back
📝 Javascript ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
/* ---------- 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 };