A BATCH button has been added to the RS Intermediate Spline Mask node for batch image processing with a permanent mask
This commit is contained in:
@@ -1,18 +1,3 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright 2025-2026 Raykosan (RaykoStudio)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import time
|
||||
import json
|
||||
@@ -30,6 +15,7 @@ except ImportError:
|
||||
print("\033[91m[InSPLINE ] Warning: Server modules not available.\033[0m")
|
||||
|
||||
PENDING_DECISIONS = {}
|
||||
_SAVED_MASK_PRESETS = {}
|
||||
|
||||
class RaykoIntermediateSplineMask:
|
||||
@classmethod
|
||||
@@ -49,10 +35,6 @@ class RaykoIntermediateSplineMask:
|
||||
}
|
||||
return inputs
|
||||
|
||||
# 🔹 ДОБАВЛЕНО: Принудительный запуск ноды при каждом Queue
|
||||
# ComfyUI кэширует выводы, если входы не меняются.
|
||||
# Для интерактивных нод с паузой это ломает логику.
|
||||
# Возврат time.time() гарантирует, что кэш всегда будет считаться "устаревшим".
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, **kwargs):
|
||||
return time.time()
|
||||
@@ -66,66 +48,99 @@ class RaykoIntermediateSplineMask:
|
||||
def create_mask(self, image, coordinates="[]", unique_id=None, prompt_id=None):
|
||||
unique_id = str(unique_id) if unique_id else "unknown"
|
||||
|
||||
# Проверка batch mode
|
||||
saved_preset = _SAVED_MASK_PRESETS.get(unique_id, {})
|
||||
batch_mode_active = saved_preset.get("active", False)
|
||||
|
||||
if SERVER_AVAILABLE and unique_id:
|
||||
if unique_id not in PENDING_DECISIONS:
|
||||
PENDING_DECISIONS[unique_id] = {
|
||||
"status": "pending",
|
||||
"coordinates": coordinates
|
||||
}
|
||||
# Если batch активен и есть сохраненные координаты - используем их без паузы
|
||||
if batch_mode_active and saved_preset.get("coordinates"):
|
||||
coordinates = saved_preset["coordinates"]
|
||||
print(f"[InSPLINE ] Batch mode: using saved mask for node {unique_id}")
|
||||
|
||||
# ← ДОБАВЛЕНО: Сохраняем изображение и отправляем событие для обновления UI
|
||||
if prompt_id is None:
|
||||
import uuid
|
||||
prompt_id = str(uuid.uuid4())
|
||||
|
||||
try:
|
||||
img_tensor = image[0]
|
||||
i = 255. * img_tensor.cpu().numpy()
|
||||
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
||||
|
||||
filename = f"inspline_{unique_id}_{prompt_id}.png"
|
||||
subfolder = "inspline"
|
||||
full_output_folder = os.path.join(folder_paths.get_temp_directory(), subfolder)
|
||||
os.makedirs(full_output_folder, exist_ok=True)
|
||||
img.save(os.path.join(full_output_folder, filename))
|
||||
|
||||
PromptServer.instance.send_sync("rayko.inspline.show", {
|
||||
"node_id": unique_id,
|
||||
"image_url": f"/view?filename={filename}&type=temp&subfolder={subfolder}"
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"[InSPLINE 🦊] Error saving image in batch mode: {e}")
|
||||
else:
|
||||
PENDING_DECISIONS[unique_id]["coordinates"] = coordinates
|
||||
|
||||
if prompt_id is None:
|
||||
import uuid
|
||||
prompt_id = str(uuid.uuid4())
|
||||
# Обычный режим с паузой
|
||||
if unique_id not in PENDING_DECISIONS:
|
||||
PENDING_DECISIONS[unique_id] = {
|
||||
"status": "pending",
|
||||
"coordinates": coordinates
|
||||
}
|
||||
else:
|
||||
PENDING_DECISIONS[unique_id]["coordinates"] = coordinates
|
||||
|
||||
if prompt_id is None:
|
||||
import uuid
|
||||
prompt_id = str(uuid.uuid4())
|
||||
|
||||
try:
|
||||
img_tensor = image[0]
|
||||
i = 255. * img_tensor.cpu().numpy()
|
||||
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
||||
|
||||
filename = f"inspline_{unique_id}_{prompt_id}.png"
|
||||
subfolder = "inspline"
|
||||
full_output_folder = os.path.join(folder_paths.get_temp_directory(), subfolder)
|
||||
os.makedirs(full_output_folder, exist_ok=True)
|
||||
img.save(os.path.join(full_output_folder, filename))
|
||||
try:
|
||||
img_tensor = image[0]
|
||||
i = 255. * img_tensor.cpu().numpy()
|
||||
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
||||
|
||||
filename = f"inspline_{unique_id}_{prompt_id}.png"
|
||||
subfolder = "inspline"
|
||||
full_output_folder = os.path.join(folder_paths.get_temp_directory(), subfolder)
|
||||
os.makedirs(full_output_folder, exist_ok=True)
|
||||
img.save(os.path.join(full_output_folder, filename))
|
||||
|
||||
PromptServer.instance.send_sync("rayko.inspline.show", {
|
||||
"node_id": unique_id,
|
||||
"image_url": f"/view?filename={filename}&type=temp&subfolder={subfolder}"
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"[InSPLINE 🦊] Error saving image: {e}")
|
||||
PromptServer.instance.send_sync("rayko.inspline.show", {
|
||||
"node_id": unique_id,
|
||||
"image_url": f"/view?filename={filename}&type=temp&subfolder={subfolder}"
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"[InSPLINE 🦊] Error saving image: {e}")
|
||||
|
||||
while True:
|
||||
state = PENDING_DECISIONS.get(unique_id, {})
|
||||
current_status = state.get("status", "pending")
|
||||
current_coordinates = state.get("coordinates", coordinates)
|
||||
|
||||
if current_status == "approved":
|
||||
coordinates = current_coordinates
|
||||
if unique_id in PENDING_DECISIONS:
|
||||
del PENDING_DECISIONS[unique_id]
|
||||
break
|
||||
|
||||
if current_status == "cancelled":
|
||||
if unique_id in PENDING_DECISIONS:
|
||||
del PENDING_DECISIONS[unique_id]
|
||||
break
|
||||
while True:
|
||||
state = PENDING_DECISIONS.get(unique_id, {})
|
||||
current_status = state.get("status", "pending")
|
||||
current_coordinates = state.get("coordinates", coordinates)
|
||||
|
||||
if current_status == "approved":
|
||||
coordinates = current_coordinates
|
||||
if unique_id in PENDING_DECISIONS:
|
||||
del PENDING_DECISIONS[unique_id]
|
||||
break
|
||||
|
||||
if current_status == "cancelled":
|
||||
if unique_id in PENDING_DECISIONS:
|
||||
del PENDING_DECISIONS[unique_id]
|
||||
break
|
||||
|
||||
if current_status == "removed":
|
||||
print(f"[InSPLINE 🦊] Node {unique_id} REMOVED! Cleaning up...")
|
||||
if unique_id in PENDING_DECISIONS:
|
||||
del PENDING_DECISIONS[unique_id]
|
||||
break
|
||||
|
||||
if current_status == "rejected":
|
||||
PENDING_DECISIONS[unique_id]["status"] = "pending"
|
||||
coordinates = current_coordinates
|
||||
|
||||
time.sleep(0.3)
|
||||
if current_status == "removed":
|
||||
print(f"[InSPLINE 🦊] Node {unique_id} REMOVED! Cleaning up...")
|
||||
if unique_id in PENDING_DECISIONS:
|
||||
del PENDING_DECISIONS[unique_id]
|
||||
break
|
||||
|
||||
if current_status == "rejected":
|
||||
PENDING_DECISIONS[unique_id]["status"] = "pending"
|
||||
coordinates = current_coordinates
|
||||
|
||||
time.sleep(0.3)
|
||||
else:
|
||||
print("[InSPLINE 🦊] Server not available, skipping pause logic.")
|
||||
print("[InSPLINE ] Server not available, skipping pause logic.")
|
||||
|
||||
if image is None or len(image) == 0:
|
||||
h, w = 512, 512
|
||||
@@ -166,12 +181,19 @@ if SERVER_AVAILABLE:
|
||||
node_id = str(data.get("node_id"))
|
||||
decision = data.get("decision")
|
||||
coordinates = data.get("coordinates")
|
||||
batch_mode = data.get("batch_mode", False)
|
||||
|
||||
if node_id in PENDING_DECISIONS:
|
||||
if decision == "approve":
|
||||
PENDING_DECISIONS[node_id]["status"] = "approved"
|
||||
if coordinates:
|
||||
PENDING_DECISIONS[node_id]["coordinates"] = coordinates
|
||||
# Сохраняем preset если batch_mode активен
|
||||
if batch_mode and coordinates:
|
||||
_SAVED_MASK_PRESETS[node_id] = {
|
||||
"coordinates": coordinates,
|
||||
"active": True
|
||||
}
|
||||
|
||||
elif decision == "reject":
|
||||
PENDING_DECISIONS[node_id]["status"] = "rejected"
|
||||
@@ -180,6 +202,9 @@ if SERVER_AVAILABLE:
|
||||
|
||||
elif decision == "cancel":
|
||||
PENDING_DECISIONS[node_id]["status"] = "cancelled"
|
||||
# Очищаем preset при cancel
|
||||
if node_id in _SAVED_MASK_PRESETS:
|
||||
del _SAVED_MASK_PRESETS[node_id]
|
||||
|
||||
return web.Response(status=200, text="Decision recorded")
|
||||
else:
|
||||
@@ -206,6 +231,42 @@ if SERVER_AVAILABLE:
|
||||
except Exception as e:
|
||||
print(f"[InSPLINE 🦊] Cleanup Error: {e}")
|
||||
return web.Response(status=500, text=str(e))
|
||||
|
||||
@PromptServer.instance.routes.post("/rayko/inspline/batch_toggle")
|
||||
async def inspline_batch_toggle(request):
|
||||
try:
|
||||
data = await request.json()
|
||||
node_id = str(data.get("node_id"))
|
||||
enabled = data.get("enabled", False)
|
||||
|
||||
if node_id in _SAVED_MASK_PRESETS:
|
||||
_SAVED_MASK_PRESETS[node_id]["active"] = enabled
|
||||
elif enabled and node_id in PENDING_DECISIONS:
|
||||
coordinates = PENDING_DECISIONS[node_id].get("coordinates")
|
||||
if coordinates:
|
||||
_SAVED_MASK_PRESETS[node_id] = {
|
||||
"coordinates": coordinates,
|
||||
"active": True
|
||||
}
|
||||
|
||||
return web.Response(status=200, text="OK")
|
||||
except Exception as e:
|
||||
print(f"[InSPLINE 🦊] Batch toggle Error: {e}")
|
||||
return web.Response(status=500, text=str(e))
|
||||
|
||||
@PromptServer.instance.routes.post("/rayko/inspline/clear_preset")
|
||||
async def inspline_clear_preset(request):
|
||||
try:
|
||||
data = await request.json()
|
||||
node_id = str(data.get("node_id"))
|
||||
|
||||
if node_id in _SAVED_MASK_PRESETS:
|
||||
del _SAVED_MASK_PRESETS[node_id]
|
||||
|
||||
return web.Response(status=200, text="OK")
|
||||
except Exception as e:
|
||||
print(f"[InSPLINE 🦊] Clear preset Error: {e}")
|
||||
return web.Response(status=500, text=str(e))
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"RaykoIntermediateSplineMask": RaykoIntermediateSplineMask}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"RaykoIntermediateSplineMask": "🦊 RS Intermediate Spline Mask"}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"RaykoIntermediateSplineMask": " RS Intermediate Spline Mask"}
|
||||
+1
-1
@@ -29,7 +29,7 @@ IGNORED_MODULES: Set[str] = {'__init__.py'}
|
||||
REQUIRED_MODULES: Set[str] = set()
|
||||
|
||||
PACKAGE_DISPLAY_NAME = "ComfyUI_RaykoStudio"
|
||||
__version__ = "0.23.3"
|
||||
__version__ = "0.23.4"
|
||||
|
||||
def load_modules():
|
||||
print(f"🦊 {PACKAGE_DISPLAY_NAME}: Version: {__version__}")
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ComfyUI_RaykoStudio",
|
||||
"version": "0.23.3",
|
||||
"version": "0.23.4",
|
||||
"description": "Set of custom nodes for ComfyUI providing additional image processing capabilities",
|
||||
"author": "Raykosan",
|
||||
"license": "Apache License 2.0",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "ComfyUI_RaykoStudio"
|
||||
description = "Set of custom nodes for ComfyUI providing additional image processing capabilities"
|
||||
version = "0.23.3"
|
||||
version = "0.23.4"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = ["torch>=1.7.0", "numpy>=1.19.0", "Pillow>=8.0.0", "freetype-py>=2.5.1"]
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@ console.log("[InSPLINE ] intermediate_spline_mask.js LOADED!");
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
|
||||
let queueWasRunning = false;
|
||||
let batchResetTimer = null;
|
||||
let allNodes = [];
|
||||
|
||||
app.registerExtension({
|
||||
name: "RaykoIntermediateSplineMask",
|
||||
|
||||
@@ -18,6 +22,8 @@ app.registerExtension({
|
||||
node.imageReady = false;
|
||||
node.currentStatus = "Waiting...";
|
||||
node.buttons = [];
|
||||
node.batchMode = false;
|
||||
node.hasPreset = false;
|
||||
|
||||
const coordsWidget = node.widgets?.find(w => w.name === "coordinates");
|
||||
if (coordsWidget) {
|
||||
@@ -27,15 +33,16 @@ app.registerExtension({
|
||||
};
|
||||
}
|
||||
|
||||
node.addButton = (label, color, callback) => {
|
||||
node.addButton = (label, color, callback, isBatch = false) => {
|
||||
node.buttons.push({
|
||||
label: label, color: color, callback: callback,
|
||||
x: 0, y: 0, w: 0, h: 30, hover: false
|
||||
x: 0, y: 0, w: 0, h: 30, hover: false, isBatch: isBatch
|
||||
});
|
||||
};
|
||||
|
||||
node.addButton("⚙️ BATCH", "#2196F3", () => node.toggleBatchMode(), true);
|
||||
node.addButton("✔️ ACCEPT", "#28a745", () => node.sendDecision("approve"));
|
||||
node.addButton("🔴 CLEAR POINTS", "#dc3545", () => node.sendDecision("reject"));
|
||||
node.addButton("🔴 CLEAR MASK", "#dc3545", () => node.sendDecision("reject"));
|
||||
node.addButton("❌ CANCEL", "#666666", () => node.sendDecision("cancel"));
|
||||
|
||||
node.setSize([450, 600]);
|
||||
@@ -159,9 +166,9 @@ app.registerExtension({
|
||||
const createOverlayCanvas = () => {
|
||||
if (_overlayCanvas) return;
|
||||
_overlayCanvas = document.createElement("canvas");
|
||||
_overlayCanvas.className = "rayko-inspline-overlay"; // <-- Добавлен класс для надёжного поиска
|
||||
_overlayCanvas.className = "rayko-inspline-overlay";
|
||||
_overlayCanvas.style.cssText = `
|
||||
position: fixed !important; z-index: 900 !important; // <-- Изменено на 900
|
||||
position: fixed !important; z-index: 900 !important;
|
||||
pointer-events: auto !important; cursor: crosshair !important;
|
||||
background: transparent !important; touch-action: none;
|
||||
border: 1px dashed #00FF00 !important; box-sizing: border-box !important;
|
||||
@@ -297,22 +304,31 @@ app.registerExtension({
|
||||
|
||||
const btnH = 28;
|
||||
const btnY = h - 45;
|
||||
const btnW = (w - 50) / 3;
|
||||
const btnW = (w - 60) / 4 + 3;
|
||||
|
||||
this.buttons[0].x = 15; this.buttons[0].y = btnY; this.buttons[0].w = btnW; this.buttons[0].h = btnH;
|
||||
this.buttons[1].x = 20 + btnW; this.buttons[1].y = btnY; this.buttons[1].w = btnW; this.buttons[1].h = btnH;
|
||||
this.buttons[2].x = 25 + (btnW * 2); this.buttons[2].y = btnY; this.buttons[2].w = btnW; this.buttons[2].h = btnH;
|
||||
this.buttons[3].x = 30 + (btnW * 3); this.buttons[3].y = btnY; this.buttons[3].w = btnW; this.buttons[3].h = btnH;
|
||||
|
||||
for (let btn of this.buttons) {
|
||||
let btnColor = btn.color;
|
||||
if (btn.isBatch && node.batchMode) {
|
||||
btnColor = "#ff9800";
|
||||
btn.label = "️⚙️ BATCH ON";
|
||||
} else if (btn.isBatch) {
|
||||
btn.label = "️⚙️ BATCH";
|
||||
}
|
||||
|
||||
ctx.fillStyle = btn.hover ? "#444" : "#2a2a2a";
|
||||
ctx.beginPath();
|
||||
if (ctx.roundRect) ctx.roundRect(btn.x, btn.y, btn.w, btn.h, 6);
|
||||
else ctx.rect(btn.x, btn.y, btn.w, btn.h);
|
||||
ctx.fill();
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeStyle = btn.color;
|
||||
ctx.strokeStyle = btnColor;
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = btn.color;
|
||||
ctx.fillStyle = btnColor;
|
||||
ctx.font = "bold 11px Arial";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
@@ -325,6 +341,7 @@ app.registerExtension({
|
||||
const onResize = node.onResize;
|
||||
node.onResize = function(size) {
|
||||
if (onResize) onResize.apply(this, arguments);
|
||||
if (size[0] < 450) size[0] = 450;
|
||||
_lastRect = null;
|
||||
};
|
||||
|
||||
@@ -354,6 +371,27 @@ app.registerExtension({
|
||||
}
|
||||
};
|
||||
|
||||
node.toggleBatchMode = async function() {
|
||||
node.batchMode = !node.batchMode;
|
||||
if (node.batchMode) {
|
||||
node.hasPreset = false;
|
||||
}
|
||||
this.setDirtyCanvas(true, true);
|
||||
|
||||
try {
|
||||
await fetch("/rayko/inspline/batch_toggle", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
node_id: this.id.toString(),
|
||||
enabled: node.batchMode
|
||||
})
|
||||
});
|
||||
} catch(e) {
|
||||
console.error("[InSPLINE ] Batch toggle failed:", e);
|
||||
}
|
||||
};
|
||||
|
||||
node.sendDecision = async function(decision) {
|
||||
const currentCoords = node.properties?.spline_coords || "[]";
|
||||
this.currentStatus = `Sending ${decision}...`;
|
||||
@@ -363,6 +401,16 @@ app.registerExtension({
|
||||
try {
|
||||
await api.interrupt();
|
||||
} catch (e) { console.error("Interrupt failed:", e); }
|
||||
|
||||
node.batchMode = false;
|
||||
node.hasPreset = false;
|
||||
try {
|
||||
await fetch("/rayko/inspline/clear_preset", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ node_id: this.id.toString() })
|
||||
});
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
if (decision === "reject") {
|
||||
@@ -377,15 +425,21 @@ app.registerExtension({
|
||||
body: JSON.stringify({
|
||||
node_id: this.id.toString(),
|
||||
decision: decision,
|
||||
coordinates: currentCoords
|
||||
coordinates: currentCoords,
|
||||
batch_mode: node.batchMode
|
||||
})
|
||||
});
|
||||
|
||||
if (resp.ok) {
|
||||
if (decision === "approve") {
|
||||
this.currentStatus = "✅ Approved! Processing...";
|
||||
if (node.batchMode) {
|
||||
node.hasPreset = true;
|
||||
// batchMode НЕ сбрасываем - кнопка остаётся оранжевой до конца очереди
|
||||
} else {
|
||||
this.currentStatus = "✅ Approved! Processing...";
|
||||
}
|
||||
} else if (decision === "reject") {
|
||||
this.currentStatus = "🔄 Points cleared. Draw & Approve!";
|
||||
this.currentStatus = "🔄 Mask cleared. Draw & Approve!";
|
||||
} else {
|
||||
this.currentStatus = "❌ Cancelled.";
|
||||
}
|
||||
@@ -407,7 +461,7 @@ app.registerExtension({
|
||||
_overlayCanvas = null;
|
||||
}
|
||||
|
||||
fetch("/rayko/inspline/cleanup", {
|
||||
fetch("/rayko/inspline/clear_preset", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
@@ -429,6 +483,8 @@ app.registerExtension({
|
||||
}
|
||||
};
|
||||
|
||||
allNodes.push({node});
|
||||
|
||||
} catch (error) {
|
||||
console.error("[InSPLINE ] Critical Error:", error);
|
||||
}
|
||||
@@ -443,7 +499,6 @@ app.registerExtension({
|
||||
node.image.src = image_url + "&t=" + Date.now();
|
||||
node.image.onload = () => {
|
||||
node.imageReady = true;
|
||||
// ИСПРАВЛЕНО: поиск по классу вместо жёсткой привязки к z-index
|
||||
const overlay = document.querySelector('.rayko-inspline-overlay');
|
||||
if (overlay) {
|
||||
overlay.style.display = "block";
|
||||
@@ -455,10 +510,38 @@ app.registerExtension({
|
||||
node.setDirtyCanvas(true, true);
|
||||
};
|
||||
if (node.size[1] < 600) node.setSize([node.size[0], 600]);
|
||||
node.currentStatus = "🎨 Draw mask, then APPROVE!";
|
||||
node.currentStatus = " Draw mask, then APPROVE!";
|
||||
app.canvas.centerOnNode(node);
|
||||
node.setDirtyCanvas(true, true);
|
||||
}
|
||||
});
|
||||
|
||||
api.addEventListener("status", (e) => {
|
||||
const remaining = e.detail?.exec_info?.queue_remaining;
|
||||
if (remaining > 0) {
|
||||
queueWasRunning = true;
|
||||
if (batchResetTimer) { clearTimeout(batchResetTimer); batchResetTimer = null; }
|
||||
} else if (remaining === 0 && queueWasRunning) {
|
||||
queueWasRunning = false;
|
||||
if (!batchResetTimer) {
|
||||
batchResetTimer = setTimeout(() => {
|
||||
allNodes.forEach(({node}) => {
|
||||
if (node && (node.batchMode || node.hasPreset)) {
|
||||
node.batchMode = false;
|
||||
node.hasPreset = false;
|
||||
node.setDirtyCanvas(true, true);
|
||||
|
||||
fetch("/rayko/inspline/clear_preset", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ node_id: String(node.id) })
|
||||
}).catch(err => console.error("Failed to clear preset:", err));
|
||||
}
|
||||
});
|
||||
batchResetTimer = null;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user