261 lines
7.8 KiB
JavaScript
261 lines
7.8 KiB
JavaScript
import { COLORS } from "../constants.js";
|
|
import { fitString } from "../utils.js";
|
|
import { showSearchOverlay } from "./search_overlay.js";
|
|
import { app } from "/scripts/app.js";
|
|
|
|
// --- Constants ---
|
|
|
|
const ROW_HEIGHT = 20;
|
|
const ROW_MARGIN = 10;
|
|
const WEIGHT_WIDTH = 90;
|
|
const REMOVE_WIDTH = 24;
|
|
const INNER_MARGIN = 4;
|
|
const ARROW_WIDTH = 18;
|
|
|
|
// --- LoraRowWidget ---
|
|
|
|
export class LoraRowWidget {
|
|
constructor(name, loras, defaultWeight, onChange) {
|
|
this.name = name;
|
|
this.type = "custom";
|
|
this.value = { lora: loras[0] ?? "", weight: defaultWeight };
|
|
this.loras = loras;
|
|
this.onChange = onChange;
|
|
this.last_y = 0;
|
|
|
|
// Hit areas: [x, width]
|
|
this._hitLora = [0, 0];
|
|
this._hitWeightDec = [0, 0];
|
|
this._hitWeightVal = [0, 0];
|
|
this._hitWeightInc = [0, 0];
|
|
this._hitRemove = [0, 0];
|
|
|
|
// Drag state
|
|
this._dragging = false;
|
|
this._dragStartX = 0;
|
|
this._dragStartWeight = 0;
|
|
this._hasDragged = false;
|
|
}
|
|
|
|
computeSize() {
|
|
return [0, ROW_HEIGHT];
|
|
}
|
|
|
|
// --- Draw ---
|
|
|
|
draw(ctx, node, width, posY, height) {
|
|
this.last_y = posY;
|
|
const h = height ?? ROW_HEIGHT;
|
|
const midY = posY + h * 0.5;
|
|
const margin = ROW_MARGIN;
|
|
|
|
ctx.save();
|
|
|
|
// Background
|
|
ctx.fillStyle = "#1a1a1a";
|
|
ctx.beginPath();
|
|
ctx.roundRect(margin, posY + 2, width - margin * 2, h - 4, 4);
|
|
ctx.fill();
|
|
|
|
// Remove button zone
|
|
const removeX = width - margin - REMOVE_WIDTH;
|
|
ctx.fillStyle = "#442222";
|
|
ctx.beginPath();
|
|
ctx.roundRect(removeX, posY + 4, REMOVE_WIDTH - 2, h - 8, 3);
|
|
ctx.fill();
|
|
ctx.fillStyle = "#ff6666";
|
|
ctx.font = "12px monospace";
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText("✕", removeX + (REMOVE_WIDTH - 2) / 2, midY);
|
|
this._hitRemove = [removeX, REMOVE_WIDTH - 2];
|
|
|
|
// Weight zone
|
|
const weightX = removeX - INNER_MARGIN - WEIGHT_WIDTH;
|
|
ctx.font = "11px monospace";
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
|
|
// Weight - Left arrow
|
|
ctx.fillStyle = "#2a2a3a";
|
|
ctx.beginPath();
|
|
ctx.roundRect(weightX, posY + 4, ARROW_WIDTH, h - 8, 3);
|
|
ctx.fill();
|
|
ctx.fillStyle = COLORS.text;
|
|
ctx.fillText("◀", weightX + ARROW_WIDTH / 2, midY);
|
|
this._hitWeightDec = [weightX, ARROW_WIDTH];
|
|
|
|
// Weight - Central value
|
|
ctx.fillStyle = "#222233";
|
|
ctx.beginPath();
|
|
ctx.roundRect(
|
|
weightX + ARROW_WIDTH + 2,
|
|
posY + 4,
|
|
WEIGHT_WIDTH - ARROW_WIDTH * 2 - 4,
|
|
h - 8,
|
|
3,
|
|
);
|
|
ctx.fill();
|
|
ctx.fillStyle = COLORS.text;
|
|
ctx.fillText(
|
|
this.value.weight.toFixed(2),
|
|
weightX + WEIGHT_WIDTH / 2,
|
|
midY,
|
|
);
|
|
this._hitWeightVal = [
|
|
weightX + ARROW_WIDTH + 2,
|
|
WEIGHT_WIDTH - ARROW_WIDTH * 2 - 4,
|
|
];
|
|
|
|
// Weight - Right arrow
|
|
ctx.fillStyle = "#2a2a3a";
|
|
ctx.beginPath();
|
|
ctx.roundRect(
|
|
weightX + WEIGHT_WIDTH - ARROW_WIDTH,
|
|
posY + 4,
|
|
ARROW_WIDTH,
|
|
h - 8,
|
|
3,
|
|
);
|
|
ctx.fill();
|
|
ctx.fillStyle = COLORS.text;
|
|
ctx.fillText("▶", weightX + WEIGHT_WIDTH - ARROW_WIDTH / 2, midY);
|
|
this._hitWeightInc = [
|
|
weightX + WEIGHT_WIDTH - ARROW_WIDTH,
|
|
ARROW_WIDTH,
|
|
];
|
|
|
|
// LoRA name zone
|
|
const loraX = margin + INNER_MARGIN;
|
|
const loraWidth = weightX - loraX - INNER_MARGIN;
|
|
ctx.fillStyle = COLORS.text;
|
|
ctx.font = "11px monospace";
|
|
ctx.textAlign = "left";
|
|
ctx.textBaseline = "middle";
|
|
const label = this.value.lora || "None";
|
|
ctx.fillText(fitString(ctx, label, loraWidth), loraX, midY);
|
|
this._hitLora = [loraX, loraWidth];
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
// --- Events ---
|
|
|
|
mouse(event, pos, node) {
|
|
const x = pos[0];
|
|
const type = event.type;
|
|
|
|
// Remove
|
|
if (this._inHit(x, this._hitRemove)) {
|
|
if (type === "pointerdown" || type === "mousedown") {
|
|
this.onChange("remove");
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Weight drag - Left arrow
|
|
if (this._inHit(x, this._hitWeightDec)) {
|
|
if (type === "pointerdown" || type === "mousedown") {
|
|
this.value.weight =
|
|
Math.round((this.value.weight - 0.05) * 100) / 100;
|
|
this.onChange("weight");
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Weight drag - Right arrow
|
|
if (this._inHit(x, this._hitWeightInc)) {
|
|
if (type === "pointerdown" || type === "mousedown") {
|
|
this.value.weight =
|
|
Math.round((this.value.weight + 0.05) * 100) / 100;
|
|
this.onChange("weight");
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Weight drag - Central value: drag + prompt click
|
|
if (this._inHit(x, this._hitWeightVal)) {
|
|
if (type === "pointerdown" || type === "mousedown") {
|
|
this._dragging = true;
|
|
this._dragStartX = event.clientX ?? event.x ?? 0;
|
|
this._dragStartWeight = this.value.weight;
|
|
this._hasDragged = false;
|
|
return true;
|
|
}
|
|
if (
|
|
(type === "pointerup" || type === "mouseup") &&
|
|
!this._hasDragged
|
|
) {
|
|
app.canvas.prompt(
|
|
"Weight",
|
|
this.value.weight,
|
|
(v) => {
|
|
const parsed = parseFloat(v);
|
|
if (!isNaN(parsed)) {
|
|
this.value.weight = Math.round(parsed * 100) / 100;
|
|
this.onChange("weight");
|
|
}
|
|
},
|
|
event,
|
|
);
|
|
this._dragging = false;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (type === "pointermove" || type === "mousemove") {
|
|
if (this._dragging) {
|
|
const clientX = event.clientX ?? event.x ?? 0;
|
|
const delta = (clientX - this._dragStartX) * 0.01;
|
|
if (Math.abs(delta) > 0.005) this._hasDragged = true;
|
|
this.value.weight =
|
|
Math.round((this._dragStartWeight + delta) * 100) / 100;
|
|
this.onChange("weight");
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (type === "pointerup" || type === "mouseup") {
|
|
this._dragging = false;
|
|
}
|
|
|
|
// LoRA name click -> ContextMenu
|
|
if (this._inHit(x, this._hitLora)) {
|
|
if (type === "pointerdown" || type === "mousedown") {
|
|
this._showLoraMenu(event, node);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// --- Utils ---
|
|
|
|
_inHit(x, hit) {
|
|
return x >= hit[0] && x <= hit[0] + hit[1];
|
|
}
|
|
|
|
// --- Menu ---
|
|
|
|
_showLoraMenu(event, node) {
|
|
showSearchOverlay(event, {
|
|
items: this.loras.map((l) => ({ label: l, value: l })),
|
|
placeholder: "Search LoRA...",
|
|
initialValue: this.value.lora,
|
|
maxVisible: 15,
|
|
onFilter: (query, items) =>
|
|
query
|
|
? items.filter((i) =>
|
|
i.label.toLowerCase().includes(query.toLowerCase()),
|
|
)
|
|
: items,
|
|
onSelect: (value) => {
|
|
this.value.lora = value;
|
|
this.onChange("lora");
|
|
if (node.graph) node.graph.setDirtyCanvas(true, true);
|
|
},
|
|
});
|
|
}
|
|
}
|