update PainterNode

+ The canvas data storage structure has been modified.
+ Data is now stored on the backend rather than within the workflow itself, preventing the workflow JSON from becoming bloated.
  Data is stored in `ComfyUI\user\__alekpet\painter_assets`, with each item assigned a unique identifier.
+ Automatic migration has been added to convert workflows using the old PainterNode format to the new data storage method.
+ Code cleanup (removal of legacy code).
This commit is contained in:
AlekPet
2026-09-27 12:57:38 +03:00
parent 1e3a7bf0e7
commit 8502b20091
4 changed files with 302 additions and 526 deletions
+127
View File
@@ -0,0 +1,127 @@
"""
Backend storage for PainterNode canvas state.
Workflow files contain only a small asset reference. The Fabric.js canvas JSON
(which may contain base64 encoded images) is stored server-side instead.
"""
import json
import os
import re
import tempfile
from aiohttp import web
from server import PromptServer
import folder_paths
ASSET_ID_RE = re.compile(
r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-"
r"[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$"
)
def _asset_root():
# Keep Painter data outside the workflow and outside the custom-node directory so updating/reinstalling the extension does not delete it.
if hasattr(folder_paths, "get_system_user_directory"):
root = folder_paths.get_system_user_directory("alekpet")
else:
root = os.path.join(folder_paths.get_user_directory(), "__alekpet")
root = os.path.join(root, "painter_assets")
os.makedirs(root, exist_ok=True)
return root
def _asset_dir(asset_id):
if not isinstance(asset_id, str) or not ASSET_ID_RE.fullmatch(asset_id):
raise ValueError("Invalid Painter asset_id")
return os.path.join(_asset_root(), asset_id)
def _json_response_error(message, status=400):
return web.json_response({"success": False, "error": message}, status=status)
@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")
if not os.path.isfile(state_path):
return _json_response_error("Painter asset not found", 404)
with open(state_path, "r", encoding="utf-8") as f:
state = json.load(f)
return web.json_response(
{
"success": True,
"asset_id": request.match_info["asset_id"],
"state": state,
}
)
except ValueError as e:
return _json_response_error(str(e), 400)
except (OSError, json.JSONDecodeError) as e:
return _json_response_error(str(e), 500)
@PromptServer.instance.routes.post("/alekpet/painter_asset/{asset_id}")
async def save_painter_asset(request):
try:
asset_id = request.match_info["asset_id"]
asset_dir = _asset_dir(asset_id)
payload = await request.json()
state = payload.get("state")
if not isinstance(state, dict):
return _json_response_error("state must be an object", 400)
os.makedirs(asset_dir, exist_ok=True)
fd, tmp_path = tempfile.mkstemp(
prefix=".state-",
suffix=".tmp",
dir=asset_dir,
)
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(
state,
f,
ensure_ascii=False,
separators=(",", ":"),
)
f.flush()
os.fsync(f.fileno())
os.replace(tmp_path, os.path.join(asset_dir, "state.json"))
finally:
if os.path.exists(tmp_path):
os.unlink(tmp_path)
return web.json_response(
{
"success": True,
"asset_id": asset_id,
}
)
except ValueError as e:
return _json_response_error(str(e), 400)
except (OSError, json.JSONDecodeError) as e:
return _json_response_error(str(e), 500)
@PromptServer.instance.routes.delete("/alekpet/painter_asset/{asset_id}")
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})
except ValueError as e:
return _json_response_error(str(e), 400)
except OSError as e:
return _json_response_error(str(e), 500)
+3 -2
View File
@@ -13,6 +13,7 @@ import glob
import folder_paths
import node_helpers
from . import painter_asset_storage
# Directory node save settings
CHUNK_SIZE = 1024
@@ -399,7 +400,7 @@ class PainterNode(object):
PromptServer.instance.send_sync(
"alekpet_get_image", {"unique_id": unique_id, "images": input_images}
)
if not wait_canvas_change(unique_id):
print(f"Painter_{unique_id}: Failed to get image!")
else:
@@ -407,7 +408,7 @@ class PainterNode(object):
# end - Piping image input
image_path = folder_paths.get_annotated_filepath(image)
img = node_helpers.pillow(Image.open, image_path)
output_images = []
@@ -108,283 +108,6 @@ function formatBytes(bytes, decimals = 2) {
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
}
// Workflow state manager class
class WorkflowStateManager {
constructor(debug) {
if (WorkflowStateManager.instance) {
return WorkflowStateManager.instance;
}
this.debug = debug;
this._workflowManager = app?.extensionManager?.workflow;
this._currentWorkflow = null;
this._previousWorkflow = null;
this.workflowsStores = [];
this.countLoaded = 0;
this.nodeStores = [];
this.newSave = false;
WorkflowStateManager.instance = this;
}
static getInstance(debug) {
if (!WorkflowStateManager.instance) {
WorkflowStateManager.instance = new WorkflowStateManager(debug);
}
return WorkflowStateManager.instance;
}
get workflowManager() {
return this._workflowManager;
}
get previousWorkflow() {
return this._previousWorkflow;
}
set previousWorkflow(new_value) {
this._previousWorkflow = new_value;
}
get currentWorkflow() {
return this._currentWorkflow;
}
set currentWorkflow(new_value) {
this._currentWorkflow = new_value;
}
// Load settings from json file
async loadData(workflowName = null) {
if (!this.currentWorkflow) {
this.debug && console.log("❗ [PainterNode] No currentWorkflow");
return;
} else {
this.debug &&
console.log(
"🆗 [PainterNode] Data loaded workflow: ",
this.currentWorkflow
);
}
try {
const rawResponse = await api.fetchApi(
`/alekpet/loading_node_settings/Painter_${
workflowName ?? this.currentWorkflow
}`
);
if (rawResponse.status !== 200)
throw new Error(
`Error painter load file settings: ${rawResponse.statusText}`
);
const data = await rawResponse?.json();
if (!data) return {};
return data.settings_nodes;
} catch (e) {
console.log(e);
return {};
}
}
// Remove settings from json file
async removeData(workflow_name, painters_data = []) {
try {
console.log(workflow_name, painters_data);
const rawResponse = await fetch("/alekpet/remove_node_settings", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: workflow_name ?? `Painter_${this.currentWorkflow}`,
painters_data: painters_data,
}),
});
if (rawResponse.status !== 200)
throw new Error(
`Error painter remove file settings: ${rawResponse.statusText}`
);
const jsonData = await rawResponse?.json();
console.log(jsonData.message);
} catch (e) {
console.log(e);
}
}
// Get all data form JSON's files
async loadingAllDataJSON() {
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 [];
}
}
checkWorkflowExist(workflow) {
return !this.workflowsStores.includes(workflow);
}
updateWorkflows() {
return this.workflowManager.workflows.map((w) => w.filename);
}
updateWorkflow(newValue) {
this.previousWorkflow = this.currentWorkflow;
this.currentWorkflow = newValue;
this.debug &&
console.log(
`🔀 Previous workflow: ${this.previousWorkflow}, Current workflow: ${this.currentWorkflow}`
);
}
openWorkflow(filename) {
this.updateWorkflow(filename);
this.nodeStores = app.graph._nodes.filter((n) => n.type === "PainterNode");
this.countLoaded = 0;
this.workflowsStores = this.updateWorkflows();
this.debug && console.log("🔄 Change workflow:", this.currentWorkflow);
}
async setEvents() {
// Проверяем доступность объекта
if (!this.workflowManager) {
throw new Error("Workflow manager is not available");
}
const painters_settings_json = JSON.parse(
localStorage.getItem("alekpet.PainterNode.SaveSettingsJson", false)
);
if (!painters_settings_json) {
console.log("Painter settings save to JSON -> OFF");
return;
}
const self = this;
await new Promise((resolve) => {
this.workflowManager.$onAction(({ name, args, after }) => {
if (name === "createTemporary") {
if (app.graph._nodes.filter((n) => n.type === "PainterNode").length) {
this.newSave = true;
}
}
if (name === "deleteWorkflow") {
const filename = args[0].filename;
if (filename) {
this.removeData(`Painter_${filename}`);
}
}
if (name === "openWorkflow") {
after((result) => {
this.openWorkflow(result.filename);
resolve(true);
// Rename
const originalRename = result.rename;
result.rename = async function () {
const prevName = this.filename;
// Check if confirm dialog workflow exist overwrite
let confirmed = false;
// const originalConfirm = window.confirm;
// window.confirm = (message) => {
// confirmed = originalConfirm(message);
// return confirmed;
// };
const res = await originalRename.apply(this, arguments);
const newName = this.filename;
if (newName === prevName) return res; // names equals, nothing rename
// Rename backend
// let updateComplete = false;
try {
// -- Check if new name exist inside folder JSON files
// const checkResponse = await api.fetchApi(
// `/alekpet/file_exist_node_settings/Painter_${newName}`
// );
// if (checkResponse.status !== 200)
// throw new Error(
// `Error check exist workflow name file settings: ${checkResponse.statusText}`
// );
// const checkData = await checkResponse?.json();
// if (checkData?.isExists) {
// confirmed = await comfyuiDesktopConfirm(
// `Workflow new name '${newName}' already exists among the JSON files! Rename?`
// );
// }
// -- end - Check if new name exist inside folder JSON files
const rawResponse = await fetch(
"/alekpet/rename_node_settings",
{
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
old_name: `Painter_${prevName}`,
new_name: `Painter_${newName}`,
overwrite: confirmed,
}),
}
);
if (rawResponse.status === 200) {
const json_data = await rawResponse?.json();
console.log(json_data?.message);
// updateComplete = true;
} else {
console.error(
`Error rename workflow file settings ${rawResponse.statusText}`
);
}
} catch (e) {
console.log(e);
}
self.debug && console.log("Rename workflow!");
return res;
};
return result;
});
}
});
});
}
}
// LocalStorage Init
class StorageClass {
constructor(node, debug = false) {
@@ -399,8 +122,6 @@ class StorageClass {
this.node = node;
this.name = node.name;
this.workflowStateManager = WorkflowStateManager.getInstance(debug);
this.settings_painter_node_default = {
undo_history: [],
redo_history: [],
@@ -429,154 +150,10 @@ class StorageClass {
},
};
this.settings_painter_node_all = {};
this.settings_painter_node = JSON.parse(
JSON.stringify(this.settings_painter_node_default)
);
}
getSettingsPainterNode() {
return this.settings_painter_node;
}
async getData(showModal = true) {
// Get settings node
if (showModal) {
const parent = this.node.painter.panelPaintBoxRight;
const message = createWindowModal({
...THEMES_MODAL_WINDOW.warning,
textTitle: "Loading",
textBody: [
makeElement("div", {
innerHTML:
"Please wait, <span style='font-weight: bold; color: orange'>Painter node</span> settings are loading. Loading times may take a long time if large images have been added to the canvas!",
}),
],
options: {
auto: {
autohide: true,
autoshow: true,
autoremove: true,
autoremove: true,
timewait: 500,
propStyles: { opacity: 0 },
propPreStyles: { display: "flex" },
},
parent,
},
});
}
this.settings_painter_node_all = await this.workflowStateManager.loadData();
// -- end - Get settings node
if (
!this.settings_painter_node_all ||
isEmptyObject(this.settings_painter_node_all)
) {
this.settings_painter_node_all = {
painters_data: { [this.name]: {} },
};
this.settings_painter_node = {};
}
// --- Realize save copy before change workflow. Dev....
if (this.workflowStateManager.newSave) {
const json_data = await this.workflowStateManager.loadingAllDataJSON();
let previousData = json_data.filter(
(d) =>
d.name === `Painter_${this.workflowStateManager.previousWorkflow}`
);
previousData = previousData.length ? previousData[0].value : null;
if (
previousData &&
this.workflowStateManager.currentWorkflow !==
this.workflowStateManager.previousWorkflow
) {
this.settings_painter_node_all.painters_data = JSON.parse(
JSON.stringify(previousData.painters_data)
);
console.log(
`Copy painters data '${Object.keys(previousData.painters_data).join(
","
)}' from previous workflow '${
this.workflowStateManager.previousWorkflow
}' to '${this.workflowStateManager.currentWorkflow}' completed!`
);
}
}
// --- end - Realize save copy before change workflow. Dev....
if (
!this.settings_painter_node_all.painters_data.hasOwnProperty(this.name)
) {
this.settings_painter_node_all.painters_data[this.name] = {};
}
if (
this.settings_painter_node_all.painters_data[this.name] &&
isEmptyObject(this.settings_painter_node_all.painters_data[this.name])
) {
this.settings_painter_node_all.painters_data[this.name] = JSON.parse(
JSON.stringify(this.settings_painter_node_default)
);
}
this.settings_painter_node =
this.settings_painter_node_all.painters_data[this.name];
this.workflowStateManager.countLoaded += 1;
if (
this.workflowStateManager.newSave &&
this.workflowStateManager.countLoaded ===
this.workflowStateManager.nodeStores.length
) {
this.workflowStateManager.newSave = false;
this.workflowStateManager.countLoaded = 0;
}
return this.settings_painter_node;
}
// Write settings in the file json
async saveData(workflowName = null) {
try {
let savedData = await this.workflowStateManager.loadData(workflowName);
if (Object.keys(savedData).length > 0) {
savedData.painters_data[this.name] = JSON.parse(
JSON.stringify(this.settings_painter_node)
);
} else {
savedData = this.settings_painter_node_all;
}
const formData = new FormData();
formData.append(
"name",
`Painter_${workflowName ?? this.workflowStateManager.currentWorkflow}`
);
formData.append(
"data",
new Blob([JSON.stringify(savedData)], {
type: "application/json",
})
);
const rawResponse = await fetch("/alekpet/save_node_settings", {
method: "POST",
body: formData,
});
if (rawResponse.status !== 200) {
throw new Error(
`Error painter save file settings ${rawResponse.statusText}`
);
}
} catch (e) {
console.log(e);
}
}
}
export {
@@ -587,5 +164,4 @@ export {
HsvToRgb,
StorageClass,
formatBytes,
WorkflowStateManager,
};
+172 -100
View File
@@ -35,12 +35,30 @@ import { MyPaintManager } from "./lib/painternode/manager_mypaint.js";
const DEBUG = false;
const extensionName = "alekpet.PainterNode";
const CLONE_CACHE = new Map();
const PAINTER_ASSET_VERSION = 2;
// Save settings in JSON file on the extension folder [big data settings includes images] if true else localStorage
let painters_settings_json = JSON.parse(
localStorage.getItem(`${extensionName}.SaveSettingsJson`, false)
);
//
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 &&
typeof value === "object" &&
value.version === PAINTER_ASSET_VERSION &&
typeof value.asset_id === "string" &&
value.asset_id.length > 0
);
}
const removeIcon =
"data:image/svg+xml,%3Csvg version='1.1' id='Ebene_1' x='0px' y='0px' width='595.275px' height='595.275px' viewBox='200 215 230 470' xmlns='http://www.w3.org/2000/svg'%3E%3Cdefs%3E%3C/defs%3E%3Crect x='125.3' y='264.6' width='350.378' height='349.569' style='fill: rgb(237, 0, 0); stroke: rgb(197, 2, 2);' rx='58.194' ry='58.194'%3E%3C/rect%3E%3Cg%3E%3Crect x='267.162' y='307.978' transform='matrix(0.7071 -0.7071 0.7071 0.7071 -222.6202 340.6915)' style='fill:white;' width='65.545' height='262.18' rx='32.772' ry='32.772'%3E%3C/rect%3E%3Crect x='266.988' y='308.153' transform='matrix(0.7071 0.7071 -0.7071 0.7071 398.3889 -83.3116)' style='fill:white;' width='65.544' height='262.179' rx='32.772' ry='32.772'%3E%3C/rect%3E%3C/g%3E%3C/svg%3E";
@@ -140,6 +158,11 @@ class Painter {
this.redo_history = [];
this.storageCls = this.node.storageCls;
this.node.painterAsset = this.node.painterAsset || {
version: PAINTER_ASSET_VERSION,
asset_id: createPainterAssetId(),
};
this._persistPromise = Promise.resolve();
// [Crop] Object
this.crop_object = null;
@@ -205,10 +228,12 @@ class Painter {
async saveSettingsPainterNode() {
this.canvasSaveSettingsPainter();
const persistPromise = this.persistPainterState();
// Save data
app?.extensionManager?.workflow?.activeWorkflow?.changeTracker?.captureCanvasState();
if (painters_settings_json) await this.node.storageCls.saveData();
await persistPromise;
}
initCanvas(canvasEl) {
@@ -2460,6 +2485,77 @@ class Painter {
} catch (e) {}
}
getWorkflowValue() {
return {
version: PAINTER_ASSET_VERSION,
asset_id: this.node.painterAsset.asset_id,
};
}
async persistPainterState(state = null) {
const save_data =
this.storageCls.settings_painter_node.settings.saveImage ?? true;
if (!save_data) return;
const nextState = state || {
...this.storageCls.settings_painter_node,
canvas_settings: this.canvas.toJSON(["mypaintlib"]),
};
const request = async () => {
const response = await api.fetchApi(
`/alekpet/painter_asset/${encodeURIComponent(
this.node.painterAsset.asset_id
)}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ state: nextState }),
}
);
if (!response.ok) {
throw new Error(
`Painter asset save failed: ${response.status} ${response.statusText}`
);
}
return response.json();
};
// Serialize writes
this._persistPromise = this._persistPromise.catch(() => {}).then(request);
try {
return await this._persistPromise;
} catch (error) {
console.error("[PainterNode] Failed to save backend asset:", error);
return null;
}
}
async loadPainterAsset(assetId) {
const response = await api.fetchApi(
`/alekpet/painter_asset/${encodeURIComponent(assetId)}`
);
if (response.status === 404) {
return null;
}
if (!response.ok) {
throw new Error(
`Painter asset load failed: ${response.status} ${response.statusText}`
);
}
const result = await response.json();
return result?.state ?? null;
}
undoRedoLoadData(data) {
this.canvas.loadFromJSON(data, () => {
this.canvas.renderAll();
@@ -2790,21 +2886,41 @@ class Painter {
// - end
}
async loadCanvasData(data, painter_idx) {
if (painters_settings_json && data === null) {
data = await this.storageCls.getData();
async loadCanvasData(data, painter_idx, options = {}) {
// New workflows store only {version, asset_id}
if (isPainterAssetRef(data)) {
this.node.painterAsset = {
version: PAINTER_ASSET_VERSION,
asset_id: options.assetId || data.asset_id,
};
if (!data) {
let backendState = null;
try {
backendState = await this.loadPainterAsset(
this.node.painterAsset.asset_id
);
} catch (error) {
console.error("[PainterNode] Failed to load backend asset:", error);
}
if (backendState) {
data = backendState;
} else {
// The workflow may reference an asset that was deleted
data = JSON.parse(
JSON.stringify(this.storageCls.settings_painter_node_default)
);
}
this.storageCls.settings_painter_node = data;
} else if (data && data.canvas_settings) {
// Legacy workflow migration
this.node.painterAsset = {
version: PAINTER_ASSET_VERSION,
asset_id: createPainterAssetId(),
};
}
if (data) {
Object.assign(this.node.widgets[painter_idx].value, data);
this.storageCls.settings_painter_node = data;
if (data?.settings) {
const {
@@ -2827,14 +2943,19 @@ class Painter {
// Loading canvas data
if (data?.canvas_settings) {
this.canvasLoadSettingPainter(data).then((result) => {
await this.canvasLoadSettingPainter(data).then((result) => {
if (result) {
this.canvas.renderAll();
this.uploadPaintFile(this.name);
}
});
}
// Migrate legacy workflow data, or repair a missing backend asset
await this.persistPainterState(data);
if (data?.canvas_settings && options.migrateImage !== false) {
await this.uploadPaintFile(this.node.name);
}
this.node.setSize(this.node.size);
app.graph.setDirtyCanvas(true, false);
}
@@ -2871,10 +2992,26 @@ function PainterWidget(node, inputName, inputData, app) {
wrapperPainter,
{
setValue(v) {
node.painter.storageCls.settings_painter_node = v;
if (isPainterAssetRef(v)) {
node.painter.node.painterAsset = {
version: PAINTER_ASSET_VERSION,
asset_id: v.asset_id,
};
return;
}
// Legacy workflows are loaded asynchronously by onConfigure
if (v?.canvas_settings) {
node.painter.node._legacyPainterData = v;
return;
}
if (v) {
node.painter.storageCls.settings_painter_node = v;
}
},
getValue() {
return node.painter.storageCls.settings_painter_node;
return node.painter.getWorkflowValue();
},
}
);
@@ -2962,8 +3099,11 @@ function PainterWidget(node, inputName, inputData, app) {
// Node serialize
node.onSerialize = (n) => {
if (painters_settings_json) {
n.widgets_values[3] = null;
const painterIndex = n.widgets_values?.findIndex?.(
(_, index) => node.widgets[index]?.type === "painter_node_alekpet"
);
if (painterIndex >= 0) {
n.widgets_values[painterIndex] = node.painter.getWorkflowValue();
}
};
@@ -3131,21 +3271,6 @@ app.registerExtension({
});
},
});
// Add settings params painter node
app.ui.settings.addSetting({
id: `${extensionName}.SaveSettingsJson`,
name: "🔸 Save settings to JSON file (BETA)",
defaultValue: false,
type: "boolean",
onChange: (e) => {
painters_settings_json = !!e;
localStorage.setItem(
`${extensionName}.SaveSettingsJson`,
painters_settings_json
);
},
});
// end -- Settings
},
async setup(app) {},
@@ -3194,10 +3319,6 @@ app.registerExtension({
this.title = `${this.type} - ${this.painter.storageCls.settings_painter_node.settings.currentCanvasSize.width}x${this.painter.storageCls.settings_painter_node.settings.currentCanvasSize.height}`;
if (!painters_settings_json && !this?.widgets_values) {
this.painter.uploadPaintFile(nodeNamePNG);
}
// Resize window
window.addEventListener("resize", (e) => resizeCanvas(this), false);
return r;
@@ -3208,10 +3329,11 @@ app.registerExtension({
const cloneNode = origClone?.apply(this, arguments);
if (!cloneNode) return origClone;
CLONE_CACHE.set(
this.id,
structuredClone(this.painter.storageCls.settings_painter_node)
);
CLONE_CACHE.set(this.id, {
state: structuredClone(this.painter.storageCls.settings_painter_node),
assetId: createPainterAssetId(),
});
return cloneNode;
};
@@ -3221,34 +3343,6 @@ app.registerExtension({
await this.getTitle();
if (painters_settings_json) {
if (this.storageCls.workflowStateManager.currentWorkflow) {
DEBUG &&
console.log(
`⚠️ [PainterNode] currentWorkflow is already set, skip: ${this.name} -> ${this.storageCls.workflowStateManager.currentWorkflow}`
);
}
// Если `setEvents` уже выполняется, ждем его завершения
if (setEventsPromise) {
DEBUG &&
console.log(
`⏳ [PainterNode] Waiting for setEvents to complete...`
);
await setEventsPromise;
DEBUG &&
console.log(`✅ [PainterNode] setEvents completed, continue`);
} else {
// Если это первый узел, запускаем `setEvents`
DEBUG &&
console.log(`🚀 [PainterNode] The first node calls setEvents...`);
setEventsPromise = this.storageCls.workflowStateManager.setEvents();
await setEventsPromise;
DEBUG && console.log(`✅ [PainterNode] setEvents completed`);
}
}
setTimeout(async () => {
console.log(`🔧 Configure PainterNode: ${this.name}`);
@@ -3258,19 +3352,24 @@ app.registerExtension({
if (painter_idx < 0) return;
let data = widget.widgets_values[painter_idx];
let cloneState = null;
if (!data) {
for (const [originalId, cachedSettings] of CLONE_CACHE.entries()) {
for (const [originalId, cached] of CLONE_CACHE.entries()) {
console.log(
`♻️ Recovering cloned data from node ${originalId} to node ${this.id}`
);
data = cachedSettings;
cloneState = cached.state;
data = cloneState;
CLONE_CACHE.delete(originalId);
break;
}
}
this.painter.loadCanvasData(data, painter_idx);
await this.painter.loadCanvasData(data, painter_idx, {
assetId: cloneState ? createPainterAssetId() : undefined,
migrateImage: true,
});
});
};
@@ -3305,33 +3404,6 @@ app.registerExtension({
},
});
}
setTimeout(() => {
const removeIndex = options.findIndex((m) => m?.content === "Remove"),
removeButton = options[removeIndex];
if (!!removeButton) {
const remove_callback = removeButton.callback;
const self = this;
removeButton.callback = async function () {
const nodeName = Array.from(arguments).find((f) => f?.name).name;
remove_callback.apply(this, arguments);
if (!painters_settings_json) return;
if (
await comfyuiDesktopConfirm(
`Remove data ${nodeName} from JSON?`
)
) {
self.storageCls.workflowStateManager.removeData(null, [
nodeName,
]);
}
};
}
}, 0);
};
// end - ExtraMenuOptions
}