📜
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, counters: { objects: 0, lines: 0 } } */ const workspace = { objects: [], activeObjectId: null, counters: { objects: 0, lines: 0 } }; /* ---------- DOM Elements ---------- */ let objectsBar, objectPills, addObjectBtn, renameObjectBtn, removeObjectBtn; let linesBar, linePills, addLineBtn, renameLineBtn, removeLineBtn, clearLineBtn; let tankStrip, tankCount, workspaceGridEl; /* ---------- ID Management ---------- */ function renumberObjectIds(obj) { let counter = 1; // Set object ID let objIdAttr = obj.attributes.find(attr => attr.key === 'id'); if (objIdAttr) { objIdAttr.value = counter.toString(); } counter++; // Renumber lines and their tiles sequentially obj.lines.forEach(line => { // Set line ID let lineIdAttr = line.attributes.find(attr => attr.key === 'id'); if (lineIdAttr) { lineIdAttr.value = counter.toString(); } counter++; // Set tile IDs for this line line.items.forEach(tile => { tile.id = counter; counter++; }); }); // Redraw grid to update ID previews if (window.CoreAPI && typeof window.CoreAPI.drawGrid === 'function') { window.CoreAPI.drawGrid(); } } /* ---------- 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(); }; // Add double-click to open properties b.ondblclick = () => { if (window.AttributesAPI && typeof window.AttributesAPI.openObjectView === 'function') { window.AttributesAPI.openObjectView(); } }; 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(), // Keep complex ID for internal tracking name: `Object ${idx}`, attributes: [ { key: 'id', value: '1' } // Will be renumbered ], lines: [{ id: window.CoreAPI.rid(), // Keep complex ID for internal tracking name: 'Line 1', attributes: [ { key: 'id', value: '2' }, // Will be renumbered { key: 'url', value: '' } ], items: [] }], activeLineId: null }; obj.activeLineId = obj.lines[0].id; workspace.objects.push(obj); workspace.activeObjectId = obj.id; // Renumber everything in this object renumberObjectIds(obj); 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(), // Keep complex ID for internal tracking name: `Line ${idx}`, attributes: [ { key: 'id', value: '0' }, // Will be renumbered { key: 'url', value: '' } ], items: [] }; o.lines.push(line); o.activeLineId = line.id; // Renumber everything in this object renumberObjectIds(o); 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; // Renumber everything in this object renumberObjectIds(o); renderLines(); renderActiveTank(); } } catch (error) { alert(`Error removing line: ${error.message}`); } } function clearLine() { try { const l = activeLine(); if(!l) return; l.items = []; // Renumber everything in the object after clearing tiles const o = activeObject(); if (o) renumberObjectIds(o); 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 = `Index: ${t.index || 'N/A'}, Frame: ${t.frameKey || 'N/A'}\nSize: ${t.w}×${t.h} | Center: (${t.centerX || t.x},${t.centerY || t.y})\nType: ${t.type || 'static'}`; const img = document.createElement('img'); img.src = t.thumbDataURL; const badge = document.createElement('div'); badge.className = 'badge'; badge.textContent = t.id !== undefined ? `#${t.id}` : t.badge; const remove = document.createElement('button'); remove.className = 'remove'; remove.textContent = '×'; remove.title = 'Remove'; remove.onclick = () => { l.items.splice(i, 1); // Renumber everything in the object after removing tile const o = activeObject(); if (o) renumberObjectIds(o); renderActiveTank(); }; item.appendChild(img); item.appendChild(badge); item.appendChild(remove); tankStrip.appendChild(item); }); const totalFrames = l.items.reduce((sum, item) => sum + (item.frameCount || 1), 0); tankCount.textContent = `${l.items.length} tile${l.items.length === 1 ? '' : 's'} (${totalFrames} frame${totalFrames === 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 object to access it for renumbering const currentObject = activeObject(); if (!currentObject) { alert('No active object'); return; } // Get current image info const currentImageUrl = window.CoreAPI.imgEl?.src || ''; const imgWidth = window.CoreAPI.imgW; const imgHeight = window.CoreAPI.imgH; const tileWidth = w; const tileHeight = h; // 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; } } // Calculate automatic attributes const cols = Math.floor(imgWidth / tileWidth); const rows = Math.floor(imgHeight / tileHeight); const index = row * cols + col; const centerX = x + (tileWidth / 2); const centerY = y + (tileHeight / 2); // Generate deterministic keys for Phaser const imageBaseName = currentImageUrl.split('/').pop().split('.')[0] || 'sprite'; const atlasKey = `${imageBaseName}_atlas`; const frameKey = `${imageBaseName}_${index}`; // Create comprehensive tile data (ID will be set by renumbering) const tileData = { // Original display data x, y, w: tileWidth, h: tileHeight, row, col, badge: badgeText, thumbDataURL: dataURL, // Automatic attributes (ID will be set during renumbering) id: 0, // Placeholder, will be renumbered url: currentImageUrl, imgWidth, imgHeight, tileWidth, tileHeight, rows, cols, index, frames: [index], // Single frame for now, can be extended for multi-tile frameCount: 1, centerX, centerY, atlasKey, frameKey, type: 'static' }; // Add the tile to the line l.items.push(tileData); // If this is the first tile in the line, set up automatic line attributes if (l.items.length === 1) { // Update automatic attributes const autoAttrs = { url: currentImageUrl, imgWidth: imgWidth.toString(), imgHeight: imgHeight.toString(), tileWidth: tileWidth.toString(), tileHeight: tileHeight.toString(), rows: rows.toString(), cols: cols.toString() }; // Set or update automatic attributes Object.entries(autoAttrs).forEach(([key, value]) => { let attr = l.attributes.find(a => a.key === key); if (attr) { attr.value = value; } else { l.attributes.push({ key, value }); } }); // Add default changeable attributes if they don't exist const defaultChangeableAttrs = { frameRate: '10', playMode: 'loop', 'origin.x': '0.5', 'origin.y': '0.5', 'physics.bodyType': 'static', collides: 'false', tags: '', blockType: 'decor', license: 'CC0' }; Object.entries(defaultChangeableAttrs).forEach(([key, value]) => { if (!l.attributes.find(a => a.key === key)) { l.attributes.push({ key, value }); } }); } // Renumber everything in this object after adding tile renumberObjectIds(currentObject); renderActiveTank(); } catch (error) { alert(`Error capturing tile: ${error.message}`); } }); } catch (error) { alert(`Failed to initialize tile capture: ${error.message}`); throw error; } } /* ---------- Event Handlers ---------- */ function initWorkspaceEvents() { try { 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); } 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 and dependencies if (!objectsBar || !tankStrip || !workspaceGridEl) { throw new Error('Required DOM elements not found'); } if (!window.CoreAPI || typeof window.CoreAPI.rid !== 'function') { throw new Error('CoreAPI not available'); } // Create initial object const initialObject = { id: window.CoreAPI.rid(), // Keep complex ID for internal tracking name: 'Object 1', attributes: [ { key: 'id', value: '1' } // Will be renumbered ], lines: [{ id: window.CoreAPI.rid(), // Keep complex ID for internal tracking name: 'Line 1', attributes: [ { key: 'id', value: '2' }, // Will be renumbered { key: 'url', value: '' } ], items: [] }], activeLineId: null }; initialObject.activeLineId = initialObject.lines[0].id; workspace.objects.push(initialObject); workspace.activeObjectId = initialObject.id; // Renumber the initial object renumberObjectIds(initialObject); 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 };