mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-25 14:25:44 -03:00
Revert "Refactor Canvas class as facade and clean up CanvasLayers"
This reverts commit 9dcf38b36d.
This commit is contained in:
657
js/Canvas.js
657
js/Canvas.js
@@ -13,14 +13,6 @@ import { mask_editor_showing } from "./utils/mask_utils.js";
|
|||||||
|
|
||||||
const log = createModuleLogger('Canvas');
|
const log = createModuleLogger('Canvas');
|
||||||
|
|
||||||
/**
|
|
||||||
* Canvas - Fasada dla systemu rysowania
|
|
||||||
*
|
|
||||||
* Klasa Canvas pełni rolę fasady, oferując uproszczony interfejs wysokiego poziomu
|
|
||||||
* dla złożonego systemu rysowania. Zamiast eksponować wszystkie metody modułów,
|
|
||||||
* udostępnia tylko kluczowe operacje i umożliwia bezpośredni dostęp do modułów
|
|
||||||
* gdy potrzebna jest bardziej szczegółowa kontrola.
|
|
||||||
*/
|
|
||||||
export class Canvas {
|
export class Canvas {
|
||||||
constructor(node, widget, callbacks = {}) {
|
constructor(node, widget, callbacks = {}) {
|
||||||
this.node = node;
|
this.node = node;
|
||||||
@@ -49,115 +41,187 @@ export class Canvas {
|
|||||||
|
|
||||||
this.dataInitialized = false;
|
this.dataInitialized = false;
|
||||||
this.pendingDataCheck = null;
|
this.pendingDataCheck = null;
|
||||||
this.imageCache = new Map();
|
|
||||||
|
|
||||||
// Inicjalizacja modułów
|
|
||||||
this._initializeModules(callbacks);
|
|
||||||
|
|
||||||
// Podstawowa konfiguracja
|
|
||||||
this._setupCanvas();
|
|
||||||
|
|
||||||
// Delegacja interaction dla kompatybilności wstecznej
|
|
||||||
this.interaction = this.canvasInteractions.interaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inicjalizuje moduły systemu canvas
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_initializeModules(callbacks) {
|
|
||||||
// Moduły są publiczne dla bezpośredniego dostępu gdy potrzebne
|
|
||||||
this.maskTool = new MaskTool(this, {onStateChange: this.onStateChange});
|
this.maskTool = new MaskTool(this, {onStateChange: this.onStateChange});
|
||||||
|
this.initCanvas();
|
||||||
this.canvasState = new CanvasState(this);
|
this.canvasState = new CanvasState(this);
|
||||||
this.canvasInteractions = new CanvasInteractions(this);
|
this.canvasInteractions = new CanvasInteractions(this);
|
||||||
this.canvasLayers = new CanvasLayers(this);
|
this.canvasLayers = new CanvasLayers(this);
|
||||||
this.canvasRenderer = new CanvasRenderer(this);
|
this.canvasRenderer = new CanvasRenderer(this);
|
||||||
this.canvasIO = new CanvasIO(this);
|
this.canvasIO = new CanvasIO(this);
|
||||||
this.imageReferenceManager = new ImageReferenceManager(this);
|
this.imageReferenceManager = new ImageReferenceManager(this);
|
||||||
}
|
this.interaction = this.canvasInteractions.interaction;
|
||||||
|
|
||||||
|
this.setupEventListeners();
|
||||||
|
this.initNodeData();
|
||||||
|
|
||||||
/**
|
|
||||||
* Konfiguruje podstawowe właściwości canvas
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_setupCanvas() {
|
|
||||||
this.initCanvas();
|
|
||||||
this.canvasInteractions.setupEventListeners();
|
|
||||||
this.canvasIO.initNodeData();
|
|
||||||
|
|
||||||
// Inicjalizacja warstw z domyślną przezroczystością
|
|
||||||
this.layers = this.layers.map(layer => ({
|
this.layers = this.layers.map(layer => ({
|
||||||
...layer,
|
...layer,
|
||||||
opacity: 1
|
opacity: 1
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
this.imageCache = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
async loadStateFromDB() {
|
||||||
// GŁÓWNE OPERACJE FASADY
|
return this.canvasState.loadStateFromDB();
|
||||||
// ==========================================
|
}
|
||||||
|
|
||||||
|
async saveStateToDB(immediate = false) {
|
||||||
|
return this.canvasState.saveStateToDB(immediate);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Ładuje stan canvas z bazy danych
|
|
||||||
*/
|
|
||||||
async loadInitialState() {
|
async loadInitialState() {
|
||||||
log.info("Loading initial state for node:", this.node.id);
|
log.info("Loading initial state for node:", this.node.id);
|
||||||
const loaded = await this.canvasState.loadStateFromDB();
|
const loaded = await this.loadStateFromDB();
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
log.info("No saved state found, initializing from node data.");
|
log.info("No saved state found, initializing from node data.");
|
||||||
await this.canvasIO.initNodeData();
|
await this.initNodeData();
|
||||||
}
|
}
|
||||||
this.saveState();
|
this.saveState();
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
_notifyStateChange() {
|
||||||
* Zapisuje obecny stan
|
if (this.onStateChange) {
|
||||||
* @param {boolean} replaceLast - Czy zastąpić ostatni stan w historii
|
this.onStateChange();
|
||||||
*/
|
}
|
||||||
|
}
|
||||||
|
|
||||||
saveState(replaceLast = false) {
|
saveState(replaceLast = false) {
|
||||||
this.canvasState.saveState(replaceLast);
|
this.canvasState.saveState(replaceLast);
|
||||||
this.incrementOperationCount();
|
this.incrementOperationCount();
|
||||||
this._notifyStateChange();
|
this._notifyStateChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Cofnij ostatnią operację
|
|
||||||
*/
|
|
||||||
undo() {
|
undo() {
|
||||||
this.canvasState.undo();
|
this.canvasState.undo();
|
||||||
this.incrementOperationCount();
|
this.incrementOperationCount();
|
||||||
this._notifyStateChange();
|
this._notifyStateChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Ponów cofniętą operację
|
|
||||||
*/
|
|
||||||
redo() {
|
redo() {
|
||||||
this.canvasState.redo();
|
this.canvasState.redo();
|
||||||
this.incrementOperationCount();
|
this.incrementOperationCount();
|
||||||
this._notifyStateChange();
|
this._notifyStateChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
updateSelectionAfterHistory() {
|
||||||
* Renderuje canvas
|
const newSelectedLayers = [];
|
||||||
*/
|
if (this.selectedLayers) {
|
||||||
render() {
|
this.selectedLayers.forEach(sl => {
|
||||||
this.canvasRenderer.render();
|
const found = this.layers.find(l => l.id === sl.id);
|
||||||
|
if (found) newSelectedLayers.push(found);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.updateSelection(newSelectedLayers);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
updateHistoryButtons() {
|
||||||
* Dodaje warstwę z obrazem
|
if (this.onHistoryChange) {
|
||||||
* @param {Image} image - Obraz do dodania
|
const historyInfo = this.canvasState.getHistoryInfo();
|
||||||
* @param {Object} layerProps - Właściwości warstwy
|
this.onHistoryChange({
|
||||||
* @param {string} addMode - Tryb dodawania
|
canUndo: historyInfo.canUndo,
|
||||||
*/
|
canRedo: historyInfo.canRedo
|
||||||
async addLayer(image, layerProps = {}, addMode = 'default') {
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initCanvas() {
|
||||||
|
this.canvas.width = this.width;
|
||||||
|
this.canvas.height = this.height;
|
||||||
|
this.canvas.style.border = '1px solid black';
|
||||||
|
this.canvas.style.maxWidth = '100%';
|
||||||
|
this.canvas.style.backgroundColor = '#606060';
|
||||||
|
this.canvas.style.width = '100%';
|
||||||
|
this.canvas.style.height = '100%';
|
||||||
|
|
||||||
|
|
||||||
|
this.canvas.tabIndex = 0;
|
||||||
|
this.canvas.style.outline = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
this.canvasInteractions.setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSelection(newSelection) {
|
||||||
|
this.selectedLayers = newSelection || [];
|
||||||
|
this.selectedLayer = this.selectedLayers.length > 0 ? this.selectedLayers[this.selectedLayers.length - 1] : null;
|
||||||
|
if (this.onSelectionChange) {
|
||||||
|
this.onSelectionChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async copySelectedLayers() {
|
||||||
|
return this.canvasLayers.copySelectedLayers();
|
||||||
|
}
|
||||||
|
|
||||||
|
pasteLayers() {
|
||||||
|
return this.canvasLayers.pasteLayers();
|
||||||
|
}
|
||||||
|
|
||||||
|
async handlePaste(addMode) {
|
||||||
|
return this.canvasLayers.handlePaste(addMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
handleMouseMove(e) {
|
||||||
|
this.canvasInteractions.handleMouseMove(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
handleMouseUp(e) {
|
||||||
|
this.canvasInteractions.handleMouseUp(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
handleMouseLeave(e) {
|
||||||
|
this.canvasInteractions.handleMouseLeave(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
handleWheel(e) {
|
||||||
|
this.canvasInteractions.handleWheel(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyDown(e) {
|
||||||
|
this.canvasInteractions.handleKeyDown(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyUp(e) {
|
||||||
|
this.canvasInteractions.handleKeyUp(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
isRotationHandle(x, y) {
|
||||||
|
return this.canvasLayers.isRotationHandle(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addLayerWithImage(image, layerProps = {}, addMode = 'default') {
|
||||||
return this.canvasLayers.addLayerWithImage(image, layerProps, addMode);
|
return this.canvasLayers.addLayerWithImage(image, layerProps, addMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Usuwa wybrane warstwy
|
async addLayer(image, addMode = 'default') {
|
||||||
*/
|
return this.addLayerWithImage(image, {}, addMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeLayer(index) {
|
||||||
|
if (index >= 0 && index < this.layers.length) {
|
||||||
|
const layer = this.layers[index];
|
||||||
|
if (layer.imageId) {
|
||||||
|
const isImageUsedElsewhere = this.layers.some((l, i) => i !== index && l.imageId === layer.imageId);
|
||||||
|
if (!isImageUsedElsewhere) {
|
||||||
|
await removeImage(layer.imageId);
|
||||||
|
this.imageCache.delete(layer.imageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.layers.splice(index, 1);
|
||||||
|
this.selectedLayer = this.layers[this.layers.length - 1] || null;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
removeSelectedLayers() {
|
removeSelectedLayers() {
|
||||||
if (this.selectedLayers.length > 0) {
|
if (this.selectedLayers.length > 0) {
|
||||||
this.saveState();
|
this.saveState();
|
||||||
@@ -168,49 +232,239 @@ export class Canvas {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getMouseWorldCoordinates(e) {
|
||||||
* Aktualizuje zaznaczenie warstw
|
const rect = this.canvas.getBoundingClientRect();
|
||||||
* @param {Array} newSelection - Nowa lista zaznaczonych warstw
|
|
||||||
*/
|
const mouseX_DOM = e.clientX - rect.left;
|
||||||
updateSelection(newSelection) {
|
const mouseY_DOM = e.clientY - rect.top;
|
||||||
this.selectedLayers = newSelection || [];
|
|
||||||
this.selectedLayer = this.selectedLayers.length > 0 ? this.selectedLayers[this.selectedLayers.length - 1] : null;
|
const scaleX = this.offscreenCanvas.width / rect.width;
|
||||||
if (this.onSelectionChange) {
|
const scaleY = this.offscreenCanvas.height / rect.height;
|
||||||
this.onSelectionChange();
|
|
||||||
}
|
const mouseX_Buffer = mouseX_DOM * scaleX;
|
||||||
|
const mouseY_Buffer = mouseY_DOM * scaleY;
|
||||||
|
|
||||||
|
const worldX = (mouseX_Buffer / this.viewport.zoom) + this.viewport.x;
|
||||||
|
const worldY = (mouseY_Buffer / this.viewport.zoom) + this.viewport.y;
|
||||||
|
|
||||||
|
return {x: worldX, y: worldY};
|
||||||
|
}
|
||||||
|
|
||||||
|
getMouseViewCoordinates(e) {
|
||||||
|
const rect = this.canvas.getBoundingClientRect();
|
||||||
|
const mouseX_DOM = e.clientX - rect.left;
|
||||||
|
const mouseY_DOM = e.clientY - rect.top;
|
||||||
|
|
||||||
|
const scaleX = this.canvas.width / rect.width;
|
||||||
|
const scaleY = this.canvas.height / rect.height;
|
||||||
|
|
||||||
|
const mouseX_Canvas = mouseX_DOM * scaleX;
|
||||||
|
const mouseY_Canvas = mouseY_DOM * scaleY;
|
||||||
|
|
||||||
|
return { x: mouseX_Canvas, y: mouseY_Canvas };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
moveLayer(fromIndex, toIndex) {
|
||||||
|
return this.canvasLayers.moveLayer(fromIndex, toIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
resizeLayer(scale) {
|
||||||
|
this.selectedLayers.forEach(layer => {
|
||||||
|
layer.width *= scale;
|
||||||
|
layer.height *= scale;
|
||||||
|
});
|
||||||
|
this.render();
|
||||||
|
this.saveState();
|
||||||
|
}
|
||||||
|
|
||||||
|
rotateLayer(angle) {
|
||||||
|
this.selectedLayers.forEach(layer => {
|
||||||
|
layer.rotation += angle;
|
||||||
|
});
|
||||||
|
this.render();
|
||||||
|
this.saveState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Zmienia rozmiar obszaru wyjściowego
|
|
||||||
* @param {number} width - Nowa szerokość
|
|
||||||
* @param {number} height - Nowa wysokość
|
|
||||||
* @param {boolean} saveHistory - Czy zapisać w historii
|
|
||||||
*/
|
|
||||||
updateOutputAreaSize(width, height, saveHistory = true) {
|
updateOutputAreaSize(width, height, saveHistory = true) {
|
||||||
return this.canvasLayers.updateOutputAreaSize(width, height, saveHistory);
|
return this.canvasLayers.updateOutputAreaSize(width, height, saveHistory);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
render() {
|
||||||
* Eksportuje spłaszczony canvas jako blob
|
this.canvasRenderer.render();
|
||||||
*/
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getHandles(layer) {
|
||||||
|
return this.canvasLayers.getHandles(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
getHandleAtPosition(worldX, worldY) {
|
||||||
|
return this.canvasLayers.getHandleAtPosition(worldX, worldY);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async getFlattenedCanvasAsBlob() {
|
async getFlattenedCanvasAsBlob() {
|
||||||
return this.canvasLayers.getFlattenedCanvasAsBlob();
|
return this.canvasLayers.getFlattenedCanvasAsBlob();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
async getFlattenedSelectionAsBlob() {
|
||||||
* Importuje najnowszy obraz
|
return this.canvasLayers.getFlattenedSelectionAsBlob();
|
||||||
*/
|
}
|
||||||
|
|
||||||
|
moveLayerUp() {
|
||||||
|
return this.canvasLayers.moveLayerUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
moveLayerDown() {
|
||||||
|
return this.canvasLayers.moveLayerDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getLayerAtPosition(worldX, worldY) {
|
||||||
|
return this.canvasLayers.getLayerAtPosition(worldX, worldY);
|
||||||
|
}
|
||||||
|
|
||||||
|
getResizeHandle(x, y) {
|
||||||
|
return this.canvasLayers.getResizeHandle(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
async mirrorHorizontal() {
|
||||||
|
return this.canvasLayers.mirrorHorizontal();
|
||||||
|
}
|
||||||
|
|
||||||
|
async mirrorVertical() {
|
||||||
|
return this.canvasLayers.mirrorVertical();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLayerImageData(layer) {
|
||||||
|
return this.canvasLayers.getLayerImageData(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
addMattedLayer(image, mask) {
|
||||||
|
return this.canvasLayers.addMattedLayer(image, mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addInputToCanvas(inputImage, inputMask) {
|
||||||
|
return this.canvasIO.addInputToCanvas(inputImage, inputMask);
|
||||||
|
}
|
||||||
|
|
||||||
|
async convertTensorToImage(tensor) {
|
||||||
|
return this.canvasIO.convertTensorToImage(tensor);
|
||||||
|
}
|
||||||
|
|
||||||
|
async convertTensorToMask(tensor) {
|
||||||
|
return this.canvasIO.convertTensorToMask(tensor);
|
||||||
|
}
|
||||||
|
|
||||||
|
async initNodeData() {
|
||||||
|
return this.canvasIO.initNodeData();
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleDataCheck() {
|
||||||
|
return this.canvasIO.scheduleDataCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
async processImageData(imageData) {
|
||||||
|
return this.canvasIO.processImageData(imageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
addScaledLayer(image, scale) {
|
||||||
|
return this.canvasIO.addScaledLayer(image, scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
convertTensorToImageData(tensor) {
|
||||||
|
return this.canvasIO.convertTensorToImageData(tensor);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createImageFromData(imageData) {
|
||||||
|
return this.canvasIO.createImageFromData(imageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async retryDataLoad(maxRetries = 3, delay = 1000) {
|
||||||
|
return this.canvasIO.retryDataLoad(maxRetries, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
async processMaskData(maskData) {
|
||||||
|
return this.canvasIO.processMaskData(maskData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadImageFromCache(base64Data) {
|
||||||
|
return this.canvasIO.loadImageFromCache(base64Data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async importImage(cacheData) {
|
||||||
|
return this.canvasIO.importImage(cacheData);
|
||||||
|
}
|
||||||
|
|
||||||
async importLatestImage() {
|
async importLatestImage() {
|
||||||
return this.canvasIO.importLatestImage();
|
return this.canvasIO.importLatestImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
showBlendModeMenu(x, y) {
|
||||||
// OPERACJE NA MASCE
|
return this.canvasLayers.showBlendModeMenu(x, y);
|
||||||
// ==========================================
|
}
|
||||||
|
|
||||||
|
handleBlendModeSelection(mode) {
|
||||||
|
return this.canvasLayers.handleBlendModeSelection(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
showOpacitySlider(mode) {
|
||||||
|
return this.canvasLayers.showOpacitySlider(mode);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uruchamia edytor masek
|
* Zwiększa licznik operacji (wywoływane przy każdej operacji na canvas)
|
||||||
*/
|
*/
|
||||||
|
incrementOperationCount() {
|
||||||
|
if (this.imageReferenceManager) {
|
||||||
|
this.imageReferenceManager.incrementOperationCount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ręczne uruchomienie garbage collection
|
||||||
|
*/
|
||||||
|
async runGarbageCollection() {
|
||||||
|
if (this.imageReferenceManager) {
|
||||||
|
await this.imageReferenceManager.manualGarbageCollection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zwraca statystyki garbage collection
|
||||||
|
*/
|
||||||
|
getGarbageCollectionStats() {
|
||||||
|
if (this.imageReferenceManager) {
|
||||||
|
const stats = this.imageReferenceManager.getStats();
|
||||||
|
return {
|
||||||
|
...stats,
|
||||||
|
operationCount: this.imageReferenceManager.operationCount,
|
||||||
|
operationThreshold: this.imageReferenceManager.operationThreshold
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ustawia próg operacji dla automatycznego GC
|
||||||
|
*/
|
||||||
|
setGarbageCollectionThreshold(threshold) {
|
||||||
|
if (this.imageReferenceManager) {
|
||||||
|
this.imageReferenceManager.setOperationThreshold(threshold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Czyści zasoby canvas (wywoływane przy usuwaniu)
|
||||||
|
*/
|
||||||
|
destroy() {
|
||||||
|
if (this.imageReferenceManager) {
|
||||||
|
this.imageReferenceManager.destroy();
|
||||||
|
}
|
||||||
|
log.info("Canvas destroyed");
|
||||||
|
}
|
||||||
|
|
||||||
async startMaskEditor() {
|
async startMaskEditor() {
|
||||||
const blob = await this.canvasLayers.getFlattenedCanvasAsBlob();
|
const blob = await this.canvasLayers.getFlattenedCanvasAsBlob();
|
||||||
if (!blob) {
|
if (!blob) {
|
||||||
@@ -257,133 +511,16 @@ export class Canvas {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// METODY POMOCNICZE
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inicjalizuje podstawowe właściwości canvas
|
|
||||||
*/
|
|
||||||
initCanvas() {
|
|
||||||
this.canvas.width = this.width;
|
|
||||||
this.canvas.height = this.height;
|
|
||||||
this.canvas.style.border = '1px solid black';
|
|
||||||
this.canvas.style.maxWidth = '100%';
|
|
||||||
this.canvas.style.backgroundColor = '#606060';
|
|
||||||
this.canvas.style.width = '100%';
|
|
||||||
this.canvas.style.height = '100%';
|
|
||||||
this.canvas.tabIndex = 0;
|
|
||||||
this.canvas.style.outline = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pobiera współrzędne myszy w układzie świata
|
|
||||||
* @param {MouseEvent} e - Zdarzenie myszy
|
|
||||||
*/
|
|
||||||
getMouseWorldCoordinates(e) {
|
|
||||||
const rect = this.canvas.getBoundingClientRect();
|
|
||||||
|
|
||||||
const mouseX_DOM = e.clientX - rect.left;
|
|
||||||
const mouseY_DOM = e.clientY - rect.top;
|
|
||||||
|
|
||||||
const scaleX = this.offscreenCanvas.width / rect.width;
|
|
||||||
const scaleY = this.offscreenCanvas.height / rect.height;
|
|
||||||
|
|
||||||
const mouseX_Buffer = mouseX_DOM * scaleX;
|
|
||||||
const mouseY_Buffer = mouseY_DOM * scaleY;
|
|
||||||
|
|
||||||
const worldX = (mouseX_Buffer / this.viewport.zoom) + this.viewport.x;
|
|
||||||
const worldY = (mouseY_Buffer / this.viewport.zoom) + this.viewport.y;
|
|
||||||
|
|
||||||
return {x: worldX, y: worldY};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pobiera współrzędne myszy w układzie widoku
|
|
||||||
* @param {MouseEvent} e - Zdarzenie myszy
|
|
||||||
*/
|
|
||||||
getMouseViewCoordinates(e) {
|
|
||||||
const rect = this.canvas.getBoundingClientRect();
|
|
||||||
const mouseX_DOM = e.clientX - rect.left;
|
|
||||||
const mouseY_DOM = e.clientY - rect.top;
|
|
||||||
|
|
||||||
const scaleX = this.canvas.width / rect.width;
|
|
||||||
const scaleY = this.canvas.height / rect.height;
|
|
||||||
|
|
||||||
const mouseX_Canvas = mouseX_DOM * scaleX;
|
|
||||||
const mouseY_Canvas = mouseY_DOM * scaleY;
|
|
||||||
|
|
||||||
return { x: mouseX_Canvas, y: mouseY_Canvas };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Aktualizuje zaznaczenie po operacji historii
|
|
||||||
*/
|
|
||||||
updateSelectionAfterHistory() {
|
|
||||||
const newSelectedLayers = [];
|
|
||||||
if (this.selectedLayers) {
|
|
||||||
this.selectedLayers.forEach(sl => {
|
|
||||||
const found = this.layers.find(l => l.id === sl.id);
|
|
||||||
if (found) newSelectedLayers.push(found);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.updateSelection(newSelectedLayers);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Aktualizuje przyciski historii
|
|
||||||
*/
|
|
||||||
updateHistoryButtons() {
|
|
||||||
if (this.onHistoryChange) {
|
|
||||||
const historyInfo = this.canvasState.getHistoryInfo();
|
|
||||||
this.onHistoryChange({
|
|
||||||
canUndo: historyInfo.canUndo,
|
|
||||||
canRedo: historyInfo.canRedo
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Zwiększa licznik operacji (dla garbage collection)
|
|
||||||
*/
|
|
||||||
incrementOperationCount() {
|
|
||||||
if (this.imageReferenceManager) {
|
|
||||||
this.imageReferenceManager.incrementOperationCount();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Czyści zasoby canvas
|
|
||||||
*/
|
|
||||||
destroy() {
|
|
||||||
if (this.imageReferenceManager) {
|
|
||||||
this.imageReferenceManager.destroy();
|
|
||||||
}
|
|
||||||
log.info("Canvas destroyed");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Powiadamia o zmianie stanu
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_notifyStateChange() {
|
|
||||||
if (this.onStateChange) {
|
|
||||||
this.onStateChange();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// METODY DLA EDYTORA MASEK
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
waitWhileMaskEditing() {
|
waitWhileMaskEditing() {
|
||||||
|
// Czekamy, aż edytor się pojawi, a potem zniknie.
|
||||||
if (mask_editor_showing(app)) {
|
if (mask_editor_showing(app)) {
|
||||||
this.editorWasShowing = true;
|
this.editorWasShowing = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mask_editor_showing(app) && this.editorWasShowing) {
|
if (!mask_editor_showing(app) && this.editorWasShowing) {
|
||||||
|
// Edytor był widoczny i już go nie ma
|
||||||
this.editorWasShowing = false;
|
this.editorWasShowing = false;
|
||||||
setTimeout(() => this.handleMaskEditorClose(), 100);
|
setTimeout(() => this.handleMaskEditorClose(), 100); // Dajemy chwilę na aktualizację
|
||||||
} else {
|
} else {
|
||||||
setTimeout(this.waitWhileMaskEditing.bind(this), 100);
|
setTimeout(this.waitWhileMaskEditing.bind(this), 100);
|
||||||
}
|
}
|
||||||
@@ -410,22 +547,28 @@ export class Canvas {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Używamy wymiarów naszego płótna, aby zapewnić spójność
|
||||||
const tempCanvas = document.createElement('canvas');
|
const tempCanvas = document.createElement('canvas');
|
||||||
tempCanvas.width = this.width;
|
tempCanvas.width = this.width;
|
||||||
tempCanvas.height = this.height;
|
tempCanvas.height = this.height;
|
||||||
const tempCtx = tempCanvas.getContext('2d');
|
const tempCtx = tempCanvas.getContext('2d');
|
||||||
|
|
||||||
|
// Rysujemy obrazek z edytora, który zawiera maskę w kanale alfa
|
||||||
tempCtx.drawImage(resultImage, 0, 0, this.width, this.height);
|
tempCtx.drawImage(resultImage, 0, 0, this.width, this.height);
|
||||||
|
|
||||||
const imageData = tempCtx.getImageData(0, 0, this.width, this.height);
|
const imageData = tempCtx.getImageData(0, 0, this.width, this.height);
|
||||||
const data = imageData.data;
|
const data = imageData.data;
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i += 4) {
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
const originalAlpha = data[i + 3];
|
const originalAlpha = data[i + 3];
|
||||||
data[i] = 255;
|
|
||||||
data[i + 1] = 255;
|
// Ustawiamy biały kolor
|
||||||
data[i + 2] = 255;
|
data[i] = 255; // R
|
||||||
data[i + 3] = 255 - originalAlpha;
|
data[i + 1] = 255; // G
|
||||||
|
data[i + 2] = 255; // B
|
||||||
|
|
||||||
|
// Odwracamy kanał alfa
|
||||||
|
data[i + 3] = 255 - originalAlpha;
|
||||||
}
|
}
|
||||||
|
|
||||||
tempCtx.putImageData(imageData, 0, 0);
|
tempCtx.putImageData(imageData, 0, 0);
|
||||||
@@ -434,17 +577,19 @@ export class Canvas {
|
|||||||
maskAsImage.src = tempCanvas.toDataURL();
|
maskAsImage.src = tempCanvas.toDataURL();
|
||||||
await new Promise(resolve => maskAsImage.onload = resolve);
|
await new Promise(resolve => maskAsImage.onload = resolve);
|
||||||
|
|
||||||
|
// Łączymy nową maskę z istniejącą, zamiast ją nadpisywać
|
||||||
const maskCtx = this.maskTool.maskCtx;
|
const maskCtx = this.maskTool.maskCtx;
|
||||||
const destX = -this.maskTool.x;
|
const destX = -this.maskTool.x;
|
||||||
const destY = -this.maskTool.y;
|
const destY = -this.maskTool.y;
|
||||||
|
|
||||||
maskCtx.globalCompositeOperation = 'screen';
|
maskCtx.globalCompositeOperation = 'screen';
|
||||||
maskCtx.drawImage(maskAsImage, destX, destY);
|
maskCtx.drawImage(maskAsImage, destX, destY);
|
||||||
maskCtx.globalCompositeOperation = 'source-over';
|
maskCtx.globalCompositeOperation = 'source-over'; // Przywracamy domyślny tryb
|
||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
this.saveState();
|
this.saveState();
|
||||||
|
|
||||||
|
// Zaktualizuj podgląd węzła nowym, spłaszczonym obrazem
|
||||||
const new_preview = new Image();
|
const new_preview = new Image();
|
||||||
const blob = await this.canvasLayers.getFlattenedCanvasAsBlob();
|
const blob = await this.canvasLayers.getFlattenedCanvasAsBlob();
|
||||||
if (blob) {
|
if (blob) {
|
||||||
@@ -457,72 +602,4 @@ export class Canvas {
|
|||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// METODY DELEGUJĄCE DLA KOMPATYBILNOŚCI
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Te metody są zachowane tymczasowo dla kompatybilności wstecznej.
|
|
||||||
* W nowych implementacjach należy używać bezpośrednio odpowiednich modułów:
|
|
||||||
* - this.canvasLayers dla operacji na warstwach
|
|
||||||
* - this.canvasInteractions dla obsługi interakcji
|
|
||||||
* - this.canvasIO dla operacji I/O
|
|
||||||
* - this.canvasState dla zarządzania stanem
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Delegacje do CanvasState
|
|
||||||
async saveStateToDB(immediate = false) { return this.canvasState.saveStateToDB(immediate); }
|
|
||||||
|
|
||||||
// Delegacje do CanvasLayers
|
|
||||||
async copySelectedLayers() { return this.canvasLayers.copySelectedLayers(); }
|
|
||||||
async handlePaste(addMode) { return this.canvasLayers.handlePaste(addMode); }
|
|
||||||
async addLayerWithImage(image, layerProps = {}, addMode = 'default') {
|
|
||||||
return this.canvasLayers.addLayerWithImage(image, layerProps, addMode);
|
|
||||||
}
|
|
||||||
moveLayerUp() { return this.canvasLayers.moveLayerUp(); }
|
|
||||||
moveLayerDown() { return this.canvasLayers.moveLayerDown(); }
|
|
||||||
resizeLayer(scale) {
|
|
||||||
this.selectedLayers.forEach(layer => {
|
|
||||||
layer.width *= scale;
|
|
||||||
layer.height *= scale;
|
|
||||||
});
|
|
||||||
this.render();
|
|
||||||
this.saveState();
|
|
||||||
}
|
|
||||||
rotateLayer(angle) {
|
|
||||||
this.selectedLayers.forEach(layer => {
|
|
||||||
layer.rotation += angle;
|
|
||||||
});
|
|
||||||
this.render();
|
|
||||||
this.saveState();
|
|
||||||
}
|
|
||||||
getLayerAtPosition(worldX, worldY) { return this.canvasLayers.getLayerAtPosition(worldX, worldY); }
|
|
||||||
getHandles(layer) { return this.canvasLayers.getHandles(layer); }
|
|
||||||
getHandleAtPosition(worldX, worldY) { return this.canvasLayers.getHandleAtPosition(worldX, worldY); }
|
|
||||||
async mirrorHorizontal() { return this.canvasLayers.mirrorHorizontal(); }
|
|
||||||
async mirrorVertical() { return this.canvasLayers.mirrorVertical(); }
|
|
||||||
async getLayerImageData(layer) { return this.canvasLayers.getLayerImageData(layer); }
|
|
||||||
showBlendModeMenu(x, y) { return this.canvasLayers.showBlendModeMenu(x, y); }
|
|
||||||
// Delegacje do CanvasInteractions
|
|
||||||
handleMouseMove(e) { this.canvasInteractions.handleMouseMove(e); }
|
|
||||||
|
|
||||||
|
|
||||||
// Delegacje do ImageReferenceManager
|
|
||||||
async runGarbageCollection() {
|
|
||||||
if (this.imageReferenceManager) {
|
|
||||||
await this.imageReferenceManager.manualGarbageCollection();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
getGarbageCollectionStats() {
|
|
||||||
if (this.imageReferenceManager) {
|
|
||||||
const stats = this.imageReferenceManager.getStats();
|
|
||||||
return {
|
|
||||||
...stats,
|
|
||||||
operationCount: this.imageReferenceManager.operationCount,
|
|
||||||
operationThreshold: this.imageReferenceManager.operationThreshold
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,6 +162,32 @@ export class CanvasLayers {
|
|||||||
return this.addLayerWithImage(image);
|
return this.addLayerWithImage(image);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async removeLayer(index) {
|
||||||
|
if (index >= 0 && index < this.canvasLayers.layers.length) {
|
||||||
|
const layer = this.canvasLayers.layers[index];
|
||||||
|
if (layer.imageId) {
|
||||||
|
const isImageUsedElsewhere = this.canvasLayers.layers.some((l, i) => i !== index && l.imageId === layer.imageId);
|
||||||
|
if (!isImageUsedElsewhere) {
|
||||||
|
await removeImage(layer.imageId);
|
||||||
|
this.canvasLayers.imageCache.delete(layer.imageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.canvasLayers.layers.splice(index, 1);
|
||||||
|
this.canvasLayers.selectedLayer = this.canvasLayers.layers[this.canvasLayers.layers.length - 1] || null;
|
||||||
|
this.canvasLayers.render();
|
||||||
|
this.canvasLayers.saveState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
moveLayer(fromIndex, toIndex) {
|
||||||
|
if (fromIndex >= 0 && fromIndex < this.canvasLayers.layers.length &&
|
||||||
|
toIndex >= 0 && toIndex < this.canvasLayers.layers.length) {
|
||||||
|
const layer = this.canvasLayers.layers.splice(fromIndex, 1)[0];
|
||||||
|
this.canvasLayers.layers.splice(toIndex, 0, layer);
|
||||||
|
this.canvasLayers.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
moveLayerUp() {
|
moveLayerUp() {
|
||||||
if (this.canvasLayers.selectedLayers.length === 0) return;
|
if (this.canvasLayers.selectedLayers.length === 0) return;
|
||||||
const selectedIndicesSet = new Set(this.canvasLayers.selectedLayers.map(layer => this.canvasLayers.layers.indexOf(layer)));
|
const selectedIndicesSet = new Set(this.canvasLayers.selectedLayers.map(layer => this.canvasLayers.layers.indexOf(layer)));
|
||||||
@@ -335,6 +361,33 @@ export class CanvasLayers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addMattedLayer(image, mask) {
|
||||||
|
const layer = {
|
||||||
|
image: image,
|
||||||
|
mask: mask,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: image.width,
|
||||||
|
height: image.height,
|
||||||
|
rotation: 0,
|
||||||
|
zIndex: this.canvasLayers.layers.length
|
||||||
|
};
|
||||||
|
|
||||||
|
this.canvasLayers.layers.push(layer);
|
||||||
|
this.canvasLayers.selectedLayer = layer;
|
||||||
|
this.canvasLayers.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
isRotationHandle(x, y) {
|
||||||
|
if (!this.canvasLayers.selectedLayer) return false;
|
||||||
|
|
||||||
|
const handleX = this.canvasLayers.selectedLayer.x + this.canvasLayers.selectedLayer.width / 2;
|
||||||
|
const handleY = this.canvasLayers.selectedLayer.y - 20;
|
||||||
|
const handleRadius = 5;
|
||||||
|
|
||||||
|
return Math.sqrt(Math.pow(x - handleX, 2) + Math.pow(y - handleY, 2)) <= handleRadius;
|
||||||
|
}
|
||||||
|
|
||||||
getHandles(layer) {
|
getHandles(layer) {
|
||||||
if (!layer) return {};
|
if (!layer) return {};
|
||||||
|
|
||||||
@@ -389,6 +442,34 @@ export class CanvasLayers {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getResizeHandle(x, y) {
|
||||||
|
if (!this.canvasLayers.selectedLayer) return null;
|
||||||
|
|
||||||
|
const handleRadius = 5;
|
||||||
|
const handles = {
|
||||||
|
'nw': {x: this.canvasLayers.selectedLayer.x, y: this.canvasLayers.selectedLayer.y},
|
||||||
|
'ne': {
|
||||||
|
x: this.canvasLayers.selectedLayer.x + this.canvasLayers.selectedLayer.width,
|
||||||
|
y: this.canvasLayers.selectedLayer.y
|
||||||
|
},
|
||||||
|
'se': {
|
||||||
|
x: this.canvasLayers.selectedLayer.x + this.canvasLayers.selectedLayer.width,
|
||||||
|
y: this.canvasLayers.selectedLayer.y + this.canvasLayers.selectedLayer.height
|
||||||
|
},
|
||||||
|
'sw': {
|
||||||
|
x: this.canvasLayers.selectedLayer.x,
|
||||||
|
y: this.canvasLayers.selectedLayer.y + this.canvasLayers.selectedLayer.height
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [position, point] of Object.entries(handles)) {
|
||||||
|
if (Math.sqrt(Math.pow(x - point.x, 2) + Math.pow(y - point.y, 2)) <= handleRadius) {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
showBlendModeMenu(x, y) {
|
showBlendModeMenu(x, y) {
|
||||||
this.closeBlendModeMenu();
|
this.closeBlendModeMenu();
|
||||||
|
|
||||||
@@ -510,6 +591,17 @@ export class CanvasLayers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleBlendModeSelection(mode) {
|
||||||
|
if (this.selectedBlendMode === mode && !this.isAdjustingOpacity) {
|
||||||
|
this.applyBlendMode(mode, this.blendOpacity);
|
||||||
|
this.closeBlendModeMenu();
|
||||||
|
} else {
|
||||||
|
this.selectedBlendMode = mode;
|
||||||
|
this.isAdjustingOpacity = true;
|
||||||
|
this.showOpacitySlider(mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
showOpacitySlider(mode) {
|
showOpacitySlider(mode) {
|
||||||
const slider = document.createElement('input');
|
const slider = document.createElement('input');
|
||||||
slider.type = 'range';
|
slider.type = 'range';
|
||||||
@@ -642,4 +734,36 @@ export class CanvasLayers {
|
|||||||
}, 'image/png');
|
}, 'image/png');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getFlattenedCanvasAsDataURL() {
|
||||||
|
if (this.canvasLayers.layers.length === 0) return null;
|
||||||
|
|
||||||
|
const tempCanvas = document.createElement('canvas');
|
||||||
|
tempCanvas.width = this.canvasLayers.width;
|
||||||
|
tempCanvas.height = this.canvasLayers.height;
|
||||||
|
const tempCtx = tempCanvas.getContext('2d');
|
||||||
|
|
||||||
|
const sortedLayers = [...this.canvasLayers.layers].sort((a, b) => a.zIndex - b.zIndex);
|
||||||
|
sortedLayers.forEach(layer => {
|
||||||
|
if (!layer.image) return;
|
||||||
|
|
||||||
|
tempCtx.save();
|
||||||
|
tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
|
||||||
|
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
|
||||||
|
const centerX = layer.x + layer.width / 2;
|
||||||
|
const centerY = layer.y + layer.height / 2;
|
||||||
|
tempCtx.translate(centerX, centerY);
|
||||||
|
tempCtx.rotate(layer.rotation * Math.PI / 180);
|
||||||
|
tempCtx.drawImage(
|
||||||
|
layer.image,
|
||||||
|
-layer.width / 2,
|
||||||
|
-layer.height / 2,
|
||||||
|
layer.width,
|
||||||
|
layer.height
|
||||||
|
);
|
||||||
|
tempCtx.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
return tempCanvas.toDataURL('image/png');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user