PainterNode update

+ Added a path in `painter_asset_storage.py` to retrieve saved assets for management.
+ Refactored the saved data manager.
+ Added the ability to insert saved canvases into the current workflow in PainterNode.
+ Added a button to permanently delete saved canvas data.
+ Added new style classes and removed unused ones.
+ Cleaned up the code.
+ Updated the version in `pyproject.toml`.
This commit is contained in:
AlekPet
2026-09-27 15:42:01 +03:00
parent 8502b20091
commit f037c4a4e9
7 changed files with 382 additions and 585 deletions
+44 -5
View File
@@ -7,6 +7,7 @@ Workflow files contain only a small asset reference. The Fabric.js canvas JSON
import json
import os
import re
import shutil
import tempfile
from aiohttp import web
@@ -42,11 +43,51 @@ def _json_response_error(message, status=400):
return web.json_response({"success": False, "error": message}, status=status)
@PromptServer.instance.routes.get("/alekpet/painter_assets")
async def list_painter_assets(request):
try:
root = _asset_root()
assets = []
for asset_id in os.listdir(root):
if not ASSET_ID_RE.fullmatch(asset_id):
continue
asset_dir = os.path.join(root, asset_id)
state_path = os.path.join(asset_dir, "state.json")
if not os.path.isfile(state_path):
continue
stat = os.stat(state_path)
width = height = None
try:
with open(state_path, "r", encoding="utf-8") as f:
state = json.load(f)
size = state.get("settings", {}).get("currentCanvasSize", {})
width = size.get("width")
height = size.get("height")
except (OSError, json.JSONDecodeError, AttributeError):
pass
assets.append(
{
"asset_id": asset_id,
"size": stat.st_size,
"updated_at": stat.st_mtime,
"width": width,
"height": height,
}
)
assets.sort(key=lambda item: item["updated_at"], reverse=True)
return web.json_response({"success": True, "assets": assets})
except OSError as e:
return _json_response_error(str(e), 500)
@PromptServer.instance.routes.get("/alekpet/painter_asset/{asset_id}")
async def get_painter_asset(request):
try:
asset_dir = _asset_dir(request.match_info["asset_id"])
state_path = os.path.join(asset_dir, "state.json")
asset_id = request.match_info["asset_id"]
state_path = os.path.join(_asset_dir(asset_id), "state.json")
if not os.path.isfile(state_path):
return _json_response_error("Painter asset not found", 404)
@@ -57,7 +98,7 @@ async def get_painter_asset(request):
return web.json_response(
{
"success": True,
"asset_id": request.match_info["asset_id"],
"asset_id": asset_id,
"state": state,
}
)
@@ -116,8 +157,6 @@ async def delete_painter_asset(request):
try:
asset_dir = _asset_dir(request.match_info["asset_id"])
if os.path.isdir(asset_dir):
import shutil
shutil.rmtree(asset_dir)
return web.json_response({"success": True})
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui_custom_nodes_alekpet"
description = "Nodes: PoseNode, PainterNode, TranslateTextNode, TranslateCLIPTextEncodeNode, DeepTranslatorTextNode, DeepTranslatorCLIPTextEncodeNode, ArgosTranslateTextNode, ArgosTranslateCLIPTextEncodeNode, ChatGLM4TranslateCLIPTextEncodeNode, ChatGLM4TranslateTextNode, ChatGLM4InstructNode, ChatGLM4InstructMediaNode, ChatGLMImageGenerateNode, ChatGLMVideoGenerateNode, PreviewTextNode, HexToHueNode, ColorsCorrectNode, IDENode."
version = "1.1.11"
version = "1.2.0"
license = { file = "LICENSE" }
[project.urls]
@@ -462,79 +462,80 @@
/* -- end Styles Settigs Kistey -- */
/* Settings store managing data */
.painter_storage_box {
width: 30vw;
.alekpet_painter_storage_overlay {
position: fixed;
inset: 0;
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.55);
}
.alekpet_painter_storage_box {
width: min(900px, 90vw);
max-height: 85vh;
display: flex;
flex-direction: column;
gap: 20px;
color: var(--primary-fg);
}
.painter_storage_header {
display: flex;
justify-content: space-around;
background: var(--content-bg);
padding: 3px;
background: var(--comfy-menu-bg, #202020);
color: var(--input-text, #ddd);
border: 1px solid var(--border-color, #555);
border-radius: 8px;
align-items: center;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
}
.painter_storage_body {
.alekpet_painter_storage_header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
border-bottom: 1px solid var(--border-color, #444);
}
.alekpet_painter_storage_toolbar {
display: flex;
gap: 8px;
padding: 10px 14px;
}
.alekpet_painter_storage_items {
overflow: auto;
padding: 0 14px 14px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 10px;
}
.alekpet_painter_storage_item {
grid-column: 1/-1;
padding: 20px;
text-align: center;
}
.painter_storage_items_box {
.alekpet_painter_storage_item_card {
display: flex;
flex-direction: column;
gap: 5px;
align-items: stretch;
gap: 8px;
padding: 8px;
border: 1px solid var(--border-color, #444);
border-radius: 6px;
background: var(--comfy-input-bg, #181818);
}
.alekpet_painter_storage_item_preview_wrapper {
height: 180px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background: #000;
}
.painter_storage_workflows_item {
cursor: pointer;
}
.painter_storage_workflows_item:hover {
background: var(--primary-hover-bg) !important;
}
.painter_storage_grid {
display: grid;
grid-template-columns: 0.2fr 0.2fr repeat(3, 1fr);
justify-content: center;
place-items: center;
}
.painter_storage_items_header {
background: var(--primary-bg);
text-transform: uppercase;
}
.painter_storage_painters_item_preview_canvas {
width: 60% !important;
height: auto !important;
}
/* Workflows */
.painter_storage_painters_wrapper {
border: 1px solid var(--border-color);
background: var(--p-button-outlined-contrast-hover-background);
}
.painter_storage_painters_grid {
grid-template-columns: 0.2fr repeat(4, 1fr);
}
.painter_storage_painters_header {
background: var(--descrip-text);
}
.painter_storage_painters_item_name {
text-wrap: nowrap;
width: 90%;
.alekpet_painter_storage_item_id_text {
font-family: monospace;
font-size: 11px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* -- end Settings store managing data */
@@ -1,485 +0,0 @@
import { api } from "../../../../scripts/api.js";
import { fabric } from "./fabric.js";
import { formatBytes } from "./helpers.js";
import { ComfyDialog } from "../../../../../scripts/ui.js";
import {
makeElement,
createWindowModal,
animateClick,
THEMES_MODAL_WINDOW,
comfyuiDesktopConfirm,
} from "../../utils.js";
export class PainterStorageDialog extends ComfyDialog {
// Remove record in LocalStorage or JSON file
async removeRecord(workflow_name, painters_data = []) {
if (!(await comfyuiDesktopConfirm(`Delete record "${workflow_name}"?`)))
return;
try {
const rawResponse = await fetch("/alekpet/remove_node_settings", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: workflow_name,
painters_data: painters_data,
}),
});
if (rawResponse.status !== 200) {
throw new Error(
`Error painter remove file settings: ${rawResponse.statusText}`
);
}
const jsonData = await rawResponse.json();
return {
status: true,
remove_info: jsonData?.workflow_info,
message: jsonData.message,
};
} catch (e) {
console.log(e);
return { status: false, remove_info: null, message: e.message };
}
}
// Create menu elements
createMenuElements(_data) {
const data = _data.sort((a, b) => {
a = JSON.stringify(a.value).length;
b = JSON.stringify(b.value).length;
return b - a;
});
if (!data?.length) {
return makeElement("div", {
class: ["painter_storage_items_box"],
textContent: "Records is not found!",
});
}
const itemBox = makeElement("div", {
class: ["painter_storage_items_box"],
children: [
makeElement("div", {
class: ["painter_storage_grid", "painter_storage_items_header"],
children: [
makeElement("div", { textContent: "👓" }),
makeElement("div", { textContent: "#" }),
makeElement("div", { textContent: "Name workflow" }),
makeElement("div", { textContent: "Size" }),
makeElement("div", { textContent: "Action" }),
],
}),
],
});
// Check if body item is empty, show message no records
async function checkNoItems() {
if (
document.querySelector(".painter_storage_body").children.length === 1
) {
const data = await this.loadingDataJSON();
if (data) this.updateBodyData(data);
}
}
// Sort workflows for size
function sortWorkflowSize(paint_data) {
return Object.keys(paint_data.painters_data)
.map((w) => ({
painter_name: w,
painter_data: paint_data.painters_data[w],
}))
.sort((a, b) => {
a = JSON.stringify(a.painter_data).length;
b = JSON.stringify(b.painter_data).length;
return b - a;
});
}
// Create painters elements
function createItemsPainters(workflow_name, workflow_data) {
// Wrapper
const itemPaintersWrapper = makeElement("div", {
class: ["painter_storage_painters_wrapper"],
style: { display: "none", opacity: 0, transition: "all 0.4s" },
});
// Header
const itemPainterHeader = makeElement("div", {
class: [
"painter_storage_grid",
"painter_storage_painters_header",
"painter_storage_painters_grid",
],
children: [
makeElement("div", { textContent: "#" }),
makeElement("div", { textContent: "Painter name" }),
makeElement("div", { textContent: "Preview" }),
makeElement("div", { textContent: "Size" }),
makeElement("div", { textContent: "Action" }),
],
parent: itemPaintersWrapper,
});
for (const [idx, { painter_name, painter_data }] of sortWorkflowSize(
workflow_data
).entries()) {
const itemPainter = makeElement("div", {
class: [
"painter_storage_grid",
"painter_storage_painters_item",
"painter_storage_painters_grid",
],
});
const numPainter = makeElement("div", { textContent: idx + 1 });
const namePainter = makeElement("div", {
class: ["painter_storage_painters_item_name"],
textContent: painter_name,
title: painter_name,
});
const previewPainter = makeElement("div", {
class: ["painter_storage_painters_item_preview"],
});
// Create canvas from preview
const canvasEl = makeElement("canvas", {
class: ["painter_storage_painters_item_preview_canvas"],
});
previewPainter.append(canvasEl);
// Fabric canvas load
const fcanvas = new fabric.StaticCanvas(canvasEl);
const canvasValue =
typeof painter_data === "string"
? JSON.parse(painter_data)
: painter_data;
if (canvasValue?.settings?.currentCanvasSize) {
const { width, height } = canvasValue.settings.currentCanvasSize;
fcanvas.setDimensions({ width, height }, { backstoreOnly: true });
}
fcanvas.loadFromJSON(canvasValue.canvas_settings, () => {
fcanvas.renderAll();
});
const sizePainter = makeElement("div", {
innerHTML: formatBytes(new Blob([JSON.stringify(painter_data)]).size),
});
const removeButtonPainter = makeElement("button", {
style: {
color: "var(--error-text)",
padding: "7px",
fontSize: "1rem",
},
textContent: "Delete",
title: "Delete painter!",
onclick: async (e) => {
e.stopPropagation();
if (!confirm(`Delete painter record "${painter_name}"?`)) return;
const result = await this.removeRecord(workflow_name, [
painter_name,
]);
if (!result.status) {
createWindowModal({
...THEMES_MODAL_WINDOW.error,
stylesBox: {
...THEMES_MODAL_WINDOW.error.stylesBox,
lineHeight: 1.5,
},
textTitle: "Error",
textBody: result.message,
options: {
parent: document.body,
overlay: { overlay_enabled: true },
auto: {
autoshow: true,
autoremove: true,
autohide: true,
timewait: 500,
},
close: { showClose: false },
},
});
return;
}
let textMessage = `<div>Painter name <span style="color: chartreuse; font-weight: 600;">"${painter_name}"</span> deleted from json file workflow <span style="color: darkseagreen; font-weight: 600;">"${workflow_name}"</span>!</div>`;
if (
result.remove_info.workflow_record &&
result.remove_info.paint_record
) {
textMessage = `<div>Workflow name <span style="color: chartreuse; font-weight: 600;">"${workflow_name}"</span> and record <span style="color: darkseagreen; font-weight: 600;">"${painter_name}"</span> deleted from json file!</div>`;
}
createWindowModal({
...THEMES_MODAL_WINDOW.normal,
stylesBox: {
...THEMES_MODAL_WINDOW.normal.stylesBox,
lineHeight: 1.5,
},
textTitle: "Information",
textBody: textMessage,
options: {
parent: document.body,
overlay: { overlay_enabled: true },
auto: {
autoshow: true,
autoremove: true,
autohide: true,
timewait: 500,
},
close: { showClose: false },
},
});
if (
result.remove_info.workflow_record &&
result.remove_info.paint_record
) {
itemPainter.closest(".painter_storage_painters_wrapper").remove();
checkNoItems.call(this);
return;
}
itemPainter.remove();
},
});
itemPainter.append(
numPainter,
namePainter,
previewPainter,
sizePainter,
removeButtonPainter
);
itemPaintersWrapper.append(itemPainter);
}
return itemPaintersWrapper;
}
// Create workflows elements
function createWorkflowStorage(idx, workflow_name, workflow_value) {
const itemWorkflow = makeElement("div", {
class: ["painter_storage_grid", "painter_storage_workflows_item"],
title: `Show painters records workflow ${workflow_name}`,
onclick: (e) => {
const target = e.currentTarget;
animateClick(target.nextElementSibling).then((res) => {
target.children[0].textContent = res ? "▼" : "►";
target.title = `Hide painters records workflow ${workflow_name}`;
});
},
});
const showWorkflow = makeElement("div", { textContent: "►" });
const numEl = makeElement("div", { textContent: idx + 1 });
const nameEl = makeElement("div", { textContent: workflow_name });
const sizeEl = makeElement("div", {
innerHTML: formatBytes(new Blob([JSON.stringify(workflow_value)]).size),
});
const removeButtonEl = makeElement("button", {
style: { color: "var(--error-text)", padding: "7px", fontSize: "1rem" },
textContent: "Delete",
title: "Delete workflow!",
onclick: async (e) => {
e.stopPropagation();
if (!confirm(`Delete workflow "${workflow_name}"?`)) return;
const result = await this.removeRecord(workflow_name);
if (!result.status) {
createWindowModal({
...THEMES_MODAL_WINDOW.error,
stylesBox: {
...THEMES_MODAL_WINDOW.error.stylesBox,
lineHeight: 1.5,
},
textTitle: "Error",
textBody: result.message,
options: {
parent: document.body,
overlay: { overlay_enabled: true },
auto: {
autoshow: true,
autoremove: true,
autohide: true,
timewait: 500,
},
close: { showClose: false },
},
});
return;
}
createWindowModal({
...THEMES_MODAL_WINDOW.normal,
stylesBox: {
...THEMES_MODAL_WINDOW.normal.stylesBox,
lineHeight: 1.5,
},
textTitle: "Information",
textBody: `<div>Workflow name <span style="color: chartreuse; font-weight: 600;">"${workflow_name}"</span> deleted from json files!</div>`,
options: {
parent: document.body,
overlay: { overlay_enabled: true },
auto: {
autoshow: true,
autoremove: true,
autohide: true,
timewait: 500,
},
close: { showClose: false },
},
});
itemWorkflow.closest(".painter_storage_painters_wrapper").remove();
checkNoItems.call(this);
},
});
itemWorkflow.append(showWorkflow, numEl, nameEl, sizeEl, removeButtonEl);
return itemWorkflow;
}
for (const [idx, { name, value }] of data.entries()) {
const itemWrapper = makeElement("div", {
class: ["painter_storage_painters_wrapper"],
style: {
background: `var(${
(idx + 1) % 2 !== 0 ? "--tr-odd-bg-color" : "--tr-even-bg-color"
})`,
},
});
const itemPaint = createWorkflowStorage.call(this, idx, name, value);
const itemWorkflows = createItemsPainters.call(this, name, value);
itemWrapper.append(itemPaint, itemWorkflows);
itemBox.append(itemWrapper);
}
return itemBox;
}
// Get data form JSON's files
async loadingDataJSON() {
try {
const rawResponse = await api.fetchApi(
"/alekpet/loading_all_node_settings"
);
if (rawResponse.status !== 200)
throw new Error(
`Error painter get json data settings: ${rawResponse.statusText}`
);
const data = await rawResponse?.json();
if (!data?.all_settings_nodes) return {};
return data.all_settings_nodes;
} catch (e) {
console.log(e);
return {};
}
}
updateBodyData(data) {
this.body.innerHTML = "";
this.body.append(this.createMenuElements(data));
}
// Get data form LocalStorage
loadingDataLocalStorage() {
return Object.keys(localStorage)
.filter((v) => v.indexOf("Painter_") !== -1)
.map((v) => ({ name: v, value: JSON.parse(localStorage.getItem(v)) }));
}
async updateDataStorages() {
try {
this.body.innerHTML = "";
const storagePaintersData = await this.loadingDataJSON();
console.log("Loading JSON files data");
this.body.append(this.createMenuElements(storagePaintersData));
return true;
} catch (err) {
return err;
}
}
// Main function show
async show() {
this.body = makeElement("div", {
class: ["painter_storage_body"],
});
const box = makeElement("div", {
class: ["painter_storage_box"],
children: [
makeElement("div", {
class: ["painter_storage_header"],
children: [
makeElement("div", {
innerHTML: "<b>JSON</b> storage data",
style: { flexBasis: "100%", textAlign: "center" },
}),
makeElement("button", {
textContent: "Refresh",
style: { color: "limegreen", fontSize: "1rem", padding: "7px" },
onclick: async (e) => {
const target = e.target;
target.style.color = "var(--descrip-text)";
target.disabled = true;
target.textContent = "Wait...";
await new Promise((res, rej) => {
const result = this.updateDataStorages();
result ? res(true) : rej(result);
})
.then(() => {
setTimeout(() => {
target.style.color = "limegreen";
target.disabled = false;
target.textContent = "Refresh";
}, 500);
})
.catch((e) => {
console.log(e);
});
},
}),
],
}),
this.body,
],
});
await this.updateDataStorages();
super.show(box);
this.element.style.zIndex = 9999;
}
close() {
this.element.remove();
}
}
+22 -19
View File
@@ -1,13 +1,17 @@
import { api } from "../../../../scripts/api.js";
import { app } from "../../../../scripts/app.js";
import { fabric } from "./fabric.js";
import {
makeElement,
createWindowModal,
isEmptyObject,
THEMES_MODAL_WINDOW,
comfyuiDesktopConfirm,
} from "../../utils.js";
function createPainterAssetId() {
if (globalThis.crypto?.randomUUID) {
return globalThis.crypto.randomUUID();
}
// Fallback for older embedded browsers.
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
// RGB, HSV, and HSL color conversion algorithms in JavaScript https://gist.github.com/mjackson/5311256
function rgbToHsv(r, g, b) {
@@ -96,16 +100,14 @@ function HsvToRgb(brush_settings) {
return { red, green, blue };
}
function formatBytes(bytes, decimals = 2) {
if (!+bytes) return "0 Bytes";
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
function formatBytes(bytes) {
if (!bytes) return "0 B";
const units = ["B", "KB", "MB", "GB"];
const i = Math.min(
Math.floor(Math.log(bytes) / Math.log(1024)),
units.length - 1
);
return `${(bytes / 1024 ** i).toFixed(i ? 1 : 0)} ${units[i]}`;
}
// LocalStorage Init
@@ -164,4 +166,5 @@ export {
HsvToRgb,
StorageClass,
formatBytes,
createPainterAssetId,
};
@@ -0,0 +1,250 @@
import { api } from "../../../../scripts/api.js";
import { app } from "../../../../scripts/app.js";
import { fabric } from "./fabric.js";
import { formatBytes, createPainterAssetId } from "./helpers.js";
const ASSET_VERSION = 2;
function assetRef(assetId) {
return { version: ASSET_VERSION, asset_id: assetId };
}
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
function waitForPainter(node, timeout = 5000) {
return new Promise((resolve) => {
const started = Date.now();
const tick = () => {
if (node.painter?.canvas) return resolve(node.painter);
if (Date.now() - started >= timeout) return resolve(null);
setTimeout(tick, 50);
};
tick();
});
}
export class PainterAssetDialog {
constructor() {
this.overlay = null;
this.items = null;
}
show() {
if (this.overlay) return;
this.overlay = document.createElement("div");
this.overlay.className = "alekpet_painter_storage_overlay";
const box = document.createElement("div");
box.className = "alekpet_painter_storage_box";
const header = document.createElement("div");
header.className = "alekpet_painter_storage_header";
header.innerHTML = `<strong>Painter Assets</strong>`;
const close = document.createElement("button");
close.textContent = "✕";
close.onclick = () => this.close();
header.appendChild(close);
const toolbar = document.createElement("div");
toolbar.className = "alekpet_painter_storage_toolbar";
const refresh = document.createElement("button");
refresh.textContent = "Refresh";
refresh.onclick = () => this.load();
toolbar.appendChild(refresh);
this.items = document.createElement("div");
this.items.className = "alekpet_painter_storage_items";
box.append(header, toolbar, this.items);
this.overlay.appendChild(box);
this.overlay.addEventListener("click", (event) => {
if (event.target === this.overlay) this.close();
});
document.body.appendChild(this.overlay);
this.load();
}
close() {
this.overlay?.remove();
this.overlay = null;
}
async load() {
if (!this.items) return;
this.items.innerHTML =
"<div class='alekpet_painter_storage_item'>Loading...</div>";
try {
const response = await api.fetchApi("/alekpet/painter_assets");
if (!response.ok)
throw new Error(`${response.status} ${response.statusText}`);
const result = await response.json();
const assets = Array.isArray(result?.assets) ? result.assets : [];
this.items.innerHTML = "";
if (!assets.length) {
this.items.innerHTML =
"<div class='alekpet_painter_storage_item'>No Painter assets</div>";
return;
}
assets.sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0));
for (const asset of assets) this.renderAsset(asset);
} catch (error) {
this.items.innerHTML = `<div class='alekpet_painter_storage_item' style='text-align:left;color:var(--error-text,#f66);'>${escapeHtml(error.message)}</div>`;
console.error("[PainterAssetDialog] Failed to load assets:", error);
}
}
renderAsset(asset) {
const card = document.createElement("div");
card.className = "alekpet_painter_storage_item_card";
const previewWrap = document.createElement("div");
previewWrap.className = "alekpet_painter_storage_item_preview_wrapper";
const canvasEl = document.createElement("canvas");
canvasEl.style.maxWidth = "100%";
canvasEl.style.maxHeight = "100%";
previewWrap.appendChild(canvasEl);
const id = document.createElement("div");
id.textContent = asset.asset_id;
id.title = asset.asset_id;
id.className = "alekpet_painter_storage_item_id_text";
const meta = document.createElement("div");
meta.textContent = `${formatBytes(asset.size)}${asset.width && asset.height ? ` · ${asset.width}×${asset.height}` : ""}`;
meta.style.fontSize = "11px";
const buttons = document.createElement("div");
Object.assign(buttons.style, { display: "flex", gap: "6px" });
const add = document.createElement("button");
add.textContent = "➕ Add to scene";
add.onclick = () => this.addToScene(asset.asset_id);
const remove = document.createElement("button");
remove.textContent = "🗑 Delete";
remove.onclick = () => this.deleteAsset(asset.asset_id, card);
buttons.append(add, remove);
card.append(previewWrap, id, meta, buttons);
this.items.appendChild(card);
this.loadPreview(asset.asset_id, canvasEl).catch((error) => {
console.warn("[PainterAssetDialog] Preview failed:", error);
});
}
async loadPreview(assetId, canvasEl) {
const response = await api.fetchApi(
`/alekpet/painter_asset/${encodeURIComponent(assetId)}`
);
if (!response.ok)
throw new Error(`${response.status} ${response.statusText}`);
const state = (await response.json())?.state;
if (!state?.canvas_settings) return;
const size = state.settings?.currentCanvasSize || {
width: 512,
height: 512,
};
const scale = Math.min(1, 170 / Math.max(size.width, size.height));
const width = Math.max(1, Math.round(size.width * scale));
const height = Math.max(1, Math.round(size.height * scale));
canvasEl.width = width;
canvasEl.height = height;
const preview = new fabric.StaticCanvas(canvasEl, {
width,
height,
backgroundColor: state.canvas_settings.background || "#000000",
});
await new Promise((resolve) => {
preview.loadFromJSON(state.canvas_settings, () => {
preview.setDimensions({ width, height });
preview.setViewportTransform([scale, 0, 0, scale, 0, 0]);
preview.renderAll();
resolve();
});
});
}
async addToScene(assetId) {
const LiteGraph = globalThis.LiteGraph;
if (!LiteGraph) {
console.error("[PainterAssetDialog] LiteGraph is unavailable");
return;
}
const node = LiteGraph.createNode("PainterNode");
if (!node) return;
const mouse = app.canvas?.graph_mouse;
const selected = app.canvas?.selected_nodes
? Object.values(app.canvas.selected_nodes)[0]
: null;
node.pos =
mouse?.length === 2
? [mouse[0], mouse[1]]
: selected?.pos
? [selected.pos[0] + 40, selected.pos[1] + 40]
: [100, 100];
app.graph.add(node);
const painter = await waitForPainter(node);
if (!painter) {
console.error(
"[PainterAssetDialog] PainterNode initialization timed out"
);
return;
}
const painterIndex = node.widgets?.findIndex?.(
(widget) => widget.type === "painter_node_alekpet"
);
if (painterIndex < 0) return;
await painter.loadCanvasData(assetRef(assetId), painterIndex, {
migrateImage: false,
});
painter.node.painterAsset.asset_id = createPainterAssetId();
await painter.persistPainterState();
app.graph.setDirtyCanvas(true, false);
}
async deleteAsset(assetId, card) {
if (!confirm(`Delete Painter asset ${assetId}?`)) return;
try {
const response = await api.fetchApi(
`/alekpet/painter_asset/${encodeURIComponent(assetId)}`,
{ method: "DELETE" }
);
if (!response.ok)
throw new Error(`${response.status} ${response.statusText}`);
card.remove();
if (!this.items.children.length) {
this.items.innerHTML =
"<div class='alekpet_painter_storage_item'>No Painter assets</div>";
}
} catch (error) {
console.error("[PainterAssetDialog] Failed to delete asset:", error);
alert(`Failed to delete Painter asset: ${error.message}`);
}
}
}
+5 -16
View File
@@ -13,8 +13,9 @@ import {
toRGBA,
getColorHEX,
StorageClass,
createPainterAssetId,
} from "./lib/painternode/helpers.js";
import { PainterStorageDialog } from "./lib/painternode/dialogs.js";
import { PainterAssetDialog } from "./lib/painternode/painter_asset_dialog.js";
import { addStylesheet } from "../../scripts/utils.js";
import {
showHide,
@@ -37,19 +38,6 @@ const extensionName = "alekpet.PainterNode";
const CLONE_CACHE = new Map();
const PAINTER_ASSET_VERSION = 2;
function createPainterAssetId() {
if (globalThis.crypto?.randomUUID) {
return globalThis.crypto.randomUUID();
}
// Fallback for older embedded browsers.
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
function isPainterAssetRef(value) {
return (
value &&
@@ -3257,12 +3245,13 @@ app.registerExtension({
makeElement("td", {
children: [
makeElement("button", {
textContent: "Managing Data",
textContent: "Painter Assets",
onclick: () => {
new PainterStorageDialog().show();
new PainterAssetDialog().show();
},
style: {
display: "block",
marginTop: "5px",
},
}),
],