7 Commits

Author SHA1 Message Date
Dariusz L
f3027587d6 Improve release workflow to show commit history
The release workflow now fetches the full git history and displays all commit messages since the last tag in the release notes, instead of only the latest commit. Also bumps the project version to 1.3.8 in pyproject.toml.
2025-07-21 23:15:58 +02:00
Dariusz L
20d52b632a Refactor layer rendering into reusable methods
Moved layer drawing logic into CanvasLayers._drawLayer and _drawLayers methods, replacing repeated rendering code in CanvasIO and CanvasLayers. This improves maintainability and ensures consistent handling of layer properties such as blend mode, opacity, rotation, and flipping. Also, fixed layer serialization to only generate imageId when missing, and ensured new layers have flipH/flipV set when created from matted images.
2025-07-21 23:03:52 +02:00
Dariusz L
57bd1e1499 Add layer flipH/flipV properties and rendering support
Refactors horizontal and vertical mirroring to toggle flipH/flipV properties on layers instead of modifying image data. Updates rendering logic in CanvasLayers and CanvasRenderer to apply horizontal/vertical flipping via canvas transforms. Adds flipH and flipV to Layer type and includes them in state signature calculation.
2025-07-21 22:35:18 +02:00
Dariusz L
674879b497 Increase WebSocket max message size
Sets the max_msg_size parameter to 32MB for the WebSocketResponse in handle_canvas_websocket to support larger messages.
2025-07-21 21:52:21 +02:00
Dariusz L
98d4769ba1 Fix preview visibility initialization in Canvas
Replaces setPreviewVisibility(false) with direct assignment to previewVisible in Canvas and Canvas.ts. Adds initialization of preview state based on widget value in CanvasView and CanvasView.ts to ensure correct preview visibility on widget creation.
2025-07-21 20:45:13 +02:00
Dariusz L
5419acad27 Refactor auto-refresh toggle to node widget property
Replaces the local auto-refresh toggle with a node widget property 'auto_refresh_after_generation' in both JS and TS Canvas classes. Updates Python CanvasNode to include this property in the required config and removes 'hidden' from 'trigger' and 'node_id'. This change centralizes auto-refresh state in the node widget for better consistency and UI integration.
2025-07-21 20:31:46 +02:00
Dariusz L
db65c0c72e Improve bug report template with detailed log instructions
Expanded the bug report template to include step-by-step instructions for enabling DEBUG logs in both frontend and backend, and added a required field for backend logs. Clarified instructions for gathering browser console logs and emphasized the need for up-to-date versions before reporting. These changes aim to help users provide more actionable information for debugging.
2025-07-04 07:58:34 +02:00
19 changed files with 272 additions and 383 deletions

View File

