Layer fix

This commit is contained in:
Dariusz L
2025-06-20 21:59:01 +02:00
parent beb89ed612
commit 7b0400a187
2 changed files with 43 additions and 98 deletions

View File

@@ -17,7 +17,7 @@ export class Canvas {
zoom: 0.8, zoom: 0.8,
}; };
this.interaction = { this.interaction = {
mode: 'none', // 'none', 'panning', 'dragging', 'resizing', 'rotating', 'resizingCanvas' mode: 'none',
panStart: {x: 0, y: 0}, panStart: {x: 0, y: 0},
dragStart: {x: 0, y: 0}, dragStart: {x: 0, y: 0},
transformOrigin: {}, transformOrigin: {},
@@ -25,8 +25,8 @@ export class Canvas {
resizeAnchor: {x: 0, y: 0}, resizeAnchor: {x: 0, y: 0},
canvasResizeStart: {x: 0, y: 0}, canvasResizeStart: {x: 0, y: 0},
isCtrlPressed: false, isCtrlPressed: false,
isAltPressed: false, // <-- DODANO isAltPressed: false,
hasClonedInDrag: false, // <-- DODANO hasClonedInDrag: false,
lastClickTime: 0, lastClickTime: 0,
}; };
this.originalLayerPositions = new Map(); this.originalLayerPositions = new Map();
@@ -82,7 +82,6 @@ export class Canvas {
} }
setupEventListeners() { setupEventListeners() {
// Używamy .bind(this), aby upewnić się, że 'this' wewnątrz handlerów odnosi się do instancji klasy Canvas
this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this)); this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this)); this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
this.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this)); this.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
@@ -96,8 +95,6 @@ export class Canvas {
updateSelection(newSelection) { updateSelection(newSelection) {
this.selectedLayers = newSelection || []; this.selectedLayers = newSelection || [];
this.selectedLayer = this.selectedLayers.length > 0 ? this.selectedLayers[this.selectedLayers.length - 1] : null; this.selectedLayer = this.selectedLayers.length > 0 ? this.selectedLayers[this.selectedLayers.length - 1] : null;
// Wywołaj callback, jeśli istnieje, aby zaktualizować UI
if (this.onSelectionChange) { if (this.onSelectionChange) {
this.onSelectionChange(); this.onSelectionChange();
} }
@@ -111,7 +108,7 @@ export class Canvas {
this.interaction.resizeHandle = null; this.interaction.resizeHandle = null;
this.originalLayerPositions.clear(); this.originalLayerPositions.clear();
this.canvasResizeRect = null; this.canvasResizeRect = null;
this.interaction.hasClonedInDrag = false; // <-- DODANO this.interaction.hasClonedInDrag = false;
this.canvas.style.cursor = 'default'; this.canvas.style.cursor = 'default';
} }
@@ -121,8 +118,6 @@ export class Canvas {
handleMouseDown(e) { handleMouseDown(e) {
const currentTime = Date.now(); const currentTime = Date.now();
const worldCoords = this.getMouseWorldCoordinates(e); const worldCoords = this.getMouseWorldCoordinates(e);
// Deselekcja po szybkim kliknięciu (pseudo-double-click)
if (currentTime - this.interaction.lastClickTime < 300) { if (currentTime - this.interaction.lastClickTime < 300) {
this.updateSelection([]); this.updateSelection([]);
this.selectedLayer = null; this.selectedLayer = null;
@@ -133,16 +128,12 @@ export class Canvas {
this.interaction.lastClickTime = currentTime; this.interaction.lastClickTime = currentTime;
const handle = this.getHandleAtPosition(worldCoords.x, worldCoords.y); const handle = this.getHandleAtPosition(worldCoords.x, worldCoords.y);
// 1. Interakcja z uchwytem (skalowanie/rotacja)
if (this.selectedLayer && handle) { if (this.selectedLayer && handle) {
this.startLayerTransform(handle, worldCoords); this.startLayerTransform(handle, worldCoords);
return; return;
} }
const clickedLayerResult = this.getLayerAtPosition(worldCoords.x, worldCoords.y); const clickedLayerResult = this.getLayerAtPosition(worldCoords.x, worldCoords.y);
// 2. Interakcja z warstwą (przesuwanie/selekcja)
if (clickedLayerResult) { if (clickedLayerResult) {
if (e.shiftKey && this.selectedLayers.includes(clickedLayerResult.layer)) { if (e.shiftKey && this.selectedLayers.includes(clickedLayerResult.layer)) {
this.showBlendModeMenu(e.clientX, e.clientY); this.showBlendModeMenu(e.clientX, e.clientY);
@@ -151,8 +142,6 @@ export class Canvas {
this.startLayerDrag(clickedLayerResult.layer, worldCoords); this.startLayerDrag(clickedLayerResult.layer, worldCoords);
return; return;
} }
// 3. Interakcja z tłem (zmiana rozmiaru canvasu lub panoramowanie)
if (e.shiftKey) { if (e.shiftKey) {
this.startCanvasResize(worldCoords); this.startCanvasResize(worldCoords);
} else { } else {
@@ -221,9 +210,9 @@ export class Canvas {
const rotationStep = 5 * (e.deltaY > 0 ? -1 : 1); const rotationStep = 5 * (e.deltaY > 0 ? -1 : 1);
this.selectedLayers.forEach(layer => { this.selectedLayers.forEach(layer => {
if (e.shiftKey) { // Rotacja if (e.shiftKey) {
layer.rotation += rotationStep; layer.rotation += rotationStep;
} else { // Skalowanie } else {
const oldWidth = layer.width; const oldWidth = layer.width;
const oldHeight = layer.height; const oldHeight = layer.height;
layer.width *= scaleFactor; layer.width *= scaleFactor;
@@ -232,7 +221,7 @@ export class Canvas {
layer.y += (oldHeight - layer.height) / 2; layer.y += (oldHeight - layer.height) / 2;
} }
}); });
} else { // Zoom widoku } else {
const worldCoords = this.getMouseWorldCoordinates(e); const worldCoords = this.getMouseWorldCoordinates(e);
const rect = this.canvas.getBoundingClientRect(); const rect = this.canvas.getBoundingClientRect();
const mouseBufferX = (e.clientX - rect.left) * (this.offscreenCanvas.width / rect.width); const mouseBufferX = (e.clientX - rect.left) * (this.offscreenCanvas.width / rect.width);
@@ -260,11 +249,9 @@ export class Canvas {
if (this.selectedLayer) { if (this.selectedLayer) {
if (e.key === 'Delete') { if (e.key === 'Delete') {
// Szukamy indeksu w tablicy this.layers, aby go usunąć
// Ale musimy usunąć wszystkie zaznaczone warstwy
// Filtrujemy główną tablicę warstw, usuwając te, które są zaznaczone
this.layers = this.layers.filter(l => !this.selectedLayers.includes(l)); this.layers = this.layers.filter(l => !this.selectedLayers.includes(l));
// Resetujemy zaznaczenie
this.updateSelection([]); this.updateSelection([]);
this.render(); this.render();
return; return;
@@ -312,7 +299,7 @@ export class Canvas {
*/ */
handleKeyUp(e) { handleKeyUp(e) {
if (e.key === 'Control') this.interaction.isCtrlPressed = false; if (e.key === 'Control') this.interaction.isCtrlPressed = false;
if (e.key === 'Alt') this.interaction.isAltPressed = false; // <-- DODANO if (e.key === 'Alt') this.interaction.isAltPressed = false;
} }
updateCursor(worldCoords) { updateCursor(worldCoords) {
@@ -411,35 +398,24 @@ export class Canvas {
} }
dragLayers(worldCoords) { dragLayers(worldCoords) {
// Logika klonowania warstw przy wciśniętym klawiszu Alt
if (this.interaction.isAltPressed && !this.interaction.hasClonedInDrag && this.selectedLayers.length > 0) { if (this.interaction.isAltPressed && !this.interaction.hasClonedInDrag && this.selectedLayers.length > 0) {
const newLayers = []; const newLayers = [];
// Stwórz klony zaznaczonych warstw
this.selectedLayers.forEach(layer => { this.selectedLayers.forEach(layer => {
const newLayer = { const newLayer = {
...layer, // Płytka kopia właściwości warstwy ...layer,
zIndex: this.layers.length, // Upewnij się, że nowa warstwa jest na wierzchu zIndex: this.layers.length,
}; };
this.layers.push(newLayer); this.layers.push(newLayer);
newLayers.push(newLayer); newLayers.push(newLayer);
}); });
// Zaktualizuj zaznaczenie, aby obejmowało tylko nowe warstwy
this.updateSelection(newLayers); this.updateSelection(newLayers);
this.selectedLayer = newLayers.length > 0 ? newLayers[newLayers.length - 1] : null; this.selectedLayer = newLayers.length > 0 ? newLayers[newLayers.length - 1] : null;
// Zresetuj pozycje startowe dla nowo przeciąganych klonów
this.originalLayerPositions.clear(); this.originalLayerPositions.clear();
this.selectedLayers.forEach(l => { this.selectedLayers.forEach(l => {
this.originalLayerPositions.set(l, {x: l.x, y: l.y}); this.originalLayerPositions.set(l, {x: l.x, y: l.y});
}); });
// Oznacz, że klonowanie w tej sesji przeciągania już się odbyło
this.interaction.hasClonedInDrag = true; this.interaction.hasClonedInDrag = true;
} }
// Istniejąca logika przesuwania (teraz działa na oryginalnych lub sklonowanych warstwach)
const totalDx = worldCoords.x - this.interaction.dragStart.x; const totalDx = worldCoords.x - this.interaction.dragStart.x;
const totalDy = worldCoords.y - this.interaction.dragStart.y; const totalDy = worldCoords.y - this.interaction.dragStart.y;
let finalDx = totalDx, finalDy = totalDy; let finalDx = totalDx, finalDy = totalDy;
@@ -1208,27 +1184,37 @@ export class Canvas {
moveLayerUp() { moveLayerUp() {
if (!this.selectedLayer) return; if (this.selectedLayers.length === 0) return;
const index = this.layers.indexOf(this.selectedLayer); const selectedIndicesSet = new Set(this.selectedLayers.map(layer => this.layers.indexOf(layer)));
if (index < this.layers.length - 1) {
const temp = this.layers[index].zIndex; const sortedIndices = Array.from(selectedIndicesSet).sort((a, b) => b - a);
this.layers[index].zIndex = this.layers[index + 1].zIndex;
this.layers[index + 1].zIndex = temp; sortedIndices.forEach(index => {
[this.layers[index], this.layers[index + 1]] = [this.layers[index + 1], this.layers[index]]; const targetIndex = index + 1;
this.render();
} if (targetIndex < this.layers.length && !selectedIndicesSet.has(targetIndex)) {
[this.layers[index], this.layers[targetIndex]] = [this.layers[targetIndex], this.layers[index]];
}
});
this.layers.forEach((layer, i) => layer.zIndex = i);
this.render();
} }
moveLayerDown() { moveLayerDown() {
if (!this.selectedLayer) return; if (this.selectedLayers.length === 0) return;
const index = this.layers.indexOf(this.selectedLayer); const selectedIndicesSet = new Set(this.selectedLayers.map(layer => this.layers.indexOf(layer)));
if (index > 0) {
const temp = this.layers[index].zIndex; const sortedIndices = Array.from(selectedIndicesSet).sort((a, b) => a - b);
this.layers[index].zIndex = this.layers[index - 1].zIndex;
this.layers[index - 1].zIndex = temp; sortedIndices.forEach(index => {
[this.layers[index], this.layers[index - 1]] = [this.layers[index - 1], this.layers[index]]; const targetIndex = index - 1;
this.render();
} if (targetIndex >= 0 && !selectedIndicesSet.has(targetIndex)) {
[this.layers[index], this.layers[targetIndex]] = [this.layers[targetIndex], this.layers[index]];
}
});
this.layers.forEach((layer, i) => layer.zIndex = i);
this.render();
} }

View File

@@ -217,28 +217,20 @@ async function createCanvasWidget(node, widget, app) {
textContent: "Paste Image", textContent: "Paste Image",
onclick: async () => { onclick: async () => {
try { try {
// Sprawdzenie, czy przeglądarka obsługuje API schowka
if (!navigator.clipboard || !navigator.clipboard.read) { if (!navigator.clipboard || !navigator.clipboard.read) {
alert("Your browser does not support pasting from the clipboard."); alert("Your browser does not support pasting from the clipboard.");
return; return;
} }
// Poproś o dostęp do schowka i odczytaj jego zawartość
const clipboardItems = await navigator.clipboard.read(); const clipboardItems = await navigator.clipboard.read();
let imageFound = false; let imageFound = false;
for (const item of clipboardItems) { for (const item of clipboardItems) {
// Szukaj typu danych, który jest obrazem
const imageType = item.types.find(type => type.startsWith('image/')); const imageType = item.types.find(type => type.startsWith('image/'));
if (imageType) { if (imageType) {
// Pobierz dane obrazu jako Blob
const blob = await item.getType(imageType); const blob = await item.getType(imageType);
// Ta część jest niemal identyczna jak w "Add Image"
const img = new Image(); const img = new Image();
img.onload = () => { img.onload = () => {
// Skaluj obraz, aby pasował do canvasu, zachowując proporcje
const scale = Math.min( const scale = Math.min(
canvas.width / img.width, canvas.width / img.width,
canvas.height / img.height canvas.height / img.height
@@ -255,15 +247,13 @@ async function createCanvasWidget(node, widget, app) {
}; };
canvas.layers.push(layer); canvas.layers.push(layer);
canvas.updateSelection([layer]); // Zaznacz nową warstwę canvas.updateSelection([layer]);
canvas.render(); canvas.render();
// Zwolnij zasób URL po załadowaniu obrazu
URL.revokeObjectURL(img.src); URL.revokeObjectURL(img.src);
}; };
img.src = URL.createObjectURL(blob); img.src = URL.createObjectURL(blob);
imageFound = true; imageFound = true;
break; // Znaleziono obraz, przerwij pętlę break;
} }
} }
@@ -366,9 +356,7 @@ async function createCanvasWidget(node, widget, app) {
textContent: "Remove Layer", textContent: "Remove Layer",
onclick: () => { onclick: () => {
if (canvas.selectedLayers.length > 0) { if (canvas.selectedLayers.length > 0) {
// Tworzy nową tablicę warstw, odfiltrowując te zaznaczone
canvas.layers = canvas.layers.filter(l => !canvas.selectedLayers.includes(l)); canvas.layers = canvas.layers.filter(l => !canvas.selectedLayers.includes(l));
// Czyści zaznaczenie i powiadamia UI
canvas.updateSelection([]); canvas.updateSelection([]);
canvas.render(); canvas.render();
} }
@@ -426,8 +414,6 @@ async function createCanvasWidget(node, widget, app) {
if (canvas.selectedLayers.length !== 1) { if (canvas.selectedLayers.length !== 1) {
throw new Error("Please select exactly one image layer for matting."); throw new Error("Please select exactly one image layer for matting.");
} }
// Ustaw status na 'przetwarzanie' (żółty)
statusIndicator.setStatus('processing'); statusIndicator.setStatus('processing');
const selectedLayer = canvas.selectedLayers[0]; const selectedLayer = canvas.selectedLayers[0];
@@ -467,12 +453,8 @@ async function createCanvasWidget(node, widget, app) {
await canvas.saveToServer(widget.value); await canvas.saveToServer(widget.value);
app.graph.runStep(); app.graph.runStep();
// Ustaw status na 'ukończono' (zielony)
statusIndicator.setStatus('completed'); statusIndicator.setStatus('completed');
}; };
// Tworzymy obraz z przezroczystością z serwera
newImage.src = result.matted_image; newImage.src = result.matted_image;
}; };
mattedImage.onerror = () => { mattedImage.onerror = () => {
@@ -483,7 +465,6 @@ async function createCanvasWidget(node, widget, app) {
} catch (error) { } catch (error) {
console.error("Matting error:", error); console.error("Matting error:", error);
alert(`Error during matting process: ${error.message}`); alert(`Error during matting process: ${error.message}`);
// Ustaw status na 'błąd' (czerwony)
statusIndicator.setStatus('error'); statusIndicator.setStatus('error');
} }
} }
@@ -497,13 +478,9 @@ async function createCanvasWidget(node, widget, app) {
const updateButtonStates = () => { const updateButtonStates = () => {
const selectionCount = canvas.selectedLayers.length; const selectionCount = canvas.selectedLayers.length;
const hasSelection = selectionCount > 0; const hasSelection = selectionCount > 0;
// Ogólne przyciski wymagające przynajmniej jednego zaznaczenia
controlPanel.querySelectorAll('.requires-selection').forEach(btn => { controlPanel.querySelectorAll('.requires-selection').forEach(btn => {
btn.disabled = !hasSelection; btn.disabled = !hasSelection;
}); });
// Specjalna logika dla przycisku "Matting", który wymaga DOKŁADNIE jednego zaznaczenia
const mattingBtn = controlPanel.querySelector('.matting-button'); const mattingBtn = controlPanel.querySelector('.matting-button');
if (mattingBtn) { if (mattingBtn) {
mattingBtn.disabled = selectionCount !== 1; mattingBtn.disabled = selectionCount !== 1;
@@ -568,7 +545,6 @@ async function createCanvasWidget(node, widget, app) {
} }
}, [controlPanel, canvasContainer]); }, [controlPanel, canvasContainer]);
const handleFileLoad = async (file) => { const handleFileLoad = async (file) => {
// Sprawdzamy, czy plik jest obrazem
if (!file.type.startsWith('image/')) { if (!file.type.startsWith('image/')) {
return; return;
} }
@@ -595,38 +571,30 @@ async function createCanvasWidget(node, widget, app) {
canvas.layers.push(layer); canvas.layers.push(layer);
canvas.selectedLayer = layer; canvas.selectedLayer = layer;
canvas.render(); canvas.render();
// Używamy funkcji updateOutput, aby zapisać stan i uruchomić graf
await updateOutput(); await updateOutput();
// Zwolnienie zasobu URL
URL.revokeObjectURL(img.src); URL.revokeObjectURL(img.src);
}; };
img.src = URL.createObjectURL(file); img.src = URL.createObjectURL(file);
}; };
mainContainer.addEventListener('dragover', (e) => { mainContainer.addEventListener('dragover', (e) => {
e.preventDefault(); // Niezbędne, aby zdarzenie 'drop' zadziałało e.preventDefault();
e.stopPropagation(); e.stopPropagation();
// Dodajemy klasę, aby pokazać wizualną informację zwrotną
canvasContainer.classList.add('drag-over'); canvasContainer.classList.add('drag-over');
}); });
mainContainer.addEventListener('dragleave', (e) => { mainContainer.addEventListener('dragleave', (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
// Usuwamy klasę po opuszczeniu obszaru
canvasContainer.classList.remove('drag-over'); canvasContainer.classList.remove('drag-over');
}); });
mainContainer.addEventListener('drop', async (e) => { mainContainer.addEventListener('drop', async (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
// Usuwamy klasę po upuszczeniu pliku
canvasContainer.classList.remove('drag-over'); canvasContainer.classList.remove('drag-over');
if (e.dataTransfer.files) { if (e.dataTransfer.files) {
// Przetwarzamy wszystkie upuszczone pliki
for (const file of e.dataTransfer.files) { for (const file of e.dataTransfer.files) {
await handleFileLoad(file); await handleFileLoad(file);
} }
@@ -677,14 +645,10 @@ class MattingStatusIndicator {
} }
constructor(container) { constructor(container) {
// Lista możliwych statusów, aby łatwiej nimi zarządzać
this.statuses = ['processing', 'completed', 'error']; this.statuses = ['processing', 'completed', 'error'];
this.indicator = document.createElement('div'); this.indicator = document.createElement('div');
// Ustawiamy bazową klasę, która będzie miała domyślny szary kolor
this.indicator.className = 'matting-indicator'; this.indicator.className = 'matting-indicator';
// Usunięto 'background-color' z stylów inline
this.indicator.style.cssText = ` this.indicator.style.cssText = `
width: 10px; width: 10px;
height: 10px; height: 10px;
@@ -725,16 +689,11 @@ class MattingStatusIndicator {
} }
setStatus(status) { setStatus(status) {
// 1. Usuń wszystkie poprzednie klasy statusu, pozostawiając klasę bazową
this.indicator.classList.remove(...this.statuses); this.indicator.classList.remove(...this.statuses);
// 2. Dodaj nową klasę statusu, jeśli została podana
if (status && this.statuses.includes(status)) { if (status && this.statuses.includes(status)) {
this.indicator.classList.add(status); this.indicator.classList.add(status);
} }
// 3. Usuń statusy końcowe (sukces/błąd) po 3 sekundach,
// aby wskaźnik wrócił do domyślnego szarego koloru.
if (status === 'completed' || status === 'error') { if (status === 'completed' || status === 'error') {
setTimeout(() => { setTimeout(() => {
this.indicator.classList.remove(status); this.indicator.classList.remove(status);