mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-23 05:22:11 -03:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3027587d6 | ||
|
|
20d52b632a | ||
|
|
57bd1e1499 | ||
|
|
674879b497 | ||
|
|
98d4769ba1 | ||
|
|
5419acad27 | ||
|
|
db65c0c72e |
76
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
76
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -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:
|
||||||
|
|
||||||
🔗 **Where to find the latest versions of ComfyUI and LayerForge:**
|
### 🔍 Enable Debug Logs (for **full** logs):
|
||||||
- [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)
|
#### 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'
|
||||||
|
```
|
||||||
|
|
||||||
|
➡️ **Restart ComfyUI** after applying these changes to activate full logging.
|
||||||
|
|
||||||
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.
|
||||||
|
|||||||
36
.github/workflows/release.yml
vendored
36
.github/workflows/release.yml
vendored
@@ -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 }}
|
||||||
@@ -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:
|
||||||
|
|||||||
23
js/Canvas.js
23
js/Canvas.js
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 }
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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.`);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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`);
|
|
||||||
sortedLayers.forEach((layer: Layer, index: number) => {
|
|
||||||
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();
|
this.canvas.canvasLayers.drawLayersToContext(tempCtx, this.canvas.layers);
|
||||||
tempCtx.globalCompositeOperation = layer.blendMode as any || 'normal';
|
this.canvas.canvasLayers.drawLayersToContext(visibilityCtx, this.canvas.layers);
|
||||||
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`);
|
log.debug(`Finished rendering layers`);
|
||||||
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);
|
||||||
|
|||||||
@@ -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 }
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user