import { createModuleLogger } from "./utils/LoggerUtils.js"; import { iconLoader, LAYERFORGE_TOOLS } from "./utils/IconLoader.js"; import { createCanvas } from "./utils/CommonUtils.js"; import { addStylesheet, getUrl } from "./utils/ResourceManager.js"; const log = createModuleLogger('CanvasLayersPanel'); export class CanvasLayersPanel { constructor(canvas) { this.canvas = canvas; this.container = null; this.layersContainer = null; this.draggedElements = []; this.dragInsertionLine = null; this.isMultiSelecting = false; this.lastSelectedIndex = -1; this.handleLayerClick = this.handleLayerClick.bind(this); this.handleDragStart = this.handleDragStart.bind(this); this.handleDragOver = this.handleDragOver.bind(this); this.handleDragEnd = this.handleDragEnd.bind(this); this.handleDrop = this.handleDrop.bind(this); // Preload icons this.initializeIcons(); // Load CSS for layers panel addStylesheet(getUrl('./css/layers_panel.css')); log.info('CanvasLayersPanel initialized'); } async initializeIcons() { try { await iconLoader.preloadToolIcons(); log.debug('Icons preloaded successfully'); } catch (error) { log.warn('Failed to preload icons, using fallbacks:', error); } } createIconElement(toolName, size = 16) { const iconContainer = document.createElement('div'); iconContainer.className = 'icon-container'; iconContainer.style.width = `${size}px`; iconContainer.style.height = `${size}px`; const icon = iconLoader.getIcon(toolName); if (icon) { if (icon instanceof HTMLImageElement) { const img = icon.cloneNode(); img.style.width = `${size}px`; img.style.height = `${size}px`; iconContainer.appendChild(img); } else if (icon instanceof HTMLCanvasElement) { const { canvas, ctx } = createCanvas(size, size); if (ctx) { ctx.drawImage(icon, 0, 0, size, size); } iconContainer.appendChild(canvas); } } else { // Fallback text iconContainer.classList.add('fallback-text'); iconContainer.textContent = toolName.charAt(0).toUpperCase(); iconContainer.style.fontSize = `${size * 0.6}px`; } return iconContainer; } createVisibilityIcon(isVisible) { if (isVisible) { return this.createIconElement(LAYERFORGE_TOOLS.VISIBILITY, 16); } else { // Create a "hidden" version of the visibility icon const iconContainer = document.createElement('div'); iconContainer.className = 'icon-container visibility-hidden'; iconContainer.style.width = '16px'; iconContainer.style.height = '16px'; const icon = iconLoader.getIcon(LAYERFORGE_TOOLS.VISIBILITY); if (icon) { if (icon instanceof HTMLImageElement) { const img = icon.cloneNode(); img.style.width = '16px'; img.style.height = '16px'; iconContainer.appendChild(img); } else if (icon instanceof HTMLCanvasElement) { const { canvas, ctx } = createCanvas(16, 16); if (ctx) { ctx.globalAlpha = 0.3; ctx.drawImage(icon, 0, 0, 16, 16); } iconContainer.appendChild(canvas); } } else { // Fallback iconContainer.classList.add('fallback-text'); iconContainer.textContent = 'H'; iconContainer.style.fontSize = '10px'; } return iconContainer; } } createPanelStructure() { this.container = document.createElement('div'); this.container.className = 'layers-panel'; this.container.tabIndex = 0; // Umożliwia fokus na panelu this.container.innerHTML = `
Layers
`; this.layersContainer = this.container.querySelector('#layers-container'); // Setup event listeners dla przycisków this.setupControlButtons(); // Dodaj listener dla klawiatury, aby usuwanie działało z panelu this.container.addEventListener('keydown', (e) => { if (e.key === 'Delete' || e.key === 'Backspace') { e.preventDefault(); e.stopPropagation(); this.deleteSelectedLayers(); } }); log.debug('Panel structure created'); return this.container; } setupControlButtons() { if (!this.container) return; const deleteBtn = this.container.querySelector('#delete-layer-btn'); // Add delete icon to button if (deleteBtn) { const deleteIcon = this.createIconElement(LAYERFORGE_TOOLS.DELETE, 16); deleteBtn.appendChild(deleteIcon); } deleteBtn?.addEventListener('click', () => { log.info('Delete layer button clicked'); this.deleteSelectedLayers(); }); // Initial button state update this.updateButtonStates(); } renderLayers() { if (!this.layersContainer) { log.warn('Layers container not initialized'); return; } // Wyczyść istniejącą zawartość this.layersContainer.innerHTML = ''; // Usuń linię wstawiania jeśli istnieje this.removeDragInsertionLine(); // Sortuj warstwy według zIndex (od najwyższej do najniższej) const sortedLayers = [...this.canvas.layers].sort((a, b) => b.zIndex - a.zIndex); sortedLayers.forEach((layer, index) => { const layerElement = this.createLayerElement(layer, index); if (this.layersContainer) this.layersContainer.appendChild(layerElement); }); log.debug(`Rendered ${sortedLayers.length} layers`); } createLayerElement(layer, index) { const layerRow = document.createElement('div'); layerRow.className = 'layer-row'; layerRow.draggable = true; layerRow.dataset.layerIndex = String(index); const isSelected = this.canvas.canvasSelection.selectedLayers.includes(layer); if (isSelected) { layerRow.classList.add('selected'); } // Ustawienie domyślnych właściwości jeśli nie istnieją if (!layer.name) { layer.name = this.ensureUniqueName(`Layer ${layer.zIndex + 1}`, layer); } else { // Sprawdź unikalność istniejącej nazwy (np. przy duplikowaniu) layer.name = this.ensureUniqueName(layer.name, layer); } layerRow.innerHTML = `
${layer.name} `; // Add visibility icon const visibilityToggle = layerRow.querySelector('.layer-visibility-toggle'); if (visibilityToggle) { const visibilityIcon = this.createVisibilityIcon(layer.visible); visibilityToggle.appendChild(visibilityIcon); } const thumbnailContainer = layerRow.querySelector('.layer-thumbnail'); if (thumbnailContainer) { this.generateThumbnail(layer, thumbnailContainer); } this.setupLayerEventListeners(layerRow, layer, index); return layerRow; } generateThumbnail(layer, thumbnailContainer) { if (!layer.image) { thumbnailContainer.style.background = '#4a4a4a'; return; } const { canvas, ctx } = createCanvas(48, 48, '2d', { willReadFrequently: true }); if (!ctx) return; const scale = Math.min(48 / layer.image.width, 48 / layer.image.height); const scaledWidth = layer.image.width * scale; const scaledHeight = layer.image.height * scale; // Wycentruj obraz const x = (48 - scaledWidth) / 2; const y = (48 - scaledHeight) / 2; ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high'; ctx.drawImage(layer.image, x, y, scaledWidth, scaledHeight); thumbnailContainer.appendChild(canvas); } setupLayerEventListeners(layerRow, layer, index) { layerRow.addEventListener('mousedown', (e) => { const nameElement = layerRow.querySelector('.layer-name'); if (nameElement && nameElement.classList.contains('editing')) { return; } this.handleLayerClick(e, layer, index); }); // --- PRAWY PRZYCISK: ODJAZNACZ LAYER --- layerRow.addEventListener('contextmenu', (e) => { e.preventDefault(); e.stopPropagation(); if (this.canvas.canvasSelection.selectedLayers.includes(layer)) { const newSelection = this.canvas.canvasSelection.selectedLayers.filter((l) => l !== layer); this.canvas.updateSelection(newSelection); this.updateSelectionAppearance(); this.updateButtonStates(); } }); layerRow.addEventListener('dblclick', (e) => { e.preventDefault(); e.stopPropagation(); const nameElement = layerRow.querySelector('.layer-name'); if (nameElement) { this.startEditingLayerName(nameElement, layer); } }); // Add visibility toggle event listener const visibilityToggle = layerRow.querySelector('.layer-visibility-toggle'); if (visibilityToggle) { visibilityToggle.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); this.toggleLayerVisibility(layer); }); } layerRow.addEventListener('dragstart', (e) => this.handleDragStart(e, layer, index)); layerRow.addEventListener('dragover', this.handleDragOver.bind(this)); layerRow.addEventListener('dragend', this.handleDragEnd.bind(this)); layerRow.addEventListener('drop', (e) => this.handleDrop(e, index)); } handleLayerClick(e, layer, index) { const isCtrlPressed = e.ctrlKey || e.metaKey; const isShiftPressed = e.shiftKey; // Aktualizuj wewnętrzny stan zaznaczenia w obiekcie canvas // Ta funkcja NIE powinna już wywoływać onSelectionChanged w panelu. this.canvas.updateSelectionLogic(layer, isCtrlPressed, isShiftPressed, index); // Aktualizuj tylko wygląd (klasy CSS), bez niszczenia DOM this.updateSelectionAppearance(); this.updateButtonStates(); log.debug(`Layer clicked: ${layer.name}, selection count: ${this.canvas.canvasSelection.selectedLayers.length}`); } startEditingLayerName(nameElement, layer) { const currentName = layer.name; nameElement.classList.add('editing'); const input = document.createElement('input'); input.type = 'text'; input.value = currentName; input.style.width = '100%'; nameElement.innerHTML = ''; nameElement.appendChild(input); input.focus(); input.select(); const finishEditing = () => { let newName = input.value.trim() || `Layer ${layer.zIndex + 1}`; newName = this.ensureUniqueName(newName, layer); layer.name = newName; nameElement.classList.remove('editing'); nameElement.textContent = newName; this.canvas.saveState(); log.info(`Layer renamed to: ${newName}`); }; input.addEventListener('blur', finishEditing); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { finishEditing(); } else if (e.key === 'Escape') { nameElement.classList.remove('editing'); nameElement.textContent = currentName; } }); } ensureUniqueName(proposedName, currentLayer) { const existingNames = this.canvas.layers .filter((layer) => layer !== currentLayer) .map((layer) => layer.name); if (!existingNames.includes(proposedName)) { return proposedName; } // Sprawdź czy nazwa już ma numerację w nawiasach const match = proposedName.match(/^(.+?)\s*\((\d+)\)$/); let baseName, startNumber; if (match) { baseName = match[1].trim(); startNumber = parseInt(match[2]) + 1; } else { baseName = proposedName; startNumber = 1; } // Znajdź pierwszą dostępną numerację let counter = startNumber; let uniqueName; do { uniqueName = `${baseName} (${counter})`; counter++; } while (existingNames.includes(uniqueName)); return uniqueName; } toggleLayerVisibility(layer) { layer.visible = !layer.visible; // If layer became invisible and is selected, deselect it if (!layer.visible && this.canvas.canvasSelection.selectedLayers.includes(layer)) { const newSelection = this.canvas.canvasSelection.selectedLayers.filter((l) => l !== layer); this.canvas.updateSelection(newSelection); } this.canvas.render(); this.canvas.requestSaveState(); // Update the eye icon in the panel this.renderLayers(); log.info(`Layer "${layer.name}" visibility toggled to: ${layer.visible}`); } deleteSelectedLayers() { if (this.canvas.canvasSelection.selectedLayers.length === 0) { log.debug('No layers selected for deletion'); return; } log.info(`Deleting ${this.canvas.canvasSelection.selectedLayers.length} selected layers`); this.canvas.removeSelectedLayers(); this.renderLayers(); } handleDragStart(e, layer, index) { if (!this.layersContainer || !e.dataTransfer) return; const editingElement = this.layersContainer.querySelector('.layer-name.editing'); if (editingElement) { e.preventDefault(); return; } // Jeśli przeciągana warstwa nie jest zaznaczona, zaznacz ją if (!this.canvas.canvasSelection.selectedLayers.includes(layer)) { this.canvas.updateSelection([layer]); this.renderLayers(); } this.draggedElements = [...this.canvas.canvasSelection.selectedLayers]; e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', ''); this.layersContainer.querySelectorAll('.layer-row').forEach((row, idx) => { const sortedLayers = [...this.canvas.layers].sort((a, b) => b.zIndex - a.zIndex); if (this.draggedElements.includes(sortedLayers[idx])) { row.classList.add('dragging'); } }); log.debug(`Started dragging ${this.draggedElements.length} layers`); } handleDragOver(e) { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'; const layerRow = e.currentTarget; const rect = layerRow.getBoundingClientRect(); const midpoint = rect.top + rect.height / 2; const isUpperHalf = e.clientY < midpoint; this.showDragInsertionLine(layerRow, isUpperHalf); } showDragInsertionLine(targetRow, isUpperHalf) { this.removeDragInsertionLine(); const line = document.createElement('div'); line.className = 'drag-insertion-line'; if (isUpperHalf) { line.style.top = '-1px'; } else { line.style.bottom = '-1px'; } targetRow.style.position = 'relative'; targetRow.appendChild(line); this.dragInsertionLine = line; } removeDragInsertionLine() { if (this.dragInsertionLine) { this.dragInsertionLine.remove(); this.dragInsertionLine = null; } } handleDrop(e, targetIndex) { e.preventDefault(); this.removeDragInsertionLine(); if (this.draggedElements.length === 0 || !(e.currentTarget instanceof HTMLElement)) return; const rect = e.currentTarget.getBoundingClientRect(); const midpoint = rect.top + rect.height / 2; const isUpperHalf = e.clientY < midpoint; // Oblicz docelowy indeks let insertIndex = targetIndex; if (!isUpperHalf) { insertIndex = targetIndex + 1; } // Użyj nowej, centralnej funkcji do przesuwania warstw this.canvas.canvasLayers.moveLayers(this.draggedElements, { toIndex: insertIndex }); log.info(`Dropped ${this.draggedElements.length} layers at position ${insertIndex}`); } handleDragEnd(e) { this.removeDragInsertionLine(); if (!this.layersContainer) return; this.layersContainer.querySelectorAll('.layer-row').forEach((row) => { row.classList.remove('dragging'); }); this.draggedElements = []; } onLayersChanged() { this.renderLayers(); } updateSelectionAppearance() { if (!this.layersContainer) return; const sortedLayers = [...this.canvas.layers].sort((a, b) => b.zIndex - a.zIndex); const layerRows = this.layersContainer.querySelectorAll('.layer-row'); layerRows.forEach((row, index) => { const layer = sortedLayers[index]; if (this.canvas.canvasSelection.selectedLayers.includes(layer)) { row.classList.add('selected'); } else { row.classList.remove('selected'); } }); } /** * Aktualizuje stan przycisków w zależności od zaznaczenia warstw */ updateButtonStates() { if (!this.container) return; const deleteBtn = this.container.querySelector('#delete-layer-btn'); const hasSelectedLayers = this.canvas.canvasSelection.selectedLayers.length > 0; if (deleteBtn) { deleteBtn.disabled = !hasSelectedLayers; deleteBtn.title = hasSelectedLayers ? `Delete ${this.canvas.canvasSelection.selectedLayers.length} selected layer(s)` : 'No layers selected'; } log.debug(`Button states updated - delete button ${hasSelectedLayers ? 'enabled' : 'disabled'}`); } /** * Aktualizuje panel gdy zmieni się zaznaczenie (wywoływane z zewnątrz). * Zamiast pełnego renderowania, tylko aktualizujemy wygląd. */ onSelectionChanged() { this.updateSelectionAppearance(); this.updateButtonStates(); } destroy() { if (this.container && this.container.parentNode) { this.container.parentNode.removeChild(this.container); } this.container = null; this.layersContainer = null; this.draggedElements = []; this.removeDragInsertionLine(); log.info('CanvasLayersPanel destroyed'); } }