From 2b8782a8d4e7ffa2137c6d42886babd2d006b633 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 2 Jan 2026 07:22:08 +0000 Subject: [PATCH] Update traffic statistics --- .github/workflows/git_clones.yml | 82 +++ .github/workflows/publish_action.yml | 20 + .gitignore | 43 ++ CHANGELOG.md | 56 ++ LICENSE | 674 +++++++++++++++++++++ README.md | 124 ++++ __init__.py | 15 + css/info-panel.css | 664 ++++++++++++++++++++ css/sidebar.css | 352 +++++++++++ git_clones.json | 4 + package.json | 20 + playwright.config.ts | 49 ++ pyproject.toml | 14 + src/info-panel/EventManager.ts | 227 +++++++ src/info-panel/InfoPanel.ts | 185 ++++++ src/info-panel/InformationGatherer.ts | 326 ++++++++++ src/info-panel/NodeDataExtractor.ts | 259 ++++++++ src/info-panel/PositionManager.ts | 205 +++++++ src/info-panel/StateManager.ts | 420 +++++++++++++ src/info-panel/UIManager.ts | 707 ++++++++++++++++++++++ src/info-panel/ValueFormatter.ts | 95 +++ src/info-panel/index.ts | 6 + src/magnify-glass/ConfigManager.ts | 182 ++++++ src/magnify-glass/DebugManager.ts | 181 ++++++ src/magnify-glass/EventHandler.ts | 226 +++++++ src/magnify-glass/MagnifierState.ts | 80 +++ src/magnify-glass/MagnifyGlass.ts | 427 +++++++++++++ src/magnify-glass/UiManager.ts | 420 +++++++++++++ src/magnify-glass/WebGLRenderer.ts | 437 +++++++++++++ src/magnify-glass/index.ts | 13 + src/magnify_glass.ts | 32 + src/magnify_info_panel.ts | 53 ++ src/shared/constants.ts | 86 +++ src/shared/icons.ts | 89 +++ src/shared/index.ts | 3 + src/shared/logger.ts | 87 +++ src/shared/settings.ts | 162 +++++ src/shared/settings/defaults.ts | 59 ++ src/shared/settings/glassSettings.ts | 365 +++++++++++ src/shared/settings/index.ts | 15 + src/shared/settings/panelSettings.ts | 192 ++++++ src/shared/settings/types.ts | 53 ++ src/shared/utils.ts | 161 +++++ src/sidebar/SidebarInspector.ts | 261 ++++++++ src/sidebar/SidebarSettings.ts | 697 +++++++++++++++++++++ src/sidebar/index.ts | 8 + src/sidebar/sidebar.ts | 106 ++++ src/types/comfy-scripts.d.ts | 11 + src/types/comfyui.d.ts | 387 ++++++++++++ src/types/vue-shims.d.ts | 5 + tests/e2e/example.spec.ts | 12 + tests/mocks/app.ts | 15 + tests/unit/ConfigManager.test.ts | 117 ++++ tests/unit/InformationGatherer.test.ts | 158 +++++ tests/unit/MagnifierState.test.ts | 103 ++++ tests/unit/StateManager.test.ts | 268 ++++++++ tests/unit/utils.test.ts | 29 + traffic_stats.json | 11 + tsconfig.json | 44 ++ vite.config.ts | 42 ++ vitest.config.ts | 22 + web/info-panel-old.css | 664 ++++++++++++++++++++ web/info-panel.css | 674 +++++++++++++++++++++ web/info-panel/EventManager.js | 153 +++++ web/info-panel/EventManager.js.map | 1 + web/info-panel/InfoPanel.js | 135 +++++ web/info-panel/InfoPanel.js.map | 1 + web/info-panel/InformationGatherer.js | 275 +++++++++ web/info-panel/InformationGatherer.js.map | 1 + web/info-panel/NodeDataExtractor.js | 209 +++++++ web/info-panel/NodeDataExtractor.js.map | 1 + web/info-panel/PositionManager.js | 160 +++++ web/info-panel/PositionManager.js.map | 1 + web/info-panel/StateManager.js | 285 +++++++++ web/info-panel/StateManager.js.map | 1 + web/info-panel/UIManager.js | 517 ++++++++++++++++ web/info-panel/UIManager.js.map | 1 + web/info-panel/ValueFormatter.js | 50 ++ web/info-panel/ValueFormatter.js.map | 1 + web/magnify-glass/ConfigManager.js | 148 +++++ web/magnify-glass/ConfigManager.js.map | 1 + web/magnify-glass/DebugManager.js | 128 ++++ web/magnify-glass/DebugManager.js.map | 1 + web/magnify-glass/EventHandler.js | 173 ++++++ web/magnify-glass/EventHandler.js.map | 1 + web/magnify-glass/MagnifierState.js | 64 ++ web/magnify-glass/MagnifierState.js.map | 1 + web/magnify-glass/MagnifyGlass.js | 330 ++++++++++ web/magnify-glass/MagnifyGlass.js.map | 1 + web/magnify-glass/UiManager.js | 331 ++++++++++ web/magnify-glass/UiManager.js.map | 1 + web/magnify-glass/WebGLRenderer.js | 329 ++++++++++ web/magnify-glass/WebGLRenderer.js.map | 1 + web/magnify_glass.js | 15 + web/magnify_glass.js.map | 1 + web/magnify_info_panel.js | 31 + web/magnify_info_panel.js.map | 1 + web/shared/constants.js | 63 ++ web/shared/constants.js.map | 1 + web/shared/icons.js | 61 ++ web/shared/icons.js.map | 1 + web/shared/logger.js | 44 ++ web/shared/logger.js.map | 1 + web/shared/settings.js | 459 ++++++++++++++ web/shared/settings.js.map | 1 + web/shared/settings/defaults.js | 40 ++ web/shared/settings/defaults.js.map | 1 + web/shared/settings/glassSettings.js | 328 ++++++++++ web/shared/settings/glassSettings.js.map | 1 + web/shared/settings/panelSettings.js | 172 ++++++ web/shared/settings/panelSettings.js.map | 1 + web/shared/utils.js | 53 ++ web/shared/utils.js.map | 1 + web/sidebar.css | 352 +++++++++++ web/sidebar/SidebarInspector.js | 164 +++++ web/sidebar/SidebarInspector.js.map | 1 + web/sidebar/SidebarSettings.js | 568 +++++++++++++++++ web/sidebar/SidebarSettings.js.map | 1 + web/sidebar/sidebar.js | 64 ++ web/sidebar/sidebar.js.map | 1 + 120 files changed, 17233 insertions(+) create mode 100644 .github/workflows/git_clones.yml create mode 100644 .github/workflows/publish_action.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 __init__.py create mode 100644 css/info-panel.css create mode 100644 css/sidebar.css create mode 100644 git_clones.json create mode 100644 package.json create mode 100644 playwright.config.ts create mode 100644 pyproject.toml create mode 100644 src/info-panel/EventManager.ts create mode 100644 src/info-panel/InfoPanel.ts create mode 100644 src/info-panel/InformationGatherer.ts create mode 100644 src/info-panel/NodeDataExtractor.ts create mode 100644 src/info-panel/PositionManager.ts create mode 100644 src/info-panel/StateManager.ts create mode 100644 src/info-panel/UIManager.ts create mode 100644 src/info-panel/ValueFormatter.ts create mode 100644 src/info-panel/index.ts create mode 100644 src/magnify-glass/ConfigManager.ts create mode 100644 src/magnify-glass/DebugManager.ts create mode 100644 src/magnify-glass/EventHandler.ts create mode 100644 src/magnify-glass/MagnifierState.ts create mode 100644 src/magnify-glass/MagnifyGlass.ts create mode 100644 src/magnify-glass/UiManager.ts create mode 100644 src/magnify-glass/WebGLRenderer.ts create mode 100644 src/magnify-glass/index.ts create mode 100644 src/magnify_glass.ts create mode 100644 src/magnify_info_panel.ts create mode 100644 src/shared/constants.ts create mode 100644 src/shared/icons.ts create mode 100644 src/shared/index.ts create mode 100644 src/shared/logger.ts create mode 100644 src/shared/settings.ts create mode 100644 src/shared/settings/defaults.ts create mode 100644 src/shared/settings/glassSettings.ts create mode 100644 src/shared/settings/index.ts create mode 100644 src/shared/settings/panelSettings.ts create mode 100644 src/shared/settings/types.ts create mode 100644 src/shared/utils.ts create mode 100644 src/sidebar/SidebarInspector.ts create mode 100644 src/sidebar/SidebarSettings.ts create mode 100644 src/sidebar/index.ts create mode 100644 src/sidebar/sidebar.ts create mode 100644 src/types/comfy-scripts.d.ts create mode 100644 src/types/comfyui.d.ts create mode 100644 src/types/vue-shims.d.ts create mode 100644 tests/e2e/example.spec.ts create mode 100644 tests/mocks/app.ts create mode 100644 tests/unit/ConfigManager.test.ts create mode 100644 tests/unit/InformationGatherer.test.ts create mode 100644 tests/unit/MagnifierState.test.ts create mode 100644 tests/unit/StateManager.test.ts create mode 100644 tests/unit/utils.test.ts create mode 100644 traffic_stats.json create mode 100644 tsconfig.json create mode 100644 vite.config.ts create mode 100644 vitest.config.ts create mode 100644 web/info-panel-old.css create mode 100644 web/info-panel.css create mode 100644 web/info-panel/EventManager.js create mode 100644 web/info-panel/EventManager.js.map create mode 100644 web/info-panel/InfoPanel.js create mode 100644 web/info-panel/InfoPanel.js.map create mode 100644 web/info-panel/InformationGatherer.js create mode 100644 web/info-panel/InformationGatherer.js.map create mode 100644 web/info-panel/NodeDataExtractor.js create mode 100644 web/info-panel/NodeDataExtractor.js.map create mode 100644 web/info-panel/PositionManager.js create mode 100644 web/info-panel/PositionManager.js.map create mode 100644 web/info-panel/StateManager.js create mode 100644 web/info-panel/StateManager.js.map create mode 100644 web/info-panel/UIManager.js create mode 100644 web/info-panel/UIManager.js.map create mode 100644 web/info-panel/ValueFormatter.js create mode 100644 web/info-panel/ValueFormatter.js.map create mode 100644 web/magnify-glass/ConfigManager.js create mode 100644 web/magnify-glass/ConfigManager.js.map create mode 100644 web/magnify-glass/DebugManager.js create mode 100644 web/magnify-glass/DebugManager.js.map create mode 100644 web/magnify-glass/EventHandler.js create mode 100644 web/magnify-glass/EventHandler.js.map create mode 100644 web/magnify-glass/MagnifierState.js create mode 100644 web/magnify-glass/MagnifierState.js.map create mode 100644 web/magnify-glass/MagnifyGlass.js create mode 100644 web/magnify-glass/MagnifyGlass.js.map create mode 100644 web/magnify-glass/UiManager.js create mode 100644 web/magnify-glass/UiManager.js.map create mode 100644 web/magnify-glass/WebGLRenderer.js create mode 100644 web/magnify-glass/WebGLRenderer.js.map create mode 100644 web/magnify_glass.js create mode 100644 web/magnify_glass.js.map create mode 100644 web/magnify_info_panel.js create mode 100644 web/magnify_info_panel.js.map create mode 100644 web/shared/constants.js create mode 100644 web/shared/constants.js.map create mode 100644 web/shared/icons.js create mode 100644 web/shared/icons.js.map create mode 100644 web/shared/logger.js create mode 100644 web/shared/logger.js.map create mode 100644 web/shared/settings.js create mode 100644 web/shared/settings.js.map create mode 100644 web/shared/settings/defaults.js create mode 100644 web/shared/settings/defaults.js.map create mode 100644 web/shared/settings/glassSettings.js create mode 100644 web/shared/settings/glassSettings.js.map create mode 100644 web/shared/settings/panelSettings.js create mode 100644 web/shared/settings/panelSettings.js.map create mode 100644 web/shared/utils.js create mode 100644 web/shared/utils.js.map create mode 100644 web/sidebar.css create mode 100644 web/sidebar/SidebarInspector.js create mode 100644 web/sidebar/SidebarInspector.js.map create mode 100644 web/sidebar/SidebarSettings.js create mode 100644 web/sidebar/SidebarSettings.js.map create mode 100644 web/sidebar/sidebar.js create mode 100644 web/sidebar/sidebar.js.map diff --git a/.github/workflows/git_clones.yml b/.github/workflows/git_clones.yml new file mode 100644 index 0000000..228314d --- /dev/null +++ b/.github/workflows/git_clones.yml @@ -0,0 +1,82 @@ +name: Update Clone Count Badge + +on: + schedule: + - cron: '0 0 * * *' # Runs daily at midnight + workflow_dispatch: # Allows manual trigger + +jobs: + update-badge: + runs-on: ubuntu-latest + permissions: + contents: write # Needed to push to the branch + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Fetch Traffic Stats and Save JSON + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.TRAFFIC_TOKEN }} + script: | + try { + const { owner, repo } = context.repo; + + // 1. Fetch Clones + const clones = await github.rest.repos.getClones({ + owner, + repo, + }); + + // 2. Fetch Views (Visitors) + const views = await github.rest.repos.getViews({ + owner, + repo, + }); + + const fs = require('fs'); + const data = { + clones: { + count: clones.data.count, + uniques: clones.data.uniques, + }, + views: { + count: views.data.count, + uniques: views.data.uniques, + }, + timestamp: new Date().toISOString() + }; + + console.log('Stats fetched:', JSON.stringify(data)); + + // Write to a generic stats file + fs.writeFileSync('traffic_stats.json', JSON.stringify(data, null, 2)); + // Maintain the old file for backward compatibility if needed, or just switch everything + fs.writeFileSync('git_clones.json', JSON.stringify(data.clones, null, 2)); + + } catch (error) { + console.error('Error fetching stats:', error); + process.exit(1); + } + + - name: Push to Badges Branch + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + + # Stash the files + mv traffic_stats.json /tmp/traffic_stats.json + mv git_clones.json /tmp/git_clones.json + + # Fetch the badges branch or create it + git fetch origin badges:badges || true + git checkout badges || git checkout --orphan badges + + # Restore the files + mv /tmp/traffic_stats.json traffic_stats.json + mv /tmp/git_clones.json git_clones.json + + # Commit and push + git add traffic_stats.json git_clones.json + git diff --quiet && git diff --staged --quiet || (git commit -m "Update traffic statistics" && git push origin badges) diff --git a/.github/workflows/publish_action.yml b/.github/workflows/publish_action.yml new file mode 100644 index 0000000..122a1ae --- /dev/null +++ b/.github/workflows/publish_action.yml @@ -0,0 +1,20 @@ +name: Publish to Comfy registry +on: + workflow_dispatch: + push: + branches: + - main + paths: + - "pyproject.toml" + +jobs: + publish-node: + name: Publish Custom Node to registry + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Publish Custom Node + uses: Comfy-Org/publish-node-action@main + with: + personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} ## Add your own personal access token to your Github Repository secrets and reference it here. \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..143a347 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info/ +.eggs/ + +# Node.js / TypeScript build tooling +node_modules/ +pnpm-lock.yaml +package-lock.json +yarn.lock + +# IDE / Editor +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Build artifacts (optional - see note below) +# Note: The web/ directory contains the compiled JS that ComfyUI serves. +# You should COMMIT web/ so users don't need to build from source. +# But these specific build artifacts can be ignored: +web/node_modules/ + +# TypeScript build cache +*.tsbuildinfo + +# Test artifacts +test-results/ +playwright-report/ +.playwright/ + +# Debug files +Broswer_Console_Errors.md + +# Environment +.env +.env.local \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..34ce4ab --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,56 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.5.0] - 2024-12-31 + +### Added +- **Quick Toggle Button**: Integrated Magnify Glass toggle into the main ComfyUI toolbar (bottom-right). +- **Inspector Persistence**: Panel now remembers if it was open or closed when toggling the tool. +- **Glass Preview Toggle**: New specific toggle for the visual glass circle (allowing "Inspector Only" mode). +- **Smart Workflow Logic**: + - Auto-Pin Inspector (Unlocked Mode) when hiding Glass Preview. + - Auto-Lock Inspector (Follow Mode) when showing Glass Preview. +- Professional SVG icons replacing emoji icons throughout the UI +- Full theme support for ComfyUI themes: Dark, Light, Solarized, Arc, Nord, GitHub +- External CSS stylesheet for easier customization +- Click event handlers for all floating control buttons +- Section expand/collapse functionality in info panel + +### Improved +- **Drag Safety**: Dragging is now disabled when following the glass to prevent position fighting. +- **Visibility Interlocks**: + - "Unpin" button disabled when glass is hidden (prevents following invisible cursor). + - "Hide Inspector" button disabled when glass is hidden (prevents hiding all UI). +- **Global Toggle**: The 'X' key/button now acts as a master switch, preserving your exact workspace state. + +### Changed +- Migrated from inline CSS-in-JS to external `info-panel.css` file +- Improved theme detection using ComfyUI's `Comfy.ColorPalette` setting +- More professional appearance with custom SVG icon set + +### Removed +- Vue.js dependency (reduces bundle size) +- Emoji icons (replaced with SVG for better scaling and consistency) +- Inline CSS injection (now uses external stylesheet) + +### Fixed +- Button click handlers now properly attached to floating controls +- Theme switching now works across all 6 ComfyUI themes +- Removed stale build artifacts that caused module loading errors + +## [1.4.0] - Previous Release + +### Added +- TypeScript source code +- WebGL-based magnification +- Info panel with node, media, and inspector sections +- Floating control buttons +- Multiple glass shapes and positions + +--- + +For more information, see the [README](README.md). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1fedc1a --- /dev/null +++ b/README.md @@ -0,0 +1,124 @@ +
+ +# ComfyUI-MagnifyGlass + +[![ComfyUI](https://img.shields.io/badge/ComfyUI-Extension-green?style=for-the-badge)](https://github.com/comfyanonymous/ComfyUI) +[![Dependencies](https://img.shields.io/badge/dependencies-none-brightgreen?style=for-the-badge&color=blue)](package.json) +[![License](https://img.shields.io/badge/License-GPLv3-red?style=for-the-badge)](LICENSE) +[![Version](https://img.shields.io/badge/Version-1.5.0-orange?style=for-the-badge)](https://github.com/AEmotionStudio/ComfyUI-MagnifyGlass/releases) +[![Downloads](https://img.shields.io/github/downloads/AEmotionStudio/ComfyUI-MagnifyGlass/total?style=for-the-badge&color=blueviolet)](https://github.com/AEmotionStudio/ComfyUI-MagnifyGlass/releases) +![Visitors](https://img.shields.io/badge/dynamic/json?color=blue&label=Visitors&query=views.uniques&url=https://raw.githubusercontent.com/AEmotionStudio/ComfyUI-MagnifyGlass/badges/traffic_stats.json&style=for-the-badge&logo=github) +[![Clones](https://img.shields.io/badge/dynamic/json?color=success&label=Clones&query=clones.uniques&url=https://raw.githubusercontent.com/AEmotionStudio/ComfyUI-MagnifyGlass/badges/traffic_stats.json&style=for-the-badge&logo=github)](https://github.com/AEmotionStudio/ComfyUI-MagnifyGlass/graphs/traffic) + +**A powerful, customizable magnifying glass extension for ComfyUI.** +*Inspect fine details in your generated images, node connections, and canvas with ease.* + +[Features](#features) • [Installation](#installation) • [Usage](#usage) • [Configuration](#configuration) • [Changelog](CHANGELOG.md) + +
+ +--- + +## 🚀 Latest Updates (v1.5.0) + +I've completely overhauled the UI and workflow for a seamless experience. Major highlights include: + +* **Modernized UI**: Professional SVG icons and full support for all ComfyUI themes (Dark, Solarized, Nord, etc.). +* **Smart Workflow**: The Inspector Panel now intelligently pins/unpins based on your usage, allowing for both "Follow Glass" and "Free Floating" modes. +* **Quick Access**: New toolbar toggle for instant access. + +> 📄 **See [CHANGELOG.md](CHANGELOG.md) for the complete version history and detailed patch notes.** + +--- + +## ✨ Features + +
+ Magnify Glass Demo +
+ +### 🔍 Magnifying Glass +* **High Performance**: WebGL-powered rendering for smooth zooming. +* **Smart Interactions**: Follows your cursor or stays fixed. Toggles transparently to let you click through to nodes. +* **Customizable**: Adjust zoom level (up to 10x), size, border, and shape (Circle/Square). + +### ℹ️ Inspector Panel +* **Deep Analysis**: Hover over any node to see parameters (Seed, CFG, Steps), text content, and image details. +* **Dockable Interface**: Pin the panel to the screen to keep it stable, or let it follow the glass. +* **Inspector Tab**: Technical breakdown of coordinate space and scaling for power users. + +--- + +## 📦 Installation + +### Option 1: ComfyUI Manager (Recommended) +1. Open **ComfyUI Manager**. +2. Search for **`ComfyUI-MagnifyGlass`**. +3. Click **Install**. + +### Option 2: Manual Install +```bash +cd /path/to/ComfyUI/custom_nodes +git clone https://github.com/AEmotionStudio/ComfyUI-MagnifyGlass.git +``` + +--- + +## 🎮 Usage + +| Key | Action | +| :--- | :--- | +| **`X`** | **Activate / Toggle Tool** (Master Switch) | +| **`H`** | Toggle Follow Cursor Mode | +| **`I`** | Toggle Inspector Panel Visibility | +| **`G`** | Toggle Glass Preview (Enters "Inspector Only" Mode) | +| **`U`** | Pin/Unpin Inspector Panel | +| **`O`** | Reset Offsets | +| **Arrows** | Nudge Glass Position | + +> **Pro Tip:** Toggle the **Glass Preview (`G`)** off to keep the Inspector Panel active but hide the magnifying preview. + +--- + +## ⚙️ Configuration + +Access settings via the ComfyUI Settings (⚙️) menu. + +**Magnify Glass** +| Setting | Default | Description | +| :--- | :--- | :--- | +| **Zoom Factor** | `300` | Magnification level (300 = 3x). | +| **Glass Size** | `300px` | Diameter of the lens. | +| **Shape** | `Rounded` | Circle, Square, or Rounded Square. | +| **Activation Key** | `X` | Hotkey to toggle the tool. | + +**Information Panel** +| Setting | Default | Description | +| :--- | :--- | :--- | +| **Theme** | `Auto` | Syncs with ComfyUI theme automatically. | +| **Opacity** | `100%` | Transparency of the panel background. | +| **Show Hover Controls** | `On` | Displays the quick-action toolbar on the panel. | + +--- + +## 🤝 Contributing + +Contributions are welcome! Please submit a Pull Request or open an Issue on GitHub. + +1. Fork the Project +2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) +3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to the Branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request + +--- + +
+ +**Developed by [AEmotionStudio](https://aemotionstudio.org/)** + +[![YouTube](https://img.shields.io/badge/YouTube-FF0000?style=for-the-badge&logo=youtube&logoColor=white)](https://www.youtube.com/@aemotionstudio/videos) +[![Discord](https://img.shields.io/badge/Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/UzC9353mfp) +[![Ko-fi](https://img.shields.io/badge/Ko--fi-F16061?style=for-the-badge&logo=ko-fi&logoColor=white)](https://ko-fi.com/aemotionstudio) + +
diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..d687ac4 --- /dev/null +++ b/__init__.py @@ -0,0 +1,15 @@ +# Define the web directory for our extension +WEB_DIRECTORY = "./web" + +# Required to register as a ComfyUI extension +NODE_CLASS_MAPPINGS = {} +NODE_DISPLAY_NAME_MAPPINGS = {} + +# List of JS files to be loaded +__js_files__ = ["magnify_glass.js", "magnify_info_panel.js"] + +# List of exported elements +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY', '__js_files__'] + +# Print a message to confirm the extension loaded +print("ComfyUI Magnify Glass extension loaded") \ No newline at end of file diff --git a/css/info-panel.css b/css/info-panel.css new file mode 100644 index 0000000..ce9d79d --- /dev/null +++ b/css/info-panel.css @@ -0,0 +1,664 @@ +/** + * ComfyUI MagnifyGlass - Info Panel Styles + * + * Complete CSS styles for the info panel component. + */ + +/* SVG Icon Styling */ +.magnify-info-panel svg, +.floating-controls svg { + width: 1em; + height: 1em; + vertical-align: middle; + fill: none; + stroke: currentColor; + pointer-events: none; + /* Allow clicks to pass through to button */ +} + +.header-icon svg { + width: 18px; + height: 18px; +} + +.section-icon svg { + width: 18px; + height: 18px; +} + +.control-btn svg { + width: 14px; + height: 14px; +} + +.expand-icon svg { + width: 12px; + height: 12px; +} + +.empty-state-icon svg { + width: 36px; + height: 36px; + stroke-width: 1.5; +} + +.magnify-info-panel { + border-radius: 12px; + backdrop-filter: blur(20px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.1); + overflow: hidden; + font-size: 16px; + line-height: 1.6; + pointer-events: auto; +} + +.magnify-info-panel * { + pointer-events: auto; +} + +.magnify-info-panel.theme-dark { + background: linear-gradient(135deg, rgba(0, 0, 0, 0.96), rgba(10, 10, 15, 0.96)); + color: var(--info-panel-text-color, #e0e0e0); + border: 1px solid rgba(100, 100, 120, 0.4); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 16px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(12px); +} + +.magnify-info-panel.theme-light { + background: linear-gradient(135deg, rgba(250, 250, 255, 0.96), rgba(240, 240, 250, 0.96)); + color: var(--info-panel-text-color, #2a2a2a); + border: 1px solid rgba(200, 200, 220, 0.5); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 2px 16px rgba(0, 0, 0, 0.05); + backdrop-filter: blur(12px); +} + +/* Solarized Theme - warm, muted tones */ +.magnify-info-panel.theme-solarized { + background: linear-gradient(135deg, rgba(0, 43, 54, 0.96), rgba(7, 54, 66, 0.96)); + color: var(--info-panel-text-color, #839496); + border: 1px solid rgba(88, 110, 117, 0.5); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 16px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(12px); + --info-panel-accent-color: #268bd2; +} + +/* Arc Theme - modern blue-gray */ +.magnify-info-panel.theme-arc { + background: linear-gradient(135deg, rgba(56, 60, 74, 0.96), rgba(64, 69, 82, 0.96)); + color: var(--info-panel-text-color, #d3dae3); + border: 1px solid rgba(90, 100, 120, 0.5); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25), 0 2px 16px rgba(0, 0, 0, 0.15); + backdrop-filter: blur(12px); + --info-panel-accent-color: #5294e2; +} + +/* Nord Theme - cool arctic colors */ +.magnify-info-panel.theme-nord { + background: linear-gradient(135deg, rgba(46, 52, 64, 0.96), rgba(59, 66, 82, 0.96)); + color: var(--info-panel-text-color, #d8dee9); + border: 1px solid rgba(76, 86, 106, 0.5); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25), 0 2px 16px rgba(0, 0, 0, 0.15); + backdrop-filter: blur(12px); + --info-panel-accent-color: #88c0d0; +} + +/* GitHub Theme - clean, professional */ +.magnify-info-panel.theme-github { + background: linear-gradient(135deg, rgba(255, 255, 255, 0.96), rgba(246, 248, 250, 0.96)); + color: var(--info-panel-text-color, #24292e); + border: 1px solid rgba(225, 228, 232, 0.8); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 2px 16px rgba(0, 0, 0, 0.05); + backdrop-filter: blur(12px); + --info-panel-accent-color: #0366d6; +} + +.magnify-info-panel.visible { + transform: translateY(0) !important; +} + +.magnify-info-panel.panel-pinned { + cursor: grab; + box-shadow: 0 0 0 2px rgba(255, 215, 0, 0.5), 0 20px 40px rgba(0, 0, 0, 0.3); +} + +.magnify-info-panel.panel-locked { + cursor: default !important; + box-shadow: 0 0 0 2px rgba(255, 69, 0, 0.5), 0 20px 40px rgba(0, 0, 0, 0.3); +} + +.magnify-info-panel.panel-locked::after { + content: '📌'; + position: absolute; + top: 8px; + right: 8px; + font-size: 14px; + opacity: 0.7; + pointer-events: none; + z-index: 1; +} + +.magnify-info-panel.panel-pinned.panel-dragging { + cursor: grabbing; + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5), 0 0 0 2px rgba(255, 215, 0, 0.7); + transform: scale(1.02) !important; + z-index: 100001 !important; + transition: none !important; +} + +.magnify-info-panel.panel-pinned::before { + content: ''; + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + background: linear-gradient(45deg, rgba(255, 215, 0, 0.05) 0%, transparent 100%); + pointer-events: none; + border-radius: inherit; +} + +.panel-dragging * { + pointer-events: none !important; + user-select: none !important; + transition: none !important; +} + +.magnify-info-panel.panel-minimized .panel-content { + max-height: 0 !important; + overflow: hidden; + transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.floating-controls { + position: fixed; + display: none; + /* Hidden initially - JS will show when positioned */ + padding: 4px; + background: rgba(40, 40, 50, 0.95); + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 4px; + opacity: 1; + z-index: 100000; + backdrop-filter: blur(8px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); +} + +.floating-controls.vertical-layout { + flex-direction: column; + gap: 4px; +} + +.floating-controls.horizontal-layout { + flex-direction: row; + gap: 4px; +} + +.control-btn { + background: rgba(60, 60, 70, 0.8); + border: 1px solid rgba(255, 255, 255, 0.2); + width: 28px; + height: 28px; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: #ddd; + border-radius: 3px; + transition: background-color 0.15s ease; +} + +.control-btn:hover { + background: rgba(80, 80, 90, 0.9); + color: white; +} + +.control-btn:active { + background: rgba(100, 100, 110, 0.9); +} + +.control-btn:disabled { + background: rgba(40, 40, 50, 0.5); + color: #666; + cursor: not-allowed; + opacity: 0.5; +} + +.control-btn.active { + background: rgba(255, 200, 50, 0.3); + border-color: rgba(255, 200, 50, 0.6); + color: #ffcc33; +} + +.control-btn.lock-btn.active { + background: rgba(255, 69, 0, 0.3); + border-color: rgba(255, 69, 0, 0.6); + color: #ff4500; +} + +.panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 18px; + background: linear-gradient(135deg, rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.05)); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + user-select: none; + cursor: grab; + transition: background 0.15s ease; +} + +.panel-header:hover { + background: linear-gradient(135deg, rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.08)); +} + +.panel-header:active { + cursor: grabbing; +} + +.header-content { + display: flex; + align-items: center; + gap: 10px; +} + +.header-icon { + font-size: 18px; + opacity: 0.9; +} + +.header-title { + font-weight: 600; + font-size: 18px; +} + +.header-subtitle { + font-size: 14px; + opacity: 0.7; + font-style: italic; +} + +.header-controls { + display: flex; + gap: 6px; +} + +.minimize-btn { + background: rgba(255, 255, 255, 0.1); + border: none; + border-radius: 6px; + color: inherit; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s; + font-weight: bold; + font-size: 16px; +} + +.minimize-btn:hover { + background: rgba(255, 255, 255, 0.2); + transform: scale(1.05); +} + +.minimize-btn:active { + transform: scale(0.95); +} + +.panel-content { + padding: 0; + max-height: calc(100% - 60px); + overflow-y: auto; + scrollbar-width: thin; +} + +.panel-content::-webkit-scrollbar { + width: 8px; +} + +.panel-content::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.1); +} + +.panel-content::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.3); + border-radius: 3px; +} + +.info-section { + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + margin-bottom: 8px; + border-radius: 10px; + overflow: visible; + background: rgba(255, 255, 255, 0.02); + transition: all 0.3s ease; +} + +.info-section:hover { + background: rgba(255, 255, 255, 0.04); + transform: translateY(-1px); +} + +.section-header { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 20px; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + font-weight: 500; + border-radius: 8px; + margin: 3px; + position: relative; + overflow: hidden; +} + +.section-header::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.05) 50%, transparent 100%); + transform: translateX(-100%); + transition: transform 0.6s ease; +} + +.section-header:hover::before { + transform: translateX(100%); +} + +.section-header:hover { + background: rgba(255, 255, 255, 0.08); + transform: translateX(2px); + box-shadow: inset 2px 0 0 rgba(116, 185, 255, 0.5); +} + +.section-header:active { + background: rgba(255, 255, 255, 0.12); + transform: translateX(1px); +} + +.section-header.expanded { + background: rgba(116, 185, 255, 0.1); + border-left: 2px solid var(--info-panel-accent-color, #74b9ff); +} + +.info-section[data-section="node"] { + background: linear-gradient(135deg, rgba(116, 185, 255, 0.05), rgba(160, 212, 104, 0.05)); + border: 1px solid rgba(116, 185, 255, 0.2); + border-radius: 8px; + margin: 6px; +} + +.info-section[data-section="node"] .section-header { + background: linear-gradient(135deg, rgba(116, 185, 255, 0.15), rgba(160, 212, 104, 0.15)); + border-radius: 8px 8px 0 0; + cursor: default; +} + +.info-section[data-section="node"] .section-header:hover { + transform: none; + box-shadow: none; +} + +.info-section[data-section="node"] .section-title { + color: var(--info-panel-accent-color, #74b9ff); + font-weight: 700; +} + +.info-section[data-section="inspector"] { + background: linear-gradient(135deg, rgba(100, 150, 255, 0.05), rgba(74, 144, 226, 0.05)); + border: 1px solid rgba(100, 150, 255, 0.2); + border-radius: 8px; + margin: 6px; +} + +.info-section[data-section="inspector"] .section-header { + background: linear-gradient(135deg, rgba(100, 150, 255, 0.15), rgba(74, 144, 226, 0.15)); + border-radius: 8px 8px 0 0; +} + +.info-section[data-section="inspector"] .section-title { + color: #6496ff; + font-weight: 700; +} + +.info-section[data-section="media"] { + background: linear-gradient(135deg, rgba(253, 203, 110, 0.05), rgba(255, 215, 0, 0.05)); + border: 1px solid rgba(253, 203, 110, 0.2); + border-radius: 8px; + margin: 6px; +} + +.info-section[data-section="media"] .section-header { + background: linear-gradient(135deg, rgba(253, 203, 110, 0.15), rgba(255, 215, 0, 0.15)); + border-radius: 8px 8px 0 0; +} + +.info-section[data-section="media"] .section-title { + color: var(--info-panel-accent-color, #fdcb6e); + font-weight: 700; +} + +.section-icon { + font-size: 18px; + width: 22px; + text-align: center; +} + +.section-title { + flex: 1; + font-size: 16px; + font-weight: 600; + letter-spacing: 0.3px; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.section-badge { + background: rgba(100, 150, 255, 0.2); + color: #8bb3ff; + padding: 3px 10px; + border-radius: 10px; + font-size: 13px; + font-weight: 500; +} + +.expand-icon { + font-size: 14px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + color: rgba(255, 255, 255, 0.6); + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + background: rgba(255, 255, 255, 0.05); + margin-left: auto; +} + +.section-header:hover .expand-icon { + color: var(--info-panel-accent-color, #74b9ff); + background: rgba(116, 185, 255, 0.15); + transform: scale(1.1); +} + +.section-header.expanded .expand-icon { + transform: rotate(90deg) scale(1.1); + color: var(--info-panel-accent-color, #74b9ff); + background: rgba(116, 185, 255, 0.2); +} + +.section-content { + max-height: 0; + overflow: visible; + transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease; + opacity: 0; +} + +.section-content.expanded { + max-height: 2000px; + opacity: 1; + overflow: visible; +} + +.section-body { + padding: 0 18px 16px 24px; +} + +.info-row { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 10px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + transition: all 0.2s ease; + border-radius: 4px; + margin: 0 -8px; + padding-left: 8px; + padding-right: 8px; +} + +.info-row:last-child { + border-bottom: none; +} + +.info-row:hover { + background: rgba(255, 255, 255, 0.03); + border-bottom-color: rgba(255, 255, 255, 0.08); +} + +.info-label { + font-weight: 500; + opacity: 0.85; + font-size: 15px; + min-width: 80px; + padding-right: 12px; + line-height: 1.5; +} + +.info-value { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 14px; + color: var(--info-panel-accent-color, #a0d468); + background: rgba(160, 212, 104, 0.12); + padding: 4px 10px; + border-radius: 6px; + max-width: 100%; + word-wrap: break-word; + word-break: break-word; + hyphens: auto; + line-height: 1.4; + text-align: left; + white-space: pre-wrap; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + transition: all 0.2s ease; +} + +.info-value:hover { + background: rgba(160, 212, 104, 0.18); + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +.info-value.long-text { + white-space: pre-wrap; + word-wrap: break-word; + max-width: 100%; + line-height: 1.4; +} + +.empty-state { + text-align: center; + padding: 50px 25px; + opacity: 0.6; +} + +.empty-state-icon { + font-size: 36px; + margin-bottom: 15px; + opacity: 0.5; +} + +.empty-state-text { + font-size: 16px; + font-style: italic; +} + +/* Copy to clipboard functionality */ +.info-value[title]:hover::after { + content: '📋'; + position: absolute; + right: -20px; + top: 50%; + transform: translateY(-50%); + font-size: 12px; + opacity: 0.7; +} + +/* Smooth loading states */ +.section-body.loading { + opacity: 0.5; + pointer-events: none; +} + +.section-body.loading::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 20px; + height: 20px; + border: 2px solid transparent; + border-top: 2px solid var(--info-panel-accent-color, #74b9ff); + border-radius: 50%; + animation: spin 1s linear infinite; + transform: translate(-50%, -50%); +} + +@keyframes spin { + to { + transform: translate(-50%, -50%) rotate(360deg); + } +} + +/* Enhanced scrollbar for better UX */ +.section-body::-webkit-scrollbar { + width: 4px; +} + +.section-body::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + border-radius: 2px; +} + +.section-body::-webkit-scrollbar-thumb { + background: var(--info-panel-accent-color, #74b9ff); + border-radius: 2px; + opacity: 0.6; +} + +.section-body::-webkit-scrollbar-thumb:hover { + opacity: 1; +} + +@keyframes pulse { + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.7; + } +} + +.pulse { + animation: pulse 2s infinite; +} \ No newline at end of file diff --git a/css/sidebar.css b/css/sidebar.css new file mode 100644 index 0000000..6f458a3 --- /dev/null +++ b/css/sidebar.css @@ -0,0 +1,352 @@ +/** + * ComfyUI MagnifyGlass - Sidebar CSS + * + * Styles for the sidebar settings and inspector panels. + */ + +.magnify-sidebar { + display: flex; + flex-direction: column; + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 13px; + color: var(--fg-color, #ddd); + background: transparent; + padding: 0; +} + +.magnify-sidebar-header { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; + border-bottom: 1px solid var(--border-color, #333); + background: rgba(0, 0, 0, 0.2); +} + +.magnify-sidebar-header svg { + width: 20px; + height: 20px; + fill: currentColor; +} + +.magnify-sidebar-header h2 { + margin: 0; + font-size: 14px; + font-weight: 600; +} + +.magnify-sidebar-content { + flex: 1; + overflow-y: auto; + padding: 8px 0; +} + +/* Section styling */ +.magnify-sidebar-section { + margin-bottom: 4px; +} + +.magnify-sidebar-section-header { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + cursor: pointer; + background: rgba(255, 255, 255, 0.03); + border-bottom: 1px solid var(--border-color, #333); + transition: background 0.15s; +} + +.magnify-sidebar-section-header:hover { + background: rgba(255, 255, 255, 0.06); +} + +.magnify-sidebar-section-header svg { + width: 12px; + height: 12px; + transition: transform 0.2s; +} + +.magnify-sidebar-section-header.collapsed svg { + transform: rotate(-90deg); +} + +.magnify-sidebar-section-header span { + font-weight: 500; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.magnify-sidebar-section-body { + padding: 12px 16px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.magnify-sidebar-section-body.collapsed { + display: none; +} + +/* Control row */ +.magnify-control-row { + display: flex; + flex-direction: column; + gap: 4px; +} + +.magnify-control-row label { + font-size: 11px; + color: var(--descrip-text, #999); + font-weight: 500; +} + +.magnify-control-row .control-value { + font-size: 11px; + color: var(--fg-color, #ddd); + margin-left: auto; +} + +.magnify-control-label-row { + display: flex; + justify-content: space-between; + align-items: center; +} + +/* Slider styling */ +.magnify-slider { + width: 100%; + height: 6px; + border-radius: 3px; + background: rgba(255, 255, 255, 0.1); + appearance: none; + cursor: pointer; +} + +.magnify-slider::-webkit-slider-thumb { + appearance: none; + width: 14px; + height: 14px; + border-radius: 50%; + background: var(--p-primary-color, #3b82f6); + cursor: pointer; + transition: transform 0.1s; +} + +.magnify-slider::-webkit-slider-thumb:hover { + transform: scale(1.15); +} + +.magnify-slider::-moz-range-thumb { + width: 14px; + height: 14px; + border-radius: 50%; + background: var(--p-primary-color, #3b82f6); + border: none; + cursor: pointer; +} + +/* Toggle styling */ +.magnify-toggle-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 0; +} + +.magnify-toggle-row label { + font-size: 12px; + color: var(--fg-color, #ddd); +} + +.magnify-toggle { + position: relative; + width: 36px; + height: 20px; + background: rgba(255, 255, 255, 0.1); + border-radius: 10px; + cursor: pointer; + transition: background 0.2s; +} + +.magnify-toggle.active { + background: var(--p-primary-color, #3b82f6); +} + +.magnify-toggle::after { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 16px; + height: 16px; + background: white; + border-radius: 50%; + transition: transform 0.2s; +} + +.magnify-toggle.active::after { + transform: translateX(16px); +} + +/* Select styling */ +.magnify-select { + width: 100%; + padding: 6px 10px; + border: 1px solid var(--border-color, #444); + border-radius: 4px; + background: rgba(0, 0, 0, 0.3); + color: var(--fg-color, #ddd); + font-size: 12px; + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23999' d='M6 8L2 4h8z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 8px center; +} + +.magnify-select:focus { + outline: none; + border-color: var(--p-primary-color, #3b82f6); +} + +/* Inspector styles */ +.magnify-inspector-section { + padding: 12px 16px; +} + +.magnify-inspector-group { + margin-bottom: 16px; +} + +.magnify-inspector-group:last-child { + margin-bottom: 0; +} + +.magnify-inspector-group-title { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--descrip-text, #888); + margin-bottom: 8px; + font-weight: 600; +} + +.magnify-inspector-row { + display: flex; + justify-content: space-between; + padding: 4px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.magnify-inspector-row:last-child { + border-bottom: none; +} + +.magnify-inspector-label { + color: var(--descrip-text, #999); + font-size: 12px; +} + +.magnify-inspector-value { + color: var(--fg-color, #ddd); + font-size: 12px; + font-family: monospace; + text-align: right; + max-width: 60%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.magnify-inspector-empty { + color: var(--descrip-text, #666); + font-style: italic; + font-size: 12px; + text-align: center; + padding: 20px 0; +} + +/* Node badge */ +.magnify-node-badge { + display: inline-block; + padding: 2px 8px; + background: var(--p-primary-color, #3b82f6); + border-radius: 4px; + font-size: 11px; + font-weight: 500; + margin-bottom: 8px; +} + +/* Button styling */ +.magnify-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 8px 12px; + border: none; + border-radius: 4px; + background: var(--p-primary-color, #3b82f6); + color: white; + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: background 0.15s, transform 0.1s; +} + +.magnify-btn:hover { + background: var(--p-primary-hover-color, #2563eb); +} + +.magnify-btn:active { + transform: scale(0.98); +} + +.magnify-btn-secondary { + background: rgba(255, 255, 255, 0.1); +} + +.magnify-btn-secondary:hover { + background: rgba(255, 255, 255, 0.15); +} + +/* Color picker styling */ +.magnify-color-row { + flex-direction: row; + align-items: center; + justify-content: space-between; +} + +.magnify-color-wrapper { + display: flex; + align-items: center; + gap: 8px; +} + +.magnify-color-input { + width: 28px; + height: 28px; + padding: 0; + border: 1px solid var(--border-color, #444); + border-radius: 4px; + cursor: pointer; + background: transparent; +} + +.magnify-color-input::-webkit-color-swatch-wrapper { + padding: 2px; +} + +.magnify-color-input::-webkit-color-swatch { + border-radius: 2px; + border: none; +} + +.magnify-color-preview { + font-size: 11px; + font-family: monospace; + color: var(--descrip-text, #999); +} \ No newline at end of file diff --git a/git_clones.json b/git_clones.json new file mode 100644 index 0000000..89adfb4 --- /dev/null +++ b/git_clones.json @@ -0,0 +1,4 @@ +{ + "count": 21, + "uniques": 20 +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..a0f30de --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "comfyui-magnifyglass", + "version": "1.5.0", + "type": "module", + "description": "A professional magnifying glass and info panel for ComfyUI", + "scripts": { + "dev": "vite build --watch", + "build": "vite build", + "typecheck": "tsc --noEmit", + "test:e2e": "playwright test", + "test:unit": "vitest" + }, + "devDependencies": { + "@playwright/test": "^1.57.0", + "happy-dom": "^20.0.11", + "typescript": "^5.3.0", + "vite": "^5.0.0", + "vitest": "^4.0.16" + } +} \ No newline at end of file diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..487223a --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,49 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests/e2e', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:8188', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..33cc5d2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "comfyui-magnifyglass" +description = "ComfyUI-MagnifyGlass: A powerful & customizable magnifying glass for ComfyUI. Zoom into canvas details with smooth controls, configurable activation, custom styles (shape, size, border) & WebGL performance." +version = "1.4.5" +license = {file = "LICENSE"} + +[project.urls] +Repository = "https://github.com/AEmotionStudio/ComfyUI-MagnifyGlass" +# Used by Comfy Registry https://comfyregistry.org + +[tool.comfy] +PublisherId = "aemotionstudio" +DisplayName = "ComfyUI-MagnifyGlass" +Icon = "" diff --git a/src/info-panel/EventManager.ts b/src/info-panel/EventManager.ts new file mode 100644 index 0000000..df6ffdd --- /dev/null +++ b/src/info-panel/EventManager.ts @@ -0,0 +1,227 @@ +/** + * ComfyUI MagnifyGlass - Info Panel Event Manager (TypeScript) + * + * Complete Event Manager extracted from magnify_info_panel.js + * Handles all event binding and delegation for the info panel. + */ + +import { StateManager } from './StateManager'; +import { UIManager } from './UIManager'; +import { PositionManager } from './PositionManager'; +import { isUserTyping } from '../shared/utils'; +import { Logger } from '../shared/logger'; + +export interface EventCallbacks { + toggleVisibility: () => void; + togglePin: () => void; + updatePosition: () => void; +} + +/** + * Event Manager class. + * Handles all event listeners for the info panel. + */ +export class EventManager { + stateManager: StateManager; + panelElement: HTMLElement | null; + positionManager: PositionManager; + callbacks: EventCallbacks | null; + + dragCleanup: (() => void) | null; + + boundHandleMouseMove: (e: MouseEvent) => void; + boundHandleKeyDown: (e: KeyboardEvent) => void; + boundHandleKeyUp: (e: KeyboardEvent) => void; + + constructor(stateManager: StateManager, panelElement: HTMLElement | null, positionManager: PositionManager, callbacks?: EventCallbacks) { + this.stateManager = stateManager; + this.panelElement = panelElement; + this.positionManager = positionManager; + this.callbacks = callbacks || null; + this.dragCleanup = null; + + this.boundHandleMouseMove = this.handleMouseMove.bind(this); + this.boundHandleKeyDown = this.handleKeyDown.bind(this); + this.boundHandleKeyUp = this.handleKeyUp.bind(this); + + this.setupEventListeners(); + } + + setupEventListeners(): void { + this.setupPanelEvents(); + this.setupHotkeyEvents(); + this.setupDragEvents(); + this.setupHoverEvents(); + } + + setupPanelEvents(): void { + if (!this.panelElement) return; + + // Vue handles most internal events now. + // We can keep specific global-ish listeners if needed here. + } + + setupHotkeyEvents(): void { + document.addEventListener('keydown', this.boundHandleKeyDown); + } + + setupDragEvents(): void { + if (!this.panelElement) return; + + let isDragging = false; + let rafId: number | null = null; + let targetX = 0; + let targetY = 0; + + const startDrag = (e: MouseEvent) => { + if (!this.panelElement) return; + const target = e.target as HTMLElement; + + // Only allow dragging from header (expand clickable area) + const header = target.closest('.panel-header'); + if (!header) return; + if (target.closest('button')) return; + + // Button 2 (pin icon, data-action="lock") prevents dragging + if (this.stateManager.state.isPanelLocked) return; + + // Also prevent dragging if we are "Following Glass" (not pinned to screen) + // This prevents fighting with the auto-positioner and accidental drags + if (!this.stateManager.state.isPanelPinned) return; + + e.preventDefault(); + e.stopPropagation(); + + isDragging = true; + + const startX = e.clientX; + const startY = e.clientY; + const rect = this.panelElement.getBoundingClientRect(); + const startLeft = rect.left; + const startTop = rect.top; + + // Visual feedback - dragging state + this.panelElement.style.cursor = 'grabbing'; + this.panelElement.style.opacity = '0.9'; + this.panelElement.style.transition = 'none'; // Disable transitions during drag + document.body.style.cursor = 'grabbing'; + document.body.style.userSelect = 'none'; + + const onMouseMove = (moveEvent: MouseEvent) => { + if (!isDragging) return; + + const dx = moveEvent.clientX - startX; + const dy = moveEvent.clientY - startY; + + targetX = startLeft + dx; + targetY = startTop + dy; + + // Keep within viewport bounds + const panelWidth = this.panelElement?.offsetWidth || 0; + const panelHeight = this.panelElement?.offsetHeight || 0; + targetX = Math.max(0, Math.min(targetX, window.innerWidth - panelWidth)); + targetY = Math.max(0, Math.min(targetY, window.innerHeight - panelHeight)); + + // Use RAF for smooth updates + if (rafId === null) { + rafId = requestAnimationFrame(() => { + if (this.panelElement && isDragging) { + this.panelElement.style.left = `${targetX}px`; + this.panelElement.style.top = `${targetY}px`; + } + rafId = null; + }); + } + }; + + const onMouseUp = () => { + isDragging = false; + + // Cancel any pending RAF + if (rafId !== null) { + cancelAnimationFrame(rafId); + rafId = null; + } + + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + + // Restore visual state + if (this.panelElement) { + this.panelElement.style.cursor = ''; + this.panelElement.style.opacity = ''; + this.panelElement.style.transition = ''; + + // Final position update + this.panelElement.style.left = `${targetX}px`; + this.panelElement.style.top = `${targetY}px`; + + // Save pinned position + this.stateManager.state.pinnedPosition = { x: targetX, y: targetY }; + } + + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + }; + + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + }; + + this.panelElement.addEventListener('mousedown', startDrag); + + this.dragCleanup = () => { + if (this.panelElement) this.panelElement.removeEventListener('mousedown', startDrag); + }; + } + + setupHoverEvents(): void { + if (!this.panelElement) return; + + this.panelElement.addEventListener('mouseenter', () => { + this.stateManager.state.isPanelHovered = true; + this.stateManager.clearAutoExpandTimer(); + }); + + this.panelElement.addEventListener('mouseleave', () => { + this.stateManager.state.isPanelHovered = false; + if (!this.stateManager.state.isHoveringNode) { + this.stateManager.scheduleAutoCollapse(); + } + }); + } + + handleKeyDown(e: KeyboardEvent): void { + if (isUserTyping()) return; + + const settings = this.stateManager.state.settings; + const key = e.key.toLowerCase(); + // console.log(`[InfoPanel] KeyDown: ${key}`); + + // Toggle visibility + if (key === (settings["🔍MagnifyGlass.ToggleHotkey"] as string).toLowerCase()) { + Logger.debug('Toggle visibility hotkey matched!'); + e.preventDefault(); + this.callbacks?.toggleVisibility(); + } + + // Toggle Pin + if (key === (settings["🔍MagnifyGlass.PinPanelHotkey"] as string).toLowerCase()) { + e.preventDefault(); + this.callbacks?.togglePin(); + } + } + + handleMouseMove(e: MouseEvent): void { + // Placeholder + } + + handleKeyUp(e: KeyboardEvent): void { + // Placeholder + } + + cleanup(): void { + document.removeEventListener('keydown', this.boundHandleKeyDown); + if (this.dragCleanup) this.dragCleanup(); + } +} diff --git a/src/info-panel/InfoPanel.ts b/src/info-panel/InfoPanel.ts new file mode 100644 index 0000000..0403023 --- /dev/null +++ b/src/info-panel/InfoPanel.ts @@ -0,0 +1,185 @@ +/** + * ComfyUI MagnifyGlass - Info Panel Orchestrator (TypeScript) + * + * Complete Info Panel Manager extracted from magnify_info_panel.js + * Main class that orchestrates all info panel components. + */ + +import type { MagnifyGlassInstance } from '../types/comfyui'; +import { registerPanelSettings } from '../shared/settings'; +import { Logger } from '../shared/logger'; +import { StateManager } from './StateManager'; +import { UIManager } from './UIManager'; +import { PositionManager } from './PositionManager'; +import { EventManager } from './EventManager'; +import { InformationGatherer } from './InformationGatherer'; + +/** + * Professional Info Panel Manager. + * Orchestrates all the other components for the info panel. + */ +export class InfoPanel { + magnifyGlass: MagnifyGlassInstance; + stateManager: StateManager; + uiManager: UIManager; + positionManager: PositionManager; + eventManager: EventManager; + informationGatherer: InformationGatherer; + + constructor(magnifyGlass: MagnifyGlassInstance) { + this.magnifyGlass = magnifyGlass; + + // Initialize Managers + this.stateManager = new StateManager(); + this.uiManager = new UIManager(this.stateManager); + this.informationGatherer = new InformationGatherer(); + + // Position Manager needs the UI elements + this.positionManager = new PositionManager(this.stateManager, this.uiManager.elements.panel!); + + // Event Manager needs state and UI manager + this.eventManager = new EventManager(this.stateManager, this.uiManager.elements.panel!, this.positionManager, { + toggleVisibility: () => { + if (this.stateManager.state.isPanelVisible) { + this.uiManager.hide(); + } else { + this.uiManager.show(); + } + }, + togglePin: () => { + this.stateManager.togglePinning(); + this.uiManager.updatePinnedState(); + }, + updatePosition: () => this.positionManager.positionPanel() + }); + + this.hookIntoMagnifyGlass(); + + // Register settings with ComfyUI + registerPanelSettings(this.stateManager, this.uiManager, this.positionManager); + + Logger.info('Info Panel initialized successfully'); + } + + hookIntoMagnifyGlass(): void { + // Hook into magnify glass update cycle + const originalUpdateMagnifiedView = this.magnifyGlass.updateMagnifiedView.bind(this.magnifyGlass); + + this.magnifyGlass.updateMagnifiedView = (() => { + originalUpdateMagnifiedView(); + + if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] && this.magnifyGlass.state.active) { + this.scheduleInfoUpdate(); + } + }).bind(this); + + // Hook into show/hide + const originalShow = this.magnifyGlass.ui.show.bind(this.magnifyGlass.ui); + const originalHide = this.magnifyGlass.ui.hide.bind(this.magnifyGlass.ui); + + this.magnifyGlass.ui.show = (() => { + originalShow(); + if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"]) { + // Restore state: If it was visible before hide, make it visible now. + // Otherwise only show controls. + if (this.stateManager.state.wasPanelVisibleBeforeHide) { + this.uiManager.show(); + + // Position properly + setTimeout(() => { + this.positionManager.positionPanel(); + this.positionManager.positionFloatingControls(this.uiManager.elements.controls); + }, 10); + } else { + // Do NOT force panel visible. Only show controls. + const showControls = this.stateManager.state.settings["🔍MagnifyGlass.ShowHoveringControls"] !== false; + if (showControls && this.uiManager.elements.controls) { + this.uiManager.elements.controls.style.display = 'flex'; + } + + // Ensure states are updated + this.uiManager.updateControlStates(); + + // Position controls after showing (relative to glass since panel is hidden) + setTimeout(() => { + this.positionManager.positionFloatingControls(this.uiManager.elements.controls); + }, 10); + } + } + }).bind(this); + + this.magnifyGlass.ui.hide = (() => { + // Save state before hiding + this.stateManager.state.wasPanelVisibleBeforeHide = this.stateManager.state.isPanelVisible; + + originalHide(); + this.uiManager.hide(); + // Explicitly hide floating controls when glass is disabled + if (this.uiManager.elements.controls) { + this.uiManager.elements.controls.style.display = 'none'; + } + }).bind(this); + } + + scheduleInfoUpdate(): void { + if (this.stateManager.state.updateScheduled) return; + + this.stateManager.state.updateScheduled = true; + requestAnimationFrame(() => { + this.updateInfo(); + this.stateManager.state.updateScheduled = false; + }); + } + + updateInfo(): void { + const settings = this.stateManager.state.settings; + const isActive = this.magnifyGlass.state.active; + + // Safety: If disabled or inactive, ensure hidden and return + if (!settings["🔍MagnifyGlass.InfoPanelEnabled"] || !isActive) { + if (this.uiManager.elements.controls && this.uiManager.elements.controls.style.display !== 'none') { + this.uiManager.elements.controls.style.display = 'none'; + } + return; + } + + const info = this.informationGatherer.gatherInformation(); + this.stateManager.setCurrentInfo(info); + + // Update the UI + this.uiManager.displayInfo(info); + + this.positionManager.positionPanel(); + this.positionManager.positionFloatingControls(this.uiManager.elements.controls); + + // Handle node hover state logic if needed for internal state + if (info.hoveredNode) { + this.stateManager.state.isHoveringNode = true; + this.stateManager.expandNodeSections(); + if (this.stateManager.state.lastNodeId !== info.hoveredNode.id) { + this.stateManager.state.lastNodeId = info.hoveredNode.id; + } + } else { + this.stateManager.state.isHoveringNode = false; + if (this.stateManager.state.lastNodeId !== null) { + this.stateManager.state.lastNodeId = null; + if (!this.stateManager.state.isPanelHovered) { + this.stateManager.scheduleAutoCollapse(); + } + } + } + } + + updateSettings(): void { + const changes = this.stateManager.updateSettings(); + this.uiManager.applyStyles(); + + if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] && this.magnifyGlass.state.active) { + this.uiManager.show(); + // Ensure position is updated after potential dimension changes + this.positionManager.positionPanel(); + } else { + this.uiManager.hide(); + } + } +} diff --git a/src/info-panel/InformationGatherer.ts b/src/info-panel/InformationGatherer.ts new file mode 100644 index 0000000..4f32be1 --- /dev/null +++ b/src/info-panel/InformationGatherer.ts @@ -0,0 +1,326 @@ +/** + * ComfyUI MagnifyGlass - Info Panel Information Gatherer (TypeScript) + * + * Complete Information Gatherer extracted from magnify_info_panel.js + * Responsible for collecting information about nodes, widgets, and canvas state. + */ + +import type { ComfyApp, ComfyNode, ComfyWidget, GatheredInfo, NodeInfo, WidgetInfo, MediaInfo } from '../types/comfyui'; + +declare const app: ComfyApp; + +/** + * Information Gatherer class. + * Collects information about the current state under the cursor. + */ +export class InformationGatherer { + constructor() { } + + gatherInformation(): GatheredInfo { + const magnifyGlass = window.comfyUIMagnifyGlass; + if (!magnifyGlass) { + return { + timestamp: Date.now(), + cursor: { screenX: 0, screenY: 0, canvasX: 0, canvasY: 0 }, + zoom: 1, + nodeCount: 0, + hoveredNode: null, + hoveredWidget: null, + mediaElement: null + }; + } + + const info: GatheredInfo = { + timestamp: Date.now(), + cursor: { + screenX: magnifyGlass.lastKnownMousePosition.x, + screenY: magnifyGlass.lastKnownMousePosition.y, + canvasX: magnifyGlass.state.x, + canvasY: magnifyGlass.state.y + }, + zoom: magnifyGlass.state.canvasScale, + nodeCount: app.graph?._nodes?.length ?? 0, + hoveredNode: null, + hoveredWidget: null, + mediaElement: null + }; + + if (magnifyGlass.isOverMedia && magnifyGlass.currentMediaElement) { + info.mediaElement = this.getMediaInfo(magnifyGlass.currentMediaElement); + } + + const nodeUnderCursor = this.getNodeUnderCursor(); + if (nodeUnderCursor) { + info.hoveredNode = this.getDetailedNodeInfo(nodeUnderCursor.node, nodeUnderCursor.localPos); + + const widget = this.getWidgetUnderCursor(nodeUnderCursor.node, nodeUnderCursor.localPos); + if (widget) { + info.hoveredWidget = this.getWidgetInfo(widget); + } + } + + return info; + } + + getNodeUnderCursor(): { node: ComfyNode, localPos: { x: number, y: number } } | null { + const magnifyGlass = window.comfyUIMagnifyGlass; + if (!app.graph || !app.canvas || !magnifyGlass) { + return null; + } + + try { + // Try to get the node from ComfyUI's state + if (app.canvas.node_over) { + const node = app.canvas.node_over; + if (node && node.pos && node.size) { + const canvasRect = app.canvas.canvas.getBoundingClientRect(); + const mouseX = magnifyGlass.lastKnownMousePosition.x - canvasRect.left; + const mouseY = magnifyGlass.lastKnownMousePosition.y - canvasRect.top; + + let graphPos: [number, number]; + if (app.canvas.convertOffsetToCanvasPos) { + graphPos = app.canvas.convertOffsetToCanvasPos([mouseX, mouseY]); + } else { + const ds = app.canvas.ds || { scale: 1, offset: [0, 0] }; + graphPos = [ + (mouseX / ds.scale) - (ds.offset[0] / ds.scale), + (mouseY / ds.scale) - (ds.offset[1] / ds.scale) + ]; + } + + return { + node: node, + localPos: { + x: graphPos[0] - node.pos[0], + y: graphPos[1] - node.pos[1] + } + }; + } + } + + // Fallback to manual detection + const canvasRect = app.canvas.canvas.getBoundingClientRect(); + const mouseX = magnifyGlass.lastKnownMousePosition.x - canvasRect.left; + const mouseY = magnifyGlass.lastKnownMousePosition.y - canvasRect.top; + + let graphPos: [number, number]; + if (app.canvas.convertOffsetToCanvasPos) { + graphPos = app.canvas.convertOffsetToCanvasPos([mouseX, mouseY]); + } else { + const ds = app.canvas.ds || { scale: 1, offset: [0, 0] }; + graphPos = [ + (mouseX / ds.scale) - (ds.offset[0] / ds.scale), + (mouseY / ds.scale) - (ds.offset[1] / ds.scale) + ]; + } + + for (let i = app.graph._nodes.length - 1; i >= 0; i--) { + const node = app.graph._nodes[i]; + if (!node.pos || !node.size || (node.flags && node.flags.collapsed)) continue; + + if (graphPos[0] >= node.pos[0] && + graphPos[0] <= node.pos[0] + node.size[0] && + graphPos[1] >= node.pos[1] && + graphPos[1] <= node.pos[1] + node.size[1]) { + + return { + node: node, + localPos: { + x: graphPos[0] - node.pos[0], + y: graphPos[1] - node.pos[1] + } + }; + } + } + } catch (err) { + console.warn("Error in node detection:", err); + } + + return null; + } + + getDetailedNodeInfo(node: ComfyNode, localPos: { x: number, y: number }): NodeInfo { + return { + id: node.id, + title: node.title || "Untitled Node", + type: node.type, + mode: this.getNodeModeText(node.mode), + position: { + x: Math.round(node.pos[0]), + y: Math.round(node.pos[1]), + formatted: `(${Math.round(node.pos[0])}, ${Math.round(node.pos[1])})` + }, + size: { + width: Math.round(node.size[0]), + height: Math.round(node.size[1]), + formatted: `${Math.round(node.size[0])}×${Math.round(node.size[1])}` + }, + localPosition: { + x: Math.round(localPos.x), + y: Math.round(localPos.y), + formatted: `(${Math.round(localPos.x)}, ${Math.round(localPos.y)})`, + percentage: { + x: ((localPos.x / node.size[0]) * 100).toFixed(1), + y: ((localPos.y / node.size[1]) * 100).toFixed(1) + } + }, + counts: { + widgets: node.widgets ? node.widgets.length : 0, + inputs: node.inputs ? node.inputs.length : 0, + outputs: node.outputs ? node.outputs.length : 0, + properties: node.properties ? Object.keys(node.properties).length : 0 + }, + widgets: node.widgets || [], + inputs: node.inputs || [], + outputs: node.outputs || [], + properties: node.properties || {}, + hoverRegion: this.detectNodeRegion(localPos, node) + }; + } + + getNodeInfo(node: ComfyNode): NodeInfo { + return { + id: node.id, + title: node.title || "Untitled", + type: node.type, + mode: node.mode, + size: node.size ? `${Math.round(node.size[0])}×${Math.round(node.size[1])}` : "Unknown", + position: node.pos ? `(${Math.round(node.pos[0])}, ${Math.round(node.pos[1])})` : "Unknown", + widgets: node.widgets || [], + inputs: node.inputs || [], + outputs: node.outputs || [], + properties: node.properties || {} + }; + } + + getWidgetUnderCursor(node: ComfyNode, localPos: { x: number, y: number }): ComfyWidget | null { + if (!node.widgets || !node.widgets.length) return null; + + const titleHeight = 30; + let currentY = titleHeight; + + for (const widget of node.widgets) { + const widgetHeight = 25; + + if (localPos.y >= currentY && localPos.y <= currentY + widgetHeight) { + return widget; + } + + currentY += widgetHeight + 5; + } + + return null; + } + + getWidgetInfo(widget: ComfyWidget): WidgetInfo { + return { + name: widget.name, + type: widget.type, + value: this.formatValue(widget.value), + options: widget.options || null, + min: widget.min, + max: widget.max, + step: widget.step + }; + } + + getMediaInfo(mediaElement: HTMLImageElement | HTMLVideoElement): MediaInfo { + const info: MediaInfo = { + type: mediaElement instanceof HTMLImageElement ? 'image' : 'video', + tagName: mediaElement.tagName, + src: mediaElement.src ? mediaElement.src.substring(mediaElement.src.lastIndexOf('/') + 1) : 'No source', + naturalWidth: 0, + naturalHeight: 0, + displayWidth: 0, + displayHeight: 0, + aspectRatio: '' + }; + + if (mediaElement instanceof HTMLImageElement) { + info.naturalWidth = mediaElement.naturalWidth; + info.naturalHeight = mediaElement.naturalHeight; + info.displayWidth = Math.round(mediaElement.width); + info.displayHeight = Math.round(mediaElement.height); + if (mediaElement.naturalWidth && mediaElement.naturalHeight) { + info.aspectRatio = (mediaElement.naturalWidth / mediaElement.naturalHeight).toFixed(2); + } + } else if (mediaElement instanceof HTMLVideoElement) { + info.naturalWidth = mediaElement.videoWidth; + info.naturalHeight = mediaElement.videoHeight; + info.displayWidth = Math.round(mediaElement.width); + info.displayHeight = Math.round(mediaElement.height); + info.duration = mediaElement.duration; + info.currentTime = mediaElement.currentTime; + if (mediaElement.videoWidth && mediaElement.videoHeight) { + info.aspectRatio = (mediaElement.videoWidth / mediaElement.videoHeight).toFixed(2); + } + } + + return info; + } + + getNodeModeText(mode: number): string { + const modes: { [key: number]: string } = { + 0: 'Always', + 1: 'On Event', + 2: 'Never', + 3: 'On Trigger', + 4: 'On Request' + }; + return modes[mode] || `Mode ${mode}`; + } + + detectNodeRegion(localPos: { x: number, y: number }, node: ComfyNode): string { + const titleHeight = 30; + const regions: string[] = []; + + if (localPos.y <= titleHeight) { + regions.push("Title Bar"); + } + + if (localPos.x <= 10) { + regions.push("Left Edge"); + } else if (localPos.x >= node.size[0] - 10) { + regions.push("Right Edge"); + } + + if (localPos.y <= 10) { + regions.push("Top Edge"); + } else if (localPos.y >= node.size[1] - 10) { + regions.push("Bottom Edge"); + } + + if (regions.length === 0) { + if (localPos.y > titleHeight) { + regions.push("Content Area"); + } + } + + if (localPos.x <= 20 && localPos.y > titleHeight) { + regions.push("Input Area"); + } else if (localPos.x >= node.size[0] - 20 && localPos.y > titleHeight) { + regions.push("Output Area"); + } + + return regions.length > 0 ? regions.join(", ") : "Unknown"; + } + + formatValue(value: unknown): string { + if (value === null) return "null"; + if (value === undefined) return "undefined"; + if (typeof value === "string") { + return value; // Show full text without truncation + } + if (typeof value === "number") { + return Number.isInteger(value) ? value.toString() : value.toFixed(3); + } + if (typeof value === "boolean") return value.toString(); + if (Array.isArray(value)) { + return `Array(${value.length})`; + } + if (typeof value === "object") { + return "Object"; + } + return String(value); + } +} diff --git a/src/info-panel/NodeDataExtractor.ts b/src/info-panel/NodeDataExtractor.ts new file mode 100644 index 0000000..8aa2126 --- /dev/null +++ b/src/info-panel/NodeDataExtractor.ts @@ -0,0 +1,259 @@ +/** + * ComfyUI MagnifyGlass - Node Data Extractor + * + * Utility functions for extracting specific information from nodes. + * Extracted from UIManager.ts for better modularity. + */ + +import { formatWidgetValue } from './ValueFormatter'; +import type { NodeInfo } from '../types/comfyui'; + +/** + * Interface for extracted parameter items. + */ +export interface ParameterItem { + label: string; + value: string; +} + +/** + * Get checkpoint/model info from a node. + * @param nodeInfo - Node information object + * @returns Model filename or null + */ +export function getCheckpointInfo(nodeInfo: NodeInfo): string | null { + if (nodeInfo.type && ( + nodeInfo.type.includes('CheckpointLoader') || + nodeInfo.type.includes('LoadCheckpoint') || + nodeInfo.type.includes('ModelLoader') || + nodeInfo.type.includes('UNETLoader') || + nodeInfo.type.includes('VAELoader') || + nodeInfo.type.includes('LoraLoader') + )) { + if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { + for (const widget of nodeInfo.widgets) { + if (widget.name && ( + widget.name.toLowerCase().includes('model') || + widget.name.toLowerCase().includes('checkpoint') || + widget.name.toLowerCase().includes('ckpt') || + widget.name.toLowerCase().includes('lora') || + widget.name.toLowerCase().includes('vae') || + widget.name.toLowerCase().includes('file') + )) { + const value = String(widget.value); + const filename = value.split(/[\/\\]/).pop(); + return filename || value; + } + } + } + } + return null; +} + +/** + * Image info result type. + */ +export interface ImageInfoResult { + width: string | number; + height: string | number; + src: string; +} + +/** + * Get image info from a node. + * @param nodeInfo - Node information object + * @returns Image info string, object, or null + */ +export function getImageInfo(nodeInfo: NodeInfo): string | ImageInfoResult | null { + if (nodeInfo.type && ( + nodeInfo.type.includes('SaveImage') || + nodeInfo.type.includes('PreviewImage') || + nodeInfo.type.includes('VisionOutput') || + nodeInfo.type.includes('ImageOutput') || + nodeInfo.type.includes('LoadImage') || + nodeInfo.type.includes('Display') + )) { + if (nodeInfo.widgets) { + for (const widget of nodeInfo.widgets) { + if (widget.name && ( + widget.name.toLowerCase().includes('image') || + widget.name.toLowerCase().includes('filename') || + widget.name.toLowerCase().includes('file') + )) { + return String(widget.value); + } + } + } + + if (nodeInfo.properties && (nodeInfo.properties as Record).img) { + const img = (nodeInfo.properties as Record).img as Record; + return { + width: (img.width as string | number) || 'unknown', + height: (img.height as string | number) || 'unknown', + src: img.src ? String(img.src).split(/[\/\\]/).pop() || 'Preview available' : 'Preview available' + }; + } + + if (nodeInfo.outputs) { + for (const output of nodeInfo.outputs) { + if (output.links && output.links.length > 0) { + return 'Image connected to ' + output.links.length + ' node(s)'; + } + } + } + + return 'Image node'; + } + return null; +} + +/** + * Get text box content from a node. + * @param nodeInfo - Node information object + * @returns Text content or null + */ +export function getTextBoxContent(nodeInfo: NodeInfo): string | null { + if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { + if (nodeInfo.type && nodeInfo.type.includes('CLIPTextEncode')) { + for (const widget of nodeInfo.widgets) { + if (widget.name === 'text' && typeof widget.value === 'string') { + return widget.value; + } + } + } + + for (const widget of nodeInfo.widgets) { + if ((widget.name.toLowerCase().includes('prompt') || + widget.name.toLowerCase().includes('conditioning')) && + typeof widget.value === 'string' && widget.value.length > 0) { + return widget.value; + } + } + + for (const widget of nodeInfo.widgets) { + if ((widget.type === 'text' || widget.type === 'textarea' || + widget.type === 'string' || widget.name.toLowerCase().includes('text')) && + typeof widget.value === 'string' && widget.value.length > 0) { + return widget.value; + } + } + } + return null; +} + +/** + * List of important parameter names for complex nodes. + */ +const COMPLEX_NODE_PARAMS = [ + 'seed', 'steps', 'cfg', 'scale', 'sampler', 'scheduler', + 'positive', 'negative', 'width', 'height', 'denoise', 'strength', + 'noise', 'count', 'batch', 'size', 'phase', 'color', 'intensity', + 'control_after_generate', 'control', 'after', 'generate', + 'start_at_step', 'end_at_step', 'start', 'end', + 'return_with_leftover_noise', 'leftover', 'noise_return', + 'model', 'vae', 'clip', 'lora', 'checkpoint', + 'latent', 'image', 'mask', 'filename', 'directory', + 'prompt', 'conditioning', 'filename_prefix', + 'resolution', 'num_chunks', 'seconds', 'aspect_ratio', + 'style_type', 'background', 'n', 'human', 'raw', 'guidance', + 'skip_preprocessing', 'movement_amplitude', 'animation', + 'material_type', 'b1', 'b2', 's1', 's2', 'type', 'channel', + 'sigma', 'rho', 'alpha', 'base_shift', 'shift', 'stretch', 'terminal', + 'spacing', 'style', 'eta', 'norm_threshold', 'momentum', + 'hypernetwork_name', 'reuse_threshold', 'verbose', 'layers', + 'set_cond_area', 'audioui', 'camera_pose', 'fx', 'cx', 'fy', 'cy' +]; + +/** + * Parameters for save nodes. + */ +const SAVE_NODE_PARAMS = [ + 'filename_prefix', 'filename', 'directory', 'path', + 'format', 'quality', 'extension' +]; + +/** + * Generic widget names to skip. + */ +const SKIP_WIDGET_NAMES = ['title', 'node', 'id', 'type', 'mode']; + +/** + * Check if a node type should show all widgets. + * @param nodeType - The node type string + * @returns True if all widgets should be shown + */ +function shouldShowAllWidgets(nodeType: string): boolean { + const type = nodeType.toLowerCase(); + const isSaveNode = type.includes('save') && + !type.includes('checkpoint') && + !type.includes('model') && + !type.includes('preview'); + + return ( + type.includes('ksampler') || + type.includes('sampler') || + type.includes('k_samplers') || + type.includes('checkpoint') || + type.includes('model') || + type.includes('lora') || + type.includes('controlnet') || + type.includes('advanced') || + type.includes('detailer') || + type.includes('inpaint') || + type.includes('upscale') || + type.includes('clip') || + type.includes('text') || + type.includes('encode') + ) && !isSaveNode; +} + +/** + * Get important node parameters based on node type. + * @param nodeInfo - Node information object + * @returns Array of parameter items + */ +export function getImportantNodeParameters(nodeInfo: NodeInfo): ParameterItem[] { + const parameters: ParameterItem[] = []; + + if (!nodeInfo.widgets || nodeInfo.widgets.length === 0) { + return parameters; + } + + const nodeType = nodeInfo.type || ''; + const typeLower = nodeType.toLowerCase(); + const isSaveNode = typeLower.includes('save') && + !typeLower.includes('checkpoint') && + !typeLower.includes('model') && + !typeLower.includes('preview'); + + // For complex nodes, show ALL widgets + if (nodeType && shouldShowAllWidgets(nodeType)) { + for (const widget of nodeInfo.widgets) { + if (widget.name && widget.name !== '') { + const widgetName = widget.name.toLowerCase(); + if (!SKIP_WIDGET_NAMES.some(skip => widgetName.includes(skip))) { + parameters.push({ + label: widget.name, + value: formatWidgetValue(widget.value) + }); + } + } + } + return parameters; + } + + // For other nodes, use filtered parameter lists + const importantParams = isSaveNode ? SAVE_NODE_PARAMS : COMPLEX_NODE_PARAMS; + + for (const widget of nodeInfo.widgets) { + const paramName = widget.name.toLowerCase(); + if (importantParams.some(param => paramName.includes(param))) { + parameters.push({ + label: widget.name, + value: formatWidgetValue(widget.value) + }); + } + } + + return parameters; +} diff --git a/src/info-panel/PositionManager.ts b/src/info-panel/PositionManager.ts new file mode 100644 index 0000000..fedc3a5 --- /dev/null +++ b/src/info-panel/PositionManager.ts @@ -0,0 +1,205 @@ +/** + * Position Manager class. + * Handles all positioning logic for the info panel. + */ + +import { StateManager } from './StateManager'; + +export class PositionManager { + stateManager: StateManager; + panelElement: HTMLElement; + + constructor(stateManager: StateManager, panelElement: HTMLElement) { + this.stateManager = stateManager; + this.panelElement = panelElement; + } + + positionPanel(): void { + if (!this.panelElement) return; + + // If panel is pinned, keep it at its pinned position + if (this.stateManager.state.isPanelPinned) { + this.applyPinnedPosition(); + return; + } + + // Normal positioning logic + this.calculateNormalPosition(); + } + + applyPinnedPosition(): void { + if (!this.panelElement) return; + + let { x, y } = this.stateManager.state.pinnedPosition; + + // If pinnedPosition is uninitialized (0,0), use calculateNormalPosition to get a valid position + if (x === 0 && y === 0) { + this.calculateNormalPosition(); + // Save the calculated position as the new pinned position + if (this.panelElement) { + const rect = this.panelElement.getBoundingClientRect(); + this.stateManager.state.pinnedPosition = { x: rect.left, y: rect.top }; + } + return; + } + + const panelWidth = this.panelElement.offsetWidth || 300; + const panelHeight = this.panelElement.offsetHeight || 400; + const margin = 4; + + const boundedX = Math.max(margin, Math.min(x, window.innerWidth - panelWidth - margin)); + const boundedY = Math.max(margin, Math.min(y, window.innerHeight - panelHeight - margin)); + + this.panelElement.style.left = `${boundedX}px`; + this.panelElement.style.top = `${boundedY}px`; + } + + calculateNormalPosition(): void { + const magnifyGlass = window.comfyUIMagnifyGlass; + if (!magnifyGlass || !this.panelElement) return; + + const settings = this.stateManager.state.settings; + const panelWidth = Number(settings["🔍MagnifyGlass.InfoPanelWidth"]) || 300; + const panelHeight = this.panelElement.offsetHeight || 400; + + let left: number; + let top: number; + const margin = 4; + + // Check for valid glass rect first + const glassRect = magnifyGlass.ui.glassDiv?.getBoundingClientRect(); + const hasValidGlassRect = glassRect && (glassRect.right > 0 || glassRect.top > 0); + + // Check for valid mouse position + const mouseX = magnifyGlass.lastKnownMousePosition?.x || 0; + const mouseY = magnifyGlass.lastKnownMousePosition?.y || 0; + const hasValidMousePosition = mouseX > 0 || mouseY > 0; + + if (!this.stateManager.state.isGlassPreviewVisible) { + // Glass is hidden, use mouse position + if (hasValidMousePosition) { + left = mouseX - (panelWidth / 2); + top = mouseY - 20; + } else { + // Fallback: right side of screen + left = window.innerWidth - panelWidth - 50; + top = 100; + } + } else if (hasValidGlassRect) { + // Position relative to glass + const position = settings["🔍MagnifyGlass.InfoPanelPosition"]; + switch (position) { + case "Right": + left = glassRect!.right + margin; + top = glassRect!.top; + break; + case "Left": + left = glassRect!.left - panelWidth - margin; + top = glassRect!.top; + break; + case "Top": + left = glassRect!.left; + top = glassRect!.top - panelHeight - margin; + break; + case "Bottom": + left = glassRect!.left; + top = glassRect!.bottom + margin; + break; + default: + left = glassRect!.right + margin; + top = glassRect!.top; + break; + } + } else if (hasValidMousePosition) { + // Fall back to mouse position + left = mouseX - (panelWidth / 2); + top = mouseY - 20; + } else { + // Final fallback: right side of screen + left = window.innerWidth - panelWidth - 50; + top = 100; + } + + left = Math.max(4, Math.min(left, window.innerWidth - panelWidth - 4)); + top = Math.max(4, Math.min(top, window.innerHeight - panelHeight - 4)); + + this.panelElement.style.left = `${left}px`; + this.panelElement.style.top = `${top}px`; + } + + /** + * Position the floating controls relative to the panel. + */ + positionFloatingControls(controlsElement: HTMLElement | null): void { + if (!controlsElement) return; + + // Check if controls should be visible based on settings + const settings = this.stateManager.state.settings; + if (settings["🔍MagnifyGlass.ShowHoveringControls"] === false) { + controlsElement.style.display = 'none'; + return; + } + + const isPanelVisible = this.stateManager.state.isPanelVisible; + const magnifyGlass = window.comfyUIMagnifyGlass; + let referenceRect: DOMRect | null = null; + + if (isPanelVisible && this.panelElement) { + // Position relative to panel + referenceRect = this.panelElement.getBoundingClientRect(); + } else if (magnifyGlass && magnifyGlass.ui && magnifyGlass.ui.glassDiv) { + // Position relative to magnify glass + referenceRect = magnifyGlass.ui.glassDiv.getBoundingClientRect(); + // Don't show controls if glass is hidden/invalid + if (referenceRect.width === 0 || referenceRect.height === 0) { + controlsElement.style.display = 'none'; + return; + } + } + + if (!referenceRect) return; + + const controlsPosition = this.stateManager.state.settings["🔍MagnifyGlass.ControlsPosition"] || "right"; + const margin = 4; + + let left: number; + let top: number; + + switch (controlsPosition) { + case "left": + left = referenceRect.left - controlsElement.offsetWidth - margin; + top = referenceRect.top; + break; + case "right": + left = referenceRect.right + margin; + top = referenceRect.top; + break; + case "top": + left = referenceRect.left; + top = referenceRect.top - controlsElement.offsetHeight - margin; + break; + case "bottom": + left = referenceRect.left; + top = referenceRect.bottom + margin; + break; + case "top-right": + left = referenceRect.right - controlsElement.offsetWidth; + top = referenceRect.top - controlsElement.offsetHeight - margin; + break; + default: + left = referenceRect.right + margin; + top = referenceRect.top; + } + + // Keep controls within viewport + left = Math.max(10, Math.min(left, window.innerWidth - controlsElement.offsetWidth - 10)); + top = Math.max(10, Math.min(top, window.innerHeight - controlsElement.offsetHeight - 10)); + + controlsElement.style.left = `${left}px`; + controlsElement.style.top = `${top}px`; + + // Show controls now that they're positioned (initially hidden off-screen) + controlsElement.style.display = 'flex'; + controlsElement.style.visibility = 'visible'; + } +} diff --git a/src/info-panel/StateManager.ts b/src/info-panel/StateManager.ts new file mode 100644 index 0000000..e9fecab --- /dev/null +++ b/src/info-panel/StateManager.ts @@ -0,0 +1,420 @@ +/** + * ComfyUI MagnifyGlass - Info Panel State Manager (TypeScript) + * + * Centralized state management for the information panel. + */ + +import type { GatheredInfo, SettingsChange } from '../types/comfyui'; +import { getSettingValue } from '../shared/utils'; +import { DEFAULT_PANEL_SETTINGS } from '../shared/settings'; + +export interface InfoPanelStateData { + // Panel visibility and positioning + isPanelVisible: boolean; + wasPanelVisibleBeforeHide: boolean; // Persist state across glass toggles + isPanelMinimized: boolean; + isPanelPinned: boolean; + isPanelLocked: boolean; // New lock state + isAutoPinned: boolean; // Track if panel was auto-pinned by glass hide + pinnedPosition: { x: number; y: number }; + lastPinnedPosition: { x: number; y: number } | null; // Remember last pinned location + + // Glass visibility + isGlassPreviewVisible: boolean; + + // Section expansion + expandedSections: Set; + + // Interaction states + isPanelHovered: boolean; + isHoveringNode: boolean; + + // Auto-collapse + autoExpandTimer: ReturnType | null; + lastNodeId: string | number | null; + + // Update scheduling + updateScheduled: boolean; + isInitialLoading: boolean; + + // Current data + currentInfo: GatheredInfo | null; + + // Settings cache - using index signature for dynamic ComfyUI settings + settings: { [key: string]: string | number | boolean }; + + // Auto-detected theme + currentTheme: string; +} + +/** + * State Manager class. + * Handles all state-related operations and provides clean state transitions. + */ +export class StateManager { + state: InfoPanelStateData; + + constructor() { + this.state = { + // Panel visibility and positioning + isPanelVisible: false, + wasPanelVisibleBeforeHide: false, + isPanelMinimized: true, + isPanelPinned: false, + isPanelLocked: false, // New lock state + isAutoPinned: false, // Track if panel was auto-pinned by glass hide + pinnedPosition: { x: 0, y: 0 }, + lastPinnedPosition: null, // Remember last pinned location + + // Glass visibility + isGlassPreviewVisible: true, + + // Section expansion + expandedSections: new Set(['node']), + + // Interaction states + isPanelHovered: false, + isHoveringNode: false, + + // Auto-collapse + autoExpandTimer: null, + lastNodeId: null, + + // Update scheduling + updateScheduled: false, + isInitialLoading: false, + + // Current data + currentInfo: null, + + // Settings cache + settings: {}, + + // Auto-detected theme + currentTheme: 'dark' // Will be auto-detected + }; + + this.initThemeDetection(); + this.loadSettings(); + } + + initThemeDetection(): void { + // Detect ComfyUI's current theme + this.detectCurrentTheme(); + + // Force an immediate check in case we missed the initial state + setTimeout(() => { + if (this.detectCurrentTheme()) { + this.notifyThemeChange(); + } + }, 1000); + + // Set up mutation observer to watch for theme changes + this.setupThemeObserver(); + + // Listen for system theme changes as fallback + if (window.matchMedia) { + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)'); + if (prefersDark.addListener) { + prefersDark.addListener(() => this.detectCurrentTheme()); + } else if (prefersDark.addEventListener) { + prefersDark.addEventListener('change', () => this.detectCurrentTheme()); + } + } + + // Add click listener to theme toggle buttons for immediate detection + this.setupThemeButtonListeners(); + } + + detectCurrentTheme(): boolean { + let detectedTheme = 'dark'; // Default fallback + + // Method 1: Try to read from ComfyUI's color palette setting + try { + // ComfyUI stores theme in 'Comfy.ColorPalette' setting + const colorPalette = getSettingValue('Comfy.ColorPalette', ''); + + if (colorPalette) { + // Extract theme name from various possible formats + const paletteStr = String(colorPalette).toLowerCase(); + + if (paletteStr.includes('solarized')) { + detectedTheme = 'solarized'; + } else if (paletteStr.includes('arc')) { + detectedTheme = 'arc'; + } else if (paletteStr.includes('nord')) { + detectedTheme = 'nord'; + } else if (paletteStr.includes('github')) { + detectedTheme = 'github'; + } else if (paletteStr.includes('light')) { + detectedTheme = 'light'; + } else if (paletteStr.includes('dark')) { + detectedTheme = 'dark'; + } + } + } catch (e) { + // Fallback to brightness-based detection + console.warn('ComfyUI Info Panel: Could not read theme from settings, using fallback detection'); + } + + // Method 2: Fallback - check body/html for theme classes or data attributes + if (detectedTheme === 'dark') { + const body = document.body; + const html = document.documentElement; + + // Check for data-theme attribute + const dataTheme = body.getAttribute('data-theme') || html.getAttribute('data-theme'); + if (dataTheme) { + detectedTheme = dataTheme.toLowerCase(); + } + + // Check for theme classes + const classes = (body.className + ' ' + html.className).toLowerCase(); + if (classes.includes('theme-solarized') || classes.includes('solarized')) { + detectedTheme = 'solarized'; + } else if (classes.includes('theme-arc') || classes.includes('arc-theme')) { + detectedTheme = 'arc'; + } else if (classes.includes('theme-nord') || classes.includes('nord-theme')) { + detectedTheme = 'nord'; + } else if (classes.includes('theme-github') || classes.includes('github-theme')) { + detectedTheme = 'github'; + } else if (classes.includes('theme-light') || classes.includes('light-theme')) { + detectedTheme = 'light'; + } + } + + // Method 3: Last resort - brightness detection for light vs dark + if (detectedTheme === 'dark') { + const bodyStyles = window.getComputedStyle(document.body); + const bgColor = bodyStyles.backgroundColor; + if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') { + const rgbMatch = bgColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); + if (rgbMatch) { + const [, r, g, b] = rgbMatch.map(Number); + const brightness = (r * 299 + g * 587 + b * 114) / 1000; + if (brightness > 180) { + detectedTheme = 'light'; + } + } + } + } + + // Update theme if it changed + if (this.state.currentTheme !== detectedTheme) { + this.state.currentTheme = detectedTheme; + return true; // Theme changed + } + + return false; // No change + } + + setupThemeObserver(): void { + // Watch for changes to body/html classes and attributes + const observer = new MutationObserver((mutations) => { + let shouldCheck = false; + mutations.forEach((mutation) => { + if (mutation.type === 'attributes' && + (mutation.attributeName === 'class' || + mutation.attributeName === 'data-theme' || + mutation.attributeName === 'style')) { + shouldCheck = true; + } + }); + + if (shouldCheck && this.detectCurrentTheme()) { + // Theme changed, update UI immediately + this.notifyThemeChange(); + } + }); + + // Observe changes to body, html, and vue app + observer.observe(document.body, { attributes: true, attributeOldValue: true }); + observer.observe(document.documentElement, { attributes: true, attributeOldValue: true }); + + const vueApp = document.querySelector('#vue-app'); + if (vueApp) { + observer.observe(vueApp, { attributes: true, attributeOldValue: true }); + } + + // Also watch for changes to any element with theme-related classes + const themeElements = document.querySelectorAll('[class*="theme"], [class*="dark"], [class*="light"], [data-theme]'); + themeElements.forEach(el => { + observer.observe(el, { attributes: true, attributeOldValue: true }); + }); + + // Periodic fallback check every 2 seconds + setInterval(() => { + if (this.detectCurrentTheme()) { + this.notifyThemeChange(); + } + }, 2000); + } + + setupThemeButtonListeners(): void { + // Listen for clicks on theme toggle buttons + document.addEventListener('click', (e) => { + // Check if clicked element might be a theme toggle + const target = e.target as HTMLElement; + const isThemeButton = target.textContent?.toLowerCase().includes('theme') || + target.textContent?.toLowerCase().includes('light') || + target.textContent?.toLowerCase().includes('dark') || + target.title?.toLowerCase().includes('theme') || + (typeof target.className === 'string' && target.className.toLowerCase().includes('theme')); + + if (isThemeButton) { + setTimeout(() => { + if (this.detectCurrentTheme()) { + this.notifyThemeChange(); + } + }, 100); + } + }); + } + + notifyThemeChange(): void { + // Update settings to reflect new theme + this.state.settings["🔍MagnifyGlass.InfoPanelTheme"] = this.state.currentTheme; + + // Notify UI components of theme change immediately + if (window.infoPanelManager && window.infoPanelManager.uiManager) { + window.infoPanelManager.uiManager.updateTheme(this.state.currentTheme); + } + } + + loadSettings(): void { + Object.keys(DEFAULT_PANEL_SETTINGS).forEach(key => { + // Skip the theme setting since we auto-detect it + if (key !== "🔍MagnifyGlass.InfoPanelTheme") { + this.state.settings[key] = getSettingValue(key, (DEFAULT_PANEL_SETTINGS as any)[key]); + } + }); + + // Set theme to auto-detected value + this.state.settings["🔍MagnifyGlass.InfoPanelTheme"] = this.state.currentTheme; + } + + updateSettings(): Record { + const oldSettings = { ...this.state.settings }; + this.loadSettings(); + + // Return what changed for reactive updates + const changes: Record = {}; + Object.keys(this.state.settings).forEach(key => { + if (oldSettings[key] !== this.state.settings[key]) { + changes[key] = { + old: oldSettings[key], + new: this.state.settings[key] + }; + } + }); + + return changes; + } + + togglePanelVisibility(): boolean { + this.state.isPanelVisible = !this.state.isPanelVisible; + return this.state.isPanelVisible; + } + + toggleGlassPreview(): boolean { + this.state.isGlassPreviewVisible = !this.state.isGlassPreviewVisible; + return this.state.isGlassPreviewVisible; + } + + togglePinning(): boolean { + const wasPinned = this.state.isPanelPinned; + this.state.isPanelPinned = !this.state.isPanelPinned; + + // When unpinning, also unlock and clear auto-pin flag + if (!this.state.isPanelPinned) { + this.state.isPanelLocked = false; + this.state.isAutoPinned = false; // Clear auto-pin flag when manually unpinning + } else { + // When manually pinning, also clear auto-pin flag (this is a user-initiated pin) + this.state.isAutoPinned = false; + } + + if (this.state.isPanelPinned && this.state.lastPinnedPosition) { + // Restore to last pinned position + this.state.pinnedPosition = { ...this.state.lastPinnedPosition }; + } else if (!this.state.isPanelPinned && wasPinned) { + // Save current position when unpinning + this.state.lastPinnedPosition = { ...this.state.pinnedPosition }; + } + + return this.state.isPanelPinned; + } + + toggleLocking(): boolean { + // Only allow locking when pinned + if (!this.state.isPanelPinned) { + return false; + } + + this.state.isPanelLocked = !this.state.isPanelLocked; + return this.state.isPanelLocked; + } + + toggleMinimized(): boolean { + this.state.isPanelMinimized = !this.state.isPanelMinimized; + return this.state.isPanelMinimized; + } + + toggleSection(sectionId: string): boolean { + if (sectionId === 'node') return false; // Node section always expanded + + if (this.state.expandedSections.has(sectionId)) { + this.state.expandedSections.delete(sectionId); + } else { + this.state.expandedSections.add(sectionId); + } + return true; + } + + setPinnedPosition(x: number, y: number): void { + this.state.pinnedPosition = { x, y }; + // Also save as last pinned position for memory + this.state.lastPinnedPosition = { x, y }; + } + + setCurrentInfo(info: GatheredInfo): void { + this.state.currentInfo = info; + } + + scheduleAutoCollapse(): void { + this.clearAutoExpandTimer(); + if (this.state.settings["🔍MagnifyGlass.InfoPanelAnimations"]) { + this.state.autoExpandTimer = setTimeout(() => { + if (!this.state.isPanelHovered) { + this.collapseNodeSections(); + } + }, 1500); + } + } + + clearAutoExpandTimer(): void { + if (this.state.autoExpandTimer) { + clearTimeout(this.state.autoExpandTimer); + this.state.autoExpandTimer = null; + } + } + + expandNodeSections(): void { + this.state.expandedSections.add('hoveredNode'); + this.state.expandedSections.add('node'); + this.state.expandedSections.add('cursor'); + this.state.expandedSections.add('canvas'); + this.state.expandedSections.add('magnifier'); + this.clearAutoExpandTimer(); + } + + collapseNodeSections(): void { + this.state.expandedSections.delete('hoveredNode'); + this.state.expandedSections.delete('node'); + this.state.expandedSections.delete('widget'); + } + + cleanup(): void { + this.clearAutoExpandTimer(); + } +} diff --git a/src/info-panel/UIManager.ts b/src/info-panel/UIManager.ts new file mode 100644 index 0000000..1a1b6e1 --- /dev/null +++ b/src/info-panel/UIManager.ts @@ -0,0 +1,707 @@ +/** + * ComfyUI MagnifyGlass - Info Panel UI Manager (TypeScript) + * + * Complete UI Manager extracted from magnify_info_panel.js + * Handles all DOM manipulation and UI creation for the info panel. + */ + +import { StateManager } from './StateManager'; +import { Icons } from '../shared/icons'; +import { Logger } from '../shared/logger'; +import { formatValue, getValueClass, getValueAttributes, formatWidgetValue } from './ValueFormatter'; +import { + getCheckpointInfo, + getImageInfo, + getTextBoxContent, + getImportantNodeParameters, + type ImageInfoResult +} from './NodeDataExtractor'; + +interface InfoPanelElements { + panel: HTMLDivElement | null; + header: HTMLDivElement | null; + content: HTMLDivElement | null; + controls: HTMLDivElement | null; +} + +/** + * UI Manager class. + * Handles all DOM manipulation and UI creation. + */ +export class UIManager { + stateManager: StateManager; + elements: InfoPanelElements; + + constructor(stateManager: StateManager) { + this.stateManager = stateManager; + this.elements = { + panel: null, + header: null, + content: null, + controls: null + }; + + this.createPanel(); + this.injectStyles(); + } + + /** + * Create the main panel and its components. + */ + createPanel(): void { + // Main panel container + this.elements.panel = document.createElement("div"); + this.elements.panel.id = "comfyui-magnify-info-panel-pro-v2"; + this.elements.panel.className = `magnify-info-panel theme-${this.stateManager.state.currentTheme}`; + + // Header + this.elements.header = document.createElement("div"); + this.elements.header.className = "panel-header"; + this.elements.header.innerHTML = ` +
+
${Icons.magnifyGlass}
+
Inspector
+
Real-time analysis
+
+
+ +
+ `; + + // Content container + this.elements.content = document.createElement("div"); + this.elements.content.className = "panel-content"; + + this.elements.panel.appendChild(this.elements.header); + this.elements.panel.appendChild(this.elements.content); + + this.applyStyles(); + // Apply initial minimized state if set + if (this.stateManager.state.isPanelMinimized) { + this.elements.panel.classList.add('panel-minimized'); + } + document.body.appendChild(this.elements.panel); + + // Add click event delegation for panel elements + this.elements.panel.addEventListener('click', (e: MouseEvent) => { + const target = e.target as HTMLElement; + + // Handle minimize button + const minimizeBtn = target.closest('[data-action="minimize"]'); + if (minimizeBtn) { + this.stateManager.state.isPanelMinimized = !this.stateManager.state.isPanelMinimized; + this.updateMinimizedState(); + return; + } + + // Handle section header clicks for expand/collapse + const sectionHeader = target.closest('.section-header') as HTMLElement; + if (sectionHeader) { + const sectionId = sectionHeader.getAttribute('data-section'); + // Skip node section (always expanded) + if (sectionId && sectionId !== 'node') { + const isExpanded = this.stateManager.state.expandedSections.has(sectionId); + if (isExpanded) { + this.stateManager.state.expandedSections.delete(sectionId); + } else { + this.stateManager.state.expandedSections.add(sectionId); + } + + // Update visual state + sectionHeader.classList.toggle('expanded', !isExpanded); + const sectionContent = sectionHeader.nextElementSibling as HTMLElement; + if (sectionContent && sectionContent.classList.contains('section-content')) { + sectionContent.classList.toggle('expanded', !isExpanded); + } + } + } + }); + + // Create floating controls after panel is in DOM + if (!this.elements.controls) { + this.createFloatingControls(); + // Update control states immediately after creation + this.updateControlStates(); + } + } + + /** + * Create floating control buttons. + */ + createFloatingControls(): void { + this.elements.controls = document.createElement("div"); + this.elements.controls.className = "floating-controls vertical-layout"; // Default to vertical + + /** + * BUTTON MAPPING DOCUMENTATION: + * ============================= + * Button 1 (Unlock Icon): data-action="pin" → controls isPanelPinned + * - Unpinned (default): Panel follows magnify glass + * - Pinned: Panel stays at fixed position (unlocked from following glass) + * + * Button 2 (Pin Icon): data-action="lock" → controls isPanelLocked + * - Unlocked (default): Panel can be dragged + * - Locked: Panel cannot be dragged (pinned in place) + * + * NOTE: The data-action names are counterintuitive but kept for backwards compatibility. + * The ICONS correctly represent the functionality: + * - Unlock icon = unlock from following glass + * - Pin icon = pin position (prevent drag) + */ + this.elements.controls.innerHTML = ` + + + + + `; + + // Insert before the panel in the document body, not as a child + document.body.appendChild(this.elements.controls); + + // Hide and position off-screen initially - will be shown when magnify glass activates and positions them + this.elements.controls.style.display = 'none'; + this.elements.controls.style.visibility = 'hidden'; + this.elements.controls.style.left = '-9999px'; + this.elements.controls.style.top = '-9999px'; + + // Add click event delegation for control buttons + this.elements.controls.addEventListener('click', (e: MouseEvent) => { + const target = e.target as HTMLElement; + const button = target.closest('button[data-action]') as HTMLButtonElement; + if (!button) return; + + const action = button.getAttribute('data-action'); + Logger.debug(`Control button clicked: ${action}`); + + switch (action) { + case 'pin': + this.stateManager.togglePinning(); + this.updatePinnedState(); + this.updateControlStates(); + break; + case 'lock': + this.stateManager.state.isPanelLocked = !this.stateManager.state.isPanelLocked; + this.updateControlStates(); + if (this.elements.panel) { + this.elements.panel.classList.toggle('panel-locked', this.stateManager.state.isPanelLocked); + } + break; + case 'toggle-panel': + if (this.stateManager.state.isPanelVisible) { + this.hide(); + } else { + this.show(); + } + this.updateControlStates(); + break; + case 'toggle-glass': + this.stateManager.state.isGlassPreviewVisible = !this.stateManager.state.isGlassPreviewVisible; + + // Link Glass Visibility to Panel Pinning + if (!this.stateManager.state.isGlassPreviewVisible) { + // Glass Hidden + console.log(`[MagnifyGlass Debug] Glass Hidden. pinned=${this.stateManager.state.isPanelPinned}, autoPinned=${this.stateManager.state.isAutoPinned}`); + // Only auto-pin if not already pinned by the user + if (!this.stateManager.state.isPanelPinned) { + if (this.elements.panel) { + const rect = this.elements.panel.getBoundingClientRect(); + this.stateManager.state.pinnedPosition = { x: rect.left, y: rect.top }; + } + this.stateManager.state.isPanelPinned = true; + this.stateManager.state.isAutoPinned = true; // Track that this was auto-pinned + console.log(`[MagnifyGlass Debug] Auto-Pinned panel.`); + } + this.stateManager.state.isPanelLocked = false; // Ensure dragging is allowed + } else { + // Glass Shown + console.log(`[MagnifyGlass Debug] Glass Shown. pinned=${this.stateManager.state.isPanelPinned}, autoPinned=${this.stateManager.state.isAutoPinned}`); + // Only unpin if it was auto-pinned (not user-pinned) + if (this.stateManager.state.isAutoPinned) { + this.stateManager.state.isPanelPinned = false; + this.stateManager.state.isAutoPinned = false; + console.log(`[MagnifyGlass Debug] Auto-Unpinned panel.`); + } + // If user manually pinned, keep the panel pinned + } + + this.updateControlStates(); + this.updatePinnedState(); // Update visual class + + // Toggle ONLY the visual visibility (opacity) of the glass preview + // The tool remains active so the inspector can track position + const magnifyGlass = (window as any).comfyUIMagnifyGlass; + if (magnifyGlass && magnifyGlass.ui?.setPreviewVisibility) { + magnifyGlass.ui.setPreviewVisibility(this.stateManager.state.isGlassPreviewVisible); + } + break; + } + }); + + // Set initial layout based on settings + const controlsPosition = String(this.stateManager.state.settings["🔍MagnifyGlass.ControlsPosition"] || "top-right"); + this.updateControlsLayout(controlsPosition); + + // Initial state update + this.updateControlStates(); + } + + /** + * Update control button states. + */ + updateControlStates(): void { + if (!this.elements.controls) return; + + const pinBtn = this.elements.controls.querySelector('[data-action="pin"]') as HTMLButtonElement; + const lockBtn = this.elements.controls.querySelector('[data-action="lock"]') as HTMLButtonElement; + const visibilityBtn = this.elements.controls.querySelector('[data-action="toggle-panel"]') as HTMLButtonElement; + const glassBtn = this.elements.controls.querySelector('[data-action="toggle-glass"]') as HTMLButtonElement; + const isPanelVisible = this.stateManager.state.isPanelVisible; + const isGlassVisible = this.stateManager.state.isGlassPreviewVisible; + + if (pinBtn) { + pinBtn.classList.toggle('active', this.stateManager.state.isPanelPinned); + pinBtn.title = this.stateManager.state.isPanelPinned ? "Lock Panel" : "Unlock Panel"; + pinBtn.innerHTML = this.stateManager.state.isPanelPinned ? Icons.lock : Icons.unlock; + // Hide unlock button when panel is hidden + pinBtn.style.display = isPanelVisible ? 'flex' : 'none'; + + // Disable unlock (unpin) button if glass is hidden + // This prevents entering "Follow Mouse" mode when glass is invisible + if (!isGlassVisible) { + pinBtn.disabled = true; + pinBtn.style.opacity = '0.5'; + pinBtn.title = "Cannot unlock panel from screen when glass preview is hidden"; + } else { + pinBtn.disabled = false; + pinBtn.style.opacity = ''; + } + } + + if (lockBtn) { + // Only show pin button when panel is visible AND unlocked from glass + const showLockBtn = isPanelVisible && this.stateManager.state.isPanelPinned; + lockBtn.style.display = showLockBtn ? 'flex' : 'none'; + lockBtn.classList.toggle('active', this.stateManager.state.isPanelLocked); + lockBtn.title = this.stateManager.state.isPanelLocked ? "Unpin Panel Position" : "Pin Panel Position"; + lockBtn.disabled = !this.stateManager.state.isPanelPinned; + } + + if (visibilityBtn) { + // Active means "Panel is Visible" + visibilityBtn.classList.toggle('active', isPanelVisible); + visibilityBtn.title = isPanelVisible ? "Hide Panel" : "Show Panel"; + + // Disable hide button if glass is hidden (prevent hiding everything) + if (!isGlassVisible) { + visibilityBtn.disabled = true; + visibilityBtn.style.opacity = '0.5'; + visibilityBtn.title = "Cannot hide panel when glass preview is hidden"; + } else { + visibilityBtn.disabled = false; + visibilityBtn.style.opacity = ''; + } + } + + if (glassBtn) { + glassBtn.classList.toggle('active', isGlassVisible); + glassBtn.title = isGlassVisible ? "Hide Glass Preview" : "Show Glass Preview"; + + // Only show glass toggle button if the inspector panel is visible + glassBtn.style.display = isPanelVisible ? 'flex' : 'none'; + } + } + + /** + * Update controls layout based on position setting. + * @param position + */ + updateControlsLayout(position: string): void { + if (!this.elements.controls) return; + + // Remove existing layout classes + this.elements.controls.classList.remove('horizontal-layout', 'vertical-layout'); + + // Add appropriate layout class + if (['top', 'bottom'].includes(position)) { + this.elements.controls.classList.add('horizontal-layout'); + } else { + this.elements.controls.classList.add('vertical-layout'); + } + } + + /** + * Apply current styles to elements. + */ + applyStyles(): void { + if (!this.elements.panel) return; + + const settings = this.stateManager.state.settings; + + // Debug Log + console.log(`[UIManager] applyStyles - MaxHeight: ${settings["🔍MagnifyGlass.InfoPanelMaxHeight"]}`); + + // 1. Apply dimensions + this.elements.panel.style.width = `${settings["🔍MagnifyGlass.InfoPanelWidth"]}px`; + // Set both height and max-height so the panel always shows at this size + const maxHeight = settings["🔍MagnifyGlass.InfoPanelMaxHeight"]; + this.elements.panel.style.height = `${maxHeight}px`; + this.elements.panel.style.maxHeight = `${maxHeight}px`; + + // 2. Apply static styles (only needed once or if overwritten, but safe to set) + this.elements.panel.style.position = 'absolute'; + this.elements.panel.style.zIndex = '99999'; + this.elements.panel.style.transform = 'translateY(-10px)'; + this.elements.panel.style.pointerEvents = 'auto'; + this.elements.panel.style.userSelect = 'none'; + this.elements.panel.style.fontFamily = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"; + + // 3. Apply transition + this.elements.panel.style.transition = settings["🔍MagnifyGlass.InfoPanelAnimations"] + ? 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)' + : 'none'; + + // 4. Apply Custom Colors (CSS Variables) + const textColor = settings["🔍MagnifyGlass.InfoPanelTextColor"] as string | undefined; + if (textColor && typeof textColor === 'string') { + const normalizedTextColor = textColor.startsWith('#') ? textColor : `#${textColor}`; + this.elements.panel.style.setProperty('--info-panel-text-color', normalizedTextColor); + } + + const accentColor = settings["🔍MagnifyGlass.InfoPanelAccentColor"] as string | undefined; + if (accentColor && typeof accentColor === 'string') { + const normalizedAccentColor = accentColor.startsWith('#') ? accentColor : `#${accentColor}`; + this.elements.panel.style.setProperty('--info-panel-accent-color', normalizedAccentColor); + } + + // 5. Apply Opacity + // Logic: If visible, use setting. If hidden, use 0. + // We do NOT touch 'display' here to avoid hiding it if it's currently shown. + // 'display' is managed by show() / hide() methods exclusively. + if (this.stateManager.state.isPanelVisible) { + const opacityPercent = Number(settings["🔍MagnifyGlass.InfoPanelOpacity"]) || 100; + this.elements.panel.style.opacity = (opacityPercent / 100).toString(); + } else { + this.elements.panel.style.opacity = '0'; + } + } + + /** + * Show the panel. + */ + show(): void { + if (!this.elements.panel) return; + + if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"]) { + this.elements.panel.style.display = "block"; + // Controls are shown by positionFloatingControls() after proper positioning + // Apply user's opacity setting when showing (convert percentage to decimal) + const opacityPercent = Number(this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelOpacity"]) || 100; + this.elements.panel.style.opacity = (opacityPercent / 100).toString(); + // Trigger reflow + this.elements.panel.offsetHeight; + this.elements.panel.classList.add('visible'); + this.stateManager.state.isPanelVisible = true; + this.updateControlStates(); + } + } + + /** + * Hide the panel. + */ + hide(): void { + if (!this.elements.panel) return; + + this.elements.panel.classList.remove('visible'); + setTimeout(() => { + if (!this.stateManager.state.isPanelVisible && this.elements.panel) { + this.elements.panel.style.display = "none"; + // Do NOT hide controls when panel is hidden - they should remain visible on glass + // if (this.elements.controls) { + // this.elements.controls.style.display = "none"; + // } + } + }, this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelAnimations"] ? 300 : 0); + this.stateManager.state.isPanelVisible = false; + this.updateControlStates(); + } + + /** + * Update minimized state. + */ + updateMinimizedState(): void { + if (!this.elements.panel || !this.elements.header) return; + + this.elements.panel.classList.toggle('panel-minimized', this.stateManager.state.isPanelMinimized); + const minimizeBtn = this.elements.header.querySelector('.minimize-btn'); + if (minimizeBtn) { + minimizeBtn.textContent = this.stateManager.state.isPanelMinimized ? '+' : '−'; + (minimizeBtn as HTMLElement).title = this.stateManager.state.isPanelMinimized ? 'Expand Panel' : 'Minimize Panel'; + } + } + + /** + * Update pinned state. + */ + updatePinnedState(): void { + if (!this.elements.panel) return; + + this.elements.panel.classList.toggle('panel-pinned', this.stateManager.state.isPanelPinned); + this.elements.panel.classList.toggle('panel-locked', this.stateManager.state.isPanelLocked); + this.updateControlStates(); + } + + /** + * Update theme class. + * @param newTheme + */ + updateTheme(newTheme: string): void { + if (this.elements.panel) { + this.elements.panel.className = this.elements.panel.className.replace(/theme-\w+/, `theme-${newTheme.toLowerCase()}`); + } + } + + /** + * Display information in the panel. + * @param info + */ + displayInfo(info: any): void { + const sections = this.buildSections(info); + this.renderSections(sections); + this.updateSectionStates(); + this.updateHeaderSubtitle(info); + } + + /** + * Build section data from info object. + * @param info + * @returns + */ + buildSections(info: any): any[] { + const sections: any[] = []; + const settings = this.stateManager.state.settings; + + // Inspector section + if (settings["🔍MagnifyGlass.ShowInspectorTab"] && info.cursor && info.cursor.canvas) { + const inspectorContent = [ + { label: 'Cursor Canvas', value: `(${Math.round(info.cursor.canvas.x)}, ${Math.round(info.cursor.canvas.y)})` }, + { label: 'Canvas Scale', value: `${(info.canvas.scale * 100).toFixed(1)}%` }, + { label: 'Magnifier Zoom', value: `${info.magnifier.zoomFactor}×` } + ]; + + sections.push({ + id: 'inspector', + icon: Icons.info, + title: 'Inspector', + content: inspectorContent + }); + } + + // Media section + if (info.media) { + const mediaContent = [ + { label: 'Type', value: info.media.tagName }, + { label: 'Source', value: info.media.src } + ]; + + if (info.media.naturalSize) { + mediaContent.push({ label: 'Natural', value: info.media.naturalSize }); + } + + sections.push({ + id: 'media', + icon: Icons.camera, + title: 'Media', + badge: info.media.tagName, + content: mediaContent + }); + } + + // Node Details section + if (info.hoveredNode) { + const nodeContent = [ + { label: 'Title', value: info.hoveredNode.title } + ]; + + // Check if this is a complex node that shows all widgets + const nodeType = info.hoveredNode.type ? info.hoveredNode.type.toLowerCase() : ''; + const isSaveNode = nodeType.includes('save') && + !nodeType.includes('checkpoint') && + !nodeType.includes('model') && + !nodeType.includes('preview'); + + const showAllWidgets = info.hoveredNode.type && ( + nodeType.includes('ksampler') || + nodeType.includes('sampler') || + nodeType.includes('k_samplers') || + nodeType.includes('checkpoint') || + nodeType.includes('model') || + nodeType.includes('lora') || + nodeType.includes('controlnet') || + nodeType.includes('advanced') || + nodeType.includes('detailer') || + nodeType.includes('inpaint') || + nodeType.includes('upscale') || + nodeType.includes('clip') || + nodeType.includes('text') || + nodeType.includes('encode') + ) && !isSaveNode; + + if (!showAllWidgets) { + // Add specific extractions for simple nodes + const checkpoint = getCheckpointInfo(info.hoveredNode); + if (checkpoint) { + nodeContent.push({ label: 'Model', value: checkpoint }); + } + + const imgInfo = getImageInfo(info.hoveredNode); + if (imgInfo) { + if (typeof imgInfo === 'string') { + nodeContent.push({ label: 'Image', value: imgInfo }); + } else if (typeof imgInfo === 'object' && imgInfo !== null) { + const imgResult = imgInfo as ImageInfoResult; + nodeContent.push({ label: 'Image Size', value: `${imgResult.width}×${imgResult.height}` }); + if (imgResult.src) { + nodeContent.push({ label: 'Image Source', value: imgResult.src }); + } + } + } + + const textContent = getTextBoxContent(info.hoveredNode); + if (textContent) { + nodeContent.push({ label: 'Text', value: textContent }); + } + } + + const importantParameters = getImportantNodeParameters(info.hoveredNode); + nodeContent.push(...importantParameters); + + sections.push({ + id: 'node', + icon: Icons.box, + title: 'Node', + badge: info.hoveredNode.type, + content: nodeContent + }); + } + + return sections; + } + + /** + * Render sections to the panel. + * @param sections + */ + renderSections(sections: any[]): void { + if (!this.elements.content) return; + + if (sections.length === 0) { + this.elements.content.innerHTML = ` +
+
${Icons.mapPin}
+
Empty canvas area
+
+ `; + return; + } + + this.elements.content.innerHTML = sections.map(section => ` +
+
+ ${section.icon} + ${section.title} + ${section.badge ? `${section.badge}` : ''} + ${section.id !== 'node' ? `${Icons.chevronRight}` : ''} +
+
+
+ ${section.content.map((item: any) => { + const value = formatValue(item.value, item.label); + const valueClass = getValueClass(item.value); + const valueAttributes = getValueAttributes(item.value); + return ` +
+ ${item.label} + ${value} +
`; + }).join('')} +
+
+
+ `).join(''); + } + + /** + * Update section expansion states. + */ + updateSectionStates(): void { + if (!this.elements.content) return; + + const sections = this.elements.content.querySelectorAll('.info-section'); + sections.forEach(section => { + const sectionId = (section as HTMLElement).dataset.section; + if (!sectionId) return; + + const header = section.querySelector('.section-header'); + const content = section.querySelector('.section-content'); + + if (header && content) { + if (this.stateManager.state.expandedSections.has(sectionId)) { + header.classList.add('expanded'); + content.classList.add('expanded'); + } else { + header.classList.remove('expanded'); + content.classList.remove('expanded'); + } + } + }); + } + + /** + * Update header subtitle. + * @param info + */ + updateHeaderSubtitle(info: any): void { + if (!this.elements.header) return; + + const subtitleElement = this.elements.header.querySelector('.header-subtitle') as HTMLElement; + if (subtitleElement) { + const accentColor = String(this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelAccentColor"] || ''); + if (info.hoveredNode) { + subtitleElement.textContent = `Analyzing: ${info.hoveredNode.title}`; + subtitleElement.style.color = accentColor; + } else if (info.media) { + subtitleElement.textContent = `Media: ${info.media.tagName}`; + subtitleElement.style.color = accentColor; + } else if (info.connection) { + subtitleElement.textContent = `Connection: ${info.connection.type}`; + subtitleElement.style.color = accentColor; + } else { + subtitleElement.textContent = 'Real-time analysis'; + subtitleElement.style.color = ''; + } + } + } + + /** + * Inject CSS styles by loading external stylesheet. + */ + injectStyles(): void { + if (!document.getElementById('magnify-info-panel-styles-v2')) { + const link = document.createElement('link'); + link.id = 'magnify-info-panel-styles-v2'; + link.rel = 'stylesheet'; + link.type = 'text/css'; + link.href = 'extensions/comfyui-magnifyglass/info-panel.css'; + document.head.appendChild(link); + } + } + + cleanup(): void { + if (this.elements.panel && this.elements.panel.parentNode) { + this.elements.panel.parentNode.removeChild(this.elements.panel); + } + if (this.elements.controls && this.elements.controls.parentNode) { + this.elements.controls.parentNode.removeChild(this.elements.controls); + } + } +} diff --git a/src/info-panel/ValueFormatter.ts b/src/info-panel/ValueFormatter.ts new file mode 100644 index 0000000..fde7e11 --- /dev/null +++ b/src/info-panel/ValueFormatter.ts @@ -0,0 +1,95 @@ +/** + * ComfyUI MagnifyGlass - Value Formatter Utilities + * + * Pure utility functions for formatting values in the info panel display. + * Extracted from UIManager.ts for better modularity. + */ + +/** + * Format a value for display in the info panel. + * @param value - The value to format + * @param label - Optional label to determine formatting rules + * @returns Formatted string representation + */ +export function formatValue(value: unknown, label?: string): string { + if (value === null || value === undefined) return ''; + + const str = String(value); + + // Show full text for prompts, text content, model names, and file paths + if (label && ( + label.toLowerCase().includes('text') || + label.toLowerCase().includes('prompt') || + label.toLowerCase().includes('model') || + label.toLowerCase().includes('file') || + label.toLowerCase().includes('conditioning') || + label.toLowerCase().includes('positive') || + label.toLowerCase().includes('negative') + )) { + return str; + } + + // Show full text for very long values (no truncation) + return str; +} + +/** + * Get CSS class names for styling a value. + * @param value - The value to get classes for + * @returns CSS class string + */ +export function getValueClass(value: unknown): string { + if (!value) return ''; + + const str = String(value); + const classes: string[] = []; + + // Mark text that might benefit from special styling for readability + if (str.length > 100) { + classes.push('long-text'); + } + + return classes.join(' '); +} + +/** + * Get HTML attributes for a value element. + * @param value - The value to get attributes for + * @returns HTML attribute string + */ +export function getValueAttributes(value: unknown): string { + // Since we show full text now, we don't need title attributes for long text + // Only add title for very long text that might benefit from tooltips + if (!value) return ''; + + const str = String(value); + if (str.length > 500) { // Only for extremely long text + return `title="${str.replace(/"/g, '"')}"`; + } + + return ''; +} + +/** + * Format widget value for display. + * @param value - Widget value to format + * @returns Formatted string representation + */ +export function formatWidgetValue(value: unknown): string { + if (value === null) return 'null'; + if (value === undefined) return 'undefined'; + if (typeof value === 'string') { + return value; // Show full text without truncation + } + if (typeof value === 'number') { + return Number.isInteger(value) ? value.toString() : value.toFixed(3); + } + if (typeof value === 'boolean') return value.toString(); + if (Array.isArray(value)) { + return `Array(${value.length})`; + } + if (typeof value === 'object') { + return 'Object'; + } + return String(value); +} diff --git a/src/info-panel/index.ts b/src/info-panel/index.ts new file mode 100644 index 0000000..b48465c --- /dev/null +++ b/src/info-panel/index.ts @@ -0,0 +1,6 @@ +export * from './StateManager'; +export * from './UIManager'; +export * from './PositionManager'; +export * from './InformationGatherer'; +export * from './EventManager'; +export * from './InfoPanel'; diff --git a/src/magnify-glass/ConfigManager.ts b/src/magnify-glass/ConfigManager.ts new file mode 100644 index 0000000..b402f1b --- /dev/null +++ b/src/magnify-glass/ConfigManager.ts @@ -0,0 +1,182 @@ +/** + * ComfyUI MagnifyGlass - ConfigManager (TypeScript) + * + * Manages configuration and settings for the magnifying glass. + */ + +import { getSettingValue } from '../shared/utils'; +import { STORAGE_KEYS } from '../shared/constants'; +import type { GlassPosition, GlassShape, TextureFilter } from '../shared/constants'; + +// Default settings (we'll import these properly later) +const DEFAULT_GLASS_SETTINGS = { + "🔍MagnifyGlass.ZoomFactor": 300, + "🔍MagnifyGlass.GlassSize": 300, + "🔍MagnifyGlass.BorderColor": "#6b7280", + "🔍MagnifyGlass.BorderWidth": 1, + "🔍MagnifyGlass.ActivationKey": "x", + "🔍MagnifyGlass.AltRequired": false, + "🔍MagnifyGlass.FollowCursor": false, + "🔍MagnifyGlass.DebugMode": false, + "🔍MagnifyGlass.OffsetStep": 5, + "🔍MagnifyGlass.GlassPosition": "Top-Right" as GlassPosition, + "🔍MagnifyGlass.ResetKey": "o", + "🔍MagnifyGlass.GlassShape": "Rounded Square" as GlassShape, + "🔍MagnifyGlass.BorderEnabled": true, + "🔍MagnifyGlass.TextureFiltering": "Linear" as TextureFilter, + "🔍MagnifyGlass.AlwaysActiveMode": true, + "🔍MagnifyGlass.ToggleFollowCursorKey": "h", +}; + +/** + * Configuration manager class. + * Reads settings from ComfyUI and manages persistent offsets. + */ +export class ConfigManager { + /** Zoom factor (1.0 = 100%, 3.0 = 300%) */ + zoomFactor: number; + + /** Glass size in pixels */ + glassSize: number; + + /** Border color (CSS color string) */ + borderColor: string; + + /** Border width in pixels */ + borderWidth: number; + + /** Activation key (lowercase) */ + activationKey: string; + + /** Whether Alt key is required for activation */ + altRequired: boolean; + + /** Whether glass follows cursor */ + followCursor: boolean; + + /** Debug mode enabled */ + debugMode: boolean; + + /** Offset step for arrow key adjustments */ + offsetStep: number; + + /** Glass position relative to cursor */ + glassPosition: string; + + /** Reset key (lowercase) */ + resetKey: string; + + /** Glass shape */ + glassShape: string; + + /** Whether border is enabled */ + borderEnabled: boolean; + + /** Texture filtering mode */ + textureFiltering: string; + + /** Always active mode */ + alwaysActiveMode: boolean; + + /** Toggle follow cursor key */ + toggleFollowCursorKey: string; + + /** Manual offset X in graph units */ + offsetX: number; + + /** Manual offset Y in graph units */ + offsetY: number; + + constructor() { + // Initialize properties with defaults + this.zoomFactor = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.ZoomFactor"] / 100; + this.glassSize = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.GlassSize"]; + this.borderColor = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.BorderColor"]; + this.borderWidth = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.BorderWidth"]; + this.activationKey = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.ActivationKey"]; + this.altRequired = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.AltRequired"]; + this.followCursor = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.FollowCursor"]; + this.debugMode = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.DebugMode"]; + this.offsetStep = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.OffsetStep"]; + this.glassPosition = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.GlassPosition"]; + this.resetKey = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.ResetKey"]; + this.glassShape = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.GlassShape"]; + this.borderEnabled = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.BorderEnabled"]; + this.textureFiltering = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.TextureFiltering"]; + this.alwaysActiveMode = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.AlwaysActiveMode"]; + this.toggleFollowCursorKey = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.ToggleFollowCursorKey"]; + + // Alignment adjustment parameters - managed separately + this.offsetX = 0; + this.offsetY = 0; + } + + /** + * Load settings from ComfyUI settings system. + */ + loadSettings(): void { + this.zoomFactor = getSettingValue("🔍MagnifyGlass.ZoomFactor", this.zoomFactor * 100) / 100; + this.glassSize = getSettingValue("🔍MagnifyGlass.GlassSize", this.glassSize); + this.borderColor = getSettingValue("🔍MagnifyGlass.BorderColor", this.borderColor); + this.borderWidth = getSettingValue("🔍MagnifyGlass.BorderWidth", this.borderWidth); + this.activationKey = getSettingValue("🔍MagnifyGlass.ActivationKey", this.activationKey); + this.altRequired = getSettingValue("🔍MagnifyGlass.AltRequired", this.altRequired); + this.followCursor = getSettingValue("🔍MagnifyGlass.FollowCursor", this.followCursor); + this.debugMode = getSettingValue("🔍MagnifyGlass.DebugMode", this.debugMode); + this.offsetStep = getSettingValue("🔍MagnifyGlass.OffsetStep", this.offsetStep); + this.glassPosition = getSettingValue("🔍MagnifyGlass.GlassPosition", this.glassPosition); + this.resetKey = getSettingValue("🔍MagnifyGlass.ResetKey", this.resetKey); + this.glassShape = getSettingValue("🔍MagnifyGlass.GlassShape", this.glassShape); + this.borderEnabled = getSettingValue("🔍MagnifyGlass.BorderEnabled", this.borderEnabled); + this.textureFiltering = getSettingValue("🔍MagnifyGlass.TextureFiltering", this.textureFiltering); + this.alwaysActiveMode = getSettingValue("🔍MagnifyGlass.AlwaysActiveMode", this.alwaysActiveMode); + this.toggleFollowCursorKey = getSettingValue("🔍MagnifyGlass.ToggleFollowCursorKey", this.toggleFollowCursorKey); + } + + /** + * Load saved offsets from localStorage. + */ + loadSavedOffsets(): void { + try { + const savedOffsetX = localStorage.getItem(STORAGE_KEYS.OFFSET_X); + const savedOffsetY = localStorage.getItem(STORAGE_KEYS.OFFSET_Y); + + if (savedOffsetX !== null) { + this.offsetX = parseInt(savedOffsetX, 10); + } else { + this.offsetX = 0; + } + + if (savedOffsetY !== null) { + this.offsetY = parseInt(savedOffsetY, 10); + } else { + this.offsetY = 0; + } + } catch (e) { + console.error("ComfyUI Magnifying Glass ERROR: Error loading saved offsets:", e); + this.offsetX = 0; + this.offsetY = 0; + } + } + + /** + * Save current offsets to localStorage. + */ + saveOffsets(): void { + try { + localStorage.setItem(STORAGE_KEYS.OFFSET_X, this.offsetX.toString()); + localStorage.setItem(STORAGE_KEYS.OFFSET_Y, this.offsetY.toString()); + } catch (e) { + console.error("ComfyUI Magnifying Glass ERROR: Error saving offsets:", e); + } + } + + /** + * Reset offsets to default (zero). + */ + resetOffsets(): void { + this.offsetX = 0; + this.offsetY = 0; + this.saveOffsets(); + } +} diff --git a/src/magnify-glass/DebugManager.ts b/src/magnify-glass/DebugManager.ts new file mode 100644 index 0000000..8b98da7 --- /dev/null +++ b/src/magnify-glass/DebugManager.ts @@ -0,0 +1,181 @@ +/** + * ComfyUI MagnifyGlass - DebugManager (TypeScript) + * + * Handles debug visualization and logging. + */ + +import type { ComfyApp } from '../types/comfyui'; +import type { ConfigManager } from './ConfigManager'; +import type { MagnifierState } from './MagnifierState'; +import type { UiManager } from './UiManager'; + +declare const app: ComfyApp; + +/** + * Debug Manager class. + * Provides debug logging and canvas visualization. + */ +export class DebugManager { + config: ConfigManager; + state: MagnifierState; + ui: UiManager; + + constructor(config: ConfigManager, state: MagnifierState, ui: UiManager) { + this.config = config; + this.state = state; + this.ui = ui; + } + + /** + * Log a message if debug mode is enabled. + */ + log(...args: unknown[]): void { + if (this.config.debugMode) { + console.log("ComfyUI Magnifying Glass:", ...args); + } + } + + /** + * Log an error message. + */ + error(...args: unknown[]): void { + console.error("ComfyUI Magnifying Glass ERROR:", ...args); + } + + /** + * Print detailed canvas information for debugging. + */ + printCanvasInfo(): void { + if (!this.config.debugMode) return; + + try { + // Using any for deep property access on ComfyUI objects that might vary + const canvasManager = (app as any).canvas; + const canvas = canvasManager?.graph_canvas as HTMLCanvasElement; + + if (!canvas) { + this.log("Could not find graph canvas for detailed info"); + return; + } + + this.log("---- Canvas Information ----"); + this.log(`Canvas dimensions: ${canvas.width}x${canvas.height}`); + this.log(`Canvas display size: ${canvas.clientWidth}x${canvas.clientHeight}`); + this.log(`Canvas CSS transform: ${canvas.style.transform || 'none'}`); + + const ds = canvasManager.ds; + if (ds) { + this.log(`Canvas DS scale: ${ds.scale}`); + if (ds.offset) { + this.log(`Canvas DS offset: [${ds.offset[0]}, ${ds.offset[1]}]`); + } else { + this.log("Canvas DS offset not found"); + } + } else { + this.log("Canvas DS object not found"); + } + + const screenWidth = window.innerWidth; + const screenHeight = window.innerHeight; + const canvasWidth = canvas.width; + const canvasHeight = canvas.height; + + this.log(`Window dimensions: ${screenWidth}x${screenHeight}`); + this.log(`Resolution scale factor: X=${canvasWidth / screenWidth}, Y=${canvasHeight / screenHeight}`); + this.log("---- End Canvas Information ----"); + } catch (e) { + this.log("Error in printCanvasInfo:", e); + } + } + + /** + * Update the debug visualization canvas. + */ + updateDebugView(): void { + if (!this.config.debugMode || !this.ui || !this.ui.debugCanvas || !this.ui.debugCtx) return; + + const debugCtx = this.ui.debugCtx; + const debugCanvas = this.ui.debugCanvas; + + debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height); + + // Draw background + debugCtx.fillStyle = 'rgba(0,0,0,0.8)'; + debugCtx.fillRect(0, 0, debugCanvas.width, debugCanvas.height); + + // Header + debugCtx.fillStyle = '#FFFFFF'; + debugCtx.font = '14px monospace'; + debugCtx.fillText('Magnify Glass Debug', 10, 20); + + // Position info + debugCtx.font = '12px monospace'; + debugCtx.fillText(`Cursor: (${this.state.x.toFixed(1)}, ${this.state.y.toFixed(1)})`, 10, 50); + debugCtx.fillText(`Source Rect: (${this.state.sourceX.toFixed(1)}, ${this.state.sourceY.toFixed(1)}, w:${this.state.sourceWidth.toFixed(1)}, h:${this.state.sourceHeight.toFixed(1)})`, 10, 70); + + // Canvas transform + debugCtx.fillText(`Canvas Scale: ${this.state.canvasScale.toFixed(2)}`, 10, 90); + debugCtx.fillText(`Canvas Offset: (${this.state.canvasOffsetX.toFixed(1)}, ${this.state.canvasOffsetY.toFixed(1)})`, 10, 110); + + // Offset information + debugCtx.fillStyle = '#FFFF00'; + debugCtx.fillText(`MANUAL OFFSETS: X=${this.config.offsetX}, Y=${this.config.offsetY} (Use arrow keys to adjust)`, 10, 130); + debugCtx.fillStyle = '#FFFFFF'; + + // Visualization + this.drawCanvasVisualization(debugCtx, debugCanvas); + } + + /** + * Draw a scaled visualization of the canvas and source region. + */ + drawCanvasVisualization(debugCtx: CanvasRenderingContext2D, debugCanvas: HTMLCanvasElement): void { + const canvasScale = 0.1; + const canvasVisX = 10; + const canvasVisY = 170; + const canvasVisWidth = 380; + const canvasVisHeight = 150; + + // Draw canvas representation + debugCtx.strokeStyle = '#AAAAAA'; + debugCtx.strokeRect(canvasVisX, canvasVisY, canvasVisWidth, canvasVisHeight); + debugCtx.fillStyle = '#444444'; + debugCtx.fillRect(canvasVisX, canvasVisY, canvasVisWidth, canvasVisHeight); + + // Calculate cursor position in visualization + const cursorVisX = canvasVisX + (this.state.x * canvasScale); + const cursorVisY = canvasVisY + (this.state.y * canvasScale); + + // Calculate source rect in visualization + const sourceRectVisX = canvasVisX + (this.state.sourceX * canvasScale); + const sourceRectVisY = canvasVisY + (this.state.sourceY * canvasScale); + const sourceRectVisWidth = this.state.sourceWidth * canvasScale; + const sourceRectVisHeight = this.state.sourceHeight * canvasScale; + + // Draw source rect + debugCtx.strokeStyle = '#FF0000'; + debugCtx.strokeRect(sourceRectVisX, sourceRectVisY, sourceRectVisWidth, sourceRectVisHeight); + + // Draw cursor position + debugCtx.fillStyle = '#FFFF00'; + debugCtx.beginPath(); + debugCtx.arc(cursorVisX, cursorVisY, 3, 0, Math.PI * 2); + debugCtx.fill(); + + // Draw line from cursor to source rect center + debugCtx.strokeStyle = '#00FF00'; + debugCtx.beginPath(); + debugCtx.moveTo(cursorVisX, cursorVisY); + debugCtx.lineTo(sourceRectVisX + sourceRectVisWidth / 2, sourceRectVisY + sourceRectVisHeight / 2); + debugCtx.stroke(); + + // Label for visualization + debugCtx.fillStyle = '#FFFFFF'; + debugCtx.fillText('Canvas Visualization (scaled)', canvasVisX, canvasVisY - 5); + + // Offset values + const offsetX = this.state.sourceX - this.state.x + (this.state.sourceWidth / 2); + const offsetY = this.state.sourceY - this.state.y + (this.state.sourceHeight / 2); + debugCtx.fillText(`Alignment Offset: (${offsetX.toFixed(1)}, ${offsetY.toFixed(1)})`, 10, canvasVisY + canvasVisHeight + 20); + } +} diff --git a/src/magnify-glass/EventHandler.ts b/src/magnify-glass/EventHandler.ts new file mode 100644 index 0000000..ed1e44c --- /dev/null +++ b/src/magnify-glass/EventHandler.ts @@ -0,0 +1,226 @@ +/** + * ComfyUI MagnifyGlass - EventHandler (TypeScript) + * + * Handles keyboard and mouse events for the magnifying glass. + */ + +import { isUserTyping } from '../shared/utils'; +import { Logger } from '../shared/logger'; +import { DEFAULT_PADDING, DEFAULT_GLASS_Y_OFFSET } from '../shared/constants'; +import type { MagnifyGlass } from './MagnifyGlass'; + +/** + * Event Handler class. + * Manages all keyboard and mouse event handling for the magnifier. + */ +export class EventHandler { + magnifyGlass: MagnifyGlass; + + 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); + } + + /** + * Attach all event listeners. + */ + attachListeners(): void { + document.addEventListener("keydown", this.handleKeyDown); + document.addEventListener("keyup", this.handleKeyUp); + document.addEventListener("mousemove", this.handleMouseMove); + } + + /** + * Detach all event listeners. + */ + detachListeners(): void { + document.removeEventListener("keydown", this.handleKeyDown); + document.removeEventListener("keyup", this.handleKeyUp); + document.removeEventListener("mousemove", this.handleMouseMove); + } + + /** + * Handle keydown events. + */ + handleKeyDown(e: KeyboardEvent): void { + const config = this.magnifyGlass.config; + const state = this.magnifyGlass.state; + + // Don't handle hotkeys if user is typing in an input field + if (isUserTyping()) { + return; + } + + // Magnifier activation + Logger.debug(`KeyDown: ${e.key} (config: ${config.activationKey}, active: ${state.active})`); + if (e.key.toLowerCase() === config.activationKey && + (!config.altRequired || e.altKey)) { + Logger.debug('Activation key matched!'); + if (config.alwaysActiveMode) { + this.magnifyGlass.toggle(); + } else { + if (!state.active) { + this.magnifyGlass.toggle(); + } + } + } + + // Manual offset adjustment keys (only when magnifier is active) + if (state.active) { + let offsetChanged = false; + const stepSize = e.shiftKey ? config.offsetStep * 5 : config.offsetStep; + + if (e.key === "ArrowUp") { + config.offsetY -= stepSize; + offsetChanged = true; + e.preventDefault(); + } else if (e.key === "ArrowDown") { + config.offsetY += stepSize; + offsetChanged = true; + e.preventDefault(); + } else if (e.key === "ArrowLeft") { + config.offsetX -= stepSize; + offsetChanged = true; + e.preventDefault(); + } else if (e.key === "ArrowRight") { + config.offsetX += stepSize; + offsetChanged = true; + e.preventDefault(); + } else if (e.key.toLowerCase() === config.resetKey.toLowerCase() && + (!config.altRequired || e.altKey)) { + this.magnifyGlass.resetOffsets(); + offsetChanged = true; + e.preventDefault(); + } + + // Toggle Follow Cursor Key + if (e.key.toLowerCase() === config.toggleFollowCursorKey && + (!config.altRequired || e.altKey)) { + config.followCursor = !config.followCursor; + this.magnifyGlass.debugger.log(`Follow Cursor Toggled: ${config.followCursor ? 'ON' : 'OFF'}`); + if (state.active && config.followCursor) { + this.magnifyGlass.ui.positionGlass( + this.magnifyGlass.lastKnownMousePosition.x, + this.magnifyGlass.lastKnownMousePosition.y + ); + } + e.preventDefault(); + } + + if (offsetChanged) { + config.saveOffsets(); + this.magnifyGlass.updateMagnifiedView(); + } + } + } + + /** + * Handle keyup events. + */ + handleKeyUp(e: KeyboardEvent): void { + const config = this.magnifyGlass.config; + const state = this.magnifyGlass.state; + + if (!config.alwaysActiveMode && + (e.key.toLowerCase() === config.activationKey || + (config.altRequired && e.key === "Alt"))) { + + let shouldDeactivate = false; + if (config.altRequired) { + if (e.key === "Alt" || e.key.toLowerCase() === config.activationKey) { + shouldDeactivate = true; + } + } else { + if (e.key.toLowerCase() === config.activationKey) { + shouldDeactivate = true; + } + } + + if (state.active && shouldDeactivate) { + state.active = false; + this.magnifyGlass.ui.hide(); + } + } + } + + /** + * Handle mousemove events. + */ + handleMouseMove(e: MouseEvent): void { + // Always track mouse position + this.magnifyGlass.lastKnownMousePosition.x = e.clientX; + this.magnifyGlass.lastKnownMousePosition.y = e.clientY; + + if (!this.magnifyGlass.state.active || !this.magnifyGlass.litegraphCanvas) return; + + const rect = this.magnifyGlass.litegraphCanvas.getBoundingClientRect(); + const cssMouseXOnCanvas = e.clientX - rect.left; + const cssMouseYOnCanvas = e.clientY - rect.top; + + // Check if the cursor is over the canvas element + if (cssMouseXOnCanvas >= 0 && cssMouseXOnCanvas <= rect.width && + cssMouseYOnCanvas >= 0 && cssMouseYOnCanvas <= rect.height) { + + const canvasElement = this.magnifyGlass.litegraphCanvas; + const scaleX = rect.width > 0 ? canvasElement.width / rect.width : 1; + const scaleY = rect.height > 0 ? canvasElement.height / rect.height : 1; + + const pixelX = cssMouseXOnCanvas * scaleX; + const pixelY = cssMouseYOnCanvas * scaleY; + + this.magnifyGlass.state.x = pixelX; + this.magnifyGlass.state.y = pixelY; + + this.magnifyGlass.ui.positionGlass(e.clientX, e.clientY); + this.magnifyGlass.updateMagnifiedView(); + } + } + + /** + * Update the initial position when magnifier is activated. + */ + updateInitialPosition(): void { + if (!this.magnifyGlass.litegraphCanvas) return; + + const rect = this.magnifyGlass.litegraphCanvas.getBoundingClientRect(); + const clientX = this.magnifyGlass.lastKnownMousePosition.x; + const clientY = this.magnifyGlass.lastKnownMousePosition.y; + + const isOverCanvas = clientX >= rect.left && clientX <= rect.right && + clientY >= rect.top && clientY <= rect.bottom; + + if (this.magnifyGlass.config.alwaysActiveMode || isOverCanvas) { + const cssMouseXOnCanvas = clientX - rect.left; + const cssMouseYOnCanvas = clientY - rect.top; + + const canvasElement = this.magnifyGlass.litegraphCanvas; + const scaleX = rect.width > 0 ? canvasElement.width / rect.width : 1; + const scaleY = rect.height > 0 ? canvasElement.height / rect.height : 1; + + const pixelX = cssMouseXOnCanvas * scaleX; + const pixelY = cssMouseYOnCanvas * scaleY; + + this.magnifyGlass.state.x = pixelX; + this.magnifyGlass.state.y = pixelY; + + 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`; + this.magnifyGlass.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`; + this.magnifyGlass.state.wasActivatedBefore = true; + } else { + this.magnifyGlass.ui.positionGlass(clientX, clientY); + } + + this.magnifyGlass.updateMagnifiedView(); + } + } +} diff --git a/src/magnify-glass/MagnifierState.ts b/src/magnify-glass/MagnifierState.ts new file mode 100644 index 0000000..95ef2e6 --- /dev/null +++ b/src/magnify-glass/MagnifierState.ts @@ -0,0 +1,80 @@ +/** + * ComfyUI MagnifyGlass - MagnifierState (TypeScript) + * + * Manages the state of the magnifying glass. + */ + +/** + * Magnifier state class. + * Tracks activation state, cursor position, and canvas transforms. + */ +export class MagnifierState { + /** Whether the magnifier is currently active */ + active: boolean; + + /** Track if the glass has been activated before */ + wasActivatedBefore: boolean; + + /** Cursor X relative to litegraphCanvas */ + x: number; + + /** Cursor Y relative to litegraphCanvas */ + y: number; + + /** Calculated source area X */ + sourceX: number; + + /** Calculated source area Y */ + sourceY: number; + + /** Calculated source area width */ + sourceWidth: number; + + /** Calculated source area height */ + sourceHeight: number; + + /** Current canvas scale/zoom */ + canvasScale: number; + + /** Canvas translation X */ + canvasOffsetX: number; + + /** Canvas translation Y */ + canvasOffsetY: number; + + /** Flag to manage requestAnimationFrame */ + isRenderScheduled: boolean; + + constructor() { + this.active = false; + this.wasActivatedBefore = false; + this.x = 0; + this.y = 0; + this.sourceX = 0; + this.sourceY = 0; + this.sourceWidth = 0; + this.sourceHeight = 0; + this.canvasScale = 1.0; + this.canvasOffsetX = 0; + this.canvasOffsetY = 0; + this.isRenderScheduled = false; + } + + /** + * Reset state to initial values. + */ + reset(): void { + this.active = false; + this.wasActivatedBefore = false; + this.x = 0; + this.y = 0; + this.sourceX = 0; + this.sourceY = 0; + this.sourceWidth = 0; + this.sourceHeight = 0; + this.canvasScale = 1.0; + this.canvasOffsetX = 0; + this.canvasOffsetY = 0; + this.isRenderScheduled = false; + } +} diff --git a/src/magnify-glass/MagnifyGlass.ts b/src/magnify-glass/MagnifyGlass.ts new file mode 100644 index 0000000..13290e9 --- /dev/null +++ b/src/magnify-glass/MagnifyGlass.ts @@ -0,0 +1,427 @@ +/** + * ComfyUI MagnifyGlass - Main Class (TypeScript) + * + * Orchestrates all components of the magnifying glass. + */ + +import type { ComfyApp, ComfyNode } from '../types/comfyui'; +import { findLiteGraphCanvas, rectsOverlap, Rectangle } from '../shared/utils'; +import { DEFAULT_PADDING, DEFAULT_GLASS_Y_OFFSET } from '../shared/constants'; +import { registerGlassSettings } from '../shared/settings'; +import { ConfigManager } from './ConfigManager'; +import { MagnifierState } from './MagnifierState'; +import { UiManager } from './UiManager'; +import { WebGLRenderer } from './WebGLRenderer'; +import { DebugManager } from './DebugManager'; +import { EventHandler } from './EventHandler'; + +// External ComfyUI globals +declare const app: ComfyApp; +declare const LiteGraph: unknown; + +/** + * Main MagnifyGlass class. + * Orchestrates all components and provides the main API. + */ +export class MagnifyGlass { + config: ConfigManager; + state: MagnifierState; + ui: UiManager; + renderer: WebGLRenderer | null; + debugger: DebugManager; + eventHandler: EventHandler; + + /** The LiteGraph canvas */ + litegraphCanvas: HTMLCanvasElement | null; + + /** Last known mouse position for better initial positioning */ + lastKnownMousePosition: { x: number; y: number }; + + /** Whether we're currently over media (for info panel) */ + isOverMedia: boolean; + + /** Current media element under cursor */ + currentMediaElement: HTMLImageElement | HTMLVideoElement | null; + + constructor() { + this.config = new ConfigManager(); + this.state = new MagnifierState(); + this.ui = new UiManager(this.config, this.state, () => this.toggle()); + this.renderer = null; + this.debugger = new DebugManager(this.config, this.state, this.ui); + this.eventHandler = new EventHandler(this); + + // The LiteGraph canvas + this.litegraphCanvas = null; + + // Last known mouse position for better initial positioning + this.lastKnownMousePosition = { x: 0, y: 0 }; + + // Media tracking + this.isOverMedia = false; + this.currentMediaElement = null; + } + + /** + * Initialize the magnifying glass. + */ + init(): void { + if (typeof LiteGraph === 'undefined' || typeof app === 'undefined' || !app.canvas) { + this.debugger.log("LiteGraph or app not ready, retrying in 100ms."); + setTimeout(() => this.init(), 100); + return; + } + this.debugger.log("LiteGraph and app ready."); + + // Load saved offsets first + this.config.loadSavedOffsets(); + + // Print detailed canvas information + this.debugger.printCanvasInfo(); + + // Create UI elements + this.ui.createElements(); + + // Setup WebGL renderer + this.renderer = new WebGLRenderer(this.config, this.state, this.ui); + if (!this.renderer.isValid()) { + this.ui.cleanup(); + return; + } + + // Find the LiteGraph canvas + this.litegraphCanvas = findLiteGraphCanvas(); + this.debugger.log("LiteGraph canvas found:", this.litegraphCanvas); + if (!this.litegraphCanvas) { + this.debugger.error("Could not find LiteGraph canvas. Magnifier will not work."); + this.ui.cleanup(); + return; + } + + // Attach event handlers + this.eventHandler.attachListeners(); + + // Register settings with ComfyUI + registerGlassSettings(this); + + this.debugger.log(`Initialized (WebGL) with Smart Input Detection. Press ${this.config.altRequired ? 'Alt+' : ''}${this.config.activationKey.toUpperCase()} to activate.`); + } + + /** + * Toggle the magnifying glass active state. + */ + toggle(): void { + const state = this.state; + const config = this.config; + + if (state.active) { + state.active = false; + this.ui.hide(); + } else { + state.active = true; + this.ui.show(); + // Update initial position if needed + if (this.eventHandler) { + this.eventHandler.updateInitialPosition(); + } + } + } + + /** + * Update the magnified view. + */ + updateMagnifiedView(): void { + if (!this.state.active || !this.renderer || !this.litegraphCanvas) { + return; + } + + // Get canvas transformation info + this.updateCanvasTransformation(); + + // Calculate the source region + this.calculateSourceRegion(); + + // Schedule the rendering operation + if (!this.state.isRenderScheduled) { + this.state.isRenderScheduled = true; + requestAnimationFrame(() => { + if (!this.state.active || !this.renderer || !this.litegraphCanvas) { + this.state.isRenderScheduled = false; + return; + } + // Render the magnified view + this.renderer.render(this.litegraphCanvas); + + // Update debug visualization + this.debugger.updateDebugView(); + + // Render HTML overlays + this.renderHtmlOverlays(); + + this.state.isRenderScheduled = false; + }); + } + } + + /** + * Update canvas transformation state. + */ + updateCanvasTransformation(): void { + this.state.canvasScale = 1.0; + this.state.canvasOffsetX = 0; + this.state.canvasOffsetY = 0; + + if (app?.canvas) { + const ds = (app.canvas as any).ds; + if (ds) { + if (typeof ds.scale === 'number') { + this.state.canvasScale = ds.scale; + } + + if (ds.offset) { + this.state.canvasOffsetX = ds.offset[0] || 0; + this.state.canvasOffsetY = ds.offset[1] || 0; + } + } + } + } + + /** + * Calculate the source region for magnification. + */ + calculateSourceRegion(): void { + const cursorPixelX = this.state.x; + const cursorPixelY = this.state.y; + const canvasScale = this.state.canvasScale; + const canvasOffsetX = this.state.canvasOffsetX; + const canvasOffsetY = this.state.canvasOffsetY; + + if (canvasScale === 0) return; + + // Convert cursor canvas pixels to LiteGraph graph coordinates + const cursorGraphX = (cursorPixelX - canvasOffsetX) / canvasScale; + const cursorGraphY = (cursorPixelY - canvasOffsetY) / canvasScale; + + // Apply manual offset + const targetGraphCenterX = cursorGraphX + this.config.offsetX; + const targetGraphCenterY = cursorGraphY + this.config.offsetY; + + // Calculate source dimensions in graph units + const sourceGraphWidth = (this.config.glassSize / this.config.zoomFactor) / canvasScale; + const sourceGraphHeight = (this.config.glassSize / this.config.zoomFactor) / canvasScale; + + // Calculate source top-left corner in graph coordinates + const sourceGraphX = targetGraphCenterX - (sourceGraphWidth / 2); + const sourceGraphY = targetGraphCenterY - (sourceGraphHeight / 2); + + // Convert back to canvas pixel coordinates + this.state.sourceX = (sourceGraphX * canvasScale) + canvasOffsetX; + this.state.sourceY = (sourceGraphY * canvasScale) + canvasOffsetY; + this.state.sourceWidth = sourceGraphWidth * canvasScale; + this.state.sourceHeight = sourceGraphHeight * canvasScale; + } + + /** + * Render HTML overlays for text and media in the magnified view. + */ + renderHtmlOverlays(): void { + const graph = (app as any).graph; + if (!this.state.active || !this.ui.htmlOverlayContainer || !graph || !this.litegraphCanvas) { + if (this.ui.htmlOverlayContainer) this.ui.htmlOverlayContainer.innerHTML = ''; + return; + } + + this.ui.htmlOverlayContainer.innerHTML = ''; + + const magnifyRect: Rectangle = { + x: this.state.sourceX, + y: this.state.sourceY, + width: this.state.sourceWidth, + height: this.state.sourceHeight + }; + + const nodes: ComfyNode[] = graph._nodes; + if (!nodes) return; + + for (const node of nodes) { + const widgets = (node as any).widgets; + if (!widgets) continue; + + for (const widget of widgets) { + let isTextElement = false; + let isVideoElement = false; + let isImageElement = false; + let elementToProcess: HTMLElement | null = null; + + if (widget.element) { + const element = widget.element as HTMLElement; + if (widget.type === "text" || widget.type === "string" || element.tagName === 'TEXTAREA') { + isTextElement = true; + elementToProcess = element; + } else if (element.tagName === 'VIDEO') { + isVideoElement = true; + elementToProcess = element; + } else if (element.tagName === 'IMG') { + isImageElement = true; + elementToProcess = element; + } else { + const potentialVideo = element.querySelector('video'); + if (potentialVideo) { + isVideoElement = true; + elementToProcess = potentialVideo; + } else { + const potentialImage = element.querySelector('img'); + if (potentialImage) { + isImageElement = true; + elementToProcess = potentialImage; + } + } + } + } + + if (elementToProcess && (isTextElement || isVideoElement || isImageElement)) { + const widgetRect = elementToProcess.getBoundingClientRect(); + const canvasRect = this.litegraphCanvas!.getBoundingClientRect(); + + const canvasToViewportScaleX = canvasRect.width > 0 ? (this.litegraphCanvas!.width / canvasRect.width) : 1; + const canvasToViewportScaleY = canvasRect.height > 0 ? (this.litegraphCanvas!.height / canvasRect.height) : 1; + + const widgetCanvasX = (widgetRect.left - canvasRect.left) * canvasToViewportScaleX; + const widgetCanvasY = (widgetRect.top - canvasRect.top) * canvasToViewportScaleY; + const widgetCanvasWidth = widgetRect.width * canvasToViewportScaleX; + const widgetCanvasHeight = widgetRect.height * canvasToViewportScaleY; + + const widgetSourceRect: Rectangle = { + x: widgetCanvasX, + y: widgetCanvasY, + width: widgetCanvasWidth, + height: widgetCanvasHeight + }; + + if (rectsOverlap(magnifyRect, widgetSourceRect)) { + const clonedElement = elementToProcess.cloneNode(true) as HTMLElement; + clonedElement.style.position = 'absolute'; + clonedElement.style.pointerEvents = 'none'; + + if (isTextElement) { + clonedElement.style.backgroundColor = elementToProcess.style.backgroundColor || '#222'; + clonedElement.style.color = elementToProcess.style.color || '#DDD'; + clonedElement.style.border = elementToProcess.style.border || '1px solid #555'; + (clonedElement as HTMLInputElement).disabled = true; + } else if (isVideoElement) { + const video = clonedElement as HTMLVideoElement; + const originalVideo = elementToProcess as HTMLVideoElement; + video.src = originalVideo.src; + video.autoplay = originalVideo.autoplay; + video.loop = originalVideo.loop; + video.preload = originalVideo.preload; + video.crossOrigin = originalVideo.crossOrigin; + video.muted = true; + if (!originalVideo.paused) { + video.play().catch(e => console.warn("Magnify Glass: Cloned video play failed", e)); + } + video.currentTime = originalVideo.currentTime; + } else if (isImageElement) { + const img = clonedElement as HTMLImageElement; + const originalImg = elementToProcess as HTMLImageElement; + img.src = originalImg.src; + img.alt = originalImg.alt; + } + + const relativeX = widgetSourceRect.x - magnifyRect.x; + const relativeY = widgetSourceRect.y - magnifyRect.y; + const magnifiedX = relativeX * this.config.zoomFactor; + const magnifiedY = relativeY * this.config.zoomFactor; + + clonedElement.style.left = `${magnifiedX}px`; + clonedElement.style.top = `${magnifiedY}px`; + clonedElement.style.width = `${widgetSourceRect.width}px`; + clonedElement.style.height = `${widgetSourceRect.height}px`; + clonedElement.style.transformOrigin = 'top left'; + clonedElement.style.transform = `scale(${this.config.zoomFactor})`; + + if (isTextElement && clonedElement.style.fontSize) { + const originalFontSize = parseFloat(window.getComputedStyle(elementToProcess).fontSize); + clonedElement.style.fontSize = `${originalFontSize}px`; + } + + this.ui.htmlOverlayContainer!.appendChild(clonedElement); + } + } + } + } + } + + /** + * Update config from current settings values. + */ + updateConfigFromSettings(): void { + this.config.loadSettings(); + } + + /** + * Apply UI changes based on current config. + */ + applyUiChanges(): void { + this.ui.applyStyles(); + + if (this.renderer?.gl) { + this.renderer.updateViewport(); + } + } + + + + /** + * Reset offsets and panel positions to defaults. + */ + resetOffsets(): void { + this.config.resetOffsets(); + + // Also disable follow cursor + this.config.followCursor = false; + + if (this.state.active) { + this.updateMagnifiedView(); + } + + // 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; + } + + // Reset inspector panel position if exists + const extensions = window.comfyUIMagnifyGlassExtensions; + if (extensions && extensions.length > 0) { + extensions.forEach((extension: any) => { + if (extension?.stateManager) { + extension.stateManager.state.isPanelPinned = false; + extension.stateManager.state.isPanelLocked = false; + extension.stateManager.state.isAutoPinned = false; + extension.stateManager.state.pinnedPosition = { x: 0, y: 0 }; + extension.stateManager.state.lastPinnedPosition = null; + + if (extension.positionManager) { + extension.positionManager.positionPanel(); + } + if (extension.uiManager?.updateControlStates) { + extension.uiManager.updateControlStates(); + } + } + }); + } + + this.debugger.log("Reset: Magnify glass and inspector panel positions restored to defaults"); + } + + /** + * Cleanup all resources. + */ + cleanup(): void { + this.eventHandler.detachListeners(); + this.ui.cleanup(); + } +} diff --git a/src/magnify-glass/UiManager.ts b/src/magnify-glass/UiManager.ts new file mode 100644 index 0000000..e44fae3 --- /dev/null +++ b/src/magnify-glass/UiManager.ts @@ -0,0 +1,420 @@ +/** + * ComfyUI MagnifyGlass - UiManager (TypeScript) + * + * Handles creation and management of DOM elements for the magnifying glass. + */ + +import { Z_INDEX, DEFAULT_PADDING } from '../shared/constants'; +import { Icons } from '../shared/icons'; +import { Logger } from '../shared/logger'; +import type { ConfigManager } from './ConfigManager'; +import type { MagnifierState } from './MagnifierState'; + +/** + * UI Manager class. + * Manages all DOM elements for the magnifying glass. + */ +export class UiManager { + config: ConfigManager; + state: MagnifierState; + glassDiv: HTMLDivElement | null; + glassCanvas: HTMLCanvasElement | null; + debugCanvas: HTMLCanvasElement | null; + debugCtx: CanvasRenderingContext2D | null; + htmlOverlayContainer: HTMLDivElement | null; + onToggle: (() => void) | undefined; + + constructor(config: ConfigManager, state: MagnifierState, onToggle?: () => void) { + this.config = config; + this.state = state; + this.glassDiv = null; + this.glassCanvas = null; + this.debugCanvas = null; + this.debugCtx = null; + this.htmlOverlayContainer = null; + this.onToggle = onToggle; + } + + /** + * Create all DOM elements for the magnifying glass. + */ + createElements(): void { + // Create magnifying glass container + this.glassDiv = document.createElement("div"); + this.glassDiv.id = "comfyui-magnify-glass"; + this.glassDiv.style.cssText = ` + position: absolute; + box-sizing: border-box; + width: ${this.config.glassSize}px; + height: ${this.config.glassSize}px; + border-radius: ${this.config.glassShape === "Circle" ? "50%" : "0px"}; + border: ${this.config.borderEnabled ? `${this.config.borderWidth}px solid ${this.config.borderColor}` : 'none'}; + overflow: hidden; + pointer-events: none; + z-index: ${Z_INDEX.GLASS}; + display: none; + box-shadow: 0 5px 15px rgba(0,0,0,0.3); + background-color: rgba(255,255,255,0.1); + `; + + // Create WebGL canvas for the magnifying glass + this.glassCanvas = document.createElement("canvas"); + this.glassCanvas.width = this.config.glassSize; + this.glassCanvas.height = this.config.glassSize; + this.glassCanvas.id = "comfyui-magnify-canvas"; + this.glassDiv.appendChild(this.glassCanvas); + + // Create HTML overlay container + this.htmlOverlayContainer = document.createElement("div"); + this.htmlOverlayContainer.id = "comfyui-magnify-html-overlay"; + this.htmlOverlayContainer.style.cssText = ` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + overflow: hidden; + `; + this.glassDiv.appendChild(this.htmlOverlayContainer); + + document.body.appendChild(this.glassDiv); + + // Create debug canvas if debug mode is enabled + if (this.config.debugMode) { + this.createDebugCanvas(); + } + + // Inject menu button + this.injectMenuButton(); + } + + /** + * Create the debug canvas overlay. + */ + createDebugCanvas(): void { + this.debugCanvas = document.createElement("canvas"); + this.debugCanvas.id = "comfyui-magnify-debug"; + this.debugCanvas.width = 400; + this.debugCanvas.height = 350; + this.debugCanvas.style.cssText = ` + position: fixed; + top: 10px; + right: 10px; + background: rgba(0,0,0,0.7); + border: 1px solid #fff; + z-index: ${Z_INDEX.DEBUG}; + pointer-events: none; + color: white; + font-family: monospace; + display: none; + `; + document.body.appendChild(this.debugCanvas); + this.debugCtx = this.debugCanvas.getContext('2d'); + } + + /** + * Show the magnifying glass. + */ + show(): void { + if (this.glassDiv) { + this.glassDiv.style.display = 'block'; + // We do NOT modify opacity here, allowing it to be controlled independently + } + if (this.config.debugMode && this.debugCanvas) { + this.debugCanvas.style.display = "block"; + } + } + + /** + * Set the visual visibility of the glass preview (opacity). + * This allows the tool to remain "Active" (tracking mouse) but invisible, + * so that the Inspector Panel can be used in "Inspector Only" mode. + */ + setPreviewVisibility(visible: boolean): void { + if (this.glassDiv) { + this.glassDiv.style.opacity = visible ? '1' : '0'; + // Disable pointer events when hidden to be safe, though usually they are none anyway + // this.glassDiv.style.pointerEvents = visible ? 'none' : 'none'; + } + } + + /** + * Hide the magnifying glass. + */ + hide(): void { + if (this.glassDiv) { + this.glassDiv.style.display = 'none'; + } + if (this.config.debugMode && this.debugCanvas) { + this.debugCanvas.style.display = "none"; + } + // Clear HTML overlays when hiding + if (this.htmlOverlayContainer) { + this.htmlOverlayContainer.innerHTML = ''; + } + } + + /** + * Position the glass relative to cursor. + * @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; + + switch (this.config.glassPosition) { + case "Top": + newLeft = clientX - (glassSize / 2); + newTop = clientY - glassSize - offsetAmount; + break; + case "Bottom": + newLeft = clientX - (glassSize / 2); + newTop = clientY + offsetAmount; + break; + case "Left": + newLeft = clientX - glassSize - offsetAmount; + newTop = clientY - (glassSize / 2); + break; + case "Right": + newLeft = clientX + offsetAmount; + newTop = clientY - (glassSize / 2); + break; + case "Top-Left": + newLeft = clientX - glassSize - offsetAmount; + newTop = clientY - glassSize - offsetAmount; + break; + case "Top-Right": + newLeft = clientX + offsetAmount; + newTop = clientY - glassSize - offsetAmount; + break; + case "Bottom-Left": + newLeft = clientX - glassSize - offsetAmount; + newTop = clientY + offsetAmount; + break; + case "Bottom-Right": + newLeft = clientX + offsetAmount; + newTop = clientY + offsetAmount; + break; + default: + newLeft = clientX - (glassSize / 2); + newTop = clientY + offsetAmount; + break; + } + + this.glassDiv.style.left = `${newLeft}px`; + this.glassDiv.style.top = `${newTop}px`; + + this.adjustForBoundaries(clientX, clientY); + } + + /** + * Adjust glass position to stay within viewport boundaries. + * @param clientX - Client X coordinate + * @param clientY - Client Y coordinate + */ + adjustForBoundaries(clientX: number, clientY: number): void { + if (!this.glassDiv) return; + + const glassRect = this.glassDiv.getBoundingClientRect(); + + // Check right boundary + if (glassRect.right > window.innerWidth) { + this.glassDiv.style.left = `${clientX - glassRect.width - DEFAULT_PADDING}px`; + } + + // Check left boundary + const currentRectLeft = this.glassDiv.getBoundingClientRect(); + if (currentRectLeft.left < 0) { + this.glassDiv.style.left = "10px"; + } + + // Check bottom boundary + if (glassRect.bottom > window.innerHeight) { + this.glassDiv.style.top = `${clientY - glassRect.height - DEFAULT_PADDING}px`; + } + + // Check top boundary + const currentRectTop = this.glassDiv.getBoundingClientRect(); + if (currentRectTop.top < 0) { + this.glassDiv.style.top = "10px"; + } + } + + /** + * Apply current config to UI elements. + */ + applyStyles(): void { + if (this.glassDiv) { + this.glassDiv.style.width = `${this.config.glassSize}px`; + this.glassDiv.style.height = `${this.config.glassSize}px`; + this.glassDiv.style.border = this.config.borderEnabled + ? `${this.config.borderWidth}px solid ${this.config.borderColor}` + : 'none'; + + // Reset clip-path and set default border-radius + this.glassDiv.style.clipPath = 'none'; + this.glassDiv.style.borderRadius = '0px'; + + switch (this.config.glassShape) { + case "Circle": + this.glassDiv.style.borderRadius = "50%"; + break; + case "Square": + // borderRadius is already '0px' + break; + case "Rounded Square": + this.glassDiv.style.borderRadius = "20%"; + break; + default: + this.glassDiv.style.borderRadius = "50%"; + break; + } + } + + if (this.glassCanvas) { + this.glassCanvas.width = this.config.glassSize; + this.glassCanvas.height = this.config.glassSize; + } + + // Handle debug canvas show/hide + if (this.config.debugMode) { + if (!this.debugCanvas) this.createDebugCanvas(); + if (this.state.active && this.debugCanvas) this.debugCanvas.style.display = "block"; + } else { + if (this.debugCanvas) this.debugCanvas.style.display = "none"; + } + } + + /** + * Cleanup DOM elements. + */ + cleanup(): void { + if (this.glassDiv) this.glassDiv.remove(); + if (this.debugCanvas) this.debugCanvas.remove(); + // Remove menu button if needed (though hard to track references here without saving it) + const btn = document.querySelector('.magnify-toggle-btn'); + if (btn) btn.remove(); + } + + /** + * Inject a quick toggle button into the ComfyUI menu. + */ + /** + * Inject a quick toggle button into the ComfyUI menu. + */ + injectMenuButton(): void { + // Stop after 30 seconds + const stopTime = Date.now() + 30000; + + // Define check function + const attemptInjection = () => { + const buttons = Array.from(document.querySelectorAll('button')); + + // Try to find an anchor button (Minimap or Links) + // Priority 1: Minimap + let anchorBtn = buttons.find(b => { + const title = (b.title || '').toLowerCase(); + const aria = (b.getAttribute('aria-label') || '').toLowerCase(); + const text = (b.textContent || '').toLowerCase(); + return (title.includes('map') && !title.includes('open')) || + (aria.includes('map') && !aria.includes('open')) || + (text.includes('map')); + }); + + // Priority 2: Link Visibility + if (!anchorBtn) { + anchorBtn = buttons.find(b => { + const title = (b.title || '').toLowerCase(); + const aria = (b.getAttribute('aria-label') || '').toLowerCase(); + return title.includes('link') || aria.includes('link'); + }); + } + + if (anchorBtn && anchorBtn.parentElement) { + // Determine success BEFORE creating elements to allow early return if duplicate + if (anchorBtn.parentElement.querySelector('.magnify-toggle-btn')) return true; + + Logger.debug('Found menu anchor:', anchorBtn.title || anchorBtn.getAttribute('aria-label') || 'Unknown Button'); + + const btn = document.createElement('button'); + // Copy all classes to match theme/library (e.g. p-button, comfy-btn, etc) + btn.className = anchorBtn.className + ' magnify-toggle-btn'; + + // Copy key styles that might define shape/size + const computed = window.getComputedStyle(anchorBtn); + btn.style.height = computed.height; + btn.style.minHeight = computed.minHeight; + // btn.style.width = computed.width; // Don't copy width, let it adapt + + btn.title = "Toggle Magnify Glass"; + btn.innerHTML = Icons.magnifyGlass; + + // Ensure specific display props + btn.style.display = "inline-flex"; + btn.style.alignItems = "center"; + btn.style.justifyContent = "center"; + btn.style.padding = "0 8px"; + btn.style.cursor = "pointer"; + btn.style.border = anchorBtn.style.border || computed.border; + btn.style.borderRadius = anchorBtn.style.borderRadius || computed.borderRadius; + // Use cssText if available for background/color specific overrides + if (!btn.style.background) btn.style.background = computed.background; + if (!btn.style.color) btn.style.color = computed.color; + + // Add active state tracking + btn.addEventListener('click', () => { + if (this.onToggle) { + this.onToggle(); + // Try to apply common active classes found in modern UI frameworks + btn.classList.toggle('active'); + btn.classList.toggle('p-highlight'); // PrimeVue + btn.classList.toggle('selected'); + } + }); + + // Insert logic: + const isMinimap = (anchorBtn.title || '').toLowerCase().includes('map') || + (anchorBtn.getAttribute('aria-label') || '').toLowerCase().includes('map'); + + if (isMinimap) { + // Insert AFTER Minimap + if (anchorBtn.nextSibling) { + anchorBtn.parentElement.insertBefore(btn, anchorBtn.nextSibling); + } else { + anchorBtn.parentElement.appendChild(btn); + } + } else { + // It's likely links, Insert BEFORE + anchorBtn.parentElement.insertBefore(btn, anchorBtn); + } + + Logger.debug('Menu toggle button injected successfully'); + return true; + } + return false; + }; + + // Run immediately first + if (attemptInjection()) return; + + // Then poll rapidly + const checkForMenu = setInterval(() => { + if (Date.now() > stopTime) { + console.warn("[MagnifyGlass] Menu injection timed out. Found buttons:", + Array.from(document.querySelectorAll('button')).map(b => b.title || b.getAttribute('aria-label') || b.textContent || b.className).slice(0, 5)); + clearInterval(checkForMenu); + return; + } + + if (attemptInjection()) { + clearInterval(checkForMenu); + } + }, 100); + } +} diff --git a/src/magnify-glass/WebGLRenderer.ts b/src/magnify-glass/WebGLRenderer.ts new file mode 100644 index 0000000..25fcc5c --- /dev/null +++ b/src/magnify-glass/WebGLRenderer.ts @@ -0,0 +1,437 @@ +/** + * ComfyUI MagnifyGlass - WebGLRenderer (TypeScript) + * + * Handles WebGL-based rendering of the magnified view. + */ + +import type { ConfigManager } from './ConfigManager'; +import type { MagnifierState } from './MagnifierState'; +import type { UiManager } from './UiManager'; +import { Logger } from '../shared/logger'; + +interface UniformLocations { + sourceTexture: WebGLUniformLocation | null; + textureOffset: WebGLUniformLocation | null; + textureRepeat: WebGLUniformLocation | null; + glassSize: WebGLUniformLocation | null; +} + +interface AttributeLocations { + position: number; + texCoord: number; +} + +/** + * WebGL Renderer class. + * Provides efficient WebGL-based magnification rendering. + */ +export class WebGLRenderer { + config: ConfigManager; + state: MagnifierState; + ui: UiManager; + + gl: WebGLRenderingContext | null; + program: WebGLProgram | null; + texture: WebGLTexture | null; + positionBuffer: WebGLBuffer | null; + texCoordBuffer: WebGLBuffer | null; + uniformLocations: UniformLocations | null; + attributeLocations: AttributeLocations | null; + currentFilteringMode: number | null; + + // Dirty flag optimization + lastCanvasVersion: number; + lastSourceX: number; + lastSourceY: number; + lastSourceWidth: number; + lastSourceHeight: number; + textureUploadSkipCount: number; + + vertexShaderSource: string; + fragmentShaderSource: string; + + constructor(config: ConfigManager, state: MagnifierState, ui: UiManager) { + this.config = config; + this.state = state; + this.ui = ui; + + this.gl = null; + this.program = null; + this.texture = null; + this.positionBuffer = null; + this.texCoordBuffer = null; + this.uniformLocations = null; + this.attributeLocations = null; + this.currentFilteringMode = null; + + // Initialize dirty flag tracking + this.lastCanvasVersion = -1; + this.lastSourceX = -1; + this.lastSourceY = -1; + this.lastSourceWidth = -1; + this.lastSourceHeight = -1; + this.textureUploadSkipCount = 0; + + this.vertexShaderSource = ` + attribute vec2 a_position; + attribute vec2 a_texCoord; + varying vec2 v_texCoord; + void main() { + gl_Position = vec4(a_position, 0.0, 1.0); + v_texCoord = a_texCoord; + } + `; + + this.fragmentShaderSource = ` + precision mediump float; + varying vec2 v_texCoord; + uniform sampler2D u_sourceTexture; + uniform vec2 u_textureOffset; + uniform vec2 u_textureRepeat; + uniform float u_glassSize; + + void main() { + vec2 sampleCoord = u_textureOffset + v_texCoord * u_textureRepeat; + vec4 color = texture2D(u_sourceTexture, sampleCoord); + gl_FragColor = color; + } + `; + + this.initialize(); + } + + /** + * Initialize WebGL context and resources. + */ + initialize(): void { + // Get WebGL context + if (!this.ui.glassCanvas) return; + + this.gl = this.ui.glassCanvas.getContext("webgl", { preserveDrawingBuffer: true }); + if (!this.gl) { + // Try webgl2 as fallback + this.gl = this.ui.glassCanvas.getContext("webgl2", { preserveDrawingBuffer: true }) as WebGLRenderingContext | null; + } + + if (!this.gl) { + const isFirefox = navigator.userAgent.toLowerCase().includes('firefox'); + const browserInstructions = isFirefox + ? "Firefox: Go to about:config, search for 'webgl.disabled' and set it to 'false', then restart Firefox." + : "Check your browser settings to ensure WebGL is enabled."; + + console.error("ComfyUI Magnifying Glass ERROR: WebGL not supported or context creation failed."); + console.error(`To fix: ${browserInstructions}`); + + // Show user notification + this.showWebGLError(browserInstructions); + return; + } + + // Create shader program + this.program = this.createShaderProgram(this.gl, this.vertexShaderSource, this.fragmentShaderSource); + if (!this.program) return; + + // Create position buffer + this.positionBuffer = this.gl.createBuffer(); + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer); + const positions = [-1, -1, 1, -1, -1, 1, 1, 1]; + this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(positions), this.gl.STATIC_DRAW); + + // Create texture coordinate buffer + this.texCoordBuffer = this.gl.createBuffer(); + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer); + const texCoords = [0, 1, 1, 1, 0, 0, 1, 0]; // Flipped Y for texCoords + this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(texCoords), this.gl.STATIC_DRAW); + + // Create texture + this.texture = this.gl.createTexture(); + this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture); + this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL, false); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR); + this.updateTextureFiltering(this.config.textureFiltering); + + // Get uniform and attribute locations + this.uniformLocations = { + sourceTexture: this.gl.getUniformLocation(this.program, "u_sourceTexture"), + textureOffset: this.gl.getUniformLocation(this.program, "u_textureOffset"), + textureRepeat: this.gl.getUniformLocation(this.program, "u_textureRepeat"), + glassSize: this.gl.getUniformLocation(this.program, "u_glassSize"), + }; + + this.attributeLocations = { + position: this.gl.getAttribLocation(this.program, "a_position"), + texCoord: this.gl.getAttribLocation(this.program, "a_texCoord"), + }; + } + + /** + * Update texture filtering mode. + * @param filteringModeString - "Linear" or "Nearest" + */ + updateTextureFiltering(filteringModeString: string): void { + if (!this.gl || !this.texture) return; + + let glFilterMode: number; + if (filteringModeString === "Nearest") { + glFilterMode = this.gl.NEAREST; + } else { + glFilterMode = this.gl.LINEAR; + } + + if (this.currentFilteringMode === glFilterMode) return; + + this.currentFilteringMode = glFilterMode; + + this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, glFilterMode); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, glFilterMode); + this.gl.bindTexture(this.gl.TEXTURE_2D, null); + } + + /** + * Check if renderer is valid and ready. + * @returns Boolean indicating if WebGL context and program are ready + */ + isValid(): boolean { + return this.gl !== null && this.program !== null; + } + + /** + * Create a WebGL shader program. + * @param gl - WebGL context + * @param vsSource - Vertex shader source + * @param fsSource - Fragment shader source + * @returns WebGL program or null on failure + */ + createShaderProgram(gl: WebGLRenderingContext, vsSource: string, fsSource: string): WebGLProgram | null { + const vertexShader = gl.createShader(gl.VERTEX_SHADER); + if (!vertexShader) return null; + + gl.shaderSource(vertexShader, vsSource); + gl.compileShader(vertexShader); + if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { + console.error('ComfyUI Magnifying Glass ERROR: Vertex shader compilation error:', gl.getShaderInfoLog(vertexShader)); + gl.deleteShader(vertexShader); + return null; + } + + const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); + if (!fragmentShader) { + gl.deleteShader(vertexShader); + return null; + } + + gl.shaderSource(fragmentShader, fsSource); + gl.compileShader(fragmentShader); + if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { + console.error('ComfyUI Magnifying Glass ERROR: Fragment shader compilation error:', gl.getShaderInfoLog(fragmentShader)); + gl.deleteShader(vertexShader); + gl.deleteShader(fragmentShader); + return null; + } + + const program = gl.createProgram(); + if (!program) { + gl.deleteShader(vertexShader); + gl.deleteShader(fragmentShader); + return null; + } + + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + console.error('ComfyUI Magnifying Glass ERROR: Shader program linking error:', gl.getProgramInfoLog(program)); + gl.deleteProgram(program); + gl.deleteShader(vertexShader); + gl.deleteShader(fragmentShader); + return null; + } + + return program; + } + + /** + * Render the magnified view. + * @param sourceCanvas - The source canvas to sample from + */ + render(sourceCanvas: HTMLCanvasElement): void { + if (!this.gl || !this.program || !this.texture || !this.uniformLocations || !this.attributeLocations) return; + + // Check if we need to update the texture + // We track: canvas version (via LiteGraph's change counter), source coordinates, and source dimensions + const app = (window as any).app; + const currentCanvasVersion = app?.graph?.change_counter ?? -1; + + const sourceChanged = + this.lastSourceX !== this.state.sourceX || + this.lastSourceY !== this.state.sourceY || + this.lastSourceWidth !== this.state.sourceWidth || + this.lastSourceHeight !== this.state.sourceHeight; + + const canvasChanged = currentCanvasVersion !== this.lastCanvasVersion; + + // Only upload texture if something changed + const needsTextureUpdate = canvasChanged || sourceChanged || this.lastCanvasVersion === -1; + + // Bind texture + this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture); + + if (needsTextureUpdate) { + try { + this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, sourceCanvas); + + // Update tracking variables + this.lastCanvasVersion = currentCanvasVersion; + this.lastSourceX = this.state.sourceX; + this.lastSourceY = this.state.sourceY; + this.lastSourceWidth = this.state.sourceWidth; + this.lastSourceHeight = this.state.sourceHeight; + + // Reset skip counter and log if debug mode + if (this.config.debugMode && this.textureUploadSkipCount > 0) { + Logger.debug(`Texture uploaded after skipping ${this.textureUploadSkipCount} frames`); + } + this.textureUploadSkipCount = 0; + } catch (e) { + console.error("ComfyUI Magnifying Glass ERROR: Error in texImage2D:", e); + return; + } + } else { + // Skip texture upload - just increment counter + this.textureUploadSkipCount++; + + // Periodic debug logging + if (this.config.debugMode && this.textureUploadSkipCount % 60 === 0) { + Logger.debug(`Texture upload skipped (${this.textureUploadSkipCount} frames cached)`); + } + } + + // Calculate normalized texture coordinates (UV space: 0-1) + const uvX = this.state.sourceX / sourceCanvas.width; + const uvY = this.state.sourceY / sourceCanvas.height; + const uvWidth = this.state.sourceWidth / sourceCanvas.width; + const uvHeight = this.state.sourceHeight / sourceCanvas.height; + + // Set up rendering + this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height); + this.gl.clearColor(0.0, 0.0, 0.0, 0.0); + this.gl.clear(this.gl.COLOR_BUFFER_BIT); + + this.gl.useProgram(this.program); + + // Set uniforms + this.gl.activeTexture(this.gl.TEXTURE0); + this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture); + this.gl.uniform1i(this.uniformLocations.sourceTexture, 0); + this.gl.uniform2f(this.uniformLocations.textureOffset, uvX, uvY); + this.gl.uniform2f(this.uniformLocations.textureRepeat, uvWidth, uvHeight); + this.gl.uniform1f(this.uniformLocations.glassSize, this.config.glassSize); + + // Set attributes + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer); + this.gl.vertexAttribPointer(this.attributeLocations.position, 2, this.gl.FLOAT, false, 0, 0); + this.gl.enableVertexAttribArray(this.attributeLocations.position); + + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer); + this.gl.vertexAttribPointer(this.attributeLocations.texCoord, 2, this.gl.FLOAT, false, 0, 0); + this.gl.enableVertexAttribArray(this.attributeLocations.texCoord); + + // Draw + this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4); + } + + /** + * Check for WebGL errors. + * @param label - Debug label + * @returns True if error occurred + */ + checkWebGLError(label: string): boolean { + if (!this.gl) return false; + + const err = this.gl.getError(); + if (err !== this.gl.NO_ERROR) { + console.error(`ComfyUI Magnifying Glass ERROR: WebGL Error (${label}):`, err); + return true; + } + return false; + } + + /** + * Update viewport when glass size changes. + */ + updateViewport(): void { + if (this.gl) { + this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height); + } + } + + /** + * Show WebGL error notification to user. + * @param instructions - Browser-specific instructions to fix + */ + showWebGLError(instructions: string): void { + // Create a toast notification + const toast = document.createElement('div'); + toast.id = 'magnifyglass-webgl-error'; + toast.style.cssText = ` + position: fixed; + bottom: 20px; + right: 20px; + max-width: 400px; + padding: 16px 20px; + background: linear-gradient(135deg, #ff4444 0%, #cc3333 100%); + color: white; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); + font-family: system-ui, -apple-system, sans-serif; + font-size: 14px; + line-height: 1.5; + z-index: 100000; + animation: slideIn 0.3s ease-out; + `; + + toast.innerHTML = ` +
+ ⚠️ +
+ MagnifyGlass: WebGL Disabled + ${instructions} + +
+
+ `; + + // Add animation keyframes + if (!document.getElementById('magnifyglass-toast-style')) { + const style = document.createElement('style'); + style.id = 'magnifyglass-toast-style'; + style.textContent = ` + @keyframes slideIn { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } + } + `; + document.head.appendChild(style); + } + + document.body.appendChild(toast); + + // Auto-dismiss after 15 seconds + setTimeout(() => { + if (toast.parentElement) { + toast.style.animation = 'slideIn 0.3s ease-out reverse'; + setTimeout(() => toast.remove(), 300); + } + }, 15000); + } +} diff --git a/src/magnify-glass/index.ts b/src/magnify-glass/index.ts new file mode 100644 index 0000000..f36e1e0 --- /dev/null +++ b/src/magnify-glass/index.ts @@ -0,0 +1,13 @@ +/** + * ComfyUI MagnifyGlass - Module Index (TypeScript) + * + * Re-exports all magnify-glass components. + */ + +export { MagnifyGlass } from './MagnifyGlass'; +export { MagnifierState } from './MagnifierState'; +export { ConfigManager } from './ConfigManager'; +export { UiManager } from './UiManager'; +export { WebGLRenderer } from './WebGLRenderer'; +export { DebugManager } from './DebugManager'; +export { EventHandler } from './EventHandler'; diff --git a/src/magnify_glass.ts b/src/magnify_glass.ts new file mode 100644 index 0000000..936acd9 --- /dev/null +++ b/src/magnify_glass.ts @@ -0,0 +1,32 @@ +/** + * ComfyUI Magnify Glass - Main Entry Point + * + * Initializes the Magnify Glass extension. + */ + +import { MagnifyGlass } from './magnify-glass'; +import { Logger } from './shared/logger'; +import { initSidebar } from './sidebar'; +// @ts-ignore +import { app } from "/scripts/app.js"; + +// Global declaration handled in comfyui.d.ts + +// We rely on the global 'app' object provided by ComfyUI +// Register extension +app.registerExtension({ + name: "comfyui.magnify.glass", + init() { + // Initialize the magnify glass + const magnifyGlass = new MagnifyGlass(); + magnifyGlass.init(); + + // Expose to window for other extensions (like info panel) to access + window.comfyUIMagnifyGlass = magnifyGlass; + + // Initialize sidebar + initSidebar(); + + Logger.info('Magnify Glass extension initialized'); + } +}); diff --git a/src/magnify_info_panel.ts b/src/magnify_info_panel.ts new file mode 100644 index 0000000..93da91e --- /dev/null +++ b/src/magnify_info_panel.ts @@ -0,0 +1,53 @@ +/** + * ComfyUI Magnify Info Panel - Main Entry Point + * + * Initializes the Info Panel extension. + */ + +import { InfoPanel } from './info-panel'; +import { Logger } from './shared/logger'; +// @ts-ignore +import { app } from "/scripts/app.js"; + +// We rely on the global 'app' object provided by ComfyUI +// No need to import it as it's global, but TypeScript needs to know about it. +// We handled this in types/comfyui.d.ts + +app.registerExtension({ + name: "comfyui.magnify.info.panel", + init() { + // Wait for magnify glass to be available + const checkDependencies = () => { + if (window.comfyUIMagnifyGlass) { + initializeInfoPanel(); + } else { + setTimeout(checkDependencies, 100); + } + }; + + checkDependencies(); + } +}); + +function initializeInfoPanel() { + const magnifyGlass = window.comfyUIMagnifyGlass; + if (!magnifyGlass) { + Logger.error('Failed to find MagnifyGlass instance.'); + return; + } + + try { + // Initialize the info panel + const infoPanel = new InfoPanel(magnifyGlass); + + // Expose orchestrator globally if needed + window.infoPanelManager = infoPanel as any; + + // CSS is loaded by UIManager.injectStyles() - no need to load separately + + Logger.info('Info Panel extension initialized'); + } catch (e) { + Logger.error('Error during initialization:', e); + } +} + diff --git a/src/shared/constants.ts b/src/shared/constants.ts new file mode 100644 index 0000000..d127abd --- /dev/null +++ b/src/shared/constants.ts @@ -0,0 +1,86 @@ +/** + * ComfyUI MagnifyGlass - Shared Constants + * + * Configuration constants shared between magnify glass and info panel modules. + */ + +// Glass positioning options +export const GLASS_POSITIONS = [ + "Top", + "Bottom", + "Left", + "Right", + "Top-Left", + "Top-Right", + "Bottom-Left", + "Bottom-Right" +] as const; + +export type GlassPosition = typeof GLASS_POSITIONS[number]; + +// Glass shape options +export const GLASS_SHAPES = [ + "Circle", + "Square", + "Rounded Square" +] as const; + +export type GlassShape = typeof GLASS_SHAPES[number]; + +// Texture filtering modes +export const TEXTURE_FILTERS = [ + "Linear", + "Nearest" +] as const; + +export type TextureFilter = typeof TEXTURE_FILTERS[number]; + +// Info panel positions relative to glass +export const PANEL_POSITIONS = [ + "Top", + "Bottom", + "Left", + "Right" +] as const; + +export type PanelPosition = typeof PANEL_POSITIONS[number]; + +// Available keys for settings +export const ACTIVATION_KEYS = ["x", "z", "m", "q", "v", "c"] as const; +export const RESET_KEYS = ["r", "o", "p", "k", "l"] as const; +export const TOGGLE_FOLLOW_KEYS = ["f", "g", "h", "j", "k"] as const; + +// UI Constants +export const DEFAULT_PADDING = 4; +export const DEFAULT_GLASS_Y_OFFSET = 110; // Starting Y position offset (below top bar with 3px gap) +export const DEFAULT_GLASS_SIZE = 300; +export const DEFAULT_ZOOM_FACTOR = 300; // 3x as percentage +export const DEFAULT_BORDER_WIDTH = 1; +export const DEFAULT_BORDER_COLOR = "#6b7280"; +export const DEFAULT_OFFSET_STEP = 5; + +// Panel Constants +export const DEFAULT_PANEL_WIDTH = 300; +export const DEFAULT_PANEL_MAX_HEIGHT = 300; +export const DEFAULT_PANEL_OPACITY = 100; + +// Z-Index hierarchy +export const Z_INDEX = { + GLASS: 98999, + DEBUG: 99000, + PANEL: 99999, + CONTROLS: 99998 +} as const; + +// Animation durations (ms) +export const ANIMATION = { + PANEL_TRANSITION: 300, + AUTO_COLLAPSE_DELAY: 1500, + THEME_DETECTION_INTERVAL: 2000 +} as const; + +// LocalStorage keys +export const STORAGE_KEYS = { + OFFSET_X: 'comfyui_magnify_offset_x', + OFFSET_Y: 'comfyui_magnify_offset_y' +} as const; diff --git a/src/shared/icons.ts b/src/shared/icons.ts new file mode 100644 index 0000000..b639b81 --- /dev/null +++ b/src/shared/icons.ts @@ -0,0 +1,89 @@ +/** + * ComfyUI MagnifyGlass - SVG Icons + * + * Inline SVG icons to replace emoji icons for a more professional look. + * All icons are designed to be 16x16 or scalable. + */ + +export const Icons = { + // Magnify glass icon - used for header and glass toggle + magnifyGlass: ` + + + `, + + // Unlock icon - panel is floating/following cursor + unlock: ` + + + `, + + // Lock icon - panel is pinned + lock: ` + + + `, + + // Pin icon - lock panel position + pin: ` + + + `, + + // Eye icon - toggle visibility + eye: ` + + + `, + + // Camera/image icon - media section + camera: ` + + + `, + + // Target/crosshair icon - node section + target: ` + + + + `, + + // Location/map pin icon - empty state + mapPin: ` + + + `, + + // Chevron right - expand icon + chevronRight: ` + + `, + + // Minus icon - minimize button + minus: ` + + `, + + // Inspector/info icon + info: ` + + + + `, + + // Box/node icon + box: ` + + + + ` +}; + +/** + * Get an icon wrapped in a span for proper sizing + */ +export function getIcon(name: keyof typeof Icons, className: string = ''): string { + const icon = Icons[name] || ''; + return `${icon}`; +} diff --git a/src/shared/index.ts b/src/shared/index.ts new file mode 100644 index 0000000..5c2fdd2 --- /dev/null +++ b/src/shared/index.ts @@ -0,0 +1,3 @@ +export * from './constants'; +export * from './utils'; +export * from './settings'; diff --git a/src/shared/logger.ts b/src/shared/logger.ts new file mode 100644 index 0000000..df9ae93 --- /dev/null +++ b/src/shared/logger.ts @@ -0,0 +1,87 @@ +/** + * ComfyUI MagnifyGlass - Logger Utility + * + * Conditional debug logging with environment awareness. + * Debug logs are only shown when: + * - localStorage.getItem('magnifyGlass.debug') === 'true' + * - OR window.magnifyGlassDebug === true + */ + +const PREFIX = '[MagnifyGlass]'; + +/** + * Check if debug mode is enabled. + */ +function isDebugEnabled(): boolean { + try { + return ( + localStorage.getItem('magnifyGlass.debug') === 'true' || + (window as unknown as { magnifyGlassDebug?: boolean }).magnifyGlassDebug === true + ); + } catch { + return false; + } +} + +/** + * Logger utility with conditional debug output. + */ +export const Logger = { + /** + * Debug log - only shown when debug mode is enabled. + * Use for verbose development logging. + */ + debug: (...args: unknown[]): void => { + if (isDebugEnabled()) { + console.log(PREFIX, ...args); + } + }, + + /** + * Info log - always shown. + * Use for important initialization messages. + */ + info: (...args: unknown[]): void => { + console.log(PREFIX, ...args); + }, + + /** + * Warning log - always shown. + * Use for non-critical issues. + */ + warn: (...args: unknown[]): void => { + console.warn(PREFIX, ...args); + }, + + /** + * Error log - always shown. + * Use for critical errors. + */ + error: (...args: unknown[]): void => { + console.error(PREFIX, ...args); + } +}; + +/** + * Enable debug mode programmatically. + */ +export function enableDebug(): void { + try { + localStorage.setItem('magnifyGlass.debug', 'true'); + console.log(PREFIX, 'Debug mode enabled'); + } catch { + (window as unknown as { magnifyGlassDebug?: boolean }).magnifyGlassDebug = true; + } +} + +/** + * Disable debug mode. + */ +export function disableDebug(): void { + try { + localStorage.removeItem('magnifyGlass.debug'); + console.log(PREFIX, 'Debug mode disabled'); + } catch { + (window as unknown as { magnifyGlassDebug?: boolean }).magnifyGlassDebug = false; + } +} diff --git a/src/shared/settings.ts b/src/shared/settings.ts new file mode 100644 index 0000000..a96d843 --- /dev/null +++ b/src/shared/settings.ts @@ -0,0 +1,162 @@ +/** + * ComfyUI MagnifyGlass - Settings Configuration + * + * Centralized settings definitions and registration functions. + * + * This file re-exports from the modular settings directory for backwards compatibility. + */ + +// Re-export everything from the settings module +export type { GlassSettings, PanelSettings, AllSettings } from './settings/types'; +export { DEFAULT_GLASS_SETTINGS, DEFAULT_PANEL_SETTINGS } from './settings/defaults'; +export { registerGlassSettings } from './settings/glassSettings'; +export { registerPanelSettings } from './settings/panelSettings'; + +// Legacy getSettingConfig function for backwards compatibility +// @ts-ignore +import { app } from "/scripts/app.js"; +import { PANEL_POSITIONS } from './constants'; +import { DEFAULT_PANEL_SETTINGS } from './settings/defaults'; + +/** + * Get a setting configuration object for ComfyUI settings registration. + * This function generates the setting config based on the key and default value. + * + * @param key - The setting key + * @param defaultValue - The default value for the setting + * @returns The setting configuration or null if not found + */ +export function getSettingConfig(key: string, defaultValue: any): any { + const configs: Record = { + "🔍MagnifyGlass.InfoPanelEnabled": { + id: key, + name: "📋 Magnify Glass: Info Panel", + type: "combo", + options: [ + { value: true, text: "Enabled" }, + { value: false, text: "Disabled" } + ], + defaultValue, + tooltip: "Enable or disable the professional information panel." + }, + "🔍MagnifyGlass.InfoPanelPosition": { + id: key, + name: "📋 Info Panel: Position", + type: "combo", + options: PANEL_POSITIONS.map(pos => ({ value: pos, text: pos })), + defaultValue, + tooltip: "Position of the info panel relative to the magnify glass." + }, + "🔍MagnifyGlass.InfoPanelWidth": { + id: key, + name: "📋 Info Panel: Width (px)", + type: "slider", + defaultValue, + min: 200, + max: 600, + step: 20, + tooltip: "Width of the info panel in pixels." + }, + "🔍MagnifyGlass.InfoPanelMaxHeight": { + id: key, + name: "📋 Info Panel: Max Height (px)", + type: "slider", + defaultValue, + min: 200, + max: 1500, + step: 50, + tooltip: "Maximum height of the info panel." + }, + "🔍MagnifyGlass.InfoPanelOpacity": { + id: key, + name: "📋 Info Panel: Opacity (%)", + type: "slider", + defaultValue, + min: 10, + max: 100, + step: 5, + tooltip: "Opacity of the info panel background." + }, + "🔍MagnifyGlass.InfoPanelAnimations": { + id: key, + name: "📋 Info Panel: Animations", + type: "combo", + options: [ + { value: true, text: "Enabled" }, + { value: false, text: "Disabled" } + ], + defaultValue, + tooltip: "Enable smooth animations for the info panel." + }, + "🔍MagnifyGlass.ShowInspectorTab": { + id: key, + name: "📋 Info Panel: Show Inspector", + type: "combo", + options: [ + { value: true, text: "Yes" }, + { value: false, text: "No" } + ], + defaultValue, + tooltip: "Show the inspector tab with cursor and canvas information." + }, + "🔍MagnifyGlass.ToggleHotkey": { + id: key, + name: "📋 Info Panel: Toggle Hotkey", + type: "combo", + options: ["i", "j", "k", "l", "n", "m"].map(k => ({ value: k, text: k.toUpperCase() })), + defaultValue, + tooltip: "Key to toggle the info panel visibility." + }, + "🔍MagnifyGlass.GlassPreviewToggleHotkey": { + id: key, + name: "📋 Info Panel: Glass Toggle Hotkey", + type: "combo", + options: ["g", "f", "v", "b"].map(k => ({ value: k, text: k.toUpperCase() })), + defaultValue, + tooltip: "Key to toggle the magnify glass preview visibility." + }, + "🔍MagnifyGlass.PinPanelHotkey": { + id: key, + name: "📋 Info Panel: Pin Hotkey", + type: "combo", + options: ["u", "p", "y", "t"].map(k => ({ value: k, text: k.toUpperCase() })), + defaultValue, + tooltip: "Key to pin/unlock the info panel at the current mouse location." + }, + "🔍MagnifyGlass.ShowHoveringControls": { + id: key, + name: "📋 Info Panel: Floating Controls", + type: "combo", + options: [ + { value: true, text: "Show" }, + { value: false, text: "Hide" } + ], + defaultValue, + tooltip: "Show floating control buttons near the info panel." + }, + "🔍MagnifyGlass.ControlsPosition": { + id: key, + name: "📋 Info Panel: Controls Position", + type: "combo", + options: ["top-left", "top-right", "bottom-left", "bottom-right", "top", "bottom", "left", "right"].map(p => ({ value: p, text: p })), + defaultValue, + tooltip: "Position of the floating control buttons." + }, + "🔍MagnifyGlass.InfoPanelTextColor": { + id: key, + name: "🎨 Info Panel: Text Color", + type: "color", + defaultValue, + tooltip: "Color for text in the info panel." + }, + "🔍MagnifyGlass.InfoPanelAccentColor": { + id: key, + name: "🎨 Info Panel: Accent Color", + type: "color", + defaultValue, + tooltip: "Accent color for highlights in the info panel." + } + }; + + return configs[key] || null; +} diff --git a/src/shared/settings/defaults.ts b/src/shared/settings/defaults.ts new file mode 100644 index 0000000..e003eb4 --- /dev/null +++ b/src/shared/settings/defaults.ts @@ -0,0 +1,59 @@ +/** + * ComfyUI MagnifyGlass - Default Settings Values + * + * Default values for all magnify glass and info panel settings. + */ + +import { + DEFAULT_GLASS_SIZE, + DEFAULT_ZOOM_FACTOR, + DEFAULT_BORDER_WIDTH, + DEFAULT_BORDER_COLOR, + DEFAULT_OFFSET_STEP, + DEFAULT_PANEL_WIDTH, + DEFAULT_PANEL_MAX_HEIGHT, + DEFAULT_PANEL_OPACITY +} from '../constants'; +import type { GlassSettings, PanelSettings } from './types'; + +/** + * Default settings for the magnify glass component. + */ +export const DEFAULT_GLASS_SETTINGS: GlassSettings = { + "🔍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", +}; + +/** + * Default settings for the info panel component. + */ +export const DEFAULT_PANEL_SETTINGS: PanelSettings = { + "🔍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": "left", + "🔍MagnifyGlass.InfoPanelTextColor": "#6b7280", + "🔍MagnifyGlass.InfoPanelAccentColor": "#3b82f6", +}; diff --git a/src/shared/settings/glassSettings.ts b/src/shared/settings/glassSettings.ts new file mode 100644 index 0000000..ed2976f --- /dev/null +++ b/src/shared/settings/glassSettings.ts @@ -0,0 +1,365 @@ +/** + * ComfyUI MagnifyGlass - Glass Settings Registration + * + * Registers all magnify glass settings with ComfyUI. + */ + +// @ts-ignore +import { app } from "/scripts/app.js"; +import { + GLASS_POSITIONS, + GLASS_SHAPES, + ACTIVATION_KEYS, + RESET_KEYS, + TOGGLE_FOLLOW_KEYS +} from '../constants'; +import { DEFAULT_GLASS_SETTINGS } from './defaults'; + +/** + * Register all magnify glass settings with ComfyUI. + * @param magnifyGlass - The MagnifyGlass instance + */ +export function registerGlassSettings(magnifyGlass: any): void { + 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: 1000, + step: 25, + tooltip: "Magnification level as a percentage (e.g., 300 = 3x zoom, 150 = 1.5x zoom).", + onChange: (value: any) => { + if (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: any) => { + if (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: any) => { + if (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: any) => { + if (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 })), + defaultValue: settings["🔍MagnifyGlass.ActivationKey"], + tooltip: "The key to activate the magnifier.", + onChange: (value: any) => { + if (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: any) => { + if (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: any) => { + if (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: any) => { + if (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: any) => { + if (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: any) => { + if (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: any) => { + if (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: any) => { + if (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: any) => { + if (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: any) => { + if (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: any) => { + if (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: any) => { + if (magnifyGlass?.config) { + magnifyGlass.config.toggleFollowCursorKey = String(value).toLowerCase(); + } + } + }); + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.Action.ResetPosition", + name: "🔄 Magnify Glass: Reset Position", + type: "boolean", + defaultValue: false, + tooltip: "Toggle to reset the glass position and disable follow cursor.", + onChange: (value: any) => { + if (value) { + if (magnifyGlass?.resetOffsets) { + magnifyGlass.resetOffsets(); + } + // Reset the toggle back to false + setTimeout(() => { + try { + app.ui.settings.setSettingValue("🔍MagnifyGlass.Action.ResetPosition", false); + } catch (e) { /* ignore */ } + }, 500); + } + } + }); + + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.Action.ResetAll", + name: "⚠️ Magnify Glass: Reset ALL Settings", + type: "boolean", + defaultValue: false, + tooltip: "Toggle to reset ALL settings to defaults.", + onChange: (value: any) => { + if (value) { + if (confirm("Reset ALL Magnify Glass settings?")) { + // Glass Defaults + app.ui.settings.setSettingValue('🔍MagnifyGlass.ZoomFactor', 300); + app.ui.settings.setSettingValue('🔍MagnifyGlass.GlassSize', 300); + app.ui.settings.setSettingValue('🔍MagnifyGlass.GlassShape', 'Rounded Square'); + app.ui.settings.setSettingValue('🔍MagnifyGlass.GlassPosition', 'Top-Right'); + app.ui.settings.setSettingValue('🔍MagnifyGlass.TextureFiltering', 'Linear'); + app.ui.settings.setSettingValue('🔍MagnifyGlass.BorderWidth', 1); + app.ui.settings.setSettingValue('🔍MagnifyGlass.BorderColor', '#6b7280'); + app.ui.settings.setSettingValue('🔍MagnifyGlass.BorderEnabled', true); + app.ui.settings.setSettingValue('🔍MagnifyGlass.FollowCursor', false); + app.ui.settings.setSettingValue('🔍MagnifyGlass.AlwaysActiveMode', true); + app.ui.settings.setSettingValue('🔍MagnifyGlass.ActivationKey', 'x'); + app.ui.settings.setSettingValue('🔍MagnifyGlass.ResetKey', 'o'); + app.ui.settings.setSettingValue('🔍MagnifyGlass.ToggleFollowCursorKey', 'h'); + app.ui.settings.setSettingValue('🔍MagnifyGlass.AltRequired', false); + app.ui.settings.setSettingValue('🔍MagnifyGlass.OffsetStep', 5); + app.ui.settings.setSettingValue('🔍MagnifyGlass.DebugMode', false); + + // Panel Defaults + app.ui.settings.setSettingValue('🔍MagnifyGlass.InfoPanelEnabled', true); + app.ui.settings.setSettingValue('🔍MagnifyGlass.InfoPanelPosition', 'Bottom'); + app.ui.settings.setSettingValue('🔍MagnifyGlass.InfoPanelWidth', 300); + app.ui.settings.setSettingValue('🔍MagnifyGlass.InfoPanelMaxHeight', 300); + app.ui.settings.setSettingValue('🔍MagnifyGlass.InfoPanelOpacity', 100); + app.ui.settings.setSettingValue('🔍MagnifyGlass.InfoPanelTextColor', '#6b7280'); + app.ui.settings.setSettingValue('🔍MagnifyGlass.InfoPanelAccentColor', '#3b82f6'); + app.ui.settings.setSettingValue('🔍MagnifyGlass.InfoPanelAnimations', false); + app.ui.settings.setSettingValue('🔍MagnifyGlass.ShowHoveringControls', true); + app.ui.settings.setSettingValue('🔍MagnifyGlass.ControlsPosition', 'left'); + app.ui.settings.setSettingValue('🔍MagnifyGlass.ToggleHotkey', 'i'); + app.ui.settings.setSettingValue('🔍MagnifyGlass.GlassPreviewToggleHotkey', 'g'); + app.ui.settings.setSettingValue('🔍MagnifyGlass.PinPanelHotkey', 'u'); + + // Also force position reset + if (magnifyGlass?.resetOffsets) { + magnifyGlass.resetOffsets(); + } + } + + // Reset the toggle back to false + setTimeout(() => { + try { + app.ui.settings.setSettingValue("🔍MagnifyGlass.Action.ResetAll", false); + } catch (e) { /* ignore */ } + }, 500); + } + } + }); +} diff --git a/src/shared/settings/index.ts b/src/shared/settings/index.ts new file mode 100644 index 0000000..d986cd0 --- /dev/null +++ b/src/shared/settings/index.ts @@ -0,0 +1,15 @@ +/** + * ComfyUI MagnifyGlass - Settings Module + * + * Re-exports all settings-related types, defaults, and registration functions. + */ + +// Type definitions +export type { GlassSettings, PanelSettings, AllSettings } from './types'; + +// Default values +export { DEFAULT_GLASS_SETTINGS, DEFAULT_PANEL_SETTINGS } from './defaults'; + +// Registration functions +export { registerGlassSettings } from './glassSettings'; +export { registerPanelSettings } from './panelSettings'; diff --git a/src/shared/settings/panelSettings.ts b/src/shared/settings/panelSettings.ts new file mode 100644 index 0000000..0d0e6f1 --- /dev/null +++ b/src/shared/settings/panelSettings.ts @@ -0,0 +1,192 @@ +/** + * ComfyUI MagnifyGlass - Panel Settings Registration + * + * Registers all info panel settings with ComfyUI. + */ + +// @ts-ignore +import { app } from "/scripts/app.js"; +import { PANEL_POSITIONS } from '../constants'; +import { DEFAULT_PANEL_SETTINGS } from './defaults'; + +/** + * Register all info panel settings with ComfyUI. + * @param stateManager - The StateManager instance for the info panel + * @param uiManager - The UIManager instance for the info panel + * @param positionManager - The PositionManager instance for the info panel + */ +export function registerPanelSettings(stateManager: any, uiManager: any, positionManager: any): void { + 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: any) => { + 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: any) => { + 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: any) => { + 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: any) => { + 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: any) => { + 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: any) => { + 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: any) => { + 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: any) => { + 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: any) => { + 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: any) => { + if (stateManager) { + const strValue = String(value); + const normalizedColor = strValue && !strValue.startsWith('#') ? `#${strValue}` : strValue; + stateManager.state.settings["🔍MagnifyGlass.InfoPanelAccentColor"] = normalizedColor; + if (uiManager) { + uiManager.applyStyles(); + } + } + } + }); +} diff --git a/src/shared/settings/types.ts b/src/shared/settings/types.ts new file mode 100644 index 0000000..a3fe15d --- /dev/null +++ b/src/shared/settings/types.ts @@ -0,0 +1,53 @@ +/** + * ComfyUI MagnifyGlass - Settings Type Definitions + * + * Type definitions for magnify glass and info panel settings. + */ + +/** + * Settings for the magnify glass component. + */ +export interface GlassSettings { + "🔍MagnifyGlass.ZoomFactor": number; + "🔍MagnifyGlass.GlassSize": number; + "🔍MagnifyGlass.BorderColor": string; + "🔍MagnifyGlass.BorderWidth": number; + "🔍MagnifyGlass.ActivationKey": string; + "🔍MagnifyGlass.AltRequired": boolean; + "🔍MagnifyGlass.FollowCursor": boolean; + "🔍MagnifyGlass.DebugMode": boolean; + "🔍MagnifyGlass.OffsetStep": number; + "🔍MagnifyGlass.GlassPosition": string; + "🔍MagnifyGlass.ResetKey": string; + "🔍MagnifyGlass.GlassShape": string; + "🔍MagnifyGlass.BorderEnabled": boolean; + "🔍MagnifyGlass.TextureFiltering": string; + "🔍MagnifyGlass.AlwaysActiveMode": boolean; + "🔍MagnifyGlass.ToggleFollowCursorKey": string; +} + +/** + * Settings for the info panel component. + */ +export interface PanelSettings { + "🔍MagnifyGlass.InfoPanelEnabled": boolean; + "🔍MagnifyGlass.InfoPanelPosition": string; + "🔍MagnifyGlass.InfoPanelWidth": number; + "🔍MagnifyGlass.InfoPanelOpacity": number; + "🔍MagnifyGlass.InfoPanelMaxHeight": number; + "🔍MagnifyGlass.InfoPanelAnimations": boolean; + "🔍MagnifyGlass.ShowInspectorTab": boolean; + "🔍MagnifyGlass.ToggleHotkey": string; + "🔍MagnifyGlass.GlassPreviewToggleHotkey": string; + "🔍MagnifyGlass.PinPanelHotkey": string; + "🔍MagnifyGlass.ShowHoveringControls": boolean; + "🔍MagnifyGlass.ControlsPosition": string; + "🔍MagnifyGlass.InfoPanelTextColor": string; + "🔍MagnifyGlass.InfoPanelAccentColor": string; + [key: string]: string | number | boolean; // Allow indexing +} + +/** + * Combined settings type. + */ +export type AllSettings = GlassSettings & PanelSettings; diff --git a/src/shared/utils.ts b/src/shared/utils.ts new file mode 100644 index 0000000..30c1ed7 --- /dev/null +++ b/src/shared/utils.ts @@ -0,0 +1,161 @@ +/** + * ComfyUI MagnifyGlass - Shared Utilities + * + * Common utility functions shared between magnify glass and info panel modules. + */ + +import type { ComfyApp } from '../types/comfyui'; + +// Reference to ComfyUI's app - will be set at runtime +// @ts-ignore +import { app } from "/scripts/app.js"; + +/** + * Rectangle interface for overlap calculations + */ +export interface Rectangle { + x: number; + y: number; + width: number; + height: number; +} + +/** + * Debug logger interface + */ +export interface DebugLogger { + log(...args: unknown[]): void; + error(...args: unknown[]): void; +} + +/** + * Safely get a setting value from ComfyUI settings with fallback to default. + * @param key - The setting key + * @param defaultValue - Default value if setting not found + * @returns The setting value or default + */ +export function getSettingValue(key: string, defaultValue: T): T { + try { + const value = app.ui.settings.getSettingValue(key); + return value === undefined ? defaultValue : value as T; + } catch (e) { + console.warn(`ComfyUI Magnifying Glass: Could not get setting ${key}, using default ${defaultValue}. Error: ${e}`); + return defaultValue; + } +} + +/** + * Check if the user is currently typing in an input field. + * Used for smart input detection to avoid triggering hotkeys while typing. + * @returns True if user is typing in an input field + */ +export function isUserTyping(): boolean { + const activeElement = document.activeElement as HTMLElement | null; + if (!activeElement) return false; + + // Check if the active element is an input, textarea, or contenteditable + const tagName = activeElement.tagName.toLowerCase(); + if (tagName === 'input' || tagName === 'textarea') { + return true; + } + + // Check for contenteditable elements + if (activeElement.contentEditable === 'true') { + return true; + } + + // Check if it's inside a form or has input-like classes + if (activeElement.closest('form') || + activeElement.classList.contains('cm-editor') || // CodeMirror editor + activeElement.classList.contains('monaco-editor') || // Monaco editor + activeElement.closest('.cm-editor') || + activeElement.closest('.monaco-editor')) { + return true; + } + + return false; +} + +/** + * Check if two rectangles overlap. + * @param rect1 - First rectangle + * @param rect2 - Second rectangle + * @returns True if rectangles overlap + */ +export function rectsOverlap(rect1: Rectangle, rect2: Rectangle): boolean { + return rect1.x < rect2.x + rect2.width && + rect1.x + rect1.width > rect2.x && + rect1.y < rect2.y + rect2.height && + rect1.y + rect1.height > rect2.y; +} + +/** + * Find the LiteGraph canvas element in the DOM. + * Tries multiple selectors for compatibility with different ComfyUI versions. + * @returns The canvas element or null if not found + */ +export function findLiteGraphCanvas(): HTMLCanvasElement | null { + // Try multiple selectors for compatibility + const canvas = document.getElementById("graph-canvas") as HTMLCanvasElement | null; + if (canvas) return canvas; + + const graphCanvas = document.querySelector("canvas.graphcanvas") as HTMLCanvasElement | null; + if (graphCanvas) return graphCanvas; + + // Try canvas manager (dynamic access for ComfyUI compatibility) + const appAny = app as any; + if (appAny?.canvas_manager?.container) { + const managerCanvas = appAny.canvas_manager.container.querySelector("canvas") as HTMLCanvasElement | null; + if (managerCanvas) return managerCanvas; + } + + // Try direct canvas reference + if (appAny?.canvas?.graph_canvas) { + return appAny.canvas.graph_canvas; + } + + return null; +} + +/** + * Normalize a color value to include the # symbol for CSS. + * @param color - The color value (with or without #) + * @returns The normalized color with # prefix + */ +export function normalizeColor(color: string | null | undefined): string { + if (!color) return color ?? ''; + return color.startsWith('#') ? color : `#${color}`; +} + +/** + * Clamp a value between min and max bounds. + * @param value - The value to clamp + * @param min - Minimum bound + * @param max - Maximum bound + * @returns The clamped value + */ +export function clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max); +} + +/** + * Create a debug logger that only logs when debug mode is enabled. + * @param prefix - Prefix for log messages + * @param isDebugEnabled - Function that returns whether debug is enabled + * @returns Logger object + */ +export function createDebugLogger( + prefix: string, + isDebugEnabled: () => boolean +): DebugLogger { + return { + log(...args: unknown[]): void { + if (isDebugEnabled()) { + console.log(`${prefix}:`, ...args); + } + }, + error(...args: unknown[]): void { + console.error(`${prefix} ERROR:`, ...args); + } + }; +} diff --git a/src/sidebar/SidebarInspector.ts b/src/sidebar/SidebarInspector.ts new file mode 100644 index 0000000..32829e4 --- /dev/null +++ b/src/sidebar/SidebarInspector.ts @@ -0,0 +1,261 @@ +/** + * ComfyUI MagnifyGlass - Sidebar Inspector Panel + * + * Real-time inspector view for the sidebar. + */ + +// @ts-ignore +import { app } from "/scripts/app.js"; + +/** + * Icons for the inspector panel + */ +const Icons = { + chevronDown: ``, +}; + +/** + * Inspector state + */ +interface InspectorState { + cursor: { x: number; y: number }; + canvasScale: number; + hoveredNode: any | null; +} + +let inspectorState: InspectorState = { + cursor: { x: 0, y: 0 }, + canvasScale: 1, + hoveredNode: null +}; + +let inspectorBody: HTMLElement | null = null; +let updateInterval: number | null = null; + +/** + * Format a number for display + */ +function formatNumber(value: number, decimals: number = 0): string { + return value.toFixed(decimals); +} + +/** + * Get the currently hovered node from the graph + */ +function getHoveredNode(): any | null { + try { + const graph = app.graph; + if (!graph || !graph.canvas) return null; + + const canvas = graph.canvas; + const mousePos = canvas.graph_mouse; + + if (!mousePos) return null; + + // Find node at mouse position + const nodes = graph._nodes; + for (let i = nodes.length - 1; i >= 0; i--) { + const node = nodes[i]; + if (node.pos && node.size) { + const [x, y] = node.pos; + const [w, h] = node.size; + if (mousePos[0] >= x && mousePos[0] <= x + w && + mousePos[1] >= y && mousePos[1] <= y + h) { + return node; + } + } + } + return null; + } catch { + return null; + } +} + +/** + * Get canvas information + */ +function getCanvasInfo(): { scale: number; cursor: { x: number; y: number } } { + try { + const graph = app.graph; + if (!graph || !graph.canvas) { + return { scale: 1, cursor: { x: 0, y: 0 } }; + } + + const canvas = graph.canvas; + const scale = canvas.ds?.scale || 1; + const mousePos = canvas.graph_mouse || [0, 0]; + + return { + scale: scale * 100, + cursor: { x: mousePos[0] || 0, y: mousePos[1] || 0 } + }; + } catch { + return { scale: 1, cursor: { x: 0, y: 0 } }; + } +} + +/** + * Create an inspector row + */ +function createRow(label: string, value: string): HTMLElement { + const row = document.createElement('div'); + row.className = 'magnify-inspector-row'; + + const labelEl = document.createElement('span'); + labelEl.className = 'magnify-inspector-label'; + labelEl.textContent = label; + + const valueEl = document.createElement('span'); + valueEl.className = 'magnify-inspector-value'; + valueEl.textContent = value; + valueEl.title = value; + + row.appendChild(labelEl); + row.appendChild(valueEl); + + return row; +} + +/** + * Create a group with title + */ +function createGroup(title: string): HTMLElement { + const group = document.createElement('div'); + group.className = 'magnify-inspector-group'; + + const titleEl = document.createElement('div'); + titleEl.className = 'magnify-inspector-group-title'; + titleEl.textContent = title; + + group.appendChild(titleEl); + + return group; +} + +/** + * Update the inspector display + */ +function updateInspector(): void { + if (!inspectorBody) return; + + const canvasInfo = getCanvasInfo(); + const hoveredNode = getHoveredNode(); + + // Clear body + inspectorBody.innerHTML = ''; + + // Canvas info group + const canvasGroup = createGroup('Canvas'); + canvasGroup.appendChild(createRow('Cursor', `(${formatNumber(canvasInfo.cursor.x)}, ${formatNumber(canvasInfo.cursor.y)})`)); + canvasGroup.appendChild(createRow('Scale', `${formatNumber(canvasInfo.scale, 1)}%`)); + inspectorBody.appendChild(canvasGroup); + + // MagnifyGlass info + const magnifyGlass = window.comfyUIMagnifyGlass; + if (magnifyGlass) { + const glassGroup = createGroup('Magnify Glass'); + glassGroup.appendChild(createRow('Zoom', `${formatNumber((magnifyGlass.config?.zoomFactor || 1) * 100)}%`)); + glassGroup.appendChild(createRow('Active', magnifyGlass.state?.active ? 'Yes' : 'No')); + inspectorBody.appendChild(glassGroup); + } + + // Node info group + if (hoveredNode) { + const nodeGroup = createGroup('Hovered Node'); + + // Node badge + const badge = document.createElement('div'); + badge.className = 'magnify-node-badge'; + badge.textContent = hoveredNode.type || 'Unknown'; + nodeGroup.appendChild(badge); + + nodeGroup.appendChild(createRow('Title', hoveredNode.title || 'Untitled')); + nodeGroup.appendChild(createRow('ID', String(hoveredNode.id))); + + // Position + if (hoveredNode.pos) { + nodeGroup.appendChild(createRow('Position', `(${formatNumber(hoveredNode.pos[0])}, ${formatNumber(hoveredNode.pos[1])})`)); + } + + // Size + if (hoveredNode.size) { + nodeGroup.appendChild(createRow('Size', `${formatNumber(hoveredNode.size[0])} × ${formatNumber(hoveredNode.size[1])}`)); + } + + // Widgets (first few) + if (hoveredNode.widgets && hoveredNode.widgets.length > 0) { + const widgetGroup = createGroup('Parameters'); + const widgetsToShow = hoveredNode.widgets.slice(0, 6); + for (const widget of widgetsToShow) { + if (widget.name && widget.value !== undefined) { + let value = String(widget.value); + if (value.length > 30) value = value.substring(0, 27) + '...'; + widgetGroup.appendChild(createRow(widget.name, value)); + } + } + if (hoveredNode.widgets.length > 6) { + const moreEl = document.createElement('div'); + moreEl.style.cssText = 'font-size: 11px; color: #666; padding: 4px 0;'; + moreEl.textContent = `+${hoveredNode.widgets.length - 6} more...`; + widgetGroup.appendChild(moreEl); + } + inspectorBody.appendChild(widgetGroup); + } + + inspectorBody.appendChild(nodeGroup); + } else { + // Empty state + const empty = document.createElement('div'); + empty.className = 'magnify-inspector-empty'; + empty.textContent = 'Hover over a node to inspect'; + inspectorBody.appendChild(empty); + } +} + +/** + * Render the inspector panel into the container + */ +export function renderInspectorPanel(container: HTMLElement): void { + const section = document.createElement('div'); + section.className = 'magnify-sidebar-section'; + + // Section header + const header = document.createElement('div'); + header.className = 'magnify-sidebar-section-header'; + header.innerHTML = `${Icons.chevronDown}Inspector`; + + // Section body + const body = document.createElement('div'); + body.className = 'magnify-sidebar-section-body magnify-inspector-section'; + inspectorBody = body; + + // Toggle collapse + header.addEventListener('click', () => { + header.classList.toggle('collapsed'); + body.classList.toggle('collapsed'); + }); + + section.appendChild(header); + section.appendChild(body); + container.appendChild(section); + + // Initial update + updateInspector(); + + // Start update interval + if (updateInterval) { + clearInterval(updateInterval); + } + updateInterval = window.setInterval(updateInspector, 100); +} + +/** + * Cleanup the inspector (call when sidebar is destroyed) + */ +export function cleanupInspector(): void { + if (updateInterval) { + clearInterval(updateInterval); + updateInterval = null; + } + inspectorBody = null; +} diff --git a/src/sidebar/SidebarSettings.ts b/src/sidebar/SidebarSettings.ts new file mode 100644 index 0000000..7c7339f --- /dev/null +++ b/src/sidebar/SidebarSettings.ts @@ -0,0 +1,697 @@ +/** + * ComfyUI MagnifyGlass - Sidebar Settings Panel + * + * All settings controls for the sidebar. + */ + +// @ts-ignore +import { app } from "/scripts/app.js"; +import { + GLASS_POSITIONS, + GLASS_SHAPES, + ACTIVATION_KEYS, + RESET_KEYS, + TOGGLE_FOLLOW_KEYS, + PANEL_POSITIONS +} from '../shared/constants'; + +/** + * Icons for the settings panel + */ +const Icons = { + chevronDown: ``, + reset: ``, +}; + +/** + * Create a slider control with tooltip + */ +/** + * Create a slider control with tooltip + */ +function createSlider( + label: string, + value: number, + min: number, + max: number, + step: number, + unit: string, + onChange: (value: number) => void, + tooltip?: string, + onInput?: (value: number) => void +): HTMLElement { + const row = document.createElement('div'); + row.className = 'magnify-control-row'; + if (tooltip) row.title = tooltip; + + const labelRow = document.createElement('div'); + labelRow.className = 'magnify-control-label-row'; + + const labelEl = document.createElement('label'); + labelEl.textContent = label; + + const valueEl = document.createElement('span'); + valueEl.className = 'control-value'; + valueEl.textContent = `${value}${unit}`; + + labelRow.appendChild(labelEl); + labelRow.appendChild(valueEl); + + const slider = document.createElement('input'); + slider.type = 'range'; + slider.className = 'magnify-slider'; + slider.min = String(min); + slider.max = String(max); + slider.step = String(step); + slider.value = String(value); + + // Prevent ComfyUI from capturing mouse events during drag + slider.addEventListener('mousedown', (e) => e.stopPropagation()); + slider.addEventListener('touchstart', (e) => e.stopPropagation()); + slider.addEventListener('pointerdown', (e) => e.stopPropagation()); + + slider.addEventListener('input', (e) => { + e.stopPropagation(); + const newValue = parseFloat(slider.value); + valueEl.textContent = `${newValue}${unit}`; + if (onInput) onInput(newValue); + }); + + slider.addEventListener('change', (e) => { + e.stopPropagation(); + const newValue = parseFloat(slider.value); + onChange(newValue); + }); + + row.appendChild(labelRow); + row.appendChild(slider); + + return row; +} + +/** + * Create a toggle control with tooltip + */ +function createToggle( + label: string, + checked: boolean, + onChange: (checked: boolean) => void, + tooltip?: string +): HTMLElement { + const row = document.createElement('div'); + row.className = 'magnify-toggle-row'; + if (tooltip) row.title = tooltip; + + const labelEl = document.createElement('label'); + labelEl.textContent = label; + + const toggle = document.createElement('div'); + toggle.className = `magnify-toggle${checked ? ' active' : ''}`; + + toggle.addEventListener('click', () => { + const isActive = toggle.classList.toggle('active'); + onChange(isActive); + }); + + row.appendChild(labelEl); + row.appendChild(toggle); + + return row; +} + +/** + * Create a select dropdown control with tooltip + */ +function createSelect( + label: string, + value: string, + options: readonly string[] | string[], + onChange: (value: string) => void, + tooltip?: string, + onInput?: (value: string) => void +): HTMLElement { + const row = document.createElement('div'); + row.className = 'magnify-control-row'; + if (tooltip) row.title = tooltip; + + const labelEl = document.createElement('label'); + labelEl.textContent = label; + + const select = document.createElement('select'); + select.className = 'magnify-select'; + + options.forEach(opt => { + const option = document.createElement('option'); + option.value = opt; + option.textContent = opt; + if (opt === value) option.selected = true; + select.appendChild(option); + }); + + select.addEventListener('change', () => { + // Call onInput first for live preview + if (onInput) { + onInput(select.value); + } + // Then persist the setting + onChange(select.value); + }); + + row.appendChild(labelEl); + row.appendChild(select); + + return row; +} + +/** + * Create a color picker control with tooltip + */ +/** + * Create a color picker control with tooltip + */ +function createColorPicker( + label: string, + value: string, + onChange: (value: string) => void, + tooltip?: string, + onInput?: (value: string) => void +): HTMLElement { + const row = document.createElement('div'); + row.className = 'magnify-control-row magnify-color-row'; + if (tooltip) row.title = tooltip; + + const labelEl = document.createElement('label'); + labelEl.textContent = label; + + const colorWrapper = document.createElement('div'); + colorWrapper.className = 'magnify-color-wrapper'; + + const colorInput = document.createElement('input'); + colorInput.type = 'color'; + colorInput.className = 'magnify-color-input'; + colorInput.value = value; + + const colorPreview = document.createElement('span'); + colorPreview.className = 'magnify-color-preview'; + colorPreview.textContent = value; + + colorInput.addEventListener('input', () => { + colorPreview.textContent = colorInput.value; + if (onInput) onInput(colorInput.value); + }); + + colorInput.addEventListener('change', () => { + onChange(colorInput.value); + }); + + colorWrapper.appendChild(colorInput); + colorWrapper.appendChild(colorPreview); + + row.appendChild(labelEl); + row.appendChild(colorWrapper); + + return row; +} + +/** + * Get current MagnifyGlass settings from app.ui.settings + */ +function getSettingValue(id: string, defaultValue: any): any { + try { + const value = app.ui.settings.getSettingValue(id); + return value !== undefined ? value : defaultValue; + } catch { + return defaultValue; + } +} + +/** + * Set a MagnifyGlass setting via app.ui.settings + */ +function setSettingValue(id: string, value: any): void { + try { + app.ui.settings.setSettingValue(id, value); + } catch (e) { + console.warn(`Failed to set setting ${id}:`, e); + } +} + +/** Storage key for section states */ +const SECTION_STATE_KEY = 'magnifyglass-sidebar-sections'; + +/** + * Get saved section states from localStorage + */ +function getSectionStates(): Record { + try { + const saved = localStorage.getItem(SECTION_STATE_KEY); + return saved ? JSON.parse(saved) : {}; + } catch { + return {}; + } +} + +/** + * Save section state to localStorage + */ +function saveSectionState(title: string, collapsed: boolean): void { + try { + const states = getSectionStates(); + states[title] = collapsed; + localStorage.setItem(SECTION_STATE_KEY, JSON.stringify(states)); + } catch { + // Ignore storage errors + } +} + +/** + * Create a collapsible section with persistent state + */ +function createSection(title: string, defaultCollapsed: boolean = false): { section: HTMLElement; body: HTMLElement } { + const section = document.createElement('div'); + section.className = 'magnify-sidebar-section'; + + // Check if we have a saved state for this section + const savedStates = getSectionStates(); + const collapsed = savedStates[title] !== undefined ? savedStates[title] : defaultCollapsed; + + const header = document.createElement('div'); + header.className = `magnify-sidebar-section-header${collapsed ? ' collapsed' : ''}`; + header.innerHTML = `${Icons.chevronDown}${title}`; + + const body = document.createElement('div'); + body.className = `magnify-sidebar-section-body${collapsed ? ' collapsed' : ''}`; + + header.addEventListener('click', () => { + const isCollapsed = header.classList.toggle('collapsed'); + body.classList.toggle('collapsed'); + // Save the new state + saveSectionState(title, isCollapsed); + }); + + section.appendChild(header); + section.appendChild(body); + + return { section, body }; +} + +/** + * Render the settings panel into the container + */ +export function renderSettingsPanel(container: HTMLElement): void { + // Get current hotkey values for tooltips + const activationKey = getSettingValue('🔍MagnifyGlass.ActivationKey', 'x').toUpperCase(); + const resetKey = getSettingValue('🔍MagnifyGlass.ResetKey', 'o').toUpperCase(); + const toggleFollowKey = getSettingValue('🔍MagnifyGlass.ToggleFollowCursorKey', 'h').toUpperCase(); + + // ===== Glass Settings Section ===== + const glassSection = createSection('Glass Settings'); + + glassSection.body.appendChild(createSlider('Zoom Factor', + getSettingValue('🔍MagnifyGlass.ZoomFactor', 300), 100, 1000, 25, '%', + (value) => setSettingValue('🔍MagnifyGlass.ZoomFactor', value), + 'Magnification level of the glass (100-1000%)', + (value) => { + const mg = (window as any).comfyUIMagnifyGlass; + if (mg?.config) { + mg.config.zoomFactor = value / 100; + if (mg.state.active) mg.updateMagnifiedView(); + } + } + )); + + glassSection.body.appendChild(createSlider('Glass Size', + getSettingValue('🔍MagnifyGlass.GlassSize', 250), 50, 500, 10, 'px', + (value) => setSettingValue('🔍MagnifyGlass.GlassSize', value), + 'Size of the magnifying glass in pixels', + (value) => { + const mg = (window as any).comfyUIMagnifyGlass; + if (mg?.config) { + mg.config.glassSize = value; + mg.applyUiChanges(); + if (mg.state.active) mg.updateMagnifiedView(); + } + } + )); + + glassSection.body.appendChild(createSelect('Shape', + getSettingValue('🔍MagnifyGlass.GlassShape', 'Rounded Square'), GLASS_SHAPES, + (value) => setSettingValue('🔍MagnifyGlass.GlassShape', value), + 'Shape of the magnifying glass' + )); + + glassSection.body.appendChild(createSelect('Position', + getSettingValue('🔍MagnifyGlass.GlassPosition', 'Top-Right'), GLASS_POSITIONS, + (value) => setSettingValue('🔍MagnifyGlass.GlassPosition', value), + `Default position of the glass. Press ${resetKey} to reset.` + )); + + glassSection.body.appendChild(createSelect('Filtering', + getSettingValue('🔍MagnifyGlass.TextureFiltering', 'Linear'), ['Linear', 'Nearest'], + (value) => setSettingValue('🔍MagnifyGlass.TextureFiltering', value), + 'Texture filtering: Linear (smooth) or Nearest (pixelated)' + )); + + glassSection.body.appendChild(createSlider('Border Width', + getSettingValue('🔍MagnifyGlass.BorderWidth', 2), 0, 10, 0.5, 'px', + (value) => setSettingValue('🔍MagnifyGlass.BorderWidth', value), + 'Width of the glass border in pixels', + (value) => { + const mg = (window as any).comfyUIMagnifyGlass; + if (mg?.config) { + mg.config.borderWidth = value; + mg.applyUiChanges(); + } + } + )); + + glassSection.body.appendChild(createColorPicker('Border Color', + getSettingValue('🔍MagnifyGlass.BorderColor', '#ffffff'), + (value) => setSettingValue('🔍MagnifyGlass.BorderColor', value), + 'Color of the glass border', + (value) => { + const mg = (window as any).comfyUIMagnifyGlass; + if (mg?.config) { + mg.config.borderColor = value; + mg.applyUiChanges(); + } + } + )); + + glassSection.body.appendChild(createToggle('Show Border', + getSettingValue('🔍MagnifyGlass.BorderEnabled', true), + (checked) => setSettingValue('🔍MagnifyGlass.BorderEnabled', checked), + 'Show or hide the glass border' + )); + + glassSection.body.appendChild(createToggle('Follow Cursor', + getSettingValue('🔍MagnifyGlass.FollowCursor', false), + (checked) => setSettingValue('🔍MagnifyGlass.FollowCursor', checked), + `Glass follows the cursor. Hotkey: ${toggleFollowKey}` + )); + + glassSection.body.appendChild(createToggle('Always Active Mode', + getSettingValue('🔍MagnifyGlass.AlwaysActiveMode', true), + (checked) => setSettingValue('🔍MagnifyGlass.AlwaysActiveMode', checked), + `When enabled, glass stays visible. Toggle with ${activationKey}` + )); + + container.appendChild(glassSection.section); + + // ===== Hotkeys Section ===== + const hotkeySection = createSection('Hotkeys', true); + + hotkeySection.body.appendChild(createSelect('Activation Key', + getSettingValue('🔍MagnifyGlass.ActivationKey', 'x'), ACTIVATION_KEYS, + (value) => setSettingValue('🔍MagnifyGlass.ActivationKey', value), + 'Key to toggle or hold to activate the magnifying glass' + )); + + hotkeySection.body.appendChild(createSelect('Reset Key', + getSettingValue('🔍MagnifyGlass.ResetKey', 'o'), RESET_KEYS, + (value) => setSettingValue('🔍MagnifyGlass.ResetKey', value), + 'Key to reset glass position and disable follow cursor' + )); + + hotkeySection.body.appendChild(createSelect('Toggle Follow Key', + getSettingValue('🔍MagnifyGlass.ToggleFollowCursorKey', 'h'), TOGGLE_FOLLOW_KEYS, + (value) => setSettingValue('🔍MagnifyGlass.ToggleFollowCursorKey', value), + 'Key to toggle follow cursor mode' + )); + + hotkeySection.body.appendChild(createToggle('Require Alt Key', + getSettingValue('🔍MagnifyGlass.AltRequired', false), + (checked) => setSettingValue('🔍MagnifyGlass.AltRequired', checked), + 'Require Alt key to be held with activation key' + )); + + hotkeySection.body.appendChild(createSlider('Offset Step', + getSettingValue('🔍MagnifyGlass.OffsetStep', 10), 1, 50, 1, 'px', + (value) => setSettingValue('🔍MagnifyGlass.OffsetStep', value), + 'Amount to move glass per arrow key press (pixels)' + )); + + container.appendChild(hotkeySection.section); + + // ===== Info Panel Section ===== + const panelSection = createSection('Info Panel', true); + + const toggleHotkey = getSettingValue('🔍MagnifyGlass.ToggleHotkey', 'i').toUpperCase(); + const glassToggleKey = getSettingValue('🔍MagnifyGlass.GlassPreviewToggleHotkey', 'g').toUpperCase(); + const pinHotkeyVal = getSettingValue('🔍MagnifyGlass.PinPanelHotkey', 'u').toUpperCase(); + + panelSection.body.appendChild(createToggle('Enable Panel', + getSettingValue('🔍MagnifyGlass.InfoPanelEnabled', true), + (checked) => setSettingValue('🔍MagnifyGlass.InfoPanelEnabled', checked), + `Show info panel with node details. Toggle: ${toggleHotkey}` + )); + + panelSection.body.appendChild(createSelect('Position', + getSettingValue('🔍MagnifyGlass.InfoPanelPosition', 'Bottom'), PANEL_POSITIONS, + (value) => setSettingValue('🔍MagnifyGlass.InfoPanelPosition', value), + 'Position of the info panel relative to the glass' + )); + + panelSection.body.appendChild(createSlider('Width', + getSettingValue('🔍MagnifyGlass.InfoPanelWidth', 300), 200, 600, 20, 'px', + (value) => setSettingValue('🔍MagnifyGlass.InfoPanelWidth', value), + 'Width of the info panel', + (value) => { + const infoPanel = (window as any).infoPanelManager; + if (infoPanel?.stateManager?.state?.settings) { + infoPanel.stateManager.state.settings['🔍MagnifyGlass.InfoPanelWidth'] = value; + infoPanel.uiManager.applyStyles(); + + // Ensure visible and reflow if active + if (infoPanel.magnifyGlass.state.active) { + infoPanel.uiManager.show(); + infoPanel.positionManager.positionPanel(); + } + } + } + )); + + panelSection.body.appendChild(createSlider('Max Height', + getSettingValue('🔍MagnifyGlass.InfoPanelMaxHeight', 300), 200, 1500, 50, 'px', + (value) => setSettingValue('🔍MagnifyGlass.InfoPanelMaxHeight', value), + 'Maximum height of the info panel', + (value) => { + console.log('[MaxHeight] Handler called with value:', value); + const infoPanel = (window as any).infoPanelManager; + console.log('[MaxHeight] infoPanelManager:', infoPanel ? 'found' : 'NOT FOUND'); + if (infoPanel?.stateManager?.state?.settings) { + console.log('[MaxHeight] Updating setting and calling applyStyles'); + infoPanel.stateManager.state.settings['🔍MagnifyGlass.InfoPanelMaxHeight'] = value; + infoPanel.uiManager.applyStyles(); + + // Ensure visible and reflow if active + if (infoPanel.magnifyGlass.state.active) { + console.log('[MaxHeight] Glass is active, calling show() and positionPanel()'); + infoPanel.uiManager.show(); + infoPanel.positionManager.positionPanel(); + } else { + console.log('[MaxHeight] Glass is NOT active, skipping show()'); + } + } else { + console.warn('[MaxHeight] Check failed - stateManager/state/settings missing'); + } + } + )); + + panelSection.body.appendChild(createSlider('Opacity', + getSettingValue('🔍MagnifyGlass.InfoPanelOpacity', 95), 10, 100, 5, '%', + (value) => setSettingValue('🔍MagnifyGlass.InfoPanelOpacity', value), + 'Background opacity of the info panel', + (value) => { + const infoPanel = (window as any).infoPanelManager; + if (infoPanel?.stateManager?.state?.settings) { + infoPanel.stateManager.state.settings['🔍MagnifyGlass.InfoPanelOpacity'] = value; + infoPanel.uiManager.applyStyles(); + } + } + )); + + panelSection.body.appendChild(createColorPicker('Text Color', + getSettingValue('🔍MagnifyGlass.InfoPanelTextColor', '#6b7280'), + (value) => setSettingValue('🔍MagnifyGlass.InfoPanelTextColor', value), + 'Text color in the info panel', + (value) => { + const infoPanel = (window as any).infoPanelManager; + if (infoPanel?.stateManager?.state?.settings) { + infoPanel.stateManager.state.settings['🔍MagnifyGlass.InfoPanelTextColor'] = value; + infoPanel.uiManager.applyStyles(); + } + } + )); + + panelSection.body.appendChild(createColorPicker('Accent Color', + getSettingValue('🔍MagnifyGlass.InfoPanelAccentColor', '#3b82f6'), + (value) => setSettingValue('🔍MagnifyGlass.InfoPanelAccentColor', value), + 'Accent color for highlights', + (value) => { + const infoPanel = (window as any).infoPanelManager; + if (infoPanel?.stateManager?.state?.settings) { + infoPanel.stateManager.state.settings['🔍MagnifyGlass.InfoPanelAccentColor'] = value; + infoPanel.uiManager.applyStyles(); + } + } + )); + + panelSection.body.appendChild(createToggle('Animations', + getSettingValue('🔍MagnifyGlass.InfoPanelAnimations', false), + (checked) => setSettingValue('🔍MagnifyGlass.InfoPanelAnimations', checked), + 'Enable smooth animations for panel updates' + )); + + panelSection.body.appendChild(createToggle('Hover Controls', + getSettingValue('🔍MagnifyGlass.ShowHoveringControls', true), + (checked) => setSettingValue('🔍MagnifyGlass.ShowHoveringControls', checked), + 'Show floating control buttons near the panel' + )); + + panelSection.body.appendChild(createSelect('Controls Position', + getSettingValue('🔍MagnifyGlass.ControlsPosition', 'left'), + ['top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right'], + (value) => setSettingValue('🔍MagnifyGlass.ControlsPosition', value), + 'Position of floating control buttons', + (value) => { + // Live update controls position + const infoPanel = (window as any).infoPanelManager; + if (infoPanel?.stateManager?.state?.settings) { + infoPanel.stateManager.state.settings['🔍MagnifyGlass.ControlsPosition'] = value; + infoPanel.uiManager.updateControlsLayout(value); + if (infoPanel.magnifyGlass.state.active) { + infoPanel.positionManager.positionFloatingControls(); + } + } + } + )); + + container.appendChild(panelSection.section); + + // ===== Panel Hotkeys Section ===== + const panelHotkeySection = createSection('Panel Hotkeys', true); + + panelHotkeySection.body.appendChild(createSelect('Toggle Key', + getSettingValue('🔍MagnifyGlass.ToggleHotkey', 'i'), ['i', 'j', 'k', 'l', 'n', 'm'], + (value) => setSettingValue('🔍MagnifyGlass.ToggleHotkey', value), + 'Key to toggle info panel visibility' + )); + + panelHotkeySection.body.appendChild(createSelect('Glass Toggle Key', + getSettingValue('🔍MagnifyGlass.GlassPreviewToggleHotkey', 'g'), ['g', 'f', 'v', 'b'], + (value) => setSettingValue('🔍MagnifyGlass.GlassPreviewToggleHotkey', value), + 'Key to toggle glass preview in panel' + )); + + panelHotkeySection.body.appendChild(createSelect('Pin Key', + getSettingValue('🔍MagnifyGlass.PinPanelHotkey', 'u'), ['u', 'p', 'y', 't'], + (value) => setSettingValue('🔍MagnifyGlass.PinPanelHotkey', value), + 'Key to pin/unpin the info panel position' + )); + + container.appendChild(panelHotkeySection.section); + + // ===== Debug Section ===== + const debugSection = createSection('Debug', true); + + debugSection.body.appendChild(createToggle('Debug Mode', + getSettingValue('🔍MagnifyGlass.DebugMode', false), + (checked) => setSettingValue('🔍MagnifyGlass.DebugMode', checked), + 'Enable debug logging and visual indicators' + )); + + container.appendChild(debugSection.section); + + // ===== Button Row ===== + const buttonRow = document.createElement('div'); + buttonRow.style.padding = '16px'; + buttonRow.style.borderTop = '1px solid var(--border-color, #333)'; + buttonRow.style.display = 'flex'; + buttonRow.style.flexDirection = 'column'; + buttonRow.style.gap = '8px'; + + // Reset Position Button + const resetBtn = document.createElement('button'); + resetBtn.className = 'magnify-btn'; + resetBtn.style.width = '100%'; + resetBtn.innerHTML = `${Icons.reset} Reset Position`; + resetBtn.title = `Reset glass position and disable follow cursor. Hotkey: ${resetKey}`; + + resetBtn.addEventListener('click', (e) => { + e.stopPropagation(); + const magnifyGlass = window.comfyUIMagnifyGlass; + if (magnifyGlass && magnifyGlass.resetOffsets) { + magnifyGlass.resetOffsets(); + // Update follow cursor toggle in UI + const followToggles = container.querySelectorAll('.magnify-toggle-row'); + followToggles.forEach(row => { + const label = row.querySelector('label'); + if (label && label.textContent === 'Follow Cursor') { + const toggle = row.querySelector('.magnify-toggle'); + if (toggle) toggle.classList.remove('active'); + } + }); + } + }); + + buttonRow.appendChild(resetBtn); + + // Reset All Settings Button + const resetAllBtn = document.createElement('button'); + resetAllBtn.className = 'magnify-btn magnify-btn-secondary'; + resetAllBtn.style.width = '100%'; + resetAllBtn.innerHTML = `${Icons.reset} Reset All Settings`; + resetAllBtn.title = 'Reset all settings to their default values'; + + resetAllBtn.addEventListener('click', (e) => { + e.stopPropagation(); + + // Confirm with user + if (!confirm('Reset all MagnifyGlass settings to defaults?')) { + return; + } + + // Default glass settings + setSettingValue('🔍MagnifyGlass.ZoomFactor', 300); + setSettingValue('🔍MagnifyGlass.GlassSize', 300); + setSettingValue('🔍MagnifyGlass.GlassShape', 'Rounded Square'); + setSettingValue('🔍MagnifyGlass.GlassPosition', 'Top-Right'); + setSettingValue('🔍MagnifyGlass.TextureFiltering', 'Linear'); + setSettingValue('🔍MagnifyGlass.BorderWidth', 1); + setSettingValue('🔍MagnifyGlass.BorderColor', '#6b7280'); + setSettingValue('🔍MagnifyGlass.BorderEnabled', true); + setSettingValue('🔍MagnifyGlass.FollowCursor', false); + setSettingValue('🔍MagnifyGlass.AlwaysActiveMode', true); + setSettingValue('🔍MagnifyGlass.ActivationKey', 'x'); + setSettingValue('🔍MagnifyGlass.ResetKey', 'o'); + setSettingValue('🔍MagnifyGlass.ToggleFollowCursorKey', 'h'); + setSettingValue('🔍MagnifyGlass.AltRequired', false); + setSettingValue('🔍MagnifyGlass.OffsetStep', 5); + setSettingValue('🔍MagnifyGlass.DebugMode', false); + + // Default panel settings + setSettingValue('🔍MagnifyGlass.InfoPanelEnabled', true); + setSettingValue('🔍MagnifyGlass.InfoPanelPosition', 'Bottom'); + setSettingValue('🔍MagnifyGlass.InfoPanelWidth', 300); + setSettingValue('🔍MagnifyGlass.InfoPanelMaxHeight', 300); + setSettingValue('🔍MagnifyGlass.InfoPanelOpacity', 100); + setSettingValue('🔍MagnifyGlass.InfoPanelTextColor', '#6b7280'); + setSettingValue('🔍MagnifyGlass.InfoPanelAccentColor', '#3b82f6'); + setSettingValue('🔍MagnifyGlass.InfoPanelAnimations', false); + setSettingValue('🔍MagnifyGlass.ShowHoveringControls', true); + setSettingValue('🔍MagnifyGlass.ControlsPosition', 'left'); + setSettingValue('🔍MagnifyGlass.ToggleHotkey', 'i'); + setSettingValue('🔍MagnifyGlass.GlassPreviewToggleHotkey', 'g'); + setSettingValue('🔍MagnifyGlass.PinPanelHotkey', 'u'); + + // Also reset position + const magnifyGlass = window.comfyUIMagnifyGlass; + if (magnifyGlass && magnifyGlass.resetOffsets) { + magnifyGlass.resetOffsets(); + } + }); + + buttonRow.appendChild(resetAllBtn); + container.appendChild(buttonRow); +} diff --git a/src/sidebar/index.ts b/src/sidebar/index.ts new file mode 100644 index 0000000..e6f72cd --- /dev/null +++ b/src/sidebar/index.ts @@ -0,0 +1,8 @@ +/** + * ComfyUI MagnifyGlass - Sidebar Index + * + * Re-exports all sidebar modules. + */ + +export { registerSidebar, initSidebar } from './sidebar'; +export { renderSettingsPanel } from './SidebarSettings'; diff --git a/src/sidebar/sidebar.ts b/src/sidebar/sidebar.ts new file mode 100644 index 0000000..dbb5236 --- /dev/null +++ b/src/sidebar/sidebar.ts @@ -0,0 +1,106 @@ +/** + * ComfyUI MagnifyGlass - Sidebar Entry Point + * + * Registers the MagnifyGlass sidebar tab with ComfyUI. + */ + +// @ts-ignore +import { app } from "/scripts/app.js"; +import { renderSettingsPanel } from './SidebarSettings'; + +/** + * Magnify glass icon SVG + */ +const MAGNIFY_ICON = ``; + +/** Track if sidebar has been registered */ +let sidebarRegistered = false; + +/** + * Load sidebar CSS + */ +function loadSidebarStyles(): void { + if (!document.getElementById('magnify-sidebar-styles')) { + const link = document.createElement('link'); + link.id = 'magnify-sidebar-styles'; + link.rel = 'stylesheet'; + link.type = 'text/css'; + link.href = 'extensions/comfyui-magnifyglass/sidebar.css'; + document.head.appendChild(link); + } +} + +/** + * Render the sidebar content + */ +function renderSidebar(container: HTMLElement): void { + // Load styles + loadSidebarStyles(); + + // Clear existing content to prevent duplicates + container.innerHTML = ''; + + // Create main container + const sidebar = document.createElement('div'); + sidebar.className = 'magnify-sidebar'; + + // Header + const header = document.createElement('div'); + header.className = 'magnify-sidebar-header'; + header.innerHTML = `${MAGNIFY_ICON}

Magnify G.

`; + sidebar.appendChild(header); + + // Content area + const content = document.createElement('div'); + content.className = 'magnify-sidebar-content'; + + // Render settings panel (contains all settings) + renderSettingsPanel(content); + + sidebar.appendChild(content); + container.appendChild(sidebar); +} + +/** + * Register the sidebar tab with ComfyUI + */ +export function registerSidebar(): void { + // Prevent duplicate registration + if (sidebarRegistered) { + return; + } + + // Wait for app to be ready + if (!app.extensionManager) { + console.warn('MagnifyGlass: extensionManager not available, sidebar registration skipped'); + return; + } + + try { + app.extensionManager.registerSidebarTab({ + id: 'magnifyglass', + icon: 'pi pi-search', + title: 'Magnify G.', + tooltip: 'MagnifyGlass Settings', + type: 'custom', + render: (el: HTMLElement) => { + renderSidebar(el); + } + }); + + sidebarRegistered = true; + console.log('MagnifyGlass: Sidebar registered successfully'); + } catch (e) { + console.warn('MagnifyGlass: Failed to register sidebar:', e); + } +} + +/** + * Initialize sidebar (called after app setup) + */ +export function initSidebar(): void { + // Register on next tick to ensure app is ready + setTimeout(() => { + registerSidebar(); + }, 100); +} diff --git a/src/types/comfy-scripts.d.ts b/src/types/comfy-scripts.d.ts new file mode 100644 index 0000000..167ecd9 --- /dev/null +++ b/src/types/comfy-scripts.d.ts @@ -0,0 +1,11 @@ +declare module "../../scripts/app.js" { + export const app: any; +} + +declare module "../../scripts/ui.js" { + export const ui: any; +} + +declare module "../../scripts/api.js" { + export const api: any; +} diff --git a/src/types/comfyui.d.ts b/src/types/comfyui.d.ts new file mode 100644 index 0000000..aace0b5 --- /dev/null +++ b/src/types/comfyui.d.ts @@ -0,0 +1,387 @@ +/** + * ComfyUI Type Definitions + * + * Type definitions for ComfyUI's app, canvas, and graph APIs. + * These types are based on the runtime behavior of ComfyUI. + */ + +// ============================================================================ +// ComfyUI App +// ============================================================================ + +export interface ComfyApp { + graph: ComfyGraph; + canvas: ComfyCanvas; + ui: ComfyUI; + extensionManager: ComfyExtensionManager; + registerExtension(extension: ComfyExtension): void; +} + +export interface ComfyExtension { + name: string; + setup?: () => Promise | void; + init?: () => Promise | void; + commands?: ComfyCommand[]; + keybindings?: ComfyKeybinding[]; + menuCommands?: ComfyMenuCommand[]; + settings?: ComfySetting[]; + bottomPanelTabs?: ComfyBottomPanelTab[]; + aboutPageBadges?: ComfyBadge[]; + getSelectionToolboxCommands?: (selectedItem: unknown) => string[]; +} + +export interface ComfyCommand { + id: string; + label?: string; + icon?: string; + function: () => void; +} + +export interface ComfyKeybinding { + combo: { + key: string; + alt?: boolean; + ctrl?: boolean; + shift?: boolean; + meta?: boolean; + }; + commandId: string; +} + +export interface ComfyMenuCommand { + path: string[]; + commands: string[]; +} + +export interface ComfySetting { + id: string; + name: string; + type: 'text' | 'number' | 'slider' | 'combo' | 'color' | 'boolean'; + defaultValue: unknown; + min?: number; + max?: number; + step?: number; + options?: Array<{ value: unknown; text: string }>; + tooltip?: string; + onChange?: (value: unknown) => void; +} + +export interface ComfyBottomPanelTab { + id: string; + title: string; + type: 'custom'; + render: (el: HTMLElement) => void; +} + +export interface ComfyBadge { + label: string; + url: string; + icon: string; +} + +// ============================================================================ +// ComfyUI UI +// ============================================================================ + +export interface ComfyUI { + settings: ComfySettings; +} + +export interface ComfySettings { + addSetting(setting: ComfySetting): void; + getSettingValue(id: string): unknown; + setSettingValue(id: string, value: unknown): void; +} + +// ============================================================================ +// ComfyUI Extension Manager +// ============================================================================ + +export interface ComfyExtensionManager { + setting: { + get(id: string): unknown; + set(id: string, value: unknown): void; + }; + toast: ComfyToast; + dialog: ComfyDialog; + registerSidebarTab(tab: ComfySidebarTab): void; +} + +export interface ComfySidebarTab { + id: string; + icon: string; + title: string; + tooltip: string; + type: 'custom'; + render: (el: HTMLElement) => void; +} + +export interface ComfyToast { + add(options: ComfyToastOptions): void; + addAlert(message: string): void; +} + +export interface ComfyToastOptions { + severity: 'success' | 'info' | 'warn' | 'error'; + summary: string; + detail: string; + life?: number; +} + +export interface ComfyDialog { + prompt(options: { title: string; message: string }): Promise; + confirm(options: { title: string; message: string }): Promise; +} + +// ============================================================================ +// ComfyUI Graph +// ============================================================================ + +export interface ComfyGraph { + _nodes: ComfyNode[]; + getNodeById(id: number): ComfyNode | null; +} + +export interface ComfyNode { + id: number; + title: string; + type: string; + mode: number; + pos: [number, number]; + size: [number, number]; + flags?: { + collapsed?: boolean; + }; + widgets?: ComfyWidget[]; + inputs?: ComfySlot[]; + outputs?: ComfySlot[]; + properties?: Record; +} + +export interface ComfyWidget { + name: string; + type: string; + value: unknown; + options?: Record; + min?: number; + max?: number; + step?: number; +} + +export interface ComfySlot { + name: string; + type: string; + link?: number | null; + links?: number[]; +} + +// ============================================================================ +// ComfyUI Canvas +// ============================================================================ + +export interface ComfyCanvas { + canvas: HTMLCanvasElement; + ds: { + scale: number; + offset: [number, number]; + }; + node_over: ComfyNode | null; + convertOffsetToCanvasPos?(pos: [number, number]): [number, number]; +} + +// ============================================================================ +// Global Window Extensions +// ============================================================================ + +declare global { + interface Window { + comfyUIMagnifyGlass?: MagnifyGlassInstance; + magnifyGlass?: MagnifyGlassInstance; // Alias used by info panel controls + comfyUIMagnifyGlassExtensions?: unknown[]; + infoPanelManager?: { uiManager?: { updateTheme: (theme: string) => void } }; + } + + var app: ComfyApp; +} + +export interface MagnifyGlassInstance { + state: MagnifyGlassState; + config: MagnifyGlassConfig; + ui: MagnifyGlassUI; + renderer: MagnifyGlassRenderer | null; + lastKnownMousePosition: { x: number; y: number }; + isOverMedia: boolean; + currentMediaElement: HTMLImageElement | HTMLVideoElement | null; + updateMagnifiedView(): void; + applyUiChanges(): void; + toggle(): void; + resetOffsets(): void; +} + +export interface MagnifyGlassState { + active: boolean; + x: number; + y: number; + sourceX: number; + sourceY: number; + sourceWidth: number; + sourceHeight: number; + canvasScale: number; + canvasOffsetX: number; + canvasOffsetY: number; +} + +export interface MagnifyGlassConfig { + zoomFactor: number; + glassSize: number; + borderColor: string; + borderWidth: number; + borderEnabled: boolean; + activationKey: string; + altRequired: boolean; + followCursor: boolean; + debugMode: boolean; + offsetStep: number; + offsetX: number; + offsetY: number; + glassPosition: string; + glassShape: string; + resetKey: string; + textureFiltering: string; + alwaysActiveMode: boolean; + toggleFollowCursorKey: string; +} + +export interface MagnifyGlassUI { + glassDiv: HTMLDivElement | null; + show(): void; + hide(): void; + positionGlass(x: number, y: number): void; +} + +export interface MagnifyGlassRenderer { + updateTextureFiltering(filtering: string): void; +} + +// ============================================================================ +// Info Panel Types +// ============================================================================ + +/** + * Information gathered about the current cursor position. + * Returned by InformationGatherer.gatherInformation() + */ +export interface GatheredInfo { + timestamp: number; + cursor: { + screenX: number; + screenY: number; + canvasX: number; + canvasY: number; + }; + zoom: number; + nodeCount: number; + hoveredNode: NodeInfo | null; + hoveredWidget: WidgetInfo | null; + mediaElement: MediaInfo | null; +} + +/** + * Detailed information about a node. + * Structure matches InformationGatherer.getDetailedNodeInfo() output. + */ +export interface NodeInfo { + id: number; + title: string; + type: string; + mode: string | number; + position: { + x: number; + y: number; + formatted?: string; + } | string; + size: { + width: number; + height: number; + formatted?: string; + } | string; + localPosition?: { + x: number; + y: number; + formatted?: string; + percentage?: { x: string; y: string }; + }; + counts?: { + widgets: number; + inputs: number; + outputs: number; + properties: number; + }; + widgets: ComfyWidget[]; + inputs: ComfyInput[]; + outputs: ComfyOutput[]; + properties: Record; + hoverRegion?: string; +} + +/** + * Information about a widget. + */ +export interface WidgetInfo { + name: string; + type: string; + value: string; + options?: unknown; + min?: number; + max?: number; + step?: number; +} + +/** + * Information about a media element (image or video). + */ +export interface MediaInfo { + type: 'image' | 'video'; + tagName: string; + src: string; + naturalWidth: number; + naturalHeight: number; + displayWidth: number; + displayHeight: number; + aspectRatio: string; + duration?: number; + currentTime?: number; +} + +/** + * A section in the info panel display. + */ +export interface SectionData { + id: string; + title: string; + icon: string; + visible: boolean; + expanded: boolean; + content: ParameterItem[]; +} + +/** + * A parameter item displayed in a section. + */ +export interface ParameterItem { + label: string; + value: unknown; + formattedValue?: string; + highlight?: boolean; +} + +/** + * Settings change tracking. + */ +export interface SettingsChange { + old: unknown; + new: unknown; +} + +export { }; + diff --git a/src/types/vue-shims.d.ts b/src/types/vue-shims.d.ts new file mode 100644 index 0000000..1a9fbc3 --- /dev/null +++ b/src/types/vue-shims.d.ts @@ -0,0 +1,5 @@ +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/tests/e2e/example.spec.ts b/tests/e2e/example.spec.ts new file mode 100644 index 0000000..ba173c5 --- /dev/null +++ b/tests/e2e/example.spec.ts @@ -0,0 +1,12 @@ +import { test, expect } from '@playwright/test'; + +test('magnify glass extension loads', async ({ page }) => { + // This is a placeholder test. + // To test ComfyUI, you would typically need to start the ComfyUI server and navigate to it. + // console.log('This test needs a running ComfyUI instance to verify the extension.'); + + // Example of what we might check if ComfyUI was running locally: + // await page.goto('http://127.0.0.1:8188'); + // await expect(page).toHaveTitle(/ComfyUI/); + // await expect(page.locator('.magnify-glass-icon')).toBeVisible(); // Hypothetical check +}); diff --git a/tests/mocks/app.ts b/tests/mocks/app.ts new file mode 100644 index 0000000..b8c9305 --- /dev/null +++ b/tests/mocks/app.ts @@ -0,0 +1,15 @@ +/** + * Mock for ComfyUI app object + * Used in tests to avoid importing actual ComfyUI modules + */ + +export const app = { + ui: { + settings: { + getSettingValue: (key: string) => undefined, + addSetting: () => { } + } + }, + canvas: null, + graph: null +}; diff --git a/tests/unit/ConfigManager.test.ts b/tests/unit/ConfigManager.test.ts new file mode 100644 index 0000000..faba0e6 --- /dev/null +++ b/tests/unit/ConfigManager.test.ts @@ -0,0 +1,117 @@ +/** + * ConfigManager Unit Tests + * + * Tests for the ConfigManager class which handles settings and offset management. + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +// Mock localStorage +const localStorageMock = (() => { + let store: Record = {}; + return { + getItem: (key: string) => store[key] || null, + setItem: (key: string, value: string) => { store[key] = value; }, + removeItem: (key: string) => { delete store[key]; }, + clear: () => { store = {}; } + }; +})(); + +// Mock global app object for getSettingValue +vi.mock('/scripts/app.js', () => ({ + app: { + ui: { + settings: { + getSettingValue: (key: string) => undefined + } + } + } +})); + +// Apply localStorage mock +Object.defineProperty(globalThis, 'localStorage', { + value: localStorageMock +}); + +// Import after mocks are set up +import { ConfigManager } from '../../src/magnify-glass/ConfigManager'; + +describe('ConfigManager', () => { + let configManager: ConfigManager; + + beforeEach(() => { + localStorageMock.clear(); + configManager = new ConfigManager(); + }); + + describe('constructor', () => { + it('should initialize with default values', () => { + expect(configManager.zoomFactor).toBe(3); // 300 / 100 + expect(configManager.glassSize).toBe(300); + expect(configManager.borderColor).toBe('#6b7280'); + expect(configManager.borderWidth).toBe(1); + expect(configManager.activationKey).toBe('x'); + expect(configManager.altRequired).toBe(false); + expect(configManager.followCursor).toBe(false); + expect(configManager.debugMode).toBe(false); + }); + + it('should initialize offsets to zero', () => { + expect(configManager.offsetX).toBe(0); + expect(configManager.offsetY).toBe(0); + }); + }); + + describe('loadSavedOffsets', () => { + it('should load offsets from localStorage', () => { + localStorageMock.setItem('comfyui_magnify_offset_x', '50'); + localStorageMock.setItem('comfyui_magnify_offset_y', '-25'); + + configManager.loadSavedOffsets(); + + expect(configManager.offsetX).toBe(50); + expect(configManager.offsetY).toBe(-25); + }); + + it('should default to zero if localStorage is empty', () => { + configManager.loadSavedOffsets(); + + expect(configManager.offsetX).toBe(0); + expect(configManager.offsetY).toBe(0); + }); + }); + + describe('saveOffsets', () => { + it('should save offsets to localStorage', () => { + configManager.offsetX = 100; + configManager.offsetY = -50; + + configManager.saveOffsets(); + + expect(localStorageMock.getItem('comfyui_magnify_offset_x')).toBe('100'); + expect(localStorageMock.getItem('comfyui_magnify_offset_y')).toBe('-50'); + }); + }); + + describe('resetOffsets', () => { + it('should reset offsets to zero', () => { + configManager.offsetX = 100; + configManager.offsetY = -50; + + configManager.resetOffsets(); + + expect(configManager.offsetX).toBe(0); + expect(configManager.offsetY).toBe(0); + }); + + it('should save reset offsets to localStorage', () => { + configManager.offsetX = 100; + configManager.offsetY = -50; + + configManager.resetOffsets(); + + expect(localStorageMock.getItem('comfyui_magnify_offset_x')).toBe('0'); + expect(localStorageMock.getItem('comfyui_magnify_offset_y')).toBe('0'); + }); + }); +}); diff --git a/tests/unit/InformationGatherer.test.ts b/tests/unit/InformationGatherer.test.ts new file mode 100644 index 0000000..01386c1 --- /dev/null +++ b/tests/unit/InformationGatherer.test.ts @@ -0,0 +1,158 @@ +/** + * InformationGatherer Unit Tests + * + * Tests for the InformationGatherer class which collects node and canvas information. + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +// Mock ComfyUI app +vi.mock('/scripts/app.js', () => ({ + app: { + graph: { + _nodes: [] + }, + canvas: { + canvas: { + getBoundingClientRect: () => ({ left: 0, top: 0, width: 800, height: 600, right: 800, bottom: 600 }) + }, + node_over: null, + ds: { scale: 1, offset: [0, 0] } + } + } +})); + +// Mock window.comfyUIMagnifyGlass +const mockMagnifyGlass = { + state: { + x: 100, + y: 100, + canvasScale: 1, + canvasOffsetX: 0, + canvasOffsetY: 0, + sourceX: 50, + sourceY: 50, + sourceWidth: 100, + sourceHeight: 100 + }, + config: { + zoomFactor: 3, + offsetX: 0, + offsetY: 0 + }, + lastKnownMousePosition: { x: 200, y: 200 }, + isOverMedia: false, + currentMediaElement: null +}; + +Object.defineProperty(globalThis, 'window', { + value: { + comfyUIMagnifyGlass: mockMagnifyGlass + }, + writable: true +}); + +import { InformationGatherer } from '../../src/info-panel/InformationGatherer'; + +describe('InformationGatherer', () => { + let gatherer: InformationGatherer; + + beforeEach(() => { + gatherer = new InformationGatherer(); + // Reset mock state + mockMagnifyGlass.isOverMedia = false; + mockMagnifyGlass.currentMediaElement = null; + }); + + describe('constructor', () => { + it('should create an instance', () => { + expect(gatherer).toBeDefined(); + }); + }); + + describe('gatherInformation', () => { + it('should return a GatheredInfo object with required fields', () => { + const info = gatherer.gatherInformation(); + + expect(info).toHaveProperty('timestamp'); + expect(info).toHaveProperty('cursor'); + expect(info).toHaveProperty('zoom'); + expect(info).toHaveProperty('nodeCount'); + expect(info).toHaveProperty('hoveredNode'); + expect(info).toHaveProperty('hoveredWidget'); + expect(info).toHaveProperty('mediaElement'); + }); + + it('should return cursor position from magnifyGlass state', () => { + const info = gatherer.gatherInformation(); + + expect(info.cursor.screenX).toBe(200); + expect(info.cursor.screenY).toBe(200); + expect(info.cursor.canvasX).toBe(100); + expect(info.cursor.canvasY).toBe(100); + }); + + it('should return zoom from canvas scale', () => { + const info = gatherer.gatherInformation(); + expect(info.zoom).toBe(1); + }); + + it('should return null for hoveredNode when no node is hovered', () => { + const info = gatherer.gatherInformation(); + expect(info.hoveredNode).toBeNull(); + }); + }); + + describe('formatValue', () => { + it('should format null values', () => { + expect(gatherer.formatValue(null)).toBe('null'); + }); + + it('should format undefined values', () => { + expect(gatherer.formatValue(undefined)).toBe('undefined'); + }); + + it('should format string values as-is', () => { + expect(gatherer.formatValue('hello world')).toBe('hello world'); + }); + + it('should format integer values without decimals', () => { + expect(gatherer.formatValue(42)).toBe('42'); + }); + + it('should format float values with 3 decimals', () => { + expect(gatherer.formatValue(3.14159)).toBe('3.142'); + }); + + it('should format boolean values', () => { + expect(gatherer.formatValue(true)).toBe('true'); + expect(gatherer.formatValue(false)).toBe('false'); + }); + + it('should format arrays with length', () => { + expect(gatherer.formatValue([1, 2, 3])).toBe('Array(3)'); + }); + + it('should format objects as "Object"', () => { + expect(gatherer.formatValue({ key: 'value' })).toBe('Object'); + }); + }); + + describe('getNodeModeText', () => { + it('should return "Always" for mode 0', () => { + expect(gatherer.getNodeModeText(0)).toBe('Always'); + }); + + it('should return "On Event" for mode 1', () => { + expect(gatherer.getNodeModeText(1)).toBe('On Event'); + }); + + it('should return "Never" for mode 2', () => { + expect(gatherer.getNodeModeText(2)).toBe('Never'); + }); + + it('should return fallback for unknown modes', () => { + expect(gatherer.getNodeModeText(99)).toBe('Mode 99'); + }); + }); +}); diff --git a/tests/unit/MagnifierState.test.ts b/tests/unit/MagnifierState.test.ts new file mode 100644 index 0000000..031e397 --- /dev/null +++ b/tests/unit/MagnifierState.test.ts @@ -0,0 +1,103 @@ +/** + * MagnifierState Unit Tests + * + * Tests for the MagnifierState class which manages magnifier state. + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { MagnifierState } from '../../src/magnify-glass/MagnifierState'; + +describe('MagnifierState', () => { + let state: MagnifierState; + + beforeEach(() => { + state = new MagnifierState(); + }); + + describe('constructor', () => { + it('should initialize as inactive', () => { + expect(state.active).toBe(false); + }); + + it('should initialize cursor at origin', () => { + expect(state.x).toBe(0); + expect(state.y).toBe(0); + }); + + it('should initialize source dimensions to zero', () => { + expect(state.sourceX).toBe(0); + expect(state.sourceY).toBe(0); + expect(state.sourceWidth).toBe(0); + expect(state.sourceHeight).toBe(0); + }); + + it('should initialize canvas scale to 1', () => { + expect(state.canvasScale).toBe(1.0); + }); + + it('should initialize canvas offsets to zero', () => { + expect(state.canvasOffsetX).toBe(0); + expect(state.canvasOffsetY).toBe(0); + }); + + it('should not be scheduled for render initially', () => { + expect(state.isRenderScheduled).toBe(false); + }); + }); + + describe('reset', () => { + it('should reset all state values to defaults', () => { + // Modify state + state.active = true; + state.x = 100; + state.y = 200; + state.sourceX = 50; + state.sourceY = 75; + state.canvasScale = 2.5; + + // Reset + state.reset(); + + // Verify defaults + expect(state.active).toBe(false); + expect(state.x).toBe(0); + expect(state.y).toBe(0); + expect(state.sourceX).toBe(0); + expect(state.sourceY).toBe(0); + expect(state.canvasScale).toBe(1.0); + }); + + it('should reset wasActivatedBefore flag', () => { + state.wasActivatedBefore = true; + + state.reset(); + + expect(state.wasActivatedBefore).toBe(false); + }); + }); + + describe('state mutations', () => { + it('should allow setting active state', () => { + state.active = true; + expect(state.active).toBe(true); + }); + + it('should allow setting cursor position', () => { + state.x = 150; + state.y = 250; + + expect(state.x).toBe(150); + expect(state.y).toBe(250); + }); + + it('should allow setting canvas transform values', () => { + state.canvasScale = 3.0; + state.canvasOffsetX = -100; + state.canvasOffsetY = -200; + + expect(state.canvasScale).toBe(3.0); + expect(state.canvasOffsetX).toBe(-100); + expect(state.canvasOffsetY).toBe(-200); + }); + }); +}); diff --git a/tests/unit/StateManager.test.ts b/tests/unit/StateManager.test.ts new file mode 100644 index 0000000..93bee55 --- /dev/null +++ b/tests/unit/StateManager.test.ts @@ -0,0 +1,268 @@ +/** + * StateManager Unit Tests + * + * Tests for the StateManager class which manages info panel state. + */ + +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; + +// Mock localStorage +const localStorageMock = (() => { + let store: Record = {}; + return { + getItem: (key: string) => store[key] || null, + setItem: (key: string, value: string) => { store[key] = value; }, + removeItem: (key: string) => { delete store[key]; }, + clear: () => { store = {}; } + }; +})(); + +Object.defineProperty(globalThis, 'localStorage', { + value: localStorageMock +}); + +// Mock window +Object.defineProperty(globalThis, 'window', { + value: { + matchMedia: () => ({ + addListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + }), + infoPanelManager: null + }, + writable: true +}); + +// Mock document +Object.defineProperty(globalThis, 'document', { + value: { + body: { + getAttribute: () => null, + className: '' + }, + documentElement: { + getAttribute: () => null, + className: '' + }, + querySelector: () => null, + querySelectorAll: () => [], + addEventListener: vi.fn() + }, + writable: true +}); + +// Mock getComputedStyle +(globalThis as any).getComputedStyle = () => ({ + backgroundColor: 'rgb(30, 30, 30)' +}); + +// Mock MutationObserver +(globalThis as any).MutationObserver = class { + observe = vi.fn(); + disconnect = vi.fn(); + takeRecords = vi.fn(); +}; + +// Mock app object for settings +vi.mock('/scripts/app.js', () => ({ + app: { + ui: { + settings: { + getSettingValue: (key: string) => undefined + } + } + } +})); + +import { StateManager } from '../../src/info-panel/StateManager'; + +describe('StateManager', () => { + let stateManager: StateManager; + + beforeEach(() => { + vi.useFakeTimers(); + stateManager = new StateManager(); + }); + + afterEach(() => { + vi.useRealTimers(); + stateManager.cleanup(); + }); + + describe('constructor', () => { + it('should initialize with default state values', () => { + expect(stateManager.state.isPanelVisible).toBe(false); + expect(stateManager.state.isPanelMinimized).toBe(false); + expect(stateManager.state.isPanelPinned).toBe(false); + expect(stateManager.state.isPanelLocked).toBe(false); + }); + + it('should initialize glass preview as visible', () => { + expect(stateManager.state.isGlassPreviewVisible).toBe(true); + }); + + it('should initialize with node section expanded', () => { + expect(stateManager.state.expandedSections.has('node')).toBe(true); + }); + + it('should initialize currentInfo as null', () => { + expect(stateManager.state.currentInfo).toBeNull(); + }); + + it('should initialize with dark theme as default', () => { + expect(stateManager.state.currentTheme).toBe('dark'); + }); + }); + + describe('togglePanelVisibility', () => { + it('should toggle panel visibility', () => { + expect(stateManager.state.isPanelVisible).toBe(false); + + stateManager.togglePanelVisibility(); + expect(stateManager.state.isPanelVisible).toBe(true); + + stateManager.togglePanelVisibility(); + expect(stateManager.state.isPanelVisible).toBe(false); + }); + + it('should return new visibility state', () => { + expect(stateManager.togglePanelVisibility()).toBe(true); + expect(stateManager.togglePanelVisibility()).toBe(false); + }); + }); + + describe('toggleGlassPreview', () => { + it('should toggle glass preview visibility', () => { + expect(stateManager.state.isGlassPreviewVisible).toBe(true); + + stateManager.toggleGlassPreview(); + expect(stateManager.state.isGlassPreviewVisible).toBe(false); + + stateManager.toggleGlassPreview(); + expect(stateManager.state.isGlassPreviewVisible).toBe(true); + }); + }); + + describe('togglePinning', () => { + it('should toggle pinned state', () => { + expect(stateManager.state.isPanelPinned).toBe(false); + + stateManager.togglePinning(); + expect(stateManager.state.isPanelPinned).toBe(true); + + stateManager.togglePinning(); + expect(stateManager.state.isPanelPinned).toBe(false); + }); + + it('should clear locked state when unpinning', () => { + stateManager.state.isPanelPinned = true; + stateManager.state.isPanelLocked = true; + + stateManager.togglePinning(); // Unpin + + expect(stateManager.state.isPanelLocked).toBe(false); + }); + }); + + describe('toggleLocking', () => { + it('should not allow locking when not pinned', () => { + stateManager.state.isPanelPinned = false; + + const result = stateManager.toggleLocking(); + + expect(result).toBe(false); + expect(stateManager.state.isPanelLocked).toBe(false); + }); + + it('should toggle locking when pinned', () => { + stateManager.state.isPanelPinned = true; + + stateManager.toggleLocking(); + expect(stateManager.state.isPanelLocked).toBe(true); + + stateManager.toggleLocking(); + expect(stateManager.state.isPanelLocked).toBe(false); + }); + }); + + describe('toggleMinimized', () => { + it('should toggle minimized state', () => { + expect(stateManager.state.isPanelMinimized).toBe(false); + + stateManager.toggleMinimized(); + expect(stateManager.state.isPanelMinimized).toBe(true); + + stateManager.toggleMinimized(); + expect(stateManager.state.isPanelMinimized).toBe(false); + }); + }); + + describe('toggleSection', () => { + it('should not toggle node section (always expanded)', () => { + const result = stateManager.toggleSection('node'); + expect(result).toBe(false); + }); + + it('should toggle other sections', () => { + expect(stateManager.state.expandedSections.has('media')).toBe(false); + + stateManager.toggleSection('media'); + expect(stateManager.state.expandedSections.has('media')).toBe(true); + + stateManager.toggleSection('media'); + expect(stateManager.state.expandedSections.has('media')).toBe(false); + }); + }); + + describe('setPinnedPosition', () => { + it('should set pinned position', () => { + stateManager.setPinnedPosition(100, 200); + + expect(stateManager.state.pinnedPosition).toEqual({ x: 100, y: 200 }); + expect(stateManager.state.lastPinnedPosition).toEqual({ x: 100, y: 200 }); + }); + }); + + describe('setCurrentInfo', () => { + it('should set current info', () => { + const mockInfo = { + timestamp: Date.now(), + cursor: { screenX: 0, screenY: 0, canvasX: 0, canvasY: 0 }, + zoom: 1, + nodeCount: 5, + hoveredNode: null, + hoveredWidget: null, + mediaElement: null + }; + + stateManager.setCurrentInfo(mockInfo); + + expect(stateManager.state.currentInfo).toEqual(mockInfo); + }); + }); + + describe('expandNodeSections', () => { + it('should expand all node-related sections', () => { + stateManager.expandNodeSections(); + + expect(stateManager.state.expandedSections.has('hoveredNode')).toBe(true); + expect(stateManager.state.expandedSections.has('node')).toBe(true); + expect(stateManager.state.expandedSections.has('cursor')).toBe(true); + expect(stateManager.state.expandedSections.has('canvas')).toBe(true); + expect(stateManager.state.expandedSections.has('magnifier')).toBe(true); + }); + }); + + describe('collapseNodeSections', () => { + it('should collapse node-related sections', () => { + stateManager.state.expandedSections.add('hoveredNode'); + stateManager.state.expandedSections.add('widget'); + + stateManager.collapseNodeSections(); + + expect(stateManager.state.expandedSections.has('hoveredNode')).toBe(false); + expect(stateManager.state.expandedSections.has('widget')).toBe(false); + }); + }); +}); diff --git a/tests/unit/utils.test.ts b/tests/unit/utils.test.ts new file mode 100644 index 0000000..111e24e --- /dev/null +++ b/tests/unit/utils.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from 'vitest'; +import { clamp, normalizeColor } from '../../src/shared/utils'; + +describe('Shared Utilities', () => { + describe('clamp', () => { + it('should clamp value within range', () => { + expect(clamp(150, 0, 100)).toBe(100); + expect(clamp(-50, 0, 100)).toBe(0); + expect(clamp(50, 0, 100)).toBe(50); + }); + }); + + describe('normalizeColor', () => { + it('should add # prefix if missing', () => { + expect(normalizeColor('ffffff')).toBe('#ffffff'); + expect(normalizeColor('000')).toBe('#000'); + }); + + it('should keep # prefix if present', () => { + expect(normalizeColor('#ff0000')).toBe('#ff0000'); + }); + + it('should handle falsy values', () => { + expect(normalizeColor('')).toBe(''); + expect(normalizeColor(null)).toBe(''); + expect(normalizeColor(undefined)).toBe(''); + }); + }); +}); diff --git a/traffic_stats.json b/traffic_stats.json new file mode 100644 index 0000000..858b9f2 --- /dev/null +++ b/traffic_stats.json @@ -0,0 +1,11 @@ +{ + "clones": { + "count": 21, + "uniques": 20 + }, + "views": { + "count": 21, + "uniques": 16 + }, + "timestamp": "2026-01-02T07:22:08.007Z" +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1f86930 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,44 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "allowSyntheticDefaultImports": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "baseUrl": ".", + "paths": { + "@shared/*": [ + "src/shared/*" + ], + "@magnify-glass/*": [ + "src/magnify-glass/*" + ], + "@info-panel/*": [ + "src/info-panel/*" + ] + } + }, + "include": [ + "src/**/*.ts", + "src/**/*.d.ts" + ], + "exclude": [ + "node_modules", + "dist", + "web" + ] +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..4a8b539 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,42 @@ +/// +import { defineConfig } from 'vite'; +import { resolve } from 'path'; + +export default defineConfig({ + build: { + lib: { + entry: { + 'magnify_glass': resolve(__dirname, 'src/magnify_glass.ts'), + 'magnify_info_panel': resolve(__dirname, 'src/magnify_info_panel.ts') + }, + formats: ['es'], + fileName: (format, entryName) => `${entryName}.js` + }, + outDir: 'web', + emptyOutDir: false, // Keep existing files + rollupOptions: { + external: [ + /^\/scripts\//, // Match /scripts/ (Absolute path) + /^\.\.\/\.\.\/scripts\//, // Match ../../scripts/ (Browser path - deprecated) + ], + output: { + preserveModules: true, + preserveModulesRoot: 'src', + entryFileNames: '[name].js', + } + }, + sourcemap: true, + minify: false // Keep readable for debugging + }, + resolve: { + alias: { + '@shared': resolve(__dirname, 'src/shared'), + '@magnify-glass': resolve(__dirname, 'src/magnify-glass'), + '@info-panel': resolve(__dirname, 'src/info-panel') + } + }, + test: { + environment: 'happy-dom', + globals: true + } +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..20964e8 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from 'vitest/config'; +import path from 'path'; + +export default defineConfig({ + test: { + globals: true, + environment: 'happy-dom', + include: ['tests/unit/**/*.test.ts'], + exclude: ['node_modules', 'web'], + coverage: { + reporter: ['text', 'html'], + include: ['src/**/*.ts'], + exclude: ['src/types/**'] + } + }, + resolve: { + alias: { + '/scripts/app.js': path.resolve(__dirname, 'tests/mocks/app.ts') + } + } +}); + diff --git a/web/info-panel-old.css b/web/info-panel-old.css new file mode 100644 index 0000000..ce9d79d --- /dev/null +++ b/web/info-panel-old.css @@ -0,0 +1,664 @@ +/** + * ComfyUI MagnifyGlass - Info Panel Styles + * + * Complete CSS styles for the info panel component. + */ + +/* SVG Icon Styling */ +.magnify-info-panel svg, +.floating-controls svg { + width: 1em; + height: 1em; + vertical-align: middle; + fill: none; + stroke: currentColor; + pointer-events: none; + /* Allow clicks to pass through to button */ +} + +.header-icon svg { + width: 18px; + height: 18px; +} + +.section-icon svg { + width: 18px; + height: 18px; +} + +.control-btn svg { + width: 14px; + height: 14px; +} + +.expand-icon svg { + width: 12px; + height: 12px; +} + +.empty-state-icon svg { + width: 36px; + height: 36px; + stroke-width: 1.5; +} + +.magnify-info-panel { + border-radius: 12px; + backdrop-filter: blur(20px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.1); + overflow: hidden; + font-size: 16px; + line-height: 1.6; + pointer-events: auto; +} + +.magnify-info-panel * { + pointer-events: auto; +} + +.magnify-info-panel.theme-dark { + background: linear-gradient(135deg, rgba(0, 0, 0, 0.96), rgba(10, 10, 15, 0.96)); + color: var(--info-panel-text-color, #e0e0e0); + border: 1px solid rgba(100, 100, 120, 0.4); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 16px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(12px); +} + +.magnify-info-panel.theme-light { + background: linear-gradient(135deg, rgba(250, 250, 255, 0.96), rgba(240, 240, 250, 0.96)); + color: var(--info-panel-text-color, #2a2a2a); + border: 1px solid rgba(200, 200, 220, 0.5); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 2px 16px rgba(0, 0, 0, 0.05); + backdrop-filter: blur(12px); +} + +/* Solarized Theme - warm, muted tones */ +.magnify-info-panel.theme-solarized { + background: linear-gradient(135deg, rgba(0, 43, 54, 0.96), rgba(7, 54, 66, 0.96)); + color: var(--info-panel-text-color, #839496); + border: 1px solid rgba(88, 110, 117, 0.5); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 16px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(12px); + --info-panel-accent-color: #268bd2; +} + +/* Arc Theme - modern blue-gray */ +.magnify-info-panel.theme-arc { + background: linear-gradient(135deg, rgba(56, 60, 74, 0.96), rgba(64, 69, 82, 0.96)); + color: var(--info-panel-text-color, #d3dae3); + border: 1px solid rgba(90, 100, 120, 0.5); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25), 0 2px 16px rgba(0, 0, 0, 0.15); + backdrop-filter: blur(12px); + --info-panel-accent-color: #5294e2; +} + +/* Nord Theme - cool arctic colors */ +.magnify-info-panel.theme-nord { + background: linear-gradient(135deg, rgba(46, 52, 64, 0.96), rgba(59, 66, 82, 0.96)); + color: var(--info-panel-text-color, #d8dee9); + border: 1px solid rgba(76, 86, 106, 0.5); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25), 0 2px 16px rgba(0, 0, 0, 0.15); + backdrop-filter: blur(12px); + --info-panel-accent-color: #88c0d0; +} + +/* GitHub Theme - clean, professional */ +.magnify-info-panel.theme-github { + background: linear-gradient(135deg, rgba(255, 255, 255, 0.96), rgba(246, 248, 250, 0.96)); + color: var(--info-panel-text-color, #24292e); + border: 1px solid rgba(225, 228, 232, 0.8); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 2px 16px rgba(0, 0, 0, 0.05); + backdrop-filter: blur(12px); + --info-panel-accent-color: #0366d6; +} + +.magnify-info-panel.visible { + transform: translateY(0) !important; +} + +.magnify-info-panel.panel-pinned { + cursor: grab; + box-shadow: 0 0 0 2px rgba(255, 215, 0, 0.5), 0 20px 40px rgba(0, 0, 0, 0.3); +} + +.magnify-info-panel.panel-locked { + cursor: default !important; + box-shadow: 0 0 0 2px rgba(255, 69, 0, 0.5), 0 20px 40px rgba(0, 0, 0, 0.3); +} + +.magnify-info-panel.panel-locked::after { + content: '📌'; + position: absolute; + top: 8px; + right: 8px; + font-size: 14px; + opacity: 0.7; + pointer-events: none; + z-index: 1; +} + +.magnify-info-panel.panel-pinned.panel-dragging { + cursor: grabbing; + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5), 0 0 0 2px rgba(255, 215, 0, 0.7); + transform: scale(1.02) !important; + z-index: 100001 !important; + transition: none !important; +} + +.magnify-info-panel.panel-pinned::before { + content: ''; + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + background: linear-gradient(45deg, rgba(255, 215, 0, 0.05) 0%, transparent 100%); + pointer-events: none; + border-radius: inherit; +} + +.panel-dragging * { + pointer-events: none !important; + user-select: none !important; + transition: none !important; +} + +.magnify-info-panel.panel-minimized .panel-content { + max-height: 0 !important; + overflow: hidden; + transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.floating-controls { + position: fixed; + display: none; + /* Hidden initially - JS will show when positioned */ + padding: 4px; + background: rgba(40, 40, 50, 0.95); + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 4px; + opacity: 1; + z-index: 100000; + backdrop-filter: blur(8px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); +} + +.floating-controls.vertical-layout { + flex-direction: column; + gap: 4px; +} + +.floating-controls.horizontal-layout { + flex-direction: row; + gap: 4px; +} + +.control-btn { + background: rgba(60, 60, 70, 0.8); + border: 1px solid rgba(255, 255, 255, 0.2); + width: 28px; + height: 28px; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: #ddd; + border-radius: 3px; + transition: background-color 0.15s ease; +} + +.control-btn:hover { + background: rgba(80, 80, 90, 0.9); + color: white; +} + +.control-btn:active { + background: rgba(100, 100, 110, 0.9); +} + +.control-btn:disabled { + background: rgba(40, 40, 50, 0.5); + color: #666; + cursor: not-allowed; + opacity: 0.5; +} + +.control-btn.active { + background: rgba(255, 200, 50, 0.3); + border-color: rgba(255, 200, 50, 0.6); + color: #ffcc33; +} + +.control-btn.lock-btn.active { + background: rgba(255, 69, 0, 0.3); + border-color: rgba(255, 69, 0, 0.6); + color: #ff4500; +} + +.panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 18px; + background: linear-gradient(135deg, rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.05)); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + user-select: none; + cursor: grab; + transition: background 0.15s ease; +} + +.panel-header:hover { + background: linear-gradient(135deg, rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.08)); +} + +.panel-header:active { + cursor: grabbing; +} + +.header-content { + display: flex; + align-items: center; + gap: 10px; +} + +.header-icon { + font-size: 18px; + opacity: 0.9; +} + +.header-title { + font-weight: 600; + font-size: 18px; +} + +.header-subtitle { + font-size: 14px; + opacity: 0.7; + font-style: italic; +} + +.header-controls { + display: flex; + gap: 6px; +} + +.minimize-btn { + background: rgba(255, 255, 255, 0.1); + border: none; + border-radius: 6px; + color: inherit; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s; + font-weight: bold; + font-size: 16px; +} + +.minimize-btn:hover { + background: rgba(255, 255, 255, 0.2); + transform: scale(1.05); +} + +.minimize-btn:active { + transform: scale(0.95); +} + +.panel-content { + padding: 0; + max-height: calc(100% - 60px); + overflow-y: auto; + scrollbar-width: thin; +} + +.panel-content::-webkit-scrollbar { + width: 8px; +} + +.panel-content::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.1); +} + +.panel-content::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.3); + border-radius: 3px; +} + +.info-section { + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + margin-bottom: 8px; + border-radius: 10px; + overflow: visible; + background: rgba(255, 255, 255, 0.02); + transition: all 0.3s ease; +} + +.info-section:hover { + background: rgba(255, 255, 255, 0.04); + transform: translateY(-1px); +} + +.section-header { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 20px; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + font-weight: 500; + border-radius: 8px; + margin: 3px; + position: relative; + overflow: hidden; +} + +.section-header::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.05) 50%, transparent 100%); + transform: translateX(-100%); + transition: transform 0.6s ease; +} + +.section-header:hover::before { + transform: translateX(100%); +} + +.section-header:hover { + background: rgba(255, 255, 255, 0.08); + transform: translateX(2px); + box-shadow: inset 2px 0 0 rgba(116, 185, 255, 0.5); +} + +.section-header:active { + background: rgba(255, 255, 255, 0.12); + transform: translateX(1px); +} + +.section-header.expanded { + background: rgba(116, 185, 255, 0.1); + border-left: 2px solid var(--info-panel-accent-color, #74b9ff); +} + +.info-section[data-section="node"] { + background: linear-gradient(135deg, rgba(116, 185, 255, 0.05), rgba(160, 212, 104, 0.05)); + border: 1px solid rgba(116, 185, 255, 0.2); + border-radius: 8px; + margin: 6px; +} + +.info-section[data-section="node"] .section-header { + background: linear-gradient(135deg, rgba(116, 185, 255, 0.15), rgba(160, 212, 104, 0.15)); + border-radius: 8px 8px 0 0; + cursor: default; +} + +.info-section[data-section="node"] .section-header:hover { + transform: none; + box-shadow: none; +} + +.info-section[data-section="node"] .section-title { + color: var(--info-panel-accent-color, #74b9ff); + font-weight: 700; +} + +.info-section[data-section="inspector"] { + background: linear-gradient(135deg, rgba(100, 150, 255, 0.05), rgba(74, 144, 226, 0.05)); + border: 1px solid rgba(100, 150, 255, 0.2); + border-radius: 8px; + margin: 6px; +} + +.info-section[data-section="inspector"] .section-header { + background: linear-gradient(135deg, rgba(100, 150, 255, 0.15), rgba(74, 144, 226, 0.15)); + border-radius: 8px 8px 0 0; +} + +.info-section[data-section="inspector"] .section-title { + color: #6496ff; + font-weight: 700; +} + +.info-section[data-section="media"] { + background: linear-gradient(135deg, rgba(253, 203, 110, 0.05), rgba(255, 215, 0, 0.05)); + border: 1px solid rgba(253, 203, 110, 0.2); + border-radius: 8px; + margin: 6px; +} + +.info-section[data-section="media"] .section-header { + background: linear-gradient(135deg, rgba(253, 203, 110, 0.15), rgba(255, 215, 0, 0.15)); + border-radius: 8px 8px 0 0; +} + +.info-section[data-section="media"] .section-title { + color: var(--info-panel-accent-color, #fdcb6e); + font-weight: 700; +} + +.section-icon { + font-size: 18px; + width: 22px; + text-align: center; +} + +.section-title { + flex: 1; + font-size: 16px; + font-weight: 600; + letter-spacing: 0.3px; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.section-badge { + background: rgba(100, 150, 255, 0.2); + color: #8bb3ff; + padding: 3px 10px; + border-radius: 10px; + font-size: 13px; + font-weight: 500; +} + +.expand-icon { + font-size: 14px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + color: rgba(255, 255, 255, 0.6); + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + background: rgba(255, 255, 255, 0.05); + margin-left: auto; +} + +.section-header:hover .expand-icon { + color: var(--info-panel-accent-color, #74b9ff); + background: rgba(116, 185, 255, 0.15); + transform: scale(1.1); +} + +.section-header.expanded .expand-icon { + transform: rotate(90deg) scale(1.1); + color: var(--info-panel-accent-color, #74b9ff); + background: rgba(116, 185, 255, 0.2); +} + +.section-content { + max-height: 0; + overflow: visible; + transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease; + opacity: 0; +} + +.section-content.expanded { + max-height: 2000px; + opacity: 1; + overflow: visible; +} + +.section-body { + padding: 0 18px 16px 24px; +} + +.info-row { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 10px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + transition: all 0.2s ease; + border-radius: 4px; + margin: 0 -8px; + padding-left: 8px; + padding-right: 8px; +} + +.info-row:last-child { + border-bottom: none; +} + +.info-row:hover { + background: rgba(255, 255, 255, 0.03); + border-bottom-color: rgba(255, 255, 255, 0.08); +} + +.info-label { + font-weight: 500; + opacity: 0.85; + font-size: 15px; + min-width: 80px; + padding-right: 12px; + line-height: 1.5; +} + +.info-value { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 14px; + color: var(--info-panel-accent-color, #a0d468); + background: rgba(160, 212, 104, 0.12); + padding: 4px 10px; + border-radius: 6px; + max-width: 100%; + word-wrap: break-word; + word-break: break-word; + hyphens: auto; + line-height: 1.4; + text-align: left; + white-space: pre-wrap; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + transition: all 0.2s ease; +} + +.info-value:hover { + background: rgba(160, 212, 104, 0.18); + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +.info-value.long-text { + white-space: pre-wrap; + word-wrap: break-word; + max-width: 100%; + line-height: 1.4; +} + +.empty-state { + text-align: center; + padding: 50px 25px; + opacity: 0.6; +} + +.empty-state-icon { + font-size: 36px; + margin-bottom: 15px; + opacity: 0.5; +} + +.empty-state-text { + font-size: 16px; + font-style: italic; +} + +/* Copy to clipboard functionality */ +.info-value[title]:hover::after { + content: '📋'; + position: absolute; + right: -20px; + top: 50%; + transform: translateY(-50%); + font-size: 12px; + opacity: 0.7; +} + +/* Smooth loading states */ +.section-body.loading { + opacity: 0.5; + pointer-events: none; +} + +.section-body.loading::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 20px; + height: 20px; + border: 2px solid transparent; + border-top: 2px solid var(--info-panel-accent-color, #74b9ff); + border-radius: 50%; + animation: spin 1s linear infinite; + transform: translate(-50%, -50%); +} + +@keyframes spin { + to { + transform: translate(-50%, -50%) rotate(360deg); + } +} + +/* Enhanced scrollbar for better UX */ +.section-body::-webkit-scrollbar { + width: 4px; +} + +.section-body::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + border-radius: 2px; +} + +.section-body::-webkit-scrollbar-thumb { + background: var(--info-panel-accent-color, #74b9ff); + border-radius: 2px; + opacity: 0.6; +} + +.section-body::-webkit-scrollbar-thumb:hover { + opacity: 1; +} + +@keyframes pulse { + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.7; + } +} + +.pulse { + animation: pulse 2s infinite; +} \ No newline at end of file diff --git a/web/info-panel.css b/web/info-panel.css new file mode 100644 index 0000000..864e259 --- /dev/null +++ b/web/info-panel.css @@ -0,0 +1,674 @@ +/** + * ComfyUI MagnifyGlass - Info Panel Styles + * + * Complete CSS styles for the info panel component. + */ + +/* SVG Icon Styling */ +.magnify-info-panel svg, +.floating-controls svg { + width: 1em; + height: 1em; + vertical-align: middle; + fill: none; + stroke: currentColor; + pointer-events: none; + /* Allow clicks to pass through to button */ +} + +.header-icon svg { + width: 18px; + height: 18px; +} + +.section-icon svg { + width: 18px; + height: 18px; +} + +.control-btn svg { + width: 14px; + height: 14px; +} + +.expand-icon svg { + width: 12px; + height: 12px; +} + +.empty-state-icon svg { + width: 36px; + height: 36px; + stroke-width: 1.5; +} + +.magnify-info-panel { + border-radius: 12px; + backdrop-filter: blur(20px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.1); + overflow: hidden; + font-size: 16px; + line-height: 1.6; + pointer-events: auto; + display: flex; + flex-direction: column; +} + +.magnify-info-panel * { + pointer-events: auto; +} + +.magnify-info-panel.theme-dark { + background: linear-gradient(135deg, rgba(0, 0, 0, 0.96), rgba(10, 10, 15, 0.96)); + color: var(--info-panel-text-color, #e0e0e0); + border: 1px solid rgba(100, 100, 120, 0.4); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 16px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(12px); +} + +.magnify-info-panel.theme-light { + background: linear-gradient(135deg, rgba(250, 250, 255, 0.96), rgba(240, 240, 250, 0.96)); + color: var(--info-panel-text-color, #2a2a2a); + border: 1px solid rgba(200, 200, 220, 0.5); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 2px 16px rgba(0, 0, 0, 0.05); + backdrop-filter: blur(12px); +} + +/* Solarized Theme - warm, muted tones */ +.magnify-info-panel.theme-solarized { + background: linear-gradient(135deg, rgba(0, 43, 54, 0.96), rgba(7, 54, 66, 0.96)); + color: var(--info-panel-text-color, #839496); + border: 1px solid rgba(88, 110, 117, 0.5); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 16px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(12px); + --info-panel-accent-color: #268bd2; +} + +/* Arc Theme - modern blue-gray */ +.magnify-info-panel.theme-arc { + background: linear-gradient(135deg, rgba(56, 60, 74, 0.96), rgba(64, 69, 82, 0.96)); + color: var(--info-panel-text-color, #d3dae3); + border: 1px solid rgba(90, 100, 120, 0.5); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25), 0 2px 16px rgba(0, 0, 0, 0.15); + backdrop-filter: blur(12px); + --info-panel-accent-color: #5294e2; +} + +/* Nord Theme - cool arctic colors */ +.magnify-info-panel.theme-nord { + background: linear-gradient(135deg, rgba(46, 52, 64, 0.96), rgba(59, 66, 82, 0.96)); + color: var(--info-panel-text-color, #d8dee9); + border: 1px solid rgba(76, 86, 106, 0.5); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25), 0 2px 16px rgba(0, 0, 0, 0.15); + backdrop-filter: blur(12px); + --info-panel-accent-color: #88c0d0; +} + +/* GitHub Theme - clean, professional */ +.magnify-info-panel.theme-github { + background: linear-gradient(135deg, rgba(255, 255, 255, 0.96), rgba(246, 248, 250, 0.96)); + color: var(--info-panel-text-color, #24292e); + border: 1px solid rgba(225, 228, 232, 0.8); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 2px 16px rgba(0, 0, 0, 0.05); + backdrop-filter: blur(12px); + --info-panel-accent-color: #0366d6; +} + +.magnify-info-panel.visible { + transform: translateY(0) !important; +} + +.magnify-info-panel.panel-pinned { + cursor: grab; + box-shadow: 0 0 0 2px rgba(255, 215, 0, 0.5), 0 20px 40px rgba(0, 0, 0, 0.3); +} + +.magnify-info-panel.panel-locked { + cursor: default !important; + box-shadow: 0 0 0 2px rgba(255, 69, 0, 0.5), 0 20px 40px rgba(0, 0, 0, 0.3); +} + +.magnify-info-panel.panel-locked::after { + content: '📌'; + position: absolute; + top: 8px; + right: 8px; + font-size: 14px; + opacity: 0.7; + pointer-events: none; + z-index: 1; +} + +.magnify-info-panel.panel-pinned.panel-dragging { + cursor: grabbing; + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5), 0 0 0 2px rgba(255, 215, 0, 0.7); + transform: scale(1.02) !important; + z-index: 100001 !important; + transition: none !important; +} + +.magnify-info-panel.panel-pinned::before { + content: ''; + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + background: linear-gradient(45deg, rgba(255, 215, 0, 0.05) 0%, transparent 100%); + pointer-events: none; + border-radius: inherit; +} + +.panel-dragging * { + pointer-events: none !important; + user-select: none !important; + transition: none !important; +} + +.magnify-info-panel.panel-minimized { + height: auto !important; +} + +.magnify-info-panel.panel-minimized .panel-content { + max-height: 0 !important; + height: 0 !important; + overflow: hidden; + padding: 0 !important; + margin: 0 !important; + transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.floating-controls { + position: fixed; + display: none; + /* Hidden initially - JS will show when positioned */ + padding: 4px; + background: rgba(40, 40, 50, 0.95); + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 4px; + opacity: 1; + z-index: 100000; + backdrop-filter: blur(8px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); +} + +.floating-controls.vertical-layout { + flex-direction: column; + gap: 4px; +} + +.floating-controls.horizontal-layout { + flex-direction: row; + gap: 4px; +} + +.control-btn { + background: rgba(60, 60, 70, 0.8); + border: 1px solid rgba(255, 255, 255, 0.2); + width: 28px; + height: 28px; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: #ddd; + border-radius: 3px; + transition: background-color 0.15s ease; +} + +.control-btn:hover { + background: rgba(80, 80, 90, 0.9); + color: white; +} + +.control-btn:active { + background: rgba(100, 100, 110, 0.9); +} + +.control-btn:disabled { + background: rgba(40, 40, 50, 0.5); + color: #666; + cursor: not-allowed; + opacity: 0.5; +} + +.control-btn.active { + background: rgba(255, 200, 50, 0.3); + border-color: rgba(255, 200, 50, 0.6); + color: #ffcc33; +} + +.control-btn.lock-btn.active { + background: rgba(255, 69, 0, 0.3); + border-color: rgba(255, 69, 0, 0.6); + color: #ff4500; +} + +.panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 18px; + background: linear-gradient(135deg, rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.05)); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + user-select: none; + cursor: grab; + transition: background 0.15s ease; +} + +.panel-header:hover { + background: linear-gradient(135deg, rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.08)); +} + +.panel-header:active { + cursor: grabbing; +} + +.header-content { + display: flex; + align-items: center; + gap: 10px; +} + +.header-icon { + font-size: 18px; + opacity: 0.9; +} + +.header-title { + font-weight: 600; + font-size: 18px; +} + +.header-subtitle { + font-size: 14px; + opacity: 0.7; + font-style: italic; +} + +.header-controls { + display: flex; + gap: 6px; +} + +.minimize-btn { + background: rgba(255, 255, 255, 0.1); + border: none; + border-radius: 6px; + color: inherit; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s; + font-weight: bold; + font-size: 16px; +} + +.minimize-btn:hover { + background: rgba(255, 255, 255, 0.2); + transform: scale(1.05); +} + +.minimize-btn:active { + transform: scale(0.95); +} + +.panel-content { + padding: 0; + flex: 1; + min-height: 0; + overflow-y: auto; + scrollbar-width: thin; +} + +.panel-content::-webkit-scrollbar { + width: 8px; +} + +.panel-content::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.1); +} + +.panel-content::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.3); + border-radius: 3px; +} + +.info-section { + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + margin-bottom: 8px; + border-radius: 10px; + overflow: visible; + background: rgba(255, 255, 255, 0.02); + transition: all 0.3s ease; +} + +.info-section:hover { + background: rgba(255, 255, 255, 0.04); + transform: translateY(-1px); +} + +.section-header { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 20px; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + font-weight: 500; + border-radius: 8px; + margin: 3px; + position: relative; + overflow: hidden; +} + +.section-header::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.05) 50%, transparent 100%); + transform: translateX(-100%); + transition: transform 0.6s ease; +} + +.section-header:hover::before { + transform: translateX(100%); +} + +.section-header:hover { + background: rgba(255, 255, 255, 0.08); + transform: translateX(2px); + box-shadow: inset 2px 0 0 rgba(116, 185, 255, 0.5); +} + +.section-header:active { + background: rgba(255, 255, 255, 0.12); + transform: translateX(1px); +} + +.section-header.expanded { + background: rgba(116, 185, 255, 0.1); + border-left: 2px solid var(--info-panel-accent-color, #74b9ff); +} + +.info-section[data-section="node"] { + background: linear-gradient(135deg, rgba(116, 185, 255, 0.05), rgba(160, 212, 104, 0.05)); + border: 1px solid rgba(116, 185, 255, 0.2); + border-radius: 8px; + margin: 6px; +} + +.info-section[data-section="node"] .section-header { + background: linear-gradient(135deg, rgba(116, 185, 255, 0.15), rgba(160, 212, 104, 0.15)); + border-radius: 8px 8px 0 0; + cursor: default; +} + +.info-section[data-section="node"] .section-header:hover { + transform: none; + box-shadow: none; +} + +.info-section[data-section="node"] .section-title { + color: var(--info-panel-accent-color, #74b9ff); + font-weight: 700; +} + +.info-section[data-section="inspector"] { + background: linear-gradient(135deg, rgba(100, 150, 255, 0.05), rgba(74, 144, 226, 0.05)); + border: 1px solid rgba(100, 150, 255, 0.2); + border-radius: 8px; + margin: 6px; +} + +.info-section[data-section="inspector"] .section-header { + background: linear-gradient(135deg, rgba(100, 150, 255, 0.15), rgba(74, 144, 226, 0.15)); + border-radius: 8px 8px 0 0; +} + +.info-section[data-section="inspector"] .section-title { + color: #6496ff; + font-weight: 700; +} + +.info-section[data-section="media"] { + background: linear-gradient(135deg, rgba(253, 203, 110, 0.05), rgba(255, 215, 0, 0.05)); + border: 1px solid rgba(253, 203, 110, 0.2); + border-radius: 8px; + margin: 6px; +} + +.info-section[data-section="media"] .section-header { + background: linear-gradient(135deg, rgba(253, 203, 110, 0.15), rgba(255, 215, 0, 0.15)); + border-radius: 8px 8px 0 0; +} + +.info-section[data-section="media"] .section-title { + color: var(--info-panel-accent-color, #fdcb6e); + font-weight: 700; +} + +.section-icon { + font-size: 18px; + width: 22px; + text-align: center; +} + +.section-title { + flex: 1; + font-size: 16px; + font-weight: 600; + letter-spacing: 0.3px; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.section-badge { + background: rgba(100, 150, 255, 0.2); + color: #8bb3ff; + padding: 3px 10px; + border-radius: 10px; + font-size: 13px; + font-weight: 500; +} + +.expand-icon { + font-size: 14px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + color: rgba(255, 255, 255, 0.6); + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + background: rgba(255, 255, 255, 0.05); + margin-left: auto; +} + +.section-header:hover .expand-icon { + color: var(--info-panel-accent-color, #74b9ff); + background: rgba(116, 185, 255, 0.15); + transform: scale(1.1); +} + +.section-header.expanded .expand-icon { + transform: rotate(90deg) scale(1.1); + color: var(--info-panel-accent-color, #74b9ff); + background: rgba(116, 185, 255, 0.2); +} + +.section-content { + max-height: 0; + overflow: visible; + transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease; + opacity: 0; +} + +.section-content.expanded { + max-height: 2000px; + opacity: 1; + overflow: visible; +} + +.section-body { + padding: 0 18px 16px 24px; +} + +.info-row { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 10px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + transition: all 0.2s ease; + border-radius: 4px; + margin: 0 -8px; + padding-left: 8px; + padding-right: 8px; +} + +.info-row:last-child { + border-bottom: none; +} + +.info-row:hover { + background: rgba(255, 255, 255, 0.03); + border-bottom-color: rgba(255, 255, 255, 0.08); +} + +.info-label { + font-weight: 500; + opacity: 0.85; + font-size: 15px; + min-width: 80px; + padding-right: 12px; + line-height: 1.5; +} + +.info-value { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 14px; + color: var(--info-panel-accent-color, #a0d468); + background: rgba(160, 212, 104, 0.12); + padding: 4px 10px; + border-radius: 6px; + max-width: 100%; + word-wrap: break-word; + word-break: break-word; + hyphens: auto; + line-height: 1.4; + text-align: left; + white-space: pre-wrap; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + transition: all 0.2s ease; +} + +.info-value:hover { + background: rgba(160, 212, 104, 0.18); + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +.info-value.long-text { + white-space: pre-wrap; + word-wrap: break-word; + max-width: 100%; + line-height: 1.4; +} + +.empty-state { + text-align: center; + padding: 50px 25px; + opacity: 0.6; +} + +.empty-state-icon { + font-size: 36px; + margin-bottom: 15px; + opacity: 0.5; +} + +.empty-state-text { + font-size: 16px; + font-style: italic; +} + +/* Copy to clipboard functionality */ +.info-value[title]:hover::after { + content: '📋'; + position: absolute; + right: -20px; + top: 50%; + transform: translateY(-50%); + font-size: 12px; + opacity: 0.7; +} + +/* Smooth loading states */ +.section-body.loading { + opacity: 0.5; + pointer-events: none; +} + +.section-body.loading::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 20px; + height: 20px; + border: 2px solid transparent; + border-top: 2px solid var(--info-panel-accent-color, #74b9ff); + border-radius: 50%; + animation: spin 1s linear infinite; + transform: translate(-50%, -50%); +} + +@keyframes spin { + to { + transform: translate(-50%, -50%) rotate(360deg); + } +} + +/* Enhanced scrollbar for better UX */ +.section-body::-webkit-scrollbar { + width: 4px; +} + +.section-body::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + border-radius: 2px; +} + +.section-body::-webkit-scrollbar-thumb { + background: var(--info-panel-accent-color, #74b9ff); + border-radius: 2px; + opacity: 0.6; +} + +.section-body::-webkit-scrollbar-thumb:hover { + opacity: 1; +} + +@keyframes pulse { + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.7; + } +} + +.pulse { + animation: pulse 2s infinite; +} \ No newline at end of file diff --git a/web/info-panel/EventManager.js b/web/info-panel/EventManager.js new file mode 100644 index 0000000..2807d90 --- /dev/null +++ b/web/info-panel/EventManager.js @@ -0,0 +1,153 @@ +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +import { isUserTyping } from "../shared/utils.js"; +import { Logger } from "../shared/logger.js"; +class EventManager { + constructor(stateManager, panelElement, positionManager, callbacks) { + __publicField(this, "stateManager"); + __publicField(this, "panelElement"); + __publicField(this, "positionManager"); + __publicField(this, "callbacks"); + __publicField(this, "dragCleanup"); + __publicField(this, "boundHandleMouseMove"); + __publicField(this, "boundHandleKeyDown"); + __publicField(this, "boundHandleKeyUp"); + this.stateManager = stateManager; + this.panelElement = panelElement; + this.positionManager = positionManager; + this.callbacks = callbacks || null; + this.dragCleanup = null; + this.boundHandleMouseMove = this.handleMouseMove.bind(this); + this.boundHandleKeyDown = this.handleKeyDown.bind(this); + this.boundHandleKeyUp = this.handleKeyUp.bind(this); + this.setupEventListeners(); + } + setupEventListeners() { + this.setupPanelEvents(); + this.setupHotkeyEvents(); + this.setupDragEvents(); + this.setupHoverEvents(); + } + setupPanelEvents() { + if (!this.panelElement) return; + } + setupHotkeyEvents() { + document.addEventListener("keydown", this.boundHandleKeyDown); + } + setupDragEvents() { + if (!this.panelElement) return; + let isDragging = false; + let rafId = null; + let targetX = 0; + let targetY = 0; + const startDrag = (e) => { + if (!this.panelElement) return; + const target = e.target; + const header = target.closest(".panel-header"); + if (!header) return; + if (target.closest("button")) return; + if (this.stateManager.state.isPanelLocked) return; + if (!this.stateManager.state.isPanelPinned) return; + e.preventDefault(); + e.stopPropagation(); + isDragging = true; + const startX = e.clientX; + const startY = e.clientY; + const rect = this.panelElement.getBoundingClientRect(); + const startLeft = rect.left; + const startTop = rect.top; + this.panelElement.style.cursor = "grabbing"; + this.panelElement.style.opacity = "0.9"; + this.panelElement.style.transition = "none"; + document.body.style.cursor = "grabbing"; + document.body.style.userSelect = "none"; + const onMouseMove = (moveEvent) => { + var _a, _b; + if (!isDragging) return; + const dx = moveEvent.clientX - startX; + const dy = moveEvent.clientY - startY; + targetX = startLeft + dx; + targetY = startTop + dy; + const panelWidth = ((_a = this.panelElement) == null ? void 0 : _a.offsetWidth) || 0; + const panelHeight = ((_b = this.panelElement) == null ? void 0 : _b.offsetHeight) || 0; + targetX = Math.max(0, Math.min(targetX, window.innerWidth - panelWidth)); + targetY = Math.max(0, Math.min(targetY, window.innerHeight - panelHeight)); + if (rafId === null) { + rafId = requestAnimationFrame(() => { + if (this.panelElement && isDragging) { + this.panelElement.style.left = `${targetX}px`; + this.panelElement.style.top = `${targetY}px`; + } + rafId = null; + }); + } + }; + const onMouseUp = () => { + isDragging = false; + if (rafId !== null) { + cancelAnimationFrame(rafId); + rafId = null; + } + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + if (this.panelElement) { + this.panelElement.style.cursor = ""; + this.panelElement.style.opacity = ""; + this.panelElement.style.transition = ""; + this.panelElement.style.left = `${targetX}px`; + this.panelElement.style.top = `${targetY}px`; + this.stateManager.state.pinnedPosition = { x: targetX, y: targetY }; + } + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + }; + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + }; + this.panelElement.addEventListener("mousedown", startDrag); + this.dragCleanup = () => { + if (this.panelElement) this.panelElement.removeEventListener("mousedown", startDrag); + }; + } + setupHoverEvents() { + if (!this.panelElement) return; + this.panelElement.addEventListener("mouseenter", () => { + this.stateManager.state.isPanelHovered = true; + this.stateManager.clearAutoExpandTimer(); + }); + this.panelElement.addEventListener("mouseleave", () => { + this.stateManager.state.isPanelHovered = false; + if (!this.stateManager.state.isHoveringNode) { + this.stateManager.scheduleAutoCollapse(); + } + }); + } + handleKeyDown(e) { + var _a, _b; + if (isUserTyping()) return; + const settings = this.stateManager.state.settings; + const key = e.key.toLowerCase(); + if (key === settings["🔍MagnifyGlass.ToggleHotkey"].toLowerCase()) { + Logger.debug("Toggle visibility hotkey matched!"); + e.preventDefault(); + (_a = this.callbacks) == null ? void 0 : _a.toggleVisibility(); + } + if (key === settings["🔍MagnifyGlass.PinPanelHotkey"].toLowerCase()) { + e.preventDefault(); + (_b = this.callbacks) == null ? void 0 : _b.togglePin(); + } + } + handleMouseMove(e) { + } + handleKeyUp(e) { + } + cleanup() { + document.removeEventListener("keydown", this.boundHandleKeyDown); + if (this.dragCleanup) this.dragCleanup(); + } +} +export { + EventManager +}; +//# sourceMappingURL=EventManager.js.map diff --git a/web/info-panel/EventManager.js.map b/web/info-panel/EventManager.js.map new file mode 100644 index 0000000..843c71e --- /dev/null +++ b/web/info-panel/EventManager.js.map @@ -0,0 +1 @@ +{"version":3,"file":"EventManager.js","sources":["../../src/info-panel/EventManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Info Panel Event Manager (TypeScript)\n * \n * Complete Event Manager extracted from magnify_info_panel.js\n * Handles all event binding and delegation for the info panel.\n */\n\nimport { StateManager } from './StateManager';\nimport { UIManager } from './UIManager';\nimport { PositionManager } from './PositionManager';\nimport { isUserTyping } from '../shared/utils';\nimport { Logger } from '../shared/logger';\n\nexport interface EventCallbacks {\n toggleVisibility: () => void;\n togglePin: () => void;\n updatePosition: () => void;\n}\n\n/**\n * Event Manager class.\n * Handles all event listeners for the info panel.\n */\nexport class EventManager {\n stateManager: StateManager;\n panelElement: HTMLElement | null;\n positionManager: PositionManager;\n callbacks: EventCallbacks | null;\n\n dragCleanup: (() => void) | null;\n\n boundHandleMouseMove: (e: MouseEvent) => void;\n boundHandleKeyDown: (e: KeyboardEvent) => void;\n boundHandleKeyUp: (e: KeyboardEvent) => void;\n\n constructor(stateManager: StateManager, panelElement: HTMLElement | null, positionManager: PositionManager, callbacks?: EventCallbacks) {\n this.stateManager = stateManager;\n this.panelElement = panelElement;\n this.positionManager = positionManager;\n this.callbacks = callbacks || null;\n this.dragCleanup = null;\n\n this.boundHandleMouseMove = this.handleMouseMove.bind(this);\n this.boundHandleKeyDown = this.handleKeyDown.bind(this);\n this.boundHandleKeyUp = this.handleKeyUp.bind(this);\n\n this.setupEventListeners();\n }\n\n setupEventListeners(): void {\n this.setupPanelEvents();\n this.setupHotkeyEvents();\n this.setupDragEvents();\n this.setupHoverEvents();\n }\n\n setupPanelEvents(): void {\n if (!this.panelElement) return;\n\n // Vue handles most internal events now.\n // We can keep specific global-ish listeners if needed here.\n }\n\n setupHotkeyEvents(): void {\n document.addEventListener('keydown', this.boundHandleKeyDown);\n }\n\n setupDragEvents(): void {\n if (!this.panelElement) return;\n\n let isDragging = false;\n let rafId: number | null = null;\n let targetX = 0;\n let targetY = 0;\n\n const startDrag = (e: MouseEvent) => {\n if (!this.panelElement) return;\n const target = e.target as HTMLElement;\n\n // Only allow dragging from header (expand clickable area)\n const header = target.closest('.panel-header');\n if (!header) return;\n if (target.closest('button')) return;\n\n // Button 2 (pin icon, data-action=\"lock\") prevents dragging\n if (this.stateManager.state.isPanelLocked) return;\n\n // Also prevent dragging if we are \"Following Glass\" (not pinned to screen)\n // This prevents fighting with the auto-positioner and accidental drags\n if (!this.stateManager.state.isPanelPinned) return;\n\n e.preventDefault();\n e.stopPropagation();\n\n isDragging = true;\n\n const startX = e.clientX;\n const startY = e.clientY;\n const rect = this.panelElement.getBoundingClientRect();\n const startLeft = rect.left;\n const startTop = rect.top;\n\n // Visual feedback - dragging state\n this.panelElement.style.cursor = 'grabbing';\n this.panelElement.style.opacity = '0.9';\n this.panelElement.style.transition = 'none'; // Disable transitions during drag\n document.body.style.cursor = 'grabbing';\n document.body.style.userSelect = 'none';\n\n const onMouseMove = (moveEvent: MouseEvent) => {\n if (!isDragging) return;\n\n const dx = moveEvent.clientX - startX;\n const dy = moveEvent.clientY - startY;\n\n targetX = startLeft + dx;\n targetY = startTop + dy;\n\n // Keep within viewport bounds\n const panelWidth = this.panelElement?.offsetWidth || 0;\n const panelHeight = this.panelElement?.offsetHeight || 0;\n targetX = Math.max(0, Math.min(targetX, window.innerWidth - panelWidth));\n targetY = Math.max(0, Math.min(targetY, window.innerHeight - panelHeight));\n\n // Use RAF for smooth updates\n if (rafId === null) {\n rafId = requestAnimationFrame(() => {\n if (this.panelElement && isDragging) {\n this.panelElement.style.left = `${targetX}px`;\n this.panelElement.style.top = `${targetY}px`;\n }\n rafId = null;\n });\n }\n };\n\n const onMouseUp = () => {\n isDragging = false;\n\n // Cancel any pending RAF\n if (rafId !== null) {\n cancelAnimationFrame(rafId);\n rafId = null;\n }\n\n document.removeEventListener('mousemove', onMouseMove);\n document.removeEventListener('mouseup', onMouseUp);\n\n // Restore visual state\n if (this.panelElement) {\n this.panelElement.style.cursor = '';\n this.panelElement.style.opacity = '';\n this.panelElement.style.transition = '';\n\n // Final position update\n this.panelElement.style.left = `${targetX}px`;\n this.panelElement.style.top = `${targetY}px`;\n\n // Save pinned position\n this.stateManager.state.pinnedPosition = { x: targetX, y: targetY };\n }\n\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n };\n\n document.addEventListener('mousemove', onMouseMove);\n document.addEventListener('mouseup', onMouseUp);\n };\n\n this.panelElement.addEventListener('mousedown', startDrag);\n\n this.dragCleanup = () => {\n if (this.panelElement) this.panelElement.removeEventListener('mousedown', startDrag);\n };\n }\n\n setupHoverEvents(): void {\n if (!this.panelElement) return;\n\n this.panelElement.addEventListener('mouseenter', () => {\n this.stateManager.state.isPanelHovered = true;\n this.stateManager.clearAutoExpandTimer();\n });\n\n this.panelElement.addEventListener('mouseleave', () => {\n this.stateManager.state.isPanelHovered = false;\n if (!this.stateManager.state.isHoveringNode) {\n this.stateManager.scheduleAutoCollapse();\n }\n });\n }\n\n handleKeyDown(e: KeyboardEvent): void {\n if (isUserTyping()) return;\n\n const settings = this.stateManager.state.settings;\n const key = e.key.toLowerCase();\n // console.log(`[InfoPanel] KeyDown: ${key}`);\n\n // Toggle visibility\n if (key === (settings[\"🔍MagnifyGlass.ToggleHotkey\"] as string).toLowerCase()) {\n Logger.debug('Toggle visibility hotkey matched!');\n e.preventDefault();\n this.callbacks?.toggleVisibility();\n }\n\n // Toggle Pin\n if (key === (settings[\"🔍MagnifyGlass.PinPanelHotkey\"] as string).toLowerCase()) {\n e.preventDefault();\n this.callbacks?.togglePin();\n }\n }\n\n handleMouseMove(e: MouseEvent): void {\n // Placeholder\n }\n\n handleKeyUp(e: KeyboardEvent): void {\n // Placeholder\n }\n\n cleanup(): void {\n document.removeEventListener('keydown', this.boundHandleKeyDown);\n if (this.dragCleanup) this.dragCleanup();\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/info-panel/InfoPanel.js b/web/info-panel/InfoPanel.js new file mode 100644 index 0000000..a467b8d --- /dev/null +++ b/web/info-panel/InfoPanel.js @@ -0,0 +1,135 @@ +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +import "/scripts/app.js"; +import { registerPanelSettings } from "../shared/settings/panelSettings.js"; +import { Logger } from "../shared/logger.js"; +import { StateManager } from "./StateManager.js"; +import { UIManager } from "./UIManager.js"; +import { PositionManager } from "./PositionManager.js"; +import { EventManager } from "./EventManager.js"; +import { InformationGatherer } from "./InformationGatherer.js"; +class InfoPanel { + constructor(magnifyGlass) { + __publicField(this, "magnifyGlass"); + __publicField(this, "stateManager"); + __publicField(this, "uiManager"); + __publicField(this, "positionManager"); + __publicField(this, "eventManager"); + __publicField(this, "informationGatherer"); + this.magnifyGlass = magnifyGlass; + this.stateManager = new StateManager(); + this.uiManager = new UIManager(this.stateManager); + this.informationGatherer = new InformationGatherer(); + this.positionManager = new PositionManager(this.stateManager, this.uiManager.elements.panel); + this.eventManager = new EventManager(this.stateManager, this.uiManager.elements.panel, this.positionManager, { + toggleVisibility: () => { + if (this.stateManager.state.isPanelVisible) { + this.uiManager.hide(); + } else { + this.uiManager.show(); + } + }, + togglePin: () => { + this.stateManager.togglePinning(); + this.uiManager.updatePinnedState(); + }, + updatePosition: () => this.positionManager.positionPanel() + }); + this.hookIntoMagnifyGlass(); + registerPanelSettings(this.stateManager, this.uiManager, this.positionManager); + Logger.info("Info Panel initialized successfully"); + } + hookIntoMagnifyGlass() { + const originalUpdateMagnifiedView = this.magnifyGlass.updateMagnifiedView.bind(this.magnifyGlass); + this.magnifyGlass.updateMagnifiedView = (() => { + originalUpdateMagnifiedView(); + if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] && this.magnifyGlass.state.active) { + this.scheduleInfoUpdate(); + } + }).bind(this); + const originalShow = this.magnifyGlass.ui.show.bind(this.magnifyGlass.ui); + const originalHide = this.magnifyGlass.ui.hide.bind(this.magnifyGlass.ui); + this.magnifyGlass.ui.show = (() => { + originalShow(); + if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"]) { + if (this.stateManager.state.wasPanelVisibleBeforeHide) { + this.uiManager.show(); + setTimeout(() => { + this.positionManager.positionPanel(); + this.positionManager.positionFloatingControls(this.uiManager.elements.controls); + }, 10); + } else { + const showControls = this.stateManager.state.settings["🔍MagnifyGlass.ShowHoveringControls"] !== false; + if (showControls && this.uiManager.elements.controls) { + this.uiManager.elements.controls.style.display = "flex"; + } + this.uiManager.updateControlStates(); + setTimeout(() => { + this.positionManager.positionFloatingControls(this.uiManager.elements.controls); + }, 10); + } + } + }).bind(this); + this.magnifyGlass.ui.hide = (() => { + this.stateManager.state.wasPanelVisibleBeforeHide = this.stateManager.state.isPanelVisible; + originalHide(); + this.uiManager.hide(); + if (this.uiManager.elements.controls) { + this.uiManager.elements.controls.style.display = "none"; + } + }).bind(this); + } + scheduleInfoUpdate() { + if (this.stateManager.state.updateScheduled) return; + this.stateManager.state.updateScheduled = true; + requestAnimationFrame(() => { + this.updateInfo(); + this.stateManager.state.updateScheduled = false; + }); + } + updateInfo() { + const settings = this.stateManager.state.settings; + const isActive = this.magnifyGlass.state.active; + if (!settings["🔍MagnifyGlass.InfoPanelEnabled"] || !isActive) { + if (this.uiManager.elements.controls && this.uiManager.elements.controls.style.display !== "none") { + this.uiManager.elements.controls.style.display = "none"; + } + return; + } + const info = this.informationGatherer.gatherInformation(); + this.stateManager.setCurrentInfo(info); + this.uiManager.displayInfo(info); + this.positionManager.positionPanel(); + this.positionManager.positionFloatingControls(this.uiManager.elements.controls); + if (info.hoveredNode) { + this.stateManager.state.isHoveringNode = true; + this.stateManager.expandNodeSections(); + if (this.stateManager.state.lastNodeId !== info.hoveredNode.id) { + this.stateManager.state.lastNodeId = info.hoveredNode.id; + } + } else { + this.stateManager.state.isHoveringNode = false; + if (this.stateManager.state.lastNodeId !== null) { + this.stateManager.state.lastNodeId = null; + if (!this.stateManager.state.isPanelHovered) { + this.stateManager.scheduleAutoCollapse(); + } + } + } + } + updateSettings() { + this.stateManager.updateSettings(); + this.uiManager.applyStyles(); + if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] && this.magnifyGlass.state.active) { + this.uiManager.show(); + this.positionManager.positionPanel(); + } else { + this.uiManager.hide(); + } + } +} +export { + InfoPanel +}; +//# sourceMappingURL=InfoPanel.js.map diff --git a/web/info-panel/InfoPanel.js.map b/web/info-panel/InfoPanel.js.map new file mode 100644 index 0000000..15acc9b --- /dev/null +++ b/web/info-panel/InfoPanel.js.map @@ -0,0 +1 @@ +{"version":3,"file":"InfoPanel.js","sources":["../../src/info-panel/InfoPanel.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Info Panel Orchestrator (TypeScript)\n * \n * Complete Info Panel Manager extracted from magnify_info_panel.js\n * Main class that orchestrates all info panel components.\n */\n\nimport type { MagnifyGlassInstance } from '../types/comfyui';\nimport { registerPanelSettings } from '../shared/settings';\nimport { Logger } from '../shared/logger';\nimport { StateManager } from './StateManager';\nimport { UIManager } from './UIManager';\nimport { PositionManager } from './PositionManager';\nimport { EventManager } from './EventManager';\nimport { InformationGatherer } from './InformationGatherer';\n\n/**\n * Professional Info Panel Manager.\n * Orchestrates all the other components for the info panel.\n */\nexport class InfoPanel {\n magnifyGlass: MagnifyGlassInstance;\n stateManager: StateManager;\n uiManager: UIManager;\n positionManager: PositionManager;\n eventManager: EventManager;\n informationGatherer: InformationGatherer;\n\n constructor(magnifyGlass: MagnifyGlassInstance) {\n this.magnifyGlass = magnifyGlass;\n\n // Initialize Managers\n this.stateManager = new StateManager();\n this.uiManager = new UIManager(this.stateManager);\n this.informationGatherer = new InformationGatherer();\n\n // Position Manager needs the UI elements\n this.positionManager = new PositionManager(this.stateManager, this.uiManager.elements.panel!);\n\n // Event Manager needs state and UI manager\n this.eventManager = new EventManager(this.stateManager, this.uiManager.elements.panel!, this.positionManager, {\n toggleVisibility: () => {\n if (this.stateManager.state.isPanelVisible) {\n this.uiManager.hide();\n } else {\n this.uiManager.show();\n }\n },\n togglePin: () => {\n this.stateManager.togglePinning();\n this.uiManager.updatePinnedState();\n },\n updatePosition: () => this.positionManager.positionPanel()\n });\n\n this.hookIntoMagnifyGlass();\n\n // Register settings with ComfyUI\n registerPanelSettings(this.stateManager, this.uiManager, this.positionManager);\n\n Logger.info('Info Panel initialized successfully');\n }\n\n hookIntoMagnifyGlass(): void {\n // Hook into magnify glass update cycle\n const originalUpdateMagnifiedView = this.magnifyGlass.updateMagnifiedView.bind(this.magnifyGlass);\n\n this.magnifyGlass.updateMagnifiedView = (() => {\n originalUpdateMagnifiedView();\n\n if (this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelEnabled\"] && this.magnifyGlass.state.active) {\n this.scheduleInfoUpdate();\n }\n }).bind(this);\n\n // Hook into show/hide\n const originalShow = this.magnifyGlass.ui.show.bind(this.magnifyGlass.ui);\n const originalHide = this.magnifyGlass.ui.hide.bind(this.magnifyGlass.ui);\n\n this.magnifyGlass.ui.show = (() => {\n originalShow();\n if (this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelEnabled\"]) {\n // Restore state: If it was visible before hide, make it visible now.\n // Otherwise only show controls.\n if (this.stateManager.state.wasPanelVisibleBeforeHide) {\n this.uiManager.show();\n\n // Position properly\n setTimeout(() => {\n this.positionManager.positionPanel();\n this.positionManager.positionFloatingControls(this.uiManager.elements.controls);\n }, 10);\n } else {\n // Do NOT force panel visible. Only show controls.\n const showControls = this.stateManager.state.settings[\"🔍MagnifyGlass.ShowHoveringControls\"] !== false;\n if (showControls && this.uiManager.elements.controls) {\n this.uiManager.elements.controls.style.display = 'flex';\n }\n\n // Ensure states are updated\n this.uiManager.updateControlStates();\n\n // Position controls after showing (relative to glass since panel is hidden)\n setTimeout(() => {\n this.positionManager.positionFloatingControls(this.uiManager.elements.controls);\n }, 10);\n }\n }\n }).bind(this);\n\n this.magnifyGlass.ui.hide = (() => {\n // Save state before hiding\n this.stateManager.state.wasPanelVisibleBeforeHide = this.stateManager.state.isPanelVisible;\n\n originalHide();\n this.uiManager.hide();\n // Explicitly hide floating controls when glass is disabled\n if (this.uiManager.elements.controls) {\n this.uiManager.elements.controls.style.display = 'none';\n }\n }).bind(this);\n }\n\n scheduleInfoUpdate(): void {\n if (this.stateManager.state.updateScheduled) return;\n\n this.stateManager.state.updateScheduled = true;\n requestAnimationFrame(() => {\n this.updateInfo();\n this.stateManager.state.updateScheduled = false;\n });\n }\n\n updateInfo(): void {\n const settings = this.stateManager.state.settings;\n const isActive = this.magnifyGlass.state.active;\n\n // Safety: If disabled or inactive, ensure hidden and return\n if (!settings[\"🔍MagnifyGlass.InfoPanelEnabled\"] || !isActive) {\n if (this.uiManager.elements.controls && this.uiManager.elements.controls.style.display !== 'none') {\n this.uiManager.elements.controls.style.display = 'none';\n }\n return;\n }\n\n const info = this.informationGatherer.gatherInformation();\n this.stateManager.setCurrentInfo(info);\n\n // Update the UI\n this.uiManager.displayInfo(info);\n\n this.positionManager.positionPanel();\n this.positionManager.positionFloatingControls(this.uiManager.elements.controls);\n\n // Handle node hover state logic if needed for internal state\n if (info.hoveredNode) {\n this.stateManager.state.isHoveringNode = true;\n this.stateManager.expandNodeSections();\n if (this.stateManager.state.lastNodeId !== info.hoveredNode.id) {\n this.stateManager.state.lastNodeId = info.hoveredNode.id;\n }\n } else {\n this.stateManager.state.isHoveringNode = false;\n if (this.stateManager.state.lastNodeId !== null) {\n this.stateManager.state.lastNodeId = null;\n if (!this.stateManager.state.isPanelHovered) {\n this.stateManager.scheduleAutoCollapse();\n }\n }\n }\n }\n\n updateSettings(): void {\n const changes = this.stateManager.updateSettings();\n this.uiManager.applyStyles();\n\n if (this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelEnabled\"] && this.magnifyGlass.state.active) {\n this.uiManager.show();\n // Ensure position is updated after potential dimension changes\n this.positionManager.positionPanel();\n } else {\n this.uiManager.hide();\n }\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/info-panel/InformationGatherer.js b/web/info-panel/InformationGatherer.js new file mode 100644 index 0000000..c9c3786 --- /dev/null +++ b/web/info-panel/InformationGatherer.js @@ -0,0 +1,275 @@ +class InformationGatherer { + constructor() { + } + gatherInformation() { + var _a, _b; + const magnifyGlass = window.comfyUIMagnifyGlass; + if (!magnifyGlass) { + return { + timestamp: Date.now(), + cursor: { screenX: 0, screenY: 0, canvasX: 0, canvasY: 0 }, + zoom: 1, + nodeCount: 0, + hoveredNode: null, + hoveredWidget: null, + mediaElement: null + }; + } + const info = { + timestamp: Date.now(), + cursor: { + screenX: magnifyGlass.lastKnownMousePosition.x, + screenY: magnifyGlass.lastKnownMousePosition.y, + canvasX: magnifyGlass.state.x, + canvasY: magnifyGlass.state.y + }, + zoom: magnifyGlass.state.canvasScale, + nodeCount: ((_b = (_a = app.graph) == null ? void 0 : _a._nodes) == null ? void 0 : _b.length) ?? 0, + hoveredNode: null, + hoveredWidget: null, + mediaElement: null + }; + if (magnifyGlass.isOverMedia && magnifyGlass.currentMediaElement) { + info.mediaElement = this.getMediaInfo(magnifyGlass.currentMediaElement); + } + const nodeUnderCursor = this.getNodeUnderCursor(); + if (nodeUnderCursor) { + info.hoveredNode = this.getDetailedNodeInfo(nodeUnderCursor.node, nodeUnderCursor.localPos); + const widget = this.getWidgetUnderCursor(nodeUnderCursor.node, nodeUnderCursor.localPos); + if (widget) { + info.hoveredWidget = this.getWidgetInfo(widget); + } + } + return info; + } + getNodeUnderCursor() { + const magnifyGlass = window.comfyUIMagnifyGlass; + if (!app.graph || !app.canvas || !magnifyGlass) { + return null; + } + try { + if (app.canvas.node_over) { + const node = app.canvas.node_over; + if (node && node.pos && node.size) { + const canvasRect2 = app.canvas.canvas.getBoundingClientRect(); + const mouseX2 = magnifyGlass.lastKnownMousePosition.x - canvasRect2.left; + const mouseY2 = magnifyGlass.lastKnownMousePosition.y - canvasRect2.top; + let graphPos2; + if (app.canvas.convertOffsetToCanvasPos) { + graphPos2 = app.canvas.convertOffsetToCanvasPos([mouseX2, mouseY2]); + } else { + const ds = app.canvas.ds || { scale: 1, offset: [0, 0] }; + graphPos2 = [ + mouseX2 / ds.scale - ds.offset[0] / ds.scale, + mouseY2 / ds.scale - ds.offset[1] / ds.scale + ]; + } + return { + node, + localPos: { + x: graphPos2[0] - node.pos[0], + y: graphPos2[1] - node.pos[1] + } + }; + } + } + const canvasRect = app.canvas.canvas.getBoundingClientRect(); + const mouseX = magnifyGlass.lastKnownMousePosition.x - canvasRect.left; + const mouseY = magnifyGlass.lastKnownMousePosition.y - canvasRect.top; + let graphPos; + if (app.canvas.convertOffsetToCanvasPos) { + graphPos = app.canvas.convertOffsetToCanvasPos([mouseX, mouseY]); + } else { + const ds = app.canvas.ds || { scale: 1, offset: [0, 0] }; + graphPos = [ + mouseX / ds.scale - ds.offset[0] / ds.scale, + mouseY / ds.scale - ds.offset[1] / ds.scale + ]; + } + for (let i = app.graph._nodes.length - 1; i >= 0; i--) { + const node = app.graph._nodes[i]; + if (!node.pos || !node.size || node.flags && node.flags.collapsed) continue; + if (graphPos[0] >= node.pos[0] && graphPos[0] <= node.pos[0] + node.size[0] && graphPos[1] >= node.pos[1] && graphPos[1] <= node.pos[1] + node.size[1]) { + return { + node, + localPos: { + x: graphPos[0] - node.pos[0], + y: graphPos[1] - node.pos[1] + } + }; + } + } + } catch (err) { + console.warn("Error in node detection:", err); + } + return null; + } + getDetailedNodeInfo(node, localPos) { + return { + id: node.id, + title: node.title || "Untitled Node", + type: node.type, + mode: this.getNodeModeText(node.mode), + position: { + x: Math.round(node.pos[0]), + y: Math.round(node.pos[1]), + formatted: `(${Math.round(node.pos[0])}, ${Math.round(node.pos[1])})` + }, + size: { + width: Math.round(node.size[0]), + height: Math.round(node.size[1]), + formatted: `${Math.round(node.size[0])}×${Math.round(node.size[1])}` + }, + localPosition: { + x: Math.round(localPos.x), + y: Math.round(localPos.y), + formatted: `(${Math.round(localPos.x)}, ${Math.round(localPos.y)})`, + percentage: { + x: (localPos.x / node.size[0] * 100).toFixed(1), + y: (localPos.y / node.size[1] * 100).toFixed(1) + } + }, + counts: { + widgets: node.widgets ? node.widgets.length : 0, + inputs: node.inputs ? node.inputs.length : 0, + outputs: node.outputs ? node.outputs.length : 0, + properties: node.properties ? Object.keys(node.properties).length : 0 + }, + widgets: node.widgets || [], + inputs: node.inputs || [], + outputs: node.outputs || [], + properties: node.properties || {}, + hoverRegion: this.detectNodeRegion(localPos, node) + }; + } + getNodeInfo(node) { + return { + id: node.id, + title: node.title || "Untitled", + type: node.type, + mode: node.mode, + size: node.size ? `${Math.round(node.size[0])}×${Math.round(node.size[1])}` : "Unknown", + position: node.pos ? `(${Math.round(node.pos[0])}, ${Math.round(node.pos[1])})` : "Unknown", + widgets: node.widgets || [], + inputs: node.inputs || [], + outputs: node.outputs || [], + properties: node.properties || {} + }; + } + getWidgetUnderCursor(node, localPos) { + if (!node.widgets || !node.widgets.length) return null; + const titleHeight = 30; + let currentY = titleHeight; + for (const widget of node.widgets) { + const widgetHeight = 25; + if (localPos.y >= currentY && localPos.y <= currentY + widgetHeight) { + return widget; + } + currentY += widgetHeight + 5; + } + return null; + } + getWidgetInfo(widget) { + return { + name: widget.name, + type: widget.type, + value: this.formatValue(widget.value), + options: widget.options || null, + min: widget.min, + max: widget.max, + step: widget.step + }; + } + getMediaInfo(mediaElement) { + const info = { + type: mediaElement instanceof HTMLImageElement ? "image" : "video", + tagName: mediaElement.tagName, + src: mediaElement.src ? mediaElement.src.substring(mediaElement.src.lastIndexOf("/") + 1) : "No source", + naturalWidth: 0, + naturalHeight: 0, + displayWidth: 0, + displayHeight: 0, + aspectRatio: "" + }; + if (mediaElement instanceof HTMLImageElement) { + info.naturalWidth = mediaElement.naturalWidth; + info.naturalHeight = mediaElement.naturalHeight; + info.displayWidth = Math.round(mediaElement.width); + info.displayHeight = Math.round(mediaElement.height); + if (mediaElement.naturalWidth && mediaElement.naturalHeight) { + info.aspectRatio = (mediaElement.naturalWidth / mediaElement.naturalHeight).toFixed(2); + } + } else if (mediaElement instanceof HTMLVideoElement) { + info.naturalWidth = mediaElement.videoWidth; + info.naturalHeight = mediaElement.videoHeight; + info.displayWidth = Math.round(mediaElement.width); + info.displayHeight = Math.round(mediaElement.height); + info.duration = mediaElement.duration; + info.currentTime = mediaElement.currentTime; + if (mediaElement.videoWidth && mediaElement.videoHeight) { + info.aspectRatio = (mediaElement.videoWidth / mediaElement.videoHeight).toFixed(2); + } + } + return info; + } + getNodeModeText(mode) { + const modes = { + 0: "Always", + 1: "On Event", + 2: "Never", + 3: "On Trigger", + 4: "On Request" + }; + return modes[mode] || `Mode ${mode}`; + } + detectNodeRegion(localPos, node) { + const titleHeight = 30; + const regions = []; + if (localPos.y <= titleHeight) { + regions.push("Title Bar"); + } + if (localPos.x <= 10) { + regions.push("Left Edge"); + } else if (localPos.x >= node.size[0] - 10) { + regions.push("Right Edge"); + } + if (localPos.y <= 10) { + regions.push("Top Edge"); + } else if (localPos.y >= node.size[1] - 10) { + regions.push("Bottom Edge"); + } + if (regions.length === 0) { + if (localPos.y > titleHeight) { + regions.push("Content Area"); + } + } + if (localPos.x <= 20 && localPos.y > titleHeight) { + regions.push("Input Area"); + } else if (localPos.x >= node.size[0] - 20 && localPos.y > titleHeight) { + regions.push("Output Area"); + } + return regions.length > 0 ? regions.join(", ") : "Unknown"; + } + formatValue(value) { + if (value === null) return "null"; + if (value === void 0) return "undefined"; + if (typeof value === "string") { + return value; + } + if (typeof value === "number") { + return Number.isInteger(value) ? value.toString() : value.toFixed(3); + } + if (typeof value === "boolean") return value.toString(); + if (Array.isArray(value)) { + return `Array(${value.length})`; + } + if (typeof value === "object") { + return "Object"; + } + return String(value); + } +} +export { + InformationGatherer +}; +//# sourceMappingURL=InformationGatherer.js.map diff --git a/web/info-panel/InformationGatherer.js.map b/web/info-panel/InformationGatherer.js.map new file mode 100644 index 0000000..93006b8 --- /dev/null +++ b/web/info-panel/InformationGatherer.js.map @@ -0,0 +1 @@ +{"version":3,"file":"InformationGatherer.js","sources":["../../src/info-panel/InformationGatherer.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Info Panel Information Gatherer (TypeScript)\n * \n * Complete Information Gatherer extracted from magnify_info_panel.js\n * Responsible for collecting information about nodes, widgets, and canvas state.\n */\n\nimport type { ComfyApp, ComfyNode, ComfyWidget, GatheredInfo, NodeInfo, WidgetInfo, MediaInfo } from '../types/comfyui';\n\ndeclare const app: ComfyApp;\n\n/**\n * Information Gatherer class.\n * Collects information about the current state under the cursor.\n */\nexport class InformationGatherer {\n constructor() { }\n\n gatherInformation(): GatheredInfo {\n const magnifyGlass = window.comfyUIMagnifyGlass;\n if (!magnifyGlass) {\n return {\n timestamp: Date.now(),\n cursor: { screenX: 0, screenY: 0, canvasX: 0, canvasY: 0 },\n zoom: 1,\n nodeCount: 0,\n hoveredNode: null,\n hoveredWidget: null,\n mediaElement: null\n };\n }\n\n const info: GatheredInfo = {\n timestamp: Date.now(),\n cursor: {\n screenX: magnifyGlass.lastKnownMousePosition.x,\n screenY: magnifyGlass.lastKnownMousePosition.y,\n canvasX: magnifyGlass.state.x,\n canvasY: magnifyGlass.state.y\n },\n zoom: magnifyGlass.state.canvasScale,\n nodeCount: app.graph?._nodes?.length ?? 0,\n hoveredNode: null,\n hoveredWidget: null,\n mediaElement: null\n };\n\n if (magnifyGlass.isOverMedia && magnifyGlass.currentMediaElement) {\n info.mediaElement = this.getMediaInfo(magnifyGlass.currentMediaElement);\n }\n\n const nodeUnderCursor = this.getNodeUnderCursor();\n if (nodeUnderCursor) {\n info.hoveredNode = this.getDetailedNodeInfo(nodeUnderCursor.node, nodeUnderCursor.localPos);\n\n const widget = this.getWidgetUnderCursor(nodeUnderCursor.node, nodeUnderCursor.localPos);\n if (widget) {\n info.hoveredWidget = this.getWidgetInfo(widget);\n }\n }\n\n return info;\n }\n\n getNodeUnderCursor(): { node: ComfyNode, localPos: { x: number, y: number } } | null {\n const magnifyGlass = window.comfyUIMagnifyGlass;\n if (!app.graph || !app.canvas || !magnifyGlass) {\n return null;\n }\n\n try {\n // Try to get the node from ComfyUI's state\n if (app.canvas.node_over) {\n const node = app.canvas.node_over;\n if (node && node.pos && node.size) {\n const canvasRect = app.canvas.canvas.getBoundingClientRect();\n const mouseX = magnifyGlass.lastKnownMousePosition.x - canvasRect.left;\n const mouseY = magnifyGlass.lastKnownMousePosition.y - canvasRect.top;\n\n let graphPos: [number, number];\n if (app.canvas.convertOffsetToCanvasPos) {\n graphPos = app.canvas.convertOffsetToCanvasPos([mouseX, mouseY]);\n } else {\n const ds = app.canvas.ds || { scale: 1, offset: [0, 0] };\n graphPos = [\n (mouseX / ds.scale) - (ds.offset[0] / ds.scale),\n (mouseY / ds.scale) - (ds.offset[1] / ds.scale)\n ];\n }\n\n return {\n node: node,\n localPos: {\n x: graphPos[0] - node.pos[0],\n y: graphPos[1] - node.pos[1]\n }\n };\n }\n }\n\n // Fallback to manual detection\n const canvasRect = app.canvas.canvas.getBoundingClientRect();\n const mouseX = magnifyGlass.lastKnownMousePosition.x - canvasRect.left;\n const mouseY = magnifyGlass.lastKnownMousePosition.y - canvasRect.top;\n\n let graphPos: [number, number];\n if (app.canvas.convertOffsetToCanvasPos) {\n graphPos = app.canvas.convertOffsetToCanvasPos([mouseX, mouseY]);\n } else {\n const ds = app.canvas.ds || { scale: 1, offset: [0, 0] };\n graphPos = [\n (mouseX / ds.scale) - (ds.offset[0] / ds.scale),\n (mouseY / ds.scale) - (ds.offset[1] / ds.scale)\n ];\n }\n\n for (let i = app.graph._nodes.length - 1; i >= 0; i--) {\n const node = app.graph._nodes[i];\n if (!node.pos || !node.size || (node.flags && node.flags.collapsed)) continue;\n\n if (graphPos[0] >= node.pos[0] &&\n graphPos[0] <= node.pos[0] + node.size[0] &&\n graphPos[1] >= node.pos[1] &&\n graphPos[1] <= node.pos[1] + node.size[1]) {\n\n return {\n node: node,\n localPos: {\n x: graphPos[0] - node.pos[0],\n y: graphPos[1] - node.pos[1]\n }\n };\n }\n }\n } catch (err) {\n console.warn(\"Error in node detection:\", err);\n }\n\n return null;\n }\n\n getDetailedNodeInfo(node: ComfyNode, localPos: { x: number, y: number }): NodeInfo {\n return {\n id: node.id,\n title: node.title || \"Untitled Node\",\n type: node.type,\n mode: this.getNodeModeText(node.mode),\n position: {\n x: Math.round(node.pos[0]),\n y: Math.round(node.pos[1]),\n formatted: `(${Math.round(node.pos[0])}, ${Math.round(node.pos[1])})`\n },\n size: {\n width: Math.round(node.size[0]),\n height: Math.round(node.size[1]),\n formatted: `${Math.round(node.size[0])}×${Math.round(node.size[1])}`\n },\n localPosition: {\n x: Math.round(localPos.x),\n y: Math.round(localPos.y),\n formatted: `(${Math.round(localPos.x)}, ${Math.round(localPos.y)})`,\n percentage: {\n x: ((localPos.x / node.size[0]) * 100).toFixed(1),\n y: ((localPos.y / node.size[1]) * 100).toFixed(1)\n }\n },\n counts: {\n widgets: node.widgets ? node.widgets.length : 0,\n inputs: node.inputs ? node.inputs.length : 0,\n outputs: node.outputs ? node.outputs.length : 0,\n properties: node.properties ? Object.keys(node.properties).length : 0\n },\n widgets: node.widgets || [],\n inputs: node.inputs || [],\n outputs: node.outputs || [],\n properties: node.properties || {},\n hoverRegion: this.detectNodeRegion(localPos, node)\n };\n }\n\n getNodeInfo(node: ComfyNode): NodeInfo {\n return {\n id: node.id,\n title: node.title || \"Untitled\",\n type: node.type,\n mode: node.mode,\n size: node.size ? `${Math.round(node.size[0])}×${Math.round(node.size[1])}` : \"Unknown\",\n position: node.pos ? `(${Math.round(node.pos[0])}, ${Math.round(node.pos[1])})` : \"Unknown\",\n widgets: node.widgets || [],\n inputs: node.inputs || [],\n outputs: node.outputs || [],\n properties: node.properties || {}\n };\n }\n\n getWidgetUnderCursor(node: ComfyNode, localPos: { x: number, y: number }): ComfyWidget | null {\n if (!node.widgets || !node.widgets.length) return null;\n\n const titleHeight = 30;\n let currentY = titleHeight;\n\n for (const widget of node.widgets) {\n const widgetHeight = 25;\n\n if (localPos.y >= currentY && localPos.y <= currentY + widgetHeight) {\n return widget;\n }\n\n currentY += widgetHeight + 5;\n }\n\n return null;\n }\n\n getWidgetInfo(widget: ComfyWidget): WidgetInfo {\n return {\n name: widget.name,\n type: widget.type,\n value: this.formatValue(widget.value),\n options: widget.options || null,\n min: widget.min,\n max: widget.max,\n step: widget.step\n };\n }\n\n getMediaInfo(mediaElement: HTMLImageElement | HTMLVideoElement): MediaInfo {\n const info: MediaInfo = {\n type: mediaElement instanceof HTMLImageElement ? 'image' : 'video',\n tagName: mediaElement.tagName,\n src: mediaElement.src ? mediaElement.src.substring(mediaElement.src.lastIndexOf('/') + 1) : 'No source',\n naturalWidth: 0,\n naturalHeight: 0,\n displayWidth: 0,\n displayHeight: 0,\n aspectRatio: ''\n };\n\n if (mediaElement instanceof HTMLImageElement) {\n info.naturalWidth = mediaElement.naturalWidth;\n info.naturalHeight = mediaElement.naturalHeight;\n info.displayWidth = Math.round(mediaElement.width);\n info.displayHeight = Math.round(mediaElement.height);\n if (mediaElement.naturalWidth && mediaElement.naturalHeight) {\n info.aspectRatio = (mediaElement.naturalWidth / mediaElement.naturalHeight).toFixed(2);\n }\n } else if (mediaElement instanceof HTMLVideoElement) {\n info.naturalWidth = mediaElement.videoWidth;\n info.naturalHeight = mediaElement.videoHeight;\n info.displayWidth = Math.round(mediaElement.width);\n info.displayHeight = Math.round(mediaElement.height);\n info.duration = mediaElement.duration;\n info.currentTime = mediaElement.currentTime;\n if (mediaElement.videoWidth && mediaElement.videoHeight) {\n info.aspectRatio = (mediaElement.videoWidth / mediaElement.videoHeight).toFixed(2);\n }\n }\n\n return info;\n }\n\n getNodeModeText(mode: number): string {\n const modes: { [key: number]: string } = {\n 0: 'Always',\n 1: 'On Event',\n 2: 'Never',\n 3: 'On Trigger',\n 4: 'On Request'\n };\n return modes[mode] || `Mode ${mode}`;\n }\n\n detectNodeRegion(localPos: { x: number, y: number }, node: ComfyNode): string {\n const titleHeight = 30;\n const regions: string[] = [];\n\n if (localPos.y <= titleHeight) {\n regions.push(\"Title Bar\");\n }\n\n if (localPos.x <= 10) {\n regions.push(\"Left Edge\");\n } else if (localPos.x >= node.size[0] - 10) {\n regions.push(\"Right Edge\");\n }\n\n if (localPos.y <= 10) {\n regions.push(\"Top Edge\");\n } else if (localPos.y >= node.size[1] - 10) {\n regions.push(\"Bottom Edge\");\n }\n\n if (regions.length === 0) {\n if (localPos.y > titleHeight) {\n regions.push(\"Content Area\");\n }\n }\n\n if (localPos.x <= 20 && localPos.y > titleHeight) {\n regions.push(\"Input Area\");\n } else if (localPos.x >= node.size[0] - 20 && localPos.y > titleHeight) {\n regions.push(\"Output Area\");\n }\n\n return regions.length > 0 ? regions.join(\", \") : \"Unknown\";\n }\n\n formatValue(value: unknown): string {\n if (value === null) return \"null\";\n if (value === undefined) return \"undefined\";\n if (typeof value === \"string\") {\n return value; // Show full text without truncation\n }\n if (typeof value === \"number\") {\n return Number.isInteger(value) ? value.toString() : value.toFixed(3);\n }\n if (typeof value === \"boolean\") return value.toString();\n if (Array.isArray(value)) {\n return `Array(${value.length})`;\n }\n if (typeof value === \"object\") {\n return \"Object\";\n }\n return String(value);\n }\n}\n"],"names":["canvasRect","mouseX","mouseY","graphPos"],"mappings":"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"} \ No newline at end of file diff --git a/web/info-panel/NodeDataExtractor.js b/web/info-panel/NodeDataExtractor.js new file mode 100644 index 0000000..85eb9fc --- /dev/null +++ b/web/info-panel/NodeDataExtractor.js @@ -0,0 +1,209 @@ +import { formatWidgetValue } from "./ValueFormatter.js"; +function getCheckpointInfo(nodeInfo) { + if (nodeInfo.type && (nodeInfo.type.includes("CheckpointLoader") || nodeInfo.type.includes("LoadCheckpoint") || nodeInfo.type.includes("ModelLoader") || nodeInfo.type.includes("UNETLoader") || nodeInfo.type.includes("VAELoader") || nodeInfo.type.includes("LoraLoader"))) { + if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { + for (const widget of nodeInfo.widgets) { + if (widget.name && (widget.name.toLowerCase().includes("model") || widget.name.toLowerCase().includes("checkpoint") || widget.name.toLowerCase().includes("ckpt") || widget.name.toLowerCase().includes("lora") || widget.name.toLowerCase().includes("vae") || widget.name.toLowerCase().includes("file"))) { + const value = String(widget.value); + const filename = value.split(/[\/\\]/).pop(); + return filename || value; + } + } + } + } + return null; +} +function getImageInfo(nodeInfo) { + if (nodeInfo.type && (nodeInfo.type.includes("SaveImage") || nodeInfo.type.includes("PreviewImage") || nodeInfo.type.includes("VisionOutput") || nodeInfo.type.includes("ImageOutput") || nodeInfo.type.includes("LoadImage") || nodeInfo.type.includes("Display"))) { + if (nodeInfo.widgets) { + for (const widget of nodeInfo.widgets) { + if (widget.name && (widget.name.toLowerCase().includes("image") || widget.name.toLowerCase().includes("filename") || widget.name.toLowerCase().includes("file"))) { + return String(widget.value); + } + } + } + if (nodeInfo.properties && nodeInfo.properties.img) { + const img = nodeInfo.properties.img; + return { + width: img.width || "unknown", + height: img.height || "unknown", + src: img.src ? String(img.src).split(/[\/\\]/).pop() || "Preview available" : "Preview available" + }; + } + if (nodeInfo.outputs) { + for (const output of nodeInfo.outputs) { + if (output.links && output.links.length > 0) { + return "Image connected to " + output.links.length + " node(s)"; + } + } + } + return "Image node"; + } + return null; +} +function getTextBoxContent(nodeInfo) { + if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { + if (nodeInfo.type && nodeInfo.type.includes("CLIPTextEncode")) { + for (const widget of nodeInfo.widgets) { + if (widget.name === "text" && typeof widget.value === "string") { + return widget.value; + } + } + } + for (const widget of nodeInfo.widgets) { + if ((widget.name.toLowerCase().includes("prompt") || widget.name.toLowerCase().includes("conditioning")) && typeof widget.value === "string" && widget.value.length > 0) { + return widget.value; + } + } + for (const widget of nodeInfo.widgets) { + if ((widget.type === "text" || widget.type === "textarea" || widget.type === "string" || widget.name.toLowerCase().includes("text")) && typeof widget.value === "string" && widget.value.length > 0) { + return widget.value; + } + } + } + return null; +} +const COMPLEX_NODE_PARAMS = [ + "seed", + "steps", + "cfg", + "scale", + "sampler", + "scheduler", + "positive", + "negative", + "width", + "height", + "denoise", + "strength", + "noise", + "count", + "batch", + "size", + "phase", + "color", + "intensity", + "control_after_generate", + "control", + "after", + "generate", + "start_at_step", + "end_at_step", + "start", + "end", + "return_with_leftover_noise", + "leftover", + "noise_return", + "model", + "vae", + "clip", + "lora", + "checkpoint", + "latent", + "image", + "mask", + "filename", + "directory", + "prompt", + "conditioning", + "filename_prefix", + "resolution", + "num_chunks", + "seconds", + "aspect_ratio", + "style_type", + "background", + "n", + "human", + "raw", + "guidance", + "skip_preprocessing", + "movement_amplitude", + "animation", + "material_type", + "b1", + "b2", + "s1", + "s2", + "type", + "channel", + "sigma", + "rho", + "alpha", + "base_shift", + "shift", + "stretch", + "terminal", + "spacing", + "style", + "eta", + "norm_threshold", + "momentum", + "hypernetwork_name", + "reuse_threshold", + "verbose", + "layers", + "set_cond_area", + "audioui", + "camera_pose", + "fx", + "cx", + "fy", + "cy" +]; +const SAVE_NODE_PARAMS = [ + "filename_prefix", + "filename", + "directory", + "path", + "format", + "quality", + "extension" +]; +const SKIP_WIDGET_NAMES = ["title", "node", "id", "type", "mode"]; +function shouldShowAllWidgets(nodeType) { + const type = nodeType.toLowerCase(); + const isSaveNode = type.includes("save") && !type.includes("checkpoint") && !type.includes("model") && !type.includes("preview"); + return (type.includes("ksampler") || type.includes("sampler") || type.includes("k_samplers") || type.includes("checkpoint") || type.includes("model") || type.includes("lora") || type.includes("controlnet") || type.includes("advanced") || type.includes("detailer") || type.includes("inpaint") || type.includes("upscale") || type.includes("clip") || type.includes("text") || type.includes("encode")) && !isSaveNode; +} +function getImportantNodeParameters(nodeInfo) { + const parameters = []; + if (!nodeInfo.widgets || nodeInfo.widgets.length === 0) { + return parameters; + } + const nodeType = nodeInfo.type || ""; + const typeLower = nodeType.toLowerCase(); + const isSaveNode = typeLower.includes("save") && !typeLower.includes("checkpoint") && !typeLower.includes("model") && !typeLower.includes("preview"); + if (nodeType && shouldShowAllWidgets(nodeType)) { + for (const widget of nodeInfo.widgets) { + if (widget.name && widget.name !== "") { + const widgetName = widget.name.toLowerCase(); + if (!SKIP_WIDGET_NAMES.some((skip) => widgetName.includes(skip))) { + parameters.push({ + label: widget.name, + value: formatWidgetValue(widget.value) + }); + } + } + } + return parameters; + } + const importantParams = isSaveNode ? SAVE_NODE_PARAMS : COMPLEX_NODE_PARAMS; + for (const widget of nodeInfo.widgets) { + const paramName = widget.name.toLowerCase(); + if (importantParams.some((param) => paramName.includes(param))) { + parameters.push({ + label: widget.name, + value: formatWidgetValue(widget.value) + }); + } + } + return parameters; +} +export { + getCheckpointInfo, + getImageInfo, + getImportantNodeParameters, + getTextBoxContent +}; +//# sourceMappingURL=NodeDataExtractor.js.map diff --git a/web/info-panel/NodeDataExtractor.js.map b/web/info-panel/NodeDataExtractor.js.map new file mode 100644 index 0000000..618a29b --- /dev/null +++ b/web/info-panel/NodeDataExtractor.js.map @@ -0,0 +1 @@ +{"version":3,"file":"NodeDataExtractor.js","sources":["../../src/info-panel/NodeDataExtractor.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Node Data Extractor\n * \n * Utility functions for extracting specific information from nodes.\n * Extracted from UIManager.ts for better modularity.\n */\n\nimport { formatWidgetValue } from './ValueFormatter';\nimport type { NodeInfo } from '../types/comfyui';\n\n/**\n * Interface for extracted parameter items.\n */\nexport interface ParameterItem {\n label: string;\n value: string;\n}\n\n/**\n * Get checkpoint/model info from a node.\n * @param nodeInfo - Node information object\n * @returns Model filename or null\n */\nexport function getCheckpointInfo(nodeInfo: NodeInfo): string | null {\n if (nodeInfo.type && (\n nodeInfo.type.includes('CheckpointLoader') ||\n nodeInfo.type.includes('LoadCheckpoint') ||\n nodeInfo.type.includes('ModelLoader') ||\n nodeInfo.type.includes('UNETLoader') ||\n nodeInfo.type.includes('VAELoader') ||\n nodeInfo.type.includes('LoraLoader')\n )) {\n if (nodeInfo.widgets && nodeInfo.widgets.length > 0) {\n for (const widget of nodeInfo.widgets) {\n if (widget.name && (\n widget.name.toLowerCase().includes('model') ||\n widget.name.toLowerCase().includes('checkpoint') ||\n widget.name.toLowerCase().includes('ckpt') ||\n widget.name.toLowerCase().includes('lora') ||\n widget.name.toLowerCase().includes('vae') ||\n widget.name.toLowerCase().includes('file')\n )) {\n const value = String(widget.value);\n const filename = value.split(/[\\/\\\\]/).pop();\n return filename || value;\n }\n }\n }\n }\n return null;\n}\n\n/**\n * Image info result type.\n */\nexport interface ImageInfoResult {\n width: string | number;\n height: string | number;\n src: string;\n}\n\n/**\n * Get image info from a node.\n * @param nodeInfo - Node information object\n * @returns Image info string, object, or null\n */\nexport function getImageInfo(nodeInfo: NodeInfo): string | ImageInfoResult | null {\n if (nodeInfo.type && (\n nodeInfo.type.includes('SaveImage') ||\n nodeInfo.type.includes('PreviewImage') ||\n nodeInfo.type.includes('VisionOutput') ||\n nodeInfo.type.includes('ImageOutput') ||\n nodeInfo.type.includes('LoadImage') ||\n nodeInfo.type.includes('Display')\n )) {\n if (nodeInfo.widgets) {\n for (const widget of nodeInfo.widgets) {\n if (widget.name && (\n widget.name.toLowerCase().includes('image') ||\n widget.name.toLowerCase().includes('filename') ||\n widget.name.toLowerCase().includes('file')\n )) {\n return String(widget.value);\n }\n }\n }\n\n if (nodeInfo.properties && (nodeInfo.properties as Record).img) {\n const img = (nodeInfo.properties as Record).img as Record;\n return {\n width: (img.width as string | number) || 'unknown',\n height: (img.height as string | number) || 'unknown',\n src: img.src ? String(img.src).split(/[\\/\\\\]/).pop() || 'Preview available' : 'Preview available'\n };\n }\n\n if (nodeInfo.outputs) {\n for (const output of nodeInfo.outputs) {\n if (output.links && output.links.length > 0) {\n return 'Image connected to ' + output.links.length + ' node(s)';\n }\n }\n }\n\n return 'Image node';\n }\n return null;\n}\n\n/**\n * Get text box content from a node.\n * @param nodeInfo - Node information object\n * @returns Text content or null\n */\nexport function getTextBoxContent(nodeInfo: NodeInfo): string | null {\n if (nodeInfo.widgets && nodeInfo.widgets.length > 0) {\n if (nodeInfo.type && nodeInfo.type.includes('CLIPTextEncode')) {\n for (const widget of nodeInfo.widgets) {\n if (widget.name === 'text' && typeof widget.value === 'string') {\n return widget.value;\n }\n }\n }\n\n for (const widget of nodeInfo.widgets) {\n if ((widget.name.toLowerCase().includes('prompt') ||\n widget.name.toLowerCase().includes('conditioning')) &&\n typeof widget.value === 'string' && widget.value.length > 0) {\n return widget.value;\n }\n }\n\n for (const widget of nodeInfo.widgets) {\n if ((widget.type === 'text' || widget.type === 'textarea' ||\n widget.type === 'string' || widget.name.toLowerCase().includes('text')) &&\n typeof widget.value === 'string' && widget.value.length > 0) {\n return widget.value;\n }\n }\n }\n return null;\n}\n\n/**\n * List of important parameter names for complex nodes.\n */\nconst COMPLEX_NODE_PARAMS = [\n 'seed', 'steps', 'cfg', 'scale', 'sampler', 'scheduler',\n 'positive', 'negative', 'width', 'height', 'denoise', 'strength',\n 'noise', 'count', 'batch', 'size', 'phase', 'color', 'intensity',\n 'control_after_generate', 'control', 'after', 'generate',\n 'start_at_step', 'end_at_step', 'start', 'end',\n 'return_with_leftover_noise', 'leftover', 'noise_return',\n 'model', 'vae', 'clip', 'lora', 'checkpoint',\n 'latent', 'image', 'mask', 'filename', 'directory',\n 'prompt', 'conditioning', 'filename_prefix',\n 'resolution', 'num_chunks', 'seconds', 'aspect_ratio',\n 'style_type', 'background', 'n', 'human', 'raw', 'guidance',\n 'skip_preprocessing', 'movement_amplitude', 'animation',\n 'material_type', 'b1', 'b2', 's1', 's2', 'type', 'channel',\n 'sigma', 'rho', 'alpha', 'base_shift', 'shift', 'stretch', 'terminal',\n 'spacing', 'style', 'eta', 'norm_threshold', 'momentum',\n 'hypernetwork_name', 'reuse_threshold', 'verbose', 'layers',\n 'set_cond_area', 'audioui', 'camera_pose', 'fx', 'cx', 'fy', 'cy'\n];\n\n/**\n * Parameters for save nodes.\n */\nconst SAVE_NODE_PARAMS = [\n 'filename_prefix', 'filename', 'directory', 'path',\n 'format', 'quality', 'extension'\n];\n\n/**\n * Generic widget names to skip.\n */\nconst SKIP_WIDGET_NAMES = ['title', 'node', 'id', 'type', 'mode'];\n\n/**\n * Check if a node type should show all widgets.\n * @param nodeType - The node type string\n * @returns True if all widgets should be shown\n */\nfunction shouldShowAllWidgets(nodeType: string): boolean {\n const type = nodeType.toLowerCase();\n const isSaveNode = type.includes('save') &&\n !type.includes('checkpoint') &&\n !type.includes('model') &&\n !type.includes('preview');\n\n return (\n type.includes('ksampler') ||\n type.includes('sampler') ||\n type.includes('k_samplers') ||\n type.includes('checkpoint') ||\n type.includes('model') ||\n type.includes('lora') ||\n type.includes('controlnet') ||\n type.includes('advanced') ||\n type.includes('detailer') ||\n type.includes('inpaint') ||\n type.includes('upscale') ||\n type.includes('clip') ||\n type.includes('text') ||\n type.includes('encode')\n ) && !isSaveNode;\n}\n\n/**\n * Get important node parameters based on node type.\n * @param nodeInfo - Node information object\n * @returns Array of parameter items\n */\nexport function getImportantNodeParameters(nodeInfo: NodeInfo): ParameterItem[] {\n const parameters: ParameterItem[] = [];\n\n if (!nodeInfo.widgets || nodeInfo.widgets.length === 0) {\n return parameters;\n }\n\n const nodeType = nodeInfo.type || '';\n const typeLower = nodeType.toLowerCase();\n const isSaveNode = typeLower.includes('save') &&\n !typeLower.includes('checkpoint') &&\n !typeLower.includes('model') &&\n !typeLower.includes('preview');\n\n // For complex nodes, show ALL widgets\n if (nodeType && shouldShowAllWidgets(nodeType)) {\n for (const widget of nodeInfo.widgets) {\n if (widget.name && widget.name !== '') {\n const widgetName = widget.name.toLowerCase();\n if (!SKIP_WIDGET_NAMES.some(skip => widgetName.includes(skip))) {\n parameters.push({\n label: widget.name,\n value: formatWidgetValue(widget.value)\n });\n }\n }\n }\n return parameters;\n }\n\n // For other nodes, use filtered parameter lists\n const importantParams = isSaveNode ? SAVE_NODE_PARAMS : COMPLEX_NODE_PARAMS;\n\n for (const widget of nodeInfo.widgets) {\n const paramName = widget.name.toLowerCase();\n if (importantParams.some(param => paramName.includes(param))) {\n parameters.push({\n label: widget.name,\n value: formatWidgetValue(widget.value)\n });\n }\n }\n\n return parameters;\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/info-panel/PositionManager.js b/web/info-panel/PositionManager.js new file mode 100644 index 0000000..b267ea6 --- /dev/null +++ b/web/info-panel/PositionManager.js @@ -0,0 +1,160 @@ +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +class PositionManager { + constructor(stateManager, panelElement) { + __publicField(this, "stateManager"); + __publicField(this, "panelElement"); + this.stateManager = stateManager; + this.panelElement = panelElement; + } + positionPanel() { + if (!this.panelElement) return; + if (this.stateManager.state.isPanelPinned) { + this.applyPinnedPosition(); + return; + } + this.calculateNormalPosition(); + } + applyPinnedPosition() { + if (!this.panelElement) return; + let { x, y } = this.stateManager.state.pinnedPosition; + if (x === 0 && y === 0) { + this.calculateNormalPosition(); + if (this.panelElement) { + const rect = this.panelElement.getBoundingClientRect(); + this.stateManager.state.pinnedPosition = { x: rect.left, y: rect.top }; + } + return; + } + const panelWidth = this.panelElement.offsetWidth || 300; + const panelHeight = this.panelElement.offsetHeight || 400; + const margin = 4; + const boundedX = Math.max(margin, Math.min(x, window.innerWidth - panelWidth - margin)); + const boundedY = Math.max(margin, Math.min(y, window.innerHeight - panelHeight - margin)); + this.panelElement.style.left = `${boundedX}px`; + this.panelElement.style.top = `${boundedY}px`; + } + calculateNormalPosition() { + var _a, _b, _c; + const magnifyGlass = window.comfyUIMagnifyGlass; + if (!magnifyGlass || !this.panelElement) return; + const settings = this.stateManager.state.settings; + const panelWidth = Number(settings["🔍MagnifyGlass.InfoPanelWidth"]) || 300; + const panelHeight = this.panelElement.offsetHeight || 400; + let left; + let top; + const margin = 4; + const glassRect = (_a = magnifyGlass.ui.glassDiv) == null ? void 0 : _a.getBoundingClientRect(); + const hasValidGlassRect = glassRect && (glassRect.right > 0 || glassRect.top > 0); + const mouseX = ((_b = magnifyGlass.lastKnownMousePosition) == null ? void 0 : _b.x) || 0; + const mouseY = ((_c = magnifyGlass.lastKnownMousePosition) == null ? void 0 : _c.y) || 0; + const hasValidMousePosition = mouseX > 0 || mouseY > 0; + if (!this.stateManager.state.isGlassPreviewVisible) { + if (hasValidMousePosition) { + left = mouseX - panelWidth / 2; + top = mouseY - 20; + } else { + left = window.innerWidth - panelWidth - 50; + top = 100; + } + } else if (hasValidGlassRect) { + const position = settings["🔍MagnifyGlass.InfoPanelPosition"]; + switch (position) { + case "Right": + left = glassRect.right + margin; + top = glassRect.top; + break; + case "Left": + left = glassRect.left - panelWidth - margin; + top = glassRect.top; + break; + case "Top": + left = glassRect.left; + top = glassRect.top - panelHeight - margin; + break; + case "Bottom": + left = glassRect.left; + top = glassRect.bottom + margin; + break; + default: + left = glassRect.right + margin; + top = glassRect.top; + break; + } + } else if (hasValidMousePosition) { + left = mouseX - panelWidth / 2; + top = mouseY - 20; + } else { + left = window.innerWidth - panelWidth - 50; + top = 100; + } + left = Math.max(4, Math.min(left, window.innerWidth - panelWidth - 4)); + top = Math.max(4, Math.min(top, window.innerHeight - panelHeight - 4)); + this.panelElement.style.left = `${left}px`; + this.panelElement.style.top = `${top}px`; + } + /** + * Position the floating controls relative to the panel. + */ + positionFloatingControls(controlsElement) { + if (!controlsElement) return; + const settings = this.stateManager.state.settings; + if (settings["🔍MagnifyGlass.ShowHoveringControls"] === false) { + controlsElement.style.display = "none"; + return; + } + const isPanelVisible = this.stateManager.state.isPanelVisible; + const magnifyGlass = window.comfyUIMagnifyGlass; + let referenceRect = null; + if (isPanelVisible && this.panelElement) { + referenceRect = this.panelElement.getBoundingClientRect(); + } else if (magnifyGlass && magnifyGlass.ui && magnifyGlass.ui.glassDiv) { + referenceRect = magnifyGlass.ui.glassDiv.getBoundingClientRect(); + if (referenceRect.width === 0 || referenceRect.height === 0) { + controlsElement.style.display = "none"; + return; + } + } + if (!referenceRect) return; + const controlsPosition = this.stateManager.state.settings["🔍MagnifyGlass.ControlsPosition"] || "right"; + const margin = 4; + let left; + let top; + switch (controlsPosition) { + case "left": + left = referenceRect.left - controlsElement.offsetWidth - margin; + top = referenceRect.top; + break; + case "right": + left = referenceRect.right + margin; + top = referenceRect.top; + break; + case "top": + left = referenceRect.left; + top = referenceRect.top - controlsElement.offsetHeight - margin; + break; + case "bottom": + left = referenceRect.left; + top = referenceRect.bottom + margin; + break; + case "top-right": + left = referenceRect.right - controlsElement.offsetWidth; + top = referenceRect.top - controlsElement.offsetHeight - margin; + break; + default: + left = referenceRect.right + margin; + top = referenceRect.top; + } + left = Math.max(10, Math.min(left, window.innerWidth - controlsElement.offsetWidth - 10)); + top = Math.max(10, Math.min(top, window.innerHeight - controlsElement.offsetHeight - 10)); + controlsElement.style.left = `${left}px`; + controlsElement.style.top = `${top}px`; + controlsElement.style.display = "flex"; + controlsElement.style.visibility = "visible"; + } +} +export { + PositionManager +}; +//# sourceMappingURL=PositionManager.js.map diff --git a/web/info-panel/PositionManager.js.map b/web/info-panel/PositionManager.js.map new file mode 100644 index 0000000..0110c39 --- /dev/null +++ b/web/info-panel/PositionManager.js.map @@ -0,0 +1 @@ +{"version":3,"file":"PositionManager.js","sources":["../../src/info-panel/PositionManager.ts"],"sourcesContent":["/**\n * Position Manager class.\n * Handles all positioning logic for the info panel.\n */\n\nimport { StateManager } from './StateManager';\n\nexport class PositionManager {\n stateManager: StateManager;\n panelElement: HTMLElement;\n\n constructor(stateManager: StateManager, panelElement: HTMLElement) {\n this.stateManager = stateManager;\n this.panelElement = panelElement;\n }\n\n positionPanel(): void {\n if (!this.panelElement) return;\n\n // If panel is pinned, keep it at its pinned position\n if (this.stateManager.state.isPanelPinned) {\n this.applyPinnedPosition();\n return;\n }\n\n // Normal positioning logic\n this.calculateNormalPosition();\n }\n\n applyPinnedPosition(): void {\n if (!this.panelElement) return;\n\n let { x, y } = this.stateManager.state.pinnedPosition;\n\n // If pinnedPosition is uninitialized (0,0), use calculateNormalPosition to get a valid position\n if (x === 0 && y === 0) {\n this.calculateNormalPosition();\n // Save the calculated position as the new pinned position\n if (this.panelElement) {\n const rect = this.panelElement.getBoundingClientRect();\n this.stateManager.state.pinnedPosition = { x: rect.left, y: rect.top };\n }\n return;\n }\n\n const panelWidth = this.panelElement.offsetWidth || 300;\n const panelHeight = this.panelElement.offsetHeight || 400;\n const margin = 4;\n\n const boundedX = Math.max(margin, Math.min(x, window.innerWidth - panelWidth - margin));\n const boundedY = Math.max(margin, Math.min(y, window.innerHeight - panelHeight - margin));\n\n this.panelElement.style.left = `${boundedX}px`;\n this.panelElement.style.top = `${boundedY}px`;\n }\n\n calculateNormalPosition(): void {\n const magnifyGlass = window.comfyUIMagnifyGlass;\n if (!magnifyGlass || !this.panelElement) return;\n\n const settings = this.stateManager.state.settings;\n const panelWidth = Number(settings[\"🔍MagnifyGlass.InfoPanelWidth\"]) || 300;\n const panelHeight = this.panelElement.offsetHeight || 400;\n\n let left: number;\n let top: number;\n const margin = 4;\n\n // Check for valid glass rect first\n const glassRect = magnifyGlass.ui.glassDiv?.getBoundingClientRect();\n const hasValidGlassRect = glassRect && (glassRect.right > 0 || glassRect.top > 0);\n\n // Check for valid mouse position\n const mouseX = magnifyGlass.lastKnownMousePosition?.x || 0;\n const mouseY = magnifyGlass.lastKnownMousePosition?.y || 0;\n const hasValidMousePosition = mouseX > 0 || mouseY > 0;\n\n if (!this.stateManager.state.isGlassPreviewVisible) {\n // Glass is hidden, use mouse position\n if (hasValidMousePosition) {\n left = mouseX - (panelWidth / 2);\n top = mouseY - 20;\n } else {\n // Fallback: right side of screen\n left = window.innerWidth - panelWidth - 50;\n top = 100;\n }\n } else if (hasValidGlassRect) {\n // Position relative to glass\n const position = settings[\"🔍MagnifyGlass.InfoPanelPosition\"];\n switch (position) {\n case \"Right\":\n left = glassRect!.right + margin;\n top = glassRect!.top;\n break;\n case \"Left\":\n left = glassRect!.left - panelWidth - margin;\n top = glassRect!.top;\n break;\n case \"Top\":\n left = glassRect!.left;\n top = glassRect!.top - panelHeight - margin;\n break;\n case \"Bottom\":\n left = glassRect!.left;\n top = glassRect!.bottom + margin;\n break;\n default:\n left = glassRect!.right + margin;\n top = glassRect!.top;\n break;\n }\n } else if (hasValidMousePosition) {\n // Fall back to mouse position\n left = mouseX - (panelWidth / 2);\n top = mouseY - 20;\n } else {\n // Final fallback: right side of screen\n left = window.innerWidth - panelWidth - 50;\n top = 100;\n }\n\n left = Math.max(4, Math.min(left, window.innerWidth - panelWidth - 4));\n top = Math.max(4, Math.min(top, window.innerHeight - panelHeight - 4));\n\n this.panelElement.style.left = `${left}px`;\n this.panelElement.style.top = `${top}px`;\n }\n\n /**\n * Position the floating controls relative to the panel.\n */\n positionFloatingControls(controlsElement: HTMLElement | null): void {\n if (!controlsElement) return;\n\n // Check if controls should be visible based on settings\n const settings = this.stateManager.state.settings;\n if (settings[\"🔍MagnifyGlass.ShowHoveringControls\"] === false) {\n controlsElement.style.display = 'none';\n return;\n }\n\n const isPanelVisible = this.stateManager.state.isPanelVisible;\n const magnifyGlass = window.comfyUIMagnifyGlass;\n let referenceRect: DOMRect | null = null;\n\n if (isPanelVisible && this.panelElement) {\n // Position relative to panel\n referenceRect = this.panelElement.getBoundingClientRect();\n } else if (magnifyGlass && magnifyGlass.ui && magnifyGlass.ui.glassDiv) {\n // Position relative to magnify glass\n referenceRect = magnifyGlass.ui.glassDiv.getBoundingClientRect();\n // Don't show controls if glass is hidden/invalid\n if (referenceRect.width === 0 || referenceRect.height === 0) {\n controlsElement.style.display = 'none';\n return;\n }\n }\n\n if (!referenceRect) return;\n\n const controlsPosition = this.stateManager.state.settings[\"🔍MagnifyGlass.ControlsPosition\"] || \"right\";\n const margin = 4;\n\n let left: number;\n let top: number;\n\n switch (controlsPosition) {\n case \"left\":\n left = referenceRect.left - controlsElement.offsetWidth - margin;\n top = referenceRect.top;\n break;\n case \"right\":\n left = referenceRect.right + margin;\n top = referenceRect.top;\n break;\n case \"top\":\n left = referenceRect.left;\n top = referenceRect.top - controlsElement.offsetHeight - margin;\n break;\n case \"bottom\":\n left = referenceRect.left;\n top = referenceRect.bottom + margin;\n break;\n case \"top-right\":\n left = referenceRect.right - controlsElement.offsetWidth;\n top = referenceRect.top - controlsElement.offsetHeight - margin;\n break;\n default:\n left = referenceRect.right + margin;\n top = referenceRect.top;\n }\n\n // Keep controls within viewport\n left = Math.max(10, Math.min(left, window.innerWidth - controlsElement.offsetWidth - 10));\n top = Math.max(10, Math.min(top, window.innerHeight - controlsElement.offsetHeight - 10));\n\n controlsElement.style.left = `${left}px`;\n controlsElement.style.top = `${top}px`;\n\n // Show controls now that they're positioned (initially hidden off-screen)\n controlsElement.style.display = 'flex';\n controlsElement.style.visibility = 'visible';\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/info-panel/StateManager.js b/web/info-panel/StateManager.js new file mode 100644 index 0000000..fb61a74 --- /dev/null +++ b/web/info-panel/StateManager.js @@ -0,0 +1,285 @@ +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +import { getSettingValue } from "../shared/utils.js"; +import { DEFAULT_PANEL_SETTINGS } from "../shared/settings/defaults.js"; +import "/scripts/app.js"; +class StateManager { + constructor() { + __publicField(this, "state"); + this.state = { + // Panel visibility and positioning + isPanelVisible: false, + wasPanelVisibleBeforeHide: false, + isPanelMinimized: true, + isPanelPinned: false, + isPanelLocked: false, + // New lock state + isAutoPinned: false, + // Track if panel was auto-pinned by glass hide + pinnedPosition: { x: 0, y: 0 }, + lastPinnedPosition: null, + // Remember last pinned location + // Glass visibility + isGlassPreviewVisible: true, + // Section expansion + expandedSections: /* @__PURE__ */ new Set(["node"]), + // Interaction states + isPanelHovered: false, + isHoveringNode: false, + // Auto-collapse + autoExpandTimer: null, + lastNodeId: null, + // Update scheduling + updateScheduled: false, + isInitialLoading: false, + // Current data + currentInfo: null, + // Settings cache + settings: {}, + // Auto-detected theme + currentTheme: "dark" + // Will be auto-detected + }; + this.initThemeDetection(); + this.loadSettings(); + } + initThemeDetection() { + this.detectCurrentTheme(); + setTimeout(() => { + if (this.detectCurrentTheme()) { + this.notifyThemeChange(); + } + }, 1e3); + this.setupThemeObserver(); + if (window.matchMedia) { + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)"); + if (prefersDark.addListener) { + prefersDark.addListener(() => this.detectCurrentTheme()); + } else if (prefersDark.addEventListener) { + prefersDark.addEventListener("change", () => this.detectCurrentTheme()); + } + } + this.setupThemeButtonListeners(); + } + detectCurrentTheme() { + let detectedTheme = "dark"; + try { + const colorPalette = getSettingValue("Comfy.ColorPalette", ""); + if (colorPalette) { + const paletteStr = String(colorPalette).toLowerCase(); + if (paletteStr.includes("solarized")) { + detectedTheme = "solarized"; + } else if (paletteStr.includes("arc")) { + detectedTheme = "arc"; + } else if (paletteStr.includes("nord")) { + detectedTheme = "nord"; + } else if (paletteStr.includes("github")) { + detectedTheme = "github"; + } else if (paletteStr.includes("light")) { + detectedTheme = "light"; + } else if (paletteStr.includes("dark")) { + detectedTheme = "dark"; + } + } + } catch (e) { + console.warn("ComfyUI Info Panel: Could not read theme from settings, using fallback detection"); + } + if (detectedTheme === "dark") { + const body = document.body; + const html = document.documentElement; + const dataTheme = body.getAttribute("data-theme") || html.getAttribute("data-theme"); + if (dataTheme) { + detectedTheme = dataTheme.toLowerCase(); + } + const classes = (body.className + " " + html.className).toLowerCase(); + if (classes.includes("theme-solarized") || classes.includes("solarized")) { + detectedTheme = "solarized"; + } else if (classes.includes("theme-arc") || classes.includes("arc-theme")) { + detectedTheme = "arc"; + } else if (classes.includes("theme-nord") || classes.includes("nord-theme")) { + detectedTheme = "nord"; + } else if (classes.includes("theme-github") || classes.includes("github-theme")) { + detectedTheme = "github"; + } else if (classes.includes("theme-light") || classes.includes("light-theme")) { + detectedTheme = "light"; + } + } + if (detectedTheme === "dark") { + const bodyStyles = window.getComputedStyle(document.body); + const bgColor = bodyStyles.backgroundColor; + if (bgColor && bgColor !== "rgba(0, 0, 0, 0)" && bgColor !== "transparent") { + const rgbMatch = bgColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); + if (rgbMatch) { + const [, r, g, b] = rgbMatch.map(Number); + const brightness = (r * 299 + g * 587 + b * 114) / 1e3; + if (brightness > 180) { + detectedTheme = "light"; + } + } + } + } + if (this.state.currentTheme !== detectedTheme) { + this.state.currentTheme = detectedTheme; + return true; + } + return false; + } + setupThemeObserver() { + const observer = new MutationObserver((mutations) => { + let shouldCheck = false; + mutations.forEach((mutation) => { + if (mutation.type === "attributes" && (mutation.attributeName === "class" || mutation.attributeName === "data-theme" || mutation.attributeName === "style")) { + shouldCheck = true; + } + }); + if (shouldCheck && this.detectCurrentTheme()) { + this.notifyThemeChange(); + } + }); + observer.observe(document.body, { attributes: true, attributeOldValue: true }); + observer.observe(document.documentElement, { attributes: true, attributeOldValue: true }); + const vueApp = document.querySelector("#vue-app"); + if (vueApp) { + observer.observe(vueApp, { attributes: true, attributeOldValue: true }); + } + const themeElements = document.querySelectorAll('[class*="theme"], [class*="dark"], [class*="light"], [data-theme]'); + themeElements.forEach((el) => { + observer.observe(el, { attributes: true, attributeOldValue: true }); + }); + setInterval(() => { + if (this.detectCurrentTheme()) { + this.notifyThemeChange(); + } + }, 2e3); + } + setupThemeButtonListeners() { + document.addEventListener("click", (e) => { + var _a, _b, _c, _d; + const target = e.target; + const isThemeButton = ((_a = target.textContent) == null ? void 0 : _a.toLowerCase().includes("theme")) || ((_b = target.textContent) == null ? void 0 : _b.toLowerCase().includes("light")) || ((_c = target.textContent) == null ? void 0 : _c.toLowerCase().includes("dark")) || ((_d = target.title) == null ? void 0 : _d.toLowerCase().includes("theme")) || typeof target.className === "string" && target.className.toLowerCase().includes("theme"); + if (isThemeButton) { + setTimeout(() => { + if (this.detectCurrentTheme()) { + this.notifyThemeChange(); + } + }, 100); + } + }); + } + notifyThemeChange() { + this.state.settings["🔍MagnifyGlass.InfoPanelTheme"] = this.state.currentTheme; + if (window.infoPanelManager && window.infoPanelManager.uiManager) { + window.infoPanelManager.uiManager.updateTheme(this.state.currentTheme); + } + } + loadSettings() { + Object.keys(DEFAULT_PANEL_SETTINGS).forEach((key) => { + if (key !== "🔍MagnifyGlass.InfoPanelTheme") { + this.state.settings[key] = getSettingValue(key, DEFAULT_PANEL_SETTINGS[key]); + } + }); + this.state.settings["🔍MagnifyGlass.InfoPanelTheme"] = this.state.currentTheme; + } + updateSettings() { + const oldSettings = { ...this.state.settings }; + this.loadSettings(); + const changes = {}; + Object.keys(this.state.settings).forEach((key) => { + if (oldSettings[key] !== this.state.settings[key]) { + changes[key] = { + old: oldSettings[key], + new: this.state.settings[key] + }; + } + }); + return changes; + } + togglePanelVisibility() { + this.state.isPanelVisible = !this.state.isPanelVisible; + return this.state.isPanelVisible; + } + toggleGlassPreview() { + this.state.isGlassPreviewVisible = !this.state.isGlassPreviewVisible; + return this.state.isGlassPreviewVisible; + } + togglePinning() { + const wasPinned = this.state.isPanelPinned; + this.state.isPanelPinned = !this.state.isPanelPinned; + if (!this.state.isPanelPinned) { + this.state.isPanelLocked = false; + this.state.isAutoPinned = false; + } else { + this.state.isAutoPinned = false; + } + if (this.state.isPanelPinned && this.state.lastPinnedPosition) { + this.state.pinnedPosition = { ...this.state.lastPinnedPosition }; + } else if (!this.state.isPanelPinned && wasPinned) { + this.state.lastPinnedPosition = { ...this.state.pinnedPosition }; + } + return this.state.isPanelPinned; + } + toggleLocking() { + if (!this.state.isPanelPinned) { + return false; + } + this.state.isPanelLocked = !this.state.isPanelLocked; + return this.state.isPanelLocked; + } + toggleMinimized() { + this.state.isPanelMinimized = !this.state.isPanelMinimized; + return this.state.isPanelMinimized; + } + toggleSection(sectionId) { + if (sectionId === "node") return false; + if (this.state.expandedSections.has(sectionId)) { + this.state.expandedSections.delete(sectionId); + } else { + this.state.expandedSections.add(sectionId); + } + return true; + } + setPinnedPosition(x, y) { + this.state.pinnedPosition = { x, y }; + this.state.lastPinnedPosition = { x, y }; + } + setCurrentInfo(info) { + this.state.currentInfo = info; + } + scheduleAutoCollapse() { + this.clearAutoExpandTimer(); + if (this.state.settings["🔍MagnifyGlass.InfoPanelAnimations"]) { + this.state.autoExpandTimer = setTimeout(() => { + if (!this.state.isPanelHovered) { + this.collapseNodeSections(); + } + }, 1500); + } + } + clearAutoExpandTimer() { + if (this.state.autoExpandTimer) { + clearTimeout(this.state.autoExpandTimer); + this.state.autoExpandTimer = null; + } + } + expandNodeSections() { + this.state.expandedSections.add("hoveredNode"); + this.state.expandedSections.add("node"); + this.state.expandedSections.add("cursor"); + this.state.expandedSections.add("canvas"); + this.state.expandedSections.add("magnifier"); + this.clearAutoExpandTimer(); + } + collapseNodeSections() { + this.state.expandedSections.delete("hoveredNode"); + this.state.expandedSections.delete("node"); + this.state.expandedSections.delete("widget"); + } + cleanup() { + this.clearAutoExpandTimer(); + } +} +export { + StateManager +}; +//# sourceMappingURL=StateManager.js.map diff --git a/web/info-panel/StateManager.js.map b/web/info-panel/StateManager.js.map new file mode 100644 index 0000000..ccfb2cd --- /dev/null +++ b/web/info-panel/StateManager.js.map @@ -0,0 +1 @@ +{"version":3,"file":"StateManager.js","sources":["../../src/info-panel/StateManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Info Panel State Manager (TypeScript)\n * \n * Centralized state management for the information panel.\n */\n\nimport type { GatheredInfo, SettingsChange } from '../types/comfyui';\nimport { getSettingValue } from '../shared/utils';\nimport { DEFAULT_PANEL_SETTINGS } from '../shared/settings';\n\nexport interface InfoPanelStateData {\n // Panel visibility and positioning\n isPanelVisible: boolean;\n wasPanelVisibleBeforeHide: boolean; // Persist state across glass toggles\n isPanelMinimized: boolean;\n isPanelPinned: boolean;\n isPanelLocked: boolean; // New lock state\n isAutoPinned: boolean; // Track if panel was auto-pinned by glass hide\n pinnedPosition: { x: number; y: number };\n lastPinnedPosition: { x: number; y: number } | null; // Remember last pinned location\n\n // Glass visibility\n isGlassPreviewVisible: boolean;\n\n // Section expansion\n expandedSections: Set;\n\n // Interaction states\n isPanelHovered: boolean;\n isHoveringNode: boolean;\n\n // Auto-collapse\n autoExpandTimer: ReturnType | null;\n lastNodeId: string | number | null;\n\n // Update scheduling\n updateScheduled: boolean;\n isInitialLoading: boolean;\n\n // Current data\n currentInfo: GatheredInfo | null;\n\n // Settings cache - using index signature for dynamic ComfyUI settings\n settings: { [key: string]: string | number | boolean };\n\n // Auto-detected theme\n currentTheme: string;\n}\n\n/**\n * State Manager class.\n * Handles all state-related operations and provides clean state transitions.\n */\nexport class StateManager {\n state: InfoPanelStateData;\n\n constructor() {\n this.state = {\n // Panel visibility and positioning\n isPanelVisible: false,\n wasPanelVisibleBeforeHide: false,\n isPanelMinimized: true,\n isPanelPinned: false,\n isPanelLocked: false, // New lock state\n isAutoPinned: false, // Track if panel was auto-pinned by glass hide\n pinnedPosition: { x: 0, y: 0 },\n lastPinnedPosition: null, // Remember last pinned location\n\n // Glass visibility\n isGlassPreviewVisible: true,\n\n // Section expansion\n expandedSections: new Set(['node']),\n\n // Interaction states\n isPanelHovered: false,\n isHoveringNode: false,\n\n // Auto-collapse\n autoExpandTimer: null,\n lastNodeId: null,\n\n // Update scheduling\n updateScheduled: false,\n isInitialLoading: false,\n\n // Current data\n currentInfo: null,\n\n // Settings cache\n settings: {},\n\n // Auto-detected theme\n currentTheme: 'dark' // Will be auto-detected\n };\n\n this.initThemeDetection();\n this.loadSettings();\n }\n\n initThemeDetection(): void {\n // Detect ComfyUI's current theme\n this.detectCurrentTheme();\n\n // Force an immediate check in case we missed the initial state\n setTimeout(() => {\n if (this.detectCurrentTheme()) {\n this.notifyThemeChange();\n }\n }, 1000);\n\n // Set up mutation observer to watch for theme changes\n this.setupThemeObserver();\n\n // Listen for system theme changes as fallback\n if (window.matchMedia) {\n const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');\n if (prefersDark.addListener) {\n prefersDark.addListener(() => this.detectCurrentTheme());\n } else if (prefersDark.addEventListener) {\n prefersDark.addEventListener('change', () => this.detectCurrentTheme());\n }\n }\n\n // Add click listener to theme toggle buttons for immediate detection\n this.setupThemeButtonListeners();\n }\n\n detectCurrentTheme(): boolean {\n let detectedTheme = 'dark'; // Default fallback\n\n // Method 1: Try to read from ComfyUI's color palette setting\n try {\n // ComfyUI stores theme in 'Comfy.ColorPalette' setting\n const colorPalette = getSettingValue('Comfy.ColorPalette', '');\n\n if (colorPalette) {\n // Extract theme name from various possible formats\n const paletteStr = String(colorPalette).toLowerCase();\n\n if (paletteStr.includes('solarized')) {\n detectedTheme = 'solarized';\n } else if (paletteStr.includes('arc')) {\n detectedTheme = 'arc';\n } else if (paletteStr.includes('nord')) {\n detectedTheme = 'nord';\n } else if (paletteStr.includes('github')) {\n detectedTheme = 'github';\n } else if (paletteStr.includes('light')) {\n detectedTheme = 'light';\n } else if (paletteStr.includes('dark')) {\n detectedTheme = 'dark';\n }\n }\n } catch (e) {\n // Fallback to brightness-based detection\n console.warn('ComfyUI Info Panel: Could not read theme from settings, using fallback detection');\n }\n\n // Method 2: Fallback - check body/html for theme classes or data attributes\n if (detectedTheme === 'dark') {\n const body = document.body;\n const html = document.documentElement;\n\n // Check for data-theme attribute\n const dataTheme = body.getAttribute('data-theme') || html.getAttribute('data-theme');\n if (dataTheme) {\n detectedTheme = dataTheme.toLowerCase();\n }\n\n // Check for theme classes\n const classes = (body.className + ' ' + html.className).toLowerCase();\n if (classes.includes('theme-solarized') || classes.includes('solarized')) {\n detectedTheme = 'solarized';\n } else if (classes.includes('theme-arc') || classes.includes('arc-theme')) {\n detectedTheme = 'arc';\n } else if (classes.includes('theme-nord') || classes.includes('nord-theme')) {\n detectedTheme = 'nord';\n } else if (classes.includes('theme-github') || classes.includes('github-theme')) {\n detectedTheme = 'github';\n } else if (classes.includes('theme-light') || classes.includes('light-theme')) {\n detectedTheme = 'light';\n }\n }\n\n // Method 3: Last resort - brightness detection for light vs dark\n if (detectedTheme === 'dark') {\n const bodyStyles = window.getComputedStyle(document.body);\n const bgColor = bodyStyles.backgroundColor;\n if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') {\n const rgbMatch = bgColor.match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)/);\n if (rgbMatch) {\n const [, r, g, b] = rgbMatch.map(Number);\n const brightness = (r * 299 + g * 587 + b * 114) / 1000;\n if (brightness > 180) {\n detectedTheme = 'light';\n }\n }\n }\n }\n\n // Update theme if it changed\n if (this.state.currentTheme !== detectedTheme) {\n this.state.currentTheme = detectedTheme;\n return true; // Theme changed\n }\n\n return false; // No change\n }\n\n setupThemeObserver(): void {\n // Watch for changes to body/html classes and attributes\n const observer = new MutationObserver((mutations) => {\n let shouldCheck = false;\n mutations.forEach((mutation) => {\n if (mutation.type === 'attributes' &&\n (mutation.attributeName === 'class' ||\n mutation.attributeName === 'data-theme' ||\n mutation.attributeName === 'style')) {\n shouldCheck = true;\n }\n });\n\n if (shouldCheck && this.detectCurrentTheme()) {\n // Theme changed, update UI immediately\n this.notifyThemeChange();\n }\n });\n\n // Observe changes to body, html, and vue app\n observer.observe(document.body, { attributes: true, attributeOldValue: true });\n observer.observe(document.documentElement, { attributes: true, attributeOldValue: true });\n\n const vueApp = document.querySelector('#vue-app');\n if (vueApp) {\n observer.observe(vueApp, { attributes: true, attributeOldValue: true });\n }\n\n // Also watch for changes to any element with theme-related classes\n const themeElements = document.querySelectorAll('[class*=\"theme\"], [class*=\"dark\"], [class*=\"light\"], [data-theme]');\n themeElements.forEach(el => {\n observer.observe(el, { attributes: true, attributeOldValue: true });\n });\n\n // Periodic fallback check every 2 seconds\n setInterval(() => {\n if (this.detectCurrentTheme()) {\n this.notifyThemeChange();\n }\n }, 2000);\n }\n\n setupThemeButtonListeners(): void {\n // Listen for clicks on theme toggle buttons\n document.addEventListener('click', (e) => {\n // Check if clicked element might be a theme toggle\n const target = e.target as HTMLElement;\n const isThemeButton = target.textContent?.toLowerCase().includes('theme') ||\n target.textContent?.toLowerCase().includes('light') ||\n target.textContent?.toLowerCase().includes('dark') ||\n target.title?.toLowerCase().includes('theme') ||\n (typeof target.className === 'string' && target.className.toLowerCase().includes('theme'));\n\n if (isThemeButton) {\n setTimeout(() => {\n if (this.detectCurrentTheme()) {\n this.notifyThemeChange();\n }\n }, 100);\n }\n });\n }\n\n notifyThemeChange(): void {\n // Update settings to reflect new theme\n this.state.settings[\"🔍MagnifyGlass.InfoPanelTheme\"] = this.state.currentTheme;\n\n // Notify UI components of theme change immediately\n if (window.infoPanelManager && window.infoPanelManager.uiManager) {\n window.infoPanelManager.uiManager.updateTheme(this.state.currentTheme);\n }\n }\n\n loadSettings(): void {\n Object.keys(DEFAULT_PANEL_SETTINGS).forEach(key => {\n // Skip the theme setting since we auto-detect it\n if (key !== \"🔍MagnifyGlass.InfoPanelTheme\") {\n this.state.settings[key] = getSettingValue(key, (DEFAULT_PANEL_SETTINGS as any)[key]);\n }\n });\n\n // Set theme to auto-detected value\n this.state.settings[\"🔍MagnifyGlass.InfoPanelTheme\"] = this.state.currentTheme;\n }\n\n updateSettings(): Record {\n const oldSettings = { ...this.state.settings };\n this.loadSettings();\n\n // Return what changed for reactive updates\n const changes: Record = {};\n Object.keys(this.state.settings).forEach(key => {\n if (oldSettings[key] !== this.state.settings[key]) {\n changes[key] = {\n old: oldSettings[key],\n new: this.state.settings[key]\n };\n }\n });\n\n return changes;\n }\n\n togglePanelVisibility(): boolean {\n this.state.isPanelVisible = !this.state.isPanelVisible;\n return this.state.isPanelVisible;\n }\n\n toggleGlassPreview(): boolean {\n this.state.isGlassPreviewVisible = !this.state.isGlassPreviewVisible;\n return this.state.isGlassPreviewVisible;\n }\n\n togglePinning(): boolean {\n const wasPinned = this.state.isPanelPinned;\n this.state.isPanelPinned = !this.state.isPanelPinned;\n\n // When unpinning, also unlock and clear auto-pin flag\n if (!this.state.isPanelPinned) {\n this.state.isPanelLocked = false;\n this.state.isAutoPinned = false; // Clear auto-pin flag when manually unpinning\n } else {\n // When manually pinning, also clear auto-pin flag (this is a user-initiated pin)\n this.state.isAutoPinned = false;\n }\n\n if (this.state.isPanelPinned && this.state.lastPinnedPosition) {\n // Restore to last pinned position\n this.state.pinnedPosition = { ...this.state.lastPinnedPosition };\n } else if (!this.state.isPanelPinned && wasPinned) {\n // Save current position when unpinning\n this.state.lastPinnedPosition = { ...this.state.pinnedPosition };\n }\n\n return this.state.isPanelPinned;\n }\n\n toggleLocking(): boolean {\n // Only allow locking when pinned\n if (!this.state.isPanelPinned) {\n return false;\n }\n\n this.state.isPanelLocked = !this.state.isPanelLocked;\n return this.state.isPanelLocked;\n }\n\n toggleMinimized(): boolean {\n this.state.isPanelMinimized = !this.state.isPanelMinimized;\n return this.state.isPanelMinimized;\n }\n\n toggleSection(sectionId: string): boolean {\n if (sectionId === 'node') return false; // Node section always expanded\n\n if (this.state.expandedSections.has(sectionId)) {\n this.state.expandedSections.delete(sectionId);\n } else {\n this.state.expandedSections.add(sectionId);\n }\n return true;\n }\n\n setPinnedPosition(x: number, y: number): void {\n this.state.pinnedPosition = { x, y };\n // Also save as last pinned position for memory\n this.state.lastPinnedPosition = { x, y };\n }\n\n setCurrentInfo(info: GatheredInfo): void {\n this.state.currentInfo = info;\n }\n\n scheduleAutoCollapse(): void {\n this.clearAutoExpandTimer();\n if (this.state.settings[\"🔍MagnifyGlass.InfoPanelAnimations\"]) {\n this.state.autoExpandTimer = setTimeout(() => {\n if (!this.state.isPanelHovered) {\n this.collapseNodeSections();\n }\n }, 1500);\n }\n }\n\n clearAutoExpandTimer(): void {\n if (this.state.autoExpandTimer) {\n clearTimeout(this.state.autoExpandTimer);\n this.state.autoExpandTimer = null;\n }\n }\n\n expandNodeSections(): void {\n this.state.expandedSections.add('hoveredNode');\n this.state.expandedSections.add('node');\n this.state.expandedSections.add('cursor');\n this.state.expandedSections.add('canvas');\n this.state.expandedSections.add('magnifier');\n this.clearAutoExpandTimer();\n }\n\n collapseNodeSections(): void {\n this.state.expandedSections.delete('hoveredNode');\n this.state.expandedSections.delete('node');\n this.state.expandedSections.delete('widget');\n }\n\n cleanup(): void {\n this.clearAutoExpandTimer();\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/info-panel/UIManager.js b/web/info-panel/UIManager.js new file mode 100644 index 0000000..a745929 --- /dev/null +++ b/web/info-panel/UIManager.js @@ -0,0 +1,517 @@ +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +import { Icons } from "../shared/icons.js"; +import { Logger } from "../shared/logger.js"; +import { formatValue, getValueClass, getValueAttributes } from "./ValueFormatter.js"; +import { getCheckpointInfo, getImageInfo, getTextBoxContent, getImportantNodeParameters } from "./NodeDataExtractor.js"; +class UIManager { + constructor(stateManager) { + __publicField(this, "stateManager"); + __publicField(this, "elements"); + this.stateManager = stateManager; + this.elements = { + panel: null, + header: null, + content: null, + controls: null + }; + this.createPanel(); + this.injectStyles(); + } + /** + * Create the main panel and its components. + */ + createPanel() { + this.elements.panel = document.createElement("div"); + this.elements.panel.id = "comfyui-magnify-info-panel-pro-v2"; + this.elements.panel.className = `magnify-info-panel theme-${this.stateManager.state.currentTheme}`; + this.elements.header = document.createElement("div"); + this.elements.header.className = "panel-header"; + this.elements.header.innerHTML = ` +
+
${Icons.magnifyGlass}
+
Inspector
+
Real-time analysis
+
+
+ +
+ `; + this.elements.content = document.createElement("div"); + this.elements.content.className = "panel-content"; + this.elements.panel.appendChild(this.elements.header); + this.elements.panel.appendChild(this.elements.content); + this.applyStyles(); + if (this.stateManager.state.isPanelMinimized) { + this.elements.panel.classList.add("panel-minimized"); + } + document.body.appendChild(this.elements.panel); + this.elements.panel.addEventListener("click", (e) => { + const target = e.target; + const minimizeBtn = target.closest('[data-action="minimize"]'); + if (minimizeBtn) { + this.stateManager.state.isPanelMinimized = !this.stateManager.state.isPanelMinimized; + this.updateMinimizedState(); + return; + } + const sectionHeader = target.closest(".section-header"); + if (sectionHeader) { + const sectionId = sectionHeader.getAttribute("data-section"); + if (sectionId && sectionId !== "node") { + const isExpanded = this.stateManager.state.expandedSections.has(sectionId); + if (isExpanded) { + this.stateManager.state.expandedSections.delete(sectionId); + } else { + this.stateManager.state.expandedSections.add(sectionId); + } + sectionHeader.classList.toggle("expanded", !isExpanded); + const sectionContent = sectionHeader.nextElementSibling; + if (sectionContent && sectionContent.classList.contains("section-content")) { + sectionContent.classList.toggle("expanded", !isExpanded); + } + } + } + }); + if (!this.elements.controls) { + this.createFloatingControls(); + this.updateControlStates(); + } + } + /** + * Create floating control buttons. + */ + createFloatingControls() { + this.elements.controls = document.createElement("div"); + this.elements.controls.className = "floating-controls vertical-layout"; + this.elements.controls.innerHTML = ` + + + + + `; + document.body.appendChild(this.elements.controls); + this.elements.controls.style.display = "none"; + this.elements.controls.style.visibility = "hidden"; + this.elements.controls.style.left = "-9999px"; + this.elements.controls.style.top = "-9999px"; + this.elements.controls.addEventListener("click", (e) => { + var _a; + const target = e.target; + const button = target.closest("button[data-action]"); + if (!button) return; + const action = button.getAttribute("data-action"); + Logger.debug(`Control button clicked: ${action}`); + switch (action) { + case "pin": + this.stateManager.togglePinning(); + this.updatePinnedState(); + this.updateControlStates(); + break; + case "lock": + this.stateManager.state.isPanelLocked = !this.stateManager.state.isPanelLocked; + this.updateControlStates(); + if (this.elements.panel) { + this.elements.panel.classList.toggle("panel-locked", this.stateManager.state.isPanelLocked); + } + break; + case "toggle-panel": + if (this.stateManager.state.isPanelVisible) { + this.hide(); + } else { + this.show(); + } + this.updateControlStates(); + break; + case "toggle-glass": + this.stateManager.state.isGlassPreviewVisible = !this.stateManager.state.isGlassPreviewVisible; + if (!this.stateManager.state.isGlassPreviewVisible) { + console.log(`[MagnifyGlass Debug] Glass Hidden. pinned=${this.stateManager.state.isPanelPinned}, autoPinned=${this.stateManager.state.isAutoPinned}`); + if (!this.stateManager.state.isPanelPinned) { + if (this.elements.panel) { + const rect = this.elements.panel.getBoundingClientRect(); + this.stateManager.state.pinnedPosition = { x: rect.left, y: rect.top }; + } + this.stateManager.state.isPanelPinned = true; + this.stateManager.state.isAutoPinned = true; + console.log(`[MagnifyGlass Debug] Auto-Pinned panel.`); + } + this.stateManager.state.isPanelLocked = false; + } else { + console.log(`[MagnifyGlass Debug] Glass Shown. pinned=${this.stateManager.state.isPanelPinned}, autoPinned=${this.stateManager.state.isAutoPinned}`); + if (this.stateManager.state.isAutoPinned) { + this.stateManager.state.isPanelPinned = false; + this.stateManager.state.isAutoPinned = false; + console.log(`[MagnifyGlass Debug] Auto-Unpinned panel.`); + } + } + this.updateControlStates(); + this.updatePinnedState(); + const magnifyGlass = window.comfyUIMagnifyGlass; + if (magnifyGlass && ((_a = magnifyGlass.ui) == null ? void 0 : _a.setPreviewVisibility)) { + magnifyGlass.ui.setPreviewVisibility(this.stateManager.state.isGlassPreviewVisible); + } + break; + } + }); + const controlsPosition = String(this.stateManager.state.settings["🔍MagnifyGlass.ControlsPosition"] || "top-right"); + this.updateControlsLayout(controlsPosition); + this.updateControlStates(); + } + /** + * Update control button states. + */ + updateControlStates() { + if (!this.elements.controls) return; + const pinBtn = this.elements.controls.querySelector('[data-action="pin"]'); + const lockBtn = this.elements.controls.querySelector('[data-action="lock"]'); + const visibilityBtn = this.elements.controls.querySelector('[data-action="toggle-panel"]'); + const glassBtn = this.elements.controls.querySelector('[data-action="toggle-glass"]'); + const isPanelVisible = this.stateManager.state.isPanelVisible; + const isGlassVisible = this.stateManager.state.isGlassPreviewVisible; + if (pinBtn) { + pinBtn.classList.toggle("active", this.stateManager.state.isPanelPinned); + pinBtn.title = this.stateManager.state.isPanelPinned ? "Lock Panel" : "Unlock Panel"; + pinBtn.innerHTML = this.stateManager.state.isPanelPinned ? Icons.lock : Icons.unlock; + pinBtn.style.display = isPanelVisible ? "flex" : "none"; + if (!isGlassVisible) { + pinBtn.disabled = true; + pinBtn.style.opacity = "0.5"; + pinBtn.title = "Cannot unlock panel from screen when glass preview is hidden"; + } else { + pinBtn.disabled = false; + pinBtn.style.opacity = ""; + } + } + if (lockBtn) { + const showLockBtn = isPanelVisible && this.stateManager.state.isPanelPinned; + lockBtn.style.display = showLockBtn ? "flex" : "none"; + lockBtn.classList.toggle("active", this.stateManager.state.isPanelLocked); + lockBtn.title = this.stateManager.state.isPanelLocked ? "Unpin Panel Position" : "Pin Panel Position"; + lockBtn.disabled = !this.stateManager.state.isPanelPinned; + } + if (visibilityBtn) { + visibilityBtn.classList.toggle("active", isPanelVisible); + visibilityBtn.title = isPanelVisible ? "Hide Panel" : "Show Panel"; + if (!isGlassVisible) { + visibilityBtn.disabled = true; + visibilityBtn.style.opacity = "0.5"; + visibilityBtn.title = "Cannot hide panel when glass preview is hidden"; + } else { + visibilityBtn.disabled = false; + visibilityBtn.style.opacity = ""; + } + } + if (glassBtn) { + glassBtn.classList.toggle("active", isGlassVisible); + glassBtn.title = isGlassVisible ? "Hide Glass Preview" : "Show Glass Preview"; + glassBtn.style.display = isPanelVisible ? "flex" : "none"; + } + } + /** + * Update controls layout based on position setting. + * @param position + */ + updateControlsLayout(position) { + if (!this.elements.controls) return; + this.elements.controls.classList.remove("horizontal-layout", "vertical-layout"); + if (["top", "bottom"].includes(position)) { + this.elements.controls.classList.add("horizontal-layout"); + } else { + this.elements.controls.classList.add("vertical-layout"); + } + } + /** + * Apply current styles to elements. + */ + applyStyles() { + if (!this.elements.panel) return; + const settings = this.stateManager.state.settings; + console.log(`[UIManager] applyStyles - MaxHeight: ${settings["🔍MagnifyGlass.InfoPanelMaxHeight"]}`); + this.elements.panel.style.width = `${settings["🔍MagnifyGlass.InfoPanelWidth"]}px`; + const maxHeight = settings["🔍MagnifyGlass.InfoPanelMaxHeight"]; + this.elements.panel.style.height = `${maxHeight}px`; + this.elements.panel.style.maxHeight = `${maxHeight}px`; + this.elements.panel.style.position = "absolute"; + this.elements.panel.style.zIndex = "99999"; + this.elements.panel.style.transform = "translateY(-10px)"; + this.elements.panel.style.pointerEvents = "auto"; + this.elements.panel.style.userSelect = "none"; + this.elements.panel.style.fontFamily = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"; + this.elements.panel.style.transition = settings["🔍MagnifyGlass.InfoPanelAnimations"] ? "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)" : "none"; + const textColor = settings["🔍MagnifyGlass.InfoPanelTextColor"]; + if (textColor && typeof textColor === "string") { + const normalizedTextColor = textColor.startsWith("#") ? textColor : `#${textColor}`; + this.elements.panel.style.setProperty("--info-panel-text-color", normalizedTextColor); + } + const accentColor = settings["🔍MagnifyGlass.InfoPanelAccentColor"]; + if (accentColor && typeof accentColor === "string") { + const normalizedAccentColor = accentColor.startsWith("#") ? accentColor : `#${accentColor}`; + this.elements.panel.style.setProperty("--info-panel-accent-color", normalizedAccentColor); + } + if (this.stateManager.state.isPanelVisible) { + const opacityPercent = Number(settings["🔍MagnifyGlass.InfoPanelOpacity"]) || 100; + this.elements.panel.style.opacity = (opacityPercent / 100).toString(); + } else { + this.elements.panel.style.opacity = "0"; + } + } + /** + * Show the panel. + */ + show() { + if (!this.elements.panel) return; + if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"]) { + this.elements.panel.style.display = "block"; + const opacityPercent = Number(this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelOpacity"]) || 100; + this.elements.panel.style.opacity = (opacityPercent / 100).toString(); + this.elements.panel.offsetHeight; + this.elements.panel.classList.add("visible"); + this.stateManager.state.isPanelVisible = true; + this.updateControlStates(); + } + } + /** + * Hide the panel. + */ + hide() { + if (!this.elements.panel) return; + this.elements.panel.classList.remove("visible"); + setTimeout(() => { + if (!this.stateManager.state.isPanelVisible && this.elements.panel) { + this.elements.panel.style.display = "none"; + } + }, this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelAnimations"] ? 300 : 0); + this.stateManager.state.isPanelVisible = false; + this.updateControlStates(); + } + /** + * Update minimized state. + */ + updateMinimizedState() { + if (!this.elements.panel || !this.elements.header) return; + this.elements.panel.classList.toggle("panel-minimized", this.stateManager.state.isPanelMinimized); + const minimizeBtn = this.elements.header.querySelector(".minimize-btn"); + if (minimizeBtn) { + minimizeBtn.textContent = this.stateManager.state.isPanelMinimized ? "+" : "−"; + minimizeBtn.title = this.stateManager.state.isPanelMinimized ? "Expand Panel" : "Minimize Panel"; + } + } + /** + * Update pinned state. + */ + updatePinnedState() { + if (!this.elements.panel) return; + this.elements.panel.classList.toggle("panel-pinned", this.stateManager.state.isPanelPinned); + this.elements.panel.classList.toggle("panel-locked", this.stateManager.state.isPanelLocked); + this.updateControlStates(); + } + /** + * Update theme class. + * @param newTheme + */ + updateTheme(newTheme) { + if (this.elements.panel) { + this.elements.panel.className = this.elements.panel.className.replace(/theme-\w+/, `theme-${newTheme.toLowerCase()}`); + } + } + /** + * Display information in the panel. + * @param info + */ + displayInfo(info) { + const sections = this.buildSections(info); + this.renderSections(sections); + this.updateSectionStates(); + this.updateHeaderSubtitle(info); + } + /** + * Build section data from info object. + * @param info + * @returns + */ + buildSections(info) { + const sections = []; + const settings = this.stateManager.state.settings; + if (settings["🔍MagnifyGlass.ShowInspectorTab"] && info.cursor && info.cursor.canvas) { + const inspectorContent = [ + { label: "Cursor Canvas", value: `(${Math.round(info.cursor.canvas.x)}, ${Math.round(info.cursor.canvas.y)})` }, + { label: "Canvas Scale", value: `${(info.canvas.scale * 100).toFixed(1)}%` }, + { label: "Magnifier Zoom", value: `${info.magnifier.zoomFactor}×` } + ]; + sections.push({ + id: "inspector", + icon: Icons.info, + title: "Inspector", + content: inspectorContent + }); + } + if (info.media) { + const mediaContent = [ + { label: "Type", value: info.media.tagName }, + { label: "Source", value: info.media.src } + ]; + if (info.media.naturalSize) { + mediaContent.push({ label: "Natural", value: info.media.naturalSize }); + } + sections.push({ + id: "media", + icon: Icons.camera, + title: "Media", + badge: info.media.tagName, + content: mediaContent + }); + } + if (info.hoveredNode) { + const nodeContent = [ + { label: "Title", value: info.hoveredNode.title } + ]; + const nodeType = info.hoveredNode.type ? info.hoveredNode.type.toLowerCase() : ""; + const isSaveNode = nodeType.includes("save") && !nodeType.includes("checkpoint") && !nodeType.includes("model") && !nodeType.includes("preview"); + const showAllWidgets = info.hoveredNode.type && (nodeType.includes("ksampler") || nodeType.includes("sampler") || nodeType.includes("k_samplers") || nodeType.includes("checkpoint") || nodeType.includes("model") || nodeType.includes("lora") || nodeType.includes("controlnet") || nodeType.includes("advanced") || nodeType.includes("detailer") || nodeType.includes("inpaint") || nodeType.includes("upscale") || nodeType.includes("clip") || nodeType.includes("text") || nodeType.includes("encode")) && !isSaveNode; + if (!showAllWidgets) { + const checkpoint = getCheckpointInfo(info.hoveredNode); + if (checkpoint) { + nodeContent.push({ label: "Model", value: checkpoint }); + } + const imgInfo = getImageInfo(info.hoveredNode); + if (imgInfo) { + if (typeof imgInfo === "string") { + nodeContent.push({ label: "Image", value: imgInfo }); + } else if (typeof imgInfo === "object" && imgInfo !== null) { + const imgResult = imgInfo; + nodeContent.push({ label: "Image Size", value: `${imgResult.width}×${imgResult.height}` }); + if (imgResult.src) { + nodeContent.push({ label: "Image Source", value: imgResult.src }); + } + } + } + const textContent = getTextBoxContent(info.hoveredNode); + if (textContent) { + nodeContent.push({ label: "Text", value: textContent }); + } + } + const importantParameters = getImportantNodeParameters(info.hoveredNode); + nodeContent.push(...importantParameters); + sections.push({ + id: "node", + icon: Icons.box, + title: "Node", + badge: info.hoveredNode.type, + content: nodeContent + }); + } + return sections; + } + /** + * Render sections to the panel. + * @param sections + */ + renderSections(sections) { + if (!this.elements.content) return; + if (sections.length === 0) { + this.elements.content.innerHTML = ` +
+
${Icons.mapPin}
+
Empty canvas area
+
+ `; + return; + } + this.elements.content.innerHTML = sections.map((section) => ` +
+
+ ${section.icon} + ${section.title} + ${section.badge ? `${section.badge}` : ""} + ${section.id !== "node" ? `${Icons.chevronRight}` : ""} +
+
+
+ ${section.content.map((item) => { + const value = formatValue(item.value, item.label); + const valueClass = getValueClass(item.value); + const valueAttributes = getValueAttributes(item.value); + return ` +
+ ${item.label} + ${value} +
`; + }).join("")} +
+
+
+ `).join(""); + } + /** + * Update section expansion states. + */ + updateSectionStates() { + if (!this.elements.content) return; + const sections = this.elements.content.querySelectorAll(".info-section"); + sections.forEach((section) => { + const sectionId = section.dataset.section; + if (!sectionId) return; + const header = section.querySelector(".section-header"); + const content = section.querySelector(".section-content"); + if (header && content) { + if (this.stateManager.state.expandedSections.has(sectionId)) { + header.classList.add("expanded"); + content.classList.add("expanded"); + } else { + header.classList.remove("expanded"); + content.classList.remove("expanded"); + } + } + }); + } + /** + * Update header subtitle. + * @param info + */ + updateHeaderSubtitle(info) { + if (!this.elements.header) return; + const subtitleElement = this.elements.header.querySelector(".header-subtitle"); + if (subtitleElement) { + const accentColor = String(this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelAccentColor"] || ""); + if (info.hoveredNode) { + subtitleElement.textContent = `Analyzing: ${info.hoveredNode.title}`; + subtitleElement.style.color = accentColor; + } else if (info.media) { + subtitleElement.textContent = `Media: ${info.media.tagName}`; + subtitleElement.style.color = accentColor; + } else if (info.connection) { + subtitleElement.textContent = `Connection: ${info.connection.type}`; + subtitleElement.style.color = accentColor; + } else { + subtitleElement.textContent = "Real-time analysis"; + subtitleElement.style.color = ""; + } + } + } + /** + * Inject CSS styles by loading external stylesheet. + */ + injectStyles() { + if (!document.getElementById("magnify-info-panel-styles-v2")) { + const link = document.createElement("link"); + link.id = "magnify-info-panel-styles-v2"; + link.rel = "stylesheet"; + link.type = "text/css"; + link.href = "extensions/comfyui-magnifyglass/info-panel.css"; + document.head.appendChild(link); + } + } + cleanup() { + if (this.elements.panel && this.elements.panel.parentNode) { + this.elements.panel.parentNode.removeChild(this.elements.panel); + } + if (this.elements.controls && this.elements.controls.parentNode) { + this.elements.controls.parentNode.removeChild(this.elements.controls); + } + } +} +export { + UIManager +}; +//# sourceMappingURL=UIManager.js.map diff --git a/web/info-panel/UIManager.js.map b/web/info-panel/UIManager.js.map new file mode 100644 index 0000000..66a3abc --- /dev/null +++ b/web/info-panel/UIManager.js.map @@ -0,0 +1 @@ +{"version":3,"file":"UIManager.js","sources":["../../src/info-panel/UIManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Info Panel UI Manager (TypeScript)\n * \n * Complete UI Manager extracted from magnify_info_panel.js\n * Handles all DOM manipulation and UI creation for the info panel.\n */\n\nimport { StateManager } from './StateManager';\nimport { Icons } from '../shared/icons';\nimport { Logger } from '../shared/logger';\nimport { formatValue, getValueClass, getValueAttributes, formatWidgetValue } from './ValueFormatter';\nimport {\n getCheckpointInfo,\n getImageInfo,\n getTextBoxContent,\n getImportantNodeParameters,\n type ImageInfoResult\n} from './NodeDataExtractor';\n\ninterface InfoPanelElements {\n panel: HTMLDivElement | null;\n header: HTMLDivElement | null;\n content: HTMLDivElement | null;\n controls: HTMLDivElement | null;\n}\n\n/**\n * UI Manager class.\n * Handles all DOM manipulation and UI creation.\n */\nexport class UIManager {\n stateManager: StateManager;\n elements: InfoPanelElements;\n\n constructor(stateManager: StateManager) {\n this.stateManager = stateManager;\n this.elements = {\n panel: null,\n header: null,\n content: null,\n controls: null\n };\n\n this.createPanel();\n this.injectStyles();\n }\n\n /**\n * Create the main panel and its components.\n */\n createPanel(): void {\n // Main panel container\n this.elements.panel = document.createElement(\"div\");\n this.elements.panel.id = \"comfyui-magnify-info-panel-pro-v2\";\n this.elements.panel.className = `magnify-info-panel theme-${this.stateManager.state.currentTheme}`;\n\n // Header\n this.elements.header = document.createElement(\"div\");\n this.elements.header.className = \"panel-header\";\n this.elements.header.innerHTML = `\n
\n
${Icons.magnifyGlass}
\n
Inspector
\n
Real-time analysis
\n
\n
\n \n
\n `;\n\n // Content container\n this.elements.content = document.createElement(\"div\");\n this.elements.content.className = \"panel-content\";\n\n this.elements.panel.appendChild(this.elements.header);\n this.elements.panel.appendChild(this.elements.content);\n\n this.applyStyles();\n // Apply initial minimized state if set\n if (this.stateManager.state.isPanelMinimized) {\n this.elements.panel.classList.add('panel-minimized');\n }\n document.body.appendChild(this.elements.panel);\n\n // Add click event delegation for panel elements\n this.elements.panel.addEventListener('click', (e: MouseEvent) => {\n const target = e.target as HTMLElement;\n\n // Handle minimize button\n const minimizeBtn = target.closest('[data-action=\"minimize\"]');\n if (minimizeBtn) {\n this.stateManager.state.isPanelMinimized = !this.stateManager.state.isPanelMinimized;\n this.updateMinimizedState();\n return;\n }\n\n // Handle section header clicks for expand/collapse\n const sectionHeader = target.closest('.section-header') as HTMLElement;\n if (sectionHeader) {\n const sectionId = sectionHeader.getAttribute('data-section');\n // Skip node section (always expanded)\n if (sectionId && sectionId !== 'node') {\n const isExpanded = this.stateManager.state.expandedSections.has(sectionId);\n if (isExpanded) {\n this.stateManager.state.expandedSections.delete(sectionId);\n } else {\n this.stateManager.state.expandedSections.add(sectionId);\n }\n\n // Update visual state\n sectionHeader.classList.toggle('expanded', !isExpanded);\n const sectionContent = sectionHeader.nextElementSibling as HTMLElement;\n if (sectionContent && sectionContent.classList.contains('section-content')) {\n sectionContent.classList.toggle('expanded', !isExpanded);\n }\n }\n }\n });\n\n // Create floating controls after panel is in DOM\n if (!this.elements.controls) {\n this.createFloatingControls();\n // Update control states immediately after creation\n this.updateControlStates();\n }\n }\n\n /**\n * Create floating control buttons.\n */\n createFloatingControls(): void {\n this.elements.controls = document.createElement(\"div\");\n this.elements.controls.className = \"floating-controls vertical-layout\"; // Default to vertical\n\n /**\n * BUTTON MAPPING DOCUMENTATION:\n * =============================\n * Button 1 (Unlock Icon): data-action=\"pin\" → controls isPanelPinned\n * - Unpinned (default): Panel follows magnify glass\n * - Pinned: Panel stays at fixed position (unlocked from following glass)\n * \n * Button 2 (Pin Icon): data-action=\"lock\" → controls isPanelLocked\n * - Unlocked (default): Panel can be dragged\n * - Locked: Panel cannot be dragged (pinned in place)\n * \n * NOTE: The data-action names are counterintuitive but kept for backwards compatibility.\n * The ICONS correctly represent the functionality:\n * - Unlock icon = unlock from following glass\n * - Pin icon = pin position (prevent drag)\n */\n this.elements.controls.innerHTML = `\n \n \n \n \n `;\n\n // Insert before the panel in the document body, not as a child\n document.body.appendChild(this.elements.controls);\n\n // Hide and position off-screen initially - will be shown when magnify glass activates and positions them\n this.elements.controls.style.display = 'none';\n this.elements.controls.style.visibility = 'hidden';\n this.elements.controls.style.left = '-9999px';\n this.elements.controls.style.top = '-9999px';\n\n // Add click event delegation for control buttons\n this.elements.controls.addEventListener('click', (e: MouseEvent) => {\n const target = e.target as HTMLElement;\n const button = target.closest('button[data-action]') as HTMLButtonElement;\n if (!button) return;\n\n const action = button.getAttribute('data-action');\n Logger.debug(`Control button clicked: ${action}`);\n\n switch (action) {\n case 'pin':\n this.stateManager.togglePinning();\n this.updatePinnedState();\n this.updateControlStates();\n break;\n case 'lock':\n this.stateManager.state.isPanelLocked = !this.stateManager.state.isPanelLocked;\n this.updateControlStates();\n if (this.elements.panel) {\n this.elements.panel.classList.toggle('panel-locked', this.stateManager.state.isPanelLocked);\n }\n break;\n case 'toggle-panel':\n if (this.stateManager.state.isPanelVisible) {\n this.hide();\n } else {\n this.show();\n }\n this.updateControlStates();\n break;\n case 'toggle-glass':\n this.stateManager.state.isGlassPreviewVisible = !this.stateManager.state.isGlassPreviewVisible;\n\n // Link Glass Visibility to Panel Pinning\n if (!this.stateManager.state.isGlassPreviewVisible) {\n // Glass Hidden\n console.log(`[MagnifyGlass Debug] Glass Hidden. pinned=${this.stateManager.state.isPanelPinned}, autoPinned=${this.stateManager.state.isAutoPinned}`);\n // Only auto-pin if not already pinned by the user\n if (!this.stateManager.state.isPanelPinned) {\n if (this.elements.panel) {\n const rect = this.elements.panel.getBoundingClientRect();\n this.stateManager.state.pinnedPosition = { x: rect.left, y: rect.top };\n }\n this.stateManager.state.isPanelPinned = true;\n this.stateManager.state.isAutoPinned = true; // Track that this was auto-pinned\n console.log(`[MagnifyGlass Debug] Auto-Pinned panel.`);\n }\n this.stateManager.state.isPanelLocked = false; // Ensure dragging is allowed\n } else {\n // Glass Shown\n console.log(`[MagnifyGlass Debug] Glass Shown. pinned=${this.stateManager.state.isPanelPinned}, autoPinned=${this.stateManager.state.isAutoPinned}`);\n // Only unpin if it was auto-pinned (not user-pinned)\n if (this.stateManager.state.isAutoPinned) {\n this.stateManager.state.isPanelPinned = false;\n this.stateManager.state.isAutoPinned = false;\n console.log(`[MagnifyGlass Debug] Auto-Unpinned panel.`);\n }\n // If user manually pinned, keep the panel pinned\n }\n\n this.updateControlStates();\n this.updatePinnedState(); // Update visual class\n\n // Toggle ONLY the visual visibility (opacity) of the glass preview\n // The tool remains active so the inspector can track position\n const magnifyGlass = (window as any).comfyUIMagnifyGlass;\n if (magnifyGlass && magnifyGlass.ui?.setPreviewVisibility) {\n magnifyGlass.ui.setPreviewVisibility(this.stateManager.state.isGlassPreviewVisible);\n }\n break;\n }\n });\n\n // Set initial layout based on settings\n const controlsPosition = String(this.stateManager.state.settings[\"🔍MagnifyGlass.ControlsPosition\"] || \"top-right\");\n this.updateControlsLayout(controlsPosition);\n\n // Initial state update\n this.updateControlStates();\n }\n\n /**\n * Update control button states.\n */\n updateControlStates(): void {\n if (!this.elements.controls) return;\n\n const pinBtn = this.elements.controls.querySelector('[data-action=\"pin\"]') as HTMLButtonElement;\n const lockBtn = this.elements.controls.querySelector('[data-action=\"lock\"]') as HTMLButtonElement;\n const visibilityBtn = this.elements.controls.querySelector('[data-action=\"toggle-panel\"]') as HTMLButtonElement;\n const glassBtn = this.elements.controls.querySelector('[data-action=\"toggle-glass\"]') as HTMLButtonElement;\n const isPanelVisible = this.stateManager.state.isPanelVisible;\n const isGlassVisible = this.stateManager.state.isGlassPreviewVisible;\n\n if (pinBtn) {\n pinBtn.classList.toggle('active', this.stateManager.state.isPanelPinned);\n pinBtn.title = this.stateManager.state.isPanelPinned ? \"Lock Panel\" : \"Unlock Panel\";\n pinBtn.innerHTML = this.stateManager.state.isPanelPinned ? Icons.lock : Icons.unlock;\n // Hide unlock button when panel is hidden\n pinBtn.style.display = isPanelVisible ? 'flex' : 'none';\n\n // Disable unlock (unpin) button if glass is hidden\n // This prevents entering \"Follow Mouse\" mode when glass is invisible\n if (!isGlassVisible) {\n pinBtn.disabled = true;\n pinBtn.style.opacity = '0.5';\n pinBtn.title = \"Cannot unlock panel from screen when glass preview is hidden\";\n } else {\n pinBtn.disabled = false;\n pinBtn.style.opacity = '';\n }\n }\n\n if (lockBtn) {\n // Only show pin button when panel is visible AND unlocked from glass\n const showLockBtn = isPanelVisible && this.stateManager.state.isPanelPinned;\n lockBtn.style.display = showLockBtn ? 'flex' : 'none';\n lockBtn.classList.toggle('active', this.stateManager.state.isPanelLocked);\n lockBtn.title = this.stateManager.state.isPanelLocked ? \"Unpin Panel Position\" : \"Pin Panel Position\";\n lockBtn.disabled = !this.stateManager.state.isPanelPinned;\n }\n\n if (visibilityBtn) {\n // Active means \"Panel is Visible\"\n visibilityBtn.classList.toggle('active', isPanelVisible);\n visibilityBtn.title = isPanelVisible ? \"Hide Panel\" : \"Show Panel\";\n\n // Disable hide button if glass is hidden (prevent hiding everything)\n if (!isGlassVisible) {\n visibilityBtn.disabled = true;\n visibilityBtn.style.opacity = '0.5';\n visibilityBtn.title = \"Cannot hide panel when glass preview is hidden\";\n } else {\n visibilityBtn.disabled = false;\n visibilityBtn.style.opacity = '';\n }\n }\n\n if (glassBtn) {\n glassBtn.classList.toggle('active', isGlassVisible);\n glassBtn.title = isGlassVisible ? \"Hide Glass Preview\" : \"Show Glass Preview\";\n\n // Only show glass toggle button if the inspector panel is visible\n glassBtn.style.display = isPanelVisible ? 'flex' : 'none';\n }\n }\n\n /**\n * Update controls layout based on position setting.\n * @param position \n */\n updateControlsLayout(position: string): void {\n if (!this.elements.controls) return;\n\n // Remove existing layout classes\n this.elements.controls.classList.remove('horizontal-layout', 'vertical-layout');\n\n // Add appropriate layout class\n if (['top', 'bottom'].includes(position)) {\n this.elements.controls.classList.add('horizontal-layout');\n } else {\n this.elements.controls.classList.add('vertical-layout');\n }\n }\n\n /**\n * Apply current styles to elements.\n */\n applyStyles(): void {\n if (!this.elements.panel) return;\n\n const settings = this.stateManager.state.settings;\n\n // Debug Log\n console.log(`[UIManager] applyStyles - MaxHeight: ${settings[\"🔍MagnifyGlass.InfoPanelMaxHeight\"]}`);\n\n // 1. Apply dimensions\n this.elements.panel.style.width = `${settings[\"🔍MagnifyGlass.InfoPanelWidth\"]}px`;\n // Set both height and max-height so the panel always shows at this size\n const maxHeight = settings[\"🔍MagnifyGlass.InfoPanelMaxHeight\"];\n this.elements.panel.style.height = `${maxHeight}px`;\n this.elements.panel.style.maxHeight = `${maxHeight}px`;\n\n // 2. Apply static styles (only needed once or if overwritten, but safe to set)\n this.elements.panel.style.position = 'absolute';\n this.elements.panel.style.zIndex = '99999';\n this.elements.panel.style.transform = 'translateY(-10px)';\n this.elements.panel.style.pointerEvents = 'auto';\n this.elements.panel.style.userSelect = 'none';\n this.elements.panel.style.fontFamily = \"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif\";\n\n // 3. Apply transition\n this.elements.panel.style.transition = settings[\"🔍MagnifyGlass.InfoPanelAnimations\"]\n ? 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)'\n : 'none';\n\n // 4. Apply Custom Colors (CSS Variables)\n const textColor = settings[\"🔍MagnifyGlass.InfoPanelTextColor\"] as string | undefined;\n if (textColor && typeof textColor === 'string') {\n const normalizedTextColor = textColor.startsWith('#') ? textColor : `#${textColor}`;\n this.elements.panel.style.setProperty('--info-panel-text-color', normalizedTextColor);\n }\n\n const accentColor = settings[\"🔍MagnifyGlass.InfoPanelAccentColor\"] as string | undefined;\n if (accentColor && typeof accentColor === 'string') {\n const normalizedAccentColor = accentColor.startsWith('#') ? accentColor : `#${accentColor}`;\n this.elements.panel.style.setProperty('--info-panel-accent-color', normalizedAccentColor);\n }\n\n // 5. Apply Opacity\n // Logic: If visible, use setting. If hidden, use 0.\n // We do NOT touch 'display' here to avoid hiding it if it's currently shown.\n // 'display' is managed by show() / hide() methods exclusively.\n if (this.stateManager.state.isPanelVisible) {\n const opacityPercent = Number(settings[\"🔍MagnifyGlass.InfoPanelOpacity\"]) || 100;\n this.elements.panel.style.opacity = (opacityPercent / 100).toString();\n } else {\n this.elements.panel.style.opacity = '0';\n }\n }\n\n /**\n * Show the panel.\n */\n show(): void {\n if (!this.elements.panel) return;\n\n if (this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelEnabled\"]) {\n this.elements.panel.style.display = \"block\";\n // Controls are shown by positionFloatingControls() after proper positioning\n // Apply user's opacity setting when showing (convert percentage to decimal)\n const opacityPercent = Number(this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelOpacity\"]) || 100;\n this.elements.panel.style.opacity = (opacityPercent / 100).toString();\n // Trigger reflow\n this.elements.panel.offsetHeight;\n this.elements.panel.classList.add('visible');\n this.stateManager.state.isPanelVisible = true;\n this.updateControlStates();\n }\n }\n\n /**\n * Hide the panel.\n */\n hide(): void {\n if (!this.elements.panel) return;\n\n this.elements.panel.classList.remove('visible');\n setTimeout(() => {\n if (!this.stateManager.state.isPanelVisible && this.elements.panel) {\n this.elements.panel.style.display = \"none\";\n // Do NOT hide controls when panel is hidden - they should remain visible on glass\n // if (this.elements.controls) {\n // this.elements.controls.style.display = \"none\";\n // }\n }\n }, this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelAnimations\"] ? 300 : 0);\n this.stateManager.state.isPanelVisible = false;\n this.updateControlStates();\n }\n\n /**\n * Update minimized state.\n */\n updateMinimizedState(): void {\n if (!this.elements.panel || !this.elements.header) return;\n\n this.elements.panel.classList.toggle('panel-minimized', this.stateManager.state.isPanelMinimized);\n const minimizeBtn = this.elements.header.querySelector('.minimize-btn');\n if (minimizeBtn) {\n minimizeBtn.textContent = this.stateManager.state.isPanelMinimized ? '+' : '−';\n (minimizeBtn as HTMLElement).title = this.stateManager.state.isPanelMinimized ? 'Expand Panel' : 'Minimize Panel';\n }\n }\n\n /**\n * Update pinned state.\n */\n updatePinnedState(): void {\n if (!this.elements.panel) return;\n\n this.elements.panel.classList.toggle('panel-pinned', this.stateManager.state.isPanelPinned);\n this.elements.panel.classList.toggle('panel-locked', this.stateManager.state.isPanelLocked);\n this.updateControlStates();\n }\n\n /**\n * Update theme class.\n * @param newTheme \n */\n updateTheme(newTheme: string): void {\n if (this.elements.panel) {\n this.elements.panel.className = this.elements.panel.className.replace(/theme-\\w+/, `theme-${newTheme.toLowerCase()}`);\n }\n }\n\n /**\n * Display information in the panel.\n * @param info \n */\n displayInfo(info: any): void {\n const sections = this.buildSections(info);\n this.renderSections(sections);\n this.updateSectionStates();\n this.updateHeaderSubtitle(info);\n }\n\n /**\n * Build section data from info object.\n * @param info \n * @returns \n */\n buildSections(info: any): any[] {\n const sections: any[] = [];\n const settings = this.stateManager.state.settings;\n\n // Inspector section\n if (settings[\"🔍MagnifyGlass.ShowInspectorTab\"] && info.cursor && info.cursor.canvas) {\n const inspectorContent = [\n { label: 'Cursor Canvas', value: `(${Math.round(info.cursor.canvas.x)}, ${Math.round(info.cursor.canvas.y)})` },\n { label: 'Canvas Scale', value: `${(info.canvas.scale * 100).toFixed(1)}%` },\n { label: 'Magnifier Zoom', value: `${info.magnifier.zoomFactor}×` }\n ];\n\n sections.push({\n id: 'inspector',\n icon: Icons.info,\n title: 'Inspector',\n content: inspectorContent\n });\n }\n\n // Media section\n if (info.media) {\n const mediaContent = [\n { label: 'Type', value: info.media.tagName },\n { label: 'Source', value: info.media.src }\n ];\n\n if (info.media.naturalSize) {\n mediaContent.push({ label: 'Natural', value: info.media.naturalSize });\n }\n\n sections.push({\n id: 'media',\n icon: Icons.camera,\n title: 'Media',\n badge: info.media.tagName,\n content: mediaContent\n });\n }\n\n // Node Details section\n if (info.hoveredNode) {\n const nodeContent = [\n { label: 'Title', value: info.hoveredNode.title }\n ];\n\n // Check if this is a complex node that shows all widgets\n const nodeType = info.hoveredNode.type ? info.hoveredNode.type.toLowerCase() : '';\n const isSaveNode = nodeType.includes('save') &&\n !nodeType.includes('checkpoint') &&\n !nodeType.includes('model') &&\n !nodeType.includes('preview');\n\n const showAllWidgets = info.hoveredNode.type && (\n nodeType.includes('ksampler') ||\n nodeType.includes('sampler') ||\n nodeType.includes('k_samplers') ||\n nodeType.includes('checkpoint') ||\n nodeType.includes('model') ||\n nodeType.includes('lora') ||\n nodeType.includes('controlnet') ||\n nodeType.includes('advanced') ||\n nodeType.includes('detailer') ||\n nodeType.includes('inpaint') ||\n nodeType.includes('upscale') ||\n nodeType.includes('clip') ||\n nodeType.includes('text') ||\n nodeType.includes('encode')\n ) && !isSaveNode;\n\n if (!showAllWidgets) {\n // Add specific extractions for simple nodes\n const checkpoint = getCheckpointInfo(info.hoveredNode);\n if (checkpoint) {\n nodeContent.push({ label: 'Model', value: checkpoint });\n }\n\n const imgInfo = getImageInfo(info.hoveredNode);\n if (imgInfo) {\n if (typeof imgInfo === 'string') {\n nodeContent.push({ label: 'Image', value: imgInfo });\n } else if (typeof imgInfo === 'object' && imgInfo !== null) {\n const imgResult = imgInfo as ImageInfoResult;\n nodeContent.push({ label: 'Image Size', value: `${imgResult.width}×${imgResult.height}` });\n if (imgResult.src) {\n nodeContent.push({ label: 'Image Source', value: imgResult.src });\n }\n }\n }\n\n const textContent = getTextBoxContent(info.hoveredNode);\n if (textContent) {\n nodeContent.push({ label: 'Text', value: textContent });\n }\n }\n\n const importantParameters = getImportantNodeParameters(info.hoveredNode);\n nodeContent.push(...importantParameters);\n\n sections.push({\n id: 'node',\n icon: Icons.box,\n title: 'Node',\n badge: info.hoveredNode.type,\n content: nodeContent\n });\n }\n\n return sections;\n }\n\n /**\n * Render sections to the panel.\n * @param sections \n */\n renderSections(sections: any[]): void {\n if (!this.elements.content) return;\n\n if (sections.length === 0) {\n this.elements.content.innerHTML = `\n
\n
${Icons.mapPin}
\n
Empty canvas area
\n
\n `;\n return;\n }\n\n this.elements.content.innerHTML = sections.map(section => `\n
\n
\n ${section.icon}\n ${section.title}\n ${section.badge ? `${section.badge}` : ''}\n ${section.id !== 'node' ? `${Icons.chevronRight}` : ''}\n
\n
\n
\n ${section.content.map((item: any) => {\n const value = formatValue(item.value, item.label);\n const valueClass = getValueClass(item.value);\n const valueAttributes = getValueAttributes(item.value);\n return `\n
\n ${item.label}\n ${value}\n
`;\n }).join('')}\n
\n
\n
\n `).join('');\n }\n\n /**\n * Update section expansion states.\n */\n updateSectionStates(): void {\n if (!this.elements.content) return;\n\n const sections = this.elements.content.querySelectorAll('.info-section');\n sections.forEach(section => {\n const sectionId = (section as HTMLElement).dataset.section;\n if (!sectionId) return;\n\n const header = section.querySelector('.section-header');\n const content = section.querySelector('.section-content');\n\n if (header && content) {\n if (this.stateManager.state.expandedSections.has(sectionId)) {\n header.classList.add('expanded');\n content.classList.add('expanded');\n } else {\n header.classList.remove('expanded');\n content.classList.remove('expanded');\n }\n }\n });\n }\n\n /**\n * Update header subtitle.\n * @param info \n */\n updateHeaderSubtitle(info: any): void {\n if (!this.elements.header) return;\n\n const subtitleElement = this.elements.header.querySelector('.header-subtitle') as HTMLElement;\n if (subtitleElement) {\n const accentColor = String(this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelAccentColor\"] || '');\n if (info.hoveredNode) {\n subtitleElement.textContent = `Analyzing: ${info.hoveredNode.title}`;\n subtitleElement.style.color = accentColor;\n } else if (info.media) {\n subtitleElement.textContent = `Media: ${info.media.tagName}`;\n subtitleElement.style.color = accentColor;\n } else if (info.connection) {\n subtitleElement.textContent = `Connection: ${info.connection.type}`;\n subtitleElement.style.color = accentColor;\n } else {\n subtitleElement.textContent = 'Real-time analysis';\n subtitleElement.style.color = '';\n }\n }\n }\n\n /**\n * Inject CSS styles by loading external stylesheet.\n */\n injectStyles(): void {\n if (!document.getElementById('magnify-info-panel-styles-v2')) {\n const link = document.createElement('link');\n link.id = 'magnify-info-panel-styles-v2';\n link.rel = 'stylesheet';\n link.type = 'text/css';\n link.href = 'extensions/comfyui-magnifyglass/info-panel.css';\n document.head.appendChild(link);\n }\n }\n\n cleanup(): void {\n if (this.elements.panel && this.elements.panel.parentNode) {\n this.elements.panel.parentNode.removeChild(this.elements.panel);\n }\n if (this.elements.controls && this.elements.controls.parentNode) {\n this.elements.controls.parentNode.removeChild(this.elements.controls);\n }\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/info-panel/ValueFormatter.js b/web/info-panel/ValueFormatter.js new file mode 100644 index 0000000..e5db640 --- /dev/null +++ b/web/info-panel/ValueFormatter.js @@ -0,0 +1,50 @@ +function formatValue(value, label) { + if (value === null || value === void 0) return ""; + const str = String(value); + if (label && (label.toLowerCase().includes("text") || label.toLowerCase().includes("prompt") || label.toLowerCase().includes("model") || label.toLowerCase().includes("file") || label.toLowerCase().includes("conditioning") || label.toLowerCase().includes("positive") || label.toLowerCase().includes("negative"))) { + return str; + } + return str; +} +function getValueClass(value) { + if (!value) return ""; + const str = String(value); + const classes = []; + if (str.length > 100) { + classes.push("long-text"); + } + return classes.join(" "); +} +function getValueAttributes(value) { + if (!value) return ""; + const str = String(value); + if (str.length > 500) { + return `title="${str.replace(/"/g, """)}"`; + } + return ""; +} +function formatWidgetValue(value) { + if (value === null) return "null"; + if (value === void 0) return "undefined"; + if (typeof value === "string") { + return value; + } + if (typeof value === "number") { + return Number.isInteger(value) ? value.toString() : value.toFixed(3); + } + if (typeof value === "boolean") return value.toString(); + if (Array.isArray(value)) { + return `Array(${value.length})`; + } + if (typeof value === "object") { + return "Object"; + } + return String(value); +} +export { + formatValue, + formatWidgetValue, + getValueAttributes, + getValueClass +}; +//# sourceMappingURL=ValueFormatter.js.map diff --git a/web/info-panel/ValueFormatter.js.map b/web/info-panel/ValueFormatter.js.map new file mode 100644 index 0000000..8dbb9a8 --- /dev/null +++ b/web/info-panel/ValueFormatter.js.map @@ -0,0 +1 @@ +{"version":3,"file":"ValueFormatter.js","sources":["../../src/info-panel/ValueFormatter.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Value Formatter Utilities\n * \n * Pure utility functions for formatting values in the info panel display.\n * Extracted from UIManager.ts for better modularity.\n */\n\n/**\n * Format a value for display in the info panel.\n * @param value - The value to format\n * @param label - Optional label to determine formatting rules\n * @returns Formatted string representation\n */\nexport function formatValue(value: unknown, label?: string): string {\n if (value === null || value === undefined) return '';\n\n const str = String(value);\n\n // Show full text for prompts, text content, model names, and file paths\n if (label && (\n label.toLowerCase().includes('text') ||\n label.toLowerCase().includes('prompt') ||\n label.toLowerCase().includes('model') ||\n label.toLowerCase().includes('file') ||\n label.toLowerCase().includes('conditioning') ||\n label.toLowerCase().includes('positive') ||\n label.toLowerCase().includes('negative')\n )) {\n return str;\n }\n\n // Show full text for very long values (no truncation)\n return str;\n}\n\n/**\n * Get CSS class names for styling a value.\n * @param value - The value to get classes for\n * @returns CSS class string\n */\nexport function getValueClass(value: unknown): string {\n if (!value) return '';\n\n const str = String(value);\n const classes: string[] = [];\n\n // Mark text that might benefit from special styling for readability\n if (str.length > 100) {\n classes.push('long-text');\n }\n\n return classes.join(' ');\n}\n\n/**\n * Get HTML attributes for a value element.\n * @param value - The value to get attributes for\n * @returns HTML attribute string\n */\nexport function getValueAttributes(value: unknown): string {\n // Since we show full text now, we don't need title attributes for long text\n // Only add title for very long text that might benefit from tooltips\n if (!value) return '';\n\n const str = String(value);\n if (str.length > 500) { // Only for extremely long text\n return `title=\"${str.replace(/\"/g, '"')}\"`;\n }\n\n return '';\n}\n\n/**\n * Format widget value for display.\n * @param value - Widget value to format\n * @returns Formatted string representation\n */\nexport function formatWidgetValue(value: unknown): string {\n if (value === null) return 'null';\n if (value === undefined) return 'undefined';\n if (typeof value === 'string') {\n return value; // Show full text without truncation\n }\n if (typeof value === 'number') {\n return Number.isInteger(value) ? value.toString() : value.toFixed(3);\n }\n if (typeof value === 'boolean') return value.toString();\n if (Array.isArray(value)) {\n return `Array(${value.length})`;\n }\n if (typeof value === 'object') {\n return 'Object';\n }\n return String(value);\n}\n"],"names":[],"mappings":"AAaO,SAAS,YAAY,OAAgB,OAAwB;AAChE,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAElD,QAAM,MAAM,OAAO,KAAK;AAGxB,MAAI,UACA,MAAM,YAAA,EAAc,SAAS,MAAM,KACnC,MAAM,YAAA,EAAc,SAAS,QAAQ,KACrC,MAAM,YAAA,EAAc,SAAS,OAAO,KACpC,MAAM,YAAA,EAAc,SAAS,MAAM,KACnC,MAAM,cAAc,SAAS,cAAc,KAC3C,MAAM,cAAc,SAAS,UAAU,KACvC,MAAM,cAAc,SAAS,UAAU,IACxC;AACC,WAAO;AAAA,EACX;AAGA,SAAO;AACX;AAOO,SAAS,cAAc,OAAwB;AAClD,MAAI,CAAC,MAAO,QAAO;AAEnB,QAAM,MAAM,OAAO,KAAK;AACxB,QAAM,UAAoB,CAAA;AAG1B,MAAI,IAAI,SAAS,KAAK;AAClB,YAAQ,KAAK,WAAW;AAAA,EAC5B;AAEA,SAAO,QAAQ,KAAK,GAAG;AAC3B;AAOO,SAAS,mBAAmB,OAAwB;AAGvD,MAAI,CAAC,MAAO,QAAO;AAEnB,QAAM,MAAM,OAAO,KAAK;AACxB,MAAI,IAAI,SAAS,KAAK;AAClB,WAAO,UAAU,IAAI,QAAQ,MAAM,QAAQ,CAAC;AAAA,EAChD;AAEA,SAAO;AACX;AAOO,SAAS,kBAAkB,OAAwB;AACtD,MAAI,UAAU,KAAM,QAAO;AAC3B,MAAI,UAAU,OAAW,QAAO;AAChC,MAAI,OAAO,UAAU,UAAU;AAC3B,WAAO;AAAA,EACX;AACA,MAAI,OAAO,UAAU,UAAU;AAC3B,WAAO,OAAO,UAAU,KAAK,IAAI,MAAM,aAAa,MAAM,QAAQ,CAAC;AAAA,EACvE;AACA,MAAI,OAAO,UAAU,UAAW,QAAO,MAAM,SAAA;AAC7C,MAAI,MAAM,QAAQ,KAAK,GAAG;AACtB,WAAO,SAAS,MAAM,MAAM;AAAA,EAChC;AACA,MAAI,OAAO,UAAU,UAAU;AAC3B,WAAO;AAAA,EACX;AACA,SAAO,OAAO,KAAK;AACvB;"} \ No newline at end of file diff --git a/web/magnify-glass/ConfigManager.js b/web/magnify-glass/ConfigManager.js new file mode 100644 index 0000000..be578a2 --- /dev/null +++ b/web/magnify-glass/ConfigManager.js @@ -0,0 +1,148 @@ +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +import { getSettingValue } from "../shared/utils.js"; +import { STORAGE_KEYS } from "../shared/constants.js"; +const DEFAULT_GLASS_SETTINGS = { + "🔍MagnifyGlass.ZoomFactor": 300, + "🔍MagnifyGlass.GlassSize": 300, + "🔍MagnifyGlass.BorderColor": "#6b7280", + "🔍MagnifyGlass.BorderWidth": 1, + "🔍MagnifyGlass.ActivationKey": "x", + "🔍MagnifyGlass.AltRequired": false, + "🔍MagnifyGlass.FollowCursor": false, + "🔍MagnifyGlass.DebugMode": false, + "🔍MagnifyGlass.OffsetStep": 5, + "🔍MagnifyGlass.GlassPosition": "Top-Right", + "🔍MagnifyGlass.ResetKey": "o", + "🔍MagnifyGlass.GlassShape": "Rounded Square", + "🔍MagnifyGlass.BorderEnabled": true, + "🔍MagnifyGlass.TextureFiltering": "Linear", + "🔍MagnifyGlass.AlwaysActiveMode": true, + "🔍MagnifyGlass.ToggleFollowCursorKey": "h" +}; +class ConfigManager { + constructor() { + /** Zoom factor (1.0 = 100%, 3.0 = 300%) */ + __publicField(this, "zoomFactor"); + /** Glass size in pixels */ + __publicField(this, "glassSize"); + /** Border color (CSS color string) */ + __publicField(this, "borderColor"); + /** Border width in pixels */ + __publicField(this, "borderWidth"); + /** Activation key (lowercase) */ + __publicField(this, "activationKey"); + /** Whether Alt key is required for activation */ + __publicField(this, "altRequired"); + /** Whether glass follows cursor */ + __publicField(this, "followCursor"); + /** Debug mode enabled */ + __publicField(this, "debugMode"); + /** Offset step for arrow key adjustments */ + __publicField(this, "offsetStep"); + /** Glass position relative to cursor */ + __publicField(this, "glassPosition"); + /** Reset key (lowercase) */ + __publicField(this, "resetKey"); + /** Glass shape */ + __publicField(this, "glassShape"); + /** Whether border is enabled */ + __publicField(this, "borderEnabled"); + /** Texture filtering mode */ + __publicField(this, "textureFiltering"); + /** Always active mode */ + __publicField(this, "alwaysActiveMode"); + /** Toggle follow cursor key */ + __publicField(this, "toggleFollowCursorKey"); + /** Manual offset X in graph units */ + __publicField(this, "offsetX"); + /** Manual offset Y in graph units */ + __publicField(this, "offsetY"); + this.zoomFactor = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.ZoomFactor"] / 100; + this.glassSize = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.GlassSize"]; + this.borderColor = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.BorderColor"]; + this.borderWidth = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.BorderWidth"]; + this.activationKey = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.ActivationKey"]; + this.altRequired = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.AltRequired"]; + this.followCursor = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.FollowCursor"]; + this.debugMode = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.DebugMode"]; + this.offsetStep = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.OffsetStep"]; + this.glassPosition = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.GlassPosition"]; + this.resetKey = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.ResetKey"]; + this.glassShape = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.GlassShape"]; + this.borderEnabled = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.BorderEnabled"]; + this.textureFiltering = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.TextureFiltering"]; + this.alwaysActiveMode = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.AlwaysActiveMode"]; + this.toggleFollowCursorKey = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.ToggleFollowCursorKey"]; + this.offsetX = 0; + this.offsetY = 0; + } + /** + * Load settings from ComfyUI settings system. + */ + loadSettings() { + this.zoomFactor = getSettingValue("🔍MagnifyGlass.ZoomFactor", this.zoomFactor * 100) / 100; + this.glassSize = getSettingValue("🔍MagnifyGlass.GlassSize", this.glassSize); + this.borderColor = getSettingValue("🔍MagnifyGlass.BorderColor", this.borderColor); + this.borderWidth = getSettingValue("🔍MagnifyGlass.BorderWidth", this.borderWidth); + this.activationKey = getSettingValue("🔍MagnifyGlass.ActivationKey", this.activationKey); + this.altRequired = getSettingValue("🔍MagnifyGlass.AltRequired", this.altRequired); + this.followCursor = getSettingValue("🔍MagnifyGlass.FollowCursor", this.followCursor); + this.debugMode = getSettingValue("🔍MagnifyGlass.DebugMode", this.debugMode); + this.offsetStep = getSettingValue("🔍MagnifyGlass.OffsetStep", this.offsetStep); + this.glassPosition = getSettingValue("🔍MagnifyGlass.GlassPosition", this.glassPosition); + this.resetKey = getSettingValue("🔍MagnifyGlass.ResetKey", this.resetKey); + this.glassShape = getSettingValue("🔍MagnifyGlass.GlassShape", this.glassShape); + this.borderEnabled = getSettingValue("🔍MagnifyGlass.BorderEnabled", this.borderEnabled); + this.textureFiltering = getSettingValue("🔍MagnifyGlass.TextureFiltering", this.textureFiltering); + this.alwaysActiveMode = getSettingValue("🔍MagnifyGlass.AlwaysActiveMode", this.alwaysActiveMode); + this.toggleFollowCursorKey = getSettingValue("🔍MagnifyGlass.ToggleFollowCursorKey", this.toggleFollowCursorKey); + } + /** + * Load saved offsets from localStorage. + */ + loadSavedOffsets() { + try { + const savedOffsetX = localStorage.getItem(STORAGE_KEYS.OFFSET_X); + const savedOffsetY = localStorage.getItem(STORAGE_KEYS.OFFSET_Y); + if (savedOffsetX !== null) { + this.offsetX = parseInt(savedOffsetX, 10); + } else { + this.offsetX = 0; + } + if (savedOffsetY !== null) { + this.offsetY = parseInt(savedOffsetY, 10); + } else { + this.offsetY = 0; + } + } catch (e) { + console.error("ComfyUI Magnifying Glass ERROR: Error loading saved offsets:", e); + this.offsetX = 0; + this.offsetY = 0; + } + } + /** + * Save current offsets to localStorage. + */ + saveOffsets() { + try { + localStorage.setItem(STORAGE_KEYS.OFFSET_X, this.offsetX.toString()); + localStorage.setItem(STORAGE_KEYS.OFFSET_Y, this.offsetY.toString()); + } catch (e) { + console.error("ComfyUI Magnifying Glass ERROR: Error saving offsets:", e); + } + } + /** + * Reset offsets to default (zero). + */ + resetOffsets() { + this.offsetX = 0; + this.offsetY = 0; + this.saveOffsets(); + } +} +export { + ConfigManager +}; +//# sourceMappingURL=ConfigManager.js.map diff --git a/web/magnify-glass/ConfigManager.js.map b/web/magnify-glass/ConfigManager.js.map new file mode 100644 index 0000000..862d1b8 --- /dev/null +++ b/web/magnify-glass/ConfigManager.js.map @@ -0,0 +1 @@ +{"version":3,"file":"ConfigManager.js","sources":["../../src/magnify-glass/ConfigManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - ConfigManager (TypeScript)\n * \n * Manages configuration and settings for the magnifying glass.\n */\n\nimport { getSettingValue } from '../shared/utils';\nimport { STORAGE_KEYS } from '../shared/constants';\nimport type { GlassPosition, GlassShape, TextureFilter } from '../shared/constants';\n\n// Default settings (we'll import these properly later)\nconst DEFAULT_GLASS_SETTINGS = {\n \"🔍MagnifyGlass.ZoomFactor\": 300,\n \"🔍MagnifyGlass.GlassSize\": 300,\n \"🔍MagnifyGlass.BorderColor\": \"#6b7280\",\n \"🔍MagnifyGlass.BorderWidth\": 1,\n \"🔍MagnifyGlass.ActivationKey\": \"x\",\n \"🔍MagnifyGlass.AltRequired\": false,\n \"🔍MagnifyGlass.FollowCursor\": false,\n \"🔍MagnifyGlass.DebugMode\": false,\n \"🔍MagnifyGlass.OffsetStep\": 5,\n \"🔍MagnifyGlass.GlassPosition\": \"Top-Right\" as GlassPosition,\n \"🔍MagnifyGlass.ResetKey\": \"o\",\n \"🔍MagnifyGlass.GlassShape\": \"Rounded Square\" as GlassShape,\n \"🔍MagnifyGlass.BorderEnabled\": true,\n \"🔍MagnifyGlass.TextureFiltering\": \"Linear\" as TextureFilter,\n \"🔍MagnifyGlass.AlwaysActiveMode\": true,\n \"🔍MagnifyGlass.ToggleFollowCursorKey\": \"h\",\n};\n\n/**\n * Configuration manager class.\n * Reads settings from ComfyUI and manages persistent offsets.\n */\nexport class ConfigManager {\n /** Zoom factor (1.0 = 100%, 3.0 = 300%) */\n zoomFactor: number;\n\n /** Glass size in pixels */\n glassSize: number;\n\n /** Border color (CSS color string) */\n borderColor: string;\n\n /** Border width in pixels */\n borderWidth: number;\n\n /** Activation key (lowercase) */\n activationKey: string;\n\n /** Whether Alt key is required for activation */\n altRequired: boolean;\n\n /** Whether glass follows cursor */\n followCursor: boolean;\n\n /** Debug mode enabled */\n debugMode: boolean;\n\n /** Offset step for arrow key adjustments */\n offsetStep: number;\n\n /** Glass position relative to cursor */\n glassPosition: string;\n\n /** Reset key (lowercase) */\n resetKey: string;\n\n /** Glass shape */\n glassShape: string;\n\n /** Whether border is enabled */\n borderEnabled: boolean;\n\n /** Texture filtering mode */\n textureFiltering: string;\n\n /** Always active mode */\n alwaysActiveMode: boolean;\n\n /** Toggle follow cursor key */\n toggleFollowCursorKey: string;\n\n /** Manual offset X in graph units */\n offsetX: number;\n\n /** Manual offset Y in graph units */\n offsetY: number;\n\n constructor() {\n // Initialize properties with defaults\n this.zoomFactor = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.ZoomFactor\"] / 100;\n this.glassSize = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.GlassSize\"];\n this.borderColor = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.BorderColor\"];\n this.borderWidth = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.BorderWidth\"];\n this.activationKey = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.ActivationKey\"];\n this.altRequired = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.AltRequired\"];\n this.followCursor = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.FollowCursor\"];\n this.debugMode = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.DebugMode\"];\n this.offsetStep = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.OffsetStep\"];\n this.glassPosition = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.GlassPosition\"];\n this.resetKey = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.ResetKey\"];\n this.glassShape = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.GlassShape\"];\n this.borderEnabled = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.BorderEnabled\"];\n this.textureFiltering = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.TextureFiltering\"];\n this.alwaysActiveMode = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.AlwaysActiveMode\"];\n this.toggleFollowCursorKey = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.ToggleFollowCursorKey\"];\n\n // Alignment adjustment parameters - managed separately\n this.offsetX = 0;\n this.offsetY = 0;\n }\n\n /**\n * Load settings from ComfyUI settings system.\n */\n loadSettings(): void {\n this.zoomFactor = getSettingValue(\"🔍MagnifyGlass.ZoomFactor\", this.zoomFactor * 100) / 100;\n this.glassSize = getSettingValue(\"🔍MagnifyGlass.GlassSize\", this.glassSize);\n this.borderColor = getSettingValue(\"🔍MagnifyGlass.BorderColor\", this.borderColor);\n this.borderWidth = getSettingValue(\"🔍MagnifyGlass.BorderWidth\", this.borderWidth);\n this.activationKey = getSettingValue(\"🔍MagnifyGlass.ActivationKey\", this.activationKey);\n this.altRequired = getSettingValue(\"🔍MagnifyGlass.AltRequired\", this.altRequired);\n this.followCursor = getSettingValue(\"🔍MagnifyGlass.FollowCursor\", this.followCursor);\n this.debugMode = getSettingValue(\"🔍MagnifyGlass.DebugMode\", this.debugMode);\n this.offsetStep = getSettingValue(\"🔍MagnifyGlass.OffsetStep\", this.offsetStep);\n this.glassPosition = getSettingValue(\"🔍MagnifyGlass.GlassPosition\", this.glassPosition);\n this.resetKey = getSettingValue(\"🔍MagnifyGlass.ResetKey\", this.resetKey);\n this.glassShape = getSettingValue(\"🔍MagnifyGlass.GlassShape\", this.glassShape);\n this.borderEnabled = getSettingValue(\"🔍MagnifyGlass.BorderEnabled\", this.borderEnabled);\n this.textureFiltering = getSettingValue(\"🔍MagnifyGlass.TextureFiltering\", this.textureFiltering);\n this.alwaysActiveMode = getSettingValue(\"🔍MagnifyGlass.AlwaysActiveMode\", this.alwaysActiveMode);\n this.toggleFollowCursorKey = getSettingValue(\"🔍MagnifyGlass.ToggleFollowCursorKey\", this.toggleFollowCursorKey);\n }\n\n /**\n * Load saved offsets from localStorage.\n */\n loadSavedOffsets(): void {\n try {\n const savedOffsetX = localStorage.getItem(STORAGE_KEYS.OFFSET_X);\n const savedOffsetY = localStorage.getItem(STORAGE_KEYS.OFFSET_Y);\n\n if (savedOffsetX !== null) {\n this.offsetX = parseInt(savedOffsetX, 10);\n } else {\n this.offsetX = 0;\n }\n\n if (savedOffsetY !== null) {\n this.offsetY = parseInt(savedOffsetY, 10);\n } else {\n this.offsetY = 0;\n }\n } catch (e) {\n console.error(\"ComfyUI Magnifying Glass ERROR: Error loading saved offsets:\", e);\n this.offsetX = 0;\n this.offsetY = 0;\n }\n }\n\n /**\n * Save current offsets to localStorage.\n */\n saveOffsets(): void {\n try {\n localStorage.setItem(STORAGE_KEYS.OFFSET_X, this.offsetX.toString());\n localStorage.setItem(STORAGE_KEYS.OFFSET_Y, this.offsetY.toString());\n } catch (e) {\n console.error(\"ComfyUI Magnifying Glass ERROR: Error saving offsets:\", e);\n }\n }\n\n /**\n * Reset offsets to default (zero).\n */\n resetOffsets(): void {\n this.offsetX = 0;\n this.offsetY = 0;\n this.saveOffsets();\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/magnify-glass/DebugManager.js b/web/magnify-glass/DebugManager.js new file mode 100644 index 0000000..602d387 --- /dev/null +++ b/web/magnify-glass/DebugManager.js @@ -0,0 +1,128 @@ +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +class DebugManager { + constructor(config, state, ui) { + __publicField(this, "config"); + __publicField(this, "state"); + __publicField(this, "ui"); + this.config = config; + this.state = state; + this.ui = ui; + } + /** + * Log a message if debug mode is enabled. + */ + log(...args) { + if (this.config.debugMode) { + console.log("ComfyUI Magnifying Glass:", ...args); + } + } + /** + * Log an error message. + */ + error(...args) { + console.error("ComfyUI Magnifying Glass ERROR:", ...args); + } + /** + * Print detailed canvas information for debugging. + */ + printCanvasInfo() { + if (!this.config.debugMode) return; + try { + const canvasManager = app.canvas; + const canvas = canvasManager == null ? void 0 : canvasManager.graph_canvas; + if (!canvas) { + this.log("Could not find graph canvas for detailed info"); + return; + } + this.log("---- Canvas Information ----"); + this.log(`Canvas dimensions: ${canvas.width}x${canvas.height}`); + this.log(`Canvas display size: ${canvas.clientWidth}x${canvas.clientHeight}`); + this.log(`Canvas CSS transform: ${canvas.style.transform || "none"}`); + const ds = canvasManager.ds; + if (ds) { + this.log(`Canvas DS scale: ${ds.scale}`); + if (ds.offset) { + this.log(`Canvas DS offset: [${ds.offset[0]}, ${ds.offset[1]}]`); + } else { + this.log("Canvas DS offset not found"); + } + } else { + this.log("Canvas DS object not found"); + } + const screenWidth = window.innerWidth; + const screenHeight = window.innerHeight; + const canvasWidth = canvas.width; + const canvasHeight = canvas.height; + this.log(`Window dimensions: ${screenWidth}x${screenHeight}`); + this.log(`Resolution scale factor: X=${canvasWidth / screenWidth}, Y=${canvasHeight / screenHeight}`); + this.log("---- End Canvas Information ----"); + } catch (e) { + this.log("Error in printCanvasInfo:", e); + } + } + /** + * Update the debug visualization canvas. + */ + updateDebugView() { + if (!this.config.debugMode || !this.ui || !this.ui.debugCanvas || !this.ui.debugCtx) return; + const debugCtx = this.ui.debugCtx; + const debugCanvas = this.ui.debugCanvas; + debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height); + debugCtx.fillStyle = "rgba(0,0,0,0.8)"; + debugCtx.fillRect(0, 0, debugCanvas.width, debugCanvas.height); + debugCtx.fillStyle = "#FFFFFF"; + debugCtx.font = "14px monospace"; + debugCtx.fillText("Magnify Glass Debug", 10, 20); + debugCtx.font = "12px monospace"; + debugCtx.fillText(`Cursor: (${this.state.x.toFixed(1)}, ${this.state.y.toFixed(1)})`, 10, 50); + debugCtx.fillText(`Source Rect: (${this.state.sourceX.toFixed(1)}, ${this.state.sourceY.toFixed(1)}, w:${this.state.sourceWidth.toFixed(1)}, h:${this.state.sourceHeight.toFixed(1)})`, 10, 70); + debugCtx.fillText(`Canvas Scale: ${this.state.canvasScale.toFixed(2)}`, 10, 90); + debugCtx.fillText(`Canvas Offset: (${this.state.canvasOffsetX.toFixed(1)}, ${this.state.canvasOffsetY.toFixed(1)})`, 10, 110); + debugCtx.fillStyle = "#FFFF00"; + debugCtx.fillText(`MANUAL OFFSETS: X=${this.config.offsetX}, Y=${this.config.offsetY} (Use arrow keys to adjust)`, 10, 130); + debugCtx.fillStyle = "#FFFFFF"; + this.drawCanvasVisualization(debugCtx, debugCanvas); + } + /** + * Draw a scaled visualization of the canvas and source region. + */ + drawCanvasVisualization(debugCtx, debugCanvas) { + const canvasScale = 0.1; + const canvasVisX = 10; + const canvasVisY = 170; + const canvasVisWidth = 380; + const canvasVisHeight = 150; + debugCtx.strokeStyle = "#AAAAAA"; + debugCtx.strokeRect(canvasVisX, canvasVisY, canvasVisWidth, canvasVisHeight); + debugCtx.fillStyle = "#444444"; + debugCtx.fillRect(canvasVisX, canvasVisY, canvasVisWidth, canvasVisHeight); + const cursorVisX = canvasVisX + this.state.x * canvasScale; + const cursorVisY = canvasVisY + this.state.y * canvasScale; + const sourceRectVisX = canvasVisX + this.state.sourceX * canvasScale; + const sourceRectVisY = canvasVisY + this.state.sourceY * canvasScale; + const sourceRectVisWidth = this.state.sourceWidth * canvasScale; + const sourceRectVisHeight = this.state.sourceHeight * canvasScale; + debugCtx.strokeStyle = "#FF0000"; + debugCtx.strokeRect(sourceRectVisX, sourceRectVisY, sourceRectVisWidth, sourceRectVisHeight); + debugCtx.fillStyle = "#FFFF00"; + debugCtx.beginPath(); + debugCtx.arc(cursorVisX, cursorVisY, 3, 0, Math.PI * 2); + debugCtx.fill(); + debugCtx.strokeStyle = "#00FF00"; + debugCtx.beginPath(); + debugCtx.moveTo(cursorVisX, cursorVisY); + debugCtx.lineTo(sourceRectVisX + sourceRectVisWidth / 2, sourceRectVisY + sourceRectVisHeight / 2); + debugCtx.stroke(); + debugCtx.fillStyle = "#FFFFFF"; + debugCtx.fillText("Canvas Visualization (scaled)", canvasVisX, canvasVisY - 5); + const offsetX = this.state.sourceX - this.state.x + this.state.sourceWidth / 2; + const offsetY = this.state.sourceY - this.state.y + this.state.sourceHeight / 2; + debugCtx.fillText(`Alignment Offset: (${offsetX.toFixed(1)}, ${offsetY.toFixed(1)})`, 10, canvasVisY + canvasVisHeight + 20); + } +} +export { + DebugManager +}; +//# sourceMappingURL=DebugManager.js.map diff --git a/web/magnify-glass/DebugManager.js.map b/web/magnify-glass/DebugManager.js.map new file mode 100644 index 0000000..d09fafb --- /dev/null +++ b/web/magnify-glass/DebugManager.js.map @@ -0,0 +1 @@ +{"version":3,"file":"DebugManager.js","sources":["../../src/magnify-glass/DebugManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - DebugManager (TypeScript)\n * \n * Handles debug visualization and logging.\n */\n\nimport type { ComfyApp } from '../types/comfyui';\nimport type { ConfigManager } from './ConfigManager';\nimport type { MagnifierState } from './MagnifierState';\nimport type { UiManager } from './UiManager';\n\ndeclare const app: ComfyApp;\n\n/**\n * Debug Manager class.\n * Provides debug logging and canvas visualization.\n */\nexport class DebugManager {\n config: ConfigManager;\n state: MagnifierState;\n ui: UiManager;\n\n constructor(config: ConfigManager, state: MagnifierState, ui: UiManager) {\n this.config = config;\n this.state = state;\n this.ui = ui;\n }\n\n /**\n * Log a message if debug mode is enabled.\n */\n log(...args: unknown[]): void {\n if (this.config.debugMode) {\n console.log(\"ComfyUI Magnifying Glass:\", ...args);\n }\n }\n\n /**\n * Log an error message.\n */\n error(...args: unknown[]): void {\n console.error(\"ComfyUI Magnifying Glass ERROR:\", ...args);\n }\n\n /**\n * Print detailed canvas information for debugging.\n */\n printCanvasInfo(): void {\n if (!this.config.debugMode) return;\n\n try {\n // Using any for deep property access on ComfyUI objects that might vary\n const canvasManager = (app as any).canvas;\n const canvas = canvasManager?.graph_canvas as HTMLCanvasElement;\n\n if (!canvas) {\n this.log(\"Could not find graph canvas for detailed info\");\n return;\n }\n\n this.log(\"---- Canvas Information ----\");\n this.log(`Canvas dimensions: ${canvas.width}x${canvas.height}`);\n this.log(`Canvas display size: ${canvas.clientWidth}x${canvas.clientHeight}`);\n this.log(`Canvas CSS transform: ${canvas.style.transform || 'none'}`);\n\n const ds = canvasManager.ds;\n if (ds) {\n this.log(`Canvas DS scale: ${ds.scale}`);\n if (ds.offset) {\n this.log(`Canvas DS offset: [${ds.offset[0]}, ${ds.offset[1]}]`);\n } else {\n this.log(\"Canvas DS offset not found\");\n }\n } else {\n this.log(\"Canvas DS object not found\");\n }\n\n const screenWidth = window.innerWidth;\n const screenHeight = window.innerHeight;\n const canvasWidth = canvas.width;\n const canvasHeight = canvas.height;\n\n this.log(`Window dimensions: ${screenWidth}x${screenHeight}`);\n this.log(`Resolution scale factor: X=${canvasWidth / screenWidth}, Y=${canvasHeight / screenHeight}`);\n this.log(\"---- End Canvas Information ----\");\n } catch (e) {\n this.log(\"Error in printCanvasInfo:\", e);\n }\n }\n\n /**\n * Update the debug visualization canvas.\n */\n updateDebugView(): void {\n if (!this.config.debugMode || !this.ui || !this.ui.debugCanvas || !this.ui.debugCtx) return;\n\n const debugCtx = this.ui.debugCtx;\n const debugCanvas = this.ui.debugCanvas;\n\n debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height);\n\n // Draw background\n debugCtx.fillStyle = 'rgba(0,0,0,0.8)';\n debugCtx.fillRect(0, 0, debugCanvas.width, debugCanvas.height);\n\n // Header\n debugCtx.fillStyle = '#FFFFFF';\n debugCtx.font = '14px monospace';\n debugCtx.fillText('Magnify Glass Debug', 10, 20);\n\n // Position info\n debugCtx.font = '12px monospace';\n debugCtx.fillText(`Cursor: (${this.state.x.toFixed(1)}, ${this.state.y.toFixed(1)})`, 10, 50);\n debugCtx.fillText(`Source Rect: (${this.state.sourceX.toFixed(1)}, ${this.state.sourceY.toFixed(1)}, w:${this.state.sourceWidth.toFixed(1)}, h:${this.state.sourceHeight.toFixed(1)})`, 10, 70);\n\n // Canvas transform\n debugCtx.fillText(`Canvas Scale: ${this.state.canvasScale.toFixed(2)}`, 10, 90);\n debugCtx.fillText(`Canvas Offset: (${this.state.canvasOffsetX.toFixed(1)}, ${this.state.canvasOffsetY.toFixed(1)})`, 10, 110);\n\n // Offset information\n debugCtx.fillStyle = '#FFFF00';\n debugCtx.fillText(`MANUAL OFFSETS: X=${this.config.offsetX}, Y=${this.config.offsetY} (Use arrow keys to adjust)`, 10, 130);\n debugCtx.fillStyle = '#FFFFFF';\n\n // Visualization\n this.drawCanvasVisualization(debugCtx, debugCanvas);\n }\n\n /**\n * Draw a scaled visualization of the canvas and source region.\n */\n drawCanvasVisualization(debugCtx: CanvasRenderingContext2D, debugCanvas: HTMLCanvasElement): void {\n const canvasScale = 0.1;\n const canvasVisX = 10;\n const canvasVisY = 170;\n const canvasVisWidth = 380;\n const canvasVisHeight = 150;\n\n // Draw canvas representation\n debugCtx.strokeStyle = '#AAAAAA';\n debugCtx.strokeRect(canvasVisX, canvasVisY, canvasVisWidth, canvasVisHeight);\n debugCtx.fillStyle = '#444444';\n debugCtx.fillRect(canvasVisX, canvasVisY, canvasVisWidth, canvasVisHeight);\n\n // Calculate cursor position in visualization\n const cursorVisX = canvasVisX + (this.state.x * canvasScale);\n const cursorVisY = canvasVisY + (this.state.y * canvasScale);\n\n // Calculate source rect in visualization\n const sourceRectVisX = canvasVisX + (this.state.sourceX * canvasScale);\n const sourceRectVisY = canvasVisY + (this.state.sourceY * canvasScale);\n const sourceRectVisWidth = this.state.sourceWidth * canvasScale;\n const sourceRectVisHeight = this.state.sourceHeight * canvasScale;\n\n // Draw source rect\n debugCtx.strokeStyle = '#FF0000';\n debugCtx.strokeRect(sourceRectVisX, sourceRectVisY, sourceRectVisWidth, sourceRectVisHeight);\n\n // Draw cursor position\n debugCtx.fillStyle = '#FFFF00';\n debugCtx.beginPath();\n debugCtx.arc(cursorVisX, cursorVisY, 3, 0, Math.PI * 2);\n debugCtx.fill();\n\n // Draw line from cursor to source rect center\n debugCtx.strokeStyle = '#00FF00';\n debugCtx.beginPath();\n debugCtx.moveTo(cursorVisX, cursorVisY);\n debugCtx.lineTo(sourceRectVisX + sourceRectVisWidth / 2, sourceRectVisY + sourceRectVisHeight / 2);\n debugCtx.stroke();\n\n // Label for visualization\n debugCtx.fillStyle = '#FFFFFF';\n debugCtx.fillText('Canvas Visualization (scaled)', canvasVisX, canvasVisY - 5);\n\n // Offset values\n const offsetX = this.state.sourceX - this.state.x + (this.state.sourceWidth / 2);\n const offsetY = this.state.sourceY - this.state.y + (this.state.sourceHeight / 2);\n debugCtx.fillText(`Alignment Offset: (${offsetX.toFixed(1)}, ${offsetY.toFixed(1)})`, 10, canvasVisY + canvasVisHeight + 20);\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/magnify-glass/EventHandler.js b/web/magnify-glass/EventHandler.js new file mode 100644 index 0000000..b8698d4 --- /dev/null +++ b/web/magnify-glass/EventHandler.js @@ -0,0 +1,173 @@ +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +import { isUserTyping } from "../shared/utils.js"; +import { Logger } from "../shared/logger.js"; +import { DEFAULT_PADDING, DEFAULT_GLASS_Y_OFFSET } from "../shared/constants.js"; +class EventHandler { + constructor(magnifyGlass) { + __publicField(this, "magnifyGlass"); + this.magnifyGlass = magnifyGlass; + this.handleKeyDown = this.handleKeyDown.bind(this); + this.handleKeyUp = this.handleKeyUp.bind(this); + this.handleMouseMove = this.handleMouseMove.bind(this); + } + /** + * Attach all event listeners. + */ + attachListeners() { + document.addEventListener("keydown", this.handleKeyDown); + document.addEventListener("keyup", this.handleKeyUp); + document.addEventListener("mousemove", this.handleMouseMove); + } + /** + * Detach all event listeners. + */ + detachListeners() { + document.removeEventListener("keydown", this.handleKeyDown); + document.removeEventListener("keyup", this.handleKeyUp); + document.removeEventListener("mousemove", this.handleMouseMove); + } + /** + * Handle keydown events. + */ + handleKeyDown(e) { + const config = this.magnifyGlass.config; + const state = this.magnifyGlass.state; + if (isUserTyping()) { + return; + } + Logger.debug(`KeyDown: ${e.key} (config: ${config.activationKey}, active: ${state.active})`); + if (e.key.toLowerCase() === config.activationKey && (!config.altRequired || e.altKey)) { + Logger.debug("Activation key matched!"); + if (config.alwaysActiveMode) { + this.magnifyGlass.toggle(); + } else { + if (!state.active) { + this.magnifyGlass.toggle(); + } + } + } + if (state.active) { + let offsetChanged = false; + const stepSize = e.shiftKey ? config.offsetStep * 5 : config.offsetStep; + if (e.key === "ArrowUp") { + config.offsetY -= stepSize; + offsetChanged = true; + e.preventDefault(); + } else if (e.key === "ArrowDown") { + config.offsetY += stepSize; + offsetChanged = true; + e.preventDefault(); + } else if (e.key === "ArrowLeft") { + config.offsetX -= stepSize; + offsetChanged = true; + e.preventDefault(); + } else if (e.key === "ArrowRight") { + config.offsetX += stepSize; + offsetChanged = true; + e.preventDefault(); + } else if (e.key.toLowerCase() === config.resetKey.toLowerCase() && (!config.altRequired || e.altKey)) { + this.magnifyGlass.resetOffsets(); + offsetChanged = true; + e.preventDefault(); + } + if (e.key.toLowerCase() === config.toggleFollowCursorKey && (!config.altRequired || e.altKey)) { + config.followCursor = !config.followCursor; + this.magnifyGlass.debugger.log(`Follow Cursor Toggled: ${config.followCursor ? "ON" : "OFF"}`); + if (state.active && config.followCursor) { + this.magnifyGlass.ui.positionGlass( + this.magnifyGlass.lastKnownMousePosition.x, + this.magnifyGlass.lastKnownMousePosition.y + ); + } + e.preventDefault(); + } + if (offsetChanged) { + config.saveOffsets(); + this.magnifyGlass.updateMagnifiedView(); + } + } + } + /** + * Handle keyup events. + */ + handleKeyUp(e) { + const config = this.magnifyGlass.config; + const state = this.magnifyGlass.state; + if (!config.alwaysActiveMode && (e.key.toLowerCase() === config.activationKey || config.altRequired && e.key === "Alt")) { + let shouldDeactivate = false; + if (config.altRequired) { + if (e.key === "Alt" || e.key.toLowerCase() === config.activationKey) { + shouldDeactivate = true; + } + } else { + if (e.key.toLowerCase() === config.activationKey) { + shouldDeactivate = true; + } + } + if (state.active && shouldDeactivate) { + state.active = false; + this.magnifyGlass.ui.hide(); + } + } + } + /** + * Handle mousemove events. + */ + handleMouseMove(e) { + this.magnifyGlass.lastKnownMousePosition.x = e.clientX; + this.magnifyGlass.lastKnownMousePosition.y = e.clientY; + if (!this.magnifyGlass.state.active || !this.magnifyGlass.litegraphCanvas) return; + const rect = this.magnifyGlass.litegraphCanvas.getBoundingClientRect(); + const cssMouseXOnCanvas = e.clientX - rect.left; + const cssMouseYOnCanvas = e.clientY - rect.top; + if (cssMouseXOnCanvas >= 0 && cssMouseXOnCanvas <= rect.width && cssMouseYOnCanvas >= 0 && cssMouseYOnCanvas <= rect.height) { + const canvasElement = this.magnifyGlass.litegraphCanvas; + const scaleX = rect.width > 0 ? canvasElement.width / rect.width : 1; + const scaleY = rect.height > 0 ? canvasElement.height / rect.height : 1; + const pixelX = cssMouseXOnCanvas * scaleX; + const pixelY = cssMouseYOnCanvas * scaleY; + this.magnifyGlass.state.x = pixelX; + this.magnifyGlass.state.y = pixelY; + this.magnifyGlass.ui.positionGlass(e.clientX, e.clientY); + this.magnifyGlass.updateMagnifiedView(); + } + } + /** + * Update the initial position when magnifier is activated. + */ + updateInitialPosition() { + if (!this.magnifyGlass.litegraphCanvas) return; + const rect = this.magnifyGlass.litegraphCanvas.getBoundingClientRect(); + const clientX = this.magnifyGlass.lastKnownMousePosition.x; + const clientY = this.magnifyGlass.lastKnownMousePosition.y; + const isOverCanvas = clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom; + if (this.magnifyGlass.config.alwaysActiveMode || isOverCanvas) { + const cssMouseXOnCanvas = clientX - rect.left; + const cssMouseYOnCanvas = clientY - rect.top; + const canvasElement = this.magnifyGlass.litegraphCanvas; + const scaleX = rect.width > 0 ? canvasElement.width / rect.width : 1; + const scaleY = rect.height > 0 ? canvasElement.height / rect.height : 1; + const pixelX = cssMouseXOnCanvas * scaleX; + const pixelY = cssMouseYOnCanvas * scaleY; + this.magnifyGlass.state.x = pixelX; + this.magnifyGlass.state.y = pixelY; + const 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.top = `${DEFAULT_GLASS_Y_OFFSET}px`; + this.magnifyGlass.state.wasActivatedBefore = true; + } else { + this.magnifyGlass.ui.positionGlass(clientX, clientY); + } + this.magnifyGlass.updateMagnifiedView(); + } + } +} +export { + EventHandler +}; +//# sourceMappingURL=EventHandler.js.map diff --git a/web/magnify-glass/EventHandler.js.map b/web/magnify-glass/EventHandler.js.map new file mode 100644 index 0000000..6232005 --- /dev/null +++ b/web/magnify-glass/EventHandler.js.map @@ -0,0 +1 @@ +{"version":3,"file":"EventHandler.js","sources":["../../src/magnify-glass/EventHandler.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - EventHandler (TypeScript)\n * \n * Handles keyboard and mouse events for the magnifying glass.\n */\n\nimport { isUserTyping } from '../shared/utils';\nimport { Logger } from '../shared/logger';\nimport { DEFAULT_PADDING, DEFAULT_GLASS_Y_OFFSET } from '../shared/constants';\nimport type { MagnifyGlass } from './MagnifyGlass';\n\n/**\n * Event Handler class.\n * Manages all keyboard and mouse event handling for the magnifier.\n */\nexport class EventHandler {\n magnifyGlass: MagnifyGlass;\n\n constructor(magnifyGlass: MagnifyGlass) {\n this.magnifyGlass = magnifyGlass;\n\n // Bind methods to maintain context\n this.handleKeyDown = this.handleKeyDown.bind(this);\n this.handleKeyUp = this.handleKeyUp.bind(this);\n this.handleMouseMove = this.handleMouseMove.bind(this);\n }\n\n /**\n * Attach all event listeners.\n */\n attachListeners(): void {\n document.addEventListener(\"keydown\", this.handleKeyDown);\n document.addEventListener(\"keyup\", this.handleKeyUp);\n document.addEventListener(\"mousemove\", this.handleMouseMove);\n }\n\n /**\n * Detach all event listeners.\n */\n detachListeners(): void {\n document.removeEventListener(\"keydown\", this.handleKeyDown);\n document.removeEventListener(\"keyup\", this.handleKeyUp);\n document.removeEventListener(\"mousemove\", this.handleMouseMove);\n }\n\n /**\n * Handle keydown events.\n */\n handleKeyDown(e: KeyboardEvent): void {\n const config = this.magnifyGlass.config;\n const state = this.magnifyGlass.state;\n\n // Don't handle hotkeys if user is typing in an input field\n if (isUserTyping()) {\n return;\n }\n\n // Magnifier activation\n Logger.debug(`KeyDown: ${e.key} (config: ${config.activationKey}, active: ${state.active})`);\n if (e.key.toLowerCase() === config.activationKey &&\n (!config.altRequired || e.altKey)) {\n Logger.debug('Activation key matched!');\n if (config.alwaysActiveMode) {\n this.magnifyGlass.toggle();\n } else {\n if (!state.active) {\n this.magnifyGlass.toggle();\n }\n }\n }\n\n // Manual offset adjustment keys (only when magnifier is active)\n if (state.active) {\n let offsetChanged = false;\n const stepSize = e.shiftKey ? config.offsetStep * 5 : config.offsetStep;\n\n if (e.key === \"ArrowUp\") {\n config.offsetY -= stepSize;\n offsetChanged = true;\n e.preventDefault();\n } else if (e.key === \"ArrowDown\") {\n config.offsetY += stepSize;\n offsetChanged = true;\n e.preventDefault();\n } else if (e.key === \"ArrowLeft\") {\n config.offsetX -= stepSize;\n offsetChanged = true;\n e.preventDefault();\n } else if (e.key === \"ArrowRight\") {\n config.offsetX += stepSize;\n offsetChanged = true;\n e.preventDefault();\n } else if (e.key.toLowerCase() === config.resetKey.toLowerCase() &&\n (!config.altRequired || e.altKey)) {\n this.magnifyGlass.resetOffsets();\n offsetChanged = true;\n e.preventDefault();\n }\n\n // Toggle Follow Cursor Key\n if (e.key.toLowerCase() === config.toggleFollowCursorKey &&\n (!config.altRequired || e.altKey)) {\n config.followCursor = !config.followCursor;\n this.magnifyGlass.debugger.log(`Follow Cursor Toggled: ${config.followCursor ? 'ON' : 'OFF'}`);\n if (state.active && config.followCursor) {\n this.magnifyGlass.ui.positionGlass(\n this.magnifyGlass.lastKnownMousePosition.x,\n this.magnifyGlass.lastKnownMousePosition.y\n );\n }\n e.preventDefault();\n }\n\n if (offsetChanged) {\n config.saveOffsets();\n this.magnifyGlass.updateMagnifiedView();\n }\n }\n }\n\n /**\n * Handle keyup events.\n */\n handleKeyUp(e: KeyboardEvent): void {\n const config = this.magnifyGlass.config;\n const state = this.magnifyGlass.state;\n\n if (!config.alwaysActiveMode &&\n (e.key.toLowerCase() === config.activationKey ||\n (config.altRequired && e.key === \"Alt\"))) {\n\n let shouldDeactivate = false;\n if (config.altRequired) {\n if (e.key === \"Alt\" || e.key.toLowerCase() === config.activationKey) {\n shouldDeactivate = true;\n }\n } else {\n if (e.key.toLowerCase() === config.activationKey) {\n shouldDeactivate = true;\n }\n }\n\n if (state.active && shouldDeactivate) {\n state.active = false;\n this.magnifyGlass.ui.hide();\n }\n }\n }\n\n /**\n * Handle mousemove events.\n */\n handleMouseMove(e: MouseEvent): void {\n // Always track mouse position\n this.magnifyGlass.lastKnownMousePosition.x = e.clientX;\n this.magnifyGlass.lastKnownMousePosition.y = e.clientY;\n\n if (!this.magnifyGlass.state.active || !this.magnifyGlass.litegraphCanvas) return;\n\n const rect = this.magnifyGlass.litegraphCanvas.getBoundingClientRect();\n const cssMouseXOnCanvas = e.clientX - rect.left;\n const cssMouseYOnCanvas = e.clientY - rect.top;\n\n // Check if the cursor is over the canvas element\n if (cssMouseXOnCanvas >= 0 && cssMouseXOnCanvas <= rect.width &&\n cssMouseYOnCanvas >= 0 && cssMouseYOnCanvas <= rect.height) {\n\n const canvasElement = this.magnifyGlass.litegraphCanvas;\n const scaleX = rect.width > 0 ? canvasElement.width / rect.width : 1;\n const scaleY = rect.height > 0 ? canvasElement.height / rect.height : 1;\n\n const pixelX = cssMouseXOnCanvas * scaleX;\n const pixelY = cssMouseYOnCanvas * scaleY;\n\n this.magnifyGlass.state.x = pixelX;\n this.magnifyGlass.state.y = pixelY;\n\n this.magnifyGlass.ui.positionGlass(e.clientX, e.clientY);\n this.magnifyGlass.updateMagnifiedView();\n }\n }\n\n /**\n * Update the initial position when magnifier is activated.\n */\n updateInitialPosition(): void {\n if (!this.magnifyGlass.litegraphCanvas) return;\n\n const rect = this.magnifyGlass.litegraphCanvas.getBoundingClientRect();\n const clientX = this.magnifyGlass.lastKnownMousePosition.x;\n const clientY = this.magnifyGlass.lastKnownMousePosition.y;\n\n const isOverCanvas = clientX >= rect.left && clientX <= rect.right &&\n clientY >= rect.top && clientY <= rect.bottom;\n\n if (this.magnifyGlass.config.alwaysActiveMode || isOverCanvas) {\n const cssMouseXOnCanvas = clientX - rect.left;\n const cssMouseYOnCanvas = clientY - rect.top;\n\n const canvasElement = this.magnifyGlass.litegraphCanvas;\n const scaleX = rect.width > 0 ? canvasElement.width / rect.width : 1;\n const scaleY = rect.height > 0 ? canvasElement.height / rect.height : 1;\n\n const pixelX = cssMouseXOnCanvas * scaleX;\n const pixelY = cssMouseYOnCanvas * scaleY;\n\n this.magnifyGlass.state.x = pixelX;\n this.magnifyGlass.state.y = pixelY;\n\n const glassSize = this.magnifyGlass.config.glassSize;\n\n if (!this.magnifyGlass.state.wasActivatedBefore && this.magnifyGlass.ui.glassDiv) {\n // First activation - position at top right of the window\n const leftPos = window.innerWidth - glassSize - DEFAULT_PADDING;\n console.log(`[MagnifyGlass] Initial position - innerWidth: ${window.innerWidth}, glassSize: ${glassSize}, padding: ${DEFAULT_PADDING}, left: ${leftPos}, yOffset: ${DEFAULT_GLASS_Y_OFFSET}`);\n this.magnifyGlass.ui.glassDiv.style.left = `${leftPos}px`;\n this.magnifyGlass.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`;\n this.magnifyGlass.state.wasActivatedBefore = true;\n } else {\n this.magnifyGlass.ui.positionGlass(clientX, clientY);\n }\n\n this.magnifyGlass.updateMagnifiedView();\n }\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/magnify-glass/MagnifierState.js b/web/magnify-glass/MagnifierState.js new file mode 100644 index 0000000..a418b20 --- /dev/null +++ b/web/magnify-glass/MagnifierState.js @@ -0,0 +1,64 @@ +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +class MagnifierState { + constructor() { + /** Whether the magnifier is currently active */ + __publicField(this, "active"); + /** Track if the glass has been activated before */ + __publicField(this, "wasActivatedBefore"); + /** Cursor X relative to litegraphCanvas */ + __publicField(this, "x"); + /** Cursor Y relative to litegraphCanvas */ + __publicField(this, "y"); + /** Calculated source area X */ + __publicField(this, "sourceX"); + /** Calculated source area Y */ + __publicField(this, "sourceY"); + /** Calculated source area width */ + __publicField(this, "sourceWidth"); + /** Calculated source area height */ + __publicField(this, "sourceHeight"); + /** Current canvas scale/zoom */ + __publicField(this, "canvasScale"); + /** Canvas translation X */ + __publicField(this, "canvasOffsetX"); + /** Canvas translation Y */ + __publicField(this, "canvasOffsetY"); + /** Flag to manage requestAnimationFrame */ + __publicField(this, "isRenderScheduled"); + this.active = false; + this.wasActivatedBefore = false; + this.x = 0; + this.y = 0; + this.sourceX = 0; + this.sourceY = 0; + this.sourceWidth = 0; + this.sourceHeight = 0; + this.canvasScale = 1; + this.canvasOffsetX = 0; + this.canvasOffsetY = 0; + this.isRenderScheduled = false; + } + /** + * Reset state to initial values. + */ + reset() { + this.active = false; + this.wasActivatedBefore = false; + this.x = 0; + this.y = 0; + this.sourceX = 0; + this.sourceY = 0; + this.sourceWidth = 0; + this.sourceHeight = 0; + this.canvasScale = 1; + this.canvasOffsetX = 0; + this.canvasOffsetY = 0; + this.isRenderScheduled = false; + } +} +export { + MagnifierState +}; +//# sourceMappingURL=MagnifierState.js.map diff --git a/web/magnify-glass/MagnifierState.js.map b/web/magnify-glass/MagnifierState.js.map new file mode 100644 index 0000000..a0e8d03 --- /dev/null +++ b/web/magnify-glass/MagnifierState.js.map @@ -0,0 +1 @@ +{"version":3,"file":"MagnifierState.js","sources":["../../src/magnify-glass/MagnifierState.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - MagnifierState (TypeScript)\n * \n * Manages the state of the magnifying glass.\n */\n\n/**\n * Magnifier state class.\n * Tracks activation state, cursor position, and canvas transforms.\n */\nexport class MagnifierState {\n /** Whether the magnifier is currently active */\n active: boolean;\n\n /** Track if the glass has been activated before */\n wasActivatedBefore: boolean;\n\n /** Cursor X relative to litegraphCanvas */\n x: number;\n\n /** Cursor Y relative to litegraphCanvas */\n y: number;\n\n /** Calculated source area X */\n sourceX: number;\n\n /** Calculated source area Y */\n sourceY: number;\n\n /** Calculated source area width */\n sourceWidth: number;\n\n /** Calculated source area height */\n sourceHeight: number;\n\n /** Current canvas scale/zoom */\n canvasScale: number;\n\n /** Canvas translation X */\n canvasOffsetX: number;\n\n /** Canvas translation Y */\n canvasOffsetY: number;\n\n /** Flag to manage requestAnimationFrame */\n isRenderScheduled: boolean;\n\n constructor() {\n this.active = false;\n this.wasActivatedBefore = false;\n this.x = 0;\n this.y = 0;\n this.sourceX = 0;\n this.sourceY = 0;\n this.sourceWidth = 0;\n this.sourceHeight = 0;\n this.canvasScale = 1.0;\n this.canvasOffsetX = 0;\n this.canvasOffsetY = 0;\n this.isRenderScheduled = false;\n }\n\n /**\n * Reset state to initial values.\n */\n reset(): void {\n this.active = false;\n this.wasActivatedBefore = false;\n this.x = 0;\n this.y = 0;\n this.sourceX = 0;\n this.sourceY = 0;\n this.sourceWidth = 0;\n this.sourceHeight = 0;\n this.canvasScale = 1.0;\n this.canvasOffsetX = 0;\n this.canvasOffsetY = 0;\n this.isRenderScheduled = false;\n }\n}\n"],"names":[],"mappings":";;;AAUO,MAAM,eAAe;AAAA,EAqCxB,cAAc;AAnCd;AAAA;AAGA;AAAA;AAGA;AAAA;AAGA;AAAA;AAGA;AAAA;AAGA;AAAA;AAGA;AAAA;AAGA;AAAA;AAGA;AAAA;AAGA;AAAA;AAGA;AAAA;AAGA;AAAA;AAGI,SAAK,SAAS;AACd,SAAK,qBAAqB;AAC1B,SAAK,IAAI;AACT,SAAK,IAAI;AACT,SAAK,UAAU;AACf,SAAK,UAAU;AACf,SAAK,cAAc;AACnB,SAAK,eAAe;AACpB,SAAK,cAAc;AACnB,SAAK,gBAAgB;AACrB,SAAK,gBAAgB;AACrB,SAAK,oBAAoB;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA,EAKA,QAAc;AACV,SAAK,SAAS;AACd,SAAK,qBAAqB;AAC1B,SAAK,IAAI;AACT,SAAK,IAAI;AACT,SAAK,UAAU;AACf,SAAK,UAAU;AACf,SAAK,cAAc;AACnB,SAAK,eAAe;AACpB,SAAK,cAAc;AACnB,SAAK,gBAAgB;AACrB,SAAK,gBAAgB;AACrB,SAAK,oBAAoB;AAAA,EAC7B;AACJ;"} \ No newline at end of file diff --git a/web/magnify-glass/MagnifyGlass.js b/web/magnify-glass/MagnifyGlass.js new file mode 100644 index 0000000..8319a3a --- /dev/null +++ b/web/magnify-glass/MagnifyGlass.js @@ -0,0 +1,330 @@ +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +import { findLiteGraphCanvas, rectsOverlap } from "../shared/utils.js"; +import { DEFAULT_PADDING, DEFAULT_GLASS_Y_OFFSET } from "../shared/constants.js"; +import { registerGlassSettings } from "../shared/settings/glassSettings.js"; +import "/scripts/app.js"; +import { ConfigManager } from "./ConfigManager.js"; +import { MagnifierState } from "./MagnifierState.js"; +import { UiManager } from "./UiManager.js"; +import { WebGLRenderer } from "./WebGLRenderer.js"; +import { DebugManager } from "./DebugManager.js"; +import { EventHandler } from "./EventHandler.js"; +class MagnifyGlass { + constructor() { + __publicField(this, "config"); + __publicField(this, "state"); + __publicField(this, "ui"); + __publicField(this, "renderer"); + __publicField(this, "debugger"); + __publicField(this, "eventHandler"); + /** The LiteGraph canvas */ + __publicField(this, "litegraphCanvas"); + /** Last known mouse position for better initial positioning */ + __publicField(this, "lastKnownMousePosition"); + /** Whether we're currently over media (for info panel) */ + __publicField(this, "isOverMedia"); + /** Current media element under cursor */ + __publicField(this, "currentMediaElement"); + this.config = new ConfigManager(); + this.state = new MagnifierState(); + this.ui = new UiManager(this.config, this.state, () => this.toggle()); + this.renderer = null; + this.debugger = new DebugManager(this.config, this.state, this.ui); + this.eventHandler = new EventHandler(this); + this.litegraphCanvas = null; + this.lastKnownMousePosition = { x: 0, y: 0 }; + this.isOverMedia = false; + this.currentMediaElement = null; + } + /** + * Initialize the magnifying glass. + */ + init() { + if (typeof LiteGraph === "undefined" || typeof app === "undefined" || !app.canvas) { + this.debugger.log("LiteGraph or app not ready, retrying in 100ms."); + setTimeout(() => this.init(), 100); + return; + } + this.debugger.log("LiteGraph and app ready."); + this.config.loadSavedOffsets(); + this.debugger.printCanvasInfo(); + this.ui.createElements(); + this.renderer = new WebGLRenderer(this.config, this.state, this.ui); + if (!this.renderer.isValid()) { + this.ui.cleanup(); + return; + } + this.litegraphCanvas = findLiteGraphCanvas(); + this.debugger.log("LiteGraph canvas found:", this.litegraphCanvas); + if (!this.litegraphCanvas) { + this.debugger.error("Could not find LiteGraph canvas. Magnifier will not work."); + this.ui.cleanup(); + return; + } + this.eventHandler.attachListeners(); + registerGlassSettings(this); + this.debugger.log(`Initialized (WebGL) with Smart Input Detection. Press ${this.config.altRequired ? "Alt+" : ""}${this.config.activationKey.toUpperCase()} to activate.`); + } + /** + * Toggle the magnifying glass active state. + */ + toggle() { + const state = this.state; + this.config; + if (state.active) { + state.active = false; + this.ui.hide(); + } else { + state.active = true; + this.ui.show(); + if (this.eventHandler) { + this.eventHandler.updateInitialPosition(); + } + } + } + /** + * Update the magnified view. + */ + updateMagnifiedView() { + if (!this.state.active || !this.renderer || !this.litegraphCanvas) { + return; + } + this.updateCanvasTransformation(); + this.calculateSourceRegion(); + if (!this.state.isRenderScheduled) { + this.state.isRenderScheduled = true; + requestAnimationFrame(() => { + if (!this.state.active || !this.renderer || !this.litegraphCanvas) { + this.state.isRenderScheduled = false; + return; + } + this.renderer.render(this.litegraphCanvas); + this.debugger.updateDebugView(); + this.renderHtmlOverlays(); + this.state.isRenderScheduled = false; + }); + } + } + /** + * Update canvas transformation state. + */ + updateCanvasTransformation() { + this.state.canvasScale = 1; + this.state.canvasOffsetX = 0; + this.state.canvasOffsetY = 0; + if (app == null ? void 0 : app.canvas) { + const ds = app.canvas.ds; + if (ds) { + if (typeof ds.scale === "number") { + this.state.canvasScale = ds.scale; + } + if (ds.offset) { + this.state.canvasOffsetX = ds.offset[0] || 0; + this.state.canvasOffsetY = ds.offset[1] || 0; + } + } + } + } + /** + * Calculate the source region for magnification. + */ + calculateSourceRegion() { + const cursorPixelX = this.state.x; + const cursorPixelY = this.state.y; + const canvasScale = this.state.canvasScale; + const canvasOffsetX = this.state.canvasOffsetX; + const canvasOffsetY = this.state.canvasOffsetY; + if (canvasScale === 0) return; + const cursorGraphX = (cursorPixelX - canvasOffsetX) / canvasScale; + const cursorGraphY = (cursorPixelY - canvasOffsetY) / canvasScale; + const targetGraphCenterX = cursorGraphX + this.config.offsetX; + const targetGraphCenterY = cursorGraphY + this.config.offsetY; + const sourceGraphWidth = this.config.glassSize / this.config.zoomFactor / canvasScale; + const sourceGraphHeight = this.config.glassSize / this.config.zoomFactor / canvasScale; + const sourceGraphX = targetGraphCenterX - sourceGraphWidth / 2; + const sourceGraphY = targetGraphCenterY - sourceGraphHeight / 2; + this.state.sourceX = sourceGraphX * canvasScale + canvasOffsetX; + this.state.sourceY = sourceGraphY * canvasScale + canvasOffsetY; + this.state.sourceWidth = sourceGraphWidth * canvasScale; + this.state.sourceHeight = sourceGraphHeight * canvasScale; + } + /** + * Render HTML overlays for text and media in the magnified view. + */ + renderHtmlOverlays() { + const graph = app.graph; + if (!this.state.active || !this.ui.htmlOverlayContainer || !graph || !this.litegraphCanvas) { + if (this.ui.htmlOverlayContainer) this.ui.htmlOverlayContainer.innerHTML = ""; + return; + } + this.ui.htmlOverlayContainer.innerHTML = ""; + const magnifyRect = { + x: this.state.sourceX, + y: this.state.sourceY, + width: this.state.sourceWidth, + height: this.state.sourceHeight + }; + const nodes = graph._nodes; + if (!nodes) return; + for (const node of nodes) { + const widgets = node.widgets; + if (!widgets) continue; + for (const widget of widgets) { + let isTextElement = false; + let isVideoElement = false; + let isImageElement = false; + let elementToProcess = null; + if (widget.element) { + const element = widget.element; + if (widget.type === "text" || widget.type === "string" || element.tagName === "TEXTAREA") { + isTextElement = true; + elementToProcess = element; + } else if (element.tagName === "VIDEO") { + isVideoElement = true; + elementToProcess = element; + } else if (element.tagName === "IMG") { + isImageElement = true; + elementToProcess = element; + } else { + const potentialVideo = element.querySelector("video"); + if (potentialVideo) { + isVideoElement = true; + elementToProcess = potentialVideo; + } else { + const potentialImage = element.querySelector("img"); + if (potentialImage) { + isImageElement = true; + elementToProcess = potentialImage; + } + } + } + } + if (elementToProcess && (isTextElement || isVideoElement || isImageElement)) { + const widgetRect = elementToProcess.getBoundingClientRect(); + const canvasRect = this.litegraphCanvas.getBoundingClientRect(); + const canvasToViewportScaleX = canvasRect.width > 0 ? this.litegraphCanvas.width / canvasRect.width : 1; + const canvasToViewportScaleY = canvasRect.height > 0 ? this.litegraphCanvas.height / canvasRect.height : 1; + const widgetCanvasX = (widgetRect.left - canvasRect.left) * canvasToViewportScaleX; + const widgetCanvasY = (widgetRect.top - canvasRect.top) * canvasToViewportScaleY; + const widgetCanvasWidth = widgetRect.width * canvasToViewportScaleX; + const widgetCanvasHeight = widgetRect.height * canvasToViewportScaleY; + const widgetSourceRect = { + x: widgetCanvasX, + y: widgetCanvasY, + width: widgetCanvasWidth, + height: widgetCanvasHeight + }; + if (rectsOverlap(magnifyRect, widgetSourceRect)) { + const clonedElement = elementToProcess.cloneNode(true); + clonedElement.style.position = "absolute"; + clonedElement.style.pointerEvents = "none"; + if (isTextElement) { + clonedElement.style.backgroundColor = elementToProcess.style.backgroundColor || "#222"; + clonedElement.style.color = elementToProcess.style.color || "#DDD"; + clonedElement.style.border = elementToProcess.style.border || "1px solid #555"; + clonedElement.disabled = true; + } else if (isVideoElement) { + const video = clonedElement; + const originalVideo = elementToProcess; + video.src = originalVideo.src; + video.autoplay = originalVideo.autoplay; + video.loop = originalVideo.loop; + video.preload = originalVideo.preload; + video.crossOrigin = originalVideo.crossOrigin; + video.muted = true; + if (!originalVideo.paused) { + video.play().catch((e) => console.warn("Magnify Glass: Cloned video play failed", e)); + } + video.currentTime = originalVideo.currentTime; + } else if (isImageElement) { + const img = clonedElement; + const originalImg = elementToProcess; + img.src = originalImg.src; + img.alt = originalImg.alt; + } + const relativeX = widgetSourceRect.x - magnifyRect.x; + const relativeY = widgetSourceRect.y - magnifyRect.y; + const magnifiedX = relativeX * this.config.zoomFactor; + const magnifiedY = relativeY * this.config.zoomFactor; + clonedElement.style.left = `${magnifiedX}px`; + clonedElement.style.top = `${magnifiedY}px`; + clonedElement.style.width = `${widgetSourceRect.width}px`; + clonedElement.style.height = `${widgetSourceRect.height}px`; + clonedElement.style.transformOrigin = "top left"; + clonedElement.style.transform = `scale(${this.config.zoomFactor})`; + if (isTextElement && clonedElement.style.fontSize) { + const originalFontSize = parseFloat(window.getComputedStyle(elementToProcess).fontSize); + clonedElement.style.fontSize = `${originalFontSize}px`; + } + this.ui.htmlOverlayContainer.appendChild(clonedElement); + } + } + } + } + } + /** + * Update config from current settings values. + */ + updateConfigFromSettings() { + this.config.loadSettings(); + } + /** + * Apply UI changes based on current config. + */ + applyUiChanges() { + var _a; + this.ui.applyStyles(); + if ((_a = this.renderer) == null ? void 0 : _a.gl) { + this.renderer.updateViewport(); + } + } + /** + * Reset offsets and panel positions to defaults. + */ + resetOffsets() { + this.config.resetOffsets(); + this.config.followCursor = false; + if (this.state.active) { + 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; + } + const extensions = window.comfyUIMagnifyGlassExtensions; + if (extensions && extensions.length > 0) { + extensions.forEach((extension) => { + var _a; + if (extension == null ? void 0 : extension.stateManager) { + extension.stateManager.state.isPanelPinned = false; + extension.stateManager.state.isPanelLocked = false; + extension.stateManager.state.isAutoPinned = false; + extension.stateManager.state.pinnedPosition = { x: 0, y: 0 }; + extension.stateManager.state.lastPinnedPosition = null; + if (extension.positionManager) { + extension.positionManager.positionPanel(); + } + if ((_a = extension.uiManager) == null ? void 0 : _a.updateControlStates) { + extension.uiManager.updateControlStates(); + } + } + }); + } + this.debugger.log("Reset: Magnify glass and inspector panel positions restored to defaults"); + } + /** + * Cleanup all resources. + */ + cleanup() { + this.eventHandler.detachListeners(); + this.ui.cleanup(); + } +} +export { + MagnifyGlass +}; +//# sourceMappingURL=MagnifyGlass.js.map diff --git a/web/magnify-glass/MagnifyGlass.js.map b/web/magnify-glass/MagnifyGlass.js.map new file mode 100644 index 0000000..0bc50d1 --- /dev/null +++ b/web/magnify-glass/MagnifyGlass.js.map @@ -0,0 +1 @@ +{"version":3,"file":"MagnifyGlass.js","sources":["../../src/magnify-glass/MagnifyGlass.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Main Class (TypeScript)\n * \n * Orchestrates all components of the magnifying glass.\n */\n\nimport type { ComfyApp, ComfyNode } from '../types/comfyui';\nimport { findLiteGraphCanvas, rectsOverlap, Rectangle } from '../shared/utils';\nimport { DEFAULT_PADDING, DEFAULT_GLASS_Y_OFFSET } from '../shared/constants';\nimport { registerGlassSettings } from '../shared/settings';\nimport { ConfigManager } from './ConfigManager';\nimport { MagnifierState } from './MagnifierState';\nimport { UiManager } from './UiManager';\nimport { WebGLRenderer } from './WebGLRenderer';\nimport { DebugManager } from './DebugManager';\nimport { EventHandler } from './EventHandler';\n\n// External ComfyUI globals\ndeclare const app: ComfyApp;\ndeclare const LiteGraph: unknown;\n\n/**\n * Main MagnifyGlass class.\n * Orchestrates all components and provides the main API.\n */\nexport class MagnifyGlass {\n config: ConfigManager;\n state: MagnifierState;\n ui: UiManager;\n renderer: WebGLRenderer | null;\n debugger: DebugManager;\n eventHandler: EventHandler;\n\n /** The LiteGraph canvas */\n litegraphCanvas: HTMLCanvasElement | null;\n\n /** Last known mouse position for better initial positioning */\n lastKnownMousePosition: { x: number; y: number };\n\n /** Whether we're currently over media (for info panel) */\n isOverMedia: boolean;\n\n /** Current media element under cursor */\n currentMediaElement: HTMLImageElement | HTMLVideoElement | null;\n\n constructor() {\n this.config = new ConfigManager();\n this.state = new MagnifierState();\n this.ui = new UiManager(this.config, this.state, () => this.toggle());\n this.renderer = null;\n this.debugger = new DebugManager(this.config, this.state, this.ui);\n this.eventHandler = new EventHandler(this);\n\n // The LiteGraph canvas\n this.litegraphCanvas = null;\n\n // Last known mouse position for better initial positioning\n this.lastKnownMousePosition = { x: 0, y: 0 };\n\n // Media tracking\n this.isOverMedia = false;\n this.currentMediaElement = null;\n }\n\n /**\n * Initialize the magnifying glass.\n */\n init(): void {\n if (typeof LiteGraph === 'undefined' || typeof app === 'undefined' || !app.canvas) {\n this.debugger.log(\"LiteGraph or app not ready, retrying in 100ms.\");\n setTimeout(() => this.init(), 100);\n return;\n }\n this.debugger.log(\"LiteGraph and app ready.\");\n\n // Load saved offsets first\n this.config.loadSavedOffsets();\n\n // Print detailed canvas information\n this.debugger.printCanvasInfo();\n\n // Create UI elements\n this.ui.createElements();\n\n // Setup WebGL renderer\n this.renderer = new WebGLRenderer(this.config, this.state, this.ui);\n if (!this.renderer.isValid()) {\n this.ui.cleanup();\n return;\n }\n\n // Find the LiteGraph canvas\n this.litegraphCanvas = findLiteGraphCanvas();\n this.debugger.log(\"LiteGraph canvas found:\", this.litegraphCanvas);\n if (!this.litegraphCanvas) {\n this.debugger.error(\"Could not find LiteGraph canvas. Magnifier will not work.\");\n this.ui.cleanup();\n return;\n }\n\n // Attach event handlers\n this.eventHandler.attachListeners();\n\n // Register settings with ComfyUI\n registerGlassSettings(this);\n\n this.debugger.log(`Initialized (WebGL) with Smart Input Detection. Press ${this.config.altRequired ? 'Alt+' : ''}${this.config.activationKey.toUpperCase()} to activate.`);\n }\n\n /**\n * Toggle the magnifying glass active state.\n */\n toggle(): void {\n const state = this.state;\n const config = this.config;\n\n if (state.active) {\n state.active = false;\n this.ui.hide();\n } else {\n state.active = true;\n this.ui.show();\n // Update initial position if needed\n if (this.eventHandler) {\n this.eventHandler.updateInitialPosition();\n }\n }\n }\n\n /**\n * Update the magnified view.\n */\n updateMagnifiedView(): void {\n if (!this.state.active || !this.renderer || !this.litegraphCanvas) {\n return;\n }\n\n // Get canvas transformation info\n this.updateCanvasTransformation();\n\n // Calculate the source region\n this.calculateSourceRegion();\n\n // Schedule the rendering operation\n if (!this.state.isRenderScheduled) {\n this.state.isRenderScheduled = true;\n requestAnimationFrame(() => {\n if (!this.state.active || !this.renderer || !this.litegraphCanvas) {\n this.state.isRenderScheduled = false;\n return;\n }\n // Render the magnified view\n this.renderer.render(this.litegraphCanvas);\n\n // Update debug visualization\n this.debugger.updateDebugView();\n\n // Render HTML overlays\n this.renderHtmlOverlays();\n\n this.state.isRenderScheduled = false;\n });\n }\n }\n\n /**\n * Update canvas transformation state.\n */\n updateCanvasTransformation(): void {\n this.state.canvasScale = 1.0;\n this.state.canvasOffsetX = 0;\n this.state.canvasOffsetY = 0;\n\n if (app?.canvas) {\n const ds = (app.canvas as any).ds;\n if (ds) {\n if (typeof ds.scale === 'number') {\n this.state.canvasScale = ds.scale;\n }\n\n if (ds.offset) {\n this.state.canvasOffsetX = ds.offset[0] || 0;\n this.state.canvasOffsetY = ds.offset[1] || 0;\n }\n }\n }\n }\n\n /**\n * Calculate the source region for magnification.\n */\n calculateSourceRegion(): void {\n const cursorPixelX = this.state.x;\n const cursorPixelY = this.state.y;\n const canvasScale = this.state.canvasScale;\n const canvasOffsetX = this.state.canvasOffsetX;\n const canvasOffsetY = this.state.canvasOffsetY;\n\n if (canvasScale === 0) return;\n\n // Convert cursor canvas pixels to LiteGraph graph coordinates\n const cursorGraphX = (cursorPixelX - canvasOffsetX) / canvasScale;\n const cursorGraphY = (cursorPixelY - canvasOffsetY) / canvasScale;\n\n // Apply manual offset\n const targetGraphCenterX = cursorGraphX + this.config.offsetX;\n const targetGraphCenterY = cursorGraphY + this.config.offsetY;\n\n // Calculate source dimensions in graph units\n const sourceGraphWidth = (this.config.glassSize / this.config.zoomFactor) / canvasScale;\n const sourceGraphHeight = (this.config.glassSize / this.config.zoomFactor) / canvasScale;\n\n // Calculate source top-left corner in graph coordinates\n const sourceGraphX = targetGraphCenterX - (sourceGraphWidth / 2);\n const sourceGraphY = targetGraphCenterY - (sourceGraphHeight / 2);\n\n // Convert back to canvas pixel coordinates\n this.state.sourceX = (sourceGraphX * canvasScale) + canvasOffsetX;\n this.state.sourceY = (sourceGraphY * canvasScale) + canvasOffsetY;\n this.state.sourceWidth = sourceGraphWidth * canvasScale;\n this.state.sourceHeight = sourceGraphHeight * canvasScale;\n }\n\n /**\n * Render HTML overlays for text and media in the magnified view.\n */\n renderHtmlOverlays(): void {\n const graph = (app as any).graph;\n if (!this.state.active || !this.ui.htmlOverlayContainer || !graph || !this.litegraphCanvas) {\n if (this.ui.htmlOverlayContainer) this.ui.htmlOverlayContainer.innerHTML = '';\n return;\n }\n\n this.ui.htmlOverlayContainer.innerHTML = '';\n\n const magnifyRect: Rectangle = {\n x: this.state.sourceX,\n y: this.state.sourceY,\n width: this.state.sourceWidth,\n height: this.state.sourceHeight\n };\n\n const nodes: ComfyNode[] = graph._nodes;\n if (!nodes) return;\n\n for (const node of nodes) {\n const widgets = (node as any).widgets;\n if (!widgets) continue;\n\n for (const widget of widgets) {\n let isTextElement = false;\n let isVideoElement = false;\n let isImageElement = false;\n let elementToProcess: HTMLElement | null = null;\n\n if (widget.element) {\n const element = widget.element as HTMLElement;\n if (widget.type === \"text\" || widget.type === \"string\" || element.tagName === 'TEXTAREA') {\n isTextElement = true;\n elementToProcess = element;\n } else if (element.tagName === 'VIDEO') {\n isVideoElement = true;\n elementToProcess = element;\n } else if (element.tagName === 'IMG') {\n isImageElement = true;\n elementToProcess = element;\n } else {\n const potentialVideo = element.querySelector('video');\n if (potentialVideo) {\n isVideoElement = true;\n elementToProcess = potentialVideo;\n } else {\n const potentialImage = element.querySelector('img');\n if (potentialImage) {\n isImageElement = true;\n elementToProcess = potentialImage;\n }\n }\n }\n }\n\n if (elementToProcess && (isTextElement || isVideoElement || isImageElement)) {\n const widgetRect = elementToProcess.getBoundingClientRect();\n const canvasRect = this.litegraphCanvas!.getBoundingClientRect();\n\n const canvasToViewportScaleX = canvasRect.width > 0 ? (this.litegraphCanvas!.width / canvasRect.width) : 1;\n const canvasToViewportScaleY = canvasRect.height > 0 ? (this.litegraphCanvas!.height / canvasRect.height) : 1;\n\n const widgetCanvasX = (widgetRect.left - canvasRect.left) * canvasToViewportScaleX;\n const widgetCanvasY = (widgetRect.top - canvasRect.top) * canvasToViewportScaleY;\n const widgetCanvasWidth = widgetRect.width * canvasToViewportScaleX;\n const widgetCanvasHeight = widgetRect.height * canvasToViewportScaleY;\n\n const widgetSourceRect: Rectangle = {\n x: widgetCanvasX,\n y: widgetCanvasY,\n width: widgetCanvasWidth,\n height: widgetCanvasHeight\n };\n\n if (rectsOverlap(magnifyRect, widgetSourceRect)) {\n const clonedElement = elementToProcess.cloneNode(true) as HTMLElement;\n clonedElement.style.position = 'absolute';\n clonedElement.style.pointerEvents = 'none';\n\n if (isTextElement) {\n clonedElement.style.backgroundColor = elementToProcess.style.backgroundColor || '#222';\n clonedElement.style.color = elementToProcess.style.color || '#DDD';\n clonedElement.style.border = elementToProcess.style.border || '1px solid #555';\n (clonedElement as HTMLInputElement).disabled = true;\n } else if (isVideoElement) {\n const video = clonedElement as HTMLVideoElement;\n const originalVideo = elementToProcess as HTMLVideoElement;\n video.src = originalVideo.src;\n video.autoplay = originalVideo.autoplay;\n video.loop = originalVideo.loop;\n video.preload = originalVideo.preload;\n video.crossOrigin = originalVideo.crossOrigin;\n video.muted = true;\n if (!originalVideo.paused) {\n video.play().catch(e => console.warn(\"Magnify Glass: Cloned video play failed\", e));\n }\n video.currentTime = originalVideo.currentTime;\n } else if (isImageElement) {\n const img = clonedElement as HTMLImageElement;\n const originalImg = elementToProcess as HTMLImageElement;\n img.src = originalImg.src;\n img.alt = originalImg.alt;\n }\n\n const relativeX = widgetSourceRect.x - magnifyRect.x;\n const relativeY = widgetSourceRect.y - magnifyRect.y;\n const magnifiedX = relativeX * this.config.zoomFactor;\n const magnifiedY = relativeY * this.config.zoomFactor;\n\n clonedElement.style.left = `${magnifiedX}px`;\n clonedElement.style.top = `${magnifiedY}px`;\n clonedElement.style.width = `${widgetSourceRect.width}px`;\n clonedElement.style.height = `${widgetSourceRect.height}px`;\n clonedElement.style.transformOrigin = 'top left';\n clonedElement.style.transform = `scale(${this.config.zoomFactor})`;\n\n if (isTextElement && clonedElement.style.fontSize) {\n const originalFontSize = parseFloat(window.getComputedStyle(elementToProcess).fontSize);\n clonedElement.style.fontSize = `${originalFontSize}px`;\n }\n\n this.ui.htmlOverlayContainer!.appendChild(clonedElement);\n }\n }\n }\n }\n }\n\n /**\n * Update config from current settings values.\n */\n updateConfigFromSettings(): void {\n this.config.loadSettings();\n }\n\n /**\n * Apply UI changes based on current config.\n */\n applyUiChanges(): void {\n this.ui.applyStyles();\n\n if (this.renderer?.gl) {\n this.renderer.updateViewport();\n }\n }\n\n\n\n /**\n * Reset offsets and panel positions to defaults.\n */\n resetOffsets(): void {\n this.config.resetOffsets();\n\n // Also disable follow cursor\n this.config.followCursor = false;\n\n if (this.state.active) {\n this.updateMagnifiedView();\n }\n\n // Reset magnify glass position to top-right corner\n if (this.ui.glassDiv) {\n const glassSize = this.config.glassSize;\n this.ui.glassDiv.style.left = `${window.innerWidth - glassSize - DEFAULT_PADDING}px`;\n this.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`;\n this.state.wasActivatedBefore = false;\n }\n\n // Reset inspector panel position if exists\n const extensions = window.comfyUIMagnifyGlassExtensions;\n if (extensions && extensions.length > 0) {\n extensions.forEach((extension: any) => {\n if (extension?.stateManager) {\n extension.stateManager.state.isPanelPinned = false;\n extension.stateManager.state.isPanelLocked = false;\n extension.stateManager.state.isAutoPinned = false;\n extension.stateManager.state.pinnedPosition = { x: 0, y: 0 };\n extension.stateManager.state.lastPinnedPosition = null;\n\n if (extension.positionManager) {\n extension.positionManager.positionPanel();\n }\n if (extension.uiManager?.updateControlStates) {\n extension.uiManager.updateControlStates();\n }\n }\n });\n }\n\n this.debugger.log(\"Reset: Magnify glass and inspector panel positions restored to defaults\");\n }\n\n /**\n * Cleanup all resources.\n */\n cleanup(): void {\n this.eventHandler.detachListeners();\n this.ui.cleanup();\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/magnify-glass/UiManager.js b/web/magnify-glass/UiManager.js new file mode 100644 index 0000000..c1b6cd2 --- /dev/null +++ b/web/magnify-glass/UiManager.js @@ -0,0 +1,331 @@ +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +import { Z_INDEX, DEFAULT_PADDING } from "../shared/constants.js"; +import { Icons } from "../shared/icons.js"; +import { Logger } from "../shared/logger.js"; +class UiManager { + constructor(config, state, onToggle) { + __publicField(this, "config"); + __publicField(this, "state"); + __publicField(this, "glassDiv"); + __publicField(this, "glassCanvas"); + __publicField(this, "debugCanvas"); + __publicField(this, "debugCtx"); + __publicField(this, "htmlOverlayContainer"); + __publicField(this, "onToggle"); + this.config = config; + this.state = state; + this.glassDiv = null; + this.glassCanvas = null; + this.debugCanvas = null; + this.debugCtx = null; + this.htmlOverlayContainer = null; + this.onToggle = onToggle; + } + /** + * Create all DOM elements for the magnifying glass. + */ + createElements() { + this.glassDiv = document.createElement("div"); + this.glassDiv.id = "comfyui-magnify-glass"; + this.glassDiv.style.cssText = ` + position: absolute; + box-sizing: border-box; + width: ${this.config.glassSize}px; + height: ${this.config.glassSize}px; + border-radius: ${this.config.glassShape === "Circle" ? "50%" : "0px"}; + border: ${this.config.borderEnabled ? `${this.config.borderWidth}px solid ${this.config.borderColor}` : "none"}; + overflow: hidden; + pointer-events: none; + z-index: ${Z_INDEX.GLASS}; + display: none; + box-shadow: 0 5px 15px rgba(0,0,0,0.3); + background-color: rgba(255,255,255,0.1); + `; + this.glassCanvas = document.createElement("canvas"); + this.glassCanvas.width = this.config.glassSize; + this.glassCanvas.height = this.config.glassSize; + this.glassCanvas.id = "comfyui-magnify-canvas"; + this.glassDiv.appendChild(this.glassCanvas); + this.htmlOverlayContainer = document.createElement("div"); + this.htmlOverlayContainer.id = "comfyui-magnify-html-overlay"; + this.htmlOverlayContainer.style.cssText = ` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + overflow: hidden; + `; + this.glassDiv.appendChild(this.htmlOverlayContainer); + document.body.appendChild(this.glassDiv); + if (this.config.debugMode) { + this.createDebugCanvas(); + } + this.injectMenuButton(); + } + /** + * Create the debug canvas overlay. + */ + createDebugCanvas() { + this.debugCanvas = document.createElement("canvas"); + this.debugCanvas.id = "comfyui-magnify-debug"; + this.debugCanvas.width = 400; + this.debugCanvas.height = 350; + this.debugCanvas.style.cssText = ` + position: fixed; + top: 10px; + right: 10px; + background: rgba(0,0,0,0.7); + border: 1px solid #fff; + z-index: ${Z_INDEX.DEBUG}; + pointer-events: none; + color: white; + font-family: monospace; + display: none; + `; + document.body.appendChild(this.debugCanvas); + this.debugCtx = this.debugCanvas.getContext("2d"); + } + /** + * Show the magnifying glass. + */ + show() { + if (this.glassDiv) { + this.glassDiv.style.display = "block"; + } + if (this.config.debugMode && this.debugCanvas) { + this.debugCanvas.style.display = "block"; + } + } + /** + * Set the visual visibility of the glass preview (opacity). + * This allows the tool to remain "Active" (tracking mouse) but invisible, + * so that the Inspector Panel can be used in "Inspector Only" mode. + */ + setPreviewVisibility(visible) { + if (this.glassDiv) { + this.glassDiv.style.opacity = visible ? "1" : "0"; + } + } + /** + * Hide the magnifying glass. + */ + hide() { + if (this.glassDiv) { + this.glassDiv.style.display = "none"; + } + if (this.config.debugMode && this.debugCanvas) { + this.debugCanvas.style.display = "none"; + } + if (this.htmlOverlayContainer) { + this.htmlOverlayContainer.innerHTML = ""; + } + } + /** + * Position the glass relative to cursor. + * @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; + switch (this.config.glassPosition) { + case "Top": + newLeft = clientX - glassSize / 2; + newTop = clientY - glassSize - offsetAmount; + break; + case "Bottom": + newLeft = clientX - glassSize / 2; + newTop = clientY + offsetAmount; + break; + case "Left": + newLeft = clientX - glassSize - offsetAmount; + newTop = clientY - glassSize / 2; + break; + case "Right": + newLeft = clientX + offsetAmount; + newTop = clientY - glassSize / 2; + break; + case "Top-Left": + newLeft = clientX - glassSize - offsetAmount; + newTop = clientY - glassSize - offsetAmount; + break; + case "Top-Right": + newLeft = clientX + offsetAmount; + newTop = clientY - glassSize - offsetAmount; + break; + case "Bottom-Left": + newLeft = clientX - glassSize - offsetAmount; + newTop = clientY + offsetAmount; + break; + case "Bottom-Right": + newLeft = clientX + offsetAmount; + newTop = clientY + offsetAmount; + break; + default: + newLeft = clientX - glassSize / 2; + newTop = clientY + offsetAmount; + break; + } + this.glassDiv.style.left = `${newLeft}px`; + this.glassDiv.style.top = `${newTop}px`; + this.adjustForBoundaries(clientX, clientY); + } + /** + * Adjust glass position to stay within viewport boundaries. + * @param clientX - Client X coordinate + * @param clientY - Client Y coordinate + */ + adjustForBoundaries(clientX, clientY) { + 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 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"; + } + } + /** + * Apply current config to UI elements. + */ + applyStyles() { + if (this.glassDiv) { + this.glassDiv.style.width = `${this.config.glassSize}px`; + this.glassDiv.style.height = `${this.config.glassSize}px`; + this.glassDiv.style.border = this.config.borderEnabled ? `${this.config.borderWidth}px solid ${this.config.borderColor}` : "none"; + this.glassDiv.style.clipPath = "none"; + this.glassDiv.style.borderRadius = "0px"; + switch (this.config.glassShape) { + case "Circle": + this.glassDiv.style.borderRadius = "50%"; + break; + case "Square": + break; + case "Rounded Square": + this.glassDiv.style.borderRadius = "20%"; + break; + default: + this.glassDiv.style.borderRadius = "50%"; + break; + } + } + if (this.glassCanvas) { + this.glassCanvas.width = this.config.glassSize; + this.glassCanvas.height = this.config.glassSize; + } + if (this.config.debugMode) { + if (!this.debugCanvas) this.createDebugCanvas(); + if (this.state.active && this.debugCanvas) this.debugCanvas.style.display = "block"; + } else { + if (this.debugCanvas) this.debugCanvas.style.display = "none"; + } + } + /** + * Cleanup DOM elements. + */ + cleanup() { + if (this.glassDiv) this.glassDiv.remove(); + if (this.debugCanvas) this.debugCanvas.remove(); + const btn = document.querySelector(".magnify-toggle-btn"); + if (btn) btn.remove(); + } + /** + * Inject a quick toggle button into the ComfyUI menu. + */ + /** + * Inject a quick toggle button into the ComfyUI menu. + */ + injectMenuButton() { + const stopTime = Date.now() + 3e4; + const attemptInjection = () => { + const buttons = Array.from(document.querySelectorAll("button")); + let anchorBtn = buttons.find((b) => { + const title = (b.title || "").toLowerCase(); + const aria = (b.getAttribute("aria-label") || "").toLowerCase(); + const text = (b.textContent || "").toLowerCase(); + return title.includes("map") && !title.includes("open") || aria.includes("map") && !aria.includes("open") || text.includes("map"); + }); + if (!anchorBtn) { + anchorBtn = buttons.find((b) => { + const title = (b.title || "").toLowerCase(); + const aria = (b.getAttribute("aria-label") || "").toLowerCase(); + return title.includes("link") || aria.includes("link"); + }); + } + if (anchorBtn && anchorBtn.parentElement) { + if (anchorBtn.parentElement.querySelector(".magnify-toggle-btn")) return true; + Logger.debug("Found menu anchor:", anchorBtn.title || anchorBtn.getAttribute("aria-label") || "Unknown Button"); + const btn = document.createElement("button"); + btn.className = anchorBtn.className + " magnify-toggle-btn"; + const computed = window.getComputedStyle(anchorBtn); + btn.style.height = computed.height; + btn.style.minHeight = computed.minHeight; + btn.title = "Toggle Magnify Glass"; + btn.innerHTML = Icons.magnifyGlass; + btn.style.display = "inline-flex"; + btn.style.alignItems = "center"; + btn.style.justifyContent = "center"; + btn.style.padding = "0 8px"; + btn.style.cursor = "pointer"; + btn.style.border = anchorBtn.style.border || computed.border; + btn.style.borderRadius = anchorBtn.style.borderRadius || computed.borderRadius; + if (!btn.style.background) btn.style.background = computed.background; + if (!btn.style.color) btn.style.color = computed.color; + btn.addEventListener("click", () => { + if (this.onToggle) { + this.onToggle(); + btn.classList.toggle("active"); + btn.classList.toggle("p-highlight"); + btn.classList.toggle("selected"); + } + }); + const isMinimap = (anchorBtn.title || "").toLowerCase().includes("map") || (anchorBtn.getAttribute("aria-label") || "").toLowerCase().includes("map"); + if (isMinimap) { + if (anchorBtn.nextSibling) { + anchorBtn.parentElement.insertBefore(btn, anchorBtn.nextSibling); + } else { + anchorBtn.parentElement.appendChild(btn); + } + } else { + anchorBtn.parentElement.insertBefore(btn, anchorBtn); + } + Logger.debug("Menu toggle button injected successfully"); + return true; + } + return false; + }; + if (attemptInjection()) return; + const checkForMenu = setInterval(() => { + if (Date.now() > stopTime) { + console.warn( + "[MagnifyGlass] Menu injection timed out. Found buttons:", + Array.from(document.querySelectorAll("button")).map((b) => b.title || b.getAttribute("aria-label") || b.textContent || b.className).slice(0, 5) + ); + clearInterval(checkForMenu); + return; + } + if (attemptInjection()) { + clearInterval(checkForMenu); + } + }, 100); + } +} +export { + UiManager +}; +//# sourceMappingURL=UiManager.js.map diff --git a/web/magnify-glass/UiManager.js.map b/web/magnify-glass/UiManager.js.map new file mode 100644 index 0000000..b3645d4 --- /dev/null +++ b/web/magnify-glass/UiManager.js.map @@ -0,0 +1 @@ +{"version":3,"file":"UiManager.js","sources":["../../src/magnify-glass/UiManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - UiManager (TypeScript)\n * \n * Handles creation and management of DOM elements for the magnifying glass.\n */\n\nimport { Z_INDEX, DEFAULT_PADDING } from '../shared/constants';\nimport { Icons } from '../shared/icons';\nimport { Logger } from '../shared/logger';\nimport type { ConfigManager } from './ConfigManager';\nimport type { MagnifierState } from './MagnifierState';\n\n/**\n * UI Manager class.\n * Manages all DOM elements for the magnifying glass.\n */\nexport class UiManager {\n config: ConfigManager;\n state: MagnifierState;\n glassDiv: HTMLDivElement | null;\n glassCanvas: HTMLCanvasElement | null;\n debugCanvas: HTMLCanvasElement | null;\n debugCtx: CanvasRenderingContext2D | null;\n htmlOverlayContainer: HTMLDivElement | null;\n onToggle: (() => void) | undefined;\n\n constructor(config: ConfigManager, state: MagnifierState, onToggle?: () => void) {\n this.config = config;\n this.state = state;\n this.glassDiv = null;\n this.glassCanvas = null;\n this.debugCanvas = null;\n this.debugCtx = null;\n this.htmlOverlayContainer = null;\n this.onToggle = onToggle;\n }\n\n /**\n * Create all DOM elements for the magnifying glass.\n */\n createElements(): void {\n // Create magnifying glass container\n this.glassDiv = document.createElement(\"div\");\n this.glassDiv.id = \"comfyui-magnify-glass\";\n this.glassDiv.style.cssText = `\n position: absolute;\n box-sizing: border-box;\n width: ${this.config.glassSize}px;\n height: ${this.config.glassSize}px;\n border-radius: ${this.config.glassShape === \"Circle\" ? \"50%\" : \"0px\"};\n border: ${this.config.borderEnabled ? `${this.config.borderWidth}px solid ${this.config.borderColor}` : 'none'};\n overflow: hidden;\n pointer-events: none;\n z-index: ${Z_INDEX.GLASS};\n display: none;\n box-shadow: 0 5px 15px rgba(0,0,0,0.3);\n background-color: rgba(255,255,255,0.1);\n `;\n\n // Create WebGL canvas for the magnifying glass\n this.glassCanvas = document.createElement(\"canvas\");\n this.glassCanvas.width = this.config.glassSize;\n this.glassCanvas.height = this.config.glassSize;\n this.glassCanvas.id = \"comfyui-magnify-canvas\";\n this.glassDiv.appendChild(this.glassCanvas);\n\n // Create HTML overlay container\n this.htmlOverlayContainer = document.createElement(\"div\");\n this.htmlOverlayContainer.id = \"comfyui-magnify-html-overlay\";\n this.htmlOverlayContainer.style.cssText = `\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n pointer-events: none;\n overflow: hidden; \n `;\n this.glassDiv.appendChild(this.htmlOverlayContainer);\n\n document.body.appendChild(this.glassDiv);\n\n // Create debug canvas if debug mode is enabled\n if (this.config.debugMode) {\n this.createDebugCanvas();\n }\n\n // Inject menu button\n this.injectMenuButton();\n }\n\n /**\n * Create the debug canvas overlay.\n */\n createDebugCanvas(): void {\n this.debugCanvas = document.createElement(\"canvas\");\n this.debugCanvas.id = \"comfyui-magnify-debug\";\n this.debugCanvas.width = 400;\n this.debugCanvas.height = 350;\n this.debugCanvas.style.cssText = `\n position: fixed;\n top: 10px;\n right: 10px;\n background: rgba(0,0,0,0.7);\n border: 1px solid #fff;\n z-index: ${Z_INDEX.DEBUG};\n pointer-events: none;\n color: white;\n font-family: monospace;\n display: none;\n `;\n document.body.appendChild(this.debugCanvas);\n this.debugCtx = this.debugCanvas.getContext('2d');\n }\n\n /**\n * Show the magnifying glass.\n */\n show(): void {\n if (this.glassDiv) {\n this.glassDiv.style.display = 'block';\n // We do NOT modify opacity here, allowing it to be controlled independently\n }\n if (this.config.debugMode && this.debugCanvas) {\n this.debugCanvas.style.display = \"block\";\n }\n }\n\n /**\n * Set the visual visibility of the glass preview (opacity).\n * This allows the tool to remain \"Active\" (tracking mouse) but invisible,\n * so that the Inspector Panel can be used in \"Inspector Only\" mode.\n */\n setPreviewVisibility(visible: boolean): void {\n if (this.glassDiv) {\n this.glassDiv.style.opacity = visible ? '1' : '0';\n // Disable pointer events when hidden to be safe, though usually they are none anyway\n // this.glassDiv.style.pointerEvents = visible ? 'none' : 'none'; \n }\n }\n\n /**\n * Hide the magnifying glass.\n */\n hide(): void {\n if (this.glassDiv) {\n this.glassDiv.style.display = 'none';\n }\n if (this.config.debugMode && this.debugCanvas) {\n this.debugCanvas.style.display = \"none\";\n }\n // Clear HTML overlays when hiding\n if (this.htmlOverlayContainer) {\n this.htmlOverlayContainer.innerHTML = '';\n }\n }\n\n /**\n * Position the glass relative to cursor.\n * @param clientX - Client X coordinate\n * @param clientY - Client Y coordinate\n */\n positionGlass(clientX: number, clientY: number): void {\n if (!this.config.followCursor || !this.glassDiv) return;\n\n const glassSize = this.config.glassSize;\n const offsetAmount = DEFAULT_PADDING;\n let newLeft: number;\n let newTop: number;\n\n switch (this.config.glassPosition) {\n case \"Top\":\n newLeft = clientX - (glassSize / 2);\n newTop = clientY - glassSize - offsetAmount;\n break;\n case \"Bottom\":\n newLeft = clientX - (glassSize / 2);\n newTop = clientY + offsetAmount;\n break;\n case \"Left\":\n newLeft = clientX - glassSize - offsetAmount;\n newTop = clientY - (glassSize / 2);\n break;\n case \"Right\":\n newLeft = clientX + offsetAmount;\n newTop = clientY - (glassSize / 2);\n break;\n case \"Top-Left\":\n newLeft = clientX - glassSize - offsetAmount;\n newTop = clientY - glassSize - offsetAmount;\n break;\n case \"Top-Right\":\n newLeft = clientX + offsetAmount;\n newTop = clientY - glassSize - offsetAmount;\n break;\n case \"Bottom-Left\":\n newLeft = clientX - glassSize - offsetAmount;\n newTop = clientY + offsetAmount;\n break;\n case \"Bottom-Right\":\n newLeft = clientX + offsetAmount;\n newTop = clientY + offsetAmount;\n break;\n default:\n newLeft = clientX - (glassSize / 2);\n newTop = clientY + offsetAmount;\n break;\n }\n\n this.glassDiv.style.left = `${newLeft}px`;\n this.glassDiv.style.top = `${newTop}px`;\n\n this.adjustForBoundaries(clientX, clientY);\n }\n\n /**\n * Adjust glass position to stay within viewport boundaries.\n * @param clientX - Client X coordinate\n * @param clientY - Client Y coordinate\n */\n adjustForBoundaries(clientX: number, clientY: number): void {\n if (!this.glassDiv) return;\n\n const glassRect = this.glassDiv.getBoundingClientRect();\n\n // Check right boundary\n if (glassRect.right > window.innerWidth) {\n this.glassDiv.style.left = `${clientX - glassRect.width - DEFAULT_PADDING}px`;\n }\n\n // Check left boundary\n const currentRectLeft = this.glassDiv.getBoundingClientRect();\n if (currentRectLeft.left < 0) {\n this.glassDiv.style.left = \"10px\";\n }\n\n // Check bottom boundary\n if (glassRect.bottom > window.innerHeight) {\n this.glassDiv.style.top = `${clientY - glassRect.height - DEFAULT_PADDING}px`;\n }\n\n // Check top boundary\n const currentRectTop = this.glassDiv.getBoundingClientRect();\n if (currentRectTop.top < 0) {\n this.glassDiv.style.top = \"10px\";\n }\n }\n\n /**\n * Apply current config to UI elements.\n */\n applyStyles(): void {\n if (this.glassDiv) {\n this.glassDiv.style.width = `${this.config.glassSize}px`;\n this.glassDiv.style.height = `${this.config.glassSize}px`;\n this.glassDiv.style.border = this.config.borderEnabled\n ? `${this.config.borderWidth}px solid ${this.config.borderColor}`\n : 'none';\n\n // Reset clip-path and set default border-radius\n this.glassDiv.style.clipPath = 'none';\n this.glassDiv.style.borderRadius = '0px';\n\n switch (this.config.glassShape) {\n case \"Circle\":\n this.glassDiv.style.borderRadius = \"50%\";\n break;\n case \"Square\":\n // borderRadius is already '0px'\n break;\n case \"Rounded Square\":\n this.glassDiv.style.borderRadius = \"20%\";\n break;\n default:\n this.glassDiv.style.borderRadius = \"50%\";\n break;\n }\n }\n\n if (this.glassCanvas) {\n this.glassCanvas.width = this.config.glassSize;\n this.glassCanvas.height = this.config.glassSize;\n }\n\n // Handle debug canvas show/hide\n if (this.config.debugMode) {\n if (!this.debugCanvas) this.createDebugCanvas();\n if (this.state.active && this.debugCanvas) this.debugCanvas.style.display = \"block\";\n } else {\n if (this.debugCanvas) this.debugCanvas.style.display = \"none\";\n }\n }\n\n /**\n * Cleanup DOM elements.\n */\n cleanup(): void {\n if (this.glassDiv) this.glassDiv.remove();\n if (this.debugCanvas) this.debugCanvas.remove();\n // Remove menu button if needed (though hard to track references here without saving it)\n const btn = document.querySelector('.magnify-toggle-btn');\n if (btn) btn.remove();\n }\n\n /**\n * Inject a quick toggle button into the ComfyUI menu.\n */\n /**\n * Inject a quick toggle button into the ComfyUI menu.\n */\n injectMenuButton(): void {\n // Stop after 30 seconds\n const stopTime = Date.now() + 30000;\n\n // Define check function\n const attemptInjection = () => {\n const buttons = Array.from(document.querySelectorAll('button'));\n\n // Try to find an anchor button (Minimap or Links)\n // Priority 1: Minimap\n let anchorBtn = buttons.find(b => {\n const title = (b.title || '').toLowerCase();\n const aria = (b.getAttribute('aria-label') || '').toLowerCase();\n const text = (b.textContent || '').toLowerCase();\n return (title.includes('map') && !title.includes('open')) ||\n (aria.includes('map') && !aria.includes('open')) ||\n (text.includes('map'));\n });\n\n // Priority 2: Link Visibility\n if (!anchorBtn) {\n anchorBtn = buttons.find(b => {\n const title = (b.title || '').toLowerCase();\n const aria = (b.getAttribute('aria-label') || '').toLowerCase();\n return title.includes('link') || aria.includes('link');\n });\n }\n\n if (anchorBtn && anchorBtn.parentElement) {\n // Determine success BEFORE creating elements to allow early return if duplicate\n if (anchorBtn.parentElement.querySelector('.magnify-toggle-btn')) return true;\n\n Logger.debug('Found menu anchor:', anchorBtn.title || anchorBtn.getAttribute('aria-label') || 'Unknown Button');\n\n const btn = document.createElement('button');\n // Copy all classes to match theme/library (e.g. p-button, comfy-btn, etc)\n btn.className = anchorBtn.className + ' magnify-toggle-btn';\n\n // Copy key styles that might define shape/size\n const computed = window.getComputedStyle(anchorBtn);\n btn.style.height = computed.height;\n btn.style.minHeight = computed.minHeight;\n // btn.style.width = computed.width; // Don't copy width, let it adapt\n\n btn.title = \"Toggle Magnify Glass\";\n btn.innerHTML = Icons.magnifyGlass;\n\n // Ensure specific display props\n btn.style.display = \"inline-flex\";\n btn.style.alignItems = \"center\";\n btn.style.justifyContent = \"center\";\n btn.style.padding = \"0 8px\";\n btn.style.cursor = \"pointer\";\n btn.style.border = anchorBtn.style.border || computed.border;\n btn.style.borderRadius = anchorBtn.style.borderRadius || computed.borderRadius;\n // Use cssText if available for background/color specific overrides\n if (!btn.style.background) btn.style.background = computed.background;\n if (!btn.style.color) btn.style.color = computed.color;\n\n // Add active state tracking\n btn.addEventListener('click', () => {\n if (this.onToggle) {\n this.onToggle();\n // Try to apply common active classes found in modern UI frameworks\n btn.classList.toggle('active');\n btn.classList.toggle('p-highlight'); // PrimeVue\n btn.classList.toggle('selected');\n }\n });\n\n // Insert logic:\n const isMinimap = (anchorBtn.title || '').toLowerCase().includes('map') ||\n (anchorBtn.getAttribute('aria-label') || '').toLowerCase().includes('map');\n\n if (isMinimap) {\n // Insert AFTER Minimap\n if (anchorBtn.nextSibling) {\n anchorBtn.parentElement.insertBefore(btn, anchorBtn.nextSibling);\n } else {\n anchorBtn.parentElement.appendChild(btn);\n }\n } else {\n // It's likely links, Insert BEFORE\n anchorBtn.parentElement.insertBefore(btn, anchorBtn);\n }\n\n Logger.debug('Menu toggle button injected successfully');\n return true;\n }\n return false;\n };\n\n // Run immediately first\n if (attemptInjection()) return;\n\n // Then poll rapidly\n const checkForMenu = setInterval(() => {\n if (Date.now() > stopTime) {\n console.warn(\"[MagnifyGlass] Menu injection timed out. Found buttons:\",\n Array.from(document.querySelectorAll('button')).map(b => b.title || b.getAttribute('aria-label') || b.textContent || b.className).slice(0, 5));\n clearInterval(checkForMenu);\n return;\n }\n\n if (attemptInjection()) {\n clearInterval(checkForMenu);\n }\n }, 100);\n }\n}\n"],"names":[],"mappings":";;;;;;AAgBO,MAAM,UAAU;AAAA,EAUnB,YAAY,QAAuB,OAAuB,UAAuB;AATjF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAGI,SAAK,SAAS;AACd,SAAK,QAAQ;AACb,SAAK,WAAW;AAChB,SAAK,cAAc;AACnB,SAAK,cAAc;AACnB,SAAK,WAAW;AAChB,SAAK,uBAAuB;AAC5B,SAAK,WAAW;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAuB;AAEnB,SAAK,WAAW,SAAS,cAAc,KAAK;AAC5C,SAAK,SAAS,KAAK;AACnB,SAAK,SAAS,MAAM,UAAU;AAAA;AAAA;AAAA,qBAGjB,KAAK,OAAO,SAAS;AAAA,sBACpB,KAAK,OAAO,SAAS;AAAA,6BACd,KAAK,OAAO,eAAe,WAAW,QAAQ,KAAK;AAAA,sBAC1D,KAAK,OAAO,gBAAgB,GAAG,KAAK,OAAO,WAAW,YAAY,KAAK,OAAO,WAAW,KAAK,MAAM;AAAA;AAAA;AAAA,uBAGnG,QAAQ,KAAK;AAAA;AAAA;AAAA;AAAA;AAO5B,SAAK,cAAc,SAAS,cAAc,QAAQ;AAClD,SAAK,YAAY,QAAQ,KAAK,OAAO;AACrC,SAAK,YAAY,SAAS,KAAK,OAAO;AACtC,SAAK,YAAY,KAAK;AACtB,SAAK,SAAS,YAAY,KAAK,WAAW;AAG1C,SAAK,uBAAuB,SAAS,cAAc,KAAK;AACxD,SAAK,qBAAqB,KAAK;AAC/B,SAAK,qBAAqB,MAAM,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAS1C,SAAK,SAAS,YAAY,KAAK,oBAAoB;AAEnD,aAAS,KAAK,YAAY,KAAK,QAAQ;AAGvC,QAAI,KAAK,OAAO,WAAW;AACvB,WAAK,kBAAA;AAAA,IACT;AAGA,SAAK,iBAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,oBAA0B;AACtB,SAAK,cAAc,SAAS,cAAc,QAAQ;AAClD,SAAK,YAAY,KAAK;AACtB,SAAK,YAAY,QAAQ;AACzB,SAAK,YAAY,SAAS;AAC1B,SAAK,YAAY,MAAM,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,uBAMlB,QAAQ,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAM5B,aAAS,KAAK,YAAY,KAAK,WAAW;AAC1C,SAAK,WAAW,KAAK,YAAY,WAAW,IAAI;AAAA,EACpD;AAAA;AAAA;AAAA;AAAA,EAKA,OAAa;AACT,QAAI,KAAK,UAAU;AACf,WAAK,SAAS,MAAM,UAAU;AAAA,IAElC;AACA,QAAI,KAAK,OAAO,aAAa,KAAK,aAAa;AAC3C,WAAK,YAAY,MAAM,UAAU;AAAA,IACrC;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,qBAAqB,SAAwB;AACzC,QAAI,KAAK,UAAU;AACf,WAAK,SAAS,MAAM,UAAU,UAAU,MAAM;AAAA,IAGlD;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,OAAa;AACT,QAAI,KAAK,UAAU;AACf,WAAK,SAAS,MAAM,UAAU;AAAA,IAClC;AACA,QAAI,KAAK,OAAO,aAAa,KAAK,aAAa;AAC3C,WAAK,YAAY,MAAM,UAAU;AAAA,IACrC;AAEA,QAAI,KAAK,sBAAsB;AAC3B,WAAK,qBAAqB,YAAY;AAAA,IAC1C;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,cAAc,SAAiB,SAAuB;AAClD,QAAI,CAAC,KAAK,OAAO,gBAAgB,CAAC,KAAK,SAAU;AAEjD,UAAM,YAAY,KAAK,OAAO;AAC9B,UAAM,eAAe;AACrB,QAAI;AACJ,QAAI;AAEJ,YAAQ,KAAK,OAAO,eAAA;AAAA,MAChB,KAAK;AACD,kBAAU,UAAW,YAAY;AACjC,iBAAS,UAAU,YAAY;AAC/B;AAAA,MACJ,KAAK;AACD,kBAAU,UAAW,YAAY;AACjC,iBAAS,UAAU;AACnB;AAAA,MACJ,KAAK;AACD,kBAAU,UAAU,YAAY;AAChC,iBAAS,UAAW,YAAY;AAChC;AAAA,MACJ,KAAK;AACD,kBAAU,UAAU;AACpB,iBAAS,UAAW,YAAY;AAChC;AAAA,MACJ,KAAK;AACD,kBAAU,UAAU,YAAY;AAChC,iBAAS,UAAU,YAAY;AAC/B;AAAA,MACJ,KAAK;AACD,kBAAU,UAAU;AACpB,iBAAS,UAAU,YAAY;AAC/B;AAAA,MACJ,KAAK;AACD,kBAAU,UAAU,YAAY;AAChC,iBAAS,UAAU;AACnB;AAAA,MACJ,KAAK;AACD,kBAAU,UAAU;AACpB,iBAAS,UAAU;AACnB;AAAA,MACJ;AACI,kBAAU,UAAW,YAAY;AACjC,iBAAS,UAAU;AACnB;AAAA,IAAA;AAGR,SAAK,SAAS,MAAM,OAAO,GAAG,OAAO;AACrC,SAAK,SAAS,MAAM,MAAM,GAAG,MAAM;AAEnC,SAAK,oBAAoB,SAAS,OAAO;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,oBAAoB,SAAiB,SAAuB;AACxD,QAAI,CAAC,KAAK,SAAU;AAEpB,UAAM,YAAY,KAAK,SAAS,sBAAA;AAGhC,QAAI,UAAU,QAAQ,OAAO,YAAY;AACrC,WAAK,SAAS,MAAM,OAAO,GAAG,UAAU,UAAU,QAAQ,eAAe;AAAA,IAC7E;AAGA,UAAM,kBAAkB,KAAK,SAAS,sBAAA;AACtC,QAAI,gBAAgB,OAAO,GAAG;AAC1B,WAAK,SAAS,MAAM,OAAO;AAAA,IAC/B;AAGA,QAAI,UAAU,SAAS,OAAO,aAAa;AACvC,WAAK,SAAS,MAAM,MAAM,GAAG,UAAU,UAAU,SAAS,eAAe;AAAA,IAC7E;AAGA,UAAM,iBAAiB,KAAK,SAAS,sBAAA;AACrC,QAAI,eAAe,MAAM,GAAG;AACxB,WAAK,SAAS,MAAM,MAAM;AAAA,IAC9B;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,cAAoB;AAChB,QAAI,KAAK,UAAU;AACf,WAAK,SAAS,MAAM,QAAQ,GAAG,KAAK,OAAO,SAAS;AACpD,WAAK,SAAS,MAAM,SAAS,GAAG,KAAK,OAAO,SAAS;AACrD,WAAK,SAAS,MAAM,SAAS,KAAK,OAAO,gBACnC,GAAG,KAAK,OAAO,WAAW,YAAY,KAAK,OAAO,WAAW,KAC7D;AAGN,WAAK,SAAS,MAAM,WAAW;AAC/B,WAAK,SAAS,MAAM,eAAe;AAEnC,cAAQ,KAAK,OAAO,YAAA;AAAA,QAChB,KAAK;AACD,eAAK,SAAS,MAAM,eAAe;AACnC;AAAA,QACJ,KAAK;AAED;AAAA,QACJ,KAAK;AACD,eAAK,SAAS,MAAM,eAAe;AACnC;AAAA,QACJ;AACI,eAAK,SAAS,MAAM,eAAe;AACnC;AAAA,MAAA;AAAA,IAEZ;AAEA,QAAI,KAAK,aAAa;AAClB,WAAK,YAAY,QAAQ,KAAK,OAAO;AACrC,WAAK,YAAY,SAAS,KAAK,OAAO;AAAA,IAC1C;AAGA,QAAI,KAAK,OAAO,WAAW;AACvB,UAAI,CAAC,KAAK,YAAa,MAAK,kBAAA;AAC5B,UAAI,KAAK,MAAM,UAAU,KAAK,YAAa,MAAK,YAAY,MAAM,UAAU;AAAA,IAChF,OAAO;AACH,UAAI,KAAK,YAAa,MAAK,YAAY,MAAM,UAAU;AAAA,IAC3D;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,UAAgB;AACZ,QAAI,KAAK,SAAU,MAAK,SAAS,OAAA;AACjC,QAAI,KAAK,YAAa,MAAK,YAAY,OAAA;AAEvC,UAAM,MAAM,SAAS,cAAc,qBAAqB;AACxD,QAAI,SAAS,OAAA;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,mBAAyB;AAErB,UAAM,WAAW,KAAK,IAAA,IAAQ;AAG9B,UAAM,mBAAmB,MAAM;AAC3B,YAAM,UAAU,MAAM,KAAK,SAAS,iBAAiB,QAAQ,CAAC;AAI9D,UAAI,YAAY,QAAQ,KAAK,CAAA,MAAK;AAC9B,cAAM,SAAS,EAAE,SAAS,IAAI,YAAA;AAC9B,cAAM,QAAQ,EAAE,aAAa,YAAY,KAAK,IAAI,YAAA;AAClD,cAAM,QAAQ,EAAE,eAAe,IAAI,YAAA;AACnC,eAAQ,MAAM,SAAS,KAAK,KAAK,CAAC,MAAM,SAAS,MAAM,KAClD,KAAK,SAAS,KAAK,KAAK,CAAC,KAAK,SAAS,MAAM,KAC7C,KAAK,SAAS,KAAK;AAAA,MAC5B,CAAC;AAGD,UAAI,CAAC,WAAW;AACZ,oBAAY,QAAQ,KAAK,CAAA,MAAK;AAC1B,gBAAM,SAAS,EAAE,SAAS,IAAI,YAAA;AAC9B,gBAAM,QAAQ,EAAE,aAAa,YAAY,KAAK,IAAI,YAAA;AAClD,iBAAO,MAAM,SAAS,MAAM,KAAK,KAAK,SAAS,MAAM;AAAA,QACzD,CAAC;AAAA,MACL;AAEA,UAAI,aAAa,UAAU,eAAe;AAEtC,YAAI,UAAU,cAAc,cAAc,qBAAqB,EAAG,QAAO;AAEzE,eAAO,MAAM,sBAAsB,UAAU,SAAS,UAAU,aAAa,YAAY,KAAK,gBAAgB;AAE9G,cAAM,MAAM,SAAS,cAAc,QAAQ;AAE3C,YAAI,YAAY,UAAU,YAAY;AAGtC,cAAM,WAAW,OAAO,iBAAiB,SAAS;AAClD,YAAI,MAAM,SAAS,SAAS;AAC5B,YAAI,MAAM,YAAY,SAAS;AAG/B,YAAI,QAAQ;AACZ,YAAI,YAAY,MAAM;AAGtB,YAAI,MAAM,UAAU;AACpB,YAAI,MAAM,aAAa;AACvB,YAAI,MAAM,iBAAiB;AAC3B,YAAI,MAAM,UAAU;AACpB,YAAI,MAAM,SAAS;AACnB,YAAI,MAAM,SAAS,UAAU,MAAM,UAAU,SAAS;AACtD,YAAI,MAAM,eAAe,UAAU,MAAM,gBAAgB,SAAS;AAElE,YAAI,CAAC,IAAI,MAAM,WAAY,KAAI,MAAM,aAAa,SAAS;AAC3D,YAAI,CAAC,IAAI,MAAM,MAAO,KAAI,MAAM,QAAQ,SAAS;AAGjD,YAAI,iBAAiB,SAAS,MAAM;AAChC,cAAI,KAAK,UAAU;AACf,iBAAK,SAAA;AAEL,gBAAI,UAAU,OAAO,QAAQ;AAC7B,gBAAI,UAAU,OAAO,aAAa;AAClC,gBAAI,UAAU,OAAO,UAAU;AAAA,UACnC;AAAA,QACJ,CAAC;AAGD,cAAM,aAAa,UAAU,SAAS,IAAI,YAAA,EAAc,SAAS,KAAK,MACjE,UAAU,aAAa,YAAY,KAAK,IAAI,YAAA,EAAc,SAAS,KAAK;AAE7E,YAAI,WAAW;AAEX,cAAI,UAAU,aAAa;AACvB,sBAAU,cAAc,aAAa,KAAK,UAAU,WAAW;AAAA,UACnE,OAAO;AACH,sBAAU,cAAc,YAAY,GAAG;AAAA,UAC3C;AAAA,QACJ,OAAO;AAEH,oBAAU,cAAc,aAAa,KAAK,SAAS;AAAA,QACvD;AAEA,eAAO,MAAM,0CAA0C;AACvD,eAAO;AAAA,MACX;AACA,aAAO;AAAA,IACX;AAGA,QAAI,mBAAoB;AAGxB,UAAM,eAAe,YAAY,MAAM;AACnC,UAAI,KAAK,IAAA,IAAQ,UAAU;AACvB,gBAAQ;AAAA,UAAK;AAAA,UACT,MAAM,KAAK,SAAS,iBAAiB,QAAQ,CAAC,EAAE,IAAI,CAAA,MAAK,EAAE,SAAS,EAAE,aAAa,YAAY,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,GAAG,CAAC;AAAA,QAAA;AAChJ,sBAAc,YAAY;AAC1B;AAAA,MACJ;AAEA,UAAI,oBAAoB;AACpB,sBAAc,YAAY;AAAA,MAC9B;AAAA,IACJ,GAAG,GAAG;AAAA,EACV;AACJ;"} \ No newline at end of file diff --git a/web/magnify-glass/WebGLRenderer.js b/web/magnify-glass/WebGLRenderer.js new file mode 100644 index 0000000..0155c89 --- /dev/null +++ b/web/magnify-glass/WebGLRenderer.js @@ -0,0 +1,329 @@ +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +import { Logger } from "../shared/logger.js"; +class WebGLRenderer { + constructor(config, state, ui) { + __publicField(this, "config"); + __publicField(this, "state"); + __publicField(this, "ui"); + __publicField(this, "gl"); + __publicField(this, "program"); + __publicField(this, "texture"); + __publicField(this, "positionBuffer"); + __publicField(this, "texCoordBuffer"); + __publicField(this, "uniformLocations"); + __publicField(this, "attributeLocations"); + __publicField(this, "currentFilteringMode"); + // Dirty flag optimization + __publicField(this, "lastCanvasVersion"); + __publicField(this, "lastSourceX"); + __publicField(this, "lastSourceY"); + __publicField(this, "lastSourceWidth"); + __publicField(this, "lastSourceHeight"); + __publicField(this, "textureUploadSkipCount"); + __publicField(this, "vertexShaderSource"); + __publicField(this, "fragmentShaderSource"); + this.config = config; + this.state = state; + this.ui = ui; + this.gl = null; + this.program = null; + this.texture = null; + this.positionBuffer = null; + this.texCoordBuffer = null; + this.uniformLocations = null; + this.attributeLocations = null; + this.currentFilteringMode = null; + this.lastCanvasVersion = -1; + this.lastSourceX = -1; + this.lastSourceY = -1; + this.lastSourceWidth = -1; + this.lastSourceHeight = -1; + this.textureUploadSkipCount = 0; + this.vertexShaderSource = ` + attribute vec2 a_position; + attribute vec2 a_texCoord; + varying vec2 v_texCoord; + void main() { + gl_Position = vec4(a_position, 0.0, 1.0); + v_texCoord = a_texCoord; + } + `; + this.fragmentShaderSource = ` + precision mediump float; + varying vec2 v_texCoord; + uniform sampler2D u_sourceTexture; + uniform vec2 u_textureOffset; + uniform vec2 u_textureRepeat; + uniform float u_glassSize; + + void main() { + vec2 sampleCoord = u_textureOffset + v_texCoord * u_textureRepeat; + vec4 color = texture2D(u_sourceTexture, sampleCoord); + gl_FragColor = color; + } + `; + this.initialize(); + } + /** + * Initialize WebGL context and resources. + */ + initialize() { + if (!this.ui.glassCanvas) return; + this.gl = this.ui.glassCanvas.getContext("webgl", { preserveDrawingBuffer: true }); + if (!this.gl) { + this.gl = this.ui.glassCanvas.getContext("webgl2", { preserveDrawingBuffer: true }); + } + if (!this.gl) { + const isFirefox = navigator.userAgent.toLowerCase().includes("firefox"); + const browserInstructions = isFirefox ? "Firefox: Go to about:config, search for 'webgl.disabled' and set it to 'false', then restart Firefox." : "Check your browser settings to ensure WebGL is enabled."; + console.error("ComfyUI Magnifying Glass ERROR: WebGL not supported or context creation failed."); + console.error(`To fix: ${browserInstructions}`); + this.showWebGLError(browserInstructions); + return; + } + this.program = this.createShaderProgram(this.gl, this.vertexShaderSource, this.fragmentShaderSource); + if (!this.program) return; + this.positionBuffer = this.gl.createBuffer(); + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer); + const positions = [-1, -1, 1, -1, -1, 1, 1, 1]; + this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(positions), this.gl.STATIC_DRAW); + this.texCoordBuffer = this.gl.createBuffer(); + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer); + const texCoords = [0, 1, 1, 1, 0, 0, 1, 0]; + this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(texCoords), this.gl.STATIC_DRAW); + this.texture = this.gl.createTexture(); + this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture); + this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL, false); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR); + this.updateTextureFiltering(this.config.textureFiltering); + this.uniformLocations = { + sourceTexture: this.gl.getUniformLocation(this.program, "u_sourceTexture"), + textureOffset: this.gl.getUniformLocation(this.program, "u_textureOffset"), + textureRepeat: this.gl.getUniformLocation(this.program, "u_textureRepeat"), + glassSize: this.gl.getUniformLocation(this.program, "u_glassSize") + }; + this.attributeLocations = { + position: this.gl.getAttribLocation(this.program, "a_position"), + texCoord: this.gl.getAttribLocation(this.program, "a_texCoord") + }; + } + /** + * Update texture filtering mode. + * @param filteringModeString - "Linear" or "Nearest" + */ + updateTextureFiltering(filteringModeString) { + if (!this.gl || !this.texture) return; + let glFilterMode; + if (filteringModeString === "Nearest") { + glFilterMode = this.gl.NEAREST; + } else { + glFilterMode = this.gl.LINEAR; + } + if (this.currentFilteringMode === glFilterMode) return; + this.currentFilteringMode = glFilterMode; + this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, glFilterMode); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, glFilterMode); + this.gl.bindTexture(this.gl.TEXTURE_2D, null); + } + /** + * Check if renderer is valid and ready. + * @returns Boolean indicating if WebGL context and program are ready + */ + isValid() { + return this.gl !== null && this.program !== null; + } + /** + * Create a WebGL shader program. + * @param gl - WebGL context + * @param vsSource - Vertex shader source + * @param fsSource - Fragment shader source + * @returns WebGL program or null on failure + */ + createShaderProgram(gl, vsSource, fsSource) { + const vertexShader = gl.createShader(gl.VERTEX_SHADER); + if (!vertexShader) return null; + gl.shaderSource(vertexShader, vsSource); + gl.compileShader(vertexShader); + if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { + console.error("ComfyUI Magnifying Glass ERROR: Vertex shader compilation error:", gl.getShaderInfoLog(vertexShader)); + gl.deleteShader(vertexShader); + return null; + } + const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); + if (!fragmentShader) { + gl.deleteShader(vertexShader); + return null; + } + gl.shaderSource(fragmentShader, fsSource); + gl.compileShader(fragmentShader); + if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { + console.error("ComfyUI Magnifying Glass ERROR: Fragment shader compilation error:", gl.getShaderInfoLog(fragmentShader)); + gl.deleteShader(vertexShader); + gl.deleteShader(fragmentShader); + return null; + } + const program = gl.createProgram(); + if (!program) { + gl.deleteShader(vertexShader); + gl.deleteShader(fragmentShader); + return null; + } + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + console.error("ComfyUI Magnifying Glass ERROR: Shader program linking error:", gl.getProgramInfoLog(program)); + gl.deleteProgram(program); + gl.deleteShader(vertexShader); + gl.deleteShader(fragmentShader); + return null; + } + return program; + } + /** + * Render the magnified view. + * @param sourceCanvas - The source canvas to sample from + */ + render(sourceCanvas) { + var _a; + if (!this.gl || !this.program || !this.texture || !this.uniformLocations || !this.attributeLocations) return; + const app = window.app; + const currentCanvasVersion = ((_a = app == null ? void 0 : app.graph) == null ? void 0 : _a.change_counter) ?? -1; + const sourceChanged = this.lastSourceX !== this.state.sourceX || this.lastSourceY !== this.state.sourceY || this.lastSourceWidth !== this.state.sourceWidth || this.lastSourceHeight !== this.state.sourceHeight; + const canvasChanged = currentCanvasVersion !== this.lastCanvasVersion; + const needsTextureUpdate = canvasChanged || sourceChanged || this.lastCanvasVersion === -1; + this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture); + if (needsTextureUpdate) { + try { + this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, sourceCanvas); + this.lastCanvasVersion = currentCanvasVersion; + this.lastSourceX = this.state.sourceX; + this.lastSourceY = this.state.sourceY; + this.lastSourceWidth = this.state.sourceWidth; + this.lastSourceHeight = this.state.sourceHeight; + if (this.config.debugMode && this.textureUploadSkipCount > 0) { + Logger.debug(`Texture uploaded after skipping ${this.textureUploadSkipCount} frames`); + } + this.textureUploadSkipCount = 0; + } catch (e) { + console.error("ComfyUI Magnifying Glass ERROR: Error in texImage2D:", e); + return; + } + } else { + this.textureUploadSkipCount++; + if (this.config.debugMode && this.textureUploadSkipCount % 60 === 0) { + Logger.debug(`Texture upload skipped (${this.textureUploadSkipCount} frames cached)`); + } + } + const uvX = this.state.sourceX / sourceCanvas.width; + const uvY = this.state.sourceY / sourceCanvas.height; + const uvWidth = this.state.sourceWidth / sourceCanvas.width; + const uvHeight = this.state.sourceHeight / sourceCanvas.height; + this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height); + this.gl.clearColor(0, 0, 0, 0); + this.gl.clear(this.gl.COLOR_BUFFER_BIT); + this.gl.useProgram(this.program); + this.gl.activeTexture(this.gl.TEXTURE0); + this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture); + this.gl.uniform1i(this.uniformLocations.sourceTexture, 0); + this.gl.uniform2f(this.uniformLocations.textureOffset, uvX, uvY); + this.gl.uniform2f(this.uniformLocations.textureRepeat, uvWidth, uvHeight); + this.gl.uniform1f(this.uniformLocations.glassSize, this.config.glassSize); + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer); + this.gl.vertexAttribPointer(this.attributeLocations.position, 2, this.gl.FLOAT, false, 0, 0); + this.gl.enableVertexAttribArray(this.attributeLocations.position); + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer); + this.gl.vertexAttribPointer(this.attributeLocations.texCoord, 2, this.gl.FLOAT, false, 0, 0); + this.gl.enableVertexAttribArray(this.attributeLocations.texCoord); + this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4); + } + /** + * Check for WebGL errors. + * @param label - Debug label + * @returns True if error occurred + */ + checkWebGLError(label) { + if (!this.gl) return false; + const err = this.gl.getError(); + if (err !== this.gl.NO_ERROR) { + console.error(`ComfyUI Magnifying Glass ERROR: WebGL Error (${label}):`, err); + return true; + } + return false; + } + /** + * Update viewport when glass size changes. + */ + updateViewport() { + if (this.gl) { + this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height); + } + } + /** + * Show WebGL error notification to user. + * @param instructions - Browser-specific instructions to fix + */ + showWebGLError(instructions) { + const toast = document.createElement("div"); + toast.id = "magnifyglass-webgl-error"; + toast.style.cssText = ` + position: fixed; + bottom: 20px; + right: 20px; + max-width: 400px; + padding: 16px 20px; + background: linear-gradient(135deg, #ff4444 0%, #cc3333 100%); + color: white; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); + font-family: system-ui, -apple-system, sans-serif; + font-size: 14px; + line-height: 1.5; + z-index: 100000; + animation: slideIn 0.3s ease-out; + `; + toast.innerHTML = ` +
+ ⚠️ +
+ MagnifyGlass: WebGL Disabled + ${instructions} + +
+
+ `; + if (!document.getElementById("magnifyglass-toast-style")) { + const style = document.createElement("style"); + style.id = "magnifyglass-toast-style"; + style.textContent = ` + @keyframes slideIn { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } + } + `; + document.head.appendChild(style); + } + document.body.appendChild(toast); + setTimeout(() => { + if (toast.parentElement) { + toast.style.animation = "slideIn 0.3s ease-out reverse"; + setTimeout(() => toast.remove(), 300); + } + }, 15e3); + } +} +export { + WebGLRenderer +}; +//# sourceMappingURL=WebGLRenderer.js.map diff --git a/web/magnify-glass/WebGLRenderer.js.map b/web/magnify-glass/WebGLRenderer.js.map new file mode 100644 index 0000000..9180722 --- /dev/null +++ b/web/magnify-glass/WebGLRenderer.js.map @@ -0,0 +1 @@ +{"version":3,"file":"WebGLRenderer.js","sources":["../../src/magnify-glass/WebGLRenderer.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - WebGLRenderer (TypeScript)\n * \n * Handles WebGL-based rendering of the magnified view.\n */\n\nimport type { ConfigManager } from './ConfigManager';\nimport type { MagnifierState } from './MagnifierState';\nimport type { UiManager } from './UiManager';\nimport { Logger } from '../shared/logger';\n\ninterface UniformLocations {\n sourceTexture: WebGLUniformLocation | null;\n textureOffset: WebGLUniformLocation | null;\n textureRepeat: WebGLUniformLocation | null;\n glassSize: WebGLUniformLocation | null;\n}\n\ninterface AttributeLocations {\n position: number;\n texCoord: number;\n}\n\n/**\n * WebGL Renderer class.\n * Provides efficient WebGL-based magnification rendering.\n */\nexport class WebGLRenderer {\n config: ConfigManager;\n state: MagnifierState;\n ui: UiManager;\n\n gl: WebGLRenderingContext | null;\n program: WebGLProgram | null;\n texture: WebGLTexture | null;\n positionBuffer: WebGLBuffer | null;\n texCoordBuffer: WebGLBuffer | null;\n uniformLocations: UniformLocations | null;\n attributeLocations: AttributeLocations | null;\n currentFilteringMode: number | null;\n\n // Dirty flag optimization\n lastCanvasVersion: number;\n lastSourceX: number;\n lastSourceY: number;\n lastSourceWidth: number;\n lastSourceHeight: number;\n textureUploadSkipCount: number;\n\n vertexShaderSource: string;\n fragmentShaderSource: string;\n\n constructor(config: ConfigManager, state: MagnifierState, ui: UiManager) {\n this.config = config;\n this.state = state;\n this.ui = ui;\n\n this.gl = null;\n this.program = null;\n this.texture = null;\n this.positionBuffer = null;\n this.texCoordBuffer = null;\n this.uniformLocations = null;\n this.attributeLocations = null;\n this.currentFilteringMode = null;\n\n // Initialize dirty flag tracking\n this.lastCanvasVersion = -1;\n this.lastSourceX = -1;\n this.lastSourceY = -1;\n this.lastSourceWidth = -1;\n this.lastSourceHeight = -1;\n this.textureUploadSkipCount = 0;\n\n this.vertexShaderSource = `\n attribute vec2 a_position;\n attribute vec2 a_texCoord;\n varying vec2 v_texCoord;\n void main() {\n gl_Position = vec4(a_position, 0.0, 1.0);\n v_texCoord = a_texCoord;\n }\n `;\n\n this.fragmentShaderSource = `\n precision mediump float;\n varying vec2 v_texCoord;\n uniform sampler2D u_sourceTexture;\n uniform vec2 u_textureOffset;\n uniform vec2 u_textureRepeat;\n uniform float u_glassSize;\n\n void main() {\n vec2 sampleCoord = u_textureOffset + v_texCoord * u_textureRepeat;\n vec4 color = texture2D(u_sourceTexture, sampleCoord);\n gl_FragColor = color;\n }\n `;\n\n this.initialize();\n }\n\n /**\n * Initialize WebGL context and resources.\n */\n initialize(): void {\n // Get WebGL context\n if (!this.ui.glassCanvas) return;\n\n this.gl = this.ui.glassCanvas.getContext(\"webgl\", { preserveDrawingBuffer: true });\n if (!this.gl) {\n // Try webgl2 as fallback\n this.gl = this.ui.glassCanvas.getContext(\"webgl2\", { preserveDrawingBuffer: true }) as WebGLRenderingContext | null;\n }\n\n if (!this.gl) {\n const isFirefox = navigator.userAgent.toLowerCase().includes('firefox');\n const browserInstructions = isFirefox\n ? \"Firefox: Go to about:config, search for 'webgl.disabled' and set it to 'false', then restart Firefox.\"\n : \"Check your browser settings to ensure WebGL is enabled.\";\n\n console.error(\"ComfyUI Magnifying Glass ERROR: WebGL not supported or context creation failed.\");\n console.error(`To fix: ${browserInstructions}`);\n\n // Show user notification\n this.showWebGLError(browserInstructions);\n return;\n }\n\n // Create shader program\n this.program = this.createShaderProgram(this.gl, this.vertexShaderSource, this.fragmentShaderSource);\n if (!this.program) return;\n\n // Create position buffer\n this.positionBuffer = this.gl.createBuffer();\n this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);\n const positions = [-1, -1, 1, -1, -1, 1, 1, 1];\n this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(positions), this.gl.STATIC_DRAW);\n\n // Create texture coordinate buffer\n this.texCoordBuffer = this.gl.createBuffer();\n this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer);\n const texCoords = [0, 1, 1, 1, 0, 0, 1, 0]; // Flipped Y for texCoords\n this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(texCoords), this.gl.STATIC_DRAW);\n\n // Create texture\n this.texture = this.gl.createTexture();\n this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);\n this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL, false);\n this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);\n this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);\n this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);\n this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);\n this.updateTextureFiltering(this.config.textureFiltering);\n\n // Get uniform and attribute locations\n this.uniformLocations = {\n sourceTexture: this.gl.getUniformLocation(this.program, \"u_sourceTexture\"),\n textureOffset: this.gl.getUniformLocation(this.program, \"u_textureOffset\"),\n textureRepeat: this.gl.getUniformLocation(this.program, \"u_textureRepeat\"),\n glassSize: this.gl.getUniformLocation(this.program, \"u_glassSize\"),\n };\n\n this.attributeLocations = {\n position: this.gl.getAttribLocation(this.program, \"a_position\"),\n texCoord: this.gl.getAttribLocation(this.program, \"a_texCoord\"),\n };\n }\n\n /**\n * Update texture filtering mode.\n * @param filteringModeString - \"Linear\" or \"Nearest\"\n */\n updateTextureFiltering(filteringModeString: string): void {\n if (!this.gl || !this.texture) return;\n\n let glFilterMode: number;\n if (filteringModeString === \"Nearest\") {\n glFilterMode = this.gl.NEAREST;\n } else {\n glFilterMode = this.gl.LINEAR;\n }\n\n if (this.currentFilteringMode === glFilterMode) return;\n\n this.currentFilteringMode = glFilterMode;\n\n this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);\n this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, glFilterMode);\n this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, glFilterMode);\n this.gl.bindTexture(this.gl.TEXTURE_2D, null);\n }\n\n /**\n * Check if renderer is valid and ready.\n * @returns Boolean indicating if WebGL context and program are ready\n */\n isValid(): boolean {\n return this.gl !== null && this.program !== null;\n }\n\n /**\n * Create a WebGL shader program.\n * @param gl - WebGL context\n * @param vsSource - Vertex shader source\n * @param fsSource - Fragment shader source\n * @returns WebGL program or null on failure\n */\n createShaderProgram(gl: WebGLRenderingContext, vsSource: string, fsSource: string): WebGLProgram | null {\n const vertexShader = gl.createShader(gl.VERTEX_SHADER);\n if (!vertexShader) return null;\n\n gl.shaderSource(vertexShader, vsSource);\n gl.compileShader(vertexShader);\n if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {\n console.error('ComfyUI Magnifying Glass ERROR: Vertex shader compilation error:', gl.getShaderInfoLog(vertexShader));\n gl.deleteShader(vertexShader);\n return null;\n }\n\n const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);\n if (!fragmentShader) {\n gl.deleteShader(vertexShader);\n return null;\n }\n\n gl.shaderSource(fragmentShader, fsSource);\n gl.compileShader(fragmentShader);\n if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {\n console.error('ComfyUI Magnifying Glass ERROR: Fragment shader compilation error:', gl.getShaderInfoLog(fragmentShader));\n gl.deleteShader(vertexShader);\n gl.deleteShader(fragmentShader);\n return null;\n }\n\n const program = gl.createProgram();\n if (!program) {\n gl.deleteShader(vertexShader);\n gl.deleteShader(fragmentShader);\n return null;\n }\n\n gl.attachShader(program, vertexShader);\n gl.attachShader(program, fragmentShader);\n gl.linkProgram(program);\n\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n console.error('ComfyUI Magnifying Glass ERROR: Shader program linking error:', gl.getProgramInfoLog(program));\n gl.deleteProgram(program);\n gl.deleteShader(vertexShader);\n gl.deleteShader(fragmentShader);\n return null;\n }\n\n return program;\n }\n\n /**\n * Render the magnified view.\n * @param sourceCanvas - The source canvas to sample from\n */\n render(sourceCanvas: HTMLCanvasElement): void {\n if (!this.gl || !this.program || !this.texture || !this.uniformLocations || !this.attributeLocations) return;\n\n // Check if we need to update the texture\n // We track: canvas version (via LiteGraph's change counter), source coordinates, and source dimensions\n const app = (window as any).app;\n const currentCanvasVersion = app?.graph?.change_counter ?? -1;\n\n const sourceChanged =\n this.lastSourceX !== this.state.sourceX ||\n this.lastSourceY !== this.state.sourceY ||\n this.lastSourceWidth !== this.state.sourceWidth ||\n this.lastSourceHeight !== this.state.sourceHeight;\n\n const canvasChanged = currentCanvasVersion !== this.lastCanvasVersion;\n\n // Only upload texture if something changed\n const needsTextureUpdate = canvasChanged || sourceChanged || this.lastCanvasVersion === -1;\n\n // Bind texture\n this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);\n\n if (needsTextureUpdate) {\n try {\n this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, sourceCanvas);\n\n // Update tracking variables\n this.lastCanvasVersion = currentCanvasVersion;\n this.lastSourceX = this.state.sourceX;\n this.lastSourceY = this.state.sourceY;\n this.lastSourceWidth = this.state.sourceWidth;\n this.lastSourceHeight = this.state.sourceHeight;\n\n // Reset skip counter and log if debug mode\n if (this.config.debugMode && this.textureUploadSkipCount > 0) {\n Logger.debug(`Texture uploaded after skipping ${this.textureUploadSkipCount} frames`);\n }\n this.textureUploadSkipCount = 0;\n } catch (e) {\n console.error(\"ComfyUI Magnifying Glass ERROR: Error in texImage2D:\", e);\n return;\n }\n } else {\n // Skip texture upload - just increment counter\n this.textureUploadSkipCount++;\n\n // Periodic debug logging\n if (this.config.debugMode && this.textureUploadSkipCount % 60 === 0) {\n Logger.debug(`Texture upload skipped (${this.textureUploadSkipCount} frames cached)`);\n }\n }\n\n // Calculate normalized texture coordinates (UV space: 0-1)\n const uvX = this.state.sourceX / sourceCanvas.width;\n const uvY = this.state.sourceY / sourceCanvas.height;\n const uvWidth = this.state.sourceWidth / sourceCanvas.width;\n const uvHeight = this.state.sourceHeight / sourceCanvas.height;\n\n // Set up rendering\n this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);\n this.gl.clearColor(0.0, 0.0, 0.0, 0.0);\n this.gl.clear(this.gl.COLOR_BUFFER_BIT);\n\n this.gl.useProgram(this.program);\n\n // Set uniforms\n this.gl.activeTexture(this.gl.TEXTURE0);\n this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);\n this.gl.uniform1i(this.uniformLocations.sourceTexture, 0);\n this.gl.uniform2f(this.uniformLocations.textureOffset, uvX, uvY);\n this.gl.uniform2f(this.uniformLocations.textureRepeat, uvWidth, uvHeight);\n this.gl.uniform1f(this.uniformLocations.glassSize, this.config.glassSize);\n\n // Set attributes\n this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);\n this.gl.vertexAttribPointer(this.attributeLocations.position, 2, this.gl.FLOAT, false, 0, 0);\n this.gl.enableVertexAttribArray(this.attributeLocations.position);\n\n this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer);\n this.gl.vertexAttribPointer(this.attributeLocations.texCoord, 2, this.gl.FLOAT, false, 0, 0);\n this.gl.enableVertexAttribArray(this.attributeLocations.texCoord);\n\n // Draw\n this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);\n }\n\n /**\n * Check for WebGL errors.\n * @param label - Debug label\n * @returns True if error occurred\n */\n checkWebGLError(label: string): boolean {\n if (!this.gl) return false;\n\n const err = this.gl.getError();\n if (err !== this.gl.NO_ERROR) {\n console.error(`ComfyUI Magnifying Glass ERROR: WebGL Error (${label}):`, err);\n return true;\n }\n return false;\n }\n\n /**\n * Update viewport when glass size changes.\n */\n updateViewport(): void {\n if (this.gl) {\n this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);\n }\n }\n\n /**\n * Show WebGL error notification to user.\n * @param instructions - Browser-specific instructions to fix\n */\n showWebGLError(instructions: string): void {\n // Create a toast notification\n const toast = document.createElement('div');\n toast.id = 'magnifyglass-webgl-error';\n toast.style.cssText = `\n position: fixed;\n bottom: 20px;\n right: 20px;\n max-width: 400px;\n padding: 16px 20px;\n background: linear-gradient(135deg, #ff4444 0%, #cc3333 100%);\n color: white;\n border-radius: 8px;\n box-shadow: 0 4px 20px rgba(0,0,0,0.3);\n font-family: system-ui, -apple-system, sans-serif;\n font-size: 14px;\n line-height: 1.5;\n z-index: 100000;\n animation: slideIn 0.3s ease-out;\n `;\n\n toast.innerHTML = `\n
\n ⚠️\n
\n MagnifyGlass: WebGL Disabled\n ${instructions}\n \n
\n
\n `;\n\n // Add animation keyframes\n if (!document.getElementById('magnifyglass-toast-style')) {\n const style = document.createElement('style');\n style.id = 'magnifyglass-toast-style';\n style.textContent = `\n @keyframes slideIn {\n from { transform: translateX(100%); opacity: 0; }\n to { transform: translateX(0); opacity: 1; }\n }\n `;\n document.head.appendChild(style);\n }\n\n document.body.appendChild(toast);\n\n // Auto-dismiss after 15 seconds\n setTimeout(() => {\n if (toast.parentElement) {\n toast.style.animation = 'slideIn 0.3s ease-out reverse';\n setTimeout(() => toast.remove(), 300);\n }\n }, 15000);\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/magnify_glass.js b/web/magnify_glass.js new file mode 100644 index 0000000..5ec9498 --- /dev/null +++ b/web/magnify_glass.js @@ -0,0 +1,15 @@ +import { MagnifyGlass } from "./magnify-glass/MagnifyGlass.js"; +import { app } from "/scripts/app.js"; +import { Logger } from "./shared/logger.js"; +import { initSidebar } from "./sidebar/sidebar.js"; +app.registerExtension({ + name: "comfyui.magnify.glass", + init() { + const magnifyGlass = new MagnifyGlass(); + magnifyGlass.init(); + window.comfyUIMagnifyGlass = magnifyGlass; + initSidebar(); + Logger.info("Magnify Glass extension initialized"); + } +}); +//# sourceMappingURL=magnify_glass.js.map diff --git a/web/magnify_glass.js.map b/web/magnify_glass.js.map new file mode 100644 index 0000000..5762b35 --- /dev/null +++ b/web/magnify_glass.js.map @@ -0,0 +1 @@ +{"version":3,"file":"magnify_glass.js","sources":["../src/magnify_glass.ts"],"sourcesContent":["/**\n * ComfyUI Magnify Glass - Main Entry Point\n * \n * Initializes the Magnify Glass extension.\n */\n\nimport { MagnifyGlass } from './magnify-glass';\nimport { Logger } from './shared/logger';\nimport { initSidebar } from './sidebar';\n// @ts-ignore\nimport { app } from \"/scripts/app.js\";\n\n// Global declaration handled in comfyui.d.ts\n\n// We rely on the global 'app' object provided by ComfyUI\n// Register extension\napp.registerExtension({\n name: \"comfyui.magnify.glass\",\n init() {\n // Initialize the magnify glass\n const magnifyGlass = new MagnifyGlass();\n magnifyGlass.init();\n\n // Expose to window for other extensions (like info panel) to access\n window.comfyUIMagnifyGlass = magnifyGlass;\n\n // Initialize sidebar\n initSidebar();\n\n Logger.info('Magnify Glass extension initialized');\n }\n});\n"],"names":[],"mappings":";;;;AAgBA,IAAI,kBAAkB;AAAA,EAClB,MAAM;AAAA,EACN,OAAO;AAEH,UAAM,eAAe,IAAI,aAAA;AACzB,iBAAa,KAAA;AAGb,WAAO,sBAAsB;AAG7B,gBAAA;AAEA,WAAO,KAAK,qCAAqC;AAAA,EACrD;AACJ,CAAC;"} \ No newline at end of file diff --git a/web/magnify_info_panel.js b/web/magnify_info_panel.js new file mode 100644 index 0000000..de0e72b --- /dev/null +++ b/web/magnify_info_panel.js @@ -0,0 +1,31 @@ +import { app } from "/scripts/app.js"; +import { Logger } from "./shared/logger.js"; +import { InfoPanel } from "./info-panel/InfoPanel.js"; +app.registerExtension({ + name: "comfyui.magnify.info.panel", + init() { + const checkDependencies = () => { + if (window.comfyUIMagnifyGlass) { + initializeInfoPanel(); + } else { + setTimeout(checkDependencies, 100); + } + }; + checkDependencies(); + } +}); +function initializeInfoPanel() { + const magnifyGlass = window.comfyUIMagnifyGlass; + if (!magnifyGlass) { + Logger.error("Failed to find MagnifyGlass instance."); + return; + } + try { + const infoPanel = new InfoPanel(magnifyGlass); + window.infoPanelManager = infoPanel; + Logger.info("Info Panel extension initialized"); + } catch (e) { + Logger.error("Error during initialization:", e); + } +} +//# sourceMappingURL=magnify_info_panel.js.map diff --git a/web/magnify_info_panel.js.map b/web/magnify_info_panel.js.map new file mode 100644 index 0000000..dc1a722 --- /dev/null +++ b/web/magnify_info_panel.js.map @@ -0,0 +1 @@ +{"version":3,"file":"magnify_info_panel.js","sources":["../src/magnify_info_panel.ts"],"sourcesContent":["/**\n * ComfyUI Magnify Info Panel - Main Entry Point\n * \n * Initializes the Info Panel extension.\n */\n\nimport { InfoPanel } from './info-panel';\nimport { Logger } from './shared/logger';\n// @ts-ignore\nimport { app } from \"/scripts/app.js\";\n\n// We rely on the global 'app' object provided by ComfyUI\n// No need to import it as it's global, but TypeScript needs to know about it.\n// We handled this in types/comfyui.d.ts\n\napp.registerExtension({\n name: \"comfyui.magnify.info.panel\",\n init() {\n // Wait for magnify glass to be available\n const checkDependencies = () => {\n if (window.comfyUIMagnifyGlass) {\n initializeInfoPanel();\n } else {\n setTimeout(checkDependencies, 100);\n }\n };\n\n checkDependencies();\n }\n});\n\nfunction initializeInfoPanel() {\n const magnifyGlass = window.comfyUIMagnifyGlass;\n if (!magnifyGlass) {\n Logger.error('Failed to find MagnifyGlass instance.');\n return;\n }\n\n try {\n // Initialize the info panel\n const infoPanel = new InfoPanel(magnifyGlass);\n\n // Expose orchestrator globally if needed\n window.infoPanelManager = infoPanel as any;\n\n // CSS is loaded by UIManager.injectStyles() - no need to load separately\n\n Logger.info('Info Panel extension initialized');\n } catch (e) {\n Logger.error('Error during initialization:', e);\n }\n}\n\n"],"names":[],"mappings":";;;AAeA,IAAI,kBAAkB;AAAA,EAClB,MAAM;AAAA,EACN,OAAO;AAEH,UAAM,oBAAoB,MAAM;AAC5B,UAAI,OAAO,qBAAqB;AAC5B,4BAAA;AAAA,MACJ,OAAO;AACH,mBAAW,mBAAmB,GAAG;AAAA,MACrC;AAAA,IACJ;AAEA,sBAAA;AAAA,EACJ;AACJ,CAAC;AAED,SAAS,sBAAsB;AAC3B,QAAM,eAAe,OAAO;AAC5B,MAAI,CAAC,cAAc;AACf,WAAO,MAAM,uCAAuC;AACpD;AAAA,EACJ;AAEA,MAAI;AAEA,UAAM,YAAY,IAAI,UAAU,YAAY;AAG5C,WAAO,mBAAmB;AAI1B,WAAO,KAAK,kCAAkC;AAAA,EAClD,SAAS,GAAG;AACR,WAAO,MAAM,gCAAgC,CAAC;AAAA,EAClD;AACJ;"} \ No newline at end of file diff --git a/web/shared/constants.js b/web/shared/constants.js new file mode 100644 index 0000000..bd933d1 --- /dev/null +++ b/web/shared/constants.js @@ -0,0 +1,63 @@ +const GLASS_POSITIONS = [ + "Top", + "Bottom", + "Left", + "Right", + "Top-Left", + "Top-Right", + "Bottom-Left", + "Bottom-Right" +]; +const GLASS_SHAPES = [ + "Circle", + "Square", + "Rounded Square" +]; +const PANEL_POSITIONS = [ + "Top", + "Bottom", + "Left", + "Right" +]; +const ACTIVATION_KEYS = ["x", "z", "m", "q", "v", "c"]; +const RESET_KEYS = ["r", "o", "p", "k", "l"]; +const TOGGLE_FOLLOW_KEYS = ["f", "g", "h", "j", "k"]; +const DEFAULT_PADDING = 4; +const DEFAULT_GLASS_Y_OFFSET = 110; +const DEFAULT_GLASS_SIZE = 300; +const DEFAULT_ZOOM_FACTOR = 300; +const DEFAULT_BORDER_WIDTH = 1; +const DEFAULT_BORDER_COLOR = "#6b7280"; +const DEFAULT_OFFSET_STEP = 5; +const DEFAULT_PANEL_WIDTH = 300; +const DEFAULT_PANEL_MAX_HEIGHT = 300; +const DEFAULT_PANEL_OPACITY = 100; +const Z_INDEX = { + GLASS: 98999, + DEBUG: 99e3 +}; +const STORAGE_KEYS = { + OFFSET_X: "comfyui_magnify_offset_x", + OFFSET_Y: "comfyui_magnify_offset_y" +}; +export { + ACTIVATION_KEYS, + DEFAULT_BORDER_COLOR, + DEFAULT_BORDER_WIDTH, + DEFAULT_GLASS_SIZE, + DEFAULT_GLASS_Y_OFFSET, + DEFAULT_OFFSET_STEP, + DEFAULT_PADDING, + DEFAULT_PANEL_MAX_HEIGHT, + DEFAULT_PANEL_OPACITY, + DEFAULT_PANEL_WIDTH, + DEFAULT_ZOOM_FACTOR, + GLASS_POSITIONS, + GLASS_SHAPES, + PANEL_POSITIONS, + RESET_KEYS, + STORAGE_KEYS, + TOGGLE_FOLLOW_KEYS, + Z_INDEX +}; +//# sourceMappingURL=constants.js.map diff --git a/web/shared/constants.js.map b/web/shared/constants.js.map new file mode 100644 index 0000000..d03447c --- /dev/null +++ b/web/shared/constants.js.map @@ -0,0 +1 @@ +{"version":3,"file":"constants.js","sources":["../../src/shared/constants.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Shared Constants\n * \n * Configuration constants shared between magnify glass and info panel modules.\n */\n\n// Glass positioning options\nexport const GLASS_POSITIONS = [\n \"Top\",\n \"Bottom\",\n \"Left\",\n \"Right\",\n \"Top-Left\",\n \"Top-Right\",\n \"Bottom-Left\",\n \"Bottom-Right\"\n] as const;\n\nexport type GlassPosition = typeof GLASS_POSITIONS[number];\n\n// Glass shape options\nexport const GLASS_SHAPES = [\n \"Circle\",\n \"Square\",\n \"Rounded Square\"\n] as const;\n\nexport type GlassShape = typeof GLASS_SHAPES[number];\n\n// Texture filtering modes\nexport const TEXTURE_FILTERS = [\n \"Linear\",\n \"Nearest\"\n] as const;\n\nexport type TextureFilter = typeof TEXTURE_FILTERS[number];\n\n// Info panel positions relative to glass\nexport const PANEL_POSITIONS = [\n \"Top\",\n \"Bottom\",\n \"Left\",\n \"Right\"\n] as const;\n\nexport type PanelPosition = typeof PANEL_POSITIONS[number];\n\n// Available keys for settings\nexport const ACTIVATION_KEYS = [\"x\", \"z\", \"m\", \"q\", \"v\", \"c\"] as const;\nexport const RESET_KEYS = [\"r\", \"o\", \"p\", \"k\", \"l\"] as const;\nexport const TOGGLE_FOLLOW_KEYS = [\"f\", \"g\", \"h\", \"j\", \"k\"] as const;\n\n// UI Constants\nexport const DEFAULT_PADDING = 4;\nexport const DEFAULT_GLASS_Y_OFFSET = 110; // Starting Y position offset (below top bar with 3px gap)\nexport const DEFAULT_GLASS_SIZE = 300;\nexport const DEFAULT_ZOOM_FACTOR = 300; // 3x as percentage\nexport const DEFAULT_BORDER_WIDTH = 1;\nexport const DEFAULT_BORDER_COLOR = \"#6b7280\";\nexport const DEFAULT_OFFSET_STEP = 5;\n\n// Panel Constants\nexport const DEFAULT_PANEL_WIDTH = 300;\nexport const DEFAULT_PANEL_MAX_HEIGHT = 300;\nexport const DEFAULT_PANEL_OPACITY = 100;\n\n// Z-Index hierarchy\nexport const Z_INDEX = {\n GLASS: 98999,\n DEBUG: 99000,\n PANEL: 99999,\n CONTROLS: 99998\n} as const;\n\n// Animation durations (ms)\nexport const ANIMATION = {\n PANEL_TRANSITION: 300,\n AUTO_COLLAPSE_DELAY: 1500,\n THEME_DETECTION_INTERVAL: 2000\n} as const;\n\n// LocalStorage keys\nexport const STORAGE_KEYS = {\n OFFSET_X: 'comfyui_magnify_offset_x',\n OFFSET_Y: 'comfyui_magnify_offset_y'\n} as const;\n"],"names":[],"mappings":"AAOO,MAAM,kBAAkB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ;AAKO,MAAM,eAAe;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AACJ;AAaO,MAAM,kBAAkB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ;AAKO,MAAM,kBAAkB,CAAC,KAAK,KAAK,KAAK,KAAK,KAAK,GAAG;AACrD,MAAM,aAAa,CAAC,KAAK,KAAK,KAAK,KAAK,GAAG;AAC3C,MAAM,qBAAqB,CAAC,KAAK,KAAK,KAAK,KAAK,GAAG;AAGnD,MAAM,kBAAkB;AACxB,MAAM,yBAAyB;AAC/B,MAAM,qBAAqB;AAC3B,MAAM,sBAAsB;AAC5B,MAAM,uBAAuB;AAC7B,MAAM,uBAAuB;AAC7B,MAAM,sBAAsB;AAG5B,MAAM,sBAAsB;AAC5B,MAAM,2BAA2B;AACjC,MAAM,wBAAwB;AAG9B,MAAM,UAAU;AAAA,EACnB,OAAO;AAAA,EACP,OAAO;AAGX;AAUO,MAAM,eAAe;AAAA,EACxB,UAAU;AAAA,EACV,UAAU;AACd;"} \ No newline at end of file diff --git a/web/shared/icons.js b/web/shared/icons.js new file mode 100644 index 0000000..bc24ff8 --- /dev/null +++ b/web/shared/icons.js @@ -0,0 +1,61 @@ +const Icons = { + // Magnify glass icon - used for header and glass toggle + magnifyGlass: ` + + + `, + // Unlock icon - panel is floating/following cursor + unlock: ` + + + `, + // Lock icon - panel is pinned + lock: ` + + + `, + // Pin icon - lock panel position + pin: ` + + + `, + // Eye icon - toggle visibility + eye: ` + + + `, + // Camera/image icon - media section + camera: ` + + + `, + // Location/map pin icon - empty state + mapPin: ` + + + `, + // Chevron right - expand icon + chevronRight: ` + + `, + // Minus icon - minimize button + minus: ` + + `, + // Inspector/info icon + info: ` + + + + `, + // Box/node icon + box: ` + + + + ` +}; +export { + Icons +}; +//# sourceMappingURL=icons.js.map diff --git a/web/shared/icons.js.map b/web/shared/icons.js.map new file mode 100644 index 0000000..e7002e6 --- /dev/null +++ b/web/shared/icons.js.map @@ -0,0 +1 @@ +{"version":3,"file":"icons.js","sources":["../../src/shared/icons.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - SVG Icons\n * \n * Inline SVG icons to replace emoji icons for a more professional look.\n * All icons are designed to be 16x16 or scalable.\n */\n\nexport const Icons = {\n // Magnify glass icon - used for header and glass toggle\n magnifyGlass: `\n \n \n `,\n\n // Unlock icon - panel is floating/following cursor\n unlock: `\n \n \n `,\n\n // Lock icon - panel is pinned\n lock: `\n \n \n `,\n\n // Pin icon - lock panel position\n pin: `\n \n \n `,\n\n // Eye icon - toggle visibility\n eye: `\n \n \n `,\n\n // Camera/image icon - media section\n camera: `\n \n \n `,\n\n // Target/crosshair icon - node section\n target: `\n \n \n \n `,\n\n // Location/map pin icon - empty state\n mapPin: `\n \n \n `,\n\n // Chevron right - expand icon\n chevronRight: `\n \n `,\n\n // Minus icon - minimize button\n minus: `\n \n `,\n\n // Inspector/info icon\n info: `\n \n \n \n `,\n\n // Box/node icon\n box: `\n \n \n \n `\n};\n\n/**\n * Get an icon wrapped in a span for proper sizing\n */\nexport function getIcon(name: keyof typeof Icons, className: string = ''): string {\n const icon = Icons[name] || '';\n return `${icon}`;\n}\n"],"names":[],"mappings":"AAOO,MAAM,QAAQ;AAAA;AAAA,EAEjB,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA,EAMd,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA,EAMR,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,EAMN,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA,EAML,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA,EAML,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA,EAaR,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA,EAMR,cAAc;AAAA;AAAA;AAAA;AAAA,EAKd,OAAO;AAAA;AAAA;AAAA;AAAA,EAKP,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAON,KAAK;AAAA;AAAA;AAAA;AAAA;AAKT;"} \ No newline at end of file diff --git a/web/shared/logger.js b/web/shared/logger.js new file mode 100644 index 0000000..d087991 --- /dev/null +++ b/web/shared/logger.js @@ -0,0 +1,44 @@ +const PREFIX = "[MagnifyGlass]"; +function isDebugEnabled() { + try { + return localStorage.getItem("magnifyGlass.debug") === "true" || window.magnifyGlassDebug === true; + } catch { + return false; + } +} +const Logger = { + /** + * Debug log - only shown when debug mode is enabled. + * Use for verbose development logging. + */ + debug: (...args) => { + if (isDebugEnabled()) { + console.log(PREFIX, ...args); + } + }, + /** + * Info log - always shown. + * Use for important initialization messages. + */ + info: (...args) => { + console.log(PREFIX, ...args); + }, + /** + * Warning log - always shown. + * Use for non-critical issues. + */ + warn: (...args) => { + console.warn(PREFIX, ...args); + }, + /** + * Error log - always shown. + * Use for critical errors. + */ + error: (...args) => { + console.error(PREFIX, ...args); + } +}; +export { + Logger +}; +//# sourceMappingURL=logger.js.map diff --git a/web/shared/logger.js.map b/web/shared/logger.js.map new file mode 100644 index 0000000..8b5db00 --- /dev/null +++ b/web/shared/logger.js.map @@ -0,0 +1 @@ +{"version":3,"file":"logger.js","sources":["../../src/shared/logger.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Logger Utility\n * \n * Conditional debug logging with environment awareness.\n * Debug logs are only shown when:\n * - localStorage.getItem('magnifyGlass.debug') === 'true'\n * - OR window.magnifyGlassDebug === true\n */\n\nconst PREFIX = '[MagnifyGlass]';\n\n/**\n * Check if debug mode is enabled.\n */\nfunction isDebugEnabled(): boolean {\n try {\n return (\n localStorage.getItem('magnifyGlass.debug') === 'true' ||\n (window as unknown as { magnifyGlassDebug?: boolean }).magnifyGlassDebug === true\n );\n } catch {\n return false;\n }\n}\n\n/**\n * Logger utility with conditional debug output.\n */\nexport const Logger = {\n /**\n * Debug log - only shown when debug mode is enabled.\n * Use for verbose development logging.\n */\n debug: (...args: unknown[]): void => {\n if (isDebugEnabled()) {\n console.log(PREFIX, ...args);\n }\n },\n\n /**\n * Info log - always shown.\n * Use for important initialization messages.\n */\n info: (...args: unknown[]): void => {\n console.log(PREFIX, ...args);\n },\n\n /**\n * Warning log - always shown.\n * Use for non-critical issues.\n */\n warn: (...args: unknown[]): void => {\n console.warn(PREFIX, ...args);\n },\n\n /**\n * Error log - always shown.\n * Use for critical errors.\n */\n error: (...args: unknown[]): void => {\n console.error(PREFIX, ...args);\n }\n};\n\n/**\n * Enable debug mode programmatically.\n */\nexport function enableDebug(): void {\n try {\n localStorage.setItem('magnifyGlass.debug', 'true');\n console.log(PREFIX, 'Debug mode enabled');\n } catch {\n (window as unknown as { magnifyGlassDebug?: boolean }).magnifyGlassDebug = true;\n }\n}\n\n/**\n * Disable debug mode.\n */\nexport function disableDebug(): void {\n try {\n localStorage.removeItem('magnifyGlass.debug');\n console.log(PREFIX, 'Debug mode disabled');\n } catch {\n (window as unknown as { magnifyGlassDebug?: boolean }).magnifyGlassDebug = false;\n }\n}\n"],"names":[],"mappings":"AASA,MAAM,SAAS;AAKf,SAAS,iBAA0B;AAC/B,MAAI;AACA,WACI,aAAa,QAAQ,oBAAoB,MAAM,UAC9C,OAAsD,sBAAsB;AAAA,EAErF,QAAQ;AACJ,WAAO;AAAA,EACX;AACJ;AAKO,MAAM,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA,EAKlB,OAAO,IAAI,SAA0B;AACjC,QAAI,kBAAkB;AAClB,cAAQ,IAAI,QAAQ,GAAG,IAAI;AAAA,IAC/B;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,IAAI,SAA0B;AAChC,YAAQ,IAAI,QAAQ,GAAG,IAAI;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,IAAI,SAA0B;AAChC,YAAQ,KAAK,QAAQ,GAAG,IAAI;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,IAAI,SAA0B;AACjC,YAAQ,MAAM,QAAQ,GAAG,IAAI;AAAA,EACjC;AACJ;"} \ No newline at end of file diff --git a/web/shared/settings.js b/web/shared/settings.js new file mode 100644 index 0000000..f6ad602 --- /dev/null +++ b/web/shared/settings.js @@ -0,0 +1,459 @@ +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 diff --git a/web/shared/settings.js.map b/web/shared/settings.js.map new file mode 100644 index 0000000..dc8d86a --- /dev/null +++ b/web/shared/settings.js.map @@ -0,0 +1 @@ +{"version":3,"file":"settings.js","sources":["../../src/shared/settings.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Settings Configuration\n * \n * Centralized settings definitions and registration functions.\n */\n\n// @ts-ignore\nimport { app } from \"/scripts/app.js\";\nimport {\n DEFAULT_GLASS_SIZE,\n DEFAULT_ZOOM_FACTOR,\n DEFAULT_BORDER_WIDTH,\n DEFAULT_BORDER_COLOR,\n DEFAULT_OFFSET_STEP,\n DEFAULT_PANEL_WIDTH,\n DEFAULT_PANEL_MAX_HEIGHT,\n DEFAULT_PANEL_OPACITY,\n GLASS_POSITIONS,\n GLASS_SHAPES,\n ACTIVATION_KEYS,\n RESET_KEYS,\n TOGGLE_FOLLOW_KEYS,\n PANEL_POSITIONS\n} from './constants';\n\n// Define the shape of our settings objects for better type safety\nexport interface GlassSettings {\n \"🔍MagnifyGlass.ZoomFactor\": number;\n \"🔍MagnifyGlass.GlassSize\": number;\n \"🔍MagnifyGlass.BorderColor\": string;\n \"🔍MagnifyGlass.BorderWidth\": number;\n \"🔍MagnifyGlass.ActivationKey\": string;\n \"🔍MagnifyGlass.AltRequired\": boolean;\n \"🔍MagnifyGlass.FollowCursor\": boolean;\n \"🔍MagnifyGlass.DebugMode\": boolean;\n \"🔍MagnifyGlass.OffsetStep\": number;\n \"🔍MagnifyGlass.GlassPosition\": string;\n \"🔍MagnifyGlass.ResetKey\": string;\n \"🔍MagnifyGlass.GlassShape\": string;\n \"🔍MagnifyGlass.BorderEnabled\": boolean;\n \"🔍MagnifyGlass.TextureFiltering\": string;\n \"🔍MagnifyGlass.AlwaysActiveMode\": boolean;\n \"🔍MagnifyGlass.ToggleFollowCursorKey\": string;\n}\n\nexport interface PanelSettings {\n \"🔍MagnifyGlass.InfoPanelEnabled\": boolean;\n \"🔍MagnifyGlass.InfoPanelPosition\": string;\n \"🔍MagnifyGlass.InfoPanelWidth\": number;\n \"🔍MagnifyGlass.InfoPanelOpacity\": number;\n \"🔍MagnifyGlass.InfoPanelMaxHeight\": number;\n \"🔍MagnifyGlass.InfoPanelAnimations\": boolean;\n \"🔍MagnifyGlass.ShowInspectorTab\": boolean;\n \"🔍MagnifyGlass.ToggleHotkey\": string;\n \"🔍MagnifyGlass.GlassPreviewToggleHotkey\": string;\n \"🔍MagnifyGlass.PinPanelHotkey\": string;\n \"🔍MagnifyGlass.ShowHoveringControls\": boolean;\n \"🔍MagnifyGlass.ControlsPosition\": string;\n \"🔍MagnifyGlass.InfoPanelTextColor\": string;\n \"🔍MagnifyGlass.InfoPanelAccentColor\": string;\n [key: string]: any; // Allow indexing\n}\n\n/**\n * Default settings for the magnify glass component.\n */\nexport const DEFAULT_GLASS_SETTINGS: GlassSettings = {\n \"🔍MagnifyGlass.ZoomFactor\": DEFAULT_ZOOM_FACTOR,\n \"🔍MagnifyGlass.GlassSize\": DEFAULT_GLASS_SIZE,\n \"🔍MagnifyGlass.BorderColor\": DEFAULT_BORDER_COLOR,\n \"🔍MagnifyGlass.BorderWidth\": DEFAULT_BORDER_WIDTH,\n \"🔍MagnifyGlass.ActivationKey\": \"x\",\n \"🔍MagnifyGlass.AltRequired\": false,\n \"🔍MagnifyGlass.FollowCursor\": false,\n \"🔍MagnifyGlass.DebugMode\": false,\n \"🔍MagnifyGlass.OffsetStep\": DEFAULT_OFFSET_STEP,\n \"🔍MagnifyGlass.GlassPosition\": \"Top-Right\",\n \"🔍MagnifyGlass.ResetKey\": \"o\",\n \"🔍MagnifyGlass.GlassShape\": \"Rounded Square\",\n \"🔍MagnifyGlass.BorderEnabled\": true,\n \"🔍MagnifyGlass.TextureFiltering\": \"Linear\",\n \"🔍MagnifyGlass.AlwaysActiveMode\": true,\n \"🔍MagnifyGlass.ToggleFollowCursorKey\": \"h\",\n};\n\n/**\n * Default settings for the info panel component.\n */\nexport const DEFAULT_PANEL_SETTINGS: PanelSettings = {\n \"🔍MagnifyGlass.InfoPanelEnabled\": true,\n \"🔍MagnifyGlass.InfoPanelPosition\": \"Bottom\",\n \"🔍MagnifyGlass.InfoPanelWidth\": DEFAULT_PANEL_WIDTH,\n \"🔍MagnifyGlass.InfoPanelOpacity\": DEFAULT_PANEL_OPACITY,\n \"🔍MagnifyGlass.InfoPanelMaxHeight\": DEFAULT_PANEL_MAX_HEIGHT,\n \"🔍MagnifyGlass.InfoPanelAnimations\": false,\n \"🔍MagnifyGlass.ShowInspectorTab\": false,\n \"🔍MagnifyGlass.ToggleHotkey\": \"i\",\n \"🔍MagnifyGlass.GlassPreviewToggleHotkey\": \"g\",\n \"🔍MagnifyGlass.PinPanelHotkey\": \"u\",\n \"🔍MagnifyGlass.ShowHoveringControls\": true,\n \"🔍MagnifyGlass.ControlsPosition\": \"bottom\",\n \"🔍MagnifyGlass.InfoPanelTextColor\": \"#6b7280\",\n \"🔍MagnifyGlass.InfoPanelAccentColor\": \"#3b82f6\",\n};\n\n/**\n * Register all magnify glass settings with ComfyUI.\n * @param magnifyGlass - The MagnifyGlass instance\n */\nexport function registerGlassSettings(magnifyGlass: any): void {\n const settings = DEFAULT_GLASS_SETTINGS;\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.ZoomFactor\",\n name: \"🔍 Magnify Glass: Zoom Factor (%)\",\n type: \"slider\",\n defaultValue: settings[\"🔍MagnifyGlass.ZoomFactor\"],\n min: 100,\n max: 1000,\n step: 25,\n tooltip: \"Magnification level as a percentage (e.g., 300 = 3x zoom, 150 = 1.5x zoom).\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.zoomFactor = typeof value === 'number' ? value / 100 : parseFloat(value) / 100;\n if (magnifyGlass.state.active) {\n magnifyGlass.updateMagnifiedView();\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.GlassSize\",\n name: \"🔍 Magnify Glass: Size (px)\",\n type: \"slider\",\n defaultValue: settings[\"🔍MagnifyGlass.GlassSize\"],\n min: 50,\n max: 100,\n step: 10,\n tooltip: \"Diameter of the magnifying glass circle in pixels.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.glassSize = parseInt(String(value), 10);\n magnifyGlass.applyUiChanges();\n if (magnifyGlass.state.active) {\n magnifyGlass.updateMagnifiedView();\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.BorderWidth\",\n name: \"🔍 Magnify Glass: Border Width (px)\",\n type: \"slider\",\n defaultValue: settings[\"🔍MagnifyGlass.BorderWidth\"],\n min: 0,\n max: 10,\n step: 0.1,\n tooltip: \"Width of the border around the magnifying glass.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.borderWidth = parseFloat(String(value));\n magnifyGlass.applyUiChanges();\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.BorderColor\",\n name: \"🎨 Magnify Glass: Border Color\",\n type: \"color\",\n defaultValue: settings[\"🔍MagnifyGlass.BorderColor\"],\n tooltip: \"Color of the border around the magnifying glass.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n const strValue = String(value);\n const normalizedColor = strValue && !strValue.startsWith('#') ? `#${strValue}` : strValue;\n magnifyGlass.config.borderColor = normalizedColor;\n if (strValue !== normalizedColor) {\n try {\n app.ui.settings.setSettingValue(\"🔍MagnifyGlass.BorderColor\", normalizedColor);\n } catch (e) {\n console.warn('Failed to save normalized border color:', e);\n }\n }\n magnifyGlass.applyUiChanges();\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.ActivationKey\",\n name: \"⌨️ Magnify Glass: Activation Key\",\n type: \"combo\",\n options: ACTIVATION_KEYS.map(k => ({ value: k, text: k })), // Assuming ACTIVATION_KEYS is simply an array of strings\n defaultValue: settings[\"🔍MagnifyGlass.ActivationKey\"],\n tooltip: \"The key to activate the magnifier.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.activationKey = String(value).toLowerCase();\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.AltRequired\",\n name: \"⌨️ Magnify Glass: Require Alt/Option Key\",\n type: \"combo\",\n options: [{ value: true, text: \"Yes\" }, { value: false, text: \"No\" }],\n defaultValue: settings[\"🔍MagnifyGlass.AltRequired\"],\n tooltip: \"If Yes, Alt (Windows/Linux) or Option (Mac) must be held for activation.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.altRequired = !!value;\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.FollowCursor\",\n name: \"🖱️ Magnify Glass: Follow Cursor Position\",\n type: \"combo\",\n options: [{ value: true, text: \"Yes\" }, { value: false, text: \"No\" }],\n defaultValue: settings[\"🔍MagnifyGlass.FollowCursor\"],\n tooltip: \"If Yes, the magnifier window moves with the cursor.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.followCursor = !!value;\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.OffsetStep\",\n name: \"⌨️ Magnify Glass: Offset Adjust Step\",\n type: \"slider\",\n defaultValue: settings[\"🔍MagnifyGlass.OffsetStep\"],\n min: 1,\n max: 50,\n step: 1,\n tooltip: \"How many graph units the view shifts when pressing arrow keys.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.offsetStep = parseInt(String(value), 10);\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.ResetKey\",\n name: \"⌨️ Magnify Glass: Reset Offset Key\",\n type: \"combo\",\n options: RESET_KEYS.map(k => ({ value: k, text: k })),\n defaultValue: settings[\"🔍MagnifyGlass.ResetKey\"],\n tooltip: \"The key to reset the magnify glass offset to default.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.resetKey = String(value).toLowerCase();\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.DebugMode\",\n name: \"🐞 Magnify Glass: Debug Mode\",\n type: \"combo\",\n options: [{ value: true, text: \"Enabled\" }, { value: false, text: \"Disabled\" }],\n defaultValue: settings[\"🔍MagnifyGlass.DebugMode\"],\n tooltip: \"Show detailed logging and the debug visualization overlay.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.debugMode = !!value;\n magnifyGlass.applyUiChanges();\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.GlassPosition\",\n name: \"🖱️ Magnify Glass: Glass Position\",\n type: \"combo\",\n options: GLASS_POSITIONS.map(p => ({ value: p, text: p === \"Bottom\" ? \"Bottom (Default)\" : p })),\n defaultValue: settings[\"🔍MagnifyGlass.GlassPosition\"],\n tooltip: \"Position of the magnifying glass relative to the cursor.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.glassPosition = String(value);\n if (magnifyGlass.state.active && !magnifyGlass.config.followCursor) {\n const { x, y } = magnifyGlass.lastKnownMousePosition;\n magnifyGlass.ui.positionGlass(x, y);\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.GlassShape\",\n name: \"🖼️ Magnify Glass: Shape\",\n type: \"combo\",\n options: GLASS_SHAPES.map(s => ({ value: s, text: s })),\n defaultValue: settings[\"🔍MagnifyGlass.GlassShape\"],\n tooltip: \"Shape of the magnifying glass.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.glassShape = String(value);\n magnifyGlass.applyUiChanges();\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.BorderEnabled\",\n name: \"🖼️ Magnify Glass: Show Border\",\n type: \"combo\",\n options: [{ value: true, text: \"Yes\" }, { value: false, text: \"No\" }],\n defaultValue: settings[\"🔍MagnifyGlass.BorderEnabled\"],\n tooltip: \"Enable or disable the border around the magnifying glass.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.borderEnabled = !!value;\n magnifyGlass.applyUiChanges();\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.TextureFiltering\",\n name: \"🖼️ Magnify Glass: Texture Filtering\",\n type: \"combo\",\n options: [\n { value: \"Linear\", text: \"Linear (Smooth)\" },\n { value: \"Nearest\", text: \"Nearest (Pixelated)\" }\n ],\n defaultValue: settings[\"🔍MagnifyGlass.TextureFiltering\"],\n tooltip: \"Controls how the magnified image is scaled.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config && magnifyGlass.renderer) {\n magnifyGlass.config.textureFiltering = String(value);\n magnifyGlass.renderer.updateTextureFiltering(value);\n if (magnifyGlass.state.active) {\n magnifyGlass.updateMagnifiedView();\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.AlwaysActiveMode\",\n name: \"🔒 Magnify Glass: Always Active Mode\",\n type: \"combo\",\n options: [{ value: true, text: \"Yes\" }, { value: false, text: \"No\" }],\n defaultValue: settings[\"🔍MagnifyGlass.AlwaysActiveMode\"],\n tooltip: \"If Yes, activating the magnifier keeps it on until activated again.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.alwaysActiveMode = !!value;\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.ToggleFollowCursorKey\",\n name: \"🔑 Magnify Glass: Toggle Follow Key\",\n type: \"combo\",\n options: TOGGLE_FOLLOW_KEYS.map(k => ({ value: k, text: k })),\n defaultValue: settings[\"🔍MagnifyGlass.ToggleFollowCursorKey\"],\n tooltip: \"The key to toggle the 'Follow Cursor' behavior.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.toggleFollowCursorKey = String(value).toLowerCase();\n }\n }\n });\n}\n\n/**\n * Register all info panel settings with ComfyUI.\n * @param stateManager - The StateManager instance for the info panel\n * @param uiManager - The UIManager instance for the info panel\n * @param positionManager - The PositionManager instance for the info panel\n */\nexport function registerPanelSettings(stateManager: any, uiManager: any, positionManager: any): void {\n const settings = DEFAULT_PANEL_SETTINGS;\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.InfoPanelEnabled\",\n name: \"📊 Info Panel: Enable\",\n type: \"combo\",\n options: [{ value: true, text: \"Yes\" }, { value: false, text: \"No\" }],\n defaultValue: settings[\"🔍MagnifyGlass.InfoPanelEnabled\"],\n tooltip: \"Enable or disable the information panel.\",\n onChange: (value: any) => {\n if (stateManager) {\n stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelEnabled\"] = !!value;\n if (!value && uiManager) {\n uiManager.hide();\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.InfoPanelPosition\",\n name: \"📊 Info Panel: Position\",\n type: \"combo\",\n options: PANEL_POSITIONS.map(p => ({ value: p, text: p })),\n defaultValue: settings[\"🔍MagnifyGlass.InfoPanelPosition\"],\n tooltip: \"Position of the info panel relative to the magnifying glass.\",\n onChange: (value: any) => {\n if (stateManager) {\n stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelPosition\"] = String(value);\n if (positionManager) {\n positionManager.positionPanel();\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.InfoPanelWidth\",\n name: \"📊 Info Panel: Width (px)\",\n type: \"slider\",\n defaultValue: settings[\"🔍MagnifyGlass.InfoPanelWidth\"],\n min: 200,\n max: 600,\n step: 20,\n tooltip: \"Width of the information panel in pixels.\",\n onChange: (value: any) => {\n if (stateManager) {\n stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelWidth\"] = parseInt(String(value), 10);\n if (uiManager) {\n uiManager.applyStyles();\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.InfoPanelOpacity\",\n name: \"📊 Info Panel: Opacity (%)\",\n type: \"slider\",\n defaultValue: settings[\"🔍MagnifyGlass.InfoPanelOpacity\"],\n min: 10,\n max: 100,\n step: 5,\n tooltip: \"Opacity of the information panel background.\",\n onChange: (value: any) => {\n if (stateManager) {\n stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelOpacity\"] = parseInt(String(value), 10);\n if (uiManager) {\n uiManager.applyStyles();\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.InfoPanelMaxHeight\",\n name: \"📊 Info Panel: Max Height (px)\",\n type: \"slider\",\n defaultValue: settings[\"🔍MagnifyGlass.InfoPanelMaxHeight\"],\n min: 200,\n max: 1500,\n step: 50,\n tooltip: \"Maximum height of the information panel in pixels.\",\n onChange: (value: any) => {\n if (stateManager) {\n stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelMaxHeight\"] = parseInt(String(value), 10);\n if (uiManager) {\n uiManager.applyStyles();\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.InfoPanelAnimations\",\n name: \"📊 Info Panel: Animations\",\n type: \"combo\",\n options: [{ value: true, text: \"Enabled\" }, { value: false, text: \"Disabled\" }],\n defaultValue: settings[\"🔍MagnifyGlass.InfoPanelAnimations\"],\n tooltip: \"Enable or disable animations for the info panel.\",\n onChange: (value: any) => {\n if (stateManager) {\n stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelAnimations\"] = !!value;\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.ShowInspectorTab\",\n name: \"📊 Info Panel: Show Inspector Tab\",\n type: \"combo\",\n options: [{ value: true, text: \"Yes\" }, { value: false, text: \"No\" }],\n defaultValue: settings[\"🔍MagnifyGlass.ShowInspectorTab\"],\n tooltip: \"Show or hide the Inspector tab with cursor and canvas information.\",\n onChange: (value: any) => {\n if (stateManager) {\n stateManager.state.settings[\"🔍MagnifyGlass.ShowInspectorTab\"] = !!value;\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.ShowHoveringControls\",\n name: \"📊 Info Panel: Show Hover Controls\",\n type: \"combo\",\n options: [{ value: true, text: \"Yes\" }, { value: false, text: \"No\" }],\n defaultValue: settings[\"🔍MagnifyGlass.ShowHoveringControls\"],\n tooltip: \"Show or hide hovering UI controls above the info panel.\",\n onChange: (value: any) => {\n if (stateManager) {\n stateManager.state.settings[\"🔍MagnifyGlass.ShowHoveringControls\"] = !!value;\n if (uiManager && uiManager.elements.controls) {\n uiManager.elements.controls.style.display = value ? 'flex' : 'none';\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.InfoPanelTextColor\",\n name: \"🎨 Info Panel: Text Color\",\n type: \"color\",\n defaultValue: settings[\"🔍MagnifyGlass.InfoPanelTextColor\"],\n tooltip: \"Custom text color for the info panel.\",\n onChange: (value: any) => {\n if (stateManager) {\n const strValue = String(value);\n const normalizedColor = strValue && !strValue.startsWith('#') ? `#${strValue}` : strValue;\n stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelTextColor\"] = normalizedColor;\n if (uiManager) {\n uiManager.applyStyles();\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.InfoPanelAccentColor\",\n name: \"🎨 Info Panel: Accent Color\",\n type: \"color\",\n defaultValue: settings[\"🔍MagnifyGlass.InfoPanelAccentColor\"],\n tooltip: \"Custom accent color for the info panel.\",\n onChange: (value: any) => {\n if (stateManager) {\n const strValue = String(value);\n const normalizedColor = strValue && !strValue.startsWith('#') ? `#${strValue}` : strValue;\n stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelAccentColor\"] = normalizedColor;\n if (uiManager) {\n uiManager.applyStyles();\n }\n }\n }\n });\n}\n\n/**\n * Get a setting configuration object for ComfyUI settings registration.\n * This function generates the setting config based on the key and default value.\n * \n * @param key - The setting key\n * @param defaultValue - The default value for the setting\n * @returns The setting configuration or null if not found\n */\nexport function getSettingConfig(key: string, defaultValue: any): any {\n const configs: Record = {\n \"🔍MagnifyGlass.InfoPanelEnabled\": {\n id: key,\n name: \"📋 Magnify Glass: Info Panel\",\n type: \"combo\",\n options: [\n { value: true, text: \"Enabled\" },\n { value: false, text: \"Disabled\" }\n ],\n defaultValue,\n tooltip: \"Enable or disable the professional information panel.\"\n },\n \"🔍MagnifyGlass.InfoPanelPosition\": {\n id: key,\n name: \"📋 Info Panel: Position\",\n type: \"combo\",\n options: PANEL_POSITIONS.map(pos => ({ value: pos, text: pos })),\n defaultValue,\n tooltip: \"Position of the info panel relative to the magnify glass.\"\n },\n \"🔍MagnifyGlass.InfoPanelWidth\": {\n id: key,\n name: \"📋 Info Panel: Width (px)\",\n type: \"slider\",\n defaultValue,\n min: 200,\n max: 600,\n step: 20,\n tooltip: \"Width of the info panel in pixels.\"\n },\n \"🔍MagnifyGlass.InfoPanelMaxHeight\": {\n id: key,\n name: \"📋 Info Panel: Max Height (px)\",\n type: \"slider\",\n defaultValue,\n min: 200,\n max: 1500,\n step: 50,\n tooltip: \"Maximum height of the info panel.\"\n },\n \"🔍MagnifyGlass.InfoPanelOpacity\": {\n id: key,\n name: \"📋 Info Panel: Opacity (%)\",\n type: \"slider\",\n defaultValue,\n min: 10,\n max: 100,\n step: 5,\n tooltip: \"Opacity of the info panel background.\"\n },\n \"🔍MagnifyGlass.InfoPanelAnimations\": {\n id: key,\n name: \"📋 Info Panel: Animations\",\n type: \"combo\",\n options: [\n { value: true, text: \"Enabled\" },\n { value: false, text: \"Disabled\" }\n ],\n defaultValue,\n tooltip: \"Enable smooth animations for the info panel.\"\n },\n \"🔍MagnifyGlass.ShowInspectorTab\": {\n id: key,\n name: \"📋 Info Panel: Show Inspector\",\n type: \"combo\",\n options: [\n { value: true, text: \"Yes\" },\n { value: false, text: \"No\" }\n ],\n defaultValue,\n tooltip: \"Show the inspector tab with cursor and canvas information.\"\n },\n \"🔍MagnifyGlass.ToggleHotkey\": {\n id: key,\n name: \"📋 Info Panel: Toggle Hotkey\",\n type: \"combo\",\n options: [\"i\", \"j\", \"k\", \"l\", \"n\", \"m\"].map(k => ({ value: k, text: k.toUpperCase() })),\n defaultValue,\n tooltip: \"Key to toggle the info panel visibility.\"\n },\n \"🔍MagnifyGlass.GlassPreviewToggleHotkey\": {\n id: key,\n name: \"📋 Info Panel: Glass Toggle Hotkey\",\n type: \"combo\",\n options: [\"g\", \"f\", \"v\", \"b\"].map(k => ({ value: k, text: k.toUpperCase() })),\n defaultValue,\n tooltip: \"Key to toggle the magnify glass preview visibility.\"\n },\n \"🔍MagnifyGlass.PinPanelHotkey\": {\n id: key,\n name: \"📋 Info Panel: Pin Hotkey\",\n type: \"combo\",\n options: [\"u\", \"p\", \"y\", \"t\"].map(k => ({ value: k, text: k.toUpperCase() })),\n defaultValue,\n tooltip: \"Key to pin/unlock the info panel at the current mouse location.\"\n },\n \"🔍MagnifyGlass.ShowHoveringControls\": {\n id: key,\n name: \"📋 Info Panel: Floating Controls\",\n type: \"combo\",\n options: [\n { value: true, text: \"Show\" },\n { value: false, text: \"Hide\" }\n ],\n defaultValue,\n tooltip: \"Show floating control buttons near the info panel.\"\n },\n \"🔍MagnifyGlass.ControlsPosition\": {\n id: key,\n name: \"📋 Info Panel: Controls Position\",\n type: \"combo\",\n options: [\"top-left\", \"top-right\", \"bottom-left\", \"bottom-right\", \"top\", \"bottom\", \"left\", \"right\"].map(p => ({ value: p, text: p })),\n defaultValue,\n tooltip: \"Position of the floating control buttons.\"\n },\n \"🔍MagnifyGlass.InfoPanelTextColor\": {\n id: key,\n name: \"🎨 Info Panel: Text Color\",\n type: \"color\",\n defaultValue,\n tooltip: \"Color for text in the info panel.\"\n },\n \"🔍MagnifyGlass.InfoPanelAccentColor\": {\n id: key,\n name: \"🎨 Info Panel: Accent Color\",\n type: \"color\",\n defaultValue,\n tooltip: \"Accent color for highlights in the info panel.\"\n }\n };\n\n return configs[key] || null;\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/shared/settings/defaults.js b/web/shared/settings/defaults.js new file mode 100644 index 0000000..aa3b036 --- /dev/null +++ b/web/shared/settings/defaults.js @@ -0,0 +1,40 @@ +import { DEFAULT_ZOOM_FACTOR, DEFAULT_GLASS_SIZE, DEFAULT_BORDER_WIDTH, DEFAULT_BORDER_COLOR, DEFAULT_OFFSET_STEP, DEFAULT_PANEL_MAX_HEIGHT, DEFAULT_PANEL_OPACITY, DEFAULT_PANEL_WIDTH } 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": "left", + "🔍MagnifyGlass.InfoPanelTextColor": "#6b7280", + "🔍MagnifyGlass.InfoPanelAccentColor": "#3b82f6" +}; +export { + DEFAULT_GLASS_SETTINGS, + DEFAULT_PANEL_SETTINGS +}; +//# sourceMappingURL=defaults.js.map diff --git a/web/shared/settings/defaults.js.map b/web/shared/settings/defaults.js.map new file mode 100644 index 0000000..8ae134c --- /dev/null +++ b/web/shared/settings/defaults.js.map @@ -0,0 +1 @@ +{"version":3,"file":"defaults.js","sources":["../../../src/shared/settings/defaults.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Default Settings Values\n * \n * Default values for all magnify glass and info panel settings.\n */\n\nimport {\n DEFAULT_GLASS_SIZE,\n DEFAULT_ZOOM_FACTOR,\n DEFAULT_BORDER_WIDTH,\n DEFAULT_BORDER_COLOR,\n DEFAULT_OFFSET_STEP,\n DEFAULT_PANEL_WIDTH,\n DEFAULT_PANEL_MAX_HEIGHT,\n DEFAULT_PANEL_OPACITY\n} from '../constants';\nimport type { GlassSettings, PanelSettings } from './types';\n\n/**\n * Default settings for the magnify glass component.\n */\nexport const DEFAULT_GLASS_SETTINGS: GlassSettings = {\n \"🔍MagnifyGlass.ZoomFactor\": DEFAULT_ZOOM_FACTOR,\n \"🔍MagnifyGlass.GlassSize\": DEFAULT_GLASS_SIZE,\n \"🔍MagnifyGlass.BorderColor\": DEFAULT_BORDER_COLOR,\n \"🔍MagnifyGlass.BorderWidth\": DEFAULT_BORDER_WIDTH,\n \"🔍MagnifyGlass.ActivationKey\": \"x\",\n \"🔍MagnifyGlass.AltRequired\": false,\n \"🔍MagnifyGlass.FollowCursor\": false,\n \"🔍MagnifyGlass.DebugMode\": false,\n \"🔍MagnifyGlass.OffsetStep\": DEFAULT_OFFSET_STEP,\n \"🔍MagnifyGlass.GlassPosition\": \"Top-Right\",\n \"🔍MagnifyGlass.ResetKey\": \"o\",\n \"🔍MagnifyGlass.GlassShape\": \"Rounded Square\",\n \"🔍MagnifyGlass.BorderEnabled\": true,\n \"🔍MagnifyGlass.TextureFiltering\": \"Linear\",\n \"🔍MagnifyGlass.AlwaysActiveMode\": true,\n \"🔍MagnifyGlass.ToggleFollowCursorKey\": \"h\",\n};\n\n/**\n * Default settings for the info panel component.\n */\nexport const DEFAULT_PANEL_SETTINGS: PanelSettings = {\n \"🔍MagnifyGlass.InfoPanelEnabled\": true,\n \"🔍MagnifyGlass.InfoPanelPosition\": \"Bottom\",\n \"🔍MagnifyGlass.InfoPanelWidth\": DEFAULT_PANEL_WIDTH,\n \"🔍MagnifyGlass.InfoPanelOpacity\": DEFAULT_PANEL_OPACITY,\n \"🔍MagnifyGlass.InfoPanelMaxHeight\": DEFAULT_PANEL_MAX_HEIGHT,\n \"🔍MagnifyGlass.InfoPanelAnimations\": false,\n \"🔍MagnifyGlass.ShowInspectorTab\": false,\n \"🔍MagnifyGlass.ToggleHotkey\": \"i\",\n \"🔍MagnifyGlass.GlassPreviewToggleHotkey\": \"g\",\n \"🔍MagnifyGlass.PinPanelHotkey\": \"u\",\n \"🔍MagnifyGlass.ShowHoveringControls\": true,\n \"🔍MagnifyGlass.ControlsPosition\": \"left\",\n \"🔍MagnifyGlass.InfoPanelTextColor\": \"#6b7280\",\n \"🔍MagnifyGlass.InfoPanelAccentColor\": \"#3b82f6\",\n};\n"],"names":[],"mappings":";AAqBO,MAAM,yBAAwC;AAAA,EACjD,6BAA6B;AAAA,EAC7B,4BAA4B;AAAA,EAC5B,8BAA8B;AAAA,EAC9B,8BAA8B;AAAA,EAC9B,gCAAgC;AAAA,EAChC,8BAA8B;AAAA,EAC9B,+BAA+B;AAAA,EAC/B,4BAA4B;AAAA,EAC5B,6BAA6B;AAAA,EAC7B,gCAAgC;AAAA,EAChC,2BAA2B;AAAA,EAC3B,6BAA6B;AAAA,EAC7B,gCAAgC;AAAA,EAChC,mCAAmC;AAAA,EACnC,mCAAmC;AAAA,EACnC,wCAAwC;AAC5C;AAKO,MAAM,yBAAwC;AAAA,EACjD,mCAAmC;AAAA,EACnC,oCAAoC;AAAA,EACpC,iCAAiC;AAAA,EACjC,mCAAmC;AAAA,EACnC,qCAAqC;AAAA,EACrC,sCAAsC;AAAA,EACtC,mCAAmC;AAAA,EACnC,+BAA+B;AAAA,EAC/B,2CAA2C;AAAA,EAC3C,iCAAiC;AAAA,EACjC,uCAAuC;AAAA,EACvC,mCAAmC;AAAA,EACnC,qCAAqC;AAAA,EACrC,uCAAuC;AAC3C;"} \ No newline at end of file diff --git a/web/shared/settings/glassSettings.js b/web/shared/settings/glassSettings.js new file mode 100644 index 0000000..7d52642 --- /dev/null +++ b/web/shared/settings/glassSettings.js @@ -0,0 +1,328 @@ +import { app } from "/scripts/app.js"; +import { ACTIVATION_KEYS, RESET_KEYS, GLASS_POSITIONS, GLASS_SHAPES, TOGGLE_FOLLOW_KEYS } from "../constants.js"; +import { DEFAULT_GLASS_SETTINGS } from "./defaults.js"; +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 })), + 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(); + } + } + }); + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.Action.ResetPosition", + name: "🔄 Magnify Glass: Reset Position", + type: "boolean", + defaultValue: false, + tooltip: "Toggle to reset the glass position and disable follow cursor.", + onChange: (value) => { + if (value) { + if (magnifyGlass == null ? void 0 : magnifyGlass.resetOffsets) { + magnifyGlass.resetOffsets(); + } + setTimeout(() => { + try { + app.ui.settings.setSettingValue("🔍MagnifyGlass.Action.ResetPosition", false); + } catch (e) { + } + }, 500); + } + } + }); + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.Action.ResetAll", + name: "⚠️ Magnify Glass: Reset ALL Settings", + type: "boolean", + defaultValue: false, + tooltip: "Toggle to reset ALL settings to defaults.", + onChange: (value) => { + if (value) { + if (confirm("Reset ALL Magnify Glass settings?")) { + app.ui.settings.setSettingValue("🔍MagnifyGlass.ZoomFactor", 300); + app.ui.settings.setSettingValue("🔍MagnifyGlass.GlassSize", 300); + app.ui.settings.setSettingValue("🔍MagnifyGlass.GlassShape", "Rounded Square"); + app.ui.settings.setSettingValue("🔍MagnifyGlass.GlassPosition", "Top-Right"); + app.ui.settings.setSettingValue("🔍MagnifyGlass.TextureFiltering", "Linear"); + app.ui.settings.setSettingValue("🔍MagnifyGlass.BorderWidth", 1); + app.ui.settings.setSettingValue("🔍MagnifyGlass.BorderColor", "#6b7280"); + app.ui.settings.setSettingValue("🔍MagnifyGlass.BorderEnabled", true); + app.ui.settings.setSettingValue("🔍MagnifyGlass.FollowCursor", false); + app.ui.settings.setSettingValue("🔍MagnifyGlass.AlwaysActiveMode", true); + app.ui.settings.setSettingValue("🔍MagnifyGlass.ActivationKey", "x"); + app.ui.settings.setSettingValue("🔍MagnifyGlass.ResetKey", "o"); + app.ui.settings.setSettingValue("🔍MagnifyGlass.ToggleFollowCursorKey", "h"); + app.ui.settings.setSettingValue("🔍MagnifyGlass.AltRequired", false); + app.ui.settings.setSettingValue("🔍MagnifyGlass.OffsetStep", 5); + app.ui.settings.setSettingValue("🔍MagnifyGlass.DebugMode", false); + app.ui.settings.setSettingValue("🔍MagnifyGlass.InfoPanelEnabled", true); + app.ui.settings.setSettingValue("🔍MagnifyGlass.InfoPanelPosition", "Bottom"); + app.ui.settings.setSettingValue("🔍MagnifyGlass.InfoPanelWidth", 300); + app.ui.settings.setSettingValue("🔍MagnifyGlass.InfoPanelMaxHeight", 300); + app.ui.settings.setSettingValue("🔍MagnifyGlass.InfoPanelOpacity", 100); + app.ui.settings.setSettingValue("🔍MagnifyGlass.InfoPanelTextColor", "#6b7280"); + app.ui.settings.setSettingValue("🔍MagnifyGlass.InfoPanelAccentColor", "#3b82f6"); + app.ui.settings.setSettingValue("🔍MagnifyGlass.InfoPanelAnimations", false); + app.ui.settings.setSettingValue("🔍MagnifyGlass.ShowHoveringControls", true); + app.ui.settings.setSettingValue("🔍MagnifyGlass.ControlsPosition", "left"); + app.ui.settings.setSettingValue("🔍MagnifyGlass.ToggleHotkey", "i"); + app.ui.settings.setSettingValue("🔍MagnifyGlass.GlassPreviewToggleHotkey", "g"); + app.ui.settings.setSettingValue("🔍MagnifyGlass.PinPanelHotkey", "u"); + if (magnifyGlass == null ? void 0 : magnifyGlass.resetOffsets) { + magnifyGlass.resetOffsets(); + } + } + setTimeout(() => { + try { + app.ui.settings.setSettingValue("🔍MagnifyGlass.Action.ResetAll", false); + } catch (e) { + } + }, 500); + } + } + }); +} +export { + registerGlassSettings +}; +//# sourceMappingURL=glassSettings.js.map diff --git a/web/shared/settings/glassSettings.js.map b/web/shared/settings/glassSettings.js.map new file mode 100644 index 0000000..7bd8808 --- /dev/null +++ b/web/shared/settings/glassSettings.js.map @@ -0,0 +1 @@ +{"version":3,"file":"glassSettings.js","sources":["../../../src/shared/settings/glassSettings.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Glass Settings Registration\n * \n * Registers all magnify glass settings with ComfyUI.\n */\n\n// @ts-ignore\nimport { app } from \"/scripts/app.js\";\nimport {\n GLASS_POSITIONS,\n GLASS_SHAPES,\n ACTIVATION_KEYS,\n RESET_KEYS,\n TOGGLE_FOLLOW_KEYS\n} from '../constants';\nimport { DEFAULT_GLASS_SETTINGS } from './defaults';\n\n/**\n * Register all magnify glass settings with ComfyUI.\n * @param magnifyGlass - The MagnifyGlass instance\n */\nexport function registerGlassSettings(magnifyGlass: any): void {\n const settings = DEFAULT_GLASS_SETTINGS;\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.ZoomFactor\",\n name: \"🔍 Magnify Glass: Zoom Factor (%)\",\n type: \"slider\",\n defaultValue: settings[\"🔍MagnifyGlass.ZoomFactor\"],\n min: 100,\n max: 1000,\n step: 25,\n tooltip: \"Magnification level as a percentage (e.g., 300 = 3x zoom, 150 = 1.5x zoom).\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.zoomFactor = typeof value === 'number' ? value / 100 : parseFloat(value) / 100;\n if (magnifyGlass.state.active) {\n magnifyGlass.updateMagnifiedView();\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.GlassSize\",\n name: \"🔍 Magnify Glass: Size (px)\",\n type: \"slider\",\n defaultValue: settings[\"🔍MagnifyGlass.GlassSize\"],\n min: 50,\n max: 100,\n step: 10,\n tooltip: \"Diameter of the magnifying glass circle in pixels.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.glassSize = parseInt(String(value), 10);\n magnifyGlass.applyUiChanges();\n if (magnifyGlass.state.active) {\n magnifyGlass.updateMagnifiedView();\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.BorderWidth\",\n name: \"🔍 Magnify Glass: Border Width (px)\",\n type: \"slider\",\n defaultValue: settings[\"🔍MagnifyGlass.BorderWidth\"],\n min: 0,\n max: 10,\n step: 0.1,\n tooltip: \"Width of the border around the magnifying glass.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.borderWidth = parseFloat(String(value));\n magnifyGlass.applyUiChanges();\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.BorderColor\",\n name: \"🎨 Magnify Glass: Border Color\",\n type: \"color\",\n defaultValue: settings[\"🔍MagnifyGlass.BorderColor\"],\n tooltip: \"Color of the border around the magnifying glass.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n const strValue = String(value);\n const normalizedColor = strValue && !strValue.startsWith('#') ? `#${strValue}` : strValue;\n magnifyGlass.config.borderColor = normalizedColor;\n if (strValue !== normalizedColor) {\n try {\n app.ui.settings.setSettingValue(\"🔍MagnifyGlass.BorderColor\", normalizedColor);\n } catch (e) {\n console.warn('Failed to save normalized border color:', e);\n }\n }\n magnifyGlass.applyUiChanges();\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.ActivationKey\",\n name: \"⌨️ Magnify Glass: Activation Key\",\n type: \"combo\",\n options: ACTIVATION_KEYS.map(k => ({ value: k, text: k })),\n defaultValue: settings[\"🔍MagnifyGlass.ActivationKey\"],\n tooltip: \"The key to activate the magnifier.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.activationKey = String(value).toLowerCase();\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.AltRequired\",\n name: \"⌨️ Magnify Glass: Require Alt/Option Key\",\n type: \"combo\",\n options: [{ value: true, text: \"Yes\" }, { value: false, text: \"No\" }],\n defaultValue: settings[\"🔍MagnifyGlass.AltRequired\"],\n tooltip: \"If Yes, Alt (Windows/Linux) or Option (Mac) must be held for activation.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.altRequired = !!value;\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.FollowCursor\",\n name: \"🖱️ Magnify Glass: Follow Cursor Position\",\n type: \"combo\",\n options: [{ value: true, text: \"Yes\" }, { value: false, text: \"No\" }],\n defaultValue: settings[\"🔍MagnifyGlass.FollowCursor\"],\n tooltip: \"If Yes, the magnifier window moves with the cursor.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.followCursor = !!value;\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.OffsetStep\",\n name: \"⌨️ Magnify Glass: Offset Adjust Step\",\n type: \"slider\",\n defaultValue: settings[\"🔍MagnifyGlass.OffsetStep\"],\n min: 1,\n max: 50,\n step: 1,\n tooltip: \"How many graph units the view shifts when pressing arrow keys.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.offsetStep = parseInt(String(value), 10);\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.ResetKey\",\n name: \"⌨️ Magnify Glass: Reset Offset Key\",\n type: \"combo\",\n options: RESET_KEYS.map(k => ({ value: k, text: k })),\n defaultValue: settings[\"🔍MagnifyGlass.ResetKey\"],\n tooltip: \"The key to reset the magnify glass offset to default.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.resetKey = String(value).toLowerCase();\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.DebugMode\",\n name: \"🐞 Magnify Glass: Debug Mode\",\n type: \"combo\",\n options: [{ value: true, text: \"Enabled\" }, { value: false, text: \"Disabled\" }],\n defaultValue: settings[\"🔍MagnifyGlass.DebugMode\"],\n tooltip: \"Show detailed logging and the debug visualization overlay.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.debugMode = !!value;\n magnifyGlass.applyUiChanges();\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.GlassPosition\",\n name: \"🖱️ Magnify Glass: Glass Position\",\n type: \"combo\",\n options: GLASS_POSITIONS.map(p => ({ value: p, text: p === \"Bottom\" ? \"Bottom (Default)\" : p })),\n defaultValue: settings[\"🔍MagnifyGlass.GlassPosition\"],\n tooltip: \"Position of the magnifying glass relative to the cursor.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.glassPosition = String(value);\n if (magnifyGlass.state.active && !magnifyGlass.config.followCursor) {\n const { x, y } = magnifyGlass.lastKnownMousePosition;\n magnifyGlass.ui.positionGlass(x, y);\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.GlassShape\",\n name: \"🖼️ Magnify Glass: Shape\",\n type: \"combo\",\n options: GLASS_SHAPES.map(s => ({ value: s, text: s })),\n defaultValue: settings[\"🔍MagnifyGlass.GlassShape\"],\n tooltip: \"Shape of the magnifying glass.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.glassShape = String(value);\n magnifyGlass.applyUiChanges();\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.BorderEnabled\",\n name: \"🖼️ Magnify Glass: Show Border\",\n type: \"combo\",\n options: [{ value: true, text: \"Yes\" }, { value: false, text: \"No\" }],\n defaultValue: settings[\"🔍MagnifyGlass.BorderEnabled\"],\n tooltip: \"Enable or disable the border around the magnifying glass.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.borderEnabled = !!value;\n magnifyGlass.applyUiChanges();\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.TextureFiltering\",\n name: \"🖼️ Magnify Glass: Texture Filtering\",\n type: \"combo\",\n options: [\n { value: \"Linear\", text: \"Linear (Smooth)\" },\n { value: \"Nearest\", text: \"Nearest (Pixelated)\" }\n ],\n defaultValue: settings[\"🔍MagnifyGlass.TextureFiltering\"],\n tooltip: \"Controls how the magnified image is scaled.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config && magnifyGlass.renderer) {\n magnifyGlass.config.textureFiltering = String(value);\n magnifyGlass.renderer.updateTextureFiltering(value);\n if (magnifyGlass.state.active) {\n magnifyGlass.updateMagnifiedView();\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.AlwaysActiveMode\",\n name: \"🔒 Magnify Glass: Always Active Mode\",\n type: \"combo\",\n options: [{ value: true, text: \"Yes\" }, { value: false, text: \"No\" }],\n defaultValue: settings[\"🔍MagnifyGlass.AlwaysActiveMode\"],\n tooltip: \"If Yes, activating the magnifier keeps it on until activated again.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.alwaysActiveMode = !!value;\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.ToggleFollowCursorKey\",\n name: \"🔑 Magnify Glass: Toggle Follow Key\",\n type: \"combo\",\n options: TOGGLE_FOLLOW_KEYS.map(k => ({ value: k, text: k })),\n defaultValue: settings[\"🔍MagnifyGlass.ToggleFollowCursorKey\"],\n tooltip: \"The key to toggle the 'Follow Cursor' behavior.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.toggleFollowCursorKey = String(value).toLowerCase();\n }\n }\n });\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.Action.ResetPosition\",\n name: \"🔄 Magnify Glass: Reset Position\",\n type: \"boolean\",\n defaultValue: false,\n tooltip: \"Toggle to reset the glass position and disable follow cursor.\",\n onChange: (value: any) => {\n if (value) {\n if (magnifyGlass?.resetOffsets) {\n magnifyGlass.resetOffsets();\n }\n // Reset the toggle back to false\n setTimeout(() => {\n try {\n app.ui.settings.setSettingValue(\"🔍MagnifyGlass.Action.ResetPosition\", false);\n } catch (e) { /* ignore */ }\n }, 500);\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.Action.ResetAll\",\n name: \"⚠️ Magnify Glass: Reset ALL Settings\",\n type: \"boolean\",\n defaultValue: false,\n tooltip: \"Toggle to reset ALL settings to defaults.\",\n onChange: (value: any) => {\n if (value) {\n if (confirm(\"Reset ALL Magnify Glass settings?\")) {\n // Glass Defaults\n app.ui.settings.setSettingValue('🔍MagnifyGlass.ZoomFactor', 300);\n app.ui.settings.setSettingValue('🔍MagnifyGlass.GlassSize', 300);\n app.ui.settings.setSettingValue('🔍MagnifyGlass.GlassShape', 'Rounded Square');\n app.ui.settings.setSettingValue('🔍MagnifyGlass.GlassPosition', 'Top-Right');\n app.ui.settings.setSettingValue('🔍MagnifyGlass.TextureFiltering', 'Linear');\n app.ui.settings.setSettingValue('🔍MagnifyGlass.BorderWidth', 1);\n app.ui.settings.setSettingValue('🔍MagnifyGlass.BorderColor', '#6b7280');\n app.ui.settings.setSettingValue('🔍MagnifyGlass.BorderEnabled', true);\n app.ui.settings.setSettingValue('🔍MagnifyGlass.FollowCursor', false);\n app.ui.settings.setSettingValue('🔍MagnifyGlass.AlwaysActiveMode', true);\n app.ui.settings.setSettingValue('🔍MagnifyGlass.ActivationKey', 'x');\n app.ui.settings.setSettingValue('🔍MagnifyGlass.ResetKey', 'o');\n app.ui.settings.setSettingValue('🔍MagnifyGlass.ToggleFollowCursorKey', 'h');\n app.ui.settings.setSettingValue('🔍MagnifyGlass.AltRequired', false);\n app.ui.settings.setSettingValue('🔍MagnifyGlass.OffsetStep', 5);\n app.ui.settings.setSettingValue('🔍MagnifyGlass.DebugMode', false);\n\n // Panel Defaults\n app.ui.settings.setSettingValue('🔍MagnifyGlass.InfoPanelEnabled', true);\n app.ui.settings.setSettingValue('🔍MagnifyGlass.InfoPanelPosition', 'Bottom');\n app.ui.settings.setSettingValue('🔍MagnifyGlass.InfoPanelWidth', 300);\n app.ui.settings.setSettingValue('🔍MagnifyGlass.InfoPanelMaxHeight', 300);\n app.ui.settings.setSettingValue('🔍MagnifyGlass.InfoPanelOpacity', 100);\n app.ui.settings.setSettingValue('🔍MagnifyGlass.InfoPanelTextColor', '#6b7280');\n app.ui.settings.setSettingValue('🔍MagnifyGlass.InfoPanelAccentColor', '#3b82f6');\n app.ui.settings.setSettingValue('🔍MagnifyGlass.InfoPanelAnimations', false);\n app.ui.settings.setSettingValue('🔍MagnifyGlass.ShowHoveringControls', true);\n app.ui.settings.setSettingValue('🔍MagnifyGlass.ControlsPosition', 'left');\n app.ui.settings.setSettingValue('🔍MagnifyGlass.ToggleHotkey', 'i');\n app.ui.settings.setSettingValue('🔍MagnifyGlass.GlassPreviewToggleHotkey', 'g');\n app.ui.settings.setSettingValue('🔍MagnifyGlass.PinPanelHotkey', 'u');\n\n // Also force position reset\n if (magnifyGlass?.resetOffsets) {\n magnifyGlass.resetOffsets();\n }\n }\n\n // Reset the toggle back to false\n setTimeout(() => {\n try {\n app.ui.settings.setSettingValue(\"🔍MagnifyGlass.Action.ResetAll\", false);\n } catch (e) { /* ignore */ }\n }, 500);\n }\n }\n });\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/shared/settings/panelSettings.js b/web/shared/settings/panelSettings.js new file mode 100644 index 0000000..831bd92 --- /dev/null +++ b/web/shared/settings/panelSettings.js @@ -0,0 +1,172 @@ +import { app } from "/scripts/app.js"; +import { PANEL_POSITIONS } from "../constants.js"; +import { DEFAULT_PANEL_SETTINGS } from "./defaults.js"; +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 { + registerPanelSettings +}; +//# sourceMappingURL=panelSettings.js.map diff --git a/web/shared/settings/panelSettings.js.map b/web/shared/settings/panelSettings.js.map new file mode 100644 index 0000000..608069c --- /dev/null +++ b/web/shared/settings/panelSettings.js.map @@ -0,0 +1 @@ +{"version":3,"file":"panelSettings.js","sources":["../../../src/shared/settings/panelSettings.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Panel Settings Registration\n * \n * Registers all info panel settings with ComfyUI.\n */\n\n// @ts-ignore\nimport { app } from \"/scripts/app.js\";\nimport { PANEL_POSITIONS } from '../constants';\nimport { DEFAULT_PANEL_SETTINGS } from './defaults';\n\n/**\n * Register all info panel settings with ComfyUI.\n * @param stateManager - The StateManager instance for the info panel\n * @param uiManager - The UIManager instance for the info panel\n * @param positionManager - The PositionManager instance for the info panel\n */\nexport function registerPanelSettings(stateManager: any, uiManager: any, positionManager: any): void {\n const settings = DEFAULT_PANEL_SETTINGS;\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.InfoPanelEnabled\",\n name: \"📊 Info Panel: Enable\",\n type: \"combo\",\n options: [{ value: true, text: \"Yes\" }, { value: false, text: \"No\" }],\n defaultValue: settings[\"🔍MagnifyGlass.InfoPanelEnabled\"],\n tooltip: \"Enable or disable the information panel.\",\n onChange: (value: any) => {\n if (stateManager) {\n stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelEnabled\"] = !!value;\n if (!value && uiManager) {\n uiManager.hide();\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.InfoPanelPosition\",\n name: \"📊 Info Panel: Position\",\n type: \"combo\",\n options: PANEL_POSITIONS.map(p => ({ value: p, text: p })),\n defaultValue: settings[\"🔍MagnifyGlass.InfoPanelPosition\"],\n tooltip: \"Position of the info panel relative to the magnifying glass.\",\n onChange: (value: any) => {\n if (stateManager) {\n stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelPosition\"] = String(value);\n if (positionManager) {\n positionManager.positionPanel();\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.InfoPanelWidth\",\n name: \"📊 Info Panel: Width (px)\",\n type: \"slider\",\n defaultValue: settings[\"🔍MagnifyGlass.InfoPanelWidth\"],\n min: 200,\n max: 600,\n step: 20,\n tooltip: \"Width of the information panel in pixels.\",\n onChange: (value: any) => {\n if (stateManager) {\n stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelWidth\"] = parseInt(String(value), 10);\n if (uiManager) {\n uiManager.applyStyles();\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.InfoPanelOpacity\",\n name: \"📊 Info Panel: Opacity (%)\",\n type: \"slider\",\n defaultValue: settings[\"🔍MagnifyGlass.InfoPanelOpacity\"],\n min: 10,\n max: 100,\n step: 5,\n tooltip: \"Opacity of the information panel background.\",\n onChange: (value: any) => {\n if (stateManager) {\n stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelOpacity\"] = parseInt(String(value), 10);\n if (uiManager) {\n uiManager.applyStyles();\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.InfoPanelMaxHeight\",\n name: \"📊 Info Panel: Max Height (px)\",\n type: \"slider\",\n defaultValue: settings[\"🔍MagnifyGlass.InfoPanelMaxHeight\"],\n min: 200,\n max: 1500,\n step: 50,\n tooltip: \"Maximum height of the information panel in pixels.\",\n onChange: (value: any) => {\n if (stateManager) {\n stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelMaxHeight\"] = parseInt(String(value), 10);\n if (uiManager) {\n uiManager.applyStyles();\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.InfoPanelAnimations\",\n name: \"📊 Info Panel: Animations\",\n type: \"combo\",\n options: [{ value: true, text: \"Enabled\" }, { value: false, text: \"Disabled\" }],\n defaultValue: settings[\"🔍MagnifyGlass.InfoPanelAnimations\"],\n tooltip: \"Enable or disable animations for the info panel.\",\n onChange: (value: any) => {\n if (stateManager) {\n stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelAnimations\"] = !!value;\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.ShowInspectorTab\",\n name: \"📊 Info Panel: Show Inspector Tab\",\n type: \"combo\",\n options: [{ value: true, text: \"Yes\" }, { value: false, text: \"No\" }],\n defaultValue: settings[\"🔍MagnifyGlass.ShowInspectorTab\"],\n tooltip: \"Show or hide the Inspector tab with cursor and canvas information.\",\n onChange: (value: any) => {\n if (stateManager) {\n stateManager.state.settings[\"🔍MagnifyGlass.ShowInspectorTab\"] = !!value;\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.ShowHoveringControls\",\n name: \"📊 Info Panel: Show Hover Controls\",\n type: \"combo\",\n options: [{ value: true, text: \"Yes\" }, { value: false, text: \"No\" }],\n defaultValue: settings[\"🔍MagnifyGlass.ShowHoveringControls\"],\n tooltip: \"Show or hide hovering UI controls above the info panel.\",\n onChange: (value: any) => {\n if (stateManager) {\n stateManager.state.settings[\"🔍MagnifyGlass.ShowHoveringControls\"] = !!value;\n if (uiManager && uiManager.elements.controls) {\n uiManager.elements.controls.style.display = value ? 'flex' : 'none';\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.InfoPanelTextColor\",\n name: \"🎨 Info Panel: Text Color\",\n type: \"color\",\n defaultValue: settings[\"🔍MagnifyGlass.InfoPanelTextColor\"],\n tooltip: \"Custom text color for the info panel.\",\n onChange: (value: any) => {\n if (stateManager) {\n const strValue = String(value);\n const normalizedColor = strValue && !strValue.startsWith('#') ? `#${strValue}` : strValue;\n stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelTextColor\"] = normalizedColor;\n if (uiManager) {\n uiManager.applyStyles();\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.InfoPanelAccentColor\",\n name: \"🎨 Info Panel: Accent Color\",\n type: \"color\",\n defaultValue: settings[\"🔍MagnifyGlass.InfoPanelAccentColor\"],\n tooltip: \"Custom accent color for the info panel.\",\n onChange: (value: any) => {\n if (stateManager) {\n const strValue = String(value);\n const normalizedColor = strValue && !strValue.startsWith('#') ? `#${strValue}` : strValue;\n stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelAccentColor\"] = normalizedColor;\n if (uiManager) {\n uiManager.applyStyles();\n }\n }\n }\n });\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/shared/utils.js b/web/shared/utils.js new file mode 100644 index 0000000..f8d6f7d --- /dev/null +++ b/web/shared/utils.js @@ -0,0 +1,53 @@ +import { app } from "/scripts/app.js"; +function getSettingValue(key, defaultValue) { + try { + const value = app.ui.settings.getSettingValue(key); + return value === void 0 ? defaultValue : value; + } catch (e) { + console.warn(`ComfyUI Magnifying Glass: Could not get setting ${key}, using default ${defaultValue}. Error: ${e}`); + return defaultValue; + } +} +function isUserTyping() { + const activeElement = document.activeElement; + if (!activeElement) return false; + const tagName = activeElement.tagName.toLowerCase(); + if (tagName === "input" || tagName === "textarea") { + return true; + } + if (activeElement.contentEditable === "true") { + return true; + } + if (activeElement.closest("form") || activeElement.classList.contains("cm-editor") || // CodeMirror editor + activeElement.classList.contains("monaco-editor") || // Monaco editor + activeElement.closest(".cm-editor") || activeElement.closest(".monaco-editor")) { + return true; + } + return false; +} +function rectsOverlap(rect1, rect2) { + return rect1.x < rect2.x + rect2.width && rect1.x + rect1.width > rect2.x && rect1.y < rect2.y + rect2.height && rect1.y + rect1.height > rect2.y; +} +function findLiteGraphCanvas() { + var _a, _b; + const canvas = document.getElementById("graph-canvas"); + if (canvas) return canvas; + const graphCanvas = document.querySelector("canvas.graphcanvas"); + if (graphCanvas) return graphCanvas; + const appAny = app; + if ((_a = appAny == null ? void 0 : appAny.canvas_manager) == null ? void 0 : _a.container) { + const managerCanvas = appAny.canvas_manager.container.querySelector("canvas"); + if (managerCanvas) return managerCanvas; + } + if ((_b = appAny == null ? void 0 : appAny.canvas) == null ? void 0 : _b.graph_canvas) { + return appAny.canvas.graph_canvas; + } + return null; +} +export { + findLiteGraphCanvas, + getSettingValue, + isUserTyping, + rectsOverlap +}; +//# sourceMappingURL=utils.js.map diff --git a/web/shared/utils.js.map b/web/shared/utils.js.map new file mode 100644 index 0000000..ecd339a --- /dev/null +++ b/web/shared/utils.js.map @@ -0,0 +1 @@ +{"version":3,"file":"utils.js","sources":["../../src/shared/utils.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Shared Utilities\n * \n * Common utility functions shared between magnify glass and info panel modules.\n */\n\nimport type { ComfyApp } from '../types/comfyui';\n\n// Reference to ComfyUI's app - will be set at runtime\n// @ts-ignore\nimport { app } from \"/scripts/app.js\";\n\n/**\n * Rectangle interface for overlap calculations\n */\nexport interface Rectangle {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/**\n * Debug logger interface\n */\nexport interface DebugLogger {\n log(...args: unknown[]): void;\n error(...args: unknown[]): void;\n}\n\n/**\n * Safely get a setting value from ComfyUI settings with fallback to default.\n * @param key - The setting key\n * @param defaultValue - Default value if setting not found\n * @returns The setting value or default\n */\nexport function getSettingValue(key: string, defaultValue: T): T {\n try {\n const value = app.ui.settings.getSettingValue(key);\n return value === undefined ? defaultValue : value as T;\n } catch (e) {\n console.warn(`ComfyUI Magnifying Glass: Could not get setting ${key}, using default ${defaultValue}. Error: ${e}`);\n return defaultValue;\n }\n}\n\n/**\n * Check if the user is currently typing in an input field.\n * Used for smart input detection to avoid triggering hotkeys while typing.\n * @returns True if user is typing in an input field\n */\nexport function isUserTyping(): boolean {\n const activeElement = document.activeElement as HTMLElement | null;\n if (!activeElement) return false;\n\n // Check if the active element is an input, textarea, or contenteditable\n const tagName = activeElement.tagName.toLowerCase();\n if (tagName === 'input' || tagName === 'textarea') {\n return true;\n }\n\n // Check for contenteditable elements\n if (activeElement.contentEditable === 'true') {\n return true;\n }\n\n // Check if it's inside a form or has input-like classes\n if (activeElement.closest('form') ||\n activeElement.classList.contains('cm-editor') || // CodeMirror editor\n activeElement.classList.contains('monaco-editor') || // Monaco editor\n activeElement.closest('.cm-editor') ||\n activeElement.closest('.monaco-editor')) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Check if two rectangles overlap.\n * @param rect1 - First rectangle\n * @param rect2 - Second rectangle\n * @returns True if rectangles overlap\n */\nexport function rectsOverlap(rect1: Rectangle, rect2: Rectangle): boolean {\n return rect1.x < rect2.x + rect2.width &&\n rect1.x + rect1.width > rect2.x &&\n rect1.y < rect2.y + rect2.height &&\n rect1.y + rect1.height > rect2.y;\n}\n\n/**\n * Find the LiteGraph canvas element in the DOM.\n * Tries multiple selectors for compatibility with different ComfyUI versions.\n * @returns The canvas element or null if not found\n */\nexport function findLiteGraphCanvas(): HTMLCanvasElement | null {\n // Try multiple selectors for compatibility\n const canvas = document.getElementById(\"graph-canvas\") as HTMLCanvasElement | null;\n if (canvas) return canvas;\n\n const graphCanvas = document.querySelector(\"canvas.graphcanvas\") as HTMLCanvasElement | null;\n if (graphCanvas) return graphCanvas;\n\n // Try canvas manager (dynamic access for ComfyUI compatibility)\n const appAny = app as any;\n if (appAny?.canvas_manager?.container) {\n const managerCanvas = appAny.canvas_manager.container.querySelector(\"canvas\") as HTMLCanvasElement | null;\n if (managerCanvas) return managerCanvas;\n }\n\n // Try direct canvas reference\n if (appAny?.canvas?.graph_canvas) {\n return appAny.canvas.graph_canvas;\n }\n\n return null;\n}\n\n/**\n * Normalize a color value to include the # symbol for CSS.\n * @param color - The color value (with or without #)\n * @returns The normalized color with # prefix\n */\nexport function normalizeColor(color: string | null | undefined): string {\n if (!color) return color ?? '';\n return color.startsWith('#') ? color : `#${color}`;\n}\n\n/**\n * Clamp a value between min and max bounds.\n * @param value - The value to clamp\n * @param min - Minimum bound\n * @param max - Maximum bound\n * @returns The clamped value\n */\nexport function clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max);\n}\n\n/**\n * Create a debug logger that only logs when debug mode is enabled.\n * @param prefix - Prefix for log messages\n * @param isDebugEnabled - Function that returns whether debug is enabled\n * @returns Logger object\n */\nexport function createDebugLogger(\n prefix: string,\n isDebugEnabled: () => boolean\n): DebugLogger {\n return {\n log(...args: unknown[]): void {\n if (isDebugEnabled()) {\n console.log(`${prefix}:`, ...args);\n }\n },\n error(...args: unknown[]): void {\n console.error(`${prefix} ERROR:`, ...args);\n }\n };\n}\n"],"names":[],"mappings":";AAoCO,SAAS,gBAAmB,KAAa,cAAoB;AAChE,MAAI;AACA,UAAM,QAAQ,IAAI,GAAG,SAAS,gBAAgB,GAAG;AACjD,WAAO,UAAU,SAAY,eAAe;AAAA,EAChD,SAAS,GAAG;AACR,YAAQ,KAAK,mDAAmD,GAAG,mBAAmB,YAAY,YAAY,CAAC,EAAE;AACjH,WAAO;AAAA,EACX;AACJ;AAOO,SAAS,eAAwB;AACpC,QAAM,gBAAgB,SAAS;AAC/B,MAAI,CAAC,cAAe,QAAO;AAG3B,QAAM,UAAU,cAAc,QAAQ,YAAA;AACtC,MAAI,YAAY,WAAW,YAAY,YAAY;AAC/C,WAAO;AAAA,EACX;AAGA,MAAI,cAAc,oBAAoB,QAAQ;AAC1C,WAAO;AAAA,EACX;AAGA,MAAI,cAAc,QAAQ,MAAM,KAC5B,cAAc,UAAU,SAAS,WAAW;AAAA,EAC5C,cAAc,UAAU,SAAS,eAAe;AAAA,EAChD,cAAc,QAAQ,YAAY,KAClC,cAAc,QAAQ,gBAAgB,GAAG;AACzC,WAAO;AAAA,EACX;AAEA,SAAO;AACX;AAQO,SAAS,aAAa,OAAkB,OAA2B;AACtE,SAAO,MAAM,IAAI,MAAM,IAAI,MAAM,SAC7B,MAAM,IAAI,MAAM,QAAQ,MAAM,KAC9B,MAAM,IAAI,MAAM,IAAI,MAAM,UAC1B,MAAM,IAAI,MAAM,SAAS,MAAM;AACvC;AAOO,SAAS,sBAAgD;;AAE5D,QAAM,SAAS,SAAS,eAAe,cAAc;AACrD,MAAI,OAAQ,QAAO;AAEnB,QAAM,cAAc,SAAS,cAAc,oBAAoB;AAC/D,MAAI,YAAa,QAAO;AAGxB,QAAM,SAAS;AACf,OAAI,sCAAQ,mBAAR,mBAAwB,WAAW;AACnC,UAAM,gBAAgB,OAAO,eAAe,UAAU,cAAc,QAAQ;AAC5E,QAAI,cAAe,QAAO;AAAA,EAC9B;AAGA,OAAI,sCAAQ,WAAR,mBAAgB,cAAc;AAC9B,WAAO,OAAO,OAAO;AAAA,EACzB;AAEA,SAAO;AACX;"} \ No newline at end of file diff --git a/web/sidebar.css b/web/sidebar.css new file mode 100644 index 0000000..6f458a3 --- /dev/null +++ b/web/sidebar.css @@ -0,0 +1,352 @@ +/** + * ComfyUI MagnifyGlass - Sidebar CSS + * + * Styles for the sidebar settings and inspector panels. + */ + +.magnify-sidebar { + display: flex; + flex-direction: column; + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 13px; + color: var(--fg-color, #ddd); + background: transparent; + padding: 0; +} + +.magnify-sidebar-header { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; + border-bottom: 1px solid var(--border-color, #333); + background: rgba(0, 0, 0, 0.2); +} + +.magnify-sidebar-header svg { + width: 20px; + height: 20px; + fill: currentColor; +} + +.magnify-sidebar-header h2 { + margin: 0; + font-size: 14px; + font-weight: 600; +} + +.magnify-sidebar-content { + flex: 1; + overflow-y: auto; + padding: 8px 0; +} + +/* Section styling */ +.magnify-sidebar-section { + margin-bottom: 4px; +} + +.magnify-sidebar-section-header { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + cursor: pointer; + background: rgba(255, 255, 255, 0.03); + border-bottom: 1px solid var(--border-color, #333); + transition: background 0.15s; +} + +.magnify-sidebar-section-header:hover { + background: rgba(255, 255, 255, 0.06); +} + +.magnify-sidebar-section-header svg { + width: 12px; + height: 12px; + transition: transform 0.2s; +} + +.magnify-sidebar-section-header.collapsed svg { + transform: rotate(-90deg); +} + +.magnify-sidebar-section-header span { + font-weight: 500; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.magnify-sidebar-section-body { + padding: 12px 16px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.magnify-sidebar-section-body.collapsed { + display: none; +} + +/* Control row */ +.magnify-control-row { + display: flex; + flex-direction: column; + gap: 4px; +} + +.magnify-control-row label { + font-size: 11px; + color: var(--descrip-text, #999); + font-weight: 500; +} + +.magnify-control-row .control-value { + font-size: 11px; + color: var(--fg-color, #ddd); + margin-left: auto; +} + +.magnify-control-label-row { + display: flex; + justify-content: space-between; + align-items: center; +} + +/* Slider styling */ +.magnify-slider { + width: 100%; + height: 6px; + border-radius: 3px; + background: rgba(255, 255, 255, 0.1); + appearance: none; + cursor: pointer; +} + +.magnify-slider::-webkit-slider-thumb { + appearance: none; + width: 14px; + height: 14px; + border-radius: 50%; + background: var(--p-primary-color, #3b82f6); + cursor: pointer; + transition: transform 0.1s; +} + +.magnify-slider::-webkit-slider-thumb:hover { + transform: scale(1.15); +} + +.magnify-slider::-moz-range-thumb { + width: 14px; + height: 14px; + border-radius: 50%; + background: var(--p-primary-color, #3b82f6); + border: none; + cursor: pointer; +} + +/* Toggle styling */ +.magnify-toggle-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 0; +} + +.magnify-toggle-row label { + font-size: 12px; + color: var(--fg-color, #ddd); +} + +.magnify-toggle { + position: relative; + width: 36px; + height: 20px; + background: rgba(255, 255, 255, 0.1); + border-radius: 10px; + cursor: pointer; + transition: background 0.2s; +} + +.magnify-toggle.active { + background: var(--p-primary-color, #3b82f6); +} + +.magnify-toggle::after { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 16px; + height: 16px; + background: white; + border-radius: 50%; + transition: transform 0.2s; +} + +.magnify-toggle.active::after { + transform: translateX(16px); +} + +/* Select styling */ +.magnify-select { + width: 100%; + padding: 6px 10px; + border: 1px solid var(--border-color, #444); + border-radius: 4px; + background: rgba(0, 0, 0, 0.3); + color: var(--fg-color, #ddd); + font-size: 12px; + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23999' d='M6 8L2 4h8z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 8px center; +} + +.magnify-select:focus { + outline: none; + border-color: var(--p-primary-color, #3b82f6); +} + +/* Inspector styles */ +.magnify-inspector-section { + padding: 12px 16px; +} + +.magnify-inspector-group { + margin-bottom: 16px; +} + +.magnify-inspector-group:last-child { + margin-bottom: 0; +} + +.magnify-inspector-group-title { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--descrip-text, #888); + margin-bottom: 8px; + font-weight: 600; +} + +.magnify-inspector-row { + display: flex; + justify-content: space-between; + padding: 4px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.magnify-inspector-row:last-child { + border-bottom: none; +} + +.magnify-inspector-label { + color: var(--descrip-text, #999); + font-size: 12px; +} + +.magnify-inspector-value { + color: var(--fg-color, #ddd); + font-size: 12px; + font-family: monospace; + text-align: right; + max-width: 60%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.magnify-inspector-empty { + color: var(--descrip-text, #666); + font-style: italic; + font-size: 12px; + text-align: center; + padding: 20px 0; +} + +/* Node badge */ +.magnify-node-badge { + display: inline-block; + padding: 2px 8px; + background: var(--p-primary-color, #3b82f6); + border-radius: 4px; + font-size: 11px; + font-weight: 500; + margin-bottom: 8px; +} + +/* Button styling */ +.magnify-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 8px 12px; + border: none; + border-radius: 4px; + background: var(--p-primary-color, #3b82f6); + color: white; + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: background 0.15s, transform 0.1s; +} + +.magnify-btn:hover { + background: var(--p-primary-hover-color, #2563eb); +} + +.magnify-btn:active { + transform: scale(0.98); +} + +.magnify-btn-secondary { + background: rgba(255, 255, 255, 0.1); +} + +.magnify-btn-secondary:hover { + background: rgba(255, 255, 255, 0.15); +} + +/* Color picker styling */ +.magnify-color-row { + flex-direction: row; + align-items: center; + justify-content: space-between; +} + +.magnify-color-wrapper { + display: flex; + align-items: center; + gap: 8px; +} + +.magnify-color-input { + width: 28px; + height: 28px; + padding: 0; + border: 1px solid var(--border-color, #444); + border-radius: 4px; + cursor: pointer; + background: transparent; +} + +.magnify-color-input::-webkit-color-swatch-wrapper { + padding: 2px; +} + +.magnify-color-input::-webkit-color-swatch { + border-radius: 2px; + border: none; +} + +.magnify-color-preview { + font-size: 11px; + font-family: monospace; + color: var(--descrip-text, #999); +} \ No newline at end of file diff --git a/web/sidebar/SidebarInspector.js b/web/sidebar/SidebarInspector.js new file mode 100644 index 0000000..540a5ec --- /dev/null +++ b/web/sidebar/SidebarInspector.js @@ -0,0 +1,164 @@ +import { app } from "/scripts/app.js"; +const Icons = { + chevronDown: `` +}; +let inspectorBody = null; +let updateInterval = null; +function formatNumber(value, decimals = 0) { + return value.toFixed(decimals); +} +function getHoveredNode() { + try { + const graph = app.graph; + if (!graph || !graph.canvas) return null; + const canvas = graph.canvas; + const mousePos = canvas.graph_mouse; + if (!mousePos) return null; + const nodes = graph._nodes; + for (let i = nodes.length - 1; i >= 0; i--) { + const node = nodes[i]; + if (node.pos && node.size) { + const [x, y] = node.pos; + const [w, h] = node.size; + if (mousePos[0] >= x && mousePos[0] <= x + w && mousePos[1] >= y && mousePos[1] <= y + h) { + return node; + } + } + } + return null; + } catch { + return null; + } +} +function getCanvasInfo() { + var _a; + try { + const graph = app.graph; + if (!graph || !graph.canvas) { + return { scale: 1, cursor: { x: 0, y: 0 } }; + } + const canvas = graph.canvas; + const scale = ((_a = canvas.ds) == null ? void 0 : _a.scale) || 1; + const mousePos = canvas.graph_mouse || [0, 0]; + return { + scale: scale * 100, + cursor: { x: mousePos[0] || 0, y: mousePos[1] || 0 } + }; + } catch { + return { scale: 1, cursor: { x: 0, y: 0 } }; + } +} +function createRow(label, value) { + const row = document.createElement("div"); + row.className = "magnify-inspector-row"; + const labelEl = document.createElement("span"); + labelEl.className = "magnify-inspector-label"; + labelEl.textContent = label; + const valueEl = document.createElement("span"); + valueEl.className = "magnify-inspector-value"; + valueEl.textContent = value; + valueEl.title = value; + row.appendChild(labelEl); + row.appendChild(valueEl); + return row; +} +function createGroup(title) { + const group = document.createElement("div"); + group.className = "magnify-inspector-group"; + const titleEl = document.createElement("div"); + titleEl.className = "magnify-inspector-group-title"; + titleEl.textContent = title; + group.appendChild(titleEl); + return group; +} +function updateInspector() { + var _a, _b; + if (!inspectorBody) return; + const canvasInfo = getCanvasInfo(); + const hoveredNode = getHoveredNode(); + inspectorBody.innerHTML = ""; + const canvasGroup = createGroup("Canvas"); + canvasGroup.appendChild(createRow("Cursor", `(${formatNumber(canvasInfo.cursor.x)}, ${formatNumber(canvasInfo.cursor.y)})`)); + canvasGroup.appendChild(createRow("Scale", `${formatNumber(canvasInfo.scale, 1)}%`)); + inspectorBody.appendChild(canvasGroup); + const magnifyGlass = window.comfyUIMagnifyGlass; + if (magnifyGlass) { + const glassGroup = createGroup("Magnify Glass"); + glassGroup.appendChild(createRow("Zoom", `${formatNumber((((_a = magnifyGlass.config) == null ? void 0 : _a.zoomFactor) || 1) * 100)}%`)); + glassGroup.appendChild(createRow("Active", ((_b = magnifyGlass.state) == null ? void 0 : _b.active) ? "Yes" : "No")); + inspectorBody.appendChild(glassGroup); + } + if (hoveredNode) { + const nodeGroup = createGroup("Hovered Node"); + const badge = document.createElement("div"); + badge.className = "magnify-node-badge"; + badge.textContent = hoveredNode.type || "Unknown"; + nodeGroup.appendChild(badge); + nodeGroup.appendChild(createRow("Title", hoveredNode.title || "Untitled")); + nodeGroup.appendChild(createRow("ID", String(hoveredNode.id))); + if (hoveredNode.pos) { + nodeGroup.appendChild(createRow("Position", `(${formatNumber(hoveredNode.pos[0])}, ${formatNumber(hoveredNode.pos[1])})`)); + } + if (hoveredNode.size) { + nodeGroup.appendChild(createRow("Size", `${formatNumber(hoveredNode.size[0])} × ${formatNumber(hoveredNode.size[1])}`)); + } + if (hoveredNode.widgets && hoveredNode.widgets.length > 0) { + const widgetGroup = createGroup("Parameters"); + const widgetsToShow = hoveredNode.widgets.slice(0, 6); + for (const widget of widgetsToShow) { + if (widget.name && widget.value !== void 0) { + let value = String(widget.value); + if (value.length > 30) value = value.substring(0, 27) + "..."; + widgetGroup.appendChild(createRow(widget.name, value)); + } + } + if (hoveredNode.widgets.length > 6) { + const moreEl = document.createElement("div"); + moreEl.style.cssText = "font-size: 11px; color: #666; padding: 4px 0;"; + moreEl.textContent = `+${hoveredNode.widgets.length - 6} more...`; + widgetGroup.appendChild(moreEl); + } + inspectorBody.appendChild(widgetGroup); + } + inspectorBody.appendChild(nodeGroup); + } else { + const empty = document.createElement("div"); + empty.className = "magnify-inspector-empty"; + empty.textContent = "Hover over a node to inspect"; + inspectorBody.appendChild(empty); + } +} +function renderInspectorPanel(container) { + const section = document.createElement("div"); + section.className = "magnify-sidebar-section"; + const header = document.createElement("div"); + header.className = "magnify-sidebar-section-header"; + header.innerHTML = `${Icons.chevronDown}Inspector`; + const body = document.createElement("div"); + body.className = "magnify-sidebar-section-body magnify-inspector-section"; + inspectorBody = body; + header.addEventListener("click", () => { + header.classList.toggle("collapsed"); + body.classList.toggle("collapsed"); + }); + section.appendChild(header); + section.appendChild(body); + container.appendChild(section); + updateInspector(); + if (updateInterval) { + clearInterval(updateInterval); + } + updateInterval = window.setInterval(updateInspector, 100); +} +function cleanupInspector() { + if (updateInterval) { + clearInterval(updateInterval); + updateInterval = null; + } + inspectorBody = null; +} +export { + cleanupInspector, + renderInspectorPanel +}; +//# sourceMappingURL=SidebarInspector.js.map diff --git a/web/sidebar/SidebarInspector.js.map b/web/sidebar/SidebarInspector.js.map new file mode 100644 index 0000000..303e6e1 --- /dev/null +++ b/web/sidebar/SidebarInspector.js.map @@ -0,0 +1 @@ +{"version":3,"file":"SidebarInspector.js","sources":["../../src/sidebar/SidebarInspector.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Sidebar Inspector Panel\n * \n * Real-time inspector view for the sidebar.\n */\n\n// @ts-ignore\nimport { app } from \"/scripts/app.js\";\n\n/**\n * Icons for the inspector panel\n */\nconst Icons = {\n chevronDown: ``,\n};\n\n/**\n * Inspector state\n */\ninterface InspectorState {\n cursor: { x: number; y: number };\n canvasScale: number;\n hoveredNode: any | null;\n}\n\nlet inspectorState: InspectorState = {\n cursor: { x: 0, y: 0 },\n canvasScale: 1,\n hoveredNode: null\n};\n\nlet inspectorBody: HTMLElement | null = null;\nlet updateInterval: number | null = null;\n\n/**\n * Format a number for display\n */\nfunction formatNumber(value: number, decimals: number = 0): string {\n return value.toFixed(decimals);\n}\n\n/**\n * Get the currently hovered node from the graph\n */\nfunction getHoveredNode(): any | null {\n try {\n const graph = app.graph;\n if (!graph || !graph.canvas) return null;\n\n const canvas = graph.canvas;\n const mousePos = canvas.graph_mouse;\n\n if (!mousePos) return null;\n\n // Find node at mouse position\n const nodes = graph._nodes;\n for (let i = nodes.length - 1; i >= 0; i--) {\n const node = nodes[i];\n if (node.pos && node.size) {\n const [x, y] = node.pos;\n const [w, h] = node.size;\n if (mousePos[0] >= x && mousePos[0] <= x + w &&\n mousePos[1] >= y && mousePos[1] <= y + h) {\n return node;\n }\n }\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Get canvas information\n */\nfunction getCanvasInfo(): { scale: number; cursor: { x: number; y: number } } {\n try {\n const graph = app.graph;\n if (!graph || !graph.canvas) {\n return { scale: 1, cursor: { x: 0, y: 0 } };\n }\n\n const canvas = graph.canvas;\n const scale = canvas.ds?.scale || 1;\n const mousePos = canvas.graph_mouse || [0, 0];\n\n return {\n scale: scale * 100,\n cursor: { x: mousePos[0] || 0, y: mousePos[1] || 0 }\n };\n } catch {\n return { scale: 1, cursor: { x: 0, y: 0 } };\n }\n}\n\n/**\n * Create an inspector row\n */\nfunction createRow(label: string, value: string): HTMLElement {\n const row = document.createElement('div');\n row.className = 'magnify-inspector-row';\n\n const labelEl = document.createElement('span');\n labelEl.className = 'magnify-inspector-label';\n labelEl.textContent = label;\n\n const valueEl = document.createElement('span');\n valueEl.className = 'magnify-inspector-value';\n valueEl.textContent = value;\n valueEl.title = value;\n\n row.appendChild(labelEl);\n row.appendChild(valueEl);\n\n return row;\n}\n\n/**\n * Create a group with title\n */\nfunction createGroup(title: string): HTMLElement {\n const group = document.createElement('div');\n group.className = 'magnify-inspector-group';\n\n const titleEl = document.createElement('div');\n titleEl.className = 'magnify-inspector-group-title';\n titleEl.textContent = title;\n\n group.appendChild(titleEl);\n\n return group;\n}\n\n/**\n * Update the inspector display\n */\nfunction updateInspector(): void {\n if (!inspectorBody) return;\n\n const canvasInfo = getCanvasInfo();\n const hoveredNode = getHoveredNode();\n\n // Clear body\n inspectorBody.innerHTML = '';\n\n // Canvas info group\n const canvasGroup = createGroup('Canvas');\n canvasGroup.appendChild(createRow('Cursor', `(${formatNumber(canvasInfo.cursor.x)}, ${formatNumber(canvasInfo.cursor.y)})`));\n canvasGroup.appendChild(createRow('Scale', `${formatNumber(canvasInfo.scale, 1)}%`));\n inspectorBody.appendChild(canvasGroup);\n\n // MagnifyGlass info\n const magnifyGlass = window.comfyUIMagnifyGlass;\n if (magnifyGlass) {\n const glassGroup = createGroup('Magnify Glass');\n glassGroup.appendChild(createRow('Zoom', `${formatNumber((magnifyGlass.config?.zoomFactor || 1) * 100)}%`));\n glassGroup.appendChild(createRow('Active', magnifyGlass.state?.active ? 'Yes' : 'No'));\n inspectorBody.appendChild(glassGroup);\n }\n\n // Node info group\n if (hoveredNode) {\n const nodeGroup = createGroup('Hovered Node');\n\n // Node badge\n const badge = document.createElement('div');\n badge.className = 'magnify-node-badge';\n badge.textContent = hoveredNode.type || 'Unknown';\n nodeGroup.appendChild(badge);\n\n nodeGroup.appendChild(createRow('Title', hoveredNode.title || 'Untitled'));\n nodeGroup.appendChild(createRow('ID', String(hoveredNode.id)));\n\n // Position\n if (hoveredNode.pos) {\n nodeGroup.appendChild(createRow('Position', `(${formatNumber(hoveredNode.pos[0])}, ${formatNumber(hoveredNode.pos[1])})`));\n }\n\n // Size\n if (hoveredNode.size) {\n nodeGroup.appendChild(createRow('Size', `${formatNumber(hoveredNode.size[0])} × ${formatNumber(hoveredNode.size[1])}`));\n }\n\n // Widgets (first few)\n if (hoveredNode.widgets && hoveredNode.widgets.length > 0) {\n const widgetGroup = createGroup('Parameters');\n const widgetsToShow = hoveredNode.widgets.slice(0, 6);\n for (const widget of widgetsToShow) {\n if (widget.name && widget.value !== undefined) {\n let value = String(widget.value);\n if (value.length > 30) value = value.substring(0, 27) + '...';\n widgetGroup.appendChild(createRow(widget.name, value));\n }\n }\n if (hoveredNode.widgets.length > 6) {\n const moreEl = document.createElement('div');\n moreEl.style.cssText = 'font-size: 11px; color: #666; padding: 4px 0;';\n moreEl.textContent = `+${hoveredNode.widgets.length - 6} more...`;\n widgetGroup.appendChild(moreEl);\n }\n inspectorBody.appendChild(widgetGroup);\n }\n\n inspectorBody.appendChild(nodeGroup);\n } else {\n // Empty state\n const empty = document.createElement('div');\n empty.className = 'magnify-inspector-empty';\n empty.textContent = 'Hover over a node to inspect';\n inspectorBody.appendChild(empty);\n }\n}\n\n/**\n * Render the inspector panel into the container\n */\nexport function renderInspectorPanel(container: HTMLElement): void {\n const section = document.createElement('div');\n section.className = 'magnify-sidebar-section';\n\n // Section header\n const header = document.createElement('div');\n header.className = 'magnify-sidebar-section-header';\n header.innerHTML = `${Icons.chevronDown}Inspector`;\n\n // Section body\n const body = document.createElement('div');\n body.className = 'magnify-sidebar-section-body magnify-inspector-section';\n inspectorBody = body;\n\n // Toggle collapse\n header.addEventListener('click', () => {\n header.classList.toggle('collapsed');\n body.classList.toggle('collapsed');\n });\n\n section.appendChild(header);\n section.appendChild(body);\n container.appendChild(section);\n\n // Initial update\n updateInspector();\n\n // Start update interval\n if (updateInterval) {\n clearInterval(updateInterval);\n }\n updateInterval = window.setInterval(updateInspector, 100);\n}\n\n/**\n * Cleanup the inspector (call when sidebar is destroyed)\n */\nexport function cleanupInspector(): void {\n if (updateInterval) {\n clearInterval(updateInterval);\n updateInterval = null;\n }\n inspectorBody = null;\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/sidebar/SidebarSettings.js b/web/sidebar/SidebarSettings.js new file mode 100644 index 0000000..1dd781e --- /dev/null +++ b/web/sidebar/SidebarSettings.js @@ -0,0 +1,568 @@ +import { app } from "/scripts/app.js"; +import { GLASS_SHAPES, GLASS_POSITIONS, ACTIVATION_KEYS, RESET_KEYS, TOGGLE_FOLLOW_KEYS, PANEL_POSITIONS } from "../shared/constants.js"; +const Icons = { + chevronDown: ``, + reset: `` +}; +function createSlider(label, value, min, max, step, unit, onChange, tooltip, onInput) { + const row = document.createElement("div"); + row.className = "magnify-control-row"; + if (tooltip) row.title = tooltip; + const labelRow = document.createElement("div"); + labelRow.className = "magnify-control-label-row"; + const labelEl = document.createElement("label"); + labelEl.textContent = label; + const valueEl = document.createElement("span"); + valueEl.className = "control-value"; + valueEl.textContent = `${value}${unit}`; + labelRow.appendChild(labelEl); + labelRow.appendChild(valueEl); + const slider = document.createElement("input"); + slider.type = "range"; + slider.className = "magnify-slider"; + slider.min = String(min); + slider.max = String(max); + slider.step = String(step); + slider.value = String(value); + slider.addEventListener("mousedown", (e) => e.stopPropagation()); + slider.addEventListener("touchstart", (e) => e.stopPropagation()); + slider.addEventListener("pointerdown", (e) => e.stopPropagation()); + slider.addEventListener("input", (e) => { + e.stopPropagation(); + const newValue = parseFloat(slider.value); + valueEl.textContent = `${newValue}${unit}`; + if (onInput) onInput(newValue); + }); + slider.addEventListener("change", (e) => { + e.stopPropagation(); + const newValue = parseFloat(slider.value); + onChange(newValue); + }); + row.appendChild(labelRow); + row.appendChild(slider); + return row; +} +function createToggle(label, checked, onChange, tooltip) { + const row = document.createElement("div"); + row.className = "magnify-toggle-row"; + if (tooltip) row.title = tooltip; + const labelEl = document.createElement("label"); + labelEl.textContent = label; + const toggle = document.createElement("div"); + toggle.className = `magnify-toggle${checked ? " active" : ""}`; + toggle.addEventListener("click", () => { + const isActive = toggle.classList.toggle("active"); + onChange(isActive); + }); + row.appendChild(labelEl); + row.appendChild(toggle); + return row; +} +function createSelect(label, value, options, onChange, tooltip, onInput) { + const row = document.createElement("div"); + row.className = "magnify-control-row"; + if (tooltip) row.title = tooltip; + const labelEl = document.createElement("label"); + labelEl.textContent = label; + const select = document.createElement("select"); + select.className = "magnify-select"; + options.forEach((opt) => { + const option = document.createElement("option"); + option.value = opt; + option.textContent = opt; + if (opt === value) option.selected = true; + select.appendChild(option); + }); + select.addEventListener("change", () => { + if (onInput) { + onInput(select.value); + } + onChange(select.value); + }); + row.appendChild(labelEl); + row.appendChild(select); + return row; +} +function createColorPicker(label, value, onChange, tooltip, onInput) { + const row = document.createElement("div"); + row.className = "magnify-control-row magnify-color-row"; + if (tooltip) row.title = tooltip; + const labelEl = document.createElement("label"); + labelEl.textContent = label; + const colorWrapper = document.createElement("div"); + colorWrapper.className = "magnify-color-wrapper"; + const colorInput = document.createElement("input"); + colorInput.type = "color"; + colorInput.className = "magnify-color-input"; + colorInput.value = value; + const colorPreview = document.createElement("span"); + colorPreview.className = "magnify-color-preview"; + colorPreview.textContent = value; + colorInput.addEventListener("input", () => { + colorPreview.textContent = colorInput.value; + if (onInput) onInput(colorInput.value); + }); + colorInput.addEventListener("change", () => { + onChange(colorInput.value); + }); + colorWrapper.appendChild(colorInput); + colorWrapper.appendChild(colorPreview); + row.appendChild(labelEl); + row.appendChild(colorWrapper); + return row; +} +function getSettingValue(id, defaultValue) { + try { + const value = app.ui.settings.getSettingValue(id); + return value !== void 0 ? value : defaultValue; + } catch { + return defaultValue; + } +} +function setSettingValue(id, value) { + try { + app.ui.settings.setSettingValue(id, value); + } catch (e) { + console.warn(`Failed to set setting ${id}:`, e); + } +} +const SECTION_STATE_KEY = "magnifyglass-sidebar-sections"; +function getSectionStates() { + try { + const saved = localStorage.getItem(SECTION_STATE_KEY); + return saved ? JSON.parse(saved) : {}; + } catch { + return {}; + } +} +function saveSectionState(title, collapsed) { + try { + const states = getSectionStates(); + states[title] = collapsed; + localStorage.setItem(SECTION_STATE_KEY, JSON.stringify(states)); + } catch { + } +} +function createSection(title, defaultCollapsed = false) { + const section = document.createElement("div"); + section.className = "magnify-sidebar-section"; + const savedStates = getSectionStates(); + const collapsed = savedStates[title] !== void 0 ? savedStates[title] : defaultCollapsed; + const header = document.createElement("div"); + header.className = `magnify-sidebar-section-header${collapsed ? " collapsed" : ""}`; + header.innerHTML = `${Icons.chevronDown}${title}`; + const body = document.createElement("div"); + body.className = `magnify-sidebar-section-body${collapsed ? " collapsed" : ""}`; + header.addEventListener("click", () => { + const isCollapsed = header.classList.toggle("collapsed"); + body.classList.toggle("collapsed"); + saveSectionState(title, isCollapsed); + }); + section.appendChild(header); + section.appendChild(body); + return { section, body }; +} +function renderSettingsPanel(container) { + const activationKey = getSettingValue("🔍MagnifyGlass.ActivationKey", "x").toUpperCase(); + const resetKey = getSettingValue("🔍MagnifyGlass.ResetKey", "o").toUpperCase(); + const toggleFollowKey = getSettingValue("🔍MagnifyGlass.ToggleFollowCursorKey", "h").toUpperCase(); + const glassSection = createSection("Glass Settings"); + glassSection.body.appendChild(createSlider( + "Zoom Factor", + getSettingValue("🔍MagnifyGlass.ZoomFactor", 300), + 100, + 1e3, + 25, + "%", + (value) => setSettingValue("🔍MagnifyGlass.ZoomFactor", value), + "Magnification level of the glass (100-1000%)", + (value) => { + const mg = window.comfyUIMagnifyGlass; + if (mg == null ? void 0 : mg.config) { + mg.config.zoomFactor = value / 100; + if (mg.state.active) mg.updateMagnifiedView(); + } + } + )); + glassSection.body.appendChild(createSlider( + "Glass Size", + getSettingValue("🔍MagnifyGlass.GlassSize", 250), + 50, + 500, + 10, + "px", + (value) => setSettingValue("🔍MagnifyGlass.GlassSize", value), + "Size of the magnifying glass in pixels", + (value) => { + const mg = window.comfyUIMagnifyGlass; + if (mg == null ? void 0 : mg.config) { + mg.config.glassSize = value; + mg.applyUiChanges(); + if (mg.state.active) mg.updateMagnifiedView(); + } + } + )); + glassSection.body.appendChild(createSelect( + "Shape", + getSettingValue("🔍MagnifyGlass.GlassShape", "Rounded Square"), + GLASS_SHAPES, + (value) => setSettingValue("🔍MagnifyGlass.GlassShape", value), + "Shape of the magnifying glass" + )); + glassSection.body.appendChild(createSelect( + "Position", + getSettingValue("🔍MagnifyGlass.GlassPosition", "Top-Right"), + GLASS_POSITIONS, + (value) => setSettingValue("🔍MagnifyGlass.GlassPosition", value), + `Default position of the glass. Press ${resetKey} to reset.` + )); + glassSection.body.appendChild(createSelect( + "Filtering", + getSettingValue("🔍MagnifyGlass.TextureFiltering", "Linear"), + ["Linear", "Nearest"], + (value) => setSettingValue("🔍MagnifyGlass.TextureFiltering", value), + "Texture filtering: Linear (smooth) or Nearest (pixelated)" + )); + glassSection.body.appendChild(createSlider( + "Border Width", + getSettingValue("🔍MagnifyGlass.BorderWidth", 2), + 0, + 10, + 0.5, + "px", + (value) => setSettingValue("🔍MagnifyGlass.BorderWidth", value), + "Width of the glass border in pixels", + (value) => { + const mg = window.comfyUIMagnifyGlass; + if (mg == null ? void 0 : mg.config) { + mg.config.borderWidth = value; + mg.applyUiChanges(); + } + } + )); + glassSection.body.appendChild(createColorPicker( + "Border Color", + getSettingValue("🔍MagnifyGlass.BorderColor", "#ffffff"), + (value) => setSettingValue("🔍MagnifyGlass.BorderColor", value), + "Color of the glass border", + (value) => { + const mg = window.comfyUIMagnifyGlass; + if (mg == null ? void 0 : mg.config) { + mg.config.borderColor = value; + mg.applyUiChanges(); + } + } + )); + glassSection.body.appendChild(createToggle( + "Show Border", + getSettingValue("🔍MagnifyGlass.BorderEnabled", true), + (checked) => setSettingValue("🔍MagnifyGlass.BorderEnabled", checked), + "Show or hide the glass border" + )); + glassSection.body.appendChild(createToggle( + "Follow Cursor", + getSettingValue("🔍MagnifyGlass.FollowCursor", false), + (checked) => setSettingValue("🔍MagnifyGlass.FollowCursor", checked), + `Glass follows the cursor. Hotkey: ${toggleFollowKey}` + )); + glassSection.body.appendChild(createToggle( + "Always Active Mode", + getSettingValue("🔍MagnifyGlass.AlwaysActiveMode", true), + (checked) => setSettingValue("🔍MagnifyGlass.AlwaysActiveMode", checked), + `When enabled, glass stays visible. Toggle with ${activationKey}` + )); + container.appendChild(glassSection.section); + const hotkeySection = createSection("Hotkeys", true); + hotkeySection.body.appendChild(createSelect( + "Activation Key", + getSettingValue("🔍MagnifyGlass.ActivationKey", "x"), + ACTIVATION_KEYS, + (value) => setSettingValue("🔍MagnifyGlass.ActivationKey", value), + "Key to toggle or hold to activate the magnifying glass" + )); + hotkeySection.body.appendChild(createSelect( + "Reset Key", + getSettingValue("🔍MagnifyGlass.ResetKey", "o"), + RESET_KEYS, + (value) => setSettingValue("🔍MagnifyGlass.ResetKey", value), + "Key to reset glass position and disable follow cursor" + )); + hotkeySection.body.appendChild(createSelect( + "Toggle Follow Key", + getSettingValue("🔍MagnifyGlass.ToggleFollowCursorKey", "h"), + TOGGLE_FOLLOW_KEYS, + (value) => setSettingValue("🔍MagnifyGlass.ToggleFollowCursorKey", value), + "Key to toggle follow cursor mode" + )); + hotkeySection.body.appendChild(createToggle( + "Require Alt Key", + getSettingValue("🔍MagnifyGlass.AltRequired", false), + (checked) => setSettingValue("🔍MagnifyGlass.AltRequired", checked), + "Require Alt key to be held with activation key" + )); + hotkeySection.body.appendChild(createSlider( + "Offset Step", + getSettingValue("🔍MagnifyGlass.OffsetStep", 10), + 1, + 50, + 1, + "px", + (value) => setSettingValue("🔍MagnifyGlass.OffsetStep", value), + "Amount to move glass per arrow key press (pixels)" + )); + container.appendChild(hotkeySection.section); + const panelSection = createSection("Info Panel", true); + const toggleHotkey = getSettingValue("🔍MagnifyGlass.ToggleHotkey", "i").toUpperCase(); + getSettingValue("🔍MagnifyGlass.GlassPreviewToggleHotkey", "g").toUpperCase(); + getSettingValue("🔍MagnifyGlass.PinPanelHotkey", "u").toUpperCase(); + panelSection.body.appendChild(createToggle( + "Enable Panel", + getSettingValue("🔍MagnifyGlass.InfoPanelEnabled", true), + (checked) => setSettingValue("🔍MagnifyGlass.InfoPanelEnabled", checked), + `Show info panel with node details. Toggle: ${toggleHotkey}` + )); + panelSection.body.appendChild(createSelect( + "Position", + getSettingValue("🔍MagnifyGlass.InfoPanelPosition", "Bottom"), + PANEL_POSITIONS, + (value) => setSettingValue("🔍MagnifyGlass.InfoPanelPosition", value), + "Position of the info panel relative to the glass" + )); + panelSection.body.appendChild(createSlider( + "Width", + getSettingValue("🔍MagnifyGlass.InfoPanelWidth", 300), + 200, + 600, + 20, + "px", + (value) => setSettingValue("🔍MagnifyGlass.InfoPanelWidth", value), + "Width of the info panel", + (value) => { + var _a, _b; + const infoPanel = window.infoPanelManager; + if ((_b = (_a = infoPanel == null ? void 0 : infoPanel.stateManager) == null ? void 0 : _a.state) == null ? void 0 : _b.settings) { + infoPanel.stateManager.state.settings["🔍MagnifyGlass.InfoPanelWidth"] = value; + infoPanel.uiManager.applyStyles(); + if (infoPanel.magnifyGlass.state.active) { + infoPanel.uiManager.show(); + infoPanel.positionManager.positionPanel(); + } + } + } + )); + panelSection.body.appendChild(createSlider( + "Max Height", + getSettingValue("🔍MagnifyGlass.InfoPanelMaxHeight", 300), + 200, + 1500, + 50, + "px", + (value) => setSettingValue("🔍MagnifyGlass.InfoPanelMaxHeight", value), + "Maximum height of the info panel", + (value) => { + var _a, _b; + console.log("[MaxHeight] Handler called with value:", value); + const infoPanel = window.infoPanelManager; + console.log("[MaxHeight] infoPanelManager:", infoPanel ? "found" : "NOT FOUND"); + if ((_b = (_a = infoPanel == null ? void 0 : infoPanel.stateManager) == null ? void 0 : _a.state) == null ? void 0 : _b.settings) { + console.log("[MaxHeight] Updating setting and calling applyStyles"); + infoPanel.stateManager.state.settings["🔍MagnifyGlass.InfoPanelMaxHeight"] = value; + infoPanel.uiManager.applyStyles(); + if (infoPanel.magnifyGlass.state.active) { + console.log("[MaxHeight] Glass is active, calling show() and positionPanel()"); + infoPanel.uiManager.show(); + infoPanel.positionManager.positionPanel(); + } else { + console.log("[MaxHeight] Glass is NOT active, skipping show()"); + } + } else { + console.warn("[MaxHeight] Check failed - stateManager/state/settings missing"); + } + } + )); + panelSection.body.appendChild(createSlider( + "Opacity", + getSettingValue("🔍MagnifyGlass.InfoPanelOpacity", 95), + 10, + 100, + 5, + "%", + (value) => setSettingValue("🔍MagnifyGlass.InfoPanelOpacity", value), + "Background opacity of the info panel", + (value) => { + var _a, _b; + const infoPanel = window.infoPanelManager; + if ((_b = (_a = infoPanel == null ? void 0 : infoPanel.stateManager) == null ? void 0 : _a.state) == null ? void 0 : _b.settings) { + infoPanel.stateManager.state.settings["🔍MagnifyGlass.InfoPanelOpacity"] = value; + infoPanel.uiManager.applyStyles(); + } + } + )); + panelSection.body.appendChild(createColorPicker( + "Text Color", + getSettingValue("🔍MagnifyGlass.InfoPanelTextColor", "#6b7280"), + (value) => setSettingValue("🔍MagnifyGlass.InfoPanelTextColor", value), + "Text color in the info panel", + (value) => { + var _a, _b; + const infoPanel = window.infoPanelManager; + if ((_b = (_a = infoPanel == null ? void 0 : infoPanel.stateManager) == null ? void 0 : _a.state) == null ? void 0 : _b.settings) { + infoPanel.stateManager.state.settings["🔍MagnifyGlass.InfoPanelTextColor"] = value; + infoPanel.uiManager.applyStyles(); + } + } + )); + panelSection.body.appendChild(createColorPicker( + "Accent Color", + getSettingValue("🔍MagnifyGlass.InfoPanelAccentColor", "#3b82f6"), + (value) => setSettingValue("🔍MagnifyGlass.InfoPanelAccentColor", value), + "Accent color for highlights", + (value) => { + var _a, _b; + const infoPanel = window.infoPanelManager; + if ((_b = (_a = infoPanel == null ? void 0 : infoPanel.stateManager) == null ? void 0 : _a.state) == null ? void 0 : _b.settings) { + infoPanel.stateManager.state.settings["🔍MagnifyGlass.InfoPanelAccentColor"] = value; + infoPanel.uiManager.applyStyles(); + } + } + )); + panelSection.body.appendChild(createToggle( + "Animations", + getSettingValue("🔍MagnifyGlass.InfoPanelAnimations", false), + (checked) => setSettingValue("🔍MagnifyGlass.InfoPanelAnimations", checked), + "Enable smooth animations for panel updates" + )); + panelSection.body.appendChild(createToggle( + "Hover Controls", + getSettingValue("🔍MagnifyGlass.ShowHoveringControls", true), + (checked) => setSettingValue("🔍MagnifyGlass.ShowHoveringControls", checked), + "Show floating control buttons near the panel" + )); + panelSection.body.appendChild(createSelect( + "Controls Position", + getSettingValue("🔍MagnifyGlass.ControlsPosition", "left"), + ["top", "bottom", "left", "right", "top-left", "top-right", "bottom-left", "bottom-right"], + (value) => setSettingValue("🔍MagnifyGlass.ControlsPosition", value), + "Position of floating control buttons", + (value) => { + var _a, _b; + const infoPanel = window.infoPanelManager; + if ((_b = (_a = infoPanel == null ? void 0 : infoPanel.stateManager) == null ? void 0 : _a.state) == null ? void 0 : _b.settings) { + infoPanel.stateManager.state.settings["🔍MagnifyGlass.ControlsPosition"] = value; + infoPanel.uiManager.updateControlsLayout(value); + if (infoPanel.magnifyGlass.state.active) { + infoPanel.positionManager.positionFloatingControls(); + } + } + } + )); + container.appendChild(panelSection.section); + const panelHotkeySection = createSection("Panel Hotkeys", true); + panelHotkeySection.body.appendChild(createSelect( + "Toggle Key", + getSettingValue("🔍MagnifyGlass.ToggleHotkey", "i"), + ["i", "j", "k", "l", "n", "m"], + (value) => setSettingValue("🔍MagnifyGlass.ToggleHotkey", value), + "Key to toggle info panel visibility" + )); + panelHotkeySection.body.appendChild(createSelect( + "Glass Toggle Key", + getSettingValue("🔍MagnifyGlass.GlassPreviewToggleHotkey", "g"), + ["g", "f", "v", "b"], + (value) => setSettingValue("🔍MagnifyGlass.GlassPreviewToggleHotkey", value), + "Key to toggle glass preview in panel" + )); + panelHotkeySection.body.appendChild(createSelect( + "Pin Key", + getSettingValue("🔍MagnifyGlass.PinPanelHotkey", "u"), + ["u", "p", "y", "t"], + (value) => setSettingValue("🔍MagnifyGlass.PinPanelHotkey", value), + "Key to pin/unpin the info panel position" + )); + container.appendChild(panelHotkeySection.section); + const debugSection = createSection("Debug", true); + debugSection.body.appendChild(createToggle( + "Debug Mode", + getSettingValue("🔍MagnifyGlass.DebugMode", false), + (checked) => setSettingValue("🔍MagnifyGlass.DebugMode", checked), + "Enable debug logging and visual indicators" + )); + container.appendChild(debugSection.section); + const buttonRow = document.createElement("div"); + buttonRow.style.padding = "16px"; + buttonRow.style.borderTop = "1px solid var(--border-color, #333)"; + buttonRow.style.display = "flex"; + buttonRow.style.flexDirection = "column"; + buttonRow.style.gap = "8px"; + const resetBtn = document.createElement("button"); + resetBtn.className = "magnify-btn"; + resetBtn.style.width = "100%"; + resetBtn.innerHTML = `${Icons.reset} Reset Position`; + resetBtn.title = `Reset glass position and disable follow cursor. Hotkey: ${resetKey}`; + resetBtn.addEventListener("click", (e) => { + e.stopPropagation(); + const magnifyGlass = window.comfyUIMagnifyGlass; + if (magnifyGlass && magnifyGlass.resetOffsets) { + magnifyGlass.resetOffsets(); + const followToggles = container.querySelectorAll(".magnify-toggle-row"); + followToggles.forEach((row) => { + const label = row.querySelector("label"); + if (label && label.textContent === "Follow Cursor") { + const toggle = row.querySelector(".magnify-toggle"); + if (toggle) toggle.classList.remove("active"); + } + }); + } + }); + buttonRow.appendChild(resetBtn); + const resetAllBtn = document.createElement("button"); + resetAllBtn.className = "magnify-btn magnify-btn-secondary"; + resetAllBtn.style.width = "100%"; + resetAllBtn.innerHTML = `${Icons.reset} Reset All Settings`; + resetAllBtn.title = "Reset all settings to their default values"; + resetAllBtn.addEventListener("click", (e) => { + e.stopPropagation(); + if (!confirm("Reset all MagnifyGlass settings to defaults?")) { + return; + } + setSettingValue("🔍MagnifyGlass.ZoomFactor", 300); + setSettingValue("🔍MagnifyGlass.GlassSize", 300); + setSettingValue("🔍MagnifyGlass.GlassShape", "Rounded Square"); + setSettingValue("🔍MagnifyGlass.GlassPosition", "Top-Right"); + setSettingValue("🔍MagnifyGlass.TextureFiltering", "Linear"); + setSettingValue("🔍MagnifyGlass.BorderWidth", 1); + setSettingValue("🔍MagnifyGlass.BorderColor", "#6b7280"); + setSettingValue("🔍MagnifyGlass.BorderEnabled", true); + setSettingValue("🔍MagnifyGlass.FollowCursor", false); + setSettingValue("🔍MagnifyGlass.AlwaysActiveMode", true); + setSettingValue("🔍MagnifyGlass.ActivationKey", "x"); + setSettingValue("🔍MagnifyGlass.ResetKey", "o"); + setSettingValue("🔍MagnifyGlass.ToggleFollowCursorKey", "h"); + setSettingValue("🔍MagnifyGlass.AltRequired", false); + setSettingValue("🔍MagnifyGlass.OffsetStep", 5); + setSettingValue("🔍MagnifyGlass.DebugMode", false); + setSettingValue("🔍MagnifyGlass.InfoPanelEnabled", true); + setSettingValue("🔍MagnifyGlass.InfoPanelPosition", "Bottom"); + setSettingValue("🔍MagnifyGlass.InfoPanelWidth", 300); + setSettingValue("🔍MagnifyGlass.InfoPanelMaxHeight", 300); + setSettingValue("🔍MagnifyGlass.InfoPanelOpacity", 100); + setSettingValue("🔍MagnifyGlass.InfoPanelTextColor", "#6b7280"); + setSettingValue("🔍MagnifyGlass.InfoPanelAccentColor", "#3b82f6"); + setSettingValue("🔍MagnifyGlass.InfoPanelAnimations", false); + setSettingValue("🔍MagnifyGlass.ShowHoveringControls", true); + setSettingValue("🔍MagnifyGlass.ControlsPosition", "left"); + setSettingValue("🔍MagnifyGlass.ToggleHotkey", "i"); + setSettingValue("🔍MagnifyGlass.GlassPreviewToggleHotkey", "g"); + setSettingValue("🔍MagnifyGlass.PinPanelHotkey", "u"); + const magnifyGlass = window.comfyUIMagnifyGlass; + if (magnifyGlass && magnifyGlass.resetOffsets) { + magnifyGlass.resetOffsets(); + } + }); + buttonRow.appendChild(resetAllBtn); + container.appendChild(buttonRow); +} +export { + renderSettingsPanel +}; +//# sourceMappingURL=SidebarSettings.js.map diff --git a/web/sidebar/SidebarSettings.js.map b/web/sidebar/SidebarSettings.js.map new file mode 100644 index 0000000..a60ddab --- /dev/null +++ b/web/sidebar/SidebarSettings.js.map @@ -0,0 +1 @@ +{"version":3,"file":"SidebarSettings.js","sources":["../../src/sidebar/SidebarSettings.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Sidebar Settings Panel\n * \n * All settings controls for the sidebar.\n */\n\n// @ts-ignore\nimport { app } from \"/scripts/app.js\";\nimport {\n GLASS_POSITIONS,\n GLASS_SHAPES,\n ACTIVATION_KEYS,\n RESET_KEYS,\n TOGGLE_FOLLOW_KEYS,\n PANEL_POSITIONS\n} from '../shared/constants';\n\n/**\n * Icons for the settings panel\n */\nconst Icons = {\n chevronDown: ``,\n reset: ``,\n};\n\n/**\n * Create a slider control with tooltip\n */\n/**\n * Create a slider control with tooltip\n */\nfunction createSlider(\n label: string,\n value: number,\n min: number,\n max: number,\n step: number,\n unit: string,\n onChange: (value: number) => void,\n tooltip?: string,\n onInput?: (value: number) => void\n): HTMLElement {\n const row = document.createElement('div');\n row.className = 'magnify-control-row';\n if (tooltip) row.title = tooltip;\n\n const labelRow = document.createElement('div');\n labelRow.className = 'magnify-control-label-row';\n\n const labelEl = document.createElement('label');\n labelEl.textContent = label;\n\n const valueEl = document.createElement('span');\n valueEl.className = 'control-value';\n valueEl.textContent = `${value}${unit}`;\n\n labelRow.appendChild(labelEl);\n labelRow.appendChild(valueEl);\n\n const slider = document.createElement('input');\n slider.type = 'range';\n slider.className = 'magnify-slider';\n slider.min = String(min);\n slider.max = String(max);\n slider.step = String(step);\n slider.value = String(value);\n\n // Prevent ComfyUI from capturing mouse events during drag\n slider.addEventListener('mousedown', (e) => e.stopPropagation());\n slider.addEventListener('touchstart', (e) => e.stopPropagation());\n slider.addEventListener('pointerdown', (e) => e.stopPropagation());\n\n slider.addEventListener('input', (e) => {\n e.stopPropagation();\n const newValue = parseFloat(slider.value);\n valueEl.textContent = `${newValue}${unit}`;\n if (onInput) onInput(newValue);\n });\n\n slider.addEventListener('change', (e) => {\n e.stopPropagation();\n const newValue = parseFloat(slider.value);\n onChange(newValue);\n });\n\n row.appendChild(labelRow);\n row.appendChild(slider);\n\n return row;\n}\n\n/**\n * Create a toggle control with tooltip\n */\nfunction createToggle(\n label: string,\n checked: boolean,\n onChange: (checked: boolean) => void,\n tooltip?: string\n): HTMLElement {\n const row = document.createElement('div');\n row.className = 'magnify-toggle-row';\n if (tooltip) row.title = tooltip;\n\n const labelEl = document.createElement('label');\n labelEl.textContent = label;\n\n const toggle = document.createElement('div');\n toggle.className = `magnify-toggle${checked ? ' active' : ''}`;\n\n toggle.addEventListener('click', () => {\n const isActive = toggle.classList.toggle('active');\n onChange(isActive);\n });\n\n row.appendChild(labelEl);\n row.appendChild(toggle);\n\n return row;\n}\n\n/**\n * Create a select dropdown control with tooltip\n */\nfunction createSelect(\n label: string,\n value: string,\n options: readonly string[] | string[],\n onChange: (value: string) => void,\n tooltip?: string,\n onInput?: (value: string) => void\n): HTMLElement {\n const row = document.createElement('div');\n row.className = 'magnify-control-row';\n if (tooltip) row.title = tooltip;\n\n const labelEl = document.createElement('label');\n labelEl.textContent = label;\n\n const select = document.createElement('select');\n select.className = 'magnify-select';\n\n options.forEach(opt => {\n const option = document.createElement('option');\n option.value = opt;\n option.textContent = opt;\n if (opt === value) option.selected = true;\n select.appendChild(option);\n });\n\n select.addEventListener('change', () => {\n // Call onInput first for live preview\n if (onInput) {\n onInput(select.value);\n }\n // Then persist the setting\n onChange(select.value);\n });\n\n row.appendChild(labelEl);\n row.appendChild(select);\n\n return row;\n}\n\n/**\n * Create a color picker control with tooltip\n */\n/**\n * Create a color picker control with tooltip\n */\nfunction createColorPicker(\n label: string,\n value: string,\n onChange: (value: string) => void,\n tooltip?: string,\n onInput?: (value: string) => void\n): HTMLElement {\n const row = document.createElement('div');\n row.className = 'magnify-control-row magnify-color-row';\n if (tooltip) row.title = tooltip;\n\n const labelEl = document.createElement('label');\n labelEl.textContent = label;\n\n const colorWrapper = document.createElement('div');\n colorWrapper.className = 'magnify-color-wrapper';\n\n const colorInput = document.createElement('input');\n colorInput.type = 'color';\n colorInput.className = 'magnify-color-input';\n colorInput.value = value;\n\n const colorPreview = document.createElement('span');\n colorPreview.className = 'magnify-color-preview';\n colorPreview.textContent = value;\n\n colorInput.addEventListener('input', () => {\n colorPreview.textContent = colorInput.value;\n if (onInput) onInput(colorInput.value);\n });\n\n colorInput.addEventListener('change', () => {\n onChange(colorInput.value);\n });\n\n colorWrapper.appendChild(colorInput);\n colorWrapper.appendChild(colorPreview);\n\n row.appendChild(labelEl);\n row.appendChild(colorWrapper);\n\n return row;\n}\n\n/**\n * Get current MagnifyGlass settings from app.ui.settings\n */\nfunction getSettingValue(id: string, defaultValue: any): any {\n try {\n const value = app.ui.settings.getSettingValue(id);\n return value !== undefined ? value : defaultValue;\n } catch {\n return defaultValue;\n }\n}\n\n/**\n * Set a MagnifyGlass setting via app.ui.settings\n */\nfunction setSettingValue(id: string, value: any): void {\n try {\n app.ui.settings.setSettingValue(id, value);\n } catch (e) {\n console.warn(`Failed to set setting ${id}:`, e);\n }\n}\n\n/** Storage key for section states */\nconst SECTION_STATE_KEY = 'magnifyglass-sidebar-sections';\n\n/**\n * Get saved section states from localStorage\n */\nfunction getSectionStates(): Record {\n try {\n const saved = localStorage.getItem(SECTION_STATE_KEY);\n return saved ? JSON.parse(saved) : {};\n } catch {\n return {};\n }\n}\n\n/**\n * Save section state to localStorage\n */\nfunction saveSectionState(title: string, collapsed: boolean): void {\n try {\n const states = getSectionStates();\n states[title] = collapsed;\n localStorage.setItem(SECTION_STATE_KEY, JSON.stringify(states));\n } catch {\n // Ignore storage errors\n }\n}\n\n/**\n * Create a collapsible section with persistent state\n */\nfunction createSection(title: string, defaultCollapsed: boolean = false): { section: HTMLElement; body: HTMLElement } {\n const section = document.createElement('div');\n section.className = 'magnify-sidebar-section';\n\n // Check if we have a saved state for this section\n const savedStates = getSectionStates();\n const collapsed = savedStates[title] !== undefined ? savedStates[title] : defaultCollapsed;\n\n const header = document.createElement('div');\n header.className = `magnify-sidebar-section-header${collapsed ? ' collapsed' : ''}`;\n header.innerHTML = `${Icons.chevronDown}${title}`;\n\n const body = document.createElement('div');\n body.className = `magnify-sidebar-section-body${collapsed ? ' collapsed' : ''}`;\n\n header.addEventListener('click', () => {\n const isCollapsed = header.classList.toggle('collapsed');\n body.classList.toggle('collapsed');\n // Save the new state\n saveSectionState(title, isCollapsed);\n });\n\n section.appendChild(header);\n section.appendChild(body);\n\n return { section, body };\n}\n\n/**\n * Render the settings panel into the container\n */\nexport function renderSettingsPanel(container: HTMLElement): void {\n // Get current hotkey values for tooltips\n const activationKey = getSettingValue('🔍MagnifyGlass.ActivationKey', 'x').toUpperCase();\n const resetKey = getSettingValue('🔍MagnifyGlass.ResetKey', 'o').toUpperCase();\n const toggleFollowKey = getSettingValue('🔍MagnifyGlass.ToggleFollowCursorKey', 'h').toUpperCase();\n\n // ===== Glass Settings Section =====\n const glassSection = createSection('Glass Settings');\n\n glassSection.body.appendChild(createSlider('Zoom Factor',\n getSettingValue('🔍MagnifyGlass.ZoomFactor', 300), 100, 1000, 25, '%',\n (value) => setSettingValue('🔍MagnifyGlass.ZoomFactor', value),\n 'Magnification level of the glass (100-1000%)',\n (value) => {\n const mg = (window as any).comfyUIMagnifyGlass;\n if (mg?.config) {\n mg.config.zoomFactor = value / 100;\n if (mg.state.active) mg.updateMagnifiedView();\n }\n }\n ));\n\n glassSection.body.appendChild(createSlider('Glass Size',\n getSettingValue('🔍MagnifyGlass.GlassSize', 250), 50, 500, 10, 'px',\n (value) => setSettingValue('🔍MagnifyGlass.GlassSize', value),\n 'Size of the magnifying glass in pixels',\n (value) => {\n const mg = (window as any).comfyUIMagnifyGlass;\n if (mg?.config) {\n mg.config.glassSize = value;\n mg.applyUiChanges();\n if (mg.state.active) mg.updateMagnifiedView();\n }\n }\n ));\n\n glassSection.body.appendChild(createSelect('Shape',\n getSettingValue('🔍MagnifyGlass.GlassShape', 'Rounded Square'), GLASS_SHAPES,\n (value) => setSettingValue('🔍MagnifyGlass.GlassShape', value),\n 'Shape of the magnifying glass'\n ));\n\n glassSection.body.appendChild(createSelect('Position',\n getSettingValue('🔍MagnifyGlass.GlassPosition', 'Top-Right'), GLASS_POSITIONS,\n (value) => setSettingValue('🔍MagnifyGlass.GlassPosition', value),\n `Default position of the glass. Press ${resetKey} to reset.`\n ));\n\n glassSection.body.appendChild(createSelect('Filtering',\n getSettingValue('🔍MagnifyGlass.TextureFiltering', 'Linear'), ['Linear', 'Nearest'],\n (value) => setSettingValue('🔍MagnifyGlass.TextureFiltering', value),\n 'Texture filtering: Linear (smooth) or Nearest (pixelated)'\n ));\n\n glassSection.body.appendChild(createSlider('Border Width',\n getSettingValue('🔍MagnifyGlass.BorderWidth', 2), 0, 10, 0.5, 'px',\n (value) => setSettingValue('🔍MagnifyGlass.BorderWidth', value),\n 'Width of the glass border in pixels',\n (value) => {\n const mg = (window as any).comfyUIMagnifyGlass;\n if (mg?.config) {\n mg.config.borderWidth = value;\n mg.applyUiChanges();\n }\n }\n ));\n\n glassSection.body.appendChild(createColorPicker('Border Color',\n getSettingValue('🔍MagnifyGlass.BorderColor', '#ffffff'),\n (value) => setSettingValue('🔍MagnifyGlass.BorderColor', value),\n 'Color of the glass border',\n (value) => {\n const mg = (window as any).comfyUIMagnifyGlass;\n if (mg?.config) {\n mg.config.borderColor = value;\n mg.applyUiChanges();\n }\n }\n ));\n\n glassSection.body.appendChild(createToggle('Show Border',\n getSettingValue('🔍MagnifyGlass.BorderEnabled', true),\n (checked) => setSettingValue('🔍MagnifyGlass.BorderEnabled', checked),\n 'Show or hide the glass border'\n ));\n\n glassSection.body.appendChild(createToggle('Follow Cursor',\n getSettingValue('🔍MagnifyGlass.FollowCursor', false),\n (checked) => setSettingValue('🔍MagnifyGlass.FollowCursor', checked),\n `Glass follows the cursor. Hotkey: ${toggleFollowKey}`\n ));\n\n glassSection.body.appendChild(createToggle('Always Active Mode',\n getSettingValue('🔍MagnifyGlass.AlwaysActiveMode', true),\n (checked) => setSettingValue('🔍MagnifyGlass.AlwaysActiveMode', checked),\n `When enabled, glass stays visible. Toggle with ${activationKey}`\n ));\n\n container.appendChild(glassSection.section);\n\n // ===== Hotkeys Section =====\n const hotkeySection = createSection('Hotkeys', true);\n\n hotkeySection.body.appendChild(createSelect('Activation Key',\n getSettingValue('🔍MagnifyGlass.ActivationKey', 'x'), ACTIVATION_KEYS,\n (value) => setSettingValue('🔍MagnifyGlass.ActivationKey', value),\n 'Key to toggle or hold to activate the magnifying glass'\n ));\n\n hotkeySection.body.appendChild(createSelect('Reset Key',\n getSettingValue('🔍MagnifyGlass.ResetKey', 'o'), RESET_KEYS,\n (value) => setSettingValue('🔍MagnifyGlass.ResetKey', value),\n 'Key to reset glass position and disable follow cursor'\n ));\n\n hotkeySection.body.appendChild(createSelect('Toggle Follow Key',\n getSettingValue('🔍MagnifyGlass.ToggleFollowCursorKey', 'h'), TOGGLE_FOLLOW_KEYS,\n (value) => setSettingValue('🔍MagnifyGlass.ToggleFollowCursorKey', value),\n 'Key to toggle follow cursor mode'\n ));\n\n hotkeySection.body.appendChild(createToggle('Require Alt Key',\n getSettingValue('🔍MagnifyGlass.AltRequired', false),\n (checked) => setSettingValue('🔍MagnifyGlass.AltRequired', checked),\n 'Require Alt key to be held with activation key'\n ));\n\n hotkeySection.body.appendChild(createSlider('Offset Step',\n getSettingValue('🔍MagnifyGlass.OffsetStep', 10), 1, 50, 1, 'px',\n (value) => setSettingValue('🔍MagnifyGlass.OffsetStep', value),\n 'Amount to move glass per arrow key press (pixels)'\n ));\n\n container.appendChild(hotkeySection.section);\n\n // ===== Info Panel Section =====\n const panelSection = createSection('Info Panel', true);\n\n const toggleHotkey = getSettingValue('🔍MagnifyGlass.ToggleHotkey', 'i').toUpperCase();\n const glassToggleKey = getSettingValue('🔍MagnifyGlass.GlassPreviewToggleHotkey', 'g').toUpperCase();\n const pinHotkeyVal = getSettingValue('🔍MagnifyGlass.PinPanelHotkey', 'u').toUpperCase();\n\n panelSection.body.appendChild(createToggle('Enable Panel',\n getSettingValue('🔍MagnifyGlass.InfoPanelEnabled', true),\n (checked) => setSettingValue('🔍MagnifyGlass.InfoPanelEnabled', checked),\n `Show info panel with node details. Toggle: ${toggleHotkey}`\n ));\n\n panelSection.body.appendChild(createSelect('Position',\n getSettingValue('🔍MagnifyGlass.InfoPanelPosition', 'Bottom'), PANEL_POSITIONS,\n (value) => setSettingValue('🔍MagnifyGlass.InfoPanelPosition', value),\n 'Position of the info panel relative to the glass'\n ));\n\n panelSection.body.appendChild(createSlider('Width',\n getSettingValue('🔍MagnifyGlass.InfoPanelWidth', 300), 200, 600, 20, 'px',\n (value) => setSettingValue('🔍MagnifyGlass.InfoPanelWidth', value),\n 'Width of the info panel',\n (value) => {\n const infoPanel = (window as any).infoPanelManager;\n if (infoPanel?.stateManager?.state?.settings) {\n infoPanel.stateManager.state.settings['🔍MagnifyGlass.InfoPanelWidth'] = value;\n infoPanel.uiManager.applyStyles();\n\n // Ensure visible and reflow if active\n if (infoPanel.magnifyGlass.state.active) {\n infoPanel.uiManager.show();\n infoPanel.positionManager.positionPanel();\n }\n }\n }\n ));\n\n panelSection.body.appendChild(createSlider('Max Height',\n getSettingValue('🔍MagnifyGlass.InfoPanelMaxHeight', 300), 200, 1500, 50, 'px',\n (value) => setSettingValue('🔍MagnifyGlass.InfoPanelMaxHeight', value),\n 'Maximum height of the info panel',\n (value) => {\n console.log('[MaxHeight] Handler called with value:', value);\n const infoPanel = (window as any).infoPanelManager;\n console.log('[MaxHeight] infoPanelManager:', infoPanel ? 'found' : 'NOT FOUND');\n if (infoPanel?.stateManager?.state?.settings) {\n console.log('[MaxHeight] Updating setting and calling applyStyles');\n infoPanel.stateManager.state.settings['🔍MagnifyGlass.InfoPanelMaxHeight'] = value;\n infoPanel.uiManager.applyStyles();\n\n // Ensure visible and reflow if active\n if (infoPanel.magnifyGlass.state.active) {\n console.log('[MaxHeight] Glass is active, calling show() and positionPanel()');\n infoPanel.uiManager.show();\n infoPanel.positionManager.positionPanel();\n } else {\n console.log('[MaxHeight] Glass is NOT active, skipping show()');\n }\n } else {\n console.warn('[MaxHeight] Check failed - stateManager/state/settings missing');\n }\n }\n ));\n\n panelSection.body.appendChild(createSlider('Opacity',\n getSettingValue('🔍MagnifyGlass.InfoPanelOpacity', 95), 10, 100, 5, '%',\n (value) => setSettingValue('🔍MagnifyGlass.InfoPanelOpacity', value),\n 'Background opacity of the info panel',\n (value) => {\n const infoPanel = (window as any).infoPanelManager;\n if (infoPanel?.stateManager?.state?.settings) {\n infoPanel.stateManager.state.settings['🔍MagnifyGlass.InfoPanelOpacity'] = value;\n infoPanel.uiManager.applyStyles();\n }\n }\n ));\n\n panelSection.body.appendChild(createColorPicker('Text Color',\n getSettingValue('🔍MagnifyGlass.InfoPanelTextColor', '#6b7280'),\n (value) => setSettingValue('🔍MagnifyGlass.InfoPanelTextColor', value),\n 'Text color in the info panel',\n (value) => {\n const infoPanel = (window as any).infoPanelManager;\n if (infoPanel?.stateManager?.state?.settings) {\n infoPanel.stateManager.state.settings['🔍MagnifyGlass.InfoPanelTextColor'] = value;\n infoPanel.uiManager.applyStyles();\n }\n }\n ));\n\n panelSection.body.appendChild(createColorPicker('Accent Color',\n getSettingValue('🔍MagnifyGlass.InfoPanelAccentColor', '#3b82f6'),\n (value) => setSettingValue('🔍MagnifyGlass.InfoPanelAccentColor', value),\n 'Accent color for highlights',\n (value) => {\n const infoPanel = (window as any).infoPanelManager;\n if (infoPanel?.stateManager?.state?.settings) {\n infoPanel.stateManager.state.settings['🔍MagnifyGlass.InfoPanelAccentColor'] = value;\n infoPanel.uiManager.applyStyles();\n }\n }\n ));\n\n panelSection.body.appendChild(createToggle('Animations',\n getSettingValue('🔍MagnifyGlass.InfoPanelAnimations', false),\n (checked) => setSettingValue('🔍MagnifyGlass.InfoPanelAnimations', checked),\n 'Enable smooth animations for panel updates'\n ));\n\n panelSection.body.appendChild(createToggle('Hover Controls',\n getSettingValue('🔍MagnifyGlass.ShowHoveringControls', true),\n (checked) => setSettingValue('🔍MagnifyGlass.ShowHoveringControls', checked),\n 'Show floating control buttons near the panel'\n ));\n\n panelSection.body.appendChild(createSelect('Controls Position',\n getSettingValue('🔍MagnifyGlass.ControlsPosition', 'left'),\n ['top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right'],\n (value) => setSettingValue('🔍MagnifyGlass.ControlsPosition', value),\n 'Position of floating control buttons',\n (value) => {\n // Live update controls position\n const infoPanel = (window as any).infoPanelManager;\n if (infoPanel?.stateManager?.state?.settings) {\n infoPanel.stateManager.state.settings['🔍MagnifyGlass.ControlsPosition'] = value;\n infoPanel.uiManager.updateControlsLayout(value);\n if (infoPanel.magnifyGlass.state.active) {\n infoPanel.positionManager.positionFloatingControls();\n }\n }\n }\n ));\n\n container.appendChild(panelSection.section);\n\n // ===== Panel Hotkeys Section =====\n const panelHotkeySection = createSection('Panel Hotkeys', true);\n\n panelHotkeySection.body.appendChild(createSelect('Toggle Key',\n getSettingValue('🔍MagnifyGlass.ToggleHotkey', 'i'), ['i', 'j', 'k', 'l', 'n', 'm'],\n (value) => setSettingValue('🔍MagnifyGlass.ToggleHotkey', value),\n 'Key to toggle info panel visibility'\n ));\n\n panelHotkeySection.body.appendChild(createSelect('Glass Toggle Key',\n getSettingValue('🔍MagnifyGlass.GlassPreviewToggleHotkey', 'g'), ['g', 'f', 'v', 'b'],\n (value) => setSettingValue('🔍MagnifyGlass.GlassPreviewToggleHotkey', value),\n 'Key to toggle glass preview in panel'\n ));\n\n panelHotkeySection.body.appendChild(createSelect('Pin Key',\n getSettingValue('🔍MagnifyGlass.PinPanelHotkey', 'u'), ['u', 'p', 'y', 't'],\n (value) => setSettingValue('🔍MagnifyGlass.PinPanelHotkey', value),\n 'Key to pin/unpin the info panel position'\n ));\n\n container.appendChild(panelHotkeySection.section);\n\n // ===== Debug Section =====\n const debugSection = createSection('Debug', true);\n\n debugSection.body.appendChild(createToggle('Debug Mode',\n getSettingValue('🔍MagnifyGlass.DebugMode', false),\n (checked) => setSettingValue('🔍MagnifyGlass.DebugMode', checked),\n 'Enable debug logging and visual indicators'\n ));\n\n container.appendChild(debugSection.section);\n\n // ===== Button Row =====\n const buttonRow = document.createElement('div');\n buttonRow.style.padding = '16px';\n buttonRow.style.borderTop = '1px solid var(--border-color, #333)';\n buttonRow.style.display = 'flex';\n buttonRow.style.flexDirection = 'column';\n buttonRow.style.gap = '8px';\n\n // Reset Position Button\n const resetBtn = document.createElement('button');\n resetBtn.className = 'magnify-btn';\n resetBtn.style.width = '100%';\n resetBtn.innerHTML = `${Icons.reset} Reset Position`;\n resetBtn.title = `Reset glass position and disable follow cursor. Hotkey: ${resetKey}`;\n\n resetBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n const magnifyGlass = window.comfyUIMagnifyGlass;\n if (magnifyGlass && magnifyGlass.resetOffsets) {\n magnifyGlass.resetOffsets();\n // Update follow cursor toggle in UI\n const followToggles = container.querySelectorAll('.magnify-toggle-row');\n followToggles.forEach(row => {\n const label = row.querySelector('label');\n if (label && label.textContent === 'Follow Cursor') {\n const toggle = row.querySelector('.magnify-toggle');\n if (toggle) toggle.classList.remove('active');\n }\n });\n }\n });\n\n buttonRow.appendChild(resetBtn);\n\n // Reset All Settings Button\n const resetAllBtn = document.createElement('button');\n resetAllBtn.className = 'magnify-btn magnify-btn-secondary';\n resetAllBtn.style.width = '100%';\n resetAllBtn.innerHTML = `${Icons.reset} Reset All Settings`;\n resetAllBtn.title = 'Reset all settings to their default values';\n\n resetAllBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n\n // Confirm with user\n if (!confirm('Reset all MagnifyGlass settings to defaults?')) {\n return;\n }\n\n // Default glass settings\n setSettingValue('🔍MagnifyGlass.ZoomFactor', 300);\n setSettingValue('🔍MagnifyGlass.GlassSize', 300);\n setSettingValue('🔍MagnifyGlass.GlassShape', 'Rounded Square');\n setSettingValue('🔍MagnifyGlass.GlassPosition', 'Top-Right');\n setSettingValue('🔍MagnifyGlass.TextureFiltering', 'Linear');\n setSettingValue('🔍MagnifyGlass.BorderWidth', 1);\n setSettingValue('🔍MagnifyGlass.BorderColor', '#6b7280');\n setSettingValue('🔍MagnifyGlass.BorderEnabled', true);\n setSettingValue('🔍MagnifyGlass.FollowCursor', false);\n setSettingValue('🔍MagnifyGlass.AlwaysActiveMode', true);\n setSettingValue('🔍MagnifyGlass.ActivationKey', 'x');\n setSettingValue('🔍MagnifyGlass.ResetKey', 'o');\n setSettingValue('🔍MagnifyGlass.ToggleFollowCursorKey', 'h');\n setSettingValue('🔍MagnifyGlass.AltRequired', false);\n setSettingValue('🔍MagnifyGlass.OffsetStep', 5);\n setSettingValue('🔍MagnifyGlass.DebugMode', false);\n\n // Default panel settings\n setSettingValue('🔍MagnifyGlass.InfoPanelEnabled', true);\n setSettingValue('🔍MagnifyGlass.InfoPanelPosition', 'Bottom');\n setSettingValue('🔍MagnifyGlass.InfoPanelWidth', 300);\n setSettingValue('🔍MagnifyGlass.InfoPanelMaxHeight', 300);\n setSettingValue('🔍MagnifyGlass.InfoPanelOpacity', 100);\n setSettingValue('🔍MagnifyGlass.InfoPanelTextColor', '#6b7280');\n setSettingValue('🔍MagnifyGlass.InfoPanelAccentColor', '#3b82f6');\n setSettingValue('🔍MagnifyGlass.InfoPanelAnimations', false);\n setSettingValue('🔍MagnifyGlass.ShowHoveringControls', true);\n setSettingValue('🔍MagnifyGlass.ControlsPosition', 'left');\n setSettingValue('🔍MagnifyGlass.ToggleHotkey', 'i');\n setSettingValue('🔍MagnifyGlass.GlassPreviewToggleHotkey', 'g');\n setSettingValue('🔍MagnifyGlass.PinPanelHotkey', 'u');\n\n // Also reset position\n const magnifyGlass = window.comfyUIMagnifyGlass;\n if (magnifyGlass && magnifyGlass.resetOffsets) {\n magnifyGlass.resetOffsets();\n }\n });\n\n buttonRow.appendChild(resetAllBtn);\n container.appendChild(buttonRow);\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/sidebar/sidebar.js b/web/sidebar/sidebar.js new file mode 100644 index 0000000..3c5cc02 --- /dev/null +++ b/web/sidebar/sidebar.js @@ -0,0 +1,64 @@ +import { app } from "/scripts/app.js"; +import { renderSettingsPanel } from "./SidebarSettings.js"; +const MAGNIFY_ICON = ``; +let sidebarRegistered = false; +function loadSidebarStyles() { + if (!document.getElementById("magnify-sidebar-styles")) { + const link = document.createElement("link"); + link.id = "magnify-sidebar-styles"; + link.rel = "stylesheet"; + link.type = "text/css"; + link.href = "extensions/comfyui-magnifyglass/sidebar.css"; + document.head.appendChild(link); + } +} +function renderSidebar(container) { + loadSidebarStyles(); + container.innerHTML = ""; + const sidebar = document.createElement("div"); + sidebar.className = "magnify-sidebar"; + const header = document.createElement("div"); + header.className = "magnify-sidebar-header"; + header.innerHTML = `${MAGNIFY_ICON}

Magnify G.

`; + sidebar.appendChild(header); + const content = document.createElement("div"); + content.className = "magnify-sidebar-content"; + renderSettingsPanel(content); + sidebar.appendChild(content); + container.appendChild(sidebar); +} +function registerSidebar() { + if (sidebarRegistered) { + return; + } + if (!app.extensionManager) { + console.warn("MagnifyGlass: extensionManager not available, sidebar registration skipped"); + return; + } + try { + app.extensionManager.registerSidebarTab({ + id: "magnifyglass", + icon: "pi pi-search", + title: "Magnify G.", + tooltip: "MagnifyGlass Settings", + type: "custom", + render: (el) => { + renderSidebar(el); + } + }); + sidebarRegistered = true; + console.log("MagnifyGlass: Sidebar registered successfully"); + } catch (e) { + console.warn("MagnifyGlass: Failed to register sidebar:", e); + } +} +function initSidebar() { + setTimeout(() => { + registerSidebar(); + }, 100); +} +export { + initSidebar, + registerSidebar +}; +//# sourceMappingURL=sidebar.js.map diff --git a/web/sidebar/sidebar.js.map b/web/sidebar/sidebar.js.map new file mode 100644 index 0000000..6a044cc --- /dev/null +++ b/web/sidebar/sidebar.js.map @@ -0,0 +1 @@ +{"version":3,"file":"sidebar.js","sources":["../../src/sidebar/sidebar.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Sidebar Entry Point\n * \n * Registers the MagnifyGlass sidebar tab with ComfyUI.\n */\n\n// @ts-ignore\nimport { app } from \"/scripts/app.js\";\nimport { renderSettingsPanel } from './SidebarSettings';\n\n/**\n * Magnify glass icon SVG\n */\nconst MAGNIFY_ICON = ``;\n\n/** Track if sidebar has been registered */\nlet sidebarRegistered = false;\n\n/**\n * Load sidebar CSS\n */\nfunction loadSidebarStyles(): void {\n if (!document.getElementById('magnify-sidebar-styles')) {\n const link = document.createElement('link');\n link.id = 'magnify-sidebar-styles';\n link.rel = 'stylesheet';\n link.type = 'text/css';\n link.href = 'extensions/comfyui-magnifyglass/sidebar.css';\n document.head.appendChild(link);\n }\n}\n\n/**\n * Render the sidebar content\n */\nfunction renderSidebar(container: HTMLElement): void {\n // Load styles\n loadSidebarStyles();\n\n // Clear existing content to prevent duplicates\n container.innerHTML = '';\n\n // Create main container\n const sidebar = document.createElement('div');\n sidebar.className = 'magnify-sidebar';\n\n // Header\n const header = document.createElement('div');\n header.className = 'magnify-sidebar-header';\n header.innerHTML = `${MAGNIFY_ICON}

Magnify G.

`;\n sidebar.appendChild(header);\n\n // Content area\n const content = document.createElement('div');\n content.className = 'magnify-sidebar-content';\n\n // Render settings panel (contains all settings)\n renderSettingsPanel(content);\n\n sidebar.appendChild(content);\n container.appendChild(sidebar);\n}\n\n/**\n * Register the sidebar tab with ComfyUI\n */\nexport function registerSidebar(): void {\n // Prevent duplicate registration\n if (sidebarRegistered) {\n return;\n }\n\n // Wait for app to be ready\n if (!app.extensionManager) {\n console.warn('MagnifyGlass: extensionManager not available, sidebar registration skipped');\n return;\n }\n\n try {\n app.extensionManager.registerSidebarTab({\n id: 'magnifyglass',\n icon: 'pi pi-search',\n title: 'Magnify G.',\n tooltip: 'MagnifyGlass Settings',\n type: 'custom',\n render: (el: HTMLElement) => {\n renderSidebar(el);\n }\n });\n\n sidebarRegistered = true;\n console.log('MagnifyGlass: Sidebar registered successfully');\n } catch (e) {\n console.warn('MagnifyGlass: Failed to register sidebar:', e);\n }\n}\n\n/**\n * Initialize sidebar (called after app setup)\n */\nexport function initSidebar(): void {\n // Register on next tick to ensure app is ready\n setTimeout(() => {\n registerSidebar();\n }, 100);\n}\n"],"names":[],"mappings":";;AAaA,MAAM,eAAe;AAGrB,IAAI,oBAAoB;AAKxB,SAAS,oBAA0B;AAC/B,MAAI,CAAC,SAAS,eAAe,wBAAwB,GAAG;AACpD,UAAM,OAAO,SAAS,cAAc,MAAM;AAC1C,SAAK,KAAK;AACV,SAAK,MAAM;AACX,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,aAAS,KAAK,YAAY,IAAI;AAAA,EAClC;AACJ;AAKA,SAAS,cAAc,WAA8B;AAEjD,oBAAA;AAGA,YAAU,YAAY;AAGtB,QAAM,UAAU,SAAS,cAAc,KAAK;AAC5C,UAAQ,YAAY;AAGpB,QAAM,SAAS,SAAS,cAAc,KAAK;AAC3C,SAAO,YAAY;AACnB,SAAO,YAAY,GAAG,YAAY;AAClC,UAAQ,YAAY,MAAM;AAG1B,QAAM,UAAU,SAAS,cAAc,KAAK;AAC5C,UAAQ,YAAY;AAGpB,sBAAoB,OAAO;AAE3B,UAAQ,YAAY,OAAO;AAC3B,YAAU,YAAY,OAAO;AACjC;AAKO,SAAS,kBAAwB;AAEpC,MAAI,mBAAmB;AACnB;AAAA,EACJ;AAGA,MAAI,CAAC,IAAI,kBAAkB;AACvB,YAAQ,KAAK,4EAA4E;AACzF;AAAA,EACJ;AAEA,MAAI;AACA,QAAI,iBAAiB,mBAAmB;AAAA,MACpC,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,OAAO;AAAA,MACP,SAAS;AAAA,MACT,MAAM;AAAA,MACN,QAAQ,CAAC,OAAoB;AACzB,sBAAc,EAAE;AAAA,MACpB;AAAA,IAAA,CACH;AAED,wBAAoB;AACpB,YAAQ,IAAI,+CAA+C;AAAA,EAC/D,SAAS,GAAG;AACR,YAAQ,KAAK,6CAA6C,CAAC;AAAA,EAC/D;AACJ;AAKO,SAAS,cAAoB;AAEhC,aAAW,MAAM;AACb,oBAAA;AAAA,EACJ,GAAG,GAAG;AACV;"} \ No newline at end of file