mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-23 13:32:11 -03:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8d33089d2 | ||
|
|
de67252a87 | ||
|
|
4acece1602 |
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -139,7 +139,7 @@ body:
|
|||||||
- type: textarea
|
- type: textarea
|
||||||
id: additional
|
id: additional
|
||||||
attributes:
|
attributes:
|
||||||
label: Additional Context, Environment (OS, ComfyUI versions, ResolutionMaster version)
|
label: Additional Context, Environment (OS, ComfyUI versions, Comfyui-LayerForge version)
|
||||||
description: Any other information that might help (OS, GPU, specific nodes involved, etc.)
|
description: Any other information that might help (OS, GPU, specific nodes involved, etc.)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export class CanvasInteractions {
|
|||||||
canvasMoveRect: null,
|
canvasMoveRect: null,
|
||||||
outputAreaTransformHandle: null,
|
outputAreaTransformHandle: null,
|
||||||
outputAreaTransformAnchor: { x: 0, y: 0 },
|
outputAreaTransformAnchor: { x: 0, y: 0 },
|
||||||
|
hoveringGrabIcon: false,
|
||||||
};
|
};
|
||||||
this.originalLayerPositions = new Map();
|
this.originalLayerPositions = new Map();
|
||||||
}
|
}
|
||||||
@@ -151,6 +152,29 @@ export class CanvasInteractions {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Sprawdza czy punkt znajduje się w obszarze ikony "grab" (środek layera)
|
||||||
|
* Zwraca layer, jeśli kliknięto w ikonę grab
|
||||||
|
*/
|
||||||
|
getGrabIconAtPosition(worldX, worldY) {
|
||||||
|
// Rozmiar ikony grab w pikselach światowych
|
||||||
|
const grabIconRadius = 20 / this.canvas.viewport.zoom;
|
||||||
|
for (const layer of this.canvas.canvasSelection.selectedLayers) {
|
||||||
|
if (!layer.visible)
|
||||||
|
continue;
|
||||||
|
const centerX = layer.x + layer.width / 2;
|
||||||
|
const centerY = layer.y + layer.height / 2;
|
||||||
|
// Sprawdź czy punkt jest w obszarze ikony grab (okrąg wokół środka)
|
||||||
|
const dx = worldX - centerX;
|
||||||
|
const dy = worldY - centerY;
|
||||||
|
const distanceSquared = dx * dx + dy * dy;
|
||||||
|
const radiusSquared = grabIconRadius * grabIconRadius;
|
||||||
|
if (distanceSquared <= radiusSquared) {
|
||||||
|
return layer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
resetInteractionState() {
|
resetInteractionState() {
|
||||||
this.interaction.mode = 'none';
|
this.interaction.mode = 'none';
|
||||||
this.interaction.resizeHandle = null;
|
this.interaction.resizeHandle = null;
|
||||||
@@ -227,6 +251,14 @@ export class CanvasInteractions {
|
|||||||
this.startLayerTransform(transformTarget.layer, transformTarget.handle, coords.world);
|
this.startLayerTransform(transformTarget.layer, transformTarget.handle, coords.world);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Check if clicking on grab icon of a selected layer
|
||||||
|
const grabIconLayer = this.getGrabIconAtPosition(coords.world.x, coords.world.y);
|
||||||
|
if (grabIconLayer) {
|
||||||
|
// Start dragging the selected layer(s) without changing selection
|
||||||
|
this.interaction.mode = 'potential-drag';
|
||||||
|
this.interaction.dragStart = { ...coords.world };
|
||||||
|
return;
|
||||||
|
}
|
||||||
const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(coords.world.x, coords.world.y);
|
const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(coords.world.x, coords.world.y);
|
||||||
if (clickedLayerResult) {
|
if (clickedLayerResult) {
|
||||||
this.prepareForDrag(clickedLayerResult.layer, coords.world);
|
this.prepareForDrag(clickedLayerResult.layer, coords.world);
|
||||||
@@ -282,6 +314,13 @@ export class CanvasInteractions {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
// Check if hovering over grab icon
|
||||||
|
const wasHovering = this.interaction.hoveringGrabIcon;
|
||||||
|
this.interaction.hoveringGrabIcon = this.getGrabIconAtPosition(coords.world.x, coords.world.y) !== null;
|
||||||
|
// Re-render if hover state changed to show/hide grab icon
|
||||||
|
if (wasHovering !== this.interaction.hoveringGrabIcon) {
|
||||||
|
this.canvas.render();
|
||||||
|
}
|
||||||
this.updateCursor(coords.world);
|
this.updateCursor(coords.world);
|
||||||
// Update brush cursor on overlay if mask tool is active
|
// Update brush cursor on overlay if mask tool is active
|
||||||
if (this.canvas.maskTool.isActive) {
|
if (this.canvas.maskTool.isActive) {
|
||||||
@@ -617,6 +656,11 @@ export class CanvasInteractions {
|
|||||||
this.canvas.canvas.style.cursor = 'grabbing';
|
this.canvas.canvas.style.cursor = 'grabbing';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Check if hovering over grab icon
|
||||||
|
if (this.interaction.hoveringGrabIcon) {
|
||||||
|
this.canvas.canvas.style.cursor = 'grab';
|
||||||
|
return;
|
||||||
|
}
|
||||||
const transformTarget = this.canvas.canvasLayers.getHandleAtPosition(worldCoords.x, worldCoords.y);
|
const transformTarget = this.canvas.canvasLayers.getHandleAtPosition(worldCoords.x, worldCoords.y);
|
||||||
if (transformTarget) {
|
if (transformTarget) {
|
||||||
const handleName = transformTarget.handle;
|
const handleName = transformTarget.handle;
|
||||||
|
|||||||
@@ -141,6 +141,10 @@ export class CanvasRenderer {
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// Draw grab icons for selected layers when hovering
|
||||||
|
if (this.canvas.canvasInteractions.interaction.hoveringGrabIcon) {
|
||||||
|
this.drawGrabIcons(ctx);
|
||||||
|
}
|
||||||
this.drawCanvasOutline(ctx);
|
this.drawCanvasOutline(ctx);
|
||||||
this.drawOutputAreaExtensionPreview(ctx); // Draw extension preview
|
this.drawOutputAreaExtensionPreview(ctx); // Draw extension preview
|
||||||
this.drawPendingGenerationAreas(ctx); // Draw snapshot outlines
|
this.drawPendingGenerationAreas(ctx); // Draw snapshot outlines
|
||||||
@@ -833,6 +837,55 @@ export class CanvasRenderer {
|
|||||||
// Just ensure it's the right size
|
// Just ensure it's the right size
|
||||||
this.updateOverlaySize();
|
this.updateOverlaySize();
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Draw grab icons in the center of selected layers
|
||||||
|
*/
|
||||||
|
drawGrabIcons(ctx) {
|
||||||
|
const selectedLayers = this.canvas.canvasSelection.selectedLayers;
|
||||||
|
if (selectedLayers.length === 0)
|
||||||
|
return;
|
||||||
|
const iconRadius = 20 / this.canvas.viewport.zoom;
|
||||||
|
const innerRadius = 12 / this.canvas.viewport.zoom;
|
||||||
|
selectedLayers.forEach((layer) => {
|
||||||
|
if (!layer.visible)
|
||||||
|
return;
|
||||||
|
const centerX = layer.x + layer.width / 2;
|
||||||
|
const centerY = layer.y + layer.height / 2;
|
||||||
|
ctx.save();
|
||||||
|
// Draw outer circle (background)
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(centerX, centerY, iconRadius, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = 'rgba(0, 150, 255, 0.7)';
|
||||||
|
ctx.fill();
|
||||||
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
|
||||||
|
ctx.lineWidth = 2 / this.canvas.viewport.zoom;
|
||||||
|
ctx.stroke();
|
||||||
|
// Draw hand/grab icon (simplified)
|
||||||
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.95)';
|
||||||
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.95)';
|
||||||
|
ctx.lineWidth = 1.5 / this.canvas.viewport.zoom;
|
||||||
|
// Draw four dots representing grab points
|
||||||
|
const dotRadius = 2 / this.canvas.viewport.zoom;
|
||||||
|
const dotDistance = 6 / this.canvas.viewport.zoom;
|
||||||
|
// Top-left
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(centerX - dotDistance, centerY - dotDistance, dotRadius, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
// Top-right
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(centerX + dotDistance, centerY - dotDistance, dotRadius, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
// Bottom-left
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(centerX - dotDistance, centerY + dotDistance, dotRadius, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
// Bottom-right
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(centerX + dotDistance, centerY + dotDistance, dotRadius, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.restore();
|
||||||
|
});
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Draw transform handles for output area when in transform mode
|
* Draw transform handles for output area when in transform mode
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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.5.10"
|
version = "1.5.11"
|
||||||
license = { text = "MIT License" }
|
license = { text = "MIT License" }
|
||||||
dependencies = ["torch", "torchvision", "transformers", "aiohttp", "numpy", "tqdm", "Pillow"]
|
dependencies = ["torch", "torchvision", "transformers", "aiohttp", "numpy", "tqdm", "Pillow"]
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ interface InteractionState {
|
|||||||
canvasMoveRect: { x: number, y: number, width: number, height: number } | null;
|
canvasMoveRect: { x: number, y: number, width: number, height: number } | null;
|
||||||
outputAreaTransformHandle: string | null;
|
outputAreaTransformHandle: string | null;
|
||||||
outputAreaTransformAnchor: Point;
|
outputAreaTransformAnchor: Point;
|
||||||
|
hoveringGrabIcon: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CanvasInteractions {
|
export class CanvasInteractions {
|
||||||
@@ -98,6 +99,7 @@ export class CanvasInteractions {
|
|||||||
canvasMoveRect: null,
|
canvasMoveRect: null,
|
||||||
outputAreaTransformHandle: null,
|
outputAreaTransformHandle: null,
|
||||||
outputAreaTransformAnchor: { x: 0, y: 0 },
|
outputAreaTransformAnchor: { x: 0, y: 0 },
|
||||||
|
hoveringGrabIcon: false,
|
||||||
};
|
};
|
||||||
this.originalLayerPositions = new Map();
|
this.originalLayerPositions = new Map();
|
||||||
}
|
}
|
||||||
@@ -234,6 +236,33 @@ export class CanvasInteractions {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sprawdza czy punkt znajduje się w obszarze ikony "grab" (środek layera)
|
||||||
|
* Zwraca layer, jeśli kliknięto w ikonę grab
|
||||||
|
*/
|
||||||
|
getGrabIconAtPosition(worldX: number, worldY: number): Layer | null {
|
||||||
|
// Rozmiar ikony grab w pikselach światowych
|
||||||
|
const grabIconRadius = 20 / this.canvas.viewport.zoom;
|
||||||
|
|
||||||
|
for (const layer of this.canvas.canvasSelection.selectedLayers) {
|
||||||
|
if (!layer.visible) continue;
|
||||||
|
|
||||||
|
const centerX = layer.x + layer.width / 2;
|
||||||
|
const centerY = layer.y + layer.height / 2;
|
||||||
|
|
||||||
|
// Sprawdź czy punkt jest w obszarze ikony grab (okrąg wokół środka)
|
||||||
|
const dx = worldX - centerX;
|
||||||
|
const dy = worldY - centerY;
|
||||||
|
const distanceSquared = dx * dx + dy * dy;
|
||||||
|
const radiusSquared = grabIconRadius * grabIconRadius;
|
||||||
|
|
||||||
|
if (distanceSquared <= radiusSquared) {
|
||||||
|
return layer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
resetInteractionState(): void {
|
resetInteractionState(): void {
|
||||||
this.interaction.mode = 'none';
|
this.interaction.mode = 'none';
|
||||||
this.interaction.resizeHandle = null;
|
this.interaction.resizeHandle = null;
|
||||||
@@ -320,6 +349,15 @@ export class CanvasInteractions {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if clicking on grab icon of a selected layer
|
||||||
|
const grabIconLayer = this.getGrabIconAtPosition(coords.world.x, coords.world.y);
|
||||||
|
if (grabIconLayer) {
|
||||||
|
// Start dragging the selected layer(s) without changing selection
|
||||||
|
this.interaction.mode = 'potential-drag';
|
||||||
|
this.interaction.dragStart = {...coords.world};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(coords.world.x, coords.world.y);
|
const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(coords.world.x, coords.world.y);
|
||||||
if (clickedLayerResult) {
|
if (clickedLayerResult) {
|
||||||
this.prepareForDrag(clickedLayerResult.layer, coords.world);
|
this.prepareForDrag(clickedLayerResult.layer, coords.world);
|
||||||
@@ -378,6 +416,15 @@ export class CanvasInteractions {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
// Check if hovering over grab icon
|
||||||
|
const wasHovering = this.interaction.hoveringGrabIcon;
|
||||||
|
this.interaction.hoveringGrabIcon = this.getGrabIconAtPosition(coords.world.x, coords.world.y) !== null;
|
||||||
|
|
||||||
|
// Re-render if hover state changed to show/hide grab icon
|
||||||
|
if (wasHovering !== this.interaction.hoveringGrabIcon) {
|
||||||
|
this.canvas.render();
|
||||||
|
}
|
||||||
|
|
||||||
this.updateCursor(coords.world);
|
this.updateCursor(coords.world);
|
||||||
// Update brush cursor on overlay if mask tool is active
|
// Update brush cursor on overlay if mask tool is active
|
||||||
if (this.canvas.maskTool.isActive) {
|
if (this.canvas.maskTool.isActive) {
|
||||||
@@ -738,6 +785,12 @@ export class CanvasInteractions {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if hovering over grab icon
|
||||||
|
if (this.interaction.hoveringGrabIcon) {
|
||||||
|
this.canvas.canvas.style.cursor = 'grab';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const transformTarget = this.canvas.canvasLayers.getHandleAtPosition(worldCoords.x, worldCoords.y);
|
const transformTarget = this.canvas.canvasLayers.getHandleAtPosition(worldCoords.x, worldCoords.y);
|
||||||
|
|
||||||
if (transformTarget) {
|
if (transformTarget) {
|
||||||
|
|||||||
@@ -188,6 +188,11 @@ export class CanvasRenderer {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Draw grab icons for selected layers when hovering
|
||||||
|
if (this.canvas.canvasInteractions.interaction.hoveringGrabIcon) {
|
||||||
|
this.drawGrabIcons(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
this.drawCanvasOutline(ctx);
|
this.drawCanvasOutline(ctx);
|
||||||
this.drawOutputAreaExtensionPreview(ctx); // Draw extension preview
|
this.drawOutputAreaExtensionPreview(ctx); // Draw extension preview
|
||||||
this.drawPendingGenerationAreas(ctx); // Draw snapshot outlines
|
this.drawPendingGenerationAreas(ctx); // Draw snapshot outlines
|
||||||
@@ -1013,6 +1018,66 @@ export class CanvasRenderer {
|
|||||||
this.updateOverlaySize();
|
this.updateOverlaySize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw grab icons in the center of selected layers
|
||||||
|
*/
|
||||||
|
drawGrabIcons(ctx: any): void {
|
||||||
|
const selectedLayers = this.canvas.canvasSelection.selectedLayers;
|
||||||
|
if (selectedLayers.length === 0) return;
|
||||||
|
|
||||||
|
const iconRadius = 20 / this.canvas.viewport.zoom;
|
||||||
|
const innerRadius = 12 / this.canvas.viewport.zoom;
|
||||||
|
|
||||||
|
selectedLayers.forEach((layer: any) => {
|
||||||
|
if (!layer.visible) return;
|
||||||
|
|
||||||
|
const centerX = layer.x + layer.width / 2;
|
||||||
|
const centerY = layer.y + layer.height / 2;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
|
||||||
|
// Draw outer circle (background)
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(centerX, centerY, iconRadius, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = 'rgba(0, 150, 255, 0.7)';
|
||||||
|
ctx.fill();
|
||||||
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
|
||||||
|
ctx.lineWidth = 2 / this.canvas.viewport.zoom;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Draw hand/grab icon (simplified)
|
||||||
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.95)';
|
||||||
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.95)';
|
||||||
|
ctx.lineWidth = 1.5 / this.canvas.viewport.zoom;
|
||||||
|
|
||||||
|
// Draw four dots representing grab points
|
||||||
|
const dotRadius = 2 / this.canvas.viewport.zoom;
|
||||||
|
const dotDistance = 6 / this.canvas.viewport.zoom;
|
||||||
|
|
||||||
|
// Top-left
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(centerX - dotDistance, centerY - dotDistance, dotRadius, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Top-right
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(centerX + dotDistance, centerY - dotDistance, dotRadius, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Bottom-left
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(centerX - dotDistance, centerY + dotDistance, dotRadius, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Bottom-right
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(centerX + dotDistance, centerY + dotDistance, dotRadius, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draw transform handles for output area when in transform mode
|
* Draw transform handles for output area when in transform mode
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user