refactor: reimplement glass positioning with dynamic anchoring for improved responsiveness and boundary handling

This commit is contained in:
AEmotionStudio
2026-01-02 15:55:09 -08:00
parent 15d1ada501
commit 81ee94a642
9 changed files with 267 additions and 99 deletions
+22 -5
View File
@@ -19,10 +19,10 @@ export class EventHandler {
constructor(magnifyGlass: MagnifyGlass) {
this.magnifyGlass = magnifyGlass;
// Bind methods to maintain context
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleKeyUp = this.handleKeyUp.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleResize = this.handleResize.bind(this);
}
/**
@@ -32,6 +32,7 @@ export class EventHandler {
document.addEventListener("keydown", this.handleKeyDown);
document.addEventListener("keyup", this.handleKeyUp);
document.addEventListener("mousemove", this.handleMouseMove);
window.addEventListener("resize", this.handleResize);
}
/**
@@ -41,6 +42,22 @@ export class EventHandler {
document.removeEventListener("keydown", this.handleKeyDown);
document.removeEventListener("keyup", this.handleKeyUp);
document.removeEventListener("mousemove", this.handleMouseMove);
window.removeEventListener("resize", this.handleResize);
}
/**
* Handle window resize events.
*/
handleResize(): void {
if (this.magnifyGlass && this.magnifyGlass.ui) {
this.magnifyGlass.ui.updateResponsivePosition();
// Also update panel position if it exists
const infoPanel = (window as any).infoPanelManager;
if (infoPanel && infoPanel.positionManager) {
// Determine if we should move it
infoPanel.positionManager.positionPanel();
}
}
}
/**
@@ -230,11 +247,11 @@ export class EventHandler {
const glassSize = this.magnifyGlass.config.glassSize;
if (!this.magnifyGlass.state.wasActivatedBefore && this.magnifyGlass.ui.glassDiv) {
// First activation - position at top right of the window
const leftPos = window.innerWidth - glassSize - DEFAULT_PADDING;
console.log(`[MagnifyGlass] Initial position - innerWidth: ${window.innerWidth}, glassSize: ${glassSize}, padding: ${DEFAULT_PADDING}, left: ${leftPos}, yOffset: ${DEFAULT_GLASS_Y_OFFSET}`);
this.magnifyGlass.ui.glassDiv.style.left = `${leftPos}px`;
// First activation - position at top right of the window (Anchored)
// Using style.right ensures it stays on the right even if window resizes before first usage
this.magnifyGlass.ui.glassDiv.style.right = `${DEFAULT_PADDING}px`;
this.magnifyGlass.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`;
this.magnifyGlass.ui.glassDiv.style.left = 'auto'; // Ensure left is unset
this.magnifyGlass.state.wasActivatedBefore = true;
} else {
this.magnifyGlass.ui.positionGlass(clientX, clientY);
+22 -4
View File
@@ -82,6 +82,10 @@ export class MagnifyGlass {
// Create UI elements
this.ui.createElements();
// Ensure glass is validly positioned (within viewport) right away
// This handles cases where config has saved positions from a different monitor size
this.ui.updateResponsivePosition();
// Setup WebGL renderer
this.renderer = new WebGLRenderer(this.config, this.state, this.ui);
if (!this.renderer.isValid()) {
@@ -387,10 +391,24 @@ export class MagnifyGlass {
// Reset magnify glass position to top-right corner
if (this.ui.glassDiv) {
const glassSize = this.config.glassSize;
this.ui.glassDiv.style.left = `${window.innerWidth - glassSize - DEFAULT_PADDING}px`;
this.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`;
this.state.wasActivatedBefore = false;
// If it's the first activation, position at top right of the window (Anchored)
// Otherwise, reset to the last known position or default if not active.
if (!this.state.wasActivatedBefore) {
const padding = DEFAULT_PADDING;
// Using style.right ensures it stays on the right even if window resizes before first usage
this.ui.glassDiv.style.right = `${padding}px`;
this.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`;
this.ui.glassDiv.style.left = 'auto'; // Ensure left is unset
this.state.wasActivatedBefore = true;
} else {
// If it was already activated, and we are resetting, we might want to keep its current position
// or explicitly reset it to the initial anchored position.
// For now, let's re-apply the anchored position on reset if it was already activated.
const padding = DEFAULT_PADDING;
this.ui.glassDiv.style.right = `${padding}px`;
this.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`;
this.ui.glassDiv.style.left = 'auto';
}
}
// Reset inspector panel position if exists
+106 -42
View File
@@ -4,7 +4,7 @@
* Handles creation and management of DOM elements for the magnifying glass.
*/
import { Z_INDEX, DEFAULT_PADDING } from '../shared/constants';
import { Z_INDEX, DEFAULT_PADDING, DEFAULT_GLASS_Y_OFFSET } from '../shared/constants';
import { Icons } from '../shared/icons';
import { Logger } from '../shared/logger';
import type { ConfigManager } from './ConfigManager';
@@ -160,57 +160,95 @@ export class UiManager {
* @param clientX - Client X coordinate
* @param clientY - Client Y coordinate
*/
/**
* Position the glass relative to cursor.
* Uses dynamic anchoring (Left/Right, Top/Bottom) based on screen quadrant.
* @param clientX - Client X coordinate
* @param clientY - Client Y coordinate
*/
positionGlass(clientX: number, clientY: number): void {
if (!this.config.followCursor || !this.glassDiv) return;
const glassSize = this.config.glassSize;
const offsetAmount = DEFAULT_PADDING;
let newLeft: number;
let newTop: number;
const vw = window.innerWidth;
const vh = window.innerHeight;
// Determine base coordinates (top-left based)
let targetX: number;
let targetY: number;
// Calculate standard Top-Left based positions first
switch (this.config.glassPosition) {
case "Top":
newLeft = clientX - (glassSize / 2);
newTop = clientY - glassSize - offsetAmount;
targetX = clientX - (glassSize / 2);
targetY = clientY - glassSize - offsetAmount;
break;
case "Bottom":
newLeft = clientX - (glassSize / 2);
newTop = clientY + offsetAmount;
targetX = clientX - (glassSize / 2);
targetY = clientY + offsetAmount;
break;
case "Left":
newLeft = clientX - glassSize - offsetAmount;
newTop = clientY - (glassSize / 2);
targetX = clientX - glassSize - offsetAmount;
targetY = clientY - (glassSize / 2);
break;
case "Right":
newLeft = clientX + offsetAmount;
newTop = clientY - (glassSize / 2);
targetX = clientX + offsetAmount;
targetY = clientY - (glassSize / 2);
break;
case "Top-Left":
newLeft = clientX - glassSize - offsetAmount;
newTop = clientY - glassSize - offsetAmount;
targetX = clientX - glassSize - offsetAmount;
targetY = clientY - glassSize - offsetAmount;
break;
case "Top-Right":
newLeft = clientX + offsetAmount;
newTop = clientY - glassSize - offsetAmount;
targetX = clientX + offsetAmount;
targetY = clientY - glassSize - offsetAmount;
break;
case "Bottom-Left":
newLeft = clientX - glassSize - offsetAmount;
newTop = clientY + offsetAmount;
targetX = clientX - glassSize - offsetAmount;
targetY = clientY + offsetAmount;
break;
case "Bottom-Right":
newLeft = clientX + offsetAmount;
newTop = clientY + offsetAmount;
targetX = clientX + offsetAmount;
targetY = clientY + offsetAmount;
break;
default:
newLeft = clientX - (glassSize / 2);
newTop = clientY + offsetAmount;
targetX = clientX - (glassSize / 2);
targetY = clientY + offsetAmount;
break;
}
this.glassDiv.style.left = `${newLeft}px`;
this.glassDiv.style.top = `${newTop}px`;
// Apply Dynamic Anchoring based on resulting position's center relative to screen
const glassCenterX = targetX + (glassSize / 2);
const glassCenterY = targetY + (glassSize / 2);
this.adjustForBoundaries(clientX, clientY);
// Horizontal Anchor
if (glassCenterX > vw / 2) {
// Anchor Right
const rightPos = vw - (targetX + glassSize);
this.glassDiv.style.right = `${rightPos}px`;
this.glassDiv.style.left = 'auto';
} else {
// Anchor Left
this.glassDiv.style.left = `${targetX}px`;
this.glassDiv.style.right = 'auto';
}
// Vertical Anchor
if (glassCenterY > vh / 2) {
// Anchor Bottom
const bottomPos = vh - (targetY + glassSize);
this.glassDiv.style.bottom = `${bottomPos}px`;
this.glassDiv.style.top = 'auto';
} else {
// Anchor Top
this.glassDiv.style.top = `${targetY}px`;
this.glassDiv.style.bottom = 'auto';
}
// We run a lightweight clamp to ensure it doesn't bleed offscreen during movement
// (Though the anchor logic implicitly handles resize, dragging near edges might need clamping)
this.adjustForBoundaries();
}
/**
@@ -218,32 +256,58 @@ export class UiManager {
* @param clientX - Client X coordinate
* @param clientY - Client Y coordinate
*/
adjustForBoundaries(clientX: number, clientY: number): void {
/**
* Adjust glass position to stay within viewport boundaries.
* Respects the current anchor (Left/Right, Top/Bottom).
*/
adjustForBoundaries(): void {
if (!this.glassDiv) return;
const glassRect = this.glassDiv.getBoundingClientRect();
const glassSize = this.config.glassSize;
const padding = DEFAULT_PADDING;
const vw = window.innerWidth;
const vh = window.innerHeight;
// Check right boundary
if (glassRect.right > window.innerWidth) {
this.glassDiv.style.left = `${clientX - glassRect.width - DEFAULT_PADDING}px`;
// Check if anchored Right or Left
const isAnchoredRight = this.glassDiv.style.right !== 'auto' && this.glassDiv.style.right !== '';
if (isAnchoredRight) {
let currentRight = parseFloat(this.glassDiv.style.right) || padding;
// Clamp Right: max(padding, min(current, vw - size - padding))
// Ensure it doesn't go off left side (vw - size - padding)
currentRight = Math.max(padding, Math.min(currentRight, vw - glassSize - padding));
this.glassDiv.style.right = `${currentRight}px`;
} else {
// Anchor Left
let currentLeft = parseFloat(this.glassDiv.style.left) || padding;
currentLeft = Math.max(padding, Math.min(currentLeft, vw - glassSize - padding));
this.glassDiv.style.left = `${currentLeft}px`;
}
// Check left boundary
const currentRectLeft = this.glassDiv.getBoundingClientRect();
if (currentRectLeft.left < 0) {
this.glassDiv.style.left = "10px";
}
// Check if anchored Bottom or Top
const isAnchoredBottom = this.glassDiv.style.bottom !== 'auto' && this.glassDiv.style.bottom !== '';
// Check bottom boundary
if (glassRect.bottom > window.innerHeight) {
this.glassDiv.style.top = `${clientY - glassRect.height - DEFAULT_PADDING}px`;
if (isAnchoredBottom) {
let currentBottom = parseFloat(this.glassDiv.style.bottom) || padding;
currentBottom = Math.max(padding, Math.min(currentBottom, vh - glassSize - padding));
this.glassDiv.style.bottom = `${currentBottom}px`;
} else {
// Anchor Top
let currentTop = parseFloat(this.glassDiv.style.top) || padding;
currentTop = Math.max(padding, Math.min(currentTop, vh - glassSize - padding));
this.glassDiv.style.top = `${currentTop}px`;
}
}
// Check top boundary
const currentRectTop = this.glassDiv.getBoundingClientRect();
if (currentRectTop.top < 0) {
this.glassDiv.style.top = "10px";
}
/**
* Update position safely on resize.
* Since we use dynamic anchoring in positionGlass, CSS handles most resize cases.
* This method ensures we verify boundaries (clamping) and fix anchors if we cross thresholds drastically.
*/
updateResponsivePosition(): void {
if (!this.glassDiv) return;
// Re-run boundary adjustments to clamp if resize pushed us offscreen
this.adjustForBoundaries();
}
/**
+18 -4
View File
@@ -11,6 +11,7 @@ class EventHandler {
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleKeyUp = this.handleKeyUp.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleResize = this.handleResize.bind(this);
}
/**
* Attach all event listeners.
@@ -19,6 +20,7 @@ class EventHandler {
document.addEventListener("keydown", this.handleKeyDown);
document.addEventListener("keyup", this.handleKeyUp);
document.addEventListener("mousemove", this.handleMouseMove);
window.addEventListener("resize", this.handleResize);
}
/**
* Detach all event listeners.
@@ -27,6 +29,19 @@ class EventHandler {
document.removeEventListener("keydown", this.handleKeyDown);
document.removeEventListener("keyup", this.handleKeyUp);
document.removeEventListener("mousemove", this.handleMouseMove);
window.removeEventListener("resize", this.handleResize);
}
/**
* Handle window resize events.
*/
handleResize() {
if (this.magnifyGlass && this.magnifyGlass.ui) {
this.magnifyGlass.ui.updateResponsivePosition();
const infoPanel = window.infoPanelManager;
if (infoPanel && infoPanel.positionManager) {
infoPanel.positionManager.positionPanel();
}
}
}
/**
* Handle keydown events.
@@ -168,12 +183,11 @@ class EventHandler {
const pixelY = cssMouseYOnCanvas * scaleY;
this.magnifyGlass.state.x = pixelX;
this.magnifyGlass.state.y = pixelY;
const glassSize = this.magnifyGlass.config.glassSize;
this.magnifyGlass.config.glassSize;
if (!this.magnifyGlass.state.wasActivatedBefore && this.magnifyGlass.ui.glassDiv) {
const leftPos = window.innerWidth - glassSize - DEFAULT_PADDING;
console.log(`[MagnifyGlass] Initial position - innerWidth: ${window.innerWidth}, glassSize: ${glassSize}, padding: ${DEFAULT_PADDING}, left: ${leftPos}, yOffset: ${DEFAULT_GLASS_Y_OFFSET}`);
this.magnifyGlass.ui.glassDiv.style.left = `${leftPos}px`;
this.magnifyGlass.ui.glassDiv.style.right = `${DEFAULT_PADDING}px`;
this.magnifyGlass.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`;
this.magnifyGlass.ui.glassDiv.style.left = "auto";
this.magnifyGlass.state.wasActivatedBefore = true;
} else {
this.magnifyGlass.ui.positionGlass(clientX, clientY);
File diff suppressed because one or more lines are too long
+13 -4
View File
@@ -51,6 +51,7 @@ class MagnifyGlass {
this.config.loadSavedOffsets();
this.debugger.printCanvasInfo();
this.ui.createElements();
this.ui.updateResponsivePosition();
this.renderer = new WebGLRenderer(this.config, this.state, this.ui);
if (!this.renderer.isValid()) {
this.ui.cleanup();
@@ -290,10 +291,18 @@ class MagnifyGlass {
this.updateMagnifiedView();
}
if (this.ui.glassDiv) {
const glassSize = this.config.glassSize;
this.ui.glassDiv.style.left = `${window.innerWidth - glassSize - DEFAULT_PADDING}px`;
this.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`;
this.state.wasActivatedBefore = false;
if (!this.state.wasActivatedBefore) {
const padding = DEFAULT_PADDING;
this.ui.glassDiv.style.right = `${padding}px`;
this.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`;
this.ui.glassDiv.style.left = "auto";
this.state.wasActivatedBefore = true;
} else {
const padding = DEFAULT_PADDING;
this.ui.glassDiv.style.right = `${padding}px`;
this.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`;
this.ui.glassDiv.style.left = "auto";
}
}
const extensions = window.comfyUIMagnifyGlassExtensions;
if (extensions && extensions.length > 0) {
File diff suppressed because one or more lines are too long
+83 -37
View File
@@ -129,77 +129,123 @@ class UiManager {
* @param clientX - Client X coordinate
* @param clientY - Client Y coordinate
*/
/**
* Position the glass relative to cursor.
* Uses dynamic anchoring (Left/Right, Top/Bottom) based on screen quadrant.
* @param clientX - Client X coordinate
* @param clientY - Client Y coordinate
*/
positionGlass(clientX, clientY) {
if (!this.config.followCursor || !this.glassDiv) return;
const glassSize = this.config.glassSize;
const offsetAmount = DEFAULT_PADDING;
let newLeft;
let newTop;
const vw = window.innerWidth;
const vh = window.innerHeight;
let targetX;
let targetY;
switch (this.config.glassPosition) {
case "Top":
newLeft = clientX - glassSize / 2;
newTop = clientY - glassSize - offsetAmount;
targetX = clientX - glassSize / 2;
targetY = clientY - glassSize - offsetAmount;
break;
case "Bottom":
newLeft = clientX - glassSize / 2;
newTop = clientY + offsetAmount;
targetX = clientX - glassSize / 2;
targetY = clientY + offsetAmount;
break;
case "Left":
newLeft = clientX - glassSize - offsetAmount;
newTop = clientY - glassSize / 2;
targetX = clientX - glassSize - offsetAmount;
targetY = clientY - glassSize / 2;
break;
case "Right":
newLeft = clientX + offsetAmount;
newTop = clientY - glassSize / 2;
targetX = clientX + offsetAmount;
targetY = clientY - glassSize / 2;
break;
case "Top-Left":
newLeft = clientX - glassSize - offsetAmount;
newTop = clientY - glassSize - offsetAmount;
targetX = clientX - glassSize - offsetAmount;
targetY = clientY - glassSize - offsetAmount;
break;
case "Top-Right":
newLeft = clientX + offsetAmount;
newTop = clientY - glassSize - offsetAmount;
targetX = clientX + offsetAmount;
targetY = clientY - glassSize - offsetAmount;
break;
case "Bottom-Left":
newLeft = clientX - glassSize - offsetAmount;
newTop = clientY + offsetAmount;
targetX = clientX - glassSize - offsetAmount;
targetY = clientY + offsetAmount;
break;
case "Bottom-Right":
newLeft = clientX + offsetAmount;
newTop = clientY + offsetAmount;
targetX = clientX + offsetAmount;
targetY = clientY + offsetAmount;
break;
default:
newLeft = clientX - glassSize / 2;
newTop = clientY + offsetAmount;
targetX = clientX - glassSize / 2;
targetY = clientY + offsetAmount;
break;
}
this.glassDiv.style.left = `${newLeft}px`;
this.glassDiv.style.top = `${newTop}px`;
this.adjustForBoundaries(clientX, clientY);
const glassCenterX = targetX + glassSize / 2;
const glassCenterY = targetY + glassSize / 2;
if (glassCenterX > vw / 2) {
const rightPos = vw - (targetX + glassSize);
this.glassDiv.style.right = `${rightPos}px`;
this.glassDiv.style.left = "auto";
} else {
this.glassDiv.style.left = `${targetX}px`;
this.glassDiv.style.right = "auto";
}
if (glassCenterY > vh / 2) {
const bottomPos = vh - (targetY + glassSize);
this.glassDiv.style.bottom = `${bottomPos}px`;
this.glassDiv.style.top = "auto";
} else {
this.glassDiv.style.top = `${targetY}px`;
this.glassDiv.style.bottom = "auto";
}
this.adjustForBoundaries();
}
/**
* Adjust glass position to stay within viewport boundaries.
* @param clientX - Client X coordinate
* @param clientY - Client Y coordinate
*/
adjustForBoundaries(clientX, clientY) {
/**
* Adjust glass position to stay within viewport boundaries.
* Respects the current anchor (Left/Right, Top/Bottom).
*/
adjustForBoundaries() {
if (!this.glassDiv) return;
const glassRect = this.glassDiv.getBoundingClientRect();
if (glassRect.right > window.innerWidth) {
this.glassDiv.style.left = `${clientX - glassRect.width - DEFAULT_PADDING}px`;
const glassSize = this.config.glassSize;
const padding = DEFAULT_PADDING;
const vw = window.innerWidth;
const vh = window.innerHeight;
const isAnchoredRight = this.glassDiv.style.right !== "auto" && this.glassDiv.style.right !== "";
if (isAnchoredRight) {
let currentRight = parseFloat(this.glassDiv.style.right) || padding;
currentRight = Math.max(padding, Math.min(currentRight, vw - glassSize - padding));
this.glassDiv.style.right = `${currentRight}px`;
} else {
let currentLeft = parseFloat(this.glassDiv.style.left) || padding;
currentLeft = Math.max(padding, Math.min(currentLeft, vw - glassSize - padding));
this.glassDiv.style.left = `${currentLeft}px`;
}
const currentRectLeft = this.glassDiv.getBoundingClientRect();
if (currentRectLeft.left < 0) {
this.glassDiv.style.left = "10px";
}
if (glassRect.bottom > window.innerHeight) {
this.glassDiv.style.top = `${clientY - glassRect.height - DEFAULT_PADDING}px`;
}
const currentRectTop = this.glassDiv.getBoundingClientRect();
if (currentRectTop.top < 0) {
this.glassDiv.style.top = "10px";
const isAnchoredBottom = this.glassDiv.style.bottom !== "auto" && this.glassDiv.style.bottom !== "";
if (isAnchoredBottom) {
let currentBottom = parseFloat(this.glassDiv.style.bottom) || padding;
currentBottom = Math.max(padding, Math.min(currentBottom, vh - glassSize - padding));
this.glassDiv.style.bottom = `${currentBottom}px`;
} else {
let currentTop = parseFloat(this.glassDiv.style.top) || padding;
currentTop = Math.max(padding, Math.min(currentTop, vh - glassSize - padding));
this.glassDiv.style.top = `${currentTop}px`;
}
}
/**
* Update position safely on resize.
* Since we use dynamic anchoring in positionGlass, CSS handles most resize cases.
* This method ensures we verify boundaries (clamping) and fix anchors if we cross thresholds drastically.
*/
updateResponsivePosition() {
if (!this.glassDiv) return;
this.adjustForBoundaries();
}
/**
* Apply current config to UI elements.
*/
File diff suppressed because one or more lines are too long