Files

373 lines
18 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import { ComfyWidgets } from '../../../scripts/widgets.js';
import {
hideWidget,
showWidget,
} from "./node_utils.js";
app.registerExtension({
name: "vrch.GamepadControlLoader",
async nodeCreated(node) {
if (node.comfyClass === "VrchGamepadLoaderNode") {
// Obtain required widgets
const indexWidget = node.widgets.find(w => w.name === "index");
const nameWidget = node.widgets.find(w => w.name === "name");
const refreshIntervalWidget = node.widgets.find(w => w.name === "refresh_interval");
const rawDataWidget = node.widgets.find(w => w.name === "raw_data");
const debugWidget = node.widgets.find(w => w.name === "debug");
// Set up the timer to fetch gamepad data
let timerId = null;
// Function to update widget visibility
const updateWidgetVisibility = () => {
if (debugWidget) {
if (debugWidget.value) {
showWidget(node, rawDataWidget);
} else {
hideWidget(node, rawDataWidget);
}
}
};
if (debugWidget) {
debugWidget.callback = (value) => {
updateWidgetVisibility();
};
}
if (rawDataWidget) {
// Set the initial visibility of the raw data widget
hideWidget(node, rawDataWidget);
}
// Function to extract gamepad data into a serializable object
const extractGamepadData = (gamepad) => {
return {
id: gamepad.id,
index: gamepad.index,
connected: gamepad.connected,
timestamp: gamepad.timestamp,
mapping: gamepad.mapping,
axes: Array.from(gamepad.axes || []),
buttons: Array.from(gamepad.buttons || []).map(btn => ({
pressed: btn.pressed,
touched: btn.touched,
value: btn.value
}))
};
};
// fetch gamepad data from browser gamepad API
const fetchGamepadData = async () => {
try {
const gamepads = navigator.getGamepads();
if (gamepads) {
const gamepad = gamepads[indexWidget.value];
// Only log in debug mode with throttling
if (debugWidget && debugWidget.value && Math.random() < 0.1) { // 90% logs are suppressed
console.log(`[VrchGamepadLoaderNode] Gamepad data for index ${indexWidget.value}:`, gamepad);
}
if (gamepad) {
// Always update name for UI responsiveness
if (nameWidget.value !== gamepad.id) {
nameWidget.value = gamepad.id;
}
// Extract gamepad data and convert to JSON string
const gamepadData = extractGamepadData(gamepad);
const dataString = JSON.stringify(gamepadData, null, 2);
// Only update if data actually changed
if (rawDataWidget.value !== dataString) {
rawDataWidget.value = dataString;
}
} else if (nameWidget.value !== "n/a") {
nameWidget.value = "n/a";
if (debugWidget && debugWidget.value) {
rawDataWidget.value = "No gamepad found";
}
}
} else if (nameWidget.value !== "n/a") {
nameWidget.value = "n/a";
if (debugWidget && debugWidget.value) {
rawDataWidget.value = "No gamepad data available";
}
}
} catch (error) {
console.error("[VrchGamepadLoaderNode] Error fetching gamepad data:", error);
}
};
// Function to start or restart the timer with the current refresh interval
const startTimer = () => {
// Clear existing timer if it exists
if (timerId !== null) {
clearInterval(timerId);
timerId = null;
}
// Get the refresh interval value (default to 100ms if not available)
const interval = refreshIntervalWidget ? Math.max(10, refreshIntervalWidget.value) : 100;
// Set up the new timer with the current interval value
timerId = setInterval(() => {
fetchGamepadData();
}, interval);
if (debugWidget && debugWidget.value) {
console.log(`[VrchGamepadLoaderNode] Gamepad data refresh interval set to ${interval}ms`);
}
};
// Add callback to refresh interval widget to update the timer when changed
if (refreshIntervalWidget) {
refreshIntervalWidget.callback = () => {
startTimer();
};
}
// Initial timer start
startTimer();
// Set a delay initialization for the widget visibility
setTimeout(() => {
updateWidgetVisibility();
}, 1000);
// Clear the timer when the node is removed
const onRemoved = this.onRemoved;
this.onRemoved = function () {
if (timerId) {
clearInterval(timerId);
timerId = null;
}
return onRemoved?.();
};
}
}
});
app.registerExtension({
name: "vrch.XboxControllerNode",
async nodeCreated(node) {
if (node.comfyClass === "VrchXboxControllerNode") {
// Define the SVG template for Xbox controller
const svgTemplate = `
<svg viewBox="0 0 300 200" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto">
<style>
/* Wireframe for buttons and controls, keep body fill */
path:not(.controller-body), rect, circle { stroke: #666; stroke-width: 2; }
/* Default state - no fill */
path:not(.controller-body):not(.pressed),
rect:not(.pressed),
circle:not(.pressed) { fill: none; }
/* Allow JavaScript to control the fill when elements get .pressed class */
.pressed { transition: fill 0.2s ease; }
path.controller-body { fill: #222; stroke: none; }
</style>
<!-- Controller base -->
<path class="controller-body" d="M150,30 C70,30 30,80 30,120 C30,160 70,170 150,170 C230,170 270,160 270,120 C270,80 230,30 150,30 Z" />
<!-- D-pad - moved down 20px -->
<circle id="dpad" cx="90" cy="130" r="10" />
<rect id="dpad-up" x="85" y="100" width="10" height="20" rx="2" />
<rect id="dpad-down" x="85" y="140" width="10" height="20" rx="2" />
<rect id="dpad-left" x="60" y="125" width="20" height="10" rx="2" />
<rect id="dpad-right" x="100" y="125" width="20" height="10" rx="2" />
<!-- Face buttons -->
<circle id="button-a" cx="210" cy="110" r="10" />
<circle id="button-b" cx="230" cy="90" r="10" />
<circle id="button-x" cx="190" cy="90" r="10" />
<circle id="button-y" cx="210" cy="70" r="10" />
<!-- Bumpers -->
<rect id="lb" x="70" y="35" width="40" height="10" rx="5" />
<rect id="rb" x="190" y="35" width="40" height="10" rx="5" />
<!-- Triggers -->
<rect id="lt" x="70" y="10" width="40" height="20" rx="5" />
<rect id="rt" x="190" y="10" width="40" height="20" rx="5" />
<!-- Sticks - right stick moved down 20px -->
<circle id="left-stick-base" cx="90" cy="70" r="15" />
<circle id="left-stick" cx="90" cy="70" r="10" />
<circle id="right-stick-base" cx="190" cy="140" r="15" />
<circle id="right-stick" cx="190" cy="140" r="10" />
<!-- Center buttons -->
<rect id="button-back" x="120" y="70" width="25" height="10" rx="2" />
<rect id="button-start" x="150" y="70" width="25" height="10" rx="2" />
<circle id="button-guide" cx="148" cy="100" r="12" />
</svg>`;
// Create and register a container for the SVG as a DOM widget
node.svgContainer = document.createElement("div");
node.svgContainer.style.width = "100%";
node.svgContainer.style.height = "250px";
node.svgContainer.style.overflow = "hidden";
node.svgContainer.innerHTML = svgTemplate;
const widget = node.addDOMWidget("svg_container", "Controller", node.svgContainer);
// Override the computeSize method to force correct sizing
widget.computeSize = function(width) {
return [width, 250]; // Force height to be 48px
};
// Function to update SVG elements based on controller state
node.updateControllerVisual = function(buttonsBoolean, buttonsFloat, leftStick, rightStick) {
if (!node.svgContainer) return;
const svg = node.svgContainer.querySelector("svg");
if (!svg) return;
// Standard Xbox mapping (may need adjustment based on browser/device)
const BUTTON_A = 0;
const BUTTON_B = 1;
const BUTTON_X = 2;
const BUTTON_Y = 3;
const BUTTON_LB = 4;
const BUTTON_RB = 5;
const BUTTON_LT = 6;
const BUTTON_RT = 7;
const BUTTON_BACK = 8;
const BUTTON_START = 9;
const BUTTON_L_STICK = 10;
const BUTTON_R_STICK = 11;
const BUTTON_DPAD_UP = 12;
const BUTTON_DPAD_DOWN = 13;
const BUTTON_DPAD_LEFT = 14;
const BUTTON_DPAD_RIGHT = 15;
const BUTTON_GUIDE = 16;
// Update face buttons
this.updateButtonElement(svg, "#button-a", buttonsBoolean[BUTTON_A], buttonsFloat[BUTTON_A], "#0f0", "#0a0");
this.updateButtonElement(svg, "#button-b", buttonsBoolean[BUTTON_B], buttonsFloat[BUTTON_B], "#f00", "#a00");
this.updateButtonElement(svg, "#button-x", buttonsBoolean[BUTTON_X], buttonsFloat[BUTTON_X], "#00f", "#00a");
this.updateButtonElement(svg, "#button-y", buttonsBoolean[BUTTON_Y], buttonsFloat[BUTTON_Y], "#ff0", "#aa0");
// Update bumpers and triggers
this.updateButtonElement(svg, "#lb", buttonsBoolean[BUTTON_LB], buttonsFloat[BUTTON_LB], "#666", "#999");
this.updateButtonElement(svg, "#rb", buttonsBoolean[BUTTON_RB], buttonsFloat[BUTTON_RB], "#666", "#999");
this.updateButtonElement(svg, "#lt", buttonsBoolean[BUTTON_LT], buttonsFloat[BUTTON_LT], "#666", "#999");
this.updateButtonElement(svg, "#rt", buttonsBoolean[BUTTON_RT], buttonsFloat[BUTTON_RT], "#666", "#999");
// Update D-pad
this.updateButtonElement(svg, "#dpad-up", buttonsBoolean[BUTTON_DPAD_UP], buttonsFloat[BUTTON_DPAD_UP], "#666", "#999");
this.updateButtonElement(svg, "#dpad-down", buttonsBoolean[BUTTON_DPAD_DOWN], buttonsFloat[BUTTON_DPAD_DOWN], "#666", "#999");
this.updateButtonElement(svg, "#dpad-left", buttonsBoolean[BUTTON_DPAD_LEFT], buttonsFloat[BUTTON_DPAD_LEFT], "#666", "#999");
this.updateButtonElement(svg, "#dpad-right", buttonsBoolean[BUTTON_DPAD_RIGHT], buttonsFloat[BUTTON_DPAD_RIGHT], "#666", "#999");
// Update center buttons
this.updateButtonElement(svg, "#button-back", buttonsBoolean[BUTTON_BACK], buttonsFloat[BUTTON_BACK], "#666", "#999");
this.updateButtonElement(svg, "#button-start", buttonsBoolean[BUTTON_START], buttonsFloat[BUTTON_START], "#666", "#999");
this.updateButtonElement(svg, "#button-guide", buttonsBoolean[BUTTON_GUIDE], buttonsFloat[BUTTON_GUIDE], "#666", "#999");
// Update analog sticks
if (leftStick && leftStick.length >= 2) {
const leftStickElem = svg.querySelector("#left-stick");
if (leftStickElem) {
const baseX = 90;
const baseY = 70;
const maxOffset = 10;
const newX = baseX + (leftStick[0] * maxOffset);
const newY = baseY + (leftStick[1] * maxOffset);
leftStickElem.setAttribute("cx", newX);
leftStickElem.setAttribute("cy", newY);
// Highlight left stick if pressed
if (buttonsBoolean[BUTTON_L_STICK]) {
leftStickElem.setAttribute("fill", "#999");
leftStickElem.classList.add("pressed");
} else {
leftStickElem.setAttribute("fill", "none");
leftStickElem.classList.remove("pressed");
}
}
}
if (rightStick && rightStick.length >= 2) {
const rightStickElem = svg.querySelector("#right-stick");
if (rightStickElem) {
const baseX = 190;
const baseY = 140;
const maxOffset = 10;
const newX = baseX + (rightStick[0] * maxOffset);
const newY = baseY + (rightStick[1] * maxOffset);
rightStickElem.setAttribute("cx", newX);
rightStickElem.setAttribute("cy", newY);
// Highlight right stick if pressed
if (buttonsBoolean[BUTTON_R_STICK]) {
rightStickElem.setAttribute("fill", "#999");
rightStickElem.classList.add("pressed");
} else {
rightStickElem.setAttribute("fill", "none");
rightStickElem.classList.remove("pressed");
}
}
}
};
// Helper function to update a button element based on its state
node.updateButtonElement = function(svg, selector, isPressed, pressure, defaultColor, activeColor) {
const element = svg.querySelector(selector);
if (!element) return;
// L2/R2 triggers - ID matching in new SVG structure
if (selector === "#lt" || selector === "#rt" || selector === "#L2" || selector === "#R2") {
// triggers pressure-based fill
if (pressure > 0) {
element.setAttribute("fill", activeColor);
element.setAttribute("fill-opacity", pressure);
element.classList.add("pressed");
} else {
element.setAttribute("fill", "none");
element.classList.remove("pressed");
}
}
// Face buttons, DPad, etc.
else if (
selector.includes("button") ||
selector.includes("dpad") ||
selector === "#lb" || selector === "#rb" ||
selector === "#L1" || selector === "#R1" ||
selector.includes("BTop") || selector.includes("BRight") ||
selector.includes("BBottom") || selector.includes("BLeft") ||
selector.includes("DUp") || selector.includes("DRight") ||
selector.includes("DDown") || selector.includes("DLeft") ||
selector.includes("LMeta") || selector.includes("RMeta")
) {
// regular buttons - just toggle fill
if (isPressed) {
element.setAttribute("fill", activeColor);
element.classList.add("pressed");
} else {
element.setAttribute("fill", "none");
element.classList.remove("pressed");
}
}
};
// Hook into the node's onExecuted callback
const originalOnExecuted = node.onExecuted;
node.onExecuted = function(message) {
if (originalOnExecuted) {
originalOnExecuted.call(node, message);
}
// Get the input data from the message
const buttonsBoolean = message?.buttonsBoolean || [];
const buttonsFloat = message?.buttonsFloat || [];
const leftStick = message?.leftStick || [0, 0];
const rightStick = message?.rightStick || [0, 0];
// Update the controller visualization
node.updateControllerVisual(buttonsBoolean, buttonsFloat, leftStick, rightStick);
};
}
}
});