Skip to content

Commit 1a0cb1e

Browse files
committed
feat: Add single-axis handles to 2D canvas
Add blue (width) and pink (height) edge handles for precise single-axis dimension adjustments. Handles support snap/no-snap with Ctrl key and include hover effects. Maintains existing corner handle functionality.
1 parent 33f69eb commit 1a0cb1e

3 files changed

Lines changed: 656 additions & 367 deletions

File tree

js/DialogManager.js

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
// DialogManager.js - Manages custom input dialogs for ResolutionMaster
2+
import { createModuleLogger } from "./utils/LoggerUtils.js";
3+
import { app } from "../../scripts/app.js";
4+
5+
const log = createModuleLogger('DialogManager');
6+
7+
export class DialogManager {
8+
constructor(resolutionMasterInstance) {
9+
this.rm = resolutionMasterInstance;
10+
this.customInputDialog = null;
11+
this.customInputOverlay = null;
12+
this.inputDialogActive = false;
13+
}
14+
15+
/**
16+
* Shows a custom value input dialog
17+
* @param {string} valueAreaKey - The control key that was clicked
18+
* @param {Event} e - The mouse event
19+
*/
20+
showCustomValueDialog(valueAreaKey, e) {
21+
if (this.inputDialogActive) return;
22+
23+
log.debug(`Clicked on value area: ${valueAreaKey}`);
24+
25+
// Determine the type and current value based on the control key
26+
let valueType, currentValue, propertyName, minValue = 0.01;
27+
28+
if (valueAreaKey === 'scaleValueArea') {
29+
valueType = 'Scale Factor';
30+
currentValue = this.rm.node.properties.upscaleValue;
31+
propertyName = 'upscaleValue';
32+
} else if (valueAreaKey === 'resolutionValueArea') {
33+
valueType = 'Resolution Scale';
34+
currentValue = this.rm.calculateResolutionScale(this.rm.node.properties.targetResolution);
35+
propertyName = 'targetResolution';
36+
} else if (valueAreaKey === 'megapixelsValueArea') {
37+
valueType = 'Megapixels';
38+
currentValue = this.rm.node.properties.targetMegapixels;
39+
propertyName = 'targetMegapixels';
40+
} else if (valueAreaKey === 'snapValueArea') {
41+
valueType = 'Snap Value';
42+
currentValue = this.rm.node.properties.snapValue;
43+
propertyName = 'snapValue';
44+
minValue = 1;
45+
} else if (valueAreaKey === 'widthValueArea') {
46+
valueType = 'Width';
47+
currentValue = this.rm.widthWidget ? this.rm.widthWidget.value : this.rm.node.properties.valueX;
48+
propertyName = 'width';
49+
minValue = 64;
50+
} else if (valueAreaKey === 'heightValueArea') {
51+
valueType = 'Height';
52+
currentValue = this.rm.heightWidget ? this.rm.heightWidget.value : this.rm.node.properties.valueY;
53+
propertyName = 'height';
54+
minValue = 64;
55+
} else {
56+
log.debug(`Unknown value area key: ${valueAreaKey}`);
57+
return;
58+
}
59+
60+
log.debug(`Opening dialog for ${valueType} with current value: ${currentValue}`);
61+
this.createCustomInputDialog(valueType, currentValue, propertyName, minValue, e);
62+
}
63+
64+
/**
65+
* Creates and displays a custom input dialog
66+
* @param {string} valueType - The type of value being edited
67+
* @param {number} currentValue - The current value
68+
* @param {string} propertyName - The property name to update
69+
* @param {number} minValue - Minimum allowed value
70+
* @param {Event} e - The mouse event for positioning
71+
*/
72+
createCustomInputDialog(valueType, currentValue, propertyName, minValue, e) {
73+
this.inputDialogActive = true;
74+
log.debug(`Creating dialog for ${valueType}, current: ${currentValue}`);
75+
76+
// Create overlay
77+
const overlay = document.createElement('div');
78+
this.customInputOverlay = overlay;
79+
overlay.style.cssText = `
80+
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
81+
background: rgba(0,0,0,0.5); z-index: 9999;
82+
`;
83+
overlay.addEventListener('mousedown', () => this.closeCustomInputDialog());
84+
document.body.appendChild(overlay);
85+
86+
// Create dialog container
87+
const dialog = document.createElement('div');
88+
this.customInputDialog = dialog;
89+
dialog.className = 'litegraph-custom-input-dialog';
90+
dialog.addEventListener('mousedown', (e) => e.stopPropagation()); // Prevent clicks inside from closing
91+
dialog.style.cssText = `
92+
position: fixed;
93+
background: linear-gradient(135deg, #2a2a2a 0%, #1e1e1e 100%);
94+
border: 2px solid #555; border-radius: 8px; padding: 20px;
95+
box-shadow: 0 8px 32px rgba(0,0,0,0.8); z-index: 10000;
96+
font-family: Arial, sans-serif; min-width: 280px;
97+
`;
98+
99+
// Position dialog
100+
const x = e.clientX ? e.clientX + 20 : (window.innerWidth - 280) / 2;
101+
const y = e.clientY ? e.clientY + 20 : (window.innerHeight - 200) / 2;
102+
dialog.style.left = `${Math.max(10, Math.min(x, window.innerWidth - 300))}px`;
103+
dialog.style.top = `${Math.max(10, Math.min(y, window.innerHeight - 200))}px`;
104+
105+
// Create dialog content
106+
dialog.innerHTML = `
107+
<div style="color: #fff; font-size: 16px; font-weight: bold; margin-bottom: 15px; text-align: center;">Set Custom ${valueType}</div>
108+
<div style="margin-bottom: 10px;">
109+
<label style="color: #ccc; font-size: 12px; display: block; margin-bottom: 5px;">Current: ${this.formatValueForDisplay(currentValue, valueType)}</label>
110+
<input type="${valueType === 'Scale Factor' ? 'text' : 'number'}" id="customValueInput" value="${currentValue}" step="0.01" min="${minValue}"
111+
style="width: 100%; padding: 8px; border: 1px solid #555; border-radius: 4px; background: #333; color: #fff; font-size: 14px; box-sizing: border-box;">
112+
</div>
113+
<div id="validationMessage" style="color: #f55; font-size: 11px; margin-bottom: 5px; min-height: 15px;"></div>
114+
<div id="infoMessage" style="color: #999; font-size: 11px; margin-bottom: 10px; min-height: 15px; text-align: center;"></div>
115+
<div style="display: flex; gap: 10px; justify-content: flex-end;">
116+
<button id="cancelBtn" style="padding: 8px 16px; border: 1px solid #555; border-radius: 4px; background: #444; color: #ccc; cursor: pointer; font-size: 12px;">Cancel</button>
117+
<button id="applyBtn" style="padding: 8px 16px; border: 1px solid #5af; border-radius: 4px; background: #5af; color: #fff; cursor: pointer; font-size: 12px;">Apply</button>
118+
</div>
119+
`;
120+
121+
document.body.appendChild(dialog);
122+
123+
// Get elements
124+
const input = dialog.querySelector('#customValueInput');
125+
const validationMsg = dialog.querySelector('#validationMessage');
126+
const infoMsg = dialog.querySelector('#infoMessage');
127+
const cancelBtn = dialog.querySelector('#cancelBtn');
128+
const applyBtn = dialog.querySelector('#applyBtn');
129+
130+
if (valueType === 'Scale Factor') {
131+
infoMsg.textContent = 'Tip: Use /2 for 0.5x, /4 for 0.25x, etc.';
132+
}
133+
134+
// Focus and select input
135+
setTimeout(() => { input.focus(); input.select(); }, 50);
136+
137+
// Real-time validation
138+
const validateInput = () => {
139+
const value = this.parseCustomInputValue(input.value, valueType);
140+
if (isNaN(value) || value < minValue) {
141+
let errorMsg = `Value must be ≥ ${minValue}`;
142+
if (typeof input.value === 'string' && input.value.startsWith('/')) {
143+
const divisor = parseFloat(input.value.substring(1));
144+
if (isNaN(divisor) || divisor === 0) errorMsg = 'Invalid divisor after /';
145+
}
146+
validationMsg.textContent = errorMsg;
147+
applyBtn.disabled = true; applyBtn.style.opacity = '0.5';
148+
return false;
149+
} else {
150+
validationMsg.textContent = '';
151+
applyBtn.disabled = false; applyBtn.style.opacity = '1';
152+
return true;
153+
}
154+
};
155+
156+
// Event listeners
157+
input.addEventListener('input', validateInput);
158+
input.addEventListener('keydown', (e) => {
159+
if (e.key === 'Enter' && validateInput()) {
160+
this.applyCustomValue(propertyName, this.parseCustomInputValue(input.value, valueType), valueType);
161+
} else if (e.key === 'Escape') {
162+
this.closeCustomInputDialog();
163+
}
164+
});
165+
cancelBtn.addEventListener('click', () => this.closeCustomInputDialog());
166+
applyBtn.addEventListener('click', () => {
167+
if (validateInput()) {
168+
this.applyCustomValue(propertyName, this.parseCustomInputValue(input.value, valueType), valueType);
169+
}
170+
});
171+
172+
validateInput();
173+
}
174+
175+
/**
176+
* Closes and cleans up the custom input dialog
177+
*/
178+
closeCustomInputDialog() {
179+
if (this.customInputDialog) {
180+
document.body.removeChild(this.customInputDialog);
181+
this.customInputDialog = null;
182+
}
183+
if (this.customInputOverlay) {
184+
document.body.removeChild(this.customInputOverlay);
185+
this.customInputOverlay = null;
186+
}
187+
this.inputDialogActive = false;
188+
}
189+
190+
/**
191+
* Parses custom input value, handling special syntax like /2 for division
192+
* @param {string|number} rawValue - The raw input value
193+
* @param {string} valueType - The type of value being parsed
194+
* @returns {number} The parsed numeric value
195+
*/
196+
parseCustomInputValue(rawValue, valueType) {
197+
if (valueType === 'Scale Factor' && typeof rawValue === 'string' && rawValue.startsWith('/')) {
198+
const divisor = parseFloat(rawValue.substring(1));
199+
if (!isNaN(divisor) && divisor !== 0) {
200+
return 1 / divisor;
201+
}
202+
}
203+
return parseFloat(rawValue);
204+
}
205+
206+
/**
207+
* Applies a custom value to the appropriate property
208+
* @param {string} propertyName - The property to update
209+
* @param {number} value - The value to apply
210+
* @param {string} valueType - The type of value
211+
*/
212+
applyCustomValue(propertyName, value, valueType) {
213+
const props = this.rm.node.properties;
214+
215+
if (propertyName === 'upscaleValue') {
216+
props.upscaleValue = value;
217+
if (props.rescaleMode === 'manual') {
218+
this.rm.updateRescaleValue();
219+
}
220+
} else if (propertyName === 'targetResolution') {
221+
// For resolution, we need to reverse-calculate the target resolution from the scale factor
222+
if (this.rm.validateWidgets()) {
223+
const currentPixels = this.rm.widthWidget.value * this.rm.heightWidget.value;
224+
const targetPixels = currentPixels * (value * value);
225+
const targetP = Math.sqrt(targetPixels / (16/9));
226+
props.targetResolution = Math.round(targetP);
227+
if (props.rescaleMode === 'resolution') {
228+
this.rm.updateRescaleValue();
229+
}
230+
}
231+
} else if (propertyName === 'targetMegapixels') {
232+
props.targetMegapixels = value;
233+
if (props.rescaleMode === 'megapixels') {
234+
this.rm.updateRescaleValue();
235+
}
236+
} else if (propertyName === 'snapValue') {
237+
props.snapValue = Math.round(value);
238+
} else if (propertyName === 'width') {
239+
const newWidth = Math.round(value);
240+
const currentHeight = this.rm.heightWidget ? this.rm.heightWidget.value : props.valueY;
241+
this.rm.setDimensions(newWidth, currentHeight);
242+
} else if (propertyName === 'height') {
243+
const newHeight = Math.round(value);
244+
const currentWidth = this.rm.widthWidget ? this.rm.widthWidget.value : props.valueX;
245+
this.rm.setDimensions(currentWidth, newHeight);
246+
}
247+
248+
this.closeCustomInputDialog();
249+
app.graph.setDirtyCanvas(true);
250+
251+
log.debug(`Applied custom ${valueType}: ${value}`);
252+
}
253+
254+
/**
255+
* Formats a value for display with appropriate units
256+
* @param {number} value - The value to format
257+
* @param {string} valueType - The type of value
258+
* @returns {string} Formatted value with units
259+
*/
260+
formatValueForDisplay(value, valueType) {
261+
if (valueType === 'Scale Factor') {
262+
return value.toFixed(1) + 'x';
263+
} else if (valueType === 'Resolution Scale') {
264+
return '×' + value.toFixed(2);
265+
} else if (valueType === 'Megapixels') {
266+
return value.toFixed(1) + 'MP';
267+
} else if (valueType === 'Width' || valueType === 'Height') {
268+
return value.toString() + 'px';
269+
} else {
270+
return value.toString();
271+
}
272+
}
273+
}

0 commit comments

Comments
 (0)