@@ -7,19 +7,46 @@ body:
attributes: attributes:
value: | value: |
**Thank you for reporting a bug!** **Thank you for reporting a bug!**
Please follow these steps to capture useful info: Please follow these steps to capture all necessary information:
### How to gather the necessary information: ### ✅ Before You Report:
🌐 **Browser & Version:** 1. Make sure you have the **latest versions**:
- Chrome: Click the three dots → Help → About Google Chrome - [ComfyUI Github](https://github.com/comfyanonymous/ComfyUI/releases)
- Firefox: Click the three bars → Help → About Firefox - [LayerForge Github](https://github.com/Azornes/Comfyui-LayerForge/releases) or via [ComfyUI Node Manager](https://registry.comfy.org/publishers/azornes/nodes/layerforge)
- Edge: Click the three dots → Help and feedback → About Microsoft Edge 2. Gather the required logs:
### 🔍 Enable Debug Logs (for **full** logs):
#### 1. Edit `config.js` (Frontend Logs):
Path:
```
ComfyUI/custom_nodes/Comfyui-LayerForge/js/config.js
```
Find:
```js
export const LOG_LEVEL = 'NONE';
```
Change to:
```js
export const LOG_LEVEL = 'DEBUG';
```
#### 2. Edit `config.py` (Backend Logs):
Path:
```
ComfyUI/custom_nodes/Comfyui-LayerForge/python/config.py
```
Find:
```python
LOG_LEVEL = 'NONE'
```
Change to:
```python
LOG_LEVEL = 'DEBUG'
```
🔗 **Where to find the latest versions of ComfyUI and LayerForge:** ➡️ **Restart ComfyUI** after applying these changes to activate full logging.
- [ComfyUI Github](https://github.com/comfyanonymous/ComfyUI/releases)
- [LayerForge Github](https://github.com/Azornes/Comfyui-LayerForge/releases/tag/v1.2.4) or [LayerForge from manager Comfyui](https://registry.comfy.org/publishers/azornes/nodes/layerforge)
Make sure you have the latest versions before reporting an issue.
- type: input - type: input
id: environment id: environment
attributes: attributes:
@@ -71,23 +98,36 @@ body:
validations: validations:
required: true required: true
- type: textarea
id: backend_logs
attributes:
label: ComfyUI (Backend) Logs
description: |
After enabling DEBUG logs, please:
1. Restart ComfyUI.
2. Reproduce the issue.
3. Copy-paste the newest **TEXT** logs from the terminal/console here.
validations:
required: true
- type: textarea - type: textarea
id: console_logs id: console_logs
attributes: attributes:
label: Browser Console Logs label: Browser Console Logs
description: | description: |
**How to capture logs:** After enabling DEBUG logs:
- **Open console:** 1. Open Developer Tools → Console.
- Chrome/Edge (Win/Linux): `Ctrl+Shift+J` - Chrome/Edge (Win/Linux): `Ctrl+Shift+J`
Mac: `Cmd+Option+J` Mac: `Cmd+Option+J`
- Firefox (Win/Linux): `Ctrl+Shift+K` - Firefox (Win/Linux): `Ctrl+Shift+K`
Mac: `Cmd+Option+K` Mac: `Cmd+Option+K`
- Safari (Mac): enable **Develop** menu in Preferences → Advanced, then `Cmd+Option+C` - Safari (Mac): enable **Develop** menu in Preferences → Advanced, then `Cmd+Option+C`
- **Clear console** before reproducing: 2. Clear console (before reproducing):
- Chrome/Edge: click “🚫 Clear console” or press `Ctrl+L` (Win/Linux) / `Cmd+K` (Mac) - Chrome/Edge: “🚫 Clear console” or `Ctrl+L` (Win/Linux) / `Cmd+K` (Mac).
- Firefox: `Ctrl+Shift+L` (newer) or `Ctrl+L` (older) (Win/Linux), Mac: `Cmd+K` - Firefox: `Ctrl+Shift+L` (newer) or `Ctrl+L` (older) (Win/Linux) / `Cmd+K` (Mac).
- Safari: click 🗑 icon or press `Cmd+K` / `Ctrl+L` - Safari: 🗑 icon or `Cmd+K`.
- Reproduce the issue and paste new logs here. 3. Reproduce the issue.
4. Copy-paste the **TEXT** logs here (no screenshots).
validations: validations:
required: true required: true
@@ -95,4 +135,4 @@ body:
attributes: attributes:
value: | value: |
**Optional:** You can also **attach a screenshot or video** to demonstrate the issue visually. **Optional:** You can also **attach a screenshot or video** to demonstrate the issue visually.
To add media, simply drag & drop or paste image/video files into this issue form. GitHub supports common image formats and MP4/GIF files. Simply drag & drop or paste image/video files into this issue form. GitHub supports common image formats and MP4/GIF files.

View File

@@ -12,6 +12,8 @@ jobs:
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0 # Pobierz pełną historię Git (potrzebne do git log)
- name: Extract base version from pyproject.toml - name: Extract base version from pyproject.toml
id: version id: version
@@ -33,12 +35,32 @@ jobs:
run: | run: |
echo "final_tag=v${{ steps.version.outputs.base_version }}" >> $GITHUB_OUTPUT echo "final_tag=v${{ steps.version.outputs.base_version }}" >> $GITHUB_OUTPUT
- name: Get latest commit message # ZMIANA: Zamiast tylko ostatniego commita, pobierz historię commitów od ostatniego tagu
id: last_commit - name: Get commit history since last tag
id: commit_history
run: | run: |
msg=$(git log -1 --pretty=%B) # Znajdź ostatni tag (jeśli istnieje)
msg=${msg//$'\n'/\\n} LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
echo "commit_msg=$msg" >> $GITHUB_OUTPUT
# Jeśli nie ma ostatniego tagu, użyj pustego (pobierz od początku repo)
if [ -z "$LAST_TAG" ]; then
RANGE="HEAD"
else
RANGE="$LAST_TAG..HEAD"
fi
# Pobierz listę commitów (tylko subject/tytuł, format: - Commit message)
HISTORY=$(git log --pretty=format:"- %s" $RANGE)
# Zastąp nowe linie na \\n, aby dobrze wyglądało w output
HISTORY=${HISTORY//$'\n'/\\n}
# Jeśli brak commitów, ustaw domyślną wiadomość
if [ -z "$HISTORY" ]; then
HISTORY="No changes since last release."
fi
echo "commit_history=$HISTORY" >> $GITHUB_OUTPUT
- name: Create GitHub Release - name: Create GitHub Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
@@ -48,9 +70,9 @@ jobs:
body: | body: |
📦 Release based on pyproject.toml version `${{ steps.version.outputs.base_version }}` 📦 Release based on pyproject.toml version `${{ steps.version.outputs.base_version }}`
📝 Last commit message: 📝 Changes since last release:
``` ```
${{ steps.last_commit.outputs.commit_msg }} ${{ steps.commit_history.outputs.commit_history }}
``` ```
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -175,8 +175,9 @@ class CanvasNode:
"required": { "required": {
"fit_on_add": ("BOOLEAN", {"default": False, "label_on": "Fit on Add/Paste", "label_off": "Default Behavior"}), "fit_on_add": ("BOOLEAN", {"default": False, "label_on": "Fit on Add/Paste", "label_off": "Default Behavior"}),
"show_preview": ("BOOLEAN", {"default": False, "label_on": "Show Preview", "label_off": "Hide Preview"}), "show_preview": ("BOOLEAN", {"default": False, "label_on": "Show Preview", "label_off": "Hide Preview"}),
"trigger": ("INT", {"default": 0, "min": 0, "max": 99999999, "step": 1, "hidden": True}), "auto_refresh_after_generation": ("BOOLEAN", {"default": False, "label_on": "True", "label_off": "False"}),
"node_id": ("STRING", {"default": "0", "hidden": True}), "trigger": ("INT", {"default": 0, "min": 0, "max": 99999999, "step": 1}),
"node_id": ("STRING", {"default": "0"}),
}, },
"hidden": { "hidden": {
"prompt": ("PROMPT",), "prompt": ("PROMPT",),
@@ -238,7 +239,7 @@ class CanvasNode:
_processing_lock = threading.Lock() _processing_lock = threading.Lock()
def process_canvas_image(self, fit_on_add, show_preview, trigger, node_id, prompt=None, unique_id=None): def process_canvas_image(self, fit_on_add, show_preview, auto_refresh_after_generation, trigger, node_id, prompt=None, unique_id=None):
try: try:
@@ -391,7 +392,7 @@ class CanvasNode:
def setup_routes(cls): def setup_routes(cls):
@PromptServer.instance.routes.get("/layerforge/canvas_ws") @PromptServer.instance.routes.get("/layerforge/canvas_ws")
async def handle_canvas_websocket(request): async def handle_canvas_websocket(request):
ws = web.WebSocketResponse() ws = web.WebSocketResponse(max_msg_size=33554432)
await ws.prepare(request) await ws.prepare(request)
async for msg in ws: async for msg in ws:

View File

@@ -83,7 +83,7 @@ export class Canvas {
dimensions: { width: this.width, height: this.height }, dimensions: { width: this.width, height: this.height },
viewport: this.viewport viewport: this.viewport
}); });
this.setPreviewVisibility(false); this.previewVisible = false;
} }
async waitForWidget(name, node, interval = 100, timeout = 20000) { async waitForWidget(name, node, interval = 100, timeout = 20000) {
const startTime = Date.now(); const startTime = Date.now();
@@ -155,7 +155,7 @@ export class Canvas {
log.debug('Initializing Canvas modules...'); log.debug('Initializing Canvas modules...');
// Stwórz opóźnioną wersję funkcji zapisu stanu // Stwórz opóźnioną wersję funkcji zapisu stanu
this.requestSaveState = debounce(() => this.saveState(), 500); this.requestSaveState = debounce(() => this.saveState(), 500);
this._addAutoRefreshToggle(); this._setupAutoRefreshHandlers();
log.debug('Canvas modules initialized successfully'); log.debug('Canvas modules initialized successfully');
} }
/** /**
@@ -321,11 +321,15 @@ export class Canvas {
async importLatestImage() { async importLatestImage() {
return this.canvasIO.importLatestImage(); return this.canvasIO.importLatestImage();
} }
_addAutoRefreshToggle() { _setupAutoRefreshHandlers() {
let autoRefreshEnabled = false;
let lastExecutionStartTime = 0; let lastExecutionStartTime = 0;
// Helper function to get auto-refresh value from node widget
const getAutoRefreshValue = () => {
const widget = this.node.widgets.find((w) => w.name === 'auto_refresh_after_generation');
return widget ? widget.value : false;
};
const handleExecutionStart = () => { const handleExecutionStart = () => {
if (autoRefreshEnabled) { if (getAutoRefreshValue()) {
lastExecutionStartTime = Date.now(); lastExecutionStartTime = Date.now();
// Store a snapshot of the context for the upcoming batch // Store a snapshot of the context for the upcoming batch
this.pendingBatchContext = { this.pendingBatchContext = {
@@ -347,7 +351,7 @@ export class Canvas {
} }
}; };
const handleExecutionSuccess = async () => { const handleExecutionSuccess = async () => {
if (autoRefreshEnabled) { if (getAutoRefreshValue()) {
log.info('Auto-refresh triggered, importing latest images.'); log.info('Auto-refresh triggered, importing latest images.');
if (!this.pendingBatchContext) { if (!this.pendingBatchContext) {
log.warn("execution_start did not fire, cannot process batch. Awaiting next execution."); log.warn("execution_start did not fire, cannot process batch. Awaiting next execution.");
@@ -366,12 +370,6 @@ export class Canvas {
this.render(); 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_start', handleExecutionStart);
api.addEventListener('execution_success', handleExecutionSuccess); api.addEventListener('execution_success', handleExecutionSuccess);
this.node.onRemoved = useChainCallback(this.node.onRemoved, () => { this.node.onRemoved = useChainCallback(this.node.onRemoved, () => {
@@ -379,6 +377,7 @@ export class Canvas {
api.removeEventListener('execution_start', handleExecutionStart); api.removeEventListener('execution_start', handleExecutionStart);
api.removeEventListener('execution_success', handleExecutionSuccess); api.removeEventListener('execution_success', handleExecutionSuccess);
}); });
log.debug('Auto-refresh handlers setup complete, reading from node widget: auto_refresh_after_generation');
} }
/** /**
* Uruchamia edytor masek * Uruchamia edytor masek

View File

@@ -62,25 +62,9 @@ export class CanvasIO {
maskCtx.fillStyle = '#ffffff'; maskCtx.fillStyle = '#ffffff';
maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height); maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
log.debug(`Canvas contexts created, starting layer rendering`); log.debug(`Canvas contexts created, starting layer rendering`);
const sortedLayers = this.canvas.layers.sort((a, b) => a.zIndex - b.zIndex); this.canvas.canvasLayers.drawLayersToContext(tempCtx, this.canvas.layers);
log.debug(`Processing ${sortedLayers.length} layers in order`); this.canvas.canvasLayers.drawLayersToContext(visibilityCtx, this.canvas.layers);
sortedLayers.forEach((layer, index) => { log.debug(`Finished rendering layers`);
log.debug(`Processing layer ${index}: zIndex=${layer.zIndex}, size=${layer.width}x${layer.height}, pos=(${layer.x},${layer.y})`);
log.debug(`Layer ${index}: blendMode=${layer.blendMode || 'normal'}, opacity=${layer.opacity !== undefined ? layer.opacity : 1}`);
tempCtx.save();
tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
tempCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2);
tempCtx.rotate(layer.rotation * Math.PI / 180);
tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
tempCtx.restore();
log.debug(`Layer ${index} rendered successfully`);
visibilityCtx.save();
visibilityCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2);
visibilityCtx.rotate(layer.rotation * Math.PI / 180);
visibilityCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
visibilityCtx.restore();
});
const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height); const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
const maskData = maskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height); const maskData = maskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
for (let i = 0; i < visibilityData.data.length; i += 4) { for (let i = 0; i < visibilityData.data.length; i += 4) {
@@ -232,21 +216,8 @@ export class CanvasIO {
throw new Error("Could not create temp context"); throw new Error("Could not create temp context");
maskCtx.fillStyle = '#ffffff'; // Start with a white mask (nothing masked) maskCtx.fillStyle = '#ffffff'; // Start with a white mask (nothing masked)
maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height); maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
const sortedLayers = this.canvas.layers.sort((a, b) => a.zIndex - b.zIndex); this.canvas.canvasLayers.drawLayersToContext(tempCtx, this.canvas.layers);
sortedLayers.forEach((layer) => { this.canvas.canvasLayers.drawLayersToContext(visibilityCtx, this.canvas.layers);
tempCtx.save();
tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
tempCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2);
tempCtx.rotate(layer.rotation * Math.PI / 180);
tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
tempCtx.restore();
visibilityCtx.save();
visibilityCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2);
visibilityCtx.rotate(layer.rotation * Math.PI / 180);
visibilityCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
visibilityCtx.restore();
});
const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height); const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
const maskData = maskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height); const maskData = maskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
for (let i = 0; i < visibilityData.data.length; i += 4) { for (let i = 0; i < visibilityData.data.length; i += 4) {

View File

@@ -303,55 +303,49 @@ export class CanvasLayers {
} }
return null; return null;
} }
_drawLayer(ctx, layer, options = {}) {
if (!layer.image)
return;
const { offsetX = 0, offsetY = 0 } = options;
ctx.save();
ctx.globalCompositeOperation = layer.blendMode || 'normal';
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
const centerX = layer.x + layer.width / 2 - offsetX;
const centerY = layer.y + layer.height / 2 - offsetY;
ctx.translate(centerX, centerY);
ctx.rotate(layer.rotation * Math.PI / 180);
const scaleH = layer.flipH ? -1 : 1;
const scaleV = layer.flipV ? -1 : 1;
if (layer.flipH || layer.flipV) {
ctx.scale(scaleH, scaleV);
}
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
ctx.restore();
}
_drawLayers(ctx, layers, options = {}) {
const sortedLayers = [...layers].sort((a, b) => a.zIndex - b.zIndex);
sortedLayers.forEach(layer => this._drawLayer(ctx, layer, options));
}
drawLayersToContext(ctx, layers, options = {}) {
this._drawLayers(ctx, layers, options);
}
async mirrorHorizontal() { async mirrorHorizontal() {
if (this.canvas.canvasSelection.selectedLayers.length === 0) if (this.canvas.canvasSelection.selectedLayers.length === 0)
return; return;
const promises = this.canvas.canvasSelection.selectedLayers.map((layer) => { this.canvas.canvasSelection.selectedLayers.forEach((layer) => {
return new Promise(resolve => { layer.flipH = !layer.flipH;
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
if (!tempCtx)
return;
tempCanvas.width = layer.image.width;
tempCanvas.height = layer.image.height;
tempCtx.translate(tempCanvas.width, 0);
tempCtx.scale(-1, 1);
tempCtx.drawImage(layer.image, 0, 0);
const newImage = new Image();
newImage.onload = () => {
layer.image = newImage;
resolve();
};
newImage.src = tempCanvas.toDataURL();
});
}); });
await Promise.all(promises);
this.canvas.render(); this.canvas.render();
this.canvas.requestSaveState(); this.canvas.requestSaveState();
} }
async mirrorVertical() { async mirrorVertical() {
if (this.canvas.canvasSelection.selectedLayers.length === 0) if (this.canvas.canvasSelection.selectedLayers.length === 0)
return; return;
const promises = this.canvas.canvasSelection.selectedLayers.map((layer) => { this.canvas.canvasSelection.selectedLayers.forEach((layer) => {
return new Promise(resolve => { layer.flipV = !layer.flipV;
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
if (!tempCtx)
return;
tempCanvas.width = layer.image.width;
tempCanvas.height = layer.image.height;
tempCtx.translate(0, tempCanvas.height);
tempCtx.scale(1, -1);
tempCtx.drawImage(layer.image, 0, 0);
const newImage = new Image();
newImage.onload = () => {
layer.image = newImage;
resolve();
};
newImage.src = tempCanvas.toDataURL();
});
}); });
await Promise.all(promises);
this.canvas.render(); this.canvas.render();
this.canvas.requestSaveState(); this.canvas.requestSaveState();
} }
@@ -363,12 +357,14 @@ export class CanvasLayers {
throw new Error("Could not create canvas context"); throw new Error("Could not create canvas context");
tempCanvas.width = layer.width; tempCanvas.width = layer.width;
tempCanvas.height = layer.height; tempCanvas.height = layer.height;
tempCtx.clearRect(0, 0, tempCanvas.width, tempCanvas.height); // We need to draw the layer relative to the new canvas, so we "move" it to 0,0
tempCtx.save(); // by creating a temporary layer object for drawing.
tempCtx.translate(layer.width / 2, layer.height / 2); const layerToDraw = {
tempCtx.rotate(layer.rotation * Math.PI / 180); ...layer,
tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height); x: 0,
tempCtx.restore(); y: 0,
};
this._drawLayer(tempCtx, layerToDraw);
const dataUrl = tempCanvas.toDataURL('image/png'); const dataUrl = tempCanvas.toDataURL('image/png');
if (!dataUrl.startsWith('data:image/png;base64,')) { if (!dataUrl.startsWith('data:image/png;base64,')) {
throw new Error("Invalid image data format"); throw new Error("Invalid image data format");
@@ -602,20 +598,7 @@ export class CanvasLayers {
reject(new Error("Could not create canvas context")); reject(new Error("Could not create canvas context"));
return; return;
} }
const sortedLayers = [...this.canvas.layers].sort((a, b) => a.zIndex - b.zIndex); this._drawLayers(tempCtx, this.canvas.layers);
sortedLayers.forEach((layer) => {
if (!layer.image)
return;
tempCtx.save();
tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
const centerX = layer.x + layer.width / 2;
const centerY = layer.y + layer.height / 2;
tempCtx.translate(centerX, centerY);
tempCtx.rotate(layer.rotation * Math.PI / 180);
tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
tempCtx.restore();
});
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height); const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
const data = imageData.data; const data = imageData.data;
const toolMaskCanvas = this.canvas.maskTool.getMask(); const toolMaskCanvas = this.canvas.maskTool.getMask();
@@ -677,20 +660,7 @@ export class CanvasLayers {
reject(new Error("Could not create canvas context")); reject(new Error("Could not create canvas context"));
return; return;
} }
const sortedLayers = [...this.canvas.layers].sort((a, b) => a.zIndex - b.zIndex); this._drawLayers(tempCtx, this.canvas.layers);
sortedLayers.forEach((layer) => {
if (!layer.image)
return;
tempCtx.save();
tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
const centerX = layer.x + layer.width / 2;
const centerY = layer.y + layer.height / 2;
tempCtx.translate(centerX, centerY);
tempCtx.rotate(layer.rotation * Math.PI / 180);
tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
tempCtx.restore();
});
tempCanvas.toBlob((blob) => { tempCanvas.toBlob((blob) => {
if (blob) { if (blob) {
resolve(blob); resolve(blob);
@@ -748,20 +718,7 @@ export class CanvasLayers {
return; return;
} }
tempCtx.translate(-minX, -minY); tempCtx.translate(-minX, -minY);
const sortedSelection = [...this.canvas.canvasSelection.selectedLayers].sort((a, b) => a.zIndex - b.zIndex); this._drawLayers(tempCtx, this.canvas.canvasSelection.selectedLayers);
sortedSelection.forEach((layer) => {
if (!layer.image)
return;
tempCtx.save();
tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
const centerX = layer.x + layer.width / 2;
const centerY = layer.y + layer.height / 2;
tempCtx.translate(centerX, centerY);
tempCtx.rotate(layer.rotation * Math.PI / 180);
tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
tempCtx.restore();
});
tempCanvas.toBlob((blob) => { tempCanvas.toBlob((blob) => {
resolve(blob); resolve(blob);
}, 'image/png'); }, 'image/png');
@@ -813,20 +770,7 @@ export class CanvasLayers {
if (!tempCtx) if (!tempCtx)
throw new Error("Could not create canvas context"); throw new Error("Could not create canvas context");
tempCtx.translate(-minX, -minY); tempCtx.translate(-minX, -minY);
const sortedSelection = [...this.canvas.canvasSelection.selectedLayers].sort((a, b) => a.zIndex - b.zIndex); this._drawLayers(tempCtx, this.canvas.canvasSelection.selectedLayers);
sortedSelection.forEach((layer) => {
if (!layer.image)
return;
tempCtx.save();
tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
const centerX = layer.x + layer.width / 2;
const centerY = layer.y + layer.height / 2;
tempCtx.translate(centerX, centerY);
tempCtx.rotate(layer.rotation * Math.PI / 180);
tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
tempCtx.restore();
});
const fusedImage = new Image(); const fusedImage = new Image();
fusedImage.src = tempCanvas.toDataURL(); fusedImage.src = tempCanvas.toDataURL();
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
@@ -866,7 +810,7 @@ export class CanvasLayers {
this.canvas.canvasLayersPanel.onLayersChanged(); this.canvas.canvasLayersPanel.onLayersChanged();
} }
log.info("Layers fused successfully", { log.info("Layers fused successfully", {
originalLayerCount: sortedSelection.length, originalLayerCount: this.canvas.canvasSelection.selectedLayers.length,
fusedDimensions: { width: fusedWidth, height: fusedHeight }, fusedDimensions: { width: fusedWidth, height: fusedHeight },
fusedPosition: { x: minX, y: minY } fusedPosition: { x: minX, y: minY }
}); });

View File

@@ -58,6 +58,11 @@ export class CanvasRenderer {
const centerY = layer.y + layer.height / 2; const centerY = layer.y + layer.height / 2;
ctx.translate(centerX, centerY); ctx.translate(centerX, centerY);
ctx.rotate(layer.rotation * Math.PI / 180); ctx.rotate(layer.rotation * Math.PI / 180);
const scaleH = layer.flipH ? -1 : 1;
const scaleV = layer.flipV ? -1 : 1;
if (layer.flipH || layer.flipV) {
ctx.scale(scaleH, scaleV);
}
ctx.imageSmoothingEnabled = true; ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high'; ctx.imageSmoothingQuality = 'high';
ctx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height); ctx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);

View File

@@ -259,13 +259,15 @@ export class CanvasState {
const newLayer = { ...layer, imageId: layer.imageId || '' }; const newLayer = { ...layer, imageId: layer.imageId || '' };
delete newLayer.image; delete newLayer.image;
if (layer.image instanceof HTMLImageElement) { if (layer.image instanceof HTMLImageElement) {
log.debug(`Layer ${index}: Using imageId instead of serializing image.`); if (layer.imageId) {
if (!layer.imageId) { newLayer.imageId = layer.imageId;
}
else {
log.debug(`Layer ${index}: No imageId found, generating new one and saving image.`);
newLayer.imageId = generateUUID(); newLayer.imageId = generateUUID();
const imageBitmap = await createImageBitmap(layer.image); const imageBitmap = await createImageBitmap(layer.image);
await saveImage(newLayer.imageId, imageBitmap); await saveImage(newLayer.imageId, imageBitmap);
} }
newLayer.imageId = layer.imageId;
} }
else if (!layer.imageId) { else if (!layer.imageId) {
log.error(`Layer ${index}: No image or imageId found, skipping layer.`); log.error(`Layer ${index}: No image or imageId found, skipping layer.`);

View File

@@ -330,7 +330,7 @@ async function createCanvasWidget(node, widget, app) {
const mattedImage = new Image(); const mattedImage = new Image();
mattedImage.src = result.matted_image; mattedImage.src = result.matted_image;
await mattedImage.decode(); await mattedImage.decode();
const newLayer = { ...selectedLayer, image: mattedImage }; const newLayer = { ...selectedLayer, image: mattedImage, flipH: false, flipV: false };
delete newLayer.imageId; delete newLayer.imageId;
canvas.layers[selectedLayerIndex] = newLayer; canvas.layers[selectedLayerIndex] = newLayer;
canvas.canvasSelection.updateSelection([newLayer]); canvas.canvasSelection.updateSelection([newLayer]);
@@ -667,6 +667,10 @@ async function createCanvasWidget(node, widget, app) {
node.setDirtyCanvas(true, true); node.setDirtyCanvas(true, true);
} }
}; };
// Inicjalizuj stan preview na podstawie aktualnej wartości widget'u
if (canvas && canvas.setPreviewVisibility) {
canvas.setPreviewVisibility(showPreviewWidget.value);
}
} }
return { return {
canvas: canvas, canvas: canvas,

View File

@@ -111,7 +111,9 @@ export function getStateSignature(layers) {
rotation: Math.round((layer.rotation || 0) * 100) / 100, rotation: Math.round((layer.rotation || 0) * 100) / 100,
zIndex: layer.zIndex, zIndex: layer.zIndex,
blendMode: layer.blendMode || 'normal', blendMode: layer.blendMode || 'normal',
opacity: layer.opacity !== undefined ? Math.round(layer.opacity * 100) / 100 : 1 opacity: layer.opacity !== undefined ? Math.round(layer.opacity * 100) / 100 : 1,
flipH: !!layer.flipH,
flipV: !!layer.flipV
}; };
if (layer.imageId) { if (layer.imageId) {
sig.imageId = layer.imageId; sig.imageId = layer.imageId;

View File

@@ -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.7" version = "1.3.8"
license = {file = "LICENSE"} license = {file = "LICENSE"}
dependencies = ["torch", "torchvision", "transformers", "aiohttp", "numpy", "tqdm", "Pillow"] dependencies = ["torch", "torchvision", "transformers", "aiohttp", "numpy", "tqdm", "Pillow"]

View File

@@ -132,7 +132,7 @@ export class Canvas {
viewport: this.viewport viewport: this.viewport
}); });
this.setPreviewVisibility(false); this.previewVisible = false;
} }
@@ -212,7 +212,7 @@ export class Canvas {
// Stwórz opóźnioną wersję funkcji zapisu stanu // Stwórz opóźnioną wersję funkcji zapisu stanu
this.requestSaveState = debounce(() => this.saveState(), 500); this.requestSaveState = debounce(() => this.saveState(), 500);
this._addAutoRefreshToggle(); this._setupAutoRefreshHandlers();
log.debug('Canvas modules initialized successfully'); log.debug('Canvas modules initialized successfully');
} }
@@ -411,12 +411,17 @@ export class Canvas {
return this.canvasIO.importLatestImage(); return this.canvasIO.importLatestImage();
} }
_addAutoRefreshToggle() { _setupAutoRefreshHandlers() {
let autoRefreshEnabled = false;
let lastExecutionStartTime = 0; let lastExecutionStartTime = 0;
// Helper function to get auto-refresh value from node widget
const getAutoRefreshValue = (): boolean => {
const widget = this.node.widgets.find((w: any) => w.name === 'auto_refresh_after_generation');
return widget ? widget.value : false;
};
const handleExecutionStart = () => { const handleExecutionStart = () => {
if (autoRefreshEnabled) { if (getAutoRefreshValue()) {
lastExecutionStartTime = Date.now(); lastExecutionStartTime = Date.now();
// Store a snapshot of the context for the upcoming batch // Store a snapshot of the context for the upcoming batch
this.pendingBatchContext = { this.pendingBatchContext = {
@@ -439,7 +444,7 @@ export class Canvas {
}; };
const handleExecutionSuccess = async () => { const handleExecutionSuccess = async () => {
if (autoRefreshEnabled) { if (getAutoRefreshValue()) {
log.info('Auto-refresh triggered, importing latest images.'); log.info('Auto-refresh triggered, importing latest images.');
if (!this.pendingBatchContext) { if (!this.pendingBatchContext) {
@@ -470,18 +475,6 @@ export class Canvas {
} }
}; };
this.node.addWidget(
'toggle',
'Auto-refresh after generation',
false,
(value: boolean) => {
autoRefreshEnabled = value;
log.debug('Auto-refresh toggled:', value);
}, {
serialize: false
}
);
api.addEventListener('execution_start', handleExecutionStart); api.addEventListener('execution_start', handleExecutionStart);
api.addEventListener('execution_success', handleExecutionSuccess); api.addEventListener('execution_success', handleExecutionSuccess);
@@ -490,6 +483,8 @@ export class Canvas {
api.removeEventListener('execution_start', handleExecutionStart); api.removeEventListener('execution_start', handleExecutionStart);
api.removeEventListener('execution_success', handleExecutionSuccess); api.removeEventListener('execution_success', handleExecutionSuccess);
}); });
log.debug('Auto-refresh handlers setup complete, reading from node widget: auto_refresh_after_generation');
} }

View File

@@ -72,27 +72,11 @@ export class CanvasIO {
maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height); maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
log.debug(`Canvas contexts created, starting layer rendering`); log.debug(`Canvas contexts created, starting layer rendering`);
const sortedLayers = this.canvas.layers.sort((a: Layer, b: Layer) => a.zIndex - b.zIndex);
log.debug(`Processing ${sortedLayers.length} layers in order`); this.canvas.canvasLayers.drawLayersToContext(tempCtx, this.canvas.layers);
sortedLayers.forEach((layer: Layer, index: number) => { this.canvas.canvasLayers.drawLayersToContext(visibilityCtx, this.canvas.layers);
log.debug(`Processing layer ${index}: zIndex=${layer.zIndex}, size=${layer.width}x${layer.height}, pos=(${layer.x},${layer.y})`);
log.debug(`Layer ${index}: blendMode=${layer.blendMode || 'normal'}, opacity=${layer.opacity !== undefined ? layer.opacity : 1}`); log.debug(`Finished rendering layers`);
tempCtx.save();
tempCtx.globalCompositeOperation = layer.blendMode as any || 'normal';
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
tempCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2);
tempCtx.rotate(layer.rotation * Math.PI / 180);
tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
tempCtx.restore();
log.debug(`Layer ${index} rendered successfully`);
visibilityCtx.save();
visibilityCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2);
visibilityCtx.rotate(layer.rotation * Math.PI / 180);
visibilityCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
visibilityCtx.restore();
});
const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height); const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
const maskData = maskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height); const maskData = maskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
for (let i = 0; i < visibilityData.data.length; i += 4) { for (let i = 0; i < visibilityData.data.length; i += 4) {
@@ -259,23 +243,8 @@ export class CanvasIO {
maskCtx.fillStyle = '#ffffff'; // Start with a white mask (nothing masked) maskCtx.fillStyle = '#ffffff'; // Start with a white mask (nothing masked)
maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height); maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
const sortedLayers = this.canvas.layers.sort((a: Layer, b: Layer) => a.zIndex - b.zIndex); this.canvas.canvasLayers.drawLayersToContext(tempCtx, this.canvas.layers);
sortedLayers.forEach((layer: Layer) => { this.canvas.canvasLayers.drawLayersToContext(visibilityCtx, this.canvas.layers);
tempCtx.save();
tempCtx.globalCompositeOperation = layer.blendMode as any || 'normal';
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
tempCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2);
tempCtx.rotate(layer.rotation * Math.PI / 180);
tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
tempCtx.restore();
visibilityCtx.save();
visibilityCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2);
visibilityCtx.rotate(layer.rotation * Math.PI / 180);
visibilityCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
visibilityCtx.restore();
});
const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height); const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
const maskData = maskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height); const maskData = maskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);

View File

@@ -349,60 +349,60 @@ export class CanvasLayers {
return null; return null;
} }
private _drawLayer(ctx: CanvasRenderingContext2D, layer: Layer, options: { offsetX?: number, offsetY?: number } = {}): void {
if (!layer.image) return;
const { offsetX = 0, offsetY = 0 } = options;
ctx.save();
ctx.globalCompositeOperation = layer.blendMode as any || 'normal';
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
const centerX = layer.x + layer.width / 2 - offsetX;
const centerY = layer.y + layer.height / 2 - offsetY;
ctx.translate(centerX, centerY);
ctx.rotate(layer.rotation * Math.PI / 180);
const scaleH = layer.flipH ? -1 : 1;
const scaleV = layer.flipV ? -1 : 1;
if (layer.flipH || layer.flipV) {
ctx.scale(scaleH, scaleV);
}
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(
layer.image,
-layer.width / 2, -layer.height / 2,
layer.width, layer.height
);
ctx.restore();
}
private _drawLayers(ctx: CanvasRenderingContext2D, layers: Layer[], options: { offsetX?: number, offsetY?: number } = {}): void {
const sortedLayers = [...layers].sort((a: Layer, b: Layer) => a.zIndex - b.zIndex);
sortedLayers.forEach(layer => this._drawLayer(ctx, layer, options));
}
public drawLayersToContext(ctx: CanvasRenderingContext2D, layers: Layer[], options: { offsetX?: number, offsetY?: number } = {}): void {
this._drawLayers(ctx, layers, options);
}
async mirrorHorizontal(): Promise<void> { async mirrorHorizontal(): Promise<void> {
if (this.canvas.canvasSelection.selectedLayers.length === 0) return; if (this.canvas.canvasSelection.selectedLayers.length === 0) return;
this.canvas.canvasSelection.selectedLayers.forEach((layer: Layer) => {
const promises = this.canvas.canvasSelection.selectedLayers.map((layer: Layer) => { layer.flipH = !layer.flipH;
return new Promise<void>(resolve => {
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
if (!tempCtx) return;
tempCanvas.width = layer.image.width;
tempCanvas.height = layer.image.height;
tempCtx.translate(tempCanvas.width, 0);
tempCtx.scale(-1, 1);
tempCtx.drawImage(layer.image, 0, 0);
const newImage = new Image();
newImage.onload = () => {
layer.image = newImage;
resolve();
};
newImage.src = tempCanvas.toDataURL();
});
}); });
await Promise.all(promises);
this.canvas.render(); this.canvas.render();
this.canvas.requestSaveState(); this.canvas.requestSaveState();
} }
async mirrorVertical(): Promise<void> { async mirrorVertical(): Promise<void> {
if (this.canvas.canvasSelection.selectedLayers.length === 0) return; if (this.canvas.canvasSelection.selectedLayers.length === 0) return;
this.canvas.canvasSelection.selectedLayers.forEach((layer: Layer) => {
const promises = this.canvas.canvasSelection.selectedLayers.map((layer: Layer) => { layer.flipV = !layer.flipV;
return new Promise<void>(resolve => {
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
if (!tempCtx) return;
tempCanvas.width = layer.image.width;
tempCanvas.height = layer.image.height;
tempCtx.translate(0, tempCanvas.height);
tempCtx.scale(1, -1);
tempCtx.drawImage(layer.image, 0, 0);
const newImage = new Image();
newImage.onload = () => {
layer.image = newImage;
resolve();
};
newImage.src = tempCanvas.toDataURL();
});
}); });
await Promise.all(promises);
this.canvas.render(); this.canvas.render();
this.canvas.requestSaveState(); this.canvas.requestSaveState();
} }
@@ -416,19 +416,15 @@ export class CanvasLayers {
tempCanvas.width = layer.width; tempCanvas.width = layer.width;
tempCanvas.height = layer.height; tempCanvas.height = layer.height;
tempCtx.clearRect(0, 0, tempCanvas.width, tempCanvas.height); // We need to draw the layer relative to the new canvas, so we "move" it to 0,0
// by creating a temporary layer object for drawing.
const layerToDraw = {
...layer,
x: 0,
y: 0,
};
tempCtx.save(); this._drawLayer(tempCtx, layerToDraw);
tempCtx.translate(layer.width / 2, layer.height / 2);
tempCtx.rotate(layer.rotation * Math.PI / 180);
tempCtx.drawImage(
layer.image,
-layer.width / 2,
-layer.height / 2,
layer.width,
layer.height
);
tempCtx.restore();
const dataUrl = tempCanvas.toDataURL('image/png'); const dataUrl = tempCanvas.toDataURL('image/png');
if (!dataUrl.startsWith('data:image/png;base64,')) { if (!dataUrl.startsWith('data:image/png;base64,')) {
@@ -697,27 +693,7 @@ export class CanvasLayers {
return; return;
} }
const sortedLayers = [...this.canvas.layers].sort((a: Layer, b: Layer) => a.zIndex - b.zIndex); this._drawLayers(tempCtx, this.canvas.layers);
sortedLayers.forEach((layer: Layer) => {
if (!layer.image) return;
tempCtx.save();
tempCtx.globalCompositeOperation = layer.blendMode as any || 'normal';
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
const centerX = layer.x + layer.width / 2;
const centerY = layer.y + layer.height / 2;
tempCtx.translate(centerX, centerY);
tempCtx.rotate(layer.rotation * Math.PI / 180);
tempCtx.drawImage(
layer.image,
-layer.width / 2,
-layer.height / 2,
layer.width,
layer.height
);
tempCtx.restore();
});
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height); const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
const data = imageData.data; const data = imageData.data;
@@ -794,27 +770,7 @@ export class CanvasLayers {
return; return;
} }
const sortedLayers = [...this.canvas.layers].sort((a: Layer, b: Layer) => a.zIndex - b.zIndex); this._drawLayers(tempCtx, this.canvas.layers);
sortedLayers.forEach((layer: Layer) => {
if (!layer.image) return;
tempCtx.save();
tempCtx.globalCompositeOperation = layer.blendMode as any || 'normal';
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
const centerX = layer.x + layer.width / 2;
const centerY = layer.y + layer.height / 2;
tempCtx.translate(centerX, centerY);
tempCtx.rotate(layer.rotation * Math.PI / 180);
tempCtx.drawImage(
layer.image,
-layer.width / 2,
-layer.height / 2,
layer.width,
layer.height
);
tempCtx.restore();
});
tempCanvas.toBlob((blob) => { tempCanvas.toBlob((blob) => {
if (blob) { if (blob) {
@@ -883,26 +839,7 @@ export class CanvasLayers {
tempCtx.translate(-minX, -minY); tempCtx.translate(-minX, -minY);
const sortedSelection = [...this.canvas.canvasSelection.selectedLayers].sort((a: Layer, b: Layer) => a.zIndex - b.zIndex); this._drawLayers(tempCtx, this.canvas.canvasSelection.selectedLayers);
sortedSelection.forEach((layer: Layer) => {
if (!layer.image) return;
tempCtx.save();
tempCtx.globalCompositeOperation = layer.blendMode as any || 'normal';
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
const centerX = layer.x + layer.width / 2;
const centerY = layer.y + layer.height / 2;
tempCtx.translate(centerX, centerY);
tempCtx.rotate(layer.rotation * Math.PI / 180);
tempCtx.drawImage(
layer.image,
-layer.width / 2, -layer.height / 2,
layer.width, layer.height
);
tempCtx.restore();
});
tempCanvas.toBlob((blob) => { tempCanvas.toBlob((blob) => {
resolve(blob); resolve(blob);
@@ -966,26 +903,7 @@ export class CanvasLayers {
tempCtx.translate(-minX, -minY); tempCtx.translate(-minX, -minY);
const sortedSelection = [...this.canvas.canvasSelection.selectedLayers].sort((a: Layer, b: Layer) => a.zIndex - b.zIndex); this._drawLayers(tempCtx, this.canvas.canvasSelection.selectedLayers);
sortedSelection.forEach((layer: Layer) => {
if (!layer.image) return;
tempCtx.save();
tempCtx.globalCompositeOperation = layer.blendMode as any || 'normal';
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
const centerX = layer.x + layer.width / 2;
const centerY = layer.y + layer.height / 2;
tempCtx.translate(centerX, centerY);
tempCtx.rotate(layer.rotation * Math.PI / 180);
tempCtx.drawImage(
layer.image,
-layer.width / 2, -layer.height / 2,
layer.width, layer.height
);
tempCtx.restore();
});
const fusedImage = new Image(); const fusedImage = new Image();
fusedImage.src = tempCanvas.toDataURL(); fusedImage.src = tempCanvas.toDataURL();
@@ -1032,7 +950,7 @@ export class CanvasLayers {
} }
log.info("Layers fused successfully", { log.info("Layers fused successfully", {
originalLayerCount: sortedSelection.length, originalLayerCount: this.canvas.canvasSelection.selectedLayers.length,
fusedDimensions: { width: fusedWidth, height: fusedHeight }, fusedDimensions: { width: fusedWidth, height: fusedHeight },
fusedPosition: { x: minX, y: minY } fusedPosition: { x: minX, y: minY }
}); });

View File

@@ -71,6 +71,13 @@ export class CanvasRenderer {
const centerY = layer.y + layer.height / 2; const centerY = layer.y + layer.height / 2;
ctx.translate(centerX, centerY); ctx.translate(centerX, centerY);
ctx.rotate(layer.rotation * Math.PI / 180); ctx.rotate(layer.rotation * Math.PI / 180);
const scaleH = layer.flipH ? -1 : 1;
const scaleV = layer.flipV ? -1 : 1;
if (layer.flipH || layer.flipV) {
ctx.scale(scaleH, scaleV);
}
ctx.imageSmoothingEnabled = true; ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high'; ctx.imageSmoothingQuality = 'high';
ctx.drawImage( ctx.drawImage(

View File

@@ -297,13 +297,14 @@ export class CanvasState {
delete (newLayer as any).image; delete (newLayer as any).image;
if (layer.image instanceof HTMLImageElement) { if (layer.image instanceof HTMLImageElement) {
log.debug(`Layer ${index}: Using imageId instead of serializing image.`); if (layer.imageId) {
if (!layer.imageId) { newLayer.imageId = layer.imageId;
} else {
log.debug(`Layer ${index}: No imageId found, generating new one and saving image.`);
newLayer.imageId = generateUUID(); newLayer.imageId = generateUUID();
const imageBitmap = await createImageBitmap(layer.image); const imageBitmap = await createImageBitmap(layer.image);
await saveImage(newLayer.imageId, imageBitmap); await saveImage(newLayer.imageId, imageBitmap);
} }
newLayer.imageId = layer.imageId;
} else if (!layer.imageId) { } else if (!layer.imageId) {
log.error(`Layer ${index}: No image or imageId found, skipping layer.`); log.error(`Layer ${index}: No image or imageId found, skipping layer.`);
return null; return null;

View File

@@ -363,7 +363,7 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
const mattedImage = new Image(); const mattedImage = new Image();
mattedImage.src = result.matted_image; mattedImage.src = result.matted_image;
await mattedImage.decode(); await mattedImage.decode();
const newLayer = {...selectedLayer, image: mattedImage} as Layer; const newLayer = {...selectedLayer, image: mattedImage, flipH: false, flipV: false} as Layer;
delete (newLayer as any).imageId; delete (newLayer as any).imageId;
canvas.layers[selectedLayerIndex] = newLayer; canvas.layers[selectedLayerIndex] = newLayer;
canvas.canvasSelection.updateSelection([newLayer]); canvas.canvasSelection.updateSelection([newLayer]);
@@ -730,6 +730,11 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
node.setDirtyCanvas(true, true); node.setDirtyCanvas(true, true);
} }
}; };
// Inicjalizuj stan preview na podstawie aktualnej wartości widget'u
if (canvas && canvas.setPreviewVisibility) {
canvas.setPreviewVisibility(showPreviewWidget.value);
}
} }
return { return {

View File

@@ -17,6 +17,8 @@ export interface Layer {
blendMode: string; blendMode: string;
opacity: number; opacity: number;
mask?: Float32Array; mask?: Float32Array;
flipH?: boolean;
flipV?: boolean;
} }
export interface ComfyNode { export interface ComfyNode {

View File

@@ -136,7 +136,9 @@ export function getStateSignature(layers: Layer[]): string {
rotation: Math.round((layer.rotation || 0) * 100) / 100, rotation: Math.round((layer.rotation || 0) * 100) / 100,
zIndex: layer.zIndex, zIndex: layer.zIndex,
blendMode: layer.blendMode || 'normal', blendMode: layer.blendMode || 'normal',
opacity: layer.opacity !== undefined ? Math.round(layer.opacity * 100) / 100 : 1 opacity: layer.opacity !== undefined ? Math.round(layer.opacity * 100) / 100 : 1,
flipH: !!layer.flipH,
flipV: !!layer.flipV
}; };
if (layer.imageId) { if (layer.imageId) {