Files

536 lines
19 KiB
JavaScript

import { app } from "../../scripts/app.js";
const RS_UPSCALER_NODE_TYPE = "RSUpscaler";
const DEFAULT_SCALE = 2.0;
const SCALE_MIN = 0.1;
const SCALE_MAX = 8.0;
const SCALE_STEP = 0.05;
const NODE_WIDTH = 300;
const NODE_HEIGHT = 130;
function getScopedStyles(scopeId) {
return `
/* === Container === */
.${scopeId} .rs-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.${scopeId} .rs-row label {
flex-shrink: 0;
font-size: 11px;
color: #aaa;
white-space: nowrap;
min-width: 40px;
font-family: sans-serif;
}
.${scopeId} .rs-row select {
flex: 1;
min-width: 0;
background: #222;
color: #fff;
border: 1px solid #444;
border-radius: 4px;
padding: 5px 8px;
font-size: 11px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
appearance: auto;
font-family: sans-serif;
}
.${scopeId} .rs-row select:hover { border-color: #ff9800; }
.${scopeId} .rs-row select:focus { border-color: #ff9800; box-shadow: 0 0 0 1px rgba(255,152,0,0.3); }
/* === Slider row === */
.${scopeId} .rs-slider-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.${scopeId} .rs-slider-row label {
flex-shrink: 0;
font-size: 11px;
color: #aaa;
white-space: nowrap;
min-width: 40px;
font-family: sans-serif;
}
/* Slider: green fill + white handle */
.${scopeId} .rs-slider-row input[type="range"] {
flex: 1;
min-width: 0;
height: 4px;
border-radius: 2px;
outline: none;
cursor: pointer;
-webkit-appearance: none;
appearance: none;
background: linear-gradient(to right,
#4CAF50 0%, #4CAF50 var(--rs-fill, 25%),
#444 var(--rs-fill, 25%), #444 100%);
}
.${scopeId} .rs-slider-row input[type="range"]::-webkit-slider-runnable-track {
height: 4px;
background: transparent;
border-radius: 2px;
}
.${scopeId} .rs-slider-row input[type="range"]::-moz-range-track {
height: 4px;
background: transparent;
border-radius: 2px;
}
.${scopeId} .rs-slider-row input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: #fff;
border: none;
cursor: pointer;
margin-top: -4px;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.${scopeId} .rs-slider-row input[type="range"]::-moz-range-thumb {
width: 12px;
height: 12px;
border-radius: 50%;
background: #fff;
border: none;
cursor: pointer;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
/* === Step buttons +/- === */
.${scopeId} .rs-step-btn {
width: 20px;
height: 26px;
background: #252525;
color: #888;
border: 1px solid #444;
border-radius: 4px;
cursor: pointer;
font-size: 9px;
display: flex;
align-items: center;
justify-content: center;
transition: 0.15s;
flex-shrink: 0;
user-select: none;
line-height: 1;
padding: 0;
}
.${scopeId} .rs-step-btn:hover {
background: #2a2a2a;
color: #fff;
border-color: #ff9800;
}
.${scopeId} .rs-step-btn:active {
background: #333;
}
/* === Value input === */
.${scopeId} .rs-value-input {
width: 35px;
min-width: 35px;
max-width: 35px;
height: 26px;
text-align: center;
background: #252525;
color: #4CAF50;
border: 1px solid #444;
border-radius: 4px;
padding: 0;
font-size: 11px;
cursor: pointer;
font-weight: 600;
transition: 0.15s;
font-family: sans-serif;
font-variant-numeric: tabular-nums;
flex-shrink: 0;
overflow: hidden;
white-space: nowrap;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.${scopeId} .rs-value-input:hover {
border-color: #ff9800;
color: #fff;
}
`;
}
const POPUP_STYLES = `
.rs-value-popup {
position: fixed;
z-index: 10004;
background: #1a1a1a;
border: 1px solid #444;
border-radius: 6px;
padding: 8px 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
display: flex;
align-items: center;
gap: 8px;
}
.rs-value-popup input[type="number"] {
width: 80px;
background: #222;
color: #fff;
border: 1px solid #444;
border-radius: 4px;
padding: 6px 10px;
font-size: 12px;
outline: none;
font-family: sans-serif;
text-align: center;
-webkit-appearance: auto !important;
-moz-appearance: auto !important;
appearance: auto !important;
}
.rs-value-popup input[type="number"]:focus {
border-color: #ff9800;
}
/* Force spinner arrows visible in Chrome/Edge */
.rs-value-popup input[type="number"]::-webkit-inner-spin-button,
.rs-value-popup input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: inner-spin-button !important;
opacity: 1 !important;
height: 28px;
}
/* Firefox */
.rs-value-popup input[type="number"] {
-moz-appearance: auto !important;
}
.rs-value-popup button {
background: #4CAF50;
color: #fff;
border: none;
border-radius: 4px;
padding: 6px 12px;
font-size: 12px;
cursor: pointer;
font-family: sans-serif;
}
.rs-value-popup button:hover {
background: #43A047;
}
`;
let popupStylesInjected = false;
function injectPopupStyles() {
if (popupStylesInjected) return;
const style = document.createElement("style");
style.textContent = POPUP_STYLES;
document.head.appendChild(style);
popupStylesInjected = true;
}
let upscaleModelsCache = null;
async function fetchUpscaleModels() {
if (upscaleModelsCache) return upscaleModelsCache;
try {
const resp = await fetch("/api/object_info/RSUpscaler");
const data = await resp.json();
upscaleModelsCache = data.RSUpscaler.input.required.upscale_model[0];
} catch (e) {
console.warn("[RS Upscaler] Failed to fetch models list:", e);
upscaleModelsCache = [];
}
return upscaleModelsCache;
}
function clampScale(v) {
return Math.max(SCALE_MIN, Math.min(SCALE_MAX, v));
}
function formatScale(v) {
return parseFloat(v).toFixed(2);
}
app.registerExtension({
name: "RaykoStudio.RSUpscaler",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== RS_UPSCALER_NODE_TYPE) return;
injectPopupStyles();
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated?.apply(this, arguments);
const scopeId = `rs-upscaler-${this.id}`;
if (!document.getElementById(`style-${scopeId}`)) {
const style = document.createElement("style");
style.id = `style-${scopeId}`;
style.textContent = getScopedStyles(scopeId);
document.head.appendChild(style);
}
this.setSize([NODE_WIDTH, NODE_HEIGHT]);
this.onResize = function (size) {
if (size[0] < NODE_WIDTH) size[0] = NODE_WIDTH;
if (size[1] < NODE_HEIGHT) size[1] = NODE_HEIGHT;
};
const widgetsToHide = ["upscale_model", "upscale_method", "upscale_x"];
this.widgets.forEach((w) => {
if (widgetsToHide.includes(w.name)) {
w.hidden = true;
w.tooltip = "";
w.type = "hidden";
if (w.element) {
w.element.style.display = "none";
w.element.style.pointerEvents = "none";
}
w.computeSize = () => [0, 0];
}
});
const container = document.createElement("div");
container.className = scopeId;
const row1 = document.createElement("div");
row1.className = "rs-row";
const modelLabel = document.createElement("label");
modelLabel.textContent = "Model";
row1.appendChild(modelLabel);
const modelSelect = document.createElement("select");
row1.appendChild(modelSelect);
container.appendChild(row1);
fetchUpscaleModels().then((models) => {
models.forEach((m) => {
const opt = document.createElement("option");
opt.value = m;
opt.textContent = m.replace(/\.(pth|bin|pt|safetensors)$/i, "");
modelSelect.appendChild(opt);
});
const currentModel = this.widgets.find((w) => w.name === "upscale_model")?.value;
if (currentModel) modelSelect.value = currentModel;
});
const row2 = document.createElement("div");
row2.className = "rs-row";
const methodLabel = document.createElement("label");
methodLabel.textContent = "Method";
row2.appendChild(methodLabel);
const methodSelect = document.createElement("select");
const methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"];
methods.forEach((m) => {
const opt = document.createElement("option");
opt.value = m;
opt.textContent = m.charAt(0).toUpperCase() + m.slice(1);
methodSelect.appendChild(opt);
});
row2.appendChild(methodSelect);
container.appendChild(row2);
const row3 = document.createElement("div");
row3.className = "rs-slider-row";
const sliderLabel = document.createElement("label");
sliderLabel.textContent = "Scale";
row3.appendChild(sliderLabel);
const slider = document.createElement("input");
slider.type = "range";
slider.min = String(SCALE_MIN);
slider.max = String(SCALE_MAX);
slider.step = String(SCALE_STEP);
slider.value = String(DEFAULT_SCALE);
row3.appendChild(slider);
const minusBtn = document.createElement("button");
minusBtn.className = "rs-step-btn";
minusBtn.textContent = "◀";
minusBtn.title = "Decrease by " + SCALE_STEP;
row3.appendChild(minusBtn);
const valueDisplay = document.createElement("div");
valueDisplay.className = "rs-value-input";
valueDisplay.textContent = formatScale(DEFAULT_SCALE);
row3.appendChild(valueDisplay);
const plusBtn = document.createElement("button");
plusBtn.className = "rs-step-btn";
plusBtn.textContent = "▶";
plusBtn.title = "Increase by " + SCALE_STEP;
row3.appendChild(plusBtn);
container.appendChild(row3);
const updateFill = () => {
const pct = ((parseFloat(slider.value) - SCALE_MIN) / (SCALE_MAX - SCALE_MIN)) * 100;
slider.style.setProperty("--rs-fill", pct + "%");
};
const setValue = (rawVal) => {
const v = clampScale(Math.round(parseFloat(rawVal) / SCALE_STEP) * SCALE_STEP);
slider.value = String(v);
valueDisplay.textContent = formatScale(v);
updateFill();
syncToNative();
};
const updateFromSlider = () => {
valueDisplay.textContent = formatScale(slider.value);
updateFill();
syncToNative();
};
slider.addEventListener("input", updateFromSlider);
slider.addEventListener("change", updateFromSlider);
minusBtn.addEventListener("click", (e) => {
e.stopPropagation();
setValue(parseFloat(slider.value) - SCALE_STEP);
});
plusBtn.addEventListener("click", (e) => {
e.stopPropagation();
setValue(parseFloat(slider.value) + SCALE_STEP);
});
valueDisplay.addEventListener("click", (e) => {
e.stopPropagation();
const existingPopup = document.querySelector(".rs-value-popup");
if (existingPopup) existingPopup.remove();
const popup = document.createElement("div");
popup.className = "rs-value-popup";
const input = document.createElement("input");
input.type = "number";
input.value = parseFloat(slider.value).toFixed(2);
input.min = String(SCALE_MIN);
input.max = String(SCALE_MAX);
input.step = String(SCALE_STEP);
input.style.cssText = [
"width: 80px",
"background: #222",
"color: #fff",
"border: 1px solid #444",
"border-radius: 4px",
"padding: 6px 10px",
"font-size: 12px",
"outline: none",
"font-family: sans-serif",
"text-align: center",
"-webkit-appearance: auto",
"-moz-appearance: auto",
"appearance: auto",
].join(";") + ";";
const saveBtn = document.createElement("button");
saveBtn.textContent = "OK";
const doSave = () => {
let num = parseFloat(input.value);
if (isNaN(num)) num = DEFAULT_SCALE;
setValue(num);
popup.remove();
};
saveBtn.onclick = (ev) => { ev.stopPropagation(); ev.preventDefault(); doSave(); };
input.onkeydown = (ev) => {
if (ev.key === "Enter") { ev.preventDefault(); doSave(); }
if (ev.key === "Escape") { popup.remove(); }
};
popup.appendChild(input);
popup.appendChild(saveBtn);
document.body.appendChild(popup);
const rect = valueDisplay.getBoundingClientRect();
const popupWidth = popup.offsetWidth || 160;
const popupHeight = popup.offsetHeight || 40;
let leftPos = rect.right + 8;
let topPos = rect.top + (rect.height - popupHeight) / 2;
if (leftPos + popupWidth > window.innerWidth - 10) {
leftPos = rect.left - popupWidth - 8;
}
if (topPos < 10) topPos = 10;
if (topPos + popupHeight > window.innerHeight - 10) {
topPos = window.innerHeight - popupHeight - 10;
}
popup.style.left = leftPos + "px";
popup.style.top = topPos + "px";
setTimeout(() => { input.focus(); input.select(); }, 50);
setTimeout(() => {
const closeHandler = (ev) => {
if (!popup.contains(ev.target)) {
popup.remove();
document.removeEventListener("mousedown", closeHandler);
}
};
document.addEventListener("mousedown", closeHandler);
}, 100);
});
this.addDOMWidget("rs_custom_widgets", "div", container);
const syncToNative = () => {
const mw = this.widgets.find((w) => w.name === "upscale_model");
const mtw = this.widgets.find((w) => w.name === "upscale_method");
const xw = this.widgets.find((w) => w.name === "upscale_x");
if (mw) mw.value = modelSelect.value;
if (mtw) mtw.value = methodSelect.value;
if (xw) xw.value = parseFloat(slider.value);
this.graph?.setDirtyCanvas(true, true);
};
modelSelect.addEventListener("change", syncToNative);
methodSelect.addEventListener("change", syncToNative);
setTimeout(() => {
syncToNative();
updateFill();
}, 100);
const onConfigure = this.onConfigure;
this.onConfigure = function (info) {
const r = onConfigure?.apply(this, arguments);
setTimeout(() => {
const mw = this.widgets.find((w) => w.name === "upscale_model");
const mtw = this.widgets.find((w) => w.name === "upscale_method");
const xw = this.widgets.find((w) => w.name === "upscale_x");
if (mw?.value) modelSelect.value = mw.value;
if (mtw?.value) methodSelect.value = mtw.value;
if (xw?.value != null) {
const v = clampScale(parseFloat(xw.value));
slider.value = String(v);
valueDisplay.textContent = formatScale(v);
} else {
slider.value = String(DEFAULT_SCALE);
valueDisplay.textContent = formatScale(DEFAULT_SCALE);
}
updateFill();
syncToNative();
}, 50);
return r;
};
const onRemoved = this.onRemoved;
this.onRemoved = function () {
const styleEl = document.getElementById(`style-${scopeId}`);
if (styleEl) styleEl.remove();
if (onRemoved) onRemoved.apply(this, arguments);
};
return result;
};
},
});