- Add code quality tools: flake8, mypy, black, pre-commit - Add .gitattributes for line ending consistency - Add .secrets.baseline for secret scanning - Update GitHub workflows for better CI/CD - Update documentation formatting and examples - Add CLAUDE.md for AI assistant guidance - Add scripts directory for automation tools - Update project configuration in pyproject.toml - Improve type hints and code formatting across all modules - Update test configurations and fixtures
325 lines
13 KiB
JavaScript
325 lines
13 KiB
JavaScript
// ComfyUI-KikoTools - Width Height Selector with Swap Button
|
||
import { app } from "../../scripts/app.js";
|
||
|
||
app.registerExtension({
|
||
name: "comfyassets.WidthHeightSelector",
|
||
async beforeRegisterNodeDef(nodeType, nodeData, _app) {
|
||
if (nodeData.name === "WidthHeightSelector") {
|
||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||
nodeType.prototype.onNodeCreated = function () {
|
||
if (onNodeCreated) onNodeCreated.apply(this, []);
|
||
|
||
// Track button click state for visual feedback
|
||
this.swapButtonPressed = false;
|
||
|
||
// Helper function to extract resolution from formatted preset string
|
||
this.extractResolutionFromPreset = function(presetValue) {
|
||
if (presetValue === "custom") return null;
|
||
|
||
// If it contains formatting metadata, extract the resolution part
|
||
if (presetValue.includes(" - ")) {
|
||
// Format is: "1024×1024 - 1:1 (1.0MP) - SDXL"
|
||
return presetValue.split(" - ")[0];
|
||
}
|
||
|
||
// Otherwise assume it's already a raw resolution
|
||
return presetValue;
|
||
};
|
||
|
||
// Override preset callback to update width/height widgets when preset changes
|
||
const presetWidget = this.widgets.find(w => w.name === "preset");
|
||
if (presetWidget) {
|
||
const originalCallback = presetWidget.callback;
|
||
presetWidget.callback = function(value, graphcanvas, node, pos, event) {
|
||
// Call original callback first
|
||
if (originalCallback) {
|
||
originalCallback.call(this, value, graphcanvas, node, pos, event);
|
||
}
|
||
|
||
// Update width/height widgets based on preset
|
||
const widthWidget = node.widgets.find(w => w.name === "width");
|
||
const heightWidget = node.widgets.find(w => w.name === "height");
|
||
|
||
if (widthWidget && heightWidget && value !== "custom") {
|
||
// Extract raw resolution from formatted preset
|
||
const rawResolution = node.extractResolutionFromPreset(value);
|
||
|
||
// Define all available presets from our preset system
|
||
const presetDimensions = {
|
||
// SDXL Presets
|
||
"1024×1024": [1024, 1024], "896×1152": [896, 1152], "832×1216": [832, 1216],
|
||
"768×1344": [768, 1344], "640×1536": [640, 1536], "1152×896": [1152, 896],
|
||
"1216×832": [1216, 832], "1344×768": [1344, 768], "1536×640": [1536, 640],
|
||
// FLUX Presets
|
||
"1920×1080": [1920, 1080], "1536×1536": [1536, 1536], "1280×768": [1280, 768],
|
||
"768×1280": [768, 1280], "1440×1080": [1440, 1080], "1080×1440": [1080, 1440],
|
||
"1728×1152": [1728, 1152], "1152×1728": [1152, 1728],
|
||
// Ultra-Wide Presets
|
||
"2560×1080": [2560, 1080], "2048×768": [2048, 768], "1792×768": [1792, 768],
|
||
"2304×768": [2304, 768], "1080×2560": [1080, 2560], "768×2048": [768, 2048],
|
||
"768×1792": [768, 1792], "768×2304": [768, 2304]
|
||
};
|
||
|
||
if (rawResolution && presetDimensions[rawResolution]) {
|
||
const [w, h] = presetDimensions[rawResolution];
|
||
widthWidget.value = w;
|
||
heightWidget.value = h;
|
||
|
||
// Trigger widget callbacks to update the UI
|
||
if (widthWidget.callback) {
|
||
widthWidget.callback(w, graphcanvas, node, pos, event);
|
||
}
|
||
if (heightWidget.callback) {
|
||
heightWidget.callback(h, graphcanvas, node, pos, event);
|
||
}
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
// Add swap functionality
|
||
this.swapDimensions = function() {
|
||
const widthWidget = this.widgets.find(w => w.name === "width");
|
||
const heightWidget = this.widgets.find(w => w.name === "height");
|
||
const presetWidget = this.widgets.find(w => w.name === "preset");
|
||
|
||
if (widthWidget && heightWidget && presetWidget) {
|
||
// Handle preset swapping first
|
||
if (presetWidget.value !== "custom") {
|
||
const currentPreset = presetWidget.value;
|
||
|
||
// Extract raw resolution from formatted preset
|
||
const rawResolution = this.extractResolutionFromPreset(currentPreset);
|
||
if (!rawResolution) return;
|
||
|
||
// Parse current preset dimensions (handle both × and x separators)
|
||
let w, h;
|
||
if (rawResolution.includes('×')) {
|
||
[w, h] = rawResolution.split('×').map(v => parseInt(v));
|
||
} else if (rawResolution.includes('x')) {
|
||
[w, h] = rawResolution.split('x').map(v => parseInt(v));
|
||
} else {
|
||
return; // Invalid preset format
|
||
}
|
||
|
||
const swappedRawPreset = `${h}×${w}`;
|
||
|
||
// Find the formatted version of the swapped preset from available options
|
||
const availablePresets = presetWidget.options.values || presetWidget.options;
|
||
let swappedFormattedPreset = null;
|
||
|
||
for (const option of availablePresets) {
|
||
if (option === "custom") continue;
|
||
const extractedRes = this.extractResolutionFromPreset(option);
|
||
if (extractedRes === swappedRawPreset) {
|
||
swappedFormattedPreset = option;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (swappedFormattedPreset) {
|
||
// Swapped preset exists, use the formatted version
|
||
presetWidget.value = swappedFormattedPreset;
|
||
widthWidget.value = h;
|
||
heightWidget.value = w;
|
||
if (presetWidget.callback) {
|
||
presetWidget.callback(swappedFormattedPreset, this, presetWidget);
|
||
}
|
||
if (widthWidget.callback) {
|
||
widthWidget.callback(h, this, widthWidget);
|
||
}
|
||
if (heightWidget.callback) {
|
||
heightWidget.callback(w, this, heightWidget);
|
||
}
|
||
} else {
|
||
// Swapped preset doesn't exist, switch to custom and swap manual values
|
||
presetWidget.value = "custom";
|
||
widthWidget.value = h;
|
||
heightWidget.value = w;
|
||
|
||
if (presetWidget.callback) {
|
||
presetWidget.callback("custom", this, presetWidget);
|
||
}
|
||
if (widthWidget.callback) {
|
||
widthWidget.callback(h, this, widthWidget);
|
||
}
|
||
if (heightWidget.callback) {
|
||
heightWidget.callback(w, this, heightWidget);
|
||
}
|
||
}
|
||
} else {
|
||
// Custom preset - just swap the width and height values
|
||
const tempWidth = widthWidget.value;
|
||
widthWidget.value = heightWidget.value;
|
||
heightWidget.value = tempWidth;
|
||
|
||
// Trigger widget change events
|
||
if (widthWidget.callback) {
|
||
widthWidget.callback(widthWidget.value, this, widthWidget);
|
||
}
|
||
if (heightWidget.callback) {
|
||
heightWidget.callback(heightWidget.value, this, heightWidget);
|
||
}
|
||
}
|
||
|
||
// Mark the graph as changed
|
||
this.graph?.setDirtyCanvas(true, true);
|
||
}
|
||
};
|
||
};
|
||
|
||
const onDrawForeground = nodeType.prototype.onDrawForeground;
|
||
nodeType.prototype.onDrawForeground = function (ctx) {
|
||
if (onDrawForeground) {
|
||
onDrawForeground.apply(this, arguments);
|
||
}
|
||
|
||
if (this.flags.collapsed) return;
|
||
|
||
// Draw swap button with consistent spacing from widgets
|
||
const swapButtonSize = 24;
|
||
const margin = 6;
|
||
const swapButtonX = this.size[0] - swapButtonSize - margin;
|
||
|
||
// Calculate button position based on widget spacing rather than bottom margin
|
||
// Estimate widget area height and add consistent spacing
|
||
const estimatedWidgetHeight = 90; // Approximate height for 3 widgets
|
||
const topMargin = 35; // Space from top to first widget
|
||
const buttonSpacing = 10; // Space between last widget and button
|
||
const swapButtonY = topMargin + estimatedWidgetHeight + buttonSpacing;
|
||
|
||
// Button background - change color based on pressed state
|
||
if (this.swapButtonPressed) {
|
||
// Darker when pressed
|
||
ctx.fillStyle = "rgba(30, 120, 200, 0.9)"; // Darker blue when clicked
|
||
} else {
|
||
// Normal state
|
||
ctx.fillStyle = "rgba(66, 165, 245, 0.8)"; // Material blue
|
||
}
|
||
ctx.beginPath();
|
||
ctx.roundRect(swapButtonX, swapButtonY, swapButtonSize, swapButtonSize, 4);
|
||
ctx.fill();
|
||
|
||
// Button border with subtle highlight
|
||
ctx.strokeStyle = this.swapButtonPressed ? "rgba(20, 100, 180, 1.0)" : "rgba(33, 150, 243, 0.9)";
|
||
ctx.lineWidth = 1;
|
||
ctx.stroke();
|
||
|
||
// Draw swap icon - modern double arrow design
|
||
ctx.strokeStyle = "rgba(255, 255, 255, 0.95)";
|
||
ctx.lineWidth = 2;
|
||
ctx.lineCap = "round";
|
||
|
||
const centerX = swapButtonX + 12;
|
||
const centerY = swapButtonY + 12;
|
||
|
||
// Top arrow (pointing right) - width to height
|
||
ctx.beginPath();
|
||
ctx.moveTo(centerX - 7, centerY - 3);
|
||
ctx.lineTo(centerX + 5, centerY - 3);
|
||
ctx.stroke();
|
||
|
||
// Top arrow head
|
||
ctx.beginPath();
|
||
ctx.moveTo(centerX + 5, centerY - 3);
|
||
ctx.lineTo(centerX + 2, centerY - 5);
|
||
ctx.moveTo(centerX + 5, centerY - 3);
|
||
ctx.lineTo(centerX + 2, centerY - 1);
|
||
ctx.stroke();
|
||
|
||
// Bottom arrow (pointing left) - height to width
|
||
ctx.beginPath();
|
||
ctx.moveTo(centerX + 5, centerY + 3);
|
||
ctx.lineTo(centerX - 7, centerY + 3);
|
||
ctx.stroke();
|
||
|
||
// Bottom arrow head
|
||
ctx.beginPath();
|
||
ctx.moveTo(centerX - 7, centerY + 3);
|
||
ctx.lineTo(centerX - 4, centerY + 1);
|
||
ctx.moveTo(centerX - 7, centerY + 3);
|
||
ctx.lineTo(centerX - 4, centerY + 5);
|
||
ctx.stroke();
|
||
|
||
// Add subtle tooltip text when hovering (if we had hover state)
|
||
// This could be extended with hover detection for better UX
|
||
};
|
||
|
||
const onMouseDown = nodeType.prototype.onMouseDown;
|
||
nodeType.prototype.onMouseDown = function (e) {
|
||
// Check if click is on swap button
|
||
const swapButtonSize = 24;
|
||
const margin = 6;
|
||
const swapButtonX = this.pos[0] + this.size[0] - swapButtonSize - margin;
|
||
|
||
// Use same positioning logic as drawing
|
||
const estimatedWidgetHeight = 90;
|
||
const topMargin = 35;
|
||
const buttonSpacing = 10;
|
||
const swapButtonY = this.pos[1] + topMargin + estimatedWidgetHeight + buttonSpacing;
|
||
|
||
if (
|
||
e.canvasX >= swapButtonX &&
|
||
e.canvasX <= swapButtonX + swapButtonSize &&
|
||
e.canvasY >= swapButtonY &&
|
||
e.canvasY <= swapButtonY + swapButtonSize
|
||
) {
|
||
// Visual feedback - set button as pressed
|
||
this.swapButtonPressed = true;
|
||
this.setDirtyCanvas(true, true);
|
||
|
||
// Execute swap
|
||
this.swapDimensions();
|
||
|
||
// Reset button state after a short delay for visual feedback
|
||
setTimeout(() => {
|
||
this.swapButtonPressed = false;
|
||
this.setDirtyCanvas(true, true);
|
||
}, 150);
|
||
|
||
return true; // Consume the event
|
||
}
|
||
|
||
// Call original onMouseDown if not clicking swap button
|
||
if (onMouseDown) {
|
||
return onMouseDown.apply(this, arguments);
|
||
}
|
||
};
|
||
|
||
// Optional: Add hover effect for better user feedback
|
||
const onMouseMove = nodeType.prototype.onMouseMove;
|
||
nodeType.prototype.onMouseMove = function (e) {
|
||
// Check if hovering over swap button
|
||
const swapButtonSize = 24;
|
||
const margin = 6;
|
||
const swapButtonX = this.pos[0] + this.size[0] - swapButtonSize - margin;
|
||
|
||
// Use same positioning logic as drawing
|
||
const estimatedWidgetHeight = 90;
|
||
const topMargin = 35;
|
||
const buttonSpacing = 10;
|
||
const swapButtonY = this.pos[1] + topMargin + estimatedWidgetHeight + buttonSpacing;
|
||
|
||
const isHovering = (
|
||
e.canvasX >= swapButtonX &&
|
||
e.canvasX <= swapButtonX + swapButtonSize &&
|
||
e.canvasY >= swapButtonY &&
|
||
e.canvasY <= swapButtonY + swapButtonSize
|
||
);
|
||
|
||
// Update cursor style for better UX (safely)
|
||
if (isHovering && this.graph && this.graph.canvas && this.graph.canvas.canvas) {
|
||
this.graph.canvas.canvas.style.cursor = "pointer";
|
||
} else if (this.graph && this.graph.canvas && this.graph.canvas.canvas) {
|
||
this.graph.canvas.canvas.style.cursor = "default";
|
||
}
|
||
|
||
// Call original onMouseMove
|
||
if (onMouseMove) {
|
||
return onMouseMove.apply(this, arguments);
|
||
}
|
||
};
|
||
}
|
||
},
|
||
});
|