mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-21 20:52:12 -03:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd8007d8aa | ||
|
|
af5e81c56b | ||
|
|
aa31a347d1 | ||
|
|
dfa7309132 | ||
|
|
2ab406ebfd | ||
|
|
d40f68b8c6 | ||
|
|
e5060fd8c3 | ||
|
|
f8eb91c4ad | ||
|
|
c4af745b2a | ||
|
|
c9c0babf3c | ||
|
|
152a3f7dff | ||
|
|
9f9a733731 | ||
|
|
3419061b6c | ||
|
|
9e4da30b59 |
@@ -28,8 +28,9 @@ import os
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
from python.logger import logger, LogLevel, debug, info, warn, error, exception
|
from python.logger import logger, LogLevel, debug, info, warn, error, exception
|
||||||
|
from python.config import LOG_LEVEL
|
||||||
|
|
||||||
logger.set_module_level('canvas_node', LogLevel.NONE)
|
logger.set_module_level('canvas_node', LogLevel[LOG_LEVEL])
|
||||||
|
|
||||||
logger.configure({
|
logger.configure({
|
||||||
'log_to_file': True,
|
'log_to_file': True,
|
||||||
@@ -333,6 +334,24 @@ class CanvasNode:
|
|||||||
latest_image_path = max(image_files, key=os.path.getctime)
|
latest_image_path = max(image_files, key=os.path.getctime)
|
||||||
return latest_image_path
|
return latest_image_path
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_latest_images(cls, since_timestamp=0):
|
||||||
|
output_dir = folder_paths.get_output_directory()
|
||||||
|
files = []
|
||||||
|
for f_name in os.listdir(output_dir):
|
||||||
|
file_path = os.path.join(output_dir, f_name)
|
||||||
|
if os.path.isfile(file_path) and file_path.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')):
|
||||||
|
try:
|
||||||
|
mtime = os.path.getmtime(file_path)
|
||||||
|
if mtime > since_timestamp:
|
||||||
|
files.append((mtime, file_path))
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
files.sort(key=lambda x: x[0])
|
||||||
|
|
||||||
|
return [f[1] for f in files]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_flow_status(cls, flow_id=None):
|
def get_flow_status(cls, flow_id=None):
|
||||||
|
|
||||||
@@ -454,6 +473,30 @@ class CanvasNode:
|
|||||||
'error': str(e)
|
'error': str(e)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@PromptServer.instance.routes.get("/layerforge/get-latest-images/{since}")
|
||||||
|
async def get_latest_images_route(request):
|
||||||
|
try:
|
||||||
|
since_timestamp = float(request.match_info.get('since', 0))
|
||||||
|
# JS Timestamps are in milliseconds, Python's are in seconds
|
||||||
|
latest_image_paths = cls.get_latest_images(since_timestamp / 1000.0)
|
||||||
|
|
||||||
|
images_data = []
|
||||||
|
for image_path in latest_image_paths:
|
||||||
|
with open(image_path, "rb") as f:
|
||||||
|
encoded_string = base64.b64encode(f.read()).decode('utf-8')
|
||||||
|
images_data.append(f"data:image/png;base64,{encoded_string}")
|
||||||
|
|
||||||
|
return web.json_response({
|
||||||
|
'success': True,
|
||||||
|
'images': images_data
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
log_error(f"Error in get_latest_images_route: {str(e)}")
|
||||||
|
return web.json_response({
|
||||||
|
'success': False,
|
||||||
|
'error': str(e)
|
||||||
|
}, status=500)
|
||||||
|
|
||||||
@PromptServer.instance.routes.get("/ycnode/get_latest_image")
|
@PromptServer.instance.routes.get("/ycnode/get_latest_image")
|
||||||
async def get_latest_image_route(request):
|
async def get_latest_image_route(request):
|
||||||
try:
|
try:
|
||||||
@@ -571,42 +614,38 @@ class BiRefNetMatting:
|
|||||||
def load_model(self, model_path):
|
def load_model(self, model_path):
|
||||||
try:
|
try:
|
||||||
if model_path not in self.model_cache:
|
if model_path not in self.model_cache:
|
||||||
|
|
||||||
full_model_path = os.path.join(self.base_path, "BiRefNet")
|
full_model_path = os.path.join(self.base_path, "BiRefNet")
|
||||||
|
|
||||||
log_info(f"Loading BiRefNet model from {full_model_path}...")
|
log_info(f"Loading BiRefNet model from {full_model_path}...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
||||||
self.model = AutoModelForImageSegmentation.from_pretrained(
|
self.model = AutoModelForImageSegmentation.from_pretrained(
|
||||||
"ZhengPeng7/BiRefNet",
|
"ZhengPeng7/BiRefNet",
|
||||||
trust_remote_code=True,
|
trust_remote_code=True,
|
||||||
cache_dir=full_model_path
|
cache_dir=full_model_path
|
||||||
)
|
)
|
||||||
|
|
||||||
self.model.eval()
|
self.model.eval()
|
||||||
if torch.cuda.is_available():
|
if torch.cuda.is_available():
|
||||||
self.model = self.model.cuda()
|
self.model = self.model.cuda()
|
||||||
|
|
||||||
self.model_cache[model_path] = self.model
|
self.model_cache[model_path] = self.model
|
||||||
log_info("Model loaded successfully from Hugging Face")
|
log_info("Model loaded successfully from Hugging Face")
|
||||||
log_debug(f"Model type: {type(self.model)}")
|
|
||||||
log_debug(f"Model device: {next(self.model.parameters()).device}")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_error(f"Failed to load model: {str(e)}")
|
log_error(f"Failed to load model from Hugging Face: {str(e)}")
|
||||||
raise
|
# Re-raise with a more informative message
|
||||||
|
raise RuntimeError(
|
||||||
|
"Failed to download or load the matting model. "
|
||||||
|
"This could be due to a network issue, file permissions, or a corrupted model cache. "
|
||||||
|
f"Please check your internet connection and the model cache path: {full_model_path}. "
|
||||||
|
f"Original error: {str(e)}"
|
||||||
|
) from e
|
||||||
else:
|
else:
|
||||||
self.model = self.model_cache[model_path]
|
self.model = self.model_cache[model_path]
|
||||||
log_debug("Using cached model")
|
log_debug("Using cached model")
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# Catch the re-raised exception or any other error
|
||||||
log_error(f"Error loading model: {str(e)}")
|
log_error(f"Error loading model: {str(e)}")
|
||||||
log_exception("Model loading failed")
|
log_exception("Model loading failed")
|
||||||
return False
|
raise # Re-raise the exception to be caught by the execute method
|
||||||
|
|
||||||
def preprocess_image(self, image):
|
def preprocess_image(self, image):
|
||||||
|
|
||||||
@@ -636,11 +675,9 @@ class BiRefNetMatting:
|
|||||||
|
|
||||||
def execute(self, image, model_path, threshold=0.5, refinement=1):
|
def execute(self, image, model_path, threshold=0.5, refinement=1):
|
||||||
try:
|
try:
|
||||||
|
|
||||||
PromptServer.instance.send_sync("matting_status", {"status": "processing"})
|
PromptServer.instance.send_sync("matting_status", {"status": "processing"})
|
||||||
|
|
||||||
if not self.load_model(model_path):
|
self.load_model(model_path)
|
||||||
raise RuntimeError("Failed to load model")
|
|
||||||
|
|
||||||
if isinstance(image, torch.Tensor):
|
if isinstance(image, torch.Tensor):
|
||||||
original_size = image.shape[-2:] if image.dim() == 4 else image.shape[-2:]
|
original_size = image.shape[-2:] if image.dim() == 4 else image.shape[-2:]
|
||||||
|
|||||||
258
js/BatchPreviewManager.js
Normal file
258
js/BatchPreviewManager.js
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
import {createModuleLogger} from "./utils/LoggerUtils.js";
|
||||||
|
|
||||||
|
const log = createModuleLogger('BatchPreviewManager');
|
||||||
|
|
||||||
|
export class BatchPreviewManager {
|
||||||
|
constructor(canvas, initialPosition = { x: 0, y: 0 }, generationArea = null) {
|
||||||
|
this.canvas = canvas;
|
||||||
|
this.active = false;
|
||||||
|
this.layers = [];
|
||||||
|
this.currentIndex = 0;
|
||||||
|
this.element = null;
|
||||||
|
this.uiInitialized = false;
|
||||||
|
this.maskWasVisible = false;
|
||||||
|
|
||||||
|
// Position in canvas world coordinates
|
||||||
|
this.worldX = initialPosition.x;
|
||||||
|
this.worldY = initialPosition.y;
|
||||||
|
this.isDragging = false;
|
||||||
|
this.generationArea = generationArea; // Store the generation area
|
||||||
|
}
|
||||||
|
|
||||||
|
updateScreenPosition(viewport) {
|
||||||
|
if (!this.active || !this.element) return;
|
||||||
|
|
||||||
|
// Translate world coordinates to screen coordinates
|
||||||
|
const screenX = (this.worldX - viewport.x) * viewport.zoom;
|
||||||
|
const screenY = (this.worldY - viewport.y) * viewport.zoom;
|
||||||
|
|
||||||
|
// We can also scale the menu with zoom, but let's keep it constant for now for readability
|
||||||
|
const scale = 1; // viewport.zoom;
|
||||||
|
|
||||||
|
// Use transform for performance
|
||||||
|
this.element.style.transform = `translate(${screenX}px, ${screenY}px) scale(${scale})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
_createUI() {
|
||||||
|
if (this.uiInitialized) return;
|
||||||
|
|
||||||
|
this.element = document.createElement('div');
|
||||||
|
this.element.id = 'layerforge-batch-preview';
|
||||||
|
this.element.style.cssText = `
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: #333;
|
||||||
|
color: white;
|
||||||
|
padding: 8px 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.5);
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
font-family: sans-serif;
|
||||||
|
z-index: 1001;
|
||||||
|
border: 1px solid #555;
|
||||||
|
cursor: move;
|
||||||
|
user-select: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.element.addEventListener('mousedown', (e) => {
|
||||||
|
if (e.target.tagName === 'BUTTON') return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
this.isDragging = true;
|
||||||
|
|
||||||
|
const handleMouseMove = (moveEvent) => {
|
||||||
|
if (this.isDragging) {
|
||||||
|
// Convert screen pixel movement to world coordinate movement
|
||||||
|
const deltaX = moveEvent.movementX / this.canvas.viewport.zoom;
|
||||||
|
const deltaY = moveEvent.movementY / this.canvas.viewport.zoom;
|
||||||
|
|
||||||
|
this.worldX += deltaX;
|
||||||
|
this.worldY += deltaY;
|
||||||
|
|
||||||
|
// The render loop will handle updating the screen position, but we need to trigger it.
|
||||||
|
this.canvas.render();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
this.isDragging = false;
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
|
});
|
||||||
|
|
||||||
|
const prevButton = this._createButton('◀', 'Previous'); // Left arrow
|
||||||
|
const nextButton = this._createButton('▶', 'Next'); // Right arrow
|
||||||
|
const confirmButton = this._createButton('✔', 'Confirm'); // Checkmark
|
||||||
|
const cancelButton = this._createButton('✖', 'Cancel All'); // X mark
|
||||||
|
const closeButton = this._createButton('➲', 'Close'); // Door icon
|
||||||
|
|
||||||
|
this.counterElement = document.createElement('span');
|
||||||
|
this.counterElement.style.minWidth = '40px';
|
||||||
|
this.counterElement.style.textAlign = 'center';
|
||||||
|
this.counterElement.style.fontWeight = 'bold';
|
||||||
|
|
||||||
|
prevButton.onclick = () => this.navigate(-1);
|
||||||
|
nextButton.onclick = () => this.navigate(1);
|
||||||
|
confirmButton.onclick = () => this.confirm();
|
||||||
|
cancelButton.onclick = () => this.cancelAndRemoveAll();
|
||||||
|
closeButton.onclick = () => this.hide();
|
||||||
|
|
||||||
|
this.element.append(prevButton, this.counterElement, nextButton, confirmButton, cancelButton, closeButton);
|
||||||
|
if (this.canvas.canvas.parentNode) {
|
||||||
|
this.canvas.canvas.parentNode.appendChild(this.element);
|
||||||
|
} else {
|
||||||
|
log.error("Could not find parent node to attach batch preview UI.");
|
||||||
|
}
|
||||||
|
this.uiInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_createButton(innerHTML, title) {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.innerHTML = innerHTML;
|
||||||
|
button.title = title;
|
||||||
|
button.style.cssText = `
|
||||||
|
background: #555;
|
||||||
|
color: white;
|
||||||
|
border: 1px solid #777;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
button.onmouseover = () => button.style.background = '#666';
|
||||||
|
button.onmouseout = () => button.style.background = '#555';
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
show(layers) {
|
||||||
|
if (!layers || layers.length <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._createUI();
|
||||||
|
|
||||||
|
// Auto-hide mask logic
|
||||||
|
this.maskWasVisible = this.canvas.maskTool.isOverlayVisible;
|
||||||
|
if (this.maskWasVisible) {
|
||||||
|
this.canvas.maskTool.toggleOverlayVisibility();
|
||||||
|
const toggleBtn = document.getElementById(`toggle-mask-btn-${this.canvas.node.id}`);
|
||||||
|
if (toggleBtn) {
|
||||||
|
toggleBtn.classList.remove('primary');
|
||||||
|
toggleBtn.textContent = "Hide Mask";
|
||||||
|
}
|
||||||
|
this.canvas.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`Showing batch preview for ${layers.length} layers.`);
|
||||||
|
this.layers = layers;
|
||||||
|
this.currentIndex = 0;
|
||||||
|
|
||||||
|
// Make the element visible BEFORE calculating its size
|
||||||
|
this.element.style.display = 'flex';
|
||||||
|
this.active = true;
|
||||||
|
|
||||||
|
// Now that it's visible, we can get its dimensions and adjust the position.
|
||||||
|
const menuWidthInWorld = this.element.offsetWidth / this.canvas.viewport.zoom;
|
||||||
|
const paddingInWorld = 20 / this.canvas.viewport.zoom;
|
||||||
|
|
||||||
|
this.worldX -= menuWidthInWorld / 2; // Center horizontally
|
||||||
|
this.worldY += paddingInWorld; // Add padding below the output area
|
||||||
|
|
||||||
|
this._update();
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
log.info('Hiding batch preview.');
|
||||||
|
if (this.element) {
|
||||||
|
this.element.remove();
|
||||||
|
}
|
||||||
|
this.active = false;
|
||||||
|
|
||||||
|
const index = this.canvas.batchPreviewManagers.indexOf(this);
|
||||||
|
if (index > -1) {
|
||||||
|
this.canvas.batchPreviewManagers.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger a final render to ensure the generation area outline is removed
|
||||||
|
this.canvas.render();
|
||||||
|
|
||||||
|
// Restore mask visibility if it was hidden by this manager
|
||||||
|
if (this.maskWasVisible && !this.canvas.maskTool.isOverlayVisible) {
|
||||||
|
this.canvas.maskTool.toggleOverlayVisibility();
|
||||||
|
const toggleBtn = document.getElementById(`toggle-mask-btn-${this.canvas.node.id}`);
|
||||||
|
if (toggleBtn) {
|
||||||
|
toggleBtn.classList.add('primary');
|
||||||
|
toggleBtn.textContent = "Show Mask";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.maskWasVisible = false; // Reset state
|
||||||
|
|
||||||
|
// Make all layers visible again upon closing
|
||||||
|
this.canvas.layers.forEach(l => l.visible = true);
|
||||||
|
this.canvas.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate(direction) {
|
||||||
|
this.currentIndex += direction;
|
||||||
|
if (this.currentIndex < 0) {
|
||||||
|
this.currentIndex = this.layers.length - 1;
|
||||||
|
} else if (this.currentIndex >= this.layers.length) {
|
||||||
|
this.currentIndex = 0;
|
||||||
|
}
|
||||||
|
this._update();
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm() {
|
||||||
|
const layerToKeep = this.layers[this.currentIndex];
|
||||||
|
log.info(`Confirming selection: Keeping layer ${layerToKeep.id}.`);
|
||||||
|
|
||||||
|
const layersToDelete = this.layers.filter(l => l.id !== layerToKeep.id);
|
||||||
|
const layerIdsToDelete = layersToDelete.map(l => l.id);
|
||||||
|
|
||||||
|
this.canvas.removeLayersByIds(layerIdsToDelete);
|
||||||
|
log.info(`Deleted ${layersToDelete.length} other layers.`);
|
||||||
|
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelAndRemoveAll() {
|
||||||
|
log.info('Cancel clicked. Removing all new layers.');
|
||||||
|
|
||||||
|
const layerIdsToDelete = this.layers.map(l => l.id);
|
||||||
|
this.canvas.removeLayersByIds(layerIdsToDelete);
|
||||||
|
log.info(`Deleted all ${layerIdsToDelete.length} new layers.`);
|
||||||
|
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
_update() {
|
||||||
|
this.counterElement.textContent = `${this.currentIndex + 1} / ${this.layers.length}`;
|
||||||
|
this._focusOnLayer(this.layers[this.currentIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
_focusOnLayer(layer) {
|
||||||
|
if (!layer) return;
|
||||||
|
log.debug(`Focusing on layer ${layer.id}`);
|
||||||
|
|
||||||
|
// Move the selected layer to the top of the layer stack
|
||||||
|
this.canvas.canvasLayers.moveLayers([layer], { toIndex: 0 });
|
||||||
|
|
||||||
|
this.canvas.updateSelection([layer]);
|
||||||
|
|
||||||
|
// Render is called by moveLayers, but we call it again to be safe
|
||||||
|
this.canvas.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
766
js/Canvas.js
766
js/Canvas.js
@@ -9,9 +9,22 @@ import {CanvasLayersPanel} from "./CanvasLayersPanel.js";
|
|||||||
import {CanvasRenderer} from "./CanvasRenderer.js";
|
import {CanvasRenderer} from "./CanvasRenderer.js";
|
||||||
import {CanvasIO} from "./CanvasIO.js";
|
import {CanvasIO} from "./CanvasIO.js";
|
||||||
import {ImageReferenceManager} from "./ImageReferenceManager.js";
|
import {ImageReferenceManager} from "./ImageReferenceManager.js";
|
||||||
|
import {BatchPreviewManager} from "./BatchPreviewManager.js";
|
||||||
import {createModuleLogger} from "./utils/LoggerUtils.js";
|
import {createModuleLogger} from "./utils/LoggerUtils.js";
|
||||||
import {mask_editor_showing, mask_editor_listen_for_cancel} from "./utils/mask_utils.js";
|
|
||||||
import { debounce } from "./utils/CommonUtils.js";
|
import { debounce } from "./utils/CommonUtils.js";
|
||||||
|
import {CanvasMask} from "./CanvasMask.js";
|
||||||
|
import {CanvasSelection} from "./CanvasSelection.js";
|
||||||
|
|
||||||
|
const useChainCallback = (original, next) => {
|
||||||
|
if (original === undefined || original === null) {
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
return function(...args) {
|
||||||
|
const originalReturn = original.apply(this, args);
|
||||||
|
const nextReturn = next.apply(this, args);
|
||||||
|
return nextReturn === undefined ? originalReturn : nextReturn;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const log = createModuleLogger('Canvas');
|
const log = createModuleLogger('Canvas');
|
||||||
|
|
||||||
@@ -32,9 +45,6 @@ export class Canvas {
|
|||||||
this.width = 512;
|
this.width = 512;
|
||||||
this.height = 512;
|
this.height = 512;
|
||||||
this.layers = [];
|
this.layers = [];
|
||||||
this.selectedLayer = null;
|
|
||||||
this.selectedLayers = [];
|
|
||||||
this.onSelectionChange = null;
|
|
||||||
this.onStateChange = callbacks.onStateChange || null;
|
this.onStateChange = callbacks.onStateChange || null;
|
||||||
this.lastMousePosition = {x: 0, y: 0};
|
this.lastMousePosition = {x: 0, y: 0};
|
||||||
|
|
||||||
@@ -146,14 +156,19 @@ export class Canvas {
|
|||||||
// Stwórz opóźnioną wersję funkcji zapisu stanu
|
// Stwórz opóźnioną wersję funkcji zapisu stanu
|
||||||
this.requestSaveState = debounce(this.saveState.bind(this), 500);
|
this.requestSaveState = debounce(this.saveState.bind(this), 500);
|
||||||
|
|
||||||
|
this._addAutoRefreshToggle();
|
||||||
this.maskTool = new MaskTool(this, {onStateChange: this.onStateChange});
|
this.maskTool = new MaskTool(this, {onStateChange: this.onStateChange});
|
||||||
|
this.canvasMask = new CanvasMask(this);
|
||||||
this.canvasState = new CanvasState(this);
|
this.canvasState = new CanvasState(this);
|
||||||
|
this.canvasSelection = new CanvasSelection(this);
|
||||||
this.canvasInteractions = new CanvasInteractions(this);
|
this.canvasInteractions = new CanvasInteractions(this);
|
||||||
this.canvasLayers = new CanvasLayers(this);
|
this.canvasLayers = new CanvasLayers(this);
|
||||||
this.canvasLayersPanel = new CanvasLayersPanel(this);
|
this.canvasLayersPanel = new CanvasLayersPanel(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.batchPreviewManagers = [];
|
||||||
|
this.pendingBatchContext = null;
|
||||||
|
|
||||||
log.debug('Canvas modules initialized successfully');
|
log.debug('Canvas modules initialized successfully');
|
||||||
}
|
}
|
||||||
@@ -274,60 +289,35 @@ export class Canvas {
|
|||||||
/**
|
/**
|
||||||
* Usuwa wybrane warstwy
|
* Usuwa wybrane warstwy
|
||||||
*/
|
*/
|
||||||
removeSelectedLayers() {
|
removeLayersByIds(layerIds) {
|
||||||
if (this.selectedLayers.length > 0) {
|
if (!layerIds || layerIds.length === 0) return;
|
||||||
log.info('Removing selected layers', {
|
|
||||||
layersToRemove: this.selectedLayers.length,
|
|
||||||
totalLayers: this.layers.length
|
|
||||||
});
|
|
||||||
|
|
||||||
this.saveState();
|
const initialCount = this.layers.length;
|
||||||
this.layers = this.layers.filter(l => !this.selectedLayers.includes(l));
|
this.saveState();
|
||||||
|
this.layers = this.layers.filter(l => !layerIds.includes(l.id));
|
||||||
this.updateSelection([]);
|
|
||||||
|
// If the current selection was part of the removal, clear it
|
||||||
this.render();
|
const newSelection = this.canvasSelection.selectedLayers.filter(l => !layerIds.includes(l.id));
|
||||||
this.saveState();
|
this.canvasSelection.updateSelection(newSelection);
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
this.saveState();
|
||||||
|
|
||||||
if (this.canvasLayersPanel) {
|
if (this.canvasLayersPanel) {
|
||||||
this.canvasLayersPanel.onLayersChanged();
|
this.canvasLayersPanel.onLayersChanged();
|
||||||
}
|
|
||||||
|
|
||||||
log.debug('Layers removed successfully, remaining layers:', this.layers.length);
|
|
||||||
} else {
|
|
||||||
log.debug('No layers selected for removal');
|
|
||||||
}
|
}
|
||||||
|
log.info(`Removed ${initialCount - this.layers.length} layers by ID.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeSelectedLayers() {
|
||||||
|
return this.canvasSelection.removeSelectedLayers();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Duplikuje zaznaczone warstwy (w pamięci, bez zapisu stanu)
|
* Duplikuje zaznaczone warstwy (w pamięci, bez zapisu stanu)
|
||||||
*/
|
*/
|
||||||
duplicateSelectedLayers() {
|
duplicateSelectedLayers() {
|
||||||
if (this.selectedLayers.length === 0) return [];
|
return this.canvasSelection.duplicateSelectedLayers();
|
||||||
|
|
||||||
const newLayers = [];
|
|
||||||
const sortedLayers = [...this.selectedLayers].sort((a,b) => a.zIndex - b.zIndex);
|
|
||||||
|
|
||||||
sortedLayers.forEach(layer => {
|
|
||||||
const newLayer = {
|
|
||||||
...layer,
|
|
||||||
id: `layer_${+new Date()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
||||||
zIndex: this.layers.length, // Nowa warstwa zawsze na wierzchu
|
|
||||||
};
|
|
||||||
this.layers.push(newLayer);
|
|
||||||
newLayers.push(newLayer);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Aktualizuj zaznaczenie, co powiadomi panel (ale nie renderuje go całego)
|
|
||||||
this.updateSelection(newLayers);
|
|
||||||
|
|
||||||
// Powiadom panel o zmianie struktury, aby się przerysował
|
|
||||||
if (this.canvasLayersPanel) {
|
|
||||||
this.canvasLayersPanel.onLayersChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info(`Duplicated ${newLayers.length} layers (in-memory).`);
|
|
||||||
return newLayers;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -336,82 +326,14 @@ export class Canvas {
|
|||||||
* @param {Array} newSelection - Nowa lista zaznaczonych warstw
|
* @param {Array} newSelection - Nowa lista zaznaczonych warstw
|
||||||
*/
|
*/
|
||||||
updateSelection(newSelection) {
|
updateSelection(newSelection) {
|
||||||
const previousSelection = this.selectedLayers.length;
|
return this.canvasSelection.updateSelection(newSelection);
|
||||||
this.selectedLayers = newSelection || [];
|
|
||||||
this.selectedLayer = this.selectedLayers.length > 0 ? this.selectedLayers[this.selectedLayers.length - 1] : null;
|
|
||||||
|
|
||||||
// Sprawdź, czy zaznaczenie faktycznie się zmieniło, aby uniknąć pętli
|
|
||||||
const hasChanged = previousSelection !== this.selectedLayers.length ||
|
|
||||||
this.selectedLayers.some((layer, i) => this.selectedLayers[i] !== (newSelection || [])[i]);
|
|
||||||
|
|
||||||
if (!hasChanged && previousSelection > 0) {
|
|
||||||
// return; // Zablokowane na razie, może powodować problemy
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug('Selection updated', {
|
|
||||||
previousCount: previousSelection,
|
|
||||||
newCount: this.selectedLayers.length,
|
|
||||||
selectedLayerIds: this.selectedLayers.map(l => l.id || 'unknown')
|
|
||||||
});
|
|
||||||
|
|
||||||
// 1. Zrenderuj ponownie canvas, aby pokazać nowe kontrolki transformacji
|
|
||||||
this.render();
|
|
||||||
|
|
||||||
// 2. Powiadom inne części aplikacji (jeśli są)
|
|
||||||
if (this.onSelectionChange) {
|
|
||||||
this.onSelectionChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Powiadom panel warstw, aby zaktualizował swój wygląd
|
|
||||||
if (this.canvasLayersPanel) {
|
|
||||||
this.canvasLayersPanel.onSelectionChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logika aktualizacji zaznaczenia, wywoływana przez panel warstw.
|
* Logika aktualizacji zaznaczenia, wywoływana przez panel warstw.
|
||||||
*/
|
*/
|
||||||
updateSelectionLogic(layer, isCtrlPressed, isShiftPressed, index) {
|
updateSelectionLogic(layer, isCtrlPressed, isShiftPressed, index) {
|
||||||
let newSelection = [...this.selectedLayers];
|
return this.canvasSelection.updateSelectionLogic(layer, isCtrlPressed, isShiftPressed, index);
|
||||||
let selectionChanged = false;
|
|
||||||
|
|
||||||
if (isShiftPressed && this.canvasLayersPanel.lastSelectedIndex !== -1) {
|
|
||||||
const sortedLayers = [...this.layers].sort((a, b) => b.zIndex - a.zIndex);
|
|
||||||
const startIndex = Math.min(this.canvasLayersPanel.lastSelectedIndex, index);
|
|
||||||
const endIndex = Math.max(this.canvasLayersPanel.lastSelectedIndex, index);
|
|
||||||
|
|
||||||
newSelection = [];
|
|
||||||
for (let i = startIndex; i <= endIndex; i++) {
|
|
||||||
if (sortedLayers[i]) {
|
|
||||||
newSelection.push(sortedLayers[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
selectionChanged = true;
|
|
||||||
} else if (isCtrlPressed) {
|
|
||||||
const layerIndex = newSelection.indexOf(layer);
|
|
||||||
if (layerIndex === -1) {
|
|
||||||
newSelection.push(layer);
|
|
||||||
} else {
|
|
||||||
newSelection.splice(layerIndex, 1);
|
|
||||||
}
|
|
||||||
this.canvasLayersPanel.lastSelectedIndex = index;
|
|
||||||
selectionChanged = true;
|
|
||||||
} else {
|
|
||||||
// Jeśli kliknięta warstwa nie jest częścią obecnego zaznaczenia,
|
|
||||||
// wyczyść zaznaczenie i zaznacz tylko ją.
|
|
||||||
if (!this.selectedLayers.includes(layer)) {
|
|
||||||
newSelection = [layer];
|
|
||||||
selectionChanged = true;
|
|
||||||
}
|
|
||||||
// Jeśli kliknięta warstwa JEST już zaznaczona (potencjalnie z innymi),
|
|
||||||
// NIE rób nic, aby umożliwić przeciąganie całej grupy.
|
|
||||||
this.canvasLayersPanel.lastSelectedIndex = index;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aktualizuj zaznaczenie tylko jeśli faktycznie się zmieniło
|
|
||||||
if (selectionChanged) {
|
|
||||||
this.updateSelection(newSelection);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -445,6 +367,87 @@ export class Canvas {
|
|||||||
return this.canvasIO.importLatestImage();
|
return this.canvasIO.importLatestImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_addAutoRefreshToggle() {
|
||||||
|
let autoRefreshEnabled = false;
|
||||||
|
let lastExecutionStartTime = 0;
|
||||||
|
|
||||||
|
const handleExecutionStart = () => {
|
||||||
|
if (autoRefreshEnabled) {
|
||||||
|
lastExecutionStartTime = Date.now();
|
||||||
|
// Store a snapshot of the context for the upcoming batch
|
||||||
|
this.pendingBatchContext = {
|
||||||
|
// For the menu position
|
||||||
|
spawnPosition: {
|
||||||
|
x: this.width / 2,
|
||||||
|
y: this.height
|
||||||
|
},
|
||||||
|
// For the image placement
|
||||||
|
outputArea: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: this.width,
|
||||||
|
height: this.height
|
||||||
|
}
|
||||||
|
};
|
||||||
|
log.debug(`Execution started, pending batch context captured:`, this.pendingBatchContext);
|
||||||
|
this.render(); // Trigger render to show the pending outline immediately
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExecutionSuccess = async () => {
|
||||||
|
if (autoRefreshEnabled) {
|
||||||
|
log.info('Auto-refresh triggered, importing latest images.');
|
||||||
|
|
||||||
|
if (!this.pendingBatchContext) {
|
||||||
|
log.warn("execution_start did not fire, cannot process batch. Awaiting next execution.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the captured output area for image import
|
||||||
|
const newLayers = await this.canvasIO.importLatestImages(
|
||||||
|
lastExecutionStartTime,
|
||||||
|
this.pendingBatchContext.outputArea
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newLayers && newLayers.length > 1) {
|
||||||
|
const newManager = new BatchPreviewManager(
|
||||||
|
this,
|
||||||
|
this.pendingBatchContext.spawnPosition,
|
||||||
|
this.pendingBatchContext.outputArea
|
||||||
|
);
|
||||||
|
this.batchPreviewManagers.push(newManager);
|
||||||
|
newManager.show(newLayers);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume the context
|
||||||
|
this.pendingBatchContext = null;
|
||||||
|
// Final render to clear the outline if it was the last one
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.node.addWidget(
|
||||||
|
'toggle',
|
||||||
|
'Auto-refresh after generation',
|
||||||
|
false,
|
||||||
|
(value) => {
|
||||||
|
autoRefreshEnabled = value;
|
||||||
|
log.debug('Auto-refresh toggled:', value);
|
||||||
|
}, {
|
||||||
|
serialize: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
api.addEventListener('execution_start', handleExecutionStart);
|
||||||
|
api.addEventListener('execution_success', handleExecutionSuccess);
|
||||||
|
|
||||||
|
this.node.onRemoved = useChainCallback(this.node.onRemoved, () => {
|
||||||
|
log.info('Node removed, cleaning up auto-refresh listeners.');
|
||||||
|
api.removeEventListener('execution_start', handleExecutionStart);
|
||||||
|
api.removeEventListener('execution_success', handleExecutionSuccess);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uruchamia edytor masek
|
* Uruchamia edytor masek
|
||||||
@@ -452,92 +455,7 @@ export class Canvas {
|
|||||||
* @param {boolean} sendCleanImage - Czy wysłać czysty obraz (bez maski) do editora
|
* @param {boolean} sendCleanImage - Czy wysłać czysty obraz (bez maski) do editora
|
||||||
*/
|
*/
|
||||||
async startMaskEditor(predefinedMask = null, sendCleanImage = true) {
|
async startMaskEditor(predefinedMask = null, sendCleanImage = true) {
|
||||||
log.info('Starting mask editor', {
|
return this.canvasMask.startMaskEditor(predefinedMask, sendCleanImage);
|
||||||
hasPredefinedMask: !!predefinedMask,
|
|
||||||
sendCleanImage,
|
|
||||||
layersCount: this.layers.length
|
|
||||||
});
|
|
||||||
|
|
||||||
this.savedMaskState = await this.saveMaskState();
|
|
||||||
this.maskEditorCancelled = false;
|
|
||||||
|
|
||||||
if (!predefinedMask && this.maskTool && this.maskTool.maskCanvas) {
|
|
||||||
try {
|
|
||||||
log.debug('Creating mask from current mask tool');
|
|
||||||
predefinedMask = await this.createMaskFromCurrentMask();
|
|
||||||
log.debug('Mask created from current mask tool successfully');
|
|
||||||
} catch (error) {
|
|
||||||
log.warn("Could not create mask from current mask:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pendingMask = predefinedMask;
|
|
||||||
|
|
||||||
let blob;
|
|
||||||
if (sendCleanImage) {
|
|
||||||
log.debug('Getting flattened canvas as blob (clean image)');
|
|
||||||
blob = await this.canvasLayers.getFlattenedCanvasAsBlob();
|
|
||||||
} else {
|
|
||||||
log.debug('Getting flattened canvas for mask editor (with mask)');
|
|
||||||
blob = await this.canvasLayers.getFlattenedCanvasForMaskEditor();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!blob) {
|
|
||||||
log.warn("Canvas is empty, cannot open mask editor.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug('Canvas blob created successfully, size:', blob.size);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const formData = new FormData();
|
|
||||||
const filename = `layerforge-mask-edit-${+new Date()}.png`;
|
|
||||||
formData.append("image", blob, filename);
|
|
||||||
formData.append("overwrite", "true");
|
|
||||||
formData.append("type", "temp");
|
|
||||||
|
|
||||||
log.debug('Uploading image to server:', filename);
|
|
||||||
|
|
||||||
const response = await api.fetchApi("/upload/image", {
|
|
||||||
method: "POST",
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to upload image: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
log.debug('Image uploaded successfully:', data);
|
|
||||||
|
|
||||||
const img = new Image();
|
|
||||||
img.src = api.apiURL(`/view?filename=${encodeURIComponent(data.name)}&type=${data.type}&subfolder=${data.subfolder}`);
|
|
||||||
await new Promise((res, rej) => {
|
|
||||||
img.onload = res;
|
|
||||||
img.onerror = rej;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.node.imgs = [img];
|
|
||||||
|
|
||||||
log.info('Opening ComfyUI mask editor');
|
|
||||||
ComfyApp.copyToClipspace(this.node);
|
|
||||||
ComfyApp.clipspace_return_node = this.node;
|
|
||||||
ComfyApp.open_maskeditor();
|
|
||||||
|
|
||||||
this.editorWasShowing = false;
|
|
||||||
this.waitWhileMaskEditing();
|
|
||||||
|
|
||||||
this.setupCancelListener();
|
|
||||||
|
|
||||||
if (predefinedMask) {
|
|
||||||
log.debug('Will apply predefined mask when editor is ready');
|
|
||||||
this.waitForMaskEditorAndApplyMask();
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
log.error("Error preparing image for mask editor:", error);
|
|
||||||
alert(`Error: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -600,14 +518,7 @@ export class Canvas {
|
|||||||
* Aktualizuje zaznaczenie po operacji historii
|
* Aktualizuje zaznaczenie po operacji historii
|
||||||
*/
|
*/
|
||||||
updateSelectionAfterHistory() {
|
updateSelectionAfterHistory() {
|
||||||
const newSelectedLayers = [];
|
return this.canvasSelection.updateSelectionAfterHistory();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -651,433 +562,4 @@ export class Canvas {
|
|||||||
this.onStateChange();
|
this.onStateChange();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Czeka na otwarcie mask editora i automatycznie nakłada predefiniowaną maskę
|
|
||||||
*/
|
|
||||||
waitForMaskEditorAndApplyMask() {
|
|
||||||
let attempts = 0;
|
|
||||||
const maxAttempts = 100; // Zwiększone do 10 sekund oczekiwania
|
|
||||||
|
|
||||||
const checkEditor = () => {
|
|
||||||
attempts++;
|
|
||||||
|
|
||||||
if (mask_editor_showing(app)) {
|
|
||||||
|
|
||||||
const useNewEditor = app.ui.settings.getSettingValue('Comfy.MaskEditor.UseNewEditor');
|
|
||||||
let editorReady = false;
|
|
||||||
|
|
||||||
if (useNewEditor) {
|
|
||||||
|
|
||||||
const MaskEditorDialog = window.MaskEditorDialog;
|
|
||||||
if (MaskEditorDialog && MaskEditorDialog.instance) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
const messageBroker = MaskEditorDialog.instance.getMessageBroker();
|
|
||||||
if (messageBroker) {
|
|
||||||
editorReady = true;
|
|
||||||
log.info("New mask editor detected as ready via MessageBroker");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
|
|
||||||
editorReady = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!editorReady) {
|
|
||||||
const maskEditorElement = document.getElementById('maskEditor');
|
|
||||||
if (maskEditorElement && maskEditorElement.style.display !== 'none') {
|
|
||||||
|
|
||||||
const canvas = maskEditorElement.querySelector('canvas');
|
|
||||||
if (canvas) {
|
|
||||||
editorReady = true;
|
|
||||||
log.info("New mask editor detected as ready via DOM element");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
|
|
||||||
const maskCanvas = document.getElementById('maskCanvas');
|
|
||||||
editorReady = maskCanvas && maskCanvas.getContext && maskCanvas.width > 0;
|
|
||||||
if (editorReady) {
|
|
||||||
log.info("Old mask editor detected as ready");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editorReady) {
|
|
||||||
|
|
||||||
log.info("Applying mask to editor after", attempts * 100, "ms wait");
|
|
||||||
setTimeout(() => {
|
|
||||||
this.applyMaskToEditor(this.pendingMask);
|
|
||||||
this.pendingMask = null;
|
|
||||||
}, 300);
|
|
||||||
} else if (attempts < maxAttempts) {
|
|
||||||
|
|
||||||
if (attempts % 10 === 0) {
|
|
||||||
log.info("Waiting for mask editor to be ready... attempt", attempts, "/", maxAttempts);
|
|
||||||
}
|
|
||||||
setTimeout(checkEditor, 100);
|
|
||||||
} else {
|
|
||||||
log.warn("Mask editor timeout - editor not ready after", maxAttempts * 100, "ms");
|
|
||||||
|
|
||||||
log.info("Attempting to apply mask anyway...");
|
|
||||||
setTimeout(() => {
|
|
||||||
this.applyMaskToEditor(this.pendingMask);
|
|
||||||
this.pendingMask = null;
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
} else if (attempts < maxAttempts) {
|
|
||||||
|
|
||||||
setTimeout(checkEditor, 100);
|
|
||||||
} else {
|
|
||||||
log.warn("Mask editor timeout - editor not showing after", maxAttempts * 100, "ms");
|
|
||||||
this.pendingMask = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
checkEditor();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Nakłada maskę na otwarty mask editor
|
|
||||||
* @param {Image|HTMLCanvasElement} maskData - Dane maski do nałożenia
|
|
||||||
*/
|
|
||||||
async applyMaskToEditor(maskData) {
|
|
||||||
try {
|
|
||||||
|
|
||||||
const useNewEditor = app.ui.settings.getSettingValue('Comfy.MaskEditor.UseNewEditor');
|
|
||||||
|
|
||||||
if (useNewEditor) {
|
|
||||||
|
|
||||||
const MaskEditorDialog = window.MaskEditorDialog;
|
|
||||||
if (MaskEditorDialog && MaskEditorDialog.instance) {
|
|
||||||
|
|
||||||
await this.applyMaskToNewEditor(maskData);
|
|
||||||
} else {
|
|
||||||
log.warn("New editor setting enabled but instance not found, trying old editor");
|
|
||||||
await this.applyMaskToOldEditor(maskData);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
|
|
||||||
await this.applyMaskToOldEditor(maskData);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Predefined mask applied to mask editor successfully");
|
|
||||||
} catch (error) {
|
|
||||||
log.error("Failed to apply predefined mask to editor:", error);
|
|
||||||
|
|
||||||
try {
|
|
||||||
log.info("Trying alternative mask application method...");
|
|
||||||
await this.applyMaskToOldEditor(maskData);
|
|
||||||
log.info("Alternative method succeeded");
|
|
||||||
} catch (fallbackError) {
|
|
||||||
log.error("Alternative method also failed:", fallbackError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Nakłada maskę na nowy mask editor (przez MessageBroker)
|
|
||||||
* @param {Image|HTMLCanvasElement} maskData - Dane maski
|
|
||||||
*/
|
|
||||||
async applyMaskToNewEditor(maskData) {
|
|
||||||
|
|
||||||
const MaskEditorDialog = window.MaskEditorDialog;
|
|
||||||
if (!MaskEditorDialog || !MaskEditorDialog.instance) {
|
|
||||||
throw new Error("New mask editor instance not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const editor = MaskEditorDialog.instance;
|
|
||||||
const messageBroker = editor.getMessageBroker();
|
|
||||||
|
|
||||||
const maskCanvas = await messageBroker.pull('maskCanvas');
|
|
||||||
const maskCtx = await messageBroker.pull('maskCtx');
|
|
||||||
const maskColor = await messageBroker.pull('getMaskColor');
|
|
||||||
|
|
||||||
const processedMask = await this.processMaskForEditor(maskData, maskCanvas.width, maskCanvas.height, maskColor);
|
|
||||||
|
|
||||||
maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
|
|
||||||
maskCtx.drawImage(processedMask, 0, 0);
|
|
||||||
|
|
||||||
messageBroker.publish('saveState');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Nakłada maskę na stary mask editor
|
|
||||||
* @param {Image|HTMLCanvasElement} maskData - Dane maski
|
|
||||||
*/
|
|
||||||
async applyMaskToOldEditor(maskData) {
|
|
||||||
|
|
||||||
const maskCanvas = document.getElementById('maskCanvas');
|
|
||||||
if (!maskCanvas) {
|
|
||||||
throw new Error("Old mask editor canvas not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const maskCtx = maskCanvas.getContext('2d', {willReadFrequently: true});
|
|
||||||
|
|
||||||
const maskColor = {r: 255, g: 255, b: 255};
|
|
||||||
const processedMask = await this.processMaskForEditor(maskData, maskCanvas.width, maskCanvas.height, maskColor);
|
|
||||||
|
|
||||||
maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
|
|
||||||
maskCtx.drawImage(processedMask, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Przetwarza maskę do odpowiedniego formatu dla editora
|
|
||||||
* @param {Image|HTMLCanvasElement} maskData - Oryginalne dane maski
|
|
||||||
* @param {number} targetWidth - Docelowa szerokość
|
|
||||||
* @param {number} targetHeight - Docelowa wysokość
|
|
||||||
* @param {Object} maskColor - Kolor maski {r, g, b}
|
|
||||||
* @returns {HTMLCanvasElement} Przetworzona maska
|
|
||||||
*/async processMaskForEditor(maskData, targetWidth, targetHeight, maskColor) {
|
|
||||||
// Współrzędne przesunięcia (pan) widoku edytora
|
|
||||||
const panX = this.maskTool.x;
|
|
||||||
const panY = this.maskTool.y;
|
|
||||||
|
|
||||||
log.info("Processing mask for editor:", {
|
|
||||||
sourceSize: {width: maskData.width, height: maskData.height},
|
|
||||||
targetSize: {width: targetWidth, height: targetHeight},
|
|
||||||
viewportPan: {x: panX, y: panY}
|
|
||||||
});
|
|
||||||
|
|
||||||
const tempCanvas = document.createElement('canvas');
|
|
||||||
tempCanvas.width = targetWidth;
|
|
||||||
tempCanvas.height = targetHeight;
|
|
||||||
const tempCtx = tempCanvas.getContext('2d', {willReadFrequently: true});
|
|
||||||
|
|
||||||
const sourceX = -panX;
|
|
||||||
const sourceY = -panY;
|
|
||||||
|
|
||||||
tempCtx.drawImage(
|
|
||||||
maskData, // Źródło: pełna maska z "output area"
|
|
||||||
sourceX, // sx: Prawdziwa współrzędna X na dużej masce (np. 1000)
|
|
||||||
sourceY, // sy: Prawdziwa współrzędna Y na dużej masce (np. 1000)
|
|
||||||
targetWidth, // sWidth: Szerokość wycinanego fragmentu
|
|
||||||
targetHeight, // sHeight: Wysokość wycinanego fragmentu
|
|
||||||
0, // dx: Gdzie wkleić w płótnie docelowym (zawsze 0)
|
|
||||||
0, // dy: Gdzie wkleić w płótnie docelowym (zawsze 0)
|
|
||||||
targetWidth, // dWidth: Szerokość wklejanego obrazu
|
|
||||||
targetHeight // dHeight: Wysokość wklejanego obrazu
|
|
||||||
);
|
|
||||||
|
|
||||||
log.info("Mask viewport cropped correctly.", {
|
|
||||||
source: "maskData",
|
|
||||||
cropArea: {x: sourceX, y: sourceY, width: targetWidth, height: targetHeight}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reszta kodu (zmiana koloru) pozostaje bez zmian
|
|
||||||
const imageData = tempCtx.getImageData(0, 0, targetWidth, targetHeight);
|
|
||||||
const data = imageData.data;
|
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i += 4) {
|
|
||||||
const alpha = data[i + 3];
|
|
||||||
if (alpha > 0) {
|
|
||||||
data[i] = maskColor.r;
|
|
||||||
data[i + 1] = maskColor.g;
|
|
||||||
data[i + 2] = maskColor.b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tempCtx.putImageData(imageData, 0, 0);
|
|
||||||
|
|
||||||
log.info("Mask processing completed - color applied.");
|
|
||||||
return tempCanvas;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tworzy obiekt Image z obecnej maski canvas
|
|
||||||
* @returns {Promise<Image>} Promise zwracający obiekt Image z maską
|
|
||||||
*/
|
|
||||||
async createMaskFromCurrentMask() {
|
|
||||||
if (!this.maskTool || !this.maskTool.maskCanvas) {
|
|
||||||
throw new Error("No mask canvas available");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const maskImage = new Image();
|
|
||||||
maskImage.onload = () => resolve(maskImage);
|
|
||||||
maskImage.onerror = reject;
|
|
||||||
maskImage.src = this.maskTool.maskCanvas.toDataURL();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
waitWhileMaskEditing() {
|
|
||||||
if (mask_editor_showing(app)) {
|
|
||||||
this.editorWasShowing = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mask_editor_showing(app) && this.editorWasShowing) {
|
|
||||||
this.editorWasShowing = false;
|
|
||||||
setTimeout(() => this.handleMaskEditorClose(), 100);
|
|
||||||
} else {
|
|
||||||
setTimeout(this.waitWhileMaskEditing.bind(this), 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Zapisuje obecny stan maski przed otwarciem editora
|
|
||||||
* @returns {Object} Zapisany stan maski
|
|
||||||
*/
|
|
||||||
async saveMaskState() {
|
|
||||||
if (!this.maskTool || !this.maskTool.maskCanvas) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const maskCanvas = this.maskTool.maskCanvas;
|
|
||||||
const savedCanvas = document.createElement('canvas');
|
|
||||||
savedCanvas.width = maskCanvas.width;
|
|
||||||
savedCanvas.height = maskCanvas.height;
|
|
||||||
const savedCtx = savedCanvas.getContext('2d', {willReadFrequently: true});
|
|
||||||
savedCtx.drawImage(maskCanvas, 0, 0);
|
|
||||||
|
|
||||||
return {
|
|
||||||
maskData: savedCanvas,
|
|
||||||
maskPosition: {
|
|
||||||
x: this.maskTool.x,
|
|
||||||
y: this.maskTool.y
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Przywraca zapisany stan maski
|
|
||||||
* @param {Object} savedState - Zapisany stan maski
|
|
||||||
*/
|
|
||||||
async restoreMaskState(savedState) {
|
|
||||||
if (!savedState || !this.maskTool) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (savedState.maskData) {
|
|
||||||
const maskCtx = this.maskTool.maskCtx;
|
|
||||||
maskCtx.clearRect(0, 0, this.maskTool.maskCanvas.width, this.maskTool.maskCanvas.height);
|
|
||||||
maskCtx.drawImage(savedState.maskData, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (savedState.maskPosition) {
|
|
||||||
this.maskTool.x = savedState.maskPosition.x;
|
|
||||||
this.maskTool.y = savedState.maskPosition.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.render();
|
|
||||||
log.info("Mask state restored after cancel");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Konfiguruje nasłuchiwanie na przycisk Cancel w mask editorze
|
|
||||||
*/
|
|
||||||
setupCancelListener() {
|
|
||||||
mask_editor_listen_for_cancel(app, () => {
|
|
||||||
log.info("Mask editor cancel button clicked");
|
|
||||||
this.maskEditorCancelled = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sprawdza czy mask editor został anulowany i obsługuje to odpowiednio
|
|
||||||
*/
|
|
||||||
async handleMaskEditorClose() {
|
|
||||||
log.info("Handling mask editor close");
|
|
||||||
log.debug("Node object after mask editor close:", this.node);
|
|
||||||
|
|
||||||
if (this.maskEditorCancelled) {
|
|
||||||
log.info("Mask editor was cancelled - restoring original mask state");
|
|
||||||
|
|
||||||
if (this.savedMaskState) {
|
|
||||||
await this.restoreMaskState(this.savedMaskState);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.maskEditorCancelled = false;
|
|
||||||
this.savedMaskState = null;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.node.imgs || !this.node.imgs.length === 0 || !this.node.imgs[0].src) {
|
|
||||||
log.warn("Mask editor was closed without a result.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("Processing mask editor result, image source:", this.node.imgs[0].src.substring(0, 100) + '...');
|
|
||||||
|
|
||||||
const resultImage = new Image();
|
|
||||||
resultImage.src = this.node.imgs[0].src;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
resultImage.onload = resolve;
|
|
||||||
resultImage.onerror = reject;
|
|
||||||
});
|
|
||||||
|
|
||||||
log.debug("Result image loaded successfully", {
|
|
||||||
width: resultImage.width,
|
|
||||||
height: resultImage.height
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
log.error("Failed to load image from mask editor.", error);
|
|
||||||
this.node.imgs = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("Creating temporary canvas for mask processing");
|
|
||||||
const tempCanvas = document.createElement('canvas');
|
|
||||||
tempCanvas.width = this.width;
|
|
||||||
tempCanvas.height = this.height;
|
|
||||||
const tempCtx = tempCanvas.getContext('2d', {willReadFrequently: true});
|
|
||||||
|
|
||||||
tempCtx.drawImage(resultImage, 0, 0, this.width, this.height);
|
|
||||||
|
|
||||||
log.debug("Processing image data to create mask");
|
|
||||||
const imageData = tempCtx.getImageData(0, 0, this.width, this.height);
|
|
||||||
const data = imageData.data;
|
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i += 4) {
|
|
||||||
const originalAlpha = data[i + 3];
|
|
||||||
data[i] = 255;
|
|
||||||
data[i + 1] = 255;
|
|
||||||
data[i + 2] = 255;
|
|
||||||
data[i + 3] = 255 - originalAlpha;
|
|
||||||
}
|
|
||||||
|
|
||||||
tempCtx.putImageData(imageData, 0, 0);
|
|
||||||
|
|
||||||
log.debug("Converting processed mask to image");
|
|
||||||
const maskAsImage = new Image();
|
|
||||||
maskAsImage.src = tempCanvas.toDataURL();
|
|
||||||
await new Promise(resolve => maskAsImage.onload = resolve);
|
|
||||||
|
|
||||||
const maskCtx = this.maskTool.maskCtx;
|
|
||||||
const destX = -this.maskTool.x;
|
|
||||||
const destY = -this.maskTool.y;
|
|
||||||
|
|
||||||
log.debug("Applying mask to canvas", {destX, destY});
|
|
||||||
|
|
||||||
maskCtx.globalCompositeOperation = 'source-over';
|
|
||||||
maskCtx.clearRect(destX, destY, this.width, this.height);
|
|
||||||
|
|
||||||
maskCtx.drawImage(maskAsImage, destX, destY);
|
|
||||||
|
|
||||||
this.render();
|
|
||||||
this.saveState();
|
|
||||||
|
|
||||||
log.debug("Creating new preview image");
|
|
||||||
const new_preview = new Image();
|
|
||||||
|
|
||||||
const blob = await this.canvasLayers.getFlattenedCanvasWithMaskAsBlob();
|
|
||||||
if (blob) {
|
|
||||||
new_preview.src = URL.createObjectURL(blob);
|
|
||||||
await new Promise(r => new_preview.onload = r);
|
|
||||||
this.node.imgs = [new_preview];
|
|
||||||
log.debug("New preview image created successfully");
|
|
||||||
} else {
|
|
||||||
this.node.imgs = [];
|
|
||||||
log.warn("Failed to create preview blob");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.render();
|
|
||||||
|
|
||||||
this.savedMaskState = null;
|
|
||||||
log.info("Mask editor result processed successfully");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -744,12 +744,7 @@ export class CanvasIO {
|
|||||||
img.src = result.image_data;
|
img.src = result.image_data;
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.canvas.canvasLayers.addLayerWithImage(img, {
|
await this.canvas.canvasLayers.addLayerWithImage(img, {}, 'fit');
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: this.canvas.width,
|
|
||||||
height: this.canvas.height,
|
|
||||||
});
|
|
||||||
log.info("Latest image imported and placed on canvas successfully.");
|
log.info("Latest image imported and placed on canvas successfully.");
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@@ -761,4 +756,41 @@ export class CanvasIO {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async importLatestImages(sinceTimestamp, targetArea = null) {
|
||||||
|
try {
|
||||||
|
log.info(`Fetching latest images since ${sinceTimestamp}...`);
|
||||||
|
const response = await fetch(`/layerforge/get-latest-images/${sinceTimestamp}`);
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success && result.images && result.images.length > 0) {
|
||||||
|
log.info(`Received ${result.images.length} new images, adding to canvas.`);
|
||||||
|
const newLayers = [];
|
||||||
|
|
||||||
|
for (const imageData of result.images) {
|
||||||
|
const img = new Image();
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
img.onload = resolve;
|
||||||
|
img.onerror = reject;
|
||||||
|
img.src = imageData;
|
||||||
|
});
|
||||||
|
const newLayer = await this.canvas.canvasLayers.addLayerWithImage(img, {}, 'fit', targetArea);
|
||||||
|
newLayers.push(newLayer);
|
||||||
|
}
|
||||||
|
log.info("All new images imported and placed on canvas successfully.");
|
||||||
|
return newLayers;
|
||||||
|
|
||||||
|
} else if (result.success) {
|
||||||
|
log.info("No new images found since last generation.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error(result.error || "Failed to fetch latest images.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Error importing latest images:", error);
|
||||||
|
alert(`Failed to import latest images: ${error.message}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,12 @@ export class CanvasInteractions {
|
|||||||
const worldCoords = this.canvas.getMouseWorldCoordinates(e);
|
const worldCoords = this.canvas.getMouseWorldCoordinates(e);
|
||||||
const viewCoords = this.canvas.getMouseViewCoordinates(e);
|
const viewCoords = this.canvas.getMouseViewCoordinates(e);
|
||||||
|
|
||||||
|
if (this.interaction.mode === 'drawingMask') {
|
||||||
|
this.canvas.maskTool.handleMouseDown(worldCoords, viewCoords);
|
||||||
|
this.canvas.render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Ostateczna, poprawna kolejność sprawdzania ---
|
// --- Ostateczna, poprawna kolejność sprawdzania ---
|
||||||
|
|
||||||
// 1. Akcje globalne z modyfikatorami (mają najwyższy priorytet)
|
// 1. Akcje globalne z modyfikatorami (mają najwyższy priorytet)
|
||||||
@@ -85,7 +91,7 @@ export class CanvasInteractions {
|
|||||||
// 2. Inne przyciski myszy
|
// 2. Inne przyciski myszy
|
||||||
if (e.button === 2) { // Prawy przycisk myszy
|
if (e.button === 2) { // Prawy przycisk myszy
|
||||||
const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(worldCoords.x, worldCoords.y);
|
const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(worldCoords.x, worldCoords.y);
|
||||||
if (clickedLayerResult && this.canvas.selectedLayers.includes(clickedLayerResult.layer)) {
|
if (clickedLayerResult && this.canvas.canvasSelection.selectedLayers.includes(clickedLayerResult.layer)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.canvas.canvasLayers.showBlendModeMenu(viewCoords.x, viewCoords.y);
|
this.canvas.canvasLayers.showBlendModeMenu(viewCoords.x, viewCoords.y);
|
||||||
}
|
}
|
||||||
@@ -115,6 +121,7 @@ export class CanvasInteractions {
|
|||||||
|
|
||||||
handleMouseMove(e) {
|
handleMouseMove(e) {
|
||||||
const worldCoords = this.canvas.getMouseWorldCoordinates(e);
|
const worldCoords = this.canvas.getMouseWorldCoordinates(e);
|
||||||
|
const viewCoords = this.canvas.getMouseViewCoordinates(e);
|
||||||
this.canvas.lastMousePosition = worldCoords; // Zawsze aktualizuj ostatnią pozycję myszy
|
this.canvas.lastMousePosition = worldCoords; // Zawsze aktualizuj ostatnią pozycję myszy
|
||||||
|
|
||||||
// Sprawdź, czy rozpocząć przeciąganie
|
// Sprawdź, czy rozpocząć przeciąganie
|
||||||
@@ -124,13 +131,17 @@ export class CanvasInteractions {
|
|||||||
if (Math.sqrt(dx * dx + dy * dy) > 3) { // Próg 3 pikseli
|
if (Math.sqrt(dx * dx + dy * dy) > 3) { // Próg 3 pikseli
|
||||||
this.interaction.mode = 'dragging';
|
this.interaction.mode = 'dragging';
|
||||||
this.originalLayerPositions.clear();
|
this.originalLayerPositions.clear();
|
||||||
this.canvas.selectedLayers.forEach(l => {
|
this.canvas.canvasSelection.selectedLayers.forEach(l => {
|
||||||
this.originalLayerPositions.set(l, {x: l.x, y: l.y});
|
this.originalLayerPositions.set(l, {x: l.x, y: l.y});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (this.interaction.mode) {
|
switch (this.interaction.mode) {
|
||||||
|
case 'drawingMask':
|
||||||
|
this.canvas.maskTool.handleMouseMove(worldCoords, viewCoords);
|
||||||
|
this.canvas.render();
|
||||||
|
break;
|
||||||
case 'panning':
|
case 'panning':
|
||||||
this.panViewport(e);
|
this.panViewport(e);
|
||||||
break;
|
break;
|
||||||
@@ -156,6 +167,13 @@ export class CanvasInteractions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleMouseUp(e) {
|
handleMouseUp(e) {
|
||||||
|
const viewCoords = this.canvas.getMouseViewCoordinates(e);
|
||||||
|
if (this.interaction.mode === 'drawingMask') {
|
||||||
|
this.canvas.maskTool.handleMouseUp(viewCoords);
|
||||||
|
this.canvas.render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.interaction.mode === 'resizingCanvas') {
|
if (this.interaction.mode === 'resizingCanvas') {
|
||||||
this.finalizeCanvasResize();
|
this.finalizeCanvasResize();
|
||||||
}
|
}
|
||||||
@@ -226,7 +244,7 @@ export class CanvasInteractions {
|
|||||||
const rotationStep = 5 * (e.deltaY > 0 ? -1 : 1);
|
const rotationStep = 5 * (e.deltaY > 0 ? -1 : 1);
|
||||||
const direction = e.deltaY < 0 ? 1 : -1; // 1 = up/right, -1 = down/left
|
const direction = e.deltaY < 0 ? 1 : -1; // 1 = up/right, -1 = down/left
|
||||||
|
|
||||||
this.canvas.selectedLayers.forEach(layer => {
|
this.canvas.canvasSelection.selectedLayers.forEach(layer => {
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
// Nowy skrót: Shift + Ctrl + Kółko do przyciągania do absolutnych wartości
|
// Nowy skrót: Shift + Ctrl + Kółko do przyciągania do absolutnych wartości
|
||||||
if (e.ctrlKey) {
|
if (e.ctrlKey) {
|
||||||
@@ -324,13 +342,10 @@ export class CanvasInteractions {
|
|||||||
this.canvas.redo();
|
this.canvas.redo();
|
||||||
break;
|
break;
|
||||||
case 'c':
|
case 'c':
|
||||||
if (this.canvas.selectedLayers.length > 0) {
|
if (this.canvas.canvasSelection.selectedLayers.length > 0) {
|
||||||
this.canvas.canvasLayers.copySelectedLayers();
|
this.canvas.canvasLayers.copySelectedLayers();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'v':
|
|
||||||
this.canvas.canvasLayers.handlePaste('mouse');
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
handled = false;
|
handled = false;
|
||||||
break;
|
break;
|
||||||
@@ -343,7 +358,7 @@ export class CanvasInteractions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Skróty kontekstowe (zależne od zaznaczenia)
|
// Skróty kontekstowe (zależne od zaznaczenia)
|
||||||
if (this.canvas.selectedLayers.length > 0) {
|
if (this.canvas.canvasSelection.selectedLayers.length > 0) {
|
||||||
const step = e.shiftKey ? 10 : 1;
|
const step = e.shiftKey ? 10 : 1;
|
||||||
let needsRender = false;
|
let needsRender = false;
|
||||||
|
|
||||||
@@ -354,12 +369,12 @@ export class CanvasInteractions {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.interaction.keyMovementInProgress = true;
|
this.interaction.keyMovementInProgress = true;
|
||||||
|
|
||||||
if (e.code === 'ArrowLeft') this.canvas.selectedLayers.forEach(l => l.x -= step);
|
if (e.code === 'ArrowLeft') this.canvas.canvasSelection.selectedLayers.forEach(l => l.x -= step);
|
||||||
if (e.code === 'ArrowRight') this.canvas.selectedLayers.forEach(l => l.x += step);
|
if (e.code === 'ArrowRight') this.canvas.canvasSelection.selectedLayers.forEach(l => l.x += step);
|
||||||
if (e.code === 'ArrowUp') this.canvas.selectedLayers.forEach(l => l.y -= step);
|
if (e.code === 'ArrowUp') this.canvas.canvasSelection.selectedLayers.forEach(l => l.y -= step);
|
||||||
if (e.code === 'ArrowDown') this.canvas.selectedLayers.forEach(l => l.y += step);
|
if (e.code === 'ArrowDown') this.canvas.canvasSelection.selectedLayers.forEach(l => l.y += step);
|
||||||
if (e.code === 'BracketLeft') this.canvas.selectedLayers.forEach(l => l.rotation -= step);
|
if (e.code === 'BracketLeft') this.canvas.canvasSelection.selectedLayers.forEach(l => l.rotation -= step);
|
||||||
if (e.code === 'BracketRight') this.canvas.selectedLayers.forEach(l => l.rotation += step);
|
if (e.code === 'BracketRight') this.canvas.canvasSelection.selectedLayers.forEach(l => l.rotation += step);
|
||||||
|
|
||||||
needsRender = true;
|
needsRender = true;
|
||||||
}
|
}
|
||||||
@@ -367,7 +382,7 @@ export class CanvasInteractions {
|
|||||||
if (e.key === 'Delete' || e.key === 'Backspace') {
|
if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.canvas.removeSelectedLayers();
|
this.canvas.canvasSelection.removeSelectedLayers();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,16 +450,16 @@ export class CanvasInteractions {
|
|||||||
prepareForDrag(layer, worldCoords) {
|
prepareForDrag(layer, worldCoords) {
|
||||||
// Zaktualizuj zaznaczenie, ale nie zapisuj stanu
|
// Zaktualizuj zaznaczenie, ale nie zapisuj stanu
|
||||||
if (this.interaction.isCtrlPressed) {
|
if (this.interaction.isCtrlPressed) {
|
||||||
const index = this.canvas.selectedLayers.indexOf(layer);
|
const index = this.canvas.canvasSelection.selectedLayers.indexOf(layer);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
this.canvas.updateSelection([...this.canvas.selectedLayers, layer]);
|
this.canvas.canvasSelection.updateSelection([...this.canvas.canvasSelection.selectedLayers, layer]);
|
||||||
} else {
|
} else {
|
||||||
const newSelection = this.canvas.selectedLayers.filter(l => l !== layer);
|
const newSelection = this.canvas.canvasSelection.selectedLayers.filter(l => l !== layer);
|
||||||
this.canvas.updateSelection(newSelection);
|
this.canvas.canvasSelection.updateSelection(newSelection);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!this.canvas.selectedLayers.includes(layer)) {
|
if (!this.canvas.canvasSelection.selectedLayers.includes(layer)) {
|
||||||
this.canvas.updateSelection([layer]);
|
this.canvas.canvasSelection.updateSelection([layer]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,7 +471,7 @@ export class CanvasInteractions {
|
|||||||
// Ta funkcja jest teraz wywoływana tylko gdy kliknięto na tło bez modyfikatorów.
|
// Ta funkcja jest teraz wywoływana tylko gdy kliknięto na tło bez modyfikatorów.
|
||||||
// Domyślna akcja: wyczyść zaznaczenie i rozpocznij panoramowanie.
|
// Domyślna akcja: wyczyść zaznaczenie i rozpocznij panoramowanie.
|
||||||
if (!this.interaction.isCtrlPressed) {
|
if (!this.interaction.isCtrlPressed) {
|
||||||
this.canvas.updateSelection([]);
|
this.canvas.canvasSelection.updateSelection([]);
|
||||||
}
|
}
|
||||||
this.interaction.mode = 'panning';
|
this.interaction.mode = 'panning';
|
||||||
this.interaction.panStart = {x: e.clientX, y: e.clientY};
|
this.interaction.panStart = {x: e.clientX, y: e.clientY};
|
||||||
@@ -514,6 +529,29 @@ export class CanvasInteractions {
|
|||||||
|
|
||||||
this.canvas.maskTool.updatePosition(-finalX, -finalY);
|
this.canvas.maskTool.updatePosition(-finalX, -finalY);
|
||||||
|
|
||||||
|
// If a batch generation is in progress, update the captured context as well
|
||||||
|
if (this.canvas.pendingBatchContext) {
|
||||||
|
this.canvas.pendingBatchContext.outputArea.x -= finalX;
|
||||||
|
this.canvas.pendingBatchContext.outputArea.y -= finalY;
|
||||||
|
|
||||||
|
// Also update the menu spawn position to keep it relative
|
||||||
|
this.canvas.pendingBatchContext.spawnPosition.x -= finalX;
|
||||||
|
this.canvas.pendingBatchContext.spawnPosition.y -= finalY;
|
||||||
|
log.debug("Updated pending batch context during canvas move:", this.canvas.pendingBatchContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also move any active batch preview menus
|
||||||
|
if (this.canvas.batchPreviewManagers && this.canvas.batchPreviewManagers.length > 0) {
|
||||||
|
this.canvas.batchPreviewManagers.forEach(manager => {
|
||||||
|
manager.worldX -= finalX;
|
||||||
|
manager.worldY -= finalY;
|
||||||
|
if (manager.generationArea) {
|
||||||
|
manager.generationArea.x -= finalX;
|
||||||
|
manager.generationArea.y -= finalY;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.canvas.viewport.x -= finalX;
|
this.canvas.viewport.x -= finalX;
|
||||||
this.canvas.viewport.y -= finalY;
|
this.canvas.viewport.y -= finalY;
|
||||||
}
|
}
|
||||||
@@ -523,7 +561,7 @@ export class CanvasInteractions {
|
|||||||
|
|
||||||
startPanning(e) {
|
startPanning(e) {
|
||||||
if (!this.interaction.isCtrlPressed) {
|
if (!this.interaction.isCtrlPressed) {
|
||||||
this.canvas.updateSelection([]);
|
this.canvas.canvasSelection.updateSelection([]);
|
||||||
}
|
}
|
||||||
this.interaction.mode = 'panning';
|
this.interaction.mode = 'panning';
|
||||||
this.interaction.panStart = {x: e.clientX, y: e.clientY};
|
this.interaction.panStart = {x: e.clientX, y: e.clientY};
|
||||||
@@ -539,9 +577,9 @@ export class CanvasInteractions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dragLayers(worldCoords) {
|
dragLayers(worldCoords) {
|
||||||
if (this.interaction.isAltPressed && !this.interaction.hasClonedInDrag && this.canvas.selectedLayers.length > 0) {
|
if (this.interaction.isAltPressed && !this.interaction.hasClonedInDrag && this.canvas.canvasSelection.selectedLayers.length > 0) {
|
||||||
// Scentralizowana logika duplikowania
|
// Scentralizowana logika duplikowania
|
||||||
const newLayers = this.canvas.duplicateSelectedLayers();
|
const newLayers = this.canvas.canvasSelection.duplicateSelectedLayers();
|
||||||
|
|
||||||
// Zresetuj pozycje przeciągania dla nowych, zduplikowanych warstw
|
// Zresetuj pozycje przeciągania dla nowych, zduplikowanych warstw
|
||||||
this.originalLayerPositions.clear();
|
this.originalLayerPositions.clear();
|
||||||
@@ -554,11 +592,11 @@ export class CanvasInteractions {
|
|||||||
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;
|
||||||
|
|
||||||
if (this.interaction.isCtrlPressed && this.canvas.selectedLayer) {
|
if (this.interaction.isCtrlPressed && this.canvas.canvasSelection.selectedLayer) {
|
||||||
const originalPos = this.originalLayerPositions.get(this.canvas.selectedLayer);
|
const originalPos = this.originalLayerPositions.get(this.canvas.canvasSelection.selectedLayer);
|
||||||
if (originalPos) {
|
if (originalPos) {
|
||||||
const tempLayerForSnap = {
|
const tempLayerForSnap = {
|
||||||
...this.canvas.selectedLayer,
|
...this.canvas.canvasSelection.selectedLayer,
|
||||||
x: originalPos.x + totalDx,
|
x: originalPos.x + totalDx,
|
||||||
y: originalPos.y + totalDy
|
y: originalPos.y + totalDy
|
||||||
};
|
};
|
||||||
@@ -568,7 +606,7 @@ export class CanvasInteractions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.canvas.selectedLayers.forEach(layer => {
|
this.canvas.canvasSelection.selectedLayers.forEach(layer => {
|
||||||
const originalPos = this.originalLayerPositions.get(layer);
|
const originalPos = this.originalLayerPositions.get(layer);
|
||||||
if (originalPos) {
|
if (originalPos) {
|
||||||
layer.x = originalPos.x + finalDx;
|
layer.x = originalPos.x + finalDx;
|
||||||
@@ -683,20 +721,43 @@ export class CanvasInteractions {
|
|||||||
if (this.interaction.canvasResizeRect && this.interaction.canvasResizeRect.width > 1 && this.interaction.canvasResizeRect.height > 1) {
|
if (this.interaction.canvasResizeRect && this.interaction.canvasResizeRect.width > 1 && this.interaction.canvasResizeRect.height > 1) {
|
||||||
const newWidth = Math.round(this.interaction.canvasResizeRect.width);
|
const newWidth = Math.round(this.interaction.canvasResizeRect.width);
|
||||||
const newHeight = Math.round(this.interaction.canvasResizeRect.height);
|
const newHeight = Math.round(this.interaction.canvasResizeRect.height);
|
||||||
const rectX = this.interaction.canvasResizeRect.x;
|
const finalX = this.interaction.canvasResizeRect.x;
|
||||||
const rectY = this.interaction.canvasResizeRect.y;
|
const finalY = this.interaction.canvasResizeRect.y;
|
||||||
|
|
||||||
this.canvas.updateOutputAreaSize(newWidth, newHeight);
|
this.canvas.updateOutputAreaSize(newWidth, newHeight);
|
||||||
|
|
||||||
this.canvas.layers.forEach(layer => {
|
this.canvas.layers.forEach(layer => {
|
||||||
layer.x -= rectX;
|
layer.x -= finalX;
|
||||||
layer.y -= rectY;
|
layer.y -= finalY;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.canvas.maskTool.updatePosition(-rectX, -rectY);
|
this.canvas.maskTool.updatePosition(-finalX, -finalY);
|
||||||
|
|
||||||
this.canvas.viewport.x -= rectX;
|
// If a batch generation is in progress, update the captured context as well
|
||||||
this.canvas.viewport.y -= rectY;
|
if (this.canvas.pendingBatchContext) {
|
||||||
|
this.canvas.pendingBatchContext.outputArea.x -= finalX;
|
||||||
|
this.canvas.pendingBatchContext.outputArea.y -= finalY;
|
||||||
|
|
||||||
|
// Also update the menu spawn position to keep it relative
|
||||||
|
this.canvas.pendingBatchContext.spawnPosition.x -= finalX;
|
||||||
|
this.canvas.pendingBatchContext.spawnPosition.y -= finalY;
|
||||||
|
log.debug("Updated pending batch context during canvas resize:", this.canvas.pendingBatchContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also move any active batch preview menus
|
||||||
|
if (this.canvas.batchPreviewManagers && this.canvas.batchPreviewManagers.length > 0) {
|
||||||
|
this.canvas.batchPreviewManagers.forEach(manager => {
|
||||||
|
manager.worldX -= finalX;
|
||||||
|
manager.worldY -= finalY;
|
||||||
|
if (manager.generationArea) {
|
||||||
|
manager.generationArea.x -= finalX;
|
||||||
|
manager.generationArea.y -= finalY;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.canvas.viewport.x -= finalX;
|
||||||
|
this.canvas.viewport.y -= finalY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ export class CanvasLayers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async copySelectedLayers() {
|
async copySelectedLayers() {
|
||||||
if (this.canvas.selectedLayers.length === 0) return;
|
if (this.canvas.canvasSelection.selectedLayers.length === 0) return;
|
||||||
|
|
||||||
this.internalClipboard = this.canvas.selectedLayers.map(layer => ({...layer}));
|
this.internalClipboard = this.canvas.canvasSelection.selectedLayers.map(layer => ({...layer}));
|
||||||
log.info(`Copied ${this.internalClipboard.length} layer(s) to internal clipboard.`);
|
log.info(`Copied ${this.internalClipboard.length} layer(s) to internal clipboard.`);
|
||||||
|
|
||||||
const blob = await this.getFlattenedSelectionAsBlob();
|
const blob = await this.getFlattenedSelectionAsBlob();
|
||||||
@@ -149,12 +149,12 @@ export class CanvasLayers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
addLayerWithImage = withErrorHandling(async (image, layerProps = {}, addMode = 'default') => {
|
addLayerWithImage = withErrorHandling(async (image, layerProps = {}, addMode = 'default', targetArea = null) => {
|
||||||
if (!image) {
|
if (!image) {
|
||||||
throw createValidationError("Image is required for layer creation");
|
throw createValidationError("Image is required for layer creation");
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug("Adding layer with image:", image, "with mode:", addMode);
|
log.debug("Adding layer with image:", image, "with mode:", addMode, "targetArea:", targetArea);
|
||||||
const imageId = generateUUID();
|
const imageId = generateUUID();
|
||||||
await saveImage(imageId, image.src);
|
await saveImage(imageId, image.src);
|
||||||
this.canvas.imageCache.set(imageId, image.src);
|
this.canvas.imageCache.set(imageId, image.src);
|
||||||
@@ -163,21 +163,25 @@ export class CanvasLayers {
|
|||||||
let finalHeight = image.height;
|
let finalHeight = image.height;
|
||||||
let finalX, finalY;
|
let finalX, finalY;
|
||||||
|
|
||||||
|
// Use the targetArea if provided, otherwise default to the current canvas dimensions
|
||||||
|
const area = targetArea || { width: this.canvas.width, height: this.canvas.height, x: 0, y: 0 };
|
||||||
|
|
||||||
if (addMode === 'fit') {
|
if (addMode === 'fit') {
|
||||||
const scale = Math.min(this.canvas.width / image.width, this.canvas.height / image.height);
|
const scale = Math.min(area.width / image.width, area.height / image.height);
|
||||||
finalWidth = image.width * scale;
|
finalWidth = image.width * scale;
|
||||||
finalHeight = image.height * scale;
|
finalHeight = image.height * scale;
|
||||||
finalX = (this.canvas.width - finalWidth) / 2;
|
finalX = area.x + (area.width - finalWidth) / 2;
|
||||||
finalY = (this.canvas.height - finalHeight) / 2;
|
finalY = area.y + (area.height - finalHeight) / 2;
|
||||||
} else if (addMode === 'mouse') {
|
} else if (addMode === 'mouse') {
|
||||||
finalX = this.canvas.lastMousePosition.x - finalWidth / 2;
|
finalX = this.canvas.lastMousePosition.x - finalWidth / 2;
|
||||||
finalY = this.canvas.lastMousePosition.y - finalHeight / 2;
|
finalY = this.canvas.lastMousePosition.y - finalHeight / 2;
|
||||||
} else { // 'center' or 'default'
|
} else { // 'center' or 'default'
|
||||||
finalX = (this.canvas.width - finalWidth) / 2;
|
finalX = area.x + (area.width - finalWidth) / 2;
|
||||||
finalY = (this.canvas.height - finalHeight) / 2;
|
finalY = area.y + (area.height - finalHeight) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
const layer = {
|
const layer = {
|
||||||
|
id: generateUUID(),
|
||||||
image: image,
|
image: image,
|
||||||
imageId: imageId,
|
imageId: imageId,
|
||||||
x: finalX,
|
x: finalX,
|
||||||
@@ -291,13 +295,13 @@ export class CanvasLayers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
moveLayerUp() {
|
moveLayerUp() {
|
||||||
if (this.canvas.selectedLayers.length === 0) return;
|
if (this.canvas.canvasSelection.selectedLayers.length === 0) return;
|
||||||
this.moveLayers(this.canvas.selectedLayers, { direction: 'up' });
|
this.moveLayers(this.canvas.canvasSelection.selectedLayers, { direction: 'up' });
|
||||||
}
|
}
|
||||||
|
|
||||||
moveLayerDown() {
|
moveLayerDown() {
|
||||||
if (this.canvas.selectedLayers.length === 0) return;
|
if (this.canvas.canvasSelection.selectedLayers.length === 0) return;
|
||||||
this.moveLayers(this.canvas.selectedLayers, { direction: 'down' });
|
this.moveLayers(this.canvas.canvasSelection.selectedLayers, { direction: 'down' });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -305,9 +309,9 @@ export class CanvasLayers {
|
|||||||
* @param {number} scale - Skala zmiany rozmiaru
|
* @param {number} scale - Skala zmiany rozmiaru
|
||||||
*/
|
*/
|
||||||
resizeLayer(scale) {
|
resizeLayer(scale) {
|
||||||
if (this.canvas.selectedLayers.length === 0) return;
|
if (this.canvas.canvasSelection.selectedLayers.length === 0) return;
|
||||||
|
|
||||||
this.canvas.selectedLayers.forEach(layer => {
|
this.canvas.canvasSelection.selectedLayers.forEach(layer => {
|
||||||
layer.width *= scale;
|
layer.width *= scale;
|
||||||
layer.height *= scale;
|
layer.height *= scale;
|
||||||
});
|
});
|
||||||
@@ -320,9 +324,9 @@ export class CanvasLayers {
|
|||||||
* @param {number} angle - Kąt obrotu w stopniach
|
* @param {number} angle - Kąt obrotu w stopniach
|
||||||
*/
|
*/
|
||||||
rotateLayer(angle) {
|
rotateLayer(angle) {
|
||||||
if (this.canvas.selectedLayers.length === 0) return;
|
if (this.canvas.canvasSelection.selectedLayers.length === 0) return;
|
||||||
|
|
||||||
this.canvas.selectedLayers.forEach(layer => {
|
this.canvas.canvasSelection.selectedLayers.forEach(layer => {
|
||||||
layer.rotation += angle;
|
layer.rotation += angle;
|
||||||
});
|
});
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
@@ -358,9 +362,9 @@ export class CanvasLayers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async mirrorHorizontal() {
|
async mirrorHorizontal() {
|
||||||
if (this.canvas.selectedLayers.length === 0) return;
|
if (this.canvas.canvasSelection.selectedLayers.length === 0) return;
|
||||||
|
|
||||||
const promises = this.canvas.selectedLayers.map(layer => {
|
const promises = this.canvas.canvasSelection.selectedLayers.map(layer => {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const tempCanvas = document.createElement('canvas');
|
const tempCanvas = document.createElement('canvas');
|
||||||
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
|
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
|
||||||
@@ -386,9 +390,9 @@ export class CanvasLayers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async mirrorVertical() {
|
async mirrorVertical() {
|
||||||
if (this.canvas.selectedLayers.length === 0) return;
|
if (this.canvas.canvasSelection.selectedLayers.length === 0) return;
|
||||||
|
|
||||||
const promises = this.canvas.selectedLayers.map(layer => {
|
const promises = this.canvas.canvasSelection.selectedLayers.map(layer => {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const tempCanvas = document.createElement('canvas');
|
const tempCanvas = document.createElement('canvas');
|
||||||
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
|
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
|
||||||
@@ -500,11 +504,11 @@ export class CanvasLayers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getHandleAtPosition(worldX, worldY) {
|
getHandleAtPosition(worldX, worldY) {
|
||||||
if (this.canvas.selectedLayers.length === 0) return null;
|
if (this.canvas.canvasSelection.selectedLayers.length === 0) return null;
|
||||||
|
|
||||||
const handleRadius = 8 / this.canvas.viewport.zoom;
|
const handleRadius = 8 / this.canvas.viewport.zoom;
|
||||||
for (let i = this.canvas.selectedLayers.length - 1; i >= 0; i--) {
|
for (let i = this.canvas.canvasSelection.selectedLayers.length - 1; i >= 0; i--) {
|
||||||
const layer = this.canvas.selectedLayers[i];
|
const layer = this.canvas.canvasSelection.selectedLayers[i];
|
||||||
const handles = this.getHandles(layer);
|
const handles = this.getHandles(layer);
|
||||||
|
|
||||||
for (const key in handles) {
|
for (const key in handles) {
|
||||||
@@ -959,13 +963,13 @@ export class CanvasLayers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getFlattenedSelectionAsBlob() {
|
async getFlattenedSelectionAsBlob() {
|
||||||
if (this.canvas.selectedLayers.length === 0) {
|
if (this.canvas.canvasSelection.selectedLayers.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||||
this.canvas.selectedLayers.forEach(layer => {
|
this.canvas.canvasSelection.selectedLayers.forEach(layer => {
|
||||||
const centerX = layer.x + layer.width / 2;
|
const centerX = layer.x + layer.width / 2;
|
||||||
const centerY = layer.y + layer.height / 2;
|
const centerY = layer.y + layer.height / 2;
|
||||||
const rad = layer.rotation * Math.PI / 180;
|
const rad = layer.rotation * Math.PI / 180;
|
||||||
@@ -1007,7 +1011,7 @@ export class CanvasLayers {
|
|||||||
|
|
||||||
tempCtx.translate(-minX, -minY);
|
tempCtx.translate(-minX, -minY);
|
||||||
|
|
||||||
const sortedSelection = [...this.canvas.selectedLayers].sort((a, b) => a.zIndex - b.zIndex);
|
const sortedSelection = [...this.canvas.canvasSelection.selectedLayers].sort((a, b) => a.zIndex - b.zIndex);
|
||||||
|
|
||||||
sortedSelection.forEach(layer => {
|
sortedSelection.forEach(layer => {
|
||||||
if (!layer.image) return;
|
if (!layer.image) return;
|
||||||
@@ -1037,12 +1041,12 @@ export class CanvasLayers {
|
|||||||
* Fuses (flattens and merges) selected layers into a single layer
|
* Fuses (flattens and merges) selected layers into a single layer
|
||||||
*/
|
*/
|
||||||
async fuseLayers() {
|
async fuseLayers() {
|
||||||
if (this.canvas.selectedLayers.length < 2) {
|
if (this.canvas.canvasSelection.selectedLayers.length < 2) {
|
||||||
alert("Please select at least 2 layers to fuse.");
|
alert("Please select at least 2 layers to fuse.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(`Fusing ${this.canvas.selectedLayers.length} selected layers`);
|
log.info(`Fusing ${this.canvas.canvasSelection.selectedLayers.length} selected layers`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Save state for undo
|
// Save state for undo
|
||||||
@@ -1050,7 +1054,7 @@ export class CanvasLayers {
|
|||||||
|
|
||||||
// Calculate bounding box of all selected layers
|
// Calculate bounding box of all selected layers
|
||||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||||
this.canvas.selectedLayers.forEach(layer => {
|
this.canvas.canvasSelection.selectedLayers.forEach(layer => {
|
||||||
const centerX = layer.x + layer.width / 2;
|
const centerX = layer.x + layer.width / 2;
|
||||||
const centerY = layer.y + layer.height / 2;
|
const centerY = layer.y + layer.height / 2;
|
||||||
const rad = layer.rotation * Math.PI / 180;
|
const rad = layer.rotation * Math.PI / 180;
|
||||||
@@ -1097,7 +1101,7 @@ export class CanvasLayers {
|
|||||||
tempCtx.translate(-minX, -minY);
|
tempCtx.translate(-minX, -minY);
|
||||||
|
|
||||||
// Sort selected layers by z-index and render them
|
// Sort selected layers by z-index and render them
|
||||||
const sortedSelection = [...this.canvas.selectedLayers].sort((a, b) => a.zIndex - b.zIndex);
|
const sortedSelection = [...this.canvas.canvasSelection.selectedLayers].sort((a, b) => a.zIndex - b.zIndex);
|
||||||
|
|
||||||
sortedSelection.forEach(layer => {
|
sortedSelection.forEach(layer => {
|
||||||
if (!layer.image) return;
|
if (!layer.image) return;
|
||||||
@@ -1127,7 +1131,7 @@ export class CanvasLayers {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Find the lowest z-index among selected layers to maintain visual order
|
// Find the lowest z-index among selected layers to maintain visual order
|
||||||
const minZIndex = Math.min(...this.canvas.selectedLayers.map(layer => layer.zIndex));
|
const minZIndex = Math.min(...this.canvas.canvasSelection.selectedLayers.map(layer => layer.zIndex));
|
||||||
|
|
||||||
// Generate unique ID for the new fused layer
|
// Generate unique ID for the new fused layer
|
||||||
const imageId = generateUUID();
|
const imageId = generateUUID();
|
||||||
@@ -1151,7 +1155,7 @@ export class CanvasLayers {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Remove selected layers from canvas
|
// Remove selected layers from canvas
|
||||||
this.canvas.layers = this.canvas.layers.filter(layer => !this.canvas.selectedLayers.includes(layer));
|
this.canvas.layers = this.canvas.layers.filter(layer => !this.canvas.canvasSelection.selectedLayers.includes(layer));
|
||||||
|
|
||||||
// Insert the fused layer at the correct position
|
// Insert the fused layer at the correct position
|
||||||
this.canvas.layers.push(fusedLayer);
|
this.canvas.layers.push(fusedLayer);
|
||||||
|
|||||||
@@ -306,7 +306,7 @@ export class CanvasLayersPanel {
|
|||||||
layerRow.dataset.layerIndex = index;
|
layerRow.dataset.layerIndex = index;
|
||||||
|
|
||||||
// Sprawdź czy warstwa jest zaznaczona
|
// Sprawdź czy warstwa jest zaznaczona
|
||||||
const isSelected = this.canvas.selectedLayers.includes(layer);
|
const isSelected = this.canvas.canvasSelection.selectedLayers.includes(layer);
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
layerRow.classList.add('selected');
|
layerRow.classList.add('selected');
|
||||||
}
|
}
|
||||||
@@ -407,7 +407,7 @@ export class CanvasLayersPanel {
|
|||||||
// Aktualizuj tylko wygląd (klasy CSS), bez niszczenia DOM
|
// Aktualizuj tylko wygląd (klasy CSS), bez niszczenia DOM
|
||||||
this.updateSelectionAppearance();
|
this.updateSelectionAppearance();
|
||||||
|
|
||||||
log.debug(`Layer clicked: ${layer.name}, selection count: ${this.canvas.selectedLayers.length}`);
|
log.debug(`Layer clicked: ${layer.name}, selection count: ${this.canvas.canvasSelection.selectedLayers.length}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -492,12 +492,12 @@ export class CanvasLayersPanel {
|
|||||||
* Usuwa zaznaczone warstwy
|
* Usuwa zaznaczone warstwy
|
||||||
*/
|
*/
|
||||||
deleteSelectedLayers() {
|
deleteSelectedLayers() {
|
||||||
if (this.canvas.selectedLayers.length === 0) {
|
if (this.canvas.canvasSelection.selectedLayers.length === 0) {
|
||||||
log.debug('No layers selected for deletion');
|
log.debug('No layers selected for deletion');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(`Deleting ${this.canvas.selectedLayers.length} selected layers`);
|
log.info(`Deleting ${this.canvas.canvasSelection.selectedLayers.length} selected layers`);
|
||||||
this.canvas.removeSelectedLayers();
|
this.canvas.removeSelectedLayers();
|
||||||
this.renderLayers();
|
this.renderLayers();
|
||||||
}
|
}
|
||||||
@@ -514,12 +514,12 @@ export class CanvasLayersPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Jeśli przeciągana warstwa nie jest zaznaczona, zaznacz ją
|
// Jeśli przeciągana warstwa nie jest zaznaczona, zaznacz ją
|
||||||
if (!this.canvas.selectedLayers.includes(layer)) {
|
if (!this.canvas.canvasSelection.selectedLayers.includes(layer)) {
|
||||||
this.canvas.updateSelection([layer]);
|
this.canvas.updateSelection([layer]);
|
||||||
this.renderLayers();
|
this.renderLayers();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.draggedElements = [...this.canvas.selectedLayers];
|
this.draggedElements = [...this.canvas.canvasSelection.selectedLayers];
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
e.dataTransfer.setData('text/plain', ''); // Wymagane przez standard
|
e.dataTransfer.setData('text/plain', ''); // Wymagane przez standard
|
||||||
|
|
||||||
@@ -635,7 +635,7 @@ export class CanvasLayersPanel {
|
|||||||
|
|
||||||
layerRows.forEach((row, index) => {
|
layerRows.forEach((row, index) => {
|
||||||
const layer = sortedLayers[index];
|
const layer = sortedLayers[index];
|
||||||
if (this.canvas.selectedLayers.includes(layer)) {
|
if (this.canvas.canvasSelection.selectedLayers.includes(layer)) {
|
||||||
row.classList.add('selected');
|
row.classList.add('selected');
|
||||||
} else {
|
} else {
|
||||||
row.classList.remove('selected');
|
row.classList.remove('selected');
|
||||||
|
|||||||
542
js/CanvasMask.js
Normal file
542
js/CanvasMask.js
Normal file
@@ -0,0 +1,542 @@
|
|||||||
|
import { app, ComfyApp } from "../../scripts/app.js";
|
||||||
|
import { api } from "../../scripts/api.js";
|
||||||
|
import { createModuleLogger } from "./utils/LoggerUtils.js";
|
||||||
|
import { mask_editor_showing, mask_editor_listen_for_cancel } from "./utils/mask_utils.js";
|
||||||
|
|
||||||
|
const log = createModuleLogger('CanvasMask');
|
||||||
|
|
||||||
|
export class CanvasMask {
|
||||||
|
constructor(canvas) {
|
||||||
|
this.canvas = canvas;
|
||||||
|
this.node = canvas.node;
|
||||||
|
this.maskTool = canvas.maskTool;
|
||||||
|
|
||||||
|
this.savedMaskState = null;
|
||||||
|
this.maskEditorCancelled = false;
|
||||||
|
this.pendingMask = null;
|
||||||
|
this.editorWasShowing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uruchamia edytor masek
|
||||||
|
* @param {Image|HTMLCanvasElement|null} predefinedMask - Opcjonalna maska do nałożenia po otwarciu editora
|
||||||
|
* @param {boolean} sendCleanImage - Czy wysłać czysty obraz (bez maski) do editora
|
||||||
|
*/
|
||||||
|
async startMaskEditor(predefinedMask = null, sendCleanImage = true) {
|
||||||
|
log.info('Starting mask editor', {
|
||||||
|
hasPredefinedMask: !!predefinedMask,
|
||||||
|
sendCleanImage,
|
||||||
|
layersCount: this.canvas.layers.length
|
||||||
|
});
|
||||||
|
|
||||||
|
this.savedMaskState = await this.saveMaskState();
|
||||||
|
this.maskEditorCancelled = false;
|
||||||
|
|
||||||
|
if (!predefinedMask && this.maskTool && this.maskTool.maskCanvas) {
|
||||||
|
try {
|
||||||
|
log.debug('Creating mask from current mask tool');
|
||||||
|
predefinedMask = await this.createMaskFromCurrentMask();
|
||||||
|
log.debug('Mask created from current mask tool successfully');
|
||||||
|
} catch (error) {
|
||||||
|
log.warn("Could not create mask from current mask:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingMask = predefinedMask;
|
||||||
|
|
||||||
|
let blob;
|
||||||
|
if (sendCleanImage) {
|
||||||
|
log.debug('Getting flattened canvas as blob (clean image)');
|
||||||
|
blob = await this.canvas.canvasLayers.getFlattenedCanvasAsBlob();
|
||||||
|
} else {
|
||||||
|
log.debug('Getting flattened canvas for mask editor (with mask)');
|
||||||
|
blob = await this.canvas.canvasLayers.getFlattenedCanvasForMaskEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!blob) {
|
||||||
|
log.warn("Canvas is empty, cannot open mask editor.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug('Canvas blob created successfully, size:', blob.size);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
const filename = `layerforge-mask-edit-${+new Date()}.png`;
|
||||||
|
formData.append("image", blob, filename);
|
||||||
|
formData.append("overwrite", "true");
|
||||||
|
formData.append("type", "temp");
|
||||||
|
|
||||||
|
log.debug('Uploading image to server:', filename);
|
||||||
|
|
||||||
|
const response = await api.fetchApi("/upload/image", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to upload image: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
log.debug('Image uploaded successfully:', data);
|
||||||
|
|
||||||
|
const img = new Image();
|
||||||
|
img.src = api.apiURL(`/view?filename=${encodeURIComponent(data.name)}&type=${data.type}&subfolder=${data.subfolder}`);
|
||||||
|
await new Promise((res, rej) => {
|
||||||
|
img.onload = res;
|
||||||
|
img.onerror = rej;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.node.imgs = [img];
|
||||||
|
|
||||||
|
log.info('Opening ComfyUI mask editor');
|
||||||
|
ComfyApp.copyToClipspace(this.node);
|
||||||
|
ComfyApp.clipspace_return_node = this.node;
|
||||||
|
ComfyApp.open_maskeditor();
|
||||||
|
|
||||||
|
this.editorWasShowing = false;
|
||||||
|
this.waitWhileMaskEditing();
|
||||||
|
|
||||||
|
this.setupCancelListener();
|
||||||
|
|
||||||
|
if (predefinedMask) {
|
||||||
|
log.debug('Will apply predefined mask when editor is ready');
|
||||||
|
this.waitForMaskEditorAndApplyMask();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Error preparing image for mask editor:", error);
|
||||||
|
alert(`Error: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Czeka na otwarcie mask editora i automatycznie nakłada predefiniowaną maskę
|
||||||
|
*/
|
||||||
|
waitForMaskEditorAndApplyMask() {
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 100; // Zwiększone do 10 sekund oczekiwania
|
||||||
|
|
||||||
|
const checkEditor = () => {
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
if (mask_editor_showing(app)) {
|
||||||
|
|
||||||
|
const useNewEditor = app.ui.settings.getSettingValue('Comfy.MaskEditor.UseNewEditor');
|
||||||
|
let editorReady = false;
|
||||||
|
|
||||||
|
if (useNewEditor) {
|
||||||
|
|
||||||
|
const MaskEditorDialog = window.MaskEditorDialog;
|
||||||
|
if (MaskEditorDialog && MaskEditorDialog.instance) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
const messageBroker = MaskEditorDialog.instance.getMessageBroker();
|
||||||
|
if (messageBroker) {
|
||||||
|
editorReady = true;
|
||||||
|
log.info("New mask editor detected as ready via MessageBroker");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
editorReady = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!editorReady) {
|
||||||
|
const maskEditorElement = document.getElementById('maskEditor');
|
||||||
|
if (maskEditorElement && maskEditorElement.style.display !== 'none') {
|
||||||
|
|
||||||
|
const canvas = maskEditorElement.querySelector('canvas');
|
||||||
|
if (canvas) {
|
||||||
|
editorReady = true;
|
||||||
|
log.info("New mask editor detected as ready via DOM element");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
const maskCanvas = document.getElementById('maskCanvas');
|
||||||
|
editorReady = maskCanvas && maskCanvas.getContext && maskCanvas.width > 0;
|
||||||
|
if (editorReady) {
|
||||||
|
log.info("Old mask editor detected as ready");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editorReady) {
|
||||||
|
|
||||||
|
log.info("Applying mask to editor after", attempts * 100, "ms wait");
|
||||||
|
setTimeout(() => {
|
||||||
|
this.applyMaskToEditor(this.pendingMask);
|
||||||
|
this.pendingMask = null;
|
||||||
|
}, 300);
|
||||||
|
} else if (attempts < maxAttempts) {
|
||||||
|
|
||||||
|
if (attempts % 10 === 0) {
|
||||||
|
log.info("Waiting for mask editor to be ready... attempt", attempts, "/", maxAttempts);
|
||||||
|
}
|
||||||
|
setTimeout(checkEditor, 100);
|
||||||
|
} else {
|
||||||
|
log.warn("Mask editor timeout - editor not ready after", maxAttempts * 100, "ms");
|
||||||
|
|
||||||
|
log.info("Attempting to apply mask anyway...");
|
||||||
|
setTimeout(() => {
|
||||||
|
this.applyMaskToEditor(this.pendingMask);
|
||||||
|
this.pendingMask = null;
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
} else if (attempts < maxAttempts) {
|
||||||
|
|
||||||
|
setTimeout(checkEditor, 100);
|
||||||
|
} else {
|
||||||
|
log.warn("Mask editor timeout - editor not showing after", maxAttempts * 100, "ms");
|
||||||
|
this.pendingMask = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nakłada maskę na otwarty mask editor
|
||||||
|
* @param {Image|HTMLCanvasElement} maskData - Dane maski do nałożenia
|
||||||
|
*/
|
||||||
|
async applyMaskToEditor(maskData) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const useNewEditor = app.ui.settings.getSettingValue('Comfy.MaskEditor.UseNewEditor');
|
||||||
|
|
||||||
|
if (useNewEditor) {
|
||||||
|
|
||||||
|
const MaskEditorDialog = window.MaskEditorDialog;
|
||||||
|
if (MaskEditorDialog && MaskEditorDialog.instance) {
|
||||||
|
|
||||||
|
await this.applyMaskToNewEditor(maskData);
|
||||||
|
} else {
|
||||||
|
log.warn("New editor setting enabled but instance not found, trying old editor");
|
||||||
|
await this.applyMaskToOldEditor(maskData);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
await this.applyMaskToOldEditor(maskData);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Predefined mask applied to mask editor successfully");
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Failed to apply predefined mask to editor:", error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.info("Trying alternative mask application method...");
|
||||||
|
await this.applyMaskToOldEditor(maskData);
|
||||||
|
log.info("Alternative method succeeded");
|
||||||
|
} catch (fallbackError) {
|
||||||
|
log.error("Alternative method also failed:", fallbackError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nakłada maskę na nowy mask editor (przez MessageBroker)
|
||||||
|
* @param {Image|HTMLCanvasElement} maskData - Dane maski
|
||||||
|
*/
|
||||||
|
async applyMaskToNewEditor(maskData) {
|
||||||
|
|
||||||
|
const MaskEditorDialog = window.MaskEditorDialog;
|
||||||
|
if (!MaskEditorDialog || !MaskEditorDialog.instance) {
|
||||||
|
throw new Error("New mask editor instance not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor = MaskEditorDialog.instance;
|
||||||
|
const messageBroker = editor.getMessageBroker();
|
||||||
|
|
||||||
|
const maskCanvas = await messageBroker.pull('maskCanvas');
|
||||||
|
const maskCtx = await messageBroker.pull('maskCtx');
|
||||||
|
const maskColor = await messageBroker.pull('getMaskColor');
|
||||||
|
|
||||||
|
const processedMask = await this.processMaskForEditor(maskData, maskCanvas.width, maskCanvas.height, maskColor);
|
||||||
|
|
||||||
|
maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
|
||||||
|
maskCtx.drawImage(processedMask, 0, 0);
|
||||||
|
|
||||||
|
messageBroker.publish('saveState');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nakłada maskę na stary mask editor
|
||||||
|
* @param {Image|HTMLCanvasElement} maskData - Dane maski
|
||||||
|
*/
|
||||||
|
async applyMaskToOldEditor(maskData) {
|
||||||
|
|
||||||
|
const maskCanvas = document.getElementById('maskCanvas');
|
||||||
|
if (!maskCanvas) {
|
||||||
|
throw new Error("Old mask editor canvas not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const maskCtx = maskCanvas.getContext('2d', {willReadFrequently: true});
|
||||||
|
|
||||||
|
const maskColor = {r: 255, g: 255, b: 255};
|
||||||
|
const processedMask = await this.processMaskForEditor(maskData, maskCanvas.width, maskCanvas.height, maskColor);
|
||||||
|
|
||||||
|
maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
|
||||||
|
maskCtx.drawImage(processedMask, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Przetwarza maskę do odpowiedniego formatu dla editora
|
||||||
|
* @param {Image|HTMLCanvasElement} maskData - Oryginalne dane maski
|
||||||
|
* @param {number} targetWidth - Docelowa szerokość
|
||||||
|
* @param {number} targetHeight - Docelowa wysokość
|
||||||
|
* @param {Object} maskColor - Kolor maski {r, g, b}
|
||||||
|
* @returns {HTMLCanvasElement} Przetworzona maska
|
||||||
|
*/async processMaskForEditor(maskData, targetWidth, targetHeight, maskColor) {
|
||||||
|
// Współrzędne przesunięcia (pan) widoku edytora
|
||||||
|
const panX = this.maskTool.x;
|
||||||
|
const panY = this.maskTool.y;
|
||||||
|
|
||||||
|
log.info("Processing mask for editor:", {
|
||||||
|
sourceSize: {width: maskData.width, height: maskData.height},
|
||||||
|
targetSize: {width: targetWidth, height: targetHeight},
|
||||||
|
viewportPan: {x: panX, y: panY}
|
||||||
|
});
|
||||||
|
|
||||||
|
const tempCanvas = document.createElement('canvas');
|
||||||
|
tempCanvas.width = targetWidth;
|
||||||
|
tempCanvas.height = targetHeight;
|
||||||
|
const tempCtx = tempCanvas.getContext('2d', {willReadFrequently: true});
|
||||||
|
|
||||||
|
const sourceX = -panX;
|
||||||
|
const sourceY = -panY;
|
||||||
|
|
||||||
|
tempCtx.drawImage(
|
||||||
|
maskData, // Źródło: pełna maska z "output area"
|
||||||
|
sourceX, // sx: Prawdziwa współrzędna X na dużej masce (np. 1000)
|
||||||
|
sourceY, // sy: Prawdziwa współrzędna Y na dużej masce (np. 1000)
|
||||||
|
targetWidth, // sWidth: Szerokość wycinanego fragmentu
|
||||||
|
targetHeight, // sHeight: Wysokość wycinanego fragmentu
|
||||||
|
0, // dx: Gdzie wkleić w płótnie docelowym (zawsze 0)
|
||||||
|
0, // dy: Gdzie wkleić w płótnie docelowym (zawsze 0)
|
||||||
|
targetWidth, // dWidth: Szerokość wklejanego obrazu
|
||||||
|
targetHeight // dHeight: Wysokość wklejanego obrazu
|
||||||
|
);
|
||||||
|
|
||||||
|
log.info("Mask viewport cropped correctly.", {
|
||||||
|
source: "maskData",
|
||||||
|
cropArea: {x: sourceX, y: sourceY, width: targetWidth, height: targetHeight}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reszta kodu (zmiana koloru) pozostaje bez zmian
|
||||||
|
const imageData = tempCtx.getImageData(0, 0, targetWidth, targetHeight);
|
||||||
|
const data = imageData.data;
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
|
const alpha = data[i + 3];
|
||||||
|
if (alpha > 0) {
|
||||||
|
data[i] = maskColor.r;
|
||||||
|
data[i + 1] = maskColor.g;
|
||||||
|
data[i + 2] = maskColor.b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tempCtx.putImageData(imageData, 0, 0);
|
||||||
|
|
||||||
|
log.info("Mask processing completed - color applied.");
|
||||||
|
return tempCanvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tworzy obiekt Image z obecnej maski canvas
|
||||||
|
* @returns {Promise<Image>} Promise zwracający obiekt Image z maską
|
||||||
|
*/
|
||||||
|
async createMaskFromCurrentMask() {
|
||||||
|
if (!this.maskTool || !this.maskTool.maskCanvas) {
|
||||||
|
throw new Error("No mask canvas available");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const maskImage = new Image();
|
||||||
|
maskImage.onload = () => resolve(maskImage);
|
||||||
|
maskImage.onerror = reject;
|
||||||
|
maskImage.src = this.maskTool.maskCanvas.toDataURL();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
waitWhileMaskEditing() {
|
||||||
|
if (mask_editor_showing(app)) {
|
||||||
|
this.editorWasShowing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mask_editor_showing(app) && this.editorWasShowing) {
|
||||||
|
this.editorWasShowing = false;
|
||||||
|
setTimeout(() => this.handleMaskEditorClose(), 100);
|
||||||
|
} else {
|
||||||
|
setTimeout(this.waitWhileMaskEditing.bind(this), 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zapisuje obecny stan maski przed otwarciem editora
|
||||||
|
* @returns {Object} Zapisany stan maski
|
||||||
|
*/
|
||||||
|
async saveMaskState() {
|
||||||
|
if (!this.maskTool || !this.maskTool.maskCanvas) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maskCanvas = this.maskTool.maskCanvas;
|
||||||
|
const savedCanvas = document.createElement('canvas');
|
||||||
|
savedCanvas.width = maskCanvas.width;
|
||||||
|
savedCanvas.height = maskCanvas.height;
|
||||||
|
const savedCtx = savedCanvas.getContext('2d', {willReadFrequently: true});
|
||||||
|
savedCtx.drawImage(maskCanvas, 0, 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
maskData: savedCanvas,
|
||||||
|
maskPosition: {
|
||||||
|
x: this.maskTool.x,
|
||||||
|
y: this.maskTool.y
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Przywraca zapisany stan maski
|
||||||
|
* @param {Object} savedState - Zapisany stan maski
|
||||||
|
*/
|
||||||
|
async restoreMaskState(savedState) {
|
||||||
|
if (!savedState || !this.maskTool) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedState.maskData) {
|
||||||
|
const maskCtx = this.maskTool.maskCtx;
|
||||||
|
maskCtx.clearRect(0, 0, this.maskTool.maskCanvas.width, this.maskTool.maskCanvas.height);
|
||||||
|
maskCtx.drawImage(savedState.maskData, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedState.maskPosition) {
|
||||||
|
this.maskTool.x = savedState.maskPosition.x;
|
||||||
|
this.maskTool.y = savedState.maskPosition.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.canvas.render();
|
||||||
|
log.info("Mask state restored after cancel");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konfiguruje nasłuchiwanie na przycisk Cancel w mask editorze
|
||||||
|
*/
|
||||||
|
setupCancelListener() {
|
||||||
|
mask_editor_listen_for_cancel(app, () => {
|
||||||
|
log.info("Mask editor cancel button clicked");
|
||||||
|
this.maskEditorCancelled = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sprawdza czy mask editor został anulowany i obsługuje to odpowiednio
|
||||||
|
*/
|
||||||
|
async handleMaskEditorClose() {
|
||||||
|
log.info("Handling mask editor close");
|
||||||
|
log.debug("Node object after mask editor close:", this.node);
|
||||||
|
|
||||||
|
if (this.maskEditorCancelled) {
|
||||||
|
log.info("Mask editor was cancelled - restoring original mask state");
|
||||||
|
|
||||||
|
if (this.savedMaskState) {
|
||||||
|
await this.restoreMaskState(this.savedMaskState);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.maskEditorCancelled = false;
|
||||||
|
this.savedMaskState = null;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.node.imgs || !this.node.imgs.length === 0 || !this.node.imgs[0].src) {
|
||||||
|
log.warn("Mask editor was closed without a result.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("Processing mask editor result, image source:", this.node.imgs[0].src.substring(0, 100) + '...');
|
||||||
|
|
||||||
|
const resultImage = new Image();
|
||||||
|
resultImage.src = this.node.imgs[0].src;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
resultImage.onload = resolve;
|
||||||
|
resultImage.onerror = reject;
|
||||||
|
});
|
||||||
|
|
||||||
|
log.debug("Result image loaded successfully", {
|
||||||
|
width: resultImage.width,
|
||||||
|
height: resultImage.height
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Failed to load image from mask editor.", error);
|
||||||
|
this.node.imgs = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("Creating temporary canvas for mask processing");
|
||||||
|
const tempCanvas = document.createElement('canvas');
|
||||||
|
tempCanvas.width = this.canvas.width;
|
||||||
|
tempCanvas.height = this.canvas.height;
|
||||||
|
const tempCtx = tempCanvas.getContext('2d', {willReadFrequently: true});
|
||||||
|
|
||||||
|
tempCtx.drawImage(resultImage, 0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
|
log.debug("Processing image data to create mask");
|
||||||
|
const imageData = tempCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
const data = imageData.data;
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
|
const originalAlpha = data[i + 3];
|
||||||
|
data[i] = 255;
|
||||||
|
data[i + 1] = 255;
|
||||||
|
data[i + 2] = 255;
|
||||||
|
data[i + 3] = 255 - originalAlpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
tempCtx.putImageData(imageData, 0, 0);
|
||||||
|
|
||||||
|
log.debug("Converting processed mask to image");
|
||||||
|
const maskAsImage = new Image();
|
||||||
|
maskAsImage.src = tempCanvas.toDataURL();
|
||||||
|
await new Promise(resolve => maskAsImage.onload = resolve);
|
||||||
|
|
||||||
|
const maskCtx = this.maskTool.maskCtx;
|
||||||
|
const destX = -this.maskTool.x;
|
||||||
|
const destY = -this.maskTool.y;
|
||||||
|
|
||||||
|
log.debug("Applying mask to canvas", {destX, destY});
|
||||||
|
|
||||||
|
maskCtx.globalCompositeOperation = 'source-over';
|
||||||
|
maskCtx.clearRect(destX, destY, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
|
maskCtx.drawImage(maskAsImage, destX, destY);
|
||||||
|
|
||||||
|
this.canvas.render();
|
||||||
|
this.canvas.saveState();
|
||||||
|
|
||||||
|
log.debug("Creating new preview image");
|
||||||
|
const new_preview = new Image();
|
||||||
|
|
||||||
|
const blob = await this.canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob();
|
||||||
|
if (blob) {
|
||||||
|
new_preview.src = URL.createObjectURL(blob);
|
||||||
|
await new Promise(r => new_preview.onload = r);
|
||||||
|
this.node.imgs = [new_preview];
|
||||||
|
log.debug("New preview image created successfully");
|
||||||
|
} else {
|
||||||
|
this.node.imgs = [];
|
||||||
|
log.warn("Failed to create preview blob");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.canvas.render();
|
||||||
|
|
||||||
|
this.savedMaskState = null;
|
||||||
|
log.info("Mask editor result processed successfully");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -75,15 +75,16 @@ export class CanvasRenderer {
|
|||||||
);
|
);
|
||||||
if (layer.mask) {
|
if (layer.mask) {
|
||||||
}
|
}
|
||||||
if (this.canvas.selectedLayers.includes(layer)) {
|
if (this.canvas.canvasSelection.selectedLayers.includes(layer)) {
|
||||||
this.drawSelectionFrame(ctx, layer);
|
this.drawSelectionFrame(ctx, layer);
|
||||||
}
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.drawCanvasOutline(ctx);
|
this.drawCanvasOutline(ctx);
|
||||||
|
this.drawPendingGenerationAreas(ctx); // Draw snapshot outlines
|
||||||
const maskImage = this.canvas.maskTool.getMask();
|
const maskImage = this.canvas.maskTool.getMask();
|
||||||
if (maskImage) {
|
if (maskImage && this.canvas.maskTool.isOverlayVisible) {
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|
||||||
@@ -112,6 +113,13 @@ export class CanvasRenderer {
|
|||||||
this.canvas.canvas.height = this.canvas.offscreenCanvas.height;
|
this.canvas.canvas.height = this.canvas.offscreenCanvas.height;
|
||||||
}
|
}
|
||||||
this.canvas.ctx.drawImage(this.canvas.offscreenCanvas, 0, 0);
|
this.canvas.ctx.drawImage(this.canvas.offscreenCanvas, 0, 0);
|
||||||
|
|
||||||
|
// Update Batch Preview UI positions
|
||||||
|
if (this.canvas.batchPreviewManagers && this.canvas.batchPreviewManagers.length > 0) {
|
||||||
|
this.canvas.batchPreviewManagers.forEach(manager => {
|
||||||
|
manager.updateScreenPosition(this.canvas.viewport);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderInteractionElements(ctx) {
|
renderInteractionElements(ctx) {
|
||||||
@@ -182,8 +190,8 @@ export class CanvasRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderLayerInfo(ctx) {
|
renderLayerInfo(ctx) {
|
||||||
if (this.canvas.selectedLayer) {
|
if (this.canvas.canvasSelection.selectedLayer) {
|
||||||
this.canvas.selectedLayers.forEach(layer => {
|
this.canvas.canvasSelection.selectedLayers.forEach(layer => {
|
||||||
if (!layer.image) return;
|
if (!layer.image) return;
|
||||||
|
|
||||||
const layerIndex = this.canvas.layers.indexOf(layer);
|
const layerIndex = this.canvas.layers.indexOf(layer);
|
||||||
@@ -321,4 +329,36 @@ export class CanvasRenderer {
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drawPendingGenerationAreas(ctx) {
|
||||||
|
const areasToDraw = [];
|
||||||
|
|
||||||
|
// 1. Get areas from active managers
|
||||||
|
if (this.canvas.batchPreviewManagers && this.canvas.batchPreviewManagers.length > 0) {
|
||||||
|
this.canvas.batchPreviewManagers.forEach(manager => {
|
||||||
|
if (manager.generationArea) {
|
||||||
|
areasToDraw.push(manager.generationArea);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Get the area from the pending context (if it exists)
|
||||||
|
if (this.canvas.pendingBatchContext && this.canvas.pendingBatchContext.outputArea) {
|
||||||
|
areasToDraw.push(this.canvas.pendingBatchContext.outputArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (areasToDraw.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Draw all collected areas
|
||||||
|
areasToDraw.forEach(area => {
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = 'rgba(0, 150, 255, 0.9)'; // Blue color
|
||||||
|
ctx.lineWidth = 3 / this.canvas.viewport.zoom;
|
||||||
|
ctx.setLineDash([12 / this.canvas.viewport.zoom, 6 / this.canvas.viewport.zoom]);
|
||||||
|
ctx.strokeRect(area.x, area.y, area.width, area.height);
|
||||||
|
ctx.restore();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
166
js/CanvasSelection.js
Normal file
166
js/CanvasSelection.js
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import { createModuleLogger } from "./utils/LoggerUtils.js";
|
||||||
|
|
||||||
|
const log = createModuleLogger('CanvasSelection');
|
||||||
|
|
||||||
|
export class CanvasSelection {
|
||||||
|
constructor(canvas) {
|
||||||
|
this.canvas = canvas;
|
||||||
|
this.selectedLayers = [];
|
||||||
|
this.selectedLayer = null;
|
||||||
|
this.onSelectionChange = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplikuje zaznaczone warstwy (w pamięci, bez zapisu stanu)
|
||||||
|
*/
|
||||||
|
duplicateSelectedLayers() {
|
||||||
|
if (this.selectedLayers.length === 0) return [];
|
||||||
|
|
||||||
|
const newLayers = [];
|
||||||
|
const sortedLayers = [...this.selectedLayers].sort((a,b) => a.zIndex - b.zIndex);
|
||||||
|
|
||||||
|
sortedLayers.forEach(layer => {
|
||||||
|
const newLayer = {
|
||||||
|
...layer,
|
||||||
|
id: `layer_${+new Date()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||||
|
zIndex: this.canvas.layers.length, // Nowa warstwa zawsze na wierzchu
|
||||||
|
};
|
||||||
|
this.canvas.layers.push(newLayer);
|
||||||
|
newLayers.push(newLayer);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Aktualizuj zaznaczenie, co powiadomi panel (ale nie renderuje go całego)
|
||||||
|
this.updateSelection(newLayers);
|
||||||
|
|
||||||
|
// Powiadom panel o zmianie struktury, aby się przerysował
|
||||||
|
if (this.canvas.canvasLayersPanel) {
|
||||||
|
this.canvas.canvasLayersPanel.onLayersChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`Duplicated ${newLayers.length} layers (in-memory).`);
|
||||||
|
return newLayers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktualizuje zaznaczenie warstw i powiadamia wszystkie komponenty.
|
||||||
|
* To jest "jedyne źródło prawdy" o zmianie zaznaczenia.
|
||||||
|
* @param {Array} newSelection - Nowa lista zaznaczonych warstw
|
||||||
|
*/
|
||||||
|
updateSelection(newSelection) {
|
||||||
|
const previousSelection = this.selectedLayers.length;
|
||||||
|
this.selectedLayers = newSelection || [];
|
||||||
|
this.selectedLayer = this.selectedLayers.length > 0 ? this.selectedLayers[this.selectedLayers.length - 1] : null;
|
||||||
|
|
||||||
|
// Sprawdź, czy zaznaczenie faktycznie się zmieniło, aby uniknąć pętli
|
||||||
|
const hasChanged = previousSelection !== this.selectedLayers.length ||
|
||||||
|
this.selectedLayers.some((layer, i) => this.selectedLayers[i] !== (newSelection || [])[i]);
|
||||||
|
|
||||||
|
if (!hasChanged && previousSelection > 0) {
|
||||||
|
// return; // Zablokowane na razie, może powodować problemy
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug('Selection updated', {
|
||||||
|
previousCount: previousSelection,
|
||||||
|
newCount: this.selectedLayers.length,
|
||||||
|
selectedLayerIds: this.selectedLayers.map(l => l.id || 'unknown')
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. Zrenderuj ponownie canvas, aby pokazać nowe kontrolki transformacji
|
||||||
|
this.canvas.render();
|
||||||
|
|
||||||
|
// 2. Powiadom inne części aplikacji (jeśli są)
|
||||||
|
if (this.onSelectionChange) {
|
||||||
|
this.onSelectionChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Powiadom panel warstw, aby zaktualizował swój wygląd
|
||||||
|
if (this.canvas.canvasLayersPanel) {
|
||||||
|
this.canvas.canvasLayersPanel.onSelectionChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logika aktualizacji zaznaczenia, wywoływana przez panel warstw.
|
||||||
|
*/
|
||||||
|
updateSelectionLogic(layer, isCtrlPressed, isShiftPressed, index) {
|
||||||
|
let newSelection = [...this.selectedLayers];
|
||||||
|
let selectionChanged = false;
|
||||||
|
|
||||||
|
if (isShiftPressed && this.canvas.canvasLayersPanel.lastSelectedIndex !== -1) {
|
||||||
|
const sortedLayers = [...this.canvas.layers].sort((a, b) => b.zIndex - a.zIndex);
|
||||||
|
const startIndex = Math.min(this.canvas.canvasLayersPanel.lastSelectedIndex, index);
|
||||||
|
const endIndex = Math.max(this.canvas.canvasLayersPanel.lastSelectedIndex, index);
|
||||||
|
|
||||||
|
newSelection = [];
|
||||||
|
for (let i = startIndex; i <= endIndex; i++) {
|
||||||
|
if (sortedLayers[i]) {
|
||||||
|
newSelection.push(sortedLayers[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectionChanged = true;
|
||||||
|
} else if (isCtrlPressed) {
|
||||||
|
const layerIndex = newSelection.indexOf(layer);
|
||||||
|
if (layerIndex === -1) {
|
||||||
|
newSelection.push(layer);
|
||||||
|
} else {
|
||||||
|
newSelection.splice(layerIndex, 1);
|
||||||
|
}
|
||||||
|
this.canvas.canvasLayersPanel.lastSelectedIndex = index;
|
||||||
|
selectionChanged = true;
|
||||||
|
} else {
|
||||||
|
// Jeśli kliknięta warstwa nie jest częścią obecnego zaznaczenia,
|
||||||
|
// wyczyść zaznaczenie i zaznacz tylko ją.
|
||||||
|
if (!this.selectedLayers.includes(layer)) {
|
||||||
|
newSelection = [layer];
|
||||||
|
selectionChanged = true;
|
||||||
|
}
|
||||||
|
// Jeśli kliknięta warstwa JEST już zaznaczona (potencjalnie z innymi),
|
||||||
|
// NIE rób nic, aby umożliwić przeciąganie całej grupy.
|
||||||
|
this.canvas.canvasLayersPanel.lastSelectedIndex = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktualizuj zaznaczenie tylko jeśli faktycznie się zmieniło
|
||||||
|
if (selectionChanged) {
|
||||||
|
this.updateSelection(newSelection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeSelectedLayers() {
|
||||||
|
if (this.selectedLayers.length > 0) {
|
||||||
|
log.info('Removing selected layers', {
|
||||||
|
layersToRemove: this.selectedLayers.length,
|
||||||
|
totalLayers: this.canvas.layers.length
|
||||||
|
});
|
||||||
|
|
||||||
|
this.canvas.saveState();
|
||||||
|
this.canvas.layers = this.canvas.layers.filter(l => !this.selectedLayers.includes(l));
|
||||||
|
|
||||||
|
this.updateSelection([]);
|
||||||
|
|
||||||
|
this.canvas.render();
|
||||||
|
this.canvas.saveState();
|
||||||
|
|
||||||
|
if (this.canvas.canvasLayersPanel) {
|
||||||
|
this.canvas.canvasLayersPanel.onLayersChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug('Layers removed successfully, remaining layers:', this.canvas.layers.length);
|
||||||
|
} else {
|
||||||
|
log.debug('No layers selected for removal');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktualizuje zaznaczenie po operacji historii
|
||||||
|
*/
|
||||||
|
updateSelectionAfterHistory() {
|
||||||
|
const newSelectedLayers = [];
|
||||||
|
if (this.selectedLayers) {
|
||||||
|
this.selectedLayers.forEach(sl => {
|
||||||
|
const found = this.canvas.layers.find(l => l.id === sl.id);
|
||||||
|
if (found) newSelectedLayers.push(found);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.updateSelection(newSelectedLayers);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -227,6 +227,7 @@ export class CanvasState {
|
|||||||
nodeId: this.canvas.node.id,
|
nodeId: this.canvas.node.id,
|
||||||
state: state
|
state: state
|
||||||
});
|
});
|
||||||
|
this.canvas.render();
|
||||||
} else {
|
} else {
|
||||||
log.warn("State saver worker not available. Saving on main thread.");
|
log.warn("State saver worker not available. Saving on main thread.");
|
||||||
await setCanvasState(this.canvas.node.id, state);
|
await setCanvasState(this.canvas.node.id, state);
|
||||||
|
|||||||
@@ -815,9 +815,9 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
button.classList.add('loading');
|
button.classList.add('loading');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (canvas.selectedLayers.length !== 1) throw new Error("Please select exactly one image layer for matting.");
|
if (canvas.canvasSelection.selectedLayers.length !== 1) throw new Error("Please select exactly one image layer for matting.");
|
||||||
|
|
||||||
const selectedLayer = canvas.selectedLayers[0];
|
const selectedLayer = canvas.canvasSelection.selectedLayers[0];
|
||||||
const selectedLayerIndex = canvas.layers.indexOf(selectedLayer);
|
const selectedLayerIndex = canvas.layers.indexOf(selectedLayer);
|
||||||
const imageData = await canvas.canvasLayers.getLayerImageData(selectedLayer);
|
const imageData = await canvas.canvasLayers.getLayerImageData(selectedLayer);
|
||||||
const response = await fetch("/matting", {
|
const response = await fetch("/matting", {
|
||||||
@@ -841,7 +841,7 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
const newLayer = {...selectedLayer, image: mattedImage};
|
const newLayer = {...selectedLayer, image: mattedImage};
|
||||||
delete newLayer.imageId;
|
delete newLayer.imageId;
|
||||||
canvas.layers[selectedLayerIndex] = newLayer;
|
canvas.layers[selectedLayerIndex] = newLayer;
|
||||||
canvas.updateSelection([newLayer]);
|
canvas.canvasSelection.updateSelection([newLayer]);
|
||||||
canvas.render();
|
canvas.render();
|
||||||
canvas.saveState();
|
canvas.saveState();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -870,6 +870,24 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
]),
|
]),
|
||||||
$el("div.painter-separator"),
|
$el("div.painter-separator"),
|
||||||
$el("div.painter-button-group", {id: "mask-controls"}, [
|
$el("div.painter-button-group", {id: "mask-controls"}, [
|
||||||
|
$el("button.painter-button.primary", {
|
||||||
|
id: `toggle-mask-btn-${node.id}`,
|
||||||
|
textContent: "Show Mask",
|
||||||
|
title: "Toggle mask overlay visibility",
|
||||||
|
onclick: (e) => {
|
||||||
|
const button = e.target;
|
||||||
|
canvas.maskTool.toggleOverlayVisibility();
|
||||||
|
canvas.render();
|
||||||
|
|
||||||
|
if (canvas.maskTool.isOverlayVisible) {
|
||||||
|
button.classList.add('primary');
|
||||||
|
button.textContent = "Show Mask";
|
||||||
|
} else {
|
||||||
|
button.classList.remove('primary');
|
||||||
|
button.textContent = "Hide Mask";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
$el("button.painter-button", {
|
$el("button.painter-button", {
|
||||||
textContent: "Edit Mask",
|
textContent: "Edit Mask",
|
||||||
title: "Open the current canvas view in the mask editor",
|
title: "Open the current canvas view in the mask editor",
|
||||||
@@ -992,7 +1010,7 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
|
|
||||||
|
|
||||||
const updateButtonStates = () => {
|
const updateButtonStates = () => {
|
||||||
const selectionCount = canvas.selectedLayers.length;
|
const selectionCount = canvas.canvasSelection.selectedLayers.length;
|
||||||
const hasSelection = selectionCount > 0;
|
const hasSelection = selectionCount > 0;
|
||||||
controlPanel.querySelectorAll('.requires-selection').forEach(btn => {
|
controlPanel.querySelectorAll('.requires-selection').forEach(btn => {
|
||||||
// Special handling for Fuse button - requires at least 2 layers
|
// Special handling for Fuse button - requires at least 2 layers
|
||||||
@@ -1008,7 +1026,7 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
canvas.onSelectionChange = updateButtonStates;
|
canvas.canvasSelection.onSelectionChange = updateButtonStates;
|
||||||
|
|
||||||
const undoButton = controlPanel.querySelector(`#undo-button-${node.id}`);
|
const undoButton = controlPanel.querySelector(`#undo-button-${node.id}`);
|
||||||
const redoButton = controlPanel.querySelector(`#redo-button-${node.id}`);
|
const redoButton = controlPanel.querySelector(`#redo-button-${node.id}`);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export class MaskTool {
|
|||||||
this.x = 0;
|
this.x = 0;
|
||||||
this.y = 0;
|
this.y = 0;
|
||||||
|
|
||||||
|
this.isOverlayVisible = true;
|
||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
this.brushSize = 20;
|
this.brushSize = 20;
|
||||||
this.brushStrength = 0.5;
|
this.brushStrength = 0.5;
|
||||||
@@ -280,6 +281,11 @@ export class MaskTool {
|
|||||||
log.info(`Mask position updated to (${this.x}, ${this.y})`);
|
log.info(`Mask position updated to (${this.x}, ${this.y})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleOverlayVisibility() {
|
||||||
|
this.isOverlayVisible = !this.isOverlayVisible;
|
||||||
|
log.info(`Mask overlay visibility toggled to: ${this.isOverlayVisible}`);
|
||||||
|
}
|
||||||
|
|
||||||
setMask(image) {
|
setMask(image) {
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
3
js/config.js
Normal file
3
js/config.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Log level for development.
|
||||||
|
// Possible values: 'DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE'
|
||||||
|
export const LOG_LEVEL = 'NONE';
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {logger, LogLevel} from "../logger.js";
|
import {logger, LogLevel} from "../logger.js";
|
||||||
|
import { LOG_LEVEL } from '../config.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tworzy obiekt loggera dla modułu z predefiniowanymi metodami
|
* Tworzy obiekt loggera dla modułu z predefiniowanymi metodami
|
||||||
@@ -11,8 +12,8 @@ import {logger, LogLevel} from "../logger.js";
|
|||||||
* @param {LogLevel} level - Poziom logowania (domyślnie DEBUG)
|
* @param {LogLevel} level - Poziom logowania (domyślnie DEBUG)
|
||||||
* @returns {Object} Obiekt z metodami logowania
|
* @returns {Object} Obiekt z metodami logowania
|
||||||
*/
|
*/
|
||||||
export function createModuleLogger(moduleName, level = LogLevel.NONE) {
|
export function createModuleLogger(moduleName) {
|
||||||
logger.setModuleLevel(moduleName, level);
|
logger.setModuleLevel(moduleName, LogLevel[LOG_LEVEL]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
debug: (...args) => logger.debug(moduleName, ...args),
|
debug: (...args) => logger.debug(moduleName, ...args),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "layerforge"
|
name = "layerforge"
|
||||||
description = "Photoshop-like layered canvas editor to your ComfyUI workflow. This node is perfect for complex compositing, inpainting, and outpainting, featuring multi-layer support, masking, blend modes, and precise transformations. Includes optional AI-powered background removal for streamlined image editing."
|
description = "Photoshop-like layered canvas editor to your ComfyUI workflow. This node is perfect for complex compositing, inpainting, and outpainting, featuring multi-layer support, masking, blend modes, and precise transformations. Includes optional AI-powered background removal for streamlined image editing."
|
||||||
version = "1.3.3.1"
|
version = "1.3.6"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
dependencies = ["torch", "torchvision", "transformers", "aiohttp", "numpy", "tqdm", "Pillow"]
|
dependencies = ["torch", "torchvision", "transformers", "aiohttp", "numpy", "tqdm", "Pillow"]
|
||||||
|
|
||||||
|
|||||||
3
python/config.py
Normal file
3
python/config.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Log level for development.
|
||||||
|
# Possible values: 'DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE'
|
||||||
|
LOG_LEVEL = 'NONE'
|
||||||
Reference in New Issue
Block a user