mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-22 13:12:10 -03:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfa7309132 | ||
|
|
2ab406ebfd | ||
|
|
d40f68b8c6 | ||
|
|
e5060fd8c3 | ||
|
|
f8eb91c4ad | ||
|
|
c4af745b2a | ||
|
|
c9c0babf3c | ||
|
|
152a3f7dff | ||
|
|
9f9a733731 | ||
|
|
3419061b6c | ||
|
|
9e4da30b59 |
@@ -333,6 +333,24 @@ class CanvasNode:
|
||||
latest_image_path = max(image_files, key=os.path.getctime)
|
||||
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
|
||||
def get_flow_status(cls, flow_id=None):
|
||||
|
||||
@@ -454,6 +472,30 @@ class CanvasNode:
|
||||
'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")
|
||||
async def get_latest_image_route(request):
|
||||
try:
|
||||
@@ -571,42 +613,38 @@ class BiRefNetMatting:
|
||||
def load_model(self, model_path):
|
||||
try:
|
||||
if model_path not in self.model_cache:
|
||||
|
||||
full_model_path = os.path.join(self.base_path, "BiRefNet")
|
||||
|
||||
log_info(f"Loading BiRefNet model from {full_model_path}...")
|
||||
|
||||
try:
|
||||
|
||||
self.model = AutoModelForImageSegmentation.from_pretrained(
|
||||
"ZhengPeng7/BiRefNet",
|
||||
trust_remote_code=True,
|
||||
cache_dir=full_model_path
|
||||
)
|
||||
|
||||
self.model.eval()
|
||||
if torch.cuda.is_available():
|
||||
self.model = self.model.cuda()
|
||||
|
||||
self.model_cache[model_path] = self.model
|
||||
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:
|
||||
log_error(f"Failed to load model: {str(e)}")
|
||||
raise
|
||||
|
||||
log_error(f"Failed to load model from Hugging Face: {str(e)}")
|
||||
# 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:
|
||||
self.model = self.model_cache[model_path]
|
||||
log_debug("Using cached model")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
# Catch the re-raised exception or any other error
|
||||
log_error(f"Error loading model: {str(e)}")
|
||||
log_exception("Model loading failed")
|
||||
return False
|
||||
raise # Re-raise the exception to be caught by the execute method
|
||||
|
||||
def preprocess_image(self, image):
|
||||
|
||||
@@ -636,11 +674,9 @@ class BiRefNetMatting:
|
||||
|
||||
def execute(self, image, model_path, threshold=0.5, refinement=1):
|
||||
try:
|
||||
|
||||
PromptServer.instance.send_sync("matting_status", {"status": "processing"})
|
||||
|
||||
if not self.load_model(model_path):
|
||||
raise RuntimeError("Failed to load model")
|
||||
self.load_model(model_path)
|
||||
|
||||
if isinstance(image, torch.Tensor):
|
||||
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();
|
||||
}
|
||||
}
|
||||
116
js/Canvas.js
116
js/Canvas.js
@@ -9,10 +9,22 @@ import {CanvasLayersPanel} from "./CanvasLayersPanel.js";
|
||||
import {CanvasRenderer} from "./CanvasRenderer.js";
|
||||
import {CanvasIO} from "./CanvasIO.js";
|
||||
import {ImageReferenceManager} from "./ImageReferenceManager.js";
|
||||
import {BatchPreviewManager} from "./BatchPreviewManager.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";
|
||||
|
||||
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');
|
||||
|
||||
/**
|
||||
@@ -146,6 +158,7 @@ export class Canvas {
|
||||
// Stwórz opóźnioną wersję funkcji zapisu stanu
|
||||
this.requestSaveState = debounce(this.saveState.bind(this), 500);
|
||||
|
||||
this._addAutoRefreshToggle();
|
||||
this.maskTool = new MaskTool(this, {onStateChange: this.onStateChange});
|
||||
this.canvasState = new CanvasState(this);
|
||||
this.canvasInteractions = new CanvasInteractions(this);
|
||||
@@ -154,6 +167,8 @@ export class Canvas {
|
||||
this.canvasRenderer = new CanvasRenderer(this);
|
||||
this.canvasIO = new CanvasIO(this);
|
||||
this.imageReferenceManager = new ImageReferenceManager(this);
|
||||
this.batchPreviewManagers = [];
|
||||
this.pendingBatchContext = null;
|
||||
|
||||
log.debug('Canvas modules initialized successfully');
|
||||
}
|
||||
@@ -274,6 +289,26 @@ export class Canvas {
|
||||
/**
|
||||
* Usuwa wybrane warstwy
|
||||
*/
|
||||
removeLayersByIds(layerIds) {
|
||||
if (!layerIds || layerIds.length === 0) return;
|
||||
|
||||
const initialCount = this.layers.length;
|
||||
this.saveState();
|
||||
this.layers = this.layers.filter(l => !layerIds.includes(l.id));
|
||||
|
||||
// If the current selection was part of the removal, clear it
|
||||
const newSelection = this.selectedLayers.filter(l => !layerIds.includes(l.id));
|
||||
this.updateSelection(newSelection);
|
||||
|
||||
this.render();
|
||||
this.saveState();
|
||||
|
||||
if (this.canvasLayersPanel) {
|
||||
this.canvasLayersPanel.onLayersChanged();
|
||||
}
|
||||
log.info(`Removed ${initialCount - this.layers.length} layers by ID.`);
|
||||
}
|
||||
|
||||
removeSelectedLayers() {
|
||||
if (this.selectedLayers.length > 0) {
|
||||
log.info('Removing selected layers', {
|
||||
@@ -445,6 +480,87 @@ export class Canvas {
|
||||
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
|
||||
|
||||
@@ -744,12 +744,7 @@ export class CanvasIO {
|
||||
img.src = result.image_data;
|
||||
});
|
||||
|
||||
await this.canvas.canvasLayers.addLayerWithImage(img, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: this.canvas.width,
|
||||
height: this.canvas.height,
|
||||
});
|
||||
await this.canvas.canvasLayers.addLayerWithImage(img, {}, 'fit');
|
||||
log.info("Latest image imported and placed on canvas successfully.");
|
||||
return true;
|
||||
} else {
|
||||
@@ -761,4 +756,41 @@ export class CanvasIO {
|
||||
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 viewCoords = this.canvas.getMouseViewCoordinates(e);
|
||||
|
||||
if (this.interaction.mode === 'drawingMask') {
|
||||
this.canvas.maskTool.handleMouseDown(worldCoords, viewCoords);
|
||||
this.canvas.render();
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Ostateczna, poprawna kolejność sprawdzania ---
|
||||
|
||||
// 1. Akcje globalne z modyfikatorami (mają najwyższy priorytet)
|
||||
@@ -115,6 +121,7 @@ export class CanvasInteractions {
|
||||
|
||||
handleMouseMove(e) {
|
||||
const worldCoords = this.canvas.getMouseWorldCoordinates(e);
|
||||
const viewCoords = this.canvas.getMouseViewCoordinates(e);
|
||||
this.canvas.lastMousePosition = worldCoords; // Zawsze aktualizuj ostatnią pozycję myszy
|
||||
|
||||
// Sprawdź, czy rozpocząć przeciąganie
|
||||
@@ -131,6 +138,10 @@ export class CanvasInteractions {
|
||||
}
|
||||
|
||||
switch (this.interaction.mode) {
|
||||
case 'drawingMask':
|
||||
this.canvas.maskTool.handleMouseMove(worldCoords, viewCoords);
|
||||
this.canvas.render();
|
||||
break;
|
||||
case 'panning':
|
||||
this.panViewport(e);
|
||||
break;
|
||||
@@ -156,6 +167,13 @@ export class CanvasInteractions {
|
||||
}
|
||||
|
||||
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') {
|
||||
this.finalizeCanvasResize();
|
||||
}
|
||||
@@ -514,6 +532,29 @@ export class CanvasInteractions {
|
||||
|
||||
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.y -= finalY;
|
||||
}
|
||||
@@ -683,20 +724,43 @@ export class CanvasInteractions {
|
||||
if (this.interaction.canvasResizeRect && this.interaction.canvasResizeRect.width > 1 && this.interaction.canvasResizeRect.height > 1) {
|
||||
const newWidth = Math.round(this.interaction.canvasResizeRect.width);
|
||||
const newHeight = Math.round(this.interaction.canvasResizeRect.height);
|
||||
const rectX = this.interaction.canvasResizeRect.x;
|
||||
const rectY = this.interaction.canvasResizeRect.y;
|
||||
const finalX = this.interaction.canvasResizeRect.x;
|
||||
const finalY = this.interaction.canvasResizeRect.y;
|
||||
|
||||
this.canvas.updateOutputAreaSize(newWidth, newHeight);
|
||||
|
||||
this.canvas.layers.forEach(layer => {
|
||||
layer.x -= rectX;
|
||||
layer.y -= rectY;
|
||||
layer.x -= finalX;
|
||||
layer.y -= finalY;
|
||||
});
|
||||
|
||||
this.canvas.maskTool.updatePosition(-rectX, -rectY);
|
||||
this.canvas.maskTool.updatePosition(-finalX, -finalY);
|
||||
|
||||
this.canvas.viewport.x -= rectX;
|
||||
this.canvas.viewport.y -= rectY;
|
||||
// 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 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
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();
|
||||
await saveImage(imageId, image.src);
|
||||
this.canvas.imageCache.set(imageId, image.src);
|
||||
@@ -163,21 +163,25 @@ export class CanvasLayers {
|
||||
let finalHeight = image.height;
|
||||
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') {
|
||||
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;
|
||||
finalHeight = image.height * scale;
|
||||
finalX = (this.canvas.width - finalWidth) / 2;
|
||||
finalY = (this.canvas.height - finalHeight) / 2;
|
||||
finalX = area.x + (area.width - finalWidth) / 2;
|
||||
finalY = area.y + (area.height - finalHeight) / 2;
|
||||
} else if (addMode === 'mouse') {
|
||||
finalX = this.canvas.lastMousePosition.x - finalWidth / 2;
|
||||
finalY = this.canvas.lastMousePosition.y - finalHeight / 2;
|
||||
} else { // 'center' or 'default'
|
||||
finalX = (this.canvas.width - finalWidth) / 2;
|
||||
finalY = (this.canvas.height - finalHeight) / 2;
|
||||
finalX = area.x + (area.width - finalWidth) / 2;
|
||||
finalY = area.y + (area.height - finalHeight) / 2;
|
||||
}
|
||||
|
||||
const layer = {
|
||||
id: generateUUID(),
|
||||
image: image,
|
||||
imageId: imageId,
|
||||
x: finalX,
|
||||
|
||||
@@ -82,8 +82,9 @@ export class CanvasRenderer {
|
||||
});
|
||||
|
||||
this.drawCanvasOutline(ctx);
|
||||
this.drawPendingGenerationAreas(ctx); // Draw snapshot outlines
|
||||
const maskImage = this.canvas.maskTool.getMask();
|
||||
if (maskImage) {
|
||||
if (maskImage && this.canvas.maskTool.isOverlayVisible) {
|
||||
|
||||
ctx.save();
|
||||
|
||||
@@ -112,6 +113,13 @@ export class CanvasRenderer {
|
||||
this.canvas.canvas.height = this.canvas.offscreenCanvas.height;
|
||||
}
|
||||
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) {
|
||||
@@ -321,4 +329,36 @@ export class CanvasRenderer {
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -870,6 +870,24 @@ async function createCanvasWidget(node, widget, app) {
|
||||
]),
|
||||
$el("div.painter-separator"),
|
||||
$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", {
|
||||
textContent: "Edit Mask",
|
||||
title: "Open the current canvas view in the mask editor",
|
||||
|
||||
@@ -13,6 +13,7 @@ export class MaskTool {
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
|
||||
this.isOverlayVisible = true;
|
||||
this.isActive = false;
|
||||
this.brushSize = 20;
|
||||
this.brushStrength = 0.5;
|
||||
@@ -280,6 +281,11 @@ export class MaskTool {
|
||||
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) {
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
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."
|
||||
version = "1.3.3.1"
|
||||
version = "1.3.5"
|
||||
license = {file = "LICENSE"}
|
||||
dependencies = ["torch", "torchvision", "transformers", "aiohttp", "numpy", "tqdm", "Pillow"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user