Code optimization

This commit is contained in:
Raykosan
2026-07-09 21:33:12 +03:00
parent 2163b8c5c2
commit 209cac2fe8
13 changed files with 2 additions and 111 deletions
-6
View File
@@ -64,17 +64,14 @@ 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:
# Если 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())
@@ -97,7 +94,6 @@ class RaykoIntermediateSplineMask:
except Exception as e:
print(f"[InSPLINE 🦊] Error saving image in batch mode: {e}")
else:
# Обычный режим с паузой
if unique_id not in PENDING_DECISIONS:
PENDING_DECISIONS[unique_id] = {
"status": "pending",
@@ -204,7 +200,6 @@ if SERVER_AVAILABLE:
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,
@@ -218,7 +213,6 @@ 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]
+2 -5
View File
@@ -107,9 +107,9 @@ class RS_Image_MultiLatent:
RETURN_NAMES = ("latent", "width_px", "height_px")
FUNCTION = "generate"
CATEGORY = "🦊 RaykoStudio"
DESCRIPTION = "Universal latent generation node - automatically adapts to any VAE architecture."
def generate(self, vae, size_mode, active_preset, preset_standard, preset_qwen, preset_krea2, width, height, megapixels, aspect_ratio, batch_size):
# Определение параметров из VAE
latent_channels = 4
scale_factor = 8
@@ -128,7 +128,6 @@ class RS_Image_MultiLatent:
except:
pass
# Валидация
if latent_channels <= 0 or latent_channels > 256:
raise ValueError(f"Invalid latent_channels: {latent_channels}")
if scale_factor <= 0 or scale_factor > 64:
@@ -136,9 +135,7 @@ class RS_Image_MultiLatent:
print(f"[RS_Image_MultiLatent] VAE: {latent_channels} channels, {scale_factor}x scale. Active: {active_preset}")
# Расчёт размеров
if size_mode == "Preset":
# Выбираем значение из активного списка
if active_preset == "Standard":
preset = preset_standard
elif active_preset == "Qwen":
@@ -147,7 +144,7 @@ class RS_Image_MultiLatent:
preset = preset_krea2
size_map = {
# Standard & Krea2 (одинаковые размеры)
# Standard & Krea2
"Square - 512x512 (1:1)": (512, 512), "Square - 640x640 (1:1)": (640, 640),
"Square - 768x768 (1:1)": (768, 768), "Square - 1024x1024 (1:1)": (1024, 1024),
"Square - 1280x1280 (1:1)": (1280, 1280), "Square - 1536x1536 (1:1)": (1536, 1536),
-4
View File
@@ -39,17 +39,13 @@ class LoadImageRGBA:
def load_image(self, image):
image_path = os.path.join(folder_paths.get_input_directory(), image)
# Загружаем изображение в PIL
img = Image.open(image_path)
# Принудительно конвертируем в RGBA, чтобы гарантировать 4 канала
if img.mode != 'RGBA':
img = img.convert('RGBA')
# Конвертируем в numpy array (нормализуем 0-1)
img_array = np.array(img).astype(np.float32) / 255.0
# Добавляем размерность батча [1, H, W, C]
tensor = torch.from_numpy(img_array).unsqueeze(0)
print(f"[rgb2rgba ] Loaded {image}: {img.size}, mode={img.mode}")
-5
View File
@@ -969,7 +969,6 @@ class RS_OverlayPro:
text_mask = masks['text_mask']
outline_mask = masks['outline_mask']
# Извлекаем shadow-параметры
enable_shadow = bool(text_params.get('enable_shadow', False))
shadow_color = text_params.get('shadow_color', '#333333')
shadow_opacity = min(1.0, max(0.0, float(text_params.get('shadow_opacity', 0.8))))
@@ -1005,7 +1004,6 @@ class RS_OverlayPro:
result = Image.new("RGBA", render_size, (0,0,0,0))
# 1. GLOW
if include_glow and config.enable_glow and config.glow_size > 0:
glow_mask = self._apply_glow_to_mask(
text_mask, config.glow_size, config.glow_spread, config.glow_opacity
@@ -1013,7 +1011,6 @@ class RS_OverlayPro:
glow_colored = self._apply_color_to_mask(glow_mask, config.glow_color)
result = Image.alpha_composite(result, glow_colored)
# 2. SHADOW (под текстом и outline)
if config.enable_shadow:
shadow_mask = self._apply_shadow_to_mask(
text_mask, config.shadow_blur, config.shadow_offset_x, config.shadow_offset_y
@@ -1021,12 +1018,10 @@ class RS_OverlayPro:
shadow_colored = self._apply_color_to_mask(shadow_mask, config.shadow_color)
result = Image.alpha_composite(result, shadow_colored)
# 3. OUTLINE
if config.has_outline:
outline_colored = self._apply_color_to_mask(outline_mask, config.outline_color)
result = Image.alpha_composite(result, outline_colored)
# 4. TEXT
text_colored = self._apply_color_to_mask(text_mask, config.text_color)
result = Image.alpha_composite(result, text_colored)
-25
View File
@@ -13,13 +13,11 @@ app.registerExtension({
nodeType.prototype.onNodeCreated = function() {
if (onNodeCreated) onNodeCreated.apply(this, arguments);
// State
this.overlay = { x: 0, y: 0, width: 100, height: 100, rotation: 0, flipH: false, flipV: false };
this.overlayRelative = { x: 0.5, y: 0.5, width: 0.3, height: 0.3, rotation: 0, flipH: false, flipV: false };
this.realOverlay = { width: 0, height: 0 };
this.realBackground = { width: 0, height: 0 };
// View
this.displayWidth = 420;
this.displayHeight = 420;
this.canvasPixelSize = 420;
@@ -27,7 +25,6 @@ app.registerExtension({
this.viewOffsetX = 0;
this.viewOffsetY = 0;
// Images & Editing
this.overlayImage = null;
this.backgroundImage = null;
this.isEditing = false;
@@ -36,21 +33,18 @@ app.registerExtension({
this.dragState = null;
this.currentSessionTimestamp = null;
// Parameters
this.opacity = 1.0;
this.featherType = "None";
this.blurRadius = 50;
this.blurHardness = 0;
this.advancedMode = false;
// Overlay DOM
this.overlayContainer = null;
this.overlayCanvas = null;
this.overlayCtx = null;
this.overlayRenderLoop = null;
this.overlayInputs = {};
// Misc
this.featherCenter = { x: 0.5, y: 0.5 };
this.canvasRealWidth = 0;
this.canvasRealHeight = 0;
@@ -68,10 +62,8 @@ app.registerExtension({
this.previewMaxSize = 512;
this.pendingEditorData = null;
// Heartbeat
this.heartbeatInterval = null;
// Helper function to sync widget values
const syncWidgetValue = (widgetName, value) => {
const widget = this.widgets?.find(w => w.name === widgetName);
if (widget) {
@@ -79,19 +71,16 @@ app.registerExtension({
}
};
// Hide default widgets
["opacity", "feather_type", "blur_radius", "blur_hardness"].forEach(n => {
const w = this.widgets?.find(w => w.name === n);
if (w) w.hidden = true;
});
// Add custom widgets
this.addWidget("slider", "opacity", 1.0, v => { this.opacity = v; syncWidgetValue("opacity", v); this.previewDirty = true; this.setDirtyCanvas(true); }, { min: 0, max: 1, step: 0.01 });
this.addWidget("combo", "feather_type", "None", v => { this.featherType = v; syncWidgetValue("feather_type", v); this.previewDirty = true; this.setDirtyCanvas(true); }, { values: ["None", "Radial Blur In", "Radial Blur Out", "Ellipse Blur In", "Ellipse Blur Out"] });
this.addWidget("slider", "blur_radius", 50, v => { this.blurRadius = v; syncWidgetValue("blur_radius", v); this.previewDirty = true; this.setDirtyCanvas(true); }, { min: 0, max: 100, step: 1 });
this.addWidget("slider", "blur_hardness", 0, v => { this.blurHardness = v; syncWidgetValue("blur_hardness", v); this.previewDirty = true; this.setDirtyCanvas(true); }, { min: 0, max: 100, step: 1 });
// Heartbeat methods
this.startHeartbeat = function() {
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
this.heartbeatInterval = setInterval(async () => {
@@ -112,7 +101,6 @@ app.registerExtension({
}
};
// Create Overlay DOM
if (!this.overlayContainer) {
this.overlayContainer = document.createElement('div');
this.overlayContainer.style.cssText = 'position:fixed;top:60px;left:0;right:0;bottom:0;background:rgba(10,10,10,0.96);z-index:999;display:none;flex-direction:row;align-items:stretch;font-family:system-ui,-apple-system,sans-serif;';
@@ -216,7 +204,6 @@ app.registerExtension({
this._resizeObserver.observe(this.overlayCanvasWrapper);
}
// API Listeners
api.addEventListener("rs-collage-start", (event) => { if (event.detail.id != this.id) return; this.pendingEditorData = event.detail; this.openDeferredEditor(); });
api.addEventListener("rs-collage-ready", (event) => { if (event.detail.id != this.id) return; this.pendingEditorData = event.detail; });
api.addEventListener("interrupted", () => {
@@ -229,7 +216,6 @@ app.registerExtension({
this.setDirtyCanvas(true);
});
// Cleanup on node removal
const origOnRemoved = this.onRemoved;
this.onRemoved = function() {
this.stopHeartbeat();
@@ -244,7 +230,6 @@ app.registerExtension({
};
};
// Helpers
nodeType.prototype._syncOverlayUI = function() {
if (!this.advancedMode) return;
this.overlayInputs.opacity.value = Math.round(this.opacity * 100);
@@ -349,7 +334,6 @@ app.registerExtension({
this.computeAndApplyView();
});
// Mouse handlers
this._overlayMouseHandler = (e) => this._handleOverlayEvent(e, e.type === 'mousedown' ? 'down' : e.type === 'mousemove' ? 'move' : 'up');
this._overlayMouseLeaveHandler = () => this.onMouseUp();
this.overlayCanvas.addEventListener('mousedown', this._overlayMouseHandler);
@@ -357,11 +341,9 @@ app.registerExtension({
this.overlayCanvas.addEventListener('mouseup', this._overlayMouseHandler);
this.overlayCanvas.addEventListener('mouseleave', this._overlayMouseLeaveHandler);
// Wheel handler (passive: false is mandatory for preventDefault)
this._overlayWheelHandler = (e) => this._handleOverlayWheel(e);
this.overlayCanvas.addEventListener('wheel', this._overlayWheelHandler, { passive: false });
// Keyboard
this._globalKeyHandler = (e) => {
if (e.key === 'Escape' && activeRSCollageNode === this) {
this.sendTransforms();
@@ -370,7 +352,6 @@ app.registerExtension({
};
window.addEventListener('keydown', this._globalKeyHandler);
// Render loop (clears canvas before drawing to prevent phantom)
this.overlayRenderLoop = () => {
if (!this.advancedMode || !this.overlayCtx) return;
this.overlayCtx.clearRect(0, 0, this.overlayCanvas.width, this.overlayCanvas.height);
@@ -380,7 +361,6 @@ app.registerExtension({
requestAnimationFrame(this.overlayRenderLoop);
};
// Dedicated overlay drawing method (separated from node drawing)
nodeType.prototype.drawOverlayCanvas = function(ctx) {
if (!this.isEditing || !this.backgroundImage) {
ctx.fillStyle = "#888";
@@ -481,7 +461,6 @@ app.registerExtension({
this.featherCenter = { x: 0.5, y: 0.5 };
this.previewDirty = true;
// Sync widget values when opening editor
const syncWidgetValue = (widgetName, value) => {
const widget = this.widgets?.find(w => w.name === widgetName);
if (widget) {
@@ -652,8 +631,6 @@ app.registerExtension({
};
nodeType.prototype.sendTransforms = async function() {
// Инвертируем координаты центра размытия при флипе,
// т.к. в Python feather применяется к уже отражённому изображению
const fcx = this.overlay.flipH ? 1.0 - this.featherCenter.x : this.featherCenter.x;
const fcy = this.overlay.flipV ? 1.0 - this.featherCenter.y : this.featherCenter.y;
@@ -710,7 +687,6 @@ app.registerExtension({
return { cSize, rectX, rectY };
};
// Node foreground drawing (ONLY for normal mode)
nodeType.prototype.onDrawForeground = function(ctx) {
if (this.advancedMode) {
ctx.clearRect(0, 0, this.size[0], this.size[1]);
@@ -863,7 +839,6 @@ app.registerExtension({
}
});
// Глобальный обработчик закрытия вкладки
window.addEventListener("beforeunload", () => {
const nodes = (app.graph && app.graph._nodes)
? app.graph._nodes.filter(n => n.type === "RSCollage")
-2
View File
@@ -154,7 +154,6 @@ app.registerExtension({
if ((this.flags && this.flags.collapsed) || (!this.rs_img1 && !this.rs_img2)) return r;
// Сохраняем состояние Canvas, чтобы не сломать нативную отрисовку LiteGraph
ctx.save();
const getVal = (name, def) => {
@@ -316,7 +315,6 @@ app.registerExtension({
this.rs_resetAllRect = { x: padding, y: resetAllY, w: buttonWidth, h: resetAllHeight };
// Восстанавливаем состояние Canvas, чтобы не сломать нативную отрисовку LiteGraph
ctx.restore();
return r;
-3
View File
@@ -39,13 +39,11 @@ app.registerExtension({
}
};
// ⚠️ Сбрасываем статус ПЕРЕД выполнением
node.onExecuted = function(message) {
this.currentStatus = "✅ Inserted";
this.setDirtyCanvas(true, true);
};
// ⚠️ Обновляем статус при запуске workflow
const onDrawForeground = node.onDrawForeground;
node.onDrawForeground = function(ctx) {
if (!this.flags.collapsed) {
@@ -64,7 +62,6 @@ app.registerExtension({
};
},
// ⚠️ Глобальный слушатель начала выполнения
setup(app) {
app.api.addEventListener("executing", (event) => {
const nodeId = event.detail;
-2
View File
@@ -11,7 +11,6 @@ app.registerExtension({
const result = onNodeCreated ? onNodeCreated.apply(this) : undefined;
initCustomPresets(this);
// === ДОБАВЛЯЕМ ОГРАНИЧЕНИЕ МИНИМАЛЬНОГО РАЗМЕРА ===
const minWidth = 240;
const minHeight = 310;
const origOnResize = this.onResize;
@@ -25,7 +24,6 @@ app.registerExtension({
this.setDirtyCanvas(true, true);
}
};
// ================================================
return result;
};
-18
View File
@@ -31,7 +31,6 @@ function setUIActive(dom, isActive) {
const pointerEvents = isActive ? "auto" : "none";
const filter = isActive ? "none" : "grayscale(100%)";
// Кнопки пресетов
dom.savePresetBtn.style.opacity = opacity;
dom.savePresetBtn.style.pointerEvents = pointerEvents;
dom.savePresetBtn.style.filter = filter;
@@ -40,38 +39,32 @@ function setUIActive(dom, isActive) {
dom.applyPresetBtn.style.pointerEvents = pointerEvents;
dom.applyPresetBtn.style.filter = filter;
// Кнопки соотношений (chipBtns)
for (const btn of dom.chipBtns) {
btn.style.opacity = opacity;
btn.style.pointerEvents = pointerEvents;
btn.style.filter = filter;
}
// Кнопки snap
for (const btn of dom.snapBtns) {
btn.style.opacity = opacity;
btn.style.pointerEvents = pointerEvents;
btn.style.filter = filter;
}
// Кнопка замка
dom.arBtn.style.opacity = opacity;
dom.arBtn.style.pointerEvents = pointerEvents;
dom.arBtn.style.filter = filter;
// Кнопка Reset
dom.resetBtn.style.opacity = opacity;
dom.resetBtn.style.pointerEvents = pointerEvents;
dom.resetBtn.style.filter = filter;
// Поля ввода W/H
dom.wInput.style.opacity = opacity;
dom.wInput.style.pointerEvents = pointerEvents;
dom.hInput.style.opacity = opacity;
dom.hInput.style.pointerEvents = pointerEvents;
// Цвета (mask)
dom.maskColorInput.picker.style.opacity = opacity;
dom.maskColorInput.picker.style.pointerEvents = pointerEvents;
dom.maskColorInput.hexInput.style.opacity = opacity;
@@ -79,7 +72,6 @@ function setUIActive(dom, isActive) {
dom.maskColorInput.swatch.style.opacity = opacity;
dom.maskColorInput.swatch.style.pointerEvents = pointerEvents;
// Цвета (bg)
dom.bgColorInput.picker.style.opacity = opacity;
dom.bgColorInput.picker.style.pointerEvents = pointerEvents;
dom.bgColorInput.hexInput.style.opacity = opacity;
@@ -87,20 +79,17 @@ function setUIActive(dom, isActive) {
dom.bgColorInput.swatch.style.opacity = opacity;
dom.bgColorInput.swatch.style.pointerEvents = pointerEvents;
// Batch кнопка
dom.batchBtn.style.opacity = opacity;
dom.batchBtn.style.pointerEvents = pointerEvents;
dom.batchBtn.style.filter = filter;
}
function resetNodeState(st, dom, widgets) {
// Сохраняем пользовательские цвета перед сбросом, чтобы не терять выбор
const savedMaskColor = st.maskColor;
const savedBgColor = st.bgColor;
Object.assign(st, createState());
// Восстанавливаем пользовательские цвета
st.maskColor = savedMaskColor;
st.bgColor = savedBgColor;
@@ -147,7 +136,6 @@ function initLayout(st, wrapEl) {
st.cr = srcToCr(def, st.sf, st.scale);
st.cropAR = defW / defH;
// Output resolution теперь всегда 0 (используем размер рамки)
st.outW = 0;
st.outH = 0;
st.arLocked = false;
@@ -238,7 +226,6 @@ function buildUI() {
const snapBtns = SNAP_DEFS.map(([key, label], i) => { const isFirst = i===0, isLast = i===SNAP_DEFS.length-1; const radius = isFirst ? "4px 0 0 4px" : (isLast ? "0 4px 4px 0" : "0"); const bl = isFirst ? "1px solid #444" : "none"; const b = mkEl("button", `padding:2px 7px;font-size:10px;border:1px solid #444;border-left:${bl};border-radius:${radius};background:#2a2a2a;color:#bbb;cursor:pointer;white-space:nowrap;`); b.textContent = label; b.dataset.snap = key; return b; });
snapRow.append(snapLabel, ...snapBtns);
// Удален виджет output_resolution из UI
ctrl.append(cropSizeRow, presetRow, snapRow);
const maskColorInput = mkColorInput("mask:", "#ff0000");
@@ -274,7 +261,6 @@ function render(st, dom) {
setMask(dom.maskTop, cr.x, cr.y, cr.w, Math.max(0, iy1 - cr.y)); setMask(dom.maskBot, cr.x, iy2, cr.w, Math.max(0, cr.y + cr.h - iy2)); setMask(dom.maskLeft, cr.x, iy1, Math.max(0, ix1 - cr.x), iy2 - iy1); setMask(dom.maskRight, ix2, iy1, Math.max(0, cr.x + cr.w - ix2), iy2 - iy1);
const padT = Math.max(0, -s.y), padB = Math.max(0, s.y + s.h - srcH), padL = Math.max(0, -s.x), padR = Math.max(0, s.x + s.w - srcW);
const outW = Math.round(s.w), outH = Math.round(s.h);
// Выходной размер теперь всегда равен размеру рамки (0 означает авто в бэкенде)
dom.sizeLabel.textContent = `${outW} × ${outH}`;
dom.sizeLabel.style.display = "block"; dom.sizeLabel.style.left = (cr.x + cr.w / 2) + "px"; dom.sizeLabel.style.top = (cr.y + cr.h - 18) + "px";
const showPad = (el, text, show, cx, cy) => { el.textContent = text; el.style.display = show ? "block" : "none"; if (show) { el.style.left = cx + "px"; el.style.top = cy + "px"; el.style.transform = "translate(-50%,-50%)"; } };
@@ -293,7 +279,6 @@ function render(st, dom) {
function fitCropInView(st, dom) { const wrapW = dom.wrap.clientWidth, wrapH = dom.wrap.clientHeight; if (wrapW <= 0 || wrapH <= 0) return; const pad = MARGIN; const bx1 = Math.min(st.sf.x, st.cr.x), by1 = Math.min(st.sf.y, st.cr.y), bx2 = Math.max(st.sf.x + st.sf.w, st.cr.x + st.cr.w), by2 = Math.max(st.sf.y + st.sf.h, st.cr.y + st.cr.h); const bw = bx2 - bx1, bh = by2 - by1; const fitZoom = Math.min((wrapW - pad * 2) / bw, (wrapH - pad * 2) / bh); const needsZoomOut = fitZoom < st.view.zoom; const z = needsZoomOut ? Math.max(0.15, fitZoom) : st.view.zoom; const scL = bx1 * z + st.view.panX, scT = by1 * z + st.view.panY, scR = bx2 * z + st.view.panX, scB = by2 * z + st.view.panY; const outOfBounds = scL < 0 || scT < 0 || scR > wrapW || scB > wrapH; if (!needsZoomOut && !outOfBounds) return; st.view.zoom = z; st.view.panX = (wrapW - bw * z) / 2 - bx1 * z; st.view.panY = (wrapH - bh * z) / 2 - by1 * z; }
function syncWidgets(st, widgets, node) { const s = quantizeSrc(crToSrc(st.cr, st.sf, st.scale)); const hex = st.maskColor.replace("#", ""); const mR = parseInt(hex.substring(0,2), 16), mG = parseInt(hex.substring(2,4), 16), mB = parseInt(hex.substring(4,6), 16);
// outW/outH всегда 0, чтобы бэкенд брал размеры рамки
if (widgets.cropState) widgets.cropState.value = `${s.x},${s.y},${s.w},${s.h},0,0,${mR},${mG},${mB}`;
if (node.graph) node.graph.setDirtyCanvas(true, true); }
function setArLocked(st, dom, locked) { st.arLocked = locked; if (locked && st.cr.h > 0) st.cropAR = st.cr.w / st.cr.h; dom.arBtn.textContent = locked ? "🔒" : "🔓"; dom.arBtn.style.border = `1px solid ${locked ? "#99c0ee" : "#444"}`; dom.arBtn.style.background = locked ? "#1a3a5a" : "#2a2a2a"; dom.arBtn.style.color = locked ? "#aadaff" : "#bbb"; dom.arBtn._active = locked; }
@@ -324,7 +309,6 @@ function wireInteractions(st, dom, widgets, node, nodeId) {
const s = quantizeSrc(crToSrc(st.cr, st.sf, st.scale));
const hex = st.maskColor.replace("#", "");
const mR = parseInt(hex.substring(0,2), 16), mG = parseInt(hex.substring(2,4), 16), mB = parseInt(hex.substring(4,6), 16);
// outW/outH всегда 0
const presetData = { crop_state: `${s.x},${s.y},${s.w},${s.h},0,0,${mR},${mG},${mB}`, maskColor: st.maskColor, bgColor: st.bgColor, outW: 0, outH: 0, cropAR: st.cropAR, arLocked: st.arLocked };
fetch("/rs_outpaint/save_preset", {
method: "POST",
@@ -444,10 +428,8 @@ function wireInteractions(st, dom, widgets, node, nodeId) {
setArLocked(st, dom, true);
st.cropAR = targetAR;
// Рассчитываем новые размеры от ИСХОДНИКА
const { w: newW, h: newH } = calcSizeByRatio(st.srcW, st.srcH, targetAR);
// Центрируем относительно ИСХОДНОГО изображения
const centerX = st.srcW / 2;
const centerY = st.srcH / 2;
-1
View File
@@ -537,7 +537,6 @@ app.registerExtension({
const btnCancel = makeBtn("❌ CANCEL", "#dc3545", () => { this.cancelEditing(); this._toggleAdvancedMode(); });
buttonContainer.appendChild(btnCancel);
// TEXT секция (всегда раскрыта, без чекбокса)
const textHeader = document.createElement('div');
textHeader.style.cssText = 'display:flex;align-items:center;gap:8px;padding:8px 0;';
const textChevron = document.createElement('span');
-4
View File
@@ -6,7 +6,6 @@ let pendingDeleteName = null;
const styleBlock = document.createElement('style');
styleBlock.innerHTML = `
/* Стили ТОЛЬКО для кастомного textarea ноды RSPrompts */
.rs-custom-textarea {
flex: 1;
width: 100%;
@@ -23,21 +22,18 @@ styleBlock.innerHTML = `
box-sizing: border-box;
}
/* Светлая тема ComfyUI (классический UI) */
body.light-mode .rs-custom-textarea {
background: #fff;
color: #000;
border-color: #ccc;
}
/* Светлая тема нового UI ComfyUI */
html[data-theme="light"] .rs-custom-textarea {
background: #fff;
color: #000;
border-color: #ccc;
}
/* Оверлей ожидания */
.rs-waiting-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
-22
View File
@@ -12,7 +12,6 @@ app.registerExtension({
nodeType.prototype.onNodeCreated = function() {
const result = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
// Initialize data
this.data = {
active_csv_file: "",
styles: [],
@@ -29,7 +28,6 @@ app.registerExtension({
const self = this;
// Find hidden widget
this.hiddenWidget = this.widgets.find(w => w.name === "node_data");
if (this.hiddenWidget) {
this.hiddenWidget.hidden = true;
@@ -41,15 +39,12 @@ app.registerExtension({
this.hiddenWidget.element.style.pointerEvents = "none";
}
// CRITICAL FIX: Load data from widget value
try {
const savedData = JSON.parse(this.hiddenWidget.value || "{}");
if (savedData && typeof savedData === 'object') {
// Restore saved data, but keep csv_list if empty
this.data = {
...this.data,
...savedData,
// Ensure csv_list doesn't override with empty from save
csv_list: this.data.csv_list.length > 0 ? this.data.csv_list : savedData.csv_list || []
};
}
@@ -57,14 +52,12 @@ app.registerExtension({
console.error("[Rayko] Error loading saved data", e);
}
// Override serializeValue to always return current data
this.hiddenWidget.serializeValue = () => {
this.syncData();
return JSON.stringify(this.data);
};
}
// Hide all other widgets
if (this.widgets) {
this.widgets.forEach(w => {
if (w.name !== "node_data") {
@@ -75,7 +68,6 @@ app.registerExtension({
this.setSize([this.targetWidth, 300]);
// --- Core Data Loader ---
this.loadCSVList = async function() {
try {
const response = await api.fetchApi("/rayko_get_csv_files");
@@ -94,7 +86,6 @@ app.registerExtension({
}
};
// --- Helper Drawing Methods ---
this.drawSeparator = function(ctx, text, x, y, w, h) {
ctx.fillStyle = "#444";
ctx.fillRect(x, y + 8, w, 1);
@@ -155,7 +146,6 @@ app.registerExtension({
ctx.fillText(label, x + w/2 + 8, y + h/2);
};
// --- Main Rendering ---
this.onDrawForeground = function(ctx, visibleRect) {
this.clickZones = [];
const startY = 80;
@@ -234,7 +224,6 @@ app.registerExtension({
}
};
// --- Input Handling ---
this.onMouseDown = function(e, pos, canvas) {
if (!this.clickZones.length) return false;
@@ -273,7 +262,6 @@ app.registerExtension({
return false;
};
// --- Popup: CSV Selector ---
this.showCSVSelector = function(clickEvent) {
const list = self.data.csv_list || [];
if (!list.length) return;
@@ -324,7 +312,6 @@ app.registerExtension({
document.addEventListener("keydown", closeEsc, true);
};
// --- Upload Logic ---
this.uploadCSVFile = async function() {
const fileInput = document.createElement("input");
fileInput.type = "file";
@@ -361,7 +348,6 @@ app.registerExtension({
fileInput.click();
};
// --- Style Selector Trigger ---
this.showStyleSelector = async function(clickEvent) {
if (!self.data.active_csv_file || self.data.active_csv_file === "No CSV files") {
alert("⚠️ Upload CSV first!");
@@ -421,7 +407,6 @@ app.registerExtension({
}
};
// --- Popup: Style Tree Menu ---
this.showStyleTreeMenu = function(tree, expandedFolders, fixedPosition) {
const currentScroll = self.menuScrollPosition || 0;
@@ -551,7 +536,6 @@ app.registerExtension({
document.addEventListener("keydown", closeEsc, true);
};
// --- Core Logic ---
this.addStyleRow = function(styleFullName, styleDisplayName) {
const exists = self.data.styles.some(row => row.name === styleFullName && row.file === self.data.active_csv_file);
if (exists) { alert("⚠️ Already added!"); return; }
@@ -567,7 +551,6 @@ app.registerExtension({
if (this.hiddenWidget) {
this.hiddenWidget.value = JSON.stringify(this.data);
}
// Mark graph as changed to trigger auto-save
if (this.graph) {
this.graph.changeTracker?.dispatchEvent(new Event("change"));
}
@@ -581,7 +564,6 @@ app.registerExtension({
}
};
// CRITICAL: Override serialize method to ensure data is saved
const onSerialize = this.onSerialize;
this.onSerialize = function(o) {
self.syncData();
@@ -590,19 +572,16 @@ app.registerExtension({
}
};
// CRITICAL: Override onConfigure to restore data after load
const onConfigure = this.onConfigure;
this.onConfigure = function(o) {
if (onConfigure) {
onConfigure.apply(this, arguments);
}
// Restore data from widget after configuration
if (this.hiddenWidget && this.hiddenWidget.value) {
try {
const restoredData = JSON.parse(this.hiddenWidget.value);
if (restoredData) {
this.data = { ...this.data, ...restoredData };
// Don't lose csv_list if empty
if (!this.data.csv_list.length && restoredData.csv_list) {
this.data.csv_list = restoredData.csv_list;
}
@@ -615,7 +594,6 @@ app.registerExtension({
}
};
// Load CSV list after everything is set up
this.loadCSVList();
return result;
-14
View File
@@ -15,21 +15,17 @@ app.registerExtension({
const result = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
const node = this;
// Инициализация данных
node.data = {
selected_image: "",
spline_coords: "[]"
};
// Настройка размеров
node.targetWidth = 450;
node.setSize([node.targetWidth, 600]);
// Находим скрытые виджеты
const imageWidget = node.widgets?.find(w => w.name === "image");
const coordsWidget = node.widgets?.find(w => w.name === "coordinates");
// Скрываем виджеты
if (imageWidget) {
imageWidget.hidden = true;
if (imageWidget.element) imageWidget.element.style.display = "none";
@@ -39,11 +35,9 @@ app.registerExtension({
if (coordsWidget.element) coordsWidget.element.style.display = "none";
}
// Загружаем сохраненные данные из виджетов
if (imageWidget && imageWidget.value) node.data.selected_image = imageWidget.value;
if (coordsWidget && coordsWidget.value) node.data.spline_coords = coordsWidget.value;
// Переопределяем serializeValue для виджетов
if (imageWidget) {
imageWidget.serializeValue = () => node.data.selected_image;
}
@@ -51,14 +45,12 @@ app.registerExtension({
coordsWidget.serializeValue = () => node.data.spline_coords;
}
// Состояние
node.image = new Image();
node.imageLoaded = false;
node.imageLoading = false;
node.buttons = [];
node.imageList = [];
// Кнопки
node.buttons = [
{ label: "🎨 IMAGE", color: "#2196F3", callback: () => node.showImageSelector(), hover: false },
{ label: "🖼️ UPLOAD", color: "#4CAF50", callback: () => node.triggerFileUpload(), hover: false },
@@ -148,12 +140,9 @@ app.registerExtension({
return false;
};
// ✅ НОВОЕ: Глобальный перехват дропа с проверкой координат ноды
// Это решает проблему, когда _overlayCanvas скрыт (display: none)
const handleCanvasDrop = (e) => {
if (!e.dataTransfer || !e.dataTransfer.files.length) return;
// Проверяем, находится ли курсор над этой конкретной нодой
const rect = app.canvas.canvas.getBoundingClientRect();
const ds = app.canvas.ds;
const nodeX = rect.left + ((node.pos[0] + ds.offset[0]) * ds.scale);
@@ -189,11 +178,9 @@ app.registerExtension({
e.clientY >= nodeY && e.clientY <= nodeY + nodeH) {
e.preventDefault();
e.stopPropagation();
// Визуальный фидбек можно добавить здесь, если захочется
}
};
// Регистрируем слушатели с capture: true, чтобы перехватить событие ДО глобального обработчика ComfyUI
app.canvas.canvas.addEventListener('dragover', handleCanvasDragOver, { capture: true });
app.canvas.canvas.addEventListener('drop', handleCanvasDrop, { capture: true });
@@ -621,7 +608,6 @@ app.registerExtension({
const menu = document.querySelector('.spline-image-menu');
if (menu) menu.remove();
// ✅ НОВОЕ: Очистка глобальных слушателей при удалении ноды (предотвращает утечки памяти)
app.canvas.canvas.removeEventListener('dragover', handleCanvasDragOver, { capture: true });
app.canvas.canvas.removeEventListener('drop', handleCanvasDrop, { capture: true });