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:
@@ -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)
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user