536 lines
19 KiB
JavaScript
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;
|
|
};
|
|
},
|
|
}); |