Fix sidebar collapse and glass media culling
- Make the sidebar tab wrapper fill the panel, and let the content keep its natural height when the parent has none, so the settings no longer collapse to a 16px scroller. - Only measure DOM images and videos under the glass, so per-frame work scales with what is under the glass instead of all media on the page. - Cap Virtual Zoom near 60 fps again, so high-refresh displays do not run two full graph draws every frame. - Cull nodes with LiteGraph's boundingRect and keep collapsed nodes, so title bars and collapsed nodes in front of a preview stay on top in the glass. - Drop the self-min on the title width and the dead pos/size guards. - Remove the stale web/shared/settings.js build output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
79c7a1e9b5
commit
6799dc8fbb
+1
-1
@@ -26,7 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **Sidebar redesign**: Built on ComfyUI's palette tokens, so it follows light and dark themes, with native section buttons, the Inspector panel switch at the top, and Glass Toggle Key under Glass Hotkeys.
|
||||
|
||||
### Removed
|
||||
- The per-frame HTML overlay copies of videos, the Virtual Zoom frame throttle, and the unused sidebar Inspector module.
|
||||
- The per-frame HTML overlay copies of videos and the unused sidebar Inspector module.
|
||||
|
||||
### Known Issues
|
||||
- With the Inspector panel off, "Move Glass Position" (drag-glass mode) is unavailable, as it lives on the panel's floating controls. Follow Cursor (`H`) and the arrow keys still move the glass.
|
||||
|
||||
+1
-1
@@ -82,7 +82,7 @@
|
||||
|
||||
/* Body: the panel's one scroller */
|
||||
.magnify-sidebar-content {
|
||||
flex: 1 1 0;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -17,6 +17,8 @@ import type { Box, CaptureRegion } from './mediaLayout';
|
||||
declare const app: any;
|
||||
|
||||
const TITLE_HEIGHT = 30;
|
||||
// Each Virtual Zoom frame is two full graph draws: cap it near 60 fps, with slack for rAF jitter at 60 Hz
|
||||
const VIRTUAL_ZOOM_MIN_INTERVAL_MS = 14;
|
||||
|
||||
interface DomMedia {
|
||||
el: HTMLImageElement | HTMLVideoElement;
|
||||
@@ -24,6 +26,12 @@ interface DomMedia {
|
||||
objectFit: string;
|
||||
}
|
||||
|
||||
/** LiteGraph's node bounds in graph space: title included, collapsed width when collapsed. */
|
||||
function boundingBox(node: any): Box {
|
||||
const [x, y, w, h] = node.boundingRect;
|
||||
return { x, y, w, h };
|
||||
}
|
||||
|
||||
export class OffscreenRenderer {
|
||||
private config: ConfigManager;
|
||||
private state: MagnifierState;
|
||||
@@ -32,6 +40,7 @@ export class OffscreenRenderer {
|
||||
private lastRenderWidth: number = 0;
|
||||
private lastRenderHeight: number = 0;
|
||||
private isCapturing: boolean = false;
|
||||
private lastVirtualZoomTime: number = 0;
|
||||
|
||||
constructor(config: ConfigManager, state: MagnifierState) {
|
||||
this.config = config;
|
||||
@@ -73,48 +82,51 @@ export class OffscreenRenderer {
|
||||
// forceDirectCapture (hotkey) trades detail for exact cursor alignment.
|
||||
const vueNodes = !!(window as any).LiteGraph?.vueNodesMode;
|
||||
const currentScale = lgCanvas.ds.scale;
|
||||
const media = this.collectDomMedia(rect, vueNodes);
|
||||
const virtualZoom = currentScale < 1.0 && !this.config.forceDirectCapture && !vueNodes;
|
||||
|
||||
if (currentScale < 1.0 && !this.config.forceDirectCapture && !vueNodes) {
|
||||
const domScale = 1 / currentScale;
|
||||
const scaled = media.map(m => ({ ...m, box: scaleAbout(m.box, pivotX, pivotY, domScale) }));
|
||||
return this.renderVirtualZoom(lgCanvas, targetCanvas, rect, dpr, pivotX, pivotY, cap, scaled);
|
||||
if (virtualZoom) {
|
||||
const now = performance.now();
|
||||
if (now - this.lastVirtualZoomTime < VIRTUAL_ZOOM_MIN_INTERVAL_MS) return this.offscreenCanvas;
|
||||
this.lastVirtualZoomTime = now;
|
||||
}
|
||||
|
||||
const media = this.collectDomMedia(rect, vueNodes, cap, pivotX, pivotY, virtualZoom ? 1 / currentScale : 1);
|
||||
if (virtualZoom) {
|
||||
return this.renderVirtualZoom(lgCanvas, targetCanvas, rect, dpr, pivotX, pivotY, cap, media);
|
||||
}
|
||||
return this.renderDirectCapture(targetCanvas, dpr, cap, media, vueNodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read phase: measure loaded, same-origin DOM images and videos (DOM widgets, or Vue node bodies)
|
||||
* before any canvas draw. Boxes are CSS pixels relative to the canvas at the real zoom.
|
||||
* under the glass before any canvas draw. Boxes are CSS pixels relative to the canvas, scaled by
|
||||
* domScale about the pivot into the capture frame.
|
||||
*/
|
||||
private collectDomMedia(rect: DOMRect, vueNodes: boolean): DomMedia[] {
|
||||
private collectDomMedia(rect: DOMRect, vueNodes: boolean, cap: CaptureRegion, pivotX: number, pivotY: number, domScale: number): DomMedia[] {
|
||||
const selector = vueNodes ? '[data-node-id] img, [data-node-id] video' : '.dom-widget img, .dom-widget video';
|
||||
const media: DomMedia[] = [];
|
||||
for (const el of document.querySelectorAll<HTMLImageElement | HTMLVideoElement>(selector)) {
|
||||
const loaded = el instanceof HTMLVideoElement ? el.readyState >= 2 : el.complete && el.naturalWidth > 0;
|
||||
if (!loaded || !isSafeMediaSource(el)) continue;
|
||||
if (!loaded) continue;
|
||||
const r = el.getBoundingClientRect();
|
||||
if (r.width === 0 || r.height === 0) continue;
|
||||
media.push({
|
||||
el,
|
||||
box: { x: r.left - rect.left, y: r.top - rect.top, w: r.width, h: r.height },
|
||||
objectFit: getComputedStyle(el).objectFit
|
||||
});
|
||||
const box = scaleAbout({ x: r.left - rect.left, y: r.top - rect.top, w: r.width, h: r.height }, pivotX, pivotY, domScale);
|
||||
if (box.x + box.w <= cap.left || box.x >= cap.left + cap.size) continue;
|
||||
if (box.y + box.h <= cap.top || box.y >= cap.top + cap.size) continue;
|
||||
if (!isSafeMediaSource(el)) continue;
|
||||
media.push({ el, box, objectFit: getComputedStyle(el).objectFit });
|
||||
}
|
||||
return media;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nodes of the graph on screen (including subgraphs) that overlap the capture region,
|
||||
* in canvas render order (back to front).
|
||||
* Nodes of the graph on screen (including subgraphs and collapsed nodes) whose title or body
|
||||
* overlaps the capture region, in canvas render order (back to front).
|
||||
*/
|
||||
private getVisibleNodes(cap: CaptureRegion, scale: number, offset: [number, number]): any[] {
|
||||
const visibleNodes = [];
|
||||
for (const node of app.canvas.visible_nodes) {
|
||||
if (!node.pos || !node.size) continue;
|
||||
if (node.flags?.collapsed) continue;
|
||||
|
||||
const box = graphBoxToCss({ x: 0, y: 0, w: node.size[0], h: node.size[1] }, node.pos, scale, offset);
|
||||
const box = graphBoxToCss(boundingBox(node), [0, 0], scale, offset);
|
||||
if (box.x + box.w < cap.left || box.x > cap.left + cap.size) continue;
|
||||
if (box.y + box.h < cap.top || box.y > cap.top + cap.size) continue;
|
||||
|
||||
@@ -315,7 +327,7 @@ export class OffscreenRenderer {
|
||||
|
||||
const sizeTextMargin = getSettingValue('Comfy.Node.AllowImageSizeDraw', true) ? 15 : 0;
|
||||
for (const [i, node] of visibleNodes.entries()) {
|
||||
const widget = node.imgs?.length && node.widgets?.find((w: any) => w.name === '$$canvas-image-preview');
|
||||
const widget = !node.flags?.collapsed && node.imgs?.length && node.widgets?.find((w: any) => w.name === '$$canvas-image-preview');
|
||||
if (!widget) continue;
|
||||
for (const { img, box } of previewImageBoxes(node.imgs, widget, node, sizeTextMargin)) {
|
||||
const g = toGlass(graphBoxToCss(box, node.pos, scale, offset), cap);
|
||||
@@ -325,7 +337,7 @@ export class OffscreenRenderer {
|
||||
// Nodes later in render order cover this preview on the canvas: put them back from the capture
|
||||
const nodeBox = toGlass(graphBoxToCss({ x: 0, y: 0, w: node.size[0], h: node.size[1] }, node.pos, scale, offset), cap);
|
||||
for (const front of visibleNodes.slice(i + 1)) {
|
||||
const f = toGlass(graphBoxToCss({ x: 0, y: -TITLE_HEIGHT, w: front.size[0], h: front.size[1] + TITLE_HEIGHT }, front.pos, scale, offset), cap);
|
||||
const f = toGlass(graphBoxToCss(boundingBox(front), [0, 0], scale, offset), cap);
|
||||
if (!overlaps(f, nodeBox)) continue;
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
@@ -368,7 +380,6 @@ export class OffscreenRenderer {
|
||||
const renderSize = this.config.glassSize;
|
||||
|
||||
for (const node of visibleNodes) {
|
||||
if (!node.pos || !node.size) continue;
|
||||
if (node.flags?.collapsed) continue;
|
||||
|
||||
const title = node.title || node.type || "Node";
|
||||
@@ -385,8 +396,7 @@ export class OffscreenRenderer {
|
||||
// Draw
|
||||
const canvasX = (nodeCssX - cap.left) * cap.k;
|
||||
const canvasY = (nodeCssY - cap.top) * cap.k;
|
||||
// Limit width for title background
|
||||
const canvasWidth = Math.min(nodeCssWidth, nodeCssWidth) * cap.k;
|
||||
const canvasWidth = nodeCssWidth * cap.k;
|
||||
const canvasHeight = TITLE_HEIGHT * scale * cap.k;
|
||||
|
||||
if (canvasX + canvasWidth > 0 && canvasX < renderSize && canvasY + canvasHeight > 0 && canvasY < renderSize) {
|
||||
@@ -461,7 +471,7 @@ export class OffscreenRenderer {
|
||||
const PADDING = 15;
|
||||
|
||||
for (const node of visibleNodes) {
|
||||
if (!node.widgets || !node.pos || !node.size) continue;
|
||||
if (!node.widgets) continue;
|
||||
if (node.flags?.collapsed) continue;
|
||||
|
||||
// Calculate node position in CSS pixels
|
||||
|
||||
@@ -49,6 +49,8 @@ function renderSidebar(container: HTMLElement): void {
|
||||
|
||||
// Clear existing content to prevent duplicates
|
||||
container.innerHTML = '';
|
||||
// The frontend's custom-tab wrapper has no height of its own; fill the panel so the content scrolls under the header
|
||||
container.style.height = '100%';
|
||||
|
||||
// Create main container (hidden initially to prevent flash of unstyled content)
|
||||
const sidebar = document.createElement('div');
|
||||
|
||||
@@ -34,6 +34,10 @@ import { OffscreenRenderer } from '../../src/magnify-glass/OffscreenRenderer';
|
||||
import { ConfigManager } from '../../src/magnify-glass/ConfigManager';
|
||||
import { MagnifierState } from '../../src/magnify-glass/MagnifierState';
|
||||
|
||||
// LiteGraph keeps boundingRect up to date for visible nodes: the body plus the 30px title above it
|
||||
const lgNode = (pos: number[], size: number[], extra: Record<string, unknown> = {}) =>
|
||||
({ pos, size, boundingRect: [pos[0], pos[1] - 30, size[0], size[1] + 30], ...extra });
|
||||
|
||||
describe('OffscreenRenderer Optimization', () => {
|
||||
let renderer: OffscreenRenderer;
|
||||
let configMock: ConfigManager;
|
||||
@@ -111,21 +115,17 @@ describe('OffscreenRenderer Optimization', () => {
|
||||
// Cursor at 100, 100. Glass size 200, zoom 2. Source size 100.
|
||||
// Capture region: 50 to 150.
|
||||
// Node 1 overlaps.
|
||||
const visibleNode = {
|
||||
pos: [50, 50], // Graph coords
|
||||
size: [100, 100],
|
||||
const visibleNode = lgNode([50, 50], [100, 100], {
|
||||
widgets: [
|
||||
{ type: 'text', name: 'visible_widget', value: 'Visible Text', computedHeight: 50, last_y: 40 }
|
||||
]
|
||||
};
|
||||
});
|
||||
// Node 2: Not Visible (far away)
|
||||
const invisibleNode = {
|
||||
pos: [1000, 1000],
|
||||
size: [100, 100],
|
||||
const invisibleNode = lgNode([1000, 1000], [100, 100], {
|
||||
widgets: [
|
||||
{ type: 'text', name: 'invisible_widget', value: 'Invisible Text', computedHeight: 50, last_y: 40 }
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
appMock.canvas.visible_nodes = [visibleNode, invisibleNode];
|
||||
|
||||
@@ -205,13 +205,24 @@ describe('OffscreenRenderer Optimization', () => {
|
||||
const drawsOf = (el: unknown) =>
|
||||
((renderer as any).offscreenCtx.drawImage as ReturnType<typeof vi.fn>).mock.calls.filter(c => c[0] === el);
|
||||
|
||||
const previewNode = (img: HTMLImageElement) => ({
|
||||
pos: [150, 100],
|
||||
size: [200, 300],
|
||||
const previewNode = (img: HTMLImageElement, extra: Record<string, unknown> = {}) => lgNode([150, 100], [200, 300], {
|
||||
imgs: [img],
|
||||
widgets: [{ name: PREVIEW_WIDGET, type: 'custom', y: 40, computedHeight: 215 }]
|
||||
widgets: [{ name: PREVIEW_WIDGET, type: 'custom', y: 40, computedHeight: 215 }],
|
||||
...extra
|
||||
});
|
||||
|
||||
const domImage = (left: number, top: number) => {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'dom-widget';
|
||||
const img = image('/view?filename=b.png&type=output', 40, 30);
|
||||
vi.spyOn(img, 'getBoundingClientRect').mockReturnValue({
|
||||
x: left, y: top, left, top, width: 40, height: 30, right: left + 40, bottom: top + 30, toJSON: () => {}
|
||||
} as DOMRect);
|
||||
wrapper.appendChild(img);
|
||||
document.body.appendChild(wrapper);
|
||||
return img;
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
delete (window as any).LiteGraph;
|
||||
@@ -233,8 +244,8 @@ describe('OffscreenRenderer Optimization', () => {
|
||||
|
||||
it('puts a node in front of a preview back over it from the capture', () => {
|
||||
const img = image('/view?filename=a.png&type=output', 100, 50);
|
||||
const front = { pos: [200, 250], size: [100, 100], widgets: [] };
|
||||
const behind = { pos: [190, 190], size: [40, 40], widgets: [] };
|
||||
const front = lgNode([200, 250], [100, 100], { widgets: [] });
|
||||
const behind = lgNode([190, 190], [40, 40], { widgets: [] });
|
||||
appMock.canvas.visible_nodes = [behind, previewNode(img), front];
|
||||
stateMock.x = 250;
|
||||
stateMock.y = 250;
|
||||
@@ -249,6 +260,51 @@ describe('OffscreenRenderer Optimization', () => {
|
||||
expect(ctx.rect.mock.calls).toEqual([[0, 40, 200, 260]]);
|
||||
});
|
||||
|
||||
it('puts a collapsed node in front back over a preview, and draws no preview for a collapsed node', () => {
|
||||
const img = image('/view?filename=a.png&type=output', 100, 50);
|
||||
const hidden = image('/view?filename=c.png&type=output', 100, 50);
|
||||
// Collapsed: LiteGraph draws only the title pill, so boundingRect is the collapsed width by the title height
|
||||
const front = previewNode(hidden, { pos: [220, 260], flags: { collapsed: true }, boundingRect: [220, 230, 60, 30] });
|
||||
appMock.canvas.visible_nodes = [previewNode(img), front];
|
||||
stateMock.x = 250;
|
||||
stateMock.y = 250;
|
||||
const canvas = targetCanvas(0, 0, 1);
|
||||
|
||||
renderer.renderHighResRegion(canvas);
|
||||
|
||||
const ctx = (renderer as any).offscreenCtx;
|
||||
// Pill (220, 230, 60x30) -> glass (40, 60, 120x60)
|
||||
expect(ctx.drawImage.mock.calls.map((c: unknown[]) => c[0])).toEqual([canvas, img, canvas]);
|
||||
expect(ctx.rect.mock.calls).toEqual([[40, 60, 120, 60]]);
|
||||
});
|
||||
|
||||
it('puts a node back whose title alone reaches into the glass', () => {
|
||||
const img = image('/view?filename=a.png&type=output', 100, 50);
|
||||
// Capture region is y 200-300; this body starts at 310, its title at 280
|
||||
const front = lgNode([200, 310], [100, 100], { widgets: [] });
|
||||
appMock.canvas.visible_nodes = [previewNode(img), front];
|
||||
stateMock.x = 250;
|
||||
stateMock.y = 250;
|
||||
|
||||
renderer.renderHighResRegion(targetCanvas(0, 0, 1));
|
||||
|
||||
expect((renderer as any).offscreenCtx.rect.mock.calls).toEqual([[0, 160, 200, 260]]);
|
||||
});
|
||||
|
||||
it('only measures DOM media under the glass', () => {
|
||||
const near = domImage(280, 180);
|
||||
const far = domImage(900, 700);
|
||||
const style = vi.spyOn(globalThis, 'getComputedStyle');
|
||||
stateMock.x = 300;
|
||||
stateMock.y = 200;
|
||||
|
||||
renderer.renderHighResRegion(targetCanvas(0, 0, 1));
|
||||
|
||||
expect(style.mock.calls.map(c => c[0])).toEqual([near]);
|
||||
expect(drawsOf(near)).toHaveLength(1);
|
||||
expect(drawsOf(far)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('passes client coordinates to setZoom', () => {
|
||||
appMock.canvas.ds = { scale: 0.5, offset: [0, 0] };
|
||||
appMock.canvas.setZoom = vi.fn((s: number) => { appMock.canvas.ds.scale = s; });
|
||||
@@ -330,18 +386,27 @@ describe('OffscreenRenderer Optimization', () => {
|
||||
expect(drawsOf(img)).toEqual([[img, 60, 60, 80, 60]]);
|
||||
});
|
||||
|
||||
it('draws the video on every call in Virtual Zoom (no throttle)', () => {
|
||||
it('caps Virtual Zoom near 60 fps and draws the video on each rendered frame', () => {
|
||||
appMock.canvas.ds = { scale: 0.5, offset: [0, 0] };
|
||||
appMock.canvas.setZoom = vi.fn((s: number) => { appMock.canvas.ds.scale = s; });
|
||||
appMock.canvas.draw = vi.fn();
|
||||
appMock.canvas.setZoom = vi.fn((s: number) => {
|
||||
appMock.canvas.ds.scale = s;
|
||||
appMock.canvas.ds.offset = [0, 0];
|
||||
});
|
||||
appMock.canvas.draw = vi.fn(() => { appMock.canvas.ds.scale = 0.5; });
|
||||
const video = domVideo(4);
|
||||
stateMock.x = 300;
|
||||
stateMock.y = 200;
|
||||
const canvas = targetCanvas(0, 0, 1);
|
||||
const now = vi.spyOn(performance, 'now').mockReturnValue(1000);
|
||||
|
||||
renderer.renderHighResRegion(canvas);
|
||||
const first = renderer.renderHighResRegion(canvas);
|
||||
now.mockReturnValue(1007); // next frame on a 144 Hz display
|
||||
const skipped = renderer.renderHighResRegion(canvas);
|
||||
now.mockReturnValue(1017); // next frame on a 60 Hz display
|
||||
renderer.renderHighResRegion(canvas);
|
||||
|
||||
expect(skipped).toBe(first);
|
||||
expect(appMock.canvas.draw).toHaveBeenCalledTimes(4);
|
||||
// Measured at 0.5, scaled x2 about the pivot (300, 200) -> (260, 160, 80x60) -> glass (20, 20, 160x120)
|
||||
expect(drawsOf(video)).toEqual([[video, 20, 20, 160, 120], [video, 20, 20, 160, 120]]);
|
||||
});
|
||||
|
||||
@@ -74,4 +74,13 @@ describe('Sidebar About', () => {
|
||||
expect(el.querySelectorAll('.magnify-about')).toHaveLength(1);
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('makes the tab wrapper fill the panel so the content does not collapse', () => {
|
||||
registerSidebar();
|
||||
const tab = vi.mocked(app.extensionManager.registerSidebarTab).mock.calls[0][0];
|
||||
const el = document.createElement('div');
|
||||
tab.render(el);
|
||||
|
||||
expect(el.style.height).toBe('100%');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,11 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
||||
import { getSettingValue } from "../shared/utils.js";
|
||||
import { captureRegion, scaleAbout, isSafeMediaSource, graphBoxToCss, previewImageBoxes, toGlass, fitContent } from "./mediaLayout.js";
|
||||
const TITLE_HEIGHT = 30;
|
||||
const VIRTUAL_ZOOM_MIN_INTERVAL_MS = 14;
|
||||
function boundingBox(node) {
|
||||
const [x, y, w, h] = node.boundingRect;
|
||||
return { x, y, w, h };
|
||||
}
|
||||
class OffscreenRenderer {
|
||||
constructor(config, state) {
|
||||
__publicField(this, "config");
|
||||
@@ -13,6 +18,7 @@ class OffscreenRenderer {
|
||||
__publicField(this, "lastRenderWidth", 0);
|
||||
__publicField(this, "lastRenderHeight", 0);
|
||||
__publicField(this, "isCapturing", false);
|
||||
__publicField(this, "lastVirtualZoomTime", 0);
|
||||
this.config = config;
|
||||
this.state = state;
|
||||
this.initOffscreenCanvas();
|
||||
@@ -41,45 +47,47 @@ class OffscreenRenderer {
|
||||
const cap = captureRegion(pivotX, pivotY, renderSize, this.config.zoomFactor);
|
||||
const vueNodes = !!((_a = window.LiteGraph) == null ? void 0 : _a.vueNodesMode);
|
||||
const currentScale = lgCanvas.ds.scale;
|
||||
const media = this.collectDomMedia(rect, vueNodes);
|
||||
if (currentScale < 1 && !this.config.forceDirectCapture && !vueNodes) {
|
||||
const domScale = 1 / currentScale;
|
||||
const scaled = media.map((m) => ({ ...m, box: scaleAbout(m.box, pivotX, pivotY, domScale) }));
|
||||
return this.renderVirtualZoom(lgCanvas, targetCanvas, rect, dpr, pivotX, pivotY, cap, scaled);
|
||||
const virtualZoom = currentScale < 1 && !this.config.forceDirectCapture && !vueNodes;
|
||||
if (virtualZoom) {
|
||||
const now = performance.now();
|
||||
if (now - this.lastVirtualZoomTime < VIRTUAL_ZOOM_MIN_INTERVAL_MS) return this.offscreenCanvas;
|
||||
this.lastVirtualZoomTime = now;
|
||||
}
|
||||
const media = this.collectDomMedia(rect, vueNodes, cap, pivotX, pivotY, virtualZoom ? 1 / currentScale : 1);
|
||||
if (virtualZoom) {
|
||||
return this.renderVirtualZoom(lgCanvas, targetCanvas, rect, dpr, pivotX, pivotY, cap, media);
|
||||
}
|
||||
return this.renderDirectCapture(targetCanvas, dpr, cap, media, vueNodes);
|
||||
}
|
||||
/**
|
||||
* Read phase: measure loaded, same-origin DOM images and videos (DOM widgets, or Vue node bodies)
|
||||
* before any canvas draw. Boxes are CSS pixels relative to the canvas at the real zoom.
|
||||
* under the glass before any canvas draw. Boxes are CSS pixels relative to the canvas, scaled by
|
||||
* domScale about the pivot into the capture frame.
|
||||
*/
|
||||
collectDomMedia(rect, vueNodes) {
|
||||
collectDomMedia(rect, vueNodes, cap, pivotX, pivotY, domScale) {
|
||||
const selector = vueNodes ? "[data-node-id] img, [data-node-id] video" : ".dom-widget img, .dom-widget video";
|
||||
const media = [];
|
||||
for (const el of document.querySelectorAll(selector)) {
|
||||
const loaded = el instanceof HTMLVideoElement ? el.readyState >= 2 : el.complete && el.naturalWidth > 0;
|
||||
if (!loaded || !isSafeMediaSource(el)) continue;
|
||||
if (!loaded) continue;
|
||||
const r = el.getBoundingClientRect();
|
||||
if (r.width === 0 || r.height === 0) continue;
|
||||
media.push({
|
||||
el,
|
||||
box: { x: r.left - rect.left, y: r.top - rect.top, w: r.width, h: r.height },
|
||||
objectFit: getComputedStyle(el).objectFit
|
||||
});
|
||||
const box = scaleAbout({ x: r.left - rect.left, y: r.top - rect.top, w: r.width, h: r.height }, pivotX, pivotY, domScale);
|
||||
if (box.x + box.w <= cap.left || box.x >= cap.left + cap.size) continue;
|
||||
if (box.y + box.h <= cap.top || box.y >= cap.top + cap.size) continue;
|
||||
if (!isSafeMediaSource(el)) continue;
|
||||
media.push({ el, box, objectFit: getComputedStyle(el).objectFit });
|
||||
}
|
||||
return media;
|
||||
}
|
||||
/**
|
||||
* Nodes of the graph on screen (including subgraphs) that overlap the capture region,
|
||||
* in canvas render order (back to front).
|
||||
* Nodes of the graph on screen (including subgraphs and collapsed nodes) whose title or body
|
||||
* overlaps the capture region, in canvas render order (back to front).
|
||||
*/
|
||||
getVisibleNodes(cap, scale, offset) {
|
||||
var _a;
|
||||
const visibleNodes = [];
|
||||
for (const node of app.canvas.visible_nodes) {
|
||||
if (!node.pos || !node.size) continue;
|
||||
if ((_a = node.flags) == null ? void 0 : _a.collapsed) continue;
|
||||
const box = graphBoxToCss({ x: 0, y: 0, w: node.size[0], h: node.size[1] }, node.pos, scale, offset);
|
||||
const box = graphBoxToCss(boundingBox(node), [0, 0], scale, offset);
|
||||
if (box.x + box.w < cap.left || box.x > cap.left + cap.size) continue;
|
||||
if (box.y + box.h < cap.top || box.y > cap.top + cap.size) continue;
|
||||
visibleNodes.push(node);
|
||||
@@ -218,7 +226,7 @@ class OffscreenRenderer {
|
||||
* into the glass. Video is redrawn every tick, so it plays.
|
||||
*/
|
||||
drawMedia(targetCanvas, dpr, visibleNodes, media, cap, scale, offset) {
|
||||
var _a, _b;
|
||||
var _a, _b, _c;
|
||||
const ctx = this.offscreenCtx;
|
||||
const renderSize = this.config.glassSize;
|
||||
const onGlass = (b) => b.w > 0 && b.h > 0 && b.x < renderSize && b.y < renderSize && b.x + b.w > 0 && b.y + b.h > 0;
|
||||
@@ -229,7 +237,7 @@ class OffscreenRenderer {
|
||||
ctx.imageSmoothingQuality = "high";
|
||||
const sizeTextMargin = getSettingValue("Comfy.Node.AllowImageSizeDraw", true) ? 15 : 0;
|
||||
for (const [i, node] of visibleNodes.entries()) {
|
||||
const widget = ((_a = node.imgs) == null ? void 0 : _a.length) && ((_b = node.widgets) == null ? void 0 : _b.find((w) => w.name === "$$canvas-image-preview"));
|
||||
const widget = !((_a = node.flags) == null ? void 0 : _a.collapsed) && ((_b = node.imgs) == null ? void 0 : _b.length) && ((_c = node.widgets) == null ? void 0 : _c.find((w) => w.name === "$$canvas-image-preview"));
|
||||
if (!widget) continue;
|
||||
for (const { img, box } of previewImageBoxes(node.imgs, widget, node, sizeTextMargin)) {
|
||||
const g = toGlass(graphBoxToCss(box, node.pos, scale, offset), cap);
|
||||
@@ -237,7 +245,7 @@ class OffscreenRenderer {
|
||||
}
|
||||
const nodeBox = toGlass(graphBoxToCss({ x: 0, y: 0, w: node.size[0], h: node.size[1] }, node.pos, scale, offset), cap);
|
||||
for (const front of visibleNodes.slice(i + 1)) {
|
||||
const f = toGlass(graphBoxToCss({ x: 0, y: -TITLE_HEIGHT, w: front.size[0], h: front.size[1] + TITLE_HEIGHT }, front.pos, scale, offset), cap);
|
||||
const f = toGlass(graphBoxToCss(boundingBox(front), [0, 0], scale, offset), cap);
|
||||
if (!overlaps(f, nodeBox)) continue;
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
@@ -269,7 +277,6 @@ class OffscreenRenderer {
|
||||
const ctx = this.offscreenCtx;
|
||||
const renderSize = this.config.glassSize;
|
||||
for (const node of visibleNodes) {
|
||||
if (!node.pos || !node.size) continue;
|
||||
if ((_a = node.flags) == null ? void 0 : _a.collapsed) continue;
|
||||
const title = node.title || node.type || "Node";
|
||||
const nodeCssX = (node.pos[0] + offset[0]) * scale;
|
||||
@@ -279,7 +286,7 @@ class OffscreenRenderer {
|
||||
if (nodeCssY + TITLE_HEIGHT * scale < cap.top || nodeCssY > cap.top + cap.size) continue;
|
||||
const canvasX = (nodeCssX - cap.left) * cap.k;
|
||||
const canvasY = (nodeCssY - cap.top) * cap.k;
|
||||
const canvasWidth = Math.min(nodeCssWidth, nodeCssWidth) * cap.k;
|
||||
const canvasWidth = nodeCssWidth * cap.k;
|
||||
const canvasHeight = TITLE_HEIGHT * scale * cap.k;
|
||||
if (canvasX + canvasWidth > 0 && canvasX < renderSize && canvasY + canvasHeight > 0 && canvasY < renderSize) {
|
||||
ctx.save();
|
||||
@@ -328,7 +335,7 @@ class OffscreenRenderer {
|
||||
const WIDGET_MARGIN = 4;
|
||||
const PADDING = 15;
|
||||
for (const node of visibleNodes) {
|
||||
if (!node.widgets || !node.pos || !node.size) continue;
|
||||
if (!node.widgets) continue;
|
||||
if ((_a = node.flags) == null ? void 0 : _a.collapsed) continue;
|
||||
const nodeCssX = (node.pos[0] + offset[0]) * scale;
|
||||
const nodeCssY = (node.pos[1] + offset[1]) * scale;
|
||||
|
||||
@@ -1,459 +0,0 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
import { DEFAULT_ZOOM_FACTOR, DEFAULT_GLASS_SIZE, DEFAULT_BORDER_WIDTH, DEFAULT_BORDER_COLOR, ACTIVATION_KEYS, DEFAULT_OFFSET_STEP, RESET_KEYS, GLASS_POSITIONS, GLASS_SHAPES, TOGGLE_FOLLOW_KEYS, DEFAULT_PANEL_MAX_HEIGHT, DEFAULT_PANEL_OPACITY, DEFAULT_PANEL_WIDTH, PANEL_POSITIONS } from "./constants.js";
|
||||
const DEFAULT_GLASS_SETTINGS = {
|
||||
"🔍MagnifyGlass.ZoomFactor": DEFAULT_ZOOM_FACTOR,
|
||||
"🔍MagnifyGlass.GlassSize": DEFAULT_GLASS_SIZE,
|
||||
"🔍MagnifyGlass.BorderColor": DEFAULT_BORDER_COLOR,
|
||||
"🔍MagnifyGlass.BorderWidth": DEFAULT_BORDER_WIDTH,
|
||||
"🔍MagnifyGlass.ActivationKey": "x",
|
||||
"🔍MagnifyGlass.AltRequired": false,
|
||||
"🔍MagnifyGlass.FollowCursor": false,
|
||||
"🔍MagnifyGlass.DebugMode": false,
|
||||
"🔍MagnifyGlass.OffsetStep": DEFAULT_OFFSET_STEP,
|
||||
"🔍MagnifyGlass.GlassPosition": "Top-Right",
|
||||
"🔍MagnifyGlass.ResetKey": "o",
|
||||
"🔍MagnifyGlass.GlassShape": "Rounded Square",
|
||||
"🔍MagnifyGlass.BorderEnabled": true,
|
||||
"🔍MagnifyGlass.TextureFiltering": "Linear",
|
||||
"🔍MagnifyGlass.AlwaysActiveMode": true,
|
||||
"🔍MagnifyGlass.ToggleFollowCursorKey": "h"
|
||||
};
|
||||
const DEFAULT_PANEL_SETTINGS = {
|
||||
"🔍MagnifyGlass.InfoPanelEnabled": true,
|
||||
"🔍MagnifyGlass.InfoPanelPosition": "Bottom",
|
||||
"🔍MagnifyGlass.InfoPanelWidth": DEFAULT_PANEL_WIDTH,
|
||||
"🔍MagnifyGlass.InfoPanelOpacity": DEFAULT_PANEL_OPACITY,
|
||||
"🔍MagnifyGlass.InfoPanelMaxHeight": DEFAULT_PANEL_MAX_HEIGHT,
|
||||
"🔍MagnifyGlass.InfoPanelAnimations": false,
|
||||
"🔍MagnifyGlass.ShowInspectorTab": false,
|
||||
"🔍MagnifyGlass.ToggleHotkey": "i",
|
||||
"🔍MagnifyGlass.GlassPreviewToggleHotkey": "g",
|
||||
"🔍MagnifyGlass.PinPanelHotkey": "u",
|
||||
"🔍MagnifyGlass.ShowHoveringControls": true,
|
||||
"🔍MagnifyGlass.ControlsPosition": "bottom",
|
||||
"🔍MagnifyGlass.InfoPanelTextColor": "#6b7280",
|
||||
"🔍MagnifyGlass.InfoPanelAccentColor": "#3b82f6"
|
||||
};
|
||||
function registerGlassSettings(magnifyGlass) {
|
||||
const settings = DEFAULT_GLASS_SETTINGS;
|
||||
app.ui.settings.addSetting({
|
||||
id: "🔍MagnifyGlass.ZoomFactor",
|
||||
name: "🔍 Magnify Glass: Zoom Factor (%)",
|
||||
type: "slider",
|
||||
defaultValue: settings["🔍MagnifyGlass.ZoomFactor"],
|
||||
min: 100,
|
||||
max: 1e3,
|
||||
step: 25,
|
||||
tooltip: "Magnification level as a percentage (e.g., 300 = 3x zoom, 150 = 1.5x zoom).",
|
||||
onChange: (value) => {
|
||||
if (magnifyGlass == null ? void 0 : magnifyGlass.config) {
|
||||
magnifyGlass.config.zoomFactor = typeof value === "number" ? value / 100 : parseFloat(value) / 100;
|
||||
if (magnifyGlass.state.active) {
|
||||
magnifyGlass.updateMagnifiedView();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "🔍MagnifyGlass.GlassSize",
|
||||
name: "🔍 Magnify Glass: Size (px)",
|
||||
type: "slider",
|
||||
defaultValue: settings["🔍MagnifyGlass.GlassSize"],
|
||||
min: 50,
|
||||
max: 100,
|
||||
step: 10,
|
||||
tooltip: "Diameter of the magnifying glass circle in pixels.",
|
||||
onChange: (value) => {
|
||||
if (magnifyGlass == null ? void 0 : magnifyGlass.config) {
|
||||
magnifyGlass.config.glassSize = parseInt(String(value), 10);
|
||||
magnifyGlass.applyUiChanges();
|
||||
if (magnifyGlass.state.active) {
|
||||
magnifyGlass.updateMagnifiedView();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "🔍MagnifyGlass.BorderWidth",
|
||||
name: "🔍 Magnify Glass: Border Width (px)",
|
||||
type: "slider",
|
||||
defaultValue: settings["🔍MagnifyGlass.BorderWidth"],
|
||||
min: 0,
|
||||
max: 10,
|
||||
step: 0.1,
|
||||
tooltip: "Width of the border around the magnifying glass.",
|
||||
onChange: (value) => {
|
||||
if (magnifyGlass == null ? void 0 : magnifyGlass.config) {
|
||||
magnifyGlass.config.borderWidth = parseFloat(String(value));
|
||||
magnifyGlass.applyUiChanges();
|
||||
}
|
||||
}
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "🔍MagnifyGlass.BorderColor",
|
||||
name: "🎨 Magnify Glass: Border Color",
|
||||
type: "color",
|
||||
defaultValue: settings["🔍MagnifyGlass.BorderColor"],
|
||||
tooltip: "Color of the border around the magnifying glass.",
|
||||
onChange: (value) => {
|
||||
if (magnifyGlass == null ? void 0 : magnifyGlass.config) {
|
||||
const strValue = String(value);
|
||||
const normalizedColor = strValue && !strValue.startsWith("#") ? `#${strValue}` : strValue;
|
||||
magnifyGlass.config.borderColor = normalizedColor;
|
||||
if (strValue !== normalizedColor) {
|
||||
try {
|
||||
app.ui.settings.setSettingValue("🔍MagnifyGlass.BorderColor", normalizedColor);
|
||||
} catch (e) {
|
||||
console.warn("Failed to save normalized border color:", e);
|
||||
}
|
||||
}
|
||||
magnifyGlass.applyUiChanges();
|
||||
}
|
||||
}
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "🔍MagnifyGlass.ActivationKey",
|
||||
name: "⌨️ Magnify Glass: Activation Key",
|
||||
type: "combo",
|
||||
options: ACTIVATION_KEYS.map((k) => ({ value: k, text: k })),
|
||||
// Assuming ACTIVATION_KEYS is simply an array of strings
|
||||
defaultValue: settings["🔍MagnifyGlass.ActivationKey"],
|
||||
tooltip: "The key to activate the magnifier.",
|
||||
onChange: (value) => {
|
||||
if (magnifyGlass == null ? void 0 : magnifyGlass.config) {
|
||||
magnifyGlass.config.activationKey = String(value).toLowerCase();
|
||||
}
|
||||
}
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "🔍MagnifyGlass.AltRequired",
|
||||
name: "⌨️ Magnify Glass: Require Alt/Option Key",
|
||||
type: "combo",
|
||||
options: [{ value: true, text: "Yes" }, { value: false, text: "No" }],
|
||||
defaultValue: settings["🔍MagnifyGlass.AltRequired"],
|
||||
tooltip: "If Yes, Alt (Windows/Linux) or Option (Mac) must be held for activation.",
|
||||
onChange: (value) => {
|
||||
if (magnifyGlass == null ? void 0 : magnifyGlass.config) {
|
||||
magnifyGlass.config.altRequired = !!value;
|
||||
}
|
||||
}
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "🔍MagnifyGlass.FollowCursor",
|
||||
name: "🖱️ Magnify Glass: Follow Cursor Position",
|
||||
type: "combo",
|
||||
options: [{ value: true, text: "Yes" }, { value: false, text: "No" }],
|
||||
defaultValue: settings["🔍MagnifyGlass.FollowCursor"],
|
||||
tooltip: "If Yes, the magnifier window moves with the cursor.",
|
||||
onChange: (value) => {
|
||||
if (magnifyGlass == null ? void 0 : magnifyGlass.config) {
|
||||
magnifyGlass.config.followCursor = !!value;
|
||||
}
|
||||
}
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "🔍MagnifyGlass.OffsetStep",
|
||||
name: "⌨️ Magnify Glass: Offset Adjust Step",
|
||||
type: "slider",
|
||||
defaultValue: settings["🔍MagnifyGlass.OffsetStep"],
|
||||
min: 1,
|
||||
max: 50,
|
||||
step: 1,
|
||||
tooltip: "How many graph units the view shifts when pressing arrow keys.",
|
||||
onChange: (value) => {
|
||||
if (magnifyGlass == null ? void 0 : magnifyGlass.config) {
|
||||
magnifyGlass.config.offsetStep = parseInt(String(value), 10);
|
||||
}
|
||||
}
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "🔍MagnifyGlass.ResetKey",
|
||||
name: "⌨️ Magnify Glass: Reset Offset Key",
|
||||
type: "combo",
|
||||
options: RESET_KEYS.map((k) => ({ value: k, text: k })),
|
||||
defaultValue: settings["🔍MagnifyGlass.ResetKey"],
|
||||
tooltip: "The key to reset the magnify glass offset to default.",
|
||||
onChange: (value) => {
|
||||
if (magnifyGlass == null ? void 0 : magnifyGlass.config) {
|
||||
magnifyGlass.config.resetKey = String(value).toLowerCase();
|
||||
}
|
||||
}
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "🔍MagnifyGlass.DebugMode",
|
||||
name: "🐞 Magnify Glass: Debug Mode",
|
||||
type: "combo",
|
||||
options: [{ value: true, text: "Enabled" }, { value: false, text: "Disabled" }],
|
||||
defaultValue: settings["🔍MagnifyGlass.DebugMode"],
|
||||
tooltip: "Show detailed logging and the debug visualization overlay.",
|
||||
onChange: (value) => {
|
||||
if (magnifyGlass == null ? void 0 : magnifyGlass.config) {
|
||||
magnifyGlass.config.debugMode = !!value;
|
||||
magnifyGlass.applyUiChanges();
|
||||
}
|
||||
}
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "🔍MagnifyGlass.GlassPosition",
|
||||
name: "🖱️ Magnify Glass: Glass Position",
|
||||
type: "combo",
|
||||
options: GLASS_POSITIONS.map((p) => ({ value: p, text: p === "Bottom" ? "Bottom (Default)" : p })),
|
||||
defaultValue: settings["🔍MagnifyGlass.GlassPosition"],
|
||||
tooltip: "Position of the magnifying glass relative to the cursor.",
|
||||
onChange: (value) => {
|
||||
if (magnifyGlass == null ? void 0 : magnifyGlass.config) {
|
||||
magnifyGlass.config.glassPosition = String(value);
|
||||
if (magnifyGlass.state.active && !magnifyGlass.config.followCursor) {
|
||||
const { x, y } = magnifyGlass.lastKnownMousePosition;
|
||||
magnifyGlass.ui.positionGlass(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "🔍MagnifyGlass.GlassShape",
|
||||
name: "🖼️ Magnify Glass: Shape",
|
||||
type: "combo",
|
||||
options: GLASS_SHAPES.map((s) => ({ value: s, text: s })),
|
||||
defaultValue: settings["🔍MagnifyGlass.GlassShape"],
|
||||
tooltip: "Shape of the magnifying glass.",
|
||||
onChange: (value) => {
|
||||
if (magnifyGlass == null ? void 0 : magnifyGlass.config) {
|
||||
magnifyGlass.config.glassShape = String(value);
|
||||
magnifyGlass.applyUiChanges();
|
||||
}
|
||||
}
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "🔍MagnifyGlass.BorderEnabled",
|
||||
name: "🖼️ Magnify Glass: Show Border",
|
||||
type: "combo",
|
||||
options: [{ value: true, text: "Yes" }, { value: false, text: "No" }],
|
||||
defaultValue: settings["🔍MagnifyGlass.BorderEnabled"],
|
||||
tooltip: "Enable or disable the border around the magnifying glass.",
|
||||
onChange: (value) => {
|
||||
if (magnifyGlass == null ? void 0 : magnifyGlass.config) {
|
||||
magnifyGlass.config.borderEnabled = !!value;
|
||||
magnifyGlass.applyUiChanges();
|
||||
}
|
||||
}
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "🔍MagnifyGlass.TextureFiltering",
|
||||
name: "🖼️ Magnify Glass: Texture Filtering",
|
||||
type: "combo",
|
||||
options: [
|
||||
{ value: "Linear", text: "Linear (Smooth)" },
|
||||
{ value: "Nearest", text: "Nearest (Pixelated)" }
|
||||
],
|
||||
defaultValue: settings["🔍MagnifyGlass.TextureFiltering"],
|
||||
tooltip: "Controls how the magnified image is scaled.",
|
||||
onChange: (value) => {
|
||||
if ((magnifyGlass == null ? void 0 : magnifyGlass.config) && magnifyGlass.renderer) {
|
||||
magnifyGlass.config.textureFiltering = String(value);
|
||||
magnifyGlass.renderer.updateTextureFiltering(value);
|
||||
if (magnifyGlass.state.active) {
|
||||
magnifyGlass.updateMagnifiedView();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "🔍MagnifyGlass.AlwaysActiveMode",
|
||||
name: "🔒 Magnify Glass: Always Active Mode",
|
||||
type: "combo",
|
||||
options: [{ value: true, text: "Yes" }, { value: false, text: "No" }],
|
||||
defaultValue: settings["🔍MagnifyGlass.AlwaysActiveMode"],
|
||||
tooltip: "If Yes, activating the magnifier keeps it on until activated again.",
|
||||
onChange: (value) => {
|
||||
if (magnifyGlass == null ? void 0 : magnifyGlass.config) {
|
||||
magnifyGlass.config.alwaysActiveMode = !!value;
|
||||
}
|
||||
}
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "🔍MagnifyGlass.ToggleFollowCursorKey",
|
||||
name: "🔑 Magnify Glass: Toggle Follow Key",
|
||||
type: "combo",
|
||||
options: TOGGLE_FOLLOW_KEYS.map((k) => ({ value: k, text: k })),
|
||||
defaultValue: settings["🔍MagnifyGlass.ToggleFollowCursorKey"],
|
||||
tooltip: "The key to toggle the 'Follow Cursor' behavior.",
|
||||
onChange: (value) => {
|
||||
if (magnifyGlass == null ? void 0 : magnifyGlass.config) {
|
||||
magnifyGlass.config.toggleFollowCursorKey = String(value).toLowerCase();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function registerPanelSettings(stateManager, uiManager, positionManager) {
|
||||
const settings = DEFAULT_PANEL_SETTINGS;
|
||||
app.ui.settings.addSetting({
|
||||
id: "🔍MagnifyGlass.InfoPanelEnabled",
|
||||
name: "📊 Info Panel: Enable",
|
||||
type: "combo",
|
||||
options: [{ value: true, text: "Yes" }, { value: false, text: "No" }],
|
||||
defaultValue: settings["🔍MagnifyGlass.InfoPanelEnabled"],
|
||||
tooltip: "Enable or disable the information panel.",
|
||||
onChange: (value) => {
|
||||
if (stateManager) {
|
||||
stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] = !!value;
|
||||
if (!value && uiManager) {
|
||||
uiManager.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "🔍MagnifyGlass.InfoPanelPosition",
|
||||
name: "📊 Info Panel: Position",
|
||||
type: "combo",
|
||||
options: PANEL_POSITIONS.map((p) => ({ value: p, text: p })),
|
||||
defaultValue: settings["🔍MagnifyGlass.InfoPanelPosition"],
|
||||
tooltip: "Position of the info panel relative to the magnifying glass.",
|
||||
onChange: (value) => {
|
||||
if (stateManager) {
|
||||
stateManager.state.settings["🔍MagnifyGlass.InfoPanelPosition"] = String(value);
|
||||
if (positionManager) {
|
||||
positionManager.positionPanel();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "🔍MagnifyGlass.InfoPanelWidth",
|
||||
name: "📊 Info Panel: Width (px)",
|
||||
type: "slider",
|
||||
defaultValue: settings["🔍MagnifyGlass.InfoPanelWidth"],
|
||||
min: 200,
|
||||
max: 600,
|
||||
step: 20,
|
||||
tooltip: "Width of the information panel in pixels.",
|
||||
onChange: (value) => {
|
||||
if (stateManager) {
|
||||
stateManager.state.settings["🔍MagnifyGlass.InfoPanelWidth"] = parseInt(String(value), 10);
|
||||
if (uiManager) {
|
||||
uiManager.applyStyles();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "🔍MagnifyGlass.InfoPanelOpacity",
|
||||
name: "📊 Info Panel: Opacity (%)",
|
||||
type: "slider",
|
||||
defaultValue: settings["🔍MagnifyGlass.InfoPanelOpacity"],
|
||||
min: 10,
|
||||
max: 100,
|
||||
step: 5,
|
||||
tooltip: "Opacity of the information panel background.",
|
||||
onChange: (value) => {
|
||||
if (stateManager) {
|
||||
stateManager.state.settings["🔍MagnifyGlass.InfoPanelOpacity"] = parseInt(String(value), 10);
|
||||
if (uiManager) {
|
||||
uiManager.applyStyles();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "🔍MagnifyGlass.InfoPanelMaxHeight",
|
||||
name: "📊 Info Panel: Max Height (px)",
|
||||
type: "slider",
|
||||
defaultValue: settings["🔍MagnifyGlass.InfoPanelMaxHeight"],
|
||||
min: 200,
|
||||
max: 1500,
|
||||
step: 50,
|
||||
tooltip: "Maximum height of the information panel in pixels.",
|
||||
onChange: (value) => {
|
||||
if (stateManager) {
|
||||
stateManager.state.settings["🔍MagnifyGlass.InfoPanelMaxHeight"] = parseInt(String(value), 10);
|
||||
if (uiManager) {
|
||||
uiManager.applyStyles();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "🔍MagnifyGlass.InfoPanelAnimations",
|
||||
name: "📊 Info Panel: Animations",
|
||||
type: "combo",
|
||||
options: [{ value: true, text: "Enabled" }, { value: false, text: "Disabled" }],
|
||||
defaultValue: settings["🔍MagnifyGlass.InfoPanelAnimations"],
|
||||
tooltip: "Enable or disable animations for the info panel.",
|
||||
onChange: (value) => {
|
||||
if (stateManager) {
|
||||
stateManager.state.settings["🔍MagnifyGlass.InfoPanelAnimations"] = !!value;
|
||||
}
|
||||
}
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "🔍MagnifyGlass.ShowInspectorTab",
|
||||
name: "📊 Info Panel: Show Inspector Tab",
|
||||
type: "combo",
|
||||
options: [{ value: true, text: "Yes" }, { value: false, text: "No" }],
|
||||
defaultValue: settings["🔍MagnifyGlass.ShowInspectorTab"],
|
||||
tooltip: "Show or hide the Inspector tab with cursor and canvas information.",
|
||||
onChange: (value) => {
|
||||
if (stateManager) {
|
||||
stateManager.state.settings["🔍MagnifyGlass.ShowInspectorTab"] = !!value;
|
||||
}
|
||||
}
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "🔍MagnifyGlass.ShowHoveringControls",
|
||||
name: "📊 Info Panel: Show Hover Controls",
|
||||
type: "combo",
|
||||
options: [{ value: true, text: "Yes" }, { value: false, text: "No" }],
|
||||
defaultValue: settings["🔍MagnifyGlass.ShowHoveringControls"],
|
||||
tooltip: "Show or hide hovering UI controls above the info panel.",
|
||||
onChange: (value) => {
|
||||
if (stateManager) {
|
||||
stateManager.state.settings["🔍MagnifyGlass.ShowHoveringControls"] = !!value;
|
||||
if (uiManager && uiManager.elements.controls) {
|
||||
uiManager.elements.controls.style.display = value ? "flex" : "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "🔍MagnifyGlass.InfoPanelTextColor",
|
||||
name: "🎨 Info Panel: Text Color",
|
||||
type: "color",
|
||||
defaultValue: settings["🔍MagnifyGlass.InfoPanelTextColor"],
|
||||
tooltip: "Custom text color for the info panel.",
|
||||
onChange: (value) => {
|
||||
if (stateManager) {
|
||||
const strValue = String(value);
|
||||
const normalizedColor = strValue && !strValue.startsWith("#") ? `#${strValue}` : strValue;
|
||||
stateManager.state.settings["🔍MagnifyGlass.InfoPanelTextColor"] = normalizedColor;
|
||||
if (uiManager) {
|
||||
uiManager.applyStyles();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "🔍MagnifyGlass.InfoPanelAccentColor",
|
||||
name: "🎨 Info Panel: Accent Color",
|
||||
type: "color",
|
||||
defaultValue: settings["🔍MagnifyGlass.InfoPanelAccentColor"],
|
||||
tooltip: "Custom accent color for the info panel.",
|
||||
onChange: (value) => {
|
||||
if (stateManager) {
|
||||
const strValue = String(value);
|
||||
const normalizedColor = strValue && !strValue.startsWith("#") ? `#${strValue}` : strValue;
|
||||
stateManager.state.settings["🔍MagnifyGlass.InfoPanelAccentColor"] = normalizedColor;
|
||||
if (uiManager) {
|
||||
uiManager.applyStyles();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
export {
|
||||
DEFAULT_GLASS_SETTINGS,
|
||||
DEFAULT_PANEL_SETTINGS,
|
||||
registerGlassSettings,
|
||||
registerPanelSettings
|
||||
};
|
||||
//# sourceMappingURL=settings.js.map
|
||||
+1
-1
@@ -82,7 +82,7 @@
|
||||
|
||||
/* Body: the panel's one scroller */
|
||||
.magnify-sidebar-content {
|
||||
flex: 1 1 0;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -24,6 +24,7 @@ function renderSidebar(container) {
|
||||
return;
|
||||
}
|
||||
container.innerHTML = "";
|
||||
container.style.height = "100%";
|
||||
const sidebar = document.createElement("div");
|
||||
sidebar.className = "magnify-sidebar";
|
||||
sidebar.style.visibility = "hidden";
|
||||
|
||||
Reference in New Issue
Block a user