From 048426796c7330fc901cf1638f9895a1ffe300c5 Mon Sep 17 00:00:00 2001 From: camenduru <54370274+camenduru@users.noreply.github.com> Date: Thu, 8 Aug 2024 12:31:01 +0300 Subject: [PATCH] test --- .ci/update_windows/update.py | 117 - .ci/update_windows/update_comfyui.bat | 8 - .../README_VERY_IMPORTANT.txt | 31 - .ci/windows_base_files/run_cpu.bat | 2 - .ci/windows_base_files/run_nvidia_gpu.bat | 2 - .github/ISSUE_TEMPLATE/bug-report.yml | 48 - .github/ISSUE_TEMPLATE/config.yml | 8 - .github/ISSUE_TEMPLATE/feature-request.yml | 32 - .github/ISSUE_TEMPLATE/user-support.yml | 32 - .github/workflows/pylint.yml | 23 - .github/workflows/stable-release.yml | 104 - .github/workflows/test-browser.yml | 76 - .github/workflows/test-build.yml | 31 - .github/workflows/test-ui.yaml | 30 - .../windows_release_dependencies.yml | 71 - .../windows_release_nightly_pytorch.yml | 90 - .github/workflows/windows_release_package.yml | 100 - .gitignore | 21 - .pylintrc | 3 - CODEOWNERS | 1 - CONTRIBUTING.md | 41 - LICENSE | 674 - README.md | 224 - app/__init__.py | 0 app/app_settings.py | 54 - app/frontend_management.py | 188 - app/user_manager.py | 205 - comfy/cldm/cldm.py | 6 +- comfy/cldm/mmdit.py | 6 +- comfy/cli_args.py | 16 +- comfy/clip_model.py | 8 +- comfy/clip_vision.py | 28 +- comfy/conds.py | 6 +- comfy/controlnet.py | 106 +- comfy/diffusers_load.py | 10 +- comfy/gligen.py | 4 +- comfy/k_diffusion/sampling.py | 8 +- comfy/ldm/audio/autoencoder.py | 4 +- comfy/ldm/audio/dit.py | 14 +- comfy/ldm/audio/embedders.py | 4 +- comfy/ldm/aura/mmdit.py | 12 +- comfy/ldm/cascade/common.py | 8 +- comfy/ldm/cascade/controlnet.py | 2 +- comfy/ldm/cascade/stage_a.py | 2 +- comfy/ldm/cascade/stage_b.py | 2 +- comfy/ldm/cascade/stage_c.py | 2 +- comfy/ldm/cascade/stage_c_coder.py | 2 +- comfy/ldm/flux/layers.py | 4 +- comfy/ldm/flux/math.py | 6 +- comfy/ldm/flux/model.py | 4 +- comfy/ldm/hydit/attn_layers.py | 2 +- comfy/ldm/hydit/models.py | 8 +- comfy/ldm/hydit/poolers.py | 6 +- comfy/ldm/models/autoencoder.py | 18 +- comfy/ldm/modules/attention.py | 8 +- comfy/ldm/modules/diffusionmodules/mmdit.py | 8 +- comfy/ldm/modules/diffusionmodules/model.py | 6 +- .../modules/diffusionmodules/openaimodel.py | 6 +- .../ldm/modules/diffusionmodules/upscaling.py | 2 +- comfy/ldm/modules/diffusionmodules/util.py | 2 +- comfy/ldm/modules/sub_quadratic_attention.py | 2 +- comfy/ldm/modules/temporal_ae.py | 4 +- comfy/lora.py | 18 +- comfy/model_base.py | 136 +- comfy/model_detection.py | 14 +- comfy/model_management.py | 4 +- comfy/model_patcher.py | 100 +- comfy/model_sampling.py | 2 +- comfy/ops.py | 80 +- comfy/sample.py | 20 +- comfy/sampler_helpers.py | 10 +- comfy/samplers.py | 16 +- comfy/sd.py | 150 +- comfy/sd1_clip.py | 8 +- comfy/sd1_tokenizer/vocab.json | 2 +- comfy/sdxl_clip.py | 2 +- comfy/supported_models.py | 24 +- comfy/taesd/taesd.py | 12 +- comfy/text_encoders/aura_t5.py | 6 +- comfy/text_encoders/bert.py | 8 +- comfy/text_encoders/flux.py | 10 +- comfy/text_encoders/hydit.py | 6 +- comfy/text_encoders/sa_t5.py | 6 +- comfy/text_encoders/sd2_clip.py | 2 +- comfy/text_encoders/sd3_clip.py | 20 +- comfy/text_encoders/t5.py | 6 +- .../text_encoders/t5_tokenizer/tokenizer.json | 2 +- comfy/types.py | 2 +- comfy/utils.py | 4 +- comfy_extras/chainner_models/model_loading.py | 2 +- comfy_extras/nodes_advanced_samplers.py | 18 +- comfy_extras/nodes_audio.py | 12 +- comfy_extras/nodes_canny.py | 6 +- comfy_extras/nodes_compositing.py | 8 +- comfy_extras/nodes_controlnet.py | 2 +- comfy_extras/nodes_custom_sampler.py | 58 +- comfy_extras/nodes_hypernetwork.py | 4 +- comfy_extras/nodes_images.py | 8 +- comfy_extras/nodes_latent.py | 8 +- comfy_extras/nodes_mask.py | 6 +- comfy_extras/nodes_model_advanced.py | 44 +- comfy_extras/nodes_model_downscale.py | 6 +- comfy_extras/nodes_model_merging.py | 46 +- .../nodes_model_merging_model_specific.py | 10 +- comfy_extras/nodes_morphology.py | 6 +- comfy_extras/nodes_pag.py | 10 +- comfy_extras/nodes_perpneg.py | 18 +- comfy_extras/nodes_photomaker.py | 26 +- comfy_extras/nodes_post_processing.py | 16 +- comfy_extras/nodes_sag.py | 8 +- comfy_extras/nodes_sd3.py | 8 +- comfy_extras/nodes_sdupscale.py | 4 +- comfy_extras/nodes_stable3d.py | 10 +- comfy_extras/nodes_stable_cascade.py | 6 +- comfy_extras/nodes_upscale_model.py | 14 +- comfy_extras/nodes_video_model.py | 16 +- comfyui_screenshot.png | Bin 118577 -> 0 bytes cuda_malloc.py | 90 - custom_nodes/example_node.py.example | 128 - custom_nodes/websocket_image_save.py | 45 - execution.py | 869 - extra_model_paths.yaml.example | 42 - fix_torch.py | 24 - input/example.png | Bin 8589 -> 0 bytes latent_preview.py | 12 +- main.py | 268 - models/checkpoints/put_checkpoints_here | 0 .../clip/put_clip_or_text_encoder_models_here | 0 .../clip_vision/put_clip_vision_models_here | 0 models/configs/anything_v3.yaml | 73 - models/configs/v1-inference.yaml | 70 - models/configs/v1-inference_clip_skip_2.yaml | 73 - .../v1-inference_clip_skip_2_fp16.yaml | 74 - models/configs/v1-inference_fp16.yaml | 71 - models/configs/v1-inpainting-inference.yaml | 71 - models/configs/v2-inference-v.yaml | 68 - models/configs/v2-inference-v_fp32.yaml | 68 - models/configs/v2-inference.yaml | 67 - models/configs/v2-inference_fp32.yaml | 67 - models/configs/v2-inpainting-inference.yaml | 158 - .../controlnet/put_controlnets_and_t2i_here | 0 models/diffusers/put_diffusers_models_here | 0 ...eddings_or_textual_inversion_concepts_here | 0 models/gligen/put_gligen_models_here | 0 models/hypernetworks/put_hypernetworks_here | 0 models/loras/put_loras_here | 0 models/photomaker/put_photomaker_models_here | 0 models/style_models/put_t2i_style_model_here | 0 models/unet/put_unet_files_here | 0 .../put_esrgan_and_other_upscale_models_here | 0 models/vae/put_vae_here | 0 ...esd_encoder_pth_and_taesd_decoder_pth_here | 0 new_updater.py | 35 - node_helpers.py | 4 +- nodes.py | 142 +- notebooks/comfyui_colab.ipynb | 329 - output/_output_images_will_be_put_here | 0 pytest.ini | 8 - requirements.txt | 20 - script_examples/basic_api_example.py | 120 - script_examples/websockets_api_example.py | 164 - .../websockets_api_example_ws_images.py | 159 - server.py | 700 - tests-ui/.gitignore | 1 - tests-ui/afterSetup.js | 9 - tests-ui/babel.config.json | 4 - tests-ui/globalSetup.js | 14 - tests-ui/jest.config.js | 11 - tests-ui/package-lock.json | 5586 ------ tests-ui/package.json | 31 - tests-ui/setup.js | 88 - tests-ui/tests/extensions.test.js | 196 - tests-ui/tests/groupNode.test.js | 1005 -- tests-ui/tests/users.test.js | 295 - tests-ui/tests/widgetInputs.test.js | 557 - tests-ui/utils/ezgraph.js | 452 - tests-ui/utils/index.js | 129 - tests-ui/utils/litegraph.js | 36 - tests-ui/utils/nopProxy.js | 6 - tests-ui/utils/setup.js | 82 - tests-unit/README.md | 8 - tests-unit/app_test/__init__.py | 0 tests-unit/app_test/frontend_manager_test.py | 100 - tests-unit/requirements.txt | 1 - tests/README.md | 29 - tests/__init__.py | 0 tests/compare/conftest.py | 41 - tests/compare/test_quality.py | 195 - tests/conftest.py | 36 - tests/inference/__init__.py | 0 .../graphs/default_graph_sdxl1_0.json | 144 - tests/inference/test_inference.py | 239 - web/extensions/core/clipspace.js | 166 - web/extensions/core/colorPalette.js | 785 - web/extensions/core/contextMenuFilter.js | 148 - web/extensions/core/dynamicPrompts.js | 48 - web/extensions/core/editAttention.js | 144 - web/extensions/core/groupNode.js | 1281 -- web/extensions/core/groupNodeManage.css | 149 - web/extensions/core/groupNodeManage.js | 422 - web/extensions/core/groupOptions.js | 259 - web/extensions/core/invertMenuScrolling.js | 36 - web/extensions/core/keybinds.js | 69 - web/extensions/core/linkRenderMode.js | 25 - web/extensions/core/maskeditor.js | 967 -- web/extensions/core/nodeTemplates.js | 412 - web/extensions/core/noteNode.js | 41 - web/extensions/core/rerouteNode.js | 274 - web/extensions/core/saveImageExtraOutput.js | 35 - web/extensions/core/simpleTouchSupport.js | 102 - web/extensions/core/slotDefaults.js | 91 - web/extensions/core/snapToGrid.js | 171 - web/extensions/core/uploadAudio.js | 186 - web/extensions/core/uploadImage.js | 12 - web/extensions/core/webcamCapture.js | 126 - web/extensions/core/widgetInputs.js | 800 - web/extensions/logging.js.example | 55 - web/fonts/materialdesignicons-webfont.woff2 | Bin 403216 -> 0 bytes web/index.html | 49 - web/jsconfig.json | 12 - web/lib/litegraph.core.js | 14424 ---------------- web/lib/litegraph.css | 693 - web/lib/litegraph.extensions.js | 21 - web/lib/materialdesignicons.min.css | 3 - web/scripts/api.js | 482 - web/scripts/app.js | 2459 --- web/scripts/changeTracker.js | 255 - web/scripts/defaultGraph.js | 119 - web/scripts/domWidget.js | 329 - web/scripts/logging.js | 370 - web/scripts/pnginfo.js | 507 - web/scripts/ui.js | 660 - web/scripts/ui/components/asyncDialog.js | 64 - web/scripts/ui/components/button.js | 163 - web/scripts/ui/components/buttonGroup.js | 45 - web/scripts/ui/components/popup.js | 128 - web/scripts/ui/components/splitButton.js | 43 - web/scripts/ui/dialog.js | 38 - web/scripts/ui/draggableList.js | 287 - web/scripts/ui/imagePreview.js | 97 - web/scripts/ui/menu/index.js | 302 - web/scripts/ui/menu/interruptButton.js | 23 - web/scripts/ui/menu/menu.css | 710 - web/scripts/ui/menu/queueButton.js | 93 - web/scripts/ui/menu/queueOptions.js | 77 - web/scripts/ui/menu/viewHistory.js | 27 - web/scripts/ui/menu/viewList.js | 203 - web/scripts/ui/menu/viewQueue.js | 55 - web/scripts/ui/menu/workflows.js | 770 - web/scripts/ui/settings.js | 333 - web/scripts/ui/spinner.css | 34 - web/scripts/ui/spinner.js | 9 - web/scripts/ui/toggleSwitch.js | 60 - web/scripts/ui/userSelection.css | 135 - web/scripts/ui/userSelection.js | 114 - web/scripts/ui/utils.js | 56 - web/scripts/utils.js | 156 - web/scripts/widgets.js | 531 - web/scripts/workflows.js | 450 - web/style.css | 647 - web/types/comfy.d.ts | 76 - web/types/litegraph.d.ts | 1506 -- web/user.css | 1 - 263 files changed, 772 insertions(+), 50564 deletions(-) delete mode 100755 .ci/update_windows/update.py delete mode 100755 .ci/update_windows/update_comfyui.bat delete mode 100755 .ci/windows_base_files/README_VERY_IMPORTANT.txt delete mode 100755 .ci/windows_base_files/run_cpu.bat delete mode 100755 .ci/windows_base_files/run_nvidia_gpu.bat delete mode 100644 .github/ISSUE_TEMPLATE/bug-report.yml delete mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/ISSUE_TEMPLATE/feature-request.yml delete mode 100644 .github/ISSUE_TEMPLATE/user-support.yml delete mode 100644 .github/workflows/pylint.yml delete mode 100644 .github/workflows/stable-release.yml delete mode 100644 .github/workflows/test-browser.yml delete mode 100644 .github/workflows/test-build.yml delete mode 100644 .github/workflows/test-ui.yaml delete mode 100644 .github/workflows/windows_release_dependencies.yml delete mode 100644 .github/workflows/windows_release_nightly_pytorch.yml delete mode 100644 .github/workflows/windows_release_package.yml delete mode 100644 .gitignore delete mode 100644 .pylintrc delete mode 100644 CODEOWNERS delete mode 100644 CONTRIBUTING.md delete mode 100644 LICENSE delete mode 100644 README.md delete mode 100644 app/__init__.py delete mode 100644 app/app_settings.py delete mode 100644 app/frontend_management.py delete mode 100644 app/user_manager.py delete mode 100644 comfyui_screenshot.png delete mode 100644 cuda_malloc.py delete mode 100644 custom_nodes/example_node.py.example delete mode 100644 custom_nodes/websocket_image_save.py delete mode 100644 execution.py delete mode 100644 extra_model_paths.yaml.example delete mode 100644 fix_torch.py delete mode 100644 input/example.png delete mode 100644 main.py delete mode 100644 models/checkpoints/put_checkpoints_here delete mode 100644 models/clip/put_clip_or_text_encoder_models_here delete mode 100644 models/clip_vision/put_clip_vision_models_here delete mode 100644 models/configs/anything_v3.yaml delete mode 100644 models/configs/v1-inference.yaml delete mode 100644 models/configs/v1-inference_clip_skip_2.yaml delete mode 100644 models/configs/v1-inference_clip_skip_2_fp16.yaml delete mode 100644 models/configs/v1-inference_fp16.yaml delete mode 100644 models/configs/v1-inpainting-inference.yaml delete mode 100644 models/configs/v2-inference-v.yaml delete mode 100644 models/configs/v2-inference-v_fp32.yaml delete mode 100644 models/configs/v2-inference.yaml delete mode 100644 models/configs/v2-inference_fp32.yaml delete mode 100644 models/configs/v2-inpainting-inference.yaml delete mode 100644 models/controlnet/put_controlnets_and_t2i_here delete mode 100644 models/diffusers/put_diffusers_models_here delete mode 100644 models/embeddings/put_embeddings_or_textual_inversion_concepts_here delete mode 100644 models/gligen/put_gligen_models_here delete mode 100644 models/hypernetworks/put_hypernetworks_here delete mode 100644 models/loras/put_loras_here delete mode 100644 models/photomaker/put_photomaker_models_here delete mode 100644 models/style_models/put_t2i_style_model_here delete mode 100644 models/unet/put_unet_files_here delete mode 100644 models/upscale_models/put_esrgan_and_other_upscale_models_here delete mode 100644 models/vae/put_vae_here delete mode 100644 models/vae_approx/put_taesd_encoder_pth_and_taesd_decoder_pth_here delete mode 100644 new_updater.py delete mode 100644 notebooks/comfyui_colab.ipynb delete mode 100644 output/_output_images_will_be_put_here delete mode 100644 pytest.ini delete mode 100644 requirements.txt delete mode 100644 script_examples/basic_api_example.py delete mode 100644 script_examples/websockets_api_example.py delete mode 100644 script_examples/websockets_api_example_ws_images.py delete mode 100644 server.py delete mode 100644 tests-ui/.gitignore delete mode 100644 tests-ui/afterSetup.js delete mode 100644 tests-ui/babel.config.json delete mode 100644 tests-ui/globalSetup.js delete mode 100644 tests-ui/jest.config.js delete mode 100644 tests-ui/package-lock.json delete mode 100644 tests-ui/package.json delete mode 100644 tests-ui/setup.js delete mode 100644 tests-ui/tests/extensions.test.js delete mode 100644 tests-ui/tests/groupNode.test.js delete mode 100644 tests-ui/tests/users.test.js delete mode 100644 tests-ui/tests/widgetInputs.test.js delete mode 100644 tests-ui/utils/ezgraph.js delete mode 100644 tests-ui/utils/index.js delete mode 100644 tests-ui/utils/litegraph.js delete mode 100644 tests-ui/utils/nopProxy.js delete mode 100644 tests-ui/utils/setup.js delete mode 100644 tests-unit/README.md delete mode 100644 tests-unit/app_test/__init__.py delete mode 100644 tests-unit/app_test/frontend_manager_test.py delete mode 100644 tests-unit/requirements.txt delete mode 100644 tests/README.md delete mode 100644 tests/__init__.py delete mode 100644 tests/compare/conftest.py delete mode 100644 tests/compare/test_quality.py delete mode 100644 tests/conftest.py delete mode 100644 tests/inference/__init__.py delete mode 100644 tests/inference/graphs/default_graph_sdxl1_0.json delete mode 100644 tests/inference/test_inference.py delete mode 100644 web/extensions/core/clipspace.js delete mode 100644 web/extensions/core/colorPalette.js delete mode 100644 web/extensions/core/contextMenuFilter.js delete mode 100644 web/extensions/core/dynamicPrompts.js delete mode 100644 web/extensions/core/editAttention.js delete mode 100644 web/extensions/core/groupNode.js delete mode 100644 web/extensions/core/groupNodeManage.css delete mode 100644 web/extensions/core/groupNodeManage.js delete mode 100644 web/extensions/core/groupOptions.js delete mode 100644 web/extensions/core/invertMenuScrolling.js delete mode 100644 web/extensions/core/keybinds.js delete mode 100644 web/extensions/core/linkRenderMode.js delete mode 100644 web/extensions/core/maskeditor.js delete mode 100644 web/extensions/core/nodeTemplates.js delete mode 100644 web/extensions/core/noteNode.js delete mode 100644 web/extensions/core/rerouteNode.js delete mode 100644 web/extensions/core/saveImageExtraOutput.js delete mode 100644 web/extensions/core/simpleTouchSupport.js delete mode 100644 web/extensions/core/slotDefaults.js delete mode 100644 web/extensions/core/snapToGrid.js delete mode 100644 web/extensions/core/uploadAudio.js delete mode 100644 web/extensions/core/uploadImage.js delete mode 100644 web/extensions/core/webcamCapture.js delete mode 100644 web/extensions/core/widgetInputs.js delete mode 100644 web/extensions/logging.js.example delete mode 100644 web/fonts/materialdesignicons-webfont.woff2 delete mode 100644 web/index.html delete mode 100644 web/jsconfig.json delete mode 100644 web/lib/litegraph.core.js delete mode 100644 web/lib/litegraph.css delete mode 100644 web/lib/litegraph.extensions.js delete mode 100644 web/lib/materialdesignicons.min.css delete mode 100644 web/scripts/api.js delete mode 100644 web/scripts/app.js delete mode 100644 web/scripts/changeTracker.js delete mode 100644 web/scripts/defaultGraph.js delete mode 100644 web/scripts/domWidget.js delete mode 100644 web/scripts/logging.js delete mode 100644 web/scripts/pnginfo.js delete mode 100644 web/scripts/ui.js delete mode 100644 web/scripts/ui/components/asyncDialog.js delete mode 100644 web/scripts/ui/components/button.js delete mode 100644 web/scripts/ui/components/buttonGroup.js delete mode 100644 web/scripts/ui/components/popup.js delete mode 100644 web/scripts/ui/components/splitButton.js delete mode 100644 web/scripts/ui/dialog.js delete mode 100644 web/scripts/ui/draggableList.js delete mode 100644 web/scripts/ui/imagePreview.js delete mode 100644 web/scripts/ui/menu/index.js delete mode 100644 web/scripts/ui/menu/interruptButton.js delete mode 100644 web/scripts/ui/menu/menu.css delete mode 100644 web/scripts/ui/menu/queueButton.js delete mode 100644 web/scripts/ui/menu/queueOptions.js delete mode 100644 web/scripts/ui/menu/viewHistory.js delete mode 100644 web/scripts/ui/menu/viewList.js delete mode 100644 web/scripts/ui/menu/viewQueue.js delete mode 100644 web/scripts/ui/menu/workflows.js delete mode 100644 web/scripts/ui/settings.js delete mode 100644 web/scripts/ui/spinner.css delete mode 100644 web/scripts/ui/spinner.js delete mode 100644 web/scripts/ui/toggleSwitch.js delete mode 100644 web/scripts/ui/userSelection.css delete mode 100644 web/scripts/ui/userSelection.js delete mode 100644 web/scripts/ui/utils.js delete mode 100644 web/scripts/utils.js delete mode 100644 web/scripts/widgets.js delete mode 100644 web/scripts/workflows.js delete mode 100644 web/style.css delete mode 100644 web/types/comfy.d.ts delete mode 100644 web/types/litegraph.d.ts delete mode 100644 web/user.css diff --git a/.ci/update_windows/update.py b/.ci/update_windows/update.py deleted file mode 100755 index cc79a914e..000000000 --- a/.ci/update_windows/update.py +++ /dev/null @@ -1,117 +0,0 @@ -import pygit2 -from datetime import datetime -import sys -import os -import shutil -import filecmp - -def pull(repo, remote_name='origin', branch='master'): - for remote in repo.remotes: - if remote.name == remote_name: - remote.fetch() - remote_master_id = repo.lookup_reference('refs/remotes/origin/%s' % (branch)).target - merge_result, _ = repo.merge_analysis(remote_master_id) - # Up to date, do nothing - if merge_result & pygit2.GIT_MERGE_ANALYSIS_UP_TO_DATE: - return - # We can just fastforward - elif merge_result & pygit2.GIT_MERGE_ANALYSIS_FASTFORWARD: - repo.checkout_tree(repo.get(remote_master_id)) - try: - master_ref = repo.lookup_reference('refs/heads/%s' % (branch)) - master_ref.set_target(remote_master_id) - except KeyError: - repo.create_branch(branch, repo.get(remote_master_id)) - repo.head.set_target(remote_master_id) - elif merge_result & pygit2.GIT_MERGE_ANALYSIS_NORMAL: - repo.merge(remote_master_id) - - if repo.index.conflicts is not None: - for conflict in repo.index.conflicts: - print('Conflicts found in:', conflict[0].path) - raise AssertionError('Conflicts, ahhhhh!!') - - user = repo.default_signature - tree = repo.index.write_tree() - commit = repo.create_commit('HEAD', - user, - user, - 'Merge!', - tree, - [repo.head.target, remote_master_id]) - # We need to do this or git CLI will think we are still merging. - repo.state_cleanup() - else: - raise AssertionError('Unknown merge analysis result') - -pygit2.option(pygit2.GIT_OPT_SET_OWNER_VALIDATION, 0) -repo_path = str(sys.argv[1]) -repo = pygit2.Repository(repo_path) -ident = pygit2.Signature('comfyui', 'comfy@ui') -try: - print("stashing current changes") - repo.stash(ident) -except KeyError: - print("nothing to stash") -backup_branch_name = 'backup_branch_{}'.format(datetime.today().strftime('%Y-%m-%d_%H_%M_%S')) -print("creating backup branch: {}".format(backup_branch_name)) -try: - repo.branches.local.create(backup_branch_name, repo.head.peel()) -except: - pass - -print("checking out master branch") -branch = repo.lookup_branch('master') -if branch is None: - ref = repo.lookup_reference('refs/remotes/origin/master') - repo.checkout(ref) - branch = repo.lookup_branch('master') - if branch is None: - repo.create_branch('master', repo.get(ref.target)) -else: - ref = repo.lookup_reference(branch.name) - repo.checkout(ref) - -print("pulling latest changes") -pull(repo) - -print("Done!") - -self_update = True -if len(sys.argv) > 2: - self_update = '--skip_self_update' not in sys.argv - -update_py_path = os.path.realpath(__file__) -repo_update_py_path = os.path.join(repo_path, ".ci/update_windows/update.py") - -cur_path = os.path.dirname(update_py_path) - - -req_path = os.path.join(cur_path, "current_requirements.txt") -repo_req_path = os.path.join(repo_path, "requirements.txt") - - -def files_equal(file1, file2): - try: - return filecmp.cmp(file1, file2, shallow=False) - except: - return False - -def file_size(f): - try: - return os.path.getsize(f) - except: - return 0 - - -if self_update and not files_equal(update_py_path, repo_update_py_path) and file_size(repo_update_py_path) > 10: - shutil.copy(repo_update_py_path, os.path.join(cur_path, "update_new.py")) - exit() - -if not os.path.exists(req_path) or not files_equal(repo_req_path, req_path): - import subprocess - try: - subprocess.check_call([sys.executable, '-s', '-m', 'pip', 'install', '-r', repo_req_path]) - shutil.copy(repo_req_path, req_path) - except: - pass diff --git a/.ci/update_windows/update_comfyui.bat b/.ci/update_windows/update_comfyui.bat deleted file mode 100755 index bb08c0de0..000000000 --- a/.ci/update_windows/update_comfyui.bat +++ /dev/null @@ -1,8 +0,0 @@ -@echo off -..\python_embeded\python.exe .\update.py ..\ComfyUI\ -if exist update_new.py ( - move /y update_new.py update.py - echo Running updater again since it got updated. - ..\python_embeded\python.exe .\update.py ..\ComfyUI\ --skip_self_update -) -if "%~1"=="" pause diff --git a/.ci/windows_base_files/README_VERY_IMPORTANT.txt b/.ci/windows_base_files/README_VERY_IMPORTANT.txt deleted file mode 100755 index 0216658de..000000000 --- a/.ci/windows_base_files/README_VERY_IMPORTANT.txt +++ /dev/null @@ -1,31 +0,0 @@ -HOW TO RUN: - -if you have a NVIDIA gpu: - -run_nvidia_gpu.bat - - - -To run it in slow CPU mode: - -run_cpu.bat - - - -IF YOU GET A RED ERROR IN THE UI MAKE SURE YOU HAVE A MODEL/CHECKPOINT IN: ComfyUI\models\checkpoints - -You can download the stable diffusion 1.5 one from: https://huggingface.co/runwayml/stable-diffusion-v1-5/blob/main/v1-5-pruned-emaonly.ckpt - - -RECOMMENDED WAY TO UPDATE: -To update the ComfyUI code: update\update_comfyui.bat - - - -To update ComfyUI with the python dependencies, note that you should ONLY run this if you have issues with python dependencies. -update\update_comfyui_and_python_dependencies.bat - - -TO SHARE MODELS BETWEEN COMFYUI AND ANOTHER UI: -In the ComfyUI directory you will find a file: extra_model_paths.yaml.example -Rename this file to: extra_model_paths.yaml and edit it with your favorite text editor. diff --git a/.ci/windows_base_files/run_cpu.bat b/.ci/windows_base_files/run_cpu.bat deleted file mode 100755 index c3ba41721..000000000 --- a/.ci/windows_base_files/run_cpu.bat +++ /dev/null @@ -1,2 +0,0 @@ -.\python_embeded\python.exe -s ComfyUI\main.py --cpu --windows-standalone-build -pause diff --git a/.ci/windows_base_files/run_nvidia_gpu.bat b/.ci/windows_base_files/run_nvidia_gpu.bat deleted file mode 100755 index 274d7c948..000000000 --- a/.ci/windows_base_files/run_nvidia_gpu.bat +++ /dev/null @@ -1,2 +0,0 @@ -.\python_embeded\python.exe -s ComfyUI\main.py --windows-standalone-build -pause diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml deleted file mode 100644 index 39d1992d7..000000000 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Bug Report -description: "Something is broken inside of ComfyUI. (Do not use this if you're just having issues and need help, or if the issue relates to a custom node)" -labels: ["Potential Bug"] -body: - - type: markdown - attributes: - value: | - Before submitting a **Bug Report**, please ensure the following: - - - **1:** You are running the latest version of ComfyUI. - - **2:** You have looked at the existing bug reports and made sure this isn't already reported. - - **3:** You confirmed that the bug is not caused by a custom node. You can disable all custom nodes by passing - `--disable-all-custom-nodes` command line argument. - - **4:** This is an actual bug in ComfyUI, not just a support question. A bug is when you can specify exact - steps to replicate what went wrong and others will be able to repeat your steps and see the same issue happen. - - If unsure, ask on the [ComfyUI Matrix Space](https://app.element.io/#/room/%23comfyui_space%3Amatrix.org) or the [Comfy Org Discord](https://discord.gg/comfyorg) first. - - type: textarea - attributes: - label: Expected Behavior - description: "What you expected to happen." - validations: - required: true - - type: textarea - attributes: - label: Actual Behavior - description: "What actually happened. Please include a screenshot of the issue if possible." - validations: - required: true - - type: textarea - attributes: - label: Steps to Reproduce - description: "Describe how to reproduce the issue. Please be sure to attach a workflow JSON or PNG, ideally one that doesn't require custom nodes to test. If the bug open happens when certain custom nodes are used, most likely that custom node is what has the bug rather than ComfyUI, in which case it should be reported to the node's author." - validations: - required: true - - type: textarea - attributes: - label: Debug Logs - description: "Please copy the output from your terminal logs here." - render: powershell - validations: - required: true - - type: textarea - attributes: - label: Other - description: "Any other additional information you think might be helpful." - validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 2c519ede5..000000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,8 +0,0 @@ -blank_issues_enabled: true -contact_links: - - name: ComfyUI Matrix Space - url: https://app.element.io/#/room/%23comfyui_space%3Amatrix.org - about: The ComfyUI Matrix Space is available for support and general discussion related to ComfyUI (Matrix is like Discord but open source). - - name: Comfy Org Discord - url: https://discord.gg/comfyorg - about: The Comfy Org Discord is available for support and general discussion related to ComfyUI. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml deleted file mode 100644 index 419721b63..000000000 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Feature Request -description: "You have an idea for something new you would like to see added to ComfyUI's core." -labels: [ "Feature" ] -body: - - type: markdown - attributes: - value: | - Before submitting a **Feature Request**, please ensure the following: - - **1:** You are running the latest version of ComfyUI. - **2:** You have looked to make sure there is not already a feature that does what you need, and there is not already a Feature Request listed for the same idea. - **3:** This is something that makes sense to add to ComfyUI Core, and wouldn't make more sense as a custom node. - - If unsure, ask on the [ComfyUI Matrix Space](https://app.element.io/#/room/%23comfyui_space%3Amatrix.org) or the [Comfy Org Discord](https://discord.gg/comfyorg) first. - - type: textarea - attributes: - label: Feature Idea - description: "Describe the feature you want to see." - validations: - required: true - - type: textarea - attributes: - label: Existing Solutions - description: "Please search through available custom nodes / extensions to see if there are existing custom solutions for this. If so, please link the options you found here as a reference." - validations: - required: false - - type: textarea - attributes: - label: Other - description: "Any other additional information you think might be helpful." - validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/user-support.yml b/.github/ISSUE_TEMPLATE/user-support.yml deleted file mode 100644 index df28804c6..000000000 --- a/.github/ISSUE_TEMPLATE/user-support.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: User Support -description: "Use this if you need help with something, or you're experiencing an issue." -labels: [ "User Support" ] -body: - - type: markdown - attributes: - value: | - Before submitting a **User Report** issue, please ensure the following: - - **1:** You are running the latest version of ComfyUI. - **2:** You have made an effort to find public answers to your question before asking here. In other words, you googled it first, and scrolled through recent help topics. - - If unsure, ask on the [ComfyUI Matrix Space](https://app.element.io/#/room/%23comfyui_space%3Amatrix.org) or the [Comfy Org Discord](https://discord.gg/comfyorg) first. - - type: textarea - attributes: - label: Your question - description: "Post your question here. Please be as detailed as possible." - validations: - required: true - - type: textarea - attributes: - label: Logs - description: "If your question relates to an issue you're experiencing, please go to `Server` -> `Logs` -> potentially set `View Type` to `Debug` as well, then copypaste all the text into here." - render: powershell - validations: - required: false - - type: textarea - attributes: - label: Other - description: "Any other additional information you think might be helpful." - validations: - required: false diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml deleted file mode 100644 index 5effbea35..000000000 --- a/.github/workflows/pylint.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Python Linting - -on: [push, pull_request] - -jobs: - pylint: - name: Run Pylint - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.x - - - name: Install Pylint - run: pip install pylint - - - name: Run Pylint - run: pylint --rcfile=.pylintrc $(find . -type f -name "*.py") diff --git a/.github/workflows/stable-release.yml b/.github/workflows/stable-release.yml deleted file mode 100644 index 658816afe..000000000 --- a/.github/workflows/stable-release.yml +++ /dev/null @@ -1,104 +0,0 @@ - -name: "Release Stable Version" - -on: - workflow_dispatch: - inputs: - git_tag: - description: 'Git tag' - required: true - type: string - cu: - description: 'CUDA version' - required: true - type: string - default: "121" - python_minor: - description: 'Python minor version' - required: true - type: string - default: "11" - python_patch: - description: 'Python patch version' - required: true - type: string - default: "9" - - -jobs: - package_comfy_windows: - permissions: - contents: "write" - packages: "write" - pull-requests: "read" - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ inputs.git_tag }} - fetch-depth: 0 - persist-credentials: false - - uses: actions/cache/restore@v4 - id: cache - with: - path: | - cu${{ inputs.cu }}_python_deps.tar - update_comfyui_and_python_dependencies.bat - key: ${{ runner.os }}-build-cu${{ inputs.cu }}-${{ inputs.python_minor }} - - shell: bash - run: | - mv cu${{ inputs.cu }}_python_deps.tar ../ - mv update_comfyui_and_python_dependencies.bat ../ - cd .. - tar xf cu${{ inputs.cu }}_python_deps.tar - pwd - ls - - - shell: bash - run: | - cd .. - cp -r ComfyUI ComfyUI_copy - curl https://www.python.org/ftp/python/3.${{ inputs.python_minor }}.${{ inputs.python_patch }}/python-3.${{ inputs.python_minor }}.${{ inputs.python_patch }}-embed-amd64.zip -o python_embeded.zip - unzip python_embeded.zip -d python_embeded - cd python_embeded - echo ${{ env.MINOR_VERSION }} - echo 'import site' >> ./python3${{ inputs.python_minor }}._pth - curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py - ./python.exe get-pip.py - ./python.exe -s -m pip install ../cu${{ inputs.cu }}_python_deps/* - sed -i '1i../ComfyUI' ./python3${{ inputs.python_minor }}._pth - cd .. - - git clone --depth 1 https://github.com/comfyanonymous/taesd - cp taesd/*.pth ./ComfyUI_copy/models/vae_approx/ - - mkdir ComfyUI_windows_portable - mv python_embeded ComfyUI_windows_portable - mv ComfyUI_copy ComfyUI_windows_portable/ComfyUI - - cd ComfyUI_windows_portable - - mkdir update - cp -r ComfyUI/.ci/update_windows/* ./update/ - cp -r ComfyUI/.ci/windows_base_files/* ./ - cp ../update_comfyui_and_python_dependencies.bat ./update/ - - cd .. - - "C:\Program Files\7-Zip\7z.exe" a -t7z -m0=lzma2 -mx=8 -mfb=64 -md=32m -ms=on -mf=BCJ2 ComfyUI_windows_portable.7z ComfyUI_windows_portable - mv ComfyUI_windows_portable.7z ComfyUI/ComfyUI_windows_portable_nvidia.7z - - cd ComfyUI_windows_portable - python_embeded/python.exe -s ComfyUI/main.py --quick-test-for-ci --cpu - - ls - - - name: Upload binaries to release - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: ComfyUI_windows_portable_nvidia.7z - tag: ${{ inputs.git_tag }} - overwrite: true - prerelease: true - make_latest: false diff --git a/.github/workflows/test-browser.yml b/.github/workflows/test-browser.yml deleted file mode 100644 index ce0bc37a3..000000000 --- a/.github/workflows/test-browser.yml +++ /dev/null @@ -1,76 +0,0 @@ -# This is a temporary action during frontend TS migration. -# This file should be removed after TS migration is completed. -# The browser test is here to ensure TS repo is working the same way as the -# current JS code. -# If you are adding UI feature, please sync your changes to the TS repo: -# huchenlei/ComfyUI_frontend and update test expectation files accordingly. -name: Playwright Browser Tests CI - -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] - -jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Checkout ComfyUI - uses: actions/checkout@v4 - with: - repository: "comfyanonymous/ComfyUI" - path: "ComfyUI" - - name: Checkout ComfyUI_frontend - uses: actions/checkout@v4 - with: - repository: "huchenlei/ComfyUI_frontend" - path: "ComfyUI_frontend" - ref: "fcc54d803e5b6a9b08a462a1d94899318c96dcbb" - - uses: actions/setup-node@v3 - with: - node-version: lts/* - - uses: actions/setup-python@v4 - with: - python-version: '3.8' - - name: Install requirements - run: | - python -m pip install --upgrade pip - pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu - pip install -r requirements.txt - pip install wait-for-it - working-directory: ComfyUI - - name: Start ComfyUI server - run: | - python main.py --cpu 2>&1 | tee console_output.log & - wait-for-it --service 127.0.0.1:8188 -t 600 - working-directory: ComfyUI - - name: Install ComfyUI_frontend dependencies - run: | - npm ci - working-directory: ComfyUI_frontend - - name: Install Playwright Browsers - run: npx playwright install --with-deps - working-directory: ComfyUI_frontend - - name: Run Playwright tests - run: npx playwright test - working-directory: ComfyUI_frontend - - name: Check for unhandled exceptions in server log - run: | - if grep -qE "Exception|Error" console_output.log; then - echo "Unhandled exception/error found in server log." - exit 1 - fi - working-directory: ComfyUI - - uses: actions/upload-artifact@v4 - if: always() - with: - name: playwright-report - path: ComfyUI_frontend/playwright-report/ - retention-days: 30 - - uses: actions/upload-artifact@v4 - if: always() - with: - name: console-output - path: ComfyUI/console_output.log - retention-days: 30 diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml deleted file mode 100644 index 444d6b254..000000000 --- a/.github/workflows/test-build.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Build package - -# -# This workflow is a test of the python package build. -# Install Python dependencies across different Python versions. -# - -on: - push: - paths: - - "requirements.txt" - - ".github/workflows/test-build.yml" - -jobs: - build: - name: Build Test - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt \ No newline at end of file diff --git a/.github/workflows/test-ui.yaml b/.github/workflows/test-ui.yaml deleted file mode 100644 index d947e9d5f..000000000 --- a/.github/workflows/test-ui.yaml +++ /dev/null @@ -1,30 +0,0 @@ -name: Tests CI - -on: [push, pull_request] - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 - with: - node-version: 18 - - uses: actions/setup-python@v4 - with: - python-version: '3.10' - - name: Install requirements - run: | - python -m pip install --upgrade pip - pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu - pip install -r requirements.txt - - name: Run Tests - run: | - npm ci - npm run test:generate - npm test -- --verbose - working-directory: ./tests-ui - - name: Run Unit Tests - run: | - pip install -r tests-unit/requirements.txt - python -m pytest tests-unit diff --git a/.github/workflows/windows_release_dependencies.yml b/.github/workflows/windows_release_dependencies.yml deleted file mode 100644 index 8d3a8665d..000000000 --- a/.github/workflows/windows_release_dependencies.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: "Windows Release dependencies" - -on: - workflow_dispatch: - inputs: - xformers: - description: 'xformers version' - required: false - type: string - default: "" - extra_dependencies: - description: 'extra dependencies' - required: false - type: string - default: "\"numpy<2\"" - cu: - description: 'cuda version' - required: true - type: string - default: "124" - - python_minor: - description: 'python minor version' - required: true - type: string - default: "11" - - python_patch: - description: 'python patch version' - required: true - type: string - default: "9" -# push: -# branches: -# - master - -jobs: - build_dependencies: - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: 3.${{ inputs.python_minor }}.${{ inputs.python_patch }} - - - shell: bash - run: | - echo "@echo off - call update_comfyui.bat nopause - echo - - echo This will try to update pytorch and all python dependencies. - echo - - echo If you just want to update normally, close this and run update_comfyui.bat instead. - echo - - pause - ..\python_embeded\python.exe -s -m pip install --upgrade torch torchvision torchaudio ${{ inputs.xformers }} --extra-index-url https://download.pytorch.org/whl/cu${{ inputs.cu }} -r ../ComfyUI/requirements.txt pygit2 - pause" > update_comfyui_and_python_dependencies.bat - - python -m pip wheel --no-cache-dir torch torchvision torchaudio ${{ inputs.xformers }} ${{ inputs.extra_dependencies }} --extra-index-url https://download.pytorch.org/whl/cu${{ inputs.cu }} -r requirements.txt pygit2 -w ./temp_wheel_dir - python -m pip install --no-cache-dir ./temp_wheel_dir/* - echo installed basic - ls -lah temp_wheel_dir - mv temp_wheel_dir cu${{ inputs.cu }}_python_deps - tar cf cu${{ inputs.cu }}_python_deps.tar cu${{ inputs.cu }}_python_deps - - - uses: actions/cache/save@v4 - with: - path: | - cu${{ inputs.cu }}_python_deps.tar - update_comfyui_and_python_dependencies.bat - key: ${{ runner.os }}-build-cu${{ inputs.cu }}-${{ inputs.python_minor }} diff --git a/.github/workflows/windows_release_nightly_pytorch.yml b/.github/workflows/windows_release_nightly_pytorch.yml deleted file mode 100644 index 0b29b4d91..000000000 --- a/.github/workflows/windows_release_nightly_pytorch.yml +++ /dev/null @@ -1,90 +0,0 @@ -name: "Windows Release Nightly pytorch" - -on: - workflow_dispatch: - inputs: - cu: - description: 'cuda version' - required: true - type: string - default: "124" - - python_minor: - description: 'python minor version' - required: true - type: string - default: "12" - - python_patch: - description: 'python patch version' - required: true - type: string - default: "4" -# push: -# branches: -# - master - -jobs: - build: - permissions: - contents: "write" - packages: "write" - pull-requests: "read" - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - persist-credentials: false - - uses: actions/setup-python@v5 - with: - python-version: 3.${{ inputs.python_minor }}.${{ inputs.python_patch }} - - shell: bash - run: | - cd .. - cp -r ComfyUI ComfyUI_copy - curl https://www.python.org/ftp/python/3.${{ inputs.python_minor }}.${{ inputs.python_patch }}/python-3.${{ inputs.python_minor }}.${{ inputs.python_patch }}-embed-amd64.zip -o python_embeded.zip - unzip python_embeded.zip -d python_embeded - cd python_embeded - echo 'import site' >> ./python3${{ inputs.python_minor }}._pth - curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py - ./python.exe get-pip.py - python -m pip wheel torch torchvision torchaudio --pre --extra-index-url https://download.pytorch.org/whl/nightly/cu${{ inputs.cu }} -r ../ComfyUI/requirements.txt pygit2 -w ../temp_wheel_dir - ls ../temp_wheel_dir - ./python.exe -s -m pip install --pre ../temp_wheel_dir/* - sed -i '1i../ComfyUI' ./python3${{ inputs.python_minor }}._pth - cd .. - - git clone --depth 1 https://github.com/comfyanonymous/taesd - cp taesd/*.pth ./ComfyUI_copy/models/vae_approx/ - - mkdir ComfyUI_windows_portable_nightly_pytorch - mv python_embeded ComfyUI_windows_portable_nightly_pytorch - mv ComfyUI_copy ComfyUI_windows_portable_nightly_pytorch/ComfyUI - - cd ComfyUI_windows_portable_nightly_pytorch - - mkdir update - cp -r ComfyUI/.ci/update_windows/* ./update/ - cp -r ComfyUI/.ci/windows_base_files/* ./ - - echo "call update_comfyui.bat nopause - ..\python_embeded\python.exe -s -m pip install --upgrade --pre torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/nightly/cu${{ inputs.cu }} -r ../ComfyUI/requirements.txt pygit2 - pause" > ./update/update_comfyui_and_python_dependencies.bat - cd .. - - "C:\Program Files\7-Zip\7z.exe" a -t7z -m0=lzma2 -mx=8 -mfb=64 -md=32m -ms=on -mf=BCJ2 ComfyUI_windows_portable_nightly_pytorch.7z ComfyUI_windows_portable_nightly_pytorch - mv ComfyUI_windows_portable_nightly_pytorch.7z ComfyUI/ComfyUI_windows_portable_nvidia_or_cpu_nightly_pytorch.7z - - cd ComfyUI_windows_portable_nightly_pytorch - python_embeded/python.exe -s ComfyUI/main.py --quick-test-for-ci --cpu - - ls - - - name: Upload binaries to release - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: ComfyUI_windows_portable_nvidia_or_cpu_nightly_pytorch.7z - tag: "latest" - overwrite: true diff --git a/.github/workflows/windows_release_package.yml b/.github/workflows/windows_release_package.yml deleted file mode 100644 index 84d99b8a6..000000000 --- a/.github/workflows/windows_release_package.yml +++ /dev/null @@ -1,100 +0,0 @@ -name: "Windows Release packaging" - -on: - workflow_dispatch: - inputs: - cu: - description: 'cuda version' - required: true - type: string - default: "124" - - python_minor: - description: 'python minor version' - required: true - type: string - default: "11" - - python_patch: - description: 'python patch version' - required: true - type: string - default: "9" -# push: -# branches: -# - master - -jobs: - package_comfyui: - permissions: - contents: "write" - packages: "write" - pull-requests: "read" - runs-on: windows-latest - steps: - - uses: actions/cache/restore@v4 - id: cache - with: - path: | - cu${{ inputs.cu }}_python_deps.tar - update_comfyui_and_python_dependencies.bat - key: ${{ runner.os }}-build-cu${{ inputs.cu }}-${{ inputs.python_minor }} - - shell: bash - run: | - mv cu${{ inputs.cu }}_python_deps.tar ../ - mv update_comfyui_and_python_dependencies.bat ../ - cd .. - tar xf cu${{ inputs.cu }}_python_deps.tar - pwd - ls - - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - persist-credentials: false - - shell: bash - run: | - cd .. - cp -r ComfyUI ComfyUI_copy - curl https://www.python.org/ftp/python/3.${{ inputs.python_minor }}.${{ inputs.python_patch }}/python-3.${{ inputs.python_minor }}.${{ inputs.python_patch }}-embed-amd64.zip -o python_embeded.zip - unzip python_embeded.zip -d python_embeded - cd python_embeded - echo 'import site' >> ./python3${{ inputs.python_minor }}._pth - curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py - ./python.exe get-pip.py - ./python.exe -s -m pip install ../cu${{ inputs.cu }}_python_deps/* - sed -i '1i../ComfyUI' ./python3${{ inputs.python_minor }}._pth - cd .. - - git clone --depth 1 https://github.com/comfyanonymous/taesd - cp taesd/*.pth ./ComfyUI_copy/models/vae_approx/ - - mkdir ComfyUI_windows_portable - mv python_embeded ComfyUI_windows_portable - mv ComfyUI_copy ComfyUI_windows_portable/ComfyUI - - cd ComfyUI_windows_portable - - mkdir update - cp -r ComfyUI/.ci/update_windows/* ./update/ - cp -r ComfyUI/.ci/windows_base_files/* ./ - cp ../update_comfyui_and_python_dependencies.bat ./update/ - - cd .. - - "C:\Program Files\7-Zip\7z.exe" a -t7z -m0=lzma2 -mx=8 -mfb=64 -md=32m -ms=on -mf=BCJ2 ComfyUI_windows_portable.7z ComfyUI_windows_portable - mv ComfyUI_windows_portable.7z ComfyUI/new_ComfyUI_windows_portable_nvidia_cu${{ inputs.cu }}_or_cpu.7z - - cd ComfyUI_windows_portable - python_embeded/python.exe -s ComfyUI/main.py --quick-test-for-ci --cpu - - ls - - - name: Upload binaries to release - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: new_ComfyUI_windows_portable_nvidia_cu${{ inputs.cu }}_or_cpu.7z - tag: "latest" - overwrite: true - diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 5092c98f4..000000000 --- a/.gitignore +++ /dev/null @@ -1,21 +0,0 @@ -__pycache__/ -*.py[cod] -/output/ -/input/ -!/input/example.png -/models/ -/temp/ -/custom_nodes/ -!custom_nodes/example_node.py.example -extra_model_paths.yaml -/.vs -.vscode/ -.idea/ -venv/ -/web/extensions/* -!/web/extensions/logging.js.example -!/web/extensions/core/ -/tests-ui/data/object_info.json -/user/ -*.log -web_custom_versions/ \ No newline at end of file diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index a5da56e57..000000000 --- a/.pylintrc +++ /dev/null @@ -1,3 +0,0 @@ -[MESSAGES CONTROL] -disable=all -enable=eval-used diff --git a/CODEOWNERS b/CODEOWNERS deleted file mode 100644 index 7c7c3e19e..000000000 --- a/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @comfyanonymous diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 048f127e7..000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,41 +0,0 @@ -# Contributing to ComfyUI - -Welcome, and thank you for your interest in contributing to ComfyUI! - -There are several ways in which you can contribute, beyond writing code. The goal of this document is to provide a high-level overview of how you can get involved. - -## Asking Questions - -Have a question? Instead of opening an issue, please ask on [Discord](https://comfy.org/discord) or [Matrix](https://app.element.io/#/room/%23comfyui_space%3Amatrix.org) channels. Our team and the community will help you. - -## Providing Feedback - -Your comments and feedback are welcome, and the development team is available via a handful of different channels. - -See the `#bug-report`, `#feature-request` and `#feedback` channels on Discord. - -## Reporting Issues - -Have you identified a reproducible problem in ComfyUI? Do you have a feature request? We want to hear about it! Here's how you can report your issue as effectively as possible. - - -### Look For an Existing Issue - -Before you create a new issue, please do a search in [open issues](https://github.com/comfyanonymous/ComfyUI/issues) to see if the issue or feature request has already been filed. - -If you find your issue already exists, make relevant comments and add your [reaction](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments). Use a reaction in place of a "+1" comment: - -* 👍 - upvote -* 👎 - downvote - -If you cannot find an existing issue that describes your bug or feature, create a new issue. We have an issue template in place to organize new issues. - - -### Creating Pull Requests - -* Please refer to the article on [creating pull requests](https://github.com/comfyanonymous/ComfyUI/wiki/How-to-Contribute-Code) and contributing to this project. - - -## Thank You - -Your contributions to open source, large or small, make great projects like this possible. Thank you for taking the time to contribute. diff --git a/LICENSE b/LICENSE deleted file mode 100644 index f288702d2..000000000 --- a/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - 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 deleted file mode 100644 index d5ded7297..000000000 --- a/README.md +++ /dev/null @@ -1,224 +0,0 @@ -ComfyUI -======= -The most powerful and modular stable diffusion GUI and backend. ------------ -![ComfyUI Screenshot](comfyui_screenshot.png) - -This ui will let you design and execute advanced stable diffusion pipelines using a graph/nodes/flowchart based interface. For some workflow examples and see what ComfyUI can do you can check out: -### [ComfyUI Examples](https://comfyanonymous.github.io/ComfyUI_examples/) - -### [Installing ComfyUI](#installing) - -## Features -- Nodes/graph/flowchart interface to experiment and create complex Stable Diffusion workflows without needing to code anything. -- Fully supports SD1.x, SD2.x, [SDXL](https://comfyanonymous.github.io/ComfyUI_examples/sdxl/), [Stable Video Diffusion](https://comfyanonymous.github.io/ComfyUI_examples/video/), [Stable Cascade](https://comfyanonymous.github.io/ComfyUI_examples/stable_cascade/), [SD3](https://comfyanonymous.github.io/ComfyUI_examples/sd3/) and [Stable Audio](https://comfyanonymous.github.io/ComfyUI_examples/audio/) -- [Flux](https://comfyanonymous.github.io/ComfyUI_examples/flux/) -- Asynchronous Queue system -- Many optimizations: Only re-executes the parts of the workflow that changes between executions. -- Smart memory management: can automatically run models on GPUs with as low as 1GB vram. -- Works even if you don't have a GPU with: ```--cpu``` (slow) -- Can load ckpt, safetensors and diffusers models/checkpoints. Standalone VAEs and CLIP models. -- Embeddings/Textual inversion -- [Loras (regular, locon and loha)](https://comfyanonymous.github.io/ComfyUI_examples/lora/) -- [Hypernetworks](https://comfyanonymous.github.io/ComfyUI_examples/hypernetworks/) -- Loading full workflows (with seeds) from generated PNG, WebP and FLAC files. -- Saving/Loading workflows as Json files. -- Nodes interface can be used to create complex workflows like one for [Hires fix](https://comfyanonymous.github.io/ComfyUI_examples/2_pass_txt2img/) or much more advanced ones. -- [Area Composition](https://comfyanonymous.github.io/ComfyUI_examples/area_composition/) -- [Inpainting](https://comfyanonymous.github.io/ComfyUI_examples/inpaint/) with both regular and inpainting models. -- [ControlNet and T2I-Adapter](https://comfyanonymous.github.io/ComfyUI_examples/controlnet/) -- [Upscale Models (ESRGAN, ESRGAN variants, SwinIR, Swin2SR, etc...)](https://comfyanonymous.github.io/ComfyUI_examples/upscale_models/) -- [unCLIP Models](https://comfyanonymous.github.io/ComfyUI_examples/unclip/) -- [GLIGEN](https://comfyanonymous.github.io/ComfyUI_examples/gligen/) -- [Model Merging](https://comfyanonymous.github.io/ComfyUI_examples/model_merging/) -- [LCM models and Loras](https://comfyanonymous.github.io/ComfyUI_examples/lcm/) -- [SDXL Turbo](https://comfyanonymous.github.io/ComfyUI_examples/sdturbo/) -- [AuraFlow](https://comfyanonymous.github.io/ComfyUI_examples/aura_flow/) -- [HunyuanDiT](https://comfyanonymous.github.io/ComfyUI_examples/hunyuan_dit/) -- Latent previews with [TAESD](#how-to-show-high-quality-previews) -- Starts up very fast. -- Works fully offline: will never download anything. -- [Config file](extra_model_paths.yaml.example) to set the search paths for models. - -Workflow examples can be found on the [Examples page](https://comfyanonymous.github.io/ComfyUI_examples/) - -## Shortcuts - -| Keybind | Explanation | -|------------------------------------|--------------------------------------------------------------------------------------------------------------------| -| Ctrl + Enter | Queue up current graph for generation | -| Ctrl + Shift + Enter | Queue up current graph as first for generation | -| Ctrl + Z/Ctrl + Y | Undo/Redo | -| Ctrl + S | Save workflow | -| Ctrl + O | Load workflow | -| Ctrl + A | Select all nodes | -| Alt + C | Collapse/uncollapse selected nodes | -| Ctrl + M | Mute/unmute selected nodes | -| Ctrl + B | Bypass selected nodes (acts like the node was removed from the graph and the wires reconnected through) | -| Delete/Backspace | Delete selected nodes | -| Ctrl + Backspace | Delete the current graph | -| Space | Move the canvas around when held and moving the cursor | -| Ctrl/Shift + Click | Add clicked node to selection | -| Ctrl + C/Ctrl + V | Copy and paste selected nodes (without maintaining connections to outputs of unselected nodes) | -| Ctrl + C/Ctrl + Shift + V | Copy and paste selected nodes (maintaining connections from outputs of unselected nodes to inputs of pasted nodes) | -| Shift + Drag | Move multiple selected nodes at the same time | -| Ctrl + D | Load default graph | -| Alt + `+` | Canvas Zoom in | -| Alt + `-` | Canvas Zoom out | -| Ctrl + Shift + LMB + Vertical drag | Canvas Zoom in/out | -| Q | Toggle visibility of the queue | -| H | Toggle visibility of history | -| R | Refresh graph | -| Double-Click LMB | Open node quick search palette | - -Ctrl can also be replaced with Cmd instead for macOS users - -# Installing - -## Windows - -There is a portable standalone build for Windows that should work for running on Nvidia GPUs or for running on your CPU only on the [releases page](https://github.com/comfyanonymous/ComfyUI/releases). - -### [Direct link to download](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_nvidia.7z) - -Simply download, extract with [7-Zip](https://7-zip.org) and run. Make sure you put your Stable Diffusion checkpoints/models (the huge ckpt/safetensors files) in: ComfyUI\models\checkpoints - -If you have trouble extracting it, right click the file -> properties -> unblock - -#### How do I share models between another UI and ComfyUI? - -See the [Config file](extra_model_paths.yaml.example) to set the search paths for models. In the standalone windows build you can find this file in the ComfyUI directory. Rename this file to extra_model_paths.yaml and edit it with your favorite text editor. - -## Jupyter Notebook - -To run it on services like paperspace, kaggle or colab you can use my [Jupyter Notebook](notebooks/comfyui_colab.ipynb) - -## Manual Install (Windows, Linux) - -Git clone this repo. - -Put your SD checkpoints (the huge ckpt/safetensors files) in: models/checkpoints - -Put your VAE in: models/vae - - -### AMD GPUs (Linux only) -AMD users can install rocm and pytorch with pip if you don't have it already installed, this is the command to install the stable version: - -```pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/rocm6.0``` - -This is the command to install the nightly with ROCm 6.0 which might have some performance improvements: - -```pip install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/rocm6.1``` - -### NVIDIA - -Nvidia users should install stable pytorch using this command: - -```pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu121``` - -This is the command to install pytorch nightly instead which might have performance improvements: - -```pip install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/cu124``` - -#### Troubleshooting - -If you get the "Torch not compiled with CUDA enabled" error, uninstall torch with: - -```pip uninstall torch``` - -And install it again with the command above. - -### Dependencies - -Install the dependencies by opening your terminal inside the ComfyUI folder and: - -```pip install -r requirements.txt``` - -After this you should have everything installed and can proceed to running ComfyUI. - -### Others: - -#### Intel GPUs - -Intel GPU support is available for all Intel GPUs supported by Intel's Extension for Pytorch (IPEX) with the support requirements listed in the [Installation](https://intel.github.io/intel-extension-for-pytorch/index.html#installation?platform=gpu) page. Choose your platform and method of install and follow the instructions. The steps are as follows: - -1. Start by installing the drivers or kernel listed or newer in the Installation page of IPEX linked above for Windows and Linux if needed. -1. Follow the instructions to install [Intel's oneAPI Basekit](https://www.intel.com/content/www/us/en/developer/tools/oneapi/base-toolkit-download.html) for your platform. -1. Install the packages for IPEX using the instructions provided in the Installation page for your platform. -1. Follow the [ComfyUI manual installation](#manual-install-windows-linux) instructions for Windows and Linux and run ComfyUI normally as described above after everything is installed. - -Additional discussion and help can be found [here](https://github.com/comfyanonymous/ComfyUI/discussions/476). - -#### Apple Mac silicon - -You can install ComfyUI in Apple Mac silicon (M1 or M2) with any recent macOS version. - -1. Install pytorch nightly. For instructions, read the [Accelerated PyTorch training on Mac](https://developer.apple.com/metal/pytorch/) Apple Developer guide (make sure to install the latest pytorch nightly). -1. Follow the [ComfyUI manual installation](#manual-install-windows-linux) instructions for Windows and Linux. -1. Install the ComfyUI [dependencies](#dependencies). If you have another Stable Diffusion UI [you might be able to reuse the dependencies](#i-already-have-another-ui-for-stable-diffusion-installed-do-i-really-have-to-install-all-of-these-dependencies). -1. Launch ComfyUI by running `python main.py` - -> **Note**: Remember to add your models, VAE, LoRAs etc. to the corresponding Comfy folders, as discussed in [ComfyUI manual installation](#manual-install-windows-linux). - -#### DirectML (AMD Cards on Windows) - -```pip install torch-directml``` Then you can launch ComfyUI with: ```python main.py --directml``` - -# Running - -```python main.py``` - -### For AMD cards not officially supported by ROCm - -Try running it with this command if you have issues: - -For 6700, 6600 and maybe other RDNA2 or older: ```HSA_OVERRIDE_GFX_VERSION=10.3.0 python main.py``` - -For AMD 7600 and maybe other RDNA3 cards: ```HSA_OVERRIDE_GFX_VERSION=11.0.0 python main.py``` - -# Notes - -Only parts of the graph that have an output with all the correct inputs will be executed. - -Only parts of the graph that change from each execution to the next will be executed, if you submit the same graph twice only the first will be executed. If you change the last part of the graph only the part you changed and the part that depends on it will be executed. - -Dragging a generated png on the webpage or loading one will give you the full workflow including seeds that were used to create it. - -You can use () to change emphasis of a word or phrase like: (good code:1.2) or (bad code:0.8). The default emphasis for () is 1.1. To use () characters in your actual prompt escape them like \\( or \\). - -You can use {day|night}, for wildcard/dynamic prompts. With this syntax "{wild|card|test}" will be randomly replaced by either "wild", "card" or "test" by the frontend every time you queue the prompt. To use {} characters in your actual prompt escape them like: \\{ or \\}. - -Dynamic prompts also support C-style comments, like `// comment` or `/* comment */`. - -To use a textual inversion concepts/embeddings in a text prompt put them in the models/embeddings directory and use them in the CLIPTextEncode node like this (you can omit the .pt extension): - -```embedding:embedding_filename.pt``` - - -## How to show high-quality previews? - -Use ```--preview-method auto``` to enable previews. - -The default installation includes a fast latent preview method that's low-resolution. To enable higher-quality previews with [TAESD](https://github.com/madebyollin/taesd), download the [taesd_decoder.pth](https://github.com/madebyollin/taesd/raw/main/taesd_decoder.pth) (for SD1.x and SD2.x) and [taesdxl_decoder.pth](https://github.com/madebyollin/taesd/raw/main/taesdxl_decoder.pth) (for SDXL) models and place them in the `models/vae_approx` folder. Once they're installed, restart ComfyUI to enable high-quality previews. - -## How to use TLS/SSL? -Generate a self-signed certificate (not appropriate for shared/production use) and key by running the command: `openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 3650 -nodes -subj "/C=XX/ST=StateName/L=CityName/O=CompanyName/OU=CompanySectionName/CN=CommonNameOrHostname"` - -Use `--tls-keyfile key.pem --tls-certfile cert.pem` to enable TLS/SSL, the app will now be accessible with `https://...` instead of `http://...`. - -> Note: Windows users can use [alexisrolland/docker-openssl](https://github.com/alexisrolland/docker-openssl) or one of the [3rd party binary distributions](https://wiki.openssl.org/index.php/Binaries) to run the command example above. -

If you use a container, note that the volume mount `-v` can be a relative path so `... -v ".\:/openssl-certs" ...` would create the key & cert files in the current directory of your command prompt or powershell terminal. - -## Support and dev channel - -[Matrix space: #comfyui_space:matrix.org](https://app.element.io/#/room/%23comfyui_space%3Amatrix.org) (it's like discord but open source). - -See also: [https://www.comfy.org/](https://www.comfy.org/) - -# QA - -### Which GPU should I buy for this? - -[See this page for some recommendations](https://github.com/comfyanonymous/ComfyUI/wiki/Which-GPU-should-I-buy-for-ComfyUI) - diff --git a/app/__init__.py b/app/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/app_settings.py b/app/app_settings.py deleted file mode 100644 index 8c6edc56c..000000000 --- a/app/app_settings.py +++ /dev/null @@ -1,54 +0,0 @@ -import os -import json -from aiohttp import web - - -class AppSettings(): - def __init__(self, user_manager): - self.user_manager = user_manager - - def get_settings(self, request): - file = self.user_manager.get_request_user_filepath( - request, "comfy.settings.json") - if os.path.isfile(file): - with open(file) as f: - return json.load(f) - else: - return {} - - def save_settings(self, request, settings): - file = self.user_manager.get_request_user_filepath( - request, "comfy.settings.json") - with open(file, "w") as f: - f.write(json.dumps(settings, indent=4)) - - def add_routes(self, routes): - @routes.get("/settings") - async def get_settings(request): - return web.json_response(self.get_settings(request)) - - @routes.get("/settings/{id}") - async def get_setting(request): - value = None - settings = self.get_settings(request) - setting_id = request.match_info.get("id", None) - if setting_id and setting_id in settings: - value = settings[setting_id] - return web.json_response(value) - - @routes.post("/settings") - async def post_settings(request): - settings = self.get_settings(request) - new_settings = await request.json() - self.save_settings(request, {**settings, **new_settings}) - return web.Response(status=200) - - @routes.post("/settings/{id}") - async def post_setting(request): - setting_id = request.match_info.get("id", None) - if not setting_id: - return web.Response(status=400) - settings = self.get_settings(request) - settings[setting_id] = await request.json() - self.save_settings(request, settings) - return web.Response(status=200) \ No newline at end of file diff --git a/app/frontend_management.py b/app/frontend_management.py deleted file mode 100644 index fb57b23f3..000000000 --- a/app/frontend_management.py +++ /dev/null @@ -1,188 +0,0 @@ -from __future__ import annotations -import argparse -import logging -import os -import re -import tempfile -import zipfile -from dataclasses import dataclass -from functools import cached_property -from pathlib import Path -from typing import TypedDict - -import requests -from typing_extensions import NotRequired -from comfy.cli_args import DEFAULT_VERSION_STRING - - -REQUEST_TIMEOUT = 10 # seconds - - -class Asset(TypedDict): - url: str - - -class Release(TypedDict): - id: int - tag_name: str - name: str - prerelease: bool - created_at: str - published_at: str - body: str - assets: NotRequired[list[Asset]] - - -@dataclass -class FrontEndProvider: - owner: str - repo: str - - @property - def folder_name(self) -> str: - return f"{self.owner}_{self.repo}" - - @property - def release_url(self) -> str: - return f"https://api.github.com/repos/{self.owner}/{self.repo}/releases" - - @cached_property - def all_releases(self) -> list[Release]: - releases = [] - api_url = self.release_url - while api_url: - response = requests.get(api_url, timeout=REQUEST_TIMEOUT) - response.raise_for_status() # Raises an HTTPError if the response was an error - releases.extend(response.json()) - # GitHub uses the Link header to provide pagination links. Check if it exists and update api_url accordingly. - if "next" in response.links: - api_url = response.links["next"]["url"] - else: - api_url = None - return releases - - @cached_property - def latest_release(self) -> Release: - latest_release_url = f"{self.release_url}/latest" - response = requests.get(latest_release_url, timeout=REQUEST_TIMEOUT) - response.raise_for_status() # Raises an HTTPError if the response was an error - return response.json() - - def get_release(self, version: str) -> Release: - if version == "latest": - return self.latest_release - else: - for release in self.all_releases: - if release["tag_name"] in [version, f"v{version}"]: - return release - raise ValueError(f"Version {version} not found in releases") - - -def download_release_asset_zip(release: Release, destination_path: str) -> None: - """Download dist.zip from github release.""" - asset_url = None - for asset in release.get("assets", []): - if asset["name"] == "dist.zip": - asset_url = asset["url"] - break - - if not asset_url: - raise ValueError("dist.zip not found in the release assets") - - # Use a temporary file to download the zip content - with tempfile.TemporaryFile() as tmp_file: - headers = {"Accept": "application/octet-stream"} - response = requests.get( - asset_url, headers=headers, allow_redirects=True, timeout=REQUEST_TIMEOUT - ) - response.raise_for_status() # Ensure we got a successful response - - # Write the content to the temporary file - tmp_file.write(response.content) - - # Go back to the beginning of the temporary file - tmp_file.seek(0) - - # Extract the zip file content to the destination path - with zipfile.ZipFile(tmp_file, "r") as zip_ref: - zip_ref.extractall(destination_path) - - -class FrontendManager: - DEFAULT_FRONTEND_PATH = str(Path(__file__).parents[1] / "web") - CUSTOM_FRONTENDS_ROOT = str(Path(__file__).parents[1] / "web_custom_versions") - - @classmethod - def parse_version_string(cls, value: str) -> tuple[str, str, str]: - """ - Args: - value (str): The version string to parse. - - Returns: - tuple[str, str]: A tuple containing provider name and version. - - Raises: - argparse.ArgumentTypeError: If the version string is invalid. - """ - VERSION_PATTERN = r"^([a-zA-Z0-9][a-zA-Z0-9-]{0,38})/([a-zA-Z0-9_.-]+)@(v?\d+\.\d+\.\d+|latest)$" - match_result = re.match(VERSION_PATTERN, value) - if match_result is None: - raise argparse.ArgumentTypeError(f"Invalid version string: {value}") - - return match_result.group(1), match_result.group(2), match_result.group(3) - - @classmethod - def init_frontend_unsafe(cls, version_string: str) -> str: - """ - Initializes the frontend for the specified version. - - Args: - version_string (str): The version string. - - Returns: - str: The path to the initialized frontend. - - Raises: - Exception: If there is an error during the initialization process. - main error source might be request timeout or invalid URL. - """ - if version_string == DEFAULT_VERSION_STRING: - return cls.DEFAULT_FRONTEND_PATH - - repo_owner, repo_name, version = cls.parse_version_string(version_string) - provider = FrontEndProvider(repo_owner, repo_name) - release = provider.get_release(version) - - semantic_version = release["tag_name"].lstrip("v") - web_root = str( - Path(cls.CUSTOM_FRONTENDS_ROOT) / provider.folder_name / semantic_version - ) - if not os.path.exists(web_root): - os.makedirs(web_root, exist_ok=True) - logging.info( - "Downloading frontend(%s) version(%s) to (%s)", - provider.folder_name, - semantic_version, - web_root, - ) - logging.debug(release) - download_release_asset_zip(release, destination_path=web_root) - return web_root - - @classmethod - def init_frontend(cls, version_string: str) -> str: - """ - Initializes the frontend with the specified version string. - - Args: - version_string (str): The version string to initialize the frontend with. - - Returns: - str: The path of the initialized frontend. - """ - try: - return cls.init_frontend_unsafe(version_string) - except Exception as e: - logging.error("Failed to initialize frontend: %s", e) - logging.info("Falling back to the default frontend.") - return cls.DEFAULT_FRONTEND_PATH diff --git a/app/user_manager.py b/app/user_manager.py deleted file mode 100644 index 53dff18b7..000000000 --- a/app/user_manager.py +++ /dev/null @@ -1,205 +0,0 @@ -import json -import os -import re -import uuid -import glob -import shutil -from aiohttp import web -from comfy.cli_args import args -from folder_paths import user_directory -from .app_settings import AppSettings - -default_user = "default" -users_file = os.path.join(user_directory, "users.json") - - -class UserManager(): - def __init__(self): - global user_directory - - self.settings = AppSettings(self) - if not os.path.exists(user_directory): - os.mkdir(user_directory) - if not args.multi_user: - print("****** User settings have been changed to be stored on the server instead of browser storage. ******") - print("****** For multi-user setups add the --multi-user CLI argument to enable multiple user profiles. ******") - - if args.multi_user: - if os.path.isfile(users_file): - with open(users_file) as f: - self.users = json.load(f) - else: - self.users = {} - else: - self.users = {"default": "default"} - - def get_request_user_id(self, request): - user = "default" - if args.multi_user and "comfy-user" in request.headers: - user = request.headers["comfy-user"] - - if user not in self.users: - raise KeyError("Unknown user: " + user) - - return user - - def get_request_user_filepath(self, request, file, type="userdata", create_dir=True): - global user_directory - - if type == "userdata": - root_dir = user_directory - else: - raise KeyError("Unknown filepath type:" + type) - - user = self.get_request_user_id(request) - path = user_root = os.path.abspath(os.path.join(root_dir, user)) - - # prevent leaving /{type} - if os.path.commonpath((root_dir, user_root)) != root_dir: - return None - - if file is not None: - # prevent leaving /{type}/{user} - path = os.path.abspath(os.path.join(user_root, file)) - if os.path.commonpath((user_root, path)) != user_root: - return None - - parent = os.path.split(path)[0] - - if create_dir and not os.path.exists(parent): - os.makedirs(parent, exist_ok=True) - - return path - - def add_user(self, name): - name = name.strip() - if not name: - raise ValueError("username not provided") - user_id = re.sub("[^a-zA-Z0-9-_]+", '-', name) - user_id = user_id + "_" + str(uuid.uuid4()) - - self.users[user_id] = name - - global users_file - with open(users_file, "w") as f: - json.dump(self.users, f) - - return user_id - - def add_routes(self, routes): - self.settings.add_routes(routes) - - @routes.get("/users") - async def get_users(request): - if args.multi_user: - return web.json_response({"storage": "server", "users": self.users}) - else: - user_dir = self.get_request_user_filepath(request, None, create_dir=False) - return web.json_response({ - "storage": "server", - "migrated": os.path.exists(user_dir) - }) - - @routes.post("/users") - async def post_users(request): - body = await request.json() - username = body["username"] - if username in self.users.values(): - return web.json_response({"error": "Duplicate username."}, status=400) - - user_id = self.add_user(username) - return web.json_response(user_id) - - @routes.get("/userdata") - async def listuserdata(request): - directory = request.rel_url.query.get('dir', '') - if not directory: - return web.Response(status=400) - - path = self.get_request_user_filepath(request, directory) - if not path: - return web.Response(status=403) - - if not os.path.exists(path): - return web.Response(status=404) - - recurse = request.rel_url.query.get('recurse', '').lower() == "true" - results = glob.glob(os.path.join( - glob.escape(path), '**/*'), recursive=recurse) - results = [os.path.relpath(x, path) for x in results if os.path.isfile(x)] - - split_path = request.rel_url.query.get('split', '').lower() == "true" - if split_path: - results = [[x] + x.split(os.sep) for x in results] - - return web.json_response(results) - - def get_user_data_path(request, check_exists = False, param = "file"): - file = request.match_info.get(param, None) - if not file: - return web.Response(status=400) - - path = self.get_request_user_filepath(request, file) - if not path: - return web.Response(status=403) - - if check_exists and not os.path.exists(path): - return web.Response(status=404) - - return path - - @routes.get("/userdata/{file}") - async def getuserdata(request): - path = get_user_data_path(request, check_exists=True) - if not isinstance(path, str): - return path - - return web.FileResponse(path) - - @routes.post("/userdata/{file}") - async def post_userdata(request): - path = get_user_data_path(request) - if not isinstance(path, str): - return path - - overwrite = request.query["overwrite"] != "false" - if not overwrite and os.path.exists(path): - return web.Response(status=409) - - body = await request.read() - - with open(path, "wb") as f: - f.write(body) - - resp = os.path.relpath(path, self.get_request_user_filepath(request, None)) - return web.json_response(resp) - - @routes.delete("/userdata/{file}") - async def delete_userdata(request): - path = get_user_data_path(request, check_exists=True) - if not isinstance(path, str): - return path - - os.remove(path) - - return web.Response(status=204) - - @routes.post("/userdata/{file}/move/{dest}") - async def move_userdata(request): - source = get_user_data_path(request, check_exists=True) - if not isinstance(source, str): - return source - - dest = get_user_data_path(request, check_exists=False, param="dest") - if not isinstance(source, str): - return dest - - overwrite = request.query["overwrite"] != "false" - if not overwrite and os.path.exists(dest): - return web.Response(status=409) - - print(f"moving '{source}' -> '{dest}'") - shutil.move(source, dest) - - resp = os.path.relpath(dest, self.get_request_user_filepath(request, None)) - return web.json_response(resp) diff --git a/comfy/cldm/cldm.py b/comfy/cldm/cldm.py index 9ec64a227..1211bc061 100644 --- a/comfy/cldm/cldm.py +++ b/comfy/cldm/cldm.py @@ -15,8 +15,8 @@ from ..ldm.modules.diffusionmodules.openaimodel import UNetModel, TimestepEmbedS from ..ldm.util import exists from .control_types import UNION_CONTROLNET_TYPES from collections import OrderedDict -import comfy.ops -from comfy.ldm.modules.attention import optimized_attention +import totoro.ops +from totoro.ldm.modules.attention import optimized_attention class OptimizedAttention(nn.Module): def __init__(self, c, nhead, dropout=0.0, dtype=None, device=None, operations=None): @@ -95,7 +95,7 @@ class ControlNet(nn.Module): attn_precision=None, union_controlnet_num_control_type=None, device=None, - operations=comfy.ops.disable_weight_init, + operations=totoro.ops.disable_weight_init, **kwargs, ): super().__init__() diff --git a/comfy/cldm/mmdit.py b/comfy/cldm/mmdit.py index 025c2fb5d..d5af44c52 100644 --- a/comfy/cldm/mmdit.py +++ b/comfy/cldm/mmdit.py @@ -1,8 +1,8 @@ import torch from typing import Dict, Optional -import comfy.ldm.modules.diffusionmodules.mmdit +import totoro.ldm.modules.diffusionmodules.mmdit -class ControlNet(comfy.ldm.modules.diffusionmodules.mmdit.MMDiT): +class ControlNet(totoro.ldm.modules.diffusionmodules.mmdit.MMDiT): def __init__( self, num_blocks = None, @@ -17,7 +17,7 @@ class ControlNet(comfy.ldm.modules.diffusionmodules.mmdit.MMDiT): for _ in range(len(self.joint_blocks)): self.controlnet_blocks.append(operations.Linear(self.hidden_size, self.hidden_size, device=device, dtype=dtype)) - self.pos_embed_input = comfy.ldm.modules.diffusionmodules.mmdit.PatchEmbed( + self.pos_embed_input = totoro.ldm.modules.diffusionmodules.mmdit.PatchEmbed( None, self.patch_size, self.in_channels, diff --git a/comfy/cli_args.py b/comfy/cli_args.py index 2397de3d6..cad76e8d4 100644 --- a/comfy/cli_args.py +++ b/comfy/cli_args.py @@ -2,7 +2,7 @@ import argparse import enum import os from typing import Optional -import comfy.options +import totoro.options class EnumAction(argparse.Action): @@ -44,10 +44,10 @@ parser.add_argument("--enable-cors-header", type=str, default=None, metavar="ORI parser.add_argument("--max-upload-size", type=float, default=100, help="Set the maximum upload size in MB.") parser.add_argument("--extra-model-paths-config", type=str, default=None, metavar="PATH", nargs='+', action='append', help="Load one or more extra_model_paths.yaml files.") -parser.add_argument("--output-directory", type=str, default=None, help="Set the ComfyUI output directory.") -parser.add_argument("--temp-directory", type=str, default=None, help="Set the ComfyUI temp directory (default is in the ComfyUI directory).") -parser.add_argument("--input-directory", type=str, default=None, help="Set the ComfyUI input directory.") -parser.add_argument("--auto-launch", action="store_true", help="Automatically launch ComfyUI in the default browser.") +parser.add_argument("--output-directory", type=str, default=None, help="Set the totoroUI output directory.") +parser.add_argument("--temp-directory", type=str, default=None, help="Set the totoroUI temp directory (default is in the totoroUI directory).") +parser.add_argument("--input-directory", type=str, default=None, help="Set the totoroUI input directory.") +parser.add_argument("--auto-launch", action="store_true", help="Automatically launch totoroUI in the default browser.") parser.add_argument("--disable-auto-launch", action="store_true", help="Disable auto launching the browser.") parser.add_argument("--cuda-device", type=int, default=None, metavar="DEVICE_ID", help="Set the id of the cuda device this instance will use.") cm_group = parser.add_mutually_exclusive_group() @@ -114,7 +114,7 @@ vram_group.add_argument("--cpu", action="store_true", help="To use the CPU for e parser.add_argument("--default-hashing-function", type=str, choices=['md5', 'sha1', 'sha256', 'sha512'], default='sha256', help="Allows you to choose the hash function to use for duplicate filename / contents comparison. Default is sha256.") -parser.add_argument("--disable-smart-memory", action="store_true", help="Force ComfyUI to agressively offload to regular ram instead of keeping models in vram when it can.") +parser.add_argument("--disable-smart-memory", action="store_true", help="Force totoroUI to agressively offload to regular ram instead of keeping models in vram when it can.") parser.add_argument("--deterministic", action="store_true", help="Make pytorch use slower deterministic algorithms when it can. Note that this might not make images deterministic in all cases.") parser.add_argument("--dont-print-server", action="store_true", help="Don't print server output.") @@ -129,7 +129,7 @@ parser.add_argument("--multi-user", action="store_true", help="Enables per-user parser.add_argument("--verbose", action="store_true", help="Enables more debug prints.") # The default built-in provider hosted under web/ -DEFAULT_VERSION_STRING = "comfyanonymous/ComfyUI@latest" +DEFAULT_VERSION_STRING = "totoroanonymous/totoroUI@latest" parser.add_argument( "--front-end-version", @@ -161,7 +161,7 @@ parser.add_argument( help="The local filesystem path to the directory where the frontend is located. Overrides --front-end-version.", ) -if comfy.options.args_parsing: +if totoro.options.args_parsing: args = parser.parse_args() else: args = parser.parse_args([]) diff --git a/comfy/clip_model.py b/comfy/clip_model.py index 3c67b737a..52cf92e5c 100644 --- a/comfy/clip_model.py +++ b/comfy/clip_model.py @@ -1,6 +1,6 @@ import torch -from comfy.ldm.modules.attention import optimized_attention_for_device -import comfy.ops +from totoro.ldm.modules.attention import optimized_attention_for_device +import totoro.ops class CLIPAttention(torch.nn.Module): def __init__(self, embed_dim, heads, dtype, device, operations): @@ -78,7 +78,7 @@ class CLIPEmbeddings(torch.nn.Module): self.position_embedding = operations.Embedding(num_positions, embed_dim, dtype=dtype, device=device) def forward(self, input_tokens, dtype=torch.float32): - return self.token_embedding(input_tokens, out_dtype=dtype) + comfy.ops.cast_to(self.position_embedding.weight, dtype=dtype, device=input_tokens.device) + return self.token_embedding(input_tokens, out_dtype=dtype) + totoro.ops.cast_to(self.position_embedding.weight, dtype=dtype, device=input_tokens.device) class CLIPTextModel_(torch.nn.Module): @@ -159,7 +159,7 @@ class CLIPVisionEmbeddings(torch.nn.Module): def forward(self, pixel_values): embeds = self.patch_embedding(pixel_values).flatten(2).transpose(1, 2) - return torch.cat([comfy.ops.cast_to_input(self.class_embedding, embeds).expand(pixel_values.shape[0], 1, -1), embeds], dim=1) + comfy.ops.cast_to_input(self.position_embedding.weight, embeds) + return torch.cat([totoro.ops.cast_to_input(self.class_embedding, embeds).expand(pixel_values.shape[0], 1, -1), embeds], dim=1) + totoro.ops.cast_to_input(self.position_embedding.weight, embeds) class CLIPVision(torch.nn.Module): diff --git a/comfy/clip_vision.py b/comfy/clip_vision.py index 20dc3345d..459133308 100644 --- a/comfy/clip_vision.py +++ b/comfy/clip_vision.py @@ -4,11 +4,11 @@ import torch import json import logging -import comfy.ops -import comfy.model_patcher -import comfy.model_management -import comfy.utils -import comfy.clip_model +import totoro.ops +import totoro.model_patcher +import totoro.model_management +import totoro.utils +import totoro.clip_model class Output: def __getitem__(self, key): @@ -35,13 +35,13 @@ class ClipVisionModel(): config = json.load(f) self.image_size = config.get("image_size", 224) - self.load_device = comfy.model_management.text_encoder_device() - offload_device = comfy.model_management.text_encoder_offload_device() - self.dtype = comfy.model_management.text_encoder_dtype(self.load_device) - self.model = comfy.clip_model.CLIPVisionModelProjection(config, self.dtype, offload_device, comfy.ops.manual_cast) + self.load_device = totoro.model_management.text_encoder_device() + offload_device = totoro.model_management.text_encoder_offload_device() + self.dtype = totoro.model_management.text_encoder_dtype(self.load_device) + self.model = totoro.clip_model.CLIPVisionModelProjection(config, self.dtype, offload_device, totoro.ops.manual_cast) self.model.eval() - self.patcher = comfy.model_patcher.ModelPatcher(self.model, load_device=self.load_device, offload_device=offload_device) + self.patcher = totoro.model_patcher.ModelPatcher(self.model, load_device=self.load_device, offload_device=offload_device) def load_sd(self, sd): return self.model.load_state_dict(sd, strict=False) @@ -50,14 +50,14 @@ class ClipVisionModel(): return self.model.state_dict() def encode_image(self, image): - comfy.model_management.load_model_gpu(self.patcher) + totoro.model_management.load_model_gpu(self.patcher) pixel_values = clip_preprocess(image.to(self.load_device), size=self.image_size).float() out = self.model(pixel_values=pixel_values, intermediate_output=-2) outputs = Output() - outputs["last_hidden_state"] = out[0].to(comfy.model_management.intermediate_device()) - outputs["image_embeds"] = out[2].to(comfy.model_management.intermediate_device()) - outputs["penultimate_hidden_states"] = out[1].to(comfy.model_management.intermediate_device()) + outputs["last_hidden_state"] = out[0].to(totoro.model_management.intermediate_device()) + outputs["image_embeds"] = out[2].to(totoro.model_management.intermediate_device()) + outputs["penultimate_hidden_states"] = out[1].to(totoro.model_management.intermediate_device()) return outputs def convert_to_transformers(sd, prefix): diff --git a/comfy/conds.py b/comfy/conds.py index 660690af8..3f1852d50 100644 --- a/comfy/conds.py +++ b/comfy/conds.py @@ -1,6 +1,6 @@ import torch import math -import comfy.utils +import totoro.utils def lcm(a, b): #TODO: eventually replace by math.lcm (added in python3.9) @@ -14,7 +14,7 @@ class CONDRegular: return self.__class__(cond) def process_cond(self, batch_size, device, **kwargs): - return self._copy_with(comfy.utils.repeat_to_batch_size(self.cond, batch_size).to(device)) + return self._copy_with(totoro.utils.repeat_to_batch_size(self.cond, batch_size).to(device)) def can_concat(self, other): if self.cond.shape != other.cond.shape: @@ -35,7 +35,7 @@ class CONDNoiseShape(CONDRegular): for i in range(dims): data = data.narrow(i + 2, area[i + dims], area[i]) - return self._copy_with(comfy.utils.repeat_to_batch_size(data, batch_size).to(device)) + return self._copy_with(totoro.utils.repeat_to_batch_size(data, batch_size).to(device)) class CONDCrossAttn(CONDRegular): diff --git a/comfy/controlnet.py b/comfy/controlnet.py index 97e4f4d0c..7e1ae9798 100644 --- a/comfy/controlnet.py +++ b/comfy/controlnet.py @@ -2,17 +2,17 @@ import torch import math import os import logging -import comfy.utils -import comfy.model_management -import comfy.model_detection -import comfy.model_patcher -import comfy.ops -import comfy.latent_formats +import totoro.utils +import totoro.model_management +import totoro.model_detection +import totoro.model_patcher +import totoro.ops +import totoro.latent_formats -import comfy.cldm.cldm -import comfy.t2i_adapter.adapter -import comfy.ldm.cascade.controlnet -import comfy.cldm.mmdit +import totoro.cldm.cldm +import totoro.t2i_adapter.adapter +import totoro.ldm.cascade.controlnet +import totoro.cldm.mmdit def broadcast_image_to(tensor, target_batch_size, batched_number): @@ -48,7 +48,7 @@ class ControlBase: self.extra_args = {} if device is None: - device = comfy.model_management.get_torch_device() + device = totoro.model_management.get_torch_device() self.device = device self.previous_controlnet = None @@ -147,7 +147,7 @@ class ControlNet(ControlBase): self.control_model = control_model self.load_device = load_device if control_model is not None: - self.control_model_wrapped = comfy.model_patcher.ModelPatcher(self.control_model, load_device=load_device, offload_device=comfy.model_management.unet_offload_device()) + self.control_model_wrapped = totoro.model_patcher.ModelPatcher(self.control_model, load_device=load_device, offload_device=totoro.model_management.unet_offload_device()) self.compression_ratio = compression_ratio self.global_average_pooling = global_average_pooling @@ -179,11 +179,11 @@ class ControlNet(ControlBase): compression_ratio = self.compression_ratio if self.vae is not None: compression_ratio *= self.vae.downscale_ratio - self.cond_hint = comfy.utils.common_upscale(self.cond_hint_original, x_noisy.shape[3] * compression_ratio, x_noisy.shape[2] * compression_ratio, self.upscale_algorithm, "center") + self.cond_hint = totoro.utils.common_upscale(self.cond_hint_original, x_noisy.shape[3] * compression_ratio, x_noisy.shape[2] * compression_ratio, self.upscale_algorithm, "center") if self.vae is not None: - loaded_models = comfy.model_management.loaded_models(only_currently_used=True) + loaded_models = totoro.model_management.loaded_models(only_currently_used=True) self.cond_hint = self.vae.encode(self.cond_hint.movedim(1, -1)) - comfy.model_management.load_models_gpu(loaded_models) + totoro.model_management.load_models_gpu(loaded_models) if self.latent_format is not None: self.cond_hint = self.latent_format.process_in(self.cond_hint) self.cond_hint = self.cond_hint.to(device=self.device, dtype=dtype) @@ -224,7 +224,7 @@ class ControlNet(ControlBase): super().cleanup() class ControlLoraOps: - class Linear(torch.nn.Module, comfy.ops.CastWeightBiasOp): + class Linear(torch.nn.Module, totoro.ops.CastWeightBiasOp): def __init__(self, in_features: int, out_features: int, bias: bool = True, device=None, dtype=None) -> None: factory_kwargs = {'device': device, 'dtype': dtype} @@ -237,13 +237,13 @@ class ControlLoraOps: self.bias = None def forward(self, input): - weight, bias = comfy.ops.cast_bias_weight(self, input) + weight, bias = totoro.ops.cast_bias_weight(self, input) if self.up is not None: return torch.nn.functional.linear(input, weight + (torch.mm(self.up.flatten(start_dim=1), self.down.flatten(start_dim=1))).reshape(self.weight.shape).type(input.dtype), bias) else: return torch.nn.functional.linear(input, weight, bias) - class Conv2d(torch.nn.Module, comfy.ops.CastWeightBiasOp): + class Conv2d(torch.nn.Module, totoro.ops.CastWeightBiasOp): def __init__( self, in_channels, @@ -277,7 +277,7 @@ class ControlLoraOps: def forward(self, input): - weight, bias = comfy.ops.cast_bias_weight(self, input) + weight, bias = totoro.ops.cast_bias_weight(self, input) if self.up is not None: return torch.nn.functional.conv2d(input, weight + (torch.mm(self.up.flatten(start_dim=1), self.down.flatten(start_dim=1))).reshape(self.weight.shape).type(input.dtype), bias, self.stride, self.padding, self.dilation, self.groups) else: @@ -298,17 +298,17 @@ class ControlLora(ControlNet): self.manual_cast_dtype = model.manual_cast_dtype dtype = model.get_dtype() if self.manual_cast_dtype is None: - class control_lora_ops(ControlLoraOps, comfy.ops.disable_weight_init): + class control_lora_ops(ControlLoraOps, totoro.ops.disable_weight_init): pass else: - class control_lora_ops(ControlLoraOps, comfy.ops.manual_cast): + class control_lora_ops(ControlLoraOps, totoro.ops.manual_cast): pass dtype = self.manual_cast_dtype controlnet_config["operations"] = control_lora_ops controlnet_config["dtype"] = dtype - self.control_model = comfy.cldm.cldm.ControlNet(**controlnet_config) - self.control_model.to(comfy.model_management.get_torch_device()) + self.control_model = totoro.cldm.cldm.ControlNet(**controlnet_config) + self.control_model.to(totoro.model_management.get_torch_device()) diffusion_model = model.diffusion_model sd = diffusion_model.state_dict() cm = self.control_model.state_dict() @@ -316,13 +316,13 @@ class ControlLora(ControlNet): for k in sd: weight = sd[k] try: - comfy.utils.set_attr_param(self.control_model, k, weight) + totoro.utils.set_attr_param(self.control_model, k, weight) except: pass for k in self.control_weights: if k not in {"lora_controlnet"}: - comfy.utils.set_attr_param(self.control_model, k, self.control_weights[k].to(dtype).to(comfy.model_management.get_torch_device())) + totoro.utils.set_attr_param(self.control_model, k, self.control_weights[k].to(dtype).to(totoro.model_management.get_torch_device())) def copy(self): c = ControlLora(self.control_weights, global_average_pooling=self.global_average_pooling) @@ -339,21 +339,21 @@ class ControlLora(ControlNet): return out def inference_memory_requirements(self, dtype): - return comfy.utils.calculate_parameters(self.control_weights) * comfy.model_management.dtype_size(dtype) + ControlBase.inference_memory_requirements(self, dtype) + return totoro.utils.calculate_parameters(self.control_weights) * totoro.model_management.dtype_size(dtype) + ControlBase.inference_memory_requirements(self, dtype) def controlnet_config(sd): - model_config = comfy.model_detection.model_config_from_unet(sd, "", True) + model_config = totoro.model_detection.model_config_from_unet(sd, "", True) supported_inference_dtypes = model_config.supported_inference_dtypes controlnet_config = model_config.unet_config - unet_dtype = comfy.model_management.unet_dtype(supported_dtypes=supported_inference_dtypes) - load_device = comfy.model_management.get_torch_device() - manual_cast_dtype = comfy.model_management.unet_manual_cast(unet_dtype, load_device) + unet_dtype = totoro.model_management.unet_dtype(supported_dtypes=supported_inference_dtypes) + load_device = totoro.model_management.get_torch_device() + manual_cast_dtype = totoro.model_management.unet_manual_cast(unet_dtype, load_device) if manual_cast_dtype is not None: - operations = comfy.ops.manual_cast + operations = totoro.ops.manual_cast else: - operations = comfy.ops.disable_weight_init + operations = totoro.ops.disable_weight_init return model_config, operations, load_device, unet_dtype, manual_cast_dtype @@ -368,23 +368,23 @@ def controlnet_load_state_dict(control_model, sd): return control_model def load_controlnet_mmdit(sd): - new_sd = comfy.model_detection.convert_diffusers_mmdit(sd, "") + new_sd = totoro.model_detection.convert_diffusers_mmdit(sd, "") model_config, operations, load_device, unet_dtype, manual_cast_dtype = controlnet_config(new_sd) - num_blocks = comfy.model_detection.count_blocks(new_sd, 'joint_blocks.{}.') + num_blocks = totoro.model_detection.count_blocks(new_sd, 'joint_blocks.{}.') for k in sd: new_sd[k] = sd[k] - control_model = comfy.cldm.mmdit.ControlNet(num_blocks=num_blocks, operations=operations, device=load_device, dtype=unet_dtype, **model_config.unet_config) + control_model = totoro.cldm.mmdit.ControlNet(num_blocks=num_blocks, operations=operations, device=load_device, dtype=unet_dtype, **model_config.unet_config) control_model = controlnet_load_state_dict(control_model, new_sd) - latent_format = comfy.latent_formats.SD3() + latent_format = totoro.latent_formats.SD3() latent_format.shift_factor = 0 #SD3 controlnet weirdness control = ControlNet(control_model, compression_ratio=1, latent_format=latent_format, load_device=load_device, manual_cast_dtype=manual_cast_dtype) return control def load_controlnet(ckpt_path, model=None): - controlnet_data = comfy.utils.load_torch_file(ckpt_path, safe_load=True) + controlnet_data = totoro.utils.load_torch_file(ckpt_path, safe_load=True) if "lora_controlnet" in controlnet_data: return ControlLora(controlnet_data) @@ -392,8 +392,8 @@ def load_controlnet(ckpt_path, model=None): supported_inference_dtypes = None if "controlnet_cond_embedding.conv_in.weight" in controlnet_data: #diffusers format - controlnet_config = comfy.model_detection.unet_config_from_diffusers_unet(controlnet_data) - diffusers_keys = comfy.utils.unet_to_diffusers(controlnet_config) + controlnet_config = totoro.model_detection.unet_config_from_diffusers_unet(controlnet_data) + diffusers_keys = totoro.utils.unet_to_diffusers(controlnet_config) diffusers_keys["controlnet_mid_block.weight"] = "middle_block_out.0.weight" diffusers_keys["controlnet_mid_block.bias"] = "middle_block_out.0.bias" @@ -460,28 +460,28 @@ def load_controlnet(ckpt_path, model=None): return net if controlnet_config is None: - model_config = comfy.model_detection.model_config_from_unet(controlnet_data, prefix, True) + model_config = totoro.model_detection.model_config_from_unet(controlnet_data, prefix, True) supported_inference_dtypes = model_config.supported_inference_dtypes controlnet_config = model_config.unet_config - load_device = comfy.model_management.get_torch_device() + load_device = totoro.model_management.get_torch_device() if supported_inference_dtypes is None: - unet_dtype = comfy.model_management.unet_dtype() + unet_dtype = totoro.model_management.unet_dtype() else: - unet_dtype = comfy.model_management.unet_dtype(supported_dtypes=supported_inference_dtypes) + unet_dtype = totoro.model_management.unet_dtype(supported_dtypes=supported_inference_dtypes) - manual_cast_dtype = comfy.model_management.unet_manual_cast(unet_dtype, load_device) + manual_cast_dtype = totoro.model_management.unet_manual_cast(unet_dtype, load_device) if manual_cast_dtype is not None: - controlnet_config["operations"] = comfy.ops.manual_cast + controlnet_config["operations"] = totoro.ops.manual_cast controlnet_config["dtype"] = unet_dtype controlnet_config.pop("out_channels") controlnet_config["hint_channels"] = controlnet_data["{}input_hint_block.0.weight".format(prefix)].shape[1] - control_model = comfy.cldm.cldm.ControlNet(**controlnet_config) + control_model = totoro.cldm.cldm.ControlNet(**controlnet_config) if pth: if 'difference' in controlnet_data: if model is not None: - comfy.model_management.load_models_gpu([model]) + totoro.model_management.load_models_gpu([model]) model_sd = model.model_state_dict() for x in controlnet_data: c_m = "control_model." @@ -548,7 +548,7 @@ class T2IAdapter(ControlBase): self.control_input = None self.cond_hint = None width, height = self.scale_image_to(x_noisy.shape[3] * self.compression_ratio, x_noisy.shape[2] * self.compression_ratio) - self.cond_hint = comfy.utils.common_upscale(self.cond_hint_original, width, height, self.upscale_algorithm, "center").float().to(self.device) + self.cond_hint = totoro.utils.common_upscale(self.cond_hint_original, width, height, self.upscale_algorithm, "center").float().to(self.device) if self.channels_in == 1 and self.cond_hint.shape[1] > 1: self.cond_hint = torch.mean(self.cond_hint, 1, keepdim=True) if x_noisy.shape[0] != self.cond_hint.shape[0]: @@ -583,12 +583,12 @@ def load_t2i_adapter(t2i_data): prefix_replace["adapter.body.{}.resnets.{}.".format(i, j)] = "body.{}.".format(i * 2 + j) prefix_replace["adapter.body.{}.".format(i, j)] = "body.{}.".format(i * 2) prefix_replace["adapter."] = "" - t2i_data = comfy.utils.state_dict_prefix_replace(t2i_data, prefix_replace) + t2i_data = totoro.utils.state_dict_prefix_replace(t2i_data, prefix_replace) keys = t2i_data.keys() if "body.0.in_conv.weight" in keys: cin = t2i_data['body.0.in_conv.weight'].shape[1] - model_ad = comfy.t2i_adapter.adapter.Adapter_light(cin=cin, channels=[320, 640, 1280, 1280], nums_rb=4) + model_ad = totoro.t2i_adapter.adapter.Adapter_light(cin=cin, channels=[320, 640, 1280, 1280], nums_rb=4) elif 'conv_in.weight' in keys: cin = t2i_data['conv_in.weight'].shape[1] channel = t2i_data['conv_in.weight'].shape[0] @@ -600,13 +600,13 @@ def load_t2i_adapter(t2i_data): xl = False if cin == 256 or cin == 768: xl = True - model_ad = comfy.t2i_adapter.adapter.Adapter(cin=cin, channels=[channel, channel*2, channel*4, channel*4][:4], nums_rb=2, ksize=ksize, sk=True, use_conv=use_conv, xl=xl) + model_ad = totoro.t2i_adapter.adapter.Adapter(cin=cin, channels=[channel, channel*2, channel*4, channel*4][:4], nums_rb=2, ksize=ksize, sk=True, use_conv=use_conv, xl=xl) elif "backbone.0.0.weight" in keys: - model_ad = comfy.ldm.cascade.controlnet.ControlNet(c_in=t2i_data['backbone.0.0.weight'].shape[1], proj_blocks=[0, 4, 8, 12, 51, 55, 59, 63]) + model_ad = totoro.ldm.cascade.controlnet.ControlNet(c_in=t2i_data['backbone.0.0.weight'].shape[1], proj_blocks=[0, 4, 8, 12, 51, 55, 59, 63]) compression_ratio = 32 upscale_algorithm = 'bilinear' elif "backbone.10.blocks.0.weight" in keys: - model_ad = comfy.ldm.cascade.controlnet.ControlNet(c_in=t2i_data['backbone.0.weight'].shape[1], bottleneck_mode="large", proj_blocks=[0, 4, 8, 12, 51, 55, 59, 63]) + model_ad = totoro.ldm.cascade.controlnet.ControlNet(c_in=t2i_data['backbone.0.weight'].shape[1], bottleneck_mode="large", proj_blocks=[0, 4, 8, 12, 51, 55, 59, 63]) compression_ratio = 1 upscale_algorithm = 'nearest-exact' else: diff --git a/comfy/diffusers_load.py b/comfy/diffusers_load.py index 98b888a19..aff71cc5f 100644 --- a/comfy/diffusers_load.py +++ b/comfy/diffusers_load.py @@ -1,6 +1,6 @@ import os -import comfy.sd +import totoro.sd def first_file(path, filenames): for f in filenames: @@ -22,15 +22,15 @@ def load_diffusers(model_path, output_vae=True, output_clip=True, embedding_dire if text_encoder2_path is not None: text_encoder_paths.append(text_encoder2_path) - unet = comfy.sd.load_unet(unet_path) + unet = totoro.sd.load_unet(unet_path) clip = None if output_clip: - clip = comfy.sd.load_clip(text_encoder_paths, embedding_directory=embedding_directory) + clip = totoro.sd.load_clip(text_encoder_paths, embedding_directory=embedding_directory) vae = None if output_vae: - sd = comfy.utils.load_torch_file(vae_path) - vae = comfy.sd.VAE(sd=sd) + sd = totoro.utils.load_torch_file(vae_path) + vae = totoro.sd.VAE(sd=sd) return (unet, clip, vae) diff --git a/comfy/gligen.py b/comfy/gligen.py index 592522767..fc5f8d9fc 100644 --- a/comfy/gligen.py +++ b/comfy/gligen.py @@ -2,8 +2,8 @@ import torch from torch import nn from .ldm.modules.attention import CrossAttention from inspect import isfunction -import comfy.ops -ops = comfy.ops.manual_cast +import totoro.ops +ops = totoro.ops.manual_cast def exists(val): return val is not None diff --git a/comfy/k_diffusion/sampling.py b/comfy/k_diffusion/sampling.py index 763d8cc78..8fc2c019d 100644 --- a/comfy/k_diffusion/sampling.py +++ b/comfy/k_diffusion/sampling.py @@ -8,7 +8,7 @@ from tqdm.auto import trange, tqdm from . import utils from . import deis -import comfy.model_patcher +import totoro.model_patcher def append_zero(x): return torch.cat([x, x.new_zeros([1])]) @@ -789,7 +789,7 @@ def sample_lcm(model, x, sigmas, extra_args=None, callback=None, disable=None, n @torch.no_grad() def sample_heunpp2(model, x, sigmas, extra_args=None, callback=None, disable=None, s_churn=0., s_tmin=0., s_tmax=float('inf'), s_noise=1.): - # From MIT licensed: https://github.com/Carzit/sd-webui-samplers-scheduler/ + extra_args = {} if extra_args is None else extra_args s_in = x.new_ones([x.shape[0]]) s_end = sigmas[-1] @@ -1007,7 +1007,7 @@ def sample_euler_cfg_pp(model, x, sigmas, extra_args=None, callback=None, disabl return args["denoised"] model_options = extra_args.get("model_options", {}).copy() - extra_args["model_options"] = comfy.model_patcher.set_model_options_post_cfg_function(model_options, post_cfg_function, disable_cfg1_optimization=True) + extra_args["model_options"] = totoro.model_patcher.set_model_options_post_cfg_function(model_options, post_cfg_function, disable_cfg1_optimization=True) s_in = x.new_ones([x.shape[0]]) for i in trange(len(sigmas) - 1, disable=disable): @@ -1033,7 +1033,7 @@ def sample_euler_ancestral_cfg_pp(model, x, sigmas, extra_args=None, callback=No return args["denoised"] model_options = extra_args.get("model_options", {}).copy() - extra_args["model_options"] = comfy.model_patcher.set_model_options_post_cfg_function(model_options, post_cfg_function, disable_cfg1_optimization=True) + extra_args["model_options"] = totoro.model_patcher.set_model_options_post_cfg_function(model_options, post_cfg_function, disable_cfg1_optimization=True) s_in = x.new_ones([x.shape[0]]) for i in trange(len(sigmas) - 1, disable=disable): diff --git a/comfy/ldm/audio/autoencoder.py b/comfy/ldm/audio/autoencoder.py index 8123e66a5..563dd34bb 100644 --- a/comfy/ldm/audio/autoencoder.py +++ b/comfy/ldm/audio/autoencoder.py @@ -4,8 +4,8 @@ import torch from torch import nn from typing import Literal, Dict, Any import math -import comfy.ops -ops = comfy.ops.disable_weight_init +import totoro.ops +ops = totoro.ops.disable_weight_init def vae_sample(mean, scale): stdev = nn.functional.softplus(scale) + 1e-4 diff --git a/comfy/ldm/audio/dit.py b/comfy/ldm/audio/dit.py index 4d2185be8..f87569342 100644 --- a/comfy/ldm/audio/dit.py +++ b/comfy/ldm/audio/dit.py @@ -1,6 +1,6 @@ # code adapted from: https://github.com/Stability-AI/stable-audio-tools -from comfy.ldm.modules.attention import optimized_attention +from totoro.ldm.modules.attention import optimized_attention import typing as tp import torch @@ -9,7 +9,7 @@ from einops import rearrange from torch import nn from torch.nn import functional as F import math -import comfy.ops +import totoro.ops class FourierFeatures(nn.Module): def __init__(self, in_features, out_features, std=1., dtype=None, device=None): @@ -19,7 +19,7 @@ class FourierFeatures(nn.Module): [out_features // 2, in_features], dtype=dtype, device=device)) def forward(self, input): - f = 2 * math.pi * input @ comfy.ops.cast_to_input(self.weight.T, input) + f = 2 * math.pi * input @ totoro.ops.cast_to_input(self.weight.T, input) return torch.cat([f.cos(), f.sin()], dim=-1) # norms @@ -40,8 +40,8 @@ class LayerNorm(nn.Module): def forward(self, x): beta = self.beta if beta is not None: - beta = comfy.ops.cast_to_input(beta, x) - return F.layer_norm(x, x.shape[-1:], weight=comfy.ops.cast_to_input(self.gamma, x), bias=beta) + beta = totoro.ops.cast_to_input(beta, x) + return F.layer_norm(x, x.shape[-1:], weight=totoro.ops.cast_to_input(self.gamma, x), bias=beta) class GLU(nn.Module): def __init__( @@ -164,14 +164,14 @@ class RotaryEmbedding(nn.Module): t = t / self.interpolation_factor - freqs = torch.einsum('i , j -> i j', t, comfy.ops.cast_to_input(self.inv_freq, t)) + freqs = torch.einsum('i , j -> i j', t, totoro.ops.cast_to_input(self.inv_freq, t)) freqs = torch.cat((freqs, freqs), dim = -1) if self.scale is None: return freqs, 1. power = (torch.arange(seq_len, device = device) - (seq_len // 2)) / self.scale_base - scale = comfy.ops.cast_to_input(self.scale, t) ** rearrange(power, 'n -> n 1') + scale = totoro.ops.cast_to_input(self.scale, t) ** rearrange(power, 'n -> n 1') scale = torch.cat((scale, scale), dim = -1) return freqs, scale diff --git a/comfy/ldm/audio/embedders.py b/comfy/ldm/audio/embedders.py index 82a3210c6..c8fe11434 100644 --- a/comfy/ldm/audio/embedders.py +++ b/comfy/ldm/audio/embedders.py @@ -6,7 +6,7 @@ from torch import Tensor, einsum from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, TypeVar, Union from einops import rearrange import math -import comfy.ops +import totoro.ops class LearnedPositionalEmbedding(nn.Module): """Used for continuous time""" @@ -27,7 +27,7 @@ class LearnedPositionalEmbedding(nn.Module): def TimePositionalEmbedding(dim: int, out_features: int) -> nn.Module: return nn.Sequential( LearnedPositionalEmbedding(dim), - comfy.ops.manual_cast.Linear(in_features=dim + 1, out_features=out_features), + totoro.ops.manual_cast.Linear(in_features=dim + 1, out_features=out_features), ) diff --git a/comfy/ldm/aura/mmdit.py b/comfy/ldm/aura/mmdit.py index cd9a42185..e1efc84c1 100644 --- a/comfy/ldm/aura/mmdit.py +++ b/comfy/ldm/aura/mmdit.py @@ -7,9 +7,9 @@ import torch import torch.nn as nn import torch.nn.functional as F -from comfy.ldm.modules.attention import optimized_attention -import comfy.ops -import comfy.ldm.common_dit +from totoro.ldm.modules.attention import optimized_attention +import totoro.ops +import totoro.ldm.common_dit def modulate(x, shift, scale): return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1) @@ -408,7 +408,7 @@ class MMDiT(nn.Module): def patchify(self, x): B, C, H, W = x.size() - x = comfy.ldm.common_dit.pad_to_patch_size(x, (self.patch_size, self.patch_size)) + x = totoro.ldm.common_dit.pad_to_patch_size(x, (self.patch_size, self.patch_size)) x = x.view( B, C, @@ -426,7 +426,7 @@ class MMDiT(nn.Module): max_dim = max(h, w) cur_dim = self.h_max - pos_encoding = comfy.ops.cast_to_input(self.positional_encoding.reshape(1, cur_dim, cur_dim, -1), x) + pos_encoding = totoro.ops.cast_to_input(self.positional_encoding.reshape(1, cur_dim, cur_dim, -1), x) if max_dim > cur_dim: pos_encoding = F.interpolate(pos_encoding.movedim(-1, 1), (max_dim, max_dim), mode="bilinear").movedim(1, -1) @@ -454,7 +454,7 @@ class MMDiT(nn.Module): t = timestep c = self.cond_seq_linear(c_seq) # B, T_c, D - c = torch.cat([comfy.ops.cast_to_input(self.register_tokens, c).repeat(c.size(0), 1, 1), c], dim=1) + c = torch.cat([totoro.ops.cast_to_input(self.register_tokens, c).repeat(c.size(0), 1, 1), c], dim=1) global_cond = self.t_embedder(t, x.dtype) # B, D diff --git a/comfy/ldm/cascade/common.py b/comfy/ldm/cascade/common.py index 3eaa0c821..908a898bc 100644 --- a/comfy/ldm/cascade/common.py +++ b/comfy/ldm/cascade/common.py @@ -1,5 +1,5 @@ """ - This file is part of ComfyUI. + This file is part of totoroUI. Copyright (C) 2024 Stability AI This program is free software: you can redistribute it and/or modify @@ -18,8 +18,8 @@ import torch import torch.nn as nn -from comfy.ldm.modules.attention import optimized_attention -import comfy.ops +from totoro.ldm.modules.attention import optimized_attention +import totoro.ops class OptimizedAttention(nn.Module): def __init__(self, c, nhead, dropout=0.0, dtype=None, device=None, operations=None): @@ -77,7 +77,7 @@ class GlobalResponseNorm(nn.Module): def forward(self, x): Gx = torch.norm(x, p=2, dim=(1, 2), keepdim=True) Nx = Gx / (Gx.mean(dim=-1, keepdim=True) + 1e-6) - return comfy.ops.cast_to_input(self.gamma, x) * (x * Nx) + comfy.ops.cast_to_input(self.beta, x) + x + return totoro.ops.cast_to_input(self.gamma, x) * (x * Nx) + totoro.ops.cast_to_input(self.beta, x) + x class ResBlock(nn.Module): diff --git a/comfy/ldm/cascade/controlnet.py b/comfy/ldm/cascade/controlnet.py index 7a52c3c26..ca46d9cfc 100644 --- a/comfy/ldm/cascade/controlnet.py +++ b/comfy/ldm/cascade/controlnet.py @@ -1,5 +1,5 @@ """ - This file is part of ComfyUI. + This file is part of totoroUI. Copyright (C) 2024 Stability AI This program is free software: you can redistribute it and/or modify diff --git a/comfy/ldm/cascade/stage_a.py b/comfy/ldm/cascade/stage_a.py index ca8867eaf..867902040 100644 --- a/comfy/ldm/cascade/stage_a.py +++ b/comfy/ldm/cascade/stage_a.py @@ -1,5 +1,5 @@ """ - This file is part of ComfyUI. + This file is part of totoroUI. Copyright (C) 2024 Stability AI This program is free software: you can redistribute it and/or modify diff --git a/comfy/ldm/cascade/stage_b.py b/comfy/ldm/cascade/stage_b.py index 7c3d8feab..6560e5cce 100644 --- a/comfy/ldm/cascade/stage_b.py +++ b/comfy/ldm/cascade/stage_b.py @@ -1,5 +1,5 @@ """ - This file is part of ComfyUI. + This file is part of totoroUI. Copyright (C) 2024 Stability AI This program is free software: you can redistribute it and/or modify diff --git a/comfy/ldm/cascade/stage_c.py b/comfy/ldm/cascade/stage_c.py index c85da1f01..9f7910ed2 100644 --- a/comfy/ldm/cascade/stage_c.py +++ b/comfy/ldm/cascade/stage_c.py @@ -1,5 +1,5 @@ """ - This file is part of ComfyUI. + This file is part of totoroUI. Copyright (C) 2024 Stability AI This program is free software: you can redistribute it and/or modify diff --git a/comfy/ldm/cascade/stage_c_coder.py b/comfy/ldm/cascade/stage_c_coder.py index 0cb7c49fc..716a30841 100644 --- a/comfy/ldm/cascade/stage_c_coder.py +++ b/comfy/ldm/cascade/stage_c_coder.py @@ -1,5 +1,5 @@ """ - This file is part of ComfyUI. + This file is part of totoroUI. Copyright (C) 2024 Stability AI This program is free software: you can redistribute it and/or modify diff --git a/comfy/ldm/flux/layers.py b/comfy/ldm/flux/layers.py index 4a0bd40c6..7fba328c4 100644 --- a/comfy/ldm/flux/layers.py +++ b/comfy/ldm/flux/layers.py @@ -6,7 +6,7 @@ from einops import rearrange from torch import Tensor, nn from .math import attention, rope -import comfy.ops +import totoro.ops class EmbedND(nn.Module): def __init__(self, dim: int, theta: int, axes_dim: list): @@ -69,7 +69,7 @@ class RMSNorm(torch.nn.Module): x_dtype = x.dtype x = x.float() rrms = torch.rsqrt(torch.mean(x**2, dim=-1, keepdim=True) + 1e-6) - return (x * rrms).to(dtype=x_dtype) * comfy.ops.cast_to(self.scale, dtype=x_dtype, device=x.device) + return (x * rrms).to(dtype=x_dtype) * totoro.ops.cast_to(self.scale, dtype=x_dtype, device=x.device) class QKNorm(torch.nn.Module): diff --git a/comfy/ldm/flux/math.py b/comfy/ldm/flux/math.py index 136ce2aa8..f61107561 100644 --- a/comfy/ldm/flux/math.py +++ b/comfy/ldm/flux/math.py @@ -1,8 +1,8 @@ import torch from einops import rearrange from torch import Tensor -from comfy.ldm.modules.attention import optimized_attention -import comfy.model_management +from totoro.ldm.modules.attention import optimized_attention +import totoro.model_management def attention(q: Tensor, k: Tensor, v: Tensor, pe: Tensor) -> Tensor: q, k = apply_rope(q, k, pe) @@ -14,7 +14,7 @@ def attention(q: Tensor, k: Tensor, v: Tensor, pe: Tensor) -> Tensor: def rope(pos: Tensor, dim: int, theta: int) -> Tensor: assert dim % 2 == 0 - if comfy.model_management.is_device_mps(pos.device) or comfy.model_management.is_intel_xpu(): + if totoro.model_management.is_device_mps(pos.device) or totoro.model_management.is_intel_xpu(): device = torch.device("cpu") else: device = pos.device diff --git a/comfy/ldm/flux/model.py b/comfy/ldm/flux/model.py index db6cf3d22..e227b7447 100644 --- a/comfy/ldm/flux/model.py +++ b/comfy/ldm/flux/model.py @@ -15,7 +15,7 @@ from .layers import ( ) from einops import rearrange, repeat -import comfy.ldm.common_dit +import totoro.ldm.common_dit @dataclass class FluxParams: @@ -126,7 +126,7 @@ class Flux(nn.Module): def forward(self, x, timestep, context, y, guidance, **kwargs): bs, c, h, w = x.shape patch_size = 2 - x = comfy.ldm.common_dit.pad_to_patch_size(x, (patch_size, patch_size)) + x = totoro.ldm.common_dit.pad_to_patch_size(x, (patch_size, patch_size)) img = rearrange(x, "b c (h ph) (w pw) -> b (h w) (c ph pw)", ph=patch_size, pw=patch_size) diff --git a/comfy/ldm/hydit/attn_layers.py b/comfy/ldm/hydit/attn_layers.py index 920b84286..ecbd21fda 100644 --- a/comfy/ldm/hydit/attn_layers.py +++ b/comfy/ldm/hydit/attn_layers.py @@ -1,7 +1,7 @@ import torch import torch.nn as nn from typing import Tuple, Union, Optional -from comfy.ldm.modules.attention import optimized_attention +from totoro.ldm.modules.attention import optimized_attention def reshape_for_broadcast(freqs_cis: Union[torch.Tensor, Tuple[torch.Tensor]], x: torch.Tensor, head_first=False): diff --git a/comfy/ldm/hydit/models.py b/comfy/ldm/hydit/models.py index c70dbf92a..ee3925ed6 100644 --- a/comfy/ldm/hydit/models.py +++ b/comfy/ldm/hydit/models.py @@ -4,9 +4,9 @@ import torch import torch.nn as nn import torch.nn.functional as F -import comfy.ops -from comfy.ldm.modules.diffusionmodules.mmdit import Mlp, TimestepEmbedder, PatchEmbed, RMSNorm -from comfy.ldm.modules.diffusionmodules.util import timestep_embedding +import totoro.ops +from totoro.ldm.modules.diffusionmodules.mmdit import Mlp, TimestepEmbedder, PatchEmbed, RMSNorm +from totoro.ldm.modules.diffusionmodules.util import timestep_embedding from torch.utils import checkpoint from .attn_layers import Attention, CrossAttention @@ -322,7 +322,7 @@ class HunYuanDiT(nn.Module): b_t5, l_t5, c_t5 = text_states_t5.shape text_states_t5 = self.mlp_t5(text_states_t5.view(-1, c_t5)).view(b_t5, l_t5, -1) - padding = comfy.ops.cast_to_input(self.text_embedding_padding, text_states) + padding = totoro.ops.cast_to_input(self.text_embedding_padding, text_states) text_states[:,-self.text_len:] = torch.where(text_states_mask[:,-self.text_len:].unsqueeze(2), text_states[:,-self.text_len:], padding[:self.text_len]) text_states_t5[:,-self.text_len_t5:] = torch.where(text_states_t5_mask[:,-self.text_len_t5:].unsqueeze(2), text_states_t5[:,-self.text_len_t5:], padding[self.text_len:]) diff --git a/comfy/ldm/hydit/poolers.py b/comfy/ldm/hydit/poolers.py index f5e5b406f..c777a97ff 100644 --- a/comfy/ldm/hydit/poolers.py +++ b/comfy/ldm/hydit/poolers.py @@ -1,8 +1,8 @@ import torch import torch.nn as nn import torch.nn.functional as F -from comfy.ldm.modules.attention import optimized_attention -import comfy.ops +from totoro.ldm.modules.attention import optimized_attention +import totoro.ops class AttentionPool(nn.Module): def __init__(self, spacial_dim: int, embed_dim: int, num_heads: int, output_dim: int = None, dtype=None, device=None, operations=None): @@ -19,7 +19,7 @@ class AttentionPool(nn.Module): x = x[:,:self.positional_embedding.shape[0] - 1] x = x.permute(1, 0, 2) # NLC -> LNC x = torch.cat([x.mean(dim=0, keepdim=True), x], dim=0) # (L+1)NC - x = x + comfy.ops.cast_to_input(self.positional_embedding[:, None, :], x) # (L+1)NC + x = x + totoro.ops.cast_to_input(self.positional_embedding[:, None, :], x) # (L+1)NC q = self.q_proj(x[:1]) k = self.k_proj(x) diff --git a/comfy/ldm/models/autoencoder.py b/comfy/ldm/models/autoencoder.py index f5f4de288..bbd9d95ab 100644 --- a/comfy/ldm/models/autoencoder.py +++ b/comfy/ldm/models/autoencoder.py @@ -2,11 +2,11 @@ import torch from contextlib import contextmanager from typing import Any, Dict, List, Optional, Tuple, Union -from comfy.ldm.modules.distributions.distributions import DiagonalGaussianDistribution +from totoro.ldm.modules.distributions.distributions import DiagonalGaussianDistribution -from comfy.ldm.util import instantiate_from_config -from comfy.ldm.modules.ema import LitEma -import comfy.ops +from totoro.ldm.util import instantiate_from_config +from totoro.ldm.modules.ema import LitEma +import totoro.ops class DiagonalGaussianRegularizer(torch.nn.Module): def __init__(self, sample: bool = True): @@ -151,21 +151,21 @@ class AutoencodingEngineLegacy(AutoencodingEngine): ddconfig = kwargs.pop("ddconfig") super().__init__( encoder_config={ - "target": "comfy.ldm.modules.diffusionmodules.model.Encoder", + "target": "totoro.ldm.modules.diffusionmodules.model.Encoder", "params": ddconfig, }, decoder_config={ - "target": "comfy.ldm.modules.diffusionmodules.model.Decoder", + "target": "totoro.ldm.modules.diffusionmodules.model.Decoder", "params": ddconfig, }, **kwargs, ) - self.quant_conv = comfy.ops.disable_weight_init.Conv2d( + self.quant_conv = totoro.ops.disable_weight_init.Conv2d( (1 + ddconfig["double_z"]) * ddconfig["z_channels"], (1 + ddconfig["double_z"]) * embed_dim, 1, ) - self.post_quant_conv = comfy.ops.disable_weight_init.Conv2d(embed_dim, ddconfig["z_channels"], 1) + self.post_quant_conv = totoro.ops.disable_weight_init.Conv2d(embed_dim, ddconfig["z_channels"], 1) self.embed_dim = embed_dim def get_autoencoder_params(self) -> list: @@ -219,7 +219,7 @@ class AutoencoderKL(AutoencodingEngineLegacy): super().__init__( regularizer_config={ "target": ( - "comfy.ldm.models.autoencoder.DiagonalGaussianRegularizer" + "totoro.ldm.models.autoencoder.DiagonalGaussianRegularizer" ) }, **kwargs, diff --git a/comfy/ldm/modules/attention.py b/comfy/ldm/modules/attention.py index 65a8bcf42..da9dcc5f5 100644 --- a/comfy/ldm/modules/attention.py +++ b/comfy/ldm/modules/attention.py @@ -9,15 +9,15 @@ import logging from .diffusionmodules.util import AlphaBlender, timestep_embedding from .sub_quadratic_attention import efficient_dot_product_attention -from comfy import model_management +from totoro import model_management if model_management.xformers_enabled(): import xformers import xformers.ops -from comfy.cli_args import args -import comfy.ops -ops = comfy.ops.disable_weight_init +from totoro.cli_args import args +import totoro.ops +ops = totoro.ops.disable_weight_init FORCE_UPCAST_ATTENTION_DTYPE = model_management.force_upcast_attention_dtype() diff --git a/comfy/ldm/modules/diffusionmodules/mmdit.py b/comfy/ldm/modules/diffusionmodules/mmdit.py index 491a58a20..8c4cd3e6a 100644 --- a/comfy/ldm/modules/diffusionmodules/mmdit.py +++ b/comfy/ldm/modules/diffusionmodules/mmdit.py @@ -8,8 +8,8 @@ import torch.nn as nn from .. import attention from einops import rearrange, repeat from .util import timestep_embedding -import comfy.ops -import comfy.ldm.common_dit +import totoro.ops +import totoro.ldm.common_dit def default(x, y): if x is not None: @@ -112,7 +112,7 @@ class PatchEmbed(nn.Module): # f"Input width ({W}) should be divisible by patch size ({self.patch_size[1]})." # ) if self.dynamic_img_pad: - x = comfy.ldm.common_dit.pad_to_patch_size(x, self.patch_size, padding_mode=self.padding_mode) + x = totoro.ldm.common_dit.pad_to_patch_size(x, self.patch_size, padding_mode=self.padding_mode) x = self.proj(x) if self.flatten: x = x.flatten(2).transpose(1, 2) # NCHW -> NLC @@ -926,7 +926,7 @@ class MMDiT(nn.Module): context = self.context_processor(context) hw = x.shape[-2:] - x = self.x_embedder(x) + comfy.ops.cast_to_input(self.cropped_pos_embed(hw, device=x.device), x) + x = self.x_embedder(x) + totoro.ops.cast_to_input(self.cropped_pos_embed(hw, device=x.device), x) c = self.t_embedder(t, dtype=x.dtype) # (N, D) if y is not None and self.y_embedder is not None: y = self.y_embedder(y) # (N, D) diff --git a/comfy/ldm/modules/diffusionmodules/model.py b/comfy/ldm/modules/diffusionmodules/model.py index 04eb83b21..aa03f9f02 100644 --- a/comfy/ldm/modules/diffusionmodules/model.py +++ b/comfy/ldm/modules/diffusionmodules/model.py @@ -6,9 +6,9 @@ import numpy as np from typing import Optional, Any import logging -from comfy import model_management -import comfy.ops -ops = comfy.ops.disable_weight_init +from totoro import model_management +import totoro.ops +ops = totoro.ops.disable_weight_init if model_management.xformers_enabled_vae(): import xformers diff --git a/comfy/ldm/modules/diffusionmodules/openaimodel.py b/comfy/ldm/modules/diffusionmodules/openaimodel.py index 6535e899c..ea06af9fa 100644 --- a/comfy/ldm/modules/diffusionmodules/openaimodel.py +++ b/comfy/ldm/modules/diffusionmodules/openaimodel.py @@ -14,9 +14,9 @@ from .util import ( AlphaBlender, ) from ..attention import SpatialTransformer, SpatialVideoTransformer, default -from comfy.ldm.util import exists -import comfy.ops -ops = comfy.ops.disable_weight_init +from totoro.ldm.util import exists +import totoro.ops +ops = totoro.ops.disable_weight_init class TimestepBlock(nn.Module): """ diff --git a/comfy/ldm/modules/diffusionmodules/upscaling.py b/comfy/ldm/modules/diffusionmodules/upscaling.py index f5ac7c2f9..7f2ef05d8 100644 --- a/comfy/ldm/modules/diffusionmodules/upscaling.py +++ b/comfy/ldm/modules/diffusionmodules/upscaling.py @@ -4,7 +4,7 @@ import numpy as np from functools import partial from .util import extract_into_tensor, make_beta_schedule -from comfy.ldm.util import default +from totoro.ldm.util import default class AbstractLowScaleModel(nn.Module): diff --git a/comfy/ldm/modules/diffusionmodules/util.py b/comfy/ldm/modules/diffusionmodules/util.py index ce14ad5e1..4f66d42dd 100644 --- a/comfy/ldm/modules/diffusionmodules/util.py +++ b/comfy/ldm/modules/diffusionmodules/util.py @@ -15,7 +15,7 @@ import torch.nn as nn import numpy as np from einops import repeat, rearrange -from comfy.ldm.util import instantiate_from_config +from totoro.ldm.util import instantiate_from_config class AlphaBlender(nn.Module): strategies = ["learned", "fixed", "learned_with_images"] diff --git a/comfy/ldm/modules/sub_quadratic_attention.py b/comfy/ldm/modules/sub_quadratic_attention.py index 1bc4138c3..17fb3f3fb 100644 --- a/comfy/ldm/modules/sub_quadratic_attention.py +++ b/comfy/ldm/modules/sub_quadratic_attention.py @@ -25,7 +25,7 @@ except ImportError: from torch import Tensor from typing import List -from comfy import model_management +from totoro import model_management def dynamic_slice( x: Tensor, diff --git a/comfy/ldm/modules/temporal_ae.py b/comfy/ldm/modules/temporal_ae.py index 2992aeafc..c0fa3b03c 100644 --- a/comfy/ldm/modules/temporal_ae.py +++ b/comfy/ldm/modules/temporal_ae.py @@ -4,8 +4,8 @@ from typing import Callable, Iterable, Union import torch from einops import rearrange, repeat -import comfy.ops -ops = comfy.ops.disable_weight_init +import totoro.ops +ops = totoro.ops.disable_weight_init from .diffusionmodules.model import ( AttnBlock, diff --git a/comfy/lora.py b/comfy/lora.py index 0a38021c2..5c3c72b04 100644 --- a/comfy/lora.py +++ b/comfy/lora.py @@ -1,4 +1,4 @@ -import comfy.utils +import totoro.utils import logging LORA_CLIP_MAP = { @@ -247,7 +247,7 @@ def model_lora_keys_unet(model, key_map={}): key_map["lora_prior_unet_{}".format(key_lora)] = k #cascade lora: TODO put lora key prefix in the model config key_map["{}".format(k[:-len(".weight")])] = k #generic lora format without any weird key names - diffusers_keys = comfy.utils.unet_to_diffusers(model.model_config.unet_config) + diffusers_keys = totoro.utils.unet_to_diffusers(model.model_config.unet_config) for k in diffusers_keys: if k.endswith(".weight"): unet_key = "diffusion_model.{}".format(diffusers_keys[k]) @@ -261,8 +261,8 @@ def model_lora_keys_unet(model, key_map={}): diffusers_lora_key = diffusers_lora_key[:-2] key_map[diffusers_lora_key] = unet_key - if isinstance(model, comfy.model_base.SD3): #Diffusers lora SD3 - diffusers_keys = comfy.utils.mmdit_to_diffusers(model.model_config.unet_config, output_prefix="diffusion_model.") + if isinstance(model, totoro.model_base.SD3): #Diffusers lora SD3 + diffusers_keys = totoro.utils.mmdit_to_diffusers(model.model_config.unet_config, output_prefix="diffusion_model.") for k in diffusers_keys: if k.endswith(".weight"): to = diffusers_keys[k] @@ -275,22 +275,22 @@ def model_lora_keys_unet(model, key_map={}): key_lora = "lora_transformer_{}".format(k[:-len(".weight")].replace(".", "_")) #OneTrainer lora key_map[key_lora] = to - if isinstance(model, comfy.model_base.AuraFlow): #Diffusers lora AuraFlow - diffusers_keys = comfy.utils.auraflow_to_diffusers(model.model_config.unet_config, output_prefix="diffusion_model.") + if isinstance(model, totoro.model_base.AuraFlow): #Diffusers lora AuraFlow + diffusers_keys = totoro.utils.auraflow_to_diffusers(model.model_config.unet_config, output_prefix="diffusion_model.") for k in diffusers_keys: if k.endswith(".weight"): to = diffusers_keys[k] key_lora = "transformer.{}".format(k[:-len(".weight")]) #simpletrainer and probably regular diffusers lora format key_map[key_lora] = to - if isinstance(model, comfy.model_base.HunyuanDiT): + if isinstance(model, totoro.model_base.HunyuanDiT): for k in sdk: if k.startswith("diffusion_model.") and k.endswith(".weight"): key_lora = k[len("diffusion_model."):-len(".weight")] key_map["base_model.model.{}".format(key_lora)] = k #official hunyuan lora format - if isinstance(model, comfy.model_base.Flux): #Diffusers lora Flux - diffusers_keys = comfy.utils.flux_to_diffusers(model.model_config.unet_config, output_prefix="diffusion_model.") + if isinstance(model, totoro.model_base.Flux): #Diffusers lora Flux + diffusers_keys = totoro.utils.flux_to_diffusers(model.model_config.unet_config, output_prefix="diffusion_model.") for k in diffusers_keys: if k.endswith(".weight"): to = diffusers_keys[k] diff --git a/comfy/model_base.py b/comfy/model_base.py index cb6949649..9ed675e67 100644 --- a/comfy/model_base.py +++ b/comfy/model_base.py @@ -1,23 +1,23 @@ import torch import logging -from comfy.ldm.modules.diffusionmodules.openaimodel import UNetModel, Timestep -from comfy.ldm.cascade.stage_c import StageC -from comfy.ldm.cascade.stage_b import StageB -from comfy.ldm.modules.encoders.noise_aug_modules import CLIPEmbeddingNoiseAugmentation -from comfy.ldm.modules.diffusionmodules.upscaling import ImageConcatWithNoiseAugmentation -from comfy.ldm.modules.diffusionmodules.mmdit import OpenAISignatureMMDITWrapper -import comfy.ldm.aura.mmdit -import comfy.ldm.hydit.models -import comfy.ldm.audio.dit -import comfy.ldm.audio.embedders -import comfy.ldm.flux.model +from totoro.ldm.modules.diffusionmodules.openaimodel import UNetModel, Timestep +from totoro.ldm.cascade.stage_c import StageC +from totoro.ldm.cascade.stage_b import StageB +from totoro.ldm.modules.encoders.noise_aug_modules import CLIPEmbeddingNoiseAugmentation +from totoro.ldm.modules.diffusionmodules.upscaling import ImageConcatWithNoiseAugmentation +from totoro.ldm.modules.diffusionmodules.mmdit import OpenAISignatureMMDITWrapper +import totoro.ldm.aura.mmdit +import totoro.ldm.hydit.models +import totoro.ldm.audio.dit +import totoro.ldm.audio.embedders +import totoro.ldm.flux.model -import comfy.model_management -import comfy.conds -import comfy.ops +import totoro.model_management +import totoro.conds +import totoro.ops from enum import Enum from . import utils -import comfy.latent_formats +import totoro.latent_formats import math class ModelType(Enum): @@ -31,7 +31,7 @@ class ModelType(Enum): FLUX = 8 -from comfy.model_sampling import EPS, V_PREDICTION, EDM, ModelSamplingDiscrete, ModelSamplingContinuousEDM, StableCascadeSampling, ModelSamplingContinuousV +from totoro.model_sampling import EPS, V_PREDICTION, EDM, ModelSamplingDiscrete, ModelSamplingContinuousEDM, StableCascadeSampling, ModelSamplingContinuousV def model_sampling(model_config, model_type): @@ -45,8 +45,8 @@ def model_sampling(model_config, model_type): c = V_PREDICTION s = ModelSamplingContinuousEDM elif model_type == ModelType.FLOW: - c = comfy.model_sampling.CONST - s = comfy.model_sampling.ModelSamplingDiscreteFlow + c = totoro.model_sampling.CONST + s = totoro.model_sampling.ModelSamplingDiscreteFlow elif model_type == ModelType.STABLE_CASCADE: c = EPS s = StableCascadeSampling @@ -57,8 +57,8 @@ def model_sampling(model_config, model_type): c = V_PREDICTION s = ModelSamplingContinuousV elif model_type == ModelType.FLUX: - c = comfy.model_sampling.CONST - s = comfy.model_sampling.ModelSamplingFlux + c = totoro.model_sampling.CONST + s = totoro.model_sampling.ModelSamplingFlux class ModelSampling(s, c): pass @@ -78,11 +78,11 @@ class BaseModel(torch.nn.Module): if not unet_config.get("disable_unet_model_creation", False): if self.manual_cast_dtype is not None: - operations = comfy.ops.manual_cast + operations = totoro.ops.manual_cast else: - operations = comfy.ops.disable_weight_init + operations = totoro.ops.disable_weight_init self.diffusion_model = unet_model(**unet_config, device=device, operations=operations) - if comfy.model_management.force_channels_last(): + if totoro.model_management.force_channels_last(): self.diffusion_model.to(memory_format=torch.channels_last) logging.debug("using channels last mode for diffusion model") logging.info("model weight dtype {}, manual cast: {}".format(self.get_dtype(), self.manual_cast_dtype)) @@ -173,23 +173,23 @@ class BaseModel(torch.nn.Module): elif ck == "masked_image": cond_concat.append(self.blank_inpaint_image_like(noise)) data = torch.cat(cond_concat, dim=1) - out['c_concat'] = comfy.conds.CONDNoiseShape(data) + out['c_concat'] = totoro.conds.CONDNoiseShape(data) adm = self.encode_adm(**kwargs) if adm is not None: - out['y'] = comfy.conds.CONDRegular(adm) + out['y'] = totoro.conds.CONDRegular(adm) cross_attn = kwargs.get("cross_attn", None) if cross_attn is not None: - out['c_crossattn'] = comfy.conds.CONDCrossAttn(cross_attn) + out['c_crossattn'] = totoro.conds.CONDCrossAttn(cross_attn) cross_attn_cnet = kwargs.get("cross_attn_controlnet", None) if cross_attn_cnet is not None: - out['crossattn_controlnet'] = comfy.conds.CONDCrossAttn(cross_attn_cnet) + out['crossattn_controlnet'] = totoro.conds.CONDCrossAttn(cross_attn_cnet) c_concat = kwargs.get("noise_concat", None) if c_concat is not None: - out['c_concat'] = comfy.conds.CONDNoiseShape(c_concat) + out['c_concat'] = totoro.conds.CONDNoiseShape(c_concat) return out @@ -249,13 +249,13 @@ class BaseModel(torch.nn.Module): self.blank_inpaint_image_like = blank_inpaint_image_like def memory_required(self, input_shape): - if comfy.model_management.xformers_enabled() or comfy.model_management.pytorch_attention_flash_attention(): + if totoro.model_management.xformers_enabled() or totoro.model_management.pytorch_attention_flash_attention(): dtype = self.get_dtype() if self.manual_cast_dtype is not None: dtype = self.manual_cast_dtype #TODO: this needs to be tweaked area = input_shape[0] * math.prod(input_shape[2:]) - return (area * comfy.model_management.dtype_size(dtype) * 0.01 * self.memory_usage_factor) * (1024 * 1024) + return (area * totoro.model_management.dtype_size(dtype) * 0.01 * self.memory_usage_factor) * (1024 * 1024) else: #TODO: this formula might be too aggressive since I tweaked the sub-quad and split algorithms to use less memory. area = input_shape[0] * math.prod(input_shape[2:]) @@ -380,7 +380,7 @@ class SVD_img2vid(BaseModel): out = {} adm = self.encode_adm(**kwargs) if adm is not None: - out['y'] = comfy.conds.CONDRegular(adm) + out['y'] = totoro.conds.CONDRegular(adm) latent_image = kwargs.get("concat_latent_image", None) noise = kwargs.get("noise", None) @@ -394,16 +394,16 @@ class SVD_img2vid(BaseModel): latent_image = utils.resize_to_batch_size(latent_image, noise.shape[0]) - out['c_concat'] = comfy.conds.CONDNoiseShape(latent_image) + out['c_concat'] = totoro.conds.CONDNoiseShape(latent_image) cross_attn = kwargs.get("cross_attn", None) if cross_attn is not None: - out['c_crossattn'] = comfy.conds.CONDCrossAttn(cross_attn) + out['c_crossattn'] = totoro.conds.CONDCrossAttn(cross_attn) if "time_conditioning" in kwargs: - out["time_context"] = comfy.conds.CONDCrossAttn(kwargs["time_conditioning"]) + out["time_context"] = totoro.conds.CONDCrossAttn(kwargs["time_conditioning"]) - out['num_video_frames'] = comfy.conds.CONDConstant(noise.shape[0]) + out['num_video_frames'] = totoro.conds.CONDConstant(noise.shape[0]) return out class SV3D_u(SVD_img2vid): @@ -439,7 +439,7 @@ class SV3D_p(SVD_img2vid): class Stable_Zero123(BaseModel): def __init__(self, model_config, model_type=ModelType.EPS, device=None, cc_projection_weight=None, cc_projection_bias=None): super().__init__(model_config, model_type, device=device) - self.cc_projection = comfy.ops.manual_cast.Linear(cc_projection_weight.shape[1], cc_projection_weight.shape[0], dtype=self.get_dtype(), device=device) + self.cc_projection = totoro.ops.manual_cast.Linear(cc_projection_weight.shape[1], cc_projection_weight.shape[0], dtype=self.get_dtype(), device=device) self.cc_projection.weight.copy_(cc_projection_weight) self.cc_projection.bias.copy_(cc_projection_bias) @@ -457,13 +457,13 @@ class Stable_Zero123(BaseModel): latent_image = utils.resize_to_batch_size(latent_image, noise.shape[0]) - out['c_concat'] = comfy.conds.CONDNoiseShape(latent_image) + out['c_concat'] = totoro.conds.CONDNoiseShape(latent_image) cross_attn = kwargs.get("cross_attn", None) if cross_attn is not None: if cross_attn.shape[-1] != 768: cross_attn = self.cc_projection(cross_attn) - out['c_crossattn'] = comfy.conds.CONDCrossAttn(cross_attn) + out['c_crossattn'] = totoro.conds.CONDCrossAttn(cross_attn) return out class SD_X4Upscaler(BaseModel): @@ -494,8 +494,8 @@ class SD_X4Upscaler(BaseModel): image = utils.resize_to_batch_size(image, noise.shape[0]) - out['c_concat'] = comfy.conds.CONDNoiseShape(image) - out['y'] = comfy.conds.CONDRegular(noise_level) + out['c_concat'] = totoro.conds.CONDNoiseShape(image) + out['y'] = totoro.conds.CONDRegular(noise_level) return out class IP2P: @@ -514,10 +514,10 @@ class IP2P: image = utils.resize_to_batch_size(image, noise.shape[0]) - out['c_concat'] = comfy.conds.CONDNoiseShape(self.process_ip2p_image_in(image)) + out['c_concat'] = totoro.conds.CONDNoiseShape(self.process_ip2p_image_in(image)) adm = self.encode_adm(**kwargs) if adm is not None: - out['y'] = comfy.conds.CONDRegular(adm) + out['y'] = totoro.conds.CONDRegular(adm) return out class SD15_instructpix2pix(IP2P, BaseModel): @@ -529,7 +529,7 @@ class SDXL_instructpix2pix(IP2P, SDXL): def __init__(self, model_config, model_type=ModelType.EPS, device=None): super().__init__(model_config, model_type, device=device) if model_type == ModelType.V_PREDICTION_EDM: - self.process_ip2p_image_in = lambda image: comfy.latent_formats.SDXL().process_in(image) #cosxl ip2p + self.process_ip2p_image_in = lambda image: totoro.latent_formats.SDXL().process_in(image) #cosxl ip2p else: self.process_ip2p_image_in = lambda image: image #diffusers ip2p @@ -543,7 +543,7 @@ class StableCascade_C(BaseModel): out = {} clip_text_pooled = kwargs["pooled_output"] if clip_text_pooled is not None: - out['clip_text_pooled'] = comfy.conds.CONDRegular(clip_text_pooled) + out['clip_text_pooled'] = totoro.conds.CONDRegular(clip_text_pooled) if "unclip_conditioning" in kwargs: embeds = [] @@ -553,13 +553,13 @@ class StableCascade_C(BaseModel): clip_img = torch.cat(embeds, dim=1) else: clip_img = torch.zeros((1, 1, 768)) - out["clip_img"] = comfy.conds.CONDRegular(clip_img) - out["sca"] = comfy.conds.CONDRegular(torch.zeros((1,))) - out["crp"] = comfy.conds.CONDRegular(torch.zeros((1,))) + out["clip_img"] = totoro.conds.CONDRegular(clip_img) + out["sca"] = totoro.conds.CONDRegular(torch.zeros((1,))) + out["crp"] = totoro.conds.CONDRegular(torch.zeros((1,))) cross_attn = kwargs.get("cross_attn", None) if cross_attn is not None: - out['clip_text'] = comfy.conds.CONDCrossAttn(cross_attn) + out['clip_text'] = totoro.conds.CONDCrossAttn(cross_attn) return out @@ -574,13 +574,13 @@ class StableCascade_B(BaseModel): clip_text_pooled = kwargs["pooled_output"] if clip_text_pooled is not None: - out['clip'] = comfy.conds.CONDRegular(clip_text_pooled) + out['clip'] = totoro.conds.CONDRegular(clip_text_pooled) #size of prior doesn't really matter if zeros because it gets resized but I still want it to get batched prior = kwargs.get("stable_cascade_prior", torch.zeros((1, 16, (noise.shape[2] * 4) // 42, (noise.shape[3] * 4) // 42), dtype=noise.dtype, layout=noise.layout, device=noise.device)) - out["effnet"] = comfy.conds.CONDRegular(prior) - out["sca"] = comfy.conds.CONDRegular(torch.zeros((1,))) + out["effnet"] = totoro.conds.CONDRegular(prior) + out["sca"] = totoro.conds.CONDRegular(torch.zeros((1,))) return out @@ -595,27 +595,27 @@ class SD3(BaseModel): out = super().extra_conds(**kwargs) cross_attn = kwargs.get("cross_attn", None) if cross_attn is not None: - out['c_crossattn'] = comfy.conds.CONDRegular(cross_attn) + out['c_crossattn'] = totoro.conds.CONDRegular(cross_attn) return out class AuraFlow(BaseModel): def __init__(self, model_config, model_type=ModelType.FLOW, device=None): - super().__init__(model_config, model_type, device=device, unet_model=comfy.ldm.aura.mmdit.MMDiT) + super().__init__(model_config, model_type, device=device, unet_model=totoro.ldm.aura.mmdit.MMDiT) def extra_conds(self, **kwargs): out = super().extra_conds(**kwargs) cross_attn = kwargs.get("cross_attn", None) if cross_attn is not None: - out['c_crossattn'] = comfy.conds.CONDRegular(cross_attn) + out['c_crossattn'] = totoro.conds.CONDRegular(cross_attn) return out class StableAudio1(BaseModel): def __init__(self, model_config, seconds_start_embedder_weights, seconds_total_embedder_weights, model_type=ModelType.V_PREDICTION_CONTINUOUS, device=None): - super().__init__(model_config, model_type, device=device, unet_model=comfy.ldm.audio.dit.AudioDiffusionTransformer) - self.seconds_start_embedder = comfy.ldm.audio.embedders.NumberConditioner(768, min_val=0, max_val=512) - self.seconds_total_embedder = comfy.ldm.audio.embedders.NumberConditioner(768, min_val=0, max_val=512) + super().__init__(model_config, model_type, device=device, unet_model=totoro.ldm.audio.dit.AudioDiffusionTransformer) + self.seconds_start_embedder = totoro.ldm.audio.embedders.NumberConditioner(768, min_val=0, max_val=512) + self.seconds_total_embedder = totoro.ldm.audio.embedders.NumberConditioner(768, min_val=0, max_val=512) self.seconds_start_embedder.load_state_dict(seconds_start_embedder_weights) self.seconds_total_embedder.load_state_dict(seconds_total_embedder_weights) @@ -632,12 +632,12 @@ class StableAudio1(BaseModel): seconds_total_embed = self.seconds_total_embedder([seconds_total])[0].to(device) global_embed = torch.cat([seconds_start_embed, seconds_total_embed], dim=-1).reshape((1, -1)) - out['global_embed'] = comfy.conds.CONDRegular(global_embed) + out['global_embed'] = totoro.conds.CONDRegular(global_embed) cross_attn = kwargs.get("cross_attn", None) if cross_attn is not None: cross_attn = torch.cat([cross_attn.to(device), seconds_start_embed.repeat((cross_attn.shape[0], 1, 1)), seconds_total_embed.repeat((cross_attn.shape[0], 1, 1))], dim=1) - out['c_crossattn'] = comfy.conds.CONDRegular(cross_attn) + out['c_crossattn'] = totoro.conds.CONDRegular(cross_attn) return out def state_dict_for_saving(self, clip_state_dict=None, vae_state_dict=None, clip_vision_state_dict=None): @@ -651,25 +651,25 @@ class StableAudio1(BaseModel): class HunyuanDiT(BaseModel): def __init__(self, model_config, model_type=ModelType.V_PREDICTION, device=None): - super().__init__(model_config, model_type, device=device, unet_model=comfy.ldm.hydit.models.HunYuanDiT) + super().__init__(model_config, model_type, device=device, unet_model=totoro.ldm.hydit.models.HunYuanDiT) def extra_conds(self, **kwargs): out = super().extra_conds(**kwargs) cross_attn = kwargs.get("cross_attn", None) if cross_attn is not None: - out['c_crossattn'] = comfy.conds.CONDRegular(cross_attn) + out['c_crossattn'] = totoro.conds.CONDRegular(cross_attn) attention_mask = kwargs.get("attention_mask", None) if attention_mask is not None: - out['text_embedding_mask'] = comfy.conds.CONDRegular(attention_mask) + out['text_embedding_mask'] = totoro.conds.CONDRegular(attention_mask) conditioning_mt5xl = kwargs.get("conditioning_mt5xl", None) if conditioning_mt5xl is not None: - out['encoder_hidden_states_t5'] = comfy.conds.CONDRegular(conditioning_mt5xl) + out['encoder_hidden_states_t5'] = totoro.conds.CONDRegular(conditioning_mt5xl) attention_mask_mt5xl = kwargs.get("attention_mask_mt5xl", None) if attention_mask_mt5xl is not None: - out['text_embedding_mask_t5'] = comfy.conds.CONDRegular(attention_mask_mt5xl) + out['text_embedding_mask_t5'] = totoro.conds.CONDRegular(attention_mask_mt5xl) width = kwargs.get("width", 768) height = kwargs.get("height", 768) @@ -678,12 +678,12 @@ class HunyuanDiT(BaseModel): target_width = kwargs.get("target_width", width) target_height = kwargs.get("target_height", height) - out['image_meta_size'] = comfy.conds.CONDRegular(torch.FloatTensor([[height, width, target_height, target_width, 0, 0]])) + out['image_meta_size'] = totoro.conds.CONDRegular(torch.FloatTensor([[height, width, target_height, target_width, 0, 0]])) return out class Flux(BaseModel): def __init__(self, model_config, model_type=ModelType.FLUX, device=None): - super().__init__(model_config, model_type, device=device, unet_model=comfy.ldm.flux.model.Flux) + super().__init__(model_config, model_type, device=device, unet_model=totoro.ldm.flux.model.Flux) def encode_adm(self, **kwargs): return kwargs["pooled_output"] @@ -692,6 +692,6 @@ class Flux(BaseModel): out = super().extra_conds(**kwargs) cross_attn = kwargs.get("cross_attn", None) if cross_attn is not None: - out['c_crossattn'] = comfy.conds.CONDRegular(cross_attn) - out['guidance'] = comfy.conds.CONDRegular(torch.FloatTensor([kwargs.get("guidance", 3.5)])) + out['c_crossattn'] = totoro.conds.CONDRegular(cross_attn) + out['guidance'] = totoro.conds.CONDRegular(torch.FloatTensor([kwargs.get("guidance", 3.5)])) return out diff --git a/comfy/model_detection.py b/comfy/model_detection.py index 15e6b735c..0f3310d3d 100644 --- a/comfy/model_detection.py +++ b/comfy/model_detection.py @@ -1,6 +1,6 @@ -import comfy.supported_models -import comfy.supported_models_base -import comfy.utils +import totoro.supported_models +import totoro.supported_models_base +import totoro.utils import math import logging import torch @@ -273,7 +273,7 @@ def detect_unet_config(state_dict, key_prefix): return unet_config def model_config_from_unet_config(unet_config, state_dict=None): - for model_config in comfy.supported_models.models: + for model_config in totoro.supported_models.models: if model_config.matches(unet_config, state_dict): return model_config(unet_config) @@ -286,7 +286,7 @@ def model_config_from_unet(state_dict, unet_key_prefix, use_base_if_no_match=Fal return None model_config = model_config_from_unet_config(unet_config, state_dict) if model_config is None and use_base_if_no_match: - return comfy.supported_models_base.BASE(unet_config) + return totoro.supported_models_base.BASE(unet_config) else: return model_config @@ -498,11 +498,11 @@ def convert_diffusers_mmdit(state_dict, output_prefix=""): if 'transformer_blocks.0.attn.add_q_proj.weight' in state_dict: #SD3 num_blocks = count_blocks(state_dict, 'transformer_blocks.{}.') depth = state_dict["pos_embed.proj.weight"].shape[0] // 64 - sd_map = comfy.utils.mmdit_to_diffusers({"depth": depth, "num_blocks": num_blocks}, output_prefix=output_prefix) + sd_map = totoro.utils.mmdit_to_diffusers({"depth": depth, "num_blocks": num_blocks}, output_prefix=output_prefix) elif 'joint_transformer_blocks.0.attn.add_k_proj.weight' in state_dict: #AuraFlow num_joint = count_blocks(state_dict, 'joint_transformer_blocks.{}.') num_single = count_blocks(state_dict, 'single_transformer_blocks.{}.') - sd_map = comfy.utils.auraflow_to_diffusers({"n_double_layers": num_joint, "n_layers": num_joint + num_single}, output_prefix=output_prefix) + sd_map = totoro.utils.auraflow_to_diffusers({"n_double_layers": num_joint, "n_layers": num_joint + num_single}, output_prefix=output_prefix) else: return None diff --git a/comfy/model_management.py b/comfy/model_management.py index ec80afea2..b039524f6 100644 --- a/comfy/model_management.py +++ b/comfy/model_management.py @@ -1,7 +1,7 @@ import psutil import logging from enum import Enum -from comfy.cli_args import args +from totoro.cli_args import args import torch import sys import platform @@ -975,7 +975,7 @@ def unload_all_models(): def resolve_lowvram_weight(weight, model, key): #TODO: remove - print("WARNING: The comfy.model_management.resolve_lowvram_weight function will be removed soon, please stop using it.") + print("WARNING: The totoro.model_management.resolve_lowvram_weight function will be removed soon, please stop using it.") return weight #TODO: might be cleaner to put this somewhere else diff --git a/comfy/model_patcher.py b/comfy/model_patcher.py index 1ef49308f..1cfd6a756 100644 --- a/comfy/model_patcher.py +++ b/comfy/model_patcher.py @@ -4,13 +4,13 @@ import inspect import logging import uuid -import comfy.utils -import comfy.model_management -from comfy.types import UnetWrapperFunction +import totoro.utils +import totoro.model_management +from totoro.types import UnetWrapperFunction def weight_decompose(dora_scale, weight, lora_diff, alpha, strength): - dora_scale = comfy.model_management.cast_to_device(dora_scale, weight.device, torch.float32) + dora_scale = totoro.model_management.cast_to_device(dora_scale, weight.device, torch.float32) lora_diff *= alpha weight_calc = weight + lora_diff.type(weight.dtype) weight_norm = ( @@ -89,7 +89,7 @@ class ModelPatcher: def model_size(self): if self.size > 0: return self.size - self.size = comfy.model_management.module_size(self.model) + self.size = totoro.model_management.module_size(self.model) return self.size def clone(self): @@ -192,7 +192,7 @@ class ModelPatcher: if name in self.object_patches_backup: return self.object_patches_backup[name] else: - return comfy.utils.get_attr(self.model, name) + return totoro.utils.get_attr(self.model, name) def model_patches_to(self, device): to = self.model_options["transformer_options"] @@ -243,7 +243,7 @@ class ModelPatcher: return list(p) def get_key_patches(self, filter_prefix=None): - comfy.model_management.unload_model_clones(self) + totoro.model_management.unload_model_clones(self) model_sd = self.model_state_dict() p = {} for k in model_sd: @@ -269,7 +269,7 @@ class ModelPatcher: if key not in self.patches: return - weight = comfy.utils.get_attr(self.model, key) + weight = totoro.utils.get_attr(self.model, key) inplace_update = self.weight_inplace_update @@ -277,18 +277,18 @@ class ModelPatcher: self.backup[key] = weight.to(device=self.offload_device, copy=inplace_update) if device_to is not None: - temp_weight = comfy.model_management.cast_to_device(weight, device_to, torch.float32, copy=True) + temp_weight = totoro.model_management.cast_to_device(weight, device_to, torch.float32, copy=True) else: temp_weight = weight.to(torch.float32, copy=True) out_weight = self.calculate_weight(self.patches[key], temp_weight, key).to(weight.dtype) if inplace_update: - comfy.utils.copy_to_param(self.model, key, out_weight) + totoro.utils.copy_to_param(self.model, key, out_weight) else: - comfy.utils.set_attr_param(self.model, key, out_weight) + totoro.utils.set_attr_param(self.model, key, out_weight) def patch_model(self, device_to=None, patch_weights=True): for k in self.object_patches: - old = comfy.utils.set_attr(self.model, k, self.object_patches[k]) + old = totoro.utils.set_attr(self.model, k, self.object_patches[k]) if k not in self.object_patches_backup: self.object_patches_backup[k] = old @@ -322,8 +322,8 @@ class ModelPatcher: patch_counter = 0 for n, m in self.model.named_modules(): lowvram_weight = False - if hasattr(m, "comfy_cast_weights"): - module_mem = comfy.model_management.module_size(m) + if hasattr(m, "totoro_cast_weights"): + module_mem = totoro.model_management.module_size(m) if mem_counter + module_mem >= lowvram_model_memory: lowvram_weight = True @@ -344,14 +344,14 @@ class ModelPatcher: m.bias_function = LowVramPatch(bias_key, self) patch_counter += 1 - m.prev_comfy_cast_weights = m.comfy_cast_weights - m.comfy_cast_weights = True + m.prev_totoro_cast_weights = m.totoro_cast_weights + m.totoro_cast_weights = True else: if hasattr(m, "weight"): self.patch_weight_to_device(weight_key) #TODO: speed this up without causing OOM self.patch_weight_to_device(bias_key) m.to(device_to) - mem_counter += comfy.model_management.module_size(m) + mem_counter += totoro.model_management.module_size(m) logging.debug("lowvram: loaded module regularly {} {}".format(n, m)) self.model_lowvram = True @@ -392,10 +392,10 @@ class ModelPatcher: if w1.shape != weight.shape: logging.warning("WARNING SHAPE MISMATCH {} WEIGHT NOT MERGED {} != {}".format(key, w1.shape, weight.shape)) else: - weight += function(strength * comfy.model_management.cast_to_device(w1, weight.device, weight.dtype)) + weight += function(strength * totoro.model_management.cast_to_device(w1, weight.device, weight.dtype)) elif patch_type == "lora": #lora/locon - mat1 = comfy.model_management.cast_to_device(v[0], weight.device, torch.float32) - mat2 = comfy.model_management.cast_to_device(v[1], weight.device, torch.float32) + mat1 = totoro.model_management.cast_to_device(v[0], weight.device, torch.float32) + mat2 = totoro.model_management.cast_to_device(v[1], weight.device, torch.float32) dora_scale = v[4] if v[2] is not None: alpha = v[2] / mat2.shape[0] @@ -404,7 +404,7 @@ class ModelPatcher: if v[3] is not None: #locon mid weights, hopefully the math is fine because I didn't properly test it - mat3 = comfy.model_management.cast_to_device(v[3], weight.device, torch.float32) + mat3 = totoro.model_management.cast_to_device(v[3], weight.device, torch.float32) final_shape = [mat2.shape[1], mat2.shape[0], mat3.shape[2], mat3.shape[3]] mat2 = torch.mm(mat2.transpose(0, 1).flatten(start_dim=1), mat3.transpose(0, 1).flatten(start_dim=1)).reshape(final_shape).transpose(0, 1) try: @@ -428,23 +428,23 @@ class ModelPatcher: if w1 is None: dim = w1_b.shape[0] - w1 = torch.mm(comfy.model_management.cast_to_device(w1_a, weight.device, torch.float32), - comfy.model_management.cast_to_device(w1_b, weight.device, torch.float32)) + w1 = torch.mm(totoro.model_management.cast_to_device(w1_a, weight.device, torch.float32), + totoro.model_management.cast_to_device(w1_b, weight.device, torch.float32)) else: - w1 = comfy.model_management.cast_to_device(w1, weight.device, torch.float32) + w1 = totoro.model_management.cast_to_device(w1, weight.device, torch.float32) if w2 is None: dim = w2_b.shape[0] if t2 is None: - w2 = torch.mm(comfy.model_management.cast_to_device(w2_a, weight.device, torch.float32), - comfy.model_management.cast_to_device(w2_b, weight.device, torch.float32)) + w2 = torch.mm(totoro.model_management.cast_to_device(w2_a, weight.device, torch.float32), + totoro.model_management.cast_to_device(w2_b, weight.device, torch.float32)) else: w2 = torch.einsum('i j k l, j r, i p -> p r k l', - comfy.model_management.cast_to_device(t2, weight.device, torch.float32), - comfy.model_management.cast_to_device(w2_b, weight.device, torch.float32), - comfy.model_management.cast_to_device(w2_a, weight.device, torch.float32)) + totoro.model_management.cast_to_device(t2, weight.device, torch.float32), + totoro.model_management.cast_to_device(w2_b, weight.device, torch.float32), + totoro.model_management.cast_to_device(w2_a, weight.device, torch.float32)) else: - w2 = comfy.model_management.cast_to_device(w2, weight.device, torch.float32) + w2 = totoro.model_management.cast_to_device(w2, weight.device, torch.float32) if len(w2.shape) == 4: w1 = w1.unsqueeze(2).unsqueeze(2) @@ -476,19 +476,19 @@ class ModelPatcher: t1 = v[5] t2 = v[6] m1 = torch.einsum('i j k l, j r, i p -> p r k l', - comfy.model_management.cast_to_device(t1, weight.device, torch.float32), - comfy.model_management.cast_to_device(w1b, weight.device, torch.float32), - comfy.model_management.cast_to_device(w1a, weight.device, torch.float32)) + totoro.model_management.cast_to_device(t1, weight.device, torch.float32), + totoro.model_management.cast_to_device(w1b, weight.device, torch.float32), + totoro.model_management.cast_to_device(w1a, weight.device, torch.float32)) m2 = torch.einsum('i j k l, j r, i p -> p r k l', - comfy.model_management.cast_to_device(t2, weight.device, torch.float32), - comfy.model_management.cast_to_device(w2b, weight.device, torch.float32), - comfy.model_management.cast_to_device(w2a, weight.device, torch.float32)) + totoro.model_management.cast_to_device(t2, weight.device, torch.float32), + totoro.model_management.cast_to_device(w2b, weight.device, torch.float32), + totoro.model_management.cast_to_device(w2a, weight.device, torch.float32)) else: - m1 = torch.mm(comfy.model_management.cast_to_device(w1a, weight.device, torch.float32), - comfy.model_management.cast_to_device(w1b, weight.device, torch.float32)) - m2 = torch.mm(comfy.model_management.cast_to_device(w2a, weight.device, torch.float32), - comfy.model_management.cast_to_device(w2b, weight.device, torch.float32)) + m1 = torch.mm(totoro.model_management.cast_to_device(w1a, weight.device, torch.float32), + totoro.model_management.cast_to_device(w1b, weight.device, torch.float32)) + m2 = torch.mm(totoro.model_management.cast_to_device(w2a, weight.device, torch.float32), + totoro.model_management.cast_to_device(w2b, weight.device, torch.float32)) try: lora_diff = (m1 * m2).reshape(weight.shape) @@ -506,10 +506,10 @@ class ModelPatcher: dora_scale = v[5] - a1 = comfy.model_management.cast_to_device(v[0].flatten(start_dim=1), weight.device, torch.float32) - a2 = comfy.model_management.cast_to_device(v[1].flatten(start_dim=1), weight.device, torch.float32) - b1 = comfy.model_management.cast_to_device(v[2].flatten(start_dim=1), weight.device, torch.float32) - b2 = comfy.model_management.cast_to_device(v[3].flatten(start_dim=1), weight.device, torch.float32) + a1 = totoro.model_management.cast_to_device(v[0].flatten(start_dim=1), weight.device, torch.float32) + a2 = totoro.model_management.cast_to_device(v[1].flatten(start_dim=1), weight.device, torch.float32) + b1 = totoro.model_management.cast_to_device(v[2].flatten(start_dim=1), weight.device, torch.float32) + b2 = totoro.model_management.cast_to_device(v[3].flatten(start_dim=1), weight.device, torch.float32) try: lora_diff = (torch.mm(b2, b1) + torch.mm(torch.mm(weight.flatten(start_dim=1), a2), a1)).reshape(weight.shape) @@ -531,9 +531,9 @@ class ModelPatcher: if unpatch_weights: if self.model_lowvram: for m in self.model.modules(): - if hasattr(m, "prev_comfy_cast_weights"): - m.comfy_cast_weights = m.prev_comfy_cast_weights - del m.prev_comfy_cast_weights + if hasattr(m, "prev_totoro_cast_weights"): + m.totoro_cast_weights = m.prev_totoro_cast_weights + del m.prev_totoro_cast_weights m.weight_function = None m.bias_function = None @@ -544,10 +544,10 @@ class ModelPatcher: if self.weight_inplace_update: for k in keys: - comfy.utils.copy_to_param(self.model, k, self.backup[k]) + totoro.utils.copy_to_param(self.model, k, self.backup[k]) else: for k in keys: - comfy.utils.set_attr_param(self.model, k, self.backup[k]) + totoro.utils.set_attr_param(self.model, k, self.backup[k]) self.backup.clear() @@ -557,7 +557,7 @@ class ModelPatcher: keys = list(self.object_patches_backup.keys()) for k in keys: - comfy.utils.set_attr(self.model, k, self.object_patches_backup[k]) + totoro.utils.set_attr(self.model, k, self.object_patches_backup[k]) self.object_patches_backup.clear() diff --git a/comfy/model_sampling.py b/comfy/model_sampling.py index 4a0f2db60..353b7a7d7 100644 --- a/comfy/model_sampling.py +++ b/comfy/model_sampling.py @@ -1,5 +1,5 @@ import torch -from comfy.ldm.modules.diffusionmodules.util import make_beta_schedule +from totoro.ldm.modules.diffusionmodules.util import make_beta_schedule import math class EPS: diff --git a/comfy/ops.py b/comfy/ops.py index 47e8d7a9d..c51d3044f 100644 --- a/comfy/ops.py +++ b/comfy/ops.py @@ -1,5 +1,5 @@ """ - This file is part of ComfyUI. + This file is part of totoroUI. Copyright (C) 2024 Stability AI This program is free software: you can redistribute it and/or modify @@ -17,7 +17,7 @@ """ import torch -import comfy.model_management +import totoro.model_management def cast_to(weight, dtype=None, device=None, non_blocking=False): @@ -34,7 +34,7 @@ def cast_bias_weight(s, input=None, dtype=None, device=None): device = input.device bias = None - non_blocking = comfy.model_management.device_should_use_non_blocking(device) + non_blocking = totoro.model_management.device_should_use_non_blocking(device) if s.bias is not None: bias = cast_to(s.bias, dtype, device, non_blocking=non_blocking) if s.bias_function is not None: @@ -45,7 +45,7 @@ def cast_bias_weight(s, input=None, dtype=None, device=None): return weight, bias class CastWeightBiasOp: - comfy_cast_weights = False + totoro_cast_weights = False weight_function = None bias_function = None @@ -54,13 +54,13 @@ class disable_weight_init: def reset_parameters(self): return None - def forward_comfy_cast_weights(self, input): + def forward_totoro_cast_weights(self, input): weight, bias = cast_bias_weight(self, input) return torch.nn.functional.linear(input, weight, bias) def forward(self, *args, **kwargs): - if self.comfy_cast_weights: - return self.forward_comfy_cast_weights(*args, **kwargs) + if self.totoro_cast_weights: + return self.forward_totoro_cast_weights(*args, **kwargs) else: return super().forward(*args, **kwargs) @@ -68,13 +68,13 @@ class disable_weight_init: def reset_parameters(self): return None - def forward_comfy_cast_weights(self, input): + def forward_totoro_cast_weights(self, input): weight, bias = cast_bias_weight(self, input) return self._conv_forward(input, weight, bias) def forward(self, *args, **kwargs): - if self.comfy_cast_weights: - return self.forward_comfy_cast_weights(*args, **kwargs) + if self.totoro_cast_weights: + return self.forward_totoro_cast_weights(*args, **kwargs) else: return super().forward(*args, **kwargs) @@ -82,13 +82,13 @@ class disable_weight_init: def reset_parameters(self): return None - def forward_comfy_cast_weights(self, input): + def forward_totoro_cast_weights(self, input): weight, bias = cast_bias_weight(self, input) return self._conv_forward(input, weight, bias) def forward(self, *args, **kwargs): - if self.comfy_cast_weights: - return self.forward_comfy_cast_weights(*args, **kwargs) + if self.totoro_cast_weights: + return self.forward_totoro_cast_weights(*args, **kwargs) else: return super().forward(*args, **kwargs) @@ -96,13 +96,13 @@ class disable_weight_init: def reset_parameters(self): return None - def forward_comfy_cast_weights(self, input): + def forward_totoro_cast_weights(self, input): weight, bias = cast_bias_weight(self, input) return self._conv_forward(input, weight, bias) def forward(self, *args, **kwargs): - if self.comfy_cast_weights: - return self.forward_comfy_cast_weights(*args, **kwargs) + if self.totoro_cast_weights: + return self.forward_totoro_cast_weights(*args, **kwargs) else: return super().forward(*args, **kwargs) @@ -110,13 +110,13 @@ class disable_weight_init: def reset_parameters(self): return None - def forward_comfy_cast_weights(self, input): + def forward_totoro_cast_weights(self, input): weight, bias = cast_bias_weight(self, input) return torch.nn.functional.group_norm(input, self.num_groups, weight, bias, self.eps) def forward(self, *args, **kwargs): - if self.comfy_cast_weights: - return self.forward_comfy_cast_weights(*args, **kwargs) + if self.totoro_cast_weights: + return self.forward_totoro_cast_weights(*args, **kwargs) else: return super().forward(*args, **kwargs) @@ -125,7 +125,7 @@ class disable_weight_init: def reset_parameters(self): return None - def forward_comfy_cast_weights(self, input): + def forward_totoro_cast_weights(self, input): if self.weight is not None: weight, bias = cast_bias_weight(self, input) else: @@ -134,8 +134,8 @@ class disable_weight_init: return torch.nn.functional.layer_norm(input, self.normalized_shape, weight, bias, self.eps) def forward(self, *args, **kwargs): - if self.comfy_cast_weights: - return self.forward_comfy_cast_weights(*args, **kwargs) + if self.totoro_cast_weights: + return self.forward_totoro_cast_weights(*args, **kwargs) else: return super().forward(*args, **kwargs) @@ -143,7 +143,7 @@ class disable_weight_init: def reset_parameters(self): return None - def forward_comfy_cast_weights(self, input, output_size=None): + def forward_totoro_cast_weights(self, input, output_size=None): num_spatial_dims = 2 output_padding = self._output_padding( input, output_size, self.stride, self.padding, self.kernel_size, @@ -155,8 +155,8 @@ class disable_weight_init: output_padding, self.groups, self.dilation) def forward(self, *args, **kwargs): - if self.comfy_cast_weights: - return self.forward_comfy_cast_weights(*args, **kwargs) + if self.totoro_cast_weights: + return self.forward_totoro_cast_weights(*args, **kwargs) else: return super().forward(*args, **kwargs) @@ -164,7 +164,7 @@ class disable_weight_init: def reset_parameters(self): return None - def forward_comfy_cast_weights(self, input, output_size=None): + def forward_totoro_cast_weights(self, input, output_size=None): num_spatial_dims = 1 output_padding = self._output_padding( input, output_size, self.stride, self.padding, self.kernel_size, @@ -176,8 +176,8 @@ class disable_weight_init: output_padding, self.groups, self.dilation) def forward(self, *args, **kwargs): - if self.comfy_cast_weights: - return self.forward_comfy_cast_weights(*args, **kwargs) + if self.totoro_cast_weights: + return self.forward_totoro_cast_weights(*args, **kwargs) else: return super().forward(*args, **kwargs) @@ -186,7 +186,7 @@ class disable_weight_init: self.bias = None return None - def forward_comfy_cast_weights(self, input, out_dtype=None): + def forward_totoro_cast_weights(self, input, out_dtype=None): output_dtype = out_dtype if self.weight.dtype == torch.float16 or self.weight.dtype == torch.bfloat16: out_dtype = None @@ -194,8 +194,8 @@ class disable_weight_init: return torch.nn.functional.embedding(input, weight, self.padding_idx, self.max_norm, self.norm_type, self.scale_grad_by_freq, self.sparse).to(dtype=output_dtype) def forward(self, *args, **kwargs): - if self.comfy_cast_weights: - return self.forward_comfy_cast_weights(*args, **kwargs) + if self.totoro_cast_weights: + return self.forward_totoro_cast_weights(*args, **kwargs) else: if "out_dtype" in kwargs: kwargs.pop("out_dtype") @@ -213,28 +213,28 @@ class disable_weight_init: class manual_cast(disable_weight_init): class Linear(disable_weight_init.Linear): - comfy_cast_weights = True + totoro_cast_weights = True class Conv1d(disable_weight_init.Conv1d): - comfy_cast_weights = True + totoro_cast_weights = True class Conv2d(disable_weight_init.Conv2d): - comfy_cast_weights = True + totoro_cast_weights = True class Conv3d(disable_weight_init.Conv3d): - comfy_cast_weights = True + totoro_cast_weights = True class GroupNorm(disable_weight_init.GroupNorm): - comfy_cast_weights = True + totoro_cast_weights = True class LayerNorm(disable_weight_init.LayerNorm): - comfy_cast_weights = True + totoro_cast_weights = True class ConvTranspose2d(disable_weight_init.ConvTranspose2d): - comfy_cast_weights = True + totoro_cast_weights = True class ConvTranspose1d(disable_weight_init.ConvTranspose1d): - comfy_cast_weights = True + totoro_cast_weights = True class Embedding(disable_weight_init.Embedding): - comfy_cast_weights = True + totoro_cast_weights = True diff --git a/comfy/sample.py b/comfy/sample.py index 98dcaca7f..ae5a309dd 100644 --- a/comfy/sample.py +++ b/comfy/sample.py @@ -1,7 +1,7 @@ import torch -import comfy.model_management -import comfy.samplers -import comfy.utils +import totoro.model_management +import totoro.samplers +import totoro.utils import numpy as np import logging @@ -27,24 +27,24 @@ def prepare_noise(latent_image, seed, noise_inds=None): def fix_empty_latent_channels(model, latent_image): latent_channels = model.get_model_object("latent_format").latent_channels #Resize the empty latent image so it has the right number of channels if latent_channels != latent_image.shape[1] and torch.count_nonzero(latent_image) == 0: - latent_image = comfy.utils.repeat_to_batch_size(latent_image, latent_channels, dim=1) + latent_image = totoro.utils.repeat_to_batch_size(latent_image, latent_channels, dim=1) return latent_image def prepare_sampling(model, noise_shape, positive, negative, noise_mask): - logging.warning("Warning: comfy.sample.prepare_sampling isn't used anymore and can be removed") + logging.warning("Warning: totoro.sample.prepare_sampling isn't used anymore and can be removed") return model, positive, negative, noise_mask, [] def cleanup_additional_models(models): - logging.warning("Warning: comfy.sample.cleanup_additional_models isn't used anymore and can be removed") + logging.warning("Warning: totoro.sample.cleanup_additional_models isn't used anymore and can be removed") def sample(model, noise, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise=1.0, disable_noise=False, start_step=None, last_step=None, force_full_denoise=False, noise_mask=None, sigmas=None, callback=None, disable_pbar=False, seed=None): - sampler = comfy.samplers.KSampler(model, steps=steps, device=model.load_device, sampler=sampler_name, scheduler=scheduler, denoise=denoise, model_options=model.model_options) + sampler = totoro.samplers.KSampler(model, steps=steps, device=model.load_device, sampler=sampler_name, scheduler=scheduler, denoise=denoise, model_options=model.model_options) samples = sampler.sample(noise, positive, negative, cfg=cfg, latent_image=latent_image, start_step=start_step, last_step=last_step, force_full_denoise=force_full_denoise, denoise_mask=noise_mask, sigmas=sigmas, callback=callback, disable_pbar=disable_pbar, seed=seed) - samples = samples.to(comfy.model_management.intermediate_device()) + samples = samples.to(totoro.model_management.intermediate_device()) return samples def sample_custom(model, noise, cfg, sampler, sigmas, positive, negative, latent_image, noise_mask=None, callback=None, disable_pbar=False, seed=None): - samples = comfy.samplers.sample(model, noise, positive, negative, cfg, model.load_device, sampler, sigmas, model_options=model.model_options, latent_image=latent_image, denoise_mask=noise_mask, callback=callback, disable_pbar=disable_pbar, seed=seed) - samples = samples.to(comfy.model_management.intermediate_device()) + samples = totoro.samplers.sample(model, noise, positive, negative, cfg, model.load_device, sampler, sigmas, model_options=model.model_options, latent_image=latent_image, denoise_mask=noise_mask, callback=callback, disable_pbar=disable_pbar, seed=seed) + samples = samples.to(totoro.model_management.intermediate_device()) return samples diff --git a/comfy/sampler_helpers.py b/comfy/sampler_helpers.py index 4a2ec123b..ac07cc984 100644 --- a/comfy/sampler_helpers.py +++ b/comfy/sampler_helpers.py @@ -1,12 +1,12 @@ import torch -import comfy.model_management -import comfy.conds +import totoro.model_management +import totoro.conds def prepare_mask(noise_mask, shape, device): """ensures noise mask is of proper dimensions""" noise_mask = torch.nn.functional.interpolate(noise_mask.reshape((-1, 1, noise_mask.shape[-2], noise_mask.shape[-1])), size=(shape[2], shape[3]), mode="bilinear") noise_mask = torch.cat([noise_mask] * shape[1], dim=1) - noise_mask = comfy.utils.repeat_to_batch_size(noise_mask, shape[0]) + noise_mask = totoro.utils.repeat_to_batch_size(noise_mask, shape[0]) noise_mask = noise_mask.to(device) return noise_mask @@ -23,7 +23,7 @@ def convert_cond(cond): temp = c[1].copy() model_conds = temp.get("model_conds", {}) if c[0] is not None: - model_conds["c_crossattn"] = comfy.conds.CONDCrossAttn(c[0]) #TODO: remove + model_conds["c_crossattn"] = totoro.conds.CONDCrossAttn(c[0]) #TODO: remove temp["cross_attn"] = c[0] temp["model_conds"] = model_conds out.append(temp) @@ -63,7 +63,7 @@ def prepare_sampling(model, noise_shape, conds): models, inference_memory = get_additional_models(conds, model.model_dtype()) memory_required = model.memory_required([noise_shape[0] * 2] + list(noise_shape[1:])) + inference_memory minimum_memory_required = model.memory_required([noise_shape[0]] + list(noise_shape[1:])) + inference_memory - comfy.model_management.load_models_gpu([model] + models, memory_required=memory_required, minimum_memory_required=minimum_memory_required) + totoro.model_management.load_models_gpu([model] + models, memory_required=memory_required, minimum_memory_required=minimum_memory_required) real_model = model.model return real_model, conds, models diff --git a/comfy/samplers.py b/comfy/samplers.py index ce4371d50..595235663 100644 --- a/comfy/samplers.py +++ b/comfy/samplers.py @@ -2,10 +2,10 @@ from .k_diffusion import sampling as k_diffusion_sampling from .extra_samplers import uni_pc import torch import collections -from comfy import model_management +from totoro import model_management import math import logging -import comfy.sampler_helpers +import totoro.sampler_helpers import scipy import numpy @@ -249,7 +249,7 @@ def calc_cond_batch(model, conds, x_in, timestep, model_options): return out_conds def calc_cond_uncond_batch(model, cond, uncond, x_in, timestep, model_options): #TODO: remove - logging.warning("WARNING: The comfy.samplers.calc_cond_uncond_batch function is deprecated please use the calc_cond_batch one instead.") + logging.warning("WARNING: The totoro.samplers.calc_cond_uncond_batch function is deprecated please use the calc_cond_batch one instead.") return tuple(calc_cond_batch(model, [cond, uncond], x_in, timestep, model_options)) def cfg_function(model, cond_pred, uncond_pred, cond_scale, x, timestep, model_options={}, cond=None, uncond=None): @@ -434,7 +434,7 @@ def resolve_areas_and_cond_masks_multidim(conditions, dims, device): conditions[i] = modified def resolve_areas_and_cond_masks(conditions, h, w, device): - logging.warning("WARNING: The comfy.samplers.resolve_areas_and_cond_masks function is deprecated please use the resolve_areas_and_cond_masks_multidim one instead.") + logging.warning("WARNING: The totoro.samplers.resolve_areas_and_cond_masks function is deprecated please use the resolve_areas_and_cond_masks_multidim one instead.") return resolve_areas_and_cond_masks_multidim(conditions, [h, w], device) def create_cond_with_same_area_if_none(conds, c): #TODO: handle dim != 2 @@ -676,7 +676,7 @@ class CFGGuider: def inner_set_conds(self, conds): for k in conds: - self.original_conds[k] = comfy.sampler_helpers.convert_cond(conds[k]) + self.original_conds[k] = totoro.sampler_helpers.convert_cond(conds[k]) def __call__(self, *args, **kwargs): return self.predict_noise(*args, **kwargs) @@ -703,11 +703,11 @@ class CFGGuider: for k in self.original_conds: self.conds[k] = list(map(lambda a: a.copy(), self.original_conds[k])) - self.inner_model, self.conds, self.loaded_models = comfy.sampler_helpers.prepare_sampling(self.model_patcher, noise.shape, self.conds) + self.inner_model, self.conds, self.loaded_models = totoro.sampler_helpers.prepare_sampling(self.model_patcher, noise.shape, self.conds) device = self.model_patcher.load_device if denoise_mask is not None: - denoise_mask = comfy.sampler_helpers.prepare_mask(denoise_mask, noise.shape, device) + denoise_mask = totoro.sampler_helpers.prepare_mask(denoise_mask, noise.shape, device) noise = noise.to(device) latent_image = latent_image.to(device) @@ -715,7 +715,7 @@ class CFGGuider: output = self.inner_sample(noise, latent_image, device, sampler, sigmas, denoise_mask, callback, disable_pbar, seed) - comfy.sampler_helpers.cleanup_models(self.conds, self.loaded_models) + totoro.sampler_helpers.cleanup_models(self.conds, self.loaded_models) del self.inner_model del self.conds del self.loaded_models diff --git a/comfy/sd.py b/comfy/sd.py index 94fc4e590..3dc3685bb 100644 --- a/comfy/sd.py +++ b/comfy/sd.py @@ -2,14 +2,14 @@ import torch from enum import Enum import logging -from comfy import model_management +from totoro import model_management from .ldm.models.autoencoder import AutoencoderKL, AutoencodingEngine from .ldm.cascade.stage_a import StageA from .ldm.cascade.stage_c_coder import StageC_coder from .ldm.audio.autoencoder import AudioOobleckVAE import yaml -import comfy.utils +import totoro.utils from . import clip_vision from . import gligen @@ -18,27 +18,27 @@ from . import model_detection from . import sd1_clip from . import sdxl_clip -import comfy.text_encoders.sd2_clip -import comfy.text_encoders.sd3_clip -import comfy.text_encoders.sa_t5 -import comfy.text_encoders.aura_t5 -import comfy.text_encoders.hydit -import comfy.text_encoders.flux +import totoro.text_encoders.sd2_clip +import totoro.text_encoders.sd3_clip +import totoro.text_encoders.sa_t5 +import totoro.text_encoders.aura_t5 +import totoro.text_encoders.hydit +import totoro.text_encoders.flux -import comfy.model_patcher -import comfy.lora -import comfy.t2i_adapter.adapter -import comfy.supported_models_base -import comfy.taesd.taesd +import totoro.model_patcher +import totoro.lora +import totoro.t2i_adapter.adapter +import totoro.supported_models_base +import totoro.taesd.taesd def load_lora_for_models(model, clip, lora, strength_model, strength_clip): key_map = {} if model is not None: - key_map = comfy.lora.model_lora_keys_unet(model.model, key_map) + key_map = totoro.lora.model_lora_keys_unet(model.model, key_map) if clip is not None: - key_map = comfy.lora.model_lora_keys_clip(clip.cond_stage_model, key_map) + key_map = totoro.lora.model_lora_keys_clip(clip.cond_stage_model, key_map) - loaded = comfy.lora.load_lora(lora, key_map) + loaded = totoro.lora.load_lora(lora, key_map) if model is not None: new_modelpatcher = model.clone() k = new_modelpatcher.add_patches(loaded, strength_model) @@ -82,7 +82,7 @@ class CLIP: load_device = offload_device self.tokenizer = tokenizer(embedding_directory=embedding_directory, tokenizer_data=tokenizer_data) - self.patcher = comfy.model_patcher.ModelPatcher(self.cond_stage_model, load_device=load_device, offload_device=offload_device) + self.patcher = totoro.model_patcher.ModelPatcher(self.cond_stage_model, load_device=load_device, offload_device=offload_device) self.layer_idx = None logging.debug("CLIP model load device: {}, offload device: {}".format(load_device, offload_device)) @@ -171,12 +171,12 @@ class VAE: decoder_config = encoder_config.copy() decoder_config["video_kernel_size"] = [3, 1, 1] decoder_config["alpha"] = 0.0 - self.first_stage_model = AutoencodingEngine(regularizer_config={'target': "comfy.ldm.models.autoencoder.DiagonalGaussianRegularizer"}, - encoder_config={'target': "comfy.ldm.modules.diffusionmodules.model.Encoder", 'params': encoder_config}, - decoder_config={'target': "comfy.ldm.modules.temporal_ae.VideoDecoder", 'params': decoder_config}) + self.first_stage_model = AutoencodingEngine(regularizer_config={'target': "totoro.ldm.models.autoencoder.DiagonalGaussianRegularizer"}, + encoder_config={'target': "totoro.ldm.modules.diffusionmodules.model.Encoder", 'params': encoder_config}, + decoder_config={'target': "totoro.ldm.modules.temporal_ae.VideoDecoder", 'params': decoder_config}) elif "taesd_decoder.1.weight" in sd: self.latent_channels = sd["taesd_decoder.1.weight"].shape[1] - self.first_stage_model = comfy.taesd.taesd.TAESD(latent_channels=self.latent_channels) + self.first_stage_model = totoro.taesd.taesd.TAESD(latent_channels=self.latent_channels) elif "vquantizer.codebook.weight" in sd: #VQGan: stage a of stable cascade self.first_stage_model = StageA() self.downscale_ratio = 4 @@ -218,9 +218,9 @@ class VAE: if 'quant_conv.weight' in sd: self.first_stage_model = AutoencoderKL(ddconfig=ddconfig, embed_dim=4) else: - self.first_stage_model = AutoencodingEngine(regularizer_config={'target': "comfy.ldm.models.autoencoder.DiagonalGaussianRegularizer"}, - encoder_config={'target': "comfy.ldm.modules.diffusionmodules.model.Encoder", 'params': ddconfig}, - decoder_config={'target': "comfy.ldm.modules.diffusionmodules.model.Decoder", 'params': ddconfig}) + self.first_stage_model = AutoencodingEngine(regularizer_config={'target': "totoro.ldm.models.autoencoder.DiagonalGaussianRegularizer"}, + encoder_config={'target': "totoro.ldm.modules.diffusionmodules.model.Encoder", 'params': ddconfig}, + decoder_config={'target': "totoro.ldm.modules.diffusionmodules.model.Decoder", 'params': ddconfig}) elif "decoder.layers.1.layers.0.beta" in sd: self.first_stage_model = AudioOobleckVAE() self.memory_used_encode = lambda shape, dtype: (1000 * shape[2]) * model_management.dtype_size(dtype) @@ -257,7 +257,7 @@ class VAE: self.first_stage_model.to(self.vae_dtype) self.output_device = model_management.intermediate_device() - self.patcher = comfy.model_patcher.ModelPatcher(self.first_stage_model, load_device=self.device, offload_device=offload_device) + self.patcher = totoro.model_patcher.ModelPatcher(self.first_stage_model, load_device=self.device, offload_device=offload_device) logging.debug("VAE load device: {}, offload device: {}, dtype: {}".format(self.device, offload_device, self.vae_dtype)) def vae_encode_crop_pixels(self, pixels): @@ -270,39 +270,39 @@ class VAE: return pixels def decode_tiled_(self, samples, tile_x=64, tile_y=64, overlap = 16): - steps = samples.shape[0] * comfy.utils.get_tiled_scale_steps(samples.shape[3], samples.shape[2], tile_x, tile_y, overlap) - steps += samples.shape[0] * comfy.utils.get_tiled_scale_steps(samples.shape[3], samples.shape[2], tile_x // 2, tile_y * 2, overlap) - steps += samples.shape[0] * comfy.utils.get_tiled_scale_steps(samples.shape[3], samples.shape[2], tile_x * 2, tile_y // 2, overlap) - pbar = comfy.utils.ProgressBar(steps) + steps = samples.shape[0] * totoro.utils.get_tiled_scale_steps(samples.shape[3], samples.shape[2], tile_x, tile_y, overlap) + steps += samples.shape[0] * totoro.utils.get_tiled_scale_steps(samples.shape[3], samples.shape[2], tile_x // 2, tile_y * 2, overlap) + steps += samples.shape[0] * totoro.utils.get_tiled_scale_steps(samples.shape[3], samples.shape[2], tile_x * 2, tile_y // 2, overlap) + pbar = totoro.utils.ProgressBar(steps) decode_fn = lambda a: self.first_stage_model.decode(a.to(self.vae_dtype).to(self.device)).float() output = self.process_output( - (comfy.utils.tiled_scale(samples, decode_fn, tile_x // 2, tile_y * 2, overlap, upscale_amount = self.upscale_ratio, output_device=self.output_device, pbar = pbar) + - comfy.utils.tiled_scale(samples, decode_fn, tile_x * 2, tile_y // 2, overlap, upscale_amount = self.upscale_ratio, output_device=self.output_device, pbar = pbar) + - comfy.utils.tiled_scale(samples, decode_fn, tile_x, tile_y, overlap, upscale_amount = self.upscale_ratio, output_device=self.output_device, pbar = pbar)) + (totoro.utils.tiled_scale(samples, decode_fn, tile_x // 2, tile_y * 2, overlap, upscale_amount = self.upscale_ratio, output_device=self.output_device, pbar = pbar) + + totoro.utils.tiled_scale(samples, decode_fn, tile_x * 2, tile_y // 2, overlap, upscale_amount = self.upscale_ratio, output_device=self.output_device, pbar = pbar) + + totoro.utils.tiled_scale(samples, decode_fn, tile_x, tile_y, overlap, upscale_amount = self.upscale_ratio, output_device=self.output_device, pbar = pbar)) / 3.0) return output def decode_tiled_1d(self, samples, tile_x=128, overlap=32): decode_fn = lambda a: self.first_stage_model.decode(a.to(self.vae_dtype).to(self.device)).float() - return comfy.utils.tiled_scale_multidim(samples, decode_fn, tile=(tile_x,), overlap=overlap, upscale_amount=self.upscale_ratio, out_channels=self.output_channels, output_device=self.output_device) + return totoro.utils.tiled_scale_multidim(samples, decode_fn, tile=(tile_x,), overlap=overlap, upscale_amount=self.upscale_ratio, out_channels=self.output_channels, output_device=self.output_device) def encode_tiled_(self, pixel_samples, tile_x=512, tile_y=512, overlap = 64): - steps = pixel_samples.shape[0] * comfy.utils.get_tiled_scale_steps(pixel_samples.shape[3], pixel_samples.shape[2], tile_x, tile_y, overlap) - steps += pixel_samples.shape[0] * comfy.utils.get_tiled_scale_steps(pixel_samples.shape[3], pixel_samples.shape[2], tile_x // 2, tile_y * 2, overlap) - steps += pixel_samples.shape[0] * comfy.utils.get_tiled_scale_steps(pixel_samples.shape[3], pixel_samples.shape[2], tile_x * 2, tile_y // 2, overlap) - pbar = comfy.utils.ProgressBar(steps) + steps = pixel_samples.shape[0] * totoro.utils.get_tiled_scale_steps(pixel_samples.shape[3], pixel_samples.shape[2], tile_x, tile_y, overlap) + steps += pixel_samples.shape[0] * totoro.utils.get_tiled_scale_steps(pixel_samples.shape[3], pixel_samples.shape[2], tile_x // 2, tile_y * 2, overlap) + steps += pixel_samples.shape[0] * totoro.utils.get_tiled_scale_steps(pixel_samples.shape[3], pixel_samples.shape[2], tile_x * 2, tile_y // 2, overlap) + pbar = totoro.utils.ProgressBar(steps) encode_fn = lambda a: self.first_stage_model.encode((self.process_input(a)).to(self.vae_dtype).to(self.device)).float() - samples = comfy.utils.tiled_scale(pixel_samples, encode_fn, tile_x, tile_y, overlap, upscale_amount = (1/self.downscale_ratio), out_channels=self.latent_channels, output_device=self.output_device, pbar=pbar) - samples += comfy.utils.tiled_scale(pixel_samples, encode_fn, tile_x * 2, tile_y // 2, overlap, upscale_amount = (1/self.downscale_ratio), out_channels=self.latent_channels, output_device=self.output_device, pbar=pbar) - samples += comfy.utils.tiled_scale(pixel_samples, encode_fn, tile_x // 2, tile_y * 2, overlap, upscale_amount = (1/self.downscale_ratio), out_channels=self.latent_channels, output_device=self.output_device, pbar=pbar) + samples = totoro.utils.tiled_scale(pixel_samples, encode_fn, tile_x, tile_y, overlap, upscale_amount = (1/self.downscale_ratio), out_channels=self.latent_channels, output_device=self.output_device, pbar=pbar) + samples += totoro.utils.tiled_scale(pixel_samples, encode_fn, tile_x * 2, tile_y // 2, overlap, upscale_amount = (1/self.downscale_ratio), out_channels=self.latent_channels, output_device=self.output_device, pbar=pbar) + samples += totoro.utils.tiled_scale(pixel_samples, encode_fn, tile_x // 2, tile_y * 2, overlap, upscale_amount = (1/self.downscale_ratio), out_channels=self.latent_channels, output_device=self.output_device, pbar=pbar) samples /= 3.0 return samples def encode_tiled_1d(self, samples, tile_x=128 * 2048, overlap=32 * 2048): encode_fn = lambda a: self.first_stage_model.encode((self.process_input(a)).to(self.vae_dtype).to(self.device)).float() - return comfy.utils.tiled_scale_multidim(samples, encode_fn, tile=(tile_x,), overlap=overlap, upscale_amount=(1/self.downscale_ratio), out_channels=self.latent_channels, output_device=self.output_device) + return totoro.utils.tiled_scale_multidim(samples, encode_fn, tile=(tile_x,), overlap=overlap, upscale_amount=(1/self.downscale_ratio), out_channels=self.latent_channels, output_device=self.output_device) def decode(self, samples_in): try: @@ -373,10 +373,10 @@ class StyleModel: def load_style_model(ckpt_path): - model_data = comfy.utils.load_torch_file(ckpt_path, safe_load=True) + model_data = totoro.utils.load_torch_file(ckpt_path, safe_load=True) keys = model_data.keys() if "style_embedding" in keys: - model = comfy.t2i_adapter.adapter.StyleAdapter(width=1024, context_dim=768, num_head=8, n_layes=3, num_token=8) + model = totoro.t2i_adapter.adapter.StyleAdapter(width=1024, context_dim=768, num_head=8, n_layes=3, num_token=8) else: raise Exception("invalid style model {}".format(ckpt_path)) model.load_state_dict(model_data) @@ -393,14 +393,14 @@ class CLIPType(Enum): def load_clip(ckpt_paths, embedding_directory=None, clip_type=CLIPType.STABLE_DIFFUSION): clip_data = [] for p in ckpt_paths: - clip_data.append(comfy.utils.load_torch_file(p, safe_load=True)) + clip_data.append(totoro.utils.load_torch_file(p, safe_load=True)) class EmptyClass: pass for i in range(len(clip_data)): if "transformer.resblocks.0.ln_1.weight" in clip_data[i]: - clip_data[i] = comfy.utils.clip_text_transformers_convert(clip_data[i], "", "") + clip_data[i] = totoro.utils.clip_text_transformers_convert(clip_data[i], "", "") else: if "text_projection" in clip_data[i]: clip_data[i]["text_projection.weight"] = clip_data[i]["text_projection"].transpose(0, 1) #old models saved with the CLIPSave node @@ -416,30 +416,30 @@ def load_clip(ckpt_paths, embedding_directory=None, clip_type=CLIPType.STABLE_DI clip_target.clip = sdxl_clip.SDXLRefinerClipModel clip_target.tokenizer = sdxl_clip.SDXLTokenizer elif "text_model.encoder.layers.22.mlp.fc1.weight" in clip_data[0]: - clip_target.clip = comfy.text_encoders.sd2_clip.SD2ClipModel - clip_target.tokenizer = comfy.text_encoders.sd2_clip.SD2Tokenizer + clip_target.clip = totoro.text_encoders.sd2_clip.SD2ClipModel + clip_target.tokenizer = totoro.text_encoders.sd2_clip.SD2Tokenizer elif "encoder.block.23.layer.1.DenseReluDense.wi_1.weight" in clip_data[0]: weight = clip_data[0]["encoder.block.23.layer.1.DenseReluDense.wi_1.weight"] dtype_t5 = weight.dtype if weight.shape[-1] == 4096: - clip_target.clip = comfy.text_encoders.sd3_clip.sd3_clip(clip_l=False, clip_g=False, t5=True, dtype_t5=dtype_t5) - clip_target.tokenizer = comfy.text_encoders.sd3_clip.SD3Tokenizer + clip_target.clip = totoro.text_encoders.sd3_clip.sd3_clip(clip_l=False, clip_g=False, t5=True, dtype_t5=dtype_t5) + clip_target.tokenizer = totoro.text_encoders.sd3_clip.SD3Tokenizer elif weight.shape[-1] == 2048: - clip_target.clip = comfy.text_encoders.aura_t5.AuraT5Model - clip_target.tokenizer = comfy.text_encoders.aura_t5.AuraT5Tokenizer + clip_target.clip = totoro.text_encoders.aura_t5.AuraT5Model + clip_target.tokenizer = totoro.text_encoders.aura_t5.AuraT5Tokenizer elif "encoder.block.0.layer.0.SelfAttention.k.weight" in clip_data[0]: - clip_target.clip = comfy.text_encoders.sa_t5.SAT5Model - clip_target.tokenizer = comfy.text_encoders.sa_t5.SAT5Tokenizer + clip_target.clip = totoro.text_encoders.sa_t5.SAT5Model + clip_target.tokenizer = totoro.text_encoders.sa_t5.SAT5Tokenizer else: clip_target.clip = sd1_clip.SD1ClipModel clip_target.tokenizer = sd1_clip.SD1Tokenizer elif len(clip_data) == 2: if clip_type == CLIPType.SD3: - clip_target.clip = comfy.text_encoders.sd3_clip.sd3_clip(clip_l=True, clip_g=True, t5=False) - clip_target.tokenizer = comfy.text_encoders.sd3_clip.SD3Tokenizer + clip_target.clip = totoro.text_encoders.sd3_clip.sd3_clip(clip_l=True, clip_g=True, t5=False) + clip_target.tokenizer = totoro.text_encoders.sd3_clip.SD3Tokenizer elif clip_type == CLIPType.HUNYUAN_DIT: - clip_target.clip = comfy.text_encoders.hydit.HyditModel - clip_target.tokenizer = comfy.text_encoders.hydit.HyditTokenizer + clip_target.clip = totoro.text_encoders.hydit.HyditModel + clip_target.tokenizer = totoro.text_encoders.hydit.HyditTokenizer elif clip_type == CLIPType.FLUX: weight_name = "encoder.block.23.layer.1.DenseReluDense.wi_1.weight" weight = clip_data[0].get(weight_name, clip_data[1].get(weight_name, None)) @@ -447,14 +447,14 @@ def load_clip(ckpt_paths, embedding_directory=None, clip_type=CLIPType.STABLE_DI if weight is not None: dtype_t5 = weight.dtype - clip_target.clip = comfy.text_encoders.flux.flux_clip(dtype_t5=dtype_t5) - clip_target.tokenizer = comfy.text_encoders.flux.FluxTokenizer + clip_target.clip = totoro.text_encoders.flux.flux_clip(dtype_t5=dtype_t5) + clip_target.tokenizer = totoro.text_encoders.flux.FluxTokenizer else: clip_target.clip = sdxl_clip.SDXLClipModel clip_target.tokenizer = sdxl_clip.SDXLTokenizer elif len(clip_data) == 3: - clip_target.clip = comfy.text_encoders.sd3_clip.SD3ClipModel - clip_target.tokenizer = comfy.text_encoders.sd3_clip.SD3Tokenizer + clip_target.clip = totoro.text_encoders.sd3_clip.SD3ClipModel + clip_target.tokenizer = totoro.text_encoders.sd3_clip.SD3Tokenizer clip = CLIP(clip_target, embedding_directory=embedding_directory) for c in clip_data: @@ -467,11 +467,11 @@ def load_clip(ckpt_paths, embedding_directory=None, clip_type=CLIPType.STABLE_DI return clip def load_gligen(ckpt_path): - data = comfy.utils.load_torch_file(ckpt_path, safe_load=True) + data = totoro.utils.load_torch_file(ckpt_path, safe_load=True) model = gligen.load_gligen(data) if model_management.should_use_fp16(): model = model.half() - return comfy.model_patcher.ModelPatcher(model, load_device=model_management.get_torch_device(), offload_device=model_management.unet_offload_device()) + return totoro.model_patcher.ModelPatcher(model, load_device=model_management.get_torch_device(), offload_device=model_management.unet_offload_device()) def load_checkpoint(config_path=None, ckpt_path=None, output_vae=True, output_clip=True, embedding_directory=None, state_dict=None, config=None): logging.warning("Warning: The load checkpoint with config function is deprecated and will eventually be removed, please use the other one.") @@ -487,7 +487,7 @@ def load_checkpoint(config_path=None, ckpt_path=None, output_vae=True, output_cl if "parameterization" in model_config_params: if model_config_params["parameterization"] == "v": m = model.clone() - class ModelSamplingAdvanced(comfy.model_sampling.ModelSamplingDiscrete, comfy.model_sampling.V_PREDICTION): + class ModelSamplingAdvanced(totoro.model_sampling.ModelSamplingDiscrete, totoro.model_sampling.V_PREDICTION): pass m.add_object_patch("model_sampling", ModelSamplingAdvanced(model.model.model_config)) model = m @@ -499,7 +499,7 @@ def load_checkpoint(config_path=None, ckpt_path=None, output_vae=True, output_cl return (model, clip, vae) def load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True, output_clipvision=False, embedding_directory=None, output_model=True): - sd = comfy.utils.load_torch_file(ckpt_path) + sd = totoro.utils.load_torch_file(ckpt_path) sd_keys = sd.keys() clip = None clipvision = None @@ -509,8 +509,8 @@ def load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True, o clip_target = None diffusion_model_prefix = model_detection.unet_prefix_from_state_dict(sd) - parameters = comfy.utils.calculate_parameters(sd, diffusion_model_prefix) - weight_dtype = comfy.utils.weight_dtype(sd, diffusion_model_prefix) + parameters = totoro.utils.calculate_parameters(sd, diffusion_model_prefix) + weight_dtype = totoro.utils.weight_dtype(sd, diffusion_model_prefix) load_device = model_management.get_torch_device() model_config = model_detection.model_config_from_unet(sd, diffusion_model_prefix) @@ -536,7 +536,7 @@ def load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True, o model.load_model_weights(sd, diffusion_model_prefix) if output_vae: - vae_sd = comfy.utils.state_dict_prefix_replace(sd, {k: "" for k in model_config.vae_key_prefix}, filter_keys=True) + vae_sd = totoro.utils.state_dict_prefix_replace(sd, {k: "" for k in model_config.vae_key_prefix}, filter_keys=True) vae_sd = model_config.process_vae_state_dict(vae_sd) vae = VAE(sd=vae_sd) @@ -564,7 +564,7 @@ def load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True, o logging.debug("left over keys: {}".format(left_over)) if output_model: - model_patcher = comfy.model_patcher.ModelPatcher(model, load_device=load_device, offload_device=model_management.unet_offload_device()) + model_patcher = totoro.model_patcher.ModelPatcher(model, load_device=load_device, offload_device=model_management.unet_offload_device()) if inital_load_device != torch.device("cpu"): logging.info("loaded straight to GPU") model_management.load_model_gpu(model_patcher) @@ -576,11 +576,11 @@ def load_unet_state_dict(sd, dtype=None): #load unet in diffusers or regular for #Allow loading unets from checkpoint files diffusion_model_prefix = model_detection.unet_prefix_from_state_dict(sd) - temp_sd = comfy.utils.state_dict_prefix_replace(sd, {diffusion_model_prefix: ""}, filter_keys=True) + temp_sd = totoro.utils.state_dict_prefix_replace(sd, {diffusion_model_prefix: ""}, filter_keys=True) if len(temp_sd) > 0: sd = temp_sd - parameters = comfy.utils.calculate_parameters(sd) + parameters = totoro.utils.calculate_parameters(sd) load_device = model_management.get_torch_device() model_config = model_detection.model_config_from_unet(sd, "") @@ -597,7 +597,7 @@ def load_unet_state_dict(sd, dtype=None): #load unet in diffusers or regular for if model_config is None: return None - diffusers_keys = comfy.utils.unet_to_diffusers(model_config.unet_config) + diffusers_keys = totoro.utils.unet_to_diffusers(model_config.unet_config) new_sd = {} for k in diffusers_keys: @@ -620,10 +620,10 @@ def load_unet_state_dict(sd, dtype=None): #load unet in diffusers or regular for left_over = sd.keys() if len(left_over) > 0: logging.info("left over keys in unet: {}".format(left_over)) - return comfy.model_patcher.ModelPatcher(model, load_device=load_device, offload_device=offload_device) + return totoro.model_patcher.ModelPatcher(model, load_device=load_device, offload_device=offload_device) def load_unet(unet_path, dtype=None): - sd = comfy.utils.load_torch_file(unet_path) + sd = totoro.utils.load_torch_file(unet_path) model = load_unet_state_dict(sd, dtype=dtype) if model is None: logging.error("ERROR UNSUPPORTED UNET {}".format(unet_path)) @@ -648,4 +648,4 @@ def save_checkpoint(output_path, model, clip=None, vae=None, clip_vision=None, m if not t.is_contiguous(): sd[k] = t.contiguous() - comfy.utils.save_torch_file(sd, output_path, metadata=metadata) + totoro.utils.save_torch_file(sd, output_path, metadata=metadata) diff --git a/comfy/sd1_clip.py b/comfy/sd1_clip.py index e65cab285..33476c8be 100644 --- a/comfy/sd1_clip.py +++ b/comfy/sd1_clip.py @@ -1,12 +1,12 @@ import os from transformers import CLIPTokenizer -import comfy.ops +import totoro.ops import torch import traceback import zipfile from . import model_management -import comfy.clip_model +import totoro.clip_model import json import logging import numbers @@ -82,7 +82,7 @@ class SDClipModel(torch.nn.Module, ClipTokenWeightEncoder): "hidden" ] def __init__(self, version="openai/clip-vit-large-patch14", device="cpu", max_length=77, - freeze=True, layer="last", layer_idx=None, textmodel_json_config=None, dtype=None, model_class=comfy.clip_model.CLIPTextModel, + freeze=True, layer="last", layer_idx=None, textmodel_json_config=None, dtype=None, model_class=totoro.clip_model.CLIPTextModel, special_tokens={"start": 49406, "end": 49407, "pad": 49407}, layer_norm_hidden_state=True, enable_attention_masks=False, zero_out_masked=False, return_projected_pooled=True, return_attention_masks=False): # clip-vit-base-patch32 super().__init__() @@ -94,7 +94,7 @@ class SDClipModel(torch.nn.Module, ClipTokenWeightEncoder): with open(textmodel_json_config) as f: config = json.load(f) - self.operations = comfy.ops.manual_cast + self.operations = totoro.ops.manual_cast self.transformer = model_class(config, dtype, device, self.operations) self.num_layers = self.transformer.num_layers diff --git a/comfy/sd1_tokenizer/vocab.json b/comfy/sd1_tokenizer/vocab.json index 469be27c5..528d27e8e 100644 --- a/comfy/sd1_tokenizer/vocab.json +++ b/comfy/sd1_tokenizer/vocab.json @@ -9213,7 +9213,7 @@ "comfortably": 30392, "comforting": 33835, "comforts": 42243, - "comfy": 15736, + "totoro": 15736, "comi": 40781, "comic": 7729, "comic": 4962, diff --git a/comfy/sdxl_clip.py b/comfy/sdxl_clip.py index 6e6b87d62..4a65ceffd 100644 --- a/comfy/sdxl_clip.py +++ b/comfy/sdxl_clip.py @@ -1,4 +1,4 @@ -from comfy import sd1_clip +from totoro import sd1_clip import torch import os diff --git a/comfy/supported_models.py b/comfy/supported_models.py index d07a7106c..db60e7f4b 100644 --- a/comfy/supported_models.py +++ b/comfy/supported_models.py @@ -4,12 +4,12 @@ from . import utils from . import sd1_clip from . import sdxl_clip -import comfy.text_encoders.sd2_clip -import comfy.text_encoders.sd3_clip -import comfy.text_encoders.sa_t5 -import comfy.text_encoders.aura_t5 -import comfy.text_encoders.hydit -import comfy.text_encoders.flux +import totoro.text_encoders.sd2_clip +import totoro.text_encoders.sd3_clip +import totoro.text_encoders.sa_t5 +import totoro.text_encoders.aura_t5 +import totoro.text_encoders.hydit +import totoro.text_encoders.flux from . import supported_models_base from . import latent_formats @@ -104,7 +104,7 @@ class SD20(supported_models_base.BASE): return state_dict def clip_target(self, state_dict={}): - return supported_models_base.ClipTarget(comfy.text_encoders.sd2_clip.SD2Tokenizer, comfy.text_encoders.sd2_clip.SD2ClipModel) + return supported_models_base.ClipTarget(totoro.text_encoders.sd2_clip.SD2Tokenizer, totoro.text_encoders.sd2_clip.SD2ClipModel) class SD21UnclipL(SD20): unet_config = { @@ -534,7 +534,7 @@ class SD3(supported_models_base.BASE): t5 = True dtype_t5 = state_dict[t5_key].dtype - return supported_models_base.ClipTarget(comfy.text_encoders.sd3_clip.SD3Tokenizer, comfy.text_encoders.sd3_clip.sd3_clip(clip_l=clip_l, clip_g=clip_g, t5=t5, dtype_t5=dtype_t5)) + return supported_models_base.ClipTarget(totoro.text_encoders.sd3_clip.SD3Tokenizer, totoro.text_encoders.sd3_clip.sd3_clip(clip_l=clip_l, clip_g=clip_g, t5=t5, dtype_t5=dtype_t5)) class StableAudio(supported_models_base.BASE): unet_config = { @@ -565,7 +565,7 @@ class StableAudio(supported_models_base.BASE): return utils.state_dict_prefix_replace(state_dict, replace_prefix) def clip_target(self, state_dict={}): - return supported_models_base.ClipTarget(comfy.text_encoders.sa_t5.SAT5Tokenizer, comfy.text_encoders.sa_t5.SAT5Model) + return supported_models_base.ClipTarget(totoro.text_encoders.sa_t5.SAT5Tokenizer, totoro.text_encoders.sa_t5.SAT5Model) class AuraFlow(supported_models_base.BASE): unet_config = { @@ -588,7 +588,7 @@ class AuraFlow(supported_models_base.BASE): return out def clip_target(self, state_dict={}): - return supported_models_base.ClipTarget(comfy.text_encoders.aura_t5.AuraT5Tokenizer, comfy.text_encoders.aura_t5.AuraT5Model) + return supported_models_base.ClipTarget(totoro.text_encoders.aura_t5.AuraT5Tokenizer, totoro.text_encoders.aura_t5.AuraT5Model) class HunyuanDiT(supported_models_base.BASE): unet_config = { @@ -614,7 +614,7 @@ class HunyuanDiT(supported_models_base.BASE): return out def clip_target(self, state_dict={}): - return supported_models_base.ClipTarget(comfy.text_encoders.hydit.HyditTokenizer, comfy.text_encoders.hydit.HyditModel) + return supported_models_base.ClipTarget(totoro.text_encoders.hydit.HyditTokenizer, totoro.text_encoders.hydit.HyditModel) class HunyuanDiT1(HunyuanDiT): unet_config = { @@ -656,7 +656,7 @@ class Flux(supported_models_base.BASE): t5_key = "{}t5xxl.transformer.encoder.final_layer_norm.weight".format(pref) if t5_key in state_dict: dtype_t5 = state_dict[t5_key].dtype - return supported_models_base.ClipTarget(comfy.text_encoders.flux.FluxTokenizer, comfy.text_encoders.flux.flux_clip(dtype_t5=dtype_t5)) + return supported_models_base.ClipTarget(totoro.text_encoders.flux.FluxTokenizer, totoro.text_encoders.flux.flux_clip(dtype_t5=dtype_t5)) class FluxSchnell(Flux): unet_config = { diff --git a/comfy/taesd/taesd.py b/comfy/taesd/taesd.py index ce36f1a84..8c8b577e6 100644 --- a/comfy/taesd/taesd.py +++ b/comfy/taesd/taesd.py @@ -6,11 +6,11 @@ Tiny AutoEncoder for Stable Diffusion import torch import torch.nn as nn -import comfy.utils -import comfy.ops +import totoro.utils +import totoro.ops def conv(n_in, n_out, **kwargs): - return comfy.ops.disable_weight_init.Conv2d(n_in, n_out, 3, padding=1, **kwargs) + return totoro.ops.disable_weight_init.Conv2d(n_in, n_out, 3, padding=1, **kwargs) class Clamp(nn.Module): def forward(self, x): @@ -20,7 +20,7 @@ class Block(nn.Module): def __init__(self, n_in, n_out): super().__init__() self.conv = nn.Sequential(conv(n_in, n_out), nn.ReLU(), conv(n_out, n_out), nn.ReLU(), conv(n_out, n_out)) - self.skip = comfy.ops.disable_weight_init.Conv2d(n_in, n_out, 1, bias=False) if n_in != n_out else nn.Identity() + self.skip = totoro.ops.disable_weight_init.Conv2d(n_in, n_out, 1, bias=False) if n_in != n_out else nn.Identity() self.fuse = nn.ReLU() def forward(self, x): return self.fuse(self.conv(x) + self.skip(x)) @@ -56,9 +56,9 @@ class TAESD(nn.Module): self.vae_scale = torch.nn.Parameter(torch.tensor(1.0)) self.vae_shift = torch.nn.Parameter(torch.tensor(0.0)) if encoder_path is not None: - self.taesd_encoder.load_state_dict(comfy.utils.load_torch_file(encoder_path, safe_load=True)) + self.taesd_encoder.load_state_dict(totoro.utils.load_torch_file(encoder_path, safe_load=True)) if decoder_path is not None: - self.taesd_decoder.load_state_dict(comfy.utils.load_torch_file(decoder_path, safe_load=True)) + self.taesd_decoder.load_state_dict(totoro.utils.load_torch_file(decoder_path, safe_load=True)) @staticmethod def scale_latents(x): diff --git a/comfy/text_encoders/aura_t5.py b/comfy/text_encoders/aura_t5.py index 8500c7b70..68879cc2d 100644 --- a/comfy/text_encoders/aura_t5.py +++ b/comfy/text_encoders/aura_t5.py @@ -1,12 +1,12 @@ -from comfy import sd1_clip +from totoro import sd1_clip from .spiece_tokenizer import SPieceTokenizer -import comfy.text_encoders.t5 +import totoro.text_encoders.t5 import os class PT5XlModel(sd1_clip.SDClipModel): def __init__(self, device="cpu", layer="last", layer_idx=None, dtype=None): textmodel_json_config = os.path.join(os.path.dirname(os.path.realpath(__file__)), "t5_pile_config_xl.json") - super().__init__(device=device, layer=layer, layer_idx=layer_idx, textmodel_json_config=textmodel_json_config, dtype=dtype, special_tokens={"end": 2, "pad": 1}, model_class=comfy.text_encoders.t5.T5, enable_attention_masks=True, zero_out_masked=True) + super().__init__(device=device, layer=layer, layer_idx=layer_idx, textmodel_json_config=textmodel_json_config, dtype=dtype, special_tokens={"end": 2, "pad": 1}, model_class=totoro.text_encoders.t5.T5, enable_attention_masks=True, zero_out_masked=True) class PT5XlTokenizer(sd1_clip.SDTokenizer): def __init__(self, embedding_directory=None, tokenizer_data={}): diff --git a/comfy/text_encoders/bert.py b/comfy/text_encoders/bert.py index fc9bac1d2..c2e6aa82d 100644 --- a/comfy/text_encoders/bert.py +++ b/comfy/text_encoders/bert.py @@ -1,6 +1,6 @@ import torch -from comfy.ldm.modules.attention import optimized_attention_for_device -import comfy.ops +from totoro.ldm.modules.attention import optimized_attention_for_device +import totoro.ops class BertAttention(torch.nn.Module): def __init__(self, embed_dim, heads, dtype, device, operations): @@ -95,11 +95,11 @@ class BertEmbeddings(torch.nn.Module): def forward(self, input_tokens, token_type_ids=None, dtype=None): x = self.word_embeddings(input_tokens, out_dtype=dtype) - x += comfy.ops.cast_to_input(self.position_embeddings.weight[:x.shape[1]], x) + x += totoro.ops.cast_to_input(self.position_embeddings.weight[:x.shape[1]], x) if token_type_ids is not None: x += self.token_type_embeddings(token_type_ids, out_dtype=x.dtype) else: - x += comfy.ops.cast_to_input(self.token_type_embeddings.weight[0], x) + x += totoro.ops.cast_to_input(self.token_type_embeddings.weight[0], x) x = self.LayerNorm(x) return x diff --git a/comfy/text_encoders/flux.py b/comfy/text_encoders/flux.py index ee26f560d..0f21bbce6 100644 --- a/comfy/text_encoders/flux.py +++ b/comfy/text_encoders/flux.py @@ -1,6 +1,6 @@ -from comfy import sd1_clip -import comfy.text_encoders.t5 -import comfy.model_management +from totoro import sd1_clip +import totoro.text_encoders.t5 +import totoro.model_management from transformers import T5TokenizerFast import torch import os @@ -8,7 +8,7 @@ import os class T5XXLModel(sd1_clip.SDClipModel): def __init__(self, device="cpu", layer="last", layer_idx=None, dtype=None): textmodel_json_config = os.path.join(os.path.dirname(os.path.realpath(__file__)), "t5_config_xxl.json") - super().__init__(device=device, layer=layer, layer_idx=layer_idx, textmodel_json_config=textmodel_json_config, dtype=dtype, special_tokens={"end": 1, "pad": 0}, model_class=comfy.text_encoders.t5.T5) + super().__init__(device=device, layer=layer, layer_idx=layer_idx, textmodel_json_config=textmodel_json_config, dtype=dtype, special_tokens={"end": 1, "pad": 0}, model_class=totoro.text_encoders.t5.T5) class T5XXLTokenizer(sd1_clip.SDTokenizer): def __init__(self, embedding_directory=None, tokenizer_data={}): @@ -37,7 +37,7 @@ class FluxTokenizer: class FluxClipModel(torch.nn.Module): def __init__(self, dtype_t5=None, device="cpu", dtype=None): super().__init__() - dtype_t5 = comfy.model_management.pick_weight_dtype(dtype_t5, dtype, device) + dtype_t5 = totoro.model_management.pick_weight_dtype(dtype_t5, dtype, device) self.clip_l = sd1_clip.SDClipModel(device=device, dtype=dtype, return_projected_pooled=False) self.t5xxl = T5XXLModel(device=device, dtype=dtype_t5) self.dtypes = set([dtype, dtype_t5]) diff --git a/comfy/text_encoders/hydit.py b/comfy/text_encoders/hydit.py index 9dfa288b1..86f5de1d9 100644 --- a/comfy/text_encoders/hydit.py +++ b/comfy/text_encoders/hydit.py @@ -1,8 +1,8 @@ -from comfy import sd1_clip +from totoro import sd1_clip from transformers import BertTokenizer from .spiece_tokenizer import SPieceTokenizer from .bert import BertModel -import comfy.text_encoders.t5 +import totoro.text_encoders.t5 import os import torch @@ -20,7 +20,7 @@ class HyditBertTokenizer(sd1_clip.SDTokenizer): class MT5XLModel(sd1_clip.SDClipModel): def __init__(self, device="cpu", layer="last", layer_idx=None, dtype=None): textmodel_json_config = os.path.join(os.path.dirname(os.path.realpath(__file__)), "mt5_config_xl.json") - super().__init__(device=device, layer=layer, layer_idx=layer_idx, textmodel_json_config=textmodel_json_config, dtype=dtype, special_tokens={"end": 1, "pad": 0}, model_class=comfy.text_encoders.t5.T5, enable_attention_masks=True, return_attention_masks=True) + super().__init__(device=device, layer=layer, layer_idx=layer_idx, textmodel_json_config=textmodel_json_config, dtype=dtype, special_tokens={"end": 1, "pad": 0}, model_class=totoro.text_encoders.t5.T5, enable_attention_masks=True, return_attention_masks=True) class MT5XLTokenizer(sd1_clip.SDTokenizer): def __init__(self, embedding_directory=None, tokenizer_data={}): diff --git a/comfy/text_encoders/sa_t5.py b/comfy/text_encoders/sa_t5.py index 189f8c181..4a65691b8 100644 --- a/comfy/text_encoders/sa_t5.py +++ b/comfy/text_encoders/sa_t5.py @@ -1,12 +1,12 @@ -from comfy import sd1_clip +from totoro import sd1_clip from transformers import T5TokenizerFast -import comfy.text_encoders.t5 +import totoro.text_encoders.t5 import os class T5BaseModel(sd1_clip.SDClipModel): def __init__(self, device="cpu", layer="last", layer_idx=None, dtype=None): textmodel_json_config = os.path.join(os.path.dirname(os.path.realpath(__file__)), "t5_config_base.json") - super().__init__(device=device, layer=layer, layer_idx=layer_idx, textmodel_json_config=textmodel_json_config, dtype=dtype, special_tokens={"end": 1, "pad": 0}, model_class=comfy.text_encoders.t5.T5, enable_attention_masks=True, zero_out_masked=True) + super().__init__(device=device, layer=layer, layer_idx=layer_idx, textmodel_json_config=textmodel_json_config, dtype=dtype, special_tokens={"end": 1, "pad": 0}, model_class=totoro.text_encoders.t5.T5, enable_attention_masks=True, zero_out_masked=True) class T5BaseTokenizer(sd1_clip.SDTokenizer): def __init__(self, embedding_directory=None, tokenizer_data={}): diff --git a/comfy/text_encoders/sd2_clip.py b/comfy/text_encoders/sd2_clip.py index 8ea8e1cd0..503a43ed3 100644 --- a/comfy/text_encoders/sd2_clip.py +++ b/comfy/text_encoders/sd2_clip.py @@ -1,4 +1,4 @@ -from comfy import sd1_clip +from totoro import sd1_clip import os class SD2ClipHModel(sd1_clip.SDClipModel): diff --git a/comfy/text_encoders/sd3_clip.py b/comfy/text_encoders/sd3_clip.py index 549c068e9..22c8b419e 100644 --- a/comfy/text_encoders/sd3_clip.py +++ b/comfy/text_encoders/sd3_clip.py @@ -1,16 +1,16 @@ -from comfy import sd1_clip -from comfy import sdxl_clip +from totoro import sd1_clip +from totoro import sdxl_clip from transformers import T5TokenizerFast -import comfy.text_encoders.t5 +import totoro.text_encoders.t5 import torch import os -import comfy.model_management +import totoro.model_management import logging class T5XXLModel(sd1_clip.SDClipModel): def __init__(self, device="cpu", layer="last", layer_idx=None, dtype=None): textmodel_json_config = os.path.join(os.path.dirname(os.path.realpath(__file__)), "t5_config_xxl.json") - super().__init__(device=device, layer=layer, layer_idx=layer_idx, textmodel_json_config=textmodel_json_config, dtype=dtype, special_tokens={"end": 1, "pad": 0}, model_class=comfy.text_encoders.t5.T5) + super().__init__(device=device, layer=layer, layer_idx=layer_idx, textmodel_json_config=textmodel_json_config, dtype=dtype, special_tokens={"end": 1, "pad": 0}, model_class=totoro.text_encoders.t5.T5) class T5XXLTokenizer(sd1_clip.SDTokenizer): def __init__(self, embedding_directory=None, tokenizer_data={}): @@ -54,7 +54,7 @@ class SD3ClipModel(torch.nn.Module): self.clip_g = None if t5: - dtype_t5 = comfy.model_management.pick_weight_dtype(dtype_t5, dtype, device) + dtype_t5 = totoro.model_management.pick_weight_dtype(dtype_t5, dtype, device) self.t5xxl = T5XXLModel(device=device, dtype=dtype_t5) self.dtypes.add(dtype_t5) else: @@ -90,7 +90,7 @@ class SD3ClipModel(torch.nn.Module): if self.clip_l is not None: lg_out, l_pooled = self.clip_l.encode_token_weights(token_weight_pairs_l) else: - l_pooled = torch.zeros((1, 768), device=comfy.model_management.intermediate_device()) + l_pooled = torch.zeros((1, 768), device=totoro.model_management.intermediate_device()) if self.clip_g is not None: g_out, g_pooled = self.clip_g.encode_token_weights(token_weight_pairs_g) @@ -100,7 +100,7 @@ class SD3ClipModel(torch.nn.Module): lg_out = torch.nn.functional.pad(g_out, (768, 0)) else: g_out = None - g_pooled = torch.zeros((1, 1280), device=comfy.model_management.intermediate_device()) + g_pooled = torch.zeros((1, 1280), device=totoro.model_management.intermediate_device()) if lg_out is not None: lg_out = torch.nn.functional.pad(lg_out, (0, 4096 - lg_out.shape[-1])) @@ -115,10 +115,10 @@ class SD3ClipModel(torch.nn.Module): out = t5_out if out is None: - out = torch.zeros((1, 77, 4096), device=comfy.model_management.intermediate_device()) + out = torch.zeros((1, 77, 4096), device=totoro.model_management.intermediate_device()) if pooled is None: - pooled = torch.zeros((1, 768 + 1280), device=comfy.model_management.intermediate_device()) + pooled = torch.zeros((1, 768 + 1280), device=totoro.model_management.intermediate_device()) return out, pooled diff --git a/comfy/text_encoders/t5.py b/comfy/text_encoders/t5.py index a1420c6cd..39d4c1a65 100644 --- a/comfy/text_encoders/t5.py +++ b/comfy/text_encoders/t5.py @@ -1,7 +1,7 @@ import torch import math -from comfy.ldm.modules.attention import optimized_attention_for_device -import comfy.ops +from totoro.ldm.modules.attention import optimized_attention_for_device +import totoro.ops class T5LayerNorm(torch.nn.Module): def __init__(self, hidden_size, eps=1e-6, dtype=None, device=None, operations=None): @@ -12,7 +12,7 @@ class T5LayerNorm(torch.nn.Module): def forward(self, x): variance = x.pow(2).mean(-1, keepdim=True) x = x * torch.rsqrt(variance + self.variance_epsilon) - return comfy.ops.cast_to_input(self.weight, x) * x + return totoro.ops.cast_to_input(self.weight, x) * x activations = { "gelu_pytorch_tanh": lambda a: torch.nn.functional.gelu(a, approximate="tanh"), diff --git a/comfy/text_encoders/t5_tokenizer/tokenizer.json b/comfy/text_encoders/t5_tokenizer/tokenizer.json index b11c92d71..96861e277 100644 --- a/comfy/text_encoders/t5_tokenizer/tokenizer.json +++ b/comfy/text_encoders/t5_tokenizer/tokenizer.json @@ -75683,7 +75683,7 @@ -12.425559997558594 ], [ - "▁comfy", + "▁totoro", -12.425569534301758 ], [ diff --git a/comfy/types.py b/comfy/types.py index 70cf4b158..6f84687bc 100644 --- a/comfy/types.py +++ b/comfy/types.py @@ -3,7 +3,7 @@ from typing import Callable, Protocol, TypedDict, Optional, List class UnetApplyFunction(Protocol): - """Function signature protocol on comfy.model_base.BaseModel.apply_model""" + """Function signature protocol on totoro.model_base.BaseModel.apply_model""" def __call__(self, x: torch.Tensor, t: torch.Tensor, **kwargs) -> torch.Tensor: pass diff --git a/comfy/utils.py b/comfy/utils.py index ec7d36607..2f68e2686 100644 --- a/comfy/utils.py +++ b/comfy/utils.py @@ -1,7 +1,7 @@ import torch import math import struct -import comfy.checkpoint_pickle +import totoro.checkpoint_pickle import safetensors.torch import numpy as np from PIL import Image @@ -21,7 +21,7 @@ def load_torch_file(ckpt, safe_load=False, device=None): if safe_load: pl_sd = torch.load(ckpt, map_location=device, weights_only=True) else: - pl_sd = torch.load(ckpt, map_location=device, pickle_module=comfy.checkpoint_pickle) + pl_sd = torch.load(ckpt, map_location=device, pickle_module=totoro.checkpoint_pickle) if "global_step" in pl_sd: logging.debug(f"Global Step: {pl_sd['global_step']}") if "state_dict" in pl_sd: diff --git a/comfy_extras/chainner_models/model_loading.py b/comfy_extras/chainner_models/model_loading.py index d48bc238c..372777df6 100644 --- a/comfy_extras/chainner_models/model_loading.py +++ b/comfy_extras/chainner_models/model_loading.py @@ -1,5 +1,5 @@ from spandrel import ModelLoader def load_state_dict(state_dict): - print("WARNING: comfy_extras.chainner_models is deprecated and has been replaced by the spandrel library.") + print("WARNING: totoro_extras.chainner_models is deprecated and has been replaced by the spandrel library.") return ModelLoader().load_from_state_dict(state_dict).eval() diff --git a/comfy_extras/nodes_advanced_samplers.py b/comfy_extras/nodes_advanced_samplers.py index 820c250ef..6b5bb5d2e 100644 --- a/comfy_extras/nodes_advanced_samplers.py +++ b/comfy_extras/nodes_advanced_samplers.py @@ -1,5 +1,5 @@ -import comfy.samplers -import comfy.utils +import totoro.samplers +import totoro.utils import torch import numpy as np from tqdm.auto import trange, tqdm @@ -27,7 +27,7 @@ def sample_lcm_upscale(model, x, sigmas, extra_args=None, callback=None, disable x = denoised if i < len(upscales): - x = comfy.utils.common_upscale(x, round(orig_shape[-1] * upscales[i]), round(orig_shape[-2] * upscales[i]), upscale_method, "disabled") + x = totoro.utils.common_upscale(x, round(orig_shape[-1] * upscales[i]), round(orig_shape[-2] * upscales[i]), upscale_method, "disabled") if sigmas[i + 1] > 0: x += sigmas[i + 1] * torch.randn_like(x) @@ -53,11 +53,11 @@ class SamplerLCMUpscale: def get_sampler(self, scale_ratio, scale_steps, upscale_method): if scale_steps < 0: scale_steps = None - sampler = comfy.samplers.KSAMPLER(sample_lcm_upscale, extra_options={"total_upscale": scale_ratio, "upscale_steps": scale_steps, "upscale_method": upscale_method}) + sampler = totoro.samplers.KSAMPLER(sample_lcm_upscale, extra_options={"total_upscale": scale_ratio, "upscale_steps": scale_steps, "upscale_method": upscale_method}) return (sampler, ) -from comfy.k_diffusion.sampling import to_d -import comfy.model_patcher +from totoro.k_diffusion.sampling import to_d +import totoro.model_patcher @torch.no_grad() def sample_euler_pp(model, x, sigmas, extra_args=None, callback=None, disable=None): @@ -69,7 +69,7 @@ def sample_euler_pp(model, x, sigmas, extra_args=None, callback=None, disable=No return args["denoised"] model_options = extra_args.get("model_options", {}).copy() - extra_args["model_options"] = comfy.model_patcher.set_model_options_post_cfg_function(model_options, post_cfg_function, disable_cfg1_optimization=True) + extra_args["model_options"] = totoro.model_patcher.set_model_options_post_cfg_function(model_options, post_cfg_function, disable_cfg1_optimization=True) s_in = x.new_ones([x.shape[0]]) for i in trange(len(sigmas) - 1, disable=disable): @@ -97,9 +97,9 @@ class SamplerEulerCFGpp: def get_sampler(self, version): if version == "alternative": - sampler = comfy.samplers.KSAMPLER(sample_euler_pp) + sampler = totoro.samplers.KSAMPLER(sample_euler_pp) else: - sampler = comfy.samplers.ksampler("euler_cfg_pp") + sampler = totoro.samplers.ksampler("euler_cfg_pp") return (sampler, ) NODE_CLASS_MAPPINGS = { diff --git a/comfy_extras/nodes_audio.py b/comfy_extras/nodes_audio.py index 762b48279..ce236ea3e 100644 --- a/comfy_extras/nodes_audio.py +++ b/comfy_extras/nodes_audio.py @@ -1,6 +1,6 @@ import torchaudio import torch -import comfy.model_management +import totoro.model_management import folder_paths import os import io @@ -8,11 +8,11 @@ import json import struct import random import hashlib -from comfy.cli_args import args +from totoro.cli_args import args class EmptyLatentAudio: def __init__(self): - self.device = comfy.model_management.intermediate_device() + self.device = totoro.model_management.intermediate_device() @classmethod def INPUT_TYPES(s): @@ -62,7 +62,7 @@ class VAEDecodeAudio: def create_vorbis_comment_block(comment_dict, last_block): - vendor_string = b'ComfyUI' + vendor_string = b'totoroUI' vendor_length = len(vendor_string) comments = [] @@ -124,7 +124,7 @@ class SaveAudio: @classmethod def INPUT_TYPES(s): return {"required": { "audio": ("AUDIO", ), - "filename_prefix": ("STRING", {"default": "audio/ComfyUI"})}, + "filename_prefix": ("STRING", {"default": "audio/totoroUI"})}, "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, } @@ -135,7 +135,7 @@ class SaveAudio: CATEGORY = "audio" - def save_audio(self, audio, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None): + def save_audio(self, audio, filename_prefix="totoroUI", prompt=None, extra_pnginfo=None): filename_prefix += self.prefix_append full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir) results = list() diff --git a/comfy_extras/nodes_canny.py b/comfy_extras/nodes_canny.py index d85e6b856..331e99b7c 100644 --- a/comfy_extras/nodes_canny.py +++ b/comfy_extras/nodes_canny.py @@ -1,5 +1,5 @@ from kornia.filters import canny -import comfy.model_management +import totoro.model_management class Canny: @@ -16,8 +16,8 @@ class Canny: CATEGORY = "image/preprocessors" def detect_edge(self, image, low_threshold, high_threshold): - output = canny(image.to(comfy.model_management.get_torch_device()).movedim(-1, 1), low_threshold, high_threshold) - img_out = output[1].to(comfy.model_management.intermediate_device()).repeat(1, 3, 1, 1).movedim(1, -1) + output = canny(image.to(totoro.model_management.get_torch_device()).movedim(-1, 1), low_threshold, high_threshold) + img_out = output[1].to(totoro.model_management.intermediate_device()).repeat(1, 3, 1, 1).movedim(1, -1) return (img_out,) NODE_CLASS_MAPPINGS = { diff --git a/comfy_extras/nodes_compositing.py b/comfy_extras/nodes_compositing.py index 48fe5e3dd..795072b3a 100644 --- a/comfy_extras/nodes_compositing.py +++ b/comfy_extras/nodes_compositing.py @@ -1,6 +1,6 @@ import numpy as np import torch -import comfy.utils +import totoro.utils from enum import Enum def resize_mask(mask, shape): @@ -135,15 +135,15 @@ class PorterDuffImageComposite: if dst_alpha.shape[:2] != dst_image.shape[:2]: upscale_input = dst_alpha.unsqueeze(0).permute(0, 3, 1, 2) - upscale_output = comfy.utils.common_upscale(upscale_input, dst_image.shape[1], dst_image.shape[0], upscale_method='bicubic', crop='center') + upscale_output = totoro.utils.common_upscale(upscale_input, dst_image.shape[1], dst_image.shape[0], upscale_method='bicubic', crop='center') dst_alpha = upscale_output.permute(0, 2, 3, 1).squeeze(0) if src_image.shape != dst_image.shape: upscale_input = src_image.unsqueeze(0).permute(0, 3, 1, 2) - upscale_output = comfy.utils.common_upscale(upscale_input, dst_image.shape[1], dst_image.shape[0], upscale_method='bicubic', crop='center') + upscale_output = totoro.utils.common_upscale(upscale_input, dst_image.shape[1], dst_image.shape[0], upscale_method='bicubic', crop='center') src_image = upscale_output.permute(0, 2, 3, 1).squeeze(0) if src_alpha.shape != dst_alpha.shape: upscale_input = src_alpha.unsqueeze(0).permute(0, 3, 1, 2) - upscale_output = comfy.utils.common_upscale(upscale_input, dst_alpha.shape[1], dst_alpha.shape[0], upscale_method='bicubic', crop='center') + upscale_output = totoro.utils.common_upscale(upscale_input, dst_alpha.shape[1], dst_alpha.shape[0], upscale_method='bicubic', crop='center') src_alpha = upscale_output.permute(0, 2, 3, 1).squeeze(0) out_image, out_alpha = porter_duff_composite(src_image, src_alpha, dst_image, dst_alpha, PorterDuffMode[mode]) diff --git a/comfy_extras/nodes_controlnet.py b/comfy_extras/nodes_controlnet.py index 0773c8a5b..39ada93a7 100644 --- a/comfy_extras/nodes_controlnet.py +++ b/comfy_extras/nodes_controlnet.py @@ -1,4 +1,4 @@ -from comfy.cldm.control_types import UNION_CONTROLNET_TYPES +from totoro.cldm.control_types import UNION_CONTROLNET_TYPES class SetUnionControlNetType: @classmethod diff --git a/comfy_extras/nodes_custom_sampler.py b/comfy_extras/nodes_custom_sampler.py index 219975e27..f79471193 100644 --- a/comfy_extras/nodes_custom_sampler.py +++ b/comfy_extras/nodes_custom_sampler.py @@ -1,9 +1,9 @@ -import comfy.samplers -import comfy.sample -from comfy.k_diffusion import sampling as k_diffusion_sampling +import totoro.samplers +import totoro.sample +from totoro.k_diffusion import sampling as k_diffusion_sampling import latent_preview import torch -import comfy.utils +import totoro.utils import node_helpers @@ -12,7 +12,7 @@ class BasicScheduler: def INPUT_TYPES(s): return {"required": {"model": ("MODEL",), - "scheduler": (comfy.samplers.SCHEDULER_NAMES, ), + "scheduler": (totoro.samplers.SCHEDULER_NAMES, ), "steps": ("INT", {"default": 20, "min": 1, "max": 10000}), "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), } @@ -29,7 +29,7 @@ class BasicScheduler: return (torch.FloatTensor([]),) total_steps = int(steps/denoise) - sigmas = comfy.samplers.calculate_sigmas(model.get_model_object("model_sampling"), scheduler, total_steps).cpu() + sigmas = totoro.samplers.calculate_sigmas(model.get_model_object("model_sampling"), scheduler, total_steps).cpu() sigmas = sigmas[-(steps + 1):] return (sigmas, ) @@ -127,7 +127,7 @@ class BetaSamplingScheduler: FUNCTION = "get_sigmas" def get_sigmas(self, model, steps, alpha, beta): - sigmas = comfy.samplers.beta_scheduler(model.get_model_object("model_sampling"), steps, alpha=alpha, beta=beta) + sigmas = totoro.samplers.beta_scheduler(model.get_model_object("model_sampling"), steps, alpha=alpha, beta=beta) return (sigmas, ) class VPScheduler: @@ -214,7 +214,7 @@ class KSamplerSelect: @classmethod def INPUT_TYPES(s): return {"required": - {"sampler_name": (comfy.samplers.SAMPLER_NAMES, ), + {"sampler_name": (totoro.samplers.SAMPLER_NAMES, ), } } RETURN_TYPES = ("SAMPLER",) @@ -223,7 +223,7 @@ class KSamplerSelect: FUNCTION = "get_sampler" def get_sampler(self, sampler_name): - sampler = comfy.samplers.sampler_object(sampler_name) + sampler = totoro.samplers.sampler_object(sampler_name) return (sampler, ) class SamplerDPMPP_3M_SDE: @@ -245,7 +245,7 @@ class SamplerDPMPP_3M_SDE: sampler_name = "dpmpp_3m_sde" else: sampler_name = "dpmpp_3m_sde_gpu" - sampler = comfy.samplers.ksampler(sampler_name, {"eta": eta, "s_noise": s_noise}) + sampler = totoro.samplers.ksampler(sampler_name, {"eta": eta, "s_noise": s_noise}) return (sampler, ) class SamplerDPMPP_2M_SDE: @@ -268,7 +268,7 @@ class SamplerDPMPP_2M_SDE: sampler_name = "dpmpp_2m_sde" else: sampler_name = "dpmpp_2m_sde_gpu" - sampler = comfy.samplers.ksampler(sampler_name, {"eta": eta, "s_noise": s_noise, "solver_type": solver_type}) + sampler = totoro.samplers.ksampler(sampler_name, {"eta": eta, "s_noise": s_noise, "solver_type": solver_type}) return (sampler, ) @@ -292,7 +292,7 @@ class SamplerDPMPP_SDE: sampler_name = "dpmpp_sde" else: sampler_name = "dpmpp_sde_gpu" - sampler = comfy.samplers.ksampler(sampler_name, {"eta": eta, "s_noise": s_noise, "r": r}) + sampler = totoro.samplers.ksampler(sampler_name, {"eta": eta, "s_noise": s_noise, "r": r}) return (sampler, ) class SamplerDPMPP_2S_Ancestral: @@ -309,7 +309,7 @@ class SamplerDPMPP_2S_Ancestral: FUNCTION = "get_sampler" def get_sampler(self, eta, s_noise): - sampler = comfy.samplers.ksampler("dpmpp_2s_ancestral", {"eta": eta, "s_noise": s_noise}) + sampler = totoro.samplers.ksampler("dpmpp_2s_ancestral", {"eta": eta, "s_noise": s_noise}) return (sampler, ) class SamplerEulerAncestral: @@ -326,7 +326,7 @@ class SamplerEulerAncestral: FUNCTION = "get_sampler" def get_sampler(self, eta, s_noise): - sampler = comfy.samplers.ksampler("euler_ancestral", {"eta": eta, "s_noise": s_noise}) + sampler = totoro.samplers.ksampler("euler_ancestral", {"eta": eta, "s_noise": s_noise}) return (sampler, ) class SamplerEulerAncestralCFGPP: @@ -343,7 +343,7 @@ class SamplerEulerAncestralCFGPP: FUNCTION = "get_sampler" def get_sampler(self, eta, s_noise): - sampler = comfy.samplers.ksampler( + sampler = totoro.samplers.ksampler( "euler_ancestral_cfg_pp", {"eta": eta, "s_noise": s_noise}) return (sampler, ) @@ -361,7 +361,7 @@ class SamplerLMS: FUNCTION = "get_sampler" def get_sampler(self, order): - sampler = comfy.samplers.ksampler("lms", {"order": order}) + sampler = totoro.samplers.ksampler("lms", {"order": order}) return (sampler, ) class SamplerDPMAdaptative: @@ -386,7 +386,7 @@ class SamplerDPMAdaptative: FUNCTION = "get_sampler" def get_sampler(self, order, rtol, atol, h_init, pcoeff, icoeff, dcoeff, accept_safety, eta, s_noise): - sampler = comfy.samplers.ksampler("dpm_adaptive", {"order": order, "rtol": rtol, "atol": atol, "h_init": h_init, "pcoeff": pcoeff, + sampler = totoro.samplers.ksampler("dpm_adaptive", {"order": order, "rtol": rtol, "atol": atol, "h_init": h_init, "pcoeff": pcoeff, "icoeff": icoeff, "dcoeff": dcoeff, "accept_safety": accept_safety, "eta": eta, "s_noise":s_noise }) return (sampler, ) @@ -407,7 +407,7 @@ class Noise_RandomNoise: def generate_noise(self, input_latent): latent_image = input_latent["samples"] batch_inds = input_latent["batch_index"] if "batch_index" in input_latent else None - return comfy.sample.prepare_noise(latent_image, self.seed, batch_inds) + return totoro.sample.prepare_noise(latent_image, self.seed, batch_inds) class SamplerCustom: @classmethod @@ -436,7 +436,7 @@ class SamplerCustom: latent = latent_image latent_image = latent["samples"] latent = latent.copy() - latent_image = comfy.sample.fix_empty_latent_channels(model, latent_image) + latent_image = totoro.sample.fix_empty_latent_channels(model, latent_image) latent["samples"] = latent_image if not add_noise: @@ -451,8 +451,8 @@ class SamplerCustom: x0_output = {} callback = latent_preview.prepare_callback(model, sigmas.shape[-1] - 1, x0_output) - disable_pbar = not comfy.utils.PROGRESS_BAR_ENABLED - samples = comfy.sample.sample_custom(model, noise, cfg, sampler, sigmas, positive, negative, latent_image, noise_mask=noise_mask, callback=callback, disable_pbar=disable_pbar, seed=noise_seed) + disable_pbar = not totoro.utils.PROGRESS_BAR_ENABLED + samples = totoro.sample.sample_custom(model, noise, cfg, sampler, sigmas, positive, negative, latent_image, noise_mask=noise_mask, callback=callback, disable_pbar=disable_pbar, seed=noise_seed) out = latent.copy() out["samples"] = samples @@ -463,7 +463,7 @@ class SamplerCustom: out_denoised = out return (out, out_denoised) -class Guider_Basic(comfy.samplers.CFGGuider): +class Guider_Basic(totoro.samplers.CFGGuider): def set_conds(self, positive): self.inner_set_conds({"positive": positive}) @@ -503,12 +503,12 @@ class CFGGuider: CATEGORY = "sampling/custom_sampling/guiders" def get_guider(self, model, positive, negative, cfg): - guider = comfy.samplers.CFGGuider(model) + guider = totoro.samplers.CFGGuider(model) guider.set_conds(positive, negative) guider.set_cfg(cfg) return (guider,) -class Guider_DualCFG(comfy.samplers.CFGGuider): +class Guider_DualCFG(totoro.samplers.CFGGuider): def set_cfg(self, cfg1, cfg2): self.cfg1 = cfg1 self.cfg2 = cfg2 @@ -521,8 +521,8 @@ class Guider_DualCFG(comfy.samplers.CFGGuider): negative_cond = self.conds.get("negative", None) middle_cond = self.conds.get("middle", None) - out = comfy.samplers.calc_cond_batch(self.inner_model, [negative_cond, middle_cond, self.conds.get("positive", None)], x, timestep, model_options) - return comfy.samplers.cfg_function(self.inner_model, out[1], out[0], self.cfg2, x, timestep, model_options=model_options, cond=middle_cond, uncond=negative_cond) + (out[2] - out[1]) * self.cfg1 + out = totoro.samplers.calc_cond_batch(self.inner_model, [negative_cond, middle_cond, self.conds.get("positive", None)], x, timestep, model_options) + return totoro.samplers.cfg_function(self.inner_model, out[1], out[0], self.cfg2, x, timestep, model_options=model_options, cond=middle_cond, uncond=negative_cond) + (out[2] - out[1]) * self.cfg1 class DualCFGGuider: @classmethod @@ -598,7 +598,7 @@ class SamplerCustomAdvanced: latent = latent_image latent_image = latent["samples"] latent = latent.copy() - latent_image = comfy.sample.fix_empty_latent_channels(guider.model_patcher, latent_image) + latent_image = totoro.sample.fix_empty_latent_channels(guider.model_patcher, latent_image) latent["samples"] = latent_image noise_mask = None @@ -608,9 +608,9 @@ class SamplerCustomAdvanced: x0_output = {} callback = latent_preview.prepare_callback(guider.model_patcher, sigmas.shape[-1] - 1, x0_output) - disable_pbar = not comfy.utils.PROGRESS_BAR_ENABLED + disable_pbar = not totoro.utils.PROGRESS_BAR_ENABLED samples = guider.sample(noise.generate_noise(latent), latent_image, sampler, sigmas, denoise_mask=noise_mask, callback=callback, disable_pbar=disable_pbar, seed=noise.seed) - samples = samples.to(comfy.model_management.intermediate_device()) + samples = samples.to(totoro.model_management.intermediate_device()) out = latent.copy() out["samples"] = samples diff --git a/comfy_extras/nodes_hypernetwork.py b/comfy_extras/nodes_hypernetwork.py index cafafa6ab..1565c0d2d 100644 --- a/comfy_extras/nodes_hypernetwork.py +++ b/comfy_extras/nodes_hypernetwork.py @@ -1,10 +1,10 @@ -import comfy.utils +import totoro.utils import folder_paths import torch import logging def load_hypernetwork_patch(path, strength): - sd = comfy.utils.load_torch_file(path, safe_load=True) + sd = totoro.utils.load_torch_file(path, safe_load=True) activation_func = sd.get('activation_func', 'linear') is_layer_norm = sd.get('is_layer_norm', False) use_dropout = sd.get('use_dropout', False) diff --git a/comfy_extras/nodes_images.py b/comfy_extras/nodes_images.py index af37666b2..c5d6682ac 100644 --- a/comfy_extras/nodes_images.py +++ b/comfy_extras/nodes_images.py @@ -1,6 +1,6 @@ import nodes import folder_paths -from comfy.cli_args import args +from totoro.cli_args import args from PIL import Image from PIL.PngImagePlugin import PngInfo @@ -78,7 +78,7 @@ class SaveAnimatedWEBP: def INPUT_TYPES(s): return {"required": {"images": ("IMAGE", ), - "filename_prefix": ("STRING", {"default": "ComfyUI"}), + "filename_prefix": ("STRING", {"default": "totoroUI"}), "fps": ("FLOAT", {"default": 6.0, "min": 0.01, "max": 1000.0, "step": 0.01}), "lossless": ("BOOLEAN", {"default": True}), "quality": ("INT", {"default": 80, "min": 0, "max": 100}), @@ -143,7 +143,7 @@ class SaveAnimatedPNG: def INPUT_TYPES(s): return {"required": {"images": ("IMAGE", ), - "filename_prefix": ("STRING", {"default": "ComfyUI"}), + "filename_prefix": ("STRING", {"default": "totoroUI"}), "fps": ("FLOAT", {"default": 6.0, "min": 0.01, "max": 1000.0, "step": 0.01}), "compress_level": ("INT", {"default": 4, "min": 0, "max": 9}) }, @@ -157,7 +157,7 @@ class SaveAnimatedPNG: CATEGORY = "image/animation" - def save_images(self, images, fps, compress_level, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None): + def save_images(self, images, fps, compress_level, filename_prefix="totoroUI", prompt=None, extra_pnginfo=None): filename_prefix += self.prefix_append full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0]) results = list() diff --git a/comfy_extras/nodes_latent.py b/comfy_extras/nodes_latent.py index eabae0885..9f33be404 100644 --- a/comfy_extras/nodes_latent.py +++ b/comfy_extras/nodes_latent.py @@ -1,10 +1,10 @@ -import comfy.utils +import totoro.utils import torch def reshape_latent_to(target_shape, latent): if latent.shape[1:] != target_shape[1:]: - latent = comfy.utils.common_upscale(latent, target_shape[3], target_shape[2], "bilinear", "center") - return comfy.utils.repeat_to_batch_size(latent, target_shape[0]) + latent = totoro.utils.common_upscale(latent, target_shape[3], target_shape[2], "bilinear", "center") + return totoro.utils.repeat_to_batch_size(latent, target_shape[0]) class LatentAdd: @@ -116,7 +116,7 @@ class LatentBatch: s2 = samples2["samples"] if s1.shape[1:] != s2.shape[1:]: - s2 = comfy.utils.common_upscale(s2, s1.shape[3], s1.shape[2], "bilinear", "center") + s2 = totoro.utils.common_upscale(s2, s1.shape[3], s1.shape[2], "bilinear", "center") s = torch.cat((s1, s2), dim=0) samples_out["samples"] = s samples_out["batch_index"] = samples1.get("batch_index", [x for x in range(0, s1.shape[0])]) + samples2.get("batch_index", [x for x in range(0, s2.shape[0])]) diff --git a/comfy_extras/nodes_mask.py b/comfy_extras/nodes_mask.py index 29589b4ab..ac3554f0c 100644 --- a/comfy_extras/nodes_mask.py +++ b/comfy_extras/nodes_mask.py @@ -1,7 +1,7 @@ import numpy as np import scipy.ndimage import torch -import comfy.utils +import totoro.utils from nodes import MAX_RESOLUTION @@ -10,7 +10,7 @@ def composite(destination, source, x, y, mask = None, multiplier = 8, resize_sou if resize_source: source = torch.nn.functional.interpolate(source, size=(destination.shape[2], destination.shape[3]), mode="bilinear") - source = comfy.utils.repeat_to_batch_size(source, destination.shape[0]) + source = totoro.utils.repeat_to_batch_size(source, destination.shape[0]) x = max(-source.shape[3] * multiplier, min(x, destination.shape[3] * multiplier)) y = max(-source.shape[2] * multiplier, min(y, destination.shape[2] * multiplier)) @@ -23,7 +23,7 @@ def composite(destination, source, x, y, mask = None, multiplier = 8, resize_sou else: mask = mask.to(destination.device, copy=True) mask = torch.nn.functional.interpolate(mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])), size=(source.shape[2], source.shape[3]), mode="bilinear") - mask = comfy.utils.repeat_to_batch_size(mask, source.shape[0]) + mask = totoro.utils.repeat_to_batch_size(mask, source.shape[0]) # calculate the bounds of the source that will be overlapping the destination # this prevents the source trying to overwrite latent pixels that are out of bounds diff --git a/comfy_extras/nodes_model_advanced.py b/comfy_extras/nodes_model_advanced.py index 918e6085a..98407c119 100644 --- a/comfy_extras/nodes_model_advanced.py +++ b/comfy_extras/nodes_model_advanced.py @@ -1,11 +1,11 @@ import folder_paths -import comfy.sd -import comfy.model_sampling -import comfy.latent_formats +import totoro.sd +import totoro.model_sampling +import totoro.latent_formats import nodes import torch -class LCM(comfy.model_sampling.EPS): +class LCM(totoro.model_sampling.EPS): def calculate_denoised(self, sigma, model_output, model_input): timestep = self.timestep(sigma).view(sigma.shape[:1] + (1,) * (model_output.ndim - 1)) sigma = sigma.view(sigma.shape[:1] + (1,) * (model_output.ndim - 1)) @@ -19,11 +19,11 @@ class LCM(comfy.model_sampling.EPS): return c_out * x0 + c_skip * model_input -class X0(comfy.model_sampling.EPS): +class X0(totoro.model_sampling.EPS): def calculate_denoised(self, sigma, model_output, model_input): return model_output -class ModelSamplingDiscreteDistilled(comfy.model_sampling.ModelSamplingDiscrete): +class ModelSamplingDiscreteDistilled(totoro.model_sampling.ModelSamplingDiscrete): original_timesteps = 50 def __init__(self, model_config=None): @@ -86,11 +86,11 @@ class ModelSamplingDiscrete: def patch(self, model, sampling, zsnr): m = model.clone() - sampling_base = comfy.model_sampling.ModelSamplingDiscrete + sampling_base = totoro.model_sampling.ModelSamplingDiscrete if sampling == "eps": - sampling_type = comfy.model_sampling.EPS + sampling_type = totoro.model_sampling.EPS elif sampling == "v_prediction": - sampling_type = comfy.model_sampling.V_PREDICTION + sampling_type = totoro.model_sampling.V_PREDICTION elif sampling == "lcm": sampling_type = LCM sampling_base = ModelSamplingDiscreteDistilled @@ -122,8 +122,8 @@ class ModelSamplingStableCascade: def patch(self, model, shift): m = model.clone() - sampling_base = comfy.model_sampling.StableCascadeSampling - sampling_type = comfy.model_sampling.EPS + sampling_base = totoro.model_sampling.StableCascadeSampling + sampling_type = totoro.model_sampling.EPS class ModelSamplingAdvanced(sampling_base, sampling_type): pass @@ -148,8 +148,8 @@ class ModelSamplingSD3: def patch(self, model, shift, multiplier=1000): m = model.clone() - sampling_base = comfy.model_sampling.ModelSamplingDiscreteFlow - sampling_type = comfy.model_sampling.CONST + sampling_base = totoro.model_sampling.ModelSamplingDiscreteFlow + sampling_type = totoro.model_sampling.CONST class ModelSamplingAdvanced(sampling_base, sampling_type): pass @@ -195,8 +195,8 @@ class ModelSamplingFlux: b = base_shift - mm * x1 shift = (width * height / (8 * 8 * 2 * 2)) * mm + b - sampling_base = comfy.model_sampling.ModelSamplingFlux - sampling_type = comfy.model_sampling.CONST + sampling_base = totoro.model_sampling.ModelSamplingFlux + sampling_type = totoro.model_sampling.CONST class ModelSamplingAdvanced(sampling_base, sampling_type): pass @@ -227,15 +227,15 @@ class ModelSamplingContinuousEDM: latent_format = None sigma_data = 1.0 if sampling == "eps": - sampling_type = comfy.model_sampling.EPS + sampling_type = totoro.model_sampling.EPS elif sampling == "v_prediction": - sampling_type = comfy.model_sampling.V_PREDICTION + sampling_type = totoro.model_sampling.V_PREDICTION elif sampling == "edm_playground_v2.5": - sampling_type = comfy.model_sampling.EDM + sampling_type = totoro.model_sampling.EDM sigma_data = 0.5 - latent_format = comfy.latent_formats.SDXL_Playground_2_5() + latent_format = totoro.latent_formats.SDXL_Playground_2_5() - class ModelSamplingAdvanced(comfy.model_sampling.ModelSamplingContinuousEDM, sampling_type): + class ModelSamplingAdvanced(totoro.model_sampling.ModelSamplingContinuousEDM, sampling_type): pass model_sampling = ModelSamplingAdvanced(model.model.model_config) @@ -265,9 +265,9 @@ class ModelSamplingContinuousV: latent_format = None sigma_data = 1.0 if sampling == "v_prediction": - sampling_type = comfy.model_sampling.V_PREDICTION + sampling_type = totoro.model_sampling.V_PREDICTION - class ModelSamplingAdvanced(comfy.model_sampling.ModelSamplingContinuousV, sampling_type): + class ModelSamplingAdvanced(totoro.model_sampling.ModelSamplingContinuousV, sampling_type): pass model_sampling = ModelSamplingAdvanced(model.model.model_config) diff --git a/comfy_extras/nodes_model_downscale.py b/comfy_extras/nodes_model_downscale.py index 58b5073ec..ec03dca68 100644 --- a/comfy_extras/nodes_model_downscale.py +++ b/comfy_extras/nodes_model_downscale.py @@ -1,5 +1,5 @@ import torch -import comfy.utils +import totoro.utils class PatchModelAddDownscale: upscale_methods = ["bicubic", "nearest-exact", "bilinear", "area", "bislerp"] @@ -28,12 +28,12 @@ class PatchModelAddDownscale: if transformer_options["block"][1] == block_number: sigma = transformer_options["sigmas"][0].item() if sigma <= sigma_start and sigma >= sigma_end: - h = comfy.utils.common_upscale(h, round(h.shape[-1] * (1.0 / downscale_factor)), round(h.shape[-2] * (1.0 / downscale_factor)), downscale_method, "disabled") + h = totoro.utils.common_upscale(h, round(h.shape[-1] * (1.0 / downscale_factor)), round(h.shape[-2] * (1.0 / downscale_factor)), downscale_method, "disabled") return h def output_block_patch(h, hsp, transformer_options): if h.shape[2] != hsp.shape[2]: - h = comfy.utils.common_upscale(h, hsp.shape[-1], hsp.shape[-2], upscale_method, "disabled") + h = totoro.utils.common_upscale(h, hsp.shape[-1], hsp.shape[-2], upscale_method, "disabled") return h, hsp m = model.clone() diff --git a/comfy_extras/nodes_model_merging.py b/comfy_extras/nodes_model_merging.py index 136f9a984..0e641948c 100644 --- a/comfy_extras/nodes_model_merging.py +++ b/comfy_extras/nodes_model_merging.py @@ -1,15 +1,15 @@ -import comfy.sd -import comfy.utils -import comfy.model_base -import comfy.model_management -import comfy.model_sampling +import totoro.sd +import totoro.utils +import totoro.model_base +import totoro.model_management +import totoro.model_sampling import torch import folder_paths import json import os -from comfy.cli_args import args +from totoro.cli_args import args class ModelMergeSimple: @classmethod @@ -174,16 +174,16 @@ def save_checkpoint(model, clip=None, vae=None, clip_vision=None, filename_prefi metadata = {} enable_modelspec = True - if isinstance(model.model, comfy.model_base.SDXL): - if isinstance(model.model, comfy.model_base.SDXL_instructpix2pix): + if isinstance(model.model, totoro.model_base.SDXL): + if isinstance(model.model, totoro.model_base.SDXL_instructpix2pix): metadata["modelspec.architecture"] = "stable-diffusion-xl-v1-edit" else: metadata["modelspec.architecture"] = "stable-diffusion-xl-v1-base" - elif isinstance(model.model, comfy.model_base.SDXLRefiner): + elif isinstance(model.model, totoro.model_base.SDXLRefiner): metadata["modelspec.architecture"] = "stable-diffusion-xl-v1-refiner" - elif isinstance(model.model, comfy.model_base.SVD_img2vid): + elif isinstance(model.model, totoro.model_base.SVD_img2vid): metadata["modelspec.architecture"] = "stable-video-diffusion-img2vid-v1" - elif isinstance(model.model, comfy.model_base.SD3): + elif isinstance(model.model, totoro.model_base.SD3): metadata["modelspec.architecture"] = "stable-diffusion-v3-medium" #TODO: other SD3 variants else: enable_modelspec = False @@ -200,14 +200,14 @@ def save_checkpoint(model, clip=None, vae=None, clip_vision=None, filename_prefi extra_keys = {} model_sampling = model.get_model_object("model_sampling") - if isinstance(model_sampling, comfy.model_sampling.ModelSamplingContinuousEDM): - if isinstance(model_sampling, comfy.model_sampling.V_PREDICTION): + if isinstance(model_sampling, totoro.model_sampling.ModelSamplingContinuousEDM): + if isinstance(model_sampling, totoro.model_sampling.V_PREDICTION): extra_keys["edm_vpred.sigma_max"] = torch.tensor(model_sampling.sigma_max).float() extra_keys["edm_vpred.sigma_min"] = torch.tensor(model_sampling.sigma_min).float() - if model.model.model_type == comfy.model_base.ModelType.EPS: + if model.model.model_type == totoro.model_base.ModelType.EPS: metadata["modelspec.predict_key"] = "epsilon" - elif model.model.model_type == comfy.model_base.ModelType.V_PREDICTION: + elif model.model.model_type == totoro.model_base.ModelType.V_PREDICTION: metadata["modelspec.predict_key"] = "v" if not args.disable_metadata: @@ -219,7 +219,7 @@ def save_checkpoint(model, clip=None, vae=None, clip_vision=None, filename_prefi output_checkpoint = f"{filename}_{counter:05}_.safetensors" output_checkpoint = os.path.join(full_output_folder, output_checkpoint) - comfy.sd.save_checkpoint(output_checkpoint, model, clip, vae, clip_vision, metadata=metadata, extra_keys=extra_keys) + totoro.sd.save_checkpoint(output_checkpoint, model, clip, vae, clip_vision, metadata=metadata, extra_keys=extra_keys) class CheckpointSave: def __init__(self): @@ -230,7 +230,7 @@ class CheckpointSave: return {"required": { "model": ("MODEL",), "clip": ("CLIP",), "vae": ("VAE",), - "filename_prefix": ("STRING", {"default": "checkpoints/ComfyUI"}),}, + "filename_prefix": ("STRING", {"default": "checkpoints/totoroUI"}),}, "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},} RETURN_TYPES = () FUNCTION = "save" @@ -249,7 +249,7 @@ class CLIPSave: @classmethod def INPUT_TYPES(s): return {"required": { "clip": ("CLIP",), - "filename_prefix": ("STRING", {"default": "clip/ComfyUI"}),}, + "filename_prefix": ("STRING", {"default": "clip/totoroUI"}),}, "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},} RETURN_TYPES = () FUNCTION = "save" @@ -270,7 +270,7 @@ class CLIPSave: for x in extra_pnginfo: metadata[x] = json.dumps(extra_pnginfo[x]) - comfy.model_management.load_models_gpu([clip.load_model()], force_patch_weights=True) + totoro.model_management.load_models_gpu([clip.load_model()], force_patch_weights=True) clip_sd = clip.get_sd() for prefix in ["clip_l.", "clip_g.", ""]: @@ -294,9 +294,9 @@ class CLIPSave: output_checkpoint = f"{filename}_{counter:05}_.safetensors" output_checkpoint = os.path.join(full_output_folder, output_checkpoint) - current_clip_sd = comfy.utils.state_dict_prefix_replace(current_clip_sd, replace_prefix) + current_clip_sd = totoro.utils.state_dict_prefix_replace(current_clip_sd, replace_prefix) - comfy.utils.save_torch_file(current_clip_sd, output_checkpoint, metadata=metadata) + totoro.utils.save_torch_file(current_clip_sd, output_checkpoint, metadata=metadata) return {} class VAESave: @@ -306,7 +306,7 @@ class VAESave: @classmethod def INPUT_TYPES(s): return {"required": { "vae": ("VAE",), - "filename_prefix": ("STRING", {"default": "vae/ComfyUI_vae"}),}, + "filename_prefix": ("STRING", {"default": "vae/totoroUI_vae"}),}, "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},} RETURN_TYPES = () FUNCTION = "save" @@ -330,7 +330,7 @@ class VAESave: output_checkpoint = f"{filename}_{counter:05}_.safetensors" output_checkpoint = os.path.join(full_output_folder, output_checkpoint) - comfy.utils.save_torch_file(vae.get_sd(), output_checkpoint, metadata=metadata) + totoro.utils.save_torch_file(vae.get_sd(), output_checkpoint, metadata=metadata) return {} NODE_CLASS_MAPPINGS = { diff --git a/comfy_extras/nodes_model_merging_model_specific.py b/comfy_extras/nodes_model_merging_model_specific.py index 9557d9b1f..63acacbe6 100644 --- a/comfy_extras/nodes_model_merging_model_specific.py +++ b/comfy_extras/nodes_model_merging_model_specific.py @@ -1,6 +1,6 @@ -import comfy_extras.nodes_model_merging +import totoro_extras.nodes_model_merging -class ModelMergeSD1(comfy_extras.nodes_model_merging.ModelMergeBlocks): +class ModelMergeSD1(totoro_extras.nodes_model_merging.ModelMergeBlocks): CATEGORY = "advanced/model_merging/model_specific" @classmethod def INPUT_TYPES(s): @@ -26,7 +26,7 @@ class ModelMergeSD1(comfy_extras.nodes_model_merging.ModelMergeBlocks): return {"required": arg_dict} -class ModelMergeSDXL(comfy_extras.nodes_model_merging.ModelMergeBlocks): +class ModelMergeSDXL(totoro_extras.nodes_model_merging.ModelMergeBlocks): CATEGORY = "advanced/model_merging/model_specific" @classmethod @@ -52,7 +52,7 @@ class ModelMergeSDXL(comfy_extras.nodes_model_merging.ModelMergeBlocks): return {"required": arg_dict} -class ModelMergeSD3_2B(comfy_extras.nodes_model_merging.ModelMergeBlocks): +class ModelMergeSD3_2B(totoro_extras.nodes_model_merging.ModelMergeBlocks): CATEGORY = "advanced/model_merging/model_specific" @classmethod @@ -75,7 +75,7 @@ class ModelMergeSD3_2B(comfy_extras.nodes_model_merging.ModelMergeBlocks): return {"required": arg_dict} -class ModelMergeFlux1(comfy_extras.nodes_model_merging.ModelMergeBlocks): +class ModelMergeFlux1(totoro_extras.nodes_model_merging.ModelMergeBlocks): CATEGORY = "advanced/model_merging/model_specific" @classmethod diff --git a/comfy_extras/nodes_morphology.py b/comfy_extras/nodes_morphology.py index 071521d87..577b7c25f 100644 --- a/comfy_extras/nodes_morphology.py +++ b/comfy_extras/nodes_morphology.py @@ -1,5 +1,5 @@ import torch -import comfy.model_management +import totoro.model_management from kornia.morphology import dilation, erosion, opening, closing, gradient, top_hat, bottom_hat @@ -18,7 +18,7 @@ class Morphology: CATEGORY = "image/postprocessing" def process(self, image, operation, kernel_size): - device = comfy.model_management.get_torch_device() + device = totoro.model_management.get_torch_device() kernel = torch.ones(kernel_size, kernel_size, device=device) image_k = image.to(device).movedim(-1, 1) if operation == "erode": @@ -37,7 +37,7 @@ class Morphology: output = bottom_hat(image_k, kernel) else: raise ValueError(f"Invalid operation {operation} for morphology. Must be one of 'erode', 'dilate', 'open', 'close', 'gradient', 'tophat', 'bottomhat'") - img_out = output.to(comfy.model_management.intermediate_device()).movedim(1, -1) + img_out = output.to(totoro.model_management.intermediate_device()).movedim(1, -1) return (img_out,) NODE_CLASS_MAPPINGS = { diff --git a/comfy_extras/nodes_pag.py b/comfy_extras/nodes_pag.py index eb28196f4..9d59f17ca 100644 --- a/comfy_extras/nodes_pag.py +++ b/comfy_extras/nodes_pag.py @@ -1,10 +1,10 @@ #Modified/simplified version of the node from: https://github.com/pamparamm/sd-perturbed-attention #If you want the one with more options see the above repo. -#My modified one here is more basic but has less chances of breaking with ComfyUI updates. +#My modified one here is more basic but has less chances of breaking with totoroUI updates. -import comfy.model_patcher -import comfy.samplers +import totoro.model_patcher +import totoro.samplers class PerturbedAttentionGuidance: @classmethod @@ -42,8 +42,8 @@ class PerturbedAttentionGuidance: return cfg_result # Replace Self-attention with PAG - model_options = comfy.model_patcher.set_model_options_patch_replace(model_options, perturbed_attention, "attn1", unet_block, unet_block_id) - (pag,) = comfy.samplers.calc_cond_batch(model, [cond], x, sigma, model_options) + model_options = totoro.model_patcher.set_model_options_patch_replace(model_options, perturbed_attention, "attn1", unet_block, unet_block_id) + (pag,) = totoro.samplers.calc_cond_batch(model, [cond], x, sigma, model_options) return cfg_result + (cond_pred - pag) * scale diff --git a/comfy_extras/nodes_perpneg.py b/comfy_extras/nodes_perpneg.py index 546276aa1..255fd475a 100644 --- a/comfy_extras/nodes_perpneg.py +++ b/comfy_extras/nodes_perpneg.py @@ -1,8 +1,8 @@ import torch -import comfy.model_management -import comfy.sampler_helpers -import comfy.samplers -import comfy.utils +import totoro.model_management +import totoro.sampler_helpers +import totoro.samplers +import totoro.utils import node_helpers def perp_neg(x, noise_pred_pos, noise_pred_neg, noise_pred_nocond, neg_scale, cond_scale): @@ -29,7 +29,7 @@ class PerpNeg: def patch(self, model, empty_conditioning, neg_scale): m = model.clone() - nocond = comfy.sampler_helpers.convert_cond(empty_conditioning) + nocond = totoro.sampler_helpers.convert_cond(empty_conditioning) def cfg_function(args): model = args["model"] @@ -39,9 +39,9 @@ class PerpNeg: x = args["input"] sigma = args["sigma"] model_options = args["model_options"] - nocond_processed = comfy.samplers.encode_model_conds(model.extra_conds, nocond, x, x.device, "negative") + nocond_processed = totoro.samplers.encode_model_conds(model.extra_conds, nocond, x, x.device, "negative") - (noise_pred_nocond,) = comfy.samplers.calc_cond_batch(model, [nocond_processed], x, sigma, model_options) + (noise_pred_nocond,) = totoro.samplers.calc_cond_batch(model, [nocond_processed], x, sigma, model_options) cfg_result = x - perp_neg(x, noise_pred_pos, noise_pred_neg, noise_pred_nocond, neg_scale, cond_scale) return cfg_result @@ -51,7 +51,7 @@ class PerpNeg: return (m, ) -class Guider_PerpNeg(comfy.samplers.CFGGuider): +class Guider_PerpNeg(totoro.samplers.CFGGuider): def set_conds(self, positive, negative, empty_negative_prompt): empty_negative_prompt = node_helpers.conditioning_set_values(empty_negative_prompt, {"prompt_type": "negative"}) self.inner_set_conds({"positive": positive, "empty_negative_prompt": empty_negative_prompt, "negative": negative}) @@ -69,7 +69,7 @@ class Guider_PerpNeg(comfy.samplers.CFGGuider): empty_cond = self.conds.get("empty_negative_prompt", None) (noise_pred_pos, noise_pred_neg, noise_pred_empty) = \ - comfy.samplers.calc_cond_batch(self.inner_model, [positive_cond, negative_cond, empty_cond], x, timestep, model_options) + totoro.samplers.calc_cond_batch(self.inner_model, [positive_cond, negative_cond, empty_cond], x, timestep, model_options) cfg_result = perp_neg(x, noise_pred_pos, noise_pred_neg, noise_pred_empty, self.neg_scale, self.cfg) # normally this would be done in cfg_function, but we skipped diff --git a/comfy_extras/nodes_photomaker.py b/comfy_extras/nodes_photomaker.py index 29d127d74..0d9b75ce4 100644 --- a/comfy_extras/nodes_photomaker.py +++ b/comfy_extras/nodes_photomaker.py @@ -1,9 +1,9 @@ import torch import torch.nn as nn import folder_paths -import comfy.clip_model -import comfy.clip_vision -import comfy.ops +import totoro.clip_model +import totoro.clip_vision +import totoro.ops # code for model from: https://github.com/TencentARC/PhotoMaker/blob/main/photomaker/model.py under Apache License Version 2.0 VISION_CONFIG_DICT = { @@ -19,7 +19,7 @@ VISION_CONFIG_DICT = { } class MLP(nn.Module): - def __init__(self, in_dim, out_dim, hidden_dim, use_residual=True, operations=comfy.ops): + def __init__(self, in_dim, out_dim, hidden_dim, use_residual=True, operations=totoro.ops): super().__init__() if use_residual: assert in_dim == out_dim @@ -88,15 +88,15 @@ class FuseModule(nn.Module): updated_prompt_embeds = prompt_embeds.view(batch_size, seq_length, -1) return updated_prompt_embeds -class PhotoMakerIDEncoder(comfy.clip_model.CLIPVisionModelProjection): +class PhotoMakerIDEncoder(totoro.clip_model.CLIPVisionModelProjection): def __init__(self): - self.load_device = comfy.model_management.text_encoder_device() - offload_device = comfy.model_management.text_encoder_offload_device() - dtype = comfy.model_management.text_encoder_dtype(self.load_device) + self.load_device = totoro.model_management.text_encoder_device() + offload_device = totoro.model_management.text_encoder_offload_device() + dtype = totoro.model_management.text_encoder_dtype(self.load_device) - super().__init__(VISION_CONFIG_DICT, dtype, offload_device, comfy.ops.manual_cast) - self.visual_projection_2 = comfy.ops.manual_cast.Linear(1024, 1280, bias=False) - self.fuse_module = FuseModule(2048, comfy.ops.manual_cast) + super().__init__(VISION_CONFIG_DICT, dtype, offload_device, totoro.ops.manual_cast) + self.visual_projection_2 = totoro.ops.manual_cast.Linear(1024, 1280, bias=False) + self.fuse_module = FuseModule(2048, totoro.ops.manual_cast) def forward(self, id_pixel_values, prompt_embeds, class_tokens_mask): b, num_inputs, c, h, w = id_pixel_values.shape @@ -128,7 +128,7 @@ class PhotoMakerLoader: def load_photomaker_model(self, photomaker_model_name): photomaker_model_path = folder_paths.get_full_path("photomaker", photomaker_model_name) photomaker_model = PhotoMakerIDEncoder() - data = comfy.utils.load_torch_file(photomaker_model_path, safe_load=True) + data = totoro.utils.load_torch_file(photomaker_model_path, safe_load=True) if "id_encoder" in data: data = data["id_encoder"] photomaker_model.load_state_dict(data) @@ -151,7 +151,7 @@ class PhotoMakerEncode: def apply_photomaker(self, photomaker, image, clip, text): special_token = "photomaker" - pixel_values = comfy.clip_vision.clip_preprocess(image.to(photomaker.load_device)).float() + pixel_values = totoro.clip_vision.clip_preprocess(image.to(photomaker.load_device)).float() try: index = text.split(" ").index(special_token) + 1 except ValueError: diff --git a/comfy_extras/nodes_post_processing.py b/comfy_extras/nodes_post_processing.py index 68f6ef51e..519f358c9 100644 --- a/comfy_extras/nodes_post_processing.py +++ b/comfy_extras/nodes_post_processing.py @@ -4,8 +4,8 @@ import torch.nn.functional as F from PIL import Image import math -import comfy.utils -import comfy.model_management +import totoro.utils +import totoro.model_management class Blend: @@ -37,7 +37,7 @@ class Blend: image2 = image2.to(image1.device) if image1.shape != image2.shape: image2 = image2.permute(0, 3, 1, 2) - image2 = comfy.utils.common_upscale(image2, image1.shape[2], image1.shape[1], upscale_method='bicubic', crop='center') + image2 = totoro.utils.common_upscale(image2, image1.shape[2], image1.shape[1], upscale_method='bicubic', crop='center') image2 = image2.permute(0, 2, 3, 1) blended_image = self.blend_mode(image1, image2, blend_mode) @@ -103,7 +103,7 @@ class Blur: if blur_radius == 0: return (image,) - image = image.to(comfy.model_management.get_torch_device()) + image = image.to(totoro.model_management.get_torch_device()) batch_size, height, width, channels = image.shape kernel_size = blur_radius * 2 + 1 @@ -114,7 +114,7 @@ class Blur: blurred = F.conv2d(padded_image, kernel, padding=kernel_size // 2, groups=channels)[:,:,blur_radius:-blur_radius, blur_radius:-blur_radius] blurred = blurred.permute(0, 2, 3, 1) - return (blurred.to(comfy.model_management.intermediate_device()),) + return (blurred.to(totoro.model_management.intermediate_device()),) class Quantize: def __init__(self): @@ -227,7 +227,7 @@ class Sharpen: return (image,) batch_size, height, width, channels = image.shape - image = image.to(comfy.model_management.get_torch_device()) + image = image.to(totoro.model_management.get_torch_device()) kernel_size = sharpen_radius * 2 + 1 kernel = gaussian_kernel(kernel_size, sigma, device=image.device) * -(alpha*10) @@ -242,7 +242,7 @@ class Sharpen: result = torch.clamp(sharpened, 0, 1) - return (result.to(comfy.model_management.intermediate_device()),) + return (result.to(totoro.model_management.intermediate_device()),) class ImageScaleToTotalPixels: upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"] @@ -266,7 +266,7 @@ class ImageScaleToTotalPixels: width = round(samples.shape[3] * scale_by) height = round(samples.shape[2] * scale_by) - s = comfy.utils.common_upscale(samples, width, height, upscale_method, "disabled") + s = totoro.utils.common_upscale(samples, width, height, upscale_method, "disabled") s = s.movedim(1,-1) return (s,) diff --git a/comfy_extras/nodes_sag.py b/comfy_extras/nodes_sag.py index 5e15b99e5..444eca093 100644 --- a/comfy_extras/nodes_sag.py +++ b/comfy_extras/nodes_sag.py @@ -4,10 +4,10 @@ import torch.nn.functional as F import math from einops import rearrange, repeat -from comfy.ldm.modules.attention import optimized_attention -import comfy.samplers +from totoro.ldm.modules.attention import optimized_attention +import totoro.samplers -# from comfy/ldm/modules/attention.py +# from totoro/ldm/modules/attention.py # but modified to return attention scores as well as output def attention_basic_with_sim(q, k, v, heads, mask=None, attn_precision=None): b, _, dim_head = q.shape @@ -149,7 +149,7 @@ class SelfAttentionGuidance: degraded = create_blur_map(uncond_pred, uncond_attn, sag_sigma, sag_threshold) degraded_noised = degraded + x - uncond_pred # call into the UNet - (sag,) = comfy.samplers.calc_cond_batch(model, [uncond], degraded_noised, sigma, model_options) + (sag,) = totoro.samplers.calc_cond_batch(model, [uncond], degraded_noised, sigma, model_options) return cfg_result + (degraded - sag) * sag_scale m.set_model_sampler_post_cfg_function(post_cfg_function, disable_cfg1_optimization=True) diff --git a/comfy_extras/nodes_sd3.py b/comfy_extras/nodes_sd3.py index ae9b85981..f65bd586f 100644 --- a/comfy_extras/nodes_sd3.py +++ b/comfy_extras/nodes_sd3.py @@ -1,6 +1,6 @@ import folder_paths -import comfy.sd -import comfy.model_management +import totoro.sd +import totoro.model_management import nodes import torch @@ -18,12 +18,12 @@ class TripleCLIPLoader: clip_path1 = folder_paths.get_full_path("clip", clip_name1) clip_path2 = folder_paths.get_full_path("clip", clip_name2) clip_path3 = folder_paths.get_full_path("clip", clip_name3) - clip = comfy.sd.load_clip(ckpt_paths=[clip_path1, clip_path2, clip_path3], embedding_directory=folder_paths.get_folder_paths("embeddings")) + clip = totoro.sd.load_clip(ckpt_paths=[clip_path1, clip_path2, clip_path3], embedding_directory=folder_paths.get_folder_paths("embeddings")) return (clip,) class EmptySD3LatentImage: def __init__(self): - self.device = comfy.model_management.intermediate_device() + self.device = totoro.model_management.intermediate_device() @classmethod def INPUT_TYPES(s): diff --git a/comfy_extras/nodes_sdupscale.py b/comfy_extras/nodes_sdupscale.py index bba67e8dd..7595d9e3b 100644 --- a/comfy_extras/nodes_sdupscale.py +++ b/comfy_extras/nodes_sdupscale.py @@ -1,5 +1,5 @@ import torch -import comfy.utils +import totoro.utils class SD_4XUpscale_Conditioning: @classmethod @@ -21,7 +21,7 @@ class SD_4XUpscale_Conditioning: width = max(1, round(images.shape[-2] * scale_ratio)) height = max(1, round(images.shape[-3] * scale_ratio)) - pixels = comfy.utils.common_upscale((images.movedim(-1,1) * 2.0) - 1.0, width // 4, height // 4, "bilinear", "center") + pixels = totoro.utils.common_upscale((images.movedim(-1,1) * 2.0) - 1.0, width // 4, height // 4, "bilinear", "center") out_cp = [] out_cn = [] diff --git a/comfy_extras/nodes_stable3d.py b/comfy_extras/nodes_stable3d.py index be2e34c28..e84dc7b7c 100644 --- a/comfy_extras/nodes_stable3d.py +++ b/comfy_extras/nodes_stable3d.py @@ -1,6 +1,6 @@ import torch import nodes -import comfy.utils +import totoro.utils def camera_embeddings(elevation, azimuth): elevation = torch.as_tensor([elevation]) @@ -42,7 +42,7 @@ class StableZero123_Conditioning: def encode(self, clip_vision, init_image, vae, width, height, batch_size, elevation, azimuth): output = clip_vision.encode_image(init_image) pooled = output.image_embeds.unsqueeze(0) - pixels = comfy.utils.common_upscale(init_image.movedim(-1,1), width, height, "bilinear", "center").movedim(1,-1) + pixels = totoro.utils.common_upscale(init_image.movedim(-1,1), width, height, "bilinear", "center").movedim(1,-1) encode_pixels = pixels[:,:,:,:3] t = vae.encode(encode_pixels) cam_embeds = camera_embeddings(elevation, azimuth) @@ -77,7 +77,7 @@ class StableZero123_Conditioning_Batched: def encode(self, clip_vision, init_image, vae, width, height, batch_size, elevation, azimuth, elevation_batch_increment, azimuth_batch_increment): output = clip_vision.encode_image(init_image) pooled = output.image_embeds.unsqueeze(0) - pixels = comfy.utils.common_upscale(init_image.movedim(-1,1), width, height, "bilinear", "center").movedim(1,-1) + pixels = totoro.utils.common_upscale(init_image.movedim(-1,1), width, height, "bilinear", "center").movedim(1,-1) encode_pixels = pixels[:,:,:,:3] t = vae.encode(encode_pixels) @@ -88,7 +88,7 @@ class StableZero123_Conditioning_Batched: azimuth += azimuth_batch_increment cam_embeds = torch.cat(cam_embeds, dim=0) - cond = torch.cat([comfy.utils.repeat_to_batch_size(pooled, batch_size), cam_embeds], dim=-1) + cond = torch.cat([totoro.utils.repeat_to_batch_size(pooled, batch_size), cam_embeds], dim=-1) positive = [[cond, {"concat_latent_image": t}]] negative = [[torch.zeros_like(pooled), {"concat_latent_image": torch.zeros_like(t)}]] @@ -116,7 +116,7 @@ class SV3D_Conditioning: def encode(self, clip_vision, init_image, vae, width, height, video_frames, elevation): output = clip_vision.encode_image(init_image) pooled = output.image_embeds.unsqueeze(0) - pixels = comfy.utils.common_upscale(init_image.movedim(-1,1), width, height, "bilinear", "center").movedim(1,-1) + pixels = totoro.utils.common_upscale(init_image.movedim(-1,1), width, height, "bilinear", "center").movedim(1,-1) encode_pixels = pixels[:,:,:,:3] t = vae.encode(encode_pixels) diff --git a/comfy_extras/nodes_stable_cascade.py b/comfy_extras/nodes_stable_cascade.py index fcbbeb27f..63525cdc3 100644 --- a/comfy_extras/nodes_stable_cascade.py +++ b/comfy_extras/nodes_stable_cascade.py @@ -1,5 +1,5 @@ """ - This file is part of ComfyUI. + This file is part of totoroUI. Copyright (C) 2024 Stability AI This program is free software: you can redistribute it and/or modify @@ -18,7 +18,7 @@ import torch import nodes -import comfy.utils +import totoro.utils class StableCascade_EmptyLatentImage: @@ -71,7 +71,7 @@ class StableCascade_StageC_VAEEncode: out_width = (width // compression) * vae.downscale_ratio out_height = (height // compression) * vae.downscale_ratio - s = comfy.utils.common_upscale(image.movedim(-1,1), out_width, out_height, "bicubic", "center").movedim(1,-1) + s = totoro.utils.common_upscale(image.movedim(-1,1), out_width, out_height, "bicubic", "center").movedim(1,-1) c_latent = vae.encode(s[:,:,:,:3]) b_latent = torch.zeros([c_latent.shape[0], 4, (height // 8) * 2, (width // 8) * 2]) diff --git a/comfy_extras/nodes_upscale_model.py b/comfy_extras/nodes_upscale_model.py index bca79ef2e..75e0ccb21 100644 --- a/comfy_extras/nodes_upscale_model.py +++ b/comfy_extras/nodes_upscale_model.py @@ -1,9 +1,9 @@ import os import logging from spandrel import ModelLoader, ImageModelDescriptor -from comfy import model_management +from totoro import model_management import torch -import comfy.utils +import totoro.utils import folder_paths try: @@ -26,9 +26,9 @@ class UpscaleModelLoader: def load_model(self, model_name): model_path = folder_paths.get_full_path("upscale_models", model_name) - sd = comfy.utils.load_torch_file(model_path, safe_load=True) + sd = totoro.utils.load_torch_file(model_path, safe_load=True) if "module.layers.0.residual_group.blocks.0.norm1.weight" in sd: - sd = comfy.utils.state_dict_prefix_replace(sd, {"module.":""}) + sd = totoro.utils.state_dict_prefix_replace(sd, {"module.":""}) out = ModelLoader().load_from_state_dict(sd).eval() if not isinstance(out, ImageModelDescriptor): @@ -65,9 +65,9 @@ class ImageUpscaleWithModel: oom = True while oom: try: - steps = in_img.shape[0] * comfy.utils.get_tiled_scale_steps(in_img.shape[3], in_img.shape[2], tile_x=tile, tile_y=tile, overlap=overlap) - pbar = comfy.utils.ProgressBar(steps) - s = comfy.utils.tiled_scale(in_img, lambda a: upscale_model(a), tile_x=tile, tile_y=tile, overlap=overlap, upscale_amount=upscale_model.scale, pbar=pbar) + steps = in_img.shape[0] * totoro.utils.get_tiled_scale_steps(in_img.shape[3], in_img.shape[2], tile_x=tile, tile_y=tile, overlap=overlap) + pbar = totoro.utils.ProgressBar(steps) + s = totoro.utils.tiled_scale(in_img, lambda a: upscale_model(a), tile_x=tile, tile_y=tile, overlap=overlap, upscale_amount=upscale_model.scale, pbar=pbar) oom = False except model_management.OOM_EXCEPTION as e: tile //= 2 diff --git a/comfy_extras/nodes_video_model.py b/comfy_extras/nodes_video_model.py index 1a0189ed4..47931394b 100644 --- a/comfy_extras/nodes_video_model.py +++ b/comfy_extras/nodes_video_model.py @@ -1,9 +1,9 @@ import nodes import torch -import comfy.utils -import comfy.sd +import totoro.utils +import totoro.sd import folder_paths -import comfy_extras.nodes_model_merging +import totoro_extras.nodes_model_merging class ImageOnlyCheckpointLoader: @@ -18,7 +18,7 @@ class ImageOnlyCheckpointLoader: def load_checkpoint(self, ckpt_name, output_vae=True, output_clip=True): ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name) - out = comfy.sd.load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=False, output_clipvision=True, embedding_directory=folder_paths.get_folder_paths("embeddings")) + out = totoro.sd.load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=False, output_clipvision=True, embedding_directory=folder_paths.get_folder_paths("embeddings")) return (out[0], out[3], out[2]) @@ -45,7 +45,7 @@ class SVD_img2vid_Conditioning: def encode(self, clip_vision, init_image, vae, width, height, video_frames, motion_bucket_id, fps, augmentation_level): output = clip_vision.encode_image(init_image) pooled = output.image_embeds.unsqueeze(0) - pixels = comfy.utils.common_upscale(init_image.movedim(-1,1), width, height, "bilinear", "center").movedim(1,-1) + pixels = totoro.utils.common_upscale(init_image.movedim(-1,1), width, height, "bilinear", "center").movedim(1,-1) encode_pixels = pixels[:,:,:,:3] if augmentation_level > 0: encode_pixels += torch.randn_like(pixels) * augmentation_level @@ -106,7 +106,7 @@ class VideoTriangleCFGGuidance: m.set_model_sampler_cfg_function(linear_cfg) return (m, ) -class ImageOnlyCheckpointSave(comfy_extras.nodes_model_merging.CheckpointSave): +class ImageOnlyCheckpointSave(totoro_extras.nodes_model_merging.CheckpointSave): CATEGORY = "_for_testing" @classmethod @@ -114,11 +114,11 @@ class ImageOnlyCheckpointSave(comfy_extras.nodes_model_merging.CheckpointSave): return {"required": { "model": ("MODEL",), "clip_vision": ("CLIP_VISION",), "vae": ("VAE",), - "filename_prefix": ("STRING", {"default": "checkpoints/ComfyUI"}),}, + "filename_prefix": ("STRING", {"default": "checkpoints/totoroUI"}),}, "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},} def save(self, model, clip_vision, vae, filename_prefix, prompt=None, extra_pnginfo=None): - comfy_extras.nodes_model_merging.save_checkpoint(model, clip_vision=clip_vision, vae=vae, filename_prefix=filename_prefix, output_dir=self.output_dir, prompt=prompt, extra_pnginfo=extra_pnginfo) + totoro_extras.nodes_model_merging.save_checkpoint(model, clip_vision=clip_vision, vae=vae, filename_prefix=filename_prefix, output_dir=self.output_dir, prompt=prompt, extra_pnginfo=extra_pnginfo) return {} NODE_CLASS_MAPPINGS = { diff --git a/comfyui_screenshot.png b/comfyui_screenshot.png deleted file mode 100644 index 73272eae69339f92c88422c45ad166cbc5900adf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118577 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 za)�WN!w(zu`1bvFks@Dt+y~-#JyukuT{BX+w`IYHZnarGCeA&!{&&<14+OpMc+$ zEQfz>5B>H1nW;V3KYDdZ6?F{T6||VOqYphkbz3PiCSj?K3o|n_VfIbPX5lASJC3F; znj0QCl#1r0u2Z5s=BEI;MD>6@Gv1(?xmmZbXh+V_={C{mc?>k!L-)%q2LW7 zPMIRNC<4OWEm!w>a$MGQ8@*g4plu|7t3P&i_yZoN}#nlr_cP0{fxD5QwXaW zTfh*|^$+&pCg4`AWXp2`8yVsqf+WVzZvqy=^quB`>Z2-;j8F>cuUUSQ z6oZ9?4y$GcC!2rr9 zmTEx5-!8fScT{(g`WI^YSqx$sGLZ7yR|21pyTRxQmCohM#XB8$O*W2gyj5nEw`cPj z3#&i5z&cKZW+{f2y#eTo0YWT4wc4t9LX-OhbNr)6eIRMAy}thjP{zDrM#^n-hrHp= zJ)iVD?Pbu77x3Y4Z2NPpy0HFcC(qbx^|s4ojnk3t-6C^?n)KIB!&l7)vLAf&{%`?2 z(il{5`r^ny?=tj~A;Bk3Qi0IxR?qjw()S~m6txz7v_Dw5=1iX|w49RF$->5FvDh#)TehkE+Z ze-;b<*I=R)Ahhwwpiw(QMN<{V;x2X|W-{N6+io_=YXdlmsjo7TURK%YW=~{Ybn3m zEu@${7%59c91D1eLbR!l@yqSx`>#$|1}&JuTKdwv zJ`=d&=B*isM=bEH$6WX{d%-F@Xl|y~2p3$uUo*aI_aP%=zslE}g2Td$KGYT!rdf8> z#g(i5zVp3j%l+6<;cA{fJq5`)h5%aXYBZu<4f|z(R`FzTKyj8EpFcl4Xd(|+#v`h# zKWFBE+7TqfC^^^cYrq+h2xs~+z{U_w?_e!`X_u#lJ5VSa%zj}Zj0;d!V>am&6l9{O z9`A$flM*R;eMEK%sraxMtI=9Sh@j(OSbf@B>64qBj65v2MN^IWfM1oFaLsE0+Ktl$?D^Xu9X_;a59wXKr!PeX*TZPM=CCY7?liO+B7cr(Et6Yic0SXe?+~ z;PbxzW@3Eiipxhs+nLlZb3HEWYVDt}8h4Qt6B8S-@;GDCz?|Gx$3%B9b6#+}a#!>kY?`!y1j?9dBCNQP6pVt=$Tx!Y94c zr{5@sh#Jfja4o2nrAXc>rr_>@xtAqj>e-9&<&Pd+J$ZtbpMR}w!HtIr8y_Z2I><0wh-6>6EccRZ zTX;mCgm{{~OT+qAedakpmHUO+vLi%3|6vnzaAU&8$j=o^XwJp#AqGDJbb~7~d@x(E z(%_|~b>YIJs_wBVw`g~ihSBbo`%|q@mD6zdx8;elo#!0`;K1ism48iHWZwvof!4Vt zu>xjVd8Zyf5!!<_No2_y%m-a7&2lUrX1SxIZD$9TcY1wTbij!_;W9kp;-b>6_nf#t z9#4AFlVWcY04E;{xpR;@4eHS>&8yq7Dyv*I-7eDo*HsAcgcmsD8hpy7s- z94`We-KiK9896jQCFK_Z1~*Dh+BKU^vs$y0I;$`RzAw6*6I>0IEO1m|mue2${|AC+ zxySGo1*u^m1tX+o)R>70^c%l8&%+udr}Lu^aeUc)JGmLN!9 zusqiP$$;56;|l~~l~3R)yg4zW#Zg}?1!H7j@88z>+E!|spZ}+-a^*4QN?WOxrRN-* z-8#S5goMZm3%hFTIEhPYWoK_@SSI74mu6ozuX*ay@AKnbn{)9;&=sF(WS5_6K6UI2 zFcIMVu{#w}`KXeKOcrn*p)tkTBQ7BUE-*lHbatNIfgg+2Chy(ltm{bkV=;gn+*|2s zPpcosk}CVyO;y}2ehU-y=10hz79rSJWagG2h2 zr4Fab6%lS(!D*@2!i{qhP6??f9M07~DHO6fad(^5NSFPjt<;mj$-R!QoQd^DIjlA7 zZ_TBpx$0<7=H(2TTXx`C!sTf7_1jA1R2|6KQfuQdG-I0k&vUef*=`NKD14&b(MKuY z1SZ5)S_Z)VyKb+zs=&>(>@0xB8|y%^hx<@lOABV&tTmGy;fs9xBrE@lvdFUwn&?_! zkTG5%A&>(W=z>t>jM|uzl%IbIC1IVZJxa!DWve`J;u8e=A#=(86>y|-qo~!G3xnmx zVb-18qS6IBDjPtkj&GI-Ocu=?xfiq)##(ZvmRrJU{owko%(U1SqoH29I zv20A6a1HkU{WW4(PD7Xrw9O8R^fc|9%8{RJv0A#eWuRXbz+D%L`NQ%b+uKnIfRPIP z=?8sBc24o%?6zr++wkQ2(DjP?h4`q9cYuz-Zh(q!-=3VB8pPw>l@CHEfLTqAMvQR) z46XgDe1C{hllt>B>c-zpA~%k&IZUjy{w|w*d*5@0b?a7hFwmk3f0=5^Mz?uv27kPa z^2ovXvvX9Bvj(Qd3%b7Tz4&fm76BR=w!k9;}tCP%2~IrW2_yA{@Fu+3dIzfZA9XNFFu0kN{CB*u`2+0TmaVT!c_Fa1uH8}jU4D;wC6o(mf|2& zB4b<|+tFpvEZ=trKn;E-ZOT`_&C&sS4}nDB0Wm5CP$`ROtAg$)v;g?0(a+M{+!chi zoW#WIqMO%{<17Q1DTwI<>x3Ee0B=ZbyoD8 z5Kbom8qloG=+6XK?ff?hG8#Yu+R-5UPnbod*D6yf~1CLY(#%P?GBXeo}Ms7Hk|^F9<%IBndemzL592wr=@@rJhW??l8N=3H`PueSn}j6iD) zY7GEqeLoSV`iTxot^C5%lxmpBYeSbFMKocFAC$gm41E6J!7}1Zb^U1-@|~C(1k&hz z=!|gnNLV;=^dP5fHt@KNQ8>_nw_#$fOq8gVEfiFe09+P18N+b@13gN>hr4PFyk^XT z7S3xQ%kY@H<)HrAd7gM%^val4U3bS3zkvVk%J|B3HzOf|N4zn=E3CGsvUWkln?k`8 z$?+1>*?oj8Tf3gIneoUw4kxLqwIC%IkI1hF+Vq#sP6X_jw^gah$$0hAnV zI|KH(Ym9UnKFwWvHeH4G+jhr$DJ=j^ZWj2k<1gM4TFJ>~Yq8_@(C#^7eFdGhN9;>% z!S@5Hm+5X(z?W;}moj@&pDvXWMr^dL7H=lUspU4G2Fsd|Qk3WW#Hwp)!ScurX9fG3 z0qql3!;=SYB61irT*R@OLylA8pXMp=w4w7llL@%IftBmHQ^s)C1|!Z2hu|~rLWGpk z+JdZhKDfPR zKbeQKG^0?kH=SgoC?+YHmy`1-`9pjAQ8Ff?vq@St@I$1pZ}WV4YJe+&Aszth{dAsH zBy{`$^SgT_or^7Q?XK&4`}*~cuRCOG&E+gztNQ<}r!zcCOb6?_?H&X~Na>2OFlKfH zA0-xYf{=FhVb@t8xdYr}GDI_Yk$<2y#JZoaF!|#jijW&>7mP3+a-4D7%Y1P?hLYM8 zbwE6r*LKOD2Twz1CcA=@rvbdc{$14?sq^%iL^etmY>e6+8EwlFam~Z z81pvYpJQ>MC!ChyM`4&^3kFwh&u%^kFR(y=zxLH41%Y_RJnNay<#iblOA8t4?gzc< z_bXpU$5)_6-Bs7>>Ki z$v{Lxt9X54n+HJSz70J4;XMd?R(Wyx#B$Ne*0Q~nyB#ZlTlJ}ac7H=li|OViq+VXU z&+P*;|KcI_NY&h{+gf(Xa>WbX$kqSOLLw+hOqS`{bCv2EKsAOo4F)ut9dx>WY2tFB z;3=VvbK{433^z@=Ba;SGz6Tv=%MS!-a+V}@7abj(zc91p*z1-bA78e~kHPh2ZLbIS ze^<(X>Y*YK|L>*zEzMfrEM~~iYTI!jOiW6)Y zBV*$zVJ|;a+0E2w-~-$E+D8jVljsc1hJ(v z`48e_4X!RZm&C*Cu7PYG92y#To8D$S93?=1!c2wb3OmYwuNE$MjD2oWr!i_LuUkzE z@x1rzK6~vn&2#y2^}27WD=WubiZk1p^tH98;S2|Qab-(VH(7$Q=GFjM|qmuBMgaSj)Zu^=@^?2HaV+ zna*z>A}WmJlysl?fDLizE8~>Hv-oT2s3{UBv_k#g$V)(<>$hh(DLq|+)(~dD+UfS@ za9Batf}ma8zH$$P#_$5!XSDd|$F_XixSgpd_4MQxO-0ZB*;!whb?&u7O|)>k{qYdj$g1Rv=bPTV6QLejzpqez}W-Ucb@>P{PNFbnhAI)=jA>rcPISium& z#beXb3J0B+7@2Ti*7-@9p)|Cv=z?e4M)i2Wxe%?y(u9O>V1uNkrw_N~Wnex$+OPhb zrNKG>mg?t@L^ z^~R3)*U3T$mp2HQt+PEKy7R%NB}*PWTyZ!eJM2yN%uJV%lc;1^Ak7jbol^au@) zgl`gHH4sAuk&+A!<95u0Y7e7YOo8|b5DaoF7L|pyn3Uc6fzZzY@Lj!frMI6S@HDu3 z3J42d2Izwo4v`}`DKC&|E`SvexMoW%Bngk(P-+pfK%_=&Y-}@Nw74UGA=|_D5j9CQ z*&h8`)aLme!)w@OhzX(?=85kc+wxjI4YuWA)&uy&fd)+q!I}eFhxuf%3T?Moc%EF!wEX^BPz(vwN;Hoi<5V zt1wS)92$9a+z7j@$ehiSsb7l%cp96UU^vK$9tB`;`HSCf8(P(4#_+{jxgL8N&?CZ- zANoarFVlV;<2O7O+#PJ3Tk?wP+`~^?98QmE9=JSL;5iik%;O|x(L|w(i!M4kk~m7x zB-nlkr64iFz{RDodBE4lM+O8X#j-VF)KHET>p zeN1iJqpKW(ULv3h3jov*oYEr^XI{VlBE){UebI8Rj*w@fJN*mwa3d&V;C}83gRRvs z02;!uR)rHQexP6V;&LcsGVoTo**vi@4}}vVp_IzczgL?5(L#b42MB*@R2Ne)gw%M1 zC#RyK4?qllPf!w$BfVz2ThXPYtn7k7=P}jI(&$z`FyYwyt`qh{;n)-h*2*!nvdWk> zBoR^!kA21Rhza{W2ntmnkeNQ~%28_8dG z>@}|UBDCgf*d%VVy51PvP55#Pg`h5DQUi{I0lkQ-r(oJD4&(|*L+!ntFD)cK;c;8s zr$hll!P9`O4dp!uaC~xdb67Hj?gZ5>CI^Iw=(+s*-dSbQRT(~8hiGeMcR}v=N?Ud> z#-+_32qv%#h-+sE^5w_kJz`?^po2iSeow|B*=4(yuC6YI7eSs?g2>F5$B-{Zkb9uY zYK#l5uC;br@B8rJI|?&Z zB&FEZWP8x%K>E(t`KpE zqp-k0_akHi6Epw7REL3UU0q#Z`_?94K*wY%CJQh8NR+>R^JdoQx3bgGutCRYH@n}t z4v7osXi$>le&FIRh)JBTgF6%Jr!d!K`_?tNx&Gr;6IGc12glpSIIX(^lDX#@4yCWb zVDJpK(CDjg&a+BdwFAY+=s>o+J(&BF*Ot~i(5knlX%!EY24buXj1#?e=kPEHvUVMv z*E^f=*=5N2mpwNXbL?;E1K;6}39E0p;LqCqxv-}|@J|HWl~vpflXCmfw1o{X;n%&c z4jJP|gnJah{=OJm@UtcJ97;nBBH+>4GJ_MBQSI07fTC+`z zj63bV9?oUfimOAV!lZWU)VDtgsHt>aYA6+n0+^)OVcNX3KBGP4e`ta9=pQi zKK7NoYOcC+`^x(iFHzp%Wh&(7p-fIo_+vjG@!oS~z1sKqSCP-W1%93QqVk!{>_+|l zRV6|B>!lcTx_>B(GCVzsU|IzjAJZ#S>rA0tgb1||fX>vw`#yVcGH_z+B7*ZM0pR%n zp8*`{4`*Wu<`1kkF~k+wKiW(%dXygf1mFkA>~$QhZxP&4jE+f=@I8t75Xk3E#8_b~ zg0Q2Ru8Z1L!AI~dRz6U$KA57+f${Oo;|0$7S@bIIVl?cfQNyW|@DUg%?hU(-ZJgE46zuWwtDm%OMkuCIW`cdKpJ*0{O7h63NG>+NYGe|ue1(_|_J%3p7LL6+V6^(l?p zMIU3*<=`y`nMl>WQ(p;>1ndJWNk**RO*$TF^?mSbjj(Vw2v^kV+C_Wy%EG682T%dX zsOIf?GlpqNPXdkv51xP+QO;b8rm0=bqG4a(lpv6{-W%sD5% zBqjwj$sbG$`nAn0{*~(0G)=H06YF!vB!&TJ4n))RNhSbFA4c2m6b%nS2j*->+Z4dV5V3 z))D>p$)*{2ETV~v6~A&lA7;XFW0OFE%^eF_L2hd5+fEzAGiL*J4>+wO1 zdAh00NE<6FD>pY;*IIWA3!$R-%^IKW3U=+@t&u|3Xt#1gdVrUuB?v93qt*=EU&RkPv+)^96KZEf(=xWyKNIfCw7^-KiYaB_wF4g!z3J;saE_G?3{L#C4+nH;Mf2c z(km3emROC=>+5ow^{YZEw@2$>x)kaeq;UJlot>RTJ|Pupn1CgMwLIx3V!q`M7r><$ z|Hv+8x-Oy=XBs$;!IZbgK-2pf`I7_>iatp2+Vc zQuu@jEACiGV}P1tF}Rjuz}e|&t&C<^!-fzIj6(4CAQ7JA@=frbiQ z5(3h3Z(rX?#0E?kMtVI(EFmOAm(YDh%DC5}Wrw4*;MM1_UIH5>^6afx4Zp87#@|GD61P+5u8)EtQ2ATp5P+iBDYZ*y3Upo|_X zDQUuiB8`FYlk=5rRzu{J?sRVL`B-*3tz-QFQ8D!O%RT;~T)Y!W$?Jj8F4)r5qE2-|#bQ`it7VMHfn zmgr`g)N;V$Ri+=uzdOwJ2@7+j0D{rY*L;%D9f&^t{sKB_#~;0QhzJA$098MRyu5)v zy2!9>6^xXTqfcokrv^cWK)Rey<_1#!{gkW;s6yB8krg4pD{X(#vAtuL+eveNeah3Op=F7&Hbp+)!l zqfZC5^n$empYxFuiS-5egX4pQBgZv070DKvdPsNoIP??toy`N?dNlBwLTM&p-}{be ztT=Pke_gNn){-qeEAR*z%M>?utZ0TUey}Gw7Ayt~Pf#zx;LZX-Dw%EAs^o@}%dUVfQyAonzJSjkVS~^gZ6O_5UNP((geI2n7aiXUrWZpO%_e(Xu`ocFA4C}rCk6RQq;TH0l?&&TKYpW;H z9qE|7xL}(wj-yD+il+SubaWXUX156ust}-qlYhbysoGcAUSnwV_3#sRQ5;po9l!=h zfdp$bj1*N0_<`CeOZbF0#nPjaHDlM5%y_70&W+O%cjbyvW?Q#DIVfJORh#&Rzx|uo zMSjdZ#EJHEpc(t@Wito876t1a;E>s4&@9mKP-EEt{qZRwM5I0Lv%e7el4`Ui1K%L) zR)=erxyK)6uBY)UN47hXB4=9knz~`C*M>ZH9-Lsymd6TFF@v#B5`=XQ3!V!-T_}bX z#>YJ=VLtf-DCI(Rukx|Ior>bla%meIW^3P?<@DPZ;$VRV3ZBh=b_0%XclzxprXb$Q zbYT|xfxwY+bBQ+kF(_Rcw93xSw$Q%O|i41Dv@kLKBmxVjcZ z0UR6PFFw{Q{LXj7Jo!+JXUaXklm?WfefgH3u?nG7=Q^eE$c+kb}G=dcV{2Q zi33VAq6?jKUW<|M<|_*AL5q@*Oq8fs)#BE43C@tyC z$-l=Jy(8k81f4Nr2;N2Hlg8NrXN_Ba842GE0;}M(3u8fCG&DvS91bS)Bga+}i5EE} zVn?J3zP$77FI&fe-}jMZs)!RFY?C0f9r#2g+|ONSo7iBtxrm0JI^^cwLWaL06qVI``mk`31BK?m<9+ccC6Y zF>qr~&#rmdPB6h&Z`$!02jL!@&-<{Kh$(TnRe&|0)CoF7%DHL=JS3z)7!(K03o1XY z+WZ&frkk2(Qg#<$F9bo0k`l(2Hw{cE#;FDHOz@DnPyu$^VYxLlH6>@=;YTVkGh=n6&7;Zo&QO=6%u3_F00K%%~7#ZwRXvF0396&vRyP+AjF0c;lVS*Bm zjfohJBWW7NMZRu2az|MJKtL%lj{;%_!f`ujz>Uqv+Z#_rrpPj@6_Jc^-@c3o2H98T z1+)}_C5GWiBk8>$#tM9&2-%=TyltLO#|;|ap_1Eg>|eQ>eOAnB)GnhCl)g_97tDgT`p?P zi&`3~OEO&=E*nZiTE>CF6q9cb@)PYft|aU`ZXZ8CD8~C&FS+~t%$XwPC)RR@>LYch z`jtrUrkD4B&?xHg%iFYc-{VKg$pO}qg-X=M-7=oU`U_dDlq95srr&5~uReeG=p;Y) zlU9pk>F3P9ZnxMD7PE(gG|5yhe0Ja%Tla(uc-x~*Oe8uE)FY@#2c34LQVkO6B=8T~ zX%b>$*%zRkMC1~mb7AH70L1-dL}U? zpH2A1#V*otZ9*ABc(Xa=qdC~`%}(4iDU|6iUcU5IJZEn&Wh#d)MX6m=#q)$ z@-wk0bHLG3Nu*!75@gBy4rWkxoxCX#{(eeEtl?|s^AIL82mQcp4>Wtk!va3#z|&-4 z!fg4F=e2qA*Wjls=skF26`fKID_mrwP=ys1$`stJn%{Nv=R0BrEZdFG@rYQvBnszp z42StMq;16?U%x(_)tXvEirD}U4P-^@mt83yv_4Q}Hx}sg=ND5vMN!H}>ujsa$@GYKvG!VrTyFQ+OWBlB)b<5^Im9<`_@5v05E=r5~ zP^i~XAou!#=KO)Q*_+x^Yjegp&AR3B9?vxer{Yt1l#u7M+#tC#B5fZlsp>xA`)-Oe z4WR(+-K^HltgIhw){t~BV@i-!UUNeO+*%@4_?4J+9#ww4_+WbEab2|4=_Bzlvm<{s z-~aMlYab=Y7TzxX=IXijavZ90=AZ86-Ye7<8PgM-$Nl@eIA_ri%xykan(rPNE5lg# zJXQ!48Xv>BNL~MmIJb<=<{fzCjy{<3Te5eXxMP&M*`ptZwL{AP_74ALKeJloz$XT1 zhyI<0Z}Zw~g;fH0N*%S{nTmlej+si4Dk+VXF3>7JYI;6uu#jETFyy%YZbq8ouL@cn zA{f-C=n6V_Y@Crc%9vd`kr6)DTvnoQaL`z4;pz#tqpjOQcLT1vgFv#1|6rQBRNukI zBIxt4+a^t@b7P}8Rp0T%U)p{NV@a;8_lk0){bK04lN1GJZL^Usr{q;zV zVb4}&OR->i3F^|X-Y;3J#brOZ!Fc5ux)^ zNi_dqGv7ndB0cwU(|PeIiG&_uCyB+El$6{JS$Fe^_51IB3VY%tG#&m- zUSbiv$vu6u3PJj`mnRO8f$G&wiBa0Ql^iBuT=L2w7 z>#+LS%Jk*?gv_tpSZ3!ux*5cSOMdW2m zThAn>s>a?Zx=%!meLG;q17PgTj-;iYv{?GUy}Wx*$Q0nekv`K8d|jiuxqNt7ZjOG+ z9pg#yTDC=D9nOC}y~yT)vP#|0&^01%fe*ND+0!KA2TUaflOI1YG(Dj!@90$9SA0!{ z8lCS~EaAXpkqeO%r1{_!m>-R8@yU0YIP4@?ZZ%x_>NVhk7En>k(hSfjyfm8Y3v zv~>_wVwS-!e3ooFCI?AnNM03YxUId-bvpIt4Cl&~D}62~0KhwOvGf|fI^xQ4!G+}q z(lC`+y2qSks$i8j13+R-^kDz5{q9j2Ei+LY+b6!A?Bd{$U0XjtTKUrh=#oOt((ve< zCHvDvXcuG4;`<=z(0|b0)|NANNMo&5>)Uv)^hRE~$J%c)PPQE%QgEWz`F{)+_j699Fv2T+9-4c*{t; zIxp8@J(I(<>`J`Fgi==^*1#CL3~{i{uS8aJtL#O1!C=PE(3glFYC&(_ryNk_PIhV> z*;{<`@!}zTpa1?Rb%heaPsdB#E$qWHSnegC)9~7{xRKJf8(kGL`d+w%^x5a#z(k8W zP0X=%2XWe;Gsq1 z8wOqJVCyV@hRmgnaXrh=ZuhWAdUH-kDfy|OKziX7EVySI7+G19aR-|!O~oK_PX749 zab?S%0;@p>xuThoI30=rkcOR9`m*OW`aQXJMIjCj8oRswOPW>)?77Ua{H!vKmf+M@ z>Ox$$@#~W8EtoNAnPwv$dI)DPqUu&tz8@uFzOyqe8(%b0m1gfYb-pl8f1=ZTJn&Hl z#cS8^FTI1ejOBVL!H!%vxJM++9!F zQ!J|aHr^tntqbE^_TW!x9|xR(w6oQMj{pi$Il!iQ;$ivG2nk_~#uLv6@CaRFHUXpt4d7K5YA=msIT+dgq4^~t}Zu_|BQTKU_#|Dv9ON0 z>Cbt{lH_7!J4>_J=$=v=H~)L_@{j$V-@m4Pd3*B59~hJkpSXw7`MoHI;gJ1im?suV&R@meooOzVcNh8t9OmtVXLI%_X}o2Q=`}dj-LP z#XVWEcVZf|GoPpuV~)|FM}Er#!N6vz;{gxRxhCL6<1A-%08kUFB<_}%_W+8kk~j{N z7T|d5#>U^E!k=)tjA99FQUJNY9nnL;(73(Pr#o%Jj7ffr~`675LhJPbi88qCQuyGG|kaQcXu!858VG5A(6}jPbnW? zM?2{rL6~WcUiz_R+O@{XPIB`!G8fE=kcjp$##1{fXx5Lz{Xf)2eY&Hf8|S?B-($M~8HAOg1(E=~^c zi-k}L#}U|DI^5bhW{tpgP!FOR^qd1$<`w1T-@bko*z2td6Z};S40?Dx=+^0qG~-Kq z3!s4p_;=5q8Njn!V+rzYVWA5qnG%LPXx)mNF$3O3cNcRE36K{(eqf7}a3f7h5`e`p z@GF4PqmAXf#C>K3Mhr1JK_3YSU}9lu1~Y@(>{H1ch}DjGc5s=2a~8U1n)g97Fa}eL zFt>zrJxWDM)YLEq*e1W#bCnLGywSUkgyZWOH)w7guKJ|fqD_rwr0@-s2wtW%7S`>G zw%qY({-7-{cnBTFHyUA55$))~0Jls_O}$OQ(*m*y7EC6Z9U)7SU}`#F{V7^D(pTuy#7f5N*RFm4i8GFP zmHHftvX(KGeSi32dRrK^I^9&8Y zp#Kic#tvAMQRP{Q2vA#uH^RUGi+XUVUm4vYR^M+SUS30G{rzU+qUe!Y@`{ zM8waMeZU_uoH+gl70{3)(a*cVW_A47nG_bnFqjp6_~X7}aTE##@ErgJ5TgzSJOuP{ z9^Cnu;^wV))49c2T%s@nJ>jHz_k-#YF%4xC`s%9VGE zT4Du~B^>jaushJu#l@V?(ERA#VwER2(>F%A6qgy^tw7)R#kAoQ|gF(7}Bva(>6AhCsK0`b7U$i#01( z_I)aqZ#(k5Yjm9&nY^rskFSkQ5-#Z%({^{8NV58_x%m=K)@5akYPcxGZ2_M_hHo7H z{AlL@eE^LzJ3B~klQ2jPSl>`M22R^4N5kIZcGGh(ewKAS+a9)+V^#f4 zMm6_?^Rf(z^>Cd}5FME(k!i1vJD(}#tLs>=-z+nO=;+|{k4L1u=eHc#o zfvPlzwnHVOACPQ9HUMF`ps;Yq6C-*?tZ;PLRKmqBU^|x1PYW`#%e#YL*oi%*$%$Tl ztN>viH`alEFib{9kagqyMd*B8T|EcrmtW<+6r?!-(}+=;u+Rl#rXRh*Ng$;}Uk0rT zC?FBwgoTfdCYfP&&QjXdgtP*5)(WhLq;m^^^9f89txYIxgJH)REl2kb+( zec}bB29_qlTXDaL<`htfHM?N=1)T7qOHeO*x_DTDBbCwI^6~~B+44X|g=1Gx&klS` zbm_d^oPe$2~WgQ^#fYFJt+%I*Utf&YgKsSKa?LtDg{rw56bb?;!FuCo9 zW-q8rAI4`-s{#K(C=~PJ>x%D?`{2YM8(MmK!di#LJV!hCw}bhRxK9yo~7_AeA&qr+r4`u+y2$$APu+Pl&jmw*XSdudBC#`*Qp?YrvY4H zQc||Y9#WJQi>*lflZd(Omv!Fha!d>*QA z)>#$m+mW<3HsLVVWgL7zNm{oFur~opJMovweGOM2R+dNJx0U|Qu(blnihamQ03;;R zyHs>Hmrb17G3ruGzUCk{zmJM0ih7!FU}H-`iK&UGbO=Tcu}O_)8vML@ZRij1C>2`w z{xJHvaFwBCb|}T;Q|>iTG%oIodL?p*6x|lV{-3~tzY$7C8FYj7A$cU1rAtuuf<+ z{dE7AhpMh*gfENc#*WFKN+#wtKq_>*sZ}v4eJ@eWl1LOdv-(#XkGm9mas9CPbZ%zC13E*cq{ZX+^1(mqdQ+Js;UwMUtuS_PTRqjZ9@$*%VEg7f5! zg`o|uX)0TvJeQOdIdyN6hVepgbqbAIgq9-*T99req~R7jb3f9LoKr>!-6UF9xfGL}>??7>|uzz)?YC-W7f{;ql`g7Rh)w zywa|@1QIjvJiooiS^!sz%01f!J_?G)gK`JX66xy@`rZ)b)=yd5)W3gE@cHW#RTrEb zFV2`YR|N2Q6BasXD={!=)KcG+WU5A8?*Q61Y#>TlsKz}z7?AG1d+-u9rRe<$1I$UW z&aW0ZdHncsjG4QCAI7i`YTV$Ma^`FZa+@<}HD3xwG2VsGJgYC7YR4rTsYR2gMB3fZ zoy4bv5`)k{1vsp#YT|?%23s6D>#rF-IV!W4Km2Bw3DG96AQnD<$N^r zK4~3bzGD+5scwxZ^-A}ddxi?)Pw!@zDf9xXB51vL;XjEBJ_8Qypy3l1VA$Is)I~;$ zw6_1P9px-UBzhP|r2bPu`roUTc0?YNa3G)Dgiu&)Mb*?tezLfYzGiYKU zxn-Nn5pAi_3Jv`4^O66MvWPu4Z+uO^c&bxxv#G|rKcnjKche#A1pY634PW4!Gdn2U zDh(ve47a-ZQVrGV8tN~QmaJg~0Y2@PW{R)3HwZ$|_7xU>u*yp@Aw6@(@)8kIVJ-sz z27w#;Y*CB1Yn=e`F{TH(xw!$7A!I?oW1yv#=tmQ?6Kpid8i3qDf1sJ7!M>OdEL%o} zM{l8b4;BV0E~xrac9Xt?N6ap5_sTF65QcXwXmJU@KjPhFrw{PVwEbi%jsf@7cje3T z#QJ=MF9RtU{aiNmdpVA=%n#i{~xLwCmqe zl81FMaUEid0dPsaCXuIa`6Po)Lq?sWxq<-@9ibZmB~WXH?_U6+)YTh+(gt5eJvE40 zLigm!z@Q+o;I?p2V`fh&WIb@N?G_`U+Zh@j-Vgh+4$w-T!d;Ne%)v1aUjD&Dhrm3H zIEeBhYPX3B>Pob=2|@!wKUr&+34@mJU`IfPnS7NNbM20hp<+>6p$iEY2+hm~(=^+9 zm9}}-YE`fKyC?ha3vRJl`~R2M>OX!gE|)T=#Qgo;*k%pyptRm^=8Z2`Z75?){<)FaW~~ zaAM#bc%toie*+8tP~k3Y8s;)Z4;!x`HwS4dyZV2Q>(n0sY?%KA8eV$nSvdR?VFbc{ zzhb0wFT_k>_BkWy0Bl2*OI1g8+cB`qh=EyESQwu6X6Su<70v=)C2%qx^C*$+K@?*1 zEr+lOgr+D8)B5#bvsz(770lptYm$}dnD_!8b*gX$@DK*&8*dpQs9BIx8WSEqwD^#X z?g=Qb8O>}PHfZSTx^>#JR7AU50YPXR0$_5VlZcZzC?r-O`|;ce+WR7^+@QWT^i6H8 z|E*gosi{Egq+BO0@N#!|NA2tA=y?7-593NN8?gGo0*}H}QJ?}39y}mtL#~55a2onP z_joIBNT#*$I>XQDK+PaQR53#%4jZKn4bZz2RvXN05niAcp4y|KP5CK=3fyQ z9$slhjH<*a5%|Wch#tZ-LBvMlwAflCEelZmD2r@TD^S1(JaO@R_wPT2_!UaS?>MW0 z)39)IsuEmmp#MSEJv}{HE5TDttOtcToI(I4bY5S27hYFZa>x4ktO8v^jJ0v+v+IC} zuufP+_1(bb!sh;pHLX#be;gG#R3gN%*l6LE)*|;g*TYND*=aF9-KRs;MElx8qN5>5 z;ACZm1JU@{7!wl{+{nw>v8@7SV z1>quQiEjH@t^uLeKs3aX;ARfNB|lP6sk%*^+KMHa+P1)-E9N>vVX=M&E2gvI^~1yo zBfFE483B`rW)LJcwD9;*xOaEIS&syL_ScpzTTUg>h1MEof;|l#MJNT{mCGK~m9AT- zW??Z%c<@8y2_^TbNIi*~VT>SnKIE8kXfb4d*d_FI3CZ-VW09P!tgf6gnGCHhq6=vG z?s3K2Ph&(OcFx(eTNn`;5l*9XTJ9mB_*c{eNakjhvfFy?E2#^wnnk@N6 z;6K1Gh+(q>6QKGMfQ`2)a%Xs*KH;JR5eS3(Vq!jnsn53K%s!}Mprcvt_!*!lA)=$_ z7~r;T>a2t!U%;otJ%)OTqz?krgCeTYY^c3p9AIa|6|X1JDLxp`E0p1URF!y{kiKn5TpaaP;H;%hzW#A0g*I^_O2vhv`3Smz9?%-*l@t%Yha`20$R#yfRBYZxNXr zwj2;9sNx;U(FTiy-d_zsqHk2tl3#9(ga4dR|8A8#rjZ-@yn%OQSsH$hAN(-5^Kz$Y9s&606Wh|8arwDpN#Xs!o8bRT+I&SX zMNvNqlcgKs!Uj-nH#7&g%3^Cs&(^A`so_)>-nI>sJ^L4{z?h+zd$C;74pL31;5db$ zw$2!Wl-|7TbK>L2kFnFhm4zNY#&G*C>iEyzU+4!@QH`fyUjW&$1}JPMxAGAEYjj zb*rhVM?6Ru@1hsG1tb!!mh57hLHvm6&|^LT&B41OszH@uwvDCz{$|K=&t^aHlN=To z**cqdT=+q~Y;DVtzUnH9-DByg3k&{14N|JIK-kg4EF>KHf4VOO) zg_XfE%#?Kzj-b&^0sU{{4(DPzgk1#ZXrLJlJ4s@oh(PGd3mYNWl3|Ck&tS>^fj!vd zwk`iYmE8Id)z{zWI{l!wH&`FepStZHDF!VO1$~a6+hF}40@5Eg;xF(o2y36nNIiZ1 zbUaW=%pZe+06rD7E?G8g(EFfzM>B<&8(5i|#gFEaGkXm5{$KDfD$D6vy2e&fb)X3S zwT}4weUi<+GqZnnkZr!?fFQ|RGi=Y)L`!cn6Bdf*?@#R`}8JM^v;(Dxsej|*4 zJ7#fZ(eh*;mjclc=q`Kp3Vl^gb@imHv;l#EEJCMZHe-uA_diMu+FBwDoeL!A{c7CY zgw-rGed<^7UrW9nxC8DBNa!kZBe3v0+lB59H1wxR+zMo~K*5GseHIatDE|>eD!W37 zlMQah{dKg+SMkDf&iI`Je~?lmbiP`8NQrQ_kP=~rbqR*R&i0+3dSc&qqNpRCW5N<` zAjnFP_o%H{_XE8GEE&wXjNOc~6}geYLq3fCXT_yjOS)n#{NJ#-HhfvkX_@t z%*Dcyn;}sMCp@}aQ5-1+;PivWMwX*?y{c@c+te89QO2!`t69as-zVI^(GECOXuplf zGoYpB<>6^)Y$QwuAjuqtK*8Sr2lPQ`1EtRgt!qEZck-o2=KmoFN=Lb0plh53O^)WD z$A6mo_;*}=4n=)D2QRn0OaARvi}d7VZ5&$YWD^Gk)RAaFK#)YBE10?tjR&Uh60W=v zn<3stlY?-PTyygr{sVkOM^PTa7`5=}Q#Z_mA$}4X5=>afc$-UMmnG>A|KGBqf~GeT zt+nu_01KO1jRSw;dQOBo;iJq4Z@zrMdKBvT$7ilYvt0!(6HaaG5gi%ZT61yjkof>K z4B#PdF7oyuekWue;LPJl-RlE!iIAu$Kh97?3x9ey$D+%~P;aq_mLS`NaBU3FJ=sJz z2}cLf=!QN+bfG)uw2k{w0I>%dJ<_Rx#?#Y_MH*F2lwFsa!aB-p$b*5UAU7mGtTPR= zz9zY8wOv;_A)S}=pDU;ga{$xsH*?>VQtDU`G2hKuXmud*FJ?p14VU2bxC@S{mQxa z_53Pmx64ATqn(xuSxZSjGr<8kZ(@QLRvf$*u|#o{YixG90=*P5W&{Vros1CLXVAxq z^70zNoY$pz9=r%bD9`8%0zcdgHZD^@xs#Bv5%xBate_Q0pgwTlc(%q3W){dJ9iE@l zd&uXSPe!K}1&rI=zQvf8O@*|wpP~&3RA;7F2B@A)iLq#C@wAchSFZ9n&9Ca_Q z)3^FUVrkX-%TQsF}~(stFMCb6b%0x*g&9_>F{nJj;7hEam4VKreY}7vw%&K z8}OFclM|~wH7`H}3+lM=#v?fSk=Xnmf@j)AO&YqLWuuS)Lv;y|ugo~>QJ1k%Rx9Ra zU@Qx>!a4{Ut2Xv5S%%D>@Nk6i>Q-_4U(~&{Z{Q{oxl_*MXgkPsz}p^2pMCtNO+*d^ zm%HRcG#@Z!k(fM!RRhx^hsC7g-6TOZd#8;PuuwN0Qd0=ddcJ+r?<*@H(ZIWcA7(X* ziXo6lX=)~)qkfX7Cjorz6Ew`xhT#!(TQ^q!^~JFWXk5#NH>8*?N&&YO&t19%yqHK} zkQ0DYth$&f(J@nlvj~zhe8q(I7~Y;qiq?-AOHUsu1w0-26GU`Y0L*R>kMZ8ufPGd z!VBINc9iz20+9lmZf?_i-f2TWK z80SKJbqfK09ZYG2Beyi{9HIb};q_m^uc0=R-`O5wHMEWZzmOO=EqwaeF?ZOx$zyZr zx3drEw{htRTvvKXqb#(xHMiwq-Sj3-?J~0;ukVS~ZHn`_GP}GHt|&)$G+k}_aahju z_PtG}$qKQ0lp6`leU1qTo#`8UTJ*N2=IzLt_j7v1MXo~{5A1f2yReH5&V|K)vn&kB zcxg9fXNzUkOD|GDGO|U(BNcv$*ypIlg{+>v!0skg#e{)B?F<~%+^x|G)qXAma09GyLnf0F<8eC-#0`c@c*5Wc)_ zto!LYp@T*h=QMiZ2HG}8^BsSkik@lj(Fa_hdb$5(S?5xhZ($~gK^kt;dm$ySf##MF z$Zp1G4cnBciaZrEBkq{^qj`TTR70%n>~bFSZnpTC{6GyDcZ{}j zL0PornpvZ3AWNdg-n6j^+Z0%TXZ$jy0dD^ZcM71s&Z9xvx!u$1ih*@deiEEwg{ zH~`zNZD^A4g3SBqB6#gP%%R*NsoHW4O@k3=mx*a*gsBs55+;rHq3I8)@Go#Kwg*=d zjwnbhiLejg!3e= zsj>>pXkk!by1KY41qHM|u*#?jm*qgeH2heG2mleh=^8VKnS=j zEK^1W#1MrL1QkLMBPG1@-0z~bwnocth8f5YB)R`R_dLJz9c?>^FbKm4`J|+u9#mmf zHa)a1!a^!PW-I9-l$-GGQCp}D4Fw<2=jfG|^+5qB+}H8-L#pg%7Xfpuq<;Jk?abOJ z){x*~HQ2tV zha`01$%D)2OH?rkXBqLdqhNc%rdg$x7T;=rfxcb|q=t0k!>8}S7YZpzuC}uKhO|I? zwFAqNLVtl|S%(VsiP9k|Ip9+fZz|w*FBWXY zOMn0*dKrk#puzp9__uR8Ip_%jT@&e46o7ISIh<(T6FVeDGb+`mjf12^eN@Q|(in&m zn2Wscic@lhxCK-&KyWIO`t3@J1DD>|HPgSCReS%(MVonQXs_o6QcCZthP3ecr_&Zk zmwJKQ5vepQ#|q5GB4ijqr=c|(gWro3IR|(=9_U*J`amwYA2ZwF1K(>Xc4hCHBDb!j#9YLqpH!F!>+a14}6Iz*R1+#Hi3 zo^wweAOjY|)R$Q>!ANY`$1C&cz9N=kh5GL)XJoo`n69_ri#Aa)=!>_-I~D^J|U$ACMb z=jW?egTuvZDZkgBgOWgyx?Gb+|HR43geK&>I?s;I?9(IdrC#{!TwRMjl!GDE$PKo5 zxjY6^IddV?MvzAL{n*Z~4YwQ4Pq&0|cPpkCf(eE0`w-wJxNq3hwDffMZKaLiF~3F!ImS52$S@>qRrSWwb3G+(Or(RycJOC8-I1QeI zc76`64VeV*GkQyET~n?-)Wry;s+yXWm>4CYagp3}ObHB$dnV)}J*rPjphA?Z6|7;;=3>?2`P{d1& zK!?O&4w|GlKYbnxd}JVeGbqvtodRSTSl~f4bb{UjK@*9i^(Mhzz8T2i=)-Vl@oAAm zCbb8`FnC9o-uukIg2ugvbI{m$Jerj9G_#4}Q6sVi`2nwABs=Oizq02)4M?++k%Vb< zYapRV_TaEUC&t(tVG3!Es676FBQ9}5-g2Mb_z0k4kFTY<%%W`v^Th|3Tf*8ruKw_gR-{USLEl%BB*v)1YBPINRyQuBfHX;CD^c2Q$6 zSBM&#zj3{ZudexHM3!V~(RZ>u=`?~Gh7sYt{1-tBBb;9l7vq-M0i-j~Cjc*?#{rDi z+s+PZjNijTSO)K#rs`wFTUFlNQS2jp{^pe!!*fdNLz0<~A{$*2tXN+ahhAe#`-Vn% z$*HJvFIi%<{K!y8t1enh4@E;BD`I+v;f_K4T5I;@iC7Pyx#5pjf_@P0J`xJhIRM{x zWgMdWQlWwl9y|!;n6(a<7ouQhE?ty*iZo!U6JFyBdGCU^8khwL6k8Hut8`3mLZkwc|#XFE0!!F5Y_MAEP$Tm*7)? zr=zvJ+i?Ct9hUazq95K^;-8H35Lc*eYz)xv1=Z4LWl)Ep6jvZE>_xvbfH`MHKs^3WgHLn79~L&glfK1GcP91sdZwxjLT#lE+;T z;O4wD5Hg=AM!^;|V5(2H zHKRJNIF60WlhD|+0TdO})+9Ogf8uxmns(!PaJF_TqAGBlGZ&o)Ex73V>_F?}Gzl}^1s~U9U$hZ~D_r8KnC%;+oYLDc zx8yyl?N@h-s6|l~d#Hy$hipNf0InDLAv4$CjeKA62zGCR7lJP}Mc4?ePHwn70~#~9 z?-`%Io-H{F1DrNag3}H&6$I@^XGl5_mg*>$pgBSkq(ON&<-gXQznPf?m=N zdRvhX-w1<%GlElc?-h4>-t&oHxcd5FU2gu3;})S?YlhiS=$+N|shK2;I{E~VnS&qo zRf!($PNUC?_tD}6n=wKY+9|Tj{c5HmPB5NpQaZh5WXSR+a-@Fzmq4=yheUPnEamM> z6EH8oZZgbl>o8Yntk^hS^V7`u-;dOrQz%3j-F3qU9m&2_X<^22*DqsM^Q+UAOHCGY Wyn7vbfzlwia_y=OE3f)~dHBC$=2n~l diff --git a/cuda_malloc.py b/cuda_malloc.py deleted file mode 100644 index eb2857c5f..000000000 --- a/cuda_malloc.py +++ /dev/null @@ -1,90 +0,0 @@ -import os -import importlib.util -from comfy.cli_args import args -import subprocess - -#Can't use pytorch to get the GPU names because the cuda malloc has to be set before the first import. -def get_gpu_names(): - if os.name == 'nt': - import ctypes - - # Define necessary C structures and types - class DISPLAY_DEVICEA(ctypes.Structure): - _fields_ = [ - ('cb', ctypes.c_ulong), - ('DeviceName', ctypes.c_char * 32), - ('DeviceString', ctypes.c_char * 128), - ('StateFlags', ctypes.c_ulong), - ('DeviceID', ctypes.c_char * 128), - ('DeviceKey', ctypes.c_char * 128) - ] - - # Load user32.dll - user32 = ctypes.windll.user32 - - # Call EnumDisplayDevicesA - def enum_display_devices(): - device_info = DISPLAY_DEVICEA() - device_info.cb = ctypes.sizeof(device_info) - device_index = 0 - gpu_names = set() - - while user32.EnumDisplayDevicesA(None, device_index, ctypes.byref(device_info), 0): - device_index += 1 - gpu_names.add(device_info.DeviceString.decode('utf-8')) - return gpu_names - return enum_display_devices() - else: - gpu_names = set() - out = subprocess.check_output(['nvidia-smi', '-L']) - for l in out.split(b'\n'): - if len(l) > 0: - gpu_names.add(l.decode('utf-8').split(' (UUID')[0]) - return gpu_names - -blacklist = {"GeForce GTX TITAN X", "GeForce GTX 980", "GeForce GTX 970", "GeForce GTX 960", "GeForce GTX 950", "GeForce 945M", - "GeForce 940M", "GeForce 930M", "GeForce 920M", "GeForce 910M", "GeForce GTX 750", "GeForce GTX 745", "Quadro K620", - "Quadro K1200", "Quadro K2200", "Quadro M500", "Quadro M520", "Quadro M600", "Quadro M620", "Quadro M1000", - "Quadro M1200", "Quadro M2000", "Quadro M2200", "Quadro M3000", "Quadro M4000", "Quadro M5000", "Quadro M5500", "Quadro M6000", - "GeForce MX110", "GeForce MX130", "GeForce 830M", "GeForce 840M", "GeForce GTX 850M", "GeForce GTX 860M", - "GeForce GTX 1650", "GeForce GTX 1630", "Tesla M4", "Tesla M6", "Tesla M10", "Tesla M40", "Tesla M60" - } - -def cuda_malloc_supported(): - try: - names = get_gpu_names() - except: - names = set() - for x in names: - if "NVIDIA" in x: - for b in blacklist: - if b in x: - return False - return True - - -if not args.cuda_malloc: - try: - version = "" - torch_spec = importlib.util.find_spec("torch") - for folder in torch_spec.submodule_search_locations: - ver_file = os.path.join(folder, "version.py") - if os.path.isfile(ver_file): - spec = importlib.util.spec_from_file_location("torch_version_import", ver_file) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - version = module.__version__ - if int(version[0]) >= 2: #enable by default for torch version 2.0 and up - args.cuda_malloc = cuda_malloc_supported() - except: - pass - - -if args.cuda_malloc and not args.disable_cuda_malloc: - env_var = os.environ.get('PYTORCH_CUDA_ALLOC_CONF', None) - if env_var is None: - env_var = "backend:cudaMallocAsync" - else: - env_var += ",backend:cudaMallocAsync" - - os.environ['PYTORCH_CUDA_ALLOC_CONF'] = env_var diff --git a/custom_nodes/example_node.py.example b/custom_nodes/example_node.py.example deleted file mode 100644 index 72ca3688c..000000000 --- a/custom_nodes/example_node.py.example +++ /dev/null @@ -1,128 +0,0 @@ -class Example: - """ - A example node - - Class methods - ------------- - INPUT_TYPES (dict): - Tell the main program input parameters of nodes. - IS_CHANGED: - optional method to control when the node is re executed. - - Attributes - ---------- - RETURN_TYPES (`tuple`): - The type of each element in the output tuple. - RETURN_NAMES (`tuple`): - Optional: The name of each output in the output tuple. - FUNCTION (`str`): - The name of the entry-point method. For example, if `FUNCTION = "execute"` then it will run Example().execute() - OUTPUT_NODE ([`bool`]): - If this node is an output node that outputs a result/image from the graph. The SaveImage node is an example. - The backend iterates on these output nodes and tries to execute all their parents if their parent graph is properly connected. - Assumed to be False if not present. - CATEGORY (`str`): - The category the node should appear in the UI. - execute(s) -> tuple || None: - The entry point method. The name of this method must be the same as the value of property `FUNCTION`. - For example, if `FUNCTION = "execute"` then this method's name must be `execute`, if `FUNCTION = "foo"` then it must be `foo`. - """ - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(s): - """ - Return a dictionary which contains config for all input fields. - Some types (string): "MODEL", "VAE", "CLIP", "CONDITIONING", "LATENT", "IMAGE", "INT", "STRING", "FLOAT". - Input types "INT", "STRING" or "FLOAT" are special values for fields on the node. - The type can be a list for selection. - - Returns: `dict`: - - Key input_fields_group (`string`): Can be either required, hidden or optional. A node class must have property `required` - - Value input_fields (`dict`): Contains input fields config: - * Key field_name (`string`): Name of a entry-point method's argument - * Value field_config (`tuple`): - + First value is a string indicate the type of field or a list for selection. - + Second value is a config for type "INT", "STRING" or "FLOAT". - """ - return { - "required": { - "image": ("IMAGE",), - "int_field": ("INT", { - "default": 0, - "min": 0, #Minimum value - "max": 4096, #Maximum value - "step": 64, #Slider's step - "display": "number" # Cosmetic only: display as "number" or "slider" - }), - "float_field": ("FLOAT", { - "default": 1.0, - "min": 0.0, - "max": 10.0, - "step": 0.01, - "round": 0.001, #The value representing the precision to round to, will be set to the step value by default. Can be set to False to disable rounding. - "display": "number"}), - "print_to_screen": (["enable", "disable"],), - "string_field": ("STRING", { - "multiline": False, #True if you want the field to look like the one on the ClipTextEncode node - "default": "Hello World!" - }), - }, - } - - RETURN_TYPES = ("IMAGE",) - #RETURN_NAMES = ("image_output_name",) - - FUNCTION = "test" - - #OUTPUT_NODE = False - - CATEGORY = "Example" - - def test(self, image, string_field, int_field, float_field, print_to_screen): - if print_to_screen == "enable": - print(f"""Your input contains: - string_field aka input text: {string_field} - int_field: {int_field} - float_field: {float_field} - """) - #do some processing on the image, in this example I just invert it - image = 1.0 - image - return (image,) - - """ - The node will always be re executed if any of the inputs change but - this method can be used to force the node to execute again even when the inputs don't change. - You can make this node return a number or a string. This value will be compared to the one returned the last time the node was - executed, if it is different the node will be executed again. - This method is used in the core repo for the LoadImage node where they return the image hash as a string, if the image hash - changes between executions the LoadImage node is executed again. - """ - #@classmethod - #def IS_CHANGED(s, image, string_field, int_field, float_field, print_to_screen): - # return "" - -# Set the web directory, any .js file in that directory will be loaded by the frontend as a frontend extension -# WEB_DIRECTORY = "./somejs" - - -# Add custom API routes, using router -from aiohttp import web -from server import PromptServer - -@PromptServer.instance.routes.get("/hello") -async def get_hello(request): - return web.json_response("hello") - - -# A dictionary that contains all nodes you want to export with their names -# NOTE: names should be globally unique -NODE_CLASS_MAPPINGS = { - "Example": Example -} - -# A dictionary that contains the friendly/humanly readable titles for the nodes -NODE_DISPLAY_NAME_MAPPINGS = { - "Example": "Example Node" -} diff --git a/custom_nodes/websocket_image_save.py b/custom_nodes/websocket_image_save.py deleted file mode 100644 index 5aa573642..000000000 --- a/custom_nodes/websocket_image_save.py +++ /dev/null @@ -1,45 +0,0 @@ -from PIL import Image, ImageOps -from io import BytesIO -import numpy as np -import struct -import comfy.utils -import time - -#You can use this node to save full size images through the websocket, the -#images will be sent in exactly the same format as the image previews: as -#binary images on the websocket with a 8 byte header indicating the type -#of binary message (first 4 bytes) and the image format (next 4 bytes). - -#Note that no metadata will be put in the images saved with this node. - -class SaveImageWebsocket: - @classmethod - def INPUT_TYPES(s): - return {"required": - {"images": ("IMAGE", ),} - } - - RETURN_TYPES = () - FUNCTION = "save_images" - - OUTPUT_NODE = True - - CATEGORY = "api/image" - - def save_images(self, images): - pbar = comfy.utils.ProgressBar(images.shape[0]) - step = 0 - for image in images: - i = 255. * image.cpu().numpy() - img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) - pbar.update_absolute(step, images.shape[0], ("PNG", img, None)) - step += 1 - - return {} - - def IS_CHANGED(s, images): - return time.time() - -NODE_CLASS_MAPPINGS = { - "SaveImageWebsocket": SaveImageWebsocket, -} diff --git a/execution.py b/execution.py deleted file mode 100644 index d207e1b9e..000000000 --- a/execution.py +++ /dev/null @@ -1,869 +0,0 @@ -import sys -import copy -import logging -import threading -import heapq -import time -import traceback -import inspect -from typing import List, Literal, NamedTuple, Optional - -import torch -import nodes - -import comfy.model_management - -def get_input_data(inputs, class_def, unique_id, outputs={}, prompt={}, extra_data={}): - valid_inputs = class_def.INPUT_TYPES() - input_data_all = {} - for x in inputs: - input_data = inputs[x] - if isinstance(input_data, list): - input_unique_id = input_data[0] - output_index = input_data[1] - if input_unique_id not in outputs: - input_data_all[x] = (None,) - continue - obj = outputs[input_unique_id][output_index] - input_data_all[x] = obj - else: - if ("required" in valid_inputs and x in valid_inputs["required"]) or ("optional" in valid_inputs and x in valid_inputs["optional"]): - input_data_all[x] = [input_data] - - if "hidden" in valid_inputs: - h = valid_inputs["hidden"] - for x in h: - if h[x] == "PROMPT": - input_data_all[x] = [prompt] - if h[x] == "EXTRA_PNGINFO": - input_data_all[x] = [extra_data.get('extra_pnginfo', None)] - if h[x] == "UNIQUE_ID": - input_data_all[x] = [unique_id] - return input_data_all - -def map_node_over_list(obj, input_data_all, func, allow_interrupt=False): - # check if node wants the lists - input_is_list = False - if hasattr(obj, "INPUT_IS_LIST"): - input_is_list = obj.INPUT_IS_LIST - - if len(input_data_all) == 0: - max_len_input = 0 - else: - max_len_input = max([len(x) for x in input_data_all.values()]) - - # get a slice of inputs, repeat last input when list isn't long enough - def slice_dict(d, i): - d_new = dict() - for k,v in d.items(): - d_new[k] = v[i if len(v) > i else -1] - return d_new - - results = [] - if input_is_list: - if allow_interrupt: - nodes.before_node_execution() - results.append(getattr(obj, func)(**input_data_all)) - elif max_len_input == 0: - if allow_interrupt: - nodes.before_node_execution() - results.append(getattr(obj, func)()) - else: - for i in range(max_len_input): - if allow_interrupt: - nodes.before_node_execution() - results.append(getattr(obj, func)(**slice_dict(input_data_all, i))) - return results - -def get_output_data(obj, input_data_all): - - results = [] - uis = [] - return_values = map_node_over_list(obj, input_data_all, obj.FUNCTION, allow_interrupt=True) - - for r in return_values: - if isinstance(r, dict): - if 'ui' in r: - uis.append(r['ui']) - if 'result' in r: - results.append(r['result']) - else: - results.append(r) - - output = [] - if len(results) > 0: - # check which outputs need concatenating - output_is_list = [False] * len(results[0]) - if hasattr(obj, "OUTPUT_IS_LIST"): - output_is_list = obj.OUTPUT_IS_LIST - - # merge node execution results - for i, is_list in zip(range(len(results[0])), output_is_list): - if is_list: - output.append([x for o in results for x in o[i]]) - else: - output.append([o[i] for o in results]) - - ui = dict() - if len(uis) > 0: - ui = {k: [y for x in uis for y in x[k]] for k in uis[0].keys()} - return output, ui - -def format_value(x): - if x is None: - return None - elif isinstance(x, (int, float, bool, str)): - return x - else: - return str(x) - -def recursive_execute(server, prompt, outputs, current_item, extra_data, executed, prompt_id, outputs_ui, object_storage): - unique_id = current_item - inputs = prompt[unique_id]['inputs'] - class_type = prompt[unique_id]['class_type'] - class_def = nodes.NODE_CLASS_MAPPINGS[class_type] - if unique_id in outputs: - return (True, None, None) - - for x in inputs: - input_data = inputs[x] - - if isinstance(input_data, list): - input_unique_id = input_data[0] - output_index = input_data[1] - if input_unique_id not in outputs: - result = recursive_execute(server, prompt, outputs, input_unique_id, extra_data, executed, prompt_id, outputs_ui, object_storage) - if result[0] is not True: - # Another node failed further upstream - return result - - input_data_all = None - try: - input_data_all = get_input_data(inputs, class_def, unique_id, outputs, prompt, extra_data) - if server.client_id is not None: - server.last_node_id = unique_id - server.send_sync("executing", { "node": unique_id, "prompt_id": prompt_id }, server.client_id) - - obj = object_storage.get((unique_id, class_type), None) - if obj is None: - obj = class_def() - object_storage[(unique_id, class_type)] = obj - - output_data, output_ui = get_output_data(obj, input_data_all) - outputs[unique_id] = output_data - if len(output_ui) > 0: - outputs_ui[unique_id] = output_ui - if server.client_id is not None: - server.send_sync("executed", { "node": unique_id, "output": output_ui, "prompt_id": prompt_id }, server.client_id) - except comfy.model_management.InterruptProcessingException as iex: - logging.info("Processing interrupted") - - # skip formatting inputs/outputs - error_details = { - "node_id": unique_id, - } - - return (False, error_details, iex) - except Exception as ex: - typ, _, tb = sys.exc_info() - exception_type = full_type_name(typ) - input_data_formatted = {} - if input_data_all is not None: - input_data_formatted = {} - for name, inputs in input_data_all.items(): - input_data_formatted[name] = [format_value(x) for x in inputs] - - output_data_formatted = {} - for node_id, node_outputs in outputs.items(): - output_data_formatted[node_id] = [[format_value(x) for x in l] for l in node_outputs] - - logging.error(f"!!! Exception during processing!!! {ex}") - logging.error(traceback.format_exc()) - - error_details = { - "node_id": unique_id, - "exception_message": str(ex), - "exception_type": exception_type, - "traceback": traceback.format_tb(tb), - "current_inputs": input_data_formatted, - "current_outputs": output_data_formatted - } - - if isinstance(ex, comfy.model_management.OOM_EXCEPTION): - logging.error("Got an OOM, unloading all loaded models.") - comfy.model_management.unload_all_models() - - return (False, error_details, ex) - - executed.add(unique_id) - - return (True, None, None) - -def recursive_will_execute(prompt, outputs, current_item, memo={}): - unique_id = current_item - - if unique_id in memo: - return memo[unique_id] - - inputs = prompt[unique_id]['inputs'] - will_execute = [] - if unique_id in outputs: - return [] - - for x in inputs: - input_data = inputs[x] - if isinstance(input_data, list): - input_unique_id = input_data[0] - output_index = input_data[1] - if input_unique_id not in outputs: - will_execute += recursive_will_execute(prompt, outputs, input_unique_id, memo) - - memo[unique_id] = will_execute + [unique_id] - return memo[unique_id] - -def recursive_output_delete_if_changed(prompt, old_prompt, outputs, current_item): - unique_id = current_item - inputs = prompt[unique_id]['inputs'] - class_type = prompt[unique_id]['class_type'] - class_def = nodes.NODE_CLASS_MAPPINGS[class_type] - - is_changed_old = '' - is_changed = '' - to_delete = False - if hasattr(class_def, 'IS_CHANGED'): - if unique_id in old_prompt and 'is_changed' in old_prompt[unique_id]: - is_changed_old = old_prompt[unique_id]['is_changed'] - if 'is_changed' not in prompt[unique_id]: - input_data_all = get_input_data(inputs, class_def, unique_id, outputs) - if input_data_all is not None: - try: - #is_changed = class_def.IS_CHANGED(**input_data_all) - is_changed = map_node_over_list(class_def, input_data_all, "IS_CHANGED") - prompt[unique_id]['is_changed'] = is_changed - except: - to_delete = True - else: - is_changed = prompt[unique_id]['is_changed'] - - if unique_id not in outputs: - return True - - if not to_delete: - if is_changed != is_changed_old: - to_delete = True - elif unique_id not in old_prompt: - to_delete = True - elif class_type != old_prompt[unique_id]['class_type']: - to_delete = True - elif inputs == old_prompt[unique_id]['inputs']: - for x in inputs: - input_data = inputs[x] - - if isinstance(input_data, list): - input_unique_id = input_data[0] - output_index = input_data[1] - if input_unique_id in outputs: - to_delete = recursive_output_delete_if_changed(prompt, old_prompt, outputs, input_unique_id) - else: - to_delete = True - if to_delete: - break - else: - to_delete = True - - if to_delete: - d = outputs.pop(unique_id) - del d - return to_delete - -class PromptExecutor: - def __init__(self, server): - self.server = server - self.reset() - - def reset(self): - self.outputs = {} - self.object_storage = {} - self.outputs_ui = {} - self.status_messages = [] - self.success = True - self.old_prompt = {} - - def add_message(self, event, data: dict, broadcast: bool): - data = { - **data, - "timestamp": int(time.time() * 1000), - } - self.status_messages.append((event, data)) - if self.server.client_id is not None or broadcast: - self.server.send_sync(event, data, self.server.client_id) - - def handle_execution_error(self, prompt_id, prompt, current_outputs, executed, error, ex): - node_id = error["node_id"] - class_type = prompt[node_id]["class_type"] - - # First, send back the status to the frontend depending - # on the exception type - if isinstance(ex, comfy.model_management.InterruptProcessingException): - mes = { - "prompt_id": prompt_id, - "node_id": node_id, - "node_type": class_type, - "executed": list(executed), - } - self.add_message("execution_interrupted", mes, broadcast=True) - else: - mes = { - "prompt_id": prompt_id, - "node_id": node_id, - "node_type": class_type, - "executed": list(executed), - - "exception_message": error["exception_message"], - "exception_type": error["exception_type"], - "traceback": error["traceback"], - "current_inputs": error["current_inputs"], - "current_outputs": error["current_outputs"], - } - self.add_message("execution_error", mes, broadcast=False) - - # Next, remove the subsequent outputs since they will not be executed - to_delete = [] - for o in self.outputs: - if (o not in current_outputs) and (o not in executed): - to_delete += [o] - if o in self.old_prompt: - d = self.old_prompt.pop(o) - del d - for o in to_delete: - d = self.outputs.pop(o) - del d - - def execute(self, prompt, prompt_id, extra_data={}, execute_outputs=[]): - nodes.interrupt_processing(False) - - if "client_id" in extra_data: - self.server.client_id = extra_data["client_id"] - else: - self.server.client_id = None - - self.status_messages = [] - self.add_message("execution_start", { "prompt_id": prompt_id}, broadcast=False) - - with torch.inference_mode(): - #delete cached outputs if nodes don't exist for them - to_delete = [] - for o in self.outputs: - if o not in prompt: - to_delete += [o] - for o in to_delete: - d = self.outputs.pop(o) - del d - to_delete = [] - for o in self.object_storage: - if o[0] not in prompt: - to_delete += [o] - else: - p = prompt[o[0]] - if o[1] != p['class_type']: - to_delete += [o] - for o in to_delete: - d = self.object_storage.pop(o) - del d - - for x in prompt: - recursive_output_delete_if_changed(prompt, self.old_prompt, self.outputs, x) - - current_outputs = set(self.outputs.keys()) - for x in list(self.outputs_ui.keys()): - if x not in current_outputs: - d = self.outputs_ui.pop(x) - del d - - comfy.model_management.cleanup_models(keep_clone_weights_loaded=True) - self.add_message("execution_cached", - { "nodes": list(current_outputs) , "prompt_id": prompt_id}, - broadcast=False) - executed = set() - output_node_id = None - to_execute = [] - - for node_id in list(execute_outputs): - to_execute += [(0, node_id)] - - while len(to_execute) > 0: - #always execute the output that depends on the least amount of unexecuted nodes first - memo = {} - to_execute = sorted(list(map(lambda a: (len(recursive_will_execute(prompt, self.outputs, a[-1], memo)), a[-1]), to_execute))) - output_node_id = to_execute.pop(0)[-1] - - # This call shouldn't raise anything if there's an error deep in - # the actual SD code, instead it will report the node where the - # error was raised - self.success, error, ex = recursive_execute(self.server, prompt, self.outputs, output_node_id, extra_data, executed, prompt_id, self.outputs_ui, self.object_storage) - if self.success is not True: - self.handle_execution_error(prompt_id, prompt, current_outputs, executed, error, ex) - break - else: - # Only execute when the while-loop ends without break - self.add_message("execution_success", { "prompt_id": prompt_id }, broadcast=False) - - for x in executed: - self.old_prompt[x] = copy.deepcopy(prompt[x]) - self.server.last_node_id = None - if comfy.model_management.DISABLE_SMART_MEMORY: - comfy.model_management.unload_all_models() - - - -def validate_inputs(prompt, item, validated): - unique_id = item - if unique_id in validated: - return validated[unique_id] - - inputs = prompt[unique_id]['inputs'] - class_type = prompt[unique_id]['class_type'] - obj_class = nodes.NODE_CLASS_MAPPINGS[class_type] - - class_inputs = obj_class.INPUT_TYPES() - required_inputs = class_inputs['required'] - - errors = [] - valid = True - - validate_function_inputs = [] - if hasattr(obj_class, "VALIDATE_INPUTS"): - validate_function_inputs = inspect.getfullargspec(obj_class.VALIDATE_INPUTS).args - - for x in required_inputs: - if x not in inputs: - error = { - "type": "required_input_missing", - "message": "Required input is missing", - "details": f"{x}", - "extra_info": { - "input_name": x - } - } - errors.append(error) - continue - - val = inputs[x] - info = required_inputs[x] - type_input = info[0] - if isinstance(val, list): - if len(val) != 2: - error = { - "type": "bad_linked_input", - "message": "Bad linked input, must be a length-2 list of [node_id, slot_index]", - "details": f"{x}", - "extra_info": { - "input_name": x, - "input_config": info, - "received_value": val - } - } - errors.append(error) - continue - - o_id = val[0] - o_class_type = prompt[o_id]['class_type'] - r = nodes.NODE_CLASS_MAPPINGS[o_class_type].RETURN_TYPES - if r[val[1]] != type_input: - received_type = r[val[1]] - details = f"{x}, {received_type} != {type_input}" - error = { - "type": "return_type_mismatch", - "message": "Return type mismatch between linked nodes", - "details": details, - "extra_info": { - "input_name": x, - "input_config": info, - "received_type": received_type, - "linked_node": val - } - } - errors.append(error) - continue - try: - r = validate_inputs(prompt, o_id, validated) - if r[0] is False: - # `r` will be set in `validated[o_id]` already - valid = False - continue - except Exception as ex: - typ, _, tb = sys.exc_info() - valid = False - exception_type = full_type_name(typ) - reasons = [{ - "type": "exception_during_inner_validation", - "message": "Exception when validating inner node", - "details": str(ex), - "extra_info": { - "input_name": x, - "input_config": info, - "exception_message": str(ex), - "exception_type": exception_type, - "traceback": traceback.format_tb(tb), - "linked_node": val - } - }] - validated[o_id] = (False, reasons, o_id) - continue - else: - try: - if type_input == "INT": - val = int(val) - inputs[x] = val - if type_input == "FLOAT": - val = float(val) - inputs[x] = val - if type_input == "STRING": - val = str(val) - inputs[x] = val - except Exception as ex: - error = { - "type": "invalid_input_type", - "message": f"Failed to convert an input value to a {type_input} value", - "details": f"{x}, {val}, {ex}", - "extra_info": { - "input_name": x, - "input_config": info, - "received_value": val, - "exception_message": str(ex) - } - } - errors.append(error) - continue - - if len(info) > 1: - if "min" in info[1] and val < info[1]["min"]: - error = { - "type": "value_smaller_than_min", - "message": "Value {} smaller than min of {}".format(val, info[1]["min"]), - "details": f"{x}", - "extra_info": { - "input_name": x, - "input_config": info, - "received_value": val, - } - } - errors.append(error) - continue - if "max" in info[1] and val > info[1]["max"]: - error = { - "type": "value_bigger_than_max", - "message": "Value {} bigger than max of {}".format(val, info[1]["max"]), - "details": f"{x}", - "extra_info": { - "input_name": x, - "input_config": info, - "received_value": val, - } - } - errors.append(error) - continue - - if x not in validate_function_inputs: - if isinstance(type_input, list): - if val not in type_input: - input_config = info - list_info = "" - - # Don't send back gigantic lists like if they're lots of - # scanned model filepaths - if len(type_input) > 20: - list_info = f"(list of length {len(type_input)})" - input_config = None - else: - list_info = str(type_input) - - error = { - "type": "value_not_in_list", - "message": "Value not in list", - "details": f"{x}: '{val}' not in {list_info}", - "extra_info": { - "input_name": x, - "input_config": input_config, - "received_value": val, - } - } - errors.append(error) - continue - - if len(validate_function_inputs) > 0: - input_data_all = get_input_data(inputs, obj_class, unique_id) - input_filtered = {} - for x in input_data_all: - if x in validate_function_inputs: - input_filtered[x] = input_data_all[x] - - #ret = obj_class.VALIDATE_INPUTS(**input_filtered) - ret = map_node_over_list(obj_class, input_filtered, "VALIDATE_INPUTS") - for x in input_filtered: - for i, r in enumerate(ret): - if r is not True: - details = f"{x}" - if r is not False: - details += f" - {str(r)}" - - error = { - "type": "custom_validation_failed", - "message": "Custom validation failed for node", - "details": details, - "extra_info": { - "input_name": x, - "input_config": info, - "received_value": val, - } - } - errors.append(error) - continue - - if len(errors) > 0 or valid is not True: - ret = (False, errors, unique_id) - else: - ret = (True, [], unique_id) - - validated[unique_id] = ret - return ret - -def full_type_name(klass): - module = klass.__module__ - if module == 'builtins': - return klass.__qualname__ - return module + '.' + klass.__qualname__ - -def validate_prompt(prompt): - outputs = set() - for x in prompt: - if 'class_type' not in prompt[x]: - error = { - "type": "invalid_prompt", - "message": f"Cannot execute because a node is missing the class_type property.", - "details": f"Node ID '#{x}'", - "extra_info": {} - } - return (False, error, [], []) - - class_type = prompt[x]['class_type'] - class_ = nodes.NODE_CLASS_MAPPINGS.get(class_type, None) - if class_ is None: - error = { - "type": "invalid_prompt", - "message": f"Cannot execute because node {class_type} does not exist.", - "details": f"Node ID '#{x}'", - "extra_info": {} - } - return (False, error, [], []) - - if hasattr(class_, 'OUTPUT_NODE') and class_.OUTPUT_NODE is True: - outputs.add(x) - - if len(outputs) == 0: - error = { - "type": "prompt_no_outputs", - "message": "Prompt has no outputs", - "details": "", - "extra_info": {} - } - return (False, error, [], []) - - good_outputs = set() - errors = [] - node_errors = {} - validated = {} - for o in outputs: - valid = False - reasons = [] - try: - m = validate_inputs(prompt, o, validated) - valid = m[0] - reasons = m[1] - except Exception as ex: - typ, _, tb = sys.exc_info() - valid = False - exception_type = full_type_name(typ) - reasons = [{ - "type": "exception_during_validation", - "message": "Exception when validating node", - "details": str(ex), - "extra_info": { - "exception_type": exception_type, - "traceback": traceback.format_tb(tb) - } - }] - validated[o] = (False, reasons, o) - - if valid is True: - good_outputs.add(o) - else: - logging.error(f"Failed to validate prompt for output {o}:") - if len(reasons) > 0: - logging.error("* (prompt):") - for reason in reasons: - logging.error(f" - {reason['message']}: {reason['details']}") - errors += [(o, reasons)] - for node_id, result in validated.items(): - valid = result[0] - reasons = result[1] - # If a node upstream has errors, the nodes downstream will also - # be reported as invalid, but there will be no errors attached. - # So don't return those nodes as having errors in the response. - if valid is not True and len(reasons) > 0: - if node_id not in node_errors: - class_type = prompt[node_id]['class_type'] - node_errors[node_id] = { - "errors": reasons, - "dependent_outputs": [], - "class_type": class_type - } - logging.error(f"* {class_type} {node_id}:") - for reason in reasons: - logging.error(f" - {reason['message']}: {reason['details']}") - node_errors[node_id]["dependent_outputs"].append(o) - logging.error("Output will be ignored") - - if len(good_outputs) == 0: - errors_list = [] - for o, errors in errors: - for error in errors: - errors_list.append(f"{error['message']}: {error['details']}") - errors_list = "\n".join(errors_list) - - error = { - "type": "prompt_outputs_failed_validation", - "message": "Prompt outputs failed validation", - "details": errors_list, - "extra_info": {} - } - - return (False, error, list(good_outputs), node_errors) - - return (True, None, list(good_outputs), node_errors) - -MAXIMUM_HISTORY_SIZE = 10000 - -class PromptQueue: - def __init__(self, server): - self.server = server - self.mutex = threading.RLock() - self.not_empty = threading.Condition(self.mutex) - self.task_counter = 0 - self.queue = [] - self.currently_running = {} - self.history = {} - self.flags = {} - server.prompt_queue = self - - def put(self, item): - with self.mutex: - heapq.heappush(self.queue, item) - self.server.queue_updated() - self.not_empty.notify() - - def get(self, timeout=None): - with self.not_empty: - while len(self.queue) == 0: - self.not_empty.wait(timeout=timeout) - if timeout is not None and len(self.queue) == 0: - return None - item = heapq.heappop(self.queue) - i = self.task_counter - self.currently_running[i] = copy.deepcopy(item) - self.task_counter += 1 - self.server.queue_updated() - return (item, i) - - class ExecutionStatus(NamedTuple): - status_str: Literal['success', 'error'] - completed: bool - messages: List[str] - - def task_done(self, item_id, outputs, - status: Optional['PromptQueue.ExecutionStatus']): - with self.mutex: - prompt = self.currently_running.pop(item_id) - if len(self.history) > MAXIMUM_HISTORY_SIZE: - self.history.pop(next(iter(self.history))) - - status_dict: Optional[dict] = None - if status is not None: - status_dict = copy.deepcopy(status._asdict()) - - self.history[prompt[1]] = { - "prompt": prompt, - "outputs": copy.deepcopy(outputs), - 'status': status_dict, - } - self.server.queue_updated() - - def get_current_queue(self): - with self.mutex: - out = [] - for x in self.currently_running.values(): - out += [x] - return (out, copy.deepcopy(self.queue)) - - def get_tasks_remaining(self): - with self.mutex: - return len(self.queue) + len(self.currently_running) - - def wipe_queue(self): - with self.mutex: - self.queue = [] - self.server.queue_updated() - - def delete_queue_item(self, function): - with self.mutex: - for x in range(len(self.queue)): - if function(self.queue[x]): - if len(self.queue) == 1: - self.wipe_queue() - else: - self.queue.pop(x) - heapq.heapify(self.queue) - self.server.queue_updated() - return True - return False - - def get_history(self, prompt_id=None, max_items=None, offset=-1): - with self.mutex: - if prompt_id is None: - out = {} - i = 0 - if offset < 0 and max_items is not None: - offset = len(self.history) - max_items - for k in self.history: - if i >= offset: - out[k] = self.history[k] - if max_items is not None and len(out) >= max_items: - break - i += 1 - return out - elif prompt_id in self.history: - return {prompt_id: copy.deepcopy(self.history[prompt_id])} - else: - return {} - - def wipe_history(self): - with self.mutex: - self.history = {} - - def delete_history_item(self, id_to_delete): - with self.mutex: - self.history.pop(id_to_delete, None) - - def set_flag(self, name, data): - with self.mutex: - self.flags[name] = data - self.not_empty.notify() - - def get_flags(self, reset=True): - with self.mutex: - if reset: - ret = self.flags - self.flags = {} - return ret - else: - return self.flags.copy() diff --git a/extra_model_paths.yaml.example b/extra_model_paths.yaml.example deleted file mode 100644 index 846d04dbe..000000000 --- a/extra_model_paths.yaml.example +++ /dev/null @@ -1,42 +0,0 @@ -#Rename this to extra_model_paths.yaml and ComfyUI will load it - - -#config for a1111 ui -#all you have to do is change the base_path to where yours is installed -a111: - base_path: path/to/stable-diffusion-webui/ - - checkpoints: models/Stable-diffusion - configs: models/Stable-diffusion - vae: models/VAE - loras: | - models/Lora - models/LyCORIS - upscale_models: | - models/ESRGAN - models/RealESRGAN - models/SwinIR - embeddings: embeddings - hypernetworks: models/hypernetworks - controlnet: models/ControlNet - -#config for comfyui -#your base path should be either an existing comfy install or a central folder where you store all of your models, loras, etc. - -#comfyui: -# base_path: path/to/comfyui/ -# checkpoints: models/checkpoints/ -# clip: models/clip/ -# clip_vision: models/clip_vision/ -# configs: models/configs/ -# controlnet: models/controlnet/ -# embeddings: models/embeddings/ -# loras: models/loras/ -# upscale_models: models/upscale_models/ -# vae: models/vae/ - -#other_ui: -# base_path: path/to/ui -# checkpoints: models/checkpoints -# gligen: models/gligen -# custom_nodes: path/custom_nodes diff --git a/fix_torch.py b/fix_torch.py deleted file mode 100644 index e350f5c7d..000000000 --- a/fix_torch.py +++ /dev/null @@ -1,24 +0,0 @@ -import importlib.util -import shutil -import os -import ctypes -import logging - - -torch_spec = importlib.util.find_spec("torch") -for folder in torch_spec.submodule_search_locations: - lib_folder = os.path.join(folder, "lib") - test_file = os.path.join(lib_folder, "fbgemm.dll") - dest = os.path.join(lib_folder, "libomp140.x86_64.dll") - if os.path.exists(dest): - break - - with open(test_file, 'rb') as f: - contents = f.read() - if b"libomp140.x86_64.dll" not in contents: - break - try: - mydll = ctypes.cdll.LoadLibrary(test_file) - except FileNotFoundError as e: - logging.warning("Detected pytorch version with libomp issue, patching.") - shutil.copyfile(os.path.join(lib_folder, "libiomp5md.dll"), dest) diff --git a/input/example.png b/input/example.png deleted file mode 100644 index 7b7f3c9cbbe6d8750c4a9eaf65d6ae4d2f108f79..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8589 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 0: - print("\nPrestartup times for custom nodes:") - for n in sorted(node_prestartup_times): - if n[2]: - import_message = "" - else: - import_message = " (PRESTARTUP FAILED)" - print("{:6.1f} seconds{}:".format(n[0], import_message), n[1]) - print() - -execute_prestartup_script() - - -# Main code -import asyncio -import itertools -import shutil -import threading -import gc - -import logging - -if os.name == "nt": - logging.getLogger("xformers").addFilter(lambda record: 'A matching Triton is not available' not in record.getMessage()) - -if __name__ == "__main__": - if args.cuda_device is not None: - os.environ['CUDA_VISIBLE_DEVICES'] = str(args.cuda_device) - logging.info("Set cuda device to: {}".format(args.cuda_device)) - - if args.deterministic: - if 'CUBLAS_WORKSPACE_CONFIG' not in os.environ: - os.environ['CUBLAS_WORKSPACE_CONFIG'] = ":4096:8" - - import cuda_malloc - -if args.windows_standalone_build: - try: - import fix_torch - except: - pass - -import comfy.utils -import yaml - -import execution -import server -from server import BinaryEventTypes -import nodes -import comfy.model_management - -def cuda_malloc_warning(): - device = comfy.model_management.get_torch_device() - device_name = comfy.model_management.get_torch_device_name(device) - cuda_malloc_warning = False - if "cudaMallocAsync" in device_name: - for b in cuda_malloc.blacklist: - if b in device_name: - cuda_malloc_warning = True - if cuda_malloc_warning: - logging.warning("\nWARNING: this card most likely does not support cuda-malloc, if you get \"CUDA error\" please run ComfyUI with: --disable-cuda-malloc\n") - -def prompt_worker(q, server): - e = execution.PromptExecutor(server) - last_gc_collect = 0 - need_gc = False - gc_collect_interval = 10.0 - - while True: - timeout = 1000.0 - if need_gc: - timeout = max(gc_collect_interval - (current_time - last_gc_collect), 0.0) - - queue_item = q.get(timeout=timeout) - if queue_item is not None: - item, item_id = queue_item - execution_start_time = time.perf_counter() - prompt_id = item[1] - server.last_prompt_id = prompt_id - - e.execute(item[2], prompt_id, item[3], item[4]) - need_gc = True - q.task_done(item_id, - e.outputs_ui, - status=execution.PromptQueue.ExecutionStatus( - status_str='success' if e.success else 'error', - completed=e.success, - messages=e.status_messages)) - if server.client_id is not None: - server.send_sync("executing", { "node": None, "prompt_id": prompt_id }, server.client_id) - - current_time = time.perf_counter() - execution_time = current_time - execution_start_time - logging.info("Prompt executed in {:.2f} seconds".format(execution_time)) - - flags = q.get_flags() - free_memory = flags.get("free_memory", False) - - if flags.get("unload_models", free_memory): - comfy.model_management.unload_all_models() - need_gc = True - last_gc_collect = 0 - - if free_memory: - e.reset() - need_gc = True - last_gc_collect = 0 - - if need_gc: - current_time = time.perf_counter() - if (current_time - last_gc_collect) > gc_collect_interval: - comfy.model_management.cleanup_models() - gc.collect() - comfy.model_management.soft_empty_cache() - last_gc_collect = current_time - need_gc = False - -async def run(server, address='', port=8188, verbose=True, call_on_start=None): - await asyncio.gather(server.start(address, port, verbose, call_on_start), server.publish_loop()) - - -def hijack_progress(server): - def hook(value, total, preview_image): - comfy.model_management.throw_exception_if_processing_interrupted() - progress = {"value": value, "max": total, "prompt_id": server.last_prompt_id, "node": server.last_node_id} - - server.send_sync("progress", progress, server.client_id) - if preview_image is not None: - server.send_sync(BinaryEventTypes.UNENCODED_PREVIEW_IMAGE, preview_image, server.client_id) - comfy.utils.set_progress_bar_global_hook(hook) - - -def cleanup_temp(): - temp_dir = folder_paths.get_temp_directory() - if os.path.exists(temp_dir): - shutil.rmtree(temp_dir, ignore_errors=True) - - -def load_extra_path_config(yaml_path): - with open(yaml_path, 'r') as stream: - config = yaml.safe_load(stream) - for c in config: - conf = config[c] - if conf is None: - continue - base_path = None - if "base_path" in conf: - base_path = conf.pop("base_path") - for x in conf: - for y in conf[x].split("\n"): - if len(y) == 0: - continue - full_path = y - if base_path is not None: - full_path = os.path.join(base_path, full_path) - logging.info("Adding extra search path {} {}".format(x, full_path)) - folder_paths.add_model_folder_path(x, full_path) - - -if __name__ == "__main__": - if args.temp_directory: - temp_dir = os.path.join(os.path.abspath(args.temp_directory), "temp") - logging.info(f"Setting temp directory to: {temp_dir}") - folder_paths.set_temp_directory(temp_dir) - cleanup_temp() - - if args.windows_standalone_build: - try: - import new_updater - new_updater.update_windows_updater() - except: - pass - - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - server = server.PromptServer(loop) - q = execution.PromptQueue(server) - - extra_model_paths_config_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "extra_model_paths.yaml") - if os.path.isfile(extra_model_paths_config_path): - load_extra_path_config(extra_model_paths_config_path) - - if args.extra_model_paths_config: - for config_path in itertools.chain(*args.extra_model_paths_config): - load_extra_path_config(config_path) - - nodes.init_extra_nodes(init_custom_nodes=not args.disable_all_custom_nodes) - - cuda_malloc_warning() - - server.add_routes() - hijack_progress(server) - - threading.Thread(target=prompt_worker, daemon=True, args=(q, server,)).start() - - if args.output_directory: - output_dir = os.path.abspath(args.output_directory) - logging.info(f"Setting output directory to: {output_dir}") - folder_paths.set_output_directory(output_dir) - - #These are the default folders that checkpoints, clip and vae models will be saved to when using CheckpointSave, etc.. nodes - folder_paths.add_model_folder_path("checkpoints", os.path.join(folder_paths.get_output_directory(), "checkpoints")) - folder_paths.add_model_folder_path("clip", os.path.join(folder_paths.get_output_directory(), "clip")) - folder_paths.add_model_folder_path("vae", os.path.join(folder_paths.get_output_directory(), "vae")) - - if args.input_directory: - input_dir = os.path.abspath(args.input_directory) - logging.info(f"Setting input directory to: {input_dir}") - folder_paths.set_input_directory(input_dir) - - if args.quick_test_for_ci: - exit(0) - - call_on_start = None - if args.auto_launch: - def startup_server(scheme, address, port): - import webbrowser - if os.name == 'nt' and address == '0.0.0.0': - address = '127.0.0.1' - webbrowser.open(f"{scheme}://{address}:{port}") - call_on_start = startup_server - - try: - loop.run_until_complete(run(server, address=args.listen, port=args.port, verbose=not args.dont_print_server, call_on_start=call_on_start)) - except KeyboardInterrupt: - logging.info("\nStopped server") - - cleanup_temp() diff --git a/models/checkpoints/put_checkpoints_here b/models/checkpoints/put_checkpoints_here deleted file mode 100644 index e69de29bb..000000000 diff --git a/models/clip/put_clip_or_text_encoder_models_here b/models/clip/put_clip_or_text_encoder_models_here deleted file mode 100644 index e69de29bb..000000000 diff --git a/models/clip_vision/put_clip_vision_models_here b/models/clip_vision/put_clip_vision_models_here deleted file mode 100644 index e69de29bb..000000000 diff --git a/models/configs/anything_v3.yaml b/models/configs/anything_v3.yaml deleted file mode 100644 index 8bcfe584a..000000000 --- a/models/configs/anything_v3.yaml +++ /dev/null @@ -1,73 +0,0 @@ -model: - base_learning_rate: 1.0e-04 - target: ldm.models.diffusion.ddpm.LatentDiffusion - params: - linear_start: 0.00085 - linear_end: 0.0120 - num_timesteps_cond: 1 - log_every_t: 200 - timesteps: 1000 - first_stage_key: "jpg" - cond_stage_key: "txt" - image_size: 64 - channels: 4 - cond_stage_trainable: false # Note: different from the one we trained before - conditioning_key: crossattn - monitor: val/loss_simple_ema - scale_factor: 0.18215 - use_ema: False - - scheduler_config: # 10000 warmup steps - target: ldm.lr_scheduler.LambdaLinearScheduler - params: - warm_up_steps: [ 10000 ] - cycle_lengths: [ 10000000000000 ] # incredibly large number to prevent corner cases - f_start: [ 1.e-6 ] - f_max: [ 1. ] - f_min: [ 1. ] - - unet_config: - target: ldm.modules.diffusionmodules.openaimodel.UNetModel - params: - image_size: 32 # unused - in_channels: 4 - out_channels: 4 - model_channels: 320 - attention_resolutions: [ 4, 2, 1 ] - num_res_blocks: 2 - channel_mult: [ 1, 2, 4, 4 ] - num_heads: 8 - use_spatial_transformer: True - transformer_depth: 1 - context_dim: 768 - use_checkpoint: True - legacy: False - - first_stage_config: - target: ldm.models.autoencoder.AutoencoderKL - params: - embed_dim: 4 - monitor: val/rec_loss - ddconfig: - double_z: true - z_channels: 4 - resolution: 256 - in_channels: 3 - out_ch: 3 - ch: 128 - ch_mult: - - 1 - - 2 - - 4 - - 4 - num_res_blocks: 2 - attn_resolutions: [] - dropout: 0.0 - lossconfig: - target: torch.nn.Identity - - cond_stage_config: - target: ldm.modules.encoders.modules.FrozenCLIPEmbedder - params: - layer: "hidden" - layer_idx: -2 diff --git a/models/configs/v1-inference.yaml b/models/configs/v1-inference.yaml deleted file mode 100644 index d4effe569..000000000 --- a/models/configs/v1-inference.yaml +++ /dev/null @@ -1,70 +0,0 @@ -model: - base_learning_rate: 1.0e-04 - target: ldm.models.diffusion.ddpm.LatentDiffusion - params: - linear_start: 0.00085 - linear_end: 0.0120 - num_timesteps_cond: 1 - log_every_t: 200 - timesteps: 1000 - first_stage_key: "jpg" - cond_stage_key: "txt" - image_size: 64 - channels: 4 - cond_stage_trainable: false # Note: different from the one we trained before - conditioning_key: crossattn - monitor: val/loss_simple_ema - scale_factor: 0.18215 - use_ema: False - - scheduler_config: # 10000 warmup steps - target: ldm.lr_scheduler.LambdaLinearScheduler - params: - warm_up_steps: [ 10000 ] - cycle_lengths: [ 10000000000000 ] # incredibly large number to prevent corner cases - f_start: [ 1.e-6 ] - f_max: [ 1. ] - f_min: [ 1. ] - - unet_config: - target: ldm.modules.diffusionmodules.openaimodel.UNetModel - params: - image_size: 32 # unused - in_channels: 4 - out_channels: 4 - model_channels: 320 - attention_resolutions: [ 4, 2, 1 ] - num_res_blocks: 2 - channel_mult: [ 1, 2, 4, 4 ] - num_heads: 8 - use_spatial_transformer: True - transformer_depth: 1 - context_dim: 768 - use_checkpoint: True - legacy: False - - first_stage_config: - target: ldm.models.autoencoder.AutoencoderKL - params: - embed_dim: 4 - monitor: val/rec_loss - ddconfig: - double_z: true - z_channels: 4 - resolution: 256 - in_channels: 3 - out_ch: 3 - ch: 128 - ch_mult: - - 1 - - 2 - - 4 - - 4 - num_res_blocks: 2 - attn_resolutions: [] - dropout: 0.0 - lossconfig: - target: torch.nn.Identity - - cond_stage_config: - target: ldm.modules.encoders.modules.FrozenCLIPEmbedder diff --git a/models/configs/v1-inference_clip_skip_2.yaml b/models/configs/v1-inference_clip_skip_2.yaml deleted file mode 100644 index 8bcfe584a..000000000 --- a/models/configs/v1-inference_clip_skip_2.yaml +++ /dev/null @@ -1,73 +0,0 @@ -model: - base_learning_rate: 1.0e-04 - target: ldm.models.diffusion.ddpm.LatentDiffusion - params: - linear_start: 0.00085 - linear_end: 0.0120 - num_timesteps_cond: 1 - log_every_t: 200 - timesteps: 1000 - first_stage_key: "jpg" - cond_stage_key: "txt" - image_size: 64 - channels: 4 - cond_stage_trainable: false # Note: different from the one we trained before - conditioning_key: crossattn - monitor: val/loss_simple_ema - scale_factor: 0.18215 - use_ema: False - - scheduler_config: # 10000 warmup steps - target: ldm.lr_scheduler.LambdaLinearScheduler - params: - warm_up_steps: [ 10000 ] - cycle_lengths: [ 10000000000000 ] # incredibly large number to prevent corner cases - f_start: [ 1.e-6 ] - f_max: [ 1. ] - f_min: [ 1. ] - - unet_config: - target: ldm.modules.diffusionmodules.openaimodel.UNetModel - params: - image_size: 32 # unused - in_channels: 4 - out_channels: 4 - model_channels: 320 - attention_resolutions: [ 4, 2, 1 ] - num_res_blocks: 2 - channel_mult: [ 1, 2, 4, 4 ] - num_heads: 8 - use_spatial_transformer: True - transformer_depth: 1 - context_dim: 768 - use_checkpoint: True - legacy: False - - first_stage_config: - target: ldm.models.autoencoder.AutoencoderKL - params: - embed_dim: 4 - monitor: val/rec_loss - ddconfig: - double_z: true - z_channels: 4 - resolution: 256 - in_channels: 3 - out_ch: 3 - ch: 128 - ch_mult: - - 1 - - 2 - - 4 - - 4 - num_res_blocks: 2 - attn_resolutions: [] - dropout: 0.0 - lossconfig: - target: torch.nn.Identity - - cond_stage_config: - target: ldm.modules.encoders.modules.FrozenCLIPEmbedder - params: - layer: "hidden" - layer_idx: -2 diff --git a/models/configs/v1-inference_clip_skip_2_fp16.yaml b/models/configs/v1-inference_clip_skip_2_fp16.yaml deleted file mode 100644 index 7eca31c7b..000000000 --- a/models/configs/v1-inference_clip_skip_2_fp16.yaml +++ /dev/null @@ -1,74 +0,0 @@ -model: - base_learning_rate: 1.0e-04 - target: ldm.models.diffusion.ddpm.LatentDiffusion - params: - linear_start: 0.00085 - linear_end: 0.0120 - num_timesteps_cond: 1 - log_every_t: 200 - timesteps: 1000 - first_stage_key: "jpg" - cond_stage_key: "txt" - image_size: 64 - channels: 4 - cond_stage_trainable: false # Note: different from the one we trained before - conditioning_key: crossattn - monitor: val/loss_simple_ema - scale_factor: 0.18215 - use_ema: False - - scheduler_config: # 10000 warmup steps - target: ldm.lr_scheduler.LambdaLinearScheduler - params: - warm_up_steps: [ 10000 ] - cycle_lengths: [ 10000000000000 ] # incredibly large number to prevent corner cases - f_start: [ 1.e-6 ] - f_max: [ 1. ] - f_min: [ 1. ] - - unet_config: - target: ldm.modules.diffusionmodules.openaimodel.UNetModel - params: - use_fp16: True - image_size: 32 # unused - in_channels: 4 - out_channels: 4 - model_channels: 320 - attention_resolutions: [ 4, 2, 1 ] - num_res_blocks: 2 - channel_mult: [ 1, 2, 4, 4 ] - num_heads: 8 - use_spatial_transformer: True - transformer_depth: 1 - context_dim: 768 - use_checkpoint: True - legacy: False - - first_stage_config: - target: ldm.models.autoencoder.AutoencoderKL - params: - embed_dim: 4 - monitor: val/rec_loss - ddconfig: - double_z: true - z_channels: 4 - resolution: 256 - in_channels: 3 - out_ch: 3 - ch: 128 - ch_mult: - - 1 - - 2 - - 4 - - 4 - num_res_blocks: 2 - attn_resolutions: [] - dropout: 0.0 - lossconfig: - target: torch.nn.Identity - - cond_stage_config: - target: ldm.modules.encoders.modules.FrozenCLIPEmbedder - params: - layer: "hidden" - layer_idx: -2 diff --git a/models/configs/v1-inference_fp16.yaml b/models/configs/v1-inference_fp16.yaml deleted file mode 100644 index 147f42b17..000000000 --- a/models/configs/v1-inference_fp16.yaml +++ /dev/null @@ -1,71 +0,0 @@ -model: - base_learning_rate: 1.0e-04 - target: ldm.models.diffusion.ddpm.LatentDiffusion - params: - linear_start: 0.00085 - linear_end: 0.0120 - num_timesteps_cond: 1 - log_every_t: 200 - timesteps: 1000 - first_stage_key: "jpg" - cond_stage_key: "txt" - image_size: 64 - channels: 4 - cond_stage_trainable: false # Note: different from the one we trained before - conditioning_key: crossattn - monitor: val/loss_simple_ema - scale_factor: 0.18215 - use_ema: False - - scheduler_config: # 10000 warmup steps - target: ldm.lr_scheduler.LambdaLinearScheduler - params: - warm_up_steps: [ 10000 ] - cycle_lengths: [ 10000000000000 ] # incredibly large number to prevent corner cases - f_start: [ 1.e-6 ] - f_max: [ 1. ] - f_min: [ 1. ] - - unet_config: - target: ldm.modules.diffusionmodules.openaimodel.UNetModel - params: - use_fp16: True - image_size: 32 # unused - in_channels: 4 - out_channels: 4 - model_channels: 320 - attention_resolutions: [ 4, 2, 1 ] - num_res_blocks: 2 - channel_mult: [ 1, 2, 4, 4 ] - num_heads: 8 - use_spatial_transformer: True - transformer_depth: 1 - context_dim: 768 - use_checkpoint: True - legacy: False - - first_stage_config: - target: ldm.models.autoencoder.AutoencoderKL - params: - embed_dim: 4 - monitor: val/rec_loss - ddconfig: - double_z: true - z_channels: 4 - resolution: 256 - in_channels: 3 - out_ch: 3 - ch: 128 - ch_mult: - - 1 - - 2 - - 4 - - 4 - num_res_blocks: 2 - attn_resolutions: [] - dropout: 0.0 - lossconfig: - target: torch.nn.Identity - - cond_stage_config: - target: ldm.modules.encoders.modules.FrozenCLIPEmbedder diff --git a/models/configs/v1-inpainting-inference.yaml b/models/configs/v1-inpainting-inference.yaml deleted file mode 100644 index 45f3f82d4..000000000 --- a/models/configs/v1-inpainting-inference.yaml +++ /dev/null @@ -1,71 +0,0 @@ -model: - base_learning_rate: 7.5e-05 - target: ldm.models.diffusion.ddpm.LatentInpaintDiffusion - params: - linear_start: 0.00085 - linear_end: 0.0120 - num_timesteps_cond: 1 - log_every_t: 200 - timesteps: 1000 - first_stage_key: "jpg" - cond_stage_key: "txt" - image_size: 64 - channels: 4 - cond_stage_trainable: false # Note: different from the one we trained before - conditioning_key: hybrid # important - monitor: val/loss_simple_ema - scale_factor: 0.18215 - finetune_keys: null - - scheduler_config: # 10000 warmup steps - target: ldm.lr_scheduler.LambdaLinearScheduler - params: - warm_up_steps: [ 2500 ] # NOTE for resuming. use 10000 if starting from scratch - cycle_lengths: [ 10000000000000 ] # incredibly large number to prevent corner cases - f_start: [ 1.e-6 ] - f_max: [ 1. ] - f_min: [ 1. ] - - unet_config: - target: ldm.modules.diffusionmodules.openaimodel.UNetModel - params: - image_size: 32 # unused - in_channels: 9 # 4 data + 4 downscaled image + 1 mask - out_channels: 4 - model_channels: 320 - attention_resolutions: [ 4, 2, 1 ] - num_res_blocks: 2 - channel_mult: [ 1, 2, 4, 4 ] - num_heads: 8 - use_spatial_transformer: True - transformer_depth: 1 - context_dim: 768 - use_checkpoint: True - legacy: False - - first_stage_config: - target: ldm.models.autoencoder.AutoencoderKL - params: - embed_dim: 4 - monitor: val/rec_loss - ddconfig: - double_z: true - z_channels: 4 - resolution: 256 - in_channels: 3 - out_ch: 3 - ch: 128 - ch_mult: - - 1 - - 2 - - 4 - - 4 - num_res_blocks: 2 - attn_resolutions: [] - dropout: 0.0 - lossconfig: - target: torch.nn.Identity - - cond_stage_config: - target: ldm.modules.encoders.modules.FrozenCLIPEmbedder - diff --git a/models/configs/v2-inference-v.yaml b/models/configs/v2-inference-v.yaml deleted file mode 100644 index 8ec8dfbfe..000000000 --- a/models/configs/v2-inference-v.yaml +++ /dev/null @@ -1,68 +0,0 @@ -model: - base_learning_rate: 1.0e-4 - target: ldm.models.diffusion.ddpm.LatentDiffusion - params: - parameterization: "v" - linear_start: 0.00085 - linear_end: 0.0120 - num_timesteps_cond: 1 - log_every_t: 200 - timesteps: 1000 - first_stage_key: "jpg" - cond_stage_key: "txt" - image_size: 64 - channels: 4 - cond_stage_trainable: false - conditioning_key: crossattn - monitor: val/loss_simple_ema - scale_factor: 0.18215 - use_ema: False # we set this to false because this is an inference only config - - unet_config: - target: ldm.modules.diffusionmodules.openaimodel.UNetModel - params: - use_checkpoint: True - use_fp16: True - image_size: 32 # unused - in_channels: 4 - out_channels: 4 - model_channels: 320 - attention_resolutions: [ 4, 2, 1 ] - num_res_blocks: 2 - channel_mult: [ 1, 2, 4, 4 ] - num_head_channels: 64 # need to fix for flash-attn - use_spatial_transformer: True - use_linear_in_transformer: True - transformer_depth: 1 - context_dim: 1024 - legacy: False - - first_stage_config: - target: ldm.models.autoencoder.AutoencoderKL - params: - embed_dim: 4 - monitor: val/rec_loss - ddconfig: - #attn_type: "vanilla-xformers" - double_z: true - z_channels: 4 - resolution: 256 - in_channels: 3 - out_ch: 3 - ch: 128 - ch_mult: - - 1 - - 2 - - 4 - - 4 - num_res_blocks: 2 - attn_resolutions: [] - dropout: 0.0 - lossconfig: - target: torch.nn.Identity - - cond_stage_config: - target: ldm.modules.encoders.modules.FrozenOpenCLIPEmbedder - params: - freeze: True - layer: "penultimate" diff --git a/models/configs/v2-inference-v_fp32.yaml b/models/configs/v2-inference-v_fp32.yaml deleted file mode 100644 index d5c9b9cb2..000000000 --- a/models/configs/v2-inference-v_fp32.yaml +++ /dev/null @@ -1,68 +0,0 @@ -model: - base_learning_rate: 1.0e-4 - target: ldm.models.diffusion.ddpm.LatentDiffusion - params: - parameterization: "v" - linear_start: 0.00085 - linear_end: 0.0120 - num_timesteps_cond: 1 - log_every_t: 200 - timesteps: 1000 - first_stage_key: "jpg" - cond_stage_key: "txt" - image_size: 64 - channels: 4 - cond_stage_trainable: false - conditioning_key: crossattn - monitor: val/loss_simple_ema - scale_factor: 0.18215 - use_ema: False # we set this to false because this is an inference only config - - unet_config: - target: ldm.modules.diffusionmodules.openaimodel.UNetModel - params: - use_checkpoint: True - use_fp16: False - image_size: 32 # unused - in_channels: 4 - out_channels: 4 - model_channels: 320 - attention_resolutions: [ 4, 2, 1 ] - num_res_blocks: 2 - channel_mult: [ 1, 2, 4, 4 ] - num_head_channels: 64 # need to fix for flash-attn - use_spatial_transformer: True - use_linear_in_transformer: True - transformer_depth: 1 - context_dim: 1024 - legacy: False - - first_stage_config: - target: ldm.models.autoencoder.AutoencoderKL - params: - embed_dim: 4 - monitor: val/rec_loss - ddconfig: - #attn_type: "vanilla-xformers" - double_z: true - z_channels: 4 - resolution: 256 - in_channels: 3 - out_ch: 3 - ch: 128 - ch_mult: - - 1 - - 2 - - 4 - - 4 - num_res_blocks: 2 - attn_resolutions: [] - dropout: 0.0 - lossconfig: - target: torch.nn.Identity - - cond_stage_config: - target: ldm.modules.encoders.modules.FrozenOpenCLIPEmbedder - params: - freeze: True - layer: "penultimate" diff --git a/models/configs/v2-inference.yaml b/models/configs/v2-inference.yaml deleted file mode 100644 index 152c4f3c2..000000000 --- a/models/configs/v2-inference.yaml +++ /dev/null @@ -1,67 +0,0 @@ -model: - base_learning_rate: 1.0e-4 - target: ldm.models.diffusion.ddpm.LatentDiffusion - params: - linear_start: 0.00085 - linear_end: 0.0120 - num_timesteps_cond: 1 - log_every_t: 200 - timesteps: 1000 - first_stage_key: "jpg" - cond_stage_key: "txt" - image_size: 64 - channels: 4 - cond_stage_trainable: false - conditioning_key: crossattn - monitor: val/loss_simple_ema - scale_factor: 0.18215 - use_ema: False # we set this to false because this is an inference only config - - unet_config: - target: ldm.modules.diffusionmodules.openaimodel.UNetModel - params: - use_checkpoint: True - use_fp16: True - image_size: 32 # unused - in_channels: 4 - out_channels: 4 - model_channels: 320 - attention_resolutions: [ 4, 2, 1 ] - num_res_blocks: 2 - channel_mult: [ 1, 2, 4, 4 ] - num_head_channels: 64 # need to fix for flash-attn - use_spatial_transformer: True - use_linear_in_transformer: True - transformer_depth: 1 - context_dim: 1024 - legacy: False - - first_stage_config: - target: ldm.models.autoencoder.AutoencoderKL - params: - embed_dim: 4 - monitor: val/rec_loss - ddconfig: - #attn_type: "vanilla-xformers" - double_z: true - z_channels: 4 - resolution: 256 - in_channels: 3 - out_ch: 3 - ch: 128 - ch_mult: - - 1 - - 2 - - 4 - - 4 - num_res_blocks: 2 - attn_resolutions: [] - dropout: 0.0 - lossconfig: - target: torch.nn.Identity - - cond_stage_config: - target: ldm.modules.encoders.modules.FrozenOpenCLIPEmbedder - params: - freeze: True - layer: "penultimate" diff --git a/models/configs/v2-inference_fp32.yaml b/models/configs/v2-inference_fp32.yaml deleted file mode 100644 index 0d03231f3..000000000 --- a/models/configs/v2-inference_fp32.yaml +++ /dev/null @@ -1,67 +0,0 @@ -model: - base_learning_rate: 1.0e-4 - target: ldm.models.diffusion.ddpm.LatentDiffusion - params: - linear_start: 0.00085 - linear_end: 0.0120 - num_timesteps_cond: 1 - log_every_t: 200 - timesteps: 1000 - first_stage_key: "jpg" - cond_stage_key: "txt" - image_size: 64 - channels: 4 - cond_stage_trainable: false - conditioning_key: crossattn - monitor: val/loss_simple_ema - scale_factor: 0.18215 - use_ema: False # we set this to false because this is an inference only config - - unet_config: - target: ldm.modules.diffusionmodules.openaimodel.UNetModel - params: - use_checkpoint: True - use_fp16: False - image_size: 32 # unused - in_channels: 4 - out_channels: 4 - model_channels: 320 - attention_resolutions: [ 4, 2, 1 ] - num_res_blocks: 2 - channel_mult: [ 1, 2, 4, 4 ] - num_head_channels: 64 # need to fix for flash-attn - use_spatial_transformer: True - use_linear_in_transformer: True - transformer_depth: 1 - context_dim: 1024 - legacy: False - - first_stage_config: - target: ldm.models.autoencoder.AutoencoderKL - params: - embed_dim: 4 - monitor: val/rec_loss - ddconfig: - #attn_type: "vanilla-xformers" - double_z: true - z_channels: 4 - resolution: 256 - in_channels: 3 - out_ch: 3 - ch: 128 - ch_mult: - - 1 - - 2 - - 4 - - 4 - num_res_blocks: 2 - attn_resolutions: [] - dropout: 0.0 - lossconfig: - target: torch.nn.Identity - - cond_stage_config: - target: ldm.modules.encoders.modules.FrozenOpenCLIPEmbedder - params: - freeze: True - layer: "penultimate" diff --git a/models/configs/v2-inpainting-inference.yaml b/models/configs/v2-inpainting-inference.yaml deleted file mode 100644 index 32a9471d7..000000000 --- a/models/configs/v2-inpainting-inference.yaml +++ /dev/null @@ -1,158 +0,0 @@ -model: - base_learning_rate: 5.0e-05 - target: ldm.models.diffusion.ddpm.LatentInpaintDiffusion - params: - linear_start: 0.00085 - linear_end: 0.0120 - num_timesteps_cond: 1 - log_every_t: 200 - timesteps: 1000 - first_stage_key: "jpg" - cond_stage_key: "txt" - image_size: 64 - channels: 4 - cond_stage_trainable: false - conditioning_key: hybrid - scale_factor: 0.18215 - monitor: val/loss_simple_ema - finetune_keys: null - use_ema: False - - unet_config: - target: ldm.modules.diffusionmodules.openaimodel.UNetModel - params: - use_checkpoint: True - image_size: 32 # unused - in_channels: 9 - out_channels: 4 - model_channels: 320 - attention_resolutions: [ 4, 2, 1 ] - num_res_blocks: 2 - channel_mult: [ 1, 2, 4, 4 ] - num_head_channels: 64 # need to fix for flash-attn - use_spatial_transformer: True - use_linear_in_transformer: True - transformer_depth: 1 - context_dim: 1024 - legacy: False - - first_stage_config: - target: ldm.models.autoencoder.AutoencoderKL - params: - embed_dim: 4 - monitor: val/rec_loss - ddconfig: - #attn_type: "vanilla-xformers" - double_z: true - z_channels: 4 - resolution: 256 - in_channels: 3 - out_ch: 3 - ch: 128 - ch_mult: - - 1 - - 2 - - 4 - - 4 - num_res_blocks: 2 - attn_resolutions: [ ] - dropout: 0.0 - lossconfig: - target: torch.nn.Identity - - cond_stage_config: - target: ldm.modules.encoders.modules.FrozenOpenCLIPEmbedder - params: - freeze: True - layer: "penultimate" - - -data: - target: ldm.data.laion.WebDataModuleFromConfig - params: - tar_base: null # for concat as in LAION-A - p_unsafe_threshold: 0.1 - filter_word_list: "data/filters.yaml" - max_pwatermark: 0.45 - batch_size: 8 - num_workers: 6 - multinode: True - min_size: 512 - train: - shards: - - "pipe:aws s3 cp s3://stability-aws/laion-a-native/part-0/{00000..18699}.tar -" - - "pipe:aws s3 cp s3://stability-aws/laion-a-native/part-1/{00000..18699}.tar -" - - "pipe:aws s3 cp s3://stability-aws/laion-a-native/part-2/{00000..18699}.tar -" - - "pipe:aws s3 cp s3://stability-aws/laion-a-native/part-3/{00000..18699}.tar -" - - "pipe:aws s3 cp s3://stability-aws/laion-a-native/part-4/{00000..18699}.tar -" #{00000-94333}.tar" - shuffle: 10000 - image_key: jpg - image_transforms: - - target: torchvision.transforms.Resize - params: - size: 512 - interpolation: 3 - - target: torchvision.transforms.RandomCrop - params: - size: 512 - postprocess: - target: ldm.data.laion.AddMask - params: - mode: "512train-large" - p_drop: 0.25 - # NOTE use enough shards to avoid empty validation loops in workers - validation: - shards: - - "pipe:aws s3 cp s3://deep-floyd-s3/datasets/laion_cleaned-part5/{93001..94333}.tar - " - shuffle: 0 - image_key: jpg - image_transforms: - - target: torchvision.transforms.Resize - params: - size: 512 - interpolation: 3 - - target: torchvision.transforms.CenterCrop - params: - size: 512 - postprocess: - target: ldm.data.laion.AddMask - params: - mode: "512train-large" - p_drop: 0.25 - -lightning: - find_unused_parameters: True - modelcheckpoint: - params: - every_n_train_steps: 5000 - - callbacks: - metrics_over_trainsteps_checkpoint: - params: - every_n_train_steps: 10000 - - image_logger: - target: main.ImageLogger - params: - enable_autocast: False - disabled: False - batch_frequency: 1000 - max_images: 4 - increase_log_steps: False - log_first_step: False - log_images_kwargs: - use_ema_scope: False - inpaint: False - plot_progressive_rows: False - plot_diffusion_rows: False - N: 4 - unconditional_guidance_scale: 5.0 - unconditional_guidance_label: [""] - ddim_steps: 50 # todo check these out for depth2img, - ddim_eta: 0.0 # todo check these out for depth2img, - - trainer: - benchmark: True - val_check_interval: 5000000 - num_sanity_val_steps: 0 - accumulate_grad_batches: 1 diff --git a/models/controlnet/put_controlnets_and_t2i_here b/models/controlnet/put_controlnets_and_t2i_here deleted file mode 100644 index e69de29bb..000000000 diff --git a/models/diffusers/put_diffusers_models_here b/models/diffusers/put_diffusers_models_here deleted file mode 100644 index e69de29bb..000000000 diff --git a/models/embeddings/put_embeddings_or_textual_inversion_concepts_here b/models/embeddings/put_embeddings_or_textual_inversion_concepts_here deleted file mode 100644 index e69de29bb..000000000 diff --git a/models/gligen/put_gligen_models_here b/models/gligen/put_gligen_models_here deleted file mode 100644 index e69de29bb..000000000 diff --git a/models/hypernetworks/put_hypernetworks_here b/models/hypernetworks/put_hypernetworks_here deleted file mode 100644 index e69de29bb..000000000 diff --git a/models/loras/put_loras_here b/models/loras/put_loras_here deleted file mode 100644 index e69de29bb..000000000 diff --git a/models/photomaker/put_photomaker_models_here b/models/photomaker/put_photomaker_models_here deleted file mode 100644 index e69de29bb..000000000 diff --git a/models/style_models/put_t2i_style_model_here b/models/style_models/put_t2i_style_model_here deleted file mode 100644 index e69de29bb..000000000 diff --git a/models/unet/put_unet_files_here b/models/unet/put_unet_files_here deleted file mode 100644 index e69de29bb..000000000 diff --git a/models/upscale_models/put_esrgan_and_other_upscale_models_here b/models/upscale_models/put_esrgan_and_other_upscale_models_here deleted file mode 100644 index e69de29bb..000000000 diff --git a/models/vae/put_vae_here b/models/vae/put_vae_here deleted file mode 100644 index e69de29bb..000000000 diff --git a/models/vae_approx/put_taesd_encoder_pth_and_taesd_decoder_pth_here b/models/vae_approx/put_taesd_encoder_pth_and_taesd_decoder_pth_here deleted file mode 100644 index e69de29bb..000000000 diff --git a/new_updater.py b/new_updater.py deleted file mode 100644 index a49e0877c..000000000 --- a/new_updater.py +++ /dev/null @@ -1,35 +0,0 @@ -import os -import shutil - -base_path = os.path.dirname(os.path.realpath(__file__)) - - -def update_windows_updater(): - top_path = os.path.dirname(base_path) - updater_path = os.path.join(base_path, ".ci/update_windows/update.py") - bat_path = os.path.join(base_path, ".ci/update_windows/update_comfyui.bat") - - dest_updater_path = os.path.join(top_path, "update/update.py") - dest_bat_path = os.path.join(top_path, "update/update_comfyui.bat") - dest_bat_deps_path = os.path.join(top_path, "update/update_comfyui_and_python_dependencies.bat") - - try: - with open(dest_bat_path, 'rb') as f: - contents = f.read() - except: - return - - if not contents.startswith(b"..\\python_embeded\\python.exe .\\update.py"): - return - - shutil.copy(updater_path, dest_updater_path) - try: - with open(dest_bat_deps_path, 'rb') as f: - contents = f.read() - contents = contents.replace(b'..\\python_embeded\\python.exe .\\update.py ..\\ComfyUI\\', b'call update_comfyui.bat nopause') - with open(dest_bat_deps_path, 'wb') as f: - f.write(contents) - except: - pass - shutil.copy(bat_path, dest_bat_path) - print("Updated the windows standalone package updater.") diff --git a/node_helpers.py b/node_helpers.py index 4b38bfff8..fb5de513a 100644 --- a/node_helpers.py +++ b/node_helpers.py @@ -1,6 +1,6 @@ import hashlib -from comfy.cli_args import args +from totoro.cli_args import args from PIL import ImageFile, UnidentifiedImageError @@ -18,7 +18,7 @@ def pillow(fn, arg): prev_value = None try: x = fn(arg) - except (OSError, UnidentifiedImageError, ValueError): #PIL issues #4472 and #2445, also fixes ComfyUI issue #3416 + except (OSError, UnidentifiedImageError, ValueError): #PIL issues #4472 and #2445, also fixes totoroUI issue #3416 prev_value = ImageFile.LOAD_TRUNCATED_IMAGES ImageFile.LOAD_TRUNCATED_IMAGES = True x = fn(arg) diff --git a/nodes.py b/nodes.py index e296597c5..93ea3b80f 100644 --- a/nodes.py +++ b/nodes.py @@ -16,19 +16,19 @@ from PIL.PngImagePlugin import PngInfo import numpy as np import safetensors.torch -sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy")) +sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "totoro")) -import comfy.diffusers_load -import comfy.samplers -import comfy.sample -import comfy.sd -import comfy.utils -import comfy.controlnet +import totoro.diffusers_load +import totoro.samplers +import totoro.sample +import totoro.sd +import totoro.utils +import totoro.controlnet -import comfy.clip_vision +import totoro.clip_vision -import comfy.model_management -from comfy.cli_args import args +import totoro.model_management +from totoro.cli_args import args import importlib @@ -37,10 +37,10 @@ import latent_preview import node_helpers def before_node_execution(): - comfy.model_management.throw_exception_if_processing_interrupted() + totoro.model_management.throw_exception_if_processing_interrupted() def interrupt_processing(value=True): - comfy.model_management.interrupt_current_processing(value) + totoro.model_management.interrupt_current_processing(value) MAX_RESOLUTION=16384 @@ -408,7 +408,7 @@ class SaveLatent: @classmethod def INPUT_TYPES(s): return {"required": { "samples": ("LATENT", ), - "filename_prefix": ("STRING", {"default": "latents/ComfyUI"})}, + "filename_prefix": ("STRING", {"default": "latents/totoroUI"})}, "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, } RETURN_TYPES = () @@ -418,7 +418,7 @@ class SaveLatent: CATEGORY = "_for_testing" - def save(self, samples, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None): + def save(self, samples, filename_prefix="totoroUI", prompt=None, extra_pnginfo=None): full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir) # support save metadata for latent sharing @@ -448,7 +448,7 @@ class SaveLatent: output["latent_tensor"] = samples["samples"] output["latent_format_version_0"] = torch.tensor([]) - comfy.utils.save_torch_file(output, file, metadata=metadata) + totoro.utils.save_torch_file(output, file, metadata=metadata) return { "ui": { "latents": results } } @@ -501,7 +501,7 @@ class CheckpointLoader: def load_checkpoint(self, config_name, ckpt_name): config_path = folder_paths.get_full_path("configs", config_name) ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name) - return comfy.sd.load_checkpoint(config_path, ckpt_path, output_vae=True, output_clip=True, embedding_directory=folder_paths.get_folder_paths("embeddings")) + return totoro.sd.load_checkpoint(config_path, ckpt_path, output_vae=True, output_clip=True, embedding_directory=folder_paths.get_folder_paths("embeddings")) class CheckpointLoaderSimple: @classmethod @@ -515,7 +515,7 @@ class CheckpointLoaderSimple: def load_checkpoint(self, ckpt_name): ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name) - out = comfy.sd.load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True, embedding_directory=folder_paths.get_folder_paths("embeddings")) + out = totoro.sd.load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True, embedding_directory=folder_paths.get_folder_paths("embeddings")) return out[:3] class DiffusersLoader: @@ -542,7 +542,7 @@ class DiffusersLoader: model_path = path break - return comfy.diffusers_load.load_diffusers(model_path, output_vae=output_vae, output_clip=output_clip, embedding_directory=folder_paths.get_folder_paths("embeddings")) + return totoro.diffusers_load.load_diffusers(model_path, output_vae=output_vae, output_clip=output_clip, embedding_directory=folder_paths.get_folder_paths("embeddings")) class unCLIPCheckpointLoader: @@ -557,7 +557,7 @@ class unCLIPCheckpointLoader: def load_checkpoint(self, ckpt_name, output_vae=True, output_clip=True): ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name) - out = comfy.sd.load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True, output_clipvision=True, embedding_directory=folder_paths.get_folder_paths("embeddings")) + out = totoro.sd.load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True, output_clipvision=True, embedding_directory=folder_paths.get_folder_paths("embeddings")) return out class CLIPSetLastLayer: @@ -608,10 +608,10 @@ class LoraLoader: del temp if lora is None: - lora = comfy.utils.load_torch_file(lora_path, safe_load=True) + lora = totoro.utils.load_torch_file(lora_path, safe_load=True) self.loaded_lora = (lora_path, lora) - model_lora, clip_lora = comfy.sd.load_lora_for_models(model, clip, lora, strength_model, strength_clip) + model_lora, clip_lora = totoro.sd.load_lora_for_models(model, clip, lora, strength_model, strength_clip) return (model_lora, clip_lora) class LoraLoaderModelOnly(LoraLoader): @@ -668,11 +668,11 @@ class VAELoader: encoder = next(filter(lambda a: a.startswith("{}_encoder.".format(name)), approx_vaes)) decoder = next(filter(lambda a: a.startswith("{}_decoder.".format(name)), approx_vaes)) - enc = comfy.utils.load_torch_file(folder_paths.get_full_path("vae_approx", encoder)) + enc = totoro.utils.load_torch_file(folder_paths.get_full_path("vae_approx", encoder)) for k in enc: sd["taesd_encoder.{}".format(k)] = enc[k] - dec = comfy.utils.load_torch_file(folder_paths.get_full_path("vae_approx", decoder)) + dec = totoro.utils.load_torch_file(folder_paths.get_full_path("vae_approx", decoder)) for k in dec: sd["taesd_decoder.{}".format(k)] = dec[k] @@ -701,8 +701,8 @@ class VAELoader: sd = self.load_taesd(vae_name) else: vae_path = folder_paths.get_full_path("vae", vae_name) - sd = comfy.utils.load_torch_file(vae_path) - vae = comfy.sd.VAE(sd=sd) + sd = totoro.utils.load_torch_file(vae_path) + vae = totoro.sd.VAE(sd=sd) return (vae,) class ControlNetLoader: @@ -717,7 +717,7 @@ class ControlNetLoader: def load_controlnet(self, control_net_name): controlnet_path = folder_paths.get_full_path("controlnet", control_net_name) - controlnet = comfy.controlnet.load_controlnet(controlnet_path) + controlnet = totoro.controlnet.load_controlnet(controlnet_path) return (controlnet,) class DiffControlNetLoader: @@ -733,7 +733,7 @@ class DiffControlNetLoader: def load_controlnet(self, model, control_net_name): controlnet_path = folder_paths.get_full_path("controlnet", control_net_name) - controlnet = comfy.controlnet.load_controlnet(controlnet_path, model) + controlnet = totoro.controlnet.load_controlnet(controlnet_path, model) return (controlnet,) @@ -833,7 +833,7 @@ class UNETLoader: dtype = torch.float8_e5m2 unet_path = folder_paths.get_full_path("unet", unet_name) - model = comfy.sd.load_unet(unet_path, dtype=dtype) + model = totoro.sd.load_unet(unet_path, dtype=dtype) return (model,) class CLIPLoader: @@ -849,16 +849,16 @@ class CLIPLoader: def load_clip(self, clip_name, type="stable_diffusion"): if type == "stable_cascade": - clip_type = comfy.sd.CLIPType.STABLE_CASCADE + clip_type = totoro.sd.CLIPType.STABLE_CASCADE elif type == "sd3": - clip_type = comfy.sd.CLIPType.SD3 + clip_type = totoro.sd.CLIPType.SD3 elif type == "stable_audio": - clip_type = comfy.sd.CLIPType.STABLE_AUDIO + clip_type = totoro.sd.CLIPType.STABLE_AUDIO else: - clip_type = comfy.sd.CLIPType.STABLE_DIFFUSION + clip_type = totoro.sd.CLIPType.STABLE_DIFFUSION clip_path = folder_paths.get_full_path("clip", clip_name) - clip = comfy.sd.load_clip(ckpt_paths=[clip_path], embedding_directory=folder_paths.get_folder_paths("embeddings"), clip_type=clip_type) + clip = totoro.sd.load_clip(ckpt_paths=[clip_path], embedding_directory=folder_paths.get_folder_paths("embeddings"), clip_type=clip_type) return (clip,) class DualCLIPLoader: @@ -877,13 +877,13 @@ class DualCLIPLoader: clip_path1 = folder_paths.get_full_path("clip", clip_name1) clip_path2 = folder_paths.get_full_path("clip", clip_name2) if type == "sdxl": - clip_type = comfy.sd.CLIPType.STABLE_DIFFUSION + clip_type = totoro.sd.CLIPType.STABLE_DIFFUSION elif type == "sd3": - clip_type = comfy.sd.CLIPType.SD3 + clip_type = totoro.sd.CLIPType.SD3 elif type == "flux": - clip_type = comfy.sd.CLIPType.FLUX + clip_type = totoro.sd.CLIPType.FLUX - clip = comfy.sd.load_clip(ckpt_paths=[clip_path1, clip_path2], embedding_directory=folder_paths.get_folder_paths("embeddings"), clip_type=clip_type) + clip = totoro.sd.load_clip(ckpt_paths=[clip_path1, clip_path2], embedding_directory=folder_paths.get_folder_paths("embeddings"), clip_type=clip_type) return (clip,) class CLIPVisionLoader: @@ -898,7 +898,7 @@ class CLIPVisionLoader: def load_clip(self, clip_name): clip_path = folder_paths.get_full_path("clip_vision", clip_name) - clip_vision = comfy.clip_vision.load(clip_path) + clip_vision = totoro.clip_vision.load(clip_path) return (clip_vision,) class CLIPVisionEncode: @@ -928,7 +928,7 @@ class StyleModelLoader: def load_style_model(self, style_model_name): style_model_path = folder_paths.get_full_path("style_models", style_model_name) - style_model = comfy.sd.load_style_model(style_model_path) + style_model = totoro.sd.load_style_model(style_model_path) return (style_model,) @@ -993,7 +993,7 @@ class GLIGENLoader: def load_gligen(self, gligen_name): gligen_path = folder_paths.get_full_path("gligen", gligen_name) - gligen = comfy.sd.load_gligen(gligen_path) + gligen = totoro.sd.load_gligen(gligen_path) return (gligen,) class GLIGENTextBoxApply: @@ -1029,7 +1029,7 @@ class GLIGENTextBoxApply: class EmptyLatentImage: def __init__(self): - self.device = comfy.model_management.intermediate_device() + self.device = totoro.model_management.intermediate_device() @classmethod def INPUT_TYPES(s): @@ -1135,7 +1135,7 @@ class LatentUpscale: width = max(64, width) height = max(64, height) - s["samples"] = comfy.utils.common_upscale(samples["samples"], width // 8, height // 8, upscale_method, crop) + s["samples"] = totoro.utils.common_upscale(samples["samples"], width // 8, height // 8, upscale_method, crop) return (s,) class LatentUpscaleBy: @@ -1154,7 +1154,7 @@ class LatentUpscaleBy: s = samples.copy() width = round(samples["samples"].shape[3] * scale_by) height = round(samples["samples"].shape[2] * scale_by) - s["samples"] = comfy.utils.common_upscale(samples["samples"], width, height, upscale_method, "disabled") + s["samples"] = totoro.utils.common_upscale(samples["samples"], width, height, upscale_method, "disabled") return (s,) class LatentRotate: @@ -1270,7 +1270,7 @@ class LatentBlend: if samples1.shape != samples2.shape: samples2.permute(0, 3, 1, 2) - samples2 = comfy.utils.common_upscale(samples2, samples1.shape[3], samples1.shape[2], 'bicubic', crop='center') + samples2 = totoro.utils.common_upscale(samples2, samples1.shape[3], samples1.shape[2], 'bicubic', crop='center') samples2.permute(0, 2, 3, 1) samples_blended = self.blend_mode(samples1, samples2, blend_mode) @@ -1335,21 +1335,21 @@ class SetLatentNoiseMask: def common_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent, denoise=1.0, disable_noise=False, start_step=None, last_step=None, force_full_denoise=False): latent_image = latent["samples"] - latent_image = comfy.sample.fix_empty_latent_channels(model, latent_image) + latent_image = totoro.sample.fix_empty_latent_channels(model, latent_image) if disable_noise: noise = torch.zeros(latent_image.size(), dtype=latent_image.dtype, layout=latent_image.layout, device="cpu") else: batch_inds = latent["batch_index"] if "batch_index" in latent else None - noise = comfy.sample.prepare_noise(latent_image, seed, batch_inds) + noise = totoro.sample.prepare_noise(latent_image, seed, batch_inds) noise_mask = None if "noise_mask" in latent: noise_mask = latent["noise_mask"] callback = latent_preview.prepare_callback(model, steps) - disable_pbar = not comfy.utils.PROGRESS_BAR_ENABLED - samples = comfy.sample.sample(model, noise, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, + disable_pbar = not totoro.utils.PROGRESS_BAR_ENABLED + samples = totoro.sample.sample(model, noise, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise=denoise, disable_noise=disable_noise, start_step=start_step, last_step=last_step, force_full_denoise=force_full_denoise, noise_mask=noise_mask, callback=callback, disable_pbar=disable_pbar, seed=seed) out = latent.copy() @@ -1364,8 +1364,8 @@ class KSampler: "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), "steps": ("INT", {"default": 20, "min": 1, "max": 10000}), "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step":0.1, "round": 0.01}), - "sampler_name": (comfy.samplers.KSampler.SAMPLERS, ), - "scheduler": (comfy.samplers.KSampler.SCHEDULERS, ), + "sampler_name": (totoro.samplers.KSampler.SAMPLERS, ), + "scheduler": (totoro.samplers.KSampler.SCHEDULERS, ), "positive": ("CONDITIONING", ), "negative": ("CONDITIONING", ), "latent_image": ("LATENT", ), @@ -1390,8 +1390,8 @@ class KSamplerAdvanced: "noise_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), "steps": ("INT", {"default": 20, "min": 1, "max": 10000}), "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step":0.1, "round": 0.01}), - "sampler_name": (comfy.samplers.KSampler.SAMPLERS, ), - "scheduler": (comfy.samplers.KSampler.SCHEDULERS, ), + "sampler_name": (totoro.samplers.KSampler.SAMPLERS, ), + "scheduler": (totoro.samplers.KSampler.SCHEDULERS, ), "positive": ("CONDITIONING", ), "negative": ("CONDITIONING", ), "latent_image": ("LATENT", ), @@ -1426,7 +1426,7 @@ class SaveImage: def INPUT_TYPES(s): return {"required": {"images": ("IMAGE", ), - "filename_prefix": ("STRING", {"default": "ComfyUI"})}, + "filename_prefix": ("STRING", {"default": "totoroUI"})}, "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, } @@ -1437,7 +1437,7 @@ class SaveImage: CATEGORY = "image" - def save_images(self, images, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None): + def save_images(self, images, filename_prefix="totoroUI", prompt=None, extra_pnginfo=None): filename_prefix += self.prefix_append full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0]) results = list() @@ -1626,7 +1626,7 @@ class ImageScale: elif height == 0: height = max(1, round(samples.shape[2] * width / samples.shape[3])) - s = comfy.utils.common_upscale(samples, width, height, upscale_method, crop) + s = totoro.utils.common_upscale(samples, width, height, upscale_method, crop) s = s.movedim(1,-1) return (s,) @@ -1646,7 +1646,7 @@ class ImageScaleBy: samples = image.movedim(-1,1) width = round(samples.shape[3] * scale_by) height = round(samples.shape[2] * scale_by) - s = comfy.utils.common_upscale(samples, width, height, upscale_method, "disabled") + s = totoro.utils.common_upscale(samples, width, height, upscale_method, "disabled") s = s.movedim(1,-1) return (s,) @@ -1678,7 +1678,7 @@ class ImageBatch: def batch(self, image1, image2): if image1.shape[1:] != image2.shape[1:]: - image2 = comfy.utils.common_upscale(image2.movedim(-1,1), image1.shape[2], image1.shape[1], "bilinear", "center").movedim(1,-1) + image2 = totoro.utils.common_upscale(image2.movedim(-1,1), image1.shape[2], image1.shape[1], "bilinear", "center").movedim(1,-1) s = torch.cat((image1, image2), dim=0) return (s,) @@ -1904,13 +1904,13 @@ def get_module_name(module_path: str) -> str: """ Returns the module name based on the given module path. Examples: - get_module_name("C:/Users/username/ComfyUI/custom_nodes/my_custom_node.py") -> "my_custom_node" - get_module_name("C:/Users/username/ComfyUI/custom_nodes/my_custom_node") -> "my_custom_node" - get_module_name("C:/Users/username/ComfyUI/custom_nodes/my_custom_node/") -> "my_custom_node" - get_module_name("C:/Users/username/ComfyUI/custom_nodes/my_custom_node/__init__.py") -> "my_custom_node" - get_module_name("C:/Users/username/ComfyUI/custom_nodes/my_custom_node/__init__") -> "my_custom_node" - get_module_name("C:/Users/username/ComfyUI/custom_nodes/my_custom_node/__init__/") -> "my_custom_node" - get_module_name("C:/Users/username/ComfyUI/custom_nodes/my_custom_node.disabled") -> "custom_nodes + get_module_name("C:/Users/username/totoroUI/custom_nodes/my_custom_node.py") -> "my_custom_node" + get_module_name("C:/Users/username/totoroUI/custom_nodes/my_custom_node") -> "my_custom_node" + get_module_name("C:/Users/username/totoroUI/custom_nodes/my_custom_node/") -> "my_custom_node" + get_module_name("C:/Users/username/totoroUI/custom_nodes/my_custom_node/__init__.py") -> "my_custom_node" + get_module_name("C:/Users/username/totoroUI/custom_nodes/my_custom_node/__init__") -> "my_custom_node" + get_module_name("C:/Users/username/totoroUI/custom_nodes/my_custom_node/__init__/") -> "my_custom_node" + get_module_name("C:/Users/username/totoroUI/custom_nodes/my_custom_node.disabled") -> "custom_nodes Args: module_path (str): The path of the module. Returns: @@ -1999,15 +1999,15 @@ def init_external_custom_nodes(): def init_builtin_extra_nodes(): """ - Initializes the built-in extra nodes in ComfyUI. + Initializes the built-in extra nodes in totoroUI. - This function loads the extra node files located in the "comfy_extras" directory and imports them into ComfyUI. + This function loads the extra node files located in the "totoro_extras" directory and imports them into totoroUI. If any of the extra node files fail to import, a warning message is logged. Returns: None """ - extras_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy_extras") + extras_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "totoro_extras") extras_files = [ "nodes_latent.py", "nodes_hypernetwork.py", @@ -2053,7 +2053,7 @@ def init_builtin_extra_nodes(): import_failed = [] for node_file in extras_files: - if not load_custom_node(os.path.join(extras_dir, node_file), module_parent="comfy_extras"): + if not load_custom_node(os.path.join(extras_dir, node_file), module_parent="totoro_extras"): import_failed.append(node_file) return import_failed @@ -2068,12 +2068,12 @@ def init_extra_nodes(init_custom_nodes=True): logging.info("Skipping loading of custom nodes") if len(import_failed) > 0: - logging.warning("WARNING: some comfy_extras/ nodes did not import correctly. This may be because they are missing some dependencies.\n") + logging.warning("WARNING: some totoro_extras/ nodes did not import correctly. This may be because they are missing some dependencies.\n") for node in import_failed: logging.warning("IMPORT FAILED: {}".format(node)) - logging.warning("\nThis issue might be caused by new missing dependencies added the last time you updated ComfyUI.") + logging.warning("\nThis issue might be caused by new missing dependencies added the last time you updated totoroUI.") if args.windows_standalone_build: - logging.warning("Please run the update script: update/update_comfyui.bat") + logging.warning("Please run the update script: update/update_totoroui.bat") else: logging.warning("Please do a: pip install -r requirements.txt") logging.warning("") diff --git a/notebooks/comfyui_colab.ipynb b/notebooks/comfyui_colab.ipynb deleted file mode 100644 index ec83265b4..000000000 --- a/notebooks/comfyui_colab.ipynb +++ /dev/null @@ -1,329 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "id": "aaaaaaaaaa" - }, - "source": [ - "Git clone the repo and install the requirements. (ignore the pip errors about protobuf)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "bbbbbbbbbb" - }, - "outputs": [], - "source": [ - "#@title Environment Setup\n", - "\n", - "from pathlib import Path\n", - "\n", - "OPTIONS = {}\n", - "\n", - "USE_GOOGLE_DRIVE = False #@param {type:\"boolean\"}\n", - "UPDATE_COMFY_UI = True #@param {type:\"boolean\"}\n", - "WORKSPACE = 'ComfyUI'\n", - "OPTIONS['USE_GOOGLE_DRIVE'] = USE_GOOGLE_DRIVE\n", - "OPTIONS['UPDATE_COMFY_UI'] = UPDATE_COMFY_UI\n", - "\n", - "if OPTIONS['USE_GOOGLE_DRIVE']:\n", - " !echo \"Mounting Google Drive...\"\n", - " %cd /\n", - " \n", - " from google.colab import drive\n", - " drive.mount('/content/drive')\n", - "\n", - " WORKSPACE = \"/content/drive/MyDrive/ComfyUI\"\n", - " %cd /content/drive/MyDrive\n", - "\n", - "![ ! -d $WORKSPACE ] && echo -= Initial setup ComfyUI =- && git clone https://github.com/comfyanonymous/ComfyUI\n", - "%cd $WORKSPACE\n", - "\n", - "if OPTIONS['UPDATE_COMFY_UI']:\n", - " !echo -= Updating ComfyUI =-\n", - " !git pull\n", - "\n", - "!echo -= Install dependencies =-\n", - "!pip install xformers!=0.0.18 -r requirements.txt --extra-index-url https://download.pytorch.org/whl/cu121 --extra-index-url https://download.pytorch.org/whl/cu118 --extra-index-url https://download.pytorch.org/whl/cu117" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "cccccccccc" - }, - "source": [ - "Download some models/checkpoints/vae or custom comfyui nodes (uncomment the commands for the ones you want)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "dddddddddd" - }, - "outputs": [], - "source": [ - "# Checkpoints\n", - "\n", - "### SDXL\n", - "### I recommend these workflow examples: https://comfyanonymous.github.io/ComfyUI_examples/sdxl/\n", - "\n", - "#!wget -c https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_base_1.0.safetensors -P ./models/checkpoints/\n", - "#!wget -c https://huggingface.co/stabilityai/stable-diffusion-xl-refiner-1.0/resolve/main/sd_xl_refiner_1.0.safetensors -P ./models/checkpoints/\n", - "\n", - "# SDXL ReVision\n", - "#!wget -c https://huggingface.co/comfyanonymous/clip_vision_g/resolve/main/clip_vision_g.safetensors -P ./models/clip_vision/\n", - "\n", - "# SD1.5\n", - "!wget -c https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.ckpt -P ./models/checkpoints/\n", - "\n", - "# SD2\n", - "#!wget -c https://huggingface.co/stabilityai/stable-diffusion-2-1-base/resolve/main/v2-1_512-ema-pruned.safetensors -P ./models/checkpoints/\n", - "#!wget -c https://huggingface.co/stabilityai/stable-diffusion-2-1/resolve/main/v2-1_768-ema-pruned.safetensors -P ./models/checkpoints/\n", - "\n", - "# Some SD1.5 anime style\n", - "#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix2/AbyssOrangeMix2_hard.safetensors -P ./models/checkpoints/\n", - "#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix3/AOM3A1_orangemixs.safetensors -P ./models/checkpoints/\n", - "#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix3/AOM3A3_orangemixs.safetensors -P ./models/checkpoints/\n", - "#!wget -c https://huggingface.co/Linaqruf/anything-v3.0/resolve/main/anything-v3-fp16-pruned.safetensors -P ./models/checkpoints/\n", - "\n", - "# Waifu Diffusion 1.5 (anime style SD2.x 768-v)\n", - "#!wget -c https://huggingface.co/waifu-diffusion/wd-1-5-beta3/resolve/main/wd-illusion-fp16.safetensors -P ./models/checkpoints/\n", - "\n", - "\n", - "# unCLIP models\n", - "#!wget -c https://huggingface.co/comfyanonymous/illuminatiDiffusionV1_v11_unCLIP/resolve/main/illuminatiDiffusionV1_v11-unclip-h-fp16.safetensors -P ./models/checkpoints/\n", - "#!wget -c https://huggingface.co/comfyanonymous/wd-1.5-beta2_unCLIP/resolve/main/wd-1-5-beta2-aesthetic-unclip-h-fp16.safetensors -P ./models/checkpoints/\n", - "\n", - "\n", - "# VAE\n", - "!wget -c https://huggingface.co/stabilityai/sd-vae-ft-mse-original/resolve/main/vae-ft-mse-840000-ema-pruned.safetensors -P ./models/vae/\n", - "#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/VAEs/orangemix.vae.pt -P ./models/vae/\n", - "#!wget -c https://huggingface.co/hakurei/waifu-diffusion-v1-4/resolve/main/vae/kl-f8-anime2.ckpt -P ./models/vae/\n", - "\n", - "\n", - "# Loras\n", - "#!wget -c https://civitai.com/api/download/models/10350 -O ./models/loras/theovercomer8sContrastFix_sd21768.safetensors #theovercomer8sContrastFix SD2.x 768-v\n", - "#!wget -c https://civitai.com/api/download/models/10638 -O ./models/loras/theovercomer8sContrastFix_sd15.safetensors #theovercomer8sContrastFix SD1.x\n", - "#!wget -c https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_offset_example-lora_1.0.safetensors -P ./models/loras/ #SDXL offset noise lora\n", - "\n", - "\n", - "# T2I-Adapter\n", - "#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_depth_sd14v1.pth -P ./models/controlnet/\n", - "#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_seg_sd14v1.pth -P ./models/controlnet/\n", - "#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_sketch_sd14v1.pth -P ./models/controlnet/\n", - "#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_keypose_sd14v1.pth -P ./models/controlnet/\n", - "#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_openpose_sd14v1.pth -P ./models/controlnet/\n", - "#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_color_sd14v1.pth -P ./models/controlnet/\n", - "#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_canny_sd14v1.pth -P ./models/controlnet/\n", - "\n", - "# T2I Styles Model\n", - "#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_style_sd14v1.pth -P ./models/style_models/\n", - "\n", - "# CLIPVision model (needed for styles model)\n", - "#!wget -c https://huggingface.co/openai/clip-vit-large-patch14/resolve/main/pytorch_model.bin -O ./models/clip_vision/clip_vit14.bin\n", - "\n", - "\n", - "# ControlNet\n", - "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11e_sd15_ip2p_fp16.safetensors -P ./models/controlnet/\n", - "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11e_sd15_shuffle_fp16.safetensors -P ./models/controlnet/\n", - "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_canny_fp16.safetensors -P ./models/controlnet/\n", - "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11f1p_sd15_depth_fp16.safetensors -P ./models/controlnet/\n", - "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_inpaint_fp16.safetensors -P ./models/controlnet/\n", - "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_lineart_fp16.safetensors -P ./models/controlnet/\n", - "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_mlsd_fp16.safetensors -P ./models/controlnet/\n", - "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_normalbae_fp16.safetensors -P ./models/controlnet/\n", - "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_openpose_fp16.safetensors -P ./models/controlnet/\n", - "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_scribble_fp16.safetensors -P ./models/controlnet/\n", - "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_seg_fp16.safetensors -P ./models/controlnet/\n", - "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_softedge_fp16.safetensors -P ./models/controlnet/\n", - "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15s2_lineart_anime_fp16.safetensors -P ./models/controlnet/\n", - "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11u_sd15_tile_fp16.safetensors -P ./models/controlnet/\n", - "\n", - "# ControlNet SDXL\n", - "#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-canny-rank256.safetensors -P ./models/controlnet/\n", - "#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-depth-rank256.safetensors -P ./models/controlnet/\n", - "#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-recolor-rank256.safetensors -P ./models/controlnet/\n", - "#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-sketch-rank256.safetensors -P ./models/controlnet/\n", - "\n", - "# Controlnet Preprocessor nodes by Fannovel16\n", - "#!cd custom_nodes && git clone https://github.com/Fannovel16/comfy_controlnet_preprocessors; cd comfy_controlnet_preprocessors && python install.py\n", - "\n", - "\n", - "# GLIGEN\n", - "#!wget -c https://huggingface.co/comfyanonymous/GLIGEN_pruned_safetensors/resolve/main/gligen_sd14_textbox_pruned_fp16.safetensors -P ./models/gligen/\n", - "\n", - "\n", - "# ESRGAN upscale model\n", - "#!wget -c https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth -P ./models/upscale_models/\n", - "#!wget -c https://huggingface.co/sberbank-ai/Real-ESRGAN/resolve/main/RealESRGAN_x2.pth -P ./models/upscale_models/\n", - "#!wget -c https://huggingface.co/sberbank-ai/Real-ESRGAN/resolve/main/RealESRGAN_x4.pth -P ./models/upscale_models/\n", - "\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "kkkkkkkkkkkkkkk" - }, - "source": [ - "### Run ComfyUI with cloudflared (Recommended Way)\n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "jjjjjjjjjjjjjj" - }, - "outputs": [], - "source": [ - "!wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb\n", - "!dpkg -i cloudflared-linux-amd64.deb\n", - "\n", - "import subprocess\n", - "import threading\n", - "import time\n", - "import socket\n", - "import urllib.request\n", - "\n", - "def iframe_thread(port):\n", - " while True:\n", - " time.sleep(0.5)\n", - " sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n", - " result = sock.connect_ex(('127.0.0.1', port))\n", - " if result == 0:\n", - " break\n", - " sock.close()\n", - " print(\"\\nComfyUI finished loading, trying to launch cloudflared (if it gets stuck here cloudflared is having issues)\\n\")\n", - "\n", - " p = subprocess.Popen([\"cloudflared\", \"tunnel\", \"--url\", \"http://127.0.0.1:{}\".format(port)], stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n", - " for line in p.stderr:\n", - " l = line.decode()\n", - " if \"trycloudflare.com \" in l:\n", - " print(\"This is the URL to access ComfyUI:\", l[l.find(\"http\"):], end='')\n", - " #print(l, end='')\n", - "\n", - "\n", - "threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n", - "\n", - "!python main.py --dont-print-server" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "kkkkkkkkkkkkkk" - }, - "source": [ - "### Run ComfyUI with localtunnel\n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "jjjjjjjjjjjjj" - }, - "outputs": [], - "source": [ - "!npm install -g localtunnel\n", - "\n", - "import subprocess\n", - "import threading\n", - "import time\n", - "import socket\n", - "import urllib.request\n", - "\n", - "def iframe_thread(port):\n", - " while True:\n", - " time.sleep(0.5)\n", - " sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n", - " result = sock.connect_ex(('127.0.0.1', port))\n", - " if result == 0:\n", - " break\n", - " sock.close()\n", - " print(\"\\nComfyUI finished loading, trying to launch localtunnel (if it gets stuck here localtunnel is having issues)\\n\")\n", - "\n", - " print(\"The password/enpoint ip for localtunnel is:\", urllib.request.urlopen('https://ipv4.icanhazip.com').read().decode('utf8').strip(\"\\n\"))\n", - " p = subprocess.Popen([\"lt\", \"--port\", \"{}\".format(port)], stdout=subprocess.PIPE)\n", - " for line in p.stdout:\n", - " print(line.decode(), end='')\n", - "\n", - "\n", - "threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n", - "\n", - "!python main.py --dont-print-server" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "gggggggggg" - }, - "source": [ - "### Run ComfyUI with colab iframe (use only in case the previous way with localtunnel doesn't work)\n", - "\n", - "You should see the ui appear in an iframe. If you get a 403 error, it's your firefox settings or an extension that's messing things up.\n", - "\n", - "If you want to open it in another window use the link.\n", - "\n", - "Note that some UI features like live image previews won't work because the colab iframe blocks websockets." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "hhhhhhhhhh" - }, - "outputs": [], - "source": [ - "import threading\n", - "import time\n", - "import socket\n", - "def iframe_thread(port):\n", - " while True:\n", - " time.sleep(0.5)\n", - " sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n", - " result = sock.connect_ex(('127.0.0.1', port))\n", - " if result == 0:\n", - " break\n", - " sock.close()\n", - " from google.colab import output\n", - " output.serve_kernel_port_as_iframe(port, height=1024)\n", - " print(\"to open it in a window you can open this link here:\")\n", - " output.serve_kernel_port_as_window(port)\n", - "\n", - "threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n", - "\n", - "!python main.py --dont-print-server" - ] - } - ], - "metadata": { - "accelerator": "GPU", - "colab": { - "provenance": [] - }, - "gpuClass": "standard", - "kernelspec": { - "display_name": "Python 3", - "name": "python3" - }, - "language_info": { - "name": "python" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/output/_output_images_will_be_put_here b/output/_output_images_will_be_put_here deleted file mode 100644 index e69de29bb..000000000 diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 8b7a747e7..000000000 --- a/pytest.ini +++ /dev/null @@ -1,8 +0,0 @@ -[pytest] -markers = - inference: mark as inference test (deselect with '-m "not inference"') -testpaths = - tests - tests-unit -addopts = -s -pythonpath = . diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 4c2c0b2b2..000000000 --- a/requirements.txt +++ /dev/null @@ -1,20 +0,0 @@ -torch -torchsde -torchvision -torchaudio -einops -transformers>=4.28.1 -tokenizers>=0.13.3 -sentencepiece -safetensors>=0.4.2 -aiohttp -pyyaml -Pillow -scipy -tqdm -psutil - -#non essential dependencies: -kornia>=0.7.1 -spandrel -soundfile diff --git a/script_examples/basic_api_example.py b/script_examples/basic_api_example.py deleted file mode 100644 index 242d3175f..000000000 --- a/script_examples/basic_api_example.py +++ /dev/null @@ -1,120 +0,0 @@ -import json -from urllib import request, parse -import random - -#This is the ComfyUI api prompt format. - -#If you want it for a specific workflow you can "enable dev mode options" -#in the settings of the UI (gear beside the "Queue Size: ") this will enable -#a button on the UI to save workflows in api format. - -#keep in mind ComfyUI is pre alpha software so this format will change a bit. - -#this is the one for the default workflow -prompt_text = """ -{ - "3": { - "class_type": "KSampler", - "inputs": { - "cfg": 8, - "denoise": 1, - "latent_image": [ - "5", - 0 - ], - "model": [ - "4", - 0 - ], - "negative": [ - "7", - 0 - ], - "positive": [ - "6", - 0 - ], - "sampler_name": "euler", - "scheduler": "normal", - "seed": 8566257, - "steps": 20 - } - }, - "4": { - "class_type": "CheckpointLoaderSimple", - "inputs": { - "ckpt_name": "v1-5-pruned-emaonly.ckpt" - } - }, - "5": { - "class_type": "EmptyLatentImage", - "inputs": { - "batch_size": 1, - "height": 512, - "width": 512 - } - }, - "6": { - "class_type": "CLIPTextEncode", - "inputs": { - "clip": [ - "4", - 1 - ], - "text": "masterpiece best quality girl" - } - }, - "7": { - "class_type": "CLIPTextEncode", - "inputs": { - "clip": [ - "4", - 1 - ], - "text": "bad hands" - } - }, - "8": { - "class_type": "VAEDecode", - "inputs": { - "samples": [ - "3", - 0 - ], - "vae": [ - "4", - 2 - ] - } - }, - "9": { - "class_type": "SaveImage", - "inputs": { - "filename_prefix": "ComfyUI", - "images": [ - "8", - 0 - ] - } - } -} -""" - -def queue_prompt(prompt): - p = {"prompt": prompt} - data = json.dumps(p).encode('utf-8') - req = request.Request("http://127.0.0.1:8188/prompt", data=data) - request.urlopen(req) - - -prompt = json.loads(prompt_text) -#set the text prompt for our positive CLIPTextEncode -prompt["6"]["inputs"]["text"] = "masterpiece best quality man" - -#set the seed for our KSampler node -prompt["3"]["inputs"]["seed"] = 5 - - -queue_prompt(prompt) - - diff --git a/script_examples/websockets_api_example.py b/script_examples/websockets_api_example.py deleted file mode 100644 index 57a6cbd9b..000000000 --- a/script_examples/websockets_api_example.py +++ /dev/null @@ -1,164 +0,0 @@ -#This is an example that uses the websockets api to know when a prompt execution is done -#Once the prompt execution is done it downloads the images using the /history endpoint - -import websocket #NOTE: websocket-client (https://github.com/websocket-client/websocket-client) -import uuid -import json -import urllib.request -import urllib.parse - -server_address = "127.0.0.1:8188" -client_id = str(uuid.uuid4()) - -def queue_prompt(prompt): - p = {"prompt": prompt, "client_id": client_id} - data = json.dumps(p).encode('utf-8') - req = urllib.request.Request("http://{}/prompt".format(server_address), data=data) - return json.loads(urllib.request.urlopen(req).read()) - -def get_image(filename, subfolder, folder_type): - data = {"filename": filename, "subfolder": subfolder, "type": folder_type} - url_values = urllib.parse.urlencode(data) - with urllib.request.urlopen("http://{}/view?{}".format(server_address, url_values)) as response: - return response.read() - -def get_history(prompt_id): - with urllib.request.urlopen("http://{}/history/{}".format(server_address, prompt_id)) as response: - return json.loads(response.read()) - -def get_images(ws, prompt): - prompt_id = queue_prompt(prompt)['prompt_id'] - output_images = {} - while True: - out = ws.recv() - if isinstance(out, str): - message = json.loads(out) - if message['type'] == 'executing': - data = message['data'] - if data['node'] is None and data['prompt_id'] == prompt_id: - break #Execution is done - else: - continue #previews are binary data - - history = get_history(prompt_id)[prompt_id] - for o in history['outputs']: - for node_id in history['outputs']: - node_output = history['outputs'][node_id] - if 'images' in node_output: - images_output = [] - for image in node_output['images']: - image_data = get_image(image['filename'], image['subfolder'], image['type']) - images_output.append(image_data) - output_images[node_id] = images_output - - return output_images - -prompt_text = """ -{ - "3": { - "class_type": "KSampler", - "inputs": { - "cfg": 8, - "denoise": 1, - "latent_image": [ - "5", - 0 - ], - "model": [ - "4", - 0 - ], - "negative": [ - "7", - 0 - ], - "positive": [ - "6", - 0 - ], - "sampler_name": "euler", - "scheduler": "normal", - "seed": 8566257, - "steps": 20 - } - }, - "4": { - "class_type": "CheckpointLoaderSimple", - "inputs": { - "ckpt_name": "v1-5-pruned-emaonly.ckpt" - } - }, - "5": { - "class_type": "EmptyLatentImage", - "inputs": { - "batch_size": 1, - "height": 512, - "width": 512 - } - }, - "6": { - "class_type": "CLIPTextEncode", - "inputs": { - "clip": [ - "4", - 1 - ], - "text": "masterpiece best quality girl" - } - }, - "7": { - "class_type": "CLIPTextEncode", - "inputs": { - "clip": [ - "4", - 1 - ], - "text": "bad hands" - } - }, - "8": { - "class_type": "VAEDecode", - "inputs": { - "samples": [ - "3", - 0 - ], - "vae": [ - "4", - 2 - ] - } - }, - "9": { - "class_type": "SaveImage", - "inputs": { - "filename_prefix": "ComfyUI", - "images": [ - "8", - 0 - ] - } - } -} -""" - -prompt = json.loads(prompt_text) -#set the text prompt for our positive CLIPTextEncode -prompt["6"]["inputs"]["text"] = "masterpiece best quality man" - -#set the seed for our KSampler node -prompt["3"]["inputs"]["seed"] = 5 - -ws = websocket.WebSocket() -ws.connect("ws://{}/ws?clientId={}".format(server_address, client_id)) -images = get_images(ws, prompt) - -#Commented out code to display the output images: - -# for node_id in images: -# for image_data in images[node_id]: -# from PIL import Image -# import io -# image = Image.open(io.BytesIO(image_data)) -# image.show() - diff --git a/script_examples/websockets_api_example_ws_images.py b/script_examples/websockets_api_example_ws_images.py deleted file mode 100644 index 737488621..000000000 --- a/script_examples/websockets_api_example_ws_images.py +++ /dev/null @@ -1,159 +0,0 @@ -#This is an example that uses the websockets api and the SaveImageWebsocket node to get images directly without -#them being saved to disk - -import websocket #NOTE: websocket-client (https://github.com/websocket-client/websocket-client) -import uuid -import json -import urllib.request -import urllib.parse - -server_address = "127.0.0.1:8188" -client_id = str(uuid.uuid4()) - -def queue_prompt(prompt): - p = {"prompt": prompt, "client_id": client_id} - data = json.dumps(p).encode('utf-8') - req = urllib.request.Request("http://{}/prompt".format(server_address), data=data) - return json.loads(urllib.request.urlopen(req).read()) - -def get_image(filename, subfolder, folder_type): - data = {"filename": filename, "subfolder": subfolder, "type": folder_type} - url_values = urllib.parse.urlencode(data) - with urllib.request.urlopen("http://{}/view?{}".format(server_address, url_values)) as response: - return response.read() - -def get_history(prompt_id): - with urllib.request.urlopen("http://{}/history/{}".format(server_address, prompt_id)) as response: - return json.loads(response.read()) - -def get_images(ws, prompt): - prompt_id = queue_prompt(prompt)['prompt_id'] - output_images = {} - current_node = "" - while True: - out = ws.recv() - if isinstance(out, str): - message = json.loads(out) - if message['type'] == 'executing': - data = message['data'] - if data['prompt_id'] == prompt_id: - if data['node'] is None: - break #Execution is done - else: - current_node = data['node'] - else: - if current_node == 'save_image_websocket_node': - images_output = output_images.get(current_node, []) - images_output.append(out[8:]) - output_images[current_node] = images_output - - return output_images - -prompt_text = """ -{ - "3": { - "class_type": "KSampler", - "inputs": { - "cfg": 8, - "denoise": 1, - "latent_image": [ - "5", - 0 - ], - "model": [ - "4", - 0 - ], - "negative": [ - "7", - 0 - ], - "positive": [ - "6", - 0 - ], - "sampler_name": "euler", - "scheduler": "normal", - "seed": 8566257, - "steps": 20 - } - }, - "4": { - "class_type": "CheckpointLoaderSimple", - "inputs": { - "ckpt_name": "v1-5-pruned-emaonly.ckpt" - } - }, - "5": { - "class_type": "EmptyLatentImage", - "inputs": { - "batch_size": 1, - "height": 512, - "width": 512 - } - }, - "6": { - "class_type": "CLIPTextEncode", - "inputs": { - "clip": [ - "4", - 1 - ], - "text": "masterpiece best quality girl" - } - }, - "7": { - "class_type": "CLIPTextEncode", - "inputs": { - "clip": [ - "4", - 1 - ], - "text": "bad hands" - } - }, - "8": { - "class_type": "VAEDecode", - "inputs": { - "samples": [ - "3", - 0 - ], - "vae": [ - "4", - 2 - ] - } - }, - "save_image_websocket_node": { - "class_type": "SaveImageWebsocket", - "inputs": { - "images": [ - "8", - 0 - ] - } - } -} -""" - -prompt = json.loads(prompt_text) -#set the text prompt for our positive CLIPTextEncode -prompt["6"]["inputs"]["text"] = "masterpiece best quality man" - -#set the seed for our KSampler node -prompt["3"]["inputs"]["seed"] = 5 - -ws = websocket.WebSocket() -ws.connect("ws://{}/ws?clientId={}".format(server_address, client_id)) -images = get_images(ws, prompt) - -#Commented out code to display the output images: - -# for node_id in images: -# for image_data in images[node_id]: -# from PIL import Image -# import io -# image = Image.open(io.BytesIO(image_data)) -# image.show() - diff --git a/server.py b/server.py deleted file mode 100644 index 1c07f9781..000000000 --- a/server.py +++ /dev/null @@ -1,700 +0,0 @@ -import os -import sys -import asyncio -import traceback - -import nodes -import folder_paths -import execution -import uuid -import urllib -import json -import glob -import struct -import ssl -import hashlib -from PIL import Image, ImageOps -from PIL.PngImagePlugin import PngInfo -from io import BytesIO - -import aiohttp -from aiohttp import web -import logging - -import mimetypes -from comfy.cli_args import args -import comfy.utils -import comfy.model_management -import node_helpers -from app.frontend_management import FrontendManager -from app.user_manager import UserManager - - -class BinaryEventTypes: - PREVIEW_IMAGE = 1 - UNENCODED_PREVIEW_IMAGE = 2 - -async def send_socket_catch_exception(function, message): - try: - await function(message) - except (aiohttp.ClientError, aiohttp.ClientPayloadError, ConnectionResetError) as err: - logging.warning("send error: {}".format(err)) - -@web.middleware -async def cache_control(request: web.Request, handler): - response: web.Response = await handler(request) - if request.path.endswith('.js') or request.path.endswith('.css'): - response.headers.setdefault('Cache-Control', 'no-cache') - return response - -def create_cors_middleware(allowed_origin: str): - @web.middleware - async def cors_middleware(request: web.Request, handler): - if request.method == "OPTIONS": - # Pre-flight request. Reply successfully: - response = web.Response() - else: - response = await handler(request) - - response.headers['Access-Control-Allow-Origin'] = allowed_origin - response.headers['Access-Control-Allow-Methods'] = 'POST, GET, DELETE, PUT, OPTIONS' - response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization' - response.headers['Access-Control-Allow-Credentials'] = 'true' - return response - - return cors_middleware - -class PromptServer(): - def __init__(self, loop): - PromptServer.instance = self - - mimetypes.init() - mimetypes.types_map['.js'] = 'application/javascript; charset=utf-8' - - self.user_manager = UserManager() - self.supports = ["custom_nodes_from_web"] - self.prompt_queue = None - self.loop = loop - self.messages = asyncio.Queue() - self.number = 0 - - middlewares = [cache_control] - if args.enable_cors_header: - middlewares.append(create_cors_middleware(args.enable_cors_header)) - - max_upload_size = round(args.max_upload_size * 1024 * 1024) - self.app = web.Application(client_max_size=max_upload_size, middlewares=middlewares) - self.sockets = dict() - self.web_root = ( - FrontendManager.init_frontend(args.front_end_version) - if args.front_end_root is None - else args.front_end_root - ) - logging.info(f"[Prompt Server] web root: {self.web_root}") - routes = web.RouteTableDef() - self.routes = routes - self.last_node_id = None - self.client_id = None - - self.on_prompt_handlers = [] - - @routes.get('/ws') - async def websocket_handler(request): - ws = web.WebSocketResponse() - await ws.prepare(request) - sid = request.rel_url.query.get('clientId', '') - if sid: - # Reusing existing session, remove old - self.sockets.pop(sid, None) - else: - sid = uuid.uuid4().hex - - self.sockets[sid] = ws - - try: - # Send initial state to the new client - await self.send("status", { "status": self.get_queue_info(), 'sid': sid }, sid) - # On reconnect if we are the currently executing client send the current node - if self.client_id == sid and self.last_node_id is not None: - await self.send("executing", { "node": self.last_node_id }, sid) - - async for msg in ws: - if msg.type == aiohttp.WSMsgType.ERROR: - logging.warning('ws connection closed with exception %s' % ws.exception()) - finally: - self.sockets.pop(sid, None) - return ws - - @routes.get("/") - async def get_root(request): - response = web.FileResponse(os.path.join(self.web_root, "index.html")) - response.headers['Cache-Control'] = 'no-cache' - response.headers["Pragma"] = "no-cache" - response.headers["Expires"] = "0" - return response - - @routes.get("/embeddings") - def get_embeddings(self): - embeddings = folder_paths.get_filename_list("embeddings") - return web.json_response(list(map(lambda a: os.path.splitext(a)[0], embeddings))) - - @routes.get("/extensions") - async def get_extensions(request): - files = glob.glob(os.path.join( - glob.escape(self.web_root), 'extensions/**/*.js'), recursive=True) - - extensions = list(map(lambda f: "/" + os.path.relpath(f, self.web_root).replace("\\", "/"), files)) - - for name, dir in nodes.EXTENSION_WEB_DIRS.items(): - files = glob.glob(os.path.join(glob.escape(dir), '**/*.js'), recursive=True) - extensions.extend(list(map(lambda f: "/extensions/" + urllib.parse.quote( - name) + "/" + os.path.relpath(f, dir).replace("\\", "/"), files))) - - return web.json_response(extensions) - - def get_dir_by_type(dir_type): - if dir_type is None: - dir_type = "input" - - if dir_type == "input": - type_dir = folder_paths.get_input_directory() - elif dir_type == "temp": - type_dir = folder_paths.get_temp_directory() - elif dir_type == "output": - type_dir = folder_paths.get_output_directory() - - return type_dir, dir_type - - def compare_image_hash(filepath, image): - hasher = node_helpers.hasher() - - # function to compare hashes of two images to see if it already exists, fix to #3465 - if os.path.exists(filepath): - a = hasher() - b = hasher() - with open(filepath, "rb") as f: - a.update(f.read()) - b.update(image.file.read()) - image.file.seek(0) - f.close() - return a.hexdigest() == b.hexdigest() - return False - - def image_upload(post, image_save_function=None): - image = post.get("image") - overwrite = post.get("overwrite") - image_is_duplicate = False - - image_upload_type = post.get("type") - upload_dir, image_upload_type = get_dir_by_type(image_upload_type) - - if image and image.file: - filename = image.filename - if not filename: - return web.Response(status=400) - - subfolder = post.get("subfolder", "") - full_output_folder = os.path.join(upload_dir, os.path.normpath(subfolder)) - filepath = os.path.abspath(os.path.join(full_output_folder, filename)) - - if os.path.commonpath((upload_dir, filepath)) != upload_dir: - return web.Response(status=400) - - if not os.path.exists(full_output_folder): - os.makedirs(full_output_folder) - - split = os.path.splitext(filename) - - if overwrite is not None and (overwrite == "true" or overwrite == "1"): - pass - else: - i = 1 - while os.path.exists(filepath): - if compare_image_hash(filepath, image): #compare hash to prevent saving of duplicates with same name, fix for #3465 - image_is_duplicate = True - break - filename = f"{split[0]} ({i}){split[1]}" - filepath = os.path.join(full_output_folder, filename) - i += 1 - - if not image_is_duplicate: - if image_save_function is not None: - image_save_function(image, post, filepath) - else: - with open(filepath, "wb") as f: - f.write(image.file.read()) - - return web.json_response({"name" : filename, "subfolder": subfolder, "type": image_upload_type}) - else: - return web.Response(status=400) - - @routes.post("/upload/image") - async def upload_image(request): - post = await request.post() - return image_upload(post) - - - @routes.post("/upload/mask") - async def upload_mask(request): - post = await request.post() - - def image_save_function(image, post, filepath): - original_ref = json.loads(post.get("original_ref")) - filename, output_dir = folder_paths.annotated_filepath(original_ref['filename']) - - # validation for security: prevent accessing arbitrary path - if filename[0] == '/' or '..' in filename: - return web.Response(status=400) - - if output_dir is None: - type = original_ref.get("type", "output") - output_dir = folder_paths.get_directory_by_type(type) - - if output_dir is None: - return web.Response(status=400) - - if original_ref.get("subfolder", "") != "": - full_output_dir = os.path.join(output_dir, original_ref["subfolder"]) - if os.path.commonpath((os.path.abspath(full_output_dir), output_dir)) != output_dir: - return web.Response(status=403) - output_dir = full_output_dir - - file = os.path.join(output_dir, filename) - - if os.path.isfile(file): - with Image.open(file) as original_pil: - metadata = PngInfo() - if hasattr(original_pil,'text'): - for key in original_pil.text: - metadata.add_text(key, original_pil.text[key]) - original_pil = original_pil.convert('RGBA') - mask_pil = Image.open(image.file).convert('RGBA') - - # alpha copy - new_alpha = mask_pil.getchannel('A') - original_pil.putalpha(new_alpha) - original_pil.save(filepath, compress_level=4, pnginfo=metadata) - - return image_upload(post, image_save_function) - - @routes.get("/view") - async def view_image(request): - if "filename" in request.rel_url.query: - filename = request.rel_url.query["filename"] - filename,output_dir = folder_paths.annotated_filepath(filename) - - # validation for security: prevent accessing arbitrary path - if filename[0] == '/' or '..' in filename: - return web.Response(status=400) - - if output_dir is None: - type = request.rel_url.query.get("type", "output") - output_dir = folder_paths.get_directory_by_type(type) - - if output_dir is None: - return web.Response(status=400) - - if "subfolder" in request.rel_url.query: - full_output_dir = os.path.join(output_dir, request.rel_url.query["subfolder"]) - if os.path.commonpath((os.path.abspath(full_output_dir), output_dir)) != output_dir: - return web.Response(status=403) - output_dir = full_output_dir - - filename = os.path.basename(filename) - file = os.path.join(output_dir, filename) - - if os.path.isfile(file): - if 'preview' in request.rel_url.query: - with Image.open(file) as img: - preview_info = request.rel_url.query['preview'].split(';') - image_format = preview_info[0] - if image_format not in ['webp', 'jpeg'] or 'a' in request.rel_url.query.get('channel', ''): - image_format = 'webp' - - quality = 90 - if preview_info[-1].isdigit(): - quality = int(preview_info[-1]) - - buffer = BytesIO() - if image_format in ['jpeg'] or request.rel_url.query.get('channel', '') == 'rgb': - img = img.convert("RGB") - img.save(buffer, format=image_format, quality=quality) - buffer.seek(0) - - return web.Response(body=buffer.read(), content_type=f'image/{image_format}', - headers={"Content-Disposition": f"filename=\"{filename}\""}) - - if 'channel' not in request.rel_url.query: - channel = 'rgba' - else: - channel = request.rel_url.query["channel"] - - if channel == 'rgb': - with Image.open(file) as img: - if img.mode == "RGBA": - r, g, b, a = img.split() - new_img = Image.merge('RGB', (r, g, b)) - else: - new_img = img.convert("RGB") - - buffer = BytesIO() - new_img.save(buffer, format='PNG') - buffer.seek(0) - - return web.Response(body=buffer.read(), content_type='image/png', - headers={"Content-Disposition": f"filename=\"{filename}\""}) - - elif channel == 'a': - with Image.open(file) as img: - if img.mode == "RGBA": - _, _, _, a = img.split() - else: - a = Image.new('L', img.size, 255) - - # alpha img - alpha_img = Image.new('RGBA', img.size) - alpha_img.putalpha(a) - alpha_buffer = BytesIO() - alpha_img.save(alpha_buffer, format='PNG') - alpha_buffer.seek(0) - - return web.Response(body=alpha_buffer.read(), content_type='image/png', - headers={"Content-Disposition": f"filename=\"{filename}\""}) - else: - return web.FileResponse(file, headers={"Content-Disposition": f"filename=\"{filename}\""}) - - return web.Response(status=404) - - @routes.get("/view_metadata/{folder_name}") - async def view_metadata(request): - folder_name = request.match_info.get("folder_name", None) - if folder_name is None: - return web.Response(status=404) - if not "filename" in request.rel_url.query: - return web.Response(status=404) - - filename = request.rel_url.query["filename"] - if not filename.endswith(".safetensors"): - return web.Response(status=404) - - safetensors_path = folder_paths.get_full_path(folder_name, filename) - if safetensors_path is None: - return web.Response(status=404) - out = comfy.utils.safetensors_header(safetensors_path, max_size=1024*1024) - if out is None: - return web.Response(status=404) - dt = json.loads(out) - if not "__metadata__" in dt: - return web.Response(status=404) - return web.json_response(dt["__metadata__"]) - - @routes.get("/system_stats") - async def get_queue(request): - device = comfy.model_management.get_torch_device() - device_name = comfy.model_management.get_torch_device_name(device) - vram_total, torch_vram_total = comfy.model_management.get_total_memory(device, torch_total_too=True) - vram_free, torch_vram_free = comfy.model_management.get_free_memory(device, torch_free_too=True) - system_stats = { - "system": { - "os": os.name, - "python_version": sys.version, - "embedded_python": os.path.split(os.path.split(sys.executable)[0])[1] == "python_embeded" - }, - "devices": [ - { - "name": device_name, - "type": device.type, - "index": device.index, - "vram_total": vram_total, - "vram_free": vram_free, - "torch_vram_total": torch_vram_total, - "torch_vram_free": torch_vram_free, - } - ] - } - return web.json_response(system_stats) - - @routes.get("/prompt") - async def get_prompt(request): - return web.json_response(self.get_queue_info()) - - def node_info(node_class): - obj_class = nodes.NODE_CLASS_MAPPINGS[node_class] - info = {} - info['input'] = obj_class.INPUT_TYPES() - info['output'] = obj_class.RETURN_TYPES - info['output_is_list'] = obj_class.OUTPUT_IS_LIST if hasattr(obj_class, 'OUTPUT_IS_LIST') else [False] * len(obj_class.RETURN_TYPES) - info['output_name'] = obj_class.RETURN_NAMES if hasattr(obj_class, 'RETURN_NAMES') else info['output'] - info['name'] = node_class - info['display_name'] = nodes.NODE_DISPLAY_NAME_MAPPINGS[node_class] if node_class in nodes.NODE_DISPLAY_NAME_MAPPINGS.keys() else node_class - info['description'] = obj_class.DESCRIPTION if hasattr(obj_class,'DESCRIPTION') else '' - info['python_module'] = getattr(obj_class, "RELATIVE_PYTHON_MODULE", "nodes") - info['category'] = 'sd' - if hasattr(obj_class, 'OUTPUT_NODE') and obj_class.OUTPUT_NODE == True: - info['output_node'] = True - else: - info['output_node'] = False - - if hasattr(obj_class, 'CATEGORY'): - info['category'] = obj_class.CATEGORY - return info - - @routes.get("/object_info") - async def get_object_info(request): - out = {} - for x in nodes.NODE_CLASS_MAPPINGS: - try: - out[x] = node_info(x) - except Exception as e: - logging.error(f"[ERROR] An error occurred while retrieving information for the '{x}' node.") - logging.error(traceback.format_exc()) - return web.json_response(out) - - @routes.get("/object_info/{node_class}") - async def get_object_info_node(request): - node_class = request.match_info.get("node_class", None) - out = {} - if (node_class is not None) and (node_class in nodes.NODE_CLASS_MAPPINGS): - out[node_class] = node_info(node_class) - return web.json_response(out) - - @routes.get("/history") - async def get_history(request): - max_items = request.rel_url.query.get("max_items", None) - if max_items is not None: - max_items = int(max_items) - return web.json_response(self.prompt_queue.get_history(max_items=max_items)) - - @routes.get("/history/{prompt_id}") - async def get_history(request): - prompt_id = request.match_info.get("prompt_id", None) - return web.json_response(self.prompt_queue.get_history(prompt_id=prompt_id)) - - @routes.get("/queue") - async def get_queue(request): - queue_info = {} - current_queue = self.prompt_queue.get_current_queue() - queue_info['queue_running'] = current_queue[0] - queue_info['queue_pending'] = current_queue[1] - return web.json_response(queue_info) - - @routes.post("/prompt") - async def post_prompt(request): - logging.info("got prompt") - resp_code = 200 - out_string = "" - json_data = await request.json() - json_data = self.trigger_on_prompt(json_data) - - if "number" in json_data: - number = float(json_data['number']) - else: - number = self.number - if "front" in json_data: - if json_data['front']: - number = -number - - self.number += 1 - - if "prompt" in json_data: - prompt = json_data["prompt"] - valid = execution.validate_prompt(prompt) - extra_data = {} - if "extra_data" in json_data: - extra_data = json_data["extra_data"] - - if "client_id" in json_data: - extra_data["client_id"] = json_data["client_id"] - if valid[0]: - prompt_id = str(uuid.uuid4()) - outputs_to_execute = valid[2] - self.prompt_queue.put((number, prompt_id, prompt, extra_data, outputs_to_execute)) - response = {"prompt_id": prompt_id, "number": number, "node_errors": valid[3]} - return web.json_response(response) - else: - logging.warning("invalid prompt: {}".format(valid[1])) - return web.json_response({"error": valid[1], "node_errors": valid[3]}, status=400) - else: - return web.json_response({"error": "no prompt", "node_errors": []}, status=400) - - @routes.post("/queue") - async def post_queue(request): - json_data = await request.json() - if "clear" in json_data: - if json_data["clear"]: - self.prompt_queue.wipe_queue() - if "delete" in json_data: - to_delete = json_data['delete'] - for id_to_delete in to_delete: - delete_func = lambda a: a[1] == id_to_delete - self.prompt_queue.delete_queue_item(delete_func) - - return web.Response(status=200) - - @routes.post("/interrupt") - async def post_interrupt(request): - nodes.interrupt_processing() - return web.Response(status=200) - - @routes.post("/free") - async def post_free(request): - json_data = await request.json() - unload_models = json_data.get("unload_models", False) - free_memory = json_data.get("free_memory", False) - if unload_models: - self.prompt_queue.set_flag("unload_models", unload_models) - if free_memory: - self.prompt_queue.set_flag("free_memory", free_memory) - return web.Response(status=200) - - @routes.post("/history") - async def post_history(request): - json_data = await request.json() - if "clear" in json_data: - if json_data["clear"]: - self.prompt_queue.wipe_history() - if "delete" in json_data: - to_delete = json_data['delete'] - for id_to_delete in to_delete: - self.prompt_queue.delete_history_item(id_to_delete) - - return web.Response(status=200) - - def add_routes(self): - self.user_manager.add_routes(self.routes) - - # Prefix every route with /api for easier matching for delegation. - # This is very useful for frontend dev server, which need to forward - # everything except serving of static files. - # Currently both the old endpoints without prefix and new endpoints with - # prefix are supported. - api_routes = web.RouteTableDef() - for route in self.routes: - # Custom nodes might add extra static routes. Only process non-static - # routes to add /api prefix. - if isinstance(route, web.RouteDef): - api_routes.route(route.method, "/api" + route.path)(route.handler, **route.kwargs) - self.app.add_routes(api_routes) - self.app.add_routes(self.routes) - - for name, dir in nodes.EXTENSION_WEB_DIRS.items(): - self.app.add_routes([ - web.static('/extensions/' + urllib.parse.quote(name), dir), - ]) - - self.app.add_routes([ - web.static('/', self.web_root), - ]) - - def get_queue_info(self): - prompt_info = {} - exec_info = {} - exec_info['queue_remaining'] = self.prompt_queue.get_tasks_remaining() - prompt_info['exec_info'] = exec_info - return prompt_info - - async def send(self, event, data, sid=None): - if event == BinaryEventTypes.UNENCODED_PREVIEW_IMAGE: - await self.send_image(data, sid=sid) - elif isinstance(data, (bytes, bytearray)): - await self.send_bytes(event, data, sid) - else: - await self.send_json(event, data, sid) - - def encode_bytes(self, event, data): - if not isinstance(event, int): - raise RuntimeError(f"Binary event types must be integers, got {event}") - - packed = struct.pack(">I", event) - message = bytearray(packed) - message.extend(data) - return message - - async def send_image(self, image_data, sid=None): - image_type = image_data[0] - image = image_data[1] - max_size = image_data[2] - if max_size is not None: - if hasattr(Image, 'Resampling'): - resampling = Image.Resampling.BILINEAR - else: - resampling = Image.ANTIALIAS - - image = ImageOps.contain(image, (max_size, max_size), resampling) - type_num = 1 - if image_type == "JPEG": - type_num = 1 - elif image_type == "PNG": - type_num = 2 - - bytesIO = BytesIO() - header = struct.pack(">I", type_num) - bytesIO.write(header) - image.save(bytesIO, format=image_type, quality=95, compress_level=1) - preview_bytes = bytesIO.getvalue() - await self.send_bytes(BinaryEventTypes.PREVIEW_IMAGE, preview_bytes, sid=sid) - - async def send_bytes(self, event, data, sid=None): - message = self.encode_bytes(event, data) - - if sid is None: - sockets = list(self.sockets.values()) - for ws in sockets: - await send_socket_catch_exception(ws.send_bytes, message) - elif sid in self.sockets: - await send_socket_catch_exception(self.sockets[sid].send_bytes, message) - - async def send_json(self, event, data, sid=None): - message = {"type": event, "data": data} - - if sid is None: - sockets = list(self.sockets.values()) - for ws in sockets: - await send_socket_catch_exception(ws.send_json, message) - elif sid in self.sockets: - await send_socket_catch_exception(self.sockets[sid].send_json, message) - - def send_sync(self, event, data, sid=None): - self.loop.call_soon_threadsafe( - self.messages.put_nowait, (event, data, sid)) - - def queue_updated(self): - self.send_sync("status", { "status": self.get_queue_info() }) - - async def publish_loop(self): - while True: - msg = await self.messages.get() - await self.send(*msg) - - async def start(self, address, port, verbose=True, call_on_start=None): - runner = web.AppRunner(self.app, access_log=None) - await runner.setup() - ssl_ctx = None - scheme = "http" - if args.tls_keyfile and args.tls_certfile: - ssl_ctx = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_SERVER, verify_mode=ssl.CERT_NONE) - ssl_ctx.load_cert_chain(certfile=args.tls_certfile, - keyfile=args.tls_keyfile) - scheme = "https" - - site = web.TCPSite(runner, address, port, ssl_context=ssl_ctx) - await site.start() - - if verbose: - logging.info("Starting server\n") - logging.info("To see the GUI go to: {}://{}:{}".format(scheme, address, port)) - if call_on_start is not None: - call_on_start(scheme, address, port) - - def add_on_prompt_handler(self, handler): - self.on_prompt_handlers.append(handler) - - def trigger_on_prompt(self, json_data): - for handler in self.on_prompt_handlers: - try: - json_data = handler(json_data) - except Exception as e: - logging.warning(f"[ERROR] An error occurred during the on_prompt_handler processing") - logging.warning(traceback.format_exc()) - - return json_data diff --git a/tests-ui/.gitignore b/tests-ui/.gitignore deleted file mode 100644 index b512c09d4..000000000 --- a/tests-ui/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules \ No newline at end of file diff --git a/tests-ui/afterSetup.js b/tests-ui/afterSetup.js deleted file mode 100644 index 983f3af64..000000000 --- a/tests-ui/afterSetup.js +++ /dev/null @@ -1,9 +0,0 @@ -const { start } = require("./utils"); -const lg = require("./utils/litegraph"); - -// Load things once per test file before to ensure its all warmed up for the tests -beforeAll(async () => { - lg.setup(global); - await start({ resetEnv: true }); - lg.teardown(global); -}); diff --git a/tests-ui/babel.config.json b/tests-ui/babel.config.json deleted file mode 100644 index f27d6c397..000000000 --- a/tests-ui/babel.config.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@babel/preset-env"], - "plugins": ["babel-plugin-transform-import-meta"] -} diff --git a/tests-ui/globalSetup.js b/tests-ui/globalSetup.js deleted file mode 100644 index b9d97f58a..000000000 --- a/tests-ui/globalSetup.js +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = async function () { - global.ResizeObserver = class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} - }; - - const { nop } = require("./utils/nopProxy"); - global.enableWebGLCanvas = nop; - - HTMLCanvasElement.prototype.getContext = nop; - - localStorage["Comfy.Settings.Comfy.Logging.Enabled"] = "false"; -}; diff --git a/tests-ui/jest.config.js b/tests-ui/jest.config.js deleted file mode 100644 index 86fff5057..000000000 --- a/tests-ui/jest.config.js +++ /dev/null @@ -1,11 +0,0 @@ -/** @type {import('jest').Config} */ -const config = { - testEnvironment: "jsdom", - setupFiles: ["./globalSetup.js"], - setupFilesAfterEnv: ["./afterSetup.js"], - clearMocks: true, - resetModules: true, - testTimeout: 10000 -}; - -module.exports = config; diff --git a/tests-ui/package-lock.json b/tests-ui/package-lock.json deleted file mode 100644 index 0f409ca24..000000000 --- a/tests-ui/package-lock.json +++ /dev/null @@ -1,5586 +0,0 @@ -{ - "name": "comfui-tests", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "comfui-tests", - "version": "1.0.0", - "license": "GPL-3.0", - "devDependencies": { - "@babel/preset-env": "^7.22.20", - "@types/jest": "^29.5.5", - "babel-plugin-transform-import-meta": "^2.2.1", - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/code-frame/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.20.tgz", - "integrity": "sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.0.tgz", - "integrity": "sha512-97z/ju/Jy1rZmDxybphrBuI+jtJjFVoz7Mr9yUQVVVi+DNZE333uFQeMOqcCIy1x3WYBIbWftUSLmbNXNT7qFQ==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-module-transforms": "^7.23.0", - "@babel/helpers": "^7.23.0", - "@babel/parser": "^7.23.0", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.0", - "@babel/types": "^7.23.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", - "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", - "dev": true, - "dependencies": { - "@babel/types": "^7.23.0", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", - "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", - "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.15", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", - "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "regexpu-core": "^5.3.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz", - "integrity": "sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==", - "dev": true, - "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", - "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz", - "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", - "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-wrap-function": "^7.22.20" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", - "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.22.15", - "@babel/helper-optimise-call-expression": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", - "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", - "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", - "dev": true, - "dependencies": { - "@babel/helper-function-name": "^7.22.5", - "@babel/template": "^7.22.15", - "@babel/types": "^7.22.19" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.23.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.1.tgz", - "integrity": "sha512-chNpneuK18yW5Oxsr+t553UZzzAs3aZnFm4bxhebsNTeshrC95yA7l5yl7GBAG+JG1rF0F7zzD2EixK9mWSDoA==", - "dev": true, - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.0", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", - "dev": true, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz", - "integrity": "sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz", - "integrity": "sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", - "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", - "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", - "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", - "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", - "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.15.tgz", - "integrity": "sha512-jBm1Es25Y+tVoTi5rfd5t1KLmL8ogLKpXszboWOTTtGFGz2RKnQe2yn7HbZ+kb/B8N0FVSGQo874NSlOU1T4+w==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.9", - "@babel/plugin-syntax-async-generators": "^7.8.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", - "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", - "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.0.tgz", - "integrity": "sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", - "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz", - "integrity": "sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.11", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz", - "integrity": "sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", - "@babel/helper-split-export-declaration": "^7.22.6", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", - "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/template": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.0.tgz", - "integrity": "sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", - "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", - "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz", - "integrity": "sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", - "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", - "dev": true, - "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz", - "integrity": "sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz", - "integrity": "sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", - "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", - "dev": true, - "dependencies": { - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz", - "integrity": "sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-json-strings": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", - "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz", - "integrity": "sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", - "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.0.tgz", - "integrity": "sha512-xWT5gefv2HGSm4QHtgc1sYPbseOyf+FFDo2JbpE25GWl5BqTGO9IMwTYJRoIdjsF85GE+VegHxSCUt5EvoYTAw==", - "dev": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.23.0", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.0.tgz", - "integrity": "sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==", - "dev": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.23.0", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.0.tgz", - "integrity": "sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg==", - "dev": true, - "dependencies": { - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.23.0", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", - "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", - "dev": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", - "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", - "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz", - "integrity": "sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz", - "integrity": "sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz", - "integrity": "sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", - "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz", - "integrity": "sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.0.tgz", - "integrity": "sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz", - "integrity": "sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", - "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz", - "integrity": "sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.11", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", - "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz", - "integrity": "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "regenerator-transform": "^0.15.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", - "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", - "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", - "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", - "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", - "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", - "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz", - "integrity": "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", - "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", - "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", - "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.20.tgz", - "integrity": "sha512-11MY04gGC4kSzlPHRfvVkNAZhUxOvm7DCJ37hPDnUENwe06npjIRAfInEMTGSb4LZK5ZgDFkv5hw0lGebHeTyg==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.22.20", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.15", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.15", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.22.5", - "@babel/plugin-syntax-import-attributes": "^7.22.5", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.22.5", - "@babel/plugin-transform-async-generator-functions": "^7.22.15", - "@babel/plugin-transform-async-to-generator": "^7.22.5", - "@babel/plugin-transform-block-scoped-functions": "^7.22.5", - "@babel/plugin-transform-block-scoping": "^7.22.15", - "@babel/plugin-transform-class-properties": "^7.22.5", - "@babel/plugin-transform-class-static-block": "^7.22.11", - "@babel/plugin-transform-classes": "^7.22.15", - "@babel/plugin-transform-computed-properties": "^7.22.5", - "@babel/plugin-transform-destructuring": "^7.22.15", - "@babel/plugin-transform-dotall-regex": "^7.22.5", - "@babel/plugin-transform-duplicate-keys": "^7.22.5", - "@babel/plugin-transform-dynamic-import": "^7.22.11", - "@babel/plugin-transform-exponentiation-operator": "^7.22.5", - "@babel/plugin-transform-export-namespace-from": "^7.22.11", - "@babel/plugin-transform-for-of": "^7.22.15", - "@babel/plugin-transform-function-name": "^7.22.5", - "@babel/plugin-transform-json-strings": "^7.22.11", - "@babel/plugin-transform-literals": "^7.22.5", - "@babel/plugin-transform-logical-assignment-operators": "^7.22.11", - "@babel/plugin-transform-member-expression-literals": "^7.22.5", - "@babel/plugin-transform-modules-amd": "^7.22.5", - "@babel/plugin-transform-modules-commonjs": "^7.22.15", - "@babel/plugin-transform-modules-systemjs": "^7.22.11", - "@babel/plugin-transform-modules-umd": "^7.22.5", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.22.5", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", - "@babel/plugin-transform-numeric-separator": "^7.22.11", - "@babel/plugin-transform-object-rest-spread": "^7.22.15", - "@babel/plugin-transform-object-super": "^7.22.5", - "@babel/plugin-transform-optional-catch-binding": "^7.22.11", - "@babel/plugin-transform-optional-chaining": "^7.22.15", - "@babel/plugin-transform-parameters": "^7.22.15", - "@babel/plugin-transform-private-methods": "^7.22.5", - "@babel/plugin-transform-private-property-in-object": "^7.22.11", - "@babel/plugin-transform-property-literals": "^7.22.5", - "@babel/plugin-transform-regenerator": "^7.22.10", - "@babel/plugin-transform-reserved-words": "^7.22.5", - "@babel/plugin-transform-shorthand-properties": "^7.22.5", - "@babel/plugin-transform-spread": "^7.22.5", - "@babel/plugin-transform-sticky-regex": "^7.22.5", - "@babel/plugin-transform-template-literals": "^7.22.5", - "@babel/plugin-transform-typeof-symbol": "^7.22.5", - "@babel/plugin-transform-unicode-escapes": "^7.22.10", - "@babel/plugin-transform-unicode-property-regex": "^7.22.5", - "@babel/plugin-transform-unicode-regex": "^7.22.5", - "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "@babel/types": "^7.22.19", - "babel-plugin-polyfill-corejs2": "^0.4.5", - "babel-plugin-polyfill-corejs3": "^0.8.3", - "babel-plugin-polyfill-regenerator": "^0.5.2", - "core-js-compat": "^3.31.0", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.6-no-external-plugins", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", - "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", - "dev": true - }, - "node_modules/@babel/runtime": { - "version": "7.23.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz", - "integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==", - "dev": true, - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", - "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/core": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", - "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", - "dev": true, - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/reporters": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.7.0", - "jest-config": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-resolve-dependencies": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "jest-watcher": "^29.7.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "dev": true, - "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "dev": true, - "dependencies": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, - "dependencies": { - "jest-get-type": "^29.6.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", - "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", - "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", - "dev": true, - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", - "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-result": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "dev": true, - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", - "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "dev": true, - "dependencies": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", - "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", - "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", - "dev": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^3.0.0" - } - }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.2.tgz", - "integrity": "sha512-pNpr1T1xLUc2l3xJKuPtsEky3ybxN3m4fJkknfIpTCTfIZCDW57oAg+EfCgIIp2rvCe0Wn++/FfodDS4YXxBwA==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.6.5", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.5.tgz", - "integrity": "sha512-h9yIuWbJKdOPLJTbmSpPzkF67e659PbQDba7ifWm5BJ8xTv+sDmS7rFmywkWOvXedGTivCdeGSIIX8WLcRTz8w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.2.tgz", - "integrity": "sha512-/AVzPICMhMOMYoSx9MoKpGDKdBRsIXMNByh1PXSZoa+v6ZoLa8xxtsT/uLQ/NJm0XVAWl/BvId4MlDeXJaeIZQ==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.2.tgz", - "integrity": "sha512-ojlGK1Hsfce93J0+kn3H5R73elidKUaZonirN33GSmgTUMpzI/MIFfSpF3haANe3G1bEBS9/9/QEqwTzwqFsKw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.20.7" - } - }, - "node_modules/@types/graceful-fs": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.7.tgz", - "integrity": "sha512-MhzcwU8aUygZroVwL2jeYk6JisJrPl/oov/gsgGCue9mkgl9wjGbzReYQClxiUgFDnib9FuHqTndccKeZKxTRw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", - "dev": true - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-gPQuzaPR5h/djlAv2apEG1HVOyj1IUs7GpfMZixU0/0KXT3pm64ylHuMUI1/Akh+sq/iikxg6Z2j+fcMDXaaTQ==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.2.tgz", - "integrity": "sha512-kv43F9eb3Lhj+lr/Hn6OcLCs/sSM8bt+fIaP11rCYngfV6NVjzWXJ17owQtDQTL9tQ8WSLUrGsSJ6rJz0F1w1A==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "29.5.5", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.5.tgz", - "integrity": "sha512-ebylz2hnsWR9mYvmBFbXJXr+33UPc4+ZdxyDXh5w0FlPBTfCVN3wPL+kuOiQt3xvrK419v7XWeAs+AeOksafXg==", - "dev": true, - "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" - } - }, - "node_modules/@types/jsdom": { - "version": "20.0.1", - "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", - "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", - "dev": true, - "dependencies": { - "@types/node": "*", - "@types/tough-cookie": "*", - "parse5": "^7.0.0" - } - }, - "node_modules/@types/node": { - "version": "20.8.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.3.tgz", - "integrity": "sha512-jxiZQFpb+NlH5kjW49vXxvxTjeeqlbsnTAdBTKpzEdPs9itay7MscYXz3Fo9VYFEsfQ6LJFitHad3faerLAjCw==", - "dev": true - }, - "node_modules/@types/stack-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", - "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", - "dev": true - }, - "node_modules/@types/tough-cookie": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.3.tgz", - "integrity": "sha512-THo502dA5PzG/sfQH+42Lw3fvmYkceefOspdCwpHRul8ik2Jv1K8I5OZz1AT3/rs46kwgMCe9bSBmDLYkkOMGg==", - "dev": true - }, - "node_modules/@types/yargs": { - "version": "17.0.28", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.28.tgz", - "integrity": "sha512-N3e3fkS86hNhtk6BEnc0rj3zcehaxx8QWhCROJkqpl5Zaoi7nAic3jH8q94jVD3zu5LGk+PUB6KAiDmimYOEQw==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.1.tgz", - "integrity": "sha512-axdPBuLuEJt0c4yI5OZssC19K2Mq1uKdrfZBzuxLvaztgqUtFYZUNw7lETExPYJR9jdEoIg4mb7RQKRQzOkeGQ==", - "dev": true - }, - "node_modules/abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "dev": true - }, - "node_modules/acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-globals": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", - "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", - "dev": true, - "dependencies": { - "acorn": "^8.1.0", - "acorn-walk": "^8.0.2" - } - }, - "node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true - }, - "node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, - "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz", - "integrity": "sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.4.2", - "semver": "^6.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.4.tgz", - "integrity": "sha512-9l//BZZsPR+5XjyJMPtZSK4jv0BsTO1zDac2GC6ygx9WLGlcsnRd1Co0B2zT5fF5Ic6BZy+9m3HNZ3QcOeDKfg==", - "dev": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.2", - "core-js-compat": "^3.32.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz", - "integrity": "sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA==", - "dev": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-transform-import-meta": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-import-meta/-/babel-plugin-transform-import-meta-2.2.1.tgz", - "integrity": "sha512-AxNh27Pcg8Kt112RGa3Vod2QS2YXKKJ6+nSvRtv7qQTJAdx0MZa4UHZ4lnxHUWA2MNbLuZQv5FVab4P1CoLOWw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.4.4", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@babel/core": "^7.10.0" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", - "dev": true, - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "dev": true, - "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.13" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001546", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001546.tgz", - "integrity": "sha512-zvtSJwuQFpewSyRrI3AsftF6rM0X80mZkChIt1spBGEvRglCrjTniXvinc8JKRoqTwXAgvqTImaN9igfSMtUBw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", - "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", - "dev": true - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "dev": true - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/core-js-compat": { - "version": "3.33.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.0.tgz", - "integrity": "sha512-0w4LcLXsVEuNkIqwjjf9rjCoPhK8uqA4tMRh4Ge26vfLtUutshn+aRJU21I9LCJlh2QQHfisNToLjw1XEJLTWw==", - "dev": true, - "dependencies": { - "browserslist": "^4.22.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/create-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", - "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "prompts": "^2.0.1" - }, - "bin": { - "create-jest": "bin/create-jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cssom": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", - "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", - "dev": true - }, - "node_modules/cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "dev": true, - "dependencies": { - "cssom": "~0.3.6" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cssstyle/node_modules/cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - }, - "node_modules/data-urls": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", - "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", - "dev": true, - "dependencies": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", - "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", - "dev": true - }, - "node_modules/dedent": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", - "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", - "dev": true, - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/domexception": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", - "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", - "dev": true, - "dependencies": { - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.4.544", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.544.tgz", - "integrity": "sha512-54z7squS1FyFRSUqq/knOFSptjjogLZXbKcYk3B0qkE1KZzvqASwRZnY2KzZQJqIYLVD38XZeoiMRflYSwyO4w==", - "dev": true - }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, - "dependencies": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "node_modules/has": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", - "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", - "dev": true, - "dependencies": { - "whatwg-encoding": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", - "dev": true, - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "node_modules/is-core-module": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", - "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz", - "integrity": "sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==", - "dev": true, - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", - "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", - "dev": true, - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", - "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "dev": true, - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/types": "^29.6.3", - "import-local": "^3.0.2", - "jest-cli": "^29.7.0" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", - "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", - "dev": true, - "dependencies": { - "execa": "^5.0.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", - "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^1.0.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.7.0", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0", - "pretty-format": "^29.7.0", - "pure-rand": "^6.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-cli": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", - "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "dev": true, - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "create-jest": "^29.7.0", - "exit": "^0.1.2", - "import-local": "^3.0.2", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "yargs": "^17.3.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-config": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", - "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-jest": "^29.7.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-docblock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", - "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", - "dev": true, - "dependencies": { - "detect-newline": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-each": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", - "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "jest-util": "^29.7.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-environment-jsdom": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", - "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/jsdom": "^20.0.0", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0", - "jsdom": "^20.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jest-environment-node": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", - "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-leak-detector": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", - "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", - "dev": true, - "dependencies": { - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-mock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", - "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", - "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "dev": true, - "dependencies": { - "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runner": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", - "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", - "dev": true, - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/environment": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-leak-detector": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-resolve": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-util": "^29.7.0", - "jest-watcher": "^29.7.0", - "jest-worker": "^29.7.0", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runtime": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", - "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/globals": "^29.7.0", - "@jest/source-map": "^29.6.3", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", - "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "natural-compare": "^1.4.0", - "pretty-format": "^29.7.0", - "semver": "^7.5.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-snapshot/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", - "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "leven": "^3.1.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watcher": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "dev": true, - "dependencies": { - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "jest-util": "^29.7.0", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "dev": true, - "dependencies": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsdom": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", - "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", - "dev": true, - "dependencies": { - "abab": "^2.0.6", - "acorn": "^8.8.1", - "acorn-globals": "^7.0.0", - "cssom": "^0.5.0", - "cssstyle": "^2.3.0", - "data-urls": "^3.0.2", - "decimal.js": "^10.4.2", - "domexception": "^4.0.0", - "escodegen": "^2.0.0", - "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.2", - "parse5": "^7.1.1", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.2", - "w3c-xmlserializer": "^4.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0", - "ws": "^8.11.0", - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/make-dir/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "dependencies": { - "tmpl": "1.0.5" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true - }, - "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", - "dev": true - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nwsapi": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", - "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", - "dev": true - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "dev": true, - "dependencies": { - "entities": "^4.4.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true - }, - "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/pure-rand": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", - "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ] - }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true - }, - "node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", - "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", - "dev": true, - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", - "dev": true - }, - "node_modules/regenerator-transform": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", - "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, - "node_modules/regexpu-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", - "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", - "dev": true, - "dependencies": { - "@babel/regjsgen": "^0.8.0", - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", - "dev": true, - "dependencies": { - "jsesc": "~0.5.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true - }, - "node_modules/resolve": { - "version": "1.22.6", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", - "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve.exports": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", - "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tough-cookie": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", - "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", - "dev": true, - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", - "dev": true, - "dependencies": { - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dev": true, - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, - "node_modules/v8-to-istanbul": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.3.tgz", - "integrity": "sha512-9lDD+EVI2fjFsMWXc6dy5JJzBsVTcQ2fVkfBvncZ6xJWG9wtBhOldG+mHkSL0+V1K/xgZz0JDO5UT5hFwHUghg==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/w3c-xmlserializer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", - "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", - "dev": true, - "dependencies": { - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "dependencies": { - "makeerror": "1.0.12" - } - }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", - "dev": true, - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", - "dev": true, - "dependencies": { - "tr46": "^3.0.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/tests-ui/package.json b/tests-ui/package.json deleted file mode 100644 index ae7e49084..000000000 --- a/tests-ui/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "comfui-tests", - "version": "1.0.0", - "description": "UI tests", - "main": "index.js", - "scripts": { - "test": "jest", - "test:generate": "node setup.js" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/comfyanonymous/ComfyUI.git" - }, - "keywords": [ - "comfyui", - "test" - ], - "author": "comfyanonymous", - "license": "GPL-3.0", - "bugs": { - "url": "https://github.com/comfyanonymous/ComfyUI/issues" - }, - "homepage": "https://github.com/comfyanonymous/ComfyUI#readme", - "devDependencies": { - "@babel/preset-env": "^7.22.20", - "@types/jest": "^29.5.5", - "babel-plugin-transform-import-meta": "^2.2.1", - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0" - } -} diff --git a/tests-ui/setup.js b/tests-ui/setup.js deleted file mode 100644 index 8bbd9dcdf..000000000 --- a/tests-ui/setup.js +++ /dev/null @@ -1,88 +0,0 @@ -const { spawn } = require("child_process"); -const { resolve } = require("path"); -const { existsSync, mkdirSync, writeFileSync } = require("fs"); -const http = require("http"); - -async function setup() { - // Wait up to 30s for it to start - let success = false; - let child; - for (let i = 0; i < 30; i++) { - try { - await new Promise((res, rej) => { - http - .get("http://127.0.0.1:8188/object_info", (resp) => { - let data = ""; - resp.on("data", (chunk) => { - data += chunk; - }); - resp.on("end", () => { - // Modify the response data to add some checkpoints - const objectInfo = JSON.parse(data); - objectInfo.CheckpointLoaderSimple.input.required.ckpt_name[0] = ["model1.safetensors", "model2.ckpt"]; - objectInfo.VAELoader.input.required.vae_name[0] = ["vae1.safetensors", "vae2.ckpt"]; - - data = JSON.stringify(objectInfo, undefined, "\t"); - - const outDir = resolve("./data"); - if (!existsSync(outDir)) { - mkdirSync(outDir); - } - - const outPath = resolve(outDir, "object_info.json"); - console.log(`Writing ${Object.keys(objectInfo).length} nodes to ${outPath}`); - writeFileSync(outPath, data, { - encoding: "utf8", - }); - res(); - }); - }) - .on("error", rej); - }); - success = true; - break; - } catch (error) { - console.log(i + "/30", error); - if (i === 0) { - // Start the server on first iteration if it fails to connect - console.log("Starting ComfyUI server..."); - - let python = resolve("../../python_embeded/python.exe"); - let args; - let cwd; - if (existsSync(python)) { - args = ["-s", "ComfyUI/main.py"]; - cwd = "../.."; - } else { - python = "python"; - args = ["main.py"]; - cwd = ".."; - } - args.push("--cpu"); - console.log(python, ...args); - child = spawn(python, args, { cwd }); - child.on("error", (err) => { - console.log(`Server error (${err})`); - i = 30; - }); - child.on("exit", (code) => { - if (!success) { - console.log(`Server exited (${code})`); - i = 30; - } - }); - } - await new Promise((r) => { - setTimeout(r, 1000); - }); - } - } - - child?.kill(); - - if (!success) { - throw new Error("Waiting for server failed..."); - } -} - - setup(); \ No newline at end of file diff --git a/tests-ui/tests/extensions.test.js b/tests-ui/tests/extensions.test.js deleted file mode 100644 index 159e5113a..000000000 --- a/tests-ui/tests/extensions.test.js +++ /dev/null @@ -1,196 +0,0 @@ -// @ts-check -/// -const { start } = require("../utils"); -const lg = require("../utils/litegraph"); - -describe("extensions", () => { - beforeEach(() => { - lg.setup(global); - }); - - afterEach(() => { - lg.teardown(global); - }); - - it("calls each extension hook", async () => { - const mockExtension = { - name: "TestExtension", - init: jest.fn(), - setup: jest.fn(), - addCustomNodeDefs: jest.fn(), - getCustomWidgets: jest.fn(), - beforeRegisterNodeDef: jest.fn(), - registerCustomNodes: jest.fn(), - loadedGraphNode: jest.fn(), - nodeCreated: jest.fn(), - beforeConfigureGraph: jest.fn(), - afterConfigureGraph: jest.fn(), - }; - - const { app, ez, graph } = await start({ - async preSetup(app) { - app.registerExtension(mockExtension); - }, - }); - - // Basic initialisation hooks should be called once, with app - expect(mockExtension.init).toHaveBeenCalledTimes(1); - expect(mockExtension.init).toHaveBeenCalledWith(app); - - // Adding custom node defs should be passed the full list of nodes - expect(mockExtension.addCustomNodeDefs).toHaveBeenCalledTimes(1); - expect(mockExtension.addCustomNodeDefs.mock.calls[0][1]).toStrictEqual(app); - const defs = mockExtension.addCustomNodeDefs.mock.calls[0][0]; - expect(defs).toHaveProperty("KSampler"); - expect(defs).toHaveProperty("LoadImage"); - - // Get custom widgets is called once and should return new widget types - expect(mockExtension.getCustomWidgets).toHaveBeenCalledTimes(1); - expect(mockExtension.getCustomWidgets).toHaveBeenCalledWith(app); - - // Before register node def will be called once per node type - const nodeNames = Object.keys(defs); - const nodeCount = nodeNames.length; - expect(mockExtension.beforeRegisterNodeDef).toHaveBeenCalledTimes(nodeCount); - for (let i = 0; i < 10; i++) { - // It should be send the JS class and the original JSON definition - const nodeClass = mockExtension.beforeRegisterNodeDef.mock.calls[i][0]; - const nodeDef = mockExtension.beforeRegisterNodeDef.mock.calls[i][1]; - - expect(nodeClass.name).toBe("ComfyNode"); - expect(nodeClass.comfyClass).toBe(nodeNames[i]); - expect(nodeDef.name).toBe(nodeNames[i]); - expect(nodeDef).toHaveProperty("input"); - expect(nodeDef).toHaveProperty("output"); - } - - // Register custom nodes is called once after registerNode defs to allow adding other frontend nodes - expect(mockExtension.registerCustomNodes).toHaveBeenCalledTimes(1); - - // Before configure graph will be called here as the default graph is being loaded - expect(mockExtension.beforeConfigureGraph).toHaveBeenCalledTimes(1); - // it gets sent the graph data that is going to be loaded - const graphData = mockExtension.beforeConfigureGraph.mock.calls[0][0]; - - // A node created is fired for each node constructor that is called - expect(mockExtension.nodeCreated).toHaveBeenCalledTimes(graphData.nodes.length); - for (let i = 0; i < graphData.nodes.length; i++) { - expect(mockExtension.nodeCreated.mock.calls[i][0].type).toBe(graphData.nodes[i].type); - } - - // Each node then calls loadedGraphNode to allow them to be updated - expect(mockExtension.loadedGraphNode).toHaveBeenCalledTimes(graphData.nodes.length); - for (let i = 0; i < graphData.nodes.length; i++) { - expect(mockExtension.loadedGraphNode.mock.calls[i][0].type).toBe(graphData.nodes[i].type); - } - - // After configure is then called once all the setup is done - expect(mockExtension.afterConfigureGraph).toHaveBeenCalledTimes(1); - - expect(mockExtension.setup).toHaveBeenCalledTimes(1); - expect(mockExtension.setup).toHaveBeenCalledWith(app); - - // Ensure hooks are called in the correct order - const callOrder = [ - "init", - "addCustomNodeDefs", - "getCustomWidgets", - "beforeRegisterNodeDef", - "registerCustomNodes", - "beforeConfigureGraph", - "nodeCreated", - "loadedGraphNode", - "afterConfigureGraph", - "setup", - ]; - for (let i = 1; i < callOrder.length; i++) { - const fn1 = mockExtension[callOrder[i - 1]]; - const fn2 = mockExtension[callOrder[i]]; - expect(fn1.mock.invocationCallOrder[0]).toBeLessThan(fn2.mock.invocationCallOrder[0]); - } - - graph.clear(); - - // Ensure adding a new node calls the correct callback - ez.LoadImage(); - expect(mockExtension.loadedGraphNode).toHaveBeenCalledTimes(graphData.nodes.length); - expect(mockExtension.nodeCreated).toHaveBeenCalledTimes(graphData.nodes.length + 1); - expect(mockExtension.nodeCreated.mock.lastCall[0].type).toBe("LoadImage"); - - // Reload the graph to ensure correct hooks are fired - await graph.reload(); - - // These hooks should not be fired again - expect(mockExtension.init).toHaveBeenCalledTimes(1); - expect(mockExtension.addCustomNodeDefs).toHaveBeenCalledTimes(1); - expect(mockExtension.getCustomWidgets).toHaveBeenCalledTimes(1); - expect(mockExtension.registerCustomNodes).toHaveBeenCalledTimes(1); - expect(mockExtension.beforeRegisterNodeDef).toHaveBeenCalledTimes(nodeCount); - expect(mockExtension.setup).toHaveBeenCalledTimes(1); - - // These should be called again - expect(mockExtension.beforeConfigureGraph).toHaveBeenCalledTimes(2); - expect(mockExtension.nodeCreated).toHaveBeenCalledTimes(graphData.nodes.length + 2); - expect(mockExtension.loadedGraphNode).toHaveBeenCalledTimes(graphData.nodes.length + 1); - expect(mockExtension.afterConfigureGraph).toHaveBeenCalledTimes(2); - }, 15000); - - it("allows custom nodeDefs and widgets to be registered", async () => { - const widgetMock = jest.fn((node, inputName, inputData, app) => { - expect(node.constructor.comfyClass).toBe("TestNode"); - expect(inputName).toBe("test_input"); - expect(inputData[0]).toBe("CUSTOMWIDGET"); - expect(inputData[1]?.hello).toBe("world"); - expect(app).toStrictEqual(app); - - return { - widget: node.addWidget("button", inputName, "hello", () => {}), - }; - }); - - // Register our extension that adds a custom node + widget type - const mockExtension = { - name: "TestExtension", - addCustomNodeDefs: (nodeDefs) => { - nodeDefs["TestNode"] = { - output: [], - output_name: [], - output_is_list: [], - name: "TestNode", - display_name: "TestNode", - category: "Test", - input: { - required: { - test_input: ["CUSTOMWIDGET", { hello: "world" }], - }, - }, - }; - }, - getCustomWidgets: jest.fn(() => { - return { - CUSTOMWIDGET: widgetMock, - }; - }), - }; - - const { graph, ez } = await start({ - async preSetup(app) { - app.registerExtension(mockExtension); - }, - }); - - expect(mockExtension.getCustomWidgets).toBeCalledTimes(1); - - graph.clear(); - expect(widgetMock).toBeCalledTimes(0); - const node = ez.TestNode(); - expect(widgetMock).toBeCalledTimes(1); - - // Ensure our custom widget is created - expect(node.inputs.length).toBe(0); - expect(node.widgets.length).toBe(1); - const w = node.widgets[0].widget; - expect(w.name).toBe("test_input"); - expect(w.type).toBe("button"); - }); -}); diff --git a/tests-ui/tests/groupNode.test.js b/tests-ui/tests/groupNode.test.js deleted file mode 100644 index 53a5828d1..000000000 --- a/tests-ui/tests/groupNode.test.js +++ /dev/null @@ -1,1005 +0,0 @@ -// @ts-check -/// - -const { start, createDefaultWorkflow, getNodeDef, checkBeforeAndAfterReload } = require("../utils"); -const lg = require("../utils/litegraph"); - -describe("group node", () => { - beforeEach(() => { - lg.setup(global); - }); - - afterEach(() => { - lg.teardown(global); - }); - - /** - * - * @param {*} app - * @param {*} graph - * @param {*} name - * @param {*} nodes - * @returns { Promise> } - */ - async function convertToGroup(app, graph, name, nodes) { - // Select the nodes we are converting - for (const n of nodes) { - n.select(true); - } - - expect(Object.keys(app.canvas.selected_nodes).sort((a, b) => +a - +b)).toEqual( - nodes.map((n) => n.id + "").sort((a, b) => +a - +b) - ); - - global.prompt = jest.fn().mockImplementation(() => name); - const groupNode = await nodes[0].menu["Convert to Group Node"].call(false); - - // Check group name was requested - expect(window.prompt).toHaveBeenCalled(); - - // Ensure old nodes are removed - for (const n of nodes) { - expect(n.isRemoved).toBeTruthy(); - } - - expect(groupNode.type).toEqual("workflow/" + name); - - return graph.find(groupNode); - } - - /** - * @param { Record | number[] } idMap - * @param { Record> } valueMap - */ - function getOutput(idMap = {}, valueMap = {}) { - if (idMap instanceof Array) { - idMap = idMap.reduce((p, n) => { - p[n] = n + ""; - return p; - }, {}); - } - const expected = { - 1: { inputs: { ckpt_name: "model1.safetensors", ...valueMap?.[1] }, class_type: "CheckpointLoaderSimple" }, - 2: { inputs: { text: "positive", clip: ["1", 1], ...valueMap?.[2] }, class_type: "CLIPTextEncode" }, - 3: { inputs: { text: "negative", clip: ["1", 1], ...valueMap?.[3] }, class_type: "CLIPTextEncode" }, - 4: { inputs: { width: 512, height: 512, batch_size: 1, ...valueMap?.[4] }, class_type: "EmptyLatentImage" }, - 5: { - inputs: { - seed: 0, - steps: 20, - cfg: 8, - sampler_name: "euler", - scheduler: "normal", - denoise: 1, - model: ["1", 0], - positive: ["2", 0], - negative: ["3", 0], - latent_image: ["4", 0], - ...valueMap?.[5], - }, - class_type: "KSampler", - }, - 6: { inputs: { samples: ["5", 0], vae: ["1", 2], ...valueMap?.[6] }, class_type: "VAEDecode" }, - 7: { inputs: { filename_prefix: "ComfyUI", images: ["6", 0], ...valueMap?.[7] }, class_type: "SaveImage" }, - }; - - // Map old IDs to new at the top level - const mapped = {}; - for (const oldId in idMap) { - mapped[idMap[oldId]] = expected[oldId]; - delete expected[oldId]; - } - Object.assign(mapped, expected); - - // Map old IDs to new inside links - for (const k in mapped) { - for (const input in mapped[k].inputs) { - const v = mapped[k].inputs[input]; - if (v instanceof Array) { - if (v[0] in idMap) { - v[0] = idMap[v[0]] + ""; - } - } - } - } - - return mapped; - } - - test("can be created from selected nodes", async () => { - const { ez, graph, app } = await start(); - const nodes = createDefaultWorkflow(ez, graph); - const group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg, nodes.empty]); - - // Ensure links are now to the group node - expect(group.inputs).toHaveLength(2); - expect(group.outputs).toHaveLength(3); - - expect(group.inputs.map((i) => i.input.name)).toEqual(["clip", "CLIPTextEncode clip"]); - expect(group.outputs.map((i) => i.output.name)).toEqual(["LATENT", "CONDITIONING", "CLIPTextEncode CONDITIONING"]); - - // ckpt clip to both clip inputs on the group - expect(nodes.ckpt.outputs.CLIP.connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([ - [group.id, 0], - [group.id, 1], - ]); - - // group conditioning to sampler - expect(group.outputs["CONDITIONING"].connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([ - [nodes.sampler.id, 1], - ]); - // group conditioning 2 to sampler - expect( - group.outputs["CLIPTextEncode CONDITIONING"].connections.map((t) => [t.targetNode.id, t.targetInput.index]) - ).toEqual([[nodes.sampler.id, 2]]); - // group latent to sampler - expect(group.outputs["LATENT"].connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([ - [nodes.sampler.id, 3], - ]); - }); - - test("maintains all output links on conversion", async () => { - const { ez, graph, app } = await start(); - const nodes = createDefaultWorkflow(ez, graph); - const save2 = ez.SaveImage(...nodes.decode.outputs); - const save3 = ez.SaveImage(...nodes.decode.outputs); - // Ensure an output with multiple links maintains them on convert to group - const group = await convertToGroup(app, graph, "test", [nodes.sampler, nodes.decode]); - expect(group.outputs[0].connections.length).toBe(3); - expect(group.outputs[0].connections[0].targetNode.id).toBe(nodes.save.id); - expect(group.outputs[0].connections[1].targetNode.id).toBe(save2.id); - expect(group.outputs[0].connections[2].targetNode.id).toBe(save3.id); - - // and they're still linked when converting back to nodes - const newNodes = group.menu["Convert to nodes"].call(); - const decode = graph.find(newNodes.find((n) => n.type === "VAEDecode")); - expect(decode.outputs[0].connections.length).toBe(3); - expect(decode.outputs[0].connections[0].targetNode.id).toBe(nodes.save.id); - expect(decode.outputs[0].connections[1].targetNode.id).toBe(save2.id); - expect(decode.outputs[0].connections[2].targetNode.id).toBe(save3.id); - }); - test("can be be converted back to nodes", async () => { - const { ez, graph, app } = await start(); - const nodes = createDefaultWorkflow(ez, graph); - const toConvert = [nodes.pos, nodes.neg, nodes.empty, nodes.sampler]; - const group = await convertToGroup(app, graph, "test", toConvert); - - // Edit some values to ensure they are set back onto the converted nodes - expect(group.widgets["text"].value).toBe("positive"); - group.widgets["text"].value = "pos"; - expect(group.widgets["CLIPTextEncode text"].value).toBe("negative"); - group.widgets["CLIPTextEncode text"].value = "neg"; - expect(group.widgets["width"].value).toBe(512); - group.widgets["width"].value = 1024; - expect(group.widgets["sampler_name"].value).toBe("euler"); - group.widgets["sampler_name"].value = "ddim"; - expect(group.widgets["control_after_generate"].value).toBe("randomize"); - group.widgets["control_after_generate"].value = "fixed"; - - /** @type { Array } */ - group.menu["Convert to nodes"].call(); - - // ensure widget values are set - const pos = graph.find(nodes.pos.id); - expect(pos.node.type).toBe("CLIPTextEncode"); - expect(pos.widgets["text"].value).toBe("pos"); - const neg = graph.find(nodes.neg.id); - expect(neg.node.type).toBe("CLIPTextEncode"); - expect(neg.widgets["text"].value).toBe("neg"); - const empty = graph.find(nodes.empty.id); - expect(empty.node.type).toBe("EmptyLatentImage"); - expect(empty.widgets["width"].value).toBe(1024); - const sampler = graph.find(nodes.sampler.id); - expect(sampler.node.type).toBe("KSampler"); - expect(sampler.widgets["sampler_name"].value).toBe("ddim"); - expect(sampler.widgets["control_after_generate"].value).toBe("fixed"); - - // validate links - expect(nodes.ckpt.outputs.CLIP.connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([ - [pos.id, 0], - [neg.id, 0], - ]); - - expect(pos.outputs["CONDITIONING"].connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([ - [nodes.sampler.id, 1], - ]); - - expect(neg.outputs["CONDITIONING"].connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([ - [nodes.sampler.id, 2], - ]); - - expect(empty.outputs["LATENT"].connections.map((t) => [t.targetNode.id, t.targetInput.index])).toEqual([ - [nodes.sampler.id, 3], - ]); - }); - test("it can embed reroutes as inputs", async () => { - const { ez, graph, app } = await start(); - const nodes = createDefaultWorkflow(ez, graph); - - // Add and connect a reroute to the clip text encodes - const reroute = ez.Reroute(); - nodes.ckpt.outputs.CLIP.connectTo(reroute.inputs[0]); - reroute.outputs[0].connectTo(nodes.pos.inputs[0]); - reroute.outputs[0].connectTo(nodes.neg.inputs[0]); - - // Convert to group and ensure we only have 1 input of the correct type - const group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg, nodes.empty, reroute]); - expect(group.inputs).toHaveLength(1); - expect(group.inputs[0].input.type).toEqual("CLIP"); - - expect((await graph.toPrompt()).output).toEqual(getOutput()); - }); - test("it can embed reroutes as outputs", async () => { - const { ez, graph, app } = await start(); - const nodes = createDefaultWorkflow(ez, graph); - - // Add a reroute with no output so we output IMAGE even though its used internally - const reroute = ez.Reroute(); - nodes.decode.outputs.IMAGE.connectTo(reroute.inputs[0]); - - // Convert to group and ensure there is an IMAGE output - const group = await convertToGroup(app, graph, "test", [nodes.decode, nodes.save, reroute]); - expect(group.outputs).toHaveLength(1); - expect(group.outputs[0].output.type).toEqual("IMAGE"); - expect((await graph.toPrompt()).output).toEqual(getOutput([nodes.decode.id, nodes.save.id])); - }); - test("it can embed reroutes as pipes", async () => { - const { ez, graph, app } = await start(); - const nodes = createDefaultWorkflow(ez, graph); - - // Use reroutes as a pipe - const rerouteModel = ez.Reroute(); - const rerouteClip = ez.Reroute(); - const rerouteVae = ez.Reroute(); - nodes.ckpt.outputs.MODEL.connectTo(rerouteModel.inputs[0]); - nodes.ckpt.outputs.CLIP.connectTo(rerouteClip.inputs[0]); - nodes.ckpt.outputs.VAE.connectTo(rerouteVae.inputs[0]); - - const group = await convertToGroup(app, graph, "test", [rerouteModel, rerouteClip, rerouteVae]); - - expect(group.outputs).toHaveLength(3); - expect(group.outputs.map((o) => o.output.type)).toEqual(["MODEL", "CLIP", "VAE"]); - - expect(group.outputs).toHaveLength(3); - expect(group.outputs.map((o) => o.output.type)).toEqual(["MODEL", "CLIP", "VAE"]); - - group.outputs[0].connectTo(nodes.sampler.inputs.model); - group.outputs[1].connectTo(nodes.pos.inputs.clip); - group.outputs[1].connectTo(nodes.neg.inputs.clip); - }); - test("can handle reroutes used internally", async () => { - const { ez, graph, app } = await start(); - const nodes = createDefaultWorkflow(ez, graph); - - let reroutes = []; - let prevNode = nodes.ckpt; - for (let i = 0; i < 5; i++) { - const reroute = ez.Reroute(); - prevNode.outputs[0].connectTo(reroute.inputs[0]); - prevNode = reroute; - reroutes.push(reroute); - } - prevNode.outputs[0].connectTo(nodes.sampler.inputs.model); - - const group = await convertToGroup(app, graph, "test", [...reroutes, ...Object.values(nodes)]); - expect((await graph.toPrompt()).output).toEqual(getOutput()); - - group.menu["Convert to nodes"].call(); - expect((await graph.toPrompt()).output).toEqual(getOutput()); - }); - test("creates with widget values from inner nodes", async () => { - const { ez, graph, app } = await start(); - const nodes = createDefaultWorkflow(ez, graph); - - nodes.ckpt.widgets.ckpt_name.value = "model2.ckpt"; - nodes.pos.widgets.text.value = "hello"; - nodes.neg.widgets.text.value = "world"; - nodes.empty.widgets.width.value = 256; - nodes.empty.widgets.height.value = 1024; - nodes.sampler.widgets.seed.value = 1; - nodes.sampler.widgets.control_after_generate.value = "increment"; - nodes.sampler.widgets.steps.value = 8; - nodes.sampler.widgets.cfg.value = 4.5; - nodes.sampler.widgets.sampler_name.value = "uni_pc"; - nodes.sampler.widgets.scheduler.value = "karras"; - nodes.sampler.widgets.denoise.value = 0.9; - - const group = await convertToGroup(app, graph, "test", [ - nodes.ckpt, - nodes.pos, - nodes.neg, - nodes.empty, - nodes.sampler, - ]); - - expect(group.widgets["ckpt_name"].value).toEqual("model2.ckpt"); - expect(group.widgets["text"].value).toEqual("hello"); - expect(group.widgets["CLIPTextEncode text"].value).toEqual("world"); - expect(group.widgets["width"].value).toEqual(256); - expect(group.widgets["height"].value).toEqual(1024); - expect(group.widgets["seed"].value).toEqual(1); - expect(group.widgets["control_after_generate"].value).toEqual("increment"); - expect(group.widgets["steps"].value).toEqual(8); - expect(group.widgets["cfg"].value).toEqual(4.5); - expect(group.widgets["sampler_name"].value).toEqual("uni_pc"); - expect(group.widgets["scheduler"].value).toEqual("karras"); - expect(group.widgets["denoise"].value).toEqual(0.9); - - expect((await graph.toPrompt()).output).toEqual( - getOutput([nodes.ckpt.id, nodes.pos.id, nodes.neg.id, nodes.empty.id, nodes.sampler.id], { - [nodes.ckpt.id]: { ckpt_name: "model2.ckpt" }, - [nodes.pos.id]: { text: "hello" }, - [nodes.neg.id]: { text: "world" }, - [nodes.empty.id]: { width: 256, height: 1024 }, - [nodes.sampler.id]: { - seed: 1, - steps: 8, - cfg: 4.5, - sampler_name: "uni_pc", - scheduler: "karras", - denoise: 0.9, - }, - }) - ); - }); - test("group inputs can be reroutes", async () => { - const { ez, graph, app } = await start(); - const nodes = createDefaultWorkflow(ez, graph); - const group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg]); - - const reroute = ez.Reroute(); - nodes.ckpt.outputs.CLIP.connectTo(reroute.inputs[0]); - - reroute.outputs[0].connectTo(group.inputs[0]); - reroute.outputs[0].connectTo(group.inputs[1]); - - expect((await graph.toPrompt()).output).toEqual(getOutput([nodes.pos.id, nodes.neg.id])); - }); - test("group outputs can be reroutes", async () => { - const { ez, graph, app } = await start(); - const nodes = createDefaultWorkflow(ez, graph); - const group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg]); - - const reroute1 = ez.Reroute(); - const reroute2 = ez.Reroute(); - group.outputs[0].connectTo(reroute1.inputs[0]); - group.outputs[1].connectTo(reroute2.inputs[0]); - - reroute1.outputs[0].connectTo(nodes.sampler.inputs.positive); - reroute2.outputs[0].connectTo(nodes.sampler.inputs.negative); - - expect((await graph.toPrompt()).output).toEqual(getOutput([nodes.pos.id, nodes.neg.id])); - }); - test("groups can connect to each other", async () => { - const { ez, graph, app } = await start(); - const nodes = createDefaultWorkflow(ez, graph); - const group1 = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg]); - const group2 = await convertToGroup(app, graph, "test2", [nodes.empty, nodes.sampler]); - - group1.outputs[0].connectTo(group2.inputs["positive"]); - group1.outputs[1].connectTo(group2.inputs["negative"]); - - expect((await graph.toPrompt()).output).toEqual( - getOutput([nodes.pos.id, nodes.neg.id, nodes.empty.id, nodes.sampler.id]) - ); - }); - test("groups can connect to each other via internal reroutes", async () => { - const { ez, graph, app } = await start(); - - const latent = ez.EmptyLatentImage(); - const vae = ez.VAELoader(); - const latentReroute = ez.Reroute(); - const vaeReroute = ez.Reroute(); - - latent.outputs[0].connectTo(latentReroute.inputs[0]); - vae.outputs[0].connectTo(vaeReroute.inputs[0]); - - const group1 = await convertToGroup(app, graph, "test", [latentReroute, vaeReroute]); - group1.menu.Clone.call(); - expect(app.graph._nodes).toHaveLength(4); - const group2 = graph.find(app.graph._nodes[3]); - expect(group2.node.type).toEqual("workflow/test"); - expect(group2.id).not.toEqual(group1.id); - - group1.outputs.VAE.connectTo(group2.inputs.VAE); - group1.outputs.LATENT.connectTo(group2.inputs.LATENT); - - const decode = ez.VAEDecode(group2.outputs.LATENT, group2.outputs.VAE); - const preview = ez.PreviewImage(decode.outputs[0]); - - const output = { - [latent.id]: { inputs: { width: 512, height: 512, batch_size: 1 }, class_type: "EmptyLatentImage" }, - [vae.id]: { inputs: { vae_name: "vae1.safetensors" }, class_type: "VAELoader" }, - [decode.id]: { inputs: { samples: [latent.id + "", 0], vae: [vae.id + "", 0] }, class_type: "VAEDecode" }, - [preview.id]: { inputs: { images: [decode.id + "", 0] }, class_type: "PreviewImage" }, - }; - expect((await graph.toPrompt()).output).toEqual(output); - - // Ensure missing connections dont cause errors - group2.inputs.VAE.disconnect(); - delete output[decode.id].inputs.vae; - expect((await graph.toPrompt()).output).toEqual(output); - }); - test("displays generated image on group node", async () => { - const { ez, graph, app } = await start(); - const nodes = createDefaultWorkflow(ez, graph); - let group = await convertToGroup(app, graph, "test", [ - nodes.pos, - nodes.neg, - nodes.empty, - nodes.sampler, - nodes.decode, - nodes.save, - ]); - - const { api } = require("../../web/scripts/api"); - - api.dispatchEvent(new CustomEvent("execution_start", {})); - api.dispatchEvent(new CustomEvent("executing", { detail: `${nodes.save.id}` })); - // Event should be forwarded to group node id - expect(+app.runningNodeId).toEqual(group.id); - expect(group.node["imgs"]).toBeFalsy(); - api.dispatchEvent( - new CustomEvent("executed", { - detail: { - node: `${nodes.save.id}`, - output: { - images: [ - { - filename: "test.png", - type: "output", - }, - ], - }, - }, - }) - ); - - // Trigger paint - group.node.onDrawBackground?.(app.canvas.ctx, app.canvas.canvas); - - expect(group.node["images"]).toEqual([ - { - filename: "test.png", - type: "output", - }, - ]); - - // Reload - const workflow = JSON.stringify((await graph.toPrompt()).workflow); - await app.loadGraphData(JSON.parse(workflow)); - group = graph.find(group); - - // Trigger inner nodes to get created - group.node["getInnerNodes"](); - - // Check it works for internal node ids - api.dispatchEvent(new CustomEvent("execution_start", {})); - api.dispatchEvent(new CustomEvent("executing", { detail: `${group.id}:5` })); - // Event should be forwarded to group node id - expect(+app.runningNodeId).toEqual(group.id); - expect(group.node["imgs"]).toBeFalsy(); - api.dispatchEvent( - new CustomEvent("executed", { - detail: { - node: `${group.id}:5`, - output: { - images: [ - { - filename: "test2.png", - type: "output", - }, - ], - }, - }, - }) - ); - - // Trigger paint - group.node.onDrawBackground?.(app.canvas.ctx, app.canvas.canvas); - - expect(group.node["images"]).toEqual([ - { - filename: "test2.png", - type: "output", - }, - ]); - }); - test("allows widgets to be converted to inputs", async () => { - const { ez, graph, app } = await start(); - const nodes = createDefaultWorkflow(ez, graph); - const group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg]); - group.widgets[0].convertToInput(); - - const primitive = ez.PrimitiveNode(); - primitive.outputs[0].connectTo(group.inputs["text"]); - primitive.widgets[0].value = "hello"; - - expect((await graph.toPrompt()).output).toEqual( - getOutput([nodes.pos.id, nodes.neg.id], { - [nodes.pos.id]: { text: "hello" }, - }) - ); - }); - test("can be copied", async () => { - const { ez, graph, app } = await start(); - const nodes = createDefaultWorkflow(ez, graph); - - const group1 = await convertToGroup(app, graph, "test", [ - nodes.pos, - nodes.neg, - nodes.empty, - nodes.sampler, - nodes.decode, - nodes.save, - ]); - - group1.widgets["text"].value = "hello"; - group1.widgets["width"].value = 256; - group1.widgets["seed"].value = 1; - - // Clone the node - group1.menu.Clone.call(); - expect(app.graph._nodes).toHaveLength(3); - const group2 = graph.find(app.graph._nodes[2]); - expect(group2.node.type).toEqual("workflow/test"); - expect(group2.id).not.toEqual(group1.id); - - // Reconnect ckpt - nodes.ckpt.outputs.MODEL.connectTo(group2.inputs["model"]); - nodes.ckpt.outputs.CLIP.connectTo(group2.inputs["clip"]); - nodes.ckpt.outputs.CLIP.connectTo(group2.inputs["CLIPTextEncode clip"]); - nodes.ckpt.outputs.VAE.connectTo(group2.inputs["vae"]); - - group2.widgets["text"].value = "world"; - group2.widgets["width"].value = 1024; - group2.widgets["seed"].value = 100; - - let i = 0; - expect((await graph.toPrompt()).output).toEqual({ - ...getOutput([nodes.empty.id, nodes.pos.id, nodes.neg.id, nodes.sampler.id, nodes.decode.id, nodes.save.id], { - [nodes.empty.id]: { width: 256 }, - [nodes.pos.id]: { text: "hello" }, - [nodes.sampler.id]: { seed: 1 }, - }), - ...getOutput( - { - [nodes.empty.id]: `${group2.id}:${i++}`, - [nodes.pos.id]: `${group2.id}:${i++}`, - [nodes.neg.id]: `${group2.id}:${i++}`, - [nodes.sampler.id]: `${group2.id}:${i++}`, - [nodes.decode.id]: `${group2.id}:${i++}`, - [nodes.save.id]: `${group2.id}:${i++}`, - }, - { - [nodes.empty.id]: { width: 1024 }, - [nodes.pos.id]: { text: "world" }, - [nodes.sampler.id]: { seed: 100 }, - } - ), - }); - - graph.arrange(); - }); - test("is embedded in workflow", async () => { - let { ez, graph, app } = await start(); - const nodes = createDefaultWorkflow(ez, graph); - let group = await convertToGroup(app, graph, "test", [nodes.pos, nodes.neg]); - const workflow = JSON.stringify((await graph.toPrompt()).workflow); - - // Clear the environment - ({ ez, graph, app } = await start({ - resetEnv: true, - })); - // Ensure the node isnt registered - expect(() => ez["workflow/test"]).toThrow(); - - // Reload the workflow - await app.loadGraphData(JSON.parse(workflow)); - - // Ensure the node is found - group = graph.find(group); - - // Generate prompt and ensure it is as expected - expect((await graph.toPrompt()).output).toEqual( - getOutput({ - [nodes.pos.id]: `${group.id}:0`, - [nodes.neg.id]: `${group.id}:1`, - }) - ); - }); - test("shows missing node error on missing internal node when loading graph data", async () => { - const { graph } = await start(); - - const dialogShow = jest.spyOn(graph.app.ui.dialog, "show"); - await graph.app.loadGraphData({ - last_node_id: 3, - last_link_id: 1, - nodes: [ - { - id: 3, - type: "workflow/testerror", - }, - ], - links: [], - groups: [], - config: {}, - extra: { - groupNodes: { - testerror: { - nodes: [ - { - type: "NotKSampler", - }, - { - type: "NotVAEDecode", - }, - ], - }, - }, - }, - }); - - expect(dialogShow).toBeCalledTimes(1); - const call = dialogShow.mock.calls[0][0].innerHTML; - expect(call).toContain("the following node types were not found"); - expect(call).toContain("NotKSampler"); - expect(call).toContain("NotVAEDecode"); - expect(call).toContain("workflow/testerror"); - }); - test("maintains widget inputs on conversion back to nodes", async () => { - const { ez, graph, app } = await start(); - let pos = ez.CLIPTextEncode({ text: "positive" }); - pos.node.title = "Positive"; - let neg = ez.CLIPTextEncode({ text: "negative" }); - neg.node.title = "Negative"; - pos.widgets.text.convertToInput(); - neg.widgets.text.convertToInput(); - - let primitive = ez.PrimitiveNode(); - primitive.outputs[0].connectTo(pos.inputs.text); - primitive.outputs[0].connectTo(neg.inputs.text); - - const group = await convertToGroup(app, graph, "test", [pos, neg, primitive]); - // This will use a primitive widget named 'value' - expect(group.widgets.length).toBe(1); - expect(group.widgets["value"].value).toBe("positive"); - - const newNodes = group.menu["Convert to nodes"].call(); - pos = graph.find(newNodes.find((n) => n.title === "Positive")); - neg = graph.find(newNodes.find((n) => n.title === "Negative")); - primitive = graph.find(newNodes.find((n) => n.type === "PrimitiveNode")); - - expect(pos.inputs).toHaveLength(2); - expect(neg.inputs).toHaveLength(2); - expect(primitive.outputs[0].connections).toHaveLength(2); - - expect((await graph.toPrompt()).output).toEqual({ - 1: { inputs: { text: "positive" }, class_type: "CLIPTextEncode" }, - 2: { inputs: { text: "positive" }, class_type: "CLIPTextEncode" }, - }); - }); - test("correctly handles widget inputs", async () => { - const { ez, graph, app } = await start(); - const upscaleMethods = (await getNodeDef("ImageScaleBy")).input.required["upscale_method"][0]; - - const image = ez.LoadImage(); - const scale1 = ez.ImageScaleBy(image.outputs[0]); - const scale2 = ez.ImageScaleBy(image.outputs[0]); - const preview1 = ez.PreviewImage(scale1.outputs[0]); - const preview2 = ez.PreviewImage(scale2.outputs[0]); - scale1.widgets.upscale_method.value = upscaleMethods[1]; - scale1.widgets.upscale_method.convertToInput(); - - const group = await convertToGroup(app, graph, "test", [scale1, scale2]); - expect(group.inputs.length).toBe(3); - expect(group.inputs[0].input.type).toBe("IMAGE"); - expect(group.inputs[1].input.type).toBe("IMAGE"); - expect(group.inputs[2].input.type).toBe("COMBO"); - - // Ensure links are maintained - expect(group.inputs[0].connection?.originNode?.id).toBe(image.id); - expect(group.inputs[1].connection?.originNode?.id).toBe(image.id); - expect(group.inputs[2].connection).toBeFalsy(); - - // Ensure primitive gets correct type - const primitive = ez.PrimitiveNode(); - primitive.outputs[0].connectTo(group.inputs[2]); - expect(primitive.widgets.value.widget.options.values).toBe(upscaleMethods); - expect(primitive.widgets.value.value).toBe(upscaleMethods[1]); // Ensure value is copied - primitive.widgets.value.value = upscaleMethods[1]; - - await checkBeforeAndAfterReload(graph, async (r) => { - const scale1id = r ? `${group.id}:0` : scale1.id; - const scale2id = r ? `${group.id}:1` : scale2.id; - // Ensure widget value is applied to prompt - expect((await graph.toPrompt()).output).toStrictEqual({ - [image.id]: { inputs: { image: "example.png", upload: "image" }, class_type: "LoadImage" }, - [scale1id]: { - inputs: { upscale_method: upscaleMethods[1], scale_by: 1, image: [`${image.id}`, 0] }, - class_type: "ImageScaleBy", - }, - [scale2id]: { - inputs: { upscale_method: "nearest-exact", scale_by: 1, image: [`${image.id}`, 0] }, - class_type: "ImageScaleBy", - }, - [preview1.id]: { inputs: { images: [`${scale1id}`, 0] }, class_type: "PreviewImage" }, - [preview2.id]: { inputs: { images: [`${scale2id}`, 0] }, class_type: "PreviewImage" }, - }); - }); - }); - test("adds widgets in node execution order", async () => { - const { ez, graph, app } = await start(); - const scale = ez.LatentUpscale(); - const save = ez.SaveImage(); - const empty = ez.EmptyLatentImage(); - const decode = ez.VAEDecode(); - - scale.outputs.LATENT.connectTo(decode.inputs.samples); - decode.outputs.IMAGE.connectTo(save.inputs.images); - empty.outputs.LATENT.connectTo(scale.inputs.samples); - - const group = await convertToGroup(app, graph, "test", [scale, save, empty, decode]); - const widgets = group.widgets.map((w) => w.widget.name); - expect(widgets).toStrictEqual([ - "width", - "height", - "batch_size", - "upscale_method", - "LatentUpscale width", - "LatentUpscale height", - "crop", - "filename_prefix", - ]); - }); - test("adds output for external links when converting to group", async () => { - const { ez, graph, app } = await start(); - const img = ez.EmptyLatentImage(); - let decode = ez.VAEDecode(...img.outputs); - const preview1 = ez.PreviewImage(...decode.outputs); - const preview2 = ez.PreviewImage(...decode.outputs); - - const group = await convertToGroup(app, graph, "test", [img, decode, preview1]); - - // Ensure we have an output connected to the 2nd preview node - expect(group.outputs.length).toBe(1); - expect(group.outputs[0].connections.length).toBe(1); - expect(group.outputs[0].connections[0].targetNode.id).toBe(preview2.id); - - // Convert back and ensure bothe previews are still connected - group.menu["Convert to nodes"].call(); - decode = graph.find(decode); - expect(decode.outputs[0].connections.length).toBe(2); - expect(decode.outputs[0].connections[0].targetNode.id).toBe(preview1.id); - expect(decode.outputs[0].connections[1].targetNode.id).toBe(preview2.id); - }); - test("adds output for external links when converting to group when nodes are not in execution order", async () => { - const { ez, graph, app } = await start(); - const sampler = ez.KSampler(); - const ckpt = ez.CheckpointLoaderSimple(); - const empty = ez.EmptyLatentImage(); - const pos = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: "positive" }); - const neg = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: "negative" }); - const decode1 = ez.VAEDecode(sampler.outputs.LATENT, ckpt.outputs.VAE); - const save = ez.SaveImage(decode1.outputs.IMAGE); - ckpt.outputs.MODEL.connectTo(sampler.inputs.model); - pos.outputs.CONDITIONING.connectTo(sampler.inputs.positive); - neg.outputs.CONDITIONING.connectTo(sampler.inputs.negative); - empty.outputs.LATENT.connectTo(sampler.inputs.latent_image); - - const encode = ez.VAEEncode(decode1.outputs.IMAGE); - const vae = ez.VAELoader(); - const decode2 = ez.VAEDecode(encode.outputs.LATENT, vae.outputs.VAE); - const preview = ez.PreviewImage(decode2.outputs.IMAGE); - vae.outputs.VAE.connectTo(encode.inputs.vae); - - const group = await convertToGroup(app, graph, "test", [vae, decode1, encode, sampler]); - - expect(group.outputs.length).toBe(3); - expect(group.outputs[0].output.name).toBe("VAE"); - expect(group.outputs[0].output.type).toBe("VAE"); - expect(group.outputs[1].output.name).toBe("IMAGE"); - expect(group.outputs[1].output.type).toBe("IMAGE"); - expect(group.outputs[2].output.name).toBe("LATENT"); - expect(group.outputs[2].output.type).toBe("LATENT"); - - expect(group.outputs[0].connections.length).toBe(1); - expect(group.outputs[0].connections[0].targetNode.id).toBe(decode2.id); - expect(group.outputs[0].connections[0].targetInput.index).toBe(1); - - expect(group.outputs[1].connections.length).toBe(1); - expect(group.outputs[1].connections[0].targetNode.id).toBe(save.id); - expect(group.outputs[1].connections[0].targetInput.index).toBe(0); - - expect(group.outputs[2].connections.length).toBe(1); - expect(group.outputs[2].connections[0].targetNode.id).toBe(decode2.id); - expect(group.outputs[2].connections[0].targetInput.index).toBe(0); - - expect((await graph.toPrompt()).output).toEqual({ - ...getOutput({ 1: ckpt.id, 2: pos.id, 3: neg.id, 4: empty.id, 5: sampler.id, 6: decode1.id, 7: save.id }), - [vae.id]: { inputs: { vae_name: "vae1.safetensors" }, class_type: vae.node.type }, - [encode.id]: { inputs: { pixels: ["6", 0], vae: [vae.id + "", 0] }, class_type: encode.node.type }, - [decode2.id]: { inputs: { samples: [encode.id + "", 0], vae: [vae.id + "", 0] }, class_type: decode2.node.type }, - [preview.id]: { inputs: { images: [decode2.id + "", 0] }, class_type: preview.node.type }, - }); - }); - test("works with IMAGEUPLOAD widget", async () => { - const { ez, graph, app } = await start(); - const img = ez.LoadImage(); - const preview1 = ez.PreviewImage(img.outputs[0]); - - const group = await convertToGroup(app, graph, "test", [img, preview1]); - const widget = group.widgets["upload"]; - expect(widget).toBeTruthy(); - expect(widget.widget.type).toBe("button"); - }); - test("internal primitive populates widgets for all linked inputs", async () => { - const { ez, graph, app } = await start(); - const img = ez.LoadImage(); - const scale1 = ez.ImageScale(img.outputs[0]); - const scale2 = ez.ImageScale(img.outputs[0]); - ez.PreviewImage(scale1.outputs[0]); - ez.PreviewImage(scale2.outputs[0]); - - scale1.widgets.width.convertToInput(); - scale2.widgets.height.convertToInput(); - - const primitive = ez.PrimitiveNode(); - primitive.outputs[0].connectTo(scale1.inputs.width); - primitive.outputs[0].connectTo(scale2.inputs.height); - - const group = await convertToGroup(app, graph, "test", [img, primitive, scale1, scale2]); - group.widgets.value.value = 100; - expect((await graph.toPrompt()).output).toEqual({ - 1: { - inputs: { image: img.widgets.image.value, upload: "image" }, - class_type: "LoadImage", - }, - 2: { - inputs: { upscale_method: "nearest-exact", width: 100, height: 512, crop: "disabled", image: ["1", 0] }, - class_type: "ImageScale", - }, - 3: { - inputs: { upscale_method: "nearest-exact", width: 512, height: 100, crop: "disabled", image: ["1", 0] }, - class_type: "ImageScale", - }, - 4: { inputs: { images: ["2", 0] }, class_type: "PreviewImage" }, - 5: { inputs: { images: ["3", 0] }, class_type: "PreviewImage" }, - }); - }); - test("primitive control widgets values are copied on convert", async () => { - const { ez, graph, app } = await start(); - const sampler = ez.KSampler(); - sampler.widgets.seed.convertToInput(); - sampler.widgets.sampler_name.convertToInput(); - - let p1 = ez.PrimitiveNode(); - let p2 = ez.PrimitiveNode(); - p1.outputs[0].connectTo(sampler.inputs.seed); - p2.outputs[0].connectTo(sampler.inputs.sampler_name); - - p1.widgets.control_after_generate.value = "increment"; - p2.widgets.control_after_generate.value = "decrement"; - p2.widgets.control_filter_list.value = "/.*/"; - - p2.node.title = "p2"; - - const group = await convertToGroup(app, graph, "test", [sampler, p1, p2]); - expect(group.widgets.control_after_generate.value).toBe("increment"); - expect(group.widgets["p2 control_after_generate"].value).toBe("decrement"); - expect(group.widgets["p2 control_filter_list"].value).toBe("/.*/"); - - group.widgets.control_after_generate.value = "fixed"; - group.widgets["p2 control_after_generate"].value = "randomize"; - group.widgets["p2 control_filter_list"].value = "/.+/"; - - group.menu["Convert to nodes"].call(); - p1 = graph.find(p1); - p2 = graph.find(p2); - - expect(p1.widgets.control_after_generate.value).toBe("fixed"); - expect(p2.widgets.control_after_generate.value).toBe("randomize"); - expect(p2.widgets.control_filter_list.value).toBe("/.+/"); - }); - test("internal reroutes work with converted inputs and merge options", async () => { - const { ez, graph, app } = await start(); - const vae = ez.VAELoader(); - const latent = ez.EmptyLatentImage(); - const decode = ez.VAEDecode(latent.outputs.LATENT, vae.outputs.VAE); - const scale = ez.ImageScale(decode.outputs.IMAGE); - ez.PreviewImage(scale.outputs.IMAGE); - - const r1 = ez.Reroute(); - const r2 = ez.Reroute(); - - latent.widgets.width.value = 64; - latent.widgets.height.value = 128; - - latent.widgets.width.convertToInput(); - latent.widgets.height.convertToInput(); - latent.widgets.batch_size.convertToInput(); - - scale.widgets.width.convertToInput(); - scale.widgets.height.convertToInput(); - - r1.inputs[0].input.label = "hbw"; - r1.outputs[0].connectTo(latent.inputs.height); - r1.outputs[0].connectTo(latent.inputs.batch_size); - r1.outputs[0].connectTo(scale.inputs.width); - - r2.inputs[0].input.label = "wh"; - r2.outputs[0].connectTo(latent.inputs.width); - r2.outputs[0].connectTo(scale.inputs.height); - - const group = await convertToGroup(app, graph, "test", [r1, r2, latent, decode, scale]); - - expect(group.inputs[0].input.type).toBe("VAE"); - expect(group.inputs[1].input.type).toBe("INT"); - expect(group.inputs[2].input.type).toBe("INT"); - - const p1 = ez.PrimitiveNode(); - const p2 = ez.PrimitiveNode(); - p1.outputs[0].connectTo(group.inputs[1]); - p2.outputs[0].connectTo(group.inputs[2]); - - expect(p1.widgets.value.widget.options?.min).toBe(16); // width/height min - expect(p1.widgets.value.widget.options?.max).toBe(4096); // batch max - expect(p1.widgets.value.widget.options?.step).toBe(80); // width/height step * 10 - - expect(p2.widgets.value.widget.options?.min).toBe(16); // width/height min - expect(p2.widgets.value.widget.options?.max).toBe(16384); // width/height max - expect(p2.widgets.value.widget.options?.step).toBe(80); // width/height step * 10 - - expect(p1.widgets.value.value).toBe(128); - expect(p2.widgets.value.value).toBe(64); - - p1.widgets.value.value = 16; - p2.widgets.value.value = 32; - - await checkBeforeAndAfterReload(graph, async (r) => { - const id = (v) => (r ? `${group.id}:` : "") + v; - expect((await graph.toPrompt()).output).toStrictEqual({ - 1: { inputs: { vae_name: "vae1.safetensors" }, class_type: "VAELoader" }, - [id(2)]: { inputs: { width: 32, height: 16, batch_size: 16 }, class_type: "EmptyLatentImage" }, - [id(3)]: { inputs: { samples: [id(2), 0], vae: ["1", 0] }, class_type: "VAEDecode" }, - [id(4)]: { - inputs: { upscale_method: "nearest-exact", width: 16, height: 32, crop: "disabled", image: [id(3), 0] }, - class_type: "ImageScale", - }, - 5: { inputs: { images: [id(4), 0] }, class_type: "PreviewImage" }, - }); - }); - }); - test("converted inputs with linked widgets map values correctly on creation", async () => { - const { ez, graph, app } = await start(); - const k1 = ez.KSampler(); - const k2 = ez.KSampler(); - k1.widgets.seed.convertToInput(); - k2.widgets.seed.convertToInput(); - - const rr = ez.Reroute(); - rr.outputs[0].connectTo(k1.inputs.seed); - rr.outputs[0].connectTo(k2.inputs.seed); - - const group = await convertToGroup(app, graph, "test", [k1, k2, rr]); - expect(group.widgets.steps.value).toBe(20); - expect(group.widgets.cfg.value).toBe(8); - expect(group.widgets.scheduler.value).toBe("normal"); - expect(group.widgets["KSampler steps"].value).toBe(20); - expect(group.widgets["KSampler cfg"].value).toBe(8); - expect(group.widgets["KSampler scheduler"].value).toBe("normal"); - }); - test("allow multiple of the same node type to be added", async () => { - const { ez, graph, app } = await start(); - const nodes = [...Array(10)].map(() => ez.ImageScaleBy()); - const group = await convertToGroup(app, graph, "test", nodes); - expect(group.inputs.length).toBe(10); - expect(group.outputs.length).toBe(10); - expect(group.widgets.length).toBe(20); - expect(group.widgets.map((w) => w.widget.name)).toStrictEqual( - [...Array(10)] - .map((_, i) => `${i > 0 ? "ImageScaleBy " : ""}${i > 1 ? i + " " : ""}`) - .flatMap((p) => [`${p}upscale_method`, `${p}scale_by`]) - ); - }); -}); diff --git a/tests-ui/tests/users.test.js b/tests-ui/tests/users.test.js deleted file mode 100644 index 5e0730730..000000000 --- a/tests-ui/tests/users.test.js +++ /dev/null @@ -1,295 +0,0 @@ -// @ts-check -/// -const { start } = require("../utils"); -const lg = require("../utils/litegraph"); - -describe("users", () => { - beforeEach(() => { - lg.setup(global); - }); - - afterEach(() => { - lg.teardown(global); - }); - - function expectNoUserScreen() { - // Ensure login isnt visible - const selection = document.querySelectorAll("#comfy-user-selection")?.[0]; - expect(selection["style"].display).toBe("none"); - const menu = document.querySelectorAll(".comfy-menu")?.[0]; - expect(window.getComputedStyle(menu)?.display).not.toBe("none"); - } - - describe("multi-user", () => { - function mockAddStylesheet() { - const utils = require("../../web/scripts/utils"); - utils.addStylesheet = jest.fn().mockReturnValue(Promise.resolve()); - } - - async function waitForUserScreenShow() { - mockAddStylesheet(); - - // Wait for "show" to be called - const { UserSelectionScreen } = require("../../web/scripts/ui/userSelection"); - let resolve, reject; - const fn = UserSelectionScreen.prototype.show; - const p = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - jest.spyOn(UserSelectionScreen.prototype, "show").mockImplementation(async (...args) => { - const res = fn(...args); - await new Promise(process.nextTick); // wait for promises to resolve - resolve(); - return res; - }); - // @ts-ignore - setTimeout(() => reject("timeout waiting for UserSelectionScreen to be shown."), 500); - await p; - await new Promise(process.nextTick); // wait for promises to resolve - } - - async function testUserScreen(onShown, users) { - if (!users) { - users = {}; - } - const starting = start({ - resetEnv: true, - userConfig: { storage: "server", users }, - }); - - // Ensure no current user - expect(localStorage["Comfy.userId"]).toBeFalsy(); - expect(localStorage["Comfy.userName"]).toBeFalsy(); - - await waitForUserScreenShow(); - - const selection = document.querySelectorAll("#comfy-user-selection")?.[0]; - expect(selection).toBeTruthy(); - - // Ensure login is visible - expect(window.getComputedStyle(selection)?.display).not.toBe("none"); - // Ensure menu is hidden - const menu = document.querySelectorAll(".comfy-menu")?.[0]; - expect(window.getComputedStyle(menu)?.display).toBe("none"); - - const isCreate = await onShown(selection); - - // Submit form - selection.querySelectorAll("form")[0].submit(); - await new Promise(process.nextTick); // wait for promises to resolve - - // Wait for start - const s = await starting; - - // Ensure login is removed - expect(document.querySelectorAll("#comfy-user-selection")).toHaveLength(0); - expect(window.getComputedStyle(menu)?.display).not.toBe("none"); - - // Ensure settings + templates are saved - const { api } = require("../../web/scripts/api"); - expect(api.createUser).toHaveBeenCalledTimes(+isCreate); - expect(api.storeSettings).toHaveBeenCalledTimes(+isCreate); - expect(api.storeUserData).toHaveBeenCalledTimes(+isCreate); - if (isCreate) { - expect(api.storeUserData).toHaveBeenCalledWith("comfy.templates.json", null, { stringify: false }); - expect(s.app.isNewUserSession).toBeTruthy(); - } else { - expect(s.app.isNewUserSession).toBeFalsy(); - } - - return { users, selection, ...s }; - } - - it("allows user creation if no users", async () => { - const { users } = await testUserScreen((selection) => { - // Ensure we have no users flag added - expect(selection.classList.contains("no-users")).toBeTruthy(); - - // Enter a username - const input = selection.getElementsByTagName("input")[0]; - input.focus(); - input.value = "Test User"; - - return true; - }); - - expect(users).toStrictEqual({ - "Test User!": "Test User", - }); - - expect(localStorage["Comfy.userId"]).toBe("Test User!"); - expect(localStorage["Comfy.userName"]).toBe("Test User"); - }); - it("allows user creation if no current user but other users", async () => { - const users = { - "Test User 2!": "Test User 2", - }; - - await testUserScreen((selection) => { - expect(selection.classList.contains("no-users")).toBeFalsy(); - - // Enter a username - const input = selection.getElementsByTagName("input")[0]; - input.focus(); - input.value = "Test User 3"; - return true; - }, users); - - expect(users).toStrictEqual({ - "Test User 2!": "Test User 2", - "Test User 3!": "Test User 3", - }); - - expect(localStorage["Comfy.userId"]).toBe("Test User 3!"); - expect(localStorage["Comfy.userName"]).toBe("Test User 3"); - }); - it("allows user selection if no current user but other users", async () => { - const users = { - "A!": "A", - "B!": "B", - "C!": "C", - }; - - await testUserScreen((selection) => { - expect(selection.classList.contains("no-users")).toBeFalsy(); - - // Check user list - const select = selection.getElementsByTagName("select")[0]; - const options = select.getElementsByTagName("option"); - expect( - [...options] - .filter((o) => !o.disabled) - .reduce((p, n) => { - p[n.getAttribute("value")] = n.textContent; - return p; - }, {}) - ).toStrictEqual(users); - - // Select an option - select.focus(); - select.value = options[2].value; - - return false; - }, users); - - expect(users).toStrictEqual(users); - - expect(localStorage["Comfy.userId"]).toBe("B!"); - expect(localStorage["Comfy.userName"]).toBe("B"); - }); - it("doesnt show user screen if current user", async () => { - const starting = start({ - resetEnv: true, - userConfig: { - storage: "server", - users: { - "User!": "User", - }, - }, - localStorage: { - "Comfy.userId": "User!", - "Comfy.userName": "User", - }, - }); - await new Promise(process.nextTick); // wait for promises to resolve - - expectNoUserScreen(); - - await starting; - }); - it("allows user switching", async () => { - const { app } = await start({ - resetEnv: true, - userConfig: { - storage: "server", - users: { - "User!": "User", - }, - }, - localStorage: { - "Comfy.userId": "User!", - "Comfy.userName": "User", - }, - }); - - // cant actually test switching user easily but can check the setting is present - expect(app.ui.settings.settingsLookup["Comfy.SwitchUser"]).toBeTruthy(); - }); - }); - describe("single-user", () => { - it("doesnt show user creation if no default user", async () => { - const { app } = await start({ - resetEnv: true, - userConfig: { migrated: false, storage: "server" }, - }); - expectNoUserScreen(); - - // It should store the settings - const { api } = require("../../web/scripts/api"); - expect(api.storeSettings).toHaveBeenCalledTimes(1); - expect(api.storeUserData).toHaveBeenCalledTimes(1); - expect(api.storeUserData).toHaveBeenCalledWith("comfy.templates.json", null, { stringify: false }); - expect(app.isNewUserSession).toBeTruthy(); - }); - it("doesnt show user creation if default user", async () => { - const { app } = await start({ - resetEnv: true, - userConfig: { migrated: true, storage: "server" }, - }); - expectNoUserScreen(); - - // It should store the settings - const { api } = require("../../web/scripts/api"); - expect(api.storeSettings).toHaveBeenCalledTimes(0); - expect(api.storeUserData).toHaveBeenCalledTimes(0); - expect(app.isNewUserSession).toBeFalsy(); - }); - it("doesnt allow user switching", async () => { - const { app } = await start({ - resetEnv: true, - userConfig: { migrated: true, storage: "server" }, - }); - expectNoUserScreen(); - - expect(app.ui.settings.settingsLookup["Comfy.SwitchUser"]).toBeFalsy(); - }); - }); - describe("browser-user", () => { - it("doesnt show user creation if no default user", async () => { - const { app } = await start({ - resetEnv: true, - userConfig: { migrated: false, storage: "browser" }, - }); - expectNoUserScreen(); - - // It should store the settings - const { api } = require("../../web/scripts/api"); - expect(api.storeSettings).toHaveBeenCalledTimes(0); - expect(api.storeUserData).toHaveBeenCalledTimes(0); - expect(app.isNewUserSession).toBeFalsy(); - }); - it("doesnt show user creation if default user", async () => { - const { app } = await start({ - resetEnv: true, - userConfig: { migrated: true, storage: "server" }, - }); - expectNoUserScreen(); - - // It should store the settings - const { api } = require("../../web/scripts/api"); - expect(api.storeSettings).toHaveBeenCalledTimes(0); - expect(api.storeUserData).toHaveBeenCalledTimes(0); - expect(app.isNewUserSession).toBeFalsy(); - }); - it("doesnt allow user switching", async () => { - const { app } = await start({ - resetEnv: true, - userConfig: { migrated: true, storage: "browser" }, - }); - expectNoUserScreen(); - - expect(app.ui.settings.settingsLookup["Comfy.SwitchUser"]).toBeFalsy(); - }); - }); -}); diff --git a/tests-ui/tests/widgetInputs.test.js b/tests-ui/tests/widgetInputs.test.js deleted file mode 100644 index 67e3fa341..000000000 --- a/tests-ui/tests/widgetInputs.test.js +++ /dev/null @@ -1,557 +0,0 @@ -// @ts-check -/// - -const { - start, - makeNodeDef, - checkBeforeAndAfterReload, - assertNotNullOrUndefined, - createDefaultWorkflow, -} = require("../utils"); -const lg = require("../utils/litegraph"); - -/** - * @typedef { import("../utils/ezgraph") } Ez - * @typedef { ReturnType["ez"] } EzNodeFactory - */ - -/** - * @param { EzNodeFactory } ez - * @param { InstanceType } graph - * @param { InstanceType } input - * @param { string } widgetType - * @param { number } controlWidgetCount - * @returns - */ -async function connectPrimitiveAndReload(ez, graph, input, widgetType, controlWidgetCount = 0) { - // Connect to primitive and ensure its still connected after - let primitive = ez.PrimitiveNode(); - primitive.outputs[0].connectTo(input); - - await checkBeforeAndAfterReload(graph, async () => { - primitive = graph.find(primitive); - let { connections } = primitive.outputs[0]; - expect(connections).toHaveLength(1); - expect(connections[0].targetNode.id).toBe(input.node.node.id); - - // Ensure widget is correct type - const valueWidget = primitive.widgets.value; - expect(valueWidget.widget.type).toBe(widgetType); - - // Check if control_after_generate should be added - if (controlWidgetCount) { - const controlWidget = primitive.widgets.control_after_generate; - expect(controlWidget.widget.type).toBe("combo"); - if (widgetType === "combo") { - const filterWidget = primitive.widgets.control_filter_list; - expect(filterWidget.widget.type).toBe("string"); - } - } - - // Ensure we dont have other widgets - expect(primitive.node.widgets).toHaveLength(1 + controlWidgetCount); - }); - - return primitive; -} - -describe("widget inputs", () => { - beforeEach(() => { - lg.setup(global); - }); - - afterEach(() => { - lg.teardown(global); - }); - - [ - { name: "int", type: "INT", widget: "number", control: 1 }, - { name: "float", type: "FLOAT", widget: "number", control: 1 }, - { name: "text", type: "STRING" }, - { - name: "customtext", - type: "STRING", - opt: { multiline: true }, - }, - { name: "toggle", type: "BOOLEAN" }, - { name: "combo", type: ["a", "b", "c"], control: 2 }, - ].forEach((c) => { - test(`widget conversion + primitive works on ${c.name}`, async () => { - const { ez, graph } = await start({ - mockNodeDefs: makeNodeDef("TestNode", { [c.name]: [c.type, c.opt ?? {}] }), - }); - - // Create test node and convert to input - const n = ez.TestNode(); - const w = n.widgets[c.name]; - w.convertToInput(); - expect(w.isConvertedToInput).toBeTruthy(); - const input = w.getConvertedInput(); - expect(input).toBeTruthy(); - - // @ts-ignore : input is valid here - await connectPrimitiveAndReload(ez, graph, input, c.widget ?? c.name, c.control); - }); - }); - - test("converted widget works after reload", async () => { - const { ez, graph } = await start(); - let n = ez.CheckpointLoaderSimple(); - - const inputCount = n.inputs.length; - - // Convert ckpt name to an input - n.widgets.ckpt_name.convertToInput(); - expect(n.widgets.ckpt_name.isConvertedToInput).toBeTruthy(); - expect(n.inputs.ckpt_name).toBeTruthy(); - expect(n.inputs.length).toEqual(inputCount + 1); - - // Convert back to widget and ensure input is removed - n.widgets.ckpt_name.convertToWidget(); - expect(n.widgets.ckpt_name.isConvertedToInput).toBeFalsy(); - expect(n.inputs.ckpt_name).toBeFalsy(); - expect(n.inputs.length).toEqual(inputCount); - - // Convert again and reload the graph to ensure it maintains state - n.widgets.ckpt_name.convertToInput(); - expect(n.inputs.length).toEqual(inputCount + 1); - - const primitive = await connectPrimitiveAndReload(ez, graph, n.inputs.ckpt_name, "combo", 2); - - // Disconnect & reconnect - primitive.outputs[0].connections[0].disconnect(); - let { connections } = primitive.outputs[0]; - expect(connections).toHaveLength(0); - - primitive.outputs[0].connectTo(n.inputs.ckpt_name); - ({ connections } = primitive.outputs[0]); - expect(connections).toHaveLength(1); - expect(connections[0].targetNode.id).toBe(n.node.id); - - // Convert back to widget and ensure input is removed - n.widgets.ckpt_name.convertToWidget(); - expect(n.widgets.ckpt_name.isConvertedToInput).toBeFalsy(); - expect(n.inputs.ckpt_name).toBeFalsy(); - expect(n.inputs.length).toEqual(inputCount); - }); - - test("converted widget works on clone", async () => { - const { graph, ez } = await start(); - let n = ez.CheckpointLoaderSimple(); - - // Convert the widget to an input - n.widgets.ckpt_name.convertToInput(); - expect(n.widgets.ckpt_name.isConvertedToInput).toBeTruthy(); - - // Clone the node - n.menu["Clone"].call(); - expect(graph.nodes).toHaveLength(2); - const clone = graph.nodes[1]; - expect(clone.id).not.toEqual(n.id); - - // Ensure the clone has an input - expect(clone.widgets.ckpt_name.isConvertedToInput).toBeTruthy(); - expect(clone.inputs.ckpt_name).toBeTruthy(); - - // Ensure primitive connects to both nodes - let primitive = ez.PrimitiveNode(); - primitive.outputs[0].connectTo(n.inputs.ckpt_name); - primitive.outputs[0].connectTo(clone.inputs.ckpt_name); - expect(primitive.outputs[0].connections).toHaveLength(2); - - // Convert back to widget and ensure input is removed - clone.widgets.ckpt_name.convertToWidget(); - expect(clone.widgets.ckpt_name.isConvertedToInput).toBeFalsy(); - expect(clone.inputs.ckpt_name).toBeFalsy(); - }); - - test("shows missing node error on custom node with converted input", async () => { - const { graph } = await start(); - - const dialogShow = jest.spyOn(graph.app.ui.dialog, "show"); - - await graph.app.loadGraphData({ - last_node_id: 3, - last_link_id: 4, - nodes: [ - { - id: 1, - type: "TestNode", - pos: [41.87329101561909, 389.7381480823742], - size: { 0: 220, 1: 374 }, - flags: {}, - order: 1, - mode: 0, - inputs: [{ name: "test", type: "FLOAT", link: 4, widget: { name: "test" }, slot_index: 0 }], - outputs: [], - properties: { "Node name for S&R": "TestNode" }, - widgets_values: [1], - }, - { - id: 3, - type: "PrimitiveNode", - pos: [-312, 433], - size: { 0: 210, 1: 82 }, - flags: {}, - order: 0, - mode: 0, - outputs: [{ links: [4], widget: { name: "test" } }], - title: "test", - properties: {}, - }, - ], - links: [[4, 3, 0, 1, 6, "FLOAT"]], - groups: [], - config: {}, - extra: {}, - version: 0.4, - }); - - expect(dialogShow).toBeCalledTimes(1); - expect(dialogShow.mock.calls[0][0].innerHTML).toContain("the following node types were not found"); - expect(dialogShow.mock.calls[0][0].innerHTML).toContain("TestNode"); - }); - - test("defaultInput widgets can be converted back to inputs", async () => { - const { graph, ez } = await start({ - mockNodeDefs: makeNodeDef("TestNode", { example: ["INT", { defaultInput: true }] }), - }); - - // Create test node and ensure it starts as an input - let n = ez.TestNode(); - let w = n.widgets.example; - expect(w.isConvertedToInput).toBeTruthy(); - let input = w.getConvertedInput(); - expect(input).toBeTruthy(); - - // Ensure it can be converted to - w.convertToWidget(); - expect(w.isConvertedToInput).toBeFalsy(); - expect(n.inputs.length).toEqual(0); - // and from - w.convertToInput(); - expect(w.isConvertedToInput).toBeTruthy(); - input = w.getConvertedInput(); - - // Reload and ensure it still only has 1 converted widget - if (!assertNotNullOrUndefined(input)) return; - - await connectPrimitiveAndReload(ez, graph, input, "number", 1); - n = graph.find(n); - expect(n.widgets).toHaveLength(1); - w = n.widgets.example; - expect(w.isConvertedToInput).toBeTruthy(); - - // Convert back to widget and ensure it is still a widget after reload - w.convertToWidget(); - await graph.reload(); - n = graph.find(n); - expect(n.widgets).toHaveLength(1); - expect(n.widgets[0].isConvertedToInput).toBeFalsy(); - expect(n.inputs.length).toEqual(0); - }); - - test("forceInput widgets can not be converted back to inputs", async () => { - const { graph, ez } = await start({ - mockNodeDefs: makeNodeDef("TestNode", { example: ["INT", { forceInput: true }] }), - }); - - // Create test node and ensure it starts as an input - let n = ez.TestNode(); - let w = n.widgets.example; - expect(w.isConvertedToInput).toBeTruthy(); - const input = w.getConvertedInput(); - expect(input).toBeTruthy(); - - // Convert to widget should error - expect(() => w.convertToWidget()).toThrow(); - - // Reload and ensure it still only has 1 converted widget - if (assertNotNullOrUndefined(input)) { - await connectPrimitiveAndReload(ez, graph, input, "number", 1); - n = graph.find(n); - expect(n.widgets).toHaveLength(1); - expect(n.widgets.example.isConvertedToInput).toBeTruthy(); - } - }); - - test("primitive can connect to matching combos on converted widgets", async () => { - const { ez } = await start({ - mockNodeDefs: { - ...makeNodeDef("TestNode1", { example: [["A", "B", "C"], { forceInput: true }] }), - ...makeNodeDef("TestNode2", { example: [["A", "B", "C"], { forceInput: true }] }), - }, - }); - - const n1 = ez.TestNode1(); - const n2 = ez.TestNode2(); - const p = ez.PrimitiveNode(); - p.outputs[0].connectTo(n1.inputs[0]); - p.outputs[0].connectTo(n2.inputs[0]); - expect(p.outputs[0].connections).toHaveLength(2); - const valueWidget = p.widgets.value; - expect(valueWidget.widget.type).toBe("combo"); - expect(valueWidget.widget.options.values).toEqual(["A", "B", "C"]); - }); - - test("primitive can not connect to non matching combos on converted widgets", async () => { - const { ez } = await start({ - mockNodeDefs: { - ...makeNodeDef("TestNode1", { example: [["A", "B", "C"], { forceInput: true }] }), - ...makeNodeDef("TestNode2", { example: [["A", "B"], { forceInput: true }] }), - }, - }); - - const n1 = ez.TestNode1(); - const n2 = ez.TestNode2(); - const p = ez.PrimitiveNode(); - p.outputs[0].connectTo(n1.inputs[0]); - expect(() => p.outputs[0].connectTo(n2.inputs[0])).toThrow(); - expect(p.outputs[0].connections).toHaveLength(1); - }); - - test("combo output can not connect to non matching combos list input", async () => { - const { ez } = await start({ - mockNodeDefs: { - ...makeNodeDef("TestNode1", {}, [["A", "B"]]), - ...makeNodeDef("TestNode2", { example: [["A", "B"], { forceInput: true }] }), - ...makeNodeDef("TestNode3", { example: [["A", "B", "C"], { forceInput: true }] }), - }, - }); - - const n1 = ez.TestNode1(); - const n2 = ez.TestNode2(); - const n3 = ez.TestNode3(); - - n1.outputs[0].connectTo(n2.inputs[0]); - expect(() => n1.outputs[0].connectTo(n3.inputs[0])).toThrow(); - }); - - test("combo primitive can filter list when control_after_generate called", async () => { - const { ez } = await start({ - mockNodeDefs: { - ...makeNodeDef("TestNode1", { example: [["A", "B", "C", "D", "AA", "BB", "CC", "DD", "AAA", "BBB"], {}] }), - }, - }); - - const n1 = ez.TestNode1(); - n1.widgets.example.convertToInput(); - const p = ez.PrimitiveNode(); - p.outputs[0].connectTo(n1.inputs[0]); - - const value = p.widgets.value; - const control = p.widgets.control_after_generate.widget; - const filter = p.widgets.control_filter_list; - - expect(p.widgets.length).toBe(3); - control.value = "increment"; - expect(value.value).toBe("A"); - - // Manually trigger after queue when set to increment - control["afterQueued"](); - expect(value.value).toBe("B"); - - // Filter to items containing D - filter.value = "D"; - control["afterQueued"](); - expect(value.value).toBe("D"); - control["afterQueued"](); - expect(value.value).toBe("DD"); - - // Check decrement - value.value = "BBB"; - control.value = "decrement"; - filter.value = "B"; - control["afterQueued"](); - expect(value.value).toBe("BB"); - control["afterQueued"](); - expect(value.value).toBe("B"); - - // Check regex works - value.value = "BBB"; - filter.value = "/[AB]|^C$/"; - control["afterQueued"](); - expect(value.value).toBe("AAA"); - control["afterQueued"](); - expect(value.value).toBe("BB"); - control["afterQueued"](); - expect(value.value).toBe("AA"); - control["afterQueued"](); - expect(value.value).toBe("C"); - control["afterQueued"](); - expect(value.value).toBe("B"); - control["afterQueued"](); - expect(value.value).toBe("A"); - - // Check random - control.value = "randomize"; - filter.value = "/D/"; - for (let i = 0; i < 100; i++) { - control["afterQueued"](); - expect(value.value === "D" || value.value === "DD").toBeTruthy(); - } - - // Ensure it doesnt apply when fixed - control.value = "fixed"; - value.value = "B"; - filter.value = "C"; - control["afterQueued"](); - expect(value.value).toBe("B"); - }); - - describe("reroutes", () => { - async function checkOutput(graph, values) { - expect((await graph.toPrompt()).output).toStrictEqual({ - 1: { inputs: { ckpt_name: "model1.safetensors" }, class_type: "CheckpointLoaderSimple" }, - 2: { inputs: { text: "positive", clip: ["1", 1] }, class_type: "CLIPTextEncode" }, - 3: { inputs: { text: "negative", clip: ["1", 1] }, class_type: "CLIPTextEncode" }, - 4: { - inputs: { width: values.width ?? 512, height: values.height ?? 512, batch_size: values?.batch_size ?? 1 }, - class_type: "EmptyLatentImage", - }, - 5: { - inputs: { - seed: 0, - steps: 20, - cfg: 8, - sampler_name: "euler", - scheduler: values?.scheduler ?? "normal", - denoise: 1, - model: ["1", 0], - positive: ["2", 0], - negative: ["3", 0], - latent_image: ["4", 0], - }, - class_type: "KSampler", - }, - 6: { inputs: { samples: ["5", 0], vae: ["1", 2] }, class_type: "VAEDecode" }, - 7: { - inputs: { filename_prefix: values.filename_prefix ?? "ComfyUI", images: ["6", 0] }, - class_type: "SaveImage", - }, - }); - } - - async function waitForWidget(node) { - // widgets are created slightly after the graph is ready - // hard to find an exact hook to get these so just wait for them to be ready - for (let i = 0; i < 10; i++) { - await new Promise((r) => setTimeout(r, 10)); - if (node.widgets?.value) { - return; - } - } - } - - it("can connect primitive via a reroute path to a widget input", async () => { - const { ez, graph } = await start(); - const nodes = createDefaultWorkflow(ez, graph); - - nodes.empty.widgets.width.convertToInput(); - nodes.sampler.widgets.scheduler.convertToInput(); - nodes.save.widgets.filename_prefix.convertToInput(); - - let widthReroute = ez.Reroute(); - let schedulerReroute = ez.Reroute(); - let fileReroute = ez.Reroute(); - - let widthNext = widthReroute; - let schedulerNext = schedulerReroute; - let fileNext = fileReroute; - - for (let i = 0; i < 5; i++) { - let next = ez.Reroute(); - widthNext.outputs[0].connectTo(next.inputs[0]); - widthNext = next; - - next = ez.Reroute(); - schedulerNext.outputs[0].connectTo(next.inputs[0]); - schedulerNext = next; - - next = ez.Reroute(); - fileNext.outputs[0].connectTo(next.inputs[0]); - fileNext = next; - } - - widthNext.outputs[0].connectTo(nodes.empty.inputs.width); - schedulerNext.outputs[0].connectTo(nodes.sampler.inputs.scheduler); - fileNext.outputs[0].connectTo(nodes.save.inputs.filename_prefix); - - let widthPrimitive = ez.PrimitiveNode(); - let schedulerPrimitive = ez.PrimitiveNode(); - let filePrimitive = ez.PrimitiveNode(); - - widthPrimitive.outputs[0].connectTo(widthReroute.inputs[0]); - schedulerPrimitive.outputs[0].connectTo(schedulerReroute.inputs[0]); - filePrimitive.outputs[0].connectTo(fileReroute.inputs[0]); - expect(widthPrimitive.widgets.value.value).toBe(512); - widthPrimitive.widgets.value.value = 1024; - expect(schedulerPrimitive.widgets.value.value).toBe("normal"); - schedulerPrimitive.widgets.value.value = "simple"; - expect(filePrimitive.widgets.value.value).toBe("ComfyUI"); - filePrimitive.widgets.value.value = "ComfyTest"; - - await checkBeforeAndAfterReload(graph, async () => { - widthPrimitive = graph.find(widthPrimitive); - schedulerPrimitive = graph.find(schedulerPrimitive); - filePrimitive = graph.find(filePrimitive); - await waitForWidget(filePrimitive); - expect(widthPrimitive.widgets.length).toBe(2); - expect(schedulerPrimitive.widgets.length).toBe(3); - expect(filePrimitive.widgets.length).toBe(1); - - await checkOutput(graph, { - width: 1024, - scheduler: "simple", - filename_prefix: "ComfyTest", - }); - }); - }); - it("can connect primitive via a reroute path to multiple widget inputs", async () => { - const { ez, graph } = await start(); - const nodes = createDefaultWorkflow(ez, graph); - - nodes.empty.widgets.width.convertToInput(); - nodes.empty.widgets.height.convertToInput(); - nodes.empty.widgets.batch_size.convertToInput(); - - let reroute = ez.Reroute(); - let prevReroute = reroute; - for (let i = 0; i < 5; i++) { - const next = ez.Reroute(); - prevReroute.outputs[0].connectTo(next.inputs[0]); - prevReroute = next; - } - - const r1 = ez.Reroute(prevReroute.outputs[0]); - const r2 = ez.Reroute(prevReroute.outputs[0]); - const r3 = ez.Reroute(r2.outputs[0]); - const r4 = ez.Reroute(r2.outputs[0]); - - r1.outputs[0].connectTo(nodes.empty.inputs.width); - r3.outputs[0].connectTo(nodes.empty.inputs.height); - r4.outputs[0].connectTo(nodes.empty.inputs.batch_size); - - let primitive = ez.PrimitiveNode(); - primitive.outputs[0].connectTo(reroute.inputs[0]); - expect(primitive.widgets.value.value).toBe(1); - primitive.widgets.value.value = 64; - - await checkBeforeAndAfterReload(graph, async (r) => { - primitive = graph.find(primitive); - await waitForWidget(primitive); - - // Ensure widget configs are merged - expect(primitive.widgets.value.widget.options?.min).toBe(16); // width/height min - expect(primitive.widgets.value.widget.options?.max).toBe(4096); // batch max - expect(primitive.widgets.value.widget.options?.step).toBe(80); // width/height step * 10 - - await checkOutput(graph, { - width: 64, - height: 64, - batch_size: 64, - }); - }); - }); - }); -}); diff --git a/tests-ui/utils/ezgraph.js b/tests-ui/utils/ezgraph.js deleted file mode 100644 index 97be7aa72..000000000 --- a/tests-ui/utils/ezgraph.js +++ /dev/null @@ -1,452 +0,0 @@ -// @ts-check -/// - -/** - * @typedef { import("../../web/scripts/app")["app"] } app - * @typedef { import("../../web/types/litegraph") } LG - * @typedef { import("../../web/types/litegraph").IWidget } IWidget - * @typedef { import("../../web/types/litegraph").ContextMenuItem } ContextMenuItem - * @typedef { import("../../web/types/litegraph").INodeInputSlot } INodeInputSlot - * @typedef { import("../../web/types/litegraph").INodeOutputSlot } INodeOutputSlot - * @typedef { InstanceType & { widgets?: Array } } LGNode - * @typedef { (...args: EzOutput[] | [...EzOutput[], Record]) => EzNode } EzNodeFactory - */ - -export class EzConnection { - /** @type { app } */ - app; - /** @type { InstanceType } */ - link; - - get originNode() { - return new EzNode(this.app, this.app.graph.getNodeById(this.link.origin_id)); - } - - get originOutput() { - return this.originNode.outputs[this.link.origin_slot]; - } - - get targetNode() { - return new EzNode(this.app, this.app.graph.getNodeById(this.link.target_id)); - } - - get targetInput() { - return this.targetNode.inputs[this.link.target_slot]; - } - - /** - * @param { app } app - * @param { InstanceType } link - */ - constructor(app, link) { - this.app = app; - this.link = link; - } - - disconnect() { - this.targetInput.disconnect(); - } -} - -export class EzSlot { - /** @type { EzNode } */ - node; - /** @type { number } */ - index; - - /** - * @param { EzNode } node - * @param { number } index - */ - constructor(node, index) { - this.node = node; - this.index = index; - } -} - -export class EzInput extends EzSlot { - /** @type { INodeInputSlot } */ - input; - - /** - * @param { EzNode } node - * @param { number } index - * @param { INodeInputSlot } input - */ - constructor(node, index, input) { - super(node, index); - this.input = input; - } - - get connection() { - const link = this.node.node.inputs?.[this.index]?.link; - if (link == null) { - return null; - } - return new EzConnection(this.node.app, this.node.app.graph.links[link]); - } - - disconnect() { - this.node.node.disconnectInput(this.index); - } -} - -export class EzOutput extends EzSlot { - /** @type { INodeOutputSlot } */ - output; - - /** - * @param { EzNode } node - * @param { number } index - * @param { INodeOutputSlot } output - */ - constructor(node, index, output) { - super(node, index); - this.output = output; - } - - get connections() { - return (this.node.node.outputs?.[this.index]?.links ?? []).map( - (l) => new EzConnection(this.node.app, this.node.app.graph.links[l]) - ); - } - - /** - * @param { EzInput } input - */ - connectTo(input) { - if (!input) throw new Error("Invalid input"); - - /** - * @type { LG["LLink"] | null } - */ - const link = this.node.node.connect(this.index, input.node.node, input.index); - if (!link) { - const inp = input.input; - const inName = inp.name || inp.label || inp.type; - throw new Error( - `Connecting from ${input.node.node.type}#${input.node.id}[${inName}#${input.index}] -> ${this.node.node.type}#${this.node.id}[${ - this.output.name ?? this.output.type - }#${this.index}] failed.` - ); - } - return link; - } -} - -export class EzNodeMenuItem { - /** @type { EzNode } */ - node; - /** @type { number } */ - index; - /** @type { ContextMenuItem } */ - item; - - /** - * @param { EzNode } node - * @param { number } index - * @param { ContextMenuItem } item - */ - constructor(node, index, item) { - this.node = node; - this.index = index; - this.item = item; - } - - call(selectNode = true) { - if (!this.item?.callback) throw new Error(`Menu Item ${this.item?.content ?? "[null]"} has no callback.`); - if (selectNode) { - this.node.select(); - } - return this.item.callback.call(this.node.node, undefined, undefined, undefined, undefined, this.node.node); - } -} - -export class EzWidget { - /** @type { EzNode } */ - node; - /** @type { number } */ - index; - /** @type { IWidget } */ - widget; - - /** - * @param { EzNode } node - * @param { number } index - * @param { IWidget } widget - */ - constructor(node, index, widget) { - this.node = node; - this.index = index; - this.widget = widget; - } - - get value() { - return this.widget.value; - } - - set value(v) { - this.widget.value = v; - this.widget.callback?.call?.(this.widget, v) - } - - get isConvertedToInput() { - // @ts-ignore : this type is valid for converted widgets - return this.widget.type === "converted-widget"; - } - - getConvertedInput() { - if (!this.isConvertedToInput) throw new Error(`Widget ${this.widget.name} is not converted to input.`); - - return this.node.inputs.find((inp) => inp.input["widget"]?.name === this.widget.name); - } - - convertToWidget() { - if (!this.isConvertedToInput) - throw new Error(`Widget ${this.widget.name} cannot be converted as it is already a widget.`); - var menu = this.node.menu["Convert Input to Widget"].item.submenu.options; - var index = menu.findIndex(a => a.content == `Convert ${this.widget.name} to widget`); - menu[index].callback.call(); - } - - convertToInput() { - if (this.isConvertedToInput) - throw new Error(`Widget ${this.widget.name} cannot be converted as it is already an input.`); - var menu = this.node.menu["Convert Widget to Input"].item.submenu.options; - var index = menu.findIndex(a => a.content == `Convert ${this.widget.name} to input`); - menu[index].callback.call(); - } -} - -export class EzNode { - /** @type { app } */ - app; - /** @type { LGNode } */ - node; - - /** - * @param { app } app - * @param { LGNode } node - */ - constructor(app, node) { - this.app = app; - this.node = node; - } - - get id() { - return this.node.id; - } - - get inputs() { - return this.#makeLookupArray("inputs", "name", EzInput); - } - - get outputs() { - return this.#makeLookupArray("outputs", "name", EzOutput); - } - - get widgets() { - return this.#makeLookupArray("widgets", "name", EzWidget); - } - - get menu() { - return this.#makeLookupArray(() => this.app.canvas.getNodeMenuOptions(this.node), "content", EzNodeMenuItem); - } - - get isRemoved() { - return !this.app.graph.getNodeById(this.id); - } - - select(addToSelection = false) { - this.app.canvas.selectNode(this.node, addToSelection); - } - - // /** - // * @template { "inputs" | "outputs" } T - // * @param { T } type - // * @returns { Record & (type extends "inputs" ? EzInput [] : EzOutput[]) } - // */ - // #getSlotItems(type) { - // // @ts-ignore : these items are correct - // return (this.node[type] ?? []).reduce((p, s, i) => { - // if (s.name in p) { - // throw new Error(`Unable to store input ${s.name} on array as name conflicts.`); - // } - // // @ts-ignore - // p.push((p[s.name] = new (type === "inputs" ? EzInput : EzOutput)(this, i, s))); - // return p; - // }, Object.assign([], { $: this })); - // } - - /** - * @template { { new(node: EzNode, index: number, obj: any): any } } T - * @param { "inputs" | "outputs" | "widgets" | (() => Array) } nodeProperty - * @param { string } nameProperty - * @param { T } ctor - * @returns { Record> & Array> } - */ - #makeLookupArray(nodeProperty, nameProperty, ctor) { - const items = typeof nodeProperty === "function" ? nodeProperty() : this.node[nodeProperty]; - // @ts-ignore - return (items ?? []).reduce((p, s, i) => { - if (!s) return p; - - const name = s[nameProperty]; - const item = new ctor(this, i, s); - // @ts-ignore - p.push(item); - if (name) { - // @ts-ignore - if (name in p) { - throw new Error(`Unable to store ${nodeProperty} ${name} on array as name conflicts.`); - } - } - // @ts-ignore - p[name] = item; - return p; - }, Object.assign([], { $: this })); - } -} - -export class EzGraph { - /** @type { app } */ - app; - - /** - * @param { app } app - */ - constructor(app) { - this.app = app; - } - - get nodes() { - return this.app.graph._nodes.map((n) => new EzNode(this.app, n)); - } - - clear() { - this.app.graph.clear(); - } - - arrange() { - this.app.graph.arrange(); - } - - stringify() { - return JSON.stringify(this.app.graph.serialize(), undefined); - } - - /** - * @param { number | LGNode | EzNode } obj - * @returns { EzNode } - */ - find(obj) { - let match; - let id; - if (typeof obj === "number") { - id = obj; - } else { - id = obj.id; - } - - match = this.app.graph.getNodeById(id); - - if (!match) { - throw new Error(`Unable to find node with ID ${id}.`); - } - - return new EzNode(this.app, match); - } - - /** - * @returns { Promise } - */ - reload() { - const graph = JSON.parse(JSON.stringify(this.app.graph.serialize())); - return new Promise((r) => { - this.app.graph.clear(); - setTimeout(async () => { - await this.app.loadGraphData(graph); - r(); - }, 10); - }); - } - - /** - * @returns { Promise<{ - * workflow: {}, - * output: Record - * }>}> } - */ - toPrompt() { - // @ts-ignore - return this.app.graphToPrompt(); - } -} - -export const Ez = { - /** - * Quickly build and interact with a ComfyUI graph - * @example - * const { ez, graph } = Ez.graph(app); - * graph.clear(); - * const [model, clip, vae] = ez.CheckpointLoaderSimple().outputs; - * const [pos] = ez.CLIPTextEncode(clip, { text: "positive" }).outputs; - * const [neg] = ez.CLIPTextEncode(clip, { text: "negative" }).outputs; - * const [latent] = ez.KSampler(model, pos, neg, ...ez.EmptyLatentImage().outputs).outputs; - * const [image] = ez.VAEDecode(latent, vae).outputs; - * const saveNode = ez.SaveImage(image); - * console.log(saveNode); - * graph.arrange(); - * @param { app } app - * @param { LG["LiteGraph"] } LiteGraph - * @param { LG["LGraphCanvas"] } LGraphCanvas - * @param { boolean } clearGraph - * @returns { { graph: EzGraph, ez: Record } } - */ - graph(app, LiteGraph = window["LiteGraph"], LGraphCanvas = window["LGraphCanvas"], clearGraph = true) { - // Always set the active canvas so things work - LGraphCanvas.active_canvas = app.canvas; - - if (clearGraph) { - app.graph.clear(); - } - - // @ts-ignore : this proxy handles utility methods & node creation - const factory = new Proxy( - {}, - { - get(_, p) { - if (typeof p !== "string") throw new Error("Invalid node"); - const node = LiteGraph.createNode(p); - if (!node) throw new Error(`Unknown node "${p}"`); - app.graph.add(node); - - /** - * @param {Parameters} args - */ - return function (...args) { - const ezNode = new EzNode(app, node); - const inputs = ezNode.inputs; - - let slot = 0; - for (const arg of args) { - if (arg instanceof EzOutput) { - arg.connectTo(inputs[slot++]); - } else { - for (const k in arg) { - ezNode.widgets[k].value = arg[k]; - } - } - } - - return ezNode; - }; - }, - } - ); - - return { graph: new EzGraph(app), ez: factory }; - }, -}; diff --git a/tests-ui/utils/index.js b/tests-ui/utils/index.js deleted file mode 100644 index 74b6cf93d..000000000 --- a/tests-ui/utils/index.js +++ /dev/null @@ -1,129 +0,0 @@ -const { mockApi } = require("./setup"); -const { Ez } = require("./ezgraph"); -const lg = require("./litegraph"); -const fs = require("fs"); -const path = require("path"); - -const html = fs.readFileSync(path.resolve(__dirname, "../../web/index.html")) - -/** - * - * @param { Parameters[0] & { - * resetEnv?: boolean, - * preSetup?(app): Promise, - * localStorage?: Record - * } } config - * @returns - */ -export async function start(config = {}) { - if(config.resetEnv) { - jest.resetModules(); - jest.resetAllMocks(); - lg.setup(global); - localStorage.clear(); - sessionStorage.clear(); - } - - Object.assign(localStorage, config.localStorage ?? {}); - document.body.innerHTML = html; - - mockApi(config); - const { app } = require("../../web/scripts/app"); - config.preSetup?.(app); - await app.setup(); - - return { ...Ez.graph(app, global["LiteGraph"], global["LGraphCanvas"]), app }; -} - -/** - * @param { ReturnType["graph"] } graph - * @param { (hasReloaded: boolean) => (Promise | void) } cb - */ -export async function checkBeforeAndAfterReload(graph, cb) { - await cb(false); - await graph.reload(); - await cb(true); -} - -/** - * @param { string } name - * @param { Record } input - * @param { (string | string[])[] | Record } output - * @returns { Record } - */ -export function makeNodeDef(name, input, output = {}) { - const nodeDef = { - name, - category: "test", - output: [], - output_name: [], - output_is_list: [], - input: { - required: {}, - }, - }; - for (const k in input) { - nodeDef.input.required[k] = typeof input[k] === "string" ? [input[k], {}] : [...input[k]]; - } - if (output instanceof Array) { - output = output.reduce((p, c) => { - p[c] = c; - return p; - }, {}); - } - for (const k in output) { - nodeDef.output.push(output[k]); - nodeDef.output_name.push(k); - nodeDef.output_is_list.push(false); - } - - return { [name]: nodeDef }; -} - -/** -/** - * @template { any } T - * @param { T } x - * @returns { x is Exclude } - */ -export function assertNotNullOrUndefined(x) { - expect(x).not.toEqual(null); - expect(x).not.toEqual(undefined); - return true; -} - -/** - * - * @param { ReturnType["ez"] } ez - * @param { ReturnType["graph"] } graph - */ -export function createDefaultWorkflow(ez, graph) { - graph.clear(); - const ckpt = ez.CheckpointLoaderSimple(); - - const pos = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: "positive" }); - const neg = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: "negative" }); - - const empty = ez.EmptyLatentImage(); - const sampler = ez.KSampler( - ckpt.outputs.MODEL, - pos.outputs.CONDITIONING, - neg.outputs.CONDITIONING, - empty.outputs.LATENT - ); - - const decode = ez.VAEDecode(sampler.outputs.LATENT, ckpt.outputs.VAE); - const save = ez.SaveImage(decode.outputs.IMAGE); - graph.arrange(); - - return { ckpt, pos, neg, empty, sampler, decode, save }; -} - -export async function getNodeDefs() { - const { api } = require("../../web/scripts/api"); - return api.getNodeDefs(); -} - -export async function getNodeDef(nodeId) { - return (await getNodeDefs())[nodeId]; -} \ No newline at end of file diff --git a/tests-ui/utils/litegraph.js b/tests-ui/utils/litegraph.js deleted file mode 100644 index 777f8c3ba..000000000 --- a/tests-ui/utils/litegraph.js +++ /dev/null @@ -1,36 +0,0 @@ -const fs = require("fs"); -const path = require("path"); -const { nop } = require("../utils/nopProxy"); - -function forEachKey(cb) { - for (const k of [ - "LiteGraph", - "LGraph", - "LLink", - "LGraphNode", - "LGraphGroup", - "DragAndScale", - "LGraphCanvas", - "ContextMenu", - ]) { - cb(k); - } -} - -export function setup(ctx) { - const lg = fs.readFileSync(path.resolve("../web/lib/litegraph.core.js"), "utf-8"); - const globalTemp = {}; - (function (console) { - eval(lg); - }).call(globalTemp, nop); - - forEachKey((k) => (ctx[k] = globalTemp[k])); - require(path.resolve("../web/lib/litegraph.extensions.js")); -} - -export function teardown(ctx) { - forEachKey((k) => delete ctx[k]); - - // Clear document after each run - document.getElementsByTagName("html")[0].innerHTML = ""; -} diff --git a/tests-ui/utils/nopProxy.js b/tests-ui/utils/nopProxy.js deleted file mode 100644 index 2502d9d03..000000000 --- a/tests-ui/utils/nopProxy.js +++ /dev/null @@ -1,6 +0,0 @@ -export const nop = new Proxy(function () {}, { - get: () => nop, - set: () => true, - apply: () => nop, - construct: () => nop, -}); diff --git a/tests-ui/utils/setup.js b/tests-ui/utils/setup.js deleted file mode 100644 index 3a6d69b72..000000000 --- a/tests-ui/utils/setup.js +++ /dev/null @@ -1,82 +0,0 @@ -require("../../web/scripts/api"); - -const fs = require("fs"); -const path = require("path"); -function* walkSync(dir) { - const files = fs.readdirSync(dir, { withFileTypes: true }); - for (const file of files) { - if (file.isDirectory()) { - yield* walkSync(path.join(dir, file.name)); - } else { - yield path.join(dir, file.name); - } - } -} - -/** - * @typedef { import("../../web/types/comfy").ComfyObjectInfo } ComfyObjectInfo - */ - -/** - * @param {{ - * mockExtensions?: string[], - * mockNodeDefs?: Record, -* settings?: Record -* userConfig?: {storage: "server" | "browser", users?: Record, migrated?: boolean }, -* userData?: Record - * }} config - */ -export function mockApi(config = {}) { - let { mockExtensions, mockNodeDefs, userConfig, settings, userData } = { - userConfig, - settings: {}, - userData: {}, - ...config, - }; - if (!mockExtensions) { - mockExtensions = Array.from(walkSync(path.resolve("../web/extensions/core"))) - .filter((x) => x.endsWith(".js")) - .map((x) => path.relative(path.resolve("../web"), x)); - } - if (!mockNodeDefs) { - mockNodeDefs = JSON.parse(fs.readFileSync(path.resolve("./data/object_info.json"))); - } - - const events = new EventTarget(); - const mockApi = { - addEventListener: events.addEventListener.bind(events), - removeEventListener: events.removeEventListener.bind(events), - dispatchEvent: events.dispatchEvent.bind(events), - getSystemStats: jest.fn(), - getExtensions: jest.fn(() => mockExtensions), - getNodeDefs: jest.fn(() => mockNodeDefs), - init: jest.fn(), - apiURL: jest.fn((x) => "../../web/" + x), - createUser: jest.fn((username) => { - if(username in userConfig.users) { - return { status: 400, json: () => "Duplicate" } - } - userConfig.users[username + "!"] = username; - return { status: 200, json: () => username + "!" } - }), - getUserConfig: jest.fn(() => userConfig ?? { storage: "browser", migrated: false }), - getSettings: jest.fn(() => settings), - storeSettings: jest.fn((v) => Object.assign(settings, v)), - getUserData: jest.fn((f) => { - if (f in userData) { - return { status: 200, json: () => userData[f] }; - } else { - return { status: 404 }; - } - }), - storeUserData: jest.fn((file, data) => { - userData[file] = data; - }), - listUserData: jest.fn(() => []) - }; - jest.mock("../../web/scripts/api", () => ({ - get api() { - return mockApi; - }, - })); -} diff --git a/tests-unit/README.md b/tests-unit/README.md deleted file mode 100644 index 94abd9853..000000000 --- a/tests-unit/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Pytest Unit Tests - -## Install test dependencies - -`pip install -r tests-units/requirements.txt` - -## Run tests -`pytest tests-units/` diff --git a/tests-unit/app_test/__init__.py b/tests-unit/app_test/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests-unit/app_test/frontend_manager_test.py b/tests-unit/app_test/frontend_manager_test.py deleted file mode 100644 index 637869cfb..000000000 --- a/tests-unit/app_test/frontend_manager_test.py +++ /dev/null @@ -1,100 +0,0 @@ -import argparse -import pytest -from requests.exceptions import HTTPError - -from app.frontend_management import ( - FrontendManager, - FrontEndProvider, - Release, -) -from comfy.cli_args import DEFAULT_VERSION_STRING - - -@pytest.fixture -def mock_releases(): - return [ - Release( - id=1, - tag_name="1.0.0", - name="Release 1.0.0", - prerelease=False, - created_at="2022-01-01T00:00:00Z", - published_at="2022-01-01T00:00:00Z", - body="Release notes for 1.0.0", - assets=[{"name": "dist.zip", "url": "https://example.com/dist.zip"}], - ), - Release( - id=2, - tag_name="2.0.0", - name="Release 2.0.0", - prerelease=False, - created_at="2022-02-01T00:00:00Z", - published_at="2022-02-01T00:00:00Z", - body="Release notes for 2.0.0", - assets=[{"name": "dist.zip", "url": "https://example.com/dist.zip"}], - ), - ] - - -@pytest.fixture -def mock_provider(mock_releases): - provider = FrontEndProvider( - owner="test-owner", - repo="test-repo", - ) - provider.all_releases = mock_releases - provider.latest_release = mock_releases[1] - FrontendManager.PROVIDERS = [provider] - return provider - - -def test_get_release(mock_provider, mock_releases): - version = "1.0.0" - release = mock_provider.get_release(version) - assert release == mock_releases[0] - - -def test_get_release_latest(mock_provider, mock_releases): - version = "latest" - release = mock_provider.get_release(version) - assert release == mock_releases[1] - - -def test_get_release_invalid_version(mock_provider): - version = "invalid" - with pytest.raises(ValueError): - mock_provider.get_release(version) - - -def test_init_frontend_default(): - version_string = DEFAULT_VERSION_STRING - frontend_path = FrontendManager.init_frontend(version_string) - assert frontend_path == FrontendManager.DEFAULT_FRONTEND_PATH - - -def test_init_frontend_invalid_version(): - version_string = "test-owner/test-repo@1.100.99" - with pytest.raises(HTTPError): - FrontendManager.init_frontend_unsafe(version_string) - - -def test_init_frontend_invalid_provider(): - version_string = "invalid/invalid@latest" - with pytest.raises(HTTPError): - FrontendManager.init_frontend_unsafe(version_string) - - -def test_parse_version_string(): - version_string = "owner/repo@1.0.0" - repo_owner, repo_name, version = FrontendManager.parse_version_string( - version_string - ) - assert repo_owner == "owner" - assert repo_name == "repo" - assert version == "1.0.0" - - -def test_parse_version_string_invalid(): - version_string = "invalid" - with pytest.raises(argparse.ArgumentTypeError): - FrontendManager.parse_version_string(version_string) diff --git a/tests-unit/requirements.txt b/tests-unit/requirements.txt deleted file mode 100644 index 0587502f8..000000000 --- a/tests-unit/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -pytest>=7.8.0 diff --git a/tests/README.md b/tests/README.md deleted file mode 100644 index 2005fd45b..000000000 --- a/tests/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# Automated Testing - -## Running tests locally - -Additional requirements for running tests: -``` -pip install pytest -pip install websocket-client==1.6.1 -opencv-python==4.6.0.66 -scikit-image==0.21.0 -``` -Run inference tests: -``` -pytest tests/inference -``` - -## Quality regression test -Compares images in 2 directories to ensure they are the same - -1) Run an inference test to save a directory of "ground truth" images -``` - pytest tests/inference --output_dir tests/inference/baseline -``` -2) Make code edits - -3) Run inference and quality comparison tests -``` -pytest -``` \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/compare/conftest.py b/tests/compare/conftest.py deleted file mode 100644 index dd5078c9e..000000000 --- a/tests/compare/conftest.py +++ /dev/null @@ -1,41 +0,0 @@ -import os -import pytest - -# Command line arguments for pytest -def pytest_addoption(parser): - parser.addoption('--baseline_dir', action="store", default='tests/inference/baseline', help='Directory for ground-truth images') - parser.addoption('--test_dir', action="store", default='tests/inference/samples', help='Directory for images to test') - parser.addoption('--metrics_file', action="store", default='tests/metrics.md', help='Output file for metrics') - parser.addoption('--img_output_dir', action="store", default='tests/compare/samples', help='Output directory for diff metric images') - -# This initializes args at the beginning of the test session -@pytest.fixture(scope="session", autouse=True) -def args_pytest(pytestconfig): - args = {} - args['baseline_dir'] = pytestconfig.getoption('baseline_dir') - args['test_dir'] = pytestconfig.getoption('test_dir') - args['metrics_file'] = pytestconfig.getoption('metrics_file') - args['img_output_dir'] = pytestconfig.getoption('img_output_dir') - - # Initialize metrics file - with open(args['metrics_file'], 'a') as f: - # if file is empty, write header - if os.stat(args['metrics_file']).st_size == 0: - f.write("| date | run | file | status | value | \n") - f.write("| --- | --- | --- | --- | --- | \n") - - return args - - -def gather_file_basenames(directory: str): - files = [] - for file in os.listdir(directory): - if file.endswith(".png"): - files.append(file) - return files - -# Creates the list of baseline file names to use as a fixture -def pytest_generate_tests(metafunc): - if "baseline_fname" in metafunc.fixturenames: - baseline_fnames = gather_file_basenames(metafunc.config.getoption("baseline_dir")) - metafunc.parametrize("baseline_fname", baseline_fnames) diff --git a/tests/compare/test_quality.py b/tests/compare/test_quality.py deleted file mode 100644 index 92a2d5a8b..000000000 --- a/tests/compare/test_quality.py +++ /dev/null @@ -1,195 +0,0 @@ -import datetime -import numpy as np -import os -from PIL import Image -import pytest -from pytest import fixture -from typing import Tuple, List - -from cv2 import imread, cvtColor, COLOR_BGR2RGB -from skimage.metrics import structural_similarity as ssim - - -""" -This test suite compares images in 2 directories by file name -The directories are specified by the command line arguments --baseline_dir and --test_dir - -""" -# ssim: Structural Similarity Index -# Returns a tuple of (ssim, diff_image) -def ssim_score(img0: np.ndarray, img1: np.ndarray) -> Tuple[float, np.ndarray]: - score, diff = ssim(img0, img1, channel_axis=-1, full=True) - # rescale the difference image to 0-255 range - diff = (diff * 255).astype("uint8") - return score, diff - -# Metrics must return a tuple of (score, diff_image) -METRICS = {"ssim": ssim_score} -METRICS_PASS_THRESHOLD = {"ssim": 0.95} - - -class TestCompareImageMetrics: - @fixture(scope="class") - def test_file_names(self, args_pytest): - test_dir = args_pytest['test_dir'] - fnames = self.gather_file_basenames(test_dir) - yield fnames - del fnames - - @fixture(scope="class", autouse=True) - def teardown(self, args_pytest): - yield - # Runs after all tests are complete - # Aggregate output files into a grid of images - baseline_dir = args_pytest['baseline_dir'] - test_dir = args_pytest['test_dir'] - img_output_dir = args_pytest['img_output_dir'] - metrics_file = args_pytest['metrics_file'] - - grid_dir = os.path.join(img_output_dir, "grid") - os.makedirs(grid_dir, exist_ok=True) - - for metric_dir in METRICS.keys(): - metric_path = os.path.join(img_output_dir, metric_dir) - for file in os.listdir(metric_path): - if file.endswith(".png"): - score = self.lookup_score_from_fname(file, metrics_file) - image_file_list = [] - image_file_list.append([ - os.path.join(baseline_dir, file), - os.path.join(test_dir, file), - os.path.join(metric_path, file) - ]) - # Create grid - image_list = [[Image.open(file) for file in files] for files in image_file_list] - grid = self.image_grid(image_list) - grid.save(os.path.join(grid_dir, f"{metric_dir}_{score:.3f}_{file}")) - - # Tests run for each baseline file name - @fixture() - def fname(self, baseline_fname): - yield baseline_fname - del baseline_fname - - def test_directories_not_empty(self, args_pytest): - baseline_dir = args_pytest['baseline_dir'] - test_dir = args_pytest['test_dir'] - assert len(os.listdir(baseline_dir)) != 0, f"Baseline directory {baseline_dir} is empty" - assert len(os.listdir(test_dir)) != 0, f"Test directory {test_dir} is empty" - - def test_dir_has_all_matching_metadata(self, fname, test_file_names, args_pytest): - # Check that all files in baseline_dir have a file in test_dir with matching metadata - baseline_file_path = os.path.join(args_pytest['baseline_dir'], fname) - file_paths = [os.path.join(args_pytest['test_dir'], f) for f in test_file_names] - file_match = self.find_file_match(baseline_file_path, file_paths) - assert file_match is not None, f"Could not find a file in {args_pytest['test_dir']} with matching metadata to {baseline_file_path}" - - # For a baseline image file, finds the corresponding file name in test_dir and - # compares the images using the metrics in METRICS - @pytest.mark.parametrize("metric", METRICS.keys()) - def test_pipeline_compare( - self, - args_pytest, - fname, - test_file_names, - metric, - ): - baseline_dir = args_pytest['baseline_dir'] - test_dir = args_pytest['test_dir'] - metrics_output_file = args_pytest['metrics_file'] - img_output_dir = args_pytest['img_output_dir'] - - baseline_file_path = os.path.join(baseline_dir, fname) - - # Find file match - file_paths = [os.path.join(test_dir, f) for f in test_file_names] - test_file = self.find_file_match(baseline_file_path, file_paths) - - # Run metrics - sample_baseline = self.read_img(baseline_file_path) - sample_secondary = self.read_img(test_file) - - score, metric_img = METRICS[metric](sample_baseline, sample_secondary) - metric_status = score > METRICS_PASS_THRESHOLD[metric] - - # Save metric values - with open(metrics_output_file, 'a') as f: - run_info = os.path.splitext(fname)[0] - metric_status_str = "PASS ✅" if metric_status else "FAIL ❌" - date_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - f.write(f"| {date_str} | {run_info} | {metric} | {metric_status_str} | {score} | \n") - - # Save metric image - metric_img_dir = os.path.join(img_output_dir, metric) - os.makedirs(metric_img_dir, exist_ok=True) - output_filename = f'{fname}' - Image.fromarray(metric_img).save(os.path.join(metric_img_dir, output_filename)) - - assert score > METRICS_PASS_THRESHOLD[metric] - - def read_img(self, filename: str) -> np.ndarray: - cvImg = imread(filename) - cvImg = cvtColor(cvImg, COLOR_BGR2RGB) - return cvImg - - def image_grid(self, img_list: list[list[Image.Image]]): - # imgs is a 2D list of images - # Assumes the input images are a rectangular grid of equal sized images - rows = len(img_list) - cols = len(img_list[0]) - - w, h = img_list[0][0].size - grid = Image.new('RGB', size=(cols*w, rows*h)) - - for i, row in enumerate(img_list): - for j, img in enumerate(row): - grid.paste(img, box=(j*w, i*h)) - return grid - - def lookup_score_from_fname(self, - fname: str, - metrics_output_file: str - ) -> float: - fname_basestr = os.path.splitext(fname)[0] - with open(metrics_output_file, 'r') as f: - for line in f: - if fname_basestr in line: - score = float(line.split('|')[5]) - return score - raise ValueError(f"Could not find score for {fname} in {metrics_output_file}") - - def gather_file_basenames(self, directory: str): - files = [] - for file in os.listdir(directory): - if file.endswith(".png"): - files.append(file) - return files - - def read_file_prompt(self, fname:str) -> str: - # Read prompt from image file metadata - img = Image.open(fname) - img.load() - return img.info['prompt'] - - def find_file_match(self, baseline_file: str, file_paths: List[str]): - # Find a file in file_paths with matching metadata to baseline_file - baseline_prompt = self.read_file_prompt(baseline_file) - - # Do not match empty prompts - if baseline_prompt is None or baseline_prompt == "": - return None - - # Find file match - # Reorder test_file_names so that the file with matching name is first - # This is an optimization because matching file names are more likely - # to have matching metadata if they were generated with the same script - basename = os.path.basename(baseline_file) - file_path_basenames = [os.path.basename(f) for f in file_paths] - if basename in file_path_basenames: - match_index = file_path_basenames.index(basename) - file_paths.insert(0, file_paths.pop(match_index)) - - for f in file_paths: - test_file_prompt = self.read_file_prompt(f) - if baseline_prompt == test_file_prompt: - return f \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 1a35880af..000000000 --- a/tests/conftest.py +++ /dev/null @@ -1,36 +0,0 @@ -import os -import pytest - -# Command line arguments for pytest -def pytest_addoption(parser): - parser.addoption('--output_dir', action="store", default='tests/inference/samples', help='Output directory for generated images') - parser.addoption("--listen", type=str, default="127.0.0.1", metavar="IP", nargs="?", const="0.0.0.0", help="Specify the IP address to listen on (default: 127.0.0.1). If --listen is provided without an argument, it defaults to 0.0.0.0. (listens on all)") - parser.addoption("--port", type=int, default=8188, help="Set the listen port.") - -# This initializes args at the beginning of the test session -@pytest.fixture(scope="session", autouse=True) -def args_pytest(pytestconfig): - args = {} - args['output_dir'] = pytestconfig.getoption('output_dir') - args['listen'] = pytestconfig.getoption('listen') - args['port'] = pytestconfig.getoption('port') - - os.makedirs(args['output_dir'], exist_ok=True) - - return args - -def pytest_collection_modifyitems(items): - # Modifies items so tests run in the correct order - - LAST_TESTS = ['test_quality'] - - # Move the last items to the end - last_items = [] - for test_name in LAST_TESTS: - for item in items.copy(): - print(item.module.__name__, item) - if item.module.__name__ == test_name: - last_items.append(item) - items.remove(item) - - items.extend(last_items) diff --git a/tests/inference/__init__.py b/tests/inference/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/inference/graphs/default_graph_sdxl1_0.json b/tests/inference/graphs/default_graph_sdxl1_0.json deleted file mode 100644 index c06c6829c..000000000 --- a/tests/inference/graphs/default_graph_sdxl1_0.json +++ /dev/null @@ -1,144 +0,0 @@ -{ - "4": { - "inputs": { - "ckpt_name": "sd_xl_base_1.0.safetensors" - }, - "class_type": "CheckpointLoaderSimple" - }, - "5": { - "inputs": { - "width": 1024, - "height": 1024, - "batch_size": 1 - }, - "class_type": "EmptyLatentImage" - }, - "6": { - "inputs": { - "text": "a photo of a cat", - "clip": [ - "4", - 1 - ] - }, - "class_type": "CLIPTextEncode" - }, - "10": { - "inputs": { - "add_noise": "enable", - "noise_seed": 42, - "steps": 20, - "cfg": 7.5, - "sampler_name": "euler", - "scheduler": "normal", - "start_at_step": 0, - "end_at_step": 32, - "return_with_leftover_noise": "enable", - "model": [ - "4", - 0 - ], - "positive": [ - "6", - 0 - ], - "negative": [ - "15", - 0 - ], - "latent_image": [ - "5", - 0 - ] - }, - "class_type": "KSamplerAdvanced" - }, - "12": { - "inputs": { - "samples": [ - "14", - 0 - ], - "vae": [ - "4", - 2 - ] - }, - "class_type": "VAEDecode" - }, - "13": { - "inputs": { - "filename_prefix": "test_inference", - "images": [ - "12", - 0 - ] - }, - "class_type": "SaveImage" - }, - "14": { - "inputs": { - "add_noise": "disable", - "noise_seed": 42, - "steps": 20, - "cfg": 7.5, - "sampler_name": "euler", - "scheduler": "normal", - "start_at_step": 32, - "end_at_step": 10000, - "return_with_leftover_noise": "disable", - "model": [ - "16", - 0 - ], - "positive": [ - "17", - 0 - ], - "negative": [ - "20", - 0 - ], - "latent_image": [ - "10", - 0 - ] - }, - "class_type": "KSamplerAdvanced" - }, - "15": { - "inputs": { - "conditioning": [ - "6", - 0 - ] - }, - "class_type": "ConditioningZeroOut" - }, - "16": { - "inputs": { - "ckpt_name": "sd_xl_refiner_1.0.safetensors" - }, - "class_type": "CheckpointLoaderSimple" - }, - "17": { - "inputs": { - "text": "a photo of a cat", - "clip": [ - "16", - 1 - ] - }, - "class_type": "CLIPTextEncode" - }, - "20": { - "inputs": { - "text": "", - "clip": [ - "16", - 1 - ] - }, - "class_type": "CLIPTextEncode" - } - } \ No newline at end of file diff --git a/tests/inference/test_inference.py b/tests/inference/test_inference.py deleted file mode 100644 index 141cc5c7e..000000000 --- a/tests/inference/test_inference.py +++ /dev/null @@ -1,239 +0,0 @@ -from copy import deepcopy -from io import BytesIO -from urllib import request -import numpy -import os -from PIL import Image -import pytest -from pytest import fixture -import time -import torch -from typing import Union -import json -import subprocess -import websocket #NOTE: websocket-client (https://github.com/websocket-client/websocket-client) -import uuid -import urllib.request -import urllib.parse - - -from comfy.samplers import KSampler - -""" -These tests generate and save images through a range of parameters -""" - -class ComfyGraph: - def __init__(self, - graph: dict, - sampler_nodes: list[str], - ): - self.graph = graph - self.sampler_nodes = sampler_nodes - - def set_prompt(self, prompt, negative_prompt=None): - # Sets the prompt for the sampler nodes (eg. base and refiner) - for node in self.sampler_nodes: - prompt_node = self.graph[node]['inputs']['positive'][0] - self.graph[prompt_node]['inputs']['text'] = prompt - if negative_prompt: - negative_prompt_node = self.graph[node]['inputs']['negative'][0] - self.graph[negative_prompt_node]['inputs']['text'] = negative_prompt - - def set_sampler_name(self, sampler_name:str, ): - # sets the sampler name for the sampler nodes (eg. base and refiner) - for node in self.sampler_nodes: - self.graph[node]['inputs']['sampler_name'] = sampler_name - - def set_scheduler(self, scheduler:str): - # sets the sampler name for the sampler nodes (eg. base and refiner) - for node in self.sampler_nodes: - self.graph[node]['inputs']['scheduler'] = scheduler - - def set_filename_prefix(self, prefix:str): - # sets the filename prefix for the save nodes - for node in self.graph: - if self.graph[node]['class_type'] == 'SaveImage': - self.graph[node]['inputs']['filename_prefix'] = prefix - - -class ComfyClient: - # From examples/websockets_api_example.py - - def connect(self, - listen:str = '127.0.0.1', - port:Union[str,int] = 8188, - client_id: str = str(uuid.uuid4()) - ): - self.client_id = client_id - self.server_address = f"{listen}:{port}" - ws = websocket.WebSocket() - ws.connect("ws://{}/ws?clientId={}".format(self.server_address, self.client_id)) - self.ws = ws - - def queue_prompt(self, prompt): - p = {"prompt": prompt, "client_id": self.client_id} - data = json.dumps(p).encode('utf-8') - req = urllib.request.Request("http://{}/prompt".format(self.server_address), data=data) - return json.loads(urllib.request.urlopen(req).read()) - - def get_image(self, filename, subfolder, folder_type): - data = {"filename": filename, "subfolder": subfolder, "type": folder_type} - url_values = urllib.parse.urlencode(data) - with urllib.request.urlopen("http://{}/view?{}".format(self.server_address, url_values)) as response: - return response.read() - - def get_history(self, prompt_id): - with urllib.request.urlopen("http://{}/history/{}".format(self.server_address, prompt_id)) as response: - return json.loads(response.read()) - - def get_images(self, graph, save=True): - prompt = graph - if not save: - # Replace save nodes with preview nodes - prompt_str = json.dumps(prompt) - prompt_str = prompt_str.replace('SaveImage', 'PreviewImage') - prompt = json.loads(prompt_str) - - prompt_id = self.queue_prompt(prompt)['prompt_id'] - output_images = {} - while True: - out = self.ws.recv() - if isinstance(out, str): - message = json.loads(out) - if message['type'] == 'executing': - data = message['data'] - if data['node'] is None and data['prompt_id'] == prompt_id: - break #Execution is done - else: - continue #previews are binary data - - history = self.get_history(prompt_id)[prompt_id] - for o in history['outputs']: - for node_id in history['outputs']: - node_output = history['outputs'][node_id] - if 'images' in node_output: - images_output = [] - for image in node_output['images']: - image_data = self.get_image(image['filename'], image['subfolder'], image['type']) - images_output.append(image_data) - output_images[node_id] = images_output - - return output_images - -# -# Initialize graphs -# -default_graph_file = 'tests/inference/graphs/default_graph_sdxl1_0.json' -with open(default_graph_file, 'r') as file: - default_graph = json.loads(file.read()) -DEFAULT_COMFY_GRAPH = ComfyGraph(graph=default_graph, sampler_nodes=['10','14']) -DEFAULT_COMFY_GRAPH_ID = os.path.splitext(os.path.basename(default_graph_file))[0] - -# -# Loop through these variables -# -comfy_graph_list = [DEFAULT_COMFY_GRAPH] -comfy_graph_ids = [DEFAULT_COMFY_GRAPH_ID] -prompt_list = [ - 'a painting of a cat', -] - -sampler_list = KSampler.SAMPLERS -scheduler_list = KSampler.SCHEDULERS - -@pytest.mark.inference -@pytest.mark.parametrize("sampler", sampler_list) -@pytest.mark.parametrize("scheduler", scheduler_list) -@pytest.mark.parametrize("prompt", prompt_list) -class TestInference: - # - # Initialize server and client - # - @fixture(scope="class", autouse=True) - def _server(self, args_pytest): - # Start server - p = subprocess.Popen([ - 'python','main.py', - '--output-directory', args_pytest["output_dir"], - '--listen', args_pytest["listen"], - '--port', str(args_pytest["port"]), - ]) - yield - p.kill() - torch.cuda.empty_cache() - - def start_client(self, listen:str, port:int): - # Start client - comfy_client = ComfyClient() - # Connect to server (with retries) - n_tries = 5 - for i in range(n_tries): - time.sleep(4) - try: - comfy_client.connect(listen=listen, port=port) - except ConnectionRefusedError as e: - print(e) - print(f"({i+1}/{n_tries}) Retrying...") - else: - break - return comfy_client - - # - # Client and graph fixtures with server warmup - # - # Returns a "_client_graph", which is client-graph pair corresponding to an initialized server - # The "graph" is the default graph - @fixture(scope="class", params=comfy_graph_list, ids=comfy_graph_ids, autouse=True) - def _client_graph(self, request, args_pytest, _server) -> (ComfyClient, ComfyGraph): - comfy_graph = request.param - - # Start client - comfy_client = self.start_client(args_pytest["listen"], args_pytest["port"]) - - # Warm up pipeline - comfy_client.get_images(graph=comfy_graph.graph, save=False) - - yield comfy_client, comfy_graph - del comfy_client - del comfy_graph - torch.cuda.empty_cache() - - @fixture - def client(self, _client_graph): - client = _client_graph[0] - yield client - - @fixture - def comfy_graph(self, _client_graph): - # avoid mutating the graph - graph = deepcopy(_client_graph[1]) - yield graph - - def test_comfy( - self, - client, - comfy_graph, - sampler, - scheduler, - prompt, - request - ): - test_info = request.node.name - comfy_graph.set_filename_prefix(test_info) - # Settings for comfy graph - comfy_graph.set_sampler_name(sampler) - comfy_graph.set_scheduler(scheduler) - comfy_graph.set_prompt(prompt) - - # Generate - images = client.get_images(comfy_graph.graph) - - assert len(images) != 0, "No images generated" - # assert all images are not blank - for images_output in images.values(): - for image_data in images_output: - pil_image = Image.open(BytesIO(image_data)) - assert numpy.array(pil_image).any() != 0, "Image is blank" - - diff --git a/web/extensions/core/clipspace.js b/web/extensions/core/clipspace.js deleted file mode 100644 index e376a02f7..000000000 --- a/web/extensions/core/clipspace.js +++ /dev/null @@ -1,166 +0,0 @@ -import { app } from "../../scripts/app.js"; -import { ComfyDialog, $el } from "../../scripts/ui.js"; -import { ComfyApp } from "../../scripts/app.js"; - -export class ClipspaceDialog extends ComfyDialog { - static items = []; - static instance = null; - - static registerButton(name, contextPredicate, callback) { - const item = - $el("button", { - type: "button", - textContent: name, - contextPredicate: contextPredicate, - onclick: callback - }) - - ClipspaceDialog.items.push(item); - } - - static invalidatePreview() { - if(ComfyApp.clipspace && ComfyApp.clipspace.imgs && ComfyApp.clipspace.imgs.length > 0) { - const img_preview = document.getElementById("clipspace_preview"); - if(img_preview) { - img_preview.src = ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src; - img_preview.style.maxHeight = "100%"; - img_preview.style.maxWidth = "100%"; - } - } - } - - static invalidate() { - if(ClipspaceDialog.instance) { - const self = ClipspaceDialog.instance; - // allow reconstruct controls when copying from non-image to image content. - const children = $el("div.comfy-modal-content", [ self.createImgSettings(), ...self.createButtons() ]); - - if(self.element) { - // update - self.element.removeChild(self.element.firstChild); - self.element.appendChild(children); - } - else { - // new - self.element = $el("div.comfy-modal", { parent: document.body }, [children,]); - } - - if(self.element.children[0].children.length <= 1) { - self.element.children[0].appendChild($el("p", {}, ["Unable to find the features to edit content of a format stored in the current Clipspace."])); - } - - ClipspaceDialog.invalidatePreview(); - } - } - - constructor() { - super(); - } - - createButtons(self) { - const buttons = []; - - for(let idx in ClipspaceDialog.items) { - const item = ClipspaceDialog.items[idx]; - if(!item.contextPredicate || item.contextPredicate()) - buttons.push(ClipspaceDialog.items[idx]); - } - - buttons.push( - $el("button", { - type: "button", - textContent: "Close", - onclick: () => { this.close(); } - }) - ); - - return buttons; - } - - createImgSettings() { - if(ComfyApp.clipspace.imgs) { - const combo_items = []; - const imgs = ComfyApp.clipspace.imgs; - - for(let i=0; i < imgs.length; i++) { - combo_items.push($el("option", {value:i}, [`${i}`])); - } - - const combo1 = $el("select", - {id:"clipspace_img_selector", onchange:(event) => { - ComfyApp.clipspace['selectedIndex'] = event.target.selectedIndex; - ClipspaceDialog.invalidatePreview(); - } }, combo_items); - - const row1 = - $el("tr", {}, - [ - $el("td", {}, [$el("font", {color:"white"}, ["Select Image"])]), - $el("td", {}, [combo1]) - ]); - - - const combo2 = $el("select", - {id:"clipspace_img_paste_mode", onchange:(event) => { - ComfyApp.clipspace['img_paste_mode'] = event.target.value; - } }, - [ - $el("option", {value:'selected'}, 'selected'), - $el("option", {value:'all'}, 'all') - ]); - combo2.value = ComfyApp.clipspace['img_paste_mode']; - - const row2 = - $el("tr", {}, - [ - $el("td", {}, [$el("font", {color:"white"}, ["Paste Mode"])]), - $el("td", {}, [combo2]) - ]); - - const td = $el("td", {align:'center', width:'100px', height:'100px', colSpan:'2'}, - [ $el("img",{id:"clipspace_preview", ondragstart:() => false},[]) ]); - - const row3 = - $el("tr", {}, [td]); - - return $el("table", {}, [row1, row2, row3]); - } - else { - return []; - } - } - - createImgPreview() { - if(ComfyApp.clipspace.imgs) { - return $el("img",{id:"clipspace_preview", ondragstart:() => false}); - } - else - return []; - } - - show() { - const img_preview = document.getElementById("clipspace_preview"); - ClipspaceDialog.invalidate(); - - this.element.style.display = "block"; - } -} - -app.registerExtension({ - name: "Comfy.Clipspace", - init(app) { - app.openClipspace = - function () { - if(!ClipspaceDialog.instance) { - ClipspaceDialog.instance = new ClipspaceDialog(app); - ComfyApp.clipspace_invalidate_handler = ClipspaceDialog.invalidate; - } - - if(ComfyApp.clipspace) { - ClipspaceDialog.instance.show(); - } - else - app.ui.dialog.show("Clipspace is Empty!"); - }; - } -}); \ No newline at end of file diff --git a/web/extensions/core/colorPalette.js b/web/extensions/core/colorPalette.js deleted file mode 100644 index 245f3b93e..000000000 --- a/web/extensions/core/colorPalette.js +++ /dev/null @@ -1,785 +0,0 @@ -import {app} from "../../scripts/app.js"; -import {$el} from "../../scripts/ui.js"; - -// Manage color palettes - -const colorPalettes = { - "dark": { - "id": "dark", - "name": "Dark (Default)", - "colors": { - "node_slot": { - "CLIP": "#FFD500", // bright yellow - "CLIP_VISION": "#A8DADC", // light blue-gray - "CLIP_VISION_OUTPUT": "#ad7452", // rusty brown-orange - "CONDITIONING": "#FFA931", // vibrant orange-yellow - "CONTROL_NET": "#6EE7B7", // soft mint green - "IMAGE": "#64B5F6", // bright sky blue - "LATENT": "#FF9CF9", // light pink-purple - "MASK": "#81C784", // muted green - "MODEL": "#B39DDB", // light lavender-purple - "STYLE_MODEL": "#C2FFAE", // light green-yellow - "VAE": "#FF6E6E", // bright red - "NOISE": "#B0B0B0", // gray - "GUIDER": "#66FFFF", // cyan - "SAMPLER": "#ECB4B4", // very soft red - "SIGMAS": "#CDFFCD", // soft lime green - "TAESD": "#DCC274", // cheesecake - }, - "litegraph_base": { - "BACKGROUND_IMAGE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=", - "CLEAR_BACKGROUND_COLOR": "#222", - "NODE_TITLE_COLOR": "#999", - "NODE_SELECTED_TITLE_COLOR": "#FFF", - "NODE_TEXT_SIZE": 14, - "NODE_TEXT_COLOR": "#AAA", - "NODE_SUBTEXT_SIZE": 12, - "NODE_DEFAULT_COLOR": "#333", - "NODE_DEFAULT_BGCOLOR": "#353535", - "NODE_DEFAULT_BOXCOLOR": "#666", - "NODE_DEFAULT_SHAPE": "box", - "NODE_BOX_OUTLINE_COLOR": "#FFF", - "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)", - "DEFAULT_GROUP_FONT": 24, - - "WIDGET_BGCOLOR": "#222", - "WIDGET_OUTLINE_COLOR": "#666", - "WIDGET_TEXT_COLOR": "#DDD", - "WIDGET_SECONDARY_TEXT_COLOR": "#999", - - "LINK_COLOR": "#9A9", - "EVENT_LINK_COLOR": "#A86", - "CONNECTING_LINK_COLOR": "#AFA", - }, - "comfy_base": { - "fg-color": "#fff", - "bg-color": "#202020", - "comfy-menu-bg": "#353535", - "comfy-input-bg": "#222", - "input-text": "#ddd", - "descrip-text": "#999", - "drag-text": "#ccc", - "error-text": "#ff4444", - "border-color": "#4e4e4e", - "tr-even-bg-color": "#222", - "tr-odd-bg-color": "#353535", - "content-bg": "#4e4e4e", - "content-fg": "#fff", - "content-hover-bg": "#222", - "content-hover-fg": "#fff" - } - }, - }, - "light": { - "id": "light", - "name": "Light", - "colors": { - "node_slot": { - "CLIP": "#FFA726", // orange - "CLIP_VISION": "#5C6BC0", // indigo - "CLIP_VISION_OUTPUT": "#8D6E63", // brown - "CONDITIONING": "#EF5350", // red - "CONTROL_NET": "#66BB6A", // green - "IMAGE": "#42A5F5", // blue - "LATENT": "#AB47BC", // purple - "MASK": "#9CCC65", // light green - "MODEL": "#7E57C2", // deep purple - "STYLE_MODEL": "#D4E157", // lime - "VAE": "#FF7043", // deep orange - }, - "litegraph_base": { - "BACKGROUND_IMAGE": "data:image/gif;base64,R0lGODlhZABkALMAAAAAAP///+vr6+rq6ujo6Ofn5+bm5uXl5d3d3f///wAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAkALAAAAABkAGQAAAT/UMhJq7046827HkcoHkYxjgZhnGG6si5LqnIM0/fL4qwwIMAg0CAsEovBIxKhRDaNy2GUOX0KfVFrssrNdpdaqTeKBX+dZ+jYvEaTf+y4W66mC8PUdrE879f9d2mBeoNLfH+IhYBbhIx2jkiHiomQlGKPl4uZe3CaeZifnnijgkESBqipqqusra6vsLGys62SlZO4t7qbuby7CLa+wqGWxL3Gv3jByMOkjc2lw8vOoNSi0czAncXW3Njdx9Pf48/Z4Kbbx+fQ5evZ4u3k1fKR6cn03vHlp7T9/v8A/8Gbp4+gwXoFryXMB2qgwoMMHyKEqA5fxX322FG8tzBcRnMW/zlulPbRncmQGidKjMjyYsOSKEF2FBlJQMCbOHP6c9iSZs+UnGYCdbnSo1CZI5F64kn0p1KnTH02nSoV3dGTV7FFHVqVq1dtWcMmVQZTbNGu72zqXMuW7danVL+6e4t1bEy6MeueBYLXrNO5Ze36jQtWsOG97wIj1vt3St/DjTEORss4nNq2mDP3e7w4r1bFkSET5hy6s2TRlD2/mSxXtSHQhCunXo26NevCpmvD/UU6tuullzULH76q92zdZG/Ltv1a+W+osI/nRmyc+fRi1Xdbh+68+0vv10dH3+77KD/i6IdnX669/frn5Zsjh4/2PXju8+8bzc9/6fj27LFnX11/+IUnXWl7BJfegm79FyB9JOl3oHgSklefgxAC+FmFGpqHIYcCfkhgfCohSKKJVo044YUMttggiBkmp6KFXw1oII24oYhjiDByaKOOHcp3Y5BD/njikSkO+eBREQAAOw==", - "CLEAR_BACKGROUND_COLOR": "lightgray", - "NODE_TITLE_COLOR": "#222", - "NODE_SELECTED_TITLE_COLOR": "#000", - "NODE_TEXT_SIZE": 14, - "NODE_TEXT_COLOR": "#444", - "NODE_SUBTEXT_SIZE": 12, - "NODE_DEFAULT_COLOR": "#F7F7F7", - "NODE_DEFAULT_BGCOLOR": "#F5F5F5", - "NODE_DEFAULT_BOXCOLOR": "#CCC", - "NODE_DEFAULT_SHAPE": "box", - "NODE_BOX_OUTLINE_COLOR": "#000", - "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.1)", - "DEFAULT_GROUP_FONT": 24, - - "WIDGET_BGCOLOR": "#D4D4D4", - "WIDGET_OUTLINE_COLOR": "#999", - "WIDGET_TEXT_COLOR": "#222", - "WIDGET_SECONDARY_TEXT_COLOR": "#555", - - "LINK_COLOR": "#4CAF50", - "EVENT_LINK_COLOR": "#FF9800", - "CONNECTING_LINK_COLOR": "#2196F3", - }, - "comfy_base": { - "fg-color": "#222", - "bg-color": "#DDD", - "comfy-menu-bg": "#F5F5F5", - "comfy-input-bg": "#C9C9C9", - "input-text": "#222", - "descrip-text": "#444", - "drag-text": "#555", - "error-text": "#F44336", - "border-color": "#888", - "tr-even-bg-color": "#f9f9f9", - "tr-odd-bg-color": "#fff", - "content-bg": "#e0e0e0", - "content-fg": "#222", - "content-hover-bg": "#adadad", - "content-hover-fg": "#222" - } - }, - }, - "solarized": { - "id": "solarized", - "name": "Solarized", - "colors": { - "node_slot": { - "CLIP": "#2AB7CA", // light blue - "CLIP_VISION": "#6c71c4", // blue violet - "CLIP_VISION_OUTPUT": "#859900", // olive green - "CONDITIONING": "#d33682", // magenta - "CONTROL_NET": "#d1ffd7", // light mint green - "IMAGE": "#5940bb", // deep blue violet - "LATENT": "#268bd2", // blue - "MASK": "#CCC9E7", // light purple-gray - "MODEL": "#dc322f", // red - "STYLE_MODEL": "#1a998a", // teal - "UPSCALE_MODEL": "#054A29", // dark green - "VAE": "#facfad", // light pink-orange - }, - "litegraph_base": { - "NODE_TITLE_COLOR": "#fdf6e3", // Base3 - "NODE_SELECTED_TITLE_COLOR": "#A9D400", - "NODE_TEXT_SIZE": 14, - "NODE_TEXT_COLOR": "#657b83", // Base00 - "NODE_SUBTEXT_SIZE": 12, - "NODE_DEFAULT_COLOR": "#094656", - "NODE_DEFAULT_BGCOLOR": "#073642", // Base02 - "NODE_DEFAULT_BOXCOLOR": "#839496", // Base0 - "NODE_DEFAULT_SHAPE": "box", - "NODE_BOX_OUTLINE_COLOR": "#fdf6e3", // Base3 - "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)", - "DEFAULT_GROUP_FONT": 24, - - "WIDGET_BGCOLOR": "#002b36", // Base03 - "WIDGET_OUTLINE_COLOR": "#839496", // Base0 - "WIDGET_TEXT_COLOR": "#fdf6e3", // Base3 - "WIDGET_SECONDARY_TEXT_COLOR": "#93a1a1", // Base1 - - "LINK_COLOR": "#2aa198", // Solarized Cyan - "EVENT_LINK_COLOR": "#268bd2", // Solarized Blue - "CONNECTING_LINK_COLOR": "#859900", // Solarized Green - }, - "comfy_base": { - "fg-color": "#fdf6e3", // Base3 - "bg-color": "#002b36", // Base03 - "comfy-menu-bg": "#073642", // Base02 - "comfy-input-bg": "#002b36", // Base03 - "input-text": "#93a1a1", // Base1 - "descrip-text": "#586e75", // Base01 - "drag-text": "#839496", // Base0 - "error-text": "#dc322f", // Solarized Red - "border-color": "#657b83", // Base00 - "tr-even-bg-color": "#002b36", - "tr-odd-bg-color": "#073642", - "content-bg": "#657b83", - "content-fg": "#fdf6e3", - "content-hover-bg": "#002b36", - "content-hover-fg": "#fdf6e3" - } - }, - }, - "arc": { - "id": "arc", - "name": "Arc", - "colors": { - "node_slot": { - "BOOLEAN": "", - "CLIP": "#eacb8b", - "CLIP_VISION": "#A8DADC", - "CLIP_VISION_OUTPUT": "#ad7452", - "CONDITIONING": "#cf876f", - "CONTROL_NET": "#00d78d", - "CONTROL_NET_WEIGHTS": "", - "FLOAT": "", - "GLIGEN": "", - "IMAGE": "#80a1c0", - "IMAGEUPLOAD": "", - "INT": "", - "LATENT": "#b38ead", - "LATENT_KEYFRAME": "", - "MASK": "#a3bd8d", - "MODEL": "#8978a7", - "SAMPLER": "", - "SIGMAS": "", - "STRING": "", - "STYLE_MODEL": "#C2FFAE", - "T2I_ADAPTER_WEIGHTS": "", - "TAESD": "#DCC274", - "TIMESTEP_KEYFRAME": "", - "UPSCALE_MODEL": "", - "VAE": "#be616b" - }, - "litegraph_base": { - "BACKGROUND_IMAGE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAACXBIWXMAAAsTAAALEwEAmpwYAAABcklEQVR4nO3YMUoDARgF4RfxBqZI6/0vZqFn0MYtrLIQMFN8U6V4LAtD+Jm9XG/v30OGl2e/AP7yevz4+vx45nvgF/+QGITEICQGITEIiUFIjNNC3q43u3/YnRJyPOzeQ+0e220nhRzReC8e7R7bbdvl+Jal1Bs46jEIiUFIDEJiEBKDkBhKPbZT6qHdptRTu02p53DUYxASg5AYhMQgJAYhMZR6bKfUQ7tNqad2m1LP4ajHICQGITEIiUFIDEJiKPXYTqmHdptST+02pZ7DUY9BSAxCYhASg5AYhMRQ6rGdUg/tNqWe2m1KPYejHoOQGITEICQGITEIiaHUYzulHtptSj2125R6Dkc9BiExCIlBSAxCYhASQ6nHdko9tNuUemq3KfUcjnoMQmIQEoOQGITEICSGUo/tlHpotyn11G5T6jkc9RiExCAkBiExCIlBSAylHtsp9dBuU+qp3abUczjqMQiJQUgMQmIQEoOQGITE+AHFISNQrFTGuwAAAABJRU5ErkJggg==", - "CLEAR_BACKGROUND_COLOR": "#2b2f38", - "NODE_TITLE_COLOR": "#b2b7bd", - "NODE_SELECTED_TITLE_COLOR": "#FFF", - "NODE_TEXT_SIZE": 14, - "NODE_TEXT_COLOR": "#AAA", - "NODE_SUBTEXT_SIZE": 12, - "NODE_DEFAULT_COLOR": "#2b2f38", - "NODE_DEFAULT_BGCOLOR": "#242730", - "NODE_DEFAULT_BOXCOLOR": "#6e7581", - "NODE_DEFAULT_SHAPE": "box", - "NODE_BOX_OUTLINE_COLOR": "#FFF", - "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)", - "DEFAULT_GROUP_FONT": 22, - "WIDGET_BGCOLOR": "#2b2f38", - "WIDGET_OUTLINE_COLOR": "#6e7581", - "WIDGET_TEXT_COLOR": "#DDD", - "WIDGET_SECONDARY_TEXT_COLOR": "#b2b7bd", - "LINK_COLOR": "#9A9", - "EVENT_LINK_COLOR": "#A86", - "CONNECTING_LINK_COLOR": "#AFA" - }, - "comfy_base": { - "fg-color": "#fff", - "bg-color": "#2b2f38", - "comfy-menu-bg": "#242730", - "comfy-input-bg": "#2b2f38", - "input-text": "#ddd", - "descrip-text": "#b2b7bd", - "drag-text": "#ccc", - "error-text": "#ff4444", - "border-color": "#6e7581", - "tr-even-bg-color": "#2b2f38", - "tr-odd-bg-color": "#242730", - "content-bg": "#6e7581", - "content-fg": "#fff", - "content-hover-bg": "#2b2f38", - "content-hover-fg": "#fff" - } - }, - }, - "nord": { - "id": "nord", - "name": "Nord", - "colors": { - "node_slot": { - "BOOLEAN": "", - "CLIP": "#eacb8b", - "CLIP_VISION": "#A8DADC", - "CLIP_VISION_OUTPUT": "#ad7452", - "CONDITIONING": "#cf876f", - "CONTROL_NET": "#00d78d", - "CONTROL_NET_WEIGHTS": "", - "FLOAT": "", - "GLIGEN": "", - "IMAGE": "#80a1c0", - "IMAGEUPLOAD": "", - "INT": "", - "LATENT": "#b38ead", - "LATENT_KEYFRAME": "", - "MASK": "#a3bd8d", - "MODEL": "#8978a7", - "SAMPLER": "", - "SIGMAS": "", - "STRING": "", - "STYLE_MODEL": "#C2FFAE", - "T2I_ADAPTER_WEIGHTS": "", - "TAESD": "#DCC274", - "TIMESTEP_KEYFRAME": "", - "UPSCALE_MODEL": "", - "VAE": "#be616b" - }, - "litegraph_base": { - "BACKGROUND_IMAGE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFu2lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDEgNzkuMTQ2Mjg5OSwgMjAyMy8wNi8yNS0yMDowMTo1NSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjMtMTEtMTVUMDE6MjA6NDUrMDE6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMTEtMTVUMDE6MjA6NDUrMDE6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjUwNDFhMmZjLTEzNzQtMTk0ZC1hZWY4LTYxMzM1MTVmNjUwMCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoyMzFiMTBiMC1iNGZiLTAyNGUtYjEyZS0zMDUzMDNjZDA3YzgiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDoyMzFiMTBiMC1iNGZiLTAyNGUtYjEyZS0zMDUzMDNjZDA3YzgiPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjIzMWIxMGIwLWI0ZmItMDI0ZS1iMTJlLTMwNTMwM2NkMDdjOCIgc3RFdnQ6d2hlbj0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo1MDQxYTJmYy0xMzc0LTE5NGQtYWVmOC02MTMzNTE1ZjY1MDAiIHN0RXZ0OndoZW49IjIwMjMtMTEtMTVUMDE6MjA6NDUrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNS4xIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz73jWg/AAAAyUlEQVR42u3WKwoAIBRFQRdiMb1idv9Lsxn9gEFw4Dbb8JCTojbbXEJwjJVL2HKwYMGCBQuWLbDmjr+9zrBGjHl1WVcvy2DBggULFizTWQpewSt4HzwsgwULFiwFr7MUvMtS8D54WLBgGSxYCl7BK3iXZbBgwYIFC5bpLAWv4BW8Dx6WwYIFC5aC11kK3mUpeB88LFiwDBYsBa/gFbzLMliwYMGCBct0loJX8AreBw/LYMGCBUvB6ywF77IUvA8eFixYBgsWrNfWAZPltufdad+1AAAAAElFTkSuQmCC", - "CLEAR_BACKGROUND_COLOR": "#212732", - "NODE_TITLE_COLOR": "#999", - "NODE_SELECTED_TITLE_COLOR": "#e5eaf0", - "NODE_TEXT_SIZE": 14, - "NODE_TEXT_COLOR": "#bcc2c8", - "NODE_SUBTEXT_SIZE": 12, - "NODE_DEFAULT_COLOR": "#2e3440", - "NODE_DEFAULT_BGCOLOR": "#161b22", - "NODE_DEFAULT_BOXCOLOR": "#545d70", - "NODE_DEFAULT_SHAPE": "box", - "NODE_BOX_OUTLINE_COLOR": "#e5eaf0", - "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)", - "DEFAULT_GROUP_FONT": 24, - "WIDGET_BGCOLOR": "#2e3440", - "WIDGET_OUTLINE_COLOR": "#545d70", - "WIDGET_TEXT_COLOR": "#bcc2c8", - "WIDGET_SECONDARY_TEXT_COLOR": "#999", - "LINK_COLOR": "#9A9", - "EVENT_LINK_COLOR": "#A86", - "CONNECTING_LINK_COLOR": "#AFA" - }, - "comfy_base": { - "fg-color": "#e5eaf0", - "bg-color": "#2e3440", - "comfy-menu-bg": "#161b22", - "comfy-input-bg": "#2e3440", - "input-text": "#bcc2c8", - "descrip-text": "#999", - "drag-text": "#ccc", - "error-text": "#ff4444", - "border-color": "#545d70", - "tr-even-bg-color": "#2e3440", - "tr-odd-bg-color": "#161b22", - "content-bg": "#545d70", - "content-fg": "#e5eaf0", - "content-hover-bg": "#2e3440", - "content-hover-fg": "#e5eaf0" - } - }, - }, - "github": { - "id": "github", - "name": "Github", - "colors": { - "node_slot": { - "BOOLEAN": "", - "CLIP": "#eacb8b", - "CLIP_VISION": "#A8DADC", - "CLIP_VISION_OUTPUT": "#ad7452", - "CONDITIONING": "#cf876f", - "CONTROL_NET": "#00d78d", - "CONTROL_NET_WEIGHTS": "", - "FLOAT": "", - "GLIGEN": "", - "IMAGE": "#80a1c0", - "IMAGEUPLOAD": "", - "INT": "", - "LATENT": "#b38ead", - "LATENT_KEYFRAME": "", - "MASK": "#a3bd8d", - "MODEL": "#8978a7", - "SAMPLER": "", - "SIGMAS": "", - "STRING": "", - "STYLE_MODEL": "#C2FFAE", - "T2I_ADAPTER_WEIGHTS": "", - "TAESD": "#DCC274", - "TIMESTEP_KEYFRAME": "", - "UPSCALE_MODEL": "", - "VAE": "#be616b" - }, - "litegraph_base": { - "BACKGROUND_IMAGE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGlmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDEgNzkuMTQ2Mjg5OSwgMjAyMy8wNi8yNS0yMDowMTo1NSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOmIyYzRhNjA5LWJmYTctYTg0MC1iOGFlLTk3MzE2ZjM1ZGIyNyIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjk0ZmNlZGU4LTE1MTctZmQ0MC04ZGU3LWYzOTgxM2E3ODk5ZiIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjIzMWIxMGIwLWI0ZmItMDI0ZS1iMTJlLTMwNTMwM2NkMDdjOCI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MjMxYjEwYjAtYjRmYi0wMjRlLWIxMmUtMzA1MzAzY2QwN2M4IiBzdEV2dDp3aGVuPSIyMDIzLTExLTEzVDAwOjE4OjAyKzAxOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjUuMSAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjQ4OWY1NzlmLTJkNjUtZWQ0Zi04OTg0LTA4NGE2MGE1ZTMzNSIgc3RFdnQ6d2hlbj0iMjAyMy0xMS0xNVQwMjowNDo1OSswMTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpiMmM0YTYwOS1iZmE3LWE4NDAtYjhhZS05NzMxNmYzNWRiMjciIHN0RXZ0OndoZW49IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNS4xIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4OTe6GAAAAx0lEQVR42u3WMQoAIQxFwRzJys77X8vSLiRgITif7bYbgrwYc/mKXyBoY4VVBgsWLFiwYFmOlTv+9jfDOjHmr8u6eVkGCxYsWLBgmc5S8ApewXvgYRksWLBgKXidpeBdloL3wMOCBctgwVLwCl7BuyyDBQsWLFiwTGcpeAWv4D3wsAwWLFiwFLzOUvAuS8F74GHBgmWwYCl4Ba/gXZbBggULFixYprMUvIJX8B54WAYLFixYCl5nKXiXpeA98LBgwTJYsGC9tg1o8f4TTtqzNQAAAABJRU5ErkJggg==", - "CLEAR_BACKGROUND_COLOR": "#040506", - "NODE_TITLE_COLOR": "#999", - "NODE_SELECTED_TITLE_COLOR": "#e5eaf0", - "NODE_TEXT_SIZE": 14, - "NODE_TEXT_COLOR": "#bcc2c8", - "NODE_SUBTEXT_SIZE": 12, - "NODE_DEFAULT_COLOR": "#161b22", - "NODE_DEFAULT_BGCOLOR": "#13171d", - "NODE_DEFAULT_BOXCOLOR": "#30363d", - "NODE_DEFAULT_SHAPE": "box", - "NODE_BOX_OUTLINE_COLOR": "#e5eaf0", - "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)", - "DEFAULT_GROUP_FONT": 24, - "WIDGET_BGCOLOR": "#161b22", - "WIDGET_OUTLINE_COLOR": "#30363d", - "WIDGET_TEXT_COLOR": "#bcc2c8", - "WIDGET_SECONDARY_TEXT_COLOR": "#999", - "LINK_COLOR": "#9A9", - "EVENT_LINK_COLOR": "#A86", - "CONNECTING_LINK_COLOR": "#AFA" - }, - "comfy_base": { - "fg-color": "#e5eaf0", - "bg-color": "#161b22", - "comfy-menu-bg": "#13171d", - "comfy-input-bg": "#161b22", - "input-text": "#bcc2c8", - "descrip-text": "#999", - "drag-text": "#ccc", - "error-text": "#ff4444", - "border-color": "#30363d", - "tr-even-bg-color": "#161b22", - "tr-odd-bg-color": "#13171d", - "content-bg": "#30363d", - "content-fg": "#e5eaf0", - "content-hover-bg": "#161b22", - "content-hover-fg": "#e5eaf0" - } - }, - } -}; - -const id = "Comfy.ColorPalette"; -const idCustomColorPalettes = "Comfy.CustomColorPalettes"; -const defaultColorPaletteId = "dark"; -const els = {} -// const ctxMenu = LiteGraph.ContextMenu; -app.registerExtension({ - name: id, - addCustomNodeDefs(node_defs) { - const sortObjectKeys = (unordered) => { - return Object.keys(unordered).sort().reduce((obj, key) => { - obj[key] = unordered[key]; - return obj; - }, {}); - }; - - function getSlotTypes() { - var types = []; - - const defs = node_defs; - for (const nodeId in defs) { - const nodeData = defs[nodeId]; - - var inputs = nodeData["input"]["required"]; - if (nodeData["input"]["optional"] !== undefined) { - inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"]) - } - - for (const inputName in inputs) { - const inputData = inputs[inputName]; - const type = inputData[0]; - - if (!Array.isArray(type)) { - types.push(type); - } - } - - for (const o in nodeData["output"]) { - const output = nodeData["output"][o]; - types.push(output); - } - } - - return types; - } - - function completeColorPalette(colorPalette) { - var types = getSlotTypes(); - - for (const type of types) { - if (!colorPalette.colors.node_slot[type]) { - colorPalette.colors.node_slot[type] = ""; - } - } - - colorPalette.colors.node_slot = sortObjectKeys(colorPalette.colors.node_slot); - - return colorPalette; - } - - const getColorPaletteTemplate = async () => { - let colorPalette = { - "id": "my_color_palette_unique_id", - "name": "My Color Palette", - "colors": { - "node_slot": {}, - "litegraph_base": {}, - "comfy_base": {} - } - }; - - // Copy over missing keys from default color palette - const defaultColorPalette = colorPalettes[defaultColorPaletteId]; - for (const key in defaultColorPalette.colors.litegraph_base) { - if (!colorPalette.colors.litegraph_base[key]) { - colorPalette.colors.litegraph_base[key] = ""; - } - } - for (const key in defaultColorPalette.colors.comfy_base) { - if (!colorPalette.colors.comfy_base[key]) { - colorPalette.colors.comfy_base[key] = ""; - } - } - - return completeColorPalette(colorPalette); - }; - - const getCustomColorPalettes = () => { - return app.ui.settings.getSettingValue(idCustomColorPalettes, {}); - }; - - const setCustomColorPalettes = (customColorPalettes) => { - return app.ui.settings.setSettingValue(idCustomColorPalettes, customColorPalettes); - }; - - const addCustomColorPalette = async (colorPalette) => { - if (typeof (colorPalette) !== "object") { - alert("Invalid color palette."); - return; - } - - if (!colorPalette.id) { - alert("Color palette missing id."); - return; - } - - if (!colorPalette.name) { - alert("Color palette missing name."); - return; - } - - if (!colorPalette.colors) { - alert("Color palette missing colors."); - return; - } - - if (colorPalette.colors.node_slot && typeof (colorPalette.colors.node_slot) !== "object") { - alert("Invalid color palette colors.node_slot."); - return; - } - - const customColorPalettes = getCustomColorPalettes(); - customColorPalettes[colorPalette.id] = colorPalette; - setCustomColorPalettes(customColorPalettes); - - for (const option of els.select.childNodes) { - if (option.value === "custom_" + colorPalette.id) { - els.select.removeChild(option); - } - } - - els.select.append($el("option", { - textContent: colorPalette.name + " (custom)", - value: "custom_" + colorPalette.id, - selected: true - })); - - setColorPalette("custom_" + colorPalette.id); - await loadColorPalette(colorPalette); - }; - - const deleteCustomColorPalette = async (colorPaletteId) => { - const customColorPalettes = getCustomColorPalettes(); - delete customColorPalettes[colorPaletteId]; - setCustomColorPalettes(customColorPalettes); - - for (const option of els.select.childNodes) { - if (option.value === defaultColorPaletteId) { - option.selected = true; - } - - if (option.value === "custom_" + colorPaletteId) { - els.select.removeChild(option); - } - } - - setColorPalette(defaultColorPaletteId); - await loadColorPalette(getColorPalette()); - }; - - const loadColorPalette = async (colorPalette) => { - colorPalette = await completeColorPalette(colorPalette); - if (colorPalette.colors) { - // Sets the colors of node slots and links - if (colorPalette.colors.node_slot) { - Object.assign(app.canvas.default_connection_color_byType, colorPalette.colors.node_slot); - Object.assign(LGraphCanvas.link_type_colors, colorPalette.colors.node_slot); - } - // Sets the colors of the LiteGraph objects - if (colorPalette.colors.litegraph_base) { - // Everything updates correctly in the loop, except the Node Title and Link Color for some reason - app.canvas.node_title_color = colorPalette.colors.litegraph_base.NODE_TITLE_COLOR; - app.canvas.default_link_color = colorPalette.colors.litegraph_base.LINK_COLOR; - - for (const key in colorPalette.colors.litegraph_base) { - if (colorPalette.colors.litegraph_base.hasOwnProperty(key) && LiteGraph.hasOwnProperty(key)) { - LiteGraph[key] = colorPalette.colors.litegraph_base[key]; - } - } - } - // Sets the color of ComfyUI elements - if (colorPalette.colors.comfy_base) { - const rootStyle = document.documentElement.style; - for (const key in colorPalette.colors.comfy_base) { - rootStyle.setProperty('--' + key, colorPalette.colors.comfy_base[key]); - } - } - app.canvas.draw(true, true); - } - }; - - const getColorPalette = (colorPaletteId) => { - if (!colorPaletteId) { - colorPaletteId = app.ui.settings.getSettingValue(id, defaultColorPaletteId); - } - - if (colorPaletteId.startsWith("custom_")) { - colorPaletteId = colorPaletteId.substr(7); - let customColorPalettes = getCustomColorPalettes(); - if (customColorPalettes[colorPaletteId]) { - return customColorPalettes[colorPaletteId]; - } - } - - return colorPalettes[colorPaletteId]; - }; - - const setColorPalette = (colorPaletteId) => { - app.ui.settings.setSettingValue(id, colorPaletteId); - }; - - const fileInput = $el("input", { - type: "file", - accept: ".json", - style: {display: "none"}, - parent: document.body, - onchange: () => { - const file = fileInput.files[0]; - if (file.type === "application/json" || file.name.endsWith(".json")) { - const reader = new FileReader(); - reader.onload = async () => { - await addCustomColorPalette(JSON.parse(reader.result)); - }; - reader.readAsText(file); - } - }, - }); - - app.ui.settings.addSetting({ - id, - name: "Color Palette", - type: (name, setter, value) => { - const options = [ - ...Object.values(colorPalettes).map(c=> $el("option", { - textContent: c.name, - value: c.id, - selected: c.id === value - })), - ...Object.values(getCustomColorPalettes()).map(c=>$el("option", { - textContent: `${c.name} (custom)`, - value: `custom_${c.id}`, - selected: `custom_${c.id}` === value - })) , - ]; - - els.select = $el("select", { - style: { - marginBottom: "0.15rem", - width: "100%", - }, - onchange: (e) => { - setter(e.target.value); - } - }, options) - - return $el("tr", [ - $el("td", [ - $el("label", { - for: id.replaceAll(".", "-"), - textContent: "Color palette", - }), - ]), - $el("td", [ - els.select, - $el("div", { - style: { - display: "grid", - gap: "4px", - gridAutoFlow: "column", - }, - }, [ - $el("input", { - type: "button", - value: "Export", - onclick: async () => { - const colorPaletteId = app.ui.settings.getSettingValue(id, defaultColorPaletteId); - const colorPalette = await completeColorPalette(getColorPalette(colorPaletteId)); - const json = JSON.stringify(colorPalette, null, 2); // convert the data to a JSON string - const blob = new Blob([json], {type: "application/json"}); - const url = URL.createObjectURL(blob); - const a = $el("a", { - href: url, - download: colorPaletteId + ".json", - style: {display: "none"}, - parent: document.body, - }); - a.click(); - setTimeout(function () { - a.remove(); - window.URL.revokeObjectURL(url); - }, 0); - }, - }), - $el("input", { - type: "button", - value: "Import", - onclick: () => { - fileInput.click(); - } - }), - $el("input", { - type: "button", - value: "Template", - onclick: async () => { - const colorPalette = await getColorPaletteTemplate(); - const json = JSON.stringify(colorPalette, null, 2); // convert the data to a JSON string - const blob = new Blob([json], {type: "application/json"}); - const url = URL.createObjectURL(blob); - const a = $el("a", { - href: url, - download: "color_palette.json", - style: {display: "none"}, - parent: document.body, - }); - a.click(); - setTimeout(function () { - a.remove(); - window.URL.revokeObjectURL(url); - }, 0); - } - }), - $el("input", { - type: "button", - value: "Delete", - onclick: async () => { - let colorPaletteId = app.ui.settings.getSettingValue(id, defaultColorPaletteId); - - if (colorPalettes[colorPaletteId]) { - alert("You cannot delete a built-in color palette."); - return; - } - - if (colorPaletteId.startsWith("custom_")) { - colorPaletteId = colorPaletteId.substr(7); - } - - await deleteCustomColorPalette(colorPaletteId); - } - }), - ]), - ]), - ]) - }, - defaultValue: defaultColorPaletteId, - async onChange(value) { - if (!value) { - return; - } - - let palette = colorPalettes[value]; - if (palette) { - await loadColorPalette(palette); - } else if (value.startsWith("custom_")) { - value = value.substr(7); - let customColorPalettes = getCustomColorPalettes(); - if (customColorPalettes[value]) { - palette = customColorPalettes[value]; - await loadColorPalette(customColorPalettes[value]); - } - } - - let {BACKGROUND_IMAGE, CLEAR_BACKGROUND_COLOR} = palette.colors.litegraph_base; - if (BACKGROUND_IMAGE === undefined || CLEAR_BACKGROUND_COLOR === undefined) { - const base = colorPalettes["dark"].colors.litegraph_base; - BACKGROUND_IMAGE = base.BACKGROUND_IMAGE; - CLEAR_BACKGROUND_COLOR = base.CLEAR_BACKGROUND_COLOR; - } - app.canvas.updateBackground(BACKGROUND_IMAGE, CLEAR_BACKGROUND_COLOR); - }, - }); - }, -}); diff --git a/web/extensions/core/contextMenuFilter.js b/web/extensions/core/contextMenuFilter.js deleted file mode 100644 index 0a305391a..000000000 --- a/web/extensions/core/contextMenuFilter.js +++ /dev/null @@ -1,148 +0,0 @@ -import {app} from "../../scripts/app.js"; - -// Adds filtering to combo context menus - -const ext = { - name: "Comfy.ContextMenuFilter", - init() { - const ctxMenu = LiteGraph.ContextMenu; - - LiteGraph.ContextMenu = function (values, options) { - const ctx = ctxMenu.call(this, values, options); - - // If we are a dark menu (only used for combo boxes) then add a filter input - if (options?.className === "dark" && values?.length > 10) { - const filter = document.createElement("input"); - filter.classList.add("comfy-context-menu-filter"); - filter.placeholder = "Filter list"; - this.root.prepend(filter); - - const items = Array.from(this.root.querySelectorAll(".litemenu-entry")); - let displayedItems = [...items]; - let itemCount = displayedItems.length; - - // We must request an animation frame for the current node of the active canvas to update. - requestAnimationFrame(() => { - const currentNode = LGraphCanvas.active_canvas.current_node; - const clickedComboValue = currentNode.widgets - ?.filter(w => w.type === "combo" && w.options.values.length === values.length) - .find(w => w.options.values.every((v, i) => v === values[i])) - ?.value; - - let selectedIndex = clickedComboValue ? values.findIndex(v => v === clickedComboValue) : 0; - if (selectedIndex < 0) { - selectedIndex = 0; - } - let selectedItem = displayedItems[selectedIndex]; - updateSelected(); - - // Apply highlighting to the selected item - function updateSelected() { - selectedItem?.style.setProperty("background-color", ""); - selectedItem?.style.setProperty("color", ""); - selectedItem = displayedItems[selectedIndex]; - selectedItem?.style.setProperty("background-color", "#ccc", "important"); - selectedItem?.style.setProperty("color", "#000", "important"); - } - - const positionList = () => { - const rect = this.root.getBoundingClientRect(); - - // If the top is off-screen then shift the element with scaling applied - if (rect.top < 0) { - const scale = 1 - this.root.getBoundingClientRect().height / this.root.clientHeight; - const shift = (this.root.clientHeight * scale) / 2; - this.root.style.top = -shift + "px"; - } - } - - // Arrow up/down to select items - filter.addEventListener("keydown", (event) => { - switch (event.key) { - case "ArrowUp": - event.preventDefault(); - if (selectedIndex === 0) { - selectedIndex = itemCount - 1; - } else { - selectedIndex--; - } - updateSelected(); - break; - case "ArrowRight": - event.preventDefault(); - selectedIndex = itemCount - 1; - updateSelected(); - break; - case "ArrowDown": - event.preventDefault(); - if (selectedIndex === itemCount - 1) { - selectedIndex = 0; - } else { - selectedIndex++; - } - updateSelected(); - break; - case "ArrowLeft": - event.preventDefault(); - selectedIndex = 0; - updateSelected(); - break; - case "Enter": - selectedItem?.click(); - break; - case "Escape": - this.close(); - break; - } - }); - - filter.addEventListener("input", () => { - // Hide all items that don't match our filter - const term = filter.value.toLocaleLowerCase(); - // When filtering, recompute which items are visible for arrow up/down and maintain selection. - displayedItems = items.filter(item => { - const isVisible = !term || item.textContent.toLocaleLowerCase().includes(term); - item.style.display = isVisible ? "block" : "none"; - return isVisible; - }); - - selectedIndex = 0; - if (displayedItems.includes(selectedItem)) { - selectedIndex = displayedItems.findIndex(d => d === selectedItem); - } - itemCount = displayedItems.length; - - updateSelected(); - - // If we have an event then we can try and position the list under the source - if (options.event) { - let top = options.event.clientY - 10; - - const bodyRect = document.body.getBoundingClientRect(); - const rootRect = this.root.getBoundingClientRect(); - if (bodyRect.height && top > bodyRect.height - rootRect.height - 10) { - top = Math.max(0, bodyRect.height - rootRect.height - 10); - } - - this.root.style.top = top + "px"; - positionList(); - } - }); - - requestAnimationFrame(() => { - // Focus the filter box when opening - filter.focus(); - - positionList(); - }); - }) - } - - return ctx; - }; - - LiteGraph.ContextMenu.prototype = ctxMenu.prototype; - }, -} - -app.registerExtension(ext); diff --git a/web/extensions/core/dynamicPrompts.js b/web/extensions/core/dynamicPrompts.js deleted file mode 100644 index 7417361ba..000000000 --- a/web/extensions/core/dynamicPrompts.js +++ /dev/null @@ -1,48 +0,0 @@ -import { app } from "../../scripts/app.js"; - -// Allows for simple dynamic prompt replacement -// Inputs in the format {a|b} will have a random value of a or b chosen when the prompt is queued. - -/* - * Strips C-style line and block comments from a string - */ -function stripComments(str) { - return str.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g,''); -} - -app.registerExtension({ - name: "Comfy.DynamicPrompts", - nodeCreated(node) { - if (node.widgets) { - // Locate dynamic prompt text widgets - // Include any widgets with dynamicPrompts set to true, and customtext - const widgets = node.widgets.filter( - (n) => n.dynamicPrompts - ); - for (const widget of widgets) { - // Override the serialization of the value to resolve dynamic prompts for all widgets supporting it in this node - widget.serializeValue = (workflowNode, widgetIndex) => { - let prompt = stripComments(widget.value); - while (prompt.replace("\\{", "").includes("{") && prompt.replace("\\}", "").includes("}")) { - const startIndex = prompt.replace("\\{", "00").indexOf("{"); - const endIndex = prompt.replace("\\}", "00").indexOf("}"); - - const optionsString = prompt.substring(startIndex + 1, endIndex); - const options = optionsString.split("|"); - - const randomIndex = Math.floor(Math.random() * options.length); - const randomOption = options[randomIndex]; - - prompt = prompt.substring(0, startIndex) + randomOption + prompt.substring(endIndex + 1); - } - - // Overwrite the value in the serialized workflow pnginfo - if (workflowNode?.widgets_values) - workflowNode.widgets_values[widgetIndex] = prompt; - - return prompt; - }; - } - } - }, -}); diff --git a/web/extensions/core/editAttention.js b/web/extensions/core/editAttention.js deleted file mode 100644 index 6792b2357..000000000 --- a/web/extensions/core/editAttention.js +++ /dev/null @@ -1,144 +0,0 @@ -import { app } from "../../scripts/app.js"; - -// Allows you to edit the attention weight by holding ctrl (or cmd) and using the up/down arrow keys - -app.registerExtension({ - name: "Comfy.EditAttention", - init() { - const editAttentionDelta = app.ui.settings.addSetting({ - id: "Comfy.EditAttention.Delta", - name: "Ctrl+up/down precision", - type: "slider", - attrs: { - min: 0.01, - max: 0.5, - step: 0.01, - }, - defaultValue: 0.05, - }); - - function incrementWeight(weight, delta) { - const floatWeight = parseFloat(weight); - if (isNaN(floatWeight)) return weight; - const newWeight = floatWeight + delta; - if (newWeight < 0) return "0"; - return String(Number(newWeight.toFixed(10))); - } - - function findNearestEnclosure(text, cursorPos) { - let start = cursorPos, end = cursorPos; - let openCount = 0, closeCount = 0; - - // Find opening parenthesis before cursor - while (start >= 0) { - start--; - if (text[start] === "(" && openCount === closeCount) break; - if (text[start] === "(") openCount++; - if (text[start] === ")") closeCount++; - } - if (start < 0) return false; - - openCount = 0; - closeCount = 0; - - // Find closing parenthesis after cursor - while (end < text.length) { - if (text[end] === ")" && openCount === closeCount) break; - if (text[end] === "(") openCount++; - if (text[end] === ")") closeCount++; - end++; - } - if (end === text.length) return false; - - return { start: start + 1, end: end }; - } - - function addWeightToParentheses(text) { - const parenRegex = /^\((.*)\)$/; - const parenMatch = text.match(parenRegex); - - const floatRegex = /:([+-]?(\d*\.)?\d+([eE][+-]?\d+)?)/; - const floatMatch = text.match(floatRegex); - - if (parenMatch && !floatMatch) { - return `(${parenMatch[1]}:1.0)`; - } else { - return text; - } - }; - - function editAttention(event) { - const inputField = event.composedPath()[0]; - const delta = parseFloat(editAttentionDelta.value); - - if (inputField.tagName !== "TEXTAREA") return; - if (!(event.key === "ArrowUp" || event.key === "ArrowDown")) return; - if (!event.ctrlKey && !event.metaKey) return; - - event.preventDefault(); - - let start = inputField.selectionStart; - let end = inputField.selectionEnd; - let selectedText = inputField.value.substring(start, end); - - // If there is no selection, attempt to find the nearest enclosure, or select the current word - if (!selectedText) { - const nearestEnclosure = findNearestEnclosure(inputField.value, start); - if (nearestEnclosure) { - start = nearestEnclosure.start; - end = nearestEnclosure.end; - selectedText = inputField.value.substring(start, end); - } else { - // Select the current word, find the start and end of the word - const delimiters = " .,\\/!?%^*;:{}=-_`~()\r\n\t"; - - while (!delimiters.includes(inputField.value[start - 1]) && start > 0) { - start--; - } - - while (!delimiters.includes(inputField.value[end]) && end < inputField.value.length) { - end++; - } - - selectedText = inputField.value.substring(start, end); - if (!selectedText) return; - } - } - - // If the selection ends with a space, remove it - if (selectedText[selectedText.length - 1] === " ") { - selectedText = selectedText.substring(0, selectedText.length - 1); - end -= 1; - } - - // If there are parentheses left and right of the selection, select them - if (inputField.value[start - 1] === "(" && inputField.value[end] === ")") { - start -= 1; - end += 1; - selectedText = inputField.value.substring(start, end); - } - - // If the selection is not enclosed in parentheses, add them - if (selectedText[0] !== "(" || selectedText[selectedText.length - 1] !== ")") { - selectedText = `(${selectedText})`; - } - - // If the selection does not have a weight, add a weight of 1.0 - selectedText = addWeightToParentheses(selectedText); - - // Increment the weight - const weightDelta = event.key === "ArrowUp" ? delta : -delta; - const updatedText = selectedText.replace(/\((.*):(\d+(?:\.\d+)?)\)/, (match, text, weight) => { - weight = incrementWeight(weight, weightDelta); - if (weight == 1) { - return text; - } else { - return `(${text}:${weight})`; - } - }); - - inputField.setRangeText(updatedText, start, end, "select"); - } - window.addEventListener("keydown", editAttention); - }, -}); diff --git a/web/extensions/core/groupNode.js b/web/extensions/core/groupNode.js deleted file mode 100644 index 9a2238908..000000000 --- a/web/extensions/core/groupNode.js +++ /dev/null @@ -1,1281 +0,0 @@ -import { app } from "../../scripts/app.js"; -import { api } from "../../scripts/api.js"; -import { mergeIfValid } from "./widgetInputs.js"; -import { ManageGroupDialog } from "./groupNodeManage.js"; - -const GROUP = Symbol(); - -const Workflow = { - InUse: { - Free: 0, - Registered: 1, - InWorkflow: 2, - }, - isInUseGroupNode(name) { - const id = `workflow/${name}`; - // Check if lready registered/in use in this workflow - if (app.graph.extra?.groupNodes?.[name]) { - if (app.graph._nodes.find((n) => n.type === id)) { - return Workflow.InUse.InWorkflow; - } else { - return Workflow.InUse.Registered; - } - } - return Workflow.InUse.Free; - }, - storeGroupNode(name, data) { - let extra = app.graph.extra; - if (!extra) app.graph.extra = extra = {}; - let groupNodes = extra.groupNodes; - if (!groupNodes) extra.groupNodes = groupNodes = {}; - groupNodes[name] = data; - }, -}; - -class GroupNodeBuilder { - constructor(nodes) { - this.nodes = nodes; - } - - build() { - const name = this.getName(); - if (!name) return; - - // Sort the nodes so they are in execution order - // this allows for widgets to be in the correct order when reconstructing - this.sortNodes(); - - this.nodeData = this.getNodeData(); - Workflow.storeGroupNode(name, this.nodeData); - - return { name, nodeData: this.nodeData }; - } - - getName() { - const name = prompt("Enter group name"); - if (!name) return; - const used = Workflow.isInUseGroupNode(name); - switch (used) { - case Workflow.InUse.InWorkflow: - alert( - "An in use group node with this name already exists embedded in this workflow, please remove any instances or use a new name." - ); - return; - case Workflow.InUse.Registered: - if (!confirm("A group node with this name already exists embedded in this workflow, are you sure you want to overwrite it?")) { - return; - } - break; - } - return name; - } - - sortNodes() { - // Gets the builders nodes in graph execution order - const nodesInOrder = app.graph.computeExecutionOrder(false); - this.nodes = this.nodes - .map((node) => ({ index: nodesInOrder.indexOf(node), node })) - .sort((a, b) => a.index - b.index || a.node.id - b.node.id) - .map(({ node }) => node); - } - - getNodeData() { - const storeLinkTypes = (config) => { - // Store link types for dynamically typed nodes e.g. reroutes - for (const link of config.links) { - const origin = app.graph.getNodeById(link[4]); - const type = origin.outputs[link[1]].type; - link.push(type); - } - }; - - const storeExternalLinks = (config) => { - // Store any external links to the group in the config so when rebuilding we add extra slots - config.external = []; - for (let i = 0; i < this.nodes.length; i++) { - const node = this.nodes[i]; - if (!node.outputs?.length) continue; - for (let slot = 0; slot < node.outputs.length; slot++) { - let hasExternal = false; - const output = node.outputs[slot]; - let type = output.type; - if (!output.links?.length) continue; - for (const l of output.links) { - const link = app.graph.links[l]; - if (!link) continue; - if (type === "*") type = link.type; - - if (!app.canvas.selected_nodes[link.target_id]) { - hasExternal = true; - break; - } - } - if (hasExternal) { - config.external.push([i, slot, type]); - } - } - } - }; - - // Use the built in copyToClipboard function to generate the node data we need - const backup = localStorage.getItem("litegrapheditor_clipboard"); - try { - app.canvas.copyToClipboard(this.nodes); - const config = JSON.parse(localStorage.getItem("litegrapheditor_clipboard")); - - storeLinkTypes(config); - storeExternalLinks(config); - - return config; - } finally { - localStorage.setItem("litegrapheditor_clipboard", backup); - } - } -} - -export class GroupNodeConfig { - constructor(name, nodeData) { - this.name = name; - this.nodeData = nodeData; - this.getLinks(); - - this.inputCount = 0; - this.oldToNewOutputMap = {}; - this.newToOldOutputMap = {}; - this.oldToNewInputMap = {}; - this.oldToNewWidgetMap = {}; - this.newToOldWidgetMap = {}; - this.primitiveDefs = {}; - this.widgetToPrimitive = {}; - this.primitiveToWidget = {}; - this.nodeInputs = {}; - this.outputVisibility = []; - } - - async registerType(source = "workflow") { - this.nodeDef = { - output: [], - output_name: [], - output_is_list: [], - output_is_hidden: [], - name: source + "/" + this.name, - display_name: this.name, - category: "group nodes" + ("/" + source), - input: { required: {} }, - - [GROUP]: this, - }; - - this.inputs = []; - const seenInputs = {}; - const seenOutputs = {}; - for (let i = 0; i < this.nodeData.nodes.length; i++) { - const node = this.nodeData.nodes[i]; - node.index = i; - this.processNode(node, seenInputs, seenOutputs); - } - - for (const p of this.#convertedToProcess) { - p(); - } - this.#convertedToProcess = null; - await app.registerNodeDef("workflow/" + this.name, this.nodeDef); - } - - getLinks() { - this.linksFrom = {}; - this.linksTo = {}; - this.externalFrom = {}; - - // Extract links for easy lookup - for (const l of this.nodeData.links) { - const [sourceNodeId, sourceNodeSlot, targetNodeId, targetNodeSlot] = l; - - // Skip links outside the copy config - if (sourceNodeId == null) continue; - - if (!this.linksFrom[sourceNodeId]) { - this.linksFrom[sourceNodeId] = {}; - } - if (!this.linksFrom[sourceNodeId][sourceNodeSlot]) { - this.linksFrom[sourceNodeId][sourceNodeSlot] = []; - } - this.linksFrom[sourceNodeId][sourceNodeSlot].push(l); - - if (!this.linksTo[targetNodeId]) { - this.linksTo[targetNodeId] = {}; - } - this.linksTo[targetNodeId][targetNodeSlot] = l; - } - - if (this.nodeData.external) { - for (const ext of this.nodeData.external) { - if (!this.externalFrom[ext[0]]) { - this.externalFrom[ext[0]] = { [ext[1]]: ext[2] }; - } else { - this.externalFrom[ext[0]][ext[1]] = ext[2]; - } - } - } - } - - processNode(node, seenInputs, seenOutputs) { - const def = this.getNodeDef(node); - if (!def) return; - - const inputs = { ...def.input?.required, ...def.input?.optional }; - - this.inputs.push(this.processNodeInputs(node, seenInputs, inputs)); - if (def.output?.length) this.processNodeOutputs(node, seenOutputs, def); - } - - getNodeDef(node) { - const def = globalDefs[node.type]; - if (def) return def; - - const linksFrom = this.linksFrom[node.index]; - if (node.type === "PrimitiveNode") { - // Skip as its not linked - if (!linksFrom) return; - - let type = linksFrom["0"][0][5]; - if (type === "COMBO") { - // Use the array items - const source = node.outputs[0].widget.name; - const fromTypeName = this.nodeData.nodes[linksFrom["0"][0][2]].type; - const fromType = globalDefs[fromTypeName]; - const input = fromType.input.required[source] ?? fromType.input.optional[source]; - type = input[0]; - } - - const def = (this.primitiveDefs[node.index] = { - input: { - required: { - value: [type, {}], - }, - }, - output: [type], - output_name: [], - output_is_list: [], - }); - return def; - } else if (node.type === "Reroute") { - const linksTo = this.linksTo[node.index]; - if (linksTo && linksFrom && !this.externalFrom[node.index]?.[0]) { - // Being used internally - return null; - } - - let config = {}; - let rerouteType = "*"; - if (linksFrom) { - for (const [, , id, slot] of linksFrom["0"]) { - const node = this.nodeData.nodes[id]; - const input = node.inputs[slot]; - if (rerouteType === "*") { - rerouteType = input.type; - } - if (input.widget) { - const targetDef = globalDefs[node.type]; - const targetWidget = targetDef.input.required[input.widget.name] ?? targetDef.input.optional[input.widget.name]; - - const widget = [targetWidget[0], config]; - const res = mergeIfValid( - { - widget, - }, - targetWidget, - false, - null, - widget - ); - config = res?.customConfig ?? config; - } - } - } else if (linksTo) { - const [id, slot] = linksTo["0"]; - rerouteType = this.nodeData.nodes[id].outputs[slot].type; - } else { - // Reroute used as a pipe - for (const l of this.nodeData.links) { - if (l[2] === node.index) { - rerouteType = l[5]; - break; - } - } - if (rerouteType === "*") { - // Check for an external link - const t = this.externalFrom[node.index]?.[0]; - if (t) { - rerouteType = t; - } - } - } - - config.forceInput = true; - return { - input: { - required: { - [rerouteType]: [rerouteType, config], - }, - }, - output: [rerouteType], - output_name: [], - output_is_list: [], - }; - } - - console.warn("Skipping virtual node " + node.type + " when building group node " + this.name); - } - - getInputConfig(node, inputName, seenInputs, config, extra) { - const customConfig = this.nodeData.config?.[node.index]?.input?.[inputName]; - let name = customConfig?.name ?? node.inputs?.find((inp) => inp.name === inputName)?.label ?? inputName; - let key = name; - let prefix = ""; - // Special handling for primitive to include the title if it is set rather than just "value" - if ((node.type === "PrimitiveNode" && node.title) || name in seenInputs) { - prefix = `${node.title ?? node.type} `; - key = name = `${prefix}${inputName}`; - if (name in seenInputs) { - name = `${prefix}${seenInputs[name]} ${inputName}`; - } - } - seenInputs[key] = (seenInputs[key] ?? 1) + 1; - - if (inputName === "seed" || inputName === "noise_seed") { - if (!extra) extra = {}; - extra.control_after_generate = `${prefix}control_after_generate`; - } - if (config[0] === "IMAGEUPLOAD") { - if (!extra) extra = {}; - extra.widget = this.oldToNewWidgetMap[node.index]?.[config[1]?.widget ?? "image"] ?? "image"; - } - - if (extra) { - config = [config[0], { ...config[1], ...extra }]; - } - - return { name, config, customConfig }; - } - - processWidgetInputs(inputs, node, inputNames, seenInputs) { - const slots = []; - const converted = new Map(); - const widgetMap = (this.oldToNewWidgetMap[node.index] = {}); - for (const inputName of inputNames) { - let widgetType = app.getWidgetType(inputs[inputName], inputName); - if (widgetType) { - const convertedIndex = node.inputs?.findIndex((inp) => inp.name === inputName && inp.widget?.name === inputName); - if (convertedIndex > -1) { - // This widget has been converted to a widget - // We need to store this in the correct position so link ids line up - converted.set(convertedIndex, inputName); - widgetMap[inputName] = null; - } else { - // Normal widget - const { name, config } = this.getInputConfig(node, inputName, seenInputs, inputs[inputName]); - this.nodeDef.input.required[name] = config; - widgetMap[inputName] = name; - this.newToOldWidgetMap[name] = { node, inputName }; - } - } else { - // Normal input - slots.push(inputName); - } - } - return { converted, slots }; - } - - checkPrimitiveConnection(link, inputName, inputs) { - const sourceNode = this.nodeData.nodes[link[0]]; - if (sourceNode.type === "PrimitiveNode") { - // Merge link configurations - const [sourceNodeId, _, targetNodeId, __] = link; - const primitiveDef = this.primitiveDefs[sourceNodeId]; - const targetWidget = inputs[inputName]; - const primitiveConfig = primitiveDef.input.required.value; - const output = { widget: primitiveConfig }; - const config = mergeIfValid(output, targetWidget, false, null, primitiveConfig); - primitiveConfig[1] = config?.customConfig ?? inputs[inputName][1] ? { ...inputs[inputName][1] } : {}; - - let name = this.oldToNewWidgetMap[sourceNodeId]["value"]; - name = name.substr(0, name.length - 6); - primitiveConfig[1].control_after_generate = true; - primitiveConfig[1].control_prefix = name; - - let toPrimitive = this.widgetToPrimitive[targetNodeId]; - if (!toPrimitive) { - toPrimitive = this.widgetToPrimitive[targetNodeId] = {}; - } - if (toPrimitive[inputName]) { - toPrimitive[inputName].push(sourceNodeId); - } - toPrimitive[inputName] = sourceNodeId; - - let toWidget = this.primitiveToWidget[sourceNodeId]; - if (!toWidget) { - toWidget = this.primitiveToWidget[sourceNodeId] = []; - } - toWidget.push({ nodeId: targetNodeId, inputName }); - } - } - - processInputSlots(inputs, node, slots, linksTo, inputMap, seenInputs) { - this.nodeInputs[node.index] = {}; - for (let i = 0; i < slots.length; i++) { - const inputName = slots[i]; - if (linksTo[i]) { - this.checkPrimitiveConnection(linksTo[i], inputName, inputs); - // This input is linked so we can skip it - continue; - } - - const { name, config, customConfig } = this.getInputConfig(node, inputName, seenInputs, inputs[inputName]); - - this.nodeInputs[node.index][inputName] = name; - if(customConfig?.visible === false) continue; - - this.nodeDef.input.required[name] = config; - inputMap[i] = this.inputCount++; - } - } - - processConvertedWidgets(inputs, node, slots, converted, linksTo, inputMap, seenInputs) { - // Add converted widgets sorted into their index order (ordered as they were converted) so link ids match up - const convertedSlots = [...converted.keys()].sort().map((k) => converted.get(k)); - for (let i = 0; i < convertedSlots.length; i++) { - const inputName = convertedSlots[i]; - if (linksTo[slots.length + i]) { - this.checkPrimitiveConnection(linksTo[slots.length + i], inputName, inputs); - // This input is linked so we can skip it - continue; - } - - const { name, config } = this.getInputConfig(node, inputName, seenInputs, inputs[inputName], { - defaultInput: true, - }); - - this.nodeDef.input.required[name] = config; - this.newToOldWidgetMap[name] = { node, inputName }; - - if (!this.oldToNewWidgetMap[node.index]) { - this.oldToNewWidgetMap[node.index] = {}; - } - this.oldToNewWidgetMap[node.index][inputName] = name; - - inputMap[slots.length + i] = this.inputCount++; - } - } - - #convertedToProcess = []; - processNodeInputs(node, seenInputs, inputs) { - const inputMapping = []; - - const inputNames = Object.keys(inputs); - if (!inputNames.length) return; - - const { converted, slots } = this.processWidgetInputs(inputs, node, inputNames, seenInputs); - const linksTo = this.linksTo[node.index] ?? {}; - const inputMap = (this.oldToNewInputMap[node.index] = {}); - this.processInputSlots(inputs, node, slots, linksTo, inputMap, seenInputs); - - // Converted inputs have to be processed after all other nodes as they'll be at the end of the list - this.#convertedToProcess.push(() => this.processConvertedWidgets(inputs, node, slots, converted, linksTo, inputMap, seenInputs)); - - return inputMapping; - } - - processNodeOutputs(node, seenOutputs, def) { - const oldToNew = (this.oldToNewOutputMap[node.index] = {}); - - // Add outputs - for (let outputId = 0; outputId < def.output.length; outputId++) { - const linksFrom = this.linksFrom[node.index]; - // If this output is linked internally we flag it to hide - const hasLink = linksFrom?.[outputId] && !this.externalFrom[node.index]?.[outputId]; - const customConfig = this.nodeData.config?.[node.index]?.output?.[outputId]; - const visible = customConfig?.visible ?? !hasLink; - this.outputVisibility.push(visible); - if (!visible) { - continue; - } - - oldToNew[outputId] = this.nodeDef.output.length; - this.newToOldOutputMap[this.nodeDef.output.length] = { node, slot: outputId }; - this.nodeDef.output.push(def.output[outputId]); - this.nodeDef.output_is_list.push(def.output_is_list[outputId]); - - let label = customConfig?.name; - if (!label) { - label = def.output_name?.[outputId] ?? def.output[outputId]; - const output = node.outputs.find((o) => o.name === label); - if (output?.label) { - label = output.label; - } - } - - let name = label; - if (name in seenOutputs) { - const prefix = `${node.title ?? node.type} `; - name = `${prefix}${label}`; - if (name in seenOutputs) { - name = `${prefix}${node.index} ${label}`; - } - } - seenOutputs[name] = 1; - - this.nodeDef.output_name.push(name); - } - } - - static async registerFromWorkflow(groupNodes, missingNodeTypes) { - const clean = app.clean; - app.clean = function () { - for (const g in groupNodes) { - try { - LiteGraph.unregisterNodeType("workflow/" + g); - } catch (error) {} - } - app.clean = clean; - }; - - for (const g in groupNodes) { - const groupData = groupNodes[g]; - - let hasMissing = false; - for (const n of groupData.nodes) { - // Find missing node types - if (!(n.type in LiteGraph.registered_node_types)) { - missingNodeTypes.push({ - type: n.type, - hint: ` (In group node 'workflow/${g}')`, - }); - - missingNodeTypes.push({ - type: "workflow/" + g, - action: { - text: "Remove from workflow", - callback: (e) => { - delete groupNodes[g]; - e.target.textContent = "Removed"; - e.target.style.pointerEvents = "none"; - e.target.style.opacity = 0.7; - }, - }, - }); - - hasMissing = true; - } - } - - if (hasMissing) continue; - - const config = new GroupNodeConfig(g, groupData); - await config.registerType(); - } - } -} - -export class GroupNodeHandler { - node; - groupData; - - constructor(node) { - this.node = node; - this.groupData = node.constructor?.nodeData?.[GROUP]; - - this.node.setInnerNodes = (innerNodes) => { - this.innerNodes = innerNodes; - - for (let innerNodeIndex = 0; innerNodeIndex < this.innerNodes.length; innerNodeIndex++) { - const innerNode = this.innerNodes[innerNodeIndex]; - - for (const w of innerNode.widgets ?? []) { - if (w.type === "converted-widget") { - w.serializeValue = w.origSerializeValue; - } - } - - innerNode.index = innerNodeIndex; - innerNode.getInputNode = (slot) => { - // Check if this input is internal or external - const externalSlot = this.groupData.oldToNewInputMap[innerNode.index]?.[slot]; - if (externalSlot != null) { - return this.node.getInputNode(externalSlot); - } - - // Internal link - const innerLink = this.groupData.linksTo[innerNode.index]?.[slot]; - if (!innerLink) return null; - - const inputNode = innerNodes[innerLink[0]]; - // Primitives will already apply their values - if (inputNode.type === "PrimitiveNode") return null; - - return inputNode; - }; - - innerNode.getInputLink = (slot) => { - const externalSlot = this.groupData.oldToNewInputMap[innerNode.index]?.[slot]; - if (externalSlot != null) { - // The inner node is connected via the group node inputs - const linkId = this.node.inputs[externalSlot].link; - let link = app.graph.links[linkId]; - - // Use the outer link, but update the target to the inner node - link = { - ...link, - target_id: innerNode.id, - target_slot: +slot, - }; - return link; - } - - let link = this.groupData.linksTo[innerNode.index]?.[slot]; - if (!link) return null; - // Use the inner link, but update the origin node to be inner node id - link = { - origin_id: innerNodes[link[0]].id, - origin_slot: link[1], - target_id: innerNode.id, - target_slot: +slot, - }; - return link; - }; - } - }; - - this.node.updateLink = (link) => { - // Replace the group node reference with the internal node - link = { ...link }; - const output = this.groupData.newToOldOutputMap[link.origin_slot]; - let innerNode = this.innerNodes[output.node.index]; - let l; - while (innerNode?.type === "Reroute") { - l = innerNode.getInputLink(0); - innerNode = innerNode.getInputNode(0); - } - - if (!innerNode) { - return null; - } - - if (l && GroupNodeHandler.isGroupNode(innerNode)) { - return innerNode.updateLink(l); - } - - link.origin_id = innerNode.id; - link.origin_slot = l?.origin_slot ?? output.slot; - return link; - }; - - this.node.getInnerNodes = () => { - if (!this.innerNodes) { - this.node.setInnerNodes( - this.groupData.nodeData.nodes.map((n, i) => { - const innerNode = LiteGraph.createNode(n.type); - innerNode.configure(n); - innerNode.id = `${this.node.id}:${i}`; - return innerNode; - }) - ); - } - - this.updateInnerWidgets(); - - return this.innerNodes; - }; - - this.node.recreate = async () => { - const id = this.node.id; - const sz = this.node.size; - const nodes = this.node.convertToNodes(); - - const groupNode = LiteGraph.createNode(this.node.type); - groupNode.id = id; - - // Reuse the existing nodes for this instance - groupNode.setInnerNodes(nodes); - groupNode[GROUP].populateWidgets(); - app.graph.add(groupNode); - groupNode.size = [Math.max(groupNode.size[0], sz[0]), Math.max(groupNode.size[1], sz[1])]; - - // Remove all converted nodes and relink them - groupNode[GROUP].replaceNodes(nodes); - return groupNode; - }; - - this.node.convertToNodes = () => { - const addInnerNodes = () => { - const backup = localStorage.getItem("litegrapheditor_clipboard"); - // Clone the node data so we dont mutate it for other nodes - const c = { ...this.groupData.nodeData }; - c.nodes = [...c.nodes]; - const innerNodes = this.node.getInnerNodes(); - let ids = []; - for (let i = 0; i < c.nodes.length; i++) { - let id = innerNodes?.[i]?.id; - // Use existing IDs if they are set on the inner nodes - if (id == null || isNaN(id)) { - id = undefined; - } else { - ids.push(id); - } - c.nodes[i] = { ...c.nodes[i], id }; - } - localStorage.setItem("litegrapheditor_clipboard", JSON.stringify(c)); - app.canvas.pasteFromClipboard(); - localStorage.setItem("litegrapheditor_clipboard", backup); - - const [x, y] = this.node.pos; - let top; - let left; - // Configure nodes with current widget data - const selectedIds = ids.length ? ids : Object.keys(app.canvas.selected_nodes); - const newNodes = []; - for (let i = 0; i < selectedIds.length; i++) { - const id = selectedIds[i]; - const newNode = app.graph.getNodeById(id); - const innerNode = innerNodes[i]; - newNodes.push(newNode); - - if (left == null || newNode.pos[0] < left) { - left = newNode.pos[0]; - } - if (top == null || newNode.pos[1] < top) { - top = newNode.pos[1]; - } - - if (!newNode.widgets) continue; - - const map = this.groupData.oldToNewWidgetMap[innerNode.index]; - if (map) { - const widgets = Object.keys(map); - - for (const oldName of widgets) { - const newName = map[oldName]; - if (!newName) continue; - - const widgetIndex = this.node.widgets.findIndex((w) => w.name === newName); - if (widgetIndex === -1) continue; - - // Populate the main and any linked widgets - if (innerNode.type === "PrimitiveNode") { - for (let i = 0; i < newNode.widgets.length; i++) { - newNode.widgets[i].value = this.node.widgets[widgetIndex + i].value; - } - } else { - const outerWidget = this.node.widgets[widgetIndex]; - const newWidget = newNode.widgets.find((w) => w.name === oldName); - if (!newWidget) continue; - - newWidget.value = outerWidget.value; - for (let w = 0; w < outerWidget.linkedWidgets?.length; w++) { - newWidget.linkedWidgets[w].value = outerWidget.linkedWidgets[w].value; - } - } - } - } - } - - // Shift each node - for (const newNode of newNodes) { - newNode.pos = [newNode.pos[0] - (left - x), newNode.pos[1] - (top - y)]; - } - - return { newNodes, selectedIds }; - }; - - const reconnectInputs = (selectedIds) => { - for (const innerNodeIndex in this.groupData.oldToNewInputMap) { - const id = selectedIds[innerNodeIndex]; - const newNode = app.graph.getNodeById(id); - const map = this.groupData.oldToNewInputMap[innerNodeIndex]; - for (const innerInputId in map) { - const groupSlotId = map[innerInputId]; - if (groupSlotId == null) continue; - const slot = node.inputs[groupSlotId]; - if (slot.link == null) continue; - const link = app.graph.links[slot.link]; - if (!link) continue; - // connect this node output to the input of another node - const originNode = app.graph.getNodeById(link.origin_id); - originNode.connect(link.origin_slot, newNode, +innerInputId); - } - } - }; - - const reconnectOutputs = (selectedIds) => { - for (let groupOutputId = 0; groupOutputId < node.outputs?.length; groupOutputId++) { - const output = node.outputs[groupOutputId]; - if (!output.links) continue; - const links = [...output.links]; - for (const l of links) { - const slot = this.groupData.newToOldOutputMap[groupOutputId]; - const link = app.graph.links[l]; - const targetNode = app.graph.getNodeById(link.target_id); - const newNode = app.graph.getNodeById(selectedIds[slot.node.index]); - newNode.connect(slot.slot, targetNode, link.target_slot); - } - } - }; - - const { newNodes, selectedIds } = addInnerNodes(); - reconnectInputs(selectedIds); - reconnectOutputs(selectedIds); - app.graph.remove(this.node); - - return newNodes; - }; - - const getExtraMenuOptions = this.node.getExtraMenuOptions; - this.node.getExtraMenuOptions = function (_, options) { - getExtraMenuOptions?.apply(this, arguments); - - let optionIndex = options.findIndex((o) => o.content === "Outputs"); - if (optionIndex === -1) optionIndex = options.length; - else optionIndex++; - options.splice( - optionIndex, - 0, - null, - { - content: "Convert to nodes", - callback: () => { - return this.convertToNodes(); - }, - }, - { - content: "Manage Group Node", - callback: () => { - new ManageGroupDialog(app).show(this.type); - }, - } - ); - }; - - // Draw custom collapse icon to identity this as a group - const onDrawTitleBox = this.node.onDrawTitleBox; - this.node.onDrawTitleBox = function (ctx, height, size, scale) { - onDrawTitleBox?.apply(this, arguments); - - const fill = ctx.fillStyle; - ctx.beginPath(); - ctx.rect(11, -height + 11, 2, 2); - ctx.rect(14, -height + 11, 2, 2); - ctx.rect(17, -height + 11, 2, 2); - ctx.rect(11, -height + 14, 2, 2); - ctx.rect(14, -height + 14, 2, 2); - ctx.rect(17, -height + 14, 2, 2); - ctx.rect(11, -height + 17, 2, 2); - ctx.rect(14, -height + 17, 2, 2); - ctx.rect(17, -height + 17, 2, 2); - - ctx.fillStyle = this.boxcolor || LiteGraph.NODE_DEFAULT_BOXCOLOR; - ctx.fill(); - ctx.fillStyle = fill; - }; - - // Draw progress label - const onDrawForeground = node.onDrawForeground; - const groupData = this.groupData.nodeData; - node.onDrawForeground = function (ctx) { - const r = onDrawForeground?.apply?.(this, arguments); - if (+app.runningNodeId === this.id && this.runningInternalNodeId !== null) { - const n = groupData.nodes[this.runningInternalNodeId]; - if(!n) return; - const message = `Running ${n.title || n.type} (${this.runningInternalNodeId}/${groupData.nodes.length})`; - ctx.save(); - ctx.font = "12px sans-serif"; - const sz = ctx.measureText(message); - ctx.fillStyle = node.boxcolor || LiteGraph.NODE_DEFAULT_BOXCOLOR; - ctx.beginPath(); - ctx.roundRect(0, -LiteGraph.NODE_TITLE_HEIGHT - 20, sz.width + 12, 20, 5); - ctx.fill(); - - ctx.fillStyle = "#fff"; - ctx.fillText(message, 6, -LiteGraph.NODE_TITLE_HEIGHT - 6); - ctx.restore(); - } - }; - - // Flag this node as needing to be reset - const onExecutionStart = this.node.onExecutionStart; - this.node.onExecutionStart = function () { - this.resetExecution = true; - return onExecutionStart?.apply(this, arguments); - }; - - const self = this; - const onNodeCreated = this.node.onNodeCreated; - this.node.onNodeCreated = function () { - if (!this.widgets) { - return; - } - const config = self.groupData.nodeData.config; - if (config) { - for (const n in config) { - const inputs = config[n]?.input; - for (const w in inputs) { - if (inputs[w].visible !== false) continue; - const widgetName = self.groupData.oldToNewWidgetMap[n][w]; - const widget = this.widgets.find((w) => w.name === widgetName); - if (widget) { - widget.type = "hidden"; - widget.computeSize = () => [0, -4]; - } - } - } - } - - return onNodeCreated?.apply(this, arguments); - }; - - function handleEvent(type, getId, getEvent) { - const handler = ({ detail }) => { - const id = getId(detail); - if (!id) return; - const node = app.graph.getNodeById(id); - if (node) return; - - const innerNodeIndex = this.innerNodes?.findIndex((n) => n.id == id); - if (innerNodeIndex > -1) { - this.node.runningInternalNodeId = innerNodeIndex; - api.dispatchEvent(new CustomEvent(type, { detail: getEvent(detail, this.node.id + "", this.node) })); - } - }; - api.addEventListener(type, handler); - return handler; - } - - const executing = handleEvent.call( - this, - "executing", - (d) => d, - (d, id, node) => id - ); - - const executed = handleEvent.call( - this, - "executed", - (d) => d?.node, - (d, id, node) => ({ ...d, node: id, merge: !node.resetExecution }) - ); - - const onRemoved = node.onRemoved; - this.node.onRemoved = function () { - onRemoved?.apply(this, arguments); - api.removeEventListener("executing", executing); - api.removeEventListener("executed", executed); - }; - - this.node.refreshComboInNode = (defs) => { - // Update combo widget options - for (const widgetName in this.groupData.newToOldWidgetMap) { - const widget = this.node.widgets.find((w) => w.name === widgetName); - if (widget?.type === "combo") { - const old = this.groupData.newToOldWidgetMap[widgetName]; - const def = defs[old.node.type]; - const input = def?.input?.required?.[old.inputName] ?? def?.input?.optional?.[old.inputName]; - if (!input) continue; - - widget.options.values = input[0]; - - if (old.inputName !== "image" && !widget.options.values.includes(widget.value)) { - widget.value = widget.options.values[0]; - widget.callback(widget.value); - } - } - } - }; - } - - updateInnerWidgets() { - for (const newWidgetName in this.groupData.newToOldWidgetMap) { - const newWidget = this.node.widgets.find((w) => w.name === newWidgetName); - if (!newWidget) continue; - - const newValue = newWidget.value; - const old = this.groupData.newToOldWidgetMap[newWidgetName]; - let innerNode = this.innerNodes[old.node.index]; - - if (innerNode.type === "PrimitiveNode") { - innerNode.primitiveValue = newValue; - const primitiveLinked = this.groupData.primitiveToWidget[old.node.index]; - for (const linked of primitiveLinked ?? []) { - const node = this.innerNodes[linked.nodeId]; - const widget = node.widgets.find((w) => w.name === linked.inputName); - - if (widget) { - widget.value = newValue; - } - } - continue; - } else if (innerNode.type === "Reroute") { - const rerouteLinks = this.groupData.linksFrom[old.node.index]; - if (rerouteLinks) { - for (const [_, , targetNodeId, targetSlot] of rerouteLinks["0"]) { - const node = this.innerNodes[targetNodeId]; - const input = node.inputs[targetSlot]; - if (input.widget) { - const widget = node.widgets?.find((w) => w.name === input.widget.name); - if (widget) { - widget.value = newValue; - } - } - } - } - } - - const widget = innerNode.widgets?.find((w) => w.name === old.inputName); - if (widget) { - widget.value = newValue; - } - } - } - - populatePrimitive(node, nodeId, oldName, i, linkedShift) { - // Converted widget, populate primitive if linked - const primitiveId = this.groupData.widgetToPrimitive[nodeId]?.[oldName]; - if (primitiveId == null) return; - const targetWidgetName = this.groupData.oldToNewWidgetMap[primitiveId]["value"]; - const targetWidgetIndex = this.node.widgets.findIndex((w) => w.name === targetWidgetName); - if (targetWidgetIndex > -1) { - const primitiveNode = this.innerNodes[primitiveId]; - let len = primitiveNode.widgets.length; - if (len - 1 !== this.node.widgets[targetWidgetIndex].linkedWidgets?.length) { - // Fallback handling for if some reason the primitive has a different number of widgets - // we dont want to overwrite random widgets, better to leave blank - len = 1; - } - for (let i = 0; i < len; i++) { - this.node.widgets[targetWidgetIndex + i].value = primitiveNode.widgets[i].value; - } - } - return true; - } - - populateReroute(node, nodeId, map) { - if (node.type !== "Reroute") return; - - const link = this.groupData.linksFrom[nodeId]?.[0]?.[0]; - if (!link) return; - const [, , targetNodeId, targetNodeSlot] = link; - const targetNode = this.groupData.nodeData.nodes[targetNodeId]; - const inputs = targetNode.inputs; - const targetWidget = inputs?.[targetNodeSlot]?.widget; - if (!targetWidget) return; - - const offset = inputs.length - (targetNode.widgets_values?.length ?? 0); - const v = targetNode.widgets_values?.[targetNodeSlot - offset]; - if (v == null) return; - - const widgetName = Object.values(map)[0]; - const widget = this.node.widgets.find((w) => w.name === widgetName); - if (widget) { - widget.value = v; - } - } - - populateWidgets() { - if (!this.node.widgets) return; - - for (let nodeId = 0; nodeId < this.groupData.nodeData.nodes.length; nodeId++) { - const node = this.groupData.nodeData.nodes[nodeId]; - const map = this.groupData.oldToNewWidgetMap[nodeId] ?? {}; - const widgets = Object.keys(map); - - if (!node.widgets_values?.length) { - // special handling for populating values into reroutes - // this allows primitives connect to them to pick up the correct value - this.populateReroute(node, nodeId, map); - continue; - } - - let linkedShift = 0; - for (let i = 0; i < widgets.length; i++) { - const oldName = widgets[i]; - const newName = map[oldName]; - const widgetIndex = this.node.widgets.findIndex((w) => w.name === newName); - const mainWidget = this.node.widgets[widgetIndex]; - if (this.populatePrimitive(node, nodeId, oldName, i, linkedShift) || widgetIndex === -1) { - // Find the inner widget and shift by the number of linked widgets as they will have been removed too - const innerWidget = this.innerNodes[nodeId].widgets?.find((w) => w.name === oldName); - linkedShift += innerWidget?.linkedWidgets?.length ?? 0; - } - if (widgetIndex === -1) { - continue; - } - - // Populate the main and any linked widget - mainWidget.value = node.widgets_values[i + linkedShift]; - for (let w = 0; w < mainWidget.linkedWidgets?.length; w++) { - this.node.widgets[widgetIndex + w + 1].value = node.widgets_values[i + ++linkedShift]; - } - } - } - } - - replaceNodes(nodes) { - let top; - let left; - - for (let i = 0; i < nodes.length; i++) { - const node = nodes[i]; - if (left == null || node.pos[0] < left) { - left = node.pos[0]; - } - if (top == null || node.pos[1] < top) { - top = node.pos[1]; - } - - this.linkOutputs(node, i); - app.graph.remove(node); - } - - this.linkInputs(); - this.node.pos = [left, top]; - } - - linkOutputs(originalNode, nodeId) { - if (!originalNode.outputs) return; - - for (const output of originalNode.outputs) { - if (!output.links) continue; - // Clone the links as they'll be changed if we reconnect - const links = [...output.links]; - for (const l of links) { - const link = app.graph.links[l]; - if (!link) continue; - - const targetNode = app.graph.getNodeById(link.target_id); - const newSlot = this.groupData.oldToNewOutputMap[nodeId]?.[link.origin_slot]; - if (newSlot != null) { - this.node.connect(newSlot, targetNode, link.target_slot); - } - } - } - } - - linkInputs() { - for (const link of this.groupData.nodeData.links ?? []) { - const [, originSlot, targetId, targetSlot, actualOriginId] = link; - const originNode = app.graph.getNodeById(actualOriginId); - if (!originNode) continue; // this node is in the group - originNode.connect(originSlot, this.node.id, this.groupData.oldToNewInputMap[targetId][targetSlot]); - } - } - - static getGroupData(node) { - return (node.nodeData ?? node.constructor?.nodeData)?.[GROUP]; - } - - static isGroupNode(node) { - return !!node.constructor?.nodeData?.[GROUP]; - } - - static async fromNodes(nodes) { - // Process the nodes into the stored workflow group node data - const builder = new GroupNodeBuilder(nodes); - const res = builder.build(); - if (!res) return; - - const { name, nodeData } = res; - - // Convert this data into a LG node definition and register it - const config = new GroupNodeConfig(name, nodeData); - await config.registerType(); - - const groupNode = LiteGraph.createNode(`workflow/${name}`); - // Reuse the existing nodes for this instance - groupNode.setInnerNodes(builder.nodes); - groupNode[GROUP].populateWidgets(); - app.graph.add(groupNode); - - // Remove all converted nodes and relink them - groupNode[GROUP].replaceNodes(builder.nodes); - return groupNode; - } -} - -function addConvertToGroupOptions() { - function addConvertOption(options, index) { - const selected = Object.values(app.canvas.selected_nodes ?? {}); - const disabled = selected.length < 2 || selected.find((n) => GroupNodeHandler.isGroupNode(n)); - options.splice(index + 1, null, { - content: `Convert to Group Node`, - disabled, - callback: async () => { - return await GroupNodeHandler.fromNodes(selected); - }, - }); - } - - function addManageOption(options, index) { - const groups = app.graph.extra?.groupNodes; - const disabled = !groups || !Object.keys(groups).length; - options.splice(index + 1, null, { - content: `Manage Group Nodes`, - disabled, - callback: () => { - new ManageGroupDialog(app).show(); - }, - }); - } - - // Add to canvas - const getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions; - LGraphCanvas.prototype.getCanvasMenuOptions = function () { - const options = getCanvasMenuOptions.apply(this, arguments); - const index = options.findIndex((o) => o?.content === "Add Group") + 1 || options.length; - addConvertOption(options, index); - addManageOption(options, index + 1); - return options; - }; - - // Add to nodes - const getNodeMenuOptions = LGraphCanvas.prototype.getNodeMenuOptions; - LGraphCanvas.prototype.getNodeMenuOptions = function (node) { - const options = getNodeMenuOptions.apply(this, arguments); - if (!GroupNodeHandler.isGroupNode(node)) { - const index = options.findIndex((o) => o?.content === "Outputs") + 1 || options.length - 1; - addConvertOption(options, index); - } - return options; - }; -} - -const id = "Comfy.GroupNode"; -let globalDefs; -const ext = { - name: id, - setup() { - addConvertToGroupOptions(); - }, - async beforeConfigureGraph(graphData, missingNodeTypes) { - const nodes = graphData?.extra?.groupNodes; - if (nodes) { - await GroupNodeConfig.registerFromWorkflow(nodes, missingNodeTypes); - } - }, - addCustomNodeDefs(defs) { - // Store this so we can mutate it later with group nodes - globalDefs = defs; - }, - nodeCreated(node) { - if (GroupNodeHandler.isGroupNode(node)) { - node[GROUP] = new GroupNodeHandler(node); - } - }, - async refreshComboInNodes(defs) { - // Re-register group nodes so new ones are created with the correct options - Object.assign(globalDefs, defs); - const nodes = app.graph.extra?.groupNodes; - if (nodes) { - await GroupNodeConfig.registerFromWorkflow(nodes, {}); - } - } -}; - -app.registerExtension(ext); \ No newline at end of file diff --git a/web/extensions/core/groupNodeManage.css b/web/extensions/core/groupNodeManage.css deleted file mode 100644 index 5470ecb5e..000000000 --- a/web/extensions/core/groupNodeManage.css +++ /dev/null @@ -1,149 +0,0 @@ -.comfy-group-manage { - background: var(--bg-color); - color: var(--fg-color); - padding: 0; - font-family: Arial, Helvetica, sans-serif; - border-color: black; - margin: 20vh auto; - max-height: 60vh; -} -.comfy-group-manage-outer { - max-height: 60vh; - min-width: 500px; - display: flex; - flex-direction: column; -} -.comfy-group-manage-outer > header { - display: flex; - align-items: center; - gap: 10px; - justify-content: space-between; - background: var(--comfy-menu-bg); - padding: 15px 20px; -} -.comfy-group-manage-outer > header select { - background: var(--comfy-input-bg); - border: 1px solid var(--border-color); - color: var(--input-text); - padding: 5px 10px; - border-radius: 5px; -} -.comfy-group-manage h2 { - margin: 0; - font-weight: normal; -} -.comfy-group-manage main { - display: flex; - overflow: hidden; -} -.comfy-group-manage .drag-handle { - font-weight: bold; -} -.comfy-group-manage-list { - border-right: 1px solid var(--comfy-menu-bg); -} -.comfy-group-manage-list ul { - margin: 40px 0 0; - padding: 0; - list-style: none; -} -.comfy-group-manage-list-items { - max-height: calc(100% - 40px); - overflow-y: scroll; - overflow-x: hidden; -} -.comfy-group-manage-list li { - display: flex; - padding: 10px 20px 10px 10px; - cursor: pointer; - align-items: center; - gap: 5px; -} -.comfy-group-manage-list div { - display: flex; - flex-direction: column; -} -.comfy-group-manage-list li:not(.selected):hover div { - text-decoration: underline; -} -.comfy-group-manage-list li.selected { - background: var(--border-color); -} -.comfy-group-manage-list li span { - opacity: 0.7; - font-size: smaller; -} -.comfy-group-manage-node { - flex: auto; - background: var(--border-color); - display: flex; - flex-direction: column; -} -.comfy-group-manage-node > div { - overflow: auto; -} -.comfy-group-manage-node header { - display: flex; - background: var(--bg-color); - height: 40px; -} -.comfy-group-manage-node header a { - text-align: center; - flex: auto; - border-right: 1px solid var(--comfy-menu-bg); - border-bottom: 1px solid var(--comfy-menu-bg); - padding: 10px; - cursor: pointer; - font-size: 15px; -} -.comfy-group-manage-node header a:last-child { - border-right: none; -} -.comfy-group-manage-node header a:not(.active):hover { - text-decoration: underline; -} -.comfy-group-manage-node header a.active { - background: var(--border-color); - border-bottom: none; -} -.comfy-group-manage-node-page { - display: none; - overflow: auto; -} -.comfy-group-manage-node-page.active { - display: block; -} -.comfy-group-manage-node-page div { - padding: 10px; - display: flex; - align-items: center; - gap: 10px; -} -.comfy-group-manage-node-page input { - border: none; - color: var(--input-text); - background: var(--comfy-input-bg); - padding: 5px 10px; -} -.comfy-group-manage-node-page input[type="text"] { - flex: auto; -} -.comfy-group-manage-node-page label { - display: flex; - gap: 5px; - align-items: center; -} -.comfy-group-manage footer { - border-top: 1px solid var(--comfy-menu-bg); - padding: 10px; - display: flex; - gap: 10px; -} -.comfy-group-manage footer button { - font-size: 14px; - padding: 5px 10px; - border-radius: 0; -} -.comfy-group-manage footer button:first-child { - margin-right: auto; -} diff --git a/web/extensions/core/groupNodeManage.js b/web/extensions/core/groupNodeManage.js deleted file mode 100644 index 1ab338386..000000000 --- a/web/extensions/core/groupNodeManage.js +++ /dev/null @@ -1,422 +0,0 @@ -import { $el, ComfyDialog } from "../../scripts/ui.js"; -import { DraggableList } from "../../scripts/ui/draggableList.js"; -import { addStylesheet } from "../../scripts/utils.js"; -import { GroupNodeConfig, GroupNodeHandler } from "./groupNode.js"; - -addStylesheet(import.meta.url); - -const ORDER = Symbol(); - -function merge(target, source) { - if (typeof target === "object" && typeof source === "object") { - for (const key in source) { - const sv = source[key]; - if (typeof sv === "object") { - let tv = target[key]; - if (!tv) tv = target[key] = {}; - merge(tv, source[key]); - } else { - target[key] = sv; - } - } - } - - return target; -} - -export class ManageGroupDialog extends ComfyDialog { - /** @type { Record<"Inputs" | "Outputs" | "Widgets", {tab: HTMLAnchorElement, page: HTMLElement}> } */ - tabs = {}; - /** @type { number | null | undefined } */ - selectedNodeIndex; - /** @type { keyof ManageGroupDialog["tabs"] } */ - selectedTab = "Inputs"; - /** @type { string | undefined } */ - selectedGroup; - - /** @type { Record>> } */ - modifications = {}; - - get selectedNodeInnerIndex() { - return +this.nodeItems[this.selectedNodeIndex].dataset.nodeindex; - } - - constructor(app) { - super(); - this.app = app; - this.element = $el("dialog.comfy-group-manage", { - parent: document.body, - }); - } - - changeTab(tab) { - this.tabs[this.selectedTab].tab.classList.remove("active"); - this.tabs[this.selectedTab].page.classList.remove("active"); - this.tabs[tab].tab.classList.add("active"); - this.tabs[tab].page.classList.add("active"); - this.selectedTab = tab; - } - - changeNode(index, force) { - if (!force && this.selectedNodeIndex === index) return; - - if (this.selectedNodeIndex != null) { - this.nodeItems[this.selectedNodeIndex].classList.remove("selected"); - } - this.nodeItems[index].classList.add("selected"); - this.selectedNodeIndex = index; - - if (!this.buildInputsPage() && this.selectedTab === "Inputs") { - this.changeTab("Widgets"); - } - if (!this.buildWidgetsPage() && this.selectedTab === "Widgets") { - this.changeTab("Outputs"); - } - if (!this.buildOutputsPage() && this.selectedTab === "Outputs") { - this.changeTab("Inputs"); - } - - this.changeTab(this.selectedTab); - } - - getGroupData() { - this.groupNodeType = LiteGraph.registered_node_types["workflow/" + this.selectedGroup]; - this.groupNodeDef = this.groupNodeType.nodeData; - this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType); - } - - changeGroup(group, reset = true) { - this.selectedGroup = group; - this.getGroupData(); - - const nodes = this.groupData.nodeData.nodes; - this.nodeItems = nodes.map((n, i) => - $el( - "li.draggable-item", - { - dataset: { - nodeindex: n.index + "", - }, - onclick: () => { - this.changeNode(i); - }, - }, - [ - $el("span.drag-handle"), - $el( - "div", - { - textContent: n.title ?? n.type, - }, - n.title - ? $el("span", { - textContent: n.type, - }) - : [] - ), - ] - ) - ); - - this.innerNodesList.replaceChildren(...this.nodeItems); - - if (reset) { - this.selectedNodeIndex = null; - this.changeNode(0); - } else { - const items = this.draggable.getAllItems(); - let index = items.findIndex(item => item.classList.contains("selected")); - if(index === -1) index = this.selectedNodeIndex; - this.changeNode(index, true); - } - - const ordered = [...nodes]; - this.draggable?.dispose(); - this.draggable = new DraggableList(this.innerNodesList, "li"); - this.draggable.addEventListener("dragend", ({ detail: { oldPosition, newPosition } }) => { - if (oldPosition === newPosition) return; - ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0]); - for (let i = 0; i < ordered.length; i++) { - this.storeModification({ nodeIndex: ordered[i].index, section: ORDER, prop: "order", value: i }); - } - }); - } - - storeModification({ nodeIndex, section, prop, value }) { - const groupMod = (this.modifications[this.selectedGroup] ??= {}); - const nodesMod = (groupMod.nodes ??= {}); - const nodeMod = (nodesMod[nodeIndex ?? this.selectedNodeInnerIndex] ??= {}); - const typeMod = (nodeMod[section] ??= {}); - if (typeof value === "object") { - const objMod = (typeMod[prop] ??= {}); - Object.assign(objMod, value); - } else { - typeMod[prop] = value; - } - } - - getEditElement(section, prop, value, placeholder, checked, checkable = true) { - if (value === placeholder) value = ""; - - const mods = this.modifications[this.selectedGroup]?.nodes?.[this.selectedNodeInnerIndex]?.[section]?.[prop]; - if (mods) { - if (mods.name != null) { - value = mods.name; - } - if (mods.visible != null) { - checked = mods.visible; - } - } - - return $el("div", [ - $el("input", { - value, - placeholder, - type: "text", - onchange: (e) => { - this.storeModification({ section, prop, value: { name: e.target.value } }); - }, - }), - $el("label", { textContent: "Visible" }, [ - $el("input", { - type: "checkbox", - checked, - disabled: !checkable, - onchange: (e) => { - this.storeModification({ section, prop, value: { visible: !!e.target.checked } }); - }, - }), - ]), - ]); - } - - buildWidgetsPage() { - const widgets = this.groupData.oldToNewWidgetMap[this.selectedNodeInnerIndex]; - const items = Object.keys(widgets ?? {}); - const type = app.graph.extra.groupNodes[this.selectedGroup]; - const config = type.config?.[this.selectedNodeInnerIndex]?.input; - this.widgetsPage.replaceChildren( - ...items.map((oldName) => { - return this.getEditElement("input", oldName, widgets[oldName], oldName, config?.[oldName]?.visible !== false); - }) - ); - return !!items.length; - } - - buildInputsPage() { - const inputs = this.groupData.nodeInputs[this.selectedNodeInnerIndex]; - const items = Object.keys(inputs ?? {}); - const type = app.graph.extra.groupNodes[this.selectedGroup]; - const config = type.config?.[this.selectedNodeInnerIndex]?.input; - this.inputsPage.replaceChildren( - ...items - .map((oldName) => { - let value = inputs[oldName]; - if (!value) { - return; - } - - return this.getEditElement("input", oldName, value, oldName, config?.[oldName]?.visible !== false); - }) - .filter(Boolean) - ); - return !!items.length; - } - - buildOutputsPage() { - const nodes = this.groupData.nodeData.nodes; - const innerNodeDef = this.groupData.getNodeDef(nodes[this.selectedNodeInnerIndex]); - const outputs = innerNodeDef?.output ?? []; - const groupOutputs = this.groupData.oldToNewOutputMap[this.selectedNodeInnerIndex]; - - const type = app.graph.extra.groupNodes[this.selectedGroup]; - const config = type.config?.[this.selectedNodeInnerIndex]?.output; - const node = this.groupData.nodeData.nodes[this.selectedNodeInnerIndex]; - const checkable = node.type !== "PrimitiveNode"; - this.outputsPage.replaceChildren( - ...outputs - .map((type, slot) => { - const groupOutputIndex = groupOutputs?.[slot]; - const oldName = innerNodeDef.output_name?.[slot] ?? type; - let value = config?.[slot]?.name; - const visible = config?.[slot]?.visible || groupOutputIndex != null; - if (!value || value === oldName) { - value = ""; - } - return this.getEditElement("output", slot, value, oldName, visible, checkable); - }) - .filter(Boolean) - ); - return !!outputs.length; - } - - show(type) { - const groupNodes = Object.keys(app.graph.extra?.groupNodes ?? {}).sort((a, b) => a.localeCompare(b)); - - this.innerNodesList = $el("ul.comfy-group-manage-list-items"); - this.widgetsPage = $el("section.comfy-group-manage-node-page"); - this.inputsPage = $el("section.comfy-group-manage-node-page"); - this.outputsPage = $el("section.comfy-group-manage-node-page"); - const pages = $el("div", [this.widgetsPage, this.inputsPage, this.outputsPage]); - - this.tabs = [ - ["Inputs", this.inputsPage], - ["Widgets", this.widgetsPage], - ["Outputs", this.outputsPage], - ].reduce((p, [name, page]) => { - p[name] = { - tab: $el("a", { - onclick: () => { - this.changeTab(name); - }, - textContent: name, - }), - page, - }; - return p; - }, {}); - - const outer = $el("div.comfy-group-manage-outer", [ - $el("header", [ - $el("h2", "Group Nodes"), - $el( - "select", - { - onchange: (e) => { - this.changeGroup(e.target.value); - }, - }, - groupNodes.map((g) => - $el("option", { - textContent: g, - selected: "workflow/" + g === type, - value: g, - }) - ) - ), - ]), - $el("main", [ - $el("section.comfy-group-manage-list", this.innerNodesList), - $el("section.comfy-group-manage-node", [ - $el( - "header", - Object.values(this.tabs).map((t) => t.tab) - ), - pages, - ]), - ]), - $el("footer", [ - $el( - "button.comfy-btn", - { - onclick: (e) => { - const node = app.graph._nodes.find((n) => n.type === "workflow/" + this.selectedGroup); - if (node) { - alert("This group node is in use in the current workflow, please first remove these."); - return; - } - if (confirm(`Are you sure you want to remove the node: "${this.selectedGroup}"`)) { - delete app.graph.extra.groupNodes[this.selectedGroup]; - LiteGraph.unregisterNodeType("workflow/" + this.selectedGroup); - } - this.show(); - }, - }, - "Delete Group Node" - ), - $el( - "button.comfy-btn", - { - onclick: async () => { - let nodesByType; - let recreateNodes = []; - const types = {}; - for (const g in this.modifications) { - const type = app.graph.extra.groupNodes[g]; - let config = (type.config ??= {}); - - let nodeMods = this.modifications[g]?.nodes; - if (nodeMods) { - const keys = Object.keys(nodeMods); - if (nodeMods[keys[0]][ORDER]) { - // If any node is reordered, they will all need sequencing - const orderedNodes = []; - const orderedMods = {}; - const orderedConfig = {}; - - for (const n of keys) { - const order = nodeMods[n][ORDER].order; - orderedNodes[order] = type.nodes[+n]; - orderedMods[order] = nodeMods[n]; - orderedNodes[order].index = order; - } - - // Rewrite links - for (const l of type.links) { - if (l[0] != null) l[0] = type.nodes[l[0]].index; - if (l[2] != null) l[2] = type.nodes[l[2]].index; - } - - // Rewrite externals - if (type.external) { - for (const ext of type.external) { - ext[0] = type.nodes[ext[0]]; - } - } - - // Rewrite modifications - for (const id of keys) { - if (config[id]) { - orderedConfig[type.nodes[id].index] = config[id]; - } - delete config[id]; - } - - type.nodes = orderedNodes; - nodeMods = orderedMods; - type.config = config = orderedConfig; - } - - merge(config, nodeMods); - } - - types[g] = type; - - if (!nodesByType) { - nodesByType = app.graph._nodes.reduce((p, n) => { - p[n.type] ??= []; - p[n.type].push(n); - return p; - }, {}); - } - - const nodes = nodesByType["workflow/" + g]; - if (nodes) recreateNodes.push(...nodes); - } - - await GroupNodeConfig.registerFromWorkflow(types, {}); - - for (const node of recreateNodes) { - node.recreate(); - } - - this.modifications = {}; - this.app.graph.setDirtyCanvas(true, true); - this.changeGroup(this.selectedGroup, false); - }, - }, - "Save" - ), - $el("button.comfy-btn", { onclick: () => this.element.close() }, "Close"), - ]), - ]); - - this.element.replaceChildren(outer); - this.changeGroup(type ? groupNodes.find((g) => "workflow/" + g === type) : groupNodes[0]); - this.element.showModal(); - - this.element.addEventListener("close", () => { - this.draggable?.dispose(); - }); - } -} \ No newline at end of file diff --git a/web/extensions/core/groupOptions.js b/web/extensions/core/groupOptions.js deleted file mode 100644 index 5dd21e730..000000000 --- a/web/extensions/core/groupOptions.js +++ /dev/null @@ -1,259 +0,0 @@ -import {app} from "../../scripts/app.js"; - -function setNodeMode(node, mode) { - node.mode = mode; - node.graph.change(); -} - -function addNodesToGroup(group, nodes=[]) { - var x1, y1, x2, y2; - var nx1, ny1, nx2, ny2; - var node; - - x1 = y1 = x2 = y2 = -1; - nx1 = ny1 = nx2 = ny2 = -1; - - for (var n of [group._nodes, nodes]) { - for (var i in n) { - node = n[i] - - nx1 = node.pos[0] - ny1 = node.pos[1] - nx2 = node.pos[0] + node.size[0] - ny2 = node.pos[1] + node.size[1] - - if (node.type != "Reroute") { - ny1 -= LiteGraph.NODE_TITLE_HEIGHT; - } - - if (node.flags?.collapsed) { - ny2 = ny1 + LiteGraph.NODE_TITLE_HEIGHT; - - if (node?._collapsed_width) { - nx2 = nx1 + Math.round(node._collapsed_width); - } - } - - if (x1 == -1 || nx1 < x1) { - x1 = nx1; - } - - if (y1 == -1 || ny1 < y1) { - y1 = ny1; - } - - if (x2 == -1 || nx2 > x2) { - x2 = nx2; - } - - if (y2 == -1 || ny2 > y2) { - y2 = ny2; - } - } - } - - var padding = 10; - - y1 = y1 - Math.round(group.font_size * 1.4); - - group.pos = [x1 - padding, y1 - padding]; - group.size = [x2 - x1 + padding * 2, y2 - y1 + padding * 2]; -} - -app.registerExtension({ - name: "Comfy.GroupOptions", - setup() { - const orig = LGraphCanvas.prototype.getCanvasMenuOptions; - // graph_mouse - LGraphCanvas.prototype.getCanvasMenuOptions = function () { - const options = orig.apply(this, arguments); - const group = this.graph.getGroupOnPos(this.graph_mouse[0], this.graph_mouse[1]); - if (!group) { - options.push({ - content: "Add Group For Selected Nodes", - disabled: !Object.keys(app.canvas.selected_nodes || {}).length, - callback: () => { - var group = new LiteGraph.LGraphGroup(); - addNodesToGroup(group, this.selected_nodes) - app.canvas.graph.add(group); - this.graph.change(); - } - }); - - return options; - } - - // Group nodes aren't recomputed until the group is moved, this ensures the nodes are up-to-date - group.recomputeInsideNodes(); - const nodesInGroup = group._nodes; - - options.push({ - content: "Add Selected Nodes To Group", - disabled: !Object.keys(app.canvas.selected_nodes || {}).length, - callback: () => { - addNodesToGroup(group, this.selected_nodes) - this.graph.change(); - } - }); - - // No nodes in group, return default options - if (nodesInGroup.length === 0) { - return options; - } else { - // Add a separator between the default options and the group options - options.push(null); - } - - // Check if all nodes are the same mode - let allNodesAreSameMode = true; - for (let i = 1; i < nodesInGroup.length; i++) { - if (nodesInGroup[i].mode !== nodesInGroup[0].mode) { - allNodesAreSameMode = false; - break; - } - } - - options.push({ - content: "Fit Group To Nodes", - callback: () => { - addNodesToGroup(group) - this.graph.change(); - } - }); - - options.push({ - content: "Select Nodes", - callback: () => { - this.selectNodes(nodesInGroup); - this.graph.change(); - this.canvas.focus(); - } - }); - - // Modes - // 0: Always - // 1: On Event - // 2: Never - // 3: On Trigger - // 4: Bypass - // If all nodes are the same mode, add a menu option to change the mode - if (allNodesAreSameMode) { - const mode = nodesInGroup[0].mode; - switch (mode) { - case 0: - // All nodes are always, option to disable, and bypass - options.push({ - content: "Set Group Nodes to Never", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 2); - } - } - }); - options.push({ - content: "Bypass Group Nodes", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 4); - } - } - }); - break; - case 2: - // All nodes are never, option to enable, and bypass - options.push({ - content: "Set Group Nodes to Always", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 0); - } - } - }); - options.push({ - content: "Bypass Group Nodes", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 4); - } - } - }); - break; - case 4: - // All nodes are bypass, option to enable, and disable - options.push({ - content: "Set Group Nodes to Always", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 0); - } - } - }); - options.push({ - content: "Set Group Nodes to Never", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 2); - } - } - }); - break; - default: - // All nodes are On Trigger or On Event(Or other?), option to disable, set to always, or bypass - options.push({ - content: "Set Group Nodes to Always", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 0); - } - } - }); - options.push({ - content: "Set Group Nodes to Never", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 2); - } - } - }); - options.push({ - content: "Bypass Group Nodes", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 4); - } - } - }); - break; - } - } else { - // Nodes are not all the same mode, add a menu option to change the mode to always, never, or bypass - options.push({ - content: "Set Group Nodes to Always", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 0); - } - } - }); - options.push({ - content: "Set Group Nodes to Never", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 2); - } - } - }); - options.push({ - content: "Bypass Group Nodes", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 4); - } - } - }); - } - - return options - } - } -}); diff --git a/web/extensions/core/invertMenuScrolling.js b/web/extensions/core/invertMenuScrolling.js deleted file mode 100644 index 98a1786ab..000000000 --- a/web/extensions/core/invertMenuScrolling.js +++ /dev/null @@ -1,36 +0,0 @@ -import { app } from "../../scripts/app.js"; - -// Inverts the scrolling of context menus - -const id = "Comfy.InvertMenuScrolling"; -app.registerExtension({ - name: id, - init() { - const ctxMenu = LiteGraph.ContextMenu; - const replace = () => { - LiteGraph.ContextMenu = function (values, options) { - options = options || {}; - if (options.scroll_speed) { - options.scroll_speed *= -1; - } else { - options.scroll_speed = -0.1; - } - return ctxMenu.call(this, values, options); - }; - LiteGraph.ContextMenu.prototype = ctxMenu.prototype; - }; - app.ui.settings.addSetting({ - id, - name: "Invert Menu Scrolling", - type: "boolean", - defaultValue: false, - onChange(value) { - if (value) { - replace(); - } else { - LiteGraph.ContextMenu = ctxMenu; - } - }, - }); - }, -}); diff --git a/web/extensions/core/keybinds.js b/web/extensions/core/keybinds.js deleted file mode 100644 index ac367c116..000000000 --- a/web/extensions/core/keybinds.js +++ /dev/null @@ -1,69 +0,0 @@ -import {app} from "../../scripts/app.js"; - -app.registerExtension({ - name: "Comfy.Keybinds", - init() { - const keybindListener = function (event) { - const modifierPressed = event.ctrlKey || event.metaKey; - - // Queue prompt using ctrl or command + enter - if (modifierPressed && event.key === "Enter") { - app.queuePrompt(event.shiftKey ? -1 : 0).then(); - return; - } - - const target = event.composedPath()[0]; - if (["INPUT", "TEXTAREA"].includes(target.tagName)) { - return; - } - - const modifierKeyIdMap = { - s: "#comfy-save-button", - o: "#comfy-file-input", - Backspace: "#comfy-clear-button", - d: "#comfy-load-default-button", - }; - - const modifierKeybindId = modifierKeyIdMap[event.key]; - if (modifierPressed && modifierKeybindId) { - event.preventDefault(); - - const elem = document.querySelector(modifierKeybindId); - elem.click(); - return; - } - - // Finished Handling all modifier keybinds, now handle the rest - if (event.ctrlKey || event.altKey || event.metaKey) { - return; - } - - // Close out of modals using escape - if (event.key === "Escape") { - const modals = document.querySelectorAll(".comfy-modal"); - const modal = Array.from(modals).find(modal => window.getComputedStyle(modal).getPropertyValue("display") !== "none"); - if (modal) { - modal.style.display = "none"; - } - - [...document.querySelectorAll("dialog")].forEach(d => { - d.close(); - }); - } - - const keyIdMap = { - q: "#comfy-view-queue-button", - h: "#comfy-view-history-button", - r: "#comfy-refresh-button", - }; - - const buttonId = keyIdMap[event.key]; - if (buttonId) { - const button = document.querySelector(buttonId); - button.click(); - } - } - - window.addEventListener("keydown", keybindListener, true); - } -}); diff --git a/web/extensions/core/linkRenderMode.js b/web/extensions/core/linkRenderMode.js deleted file mode 100644 index fb4df4234..000000000 --- a/web/extensions/core/linkRenderMode.js +++ /dev/null @@ -1,25 +0,0 @@ -import { app } from "../../scripts/app.js"; - -const id = "Comfy.LinkRenderMode"; -const ext = { - name: id, - async setup(app) { - app.ui.settings.addSetting({ - id, - name: "Link Render Mode", - defaultValue: 2, - type: "combo", - options: [...LiteGraph.LINK_RENDER_MODES, "Hidden"].map((m, i) => ({ - value: i, - text: m, - selected: i == app.canvas.links_render_mode, - })), - onChange(value) { - app.canvas.links_render_mode = +value; - app.graph.setDirtyCanvas(true); - }, - }); - }, -}; - -app.registerExtension(ext); diff --git a/web/extensions/core/maskeditor.js b/web/extensions/core/maskeditor.js deleted file mode 100644 index 36f7496e7..000000000 --- a/web/extensions/core/maskeditor.js +++ /dev/null @@ -1,967 +0,0 @@ -import { app } from "../../scripts/app.js"; -import { ComfyDialog, $el } from "../../scripts/ui.js"; -import { ComfyApp } from "../../scripts/app.js"; -import { api } from "../../scripts/api.js" -import { ClipspaceDialog } from "./clipspace.js"; - -// Helper function to convert a data URL to a Blob object -function dataURLToBlob(dataURL) { - const parts = dataURL.split(';base64,'); - const contentType = parts[0].split(':')[1]; - const byteString = atob(parts[1]); - const arrayBuffer = new ArrayBuffer(byteString.length); - const uint8Array = new Uint8Array(arrayBuffer); - for (let i = 0; i < byteString.length; i++) { - uint8Array[i] = byteString.charCodeAt(i); - } - return new Blob([arrayBuffer], { type: contentType }); -} - -function loadedImageToBlob(image) { - const canvas = document.createElement('canvas'); - - canvas.width = image.width; - canvas.height = image.height; - - const ctx = canvas.getContext('2d'); - - ctx.drawImage(image, 0, 0); - - const dataURL = canvas.toDataURL('image/png', 1); - const blob = dataURLToBlob(dataURL); - - return blob; -} - -function loadImage(imagePath) { - return new Promise((resolve, reject) => { - const image = new Image(); - - image.onload = function() { - resolve(image); - }; - - image.src = imagePath; - }); -} - -async function uploadMask(filepath, formData) { - await api.fetchApi('/upload/mask', { - method: 'POST', - body: formData - }).then(response => {}).catch(error => { - console.error('Error:', error); - }); - - ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']] = new Image(); - ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src = api.apiURL("/view?" + new URLSearchParams(filepath).toString() + app.getPreviewFormatParam() + app.getRandParam()); - - if(ComfyApp.clipspace.images) - ComfyApp.clipspace.images[ComfyApp.clipspace['selectedIndex']] = filepath; - - ClipspaceDialog.invalidatePreview(); -} - -function prepare_mask(image, maskCanvas, maskCtx, maskColor) { - // paste mask data into alpha channel - maskCtx.drawImage(image, 0, 0, maskCanvas.width, maskCanvas.height); - const maskData = maskCtx.getImageData(0, 0, maskCanvas.width, maskCanvas.height); - - // invert mask - for (let i = 0; i < maskData.data.length; i += 4) { - if(maskData.data[i+3] == 255) - maskData.data[i+3] = 0; - else - maskData.data[i+3] = 255; - - maskData.data[i] = maskColor.r; - maskData.data[i+1] = maskColor.g; - maskData.data[i+2] = maskColor.b; - } - - maskCtx.globalCompositeOperation = 'source-over'; - maskCtx.putImageData(maskData, 0, 0); -} - -class MaskEditorDialog extends ComfyDialog { - static instance = null; - - static getInstance() { - if(!MaskEditorDialog.instance) { - MaskEditorDialog.instance = new MaskEditorDialog(app); - } - - return MaskEditorDialog.instance; - } - - is_layout_created = false; - - constructor() { - super(); - this.element = $el("div.comfy-modal", { parent: document.body }, - [ $el("div.comfy-modal-content", - [...this.createButtons()]), - ]); - } - - createButtons() { - return []; - } - - createButton(name, callback) { - var button = document.createElement("button"); - button.style.pointerEvents = "auto"; - button.innerText = name; - button.addEventListener("click", callback); - return button; - } - - createLeftButton(name, callback) { - var button = this.createButton(name, callback); - button.style.cssFloat = "left"; - button.style.marginRight = "4px"; - return button; - } - - createRightButton(name, callback) { - var button = this.createButton(name, callback); - button.style.cssFloat = "right"; - button.style.marginLeft = "4px"; - return button; - } - - createLeftSlider(self, name, callback) { - const divElement = document.createElement('div'); - divElement.id = "maskeditor-slider"; - divElement.style.cssFloat = "left"; - divElement.style.fontFamily = "sans-serif"; - divElement.style.marginRight = "4px"; - divElement.style.color = "var(--input-text)"; - divElement.style.backgroundColor = "var(--comfy-input-bg)"; - divElement.style.borderRadius = "8px"; - divElement.style.borderColor = "var(--border-color)"; - divElement.style.borderStyle = "solid"; - divElement.style.fontSize = "15px"; - divElement.style.height = "21px"; - divElement.style.padding = "1px 6px"; - divElement.style.display = "flex"; - divElement.style.position = "relative"; - divElement.style.top = "2px"; - divElement.style.pointerEvents = "auto"; - self.brush_slider_input = document.createElement('input'); - self.brush_slider_input.setAttribute('type', 'range'); - self.brush_slider_input.setAttribute('min', '1'); - self.brush_slider_input.setAttribute('max', '100'); - self.brush_slider_input.setAttribute('value', '10'); - const labelElement = document.createElement("label"); - labelElement.textContent = name; - - divElement.appendChild(labelElement); - divElement.appendChild(self.brush_slider_input); - - self.brush_slider_input.addEventListener("change", callback); - - return divElement; - } - - createOpacitySlider(self, name, callback) { - const divElement = document.createElement('div'); - divElement.id = "maskeditor-opacity-slider"; - divElement.style.cssFloat = "left"; - divElement.style.fontFamily = "sans-serif"; - divElement.style.marginRight = "4px"; - divElement.style.color = "var(--input-text)"; - divElement.style.backgroundColor = "var(--comfy-input-bg)"; - divElement.style.borderRadius = "8px"; - divElement.style.borderColor = "var(--border-color)"; - divElement.style.borderStyle = "solid"; - divElement.style.fontSize = "15px"; - divElement.style.height = "21px"; - divElement.style.padding = "1px 6px"; - divElement.style.display = "flex"; - divElement.style.position = "relative"; - divElement.style.top = "2px"; - divElement.style.pointerEvents = "auto"; - self.opacity_slider_input = document.createElement('input'); - self.opacity_slider_input.setAttribute('type', 'range'); - self.opacity_slider_input.setAttribute('min', '0.1'); - self.opacity_slider_input.setAttribute('max', '1.0'); - self.opacity_slider_input.setAttribute('step', '0.01') - self.opacity_slider_input.setAttribute('value', '0.7'); - const labelElement = document.createElement("label"); - labelElement.textContent = name; - - divElement.appendChild(labelElement); - divElement.appendChild(self.opacity_slider_input); - - self.opacity_slider_input.addEventListener("input", callback); - - return divElement; - } - - setlayout(imgCanvas, maskCanvas) { - const self = this; - - // If it is specified as relative, using it only as a hidden placeholder for padding is recommended - // to prevent anomalies where it exceeds a certain size and goes outside of the window. - var bottom_panel = document.createElement("div"); - bottom_panel.style.position = "absolute"; - bottom_panel.style.bottom = "0px"; - bottom_panel.style.left = "20px"; - bottom_panel.style.right = "20px"; - bottom_panel.style.height = "50px"; - bottom_panel.style.pointerEvents = "none"; - - var brush = document.createElement("div"); - brush.id = "brush"; - brush.style.backgroundColor = "transparent"; - brush.style.outline = "1px dashed black"; - brush.style.boxShadow = "0 0 0 1px white"; - brush.style.borderRadius = "50%"; - brush.style.MozBorderRadius = "50%"; - brush.style.WebkitBorderRadius = "50%"; - brush.style.position = "absolute"; - brush.style.zIndex = 8889; - brush.style.pointerEvents = "none"; - this.brush = brush; - this.element.appendChild(imgCanvas); - this.element.appendChild(maskCanvas); - this.element.appendChild(bottom_panel); - document.body.appendChild(brush); - - var clearButton = this.createLeftButton("Clear", () => { - self.maskCtx.clearRect(0, 0, self.maskCanvas.width, self.maskCanvas.height); - }); - - this.brush_size_slider = this.createLeftSlider(self, "Thickness", (event) => { - self.brush_size = event.target.value; - self.updateBrushPreview(self, null, null); - }); - - this.brush_opacity_slider = this.createOpacitySlider(self, "Opacity", (event) => { - self.brush_opacity = event.target.value; - if (self.brush_color_mode !== "negative") { - self.maskCanvas.style.opacity = self.brush_opacity; - } - }); - - this.colorButton = this.createLeftButton(this.getColorButtonText(), () => { - if (self.brush_color_mode === "black") { - self.brush_color_mode = "white"; - } - else if (self.brush_color_mode === "white") { - self.brush_color_mode = "negative"; - } - else { - self.brush_color_mode = "black"; - } - - self.updateWhenBrushColorModeChanged(); - }); - - var cancelButton = this.createRightButton("Cancel", () => { - document.removeEventListener("mouseup", MaskEditorDialog.handleMouseUp); - document.removeEventListener("keydown", MaskEditorDialog.handleKeyDown); - self.close(); - }); - - this.saveButton = this.createRightButton("Save", () => { - document.removeEventListener("mouseup", MaskEditorDialog.handleMouseUp); - document.removeEventListener("keydown", MaskEditorDialog.handleKeyDown); - self.save(); - }); - - this.element.appendChild(imgCanvas); - this.element.appendChild(maskCanvas); - this.element.appendChild(bottom_panel); - - bottom_panel.appendChild(clearButton); - bottom_panel.appendChild(this.saveButton); - bottom_panel.appendChild(cancelButton); - bottom_panel.appendChild(this.brush_size_slider); - bottom_panel.appendChild(this.brush_opacity_slider); - bottom_panel.appendChild(this.colorButton); - - imgCanvas.style.position = "absolute"; - maskCanvas.style.position = "absolute"; - - imgCanvas.style.top = "200"; - imgCanvas.style.left = "0"; - - maskCanvas.style.top = imgCanvas.style.top; - maskCanvas.style.left = imgCanvas.style.left; - - const maskCanvasStyle = this.getMaskCanvasStyle(); - maskCanvas.style.mixBlendMode = maskCanvasStyle.mixBlendMode; - maskCanvas.style.opacity = maskCanvasStyle.opacity; - } - - async show() { - this.zoom_ratio = 1.0; - this.pan_x = 0; - this.pan_y = 0; - - if(!this.is_layout_created) { - // layout - const imgCanvas = document.createElement('canvas'); - const maskCanvas = document.createElement('canvas'); - - imgCanvas.id = "imageCanvas"; - maskCanvas.id = "maskCanvas"; - - this.setlayout(imgCanvas, maskCanvas); - - // prepare content - this.imgCanvas = imgCanvas; - this.maskCanvas = maskCanvas; - this.maskCtx = maskCanvas.getContext('2d', {willReadFrequently: true }); - - this.setEventHandler(maskCanvas); - - this.is_layout_created = true; - - // replacement of onClose hook since close is not real close - const self = this; - const observer = new MutationObserver(function(mutations) { - mutations.forEach(function(mutation) { - if (mutation.type === 'attributes' && mutation.attributeName === 'style') { - if(self.last_display_style && self.last_display_style != 'none' && self.element.style.display == 'none') { - document.removeEventListener("mouseup", MaskEditorDialog.handleMouseUp); - self.brush.style.display = "none"; - ComfyApp.onClipspaceEditorClosed(); - } - - self.last_display_style = self.element.style.display; - } - }); - }); - - const config = { attributes: true }; - observer.observe(this.element, config); - } - - // The keydown event needs to be reconfigured when closing the dialog as it gets removed. - document.addEventListener('keydown', MaskEditorDialog.handleKeyDown); - - if(ComfyApp.clipspace_return_node) { - this.saveButton.innerText = "Save to node"; - } - else { - this.saveButton.innerText = "Save"; - } - this.saveButton.disabled = false; - - this.element.style.display = "block"; - this.element.style.width = "85%"; - this.element.style.margin = "0 7.5%"; - this.element.style.height = "100vh"; - this.element.style.top = "50%"; - this.element.style.left = "42%"; - this.element.style.zIndex = 8888; // NOTE: alert dialog must be high priority. - - await this.setImages(this.imgCanvas); - - this.is_visible = true; - } - - isOpened() { - return this.element.style.display == "block"; - } - - invalidateCanvas(orig_image, mask_image) { - this.imgCanvas.width = orig_image.width; - this.imgCanvas.height = orig_image.height; - - this.maskCanvas.width = orig_image.width; - this.maskCanvas.height = orig_image.height; - - let imgCtx = this.imgCanvas.getContext('2d', {willReadFrequently: true }); - let maskCtx = this.maskCanvas.getContext('2d', {willReadFrequently: true }); - - imgCtx.drawImage(orig_image, 0, 0, orig_image.width, orig_image.height); - prepare_mask(mask_image, this.maskCanvas, maskCtx, this.getMaskColor()); - } - - async setImages(imgCanvas) { - let self = this; - - const imgCtx = imgCanvas.getContext('2d', {willReadFrequently: true }); - const maskCtx = this.maskCtx; - const maskCanvas = this.maskCanvas; - - imgCtx.clearRect(0,0,this.imgCanvas.width,this.imgCanvas.height); - maskCtx.clearRect(0,0,this.maskCanvas.width,this.maskCanvas.height); - - // image load - const filepath = ComfyApp.clipspace.images; - - const alpha_url = new URL(ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src) - alpha_url.searchParams.delete('channel'); - alpha_url.searchParams.delete('preview'); - alpha_url.searchParams.set('channel', 'a'); - let mask_image = await loadImage(alpha_url); - - // original image load - const rgb_url = new URL(ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src); - rgb_url.searchParams.delete('channel'); - rgb_url.searchParams.set('channel', 'rgb'); - this.image = new Image(); - this.image.onload = function() { - maskCanvas.width = self.image.width; - maskCanvas.height = self.image.height; - - self.invalidateCanvas(self.image, mask_image); - self.initializeCanvasPanZoom(); - }; - this.image.src = rgb_url; - } - - initializeCanvasPanZoom() { - // set initialize - let drawWidth = this.image.width; - let drawHeight = this.image.height; - - let width = this.element.clientWidth; - let height = this.element.clientHeight; - - if (this.image.width > width) { - drawWidth = width; - drawHeight = (drawWidth / this.image.width) * this.image.height; - } - - if (drawHeight > height) { - drawHeight = height; - drawWidth = (drawHeight / this.image.height) * this.image.width; - } - - this.zoom_ratio = drawWidth/this.image.width; - - const canvasX = (width - drawWidth) / 2; - const canvasY = (height - drawHeight) / 2; - this.pan_x = canvasX; - this.pan_y = canvasY; - - this.invalidatePanZoom(); - } - - - invalidatePanZoom() { - let raw_width = this.image.width * this.zoom_ratio; - let raw_height = this.image.height * this.zoom_ratio; - - if(this.pan_x + raw_width < 10) { - this.pan_x = 10 - raw_width; - } - - if(this.pan_y + raw_height < 10) { - this.pan_y = 10 - raw_height; - } - - let width = `${raw_width}px`; - let height = `${raw_height}px`; - - let left = `${this.pan_x}px`; - let top = `${this.pan_y}px`; - - this.maskCanvas.style.width = width; - this.maskCanvas.style.height = height; - this.maskCanvas.style.left = left; - this.maskCanvas.style.top = top; - - this.imgCanvas.style.width = width; - this.imgCanvas.style.height = height; - this.imgCanvas.style.left = left; - this.imgCanvas.style.top = top; - } - - - setEventHandler(maskCanvas) { - const self = this; - - if(!this.handler_registered) { - maskCanvas.addEventListener("contextmenu", (event) => { - event.preventDefault(); - }); - - this.element.addEventListener('wheel', (event) => this.handleWheelEvent(self,event)); - this.element.addEventListener('pointermove', (event) => this.pointMoveEvent(self,event)); - this.element.addEventListener('touchmove', (event) => this.pointMoveEvent(self,event)); - - this.element.addEventListener('dragstart', (event) => { - if(event.ctrlKey) { - event.preventDefault(); - } - }); - - maskCanvas.addEventListener('pointerdown', (event) => this.handlePointerDown(self,event)); - maskCanvas.addEventListener('pointermove', (event) => this.draw_move(self,event)); - maskCanvas.addEventListener('touchmove', (event) => this.draw_move(self,event)); - maskCanvas.addEventListener('pointerover', (event) => { this.brush.style.display = "block"; }); - maskCanvas.addEventListener('pointerleave', (event) => { this.brush.style.display = "none"; }); - - document.addEventListener('pointerup', MaskEditorDialog.handlePointerUp); - - this.handler_registered = true; - } - } - - getMaskCanvasStyle() { - if (this.brush_color_mode === "negative") { - return { - mixBlendMode: "difference", - opacity: "1", - }; - } - else { - return { - mixBlendMode: "initial", - opacity: this.brush_opacity, - }; - } - } - - getMaskColor() { - if (this.brush_color_mode === "black") { - return { r: 0, g: 0, b: 0 }; - } - if (this.brush_color_mode === "white") { - return { r: 255, g: 255, b: 255 }; - } - if (this.brush_color_mode === "negative") { - // negative effect only works with white color - return { r: 255, g: 255, b: 255 }; - } - - return { r: 0, g: 0, b: 0 }; - } - - getMaskFillStyle() { - const maskColor = this.getMaskColor(); - - return "rgb(" + maskColor.r + "," + maskColor.g + "," + maskColor.b + ")"; - } - - getColorButtonText() { - let colorCaption = "unknown"; - - if (this.brush_color_mode === "black") { - colorCaption = "black"; - } - else if (this.brush_color_mode === "white") { - colorCaption = "white"; - } - else if (this.brush_color_mode === "negative") { - colorCaption = "negative"; - } - - return "Color: " + colorCaption; - } - - updateWhenBrushColorModeChanged() { - this.colorButton.innerText = this.getColorButtonText(); - - // update mask canvas css styles - - const maskCanvasStyle = this.getMaskCanvasStyle(); - this.maskCanvas.style.mixBlendMode = maskCanvasStyle.mixBlendMode; - this.maskCanvas.style.opacity = maskCanvasStyle.opacity; - - // update mask canvas rgb colors - - const maskColor = this.getMaskColor(); - - const maskData = this.maskCtx.getImageData(0, 0, this.maskCanvas.width, this.maskCanvas.height); - - for (let i = 0; i < maskData.data.length; i += 4) { - maskData.data[i] = maskColor.r; - maskData.data[i+1] = maskColor.g; - maskData.data[i+2] = maskColor.b; - } - - this.maskCtx.putImageData(maskData, 0, 0); - } - - brush_opacity = 0.7; - brush_size = 10; - brush_color_mode = "black"; - drawing_mode = false; - lastx = -1; - lasty = -1; - lasttime = 0; - - static handleKeyDown(event) { - const self = MaskEditorDialog.instance; - if (event.key === ']') { - self.brush_size = Math.min(self.brush_size+2, 100); - self.brush_slider_input.value = self.brush_size; - } else if (event.key === '[') { - self.brush_size = Math.max(self.brush_size-2, 1); - self.brush_slider_input.value = self.brush_size; - } else if(event.key === 'Enter') { - self.save(); - } - - self.updateBrushPreview(self); - } - - static handlePointerUp(event) { - event.preventDefault(); - - this.mousedown_x = null; - this.mousedown_y = null; - - MaskEditorDialog.instance.drawing_mode = false; - } - - updateBrushPreview(self) { - const brush = self.brush; - - var centerX = self.cursorX; - var centerY = self.cursorY; - - brush.style.width = self.brush_size * 2 * this.zoom_ratio + "px"; - brush.style.height = self.brush_size * 2 * this.zoom_ratio + "px"; - brush.style.left = (centerX - self.brush_size * this.zoom_ratio) + "px"; - brush.style.top = (centerY - self.brush_size * this.zoom_ratio) + "px"; - } - - handleWheelEvent(self, event) { - event.preventDefault(); - - if(event.ctrlKey) { - // zoom canvas - if(event.deltaY < 0) { - this.zoom_ratio = Math.min(10.0, this.zoom_ratio+0.2); - } - else { - this.zoom_ratio = Math.max(0.2, this.zoom_ratio-0.2); - } - - this.invalidatePanZoom(); - } - else { - // adjust brush size - if(event.deltaY < 0) - this.brush_size = Math.min(this.brush_size+2, 100); - else - this.brush_size = Math.max(this.brush_size-2, 1); - - this.brush_slider_input.value = this.brush_size; - - this.updateBrushPreview(this); - } - } - - pointMoveEvent(self, event) { - this.cursorX = event.pageX; - this.cursorY = event.pageY; - - self.updateBrushPreview(self); - - if(event.ctrlKey) { - event.preventDefault(); - self.pan_move(self, event); - } - - let left_button_down = window.TouchEvent && event instanceof TouchEvent || event.buttons == 1; - - if(event.shiftKey && left_button_down) { - self.drawing_mode = false; - - const y = event.clientY; - let delta = (self.zoom_lasty - y)*0.005; - self.zoom_ratio = Math.max(Math.min(10.0, self.last_zoom_ratio - delta), 0.2); - - this.invalidatePanZoom(); - return; - } - } - - pan_move(self, event) { - if(event.buttons == 1) { - if(this.mousedown_x) { - let deltaX = this.mousedown_x - event.clientX; - let deltaY = this.mousedown_y - event.clientY; - - self.pan_x = this.mousedown_pan_x - deltaX; - self.pan_y = this.mousedown_pan_y - deltaY; - - self.invalidatePanZoom(); - } - } - } - - draw_move(self, event) { - if(event.ctrlKey || event.shiftKey) { - return; - } - - event.preventDefault(); - - this.cursorX = event.pageX; - this.cursorY = event.pageY; - - self.updateBrushPreview(self); - - let left_button_down = window.TouchEvent && event instanceof TouchEvent || event.buttons == 1; - let right_button_down = [2, 5, 32].includes(event.buttons); - - if (!event.altKey && left_button_down) { - var diff = performance.now() - self.lasttime; - - const maskRect = self.maskCanvas.getBoundingClientRect(); - - var x = event.offsetX; - var y = event.offsetY - - if(event.offsetX == null) { - x = event.targetTouches[0].clientX - maskRect.left; - } - - if(event.offsetY == null) { - y = event.targetTouches[0].clientY - maskRect.top; - } - - x /= self.zoom_ratio; - y /= self.zoom_ratio; - - var brush_size = this.brush_size; - if(event instanceof PointerEvent && event.pointerType == 'pen') { - brush_size *= event.pressure; - this.last_pressure = event.pressure; - } - else if(window.TouchEvent && event instanceof TouchEvent && diff < 20){ - // The firing interval of PointerEvents in Pen is unreliable, so it is supplemented by TouchEvents. - brush_size *= this.last_pressure; - } - else { - brush_size = this.brush_size; - } - - if(diff > 20 && !this.drawing_mode) - requestAnimationFrame(() => { - self.maskCtx.beginPath(); - self.maskCtx.fillStyle = this.getMaskFillStyle(); - self.maskCtx.globalCompositeOperation = "source-over"; - self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false); - self.maskCtx.fill(); - self.lastx = x; - self.lasty = y; - }); - else - requestAnimationFrame(() => { - self.maskCtx.beginPath(); - self.maskCtx.fillStyle = this.getMaskFillStyle(); - self.maskCtx.globalCompositeOperation = "source-over"; - - var dx = x - self.lastx; - var dy = y - self.lasty; - - var distance = Math.sqrt(dx * dx + dy * dy); - var directionX = dx / distance; - var directionY = dy / distance; - - for (var i = 0; i < distance; i+=5) { - var px = self.lastx + (directionX * i); - var py = self.lasty + (directionY * i); - self.maskCtx.arc(px, py, brush_size, 0, Math.PI * 2, false); - self.maskCtx.fill(); - } - self.lastx = x; - self.lasty = y; - }); - - self.lasttime = performance.now(); - } - else if((event.altKey && left_button_down) || right_button_down) { - const maskRect = self.maskCanvas.getBoundingClientRect(); - const x = (event.offsetX || event.targetTouches[0].clientX - maskRect.left) / self.zoom_ratio; - const y = (event.offsetY || event.targetTouches[0].clientY - maskRect.top) / self.zoom_ratio; - - var brush_size = this.brush_size; - if(event instanceof PointerEvent && event.pointerType == 'pen') { - brush_size *= event.pressure; - this.last_pressure = event.pressure; - } - else if(window.TouchEvent && event instanceof TouchEvent && diff < 20){ - brush_size *= this.last_pressure; - } - else { - brush_size = this.brush_size; - } - - if(diff > 20 && !drawing_mode) // cannot tracking drawing_mode for touch event - requestAnimationFrame(() => { - self.maskCtx.beginPath(); - self.maskCtx.globalCompositeOperation = "destination-out"; - self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false); - self.maskCtx.fill(); - self.lastx = x; - self.lasty = y; - }); - else - requestAnimationFrame(() => { - self.maskCtx.beginPath(); - self.maskCtx.globalCompositeOperation = "destination-out"; - - var dx = x - self.lastx; - var dy = y - self.lasty; - - var distance = Math.sqrt(dx * dx + dy * dy); - var directionX = dx / distance; - var directionY = dy / distance; - - for (var i = 0; i < distance; i+=5) { - var px = self.lastx + (directionX * i); - var py = self.lasty + (directionY * i); - self.maskCtx.arc(px, py, brush_size, 0, Math.PI * 2, false); - self.maskCtx.fill(); - } - self.lastx = x; - self.lasty = y; - }); - - self.lasttime = performance.now(); - } - } - - handlePointerDown(self, event) { - if(event.ctrlKey) { - if (event.buttons == 1) { - this.mousedown_x = event.clientX; - this.mousedown_y = event.clientY; - - this.mousedown_pan_x = this.pan_x; - this.mousedown_pan_y = this.pan_y; - } - return; - } - - var brush_size = this.brush_size; - if(event instanceof PointerEvent && event.pointerType == 'pen') { - brush_size *= event.pressure; - this.last_pressure = event.pressure; - } - - if ([0, 2, 5].includes(event.button)) { - self.drawing_mode = true; - - event.preventDefault(); - - if(event.shiftKey) { - self.zoom_lasty = event.clientY; - self.last_zoom_ratio = self.zoom_ratio; - return; - } - - const maskRect = self.maskCanvas.getBoundingClientRect(); - const x = (event.offsetX || event.targetTouches[0].clientX - maskRect.left) / self.zoom_ratio; - const y = (event.offsetY || event.targetTouches[0].clientY - maskRect.top) / self.zoom_ratio; - - self.maskCtx.beginPath(); - if (!event.altKey && event.button == 0) { - self.maskCtx.fillStyle = this.getMaskFillStyle(); - self.maskCtx.globalCompositeOperation = "source-over"; - } else { - self.maskCtx.globalCompositeOperation = "destination-out"; - } - self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false); - self.maskCtx.fill(); - self.lastx = x; - self.lasty = y; - self.lasttime = performance.now(); - } - } - - async save() { - const backupCanvas = document.createElement('canvas'); - const backupCtx = backupCanvas.getContext('2d', {willReadFrequently:true}); - backupCanvas.width = this.image.width; - backupCanvas.height = this.image.height; - - backupCtx.clearRect(0,0, backupCanvas.width, backupCanvas.height); - backupCtx.drawImage(this.maskCanvas, - 0, 0, this.maskCanvas.width, this.maskCanvas.height, - 0, 0, backupCanvas.width, backupCanvas.height); - - // paste mask data into alpha channel - const backupData = backupCtx.getImageData(0, 0, backupCanvas.width, backupCanvas.height); - - // refine mask image - for (let i = 0; i < backupData.data.length; i += 4) { - if(backupData.data[i+3] == 255) - backupData.data[i+3] = 0; - else - backupData.data[i+3] = 255; - - backupData.data[i] = 0; - backupData.data[i+1] = 0; - backupData.data[i+2] = 0; - } - - backupCtx.globalCompositeOperation = 'source-over'; - backupCtx.putImageData(backupData, 0, 0); - - const formData = new FormData(); - const filename = "clipspace-mask-" + performance.now() + ".png"; - - const item = - { - "filename": filename, - "subfolder": "clipspace", - "type": "input", - }; - - if(ComfyApp.clipspace.images) - ComfyApp.clipspace.images[0] = item; - - if(ComfyApp.clipspace.widgets) { - const index = ComfyApp.clipspace.widgets.findIndex(obj => obj.name === 'image'); - - if(index >= 0) - ComfyApp.clipspace.widgets[index].value = item; - } - - const dataURL = backupCanvas.toDataURL(); - const blob = dataURLToBlob(dataURL); - - let original_url = new URL(this.image.src); - - const original_ref = { filename: original_url.searchParams.get('filename') }; - - let original_subfolder = original_url.searchParams.get("subfolder"); - if(original_subfolder) - original_ref.subfolder = original_subfolder; - - let original_type = original_url.searchParams.get("type"); - if(original_type) - original_ref.type = original_type; - - formData.append('image', blob, filename); - formData.append('original_ref', JSON.stringify(original_ref)); - formData.append('type', "input"); - formData.append('subfolder', "clipspace"); - - this.saveButton.innerText = "Saving..."; - this.saveButton.disabled = true; - await uploadMask(item, formData); - ComfyApp.onClipspaceEditorSave(); - this.close(); - } -} - -app.registerExtension({ - name: "Comfy.MaskEditor", - init(app) { - ComfyApp.open_maskeditor = - function () { - const dlg = MaskEditorDialog.getInstance(); - if(!dlg.isOpened()) { - dlg.show(); - } - }; - - const context_predicate = () => ComfyApp.clipspace && ComfyApp.clipspace.imgs && ComfyApp.clipspace.imgs.length > 0 - ClipspaceDialog.registerButton("MaskEditor", context_predicate, ComfyApp.open_maskeditor); - } -}); diff --git a/web/extensions/core/nodeTemplates.js b/web/extensions/core/nodeTemplates.js deleted file mode 100644 index 9350ba654..000000000 --- a/web/extensions/core/nodeTemplates.js +++ /dev/null @@ -1,412 +0,0 @@ -import { app } from "../../scripts/app.js"; -import { api } from "../../scripts/api.js"; -import { ComfyDialog, $el } from "../../scripts/ui.js"; -import { GroupNodeConfig, GroupNodeHandler } from "./groupNode.js"; - -// Adds the ability to save and add multiple nodes as a template -// To save: -// Select multiple nodes (ctrl + drag to select a region or ctrl+click individual nodes) -// Right click the canvas -// Save Node Template -> give it a name -// -// To add: -// Right click the canvas -// Node templates -> click the one to add -// -// To delete/rename: -// Right click the canvas -// Node templates -> Manage -// -// To rearrange: -// Open the manage dialog and Drag and drop elements using the "Name:" label as handle - -const id = "Comfy.NodeTemplates"; -const file = "comfy.templates.json"; - -class ManageTemplates extends ComfyDialog { - constructor() { - super(); - this.load().then((v) => { - this.templates = v; - }); - - this.element.classList.add("comfy-manage-templates"); - this.draggedEl = null; - this.saveVisualCue = null; - this.emptyImg = new Image(); - this.emptyImg.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="; - - this.importInput = $el("input", { - type: "file", - accept: ".json", - multiple: true, - style: { display: "none" }, - parent: document.body, - onchange: () => this.importAll(), - }); - } - - createButtons() { - const btns = super.createButtons(); - btns[0].textContent = "Close"; - btns[0].onclick = (e) => { - clearTimeout(this.saveVisualCue); - this.close(); - }; - btns.unshift( - $el("button", { - type: "button", - textContent: "Export", - onclick: () => this.exportAll(), - }) - ); - btns.unshift( - $el("button", { - type: "button", - textContent: "Import", - onclick: () => { - this.importInput.click(); - }, - }) - ); - return btns; - } - - async load() { - let templates = []; - if (app.storageLocation === "server") { - if (app.isNewUserSession) { - // New user so migrate existing templates - const json = localStorage.getItem(id); - if (json) { - templates = JSON.parse(json); - } - await api.storeUserData(file, json, { stringify: false }); - } else { - const res = await api.getUserData(file); - if (res.status === 200) { - try { - templates = await res.json(); - } catch (error) { - } - } else if (res.status !== 404) { - console.error(res.status + " " + res.statusText); - } - } - } else { - const json = localStorage.getItem(id); - if (json) { - templates = JSON.parse(json); - } - } - - return templates ?? []; - } - - async store() { - if(app.storageLocation === "server") { - const templates = JSON.stringify(this.templates, undefined, 4); - localStorage.setItem(id, templates); // Backwards compatibility - try { - await api.storeUserData(file, templates, { stringify: false }); - } catch (error) { - console.error(error); - alert(error.message); - } - } else { - localStorage.setItem(id, JSON.stringify(this.templates)); - } - } - - async importAll() { - for (const file of this.importInput.files) { - if (file.type === "application/json" || file.name.endsWith(".json")) { - const reader = new FileReader(); - reader.onload = async () => { - const importFile = JSON.parse(reader.result); - if (importFile?.templates) { - for (const template of importFile.templates) { - if (template?.name && template?.data) { - this.templates.push(template); - } - } - await this.store(); - } - }; - await reader.readAsText(file); - } - } - - this.importInput.value = null; - - this.close(); - } - - exportAll() { - if (this.templates.length == 0) { - alert("No templates to export."); - return; - } - - const json = JSON.stringify({ templates: this.templates }, null, 2); // convert the data to a JSON string - const blob = new Blob([json], { type: "application/json" }); - const url = URL.createObjectURL(blob); - const a = $el("a", { - href: url, - download: "node_templates.json", - style: { display: "none" }, - parent: document.body, - }); - a.click(); - setTimeout(function () { - a.remove(); - window.URL.revokeObjectURL(url); - }, 0); - } - - show() { - // Show list of template names + delete button - super.show( - $el( - "div", - {}, - this.templates.flatMap((t,i) => { - let nameInput; - return [ - $el( - "div", - { - dataset: { id: i }, - className: "tempateManagerRow", - style: { - display: "grid", - gridTemplateColumns: "1fr auto", - border: "1px dashed transparent", - gap: "5px", - backgroundColor: "var(--comfy-menu-bg)" - }, - ondragstart: (e) => { - this.draggedEl = e.currentTarget; - e.currentTarget.style.opacity = "0.6"; - e.currentTarget.style.border = "1px dashed yellow"; - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setDragImage(this.emptyImg, 0, 0); - }, - ondragend: (e) => { - e.target.style.opacity = "1"; - e.currentTarget.style.border = "1px dashed transparent"; - e.currentTarget.removeAttribute("draggable"); - - // rearrange the elements - this.element.querySelectorAll('.tempateManagerRow').forEach((el,i) => { - var prev_i = el.dataset.id; - - if ( el == this.draggedEl && prev_i != i ) { - this.templates.splice(i, 0, this.templates.splice(prev_i, 1)[0]); - } - el.dataset.id = i; - }); - this.store(); - }, - ondragover: (e) => { - e.preventDefault(); - if ( e.currentTarget == this.draggedEl ) - return; - - let rect = e.currentTarget.getBoundingClientRect(); - if (e.clientY > rect.top + rect.height / 2) { - e.currentTarget.parentNode.insertBefore(this.draggedEl, e.currentTarget.nextSibling); - } else { - e.currentTarget.parentNode.insertBefore(this.draggedEl, e.currentTarget); - } - } - }, - [ - $el( - "label", - { - textContent: "Name: ", - style: { - cursor: "grab", - }, - onmousedown: (e) => { - // enable dragging only from the label - if (e.target.localName == 'label') - e.currentTarget.parentNode.draggable = 'true'; - } - }, - [ - $el("input", { - value: t.name, - dataset: { name: t.name }, - style: { - transitionProperty: 'background-color', - transitionDuration: '0s', - }, - onchange: (e) => { - clearTimeout(this.saveVisualCue); - var el = e.target; - var row = el.parentNode.parentNode; - this.templates[row.dataset.id].name = el.value.trim() || 'untitled'; - this.store(); - el.style.backgroundColor = 'rgb(40, 95, 40)'; - el.style.transitionDuration = '0s'; - this.saveVisualCue = setTimeout(function () { - el.style.transitionDuration = '.7s'; - el.style.backgroundColor = 'var(--comfy-input-bg)'; - }, 15); - }, - onkeypress: (e) => { - var el = e.target; - clearTimeout(this.saveVisualCue); - el.style.transitionDuration = '0s'; - el.style.backgroundColor = 'var(--comfy-input-bg)'; - }, - $: (el) => (nameInput = el), - }) - ] - ), - $el( - "div", - {}, - [ - $el("button", { - textContent: "Export", - style: { - fontSize: "12px", - fontWeight: "normal", - }, - onclick: (e) => { - const json = JSON.stringify({templates: [t]}, null, 2); // convert the data to a JSON string - const blob = new Blob([json], {type: "application/json"}); - const url = URL.createObjectURL(blob); - const a = $el("a", { - href: url, - download: (nameInput.value || t.name) + ".json", - style: {display: "none"}, - parent: document.body, - }); - a.click(); - setTimeout(function () { - a.remove(); - window.URL.revokeObjectURL(url); - }, 0); - }, - }), - $el("button", { - textContent: "Delete", - style: { - fontSize: "12px", - color: "red", - fontWeight: "normal", - }, - onclick: (e) => { - const item = e.target.parentNode.parentNode; - item.parentNode.removeChild(item); - this.templates.splice(item.dataset.id*1, 1); - this.store(); - // update the rows index, setTimeout ensures that the list is updated - var that = this; - setTimeout(function (){ - that.element.querySelectorAll('.tempateManagerRow').forEach((el,i) => { - el.dataset.id = i; - }); - }, 0); - }, - }), - ] - ), - ] - ) - ]; - }) - ) - ); - } -} - -app.registerExtension({ - name: id, - setup() { - const manage = new ManageTemplates(); - - const clipboardAction = async (cb) => { - // We use the clipboard functions but dont want to overwrite the current user clipboard - // Restore it after we've run our callback - const old = localStorage.getItem("litegrapheditor_clipboard"); - await cb(); - localStorage.setItem("litegrapheditor_clipboard", old); - }; - - const orig = LGraphCanvas.prototype.getCanvasMenuOptions; - LGraphCanvas.prototype.getCanvasMenuOptions = function () { - const options = orig.apply(this, arguments); - - options.push(null); - options.push({ - content: `Save Selected as Template`, - disabled: !Object.keys(app.canvas.selected_nodes || {}).length, - callback: () => { - const name = prompt("Enter name"); - if (!name?.trim()) return; - - clipboardAction(() => { - app.canvas.copyToClipboard(); - let data = localStorage.getItem("litegrapheditor_clipboard"); - data = JSON.parse(data); - const nodeIds = Object.keys(app.canvas.selected_nodes); - for (let i = 0; i < nodeIds.length; i++) { - const node = app.graph.getNodeById(nodeIds[i]); - const nodeData = node?.constructor.nodeData; - - let groupData = GroupNodeHandler.getGroupData(node); - if (groupData) { - groupData = groupData.nodeData; - if (!data.groupNodes) { - data.groupNodes = {}; - } - data.groupNodes[nodeData.name] = groupData; - data.nodes[i].type = nodeData.name; - } - } - - manage.templates.push({ - name, - data: JSON.stringify(data), - }); - manage.store(); - }); - }, - }); - - // Map each template to a menu item - const subItems = manage.templates.map((t) => { - return { - content: t.name, - callback: () => { - clipboardAction(async () => { - const data = JSON.parse(t.data); - await GroupNodeConfig.registerFromWorkflow(data.groupNodes, {}); - localStorage.setItem("litegrapheditor_clipboard", t.data); - app.canvas.pasteFromClipboard(); - }); - }, - }; - }); - - subItems.push(null, { - content: "Manage", - callback: () => manage.show(), - }); - - options.push({ - content: "Node Templates", - submenu: { - options: subItems, - }, - }); - - return options; - }; - }, -}); diff --git a/web/extensions/core/noteNode.js b/web/extensions/core/noteNode.js deleted file mode 100644 index 8d89054e9..000000000 --- a/web/extensions/core/noteNode.js +++ /dev/null @@ -1,41 +0,0 @@ -import {app} from "../../scripts/app.js"; -import {ComfyWidgets} from "../../scripts/widgets.js"; -// Node that add notes to your project - -app.registerExtension({ - name: "Comfy.NoteNode", - registerCustomNodes() { - class NoteNode { - color=LGraphCanvas.node_colors.yellow.color; - bgcolor=LGraphCanvas.node_colors.yellow.bgcolor; - groupcolor = LGraphCanvas.node_colors.yellow.groupcolor; - constructor() { - if (!this.properties) { - this.properties = {}; - this.properties.text=""; - } - - ComfyWidgets.STRING(this, "", ["", {default:this.properties.text, multiline: true}], app) - - this.serialize_widgets = true; - this.isVirtualNode = true; - - } - - - } - - // Load default visibility - - LiteGraph.registerNodeType( - "Note", - Object.assign(NoteNode, { - title_mode: LiteGraph.NORMAL_TITLE, - title: "Note", - collapsable: true, - }) - ); - - NoteNode.category = "utils"; - }, -}); diff --git a/web/extensions/core/rerouteNode.js b/web/extensions/core/rerouteNode.js deleted file mode 100644 index 4feff91e5..000000000 --- a/web/extensions/core/rerouteNode.js +++ /dev/null @@ -1,274 +0,0 @@ -import { app } from "../../scripts/app.js"; -import { mergeIfValid, getWidgetConfig, setWidgetConfig } from "./widgetInputs.js"; - -// Node that allows you to redirect connections for cleaner graphs - -app.registerExtension({ - name: "Comfy.RerouteNode", - registerCustomNodes(app) { - class RerouteNode { - constructor() { - if (!this.properties) { - this.properties = {}; - } - this.properties.showOutputText = RerouteNode.defaultVisibility; - this.properties.horizontal = false; - - this.addInput("", "*"); - this.addOutput(this.properties.showOutputText ? "*" : "", "*"); - - this.onAfterGraphConfigured = function () { - requestAnimationFrame(() => { - this.onConnectionsChange(LiteGraph.INPUT, null, true, null); - }); - }; - - this.onConnectionsChange = function (type, index, connected, link_info) { - this.applyOrientation(); - - // Prevent multiple connections to different types when we have no input - if (connected && type === LiteGraph.OUTPUT) { - // Ignore wildcard nodes as these will be updated to real types - const types = new Set(this.outputs[0].links.map((l) => app.graph.links[l].type).filter((t) => t !== "*")); - if (types.size > 1) { - const linksToDisconnect = []; - for (let i = 0; i < this.outputs[0].links.length - 1; i++) { - const linkId = this.outputs[0].links[i]; - const link = app.graph.links[linkId]; - linksToDisconnect.push(link); - } - for (const link of linksToDisconnect) { - const node = app.graph.getNodeById(link.target_id); - node.disconnectInput(link.target_slot); - } - } - } - - // Find root input - let currentNode = this; - let updateNodes = []; - let inputType = null; - let inputNode = null; - while (currentNode) { - updateNodes.unshift(currentNode); - const linkId = currentNode.inputs[0].link; - if (linkId !== null) { - const link = app.graph.links[linkId]; - if (!link) return; - const node = app.graph.getNodeById(link.origin_id); - const type = node.constructor.type; - if (type === "Reroute") { - if (node === this) { - // We've found a circle - currentNode.disconnectInput(link.target_slot); - currentNode = null; - } else { - // Move the previous node - currentNode = node; - } - } else { - // We've found the end - inputNode = currentNode; - inputType = node.outputs[link.origin_slot]?.type ?? null; - break; - } - } else { - // This path has no input node - currentNode = null; - break; - } - } - - // Find all outputs - const nodes = [this]; - let outputType = null; - while (nodes.length) { - currentNode = nodes.pop(); - const outputs = (currentNode.outputs ? currentNode.outputs[0].links : []) || []; - if (outputs.length) { - for (const linkId of outputs) { - const link = app.graph.links[linkId]; - - // When disconnecting sometimes the link is still registered - if (!link) continue; - - const node = app.graph.getNodeById(link.target_id); - const type = node.constructor.type; - - if (type === "Reroute") { - // Follow reroute nodes - nodes.push(node); - updateNodes.push(node); - } else { - // We've found an output - const nodeOutType = - node.inputs && node.inputs[link?.target_slot] && node.inputs[link.target_slot].type - ? node.inputs[link.target_slot].type - : null; - if (inputType && inputType !== "*" && nodeOutType !== inputType) { - // The output doesnt match our input so disconnect it - node.disconnectInput(link.target_slot); - } else { - outputType = nodeOutType; - } - } - } - } else { - // No more outputs for this path - } - } - - const displayType = inputType || outputType || "*"; - const color = LGraphCanvas.link_type_colors[displayType]; - - let widgetConfig; - let targetWidget; - let widgetType; - // Update the types of each node - for (const node of updateNodes) { - // If we dont have an input type we are always wildcard but we'll show the output type - // This lets you change the output link to a different type and all nodes will update - node.outputs[0].type = inputType || "*"; - node.__outputType = displayType; - node.outputs[0].name = node.properties.showOutputText ? displayType : ""; - node.size = node.computeSize(); - node.applyOrientation(); - - for (const l of node.outputs[0].links || []) { - const link = app.graph.links[l]; - if (link) { - link.color = color; - - if (app.configuringGraph) continue; - const targetNode = app.graph.getNodeById(link.target_id); - const targetInput = targetNode.inputs?.[link.target_slot]; - if (targetInput?.widget) { - const config = getWidgetConfig(targetInput); - if (!widgetConfig) { - widgetConfig = config[1] ?? {}; - widgetType = config[0]; - } - if (!targetWidget) { - targetWidget = targetNode.widgets?.find((w) => w.name === targetInput.widget.name); - } - - const merged = mergeIfValid(targetInput, [config[0], widgetConfig]); - if (merged.customConfig) { - widgetConfig = merged.customConfig; - } - } - } - } - } - - for (const node of updateNodes) { - if (widgetConfig && outputType) { - node.inputs[0].widget = { name: "value" }; - setWidgetConfig(node.inputs[0], [widgetType ?? displayType, widgetConfig], targetWidget); - } else { - setWidgetConfig(node.inputs[0], null); - } - } - - if (inputNode) { - const link = app.graph.links[inputNode.inputs[0].link]; - if (link) { - link.color = color; - } - } - }; - - this.clone = function () { - const cloned = RerouteNode.prototype.clone.apply(this); - cloned.removeOutput(0); - cloned.addOutput(this.properties.showOutputText ? "*" : "", "*"); - cloned.size = cloned.computeSize(); - return cloned; - }; - - // This node is purely frontend and does not impact the resulting prompt so should not be serialized - this.isVirtualNode = true; - } - - getExtraMenuOptions(_, options) { - options.unshift( - { - content: (this.properties.showOutputText ? "Hide" : "Show") + " Type", - callback: () => { - this.properties.showOutputText = !this.properties.showOutputText; - if (this.properties.showOutputText) { - this.outputs[0].name = this.__outputType || this.outputs[0].type; - } else { - this.outputs[0].name = ""; - } - this.size = this.computeSize(); - this.applyOrientation(); - app.graph.setDirtyCanvas(true, true); - }, - }, - { - content: (RerouteNode.defaultVisibility ? "Hide" : "Show") + " Type By Default", - callback: () => { - RerouteNode.setDefaultTextVisibility(!RerouteNode.defaultVisibility); - }, - }, - { - // naming is inverted with respect to LiteGraphNode.horizontal - // LiteGraphNode.horizontal == true means that - // each slot in the inputs and outputs are layed out horizontally, - // which is the opposite of the visual orientation of the inputs and outputs as a node - content: "Set " + (this.properties.horizontal ? "Horizontal" : "Vertical"), - callback: () => { - this.properties.horizontal = !this.properties.horizontal; - this.applyOrientation(); - }, - } - ); - } - applyOrientation() { - this.horizontal = this.properties.horizontal; - if (this.horizontal) { - // we correct the input position, because LiteGraphNode.horizontal - // doesn't account for title presence - // which reroute nodes don't have - this.inputs[0].pos = [this.size[0] / 2, 0]; - } else { - delete this.inputs[0].pos; - } - app.graph.setDirtyCanvas(true, true); - } - - computeSize() { - return [ - this.properties.showOutputText && this.outputs && this.outputs.length - ? Math.max(75, LiteGraph.NODE_TEXT_SIZE * this.outputs[0].name.length * 0.6 + 40) - : 75, - 26, - ]; - } - - static setDefaultTextVisibility(visible) { - RerouteNode.defaultVisibility = visible; - if (visible) { - localStorage["Comfy.RerouteNode.DefaultVisibility"] = "true"; - } else { - delete localStorage["Comfy.RerouteNode.DefaultVisibility"]; - } - } - } - - // Load default visibility - RerouteNode.setDefaultTextVisibility(!!localStorage["Comfy.RerouteNode.DefaultVisibility"]); - - LiteGraph.registerNodeType( - "Reroute", - Object.assign(RerouteNode, { - title_mode: LiteGraph.NO_TITLE, - title: "Reroute", - collapsable: false, - }) - ); - - RerouteNode.category = "utils"; - }, -}); diff --git a/web/extensions/core/saveImageExtraOutput.js b/web/extensions/core/saveImageExtraOutput.js deleted file mode 100644 index a0506b43b..000000000 --- a/web/extensions/core/saveImageExtraOutput.js +++ /dev/null @@ -1,35 +0,0 @@ -import { app } from "../../scripts/app.js"; -import { applyTextReplacements } from "../../scripts/utils.js"; -// Use widget values and dates in output filenames - -app.registerExtension({ - name: "Comfy.SaveImageExtraOutput", - async beforeRegisterNodeDef(nodeType, nodeData, app) { - if (nodeData.name === "SaveImage") { - const onNodeCreated = nodeType.prototype.onNodeCreated; - // When the SaveImage node is created we want to override the serialization of the output name widget to run our S&R - nodeType.prototype.onNodeCreated = function () { - const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; - - const widget = this.widgets.find((w) => w.name === "filename_prefix"); - widget.serializeValue = () => { - return applyTextReplacements(app, widget.value); - }; - - return r; - }; - } else { - // When any other node is created add a property to alias the node - const onNodeCreated = nodeType.prototype.onNodeCreated; - nodeType.prototype.onNodeCreated = function () { - const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; - - if (!this.properties || !("Node name for S&R" in this.properties)) { - this.addProperty("Node name for S&R", this.constructor.type, "string"); - } - - return r; - }; - } - }, -}); diff --git a/web/extensions/core/simpleTouchSupport.js b/web/extensions/core/simpleTouchSupport.js deleted file mode 100644 index 041fc2c4c..000000000 --- a/web/extensions/core/simpleTouchSupport.js +++ /dev/null @@ -1,102 +0,0 @@ -import { app } from "../../scripts/app.js"; - -let touchZooming; -let touchCount = 0; - -app.registerExtension({ - name: "Comfy.SimpleTouchSupport", - setup() { - let zoomPos; - let touchTime; - let lastTouch; - - function getMultiTouchPos(e) { - return Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY); - } - - app.canvasEl.addEventListener( - "touchstart", - (e) => { - touchCount++; - lastTouch = null; - if (e.touches?.length === 1) { - // Store start time for press+hold for context menu - touchTime = new Date(); - lastTouch = e.touches[0]; - } else { - touchTime = null; - if (e.touches?.length === 2) { - // Store center pos for zoom - zoomPos = getMultiTouchPos(e); - app.canvas.pointer_is_down = false; - } - } - }, - true - ); - - app.canvasEl.addEventListener("touchend", (e) => { - touchZooming = false; - touchCount = e.touches?.length ?? touchCount - 1; - if (touchTime && !e.touches?.length) { - if (new Date() - touchTime > 600) { - try { - // hack to get litegraph to use this event - e.constructor = CustomEvent; - } catch (error) {} - e.clientX = lastTouch.clientX; - e.clientY = lastTouch.clientY; - - app.canvas.pointer_is_down = true; - app.canvas._mousedown_callback(e); - } - touchTime = null; - } - }); - - app.canvasEl.addEventListener( - "touchmove", - (e) => { - touchTime = null; - if (e.touches?.length === 2) { - app.canvas.pointer_is_down = false; - touchZooming = true; - LiteGraph.closeAllContextMenus(); - app.canvas.search_box?.close(); - const newZoomPos = getMultiTouchPos(e); - - const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2; - const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2; - - let scale = app.canvas.ds.scale; - const diff = zoomPos - newZoomPos; - if (diff > 0.5) { - scale *= 1 / 1.07; - } else if (diff < -0.5) { - scale *= 1.07; - } - app.canvas.ds.changeScale(scale, [midX, midY]); - app.canvas.setDirty(true, true); - zoomPos = newZoomPos; - } - }, - true - ); - }, -}); - -const processMouseDown = LGraphCanvas.prototype.processMouseDown; -LGraphCanvas.prototype.processMouseDown = function (e) { - if (touchZooming || touchCount) { - return; - } - return processMouseDown.apply(this, arguments); -}; - -const processMouseMove = LGraphCanvas.prototype.processMouseMove; -LGraphCanvas.prototype.processMouseMove = function (e) { - if (touchZooming || touchCount > 1) { - return; - } - return processMouseMove.apply(this, arguments); -}; diff --git a/web/extensions/core/slotDefaults.js b/web/extensions/core/slotDefaults.js deleted file mode 100644 index 718d25405..000000000 --- a/web/extensions/core/slotDefaults.js +++ /dev/null @@ -1,91 +0,0 @@ -import { app } from "../../scripts/app.js"; -import { ComfyWidgets } from "../../scripts/widgets.js"; -// Adds defaults for quickly adding nodes with middle click on the input/output - -app.registerExtension({ - name: "Comfy.SlotDefaults", - suggestionsNumber: null, - init() { - LiteGraph.search_filter_enabled = true; - LiteGraph.middle_click_slot_add_default_node = true; - this.suggestionsNumber = app.ui.settings.addSetting({ - id: "Comfy.NodeSuggestions.number", - name: "Number of nodes suggestions", - type: "slider", - attrs: { - min: 1, - max: 100, - step: 1, - }, - defaultValue: 5, - onChange: (newVal, oldVal) => { - this.setDefaults(newVal); - } - }); - }, - slot_types_default_out: {}, - slot_types_default_in: {}, - async beforeRegisterNodeDef(nodeType, nodeData, app) { - var nodeId = nodeData.name; - var inputs = []; - inputs = nodeData["input"]["required"]; //only show required inputs to reduce the mess also not logical to create node with optional inputs - for (const inputKey in inputs) { - var input = (inputs[inputKey]); - if (typeof input[0] !== "string") continue; - - var type = input[0] - if (type in ComfyWidgets) { - var customProperties = input[1] - if (!(customProperties?.forceInput)) continue; //ignore widgets that don't force input - } - - if (!(type in this.slot_types_default_out)) { - this.slot_types_default_out[type] = ["Reroute"]; - } - if (this.slot_types_default_out[type].includes(nodeId)) continue; - this.slot_types_default_out[type].push(nodeId); - - // Input types have to be stored as lower case - // Store each node that can handle this input type - const lowerType = type.toLocaleLowerCase(); - if (!(lowerType in LiteGraph.registered_slot_in_types)) { - LiteGraph.registered_slot_in_types[lowerType] = { nodes: [] }; - } - LiteGraph.registered_slot_in_types[lowerType].nodes.push(nodeType.comfyClass); - } - - var outputs = nodeData["output"]; - for (const key in outputs) { - var type = outputs[key]; - if (!(type in this.slot_types_default_in)) { - this.slot_types_default_in[type] = ["Reroute"];// ["Reroute", "Primitive"]; primitive doesn't always work :'() - } - - this.slot_types_default_in[type].push(nodeId); - - // Store each node that can handle this output type - if (!(type in LiteGraph.registered_slot_out_types)) { - LiteGraph.registered_slot_out_types[type] = { nodes: [] }; - } - LiteGraph.registered_slot_out_types[type].nodes.push(nodeType.comfyClass); - - if(!LiteGraph.slot_types_out.includes(type)) { - LiteGraph.slot_types_out.push(type); - } - } - var maxNum = this.suggestionsNumber.value; - this.setDefaults(maxNum); - }, - setDefaults(maxNum) { - - LiteGraph.slot_types_default_out = {}; - LiteGraph.slot_types_default_in = {}; - - for (const type in this.slot_types_default_out) { - LiteGraph.slot_types_default_out[type] = this.slot_types_default_out[type].slice(0, maxNum); - } - for (const type in this.slot_types_default_in) { - LiteGraph.slot_types_default_in[type] = this.slot_types_default_in[type].slice(0, maxNum); - } - } -}); diff --git a/web/extensions/core/snapToGrid.js b/web/extensions/core/snapToGrid.js deleted file mode 100644 index aac017748..000000000 --- a/web/extensions/core/snapToGrid.js +++ /dev/null @@ -1,171 +0,0 @@ -import { app } from "../../scripts/app.js"; - -// Shift + drag/resize to snap to grid - -/** Rounds a Vector2 in-place to the current CANVAS_GRID_SIZE. */ -function roundVectorToGrid(vec) { - vec[0] = LiteGraph.CANVAS_GRID_SIZE * Math.round(vec[0] / LiteGraph.CANVAS_GRID_SIZE); - vec[1] = LiteGraph.CANVAS_GRID_SIZE * Math.round(vec[1] / LiteGraph.CANVAS_GRID_SIZE); - return vec; -} - -app.registerExtension({ - name: "Comfy.SnapToGrid", - init() { - // Add setting to control grid size - app.ui.settings.addSetting({ - id: "Comfy.SnapToGrid.GridSize", - name: "Grid Size", - type: "slider", - attrs: { - min: 1, - max: 500, - }, - tooltip: - "When dragging and resizing nodes while holding shift they will be aligned to the grid, this controls the size of that grid.", - defaultValue: LiteGraph.CANVAS_GRID_SIZE, - onChange(value) { - LiteGraph.CANVAS_GRID_SIZE = +value; - }, - }); - - // After moving a node, if the shift key is down align it to grid - const onNodeMoved = app.canvas.onNodeMoved; - app.canvas.onNodeMoved = function (node) { - const r = onNodeMoved?.apply(this, arguments); - - if (app.shiftDown) { - // Ensure all selected nodes are realigned - for (const id in this.selected_nodes) { - this.selected_nodes[id].alignToGrid(); - } - } - - return r; - }; - - // When a node is added, add a resize handler to it so we can fix align the size with the grid - const onNodeAdded = app.graph.onNodeAdded; - app.graph.onNodeAdded = function (node) { - const onResize = node.onResize; - node.onResize = function () { - if (app.shiftDown) { - roundVectorToGrid(node.size); - } - return onResize?.apply(this, arguments); - }; - return onNodeAdded?.apply(this, arguments); - }; - - // Draw a preview of where the node will go if holding shift and the node is selected - const origDrawNode = LGraphCanvas.prototype.drawNode; - LGraphCanvas.prototype.drawNode = function (node, ctx) { - if (app.shiftDown && this.node_dragged && node.id in this.selected_nodes) { - const [x, y] = roundVectorToGrid([...node.pos]); - const shiftX = x - node.pos[0]; - let shiftY = y - node.pos[1]; - - let w, h; - if (node.flags.collapsed) { - w = node._collapsed_width; - h = LiteGraph.NODE_TITLE_HEIGHT; - shiftY -= LiteGraph.NODE_TITLE_HEIGHT; - } else { - w = node.size[0]; - h = node.size[1]; - let titleMode = node.constructor.title_mode; - if (titleMode !== LiteGraph.TRANSPARENT_TITLE && titleMode !== LiteGraph.NO_TITLE) { - h += LiteGraph.NODE_TITLE_HEIGHT; - shiftY -= LiteGraph.NODE_TITLE_HEIGHT; - } - } - const f = ctx.fillStyle; - ctx.fillStyle = "rgba(100, 100, 100, 0.5)"; - ctx.fillRect(shiftX, shiftY, w, h); - ctx.fillStyle = f; - } - - return origDrawNode.apply(this, arguments); - }; - - - - /** - * The currently moving, selected group only. Set after the `selected_group` has actually started - * moving. - */ - let selectedAndMovingGroup = null; - - /** - * Handles moving a group; tracking when a group has been moved (to show the ghost in `drawGroups` - * below) as well as handle the last move call from LiteGraph's `processMouseUp`. - */ - const groupMove = LGraphGroup.prototype.move; - LGraphGroup.prototype.move = function(deltax, deltay, ignore_nodes) { - const v = groupMove.apply(this, arguments); - // When we've started moving, set `selectedAndMovingGroup` as LiteGraph sets `selected_group` - // too eagerly and we don't want to behave like we're moving until we get a delta. - if (!selectedAndMovingGroup && app.canvas.selected_group === this && (deltax || deltay)) { - selectedAndMovingGroup = this; - } - - // LiteGraph will call group.move both on mouse-move as well as mouse-up though we only want - // to snap on a mouse-up which we can determine by checking if `app.canvas.last_mouse_dragging` - // has been set to `false`. Essentially, this check here is the equivilant to calling an - // `LGraphGroup.prototype.onNodeMoved` if it had existed. - if (app.canvas.last_mouse_dragging === false && app.shiftDown) { - // After moving a group (while app.shiftDown), snap all the child nodes and, finally, - // align the group itself. - this.recomputeInsideNodes(); - for (const node of this._nodes) { - node.alignToGrid(); - } - LGraphNode.prototype.alignToGrid.apply(this); - } - return v; - }; - - /** - * Handles drawing a group when, snapping the size when one is actively being resized tracking and/or - * drawing a ghost box when one is actively being moved. This mimics the node snapping behavior for - * both. - */ - const drawGroups = LGraphCanvas.prototype.drawGroups; - LGraphCanvas.prototype.drawGroups = function (canvas, ctx) { - if (this.selected_group && app.shiftDown) { - if (this.selected_group_resizing) { - roundVectorToGrid(this.selected_group.size); - } else if (selectedAndMovingGroup) { - const [x, y] = roundVectorToGrid([...selectedAndMovingGroup.pos]); - const f = ctx.fillStyle; - const s = ctx.strokeStyle; - ctx.fillStyle = "rgba(100, 100, 100, 0.33)"; - ctx.strokeStyle = "rgba(100, 100, 100, 0.66)"; - ctx.rect(x, y, ...selectedAndMovingGroup.size); - ctx.fill(); - ctx.stroke(); - ctx.fillStyle = f; - ctx.strokeStyle = s; - } - } else if (!this.selected_group) { - selectedAndMovingGroup = null; - } - return drawGroups.apply(this, arguments); - }; - - - /** Handles adding a group in a snapping-enabled state. */ - const onGroupAdd = LGraphCanvas.onGroupAdd; - LGraphCanvas.onGroupAdd = function() { - const v = onGroupAdd.apply(app.canvas, arguments); - if (app.shiftDown) { - const lastGroup = app.graph._groups[app.graph._groups.length - 1]; - if (lastGroup) { - roundVectorToGrid(lastGroup.pos); - roundVectorToGrid(lastGroup.size); - } - } - return v; - }; - }, -}); diff --git a/web/extensions/core/uploadAudio.js b/web/extensions/core/uploadAudio.js deleted file mode 100644 index 9dfa029bf..000000000 --- a/web/extensions/core/uploadAudio.js +++ /dev/null @@ -1,186 +0,0 @@ -import { app } from "../../scripts/app.js" -import { api } from "../../scripts/api.js" - -function splitFilePath(path) { - const folder_separator = path.lastIndexOf("/") - if (folder_separator === -1) { - return ["", path] - } - return [ - path.substring(0, folder_separator), - path.substring(folder_separator + 1) - ] -} - -function getResourceURL(subfolder, filename, type = "input") { - const params = [ - "filename=" + encodeURIComponent(filename), - "type=" + type, - "subfolder=" + subfolder, - app.getRandParam().substring(1) - ].join("&") - - return `/view?${params}` -} - -async function uploadFile( - audioWidget, - audioUIWidget, - file, - updateNode, - pasted = false -) { - try { - // Wrap file in formdata so it includes filename - const body = new FormData() - body.append("image", file) - if (pasted) body.append("subfolder", "pasted") - const resp = await api.fetchApi("/upload/image", { - method: "POST", - body - }) - - if (resp.status === 200) { - const data = await resp.json() - // Add the file to the dropdown list and update the widget value - let path = data.name - if (data.subfolder) path = data.subfolder + "/" + path - - if (!audioWidget.options.values.includes(path)) { - audioWidget.options.values.push(path) - } - - if (updateNode) { - audioUIWidget.element.src = api.apiURL( - getResourceURL(...splitFilePath(path)) - ) - audioWidget.value = path - } - } else { - alert(resp.status + " - " + resp.statusText) - } - } catch (error) { - alert(error) - } -} - -// AudioWidget MUST be registered first, as AUDIOUPLOAD depends on AUDIO_UI to be -// present. -app.registerExtension({ - name: "Comfy.AudioWidget", - async beforeRegisterNodeDef(nodeType, nodeData) { - if (["LoadAudio", "SaveAudio", "PreviewAudio"].includes(nodeType.comfyClass)) { - nodeData.input.required.audioUI = ["AUDIO_UI"] - } - }, - getCustomWidgets() { - return { - AUDIO_UI(node, inputName) { - const audio = document.createElement("audio") - audio.controls = true - audio.classList.add("comfy-audio") - audio.setAttribute("name", "media") - - const audioUIWidget = node.addDOMWidget( - inputName, - /* name=*/ "audioUI", - audio - ) - // @ts-ignore - // TODO: Sort out the DOMWidget type. - audioUIWidget.serialize = false - - const isOutputNode = node.constructor.nodeData.output_node - if (isOutputNode) { - // Hide the audio widget when there is no audio initially. - audioUIWidget.element.classList.add("empty-audio-widget") - // Populate the audio widget UI on node execution. - const onExecuted = node.onExecuted - node.onExecuted = function(message) { - onExecuted?.apply(this, arguments) - const audios = message.audio - if (!audios) return - const audio = audios[0] - audioUIWidget.element.src = api.apiURL( - getResourceURL(audio.subfolder, audio.filename, audio.type) - ) - audioUIWidget.element.classList.remove("empty-audio-widget") - } - } - return { widget: audioUIWidget } - } - } - }, - onNodeOutputsUpdated(nodeOutputs) { - for (const [nodeId, output] of Object.entries(nodeOutputs)) { - const node = app.graph.getNodeById(Number.parseInt(nodeId)); - if ("audio" in output) { - const audioUIWidget = node.widgets.find((w) => w.name === "audioUI"); - const audio = output.audio[0]; - audioUIWidget.element.src = api.apiURL(getResourceURL(audio.subfolder, audio.filename, audio.type)); - audioUIWidget.element.classList.remove("empty-audio-widget"); - } - } - }, -}) - -app.registerExtension({ - name: "Comfy.UploadAudio", - async beforeRegisterNodeDef(nodeType, nodeData) { - if (nodeData?.input?.required?.audio?.[1]?.audio_upload === true) { - nodeData.input.required.upload = ["AUDIOUPLOAD"] - } - }, - getCustomWidgets() { - return { - AUDIOUPLOAD(node, inputName) { - // The widget that allows user to select file. - const audioWidget = node.widgets.find(w => w.name === "audio") - const audioUIWidget = node.widgets.find(w => w.name === "audioUI") - - const onAudioWidgetUpdate = () => { - audioUIWidget.element.src = api.apiURL( - getResourceURL(...splitFilePath(audioWidget.value)) - ) - } - // Initially load default audio file to audioUIWidget. - if (audioWidget.value) { - onAudioWidgetUpdate() - } - audioWidget.callback = onAudioWidgetUpdate - - // Load saved audio file widget values if restoring from workflow - const onGraphConfigured = node.onGraphConfigured; - node.onGraphConfigured = function() { - onGraphConfigured?.apply(this, arguments) - if (audioWidget.value) { - onAudioWidgetUpdate() - } - } - - const fileInput = document.createElement("input") - fileInput.type = "file" - fileInput.accept = "audio/*" - fileInput.style.display = "none" - fileInput.onchange = () => { - if (fileInput.files.length) { - uploadFile(audioWidget, audioUIWidget, fileInput.files[0], true) - } - } - // The widget to pop up the upload dialog. - const uploadWidget = node.addWidget( - "button", - inputName, - /* value=*/ "", - () => { - fileInput.click() - } - ) - uploadWidget.label = "choose file to upload" - uploadWidget.serialize = false - - return { widget: uploadWidget } - } - } - } -}) diff --git a/web/extensions/core/uploadImage.js b/web/extensions/core/uploadImage.js deleted file mode 100644 index 530c4599e..000000000 --- a/web/extensions/core/uploadImage.js +++ /dev/null @@ -1,12 +0,0 @@ -import { app } from "../../scripts/app.js"; - -// Adds an upload button to the nodes - -app.registerExtension({ - name: "Comfy.UploadImage", - async beforeRegisterNodeDef(nodeType, nodeData, app) { - if (nodeData?.input?.required?.image?.[1]?.image_upload === true) { - nodeData.input.required.upload = ["IMAGEUPLOAD"]; - } - }, -}); diff --git a/web/extensions/core/webcamCapture.js b/web/extensions/core/webcamCapture.js deleted file mode 100644 index dd5725bd4..000000000 --- a/web/extensions/core/webcamCapture.js +++ /dev/null @@ -1,126 +0,0 @@ -import { app } from "../../scripts/app.js"; -import { api } from "../../scripts/api.js"; - -const WEBCAM_READY = Symbol(); - -app.registerExtension({ - name: "Comfy.WebcamCapture", - getCustomWidgets(app) { - return { - WEBCAM(node, inputName) { - let res; - node[WEBCAM_READY] = new Promise((resolve) => (res = resolve)); - - const container = document.createElement("div"); - container.style.background = "rgba(0,0,0,0.25)"; - container.style.textAlign = "center"; - - const video = document.createElement("video"); - video.style.height = video.style.width = "100%"; - - const loadVideo = async () => { - try { - const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }); - container.replaceChildren(video); - - setTimeout(() => res(video), 500); // Fallback as loadedmetadata doesnt fire sometimes? - video.addEventListener("loadedmetadata", () => res(video), false); - video.srcObject = stream; - video.play(); - } catch (error) { - const label = document.createElement("div"); - label.style.color = "red"; - label.style.overflow = "auto"; - label.style.maxHeight = "100%"; - label.style.whiteSpace = "pre-wrap"; - - if (window.isSecureContext) { - label.textContent = "Unable to load webcam, please ensure access is granted:\n" + error.message; - } else { - label.textContent = "Unable to load webcam. A secure context is required, if you are not accessing ComfyUI on localhost (127.0.0.1) you will have to enable TLS (https)\n\n" + error.message; - } - - container.replaceChildren(label); - } - }; - - loadVideo(); - - return { widget: node.addDOMWidget(inputName, "WEBCAM", container) }; - }, - }; - }, - nodeCreated(node) { - if ((node.type, node.constructor.comfyClass !== "WebcamCapture")) return; - - let video; - const camera = node.widgets.find((w) => w.name === "image"); - const w = node.widgets.find((w) => w.name === "width"); - const h = node.widgets.find((w) => w.name === "height"); - const captureOnQueue = node.widgets.find((w) => w.name === "capture_on_queue"); - - const canvas = document.createElement("canvas"); - - const capture = () => { - canvas.width = w.value; - canvas.height = h.value; - const ctx = canvas.getContext("2d"); - ctx.drawImage(video, 0, 0, w.value, h.value); - const data = canvas.toDataURL("image/png"); - - const img = new Image(); - img.onload = () => { - node.imgs = [img]; - app.graph.setDirtyCanvas(true); - requestAnimationFrame(() => { - node.setSizeForImage?.(); - }); - }; - img.src = data; - }; - - const btn = node.addWidget("button", "waiting for camera...", "capture", capture); - btn.disabled = true; - btn.serializeValue = () => undefined; - - camera.serializeValue = async () => { - if (captureOnQueue.value) { - capture(); - } else if (!node.imgs?.length) { - const err = `No webcam image captured`; - alert(err); - throw new Error(err); - } - - // Upload image to temp storage - const blob = await new Promise((r) => canvas.toBlob(r)); - const name = `${+new Date()}.png`; - const file = new File([blob], name); - const body = new FormData(); - body.append("image", file); - body.append("subfolder", "webcam"); - body.append("type", "temp"); - const resp = await api.fetchApi("/upload/image", { - method: "POST", - body, - }); - if (resp.status !== 200) { - const err = `Error uploading camera image: ${resp.status} - ${resp.statusText}`; - alert(err); - throw new Error(err); - } - return `webcam/${name} [temp]`; - }; - - node[WEBCAM_READY].then((v) => { - video = v; - // If width isnt specified then use video output resolution - if (!w.value) { - w.value = video.videoWidth || 640; - h.value = video.videoHeight || 480; - } - btn.disabled = false; - btn.label = "capture"; - }); - }, -}); diff --git a/web/extensions/core/widgetInputs.js b/web/extensions/core/widgetInputs.js deleted file mode 100644 index f1a1d22cd..000000000 --- a/web/extensions/core/widgetInputs.js +++ /dev/null @@ -1,800 +0,0 @@ -import { ComfyWidgets, addValueControlWidgets } from "../../scripts/widgets.js"; -import { app } from "../../scripts/app.js"; -import { applyTextReplacements } from "../../scripts/utils.js"; - -const CONVERTED_TYPE = "converted-widget"; -const VALID_TYPES = ["STRING", "combo", "number", "BOOLEAN"]; -const CONFIG = Symbol(); -const GET_CONFIG = Symbol(); -const TARGET = Symbol(); // Used for reroutes to specify the real target widget - -export function getWidgetConfig(slot) { - return slot.widget[CONFIG] ?? slot.widget[GET_CONFIG](); -} - -function getConfig(widgetName) { - const { nodeData } = this.constructor; - return nodeData?.input?.required[widgetName] ?? nodeData?.input?.optional?.[widgetName]; -} - -function isConvertableWidget(widget, config) { - return (VALID_TYPES.includes(widget.type) || VALID_TYPES.includes(config[0])) && !widget.options?.forceInput; -} - -function hideWidget(node, widget, suffix = "") { - if (widget.type?.startsWith(CONVERTED_TYPE)) return; - widget.origType = widget.type; - widget.origComputeSize = widget.computeSize; - widget.origSerializeValue = widget.serializeValue; - widget.computeSize = () => [0, -4]; // -4 is due to the gap litegraph adds between widgets automatically - widget.type = CONVERTED_TYPE + suffix; - widget.serializeValue = () => { - // Prevent serializing the widget if we have no input linked - if (!node.inputs) { - return undefined; - } - let node_input = node.inputs.find((i) => i.widget?.name === widget.name); - - if (!node_input || !node_input.link) { - return undefined; - } - return widget.origSerializeValue ? widget.origSerializeValue() : widget.value; - }; - - // Hide any linked widgets, e.g. seed+seedControl - if (widget.linkedWidgets) { - for (const w of widget.linkedWidgets) { - hideWidget(node, w, ":" + widget.name); - } - } -} - -function showWidget(widget) { - widget.type = widget.origType; - widget.computeSize = widget.origComputeSize; - widget.serializeValue = widget.origSerializeValue; - - delete widget.origType; - delete widget.origComputeSize; - delete widget.origSerializeValue; - - // Hide any linked widgets, e.g. seed+seedControl - if (widget.linkedWidgets) { - for (const w of widget.linkedWidgets) { - showWidget(w); - } - } -} - -function convertToInput(node, widget, config) { - hideWidget(node, widget); - - const { type } = getWidgetType(config); - - // Add input and store widget config for creating on primitive node - const sz = node.size; - node.addInput(widget.name, type, { - widget: { name: widget.name, [GET_CONFIG]: () => config }, - }); - - for (const widget of node.widgets) { - widget.last_y += LiteGraph.NODE_SLOT_HEIGHT; - } - - // Restore original size but grow if needed - node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]); -} - -function convertToWidget(node, widget) { - showWidget(widget); - const sz = node.size; - node.removeInput(node.inputs.findIndex((i) => i.widget?.name === widget.name)); - - for (const widget of node.widgets) { - widget.last_y -= LiteGraph.NODE_SLOT_HEIGHT; - } - - // Restore original size but grow if needed - node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]); -} - -function getWidgetType(config) { - // Special handling for COMBO so we restrict links based on the entries - let type = config[0]; - if (type instanceof Array) { - type = "COMBO"; - } - return { type }; -} - -function isValidCombo(combo, obj) { - // New input isnt a combo - if (!(obj instanceof Array)) { - console.log(`connection rejected: tried to connect combo to ${obj}`); - return false; - } - // New imput combo has a different size - if (combo.length !== obj.length) { - console.log(`connection rejected: combo lists dont match`); - return false; - } - // New input combo has different elements - if (combo.find((v, i) => obj[i] !== v)) { - console.log(`connection rejected: combo lists dont match`); - return false; - } - - return true; -} - -export function setWidgetConfig(slot, config, target) { - if (!slot.widget) return; - if (config) { - slot.widget[GET_CONFIG] = () => config; - slot.widget[TARGET] = target; - } else { - delete slot.widget; - } - - if (slot.link) { - const link = app.graph.links[slot.link]; - if (link) { - const originNode = app.graph.getNodeById(link.origin_id); - if (originNode.type === "PrimitiveNode") { - if (config) { - originNode.recreateWidget(); - } else if(!app.configuringGraph) { - originNode.disconnectOutput(0); - originNode.onLastDisconnect(); - } - } - } - } -} - -export function mergeIfValid(output, config2, forceUpdate, recreateWidget, config1) { - if (!config1) { - config1 = output.widget[CONFIG] ?? output.widget[GET_CONFIG](); - } - - if (config1[0] instanceof Array) { - if (!isValidCombo(config1[0], config2[0])) return false; - } else if (config1[0] !== config2[0]) { - // Types dont match - console.log(`connection rejected: types dont match`, config1[0], config2[0]); - return false; - } - - const keys = new Set([...Object.keys(config1[1] ?? {}), ...Object.keys(config2[1] ?? {})]); - - let customConfig; - const getCustomConfig = () => { - if (!customConfig) { - if (typeof structuredClone === "undefined") { - customConfig = JSON.parse(JSON.stringify(config1[1] ?? {})); - } else { - customConfig = structuredClone(config1[1] ?? {}); - } - } - return customConfig; - }; - - const isNumber = config1[0] === "INT" || config1[0] === "FLOAT"; - for (const k of keys.values()) { - if (k !== "default" && k !== "forceInput" && k !== "defaultInput" && k !== "control_after_generate" && k !== "multiline") { - let v1 = config1[1][k]; - let v2 = config2[1]?.[k]; - - if (v1 === v2 || (!v1 && !v2)) continue; - - if (isNumber) { - if (k === "min") { - const theirMax = config2[1]?.["max"]; - if (theirMax != null && v1 > theirMax) { - console.log("connection rejected: min > max", v1, theirMax); - return false; - } - getCustomConfig()[k] = v1 == null ? v2 : v2 == null ? v1 : Math.max(v1, v2); - continue; - } else if (k === "max") { - const theirMin = config2[1]?.["min"]; - if (theirMin != null && v1 < theirMin) { - console.log("connection rejected: max < min", v1, theirMin); - return false; - } - getCustomConfig()[k] = v1 == null ? v2 : v2 == null ? v1 : Math.min(v1, v2); - continue; - } else if (k === "step") { - let step; - if (v1 == null) { - // No current step - step = v2; - } else if (v2 == null) { - // No new step - step = v1; - } else { - if (v1 < v2) { - // Ensure v1 is larger for the mod - const a = v2; - v2 = v1; - v1 = a; - } - if (v1 % v2) { - console.log("connection rejected: steps not divisible", "current:", v1, "new:", v2); - return false; - } - - step = v1; - } - - getCustomConfig()[k] = step; - continue; - } - } - - console.log(`connection rejected: config ${k} values dont match`, v1, v2); - return false; - } - } - - if (customConfig || forceUpdate) { - if (customConfig) { - output.widget[CONFIG] = [config1[0], customConfig]; - } - - const widget = recreateWidget?.call(this); - // When deleting a node this can be null - if (widget) { - const min = widget.options.min; - const max = widget.options.max; - if (min != null && widget.value < min) widget.value = min; - if (max != null && widget.value > max) widget.value = max; - widget.callback(widget.value); - } - } - - return { customConfig }; -} - -let useConversionSubmenusSetting; -app.registerExtension({ - name: "Comfy.WidgetInputs", - init() { - useConversionSubmenusSetting = app.ui.settings.addSetting({ - id: "Comfy.NodeInputConversionSubmenus", - name: "Node widget/input conversion sub-menus", - tooltip: "In the node context menu, place the entries that convert between input/widget in sub-menus.", - type: "boolean", - defaultValue: true, - }); - }, - async beforeRegisterNodeDef(nodeType, nodeData, app) { - // Add menu options to conver to/from widgets - const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; - nodeType.prototype.convertWidgetToInput = function (widget) { - const config = getConfig.call(this, widget.name) ?? [widget.type, widget.options || {}]; - if (!isConvertableWidget(widget, config)) return false; - convertToInput(this, widget, config); - return true; - }; - nodeType.prototype.getExtraMenuOptions = function (_, options) { - const r = origGetExtraMenuOptions ? origGetExtraMenuOptions.apply(this, arguments) : undefined; - - if (this.widgets) { - let toInput = []; - let toWidget = []; - for (const w of this.widgets) { - if (w.options?.forceInput) { - continue; - } - if (w.type === CONVERTED_TYPE) { - toWidget.push({ - content: `Convert ${w.name} to widget`, - callback: () => convertToWidget(this, w), - }); - } else { - const config = getConfig.call(this, w.name) ?? [w.type, w.options || {}]; - if (isConvertableWidget(w, config)) { - toInput.push({ - content: `Convert ${w.name} to input`, - callback: () => convertToInput(this, w, config), - }); - } - } - } - - //Convert.. main menu - if (toInput.length) { - if (useConversionSubmenusSetting.value) { - options.push({ - content: "Convert Widget to Input", - submenu: { - options: toInput, - }, - }); - } else { - options.push(...toInput, null); - } - } - if (toWidget.length) { - if (useConversionSubmenusSetting.value) { - options.push({ - content: "Convert Input to Widget", - submenu: { - options: toWidget, - }, - }); - } else { - options.push(...toWidget, null); - } - } - } - - return r; - }; - - nodeType.prototype.onGraphConfigured = function () { - if (!this.inputs) return; - - for (const input of this.inputs) { - if (input.widget) { - if (!input.widget[GET_CONFIG]) { - input.widget[GET_CONFIG] = () => getConfig.call(this, input.widget.name); - } - - // Cleanup old widget config - if (input.widget.config) { - if (input.widget.config[0] instanceof Array) { - // If we are an old converted combo then replace the input type and the stored link data - input.type = "COMBO"; - - const link = app.graph.links[input.link]; - if (link) { - link.type = input.type; - } - } - delete input.widget.config; - } - - const w = this.widgets.find((w) => w.name === input.widget.name); - if (w) { - hideWidget(this, w); - } else { - convertToWidget(this, input); - } - } - } - }; - - const origOnNodeCreated = nodeType.prototype.onNodeCreated; - nodeType.prototype.onNodeCreated = function () { - const r = origOnNodeCreated ? origOnNodeCreated.apply(this) : undefined; - - // When node is created, convert any force/default inputs - if (!app.configuringGraph && this.widgets) { - for (const w of this.widgets) { - if (w?.options?.forceInput || w?.options?.defaultInput) { - const config = getConfig.call(this, w.name) ?? [w.type, w.options || {}]; - convertToInput(this, w, config); - } - } - } - - return r; - }; - - const origOnConfigure = nodeType.prototype.onConfigure; - nodeType.prototype.onConfigure = function () { - const r = origOnConfigure ? origOnConfigure.apply(this, arguments) : undefined; - if (!app.configuringGraph && this.inputs) { - // On copy + paste of nodes, ensure that widget configs are set up - for (const input of this.inputs) { - if (input.widget && !input.widget[GET_CONFIG]) { - input.widget[GET_CONFIG] = () => getConfig.call(this, input.widget.name); - const w = this.widgets.find((w) => w.name === input.widget.name); - if (w) { - hideWidget(this, w); - } - } - } - } - - return r; - }; - - function isNodeAtPos(pos) { - for (const n of app.graph._nodes) { - if (n.pos[0] === pos[0] && n.pos[1] === pos[1]) { - return true; - } - } - return false; - } - - // Double click a widget input to automatically attach a primitive - const origOnInputDblClick = nodeType.prototype.onInputDblClick; - const ignoreDblClick = Symbol(); - nodeType.prototype.onInputDblClick = function (slot) { - const r = origOnInputDblClick ? origOnInputDblClick.apply(this, arguments) : undefined; - - const input = this.inputs[slot]; - if (!input.widget || !input[ignoreDblClick]) { - // Not a widget input or already handled input - if (!(input.type in ComfyWidgets) && !(input.widget[GET_CONFIG]?.()?.[0] instanceof Array)) { - return r; //also Not a ComfyWidgets input or combo (do nothing) - } - } - - // Create a primitive node - const node = LiteGraph.createNode("PrimitiveNode"); - app.graph.add(node); - - // Calculate a position that wont directly overlap another node - const pos = [this.pos[0] - node.size[0] - 30, this.pos[1]]; - while (isNodeAtPos(pos)) { - pos[1] += LiteGraph.NODE_TITLE_HEIGHT; - } - - node.pos = pos; - node.connect(0, this, slot); - node.title = input.name; - - // Prevent adding duplicates due to triple clicking - input[ignoreDblClick] = true; - setTimeout(() => { - delete input[ignoreDblClick]; - }, 300); - - return r; - }; - - // Prevent connecting COMBO lists to converted inputs that dont match types - const onConnectInput = nodeType.prototype.onConnectInput; - nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) { - const v = onConnectInput?.(this, arguments); - // Not a combo, ignore - if (type !== "COMBO") return v; - // Primitive output, allow that to handle - if (originNode.outputs[originSlot].widget) return v; - - // Ensure target is also a combo - const targetCombo = this.inputs[targetSlot].widget?.[GET_CONFIG]?.()?.[0]; - if (!targetCombo || !(targetCombo instanceof Array)) return v; - - // Check they match - const originConfig = originNode.constructor?.nodeData?.output?.[originSlot]; - if (!originConfig || !isValidCombo(targetCombo, originConfig)) { - return false; - } - - return v; - }; - }, - registerCustomNodes() { - const replacePropertyName = "Run widget replace on values"; - class PrimitiveNode { - constructor() { - this.addOutput("connect to widget input", "*"); - this.serialize_widgets = true; - this.isVirtualNode = true; - - if (!this.properties || !(replacePropertyName in this.properties)) { - this.addProperty(replacePropertyName, false, "boolean"); - } - } - - applyToGraph(extraLinks = []) { - if (!this.outputs[0].links?.length) return; - - function get_links(node) { - let links = []; - for (const l of node.outputs[0].links) { - const linkInfo = app.graph.links[l]; - const n = node.graph.getNodeById(linkInfo.target_id); - if (n.type == "Reroute") { - links = links.concat(get_links(n)); - } else { - links.push(l); - } - } - return links; - } - - let links = [...get_links(this).map((l) => app.graph.links[l]), ...extraLinks]; - let v = this.widgets?.[0].value; - if(v && this.properties[replacePropertyName]) { - v = applyTextReplacements(app, v); - } - - // For each output link copy our value over the original widget value - for (const linkInfo of links) { - const node = this.graph.getNodeById(linkInfo.target_id); - const input = node.inputs[linkInfo.target_slot]; - let widget; - if (input.widget[TARGET]) { - widget = input.widget[TARGET]; - } else { - const widgetName = input.widget.name; - if (widgetName) { - widget = node.widgets.find((w) => w.name === widgetName); - } - } - - if (widget) { - widget.value = v; - if (widget.callback) { - widget.callback(widget.value, app.canvas, node, app.canvas.graph_mouse, {}); - } - } - } - } - - refreshComboInNode() { - const widget = this.widgets?.[0]; - if (widget?.type === "combo") { - widget.options.values = this.outputs[0].widget[GET_CONFIG]()[0]; - - if (!widget.options.values.includes(widget.value)) { - widget.value = widget.options.values[0]; - widget.callback(widget.value); - } - } - } - - onAfterGraphConfigured() { - if (this.outputs[0].links?.length && !this.widgets?.length) { - if (!this.#onFirstConnection()) return; - - // Populate widget values from config data - if (this.widgets) { - for (let i = 0; i < this.widgets_values.length; i++) { - const w = this.widgets[i]; - if (w) { - w.value = this.widgets_values[i]; - } - } - } - - // Merge values if required - this.#mergeWidgetConfig(); - } - } - - onConnectionsChange(_, index, connected) { - if (app.configuringGraph) { - // Dont run while the graph is still setting up - return; - } - - const links = this.outputs[0].links; - if (connected) { - if (links?.length && !this.widgets?.length) { - this.#onFirstConnection(); - } - } else { - // We may have removed a link that caused the constraints to change - this.#mergeWidgetConfig(); - - if (!links?.length) { - this.onLastDisconnect(); - } - } - } - - onConnectOutput(slot, type, input, target_node, target_slot) { - // Fires before the link is made allowing us to reject it if it isn't valid - // No widget, we cant connect - if (!input.widget) { - if (!(input.type in ComfyWidgets)) return false; - } - - if (this.outputs[slot].links?.length) { - const valid = this.#isValidConnection(input); - if (valid) { - // On connect of additional outputs, copy our value to their widget - this.applyToGraph([{ target_id: target_node.id, target_slot }]); - } - return valid; - } - } - - #onFirstConnection(recreating) { - // First connection can fire before the graph is ready on initial load so random things can be missing - if (!this.outputs[0].links) { - this.onLastDisconnect(); - return; - } - const linkId = this.outputs[0].links[0]; - const link = this.graph.links[linkId]; - if (!link) return; - - const theirNode = this.graph.getNodeById(link.target_id); - if (!theirNode || !theirNode.inputs) return; - - const input = theirNode.inputs[link.target_slot]; - if (!input) return; - - let widget; - if (!input.widget) { - if (!(input.type in ComfyWidgets)) return; - widget = { name: input.name, [GET_CONFIG]: () => [input.type, {}] }; //fake widget - } else { - widget = input.widget; - } - - const config = widget[GET_CONFIG]?.(); - if (!config) return; - - const { type } = getWidgetType(config); - // Update our output to restrict to the widget type - this.outputs[0].type = type; - this.outputs[0].name = type; - this.outputs[0].widget = widget; - - this.#createWidget(widget[CONFIG] ?? config, theirNode, widget.name, recreating, widget[TARGET]); - } - - #createWidget(inputData, node, widgetName, recreating, targetWidget) { - let type = inputData[0]; - - if (type instanceof Array) { - type = "COMBO"; - } - - let widget; - if (type in ComfyWidgets) { - widget = (ComfyWidgets[type](this, "value", inputData, app) || {}).widget; - } else { - widget = this.addWidget(type, "value", null, () => {}, {}); - } - - if (targetWidget) { - widget.value = targetWidget.value; - } else if (node?.widgets && widget) { - const theirWidget = node.widgets.find((w) => w.name === widgetName); - if (theirWidget) { - widget.value = theirWidget.value; - } - } - - if (!inputData?.[1]?.control_after_generate && (widget.type === "number" || widget.type === "combo")) { - let control_value = this.widgets_values?.[1]; - if (!control_value) { - control_value = "fixed"; - } - addValueControlWidgets(this, widget, control_value, undefined, inputData); - let filter = this.widgets_values?.[2]; - if (filter && this.widgets.length === 3) { - this.widgets[2].value = filter; - } - } - - // Restore any saved control values - const controlValues = this.controlValues; - if(this.lastType === this.widgets[0].type && controlValues?.length === this.widgets.length - 1) { - for(let i = 0; i < controlValues.length; i++) { - this.widgets[i + 1].value = controlValues[i]; - } - } - - // When our value changes, update other widgets to reflect our changes - // e.g. so LoadImage shows correct image - const callback = widget.callback; - const self = this; - widget.callback = function () { - const r = callback ? callback.apply(this, arguments) : undefined; - self.applyToGraph(); - return r; - }; - - if (!recreating) { - // Grow our node if required - const sz = this.computeSize(); - if (this.size[0] < sz[0]) { - this.size[0] = sz[0]; - } - if (this.size[1] < sz[1]) { - this.size[1] = sz[1]; - } - - requestAnimationFrame(() => { - if (this.onResize) { - this.onResize(this.size); - } - }); - } - } - - recreateWidget() { - const values = this.widgets?.map((w) => w.value); - this.#removeWidgets(); - this.#onFirstConnection(true); - if (values?.length) { - for (let i = 0; i < this.widgets?.length; i++) this.widgets[i].value = values[i]; - } - return this.widgets?.[0]; - } - - #mergeWidgetConfig() { - // Merge widget configs if the node has multiple outputs - const output = this.outputs[0]; - const links = output.links; - - const hasConfig = !!output.widget[CONFIG]; - if (hasConfig) { - delete output.widget[CONFIG]; - } - - if (links?.length < 2 && hasConfig) { - // Copy the widget options from the source - if (links.length) { - this.recreateWidget(); - } - - return; - } - - const config1 = output.widget[GET_CONFIG](); - const isNumber = config1[0] === "INT" || config1[0] === "FLOAT"; - if (!isNumber) return; - - for (const linkId of links) { - const link = app.graph.links[linkId]; - if (!link) continue; // Can be null when removing a node - - const theirNode = app.graph.getNodeById(link.target_id); - const theirInput = theirNode.inputs[link.target_slot]; - - // Call is valid connection so it can merge the configs when validating - this.#isValidConnection(theirInput, hasConfig); - } - } - - #isValidConnection(input, forceUpdate) { - // Only allow connections where the configs match - const output = this.outputs[0]; - const config2 = input.widget[GET_CONFIG](); - return !!mergeIfValid.call(this, output, config2, forceUpdate, this.recreateWidget); - } - - #removeWidgets() { - if (this.widgets) { - // Allow widgets to cleanup - for (const w of this.widgets) { - if (w.onRemove) { - w.onRemove(); - } - } - - // Temporarily store the current values in case the node is being recreated - // e.g. by group node conversion - this.controlValues = []; - this.lastType = this.widgets[0]?.type; - for(let i = 1; i < this.widgets.length; i++) { - this.controlValues.push(this.widgets[i].value); - } - setTimeout(() => { delete this.lastType; delete this.controlValues }, 15); - this.widgets.length = 0; - } - } - - onLastDisconnect() { - // We cant remove + re-add the output here as if you drag a link over the same link - // it removes, then re-adds, causing it to break - this.outputs[0].type = "*"; - this.outputs[0].name = "connect to widget input"; - delete this.outputs[0].widget; - - this.#removeWidgets(); - } - } - - LiteGraph.registerNodeType( - "PrimitiveNode", - Object.assign(PrimitiveNode, { - title: "Primitive", - }) - ); - PrimitiveNode.category = "utils"; - }, -}); diff --git a/web/extensions/logging.js.example b/web/extensions/logging.js.example deleted file mode 100644 index d015096a2..000000000 --- a/web/extensions/logging.js.example +++ /dev/null @@ -1,55 +0,0 @@ -import { app } from "../scripts/app.js"; - -const ext = { - // Unique name for the extension - name: "Example.LoggingExtension", - async init(app) { - // Any initial setup to run as soon as the page loads - console.log("[logging]", "extension init"); - }, - async setup(app) { - // Any setup to run after the app is created - console.log("[logging]", "extension setup"); - }, - async addCustomNodeDefs(defs, app) { - // Add custom node definitions - // These definitions will be configured and registered automatically - // defs is a lookup core nodes, add yours into this - console.log("[logging]", "add custom node definitions", "current nodes:", Object.keys(defs)); - }, - async getCustomWidgets(app) { - // Return custom widget types - // See ComfyWidgets for widget examples - console.log("[logging]", "provide custom widgets"); - }, - async beforeRegisterNodeDef(nodeType, nodeData, app) { - // Run custom logic before a node definition is registered with the graph - console.log("[logging]", "before register node: ", nodeType, nodeData); - - // This fires for every node definition so only log once - delete ext.beforeRegisterNodeDef; - }, - async registerCustomNodes(app) { - // Register any custom node implementations here allowing for more flexability than a custom node def - console.log("[logging]", "register custom nodes"); - }, - loadedGraphNode(node, app) { - // Fires for each node when loading/dragging/etc a workflow json or png - // If you break something in the backend and want to patch workflows in the frontend - // This is the place to do this - console.log("[logging]", "loaded graph node: ", node); - - // This fires for every node on each load so only log once - delete ext.loadedGraphNode; - }, - nodeCreated(node, app) { - // Fires every time a node is constructed - // You can modify widgets/add handlers/etc here - console.log("[logging]", "node created: ", node); - - // This fires for every node so only log once - delete ext.nodeCreated; - } -}; - -app.registerExtension(ext); diff --git a/web/fonts/materialdesignicons-webfont.woff2 b/web/fonts/materialdesignicons-webfont.woff2 deleted file mode 100644 index 8c69b85f666bf3996d7e3194623bf53e774c44a3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 403216 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

A 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

~kC(4S|_D1W! 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%0A?P6XIbsbH%^bgaA!ca7le#O1f9ZxwdH9s;d7b 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 diff --git a/web/index.html b/web/index.html deleted file mode 100644 index 5ebf03c01..000000000 --- a/web/index.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - - ComfyUI - - - - - - - - - - -

- - diff --git a/web/jsconfig.json b/web/jsconfig.json deleted file mode 100644 index cd55a0cc7..000000000 --- a/web/jsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "baseUrl": ".", - "paths": { - "/*": ["./*"] - }, - "lib": ["DOM", "ES2022", "DOM.Iterable"], - "target": "ES2015", - "module": "es2020" - }, - "include": ["."] -} diff --git a/web/lib/litegraph.core.js b/web/lib/litegraph.core.js deleted file mode 100644 index 427a62b59..000000000 --- a/web/lib/litegraph.core.js +++ /dev/null @@ -1,14424 +0,0 @@ -//packer version - - -(function(global) { - // ************************************************************* - // LiteGraph CLASS ******* - // ************************************************************* - - /** - * The Global Scope. It contains all the registered node classes. - * - * @class LiteGraph - * @constructor - */ - - var LiteGraph = (global.LiteGraph = { - VERSION: 0.4, - - CANVAS_GRID_SIZE: 10, - - NODE_TITLE_HEIGHT: 30, - NODE_TITLE_TEXT_Y: 20, - NODE_SLOT_HEIGHT: 20, - NODE_WIDGET_HEIGHT: 20, - NODE_WIDTH: 140, - NODE_MIN_WIDTH: 50, - NODE_COLLAPSED_RADIUS: 10, - NODE_COLLAPSED_WIDTH: 80, - NODE_TITLE_COLOR: "#999", - NODE_SELECTED_TITLE_COLOR: "#FFF", - NODE_TEXT_SIZE: 14, - NODE_TEXT_COLOR: "#AAA", - NODE_SUBTEXT_SIZE: 12, - NODE_DEFAULT_COLOR: "#333", - NODE_DEFAULT_BGCOLOR: "#353535", - NODE_DEFAULT_BOXCOLOR: "#666", - NODE_DEFAULT_SHAPE: "box", - NODE_BOX_OUTLINE_COLOR: "#FFF", - DEFAULT_SHADOW_COLOR: "rgba(0,0,0,0.5)", - DEFAULT_GROUP_FONT: 24, - - WIDGET_BGCOLOR: "#222", - WIDGET_OUTLINE_COLOR: "#666", - WIDGET_TEXT_COLOR: "#DDD", - WIDGET_SECONDARY_TEXT_COLOR: "#999", - - LINK_COLOR: "#9A9", - EVENT_LINK_COLOR: "#A86", - CONNECTING_LINK_COLOR: "#AFA", - - MAX_NUMBER_OF_NODES: 10000, //avoid infinite loops - DEFAULT_POSITION: [100, 100], //default node position - VALID_SHAPES: ["default", "box", "round", "card"], //,"circle" - - //shapes are used for nodes but also for slots - BOX_SHAPE: 1, - ROUND_SHAPE: 2, - CIRCLE_SHAPE: 3, - CARD_SHAPE: 4, - ARROW_SHAPE: 5, - GRID_SHAPE: 6, // intended for slot arrays - - //enums - INPUT: 1, - OUTPUT: 2, - - EVENT: -1, //for outputs - ACTION: -1, //for inputs - - NODE_MODES: ["Always", "On Event", "Never", "On Trigger"], // helper, will add "On Request" and more in the future - NODE_MODES_COLORS:["#666","#422","#333","#224","#626"], // use with node_box_coloured_by_mode - ALWAYS: 0, - ON_EVENT: 1, - NEVER: 2, - ON_TRIGGER: 3, - - UP: 1, - DOWN: 2, - LEFT: 3, - RIGHT: 4, - CENTER: 5, - - LINK_RENDER_MODES: ["Straight", "Linear", "Spline"], // helper - STRAIGHT_LINK: 0, - LINEAR_LINK: 1, - SPLINE_LINK: 2, - - NORMAL_TITLE: 0, - NO_TITLE: 1, - TRANSPARENT_TITLE: 2, - AUTOHIDE_TITLE: 3, - VERTICAL_LAYOUT: "vertical", // arrange nodes vertically - - proxy: null, //used to redirect calls - node_images_path: "", - - debug: false, - catch_exceptions: true, - throw_errors: true, - allow_scripts: false, //if set to true some nodes like Formula would be allowed to evaluate code that comes from unsafe sources (like node configuration), which could lead to exploits - registered_node_types: {}, //nodetypes by string - node_types_by_file_extension: {}, //used for dropping files in the canvas - Nodes: {}, //node types by classname - Globals: {}, //used to store vars between graphs - - searchbox_extras: {}, //used to add extra features to the search box - auto_sort_node_types: false, // [true!] If set to true, will automatically sort node types / categories in the context menus - - node_box_coloured_when_on: false, // [true!] this make the nodes box (top left circle) coloured when triggered (execute/action), visual feedback - node_box_coloured_by_mode: false, // [true!] nodebox based on node mode, visual feedback - - dialog_close_on_mouse_leave: false, // [false on mobile] better true if not touch device, TODO add an helper/listener to close if false - dialog_close_on_mouse_leave_delay: 500, - - shift_click_do_break_link_from: false, // [false!] prefer false if results too easy to break links - implement with ALT or TODO custom keys - click_do_break_link_to: false, // [false!]prefer false, way too easy to break links - - search_hide_on_mouse_leave: true, // [false on mobile] better true if not touch device, TODO add an helper/listener to close if false - search_filter_enabled: false, // [true!] enable filtering slots type in the search widget, !requires auto_load_slot_types or manual set registered_slot_[in/out]_types and slot_types_[in/out] - search_show_all_on_open: true, // [true!] opens the results list when opening the search widget - - auto_load_slot_types: false, // [if want false, use true, run, get vars values to be statically set, than disable] nodes types and nodeclass association with node types need to be calculated, if dont want this, calculate once and set registered_slot_[in/out]_types and slot_types_[in/out] - - // set these values if not using auto_load_slot_types - registered_slot_in_types: {}, // slot types for nodeclass - registered_slot_out_types: {}, // slot types for nodeclass - slot_types_in: [], // slot types IN - slot_types_out: [], // slot types OUT - slot_types_default_in: [], // specify for each IN slot type a(/many) default node(s), use single string, array, or object (with node, title, parameters, ..) like for search - slot_types_default_out: [], // specify for each OUT slot type a(/many) default node(s), use single string, array, or object (with node, title, parameters, ..) like for search - - alt_drag_do_clone_nodes: false, // [true!] very handy, ALT click to clone and drag the new node - - do_add_triggers_slots: false, // [true!] will create and connect event slots when using action/events connections, !WILL CHANGE node mode when using onTrigger (enable mode colors), onExecuted does not need this - - allow_multi_output_for_events: true, // [false!] being events, it is strongly reccomended to use them sequentially, one by one - - middle_click_slot_add_default_node: false, //[true!] allows to create and connect a ndoe clicking with the third button (wheel) - - release_link_on_empty_shows_menu: false, //[true!] dragging a link to empty space will open a menu, add from list, search or defaults - - pointerevents_method: "pointer", // "mouse"|"pointer" use mouse for retrocompatibility issues? (none found @ now) - // TODO implement pointercancel, gotpointercapture, lostpointercapture, (pointerover, pointerout if necessary) - - ctrl_shift_v_paste_connect_unselected_outputs: true, //[true!] allows ctrl + shift + v to paste nodes with the outputs of the unselected nodes connected with the inputs of the newly pasted nodes - - // if true, all newly created nodes/links will use string UUIDs for their id fields instead of integers. - // use this if you must have node IDs that are unique across all graphs and subgraphs. - use_uuids: false, - - /** - * Register a node class so it can be listed when the user wants to create a new one - * @method registerNodeType - * @param {String} type name of the node and path - * @param {Class} base_class class containing the structure of a node - */ - - registerNodeType: function(type, base_class) { - if (!base_class.prototype) { - throw "Cannot register a simple object, it must be a class with a prototype"; - } - base_class.type = type; - - if (LiteGraph.debug) { - console.log("Node registered: " + type); - } - - const classname = base_class.name; - - const pos = type.lastIndexOf("/"); - base_class.category = type.substring(0, pos); - - if (!base_class.title) { - base_class.title = classname; - } - - //extend class - for (var i in LGraphNode.prototype) { - if (!base_class.prototype[i]) { - base_class.prototype[i] = LGraphNode.prototype[i]; - } - } - - const prev = this.registered_node_types[type]; - if(prev) { - console.log("replacing node type: " + type); - } - if( !Object.prototype.hasOwnProperty.call( base_class.prototype, "shape") ) { - Object.defineProperty(base_class.prototype, "shape", { - set: function(v) { - switch (v) { - case "default": - delete this._shape; - break; - case "box": - this._shape = LiteGraph.BOX_SHAPE; - break; - case "round": - this._shape = LiteGraph.ROUND_SHAPE; - break; - case "circle": - this._shape = LiteGraph.CIRCLE_SHAPE; - break; - case "card": - this._shape = LiteGraph.CARD_SHAPE; - break; - default: - this._shape = v; - } - }, - get: function() { - return this._shape; - }, - enumerable: true, - configurable: true - }); - - - //used to know which nodes to create when dragging files to the canvas - if (base_class.supported_extensions) { - for (let i in base_class.supported_extensions) { - const ext = base_class.supported_extensions[i]; - if(ext && ext.constructor === String) { - this.node_types_by_file_extension[ ext.toLowerCase() ] = base_class; - } - } - } - } - - this.registered_node_types[type] = base_class; - if (base_class.constructor.name) { - this.Nodes[classname] = base_class; - } - if (LiteGraph.onNodeTypeRegistered) { - LiteGraph.onNodeTypeRegistered(type, base_class); - } - if (prev && LiteGraph.onNodeTypeReplaced) { - LiteGraph.onNodeTypeReplaced(type, base_class, prev); - } - - //warnings - if (base_class.prototype.onPropertyChange) { - console.warn( - "LiteGraph node class " + - type + - " has onPropertyChange method, it must be called onPropertyChanged with d at the end" - ); - } - - // TODO one would want to know input and ouput :: this would allow through registerNodeAndSlotType to get all the slots types - if (this.auto_load_slot_types) { - new base_class(base_class.title || "tmpnode"); - } - }, - - /** - * removes a node type from the system - * @method unregisterNodeType - * @param {String|Object} type name of the node or the node constructor itself - */ - unregisterNodeType: function(type) { - const base_class = - type.constructor === String - ? this.registered_node_types[type] - : type; - if (!base_class) { - throw "node type not found: " + type; - } - delete this.registered_node_types[base_class.type]; - if (base_class.constructor.name) { - delete this.Nodes[base_class.constructor.name]; - } - }, - - /** - * Save a slot type and his node - * @method registerSlotType - * @param {String|Object} type name of the node or the node constructor itself - * @param {String} slot_type name of the slot type (variable type), eg. string, number, array, boolean, .. - */ - registerNodeAndSlotType: function(type, slot_type, out){ - out = out || false; - const base_class = - type.constructor === String && - this.registered_node_types[type] !== "anonymous" - ? this.registered_node_types[type] - : type; - - const class_type = base_class.constructor.type; - - let allTypes = []; - if (typeof slot_type === "string") { - allTypes = slot_type.split(","); - } else if (slot_type == this.EVENT || slot_type == this.ACTION) { - allTypes = ["_event_"]; - } else { - allTypes = ["*"]; - } - - for (let i = 0; i < allTypes.length; ++i) { - let slotType = allTypes[i]; - if (slotType === "") { - slotType = "*"; - } - const registerTo = out - ? "registered_slot_out_types" - : "registered_slot_in_types"; - if (this[registerTo][slotType] === undefined) { - this[registerTo][slotType] = { nodes: [] }; - } - if (!this[registerTo][slotType].nodes.includes(class_type)) { - this[registerTo][slotType].nodes.push(class_type); - } - - // check if is a new type - if (!out) { - if (!this.slot_types_in.includes(slotType.toLowerCase())) { - this.slot_types_in.push(slotType.toLowerCase()); - this.slot_types_in.sort(); - } - } else { - if (!this.slot_types_out.includes(slotType.toLowerCase())) { - this.slot_types_out.push(slotType.toLowerCase()); - this.slot_types_out.sort(); - } - } - } - }, - - /** - * Create a new nodetype by passing a function, it wraps it with a proper class and generates inputs according to the parameters of the function. - * Useful to wrap simple methods that do not require properties, and that only process some input to generate an output. - * @method wrapFunctionAsNode - * @param {String} name node name with namespace (p.e.: 'math/sum') - * @param {Function} func - * @param {Array} param_types [optional] an array containing the type of every parameter, otherwise parameters will accept any type - * @param {String} return_type [optional] string with the return type, otherwise it will be generic - * @param {Object} properties [optional] properties to be configurable - */ - wrapFunctionAsNode: function( - name, - func, - param_types, - return_type, - properties - ) { - var params = Array(func.length); - var code = ""; - var names = LiteGraph.getParameterNames(func); - for (var i = 0; i < names.length; ++i) { - code += - "this.addInput('" + - names[i] + - "'," + - (param_types && param_types[i] - ? "'" + param_types[i] + "'" - : "0") + - ");\n"; - } - code += - "this.addOutput('out'," + - (return_type ? "'" + return_type + "'" : 0) + - ");\n"; - if (properties) { - code += - "this.properties = " + JSON.stringify(properties) + ";\n"; - } - var classobj = Function(code); - classobj.title = name.split("/").pop(); - classobj.desc = "Generated from " + func.name; - classobj.prototype.onExecute = function onExecute() { - for (var i = 0; i < params.length; ++i) { - params[i] = this.getInputData(i); - } - var r = func.apply(this, params); - this.setOutputData(0, r); - }; - this.registerNodeType(name, classobj); - }, - - /** - * Removes all previously registered node's types - */ - clearRegisteredTypes: function() { - this.registered_node_types = {}; - this.node_types_by_file_extension = {}; - this.Nodes = {}; - this.searchbox_extras = {}; - }, - - /** - * Adds this method to all nodetypes, existing and to be created - * (You can add it to LGraphNode.prototype but then existing node types wont have it) - * @method addNodeMethod - * @param {Function} func - */ - addNodeMethod: function(name, func) { - LGraphNode.prototype[name] = func; - for (var i in this.registered_node_types) { - var type = this.registered_node_types[i]; - if (type.prototype[name]) { - type.prototype["_" + name] = type.prototype[name]; - } //keep old in case of replacing - type.prototype[name] = func; - } - }, - - /** - * Create a node of a given type with a name. The node is not attached to any graph yet. - * @method createNode - * @param {String} type full name of the node class. p.e. "math/sin" - * @param {String} name a name to distinguish from other nodes - * @param {Object} options to set options - */ - - createNode: function(type, title, options) { - var base_class = this.registered_node_types[type]; - if (!base_class) { - if (LiteGraph.debug) { - console.log( - 'GraphNode type "' + type + '" not registered.' - ); - } - return null; - } - - var prototype = base_class.prototype || base_class; - - title = title || base_class.title || type; - - var node = null; - - if (LiteGraph.catch_exceptions) { - try { - node = new base_class(title); - } catch (err) { - console.error(err); - return null; - } - } else { - node = new base_class(title); - } - - node.type = type; - - if (!node.title && title) { - node.title = title; - } - if (!node.properties) { - node.properties = {}; - } - if (!node.properties_info) { - node.properties_info = []; - } - if (!node.flags) { - node.flags = {}; - } - if (!node.size) { - node.size = node.computeSize(); - //call onresize? - } - if (!node.pos) { - node.pos = LiteGraph.DEFAULT_POSITION.concat(); - } - if (!node.mode) { - node.mode = LiteGraph.ALWAYS; - } - - //extra options - if (options) { - for (var i in options) { - node[i] = options[i]; - } - } - - // callback - if ( node.onNodeCreated ) { - node.onNodeCreated(); - } - - return node; - }, - - /** - * Returns a registered node type with a given name - * @method getNodeType - * @param {String} type full name of the node class. p.e. "math/sin" - * @return {Class} the node class - */ - getNodeType: function(type) { - return this.registered_node_types[type]; - }, - - /** - * Returns a list of node types matching one category - * @method getNodeType - * @param {String} category category name - * @return {Array} array with all the node classes - */ - - getNodeTypesInCategory: function(category, filter) { - var r = []; - for (var i in this.registered_node_types) { - var type = this.registered_node_types[i]; - if (type.filter != filter) { - continue; - } - - if (category == "") { - if (type.category == null) { - r.push(type); - } - } else if (type.category == category) { - r.push(type); - } - } - - if (this.auto_sort_node_types) { - r.sort(function(a,b){return a.title.localeCompare(b.title)}); - } - - return r; - }, - - /** - * Returns a list with all the node type categories - * @method getNodeTypesCategories - * @param {String} filter only nodes with ctor.filter equal can be shown - * @return {Array} array with all the names of the categories - */ - getNodeTypesCategories: function( filter ) { - var categories = { "": 1 }; - for (var i in this.registered_node_types) { - var type = this.registered_node_types[i]; - if ( type.category && !type.skip_list ) - { - if(type.filter != filter) - continue; - categories[type.category] = 1; - } - } - var result = []; - for (var i in categories) { - result.push(i); - } - return this.auto_sort_node_types ? result.sort() : result; - }, - - //debug purposes: reloads all the js scripts that matches a wildcard - reloadNodes: function(folder_wildcard) { - var tmp = document.getElementsByTagName("script"); - //weird, this array changes by its own, so we use a copy - var script_files = []; - for (var i=0; i < tmp.length; i++) { - script_files.push(tmp[i]); - } - - var docHeadObj = document.getElementsByTagName("head")[0]; - folder_wildcard = document.location.href + folder_wildcard; - - for (var i=0; i < script_files.length; i++) { - var src = script_files[i].src; - if ( - !src || - src.substr(0, folder_wildcard.length) != folder_wildcard - ) { - continue; - } - - try { - if (LiteGraph.debug) { - console.log("Reloading: " + src); - } - var dynamicScript = document.createElement("script"); - dynamicScript.type = "text/javascript"; - dynamicScript.src = src; - docHeadObj.appendChild(dynamicScript); - docHeadObj.removeChild(script_files[i]); - } catch (err) { - if (LiteGraph.throw_errors) { - throw err; - } - if (LiteGraph.debug) { - console.log("Error while reloading " + src); - } - } - } - - if (LiteGraph.debug) { - console.log("Nodes reloaded"); - } - }, - - //separated just to improve if it doesn't work - cloneObject: function(obj, target) { - if (obj == null) { - return null; - } - var r = JSON.parse(JSON.stringify(obj)); - if (!target) { - return r; - } - - for (var i in r) { - target[i] = r[i]; - } - return target; - }, - - /* - * https://gist.github.com/jed/982883?permalink_comment_id=852670#gistcomment-852670 - */ - uuidv4: function() { - return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,a=>(a^Math.random()*16>>a/4).toString(16)); - }, - - /** - * Returns if the types of two slots are compatible (taking into account wildcards, etc) - * @method isValidConnection - * @param {String} type_a - * @param {String} type_b - * @return {Boolean} true if they can be connected - */ - isValidConnection: function(type_a, type_b) { - if (type_a=="" || type_a==="*") type_a = 0; - if (type_b=="" || type_b==="*") type_b = 0; - if ( - !type_a //generic output - || !type_b // generic input - || type_a == type_b //same type (is valid for triggers) - || (type_a == LiteGraph.EVENT && type_b == LiteGraph.ACTION) - ) { - return true; - } - - // Enforce string type to handle toLowerCase call (-1 number not ok) - type_a = String(type_a); - type_b = String(type_b); - type_a = type_a.toLowerCase(); - type_b = type_b.toLowerCase(); - - // For nodes supporting multiple connection types - if (type_a.indexOf(",") == -1 && type_b.indexOf(",") == -1) { - return type_a == type_b; - } - - // Check all permutations to see if one is valid - var supported_types_a = type_a.split(","); - var supported_types_b = type_b.split(","); - for (var i = 0; i < supported_types_a.length; ++i) { - for (var j = 0; j < supported_types_b.length; ++j) { - if(this.isValidConnection(supported_types_a[i],supported_types_b[j])){ - //if (supported_types_a[i] == supported_types_b[j]) { - return true; - } - } - } - - return false; - }, - - /** - * Register a string in the search box so when the user types it it will recommend this node - * @method registerSearchboxExtra - * @param {String} node_type the node recommended - * @param {String} description text to show next to it - * @param {Object} data it could contain info of how the node should be configured - * @return {Boolean} true if they can be connected - */ - registerSearchboxExtra: function(node_type, description, data) { - this.searchbox_extras[description.toLowerCase()] = { - type: node_type, - desc: description, - data: data - }; - }, - - /** - * Wrapper to load files (from url using fetch or from file using FileReader) - * @method fetchFile - * @param {String|File|Blob} url the url of the file (or the file itself) - * @param {String} type an string to know how to fetch it: "text","arraybuffer","json","blob" - * @param {Function} on_complete callback(data) - * @param {Function} on_error in case of an error - * @return {FileReader|Promise} returns the object used to - */ - fetchFile: function( url, type, on_complete, on_error ) { - var that = this; - if(!url) - return null; - - type = type || "text"; - if( url.constructor === String ) - { - if (url.substr(0, 4) == "http" && LiteGraph.proxy) { - url = LiteGraph.proxy + url.substr(url.indexOf(":") + 3); - } - return fetch(url) - .then(function(response) { - if(!response.ok) - throw new Error("File not found"); //it will be catch below - if(type == "arraybuffer") - return response.arrayBuffer(); - else if(type == "text" || type == "string") - return response.text(); - else if(type == "json") - return response.json(); - else if(type == "blob") - return response.blob(); - }) - .then(function(data) { - if(on_complete) - on_complete(data); - }) - .catch(function(error) { - console.error("error fetching file:",url); - if(on_error) - on_error(error); - }); - } - else if( url.constructor === File || url.constructor === Blob) - { - var reader = new FileReader(); - reader.onload = function(e) - { - var v = e.target.result; - if( type == "json" ) - v = JSON.parse(v); - if(on_complete) - on_complete(v); - } - if(type == "arraybuffer") - return reader.readAsArrayBuffer(url); - else if(type == "text" || type == "json") - return reader.readAsText(url); - else if(type == "blob") - return reader.readAsBinaryString(url); - } - return null; - } - }); - - //timer that works everywhere - if (typeof performance != "undefined") { - LiteGraph.getTime = performance.now.bind(performance); - } else if (typeof Date != "undefined" && Date.now) { - LiteGraph.getTime = Date.now.bind(Date); - } else if (typeof process != "undefined") { - LiteGraph.getTime = function() { - var t = process.hrtime(); - return t[0] * 0.001 + t[1] * 1e-6; - }; - } else { - LiteGraph.getTime = function getTime() { - return new Date().getTime(); - }; - } - - //********************************************************************************* - // LGraph CLASS - //********************************************************************************* - - /** - * LGraph is the class that contain a full graph. We instantiate one and add nodes to it, and then we can run the execution loop. - * supported callbacks: - + onNodeAdded: when a new node is added to the graph - + onNodeRemoved: when a node inside this graph is removed - + onNodeConnectionChange: some connection has changed in the graph (connected or disconnected) - * - * @class LGraph - * @constructor - * @param {Object} o data from previous serialization [optional] - */ - - function LGraph(o) { - if (LiteGraph.debug) { - console.log("Graph created"); - } - this.list_of_graphcanvas = null; - this.clear(); - - if (o) { - this.configure(o); - } - } - - global.LGraph = LiteGraph.LGraph = LGraph; - - //default supported types - LGraph.supported_types = ["number", "string", "boolean"]; - - //used to know which types of connections support this graph (some graphs do not allow certain types) - LGraph.prototype.getSupportedTypes = function() { - return this.supported_types || LGraph.supported_types; - }; - - LGraph.STATUS_STOPPED = 1; - LGraph.STATUS_RUNNING = 2; - - /** - * Removes all nodes from this graph - * @method clear - */ - - LGraph.prototype.clear = function() { - this.stop(); - this.status = LGraph.STATUS_STOPPED; - - this.last_node_id = 0; - this.last_link_id = 0; - - this._version = -1; //used to detect changes - - //safe clear - if (this._nodes) { - for (var i = 0; i < this._nodes.length; ++i) { - var node = this._nodes[i]; - if (node.onRemoved) { - node.onRemoved(); - } - } - } - - //nodes - this._nodes = []; - this._nodes_by_id = {}; - this._nodes_in_order = []; //nodes sorted in execution order - this._nodes_executable = null; //nodes that contain onExecute sorted in execution order - - //other scene stuff - this._groups = []; - - //links - this.links = {}; //container with all the links - - //iterations - this.iteration = 0; - - //custom data - this.config = {}; - this.vars = {}; - this.extra = {}; //to store custom data - - //timing - this.globaltime = 0; - this.runningtime = 0; - this.fixedtime = 0; - this.fixedtime_lapse = 0.01; - this.elapsed_time = 0.01; - this.last_update_time = 0; - this.starttime = 0; - - this.catch_errors = true; - - this.nodes_executing = []; - this.nodes_actioning = []; - this.nodes_executedAction = []; - - //subgraph_data - this.inputs = {}; - this.outputs = {}; - - //notify canvas to redraw - this.change(); - - this.sendActionToCanvas("clear"); - }; - - /** - * Attach Canvas to this graph - * @method attachCanvas - * @param {GraphCanvas} graph_canvas - */ - - LGraph.prototype.attachCanvas = function(graphcanvas) { - if (graphcanvas.constructor != LGraphCanvas) { - throw "attachCanvas expects a LGraphCanvas instance"; - } - if (graphcanvas.graph && graphcanvas.graph != this) { - graphcanvas.graph.detachCanvas(graphcanvas); - } - - graphcanvas.graph = this; - - if (!this.list_of_graphcanvas) { - this.list_of_graphcanvas = []; - } - this.list_of_graphcanvas.push(graphcanvas); - }; - - /** - * Detach Canvas from this graph - * @method detachCanvas - * @param {GraphCanvas} graph_canvas - */ - LGraph.prototype.detachCanvas = function(graphcanvas) { - if (!this.list_of_graphcanvas) { - return; - } - - var pos = this.list_of_graphcanvas.indexOf(graphcanvas); - if (pos == -1) { - return; - } - graphcanvas.graph = null; - this.list_of_graphcanvas.splice(pos, 1); - }; - - /** - * Starts running this graph every interval milliseconds. - * @method start - * @param {number} interval amount of milliseconds between executions, if 0 then it renders to the monitor refresh rate - */ - - LGraph.prototype.start = function(interval) { - if (this.status == LGraph.STATUS_RUNNING) { - return; - } - this.status = LGraph.STATUS_RUNNING; - - if (this.onPlayEvent) { - this.onPlayEvent(); - } - - this.sendEventToAllNodes("onStart"); - - //launch - this.starttime = LiteGraph.getTime(); - this.last_update_time = this.starttime; - interval = interval || 0; - var that = this; - - //execute once per frame - if ( interval == 0 && typeof window != "undefined" && window.requestAnimationFrame ) { - function on_frame() { - if (that.execution_timer_id != -1) { - return; - } - window.requestAnimationFrame(on_frame); - if(that.onBeforeStep) - that.onBeforeStep(); - that.runStep(1, !that.catch_errors); - if(that.onAfterStep) - that.onAfterStep(); - } - this.execution_timer_id = -1; - on_frame(); - } else { //execute every 'interval' ms - this.execution_timer_id = setInterval(function() { - //execute - if(that.onBeforeStep) - that.onBeforeStep(); - that.runStep(1, !that.catch_errors); - if(that.onAfterStep) - that.onAfterStep(); - }, interval); - } - }; - - /** - * Stops the execution loop of the graph - * @method stop execution - */ - - LGraph.prototype.stop = function() { - if (this.status == LGraph.STATUS_STOPPED) { - return; - } - - this.status = LGraph.STATUS_STOPPED; - - if (this.onStopEvent) { - this.onStopEvent(); - } - - if (this.execution_timer_id != null) { - if (this.execution_timer_id != -1) { - clearInterval(this.execution_timer_id); - } - this.execution_timer_id = null; - } - - this.sendEventToAllNodes("onStop"); - }; - - /** - * Run N steps (cycles) of the graph - * @method runStep - * @param {number} num number of steps to run, default is 1 - * @param {Boolean} do_not_catch_errors [optional] if you want to try/catch errors - * @param {number} limit max number of nodes to execute (used to execute from start to a node) - */ - - LGraph.prototype.runStep = function(num, do_not_catch_errors, limit ) { - num = num || 1; - - var start = LiteGraph.getTime(); - this.globaltime = 0.001 * (start - this.starttime); - - var nodes = this._nodes_executable - ? this._nodes_executable - : this._nodes; - if (!nodes) { - return; - } - - limit = limit || nodes.length; - - if (do_not_catch_errors) { - //iterations - for (var i = 0; i < num; i++) { - for (var j = 0; j < limit; ++j) { - var node = nodes[j]; - if (node.mode == LiteGraph.ALWAYS && node.onExecute) { - //wrap node.onExecute(); - node.doExecute(); - } - } - - this.fixedtime += this.fixedtime_lapse; - if (this.onExecuteStep) { - this.onExecuteStep(); - } - } - - if (this.onAfterExecute) { - this.onAfterExecute(); - } - } else { - try { - //iterations - for (var i = 0; i < num; i++) { - for (var j = 0; j < limit; ++j) { - var node = nodes[j]; - if (node.mode == LiteGraph.ALWAYS && node.onExecute) { - node.onExecute(); - } - } - - this.fixedtime += this.fixedtime_lapse; - if (this.onExecuteStep) { - this.onExecuteStep(); - } - } - - if (this.onAfterExecute) { - this.onAfterExecute(); - } - this.errors_in_execution = false; - } catch (err) { - this.errors_in_execution = true; - if (LiteGraph.throw_errors) { - throw err; - } - if (LiteGraph.debug) { - console.log("Error during execution: " + err); - } - this.stop(); - } - } - - var now = LiteGraph.getTime(); - var elapsed = now - start; - if (elapsed == 0) { - elapsed = 1; - } - this.execution_time = 0.001 * elapsed; - this.globaltime += 0.001 * elapsed; - this.iteration += 1; - this.elapsed_time = (now - this.last_update_time) * 0.001; - this.last_update_time = now; - this.nodes_executing = []; - this.nodes_actioning = []; - this.nodes_executedAction = []; - }; - - /** - * Updates the graph execution order according to relevance of the nodes (nodes with only outputs have more relevance than - * nodes with only inputs. - * @method updateExecutionOrder - */ - LGraph.prototype.updateExecutionOrder = function() { - this._nodes_in_order = this.computeExecutionOrder(false); - this._nodes_executable = []; - for (var i = 0; i < this._nodes_in_order.length; ++i) { - if (this._nodes_in_order[i].onExecute) { - this._nodes_executable.push(this._nodes_in_order[i]); - } - } - }; - - //This is more internal, it computes the executable nodes in order and returns it - LGraph.prototype.computeExecutionOrder = function( - only_onExecute, - set_level - ) { - var L = []; - var S = []; - var M = {}; - var visited_links = {}; //to avoid repeating links - var remaining_links = {}; //to a - - //search for the nodes without inputs (starting nodes) - for (var i = 0, l = this._nodes.length; i < l; ++i) { - var node = this._nodes[i]; - if (only_onExecute && !node.onExecute) { - continue; - } - - M[node.id] = node; //add to pending nodes - - var num = 0; //num of input connections - if (node.inputs) { - for (var j = 0, l2 = node.inputs.length; j < l2; j++) { - if (node.inputs[j] && node.inputs[j].link != null) { - num += 1; - } - } - } - - if (num == 0) { - //is a starting node - S.push(node); - if (set_level) { - node._level = 1; - } - } //num of input links - else { - if (set_level) { - node._level = 0; - } - remaining_links[node.id] = num; - } - } - - while (true) { - if (S.length == 0) { - break; - } - - //get an starting node - var node = S.shift(); - L.push(node); //add to ordered list - delete M[node.id]; //remove from the pending nodes - - if (!node.outputs) { - continue; - } - - //for every output - for (var i = 0; i < node.outputs.length; i++) { - var output = node.outputs[i]; - //not connected - if ( - output == null || - output.links == null || - output.links.length == 0 - ) { - continue; - } - - //for every connection - for (var j = 0; j < output.links.length; j++) { - var link_id = output.links[j]; - var link = this.links[link_id]; - if (!link) { - continue; - } - - //already visited link (ignore it) - if (visited_links[link.id]) { - continue; - } - - var target_node = this.getNodeById(link.target_id); - if (target_node == null) { - visited_links[link.id] = true; - continue; - } - - if ( - set_level && - (!target_node._level || - target_node._level <= node._level) - ) { - target_node._level = node._level + 1; - } - - visited_links[link.id] = true; //mark as visited - remaining_links[target_node.id] -= 1; //reduce the number of links remaining - if (remaining_links[target_node.id] == 0) { - S.push(target_node); - } //if no more links, then add to starters array - } - } - } - - //the remaining ones (loops) - for (var i in M) { - L.push(M[i]); - } - - if (L.length != this._nodes.length && LiteGraph.debug) { - console.warn("something went wrong, nodes missing"); - } - - var l = L.length; - - //save order number in the node - for (var i = 0; i < l; ++i) { - L[i].order = i; - } - - //sort now by priority - L = L.sort(function(A, B) { - var Ap = A.constructor.priority || A.priority || 0; - var Bp = B.constructor.priority || B.priority || 0; - if (Ap == Bp) { - //if same priority, sort by order - return A.order - B.order; - } - return Ap - Bp; //sort by priority - }); - - //save order number in the node, again... - for (var i = 0; i < l; ++i) { - L[i].order = i; - } - - return L; - }; - - /** - * Returns all the nodes that could affect this one (ancestors) by crawling all the inputs recursively. - * It doesn't include the node itself - * @method getAncestors - * @return {Array} an array with all the LGraphNodes that affect this node, in order of execution - */ - LGraph.prototype.getAncestors = function(node) { - var ancestors = []; - var pending = [node]; - var visited = {}; - - while (pending.length) { - var current = pending.shift(); - if (!current.inputs) { - continue; - } - if (!visited[current.id] && current != node) { - visited[current.id] = true; - ancestors.push(current); - } - - for (var i = 0; i < current.inputs.length; ++i) { - var input = current.getInputNode(i); - if (input && ancestors.indexOf(input) == -1) { - pending.push(input); - } - } - } - - ancestors.sort(function(a, b) { - return a.order - b.order; - }); - return ancestors; - }; - - /** - * Positions every node in a more readable manner - * @method arrange - */ - LGraph.prototype.arrange = function (margin, layout) { - margin = margin || 100; - - const nodes = this.computeExecutionOrder(false, true); - const columns = []; - for (let i = 0; i < nodes.length; ++i) { - const node = nodes[i]; - const col = node._level || 1; - if (!columns[col]) { - columns[col] = []; - } - columns[col].push(node); - } - - let x = margin; - - for (let i = 0; i < columns.length; ++i) { - const column = columns[i]; - if (!column) { - continue; - } - let max_size = 100; - let y = margin + LiteGraph.NODE_TITLE_HEIGHT; - for (let j = 0; j < column.length; ++j) { - const node = column[j]; - node.pos[0] = (layout == LiteGraph.VERTICAL_LAYOUT) ? y : x; - node.pos[1] = (layout == LiteGraph.VERTICAL_LAYOUT) ? x : y; - const max_size_index = (layout == LiteGraph.VERTICAL_LAYOUT) ? 1 : 0; - if (node.size[max_size_index] > max_size) { - max_size = node.size[max_size_index]; - } - const node_size_index = (layout == LiteGraph.VERTICAL_LAYOUT) ? 0 : 1; - y += node.size[node_size_index] + margin + LiteGraph.NODE_TITLE_HEIGHT; - } - x += max_size + margin; - } - - this.setDirtyCanvas(true, true); - }; - - /** - * Returns the amount of time the graph has been running in milliseconds - * @method getTime - * @return {number} number of milliseconds the graph has been running - */ - LGraph.prototype.getTime = function() { - return this.globaltime; - }; - - /** - * Returns the amount of time accumulated using the fixedtime_lapse var. This is used in context where the time increments should be constant - * @method getFixedTime - * @return {number} number of milliseconds the graph has been running - */ - - LGraph.prototype.getFixedTime = function() { - return this.fixedtime; - }; - - /** - * Returns the amount of time it took to compute the latest iteration. Take into account that this number could be not correct - * if the nodes are using graphical actions - * @method getElapsedTime - * @return {number} number of milliseconds it took the last cycle - */ - - LGraph.prototype.getElapsedTime = function() { - return this.elapsed_time; - }; - - /** - * Sends an event to all the nodes, useful to trigger stuff - * @method sendEventToAllNodes - * @param {String} eventname the name of the event (function to be called) - * @param {Array} params parameters in array format - */ - LGraph.prototype.sendEventToAllNodes = function(eventname, params, mode) { - mode = mode || LiteGraph.ALWAYS; - - var nodes = this._nodes_in_order ? this._nodes_in_order : this._nodes; - if (!nodes) { - return; - } - - for (var j = 0, l = nodes.length; j < l; ++j) { - var node = nodes[j]; - - if ( - node.constructor === LiteGraph.Subgraph && - eventname != "onExecute" - ) { - if (node.mode == mode) { - node.sendEventToAllNodes(eventname, params, mode); - } - continue; - } - - if (!node[eventname] || node.mode != mode) { - continue; - } - if (params === undefined) { - node[eventname](); - } else if (params && params.constructor === Array) { - node[eventname].apply(node, params); - } else { - node[eventname](params); - } - } - }; - - LGraph.prototype.sendActionToCanvas = function(action, params) { - if (!this.list_of_graphcanvas) { - return; - } - - for (var i = 0; i < this.list_of_graphcanvas.length; ++i) { - var c = this.list_of_graphcanvas[i]; - if (c[action]) { - c[action].apply(c, params); - } - } - }; - - /** - * Adds a new node instance to this graph - * @method add - * @param {LGraphNode} node the instance of the node - */ - - LGraph.prototype.add = function(node, skip_compute_order) { - if (!node) { - return; - } - - //groups - if (node.constructor === LGraphGroup) { - this._groups.push(node); - this.setDirtyCanvas(true); - this.change(); - node.graph = this; - this._version++; - return; - } - - //nodes - if (node.id != -1 && this._nodes_by_id[node.id] != null) { - console.warn( - "LiteGraph: there is already a node with this ID, changing it" - ); - if (LiteGraph.use_uuids) { - node.id = LiteGraph.uuidv4(); - } - else { - node.id = ++this.last_node_id; - } - } - - if (this._nodes.length >= LiteGraph.MAX_NUMBER_OF_NODES) { - throw "LiteGraph: max number of nodes in a graph reached"; - } - - //give him an id - if (LiteGraph.use_uuids) { - if (node.id == null || node.id == -1) - node.id = LiteGraph.uuidv4(); - } - else { - if (node.id == null || node.id == -1) { - node.id = ++this.last_node_id; - } else if (this.last_node_id < node.id) { - this.last_node_id = node.id; - } - } - - node.graph = this; - this._version++; - - this._nodes.push(node); - this._nodes_by_id[node.id] = node; - - if (node.onAdded) { - node.onAdded(this); - } - - if (this.config.align_to_grid) { - node.alignToGrid(); - } - - if (!skip_compute_order) { - this.updateExecutionOrder(); - } - - if (this.onNodeAdded) { - this.onNodeAdded(node); - } - - this.setDirtyCanvas(true); - this.change(); - - return node; //to chain actions - }; - - /** - * Removes a node from the graph - * @method remove - * @param {LGraphNode} node the instance of the node - */ - - LGraph.prototype.remove = function(node) { - if (node.constructor === LiteGraph.LGraphGroup) { - var index = this._groups.indexOf(node); - if (index != -1) { - this._groups.splice(index, 1); - } - node.graph = null; - this._version++; - this.setDirtyCanvas(true, true); - this.change(); - return; - } - - if (this._nodes_by_id[node.id] == null) { - return; - } //not found - - if (node.ignore_remove) { - return; - } //cannot be removed - - this.beforeChange(); //sure? - almost sure is wrong - - //disconnect inputs - if (node.inputs) { - for (var i = 0; i < node.inputs.length; i++) { - var slot = node.inputs[i]; - if (slot.link != null) { - node.disconnectInput(i); - } - } - } - - //disconnect outputs - if (node.outputs) { - for (var i = 0; i < node.outputs.length; i++) { - var slot = node.outputs[i]; - if (slot.links != null && slot.links.length) { - node.disconnectOutput(i); - } - } - } - - //node.id = -1; //why? - - //callback - if (node.onRemoved) { - node.onRemoved(); - } - - node.graph = null; - this._version++; - - //remove from canvas render - if (this.list_of_graphcanvas) { - for (var i = 0; i < this.list_of_graphcanvas.length; ++i) { - var canvas = this.list_of_graphcanvas[i]; - if (canvas.selected_nodes[node.id]) { - delete canvas.selected_nodes[node.id]; - } - if (canvas.node_dragged == node) { - canvas.node_dragged = null; - } - } - } - - //remove from containers - var pos = this._nodes.indexOf(node); - if (pos != -1) { - this._nodes.splice(pos, 1); - } - delete this._nodes_by_id[node.id]; - - if (this.onNodeRemoved) { - this.onNodeRemoved(node); - } - - //close panels - this.sendActionToCanvas("checkPanels"); - - this.setDirtyCanvas(true, true); - this.afterChange(); //sure? - almost sure is wrong - this.change(); - - this.updateExecutionOrder(); - }; - - /** - * Returns a node by its id. - * @method getNodeById - * @param {Number} id - */ - - LGraph.prototype.getNodeById = function(id) { - if (id == null) { - return null; - } - return this._nodes_by_id[id]; - }; - - /** - * Returns a list of nodes that matches a class - * @method findNodesByClass - * @param {Class} classObject the class itself (not an string) - * @return {Array} a list with all the nodes of this type - */ - LGraph.prototype.findNodesByClass = function(classObject, result) { - result = result || []; - result.length = 0; - for (var i = 0, l = this._nodes.length; i < l; ++i) { - if (this._nodes[i].constructor === classObject) { - result.push(this._nodes[i]); - } - } - return result; - }; - - /** - * Returns a list of nodes that matches a type - * @method findNodesByType - * @param {String} type the name of the node type - * @return {Array} a list with all the nodes of this type - */ - LGraph.prototype.findNodesByType = function(type, result) { - var type = type.toLowerCase(); - result = result || []; - result.length = 0; - for (var i = 0, l = this._nodes.length; i < l; ++i) { - if (this._nodes[i].type.toLowerCase() == type) { - result.push(this._nodes[i]); - } - } - return result; - }; - - /** - * Returns the first node that matches a name in its title - * @method findNodeByTitle - * @param {String} name the name of the node to search - * @return {Node} the node or null - */ - LGraph.prototype.findNodeByTitle = function(title) { - for (var i = 0, l = this._nodes.length; i < l; ++i) { - if (this._nodes[i].title == title) { - return this._nodes[i]; - } - } - return null; - }; - - /** - * Returns a list of nodes that matches a name - * @method findNodesByTitle - * @param {String} name the name of the node to search - * @return {Array} a list with all the nodes with this name - */ - LGraph.prototype.findNodesByTitle = function(title) { - var result = []; - for (var i = 0, l = this._nodes.length; i < l; ++i) { - if (this._nodes[i].title == title) { - result.push(this._nodes[i]); - } - } - return result; - }; - - /** - * Returns the top-most node in this position of the canvas - * @method getNodeOnPos - * @param {number} x the x coordinate in canvas space - * @param {number} y the y coordinate in canvas space - * @param {Array} nodes_list a list with all the nodes to search from, by default is all the nodes in the graph - * @return {LGraphNode} the node at this position or null - */ - LGraph.prototype.getNodeOnPos = function(x, y, nodes_list, margin) { - nodes_list = nodes_list || this._nodes; - var nRet = null; - for (var i = nodes_list.length - 1; i >= 0; i--) { - var n = nodes_list[i]; - var skip_title = n.constructor.title_mode == LiteGraph.NO_TITLE; - if (n.isPointInside(x, y, margin, skip_title)) { - // check for lesser interest nodes (TODO check for overlapping, use the top) - /*if (typeof n == "LGraphGroup"){ - nRet = n; - }else{*/ - return n; - /*}*/ - } - } - return nRet; - }; - - /** - * Returns the top-most group in that position - * @method getGroupOnPos - * @param {number} x the x coordinate in canvas space - * @param {number} y the y coordinate in canvas space - * @return {LGraphGroup} the group or null - */ - LGraph.prototype.getGroupOnPos = function(x, y) { - for (var i = this._groups.length - 1; i >= 0; i--) { - var g = this._groups[i]; - if (g.isPointInside(x, y, 2, true)) { - return g; - } - } - return null; - }; - - /** - * Checks that the node type matches the node type registered, used when replacing a nodetype by a newer version during execution - * this replaces the ones using the old version with the new version - * @method checkNodeTypes - */ - LGraph.prototype.checkNodeTypes = function() { - var changes = false; - for (var i = 0; i < this._nodes.length; i++) { - var node = this._nodes[i]; - var ctor = LiteGraph.registered_node_types[node.type]; - if (node.constructor == ctor) { - continue; - } - console.log("node being replaced by newer version: " + node.type); - var newnode = LiteGraph.createNode(node.type); - changes = true; - this._nodes[i] = newnode; - newnode.configure(node.serialize()); - newnode.graph = this; - this._nodes_by_id[newnode.id] = newnode; - if (node.inputs) { - newnode.inputs = node.inputs.concat(); - } - if (node.outputs) { - newnode.outputs = node.outputs.concat(); - } - } - this.updateExecutionOrder(); - }; - - // ********** GLOBALS ***************** - - LGraph.prototype.onAction = function(action, param, options) { - this._input_nodes = this.findNodesByClass( - LiteGraph.GraphInput, - this._input_nodes - ); - for (var i = 0; i < this._input_nodes.length; ++i) { - var node = this._input_nodes[i]; - if (node.properties.name != action) { - continue; - } - //wrap node.onAction(action, param); - node.actionDo(action, param, options); - break; - } - }; - - LGraph.prototype.trigger = function(action, param) { - if (this.onTrigger) { - this.onTrigger(action, param); - } - }; - - /** - * Tell this graph it has a global graph input of this type - * @method addGlobalInput - * @param {String} name - * @param {String} type - * @param {*} value [optional] - */ - LGraph.prototype.addInput = function(name, type, value) { - var input = this.inputs[name]; - if (input) { - //already exist - return; - } - - this.beforeChange(); - this.inputs[name] = { name: name, type: type, value: value }; - this._version++; - this.afterChange(); - - if (this.onInputAdded) { - this.onInputAdded(name, type); - } - - if (this.onInputsOutputsChange) { - this.onInputsOutputsChange(); - } - }; - - /** - * Assign a data to the global graph input - * @method setGlobalInputData - * @param {String} name - * @param {*} data - */ - LGraph.prototype.setInputData = function(name, data) { - var input = this.inputs[name]; - if (!input) { - return; - } - input.value = data; - }; - - /** - * Returns the current value of a global graph input - * @method getInputData - * @param {String} name - * @return {*} the data - */ - LGraph.prototype.getInputData = function(name) { - var input = this.inputs[name]; - if (!input) { - return null; - } - return input.value; - }; - - /** - * Changes the name of a global graph input - * @method renameInput - * @param {String} old_name - * @param {String} new_name - */ - LGraph.prototype.renameInput = function(old_name, name) { - if (name == old_name) { - return; - } - - if (!this.inputs[old_name]) { - return false; - } - - if (this.inputs[name]) { - console.error("there is already one input with that name"); - return false; - } - - this.inputs[name] = this.inputs[old_name]; - delete this.inputs[old_name]; - this._version++; - - if (this.onInputRenamed) { - this.onInputRenamed(old_name, name); - } - - if (this.onInputsOutputsChange) { - this.onInputsOutputsChange(); - } - }; - - /** - * Changes the type of a global graph input - * @method changeInputType - * @param {String} name - * @param {String} type - */ - LGraph.prototype.changeInputType = function(name, type) { - if (!this.inputs[name]) { - return false; - } - - if ( - this.inputs[name].type && - String(this.inputs[name].type).toLowerCase() == - String(type).toLowerCase() - ) { - return; - } - - this.inputs[name].type = type; - this._version++; - if (this.onInputTypeChanged) { - this.onInputTypeChanged(name, type); - } - }; - - /** - * Removes a global graph input - * @method removeInput - * @param {String} name - * @param {String} type - */ - LGraph.prototype.removeInput = function(name) { - if (!this.inputs[name]) { - return false; - } - - delete this.inputs[name]; - this._version++; - - if (this.onInputRemoved) { - this.onInputRemoved(name); - } - - if (this.onInputsOutputsChange) { - this.onInputsOutputsChange(); - } - return true; - }; - - /** - * Creates a global graph output - * @method addOutput - * @param {String} name - * @param {String} type - * @param {*} value - */ - LGraph.prototype.addOutput = function(name, type, value) { - this.outputs[name] = { name: name, type: type, value: value }; - this._version++; - - if (this.onOutputAdded) { - this.onOutputAdded(name, type); - } - - if (this.onInputsOutputsChange) { - this.onInputsOutputsChange(); - } - }; - - /** - * Assign a data to the global output - * @method setOutputData - * @param {String} name - * @param {String} value - */ - LGraph.prototype.setOutputData = function(name, value) { - var output = this.outputs[name]; - if (!output) { - return; - } - output.value = value; - }; - - /** - * Returns the current value of a global graph output - * @method getOutputData - * @param {String} name - * @return {*} the data - */ - LGraph.prototype.getOutputData = function(name) { - var output = this.outputs[name]; - if (!output) { - return null; - } - return output.value; - }; - - /** - * Renames a global graph output - * @method renameOutput - * @param {String} old_name - * @param {String} new_name - */ - LGraph.prototype.renameOutput = function(old_name, name) { - if (!this.outputs[old_name]) { - return false; - } - - if (this.outputs[name]) { - console.error("there is already one output with that name"); - return false; - } - - this.outputs[name] = this.outputs[old_name]; - delete this.outputs[old_name]; - this._version++; - - if (this.onOutputRenamed) { - this.onOutputRenamed(old_name, name); - } - - if (this.onInputsOutputsChange) { - this.onInputsOutputsChange(); - } - }; - - /** - * Changes the type of a global graph output - * @method changeOutputType - * @param {String} name - * @param {String} type - */ - LGraph.prototype.changeOutputType = function(name, type) { - if (!this.outputs[name]) { - return false; - } - - if ( - this.outputs[name].type && - String(this.outputs[name].type).toLowerCase() == - String(type).toLowerCase() - ) { - return; - } - - this.outputs[name].type = type; - this._version++; - if (this.onOutputTypeChanged) { - this.onOutputTypeChanged(name, type); - } - }; - - /** - * Removes a global graph output - * @method removeOutput - * @param {String} name - */ - LGraph.prototype.removeOutput = function(name) { - if (!this.outputs[name]) { - return false; - } - delete this.outputs[name]; - this._version++; - - if (this.onOutputRemoved) { - this.onOutputRemoved(name); - } - - if (this.onInputsOutputsChange) { - this.onInputsOutputsChange(); - } - return true; - }; - - LGraph.prototype.triggerInput = function(name, value) { - var nodes = this.findNodesByTitle(name); - for (var i = 0; i < nodes.length; ++i) { - nodes[i].onTrigger(value); - } - }; - - LGraph.prototype.setCallback = function(name, func) { - var nodes = this.findNodesByTitle(name); - for (var i = 0; i < nodes.length; ++i) { - nodes[i].setTrigger(func); - } - }; - - //used for undo, called before any change is made to the graph - LGraph.prototype.beforeChange = function(info) { - if (this.onBeforeChange) { - this.onBeforeChange(this,info); - } - this.sendActionToCanvas("onBeforeChange", this); - }; - - //used to resend actions, called after any change is made to the graph - LGraph.prototype.afterChange = function(info) { - if (this.onAfterChange) { - this.onAfterChange(this,info); - } - this.sendActionToCanvas("onAfterChange", this); - }; - - LGraph.prototype.connectionChange = function(node, link_info) { - this.updateExecutionOrder(); - if (this.onConnectionChange) { - this.onConnectionChange(node); - } - this._version++; - this.sendActionToCanvas("onConnectionChange"); - }; - - /** - * returns if the graph is in live mode - * @method isLive - */ - - LGraph.prototype.isLive = function() { - if (!this.list_of_graphcanvas) { - return false; - } - - for (var i = 0; i < this.list_of_graphcanvas.length; ++i) { - var c = this.list_of_graphcanvas[i]; - if (c.live_mode) { - return true; - } - } - return false; - }; - - /** - * clears the triggered slot animation in all links (stop visual animation) - * @method clearTriggeredSlots - */ - LGraph.prototype.clearTriggeredSlots = function() { - for (var i in this.links) { - var link_info = this.links[i]; - if (!link_info) { - continue; - } - if (link_info._last_time) { - link_info._last_time = 0; - } - } - }; - - /* Called when something visually changed (not the graph!) */ - LGraph.prototype.change = function() { - if (LiteGraph.debug) { - console.log("Graph changed"); - } - this.sendActionToCanvas("setDirty", [true, true]); - if (this.on_change) { - this.on_change(this); - } - }; - - LGraph.prototype.setDirtyCanvas = function(fg, bg) { - this.sendActionToCanvas("setDirty", [fg, bg]); - }; - - /** - * Destroys a link - * @method removeLink - * @param {Number} link_id - */ - LGraph.prototype.removeLink = function(link_id) { - var link = this.links[link_id]; - if (!link) { - return; - } - var node = this.getNodeById(link.target_id); - if (node) { - node.disconnectInput(link.target_slot); - } - }; - - //save and recover app state *************************************** - /** - * Creates a Object containing all the info about this graph, it can be serialized - * @method serialize - * @return {Object} value of the node - */ - LGraph.prototype.serialize = function() { - var nodes_info = []; - for (var i = 0, l = this._nodes.length; i < l; ++i) { - nodes_info.push(this._nodes[i].serialize()); - } - - //pack link info into a non-verbose format - var links = []; - for (var i in this.links) { - //links is an OBJECT - var link = this.links[i]; - if (!link.serialize) { - //weird bug I havent solved yet - console.warn( - "weird LLink bug, link info is not a LLink but a regular object" - ); - var link2 = new LLink(); - for (var j in link) { - link2[j] = link[j]; - } - this.links[i] = link2; - link = link2; - } - - links.push(link.serialize()); - } - - var groups_info = []; - for (var i = 0; i < this._groups.length; ++i) { - groups_info.push(this._groups[i].serialize()); - } - - var data = { - last_node_id: this.last_node_id, - last_link_id: this.last_link_id, - nodes: nodes_info, - links: links, - groups: groups_info, - config: this.config, - extra: this.extra, - version: LiteGraph.VERSION - }; - - if(this.onSerialize) - this.onSerialize(data); - - return data; - }; - - /** - * Configure a graph from a JSON string - * @method configure - * @param {String} str configure a graph from a JSON string - * @param {Boolean} returns if there was any error parsing - */ - LGraph.prototype.configure = function(data, keep_old) { - if (!data) { - return; - } - - if (!keep_old) { - this.clear(); - } - - var nodes = data.nodes; - - //decode links info (they are very verbose) - if (data.links && data.links.constructor === Array) { - var links = []; - for (var i = 0; i < data.links.length; ++i) { - var link_data = data.links[i]; - if(!link_data) //weird bug - { - console.warn("serialized graph link data contains errors, skipping."); - continue; - } - var link = new LLink(); - link.configure(link_data); - links[link.id] = link; - } - data.links = links; - } - - //copy all stored fields - for (var i in data) { - if(i == "nodes" || i == "groups" ) //links must be accepted - continue; - this[i] = data[i]; - } - - var error = false; - - //create nodes - this._nodes = []; - if (nodes) { - for (var i = 0, l = nodes.length; i < l; ++i) { - var n_info = nodes[i]; //stored info - var node = LiteGraph.createNode(n_info.type, n_info.title); - if (!node) { - if (LiteGraph.debug) { - console.log( - "Node not found or has errors: " + n_info.type - ); - } - - //in case of error we create a replacement node to avoid losing info - node = new LGraphNode(); - node.last_serialization = n_info; - node.has_errors = true; - error = true; - //continue; - } - - node.id = n_info.id; //id it or it will create a new id - this.add(node, true); //add before configure, otherwise configure cannot create links - } - - //configure nodes afterwards so they can reach each other - for (var i = 0, l = nodes.length; i < l; ++i) { - var n_info = nodes[i]; - var node = this.getNodeById(n_info.id); - if (node) { - node.configure(n_info); - } - } - } - - //groups - this._groups.length = 0; - if (data.groups) { - for (var i = 0; i < data.groups.length; ++i) { - var group = new LiteGraph.LGraphGroup(); - group.configure(data.groups[i]); - this.add(group); - } - } - - this.updateExecutionOrder(); - - this.extra = data.extra || {}; - - if(this.onConfigure) - this.onConfigure(data); - - this._version++; - this.setDirtyCanvas(true, true); - return error; - }; - - LGraph.prototype.load = function(url, callback) { - var that = this; - - //from file - if(url.constructor === File || url.constructor === Blob) - { - var reader = new FileReader(); - reader.addEventListener('load', function(event) { - var data = JSON.parse(event.target.result); - that.configure(data); - if(callback) - callback(); - }); - - reader.readAsText(url); - return; - } - - //is a string, then an URL - var req = new XMLHttpRequest(); - req.open("GET", url, true); - req.send(null); - req.onload = function(oEvent) { - if (req.status !== 200) { - console.error("Error loading graph:", req.status, req.response); - return; - } - var data = JSON.parse( req.response ); - that.configure(data); - if(callback) - callback(); - }; - req.onerror = function(err) { - console.error("Error loading graph:", err); - }; - }; - - LGraph.prototype.onNodeTrace = function(node, msg, color) { - //TODO - }; - - //this is the class in charge of storing link information - function LLink(id, type, origin_id, origin_slot, target_id, target_slot) { - this.id = id; - this.type = type; - this.origin_id = origin_id; - this.origin_slot = origin_slot; - this.target_id = target_id; - this.target_slot = target_slot; - - this._data = null; - this._pos = new Float32Array(2); //center - } - - LLink.prototype.configure = function(o) { - if (o.constructor === Array) { - this.id = o[0]; - this.origin_id = o[1]; - this.origin_slot = o[2]; - this.target_id = o[3]; - this.target_slot = o[4]; - this.type = o[5]; - } else { - this.id = o.id; - this.type = o.type; - this.origin_id = o.origin_id; - this.origin_slot = o.origin_slot; - this.target_id = o.target_id; - this.target_slot = o.target_slot; - } - }; - - LLink.prototype.serialize = function() { - return [ - this.id, - this.origin_id, - this.origin_slot, - this.target_id, - this.target_slot, - this.type - ]; - }; - - LiteGraph.LLink = LLink; - - // ************************************************************* - // Node CLASS ******* - // ************************************************************* - - /* - title: string - pos: [x,y] - size: [x,y] - - input|output: every connection - + { name:string, type:string, pos: [x,y]=Optional, direction: "input"|"output", links: Array }); - - general properties: - + clip_area: if you render outside the node, it will be clipped - + unsafe_execution: not allowed for safe execution - + skip_repeated_outputs: when adding new outputs, it wont show if there is one already connected - + resizable: if set to false it wont be resizable with the mouse - + horizontal: slots are distributed horizontally - + widgets_start_y: widgets start at y distance from the top of the node - - flags object: - + collapsed: if it is collapsed - - supported callbacks: - + onAdded: when added to graph (warning: this is called BEFORE the node is configured when loading) - + onRemoved: when removed from graph - + onStart: when the graph starts playing - + onStop: when the graph stops playing - + onDrawForeground: render the inside widgets inside the node - + onDrawBackground: render the background area inside the node (only in edit mode) - + onMouseDown - + onMouseMove - + onMouseUp - + onMouseEnter - + onMouseLeave - + onExecute: execute the node - + onPropertyChanged: when a property is changed in the panel (return true to skip default behaviour) - + onGetInputs: returns an array of possible inputs - + onGetOutputs: returns an array of possible outputs - + onBounding: in case this node has a bigger bounding than the node itself (the callback receives the bounding as [x,y,w,h]) - + onDblClick: double clicked in the node - + onInputDblClick: input slot double clicked (can be used to automatically create a node connected) - + onOutputDblClick: output slot double clicked (can be used to automatically create a node connected) - + onConfigure: called after the node has been configured - + onSerialize: to add extra info when serializing (the callback receives the object that should be filled with the data) - + onSelected - + onDeselected - + onDropItem : DOM item dropped over the node - + onDropFile : file dropped over the node - + onConnectInput : if returns false the incoming connection will be canceled - + onConnectionsChange : a connection changed (new one or removed) (LiteGraph.INPUT or LiteGraph.OUTPUT, slot, true if connected, link_info, input_info ) - + onAction: action slot triggered - + getExtraMenuOptions: to add option to context menu -*/ - - /** - * Base Class for all the node type classes - * @class LGraphNode - * @param {String} name a name for the node - */ - - function LGraphNode(title) { - this._ctor(title); - } - - global.LGraphNode = LiteGraph.LGraphNode = LGraphNode; - - LGraphNode.prototype._ctor = function(title) { - this.title = title || "Unnamed"; - this.size = [LiteGraph.NODE_WIDTH, 60]; - this.graph = null; - - this._pos = new Float32Array(10, 10); - - Object.defineProperty(this, "pos", { - set: function(v) { - if (!v || v.length < 2) { - return; - } - this._pos[0] = v[0]; - this._pos[1] = v[1]; - }, - get: function() { - return this._pos; - }, - enumerable: true - }); - - if (LiteGraph.use_uuids) { - this.id = LiteGraph.uuidv4(); - } - else { - this.id = -1; //not know till not added - } - this.type = null; - - //inputs available: array of inputs - this.inputs = []; - this.outputs = []; - this.connections = []; - - //local data - this.properties = {}; //for the values - this.properties_info = []; //for the info - - this.flags = {}; - }; - - /** - * configure a node from an object containing the serialized info - * @method configure - */ - LGraphNode.prototype.configure = function(info) { - if (this.graph) { - this.graph._version++; - } - for (var j in info) { - if (j == "properties") { - //i don't want to clone properties, I want to reuse the old container - for (var k in info.properties) { - this.properties[k] = info.properties[k]; - if (this.onPropertyChanged) { - this.onPropertyChanged( k, info.properties[k] ); - } - } - continue; - } - - if (info[j] == null) { - continue; - } else if (typeof info[j] == "object") { - //object - if (this[j] && this[j].configure) { - this[j].configure(info[j]); - } else { - this[j] = LiteGraph.cloneObject(info[j], this[j]); - } - } //value - else { - this[j] = info[j]; - } - } - - if (!info.title) { - this.title = this.constructor.title; - } - - if (this.inputs) { - for (var i = 0; i < this.inputs.length; ++i) { - var input = this.inputs[i]; - var link_info = this.graph ? this.graph.links[input.link] : null; - if (this.onConnectionsChange) - this.onConnectionsChange( LiteGraph.INPUT, i, true, link_info, input ); //link_info has been created now, so its updated - - if( this.onInputAdded ) - this.onInputAdded(input); - - } - } - - if (this.outputs) { - for (var i = 0; i < this.outputs.length; ++i) { - var output = this.outputs[i]; - if (!output.links) { - continue; - } - for (var j = 0; j < output.links.length; ++j) { - var link_info = this.graph ? this.graph.links[output.links[j]] : null; - if (this.onConnectionsChange) - this.onConnectionsChange( LiteGraph.OUTPUT, i, true, link_info, output ); //link_info has been created now, so its updated - } - - if( this.onOutputAdded ) - this.onOutputAdded(output); - } - } - - if( this.widgets ) - { - for (var i = 0; i < this.widgets.length; ++i) - { - var w = this.widgets[i]; - if(!w) - continue; - if(w.options && w.options.property && (this.properties[ w.options.property ] != undefined)) - w.value = JSON.parse( JSON.stringify( this.properties[ w.options.property ] ) ); - } - if (info.widgets_values) { - for (var i = 0; i < info.widgets_values.length; ++i) { - if (this.widgets[i]) { - this.widgets[i].value = info.widgets_values[i]; - } - } - } - } - - if (this.onConfigure) { - this.onConfigure(info); - } - }; - - /** - * serialize the content - * @method serialize - */ - - LGraphNode.prototype.serialize = function() { - //create serialization object - var o = { - id: this.id, - type: this.type, - pos: this.pos, - size: this.size, - flags: LiteGraph.cloneObject(this.flags), - order: this.order, - mode: this.mode - }; - - //special case for when there were errors - if (this.constructor === LGraphNode && this.last_serialization) { - return this.last_serialization; - } - - if (this.inputs) { - o.inputs = this.inputs; - } - - if (this.outputs) { - //clear outputs last data (because data in connections is never serialized but stored inside the outputs info) - for (var i = 0; i < this.outputs.length; i++) { - delete this.outputs[i]._data; - } - o.outputs = this.outputs; - } - - if (this.title && this.title != this.constructor.title) { - o.title = this.title; - } - - if (this.properties) { - o.properties = LiteGraph.cloneObject(this.properties); - } - - if (this.widgets && this.serialize_widgets) { - o.widgets_values = []; - for (var i = 0; i < this.widgets.length; ++i) { - if(this.widgets[i]) - o.widgets_values[i] = this.widgets[i].value; - else - o.widgets_values[i] = null; - } - } - - if (!o.type) { - o.type = this.constructor.type; - } - - if (this.color) { - o.color = this.color; - } - if (this.bgcolor) { - o.bgcolor = this.bgcolor; - } - if (this.boxcolor) { - o.boxcolor = this.boxcolor; - } - if (this.shape) { - o.shape = this.shape; - } - - if (this.onSerialize) { - if (this.onSerialize(o)) { - console.warn( - "node onSerialize shouldnt return anything, data should be stored in the object pass in the first parameter" - ); - } - } - - return o; - }; - - /* Creates a clone of this node */ - LGraphNode.prototype.clone = function() { - var node = LiteGraph.createNode(this.type); - if (!node) { - return null; - } - - //we clone it because serialize returns shared containers - var data = LiteGraph.cloneObject(this.serialize()); - - //remove links - if (data.inputs) { - for (var i = 0; i < data.inputs.length; ++i) { - data.inputs[i].link = null; - } - } - - if (data.outputs) { - for (var i = 0; i < data.outputs.length; ++i) { - if (data.outputs[i].links) { - data.outputs[i].links.length = 0; - } - } - } - - delete data["id"]; - - if (LiteGraph.use_uuids) { - data["id"] = LiteGraph.uuidv4() - } - - //remove links - node.configure(data); - - return node; - }; - - /** - * serialize and stringify - * @method toString - */ - - LGraphNode.prototype.toString = function() { - return JSON.stringify(this.serialize()); - }; - //LGraphNode.prototype.deserialize = function(info) {} //this cannot be done from within, must be done in LiteGraph - - /** - * get the title string - * @method getTitle - */ - - LGraphNode.prototype.getTitle = function() { - return this.title || this.constructor.title; - }; - - /** - * sets the value of a property - * @method setProperty - * @param {String} name - * @param {*} value - */ - LGraphNode.prototype.setProperty = function(name, value) { - if (!this.properties) { - this.properties = {}; - } - if( value === this.properties[name] ) - return; - var prev_value = this.properties[name]; - this.properties[name] = value; - if (this.onPropertyChanged) { - if( this.onPropertyChanged(name, value, prev_value) === false ) //abort change - this.properties[name] = prev_value; - } - if(this.widgets) //widgets could be linked to properties - for(var i = 0; i < this.widgets.length; ++i) - { - var w = this.widgets[i]; - if(!w) - continue; - if(w.options.property == name) - { - w.value = value; - break; - } - } - }; - - // Execution ************************* - /** - * sets the output data - * @method setOutputData - * @param {number} slot - * @param {*} data - */ - LGraphNode.prototype.setOutputData = function(slot, data) { - if (!this.outputs) { - return; - } - - //this maybe slow and a niche case - //if(slot && slot.constructor === String) - // slot = this.findOutputSlot(slot); - - if (slot == -1 || slot >= this.outputs.length) { - return; - } - - var output_info = this.outputs[slot]; - if (!output_info) { - return; - } - - //store data in the output itself in case we want to debug - output_info._data = data; - - //if there are connections, pass the data to the connections - if (this.outputs[slot].links) { - for (var i = 0; i < this.outputs[slot].links.length; i++) { - var link_id = this.outputs[slot].links[i]; - var link = this.graph.links[link_id]; - if(link) - link.data = data; - } - } - }; - - /** - * sets the output data type, useful when you want to be able to overwrite the data type - * @method setOutputDataType - * @param {number} slot - * @param {String} datatype - */ - LGraphNode.prototype.setOutputDataType = function(slot, type) { - if (!this.outputs) { - return; - } - if (slot == -1 || slot >= this.outputs.length) { - return; - } - var output_info = this.outputs[slot]; - if (!output_info) { - return; - } - //store data in the output itself in case we want to debug - output_info.type = type; - - //if there are connections, pass the data to the connections - if (this.outputs[slot].links) { - for (var i = 0; i < this.outputs[slot].links.length; i++) { - var link_id = this.outputs[slot].links[i]; - this.graph.links[link_id].type = type; - } - } - }; - - /** - * Retrieves the input data (data traveling through the connection) from one slot - * @method getInputData - * @param {number} slot - * @param {boolean} force_update if set to true it will force the connected node of this slot to output data into this link - * @return {*} data or if it is not connected returns undefined - */ - LGraphNode.prototype.getInputData = function(slot, force_update) { - if (!this.inputs) { - return; - } //undefined; - - if (slot >= this.inputs.length || this.inputs[slot].link == null) { - return; - } - - var link_id = this.inputs[slot].link; - var link = this.graph.links[link_id]; - if (!link) { - //bug: weird case but it happens sometimes - return null; - } - - if (!force_update) { - return link.data; - } - - //special case: used to extract data from the incoming connection before the graph has been executed - var node = this.graph.getNodeById(link.origin_id); - if (!node) { - return link.data; - } - - if (node.updateOutputData) { - node.updateOutputData(link.origin_slot); - } else if (node.onExecute) { - node.onExecute(); - } - - return link.data; - }; - - /** - * Retrieves the input data type (in case this supports multiple input types) - * @method getInputDataType - * @param {number} slot - * @return {String} datatype in string format - */ - LGraphNode.prototype.getInputDataType = function(slot) { - if (!this.inputs) { - return null; - } //undefined; - - if (slot >= this.inputs.length || this.inputs[slot].link == null) { - return null; - } - var link_id = this.inputs[slot].link; - var link = this.graph.links[link_id]; - if (!link) { - //bug: weird case but it happens sometimes - return null; - } - var node = this.graph.getNodeById(link.origin_id); - if (!node) { - return link.type; - } - var output_info = node.outputs[link.origin_slot]; - if (output_info) { - return output_info.type; - } - return null; - }; - - /** - * Retrieves the input data from one slot using its name instead of slot number - * @method getInputDataByName - * @param {String} slot_name - * @param {boolean} force_update if set to true it will force the connected node of this slot to output data into this link - * @return {*} data or if it is not connected returns null - */ - LGraphNode.prototype.getInputDataByName = function( - slot_name, - force_update - ) { - var slot = this.findInputSlot(slot_name); - if (slot == -1) { - return null; - } - return this.getInputData(slot, force_update); - }; - - /** - * tells you if there is a connection in one input slot - * @method isInputConnected - * @param {number} slot - * @return {boolean} - */ - LGraphNode.prototype.isInputConnected = function(slot) { - if (!this.inputs) { - return false; - } - return slot < this.inputs.length && this.inputs[slot].link != null; - }; - - /** - * tells you info about an input connection (which node, type, etc) - * @method getInputInfo - * @param {number} slot - * @return {Object} object or null { link: id, name: string, type: string or 0 } - */ - LGraphNode.prototype.getInputInfo = function(slot) { - if (!this.inputs) { - return null; - } - if (slot < this.inputs.length) { - return this.inputs[slot]; - } - return null; - }; - - /** - * Returns the link info in the connection of an input slot - * @method getInputLink - * @param {number} slot - * @return {LLink} object or null - */ - LGraphNode.prototype.getInputLink = function(slot) { - if (!this.inputs) { - return null; - } - if (slot < this.inputs.length) { - var slot_info = this.inputs[slot]; - return this.graph.links[ slot_info.link ]; - } - return null; - }; - - /** - * returns the node connected in the input slot - * @method getInputNode - * @param {number} slot - * @return {LGraphNode} node or null - */ - LGraphNode.prototype.getInputNode = function(slot) { - if (!this.inputs) { - return null; - } - if (slot >= this.inputs.length) { - return null; - } - var input = this.inputs[slot]; - if (!input || input.link === null) { - return null; - } - var link_info = this.graph.links[input.link]; - if (!link_info) { - return null; - } - return this.graph.getNodeById(link_info.origin_id); - }; - - /** - * returns the value of an input with this name, otherwise checks if there is a property with that name - * @method getInputOrProperty - * @param {string} name - * @return {*} value - */ - LGraphNode.prototype.getInputOrProperty = function(name) { - if (!this.inputs || !this.inputs.length) { - return this.properties ? this.properties[name] : null; - } - - for (var i = 0, l = this.inputs.length; i < l; ++i) { - var input_info = this.inputs[i]; - if (name == input_info.name && input_info.link != null) { - var link = this.graph.links[input_info.link]; - if (link) { - return link.data; - } - } - } - return this.properties[name]; - }; - - /** - * tells you the last output data that went in that slot - * @method getOutputData - * @param {number} slot - * @return {Object} object or null - */ - LGraphNode.prototype.getOutputData = function(slot) { - if (!this.outputs) { - return null; - } - if (slot >= this.outputs.length) { - return null; - } - - var info = this.outputs[slot]; - return info._data; - }; - - /** - * tells you info about an output connection (which node, type, etc) - * @method getOutputInfo - * @param {number} slot - * @return {Object} object or null { name: string, type: string, links: [ ids of links in number ] } - */ - LGraphNode.prototype.getOutputInfo = function(slot) { - if (!this.outputs) { - return null; - } - if (slot < this.outputs.length) { - return this.outputs[slot]; - } - return null; - }; - - /** - * tells you if there is a connection in one output slot - * @method isOutputConnected - * @param {number} slot - * @return {boolean} - */ - LGraphNode.prototype.isOutputConnected = function(slot) { - if (!this.outputs) { - return false; - } - return ( - slot < this.outputs.length && - this.outputs[slot].links && - this.outputs[slot].links.length - ); - }; - - /** - * tells you if there is any connection in the output slots - * @method isAnyOutputConnected - * @return {boolean} - */ - LGraphNode.prototype.isAnyOutputConnected = function() { - if (!this.outputs) { - return false; - } - for (var i = 0; i < this.outputs.length; ++i) { - if (this.outputs[i].links && this.outputs[i].links.length) { - return true; - } - } - return false; - }; - - /** - * retrieves all the nodes connected to this output slot - * @method getOutputNodes - * @param {number} slot - * @return {array} - */ - LGraphNode.prototype.getOutputNodes = function(slot) { - if (!this.outputs || this.outputs.length == 0) { - return null; - } - - if (slot >= this.outputs.length) { - return null; - } - - var output = this.outputs[slot]; - if (!output.links || output.links.length == 0) { - return null; - } - - var r = []; - for (var i = 0; i < output.links.length; i++) { - var link_id = output.links[i]; - var link = this.graph.links[link_id]; - if (link) { - var target_node = this.graph.getNodeById(link.target_id); - if (target_node) { - r.push(target_node); - } - } - } - return r; - }; - - LGraphNode.prototype.addOnTriggerInput = function(){ - var trigS = this.findInputSlot("onTrigger"); - if (trigS == -1){ //!trigS || - var input = this.addInput("onTrigger", LiteGraph.EVENT, {optional: true, nameLocked: true}); - return this.findInputSlot("onTrigger"); - } - return trigS; - } - - LGraphNode.prototype.addOnExecutedOutput = function(){ - var trigS = this.findOutputSlot("onExecuted"); - if (trigS == -1){ //!trigS || - var output = this.addOutput("onExecuted", LiteGraph.ACTION, {optional: true, nameLocked: true}); - return this.findOutputSlot("onExecuted"); - } - return trigS; - } - - LGraphNode.prototype.onAfterExecuteNode = function(param, options){ - var trigS = this.findOutputSlot("onExecuted"); - if (trigS != -1){ - - //console.debug(this.id+":"+this.order+" triggering slot onAfterExecute"); - //console.debug(param); - //console.debug(options); - this.triggerSlot(trigS, param, null, options); - - } - } - - LGraphNode.prototype.changeMode = function(modeTo){ - switch(modeTo){ - case LiteGraph.ON_EVENT: - // this.addOnExecutedOutput(); - break; - - case LiteGraph.ON_TRIGGER: - this.addOnTriggerInput(); - this.addOnExecutedOutput(); - break; - - case LiteGraph.NEVER: - break; - - case LiteGraph.ALWAYS: - break; - - case LiteGraph.ON_REQUEST: - break; - - default: - return false; - break; - } - this.mode = modeTo; - return true; - }; - - /** - * Triggers the node code execution, place a boolean/counter to mark the node as being executed - * @method execute - * @param {*} param - * @param {*} options - */ - LGraphNode.prototype.doExecute = function(param, options) { - options = options || {}; - if (this.onExecute){ - - // enable this to give the event an ID - if (!options.action_call) options.action_call = this.id+"_exec_"+Math.floor(Math.random()*9999); - - this.graph.nodes_executing[this.id] = true; //.push(this.id); - - this.onExecute(param, options); - - this.graph.nodes_executing[this.id] = false; //.pop(); - - // save execution/action ref - this.exec_version = this.graph.iteration; - if(options && options.action_call){ - this.action_call = options.action_call; // if (param) - this.graph.nodes_executedAction[this.id] = options.action_call; - } - } - this.execute_triggered = 2; // the nFrames it will be used (-- each step), means "how old" is the event - if(this.onAfterExecuteNode) this.onAfterExecuteNode(param, options); // callback - }; - - /** - * Triggers an action, wrapped by logics to control execution flow - * @method actionDo - * @param {String} action name - * @param {*} param - */ - LGraphNode.prototype.actionDo = function(action, param, options) { - options = options || {}; - if (this.onAction){ - - // enable this to give the event an ID - if (!options.action_call) options.action_call = this.id+"_"+(action?action:"action")+"_"+Math.floor(Math.random()*9999); - - this.graph.nodes_actioning[this.id] = (action?action:"actioning"); //.push(this.id); - - this.onAction(action, param, options); - - this.graph.nodes_actioning[this.id] = false; //.pop(); - - // save execution/action ref - if(options && options.action_call){ - this.action_call = options.action_call; // if (param) - this.graph.nodes_executedAction[this.id] = options.action_call; - } - } - this.action_triggered = 2; // the nFrames it will be used (-- each step), means "how old" is the event - if(this.onAfterExecuteNode) this.onAfterExecuteNode(param, options); - }; - - /** - * Triggers an event in this node, this will trigger any output with the same name - * @method trigger - * @param {String} event name ( "on_play", ... ) if action is equivalent to false then the event is send to all - * @param {*} param - */ - LGraphNode.prototype.trigger = function(action, param, options) { - if (!this.outputs || !this.outputs.length) { - return; - } - - if (this.graph) - this.graph._last_trigger_time = LiteGraph.getTime(); - - for (var i = 0; i < this.outputs.length; ++i) { - var output = this.outputs[i]; - if ( !output || output.type !== LiteGraph.EVENT || (action && output.name != action) ) - continue; - this.triggerSlot(i, param, null, options); - } - }; - - /** - * Triggers a slot event in this node: cycle output slots and launch execute/action on connected nodes - * @method triggerSlot - * @param {Number} slot the index of the output slot - * @param {*} param - * @param {Number} link_id [optional] in case you want to trigger and specific output link in a slot - */ - LGraphNode.prototype.triggerSlot = function(slot, param, link_id, options) { - options = options || {}; - if (!this.outputs) { - return; - } - - if(slot == null) - { - console.error("slot must be a number"); - return; - } - - if(slot.constructor !== Number) - console.warn("slot must be a number, use node.trigger('name') if you want to use a string"); - - var output = this.outputs[slot]; - if (!output) { - return; - } - - var links = output.links; - if (!links || !links.length) { - return; - } - - if (this.graph) { - this.graph._last_trigger_time = LiteGraph.getTime(); - } - - //for every link attached here - for (var k = 0; k < links.length; ++k) { - var id = links[k]; - if (link_id != null && link_id != id) { - //to skip links - continue; - } - var link_info = this.graph.links[links[k]]; - if (!link_info) { - //not connected - continue; - } - link_info._last_time = LiteGraph.getTime(); - var node = this.graph.getNodeById(link_info.target_id); - if (!node) { - //node not found? - continue; - } - - //used to mark events in graph - var target_connection = node.inputs[link_info.target_slot]; - - if (node.mode === LiteGraph.ON_TRIGGER) - { - // generate unique trigger ID if not present - if (!options.action_call) options.action_call = this.id+"_trigg_"+Math.floor(Math.random()*9999); - if (node.onExecute) { - // -- wrapping node.onExecute(param); -- - node.doExecute(param, options); - } - } - else if (node.onAction) { - // generate unique action ID if not present - if (!options.action_call) options.action_call = this.id+"_act_"+Math.floor(Math.random()*9999); - //pass the action name - var target_connection = node.inputs[link_info.target_slot]; - // wrap node.onAction(target_connection.name, param); - node.actionDo(target_connection.name, param, options); - } - } - }; - - /** - * clears the trigger slot animation - * @method clearTriggeredSlot - * @param {Number} slot the index of the output slot - * @param {Number} link_id [optional] in case you want to trigger and specific output link in a slot - */ - LGraphNode.prototype.clearTriggeredSlot = function(slot, link_id) { - if (!this.outputs) { - return; - } - - var output = this.outputs[slot]; - if (!output) { - return; - } - - var links = output.links; - if (!links || !links.length) { - return; - } - - //for every link attached here - for (var k = 0; k < links.length; ++k) { - var id = links[k]; - if (link_id != null && link_id != id) { - //to skip links - continue; - } - var link_info = this.graph.links[links[k]]; - if (!link_info) { - //not connected - continue; - } - link_info._last_time = 0; - } - }; - - /** - * changes node size and triggers callback - * @method setSize - * @param {vec2} size - */ - LGraphNode.prototype.setSize = function(size) - { - this.size = size; - if(this.onResize) - this.onResize(this.size); - } - - /** - * add a new property to this node - * @method addProperty - * @param {string} name - * @param {*} default_value - * @param {string} type string defining the output type ("vec3","number",...) - * @param {Object} extra_info this can be used to have special properties of the property (like values, etc) - */ - LGraphNode.prototype.addProperty = function( - name, - default_value, - type, - extra_info - ) { - var o = { name: name, type: type, default_value: default_value }; - if (extra_info) { - for (var i in extra_info) { - o[i] = extra_info[i]; - } - } - if (!this.properties_info) { - this.properties_info = []; - } - this.properties_info.push(o); - if (!this.properties) { - this.properties = {}; - } - this.properties[name] = default_value; - return o; - }; - - //connections - - /** - * add a new output slot to use in this node - * @method addOutput - * @param {string} name - * @param {string} type string defining the output type ("vec3","number",...) - * @param {Object} extra_info this can be used to have special properties of an output (label, special color, position, etc) - */ - LGraphNode.prototype.addOutput = function(name, type, extra_info) { - var output = { name: name, type: type, links: null }; - if (extra_info) { - for (var i in extra_info) { - output[i] = extra_info[i]; - } - } - - if (!this.outputs) { - this.outputs = []; - } - this.outputs.push(output); - if (this.onOutputAdded) { - this.onOutputAdded(output); - } - - if (LiteGraph.auto_load_slot_types) LiteGraph.registerNodeAndSlotType(this,type,true); - - this.setSize( this.computeSize() ); - this.setDirtyCanvas(true, true); - return output; - }; - - /** - * add a new output slot to use in this node - * @method addOutputs - * @param {Array} array of triplets like [[name,type,extra_info],[...]] - */ - LGraphNode.prototype.addOutputs = function(array) { - for (var i = 0; i < array.length; ++i) { - var info = array[i]; - var o = { name: info[0], type: info[1], link: null }; - if (array[2]) { - for (var j in info[2]) { - o[j] = info[2][j]; - } - } - - if (!this.outputs) { - this.outputs = []; - } - this.outputs.push(o); - if (this.onOutputAdded) { - this.onOutputAdded(o); - } - - if (LiteGraph.auto_load_slot_types) LiteGraph.registerNodeAndSlotType(this,info[1],true); - - } - - this.setSize( this.computeSize() ); - this.setDirtyCanvas(true, true); - }; - - /** - * remove an existing output slot - * @method removeOutput - * @param {number} slot - */ - LGraphNode.prototype.removeOutput = function(slot) { - this.disconnectOutput(slot); - this.outputs.splice(slot, 1); - for (var i = slot; i < this.outputs.length; ++i) { - if (!this.outputs[i] || !this.outputs[i].links) { - continue; - } - var links = this.outputs[i].links; - for (var j = 0; j < links.length; ++j) { - var link = this.graph.links[links[j]]; - if (!link) { - continue; - } - link.origin_slot -= 1; - } - } - - this.setSize( this.computeSize() ); - if (this.onOutputRemoved) { - this.onOutputRemoved(slot); - } - this.setDirtyCanvas(true, true); - }; - - /** - * add a new input slot to use in this node - * @method addInput - * @param {string} name - * @param {string} type string defining the input type ("vec3","number",...), it its a generic one use 0 - * @param {Object} extra_info this can be used to have special properties of an input (label, color, position, etc) - */ - LGraphNode.prototype.addInput = function(name, type, extra_info) { - type = type || 0; - var input = { name: name, type: type, link: null }; - if (extra_info) { - for (var i in extra_info) { - input[i] = extra_info[i]; - } - } - - if (!this.inputs) { - this.inputs = []; - } - - this.inputs.push(input); - this.setSize( this.computeSize() ); - - if (this.onInputAdded) { - this.onInputAdded(input); - } - - LiteGraph.registerNodeAndSlotType(this,type); - - this.setDirtyCanvas(true, true); - return input; - }; - - /** - * add several new input slots in this node - * @method addInputs - * @param {Array} array of triplets like [[name,type,extra_info],[...]] - */ - LGraphNode.prototype.addInputs = function(array) { - for (var i = 0; i < array.length; ++i) { - var info = array[i]; - var o = { name: info[0], type: info[1], link: null }; - if (array[2]) { - for (var j in info[2]) { - o[j] = info[2][j]; - } - } - - if (!this.inputs) { - this.inputs = []; - } - this.inputs.push(o); - if (this.onInputAdded) { - this.onInputAdded(o); - } - - LiteGraph.registerNodeAndSlotType(this,info[1]); - } - - this.setSize( this.computeSize() ); - this.setDirtyCanvas(true, true); - }; - - /** - * remove an existing input slot - * @method removeInput - * @param {number} slot - */ - LGraphNode.prototype.removeInput = function(slot) { - this.disconnectInput(slot); - var slot_info = this.inputs.splice(slot, 1); - for (var i = slot; i < this.inputs.length; ++i) { - if (!this.inputs[i]) { - continue; - } - var link = this.graph.links[this.inputs[i].link]; - if (!link) { - continue; - } - link.target_slot -= 1; - } - this.setSize( this.computeSize() ); - if (this.onInputRemoved) { - this.onInputRemoved(slot, slot_info[0] ); - } - this.setDirtyCanvas(true, true); - }; - - /** - * add an special connection to this node (used for special kinds of graphs) - * @method addConnection - * @param {string} name - * @param {string} type string defining the input type ("vec3","number",...) - * @param {[x,y]} pos position of the connection inside the node - * @param {string} direction if is input or output - */ - LGraphNode.prototype.addConnection = function(name, type, pos, direction) { - var o = { - name: name, - type: type, - pos: pos, - direction: direction, - links: null - }; - this.connections.push(o); - return o; - }; - - /** - * computes the minimum size of a node according to its inputs and output slots - * @method computeSize - * @param {vec2} minHeight - * @return {vec2} the total size - */ - LGraphNode.prototype.computeSize = function(out) { - if (this.constructor.size) { - return this.constructor.size.concat(); - } - - var rows = Math.max( - this.inputs ? this.inputs.length : 1, - this.outputs ? this.outputs.length : 1 - ); - var size = out || new Float32Array([0, 0]); - rows = Math.max(rows, 1); - var font_size = LiteGraph.NODE_TEXT_SIZE; //although it should be graphcanvas.inner_text_font size - - var title_width = compute_text_size(this.title); - var input_width = 0; - var output_width = 0; - - if (this.inputs) { - for (var i = 0, l = this.inputs.length; i < l; ++i) { - var input = this.inputs[i]; - var text = input.label || input.name || ""; - var text_width = compute_text_size(text); - if (input_width < text_width) { - input_width = text_width; - } - } - } - - if (this.outputs) { - for (var i = 0, l = this.outputs.length; i < l; ++i) { - var output = this.outputs[i]; - var text = output.label || output.name || ""; - var text_width = compute_text_size(text); - if (output_width < text_width) { - output_width = text_width; - } - } - } - - size[0] = Math.max(input_width + output_width + 10, title_width); - size[0] = Math.max(size[0], LiteGraph.NODE_WIDTH); - if (this.widgets && this.widgets.length) { - size[0] = Math.max(size[0], LiteGraph.NODE_WIDTH * 1.5); - } - - size[1] = (this.constructor.slot_start_y || 0) + rows * LiteGraph.NODE_SLOT_HEIGHT; - - var widgets_height = 0; - if (this.widgets && this.widgets.length) { - for (var i = 0, l = this.widgets.length; i < l; ++i) { - if (this.widgets[i].computeSize) - widgets_height += this.widgets[i].computeSize(size[0])[1] + 4; - else - widgets_height += LiteGraph.NODE_WIDGET_HEIGHT + 4; - } - widgets_height += 8; - } - - //compute height using widgets height - if( this.widgets_up ) - size[1] = Math.max( size[1], widgets_height ); - else if( this.widgets_start_y != null ) - size[1] = Math.max( size[1], widgets_height + this.widgets_start_y ); - else - size[1] += widgets_height; - - function compute_text_size(text) { - if (!text) { - return 0; - } - return font_size * text.length * 0.6; - } - - if ( - this.constructor.min_height && - size[1] < this.constructor.min_height - ) { - size[1] = this.constructor.min_height; - } - - size[1] += 6; //margin - - return size; - }; - - LGraphNode.prototype.inResizeCorner = function(canvasX, canvasY) { - var rows = this.outputs ? this.outputs.length : 1; - var outputs_offset = (this.constructor.slot_start_y || 0) + rows * LiteGraph.NODE_SLOT_HEIGHT; - return isInsideRectangle(canvasX, - canvasY, - this.pos[0] + this.size[0] - 15, - this.pos[1] + Math.max(this.size[1] - 15, outputs_offset), - 20, - 20 - ); - } - - /** - * returns all the info available about a property of this node. - * - * @method getPropertyInfo - * @param {String} property name of the property - * @return {Object} the object with all the available info - */ - LGraphNode.prototype.getPropertyInfo = function( property ) - { - var info = null; - - //there are several ways to define info about a property - //legacy mode - if (this.properties_info) { - for (var i = 0; i < this.properties_info.length; ++i) { - if (this.properties_info[i].name == property) { - info = this.properties_info[i]; - break; - } - } - } - //litescene mode using the constructor - if(this.constructor["@" + property]) - info = this.constructor["@" + property]; - - if(this.constructor.widgets_info && this.constructor.widgets_info[property]) - info = this.constructor.widgets_info[property]; - - //litescene mode using the constructor - if (!info && this.onGetPropertyInfo) { - info = this.onGetPropertyInfo(property); - } - - if (!info) - info = {}; - if(!info.type) - info.type = typeof this.properties[property]; - if(info.widget == "combo") - info.type = "enum"; - - return info; - } - - /** - * Defines a widget inside the node, it will be rendered on top of the node, you can control lots of properties - * - * @method addWidget - * @param {String} type the widget type (could be "number","string","combo" - * @param {String} name the text to show on the widget - * @param {String} value the default value - * @param {Function|String} callback function to call when it changes (optionally, it can be the name of the property to modify) - * @param {Object} options the object that contains special properties of this widget - * @return {Object} the created widget object - */ - LGraphNode.prototype.addWidget = function( type, name, value, callback, options ) - { - if (!this.widgets) { - this.widgets = []; - } - - if(!options && callback && callback.constructor === Object) - { - options = callback; - callback = null; - } - - if(options && options.constructor === String) //options can be the property name - options = { property: options }; - - if(callback && callback.constructor === String) //callback can be the property name - { - if(!options) - options = {}; - options.property = callback; - callback = null; - } - - if(callback && callback.constructor !== Function) - { - console.warn("addWidget: callback must be a function"); - callback = null; - } - - var w = { - type: type.toLowerCase(), - name: name, - value: value, - callback: callback, - options: options || {} - }; - - if (w.options.y !== undefined) { - w.y = w.options.y; - } - - if (!callback && !w.options.callback && !w.options.property) { - console.warn("LiteGraph addWidget(...) without a callback or property assigned"); - } - if (type == "combo" && !w.options.values) { - throw "LiteGraph addWidget('combo',...) requires to pass values in options: { values:['red','blue'] }"; - } - this.widgets.push(w); - this.setSize( this.computeSize() ); - return w; - }; - - LGraphNode.prototype.addCustomWidget = function(custom_widget) { - if (!this.widgets) { - this.widgets = []; - } - this.widgets.push(custom_widget); - return custom_widget; - }; - - /** - * returns the bounding of the object, used for rendering purposes - * @method getBounding - * @param out {Float32Array[4]?} [optional] a place to store the output, to free garbage - * @param compute_outer {boolean?} [optional] set to true to include the shadow and connection points in the bounding calculation - * @return {Float32Array[4]} the bounding box in format of [topleft_cornerx, topleft_cornery, width, height] - */ - LGraphNode.prototype.getBounding = function(out, compute_outer) { - out = out || new Float32Array(4); - const nodePos = this.pos; - const isCollapsed = this.flags.collapsed; - const nodeSize = this.size; - - let left_offset = 0; - // 1 offset due to how nodes are rendered - let right_offset = 1 ; - let top_offset = 0; - let bottom_offset = 0; - - if (compute_outer) { - // 4 offset for collapsed node connection points - left_offset = 4; - // 6 offset for right shadow and collapsed node connection points - right_offset = 6 + left_offset; - // 4 offset for collapsed nodes top connection points - top_offset = 4; - // 5 offset for bottom shadow and collapsed node connection points - bottom_offset = 5 + top_offset; - } - - out[0] = nodePos[0] - left_offset; - out[1] = nodePos[1] - LiteGraph.NODE_TITLE_HEIGHT - top_offset; - out[2] = isCollapsed ? - (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) + right_offset : - nodeSize[0] + right_offset; - out[3] = isCollapsed ? - LiteGraph.NODE_TITLE_HEIGHT + bottom_offset : - nodeSize[1] + LiteGraph.NODE_TITLE_HEIGHT + bottom_offset; - - if (this.onBounding) { - this.onBounding(out); - } - return out; - }; - - /** - * checks if a point is inside the shape of a node - * @method isPointInside - * @param {number} x - * @param {number} y - * @return {boolean} - */ - LGraphNode.prototype.isPointInside = function(x, y, margin, skip_title) { - margin = margin || 0; - - var margin_top = this.graph && this.graph.isLive() ? 0 : LiteGraph.NODE_TITLE_HEIGHT; - if (skip_title) { - margin_top = 0; - } - if (this.flags && this.flags.collapsed) { - //if ( distance([x,y], [this.pos[0] + this.size[0]*0.5, this.pos[1] + this.size[1]*0.5]) < LiteGraph.NODE_COLLAPSED_RADIUS) - if ( - isInsideRectangle( - x, - y, - this.pos[0] - margin, - this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT - margin, - (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) + - 2 * margin, - LiteGraph.NODE_TITLE_HEIGHT + 2 * margin - ) - ) { - return true; - } - } else if ( - this.pos[0] - 4 - margin < x && - this.pos[0] + this.size[0] + 4 + margin > x && - this.pos[1] - margin_top - margin < y && - this.pos[1] + this.size[1] + margin > y - ) { - return true; - } - return false; - }; - - /** - * checks if a point is inside a node slot, and returns info about which slot - * @method getSlotInPosition - * @param {number} x - * @param {number} y - * @return {Object} if found the object contains { input|output: slot object, slot: number, link_pos: [x,y] } - */ - LGraphNode.prototype.getSlotInPosition = function(x, y) { - //search for inputs - var link_pos = new Float32Array(2); - if (this.inputs) { - for (var i = 0, l = this.inputs.length; i < l; ++i) { - var input = this.inputs[i]; - this.getConnectionPos(true, i, link_pos); - if ( - isInsideRectangle( - x, - y, - link_pos[0] - 10, - link_pos[1] - 5, - 20, - 10 - ) - ) { - return { input: input, slot: i, link_pos: link_pos }; - } - } - } - - if (this.outputs) { - for (var i = 0, l = this.outputs.length; i < l; ++i) { - var output = this.outputs[i]; - this.getConnectionPos(false, i, link_pos); - if ( - isInsideRectangle( - x, - y, - link_pos[0] - 10, - link_pos[1] - 5, - 20, - 10 - ) - ) { - return { output: output, slot: i, link_pos: link_pos }; - } - } - } - - return null; - }; - - /** - * returns the input slot with a given name (used for dynamic slots), -1 if not found - * @method findInputSlot - * @param {string} name the name of the slot - * @param {boolean} returnObj if the obj itself wanted - * @return {number_or_object} the slot (-1 if not found) - */ - LGraphNode.prototype.findInputSlot = function(name, returnObj) { - if (!this.inputs) { - return -1; - } - for (var i = 0, l = this.inputs.length; i < l; ++i) { - if (name == this.inputs[i].name) { - return !returnObj ? i : this.inputs[i]; - } - } - return -1; - }; - - /** - * returns the output slot with a given name (used for dynamic slots), -1 if not found - * @method findOutputSlot - * @param {string} name the name of the slot - * @param {boolean} returnObj if the obj itself wanted - * @return {number_or_object} the slot (-1 if not found) - */ - LGraphNode.prototype.findOutputSlot = function(name, returnObj) { - returnObj = returnObj || false; - if (!this.outputs) { - return -1; - } - for (var i = 0, l = this.outputs.length; i < l; ++i) { - if (name == this.outputs[i].name) { - return !returnObj ? i : this.outputs[i]; - } - } - return -1; - }; - - // TODO refactor: USE SINGLE findInput/findOutput functions! :: merge options - - /** - * returns the first free input slot - * @method findInputSlotFree - * @param {object} options - * @return {number_or_object} the slot (-1 if not found) - */ - LGraphNode.prototype.findInputSlotFree = function(optsIn) { - var optsIn = optsIn || {}; - var optsDef = {returnObj: false - ,typesNotAccepted: [] - }; - var opts = Object.assign(optsDef,optsIn); - if (!this.inputs) { - return -1; - } - for (var i = 0, l = this.inputs.length; i < l; ++i) { - if (this.inputs[i].link && this.inputs[i].link != null) { - continue; - } - if (opts.typesNotAccepted && opts.typesNotAccepted.includes && opts.typesNotAccepted.includes(this.inputs[i].type)){ - continue; - } - return !opts.returnObj ? i : this.inputs[i]; - } - return -1; - }; - - /** - * returns the first output slot free - * @method findOutputSlotFree - * @param {object} options - * @return {number_or_object} the slot (-1 if not found) - */ - LGraphNode.prototype.findOutputSlotFree = function(optsIn) { - var optsIn = optsIn || {}; - var optsDef = { returnObj: false - ,typesNotAccepted: [] - }; - var opts = Object.assign(optsDef,optsIn); - if (!this.outputs) { - return -1; - } - for (var i = 0, l = this.outputs.length; i < l; ++i) { - if (this.outputs[i].links && this.outputs[i].links != null) { - continue; - } - if (opts.typesNotAccepted && opts.typesNotAccepted.includes && opts.typesNotAccepted.includes(this.outputs[i].type)){ - continue; - } - return !opts.returnObj ? i : this.outputs[i]; - } - return -1; - }; - - /** - * findSlotByType for INPUTS - */ - LGraphNode.prototype.findInputSlotByType = function(type, returnObj, preferFreeSlot, doNotUseOccupied) { - return this.findSlotByType(true, type, returnObj, preferFreeSlot, doNotUseOccupied); - }; - - /** - * findSlotByType for OUTPUTS - */ - LGraphNode.prototype.findOutputSlotByType = function(type, returnObj, preferFreeSlot, doNotUseOccupied) { - return this.findSlotByType(false, type, returnObj, preferFreeSlot, doNotUseOccupied); - }; - - /** - * returns the output (or input) slot with a given type, -1 if not found - * @method findSlotByType - * @param {boolean} input uise inputs instead of outputs - * @param {string} type the type of the slot - * @param {boolean} returnObj if the obj itself wanted - * @param {boolean} preferFreeSlot if we want a free slot (if not found, will return the first of the type anyway) - * @return {number_or_object} the slot (-1 if not found) - */ - LGraphNode.prototype.findSlotByType = function(input, type, returnObj, preferFreeSlot, doNotUseOccupied) { - input = input || false; - returnObj = returnObj || false; - preferFreeSlot = preferFreeSlot || false; - doNotUseOccupied = doNotUseOccupied || false; - var aSlots = input ? this.inputs : this.outputs; - if (!aSlots) { - return -1; - } - // !! empty string type is considered 0, * !! - if (type == "" || type == "*") type = 0; - for (var i = 0, l = aSlots.length; i < l; ++i) { - var tFound = false; - var aSource = (type+"").toLowerCase().split(","); - var aDest = aSlots[i].type=="0"||aSlots[i].type=="*"?"0":aSlots[i].type; - aDest = (aDest+"").toLowerCase().split(","); - for(var sI=0;sI= 0 && target_slot !== null){ - //console.debug("CONNbyTYPE type "+target_slotType+" for "+target_slot) - return this.connect(slot, target_node, target_slot); - }else{ - //console.log("type "+target_slotType+" not found or not free?") - if (opts.createEventInCase && target_slotType == LiteGraph.EVENT){ - // WILL CREATE THE onTrigger IN SLOT - //console.debug("connect WILL CREATE THE onTrigger "+target_slotType+" to "+target_node); - return this.connect(slot, target_node, -1); - } - // connect to the first general output slot if not found a specific type and - if (opts.generalTypeInCase){ - var target_slot = target_node.findInputSlotByType(0, false, true, true); - //console.debug("connect TO a general type (*, 0), if not found the specific type ",target_slotType," to ",target_node,"RES_SLOT:",target_slot); - if (target_slot >= 0){ - return this.connect(slot, target_node, target_slot); - } - } - // connect to the first free input slot if not found a specific type and this output is general - if (opts.firstFreeIfOutputGeneralInCase && (target_slotType == 0 || target_slotType == "*" || target_slotType == "")){ - var target_slot = target_node.findInputSlotFree({typesNotAccepted: [LiteGraph.EVENT] }); - //console.debug("connect TO TheFirstFREE ",target_slotType," to ",target_node,"RES_SLOT:",target_slot); - if (target_slot >= 0){ - return this.connect(slot, target_node, target_slot); - } - } - - console.debug("no way to connect type: ",target_slotType," to targetNODE ",target_node); - //TODO filter - - return null; - } - } - - /** - * connect this node input to the output of another node BY TYPE - * @method connectByType - * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) - * @param {LGraphNode} node the target node - * @param {string} target_type the output slot type of the target node - * @return {Object} the link_info is created, otherwise null - */ - LGraphNode.prototype.connectByTypeOutput = function(slot, source_node, source_slotType, optsIn) { - var optsIn = optsIn || {}; - var optsDef = { createEventInCase: true - ,firstFreeIfInputGeneralInCase: true - ,generalTypeInCase: true - }; - var opts = Object.assign(optsDef,optsIn); - if (source_node && source_node.constructor === Number) { - source_node = this.graph.getNodeById(source_node); - } - var source_slot = source_node.findOutputSlotByType(source_slotType, false, true); - if (source_slot >= 0 && source_slot !== null){ - //console.debug("CONNbyTYPE OUT! type "+source_slotType+" for "+source_slot) - return source_node.connect(source_slot, this, slot); - }else{ - - // connect to the first general output slot if not found a specific type and - if (opts.generalTypeInCase){ - var source_slot = source_node.findOutputSlotByType(0, false, true, true); - if (source_slot >= 0){ - return source_node.connect(source_slot, this, slot); - } - } - - if (opts.createEventInCase && source_slotType == LiteGraph.EVENT){ - // WILL CREATE THE onExecuted OUT SLOT - if (LiteGraph.do_add_triggers_slots){ - var source_slot = source_node.addOnExecutedOutput(); - return source_node.connect(source_slot, this, slot); - } - } - // connect to the first free output slot if not found a specific type and this input is general - if (opts.firstFreeIfInputGeneralInCase && (source_slotType == 0 || source_slotType == "*" || source_slotType == "")){ - var source_slot = source_node.findOutputSlotFree({typesNotAccepted: [LiteGraph.EVENT] }); - if (source_slot >= 0){ - return source_node.connect(source_slot, this, slot); - } - } - - console.debug("no way to connect byOUT type: ",source_slotType," to sourceNODE ",source_node); - //TODO filter - - //console.log("type OUT! "+source_slotType+" not found or not free?") - return null; - } - } - - /** - * connect this node output to the input of another node - * @method connect - * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) - * @param {LGraphNode} node the target node - * @param {number_or_string} target_slot the input slot of the target node (could be the number of the slot or the string with the name of the slot, or -1 to connect a trigger) - * @return {Object} the link_info is created, otherwise null - */ - LGraphNode.prototype.connect = function(slot, target_node, target_slot) { - target_slot = target_slot || 0; - - if (!this.graph) { - //could be connected before adding it to a graph - console.log( - "Connect: Error, node doesn't belong to any graph. Nodes must be added first to a graph before connecting them." - ); //due to link ids being associated with graphs - return null; - } - - //seek for the output slot - if (slot.constructor === String) { - slot = this.findOutputSlot(slot); - if (slot == -1) { - if (LiteGraph.debug) { - console.log("Connect: Error, no slot of name " + slot); - } - return null; - } - } else if (!this.outputs || slot >= this.outputs.length) { - if (LiteGraph.debug) { - console.log("Connect: Error, slot number not found"); - } - return null; - } - - if (target_node && target_node.constructor === Number) { - target_node = this.graph.getNodeById(target_node); - } - if (!target_node) { - throw "target node is null"; - } - - //avoid loopback - if (target_node == this) { - return null; - } - - //you can specify the slot by name - if (target_slot.constructor === String) { - target_slot = target_node.findInputSlot(target_slot); - if (target_slot == -1) { - if (LiteGraph.debug) { - console.log( - "Connect: Error, no slot of name " + target_slot - ); - } - return null; - } - } else if (target_slot === LiteGraph.EVENT) { - - if (LiteGraph.do_add_triggers_slots){ - //search for first slot with event? :: NO this is done outside - //console.log("Connect: Creating triggerEvent"); - // force mode - target_node.changeMode(LiteGraph.ON_TRIGGER); - target_slot = target_node.findInputSlot("onTrigger"); - }else{ - return null; // -- break -- - } - } else if ( - !target_node.inputs || - target_slot >= target_node.inputs.length - ) { - if (LiteGraph.debug) { - console.log("Connect: Error, slot number not found"); - } - return null; - } - - var changed = false; - - var input = target_node.inputs[target_slot]; - var link_info = null; - var output = this.outputs[slot]; - - if (!this.outputs[slot]){ - /*console.debug("Invalid slot passed: "+slot); - console.debug(this.outputs);*/ - return null; - } - - // allow target node to change slot - if (target_node.onBeforeConnectInput) { - // This way node can choose another slot (or make a new one?) - target_slot = target_node.onBeforeConnectInput(target_slot); //callback - } - - //check target_slot and check connection types - if (target_slot===false || target_slot===null || !LiteGraph.isValidConnection(output.type, input.type)) - { - this.setDirtyCanvas(false, true); - if(changed) - this.graph.connectionChange(this, link_info); - return null; - }else{ - //console.debug("valid connection",output.type, input.type); - } - - //allows nodes to block connection, callback - if (target_node.onConnectInput) { - if ( target_node.onConnectInput(target_slot, output.type, output, this, slot) === false ) { - return null; - } - } - if (this.onConnectOutput) { // callback - if ( this.onConnectOutput(slot, input.type, input, target_node, target_slot) === false ) { - return null; - } - } - - //if there is something already plugged there, disconnect - if (target_node.inputs[target_slot] && target_node.inputs[target_slot].link != null) { - this.graph.beforeChange(); - target_node.disconnectInput(target_slot, {doProcessChange: false}); - changed = true; - } - if (output.links !== null && output.links.length){ - switch(output.type){ - case LiteGraph.EVENT: - if (!LiteGraph.allow_multi_output_for_events){ - this.graph.beforeChange(); - this.disconnectOutput(slot, false, {doProcessChange: false}); // Input(target_slot, {doProcessChange: false}); - changed = true; - } - break; - default: - break; - } - } - - var nextId - if (LiteGraph.use_uuids) - nextId = LiteGraph.uuidv4(); - else - nextId = ++this.graph.last_link_id; - - //create link class - link_info = new LLink( - nextId, - input.type || output.type, - this.id, - slot, - target_node.id, - target_slot - ); - - //add to graph links list - this.graph.links[link_info.id] = link_info; - - //connect in output - if (output.links == null) { - output.links = []; - } - output.links.push(link_info.id); - //connect in input - target_node.inputs[target_slot].link = link_info.id; - if (this.graph) { - this.graph._version++; - } - if (this.onConnectionsChange) { - this.onConnectionsChange( - LiteGraph.OUTPUT, - slot, - true, - link_info, - output - ); - } //link_info has been created now, so its updated - if (target_node.onConnectionsChange) { - target_node.onConnectionsChange( - LiteGraph.INPUT, - target_slot, - true, - link_info, - input - ); - } - if (this.graph && this.graph.onNodeConnectionChange) { - this.graph.onNodeConnectionChange( - LiteGraph.INPUT, - target_node, - target_slot, - this, - slot - ); - this.graph.onNodeConnectionChange( - LiteGraph.OUTPUT, - this, - slot, - target_node, - target_slot - ); - } - - this.setDirtyCanvas(false, true); - this.graph.afterChange(); - this.graph.connectionChange(this, link_info); - - return link_info; - }; - - /** - * disconnect one output to an specific node - * @method disconnectOutput - * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) - * @param {LGraphNode} target_node the target node to which this slot is connected [Optional, if not target_node is specified all nodes will be disconnected] - * @return {boolean} if it was disconnected successfully - */ - LGraphNode.prototype.disconnectOutput = function(slot, target_node) { - if (slot.constructor === String) { - slot = this.findOutputSlot(slot); - if (slot == -1) { - if (LiteGraph.debug) { - console.log("Connect: Error, no slot of name " + slot); - } - return false; - } - } else if (!this.outputs || slot >= this.outputs.length) { - if (LiteGraph.debug) { - console.log("Connect: Error, slot number not found"); - } - return false; - } - - //get output slot - var output = this.outputs[slot]; - if (!output || !output.links || output.links.length == 0) { - return false; - } - - //one of the output links in this slot - if (target_node) { - if (target_node.constructor === Number) { - target_node = this.graph.getNodeById(target_node); - } - if (!target_node) { - throw "Target Node not found"; - } - - for (var i = 0, l = output.links.length; i < l; i++) { - var link_id = output.links[i]; - var link_info = this.graph.links[link_id]; - - //is the link we are searching for... - if (link_info.target_id == target_node.id) { - output.links.splice(i, 1); //remove here - var input = target_node.inputs[link_info.target_slot]; - input.link = null; //remove there - delete this.graph.links[link_id]; //remove the link from the links pool - if (this.graph) { - this.graph._version++; - } - if (target_node.onConnectionsChange) { - target_node.onConnectionsChange( - LiteGraph.INPUT, - link_info.target_slot, - false, - link_info, - input - ); - } //link_info hasn't been modified so its ok - if (this.onConnectionsChange) { - this.onConnectionsChange( - LiteGraph.OUTPUT, - slot, - false, - link_info, - output - ); - } - if (this.graph && this.graph.onNodeConnectionChange) { - this.graph.onNodeConnectionChange( - LiteGraph.OUTPUT, - this, - slot - ); - } - if (this.graph && this.graph.onNodeConnectionChange) { - this.graph.onNodeConnectionChange( - LiteGraph.OUTPUT, - this, - slot - ); - this.graph.onNodeConnectionChange( - LiteGraph.INPUT, - target_node, - link_info.target_slot - ); - } - break; - } - } - } //all the links in this output slot - else { - for (var i = 0, l = output.links.length; i < l; i++) { - var link_id = output.links[i]; - var link_info = this.graph.links[link_id]; - if (!link_info) { - //bug: it happens sometimes - continue; - } - - var target_node = this.graph.getNodeById(link_info.target_id); - var input = null; - if (this.graph) { - this.graph._version++; - } - if (target_node) { - input = target_node.inputs[link_info.target_slot]; - input.link = null; //remove other side link - if (target_node.onConnectionsChange) { - target_node.onConnectionsChange( - LiteGraph.INPUT, - link_info.target_slot, - false, - link_info, - input - ); - } //link_info hasn't been modified so its ok - if (this.graph && this.graph.onNodeConnectionChange) { - this.graph.onNodeConnectionChange( - LiteGraph.INPUT, - target_node, - link_info.target_slot - ); - } - } - delete this.graph.links[link_id]; //remove the link from the links pool - if (this.onConnectionsChange) { - this.onConnectionsChange( - LiteGraph.OUTPUT, - slot, - false, - link_info, - output - ); - } - if (this.graph && this.graph.onNodeConnectionChange) { - this.graph.onNodeConnectionChange( - LiteGraph.OUTPUT, - this, - slot - ); - this.graph.onNodeConnectionChange( - LiteGraph.INPUT, - target_node, - link_info.target_slot - ); - } - } - output.links = null; - } - - this.setDirtyCanvas(false, true); - this.graph.connectionChange(this); - return true; - }; - - /** - * disconnect one input - * @method disconnectInput - * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) - * @return {boolean} if it was disconnected successfully - */ - LGraphNode.prototype.disconnectInput = function(slot) { - //seek for the output slot - if (slot.constructor === String) { - slot = this.findInputSlot(slot); - if (slot == -1) { - if (LiteGraph.debug) { - console.log("Connect: Error, no slot of name " + slot); - } - return false; - } - } else if (!this.inputs || slot >= this.inputs.length) { - if (LiteGraph.debug) { - console.log("Connect: Error, slot number not found"); - } - return false; - } - - var input = this.inputs[slot]; - if (!input) { - return false; - } - - var link_id = this.inputs[slot].link; - if(link_id != null) - { - this.inputs[slot].link = null; - - //remove other side - var link_info = this.graph.links[link_id]; - if (link_info) { - var target_node = this.graph.getNodeById(link_info.origin_id); - if (!target_node) { - return false; - } - - var output = target_node.outputs[link_info.origin_slot]; - if (!output || !output.links || output.links.length == 0) { - return false; - } - - //search in the inputs list for this link - for (var i = 0, l = output.links.length; i < l; i++) { - if (output.links[i] == link_id) { - output.links.splice(i, 1); - break; - } - } - - delete this.graph.links[link_id]; //remove from the pool - if (this.graph) { - this.graph._version++; - } - if (this.onConnectionsChange) { - this.onConnectionsChange( - LiteGraph.INPUT, - slot, - false, - link_info, - input - ); - } - if (target_node.onConnectionsChange) { - target_node.onConnectionsChange( - LiteGraph.OUTPUT, - i, - false, - link_info, - output - ); - } - if (this.graph && this.graph.onNodeConnectionChange) { - this.graph.onNodeConnectionChange( - LiteGraph.OUTPUT, - target_node, - i - ); - this.graph.onNodeConnectionChange(LiteGraph.INPUT, this, slot); - } - } - } //link != null - - this.setDirtyCanvas(false, true); - if(this.graph) - this.graph.connectionChange(this); - return true; - }; - - /** - * returns the center of a connection point in canvas coords - * @method getConnectionPos - * @param {boolean} is_input true if if a input slot, false if it is an output - * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) - * @param {vec2} out [optional] a place to store the output, to free garbage - * @return {[x,y]} the position - **/ - LGraphNode.prototype.getConnectionPos = function( - is_input, - slot_number, - out - ) { - out = out || new Float32Array(2); - var num_slots = 0; - if (is_input && this.inputs) { - num_slots = this.inputs.length; - } - if (!is_input && this.outputs) { - num_slots = this.outputs.length; - } - - var offset = LiteGraph.NODE_SLOT_HEIGHT * 0.5; - - if (this.flags.collapsed) { - var w = this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH; - if (this.horizontal) { - out[0] = this.pos[0] + w * 0.5; - if (is_input) { - out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT; - } else { - out[1] = this.pos[1]; - } - } else { - if (is_input) { - out[0] = this.pos[0]; - } else { - out[0] = this.pos[0] + w; - } - out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5; - } - return out; - } - - //weird feature that never got finished - if (is_input && slot_number == -1) { - out[0] = this.pos[0] + LiteGraph.NODE_TITLE_HEIGHT * 0.5; - out[1] = this.pos[1] + LiteGraph.NODE_TITLE_HEIGHT * 0.5; - return out; - } - - //hard-coded pos - if ( - is_input && - num_slots > slot_number && - this.inputs[slot_number].pos - ) { - out[0] = this.pos[0] + this.inputs[slot_number].pos[0]; - out[1] = this.pos[1] + this.inputs[slot_number].pos[1]; - return out; - } else if ( - !is_input && - num_slots > slot_number && - this.outputs[slot_number].pos - ) { - out[0] = this.pos[0] + this.outputs[slot_number].pos[0]; - out[1] = this.pos[1] + this.outputs[slot_number].pos[1]; - return out; - } - - //horizontal distributed slots - if (this.horizontal) { - out[0] = - this.pos[0] + (slot_number + 0.5) * (this.size[0] / num_slots); - if (is_input) { - out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT; - } else { - out[1] = this.pos[1] + this.size[1]; - } - return out; - } - - //default vertical slots - if (is_input) { - out[0] = this.pos[0] + offset; - } else { - out[0] = this.pos[0] + this.size[0] + 1 - offset; - } - out[1] = - this.pos[1] + - (slot_number + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + - (this.constructor.slot_start_y || 0); - return out; - }; - - /* Force align to grid */ - LGraphNode.prototype.alignToGrid = function() { - this.pos[0] = - LiteGraph.CANVAS_GRID_SIZE * - Math.round(this.pos[0] / LiteGraph.CANVAS_GRID_SIZE); - this.pos[1] = - LiteGraph.CANVAS_GRID_SIZE * - Math.round(this.pos[1] / LiteGraph.CANVAS_GRID_SIZE); - }; - - /* Console output */ - LGraphNode.prototype.trace = function(msg) { - if (!this.console) { - this.console = []; - } - - this.console.push(msg); - if (this.console.length > LGraphNode.MAX_CONSOLE) { - this.console.shift(); - } - - if(this.graph.onNodeTrace) - this.graph.onNodeTrace(this, msg); - }; - - /* Forces to redraw or the main canvas (LGraphNode) or the bg canvas (links) */ - LGraphNode.prototype.setDirtyCanvas = function( - dirty_foreground, - dirty_background - ) { - if (!this.graph) { - return; - } - this.graph.sendActionToCanvas("setDirty", [ - dirty_foreground, - dirty_background - ]); - }; - - LGraphNode.prototype.loadImage = function(url) { - var img = new Image(); - img.src = LiteGraph.node_images_path + url; - img.ready = false; - - var that = this; - img.onload = function() { - this.ready = true; - that.setDirtyCanvas(true); - }; - return img; - }; - - //safe LGraphNode action execution (not sure if safe) - /* -LGraphNode.prototype.executeAction = function(action) -{ - if(action == "") return false; - - if( action.indexOf(";") != -1 || action.indexOf("}") != -1) - { - this.trace("Error: Action contains unsafe characters"); - return false; - } - - var tokens = action.split("("); - var func_name = tokens[0]; - if( typeof(this[func_name]) != "function") - { - this.trace("Error: Action not found on node: " + func_name); - return false; - } - - var code = action; - - try - { - var _foo = eval; - eval = null; - (new Function("with(this) { " + code + "}")).call(this); - eval = _foo; - } - catch (err) - { - this.trace("Error executing action {" + action + "} :" + err); - return false; - } - - return true; -} -*/ - - /* Allows to get onMouseMove and onMouseUp events even if the mouse is out of focus */ - LGraphNode.prototype.captureInput = function(v) { - if (!this.graph || !this.graph.list_of_graphcanvas) { - return; - } - - var list = this.graph.list_of_graphcanvas; - - for (var i = 0; i < list.length; ++i) { - var c = list[i]; - //releasing somebody elses capture?! - if (!v && c.node_capturing_input != this) { - continue; - } - - //change - c.node_capturing_input = v ? this : null; - } - }; - - /** - * Collapse the node to make it smaller on the canvas - * @method collapse - **/ - LGraphNode.prototype.collapse = function(force) { - this.graph._version++; - if (this.constructor.collapsable === false && !force) { - return; - } - if (!this.flags.collapsed) { - this.flags.collapsed = true; - } else { - this.flags.collapsed = false; - } - this.setDirtyCanvas(true, true); - }; - - /** - * Forces the node to do not move or realign on Z - * @method pin - **/ - - LGraphNode.prototype.pin = function(v) { - this.graph._version++; - if (v === undefined) { - this.flags.pinned = !this.flags.pinned; - } else { - this.flags.pinned = v; - } - }; - - LGraphNode.prototype.localToScreen = function(x, y, graphcanvas) { - return [ - (x + this.pos[0]) * graphcanvas.scale + graphcanvas.offset[0], - (y + this.pos[1]) * graphcanvas.scale + graphcanvas.offset[1] - ]; - }; - - function LGraphGroup(title) { - this._ctor(title); - } - - global.LGraphGroup = LiteGraph.LGraphGroup = LGraphGroup; - - LGraphGroup.prototype._ctor = function(title) { - this.title = title || "Group"; - this.font_size = 24; - this.color = LGraphCanvas.node_colors.pale_blue - ? LGraphCanvas.node_colors.pale_blue.groupcolor - : "#AAA"; - this._bounding = new Float32Array([10, 10, 140, 80]); - this._pos = this._bounding.subarray(0, 2); - this._size = this._bounding.subarray(2, 4); - this._nodes = []; - this.graph = null; - - Object.defineProperty(this, "pos", { - set: function(v) { - if (!v || v.length < 2) { - return; - } - this._pos[0] = v[0]; - this._pos[1] = v[1]; - }, - get: function() { - return this._pos; - }, - enumerable: true - }); - - Object.defineProperty(this, "size", { - set: function(v) { - if (!v || v.length < 2) { - return; - } - this._size[0] = Math.max(140, v[0]); - this._size[1] = Math.max(80, v[1]); - }, - get: function() { - return this._size; - }, - enumerable: true - }); - }; - - LGraphGroup.prototype.configure = function(o) { - this.title = o.title; - this._bounding.set(o.bounding); - this.color = o.color; - if (o.font_size) { - this.font_size = o.font_size; - } - }; - - LGraphGroup.prototype.serialize = function() { - var b = this._bounding; - return { - title: this.title, - bounding: [ - Math.round(b[0]), - Math.round(b[1]), - Math.round(b[2]), - Math.round(b[3]) - ], - color: this.color, - font_size: this.font_size - }; - }; - - LGraphGroup.prototype.move = function(deltax, deltay, ignore_nodes) { - this._pos[0] += deltax; - this._pos[1] += deltay; - if (ignore_nodes) { - return; - } - for (var i = 0; i < this._nodes.length; ++i) { - var node = this._nodes[i]; - node.pos[0] += deltax; - node.pos[1] += deltay; - } - }; - - LGraphGroup.prototype.recomputeInsideNodes = function() { - this._nodes.length = 0; - var nodes = this.graph._nodes; - var node_bounding = new Float32Array(4); - - for (var i = 0; i < nodes.length; ++i) { - var node = nodes[i]; - node.getBounding(node_bounding); - if (!overlapBounding(this._bounding, node_bounding)) { - continue; - } //out of the visible area - this._nodes.push(node); - } - }; - - LGraphGroup.prototype.isPointInside = LGraphNode.prototype.isPointInside; - LGraphGroup.prototype.setDirtyCanvas = LGraphNode.prototype.setDirtyCanvas; - - //**************************************** - - //Scale and Offset - function DragAndScale(element, skip_events) { - this.offset = new Float32Array([0, 0]); - this.scale = 1; - this.max_scale = 10; - this.min_scale = 0.1; - this.onredraw = null; - this.enabled = true; - this.last_mouse = [0, 0]; - this.element = null; - this.visible_area = new Float32Array(4); - - if (element) { - this.element = element; - if (!skip_events) { - this.bindEvents(element); - } - } - } - - LiteGraph.DragAndScale = DragAndScale; - - DragAndScale.prototype.bindEvents = function(element) { - this.last_mouse = new Float32Array(2); - - this._binded_mouse_callback = this.onMouse.bind(this); - - LiteGraph.pointerListenerAdd(element,"down", this._binded_mouse_callback); - LiteGraph.pointerListenerAdd(element,"move", this._binded_mouse_callback); - LiteGraph.pointerListenerAdd(element,"up", this._binded_mouse_callback); - - element.addEventListener( - "mousewheel", - this._binded_mouse_callback, - false - ); - element.addEventListener("wheel", this._binded_mouse_callback, false); - }; - - DragAndScale.prototype.computeVisibleArea = function( viewport ) { - if (!this.element) { - this.visible_area[0] = this.visible_area[1] = this.visible_area[2] = this.visible_area[3] = 0; - return; - } - var width = this.element.width; - var height = this.element.height; - var startx = -this.offset[0]; - var starty = -this.offset[1]; - if( viewport ) - { - startx += viewport[0] / this.scale; - starty += viewport[1] / this.scale; - width = viewport[2]; - height = viewport[3]; - } - var endx = startx + width / this.scale; - var endy = starty + height / this.scale; - this.visible_area[0] = startx; - this.visible_area[1] = starty; - this.visible_area[2] = endx - startx; - this.visible_area[3] = endy - starty; - }; - - DragAndScale.prototype.onMouse = function(e) { - if (!this.enabled) { - return; - } - - var canvas = this.element; - var rect = canvas.getBoundingClientRect(); - var x = e.clientX - rect.left; - var y = e.clientY - rect.top; - e.canvasx = x; - e.canvasy = y; - e.dragging = this.dragging; - - var is_inside = !this.viewport || ( this.viewport && x >= this.viewport[0] && x < (this.viewport[0] + this.viewport[2]) && y >= this.viewport[1] && y < (this.viewport[1] + this.viewport[3]) ); - - //console.log("pointerevents: DragAndScale onMouse "+e.type+" "+is_inside); - - var ignore = false; - if (this.onmouse) { - ignore = this.onmouse(e); - } - - if (e.type == LiteGraph.pointerevents_method+"down" && is_inside) { - this.dragging = true; - LiteGraph.pointerListenerRemove(canvas,"move",this._binded_mouse_callback); - LiteGraph.pointerListenerAdd(document,"move",this._binded_mouse_callback); - LiteGraph.pointerListenerAdd(document,"up",this._binded_mouse_callback); - } else if (e.type == LiteGraph.pointerevents_method+"move") { - if (!ignore) { - var deltax = x - this.last_mouse[0]; - var deltay = y - this.last_mouse[1]; - if (this.dragging) { - this.mouseDrag(deltax, deltay); - } - } - } else if (e.type == LiteGraph.pointerevents_method+"up") { - this.dragging = false; - LiteGraph.pointerListenerRemove(document,"move",this._binded_mouse_callback); - LiteGraph.pointerListenerRemove(document,"up",this._binded_mouse_callback); - LiteGraph.pointerListenerAdd(canvas,"move",this._binded_mouse_callback); - } else if ( is_inside && - (e.type == "mousewheel" || - e.type == "wheel" || - e.type == "DOMMouseScroll") - ) { - e.eventType = "mousewheel"; - if (e.type == "wheel") { - e.wheel = -e.deltaY; - } else { - e.wheel = - e.wheelDeltaY != null ? e.wheelDeltaY : e.detail * -60; - } - - //from stack overflow - e.delta = e.wheelDelta - ? e.wheelDelta / 40 - : e.deltaY - ? -e.deltaY / 3 - : 0; - this.changeDeltaScale(1.0 + e.delta * 0.05); - } - - this.last_mouse[0] = x; - this.last_mouse[1] = y; - - if(is_inside) - { - e.preventDefault(); - e.stopPropagation(); - return false; - } - }; - - DragAndScale.prototype.toCanvasContext = function(ctx) { - ctx.scale(this.scale, this.scale); - ctx.translate(this.offset[0], this.offset[1]); - }; - - DragAndScale.prototype.convertOffsetToCanvas = function(pos) { - //return [pos[0] / this.scale - this.offset[0], pos[1] / this.scale - this.offset[1]]; - return [ - (pos[0] + this.offset[0]) * this.scale, - (pos[1] + this.offset[1]) * this.scale - ]; - }; - - DragAndScale.prototype.convertCanvasToOffset = function(pos, out) { - out = out || [0, 0]; - out[0] = pos[0] / this.scale - this.offset[0]; - out[1] = pos[1] / this.scale - this.offset[1]; - return out; - }; - - DragAndScale.prototype.mouseDrag = function(x, y) { - this.offset[0] += x / this.scale; - this.offset[1] += y / this.scale; - - if (this.onredraw) { - this.onredraw(this); - } - }; - - DragAndScale.prototype.changeScale = function(value, zooming_center) { - if (value < this.min_scale) { - value = this.min_scale; - } else if (value > this.max_scale) { - value = this.max_scale; - } - - if (value == this.scale) { - return; - } - - if (!this.element) { - return; - } - - var rect = this.element.getBoundingClientRect(); - if (!rect) { - return; - } - - zooming_center = zooming_center || [ - rect.width * 0.5, - rect.height * 0.5 - ]; - var center = this.convertCanvasToOffset(zooming_center); - this.scale = value; - if (Math.abs(this.scale - 1) < 0.01) { - this.scale = 1; - } - - var new_center = this.convertCanvasToOffset(zooming_center); - var delta_offset = [ - new_center[0] - center[0], - new_center[1] - center[1] - ]; - - this.offset[0] += delta_offset[0]; - this.offset[1] += delta_offset[1]; - - if (this.onredraw) { - this.onredraw(this); - } - }; - - DragAndScale.prototype.changeDeltaScale = function(value, zooming_center) { - this.changeScale(this.scale * value, zooming_center); - }; - - DragAndScale.prototype.reset = function() { - this.scale = 1; - this.offset[0] = 0; - this.offset[1] = 0; - }; - - //********************************************************************************* - // LGraphCanvas: LGraph renderer CLASS - //********************************************************************************* - - /** - * This class is in charge of rendering one graph inside a canvas. And provides all the interaction required. - * Valid callbacks are: onNodeSelected, onNodeDeselected, onShowNodePanel, onNodeDblClicked - * - * @class LGraphCanvas - * @constructor - * @param {HTMLCanvas} canvas the canvas where you want to render (it accepts a selector in string format or the canvas element itself) - * @param {LGraph} graph [optional] - * @param {Object} options [optional] { skip_rendering, autoresize, viewport } - */ - function LGraphCanvas(canvas, graph, options) { - this.options = options = options || {}; - - //if(graph === undefined) - // throw ("No graph assigned"); - this.background_image = LGraphCanvas.DEFAULT_BACKGROUND_IMAGE; - - if (canvas && canvas.constructor === String) { - canvas = document.querySelector(canvas); - } - - this.ds = new DragAndScale(); - this.zoom_modify_alpha = true; //otherwise it generates ugly patterns when scaling down too much - - this.title_text_font = "" + LiteGraph.NODE_TEXT_SIZE + "px Arial"; - this.inner_text_font = - "normal " + LiteGraph.NODE_SUBTEXT_SIZE + "px Arial"; - this.node_title_color = LiteGraph.NODE_TITLE_COLOR; - this.default_link_color = LiteGraph.LINK_COLOR; - this.default_connection_color = { - input_off: "#778", - input_on: "#7F7", //"#BBD" - output_off: "#778", - output_on: "#7F7" //"#BBD" - }; - this.default_connection_color_byType = { - /*number: "#7F7", - string: "#77F", - boolean: "#F77",*/ - } - this.default_connection_color_byTypeOff = { - /*number: "#474", - string: "#447", - boolean: "#744",*/ - }; - - this.highquality_render = true; - this.use_gradients = false; //set to true to render titlebar with gradients - this.editor_alpha = 1; //used for transition - this.pause_rendering = false; - this.clear_background = true; - this.clear_background_color = "#222"; - - this.read_only = false; //if set to true users cannot modify the graph - this.render_only_selected = true; - this.live_mode = false; - this.show_info = true; - this.allow_dragcanvas = true; - this.allow_dragnodes = true; - this.allow_interaction = true; //allow to control widgets, buttons, collapse, etc - this.multi_select = false; //allow selecting multi nodes without pressing extra keys - this.allow_searchbox = true; - this.allow_reconnect_links = true; //allows to change a connection with having to redo it again - this.align_to_grid = false; //snap to grid - - this.drag_mode = false; - this.dragging_rectangle = null; - - this.filter = null; //allows to filter to only accept some type of nodes in a graph - - this.set_canvas_dirty_on_mouse_event = true; //forces to redraw the canvas if the mouse does anything - this.always_render_background = false; - this.render_shadows = true; - this.render_canvas_border = true; - this.render_connections_shadows = false; //too much cpu - this.render_connections_border = true; - this.render_curved_connections = false; - this.render_connection_arrows = false; - this.render_collapsed_slots = true; - this.render_execution_order = false; - this.render_title_colored = true; - this.render_link_tooltip = true; - - this.links_render_mode = LiteGraph.SPLINE_LINK; - - this.mouse = [0, 0]; //mouse in canvas coordinates, where 0,0 is the top-left corner of the blue rectangle - this.graph_mouse = [0, 0]; //mouse in graph coordinates, where 0,0 is the top-left corner of the blue rectangle - this.canvas_mouse = this.graph_mouse; //LEGACY: REMOVE THIS, USE GRAPH_MOUSE INSTEAD - - //to personalize the search box - this.onSearchBox = null; - this.onSearchBoxSelection = null; - - //callbacks - this.onMouse = null; - this.onDrawBackground = null; //to render background objects (behind nodes and connections) in the canvas affected by transform - this.onDrawForeground = null; //to render foreground objects (above nodes and connections) in the canvas affected by transform - this.onDrawOverlay = null; //to render foreground objects not affected by transform (for GUIs) - this.onDrawLinkTooltip = null; //called when rendering a tooltip - this.onNodeMoved = null; //called after moving a node - this.onSelectionChange = null; //called if the selection changes - this.onConnectingChange = null; //called before any link changes - this.onBeforeChange = null; //called before modifying the graph - this.onAfterChange = null; //called after modifying the graph - - this.connections_width = 3; - this.round_radius = 8; - - this.current_node = null; - this.node_widget = null; //used for widgets - this.over_link_center = null; - this.last_mouse_position = [0, 0]; - this.visible_area = this.ds.visible_area; - this.visible_links = []; - - this.viewport = options.viewport || null; //to constraint render area to a portion of the canvas - - //link canvas and graph - if (graph) { - graph.attachCanvas(this); - } - - this.setCanvas(canvas,options.skip_events); - this.clear(); - - if (!options.skip_render) { - this.startRendering(); - } - - this.autoresize = options.autoresize; - } - - global.LGraphCanvas = LiteGraph.LGraphCanvas = LGraphCanvas; - - LGraphCanvas.DEFAULT_BACKGROUND_IMAGE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII="; - - LGraphCanvas.link_type_colors = { - "-1": LiteGraph.EVENT_LINK_COLOR, - number: "#AAA", - node: "#DCA" - }; - LGraphCanvas.gradients = {}; //cache of gradients - - /** - * clears all the data inside - * - * @method clear - */ - LGraphCanvas.prototype.clear = function() { - this.frame = 0; - this.last_draw_time = 0; - this.render_time = 0; - this.fps = 0; - - //this.scale = 1; - //this.offset = [0,0]; - - this.dragging_rectangle = null; - - this.selected_nodes = {}; - this.selected_group = null; - - this.visible_nodes = []; - this.node_dragged = null; - this.node_over = null; - this.node_capturing_input = null; - this.connecting_node = null; - this.highlighted_links = {}; - - this.dragging_canvas = false; - - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - this.dirty_area = null; - - this.node_in_panel = null; - this.node_widget = null; - - this.last_mouse = [0, 0]; - this.last_mouseclick = 0; - this.pointer_is_down = false; - this.pointer_is_double = false; - this.visible_area.set([0, 0, 0, 0]); - - if (this.onClear) { - this.onClear(); - } - }; - - /** - * assigns a graph, you can reassign graphs to the same canvas - * - * @method setGraph - * @param {LGraph} graph - */ - LGraphCanvas.prototype.setGraph = function(graph, skip_clear) { - if (this.graph == graph) { - return; - } - - if (!skip_clear) { - this.clear(); - } - - if (!graph && this.graph) { - this.graph.detachCanvas(this); - return; - } - - graph.attachCanvas(this); - - //remove the graph stack in case a subgraph was open - if (this._graph_stack) - this._graph_stack = null; - - this.setDirty(true, true); - }; - - /** - * returns the top level graph (in case there are subgraphs open on the canvas) - * - * @method getTopGraph - * @return {LGraph} graph - */ - LGraphCanvas.prototype.getTopGraph = function() - { - if(this._graph_stack.length) - return this._graph_stack[0]; - return this.graph; - } - - /** - * opens a graph contained inside a node in the current graph - * - * @method openSubgraph - * @param {LGraph} graph - */ - LGraphCanvas.prototype.openSubgraph = function(graph) { - if (!graph) { - throw "graph cannot be null"; - } - - if (this.graph == graph) { - throw "graph cannot be the same"; - } - - this.clear(); - - if (this.graph) { - if (!this._graph_stack) { - this._graph_stack = []; - } - this._graph_stack.push(this.graph); - } - - graph.attachCanvas(this); - this.checkPanels(); - this.setDirty(true, true); - }; - - /** - * closes a subgraph contained inside a node - * - * @method closeSubgraph - * @param {LGraph} assigns a graph - */ - LGraphCanvas.prototype.closeSubgraph = function() { - if (!this._graph_stack || this._graph_stack.length == 0) { - return; - } - var subgraph_node = this.graph._subgraph_node; - var graph = this._graph_stack.pop(); - this.selected_nodes = {}; - this.highlighted_links = {}; - graph.attachCanvas(this); - this.setDirty(true, true); - if (subgraph_node) { - this.centerOnNode(subgraph_node); - this.selectNodes([subgraph_node]); - } - // when close sub graph back to offset [0, 0] scale 1 - this.ds.offset = [0, 0] - this.ds.scale = 1 - }; - - /** - * returns the visually active graph (in case there are more in the stack) - * @method getCurrentGraph - * @return {LGraph} the active graph - */ - LGraphCanvas.prototype.getCurrentGraph = function() { - return this.graph; - }; - - /** - * assigns a canvas - * - * @method setCanvas - * @param {Canvas} assigns a canvas (also accepts the ID of the element (not a selector) - */ - LGraphCanvas.prototype.setCanvas = function(canvas, skip_events) { - var that = this; - - if (canvas) { - if (canvas.constructor === String) { - canvas = document.getElementById(canvas); - if (!canvas) { - throw "Error creating LiteGraph canvas: Canvas not found"; - } - } - } - - if (canvas === this.canvas) { - return; - } - - if (!canvas && this.canvas) { - //maybe detach events from old_canvas - if (!skip_events) { - this.unbindEvents(); - } - } - - this.canvas = canvas; - this.ds.element = canvas; - - if (!canvas) { - return; - } - - //this.canvas.tabindex = "1000"; - canvas.className += " lgraphcanvas"; - canvas.data = this; - canvas.tabindex = "1"; //to allow key events - - //bg canvas: used for non changing stuff - this.bgcanvas = null; - if (!this.bgcanvas) { - this.bgcanvas = document.createElement("canvas"); - this.bgcanvas.width = this.canvas.width; - this.bgcanvas.height = this.canvas.height; - } - - if (canvas.getContext == null) { - if (canvas.localName != "canvas") { - throw "Element supplied for LGraphCanvas must be a element, you passed a " + - canvas.localName; - } - throw "This browser doesn't support Canvas"; - } - - var ctx = (this.ctx = canvas.getContext("2d")); - if (ctx == null) { - if (!canvas.webgl_enabled) { - console.warn( - "This canvas seems to be WebGL, enabling WebGL renderer" - ); - } - this.enableWebGL(); - } - - //input: (move and up could be unbinded) - // why here? this._mousemove_callback = this.processMouseMove.bind(this); - // why here? this._mouseup_callback = this.processMouseUp.bind(this); - - if (!skip_events) { - this.bindEvents(); - } - }; - - //used in some events to capture them - LGraphCanvas.prototype._doNothing = function doNothing(e) { - //console.log("pointerevents: _doNothing "+e.type); - e.preventDefault(); - return false; - }; - LGraphCanvas.prototype._doReturnTrue = function doNothing(e) { - e.preventDefault(); - return true; - }; - - /** - * binds mouse, keyboard, touch and drag events to the canvas - * @method bindEvents - **/ - LGraphCanvas.prototype.bindEvents = function() { - if (this._events_binded) { - console.warn("LGraphCanvas: events already binded"); - return; - } - - //console.log("pointerevents: bindEvents"); - - var canvas = this.canvas; - - var ref_window = this.getCanvasWindow(); - var document = ref_window.document; //hack used when moving canvas between windows - - this._mousedown_callback = this.processMouseDown.bind(this); - this._mousewheel_callback = this.processMouseWheel.bind(this); - // why mousemove and mouseup were not binded here? - this._mousemove_callback = this.processMouseMove.bind(this); - this._mouseup_callback = this.processMouseUp.bind(this); - - //touch events -- TODO IMPLEMENT - //this._touch_callback = this.touchHandler.bind(this); - - LiteGraph.pointerListenerAdd(canvas,"down", this._mousedown_callback, true); //down do not need to store the binded - canvas.addEventListener("mousewheel", this._mousewheel_callback, false); - - LiteGraph.pointerListenerAdd(canvas,"up", this._mouseup_callback, true); // CHECK: ??? binded or not - LiteGraph.pointerListenerAdd(canvas,"move", this._mousemove_callback); - - canvas.addEventListener("contextmenu", this._doNothing); - canvas.addEventListener( - "DOMMouseScroll", - this._mousewheel_callback, - false - ); - - //touch events -- THIS WAY DOES NOT WORK, finish implementing pointerevents, than clean the touchevents - /*if( 'touchstart' in document.documentElement ) - { - canvas.addEventListener("touchstart", this._touch_callback, true); - canvas.addEventListener("touchmove", this._touch_callback, true); - canvas.addEventListener("touchend", this._touch_callback, true); - canvas.addEventListener("touchcancel", this._touch_callback, true); - }*/ - - //Keyboard ****************** - this._key_callback = this.processKey.bind(this); - - canvas.addEventListener("keydown", this._key_callback, true); - document.addEventListener("keyup", this._key_callback, true); //in document, otherwise it doesn't fire keyup - - //Dropping Stuff over nodes ************************************ - this._ondrop_callback = this.processDrop.bind(this); - - canvas.addEventListener("dragover", this._doNothing, false); - canvas.addEventListener("dragend", this._doNothing, false); - canvas.addEventListener("drop", this._ondrop_callback, false); - canvas.addEventListener("dragenter", this._doReturnTrue, false); - - this._events_binded = true; - }; - - /** - * unbinds mouse events from the canvas - * @method unbindEvents - **/ - LGraphCanvas.prototype.unbindEvents = function() { - if (!this._events_binded) { - console.warn("LGraphCanvas: no events binded"); - return; - } - - //console.log("pointerevents: unbindEvents"); - - var ref_window = this.getCanvasWindow(); - var document = ref_window.document; - - LiteGraph.pointerListenerRemove(this.canvas,"move", this._mousedown_callback); - LiteGraph.pointerListenerRemove(this.canvas,"up", this._mousedown_callback); - LiteGraph.pointerListenerRemove(this.canvas,"down", this._mousedown_callback); - this.canvas.removeEventListener( - "mousewheel", - this._mousewheel_callback - ); - this.canvas.removeEventListener( - "DOMMouseScroll", - this._mousewheel_callback - ); - this.canvas.removeEventListener("keydown", this._key_callback); - document.removeEventListener("keyup", this._key_callback); - this.canvas.removeEventListener("contextmenu", this._doNothing); - this.canvas.removeEventListener("drop", this._ondrop_callback); - this.canvas.removeEventListener("dragenter", this._doReturnTrue); - - //touch events -- THIS WAY DOES NOT WORK, finish implementing pointerevents, than clean the touchevents - /*this.canvas.removeEventListener("touchstart", this._touch_callback ); - this.canvas.removeEventListener("touchmove", this._touch_callback ); - this.canvas.removeEventListener("touchend", this._touch_callback ); - this.canvas.removeEventListener("touchcancel", this._touch_callback );*/ - - this._mousedown_callback = null; - this._mousewheel_callback = null; - this._key_callback = null; - this._ondrop_callback = null; - - this._events_binded = false; - }; - - LGraphCanvas.getFileExtension = function(url) { - var question = url.indexOf("?"); - if (question != -1) { - url = url.substr(0, question); - } - var point = url.lastIndexOf("."); - if (point == -1) { - return ""; - } - return url.substr(point + 1).toLowerCase(); - }; - - /** - * this function allows to render the canvas using WebGL instead of Canvas2D - * this is useful if you plant to render 3D objects inside your nodes, it uses litegl.js for webgl and canvas2DtoWebGL to emulate the Canvas2D calls in webGL - * @method enableWebGL - **/ - LGraphCanvas.prototype.enableWebGL = function() { - if (typeof GL === "undefined") { - throw "litegl.js must be included to use a WebGL canvas"; - } - if (typeof enableWebGLCanvas === "undefined") { - throw "webglCanvas.js must be included to use this feature"; - } - - this.gl = this.ctx = enableWebGLCanvas(this.canvas); - this.ctx.webgl = true; - this.bgcanvas = this.canvas; - this.bgctx = this.gl; - this.canvas.webgl_enabled = true; - - /* - GL.create({ canvas: this.bgcanvas }); - this.bgctx = enableWebGLCanvas( this.bgcanvas ); - window.gl = this.gl; - */ - }; - - /** - * marks as dirty the canvas, this way it will be rendered again - * - * @class LGraphCanvas - * @method setDirty - * @param {bool} fgcanvas if the foreground canvas is dirty (the one containing the nodes) - * @param {bool} bgcanvas if the background canvas is dirty (the one containing the wires) - */ - LGraphCanvas.prototype.setDirty = function(fgcanvas, bgcanvas) { - if (fgcanvas) { - this.dirty_canvas = true; - } - if (bgcanvas) { - this.dirty_bgcanvas = true; - } - }; - - /** - * Used to attach the canvas in a popup - * - * @method getCanvasWindow - * @return {window} returns the window where the canvas is attached (the DOM root node) - */ - LGraphCanvas.prototype.getCanvasWindow = function() { - if (!this.canvas) { - return window; - } - var doc = this.canvas.ownerDocument; - return doc.defaultView || doc.parentWindow; - }; - - /** - * starts rendering the content of the canvas when needed - * - * @method startRendering - */ - LGraphCanvas.prototype.startRendering = function() { - if (this.is_rendering) { - return; - } //already rendering - - this.is_rendering = true; - renderFrame.call(this); - - function renderFrame() { - if (!this.pause_rendering) { - this.draw(); - } - - var window = this.getCanvasWindow(); - if (this.is_rendering) { - window.requestAnimationFrame(renderFrame.bind(this)); - } - } - }; - - /** - * stops rendering the content of the canvas (to save resources) - * - * @method stopRendering - */ - LGraphCanvas.prototype.stopRendering = function() { - this.is_rendering = false; - /* - if(this.rendering_timer_id) - { - clearInterval(this.rendering_timer_id); - this.rendering_timer_id = null; - } - */ - }; - - /* LiteGraphCanvas input */ - - //used to block future mouse events (because of im gui) - LGraphCanvas.prototype.blockClick = function() - { - this.block_click = true; - this.last_mouseclick = 0; - } - - LGraphCanvas.prototype.processMouseDown = function(e) { - - if( this.set_canvas_dirty_on_mouse_event ) - this.dirty_canvas = true; - - if (!this.graph) { - return; - } - - this.adjustMouseEvent(e); - - var ref_window = this.getCanvasWindow(); - var document = ref_window.document; - LGraphCanvas.active_canvas = this; - var that = this; - - var x = e.clientX; - var y = e.clientY; - //console.log(y,this.viewport); - //console.log("pointerevents: processMouseDown pointerId:"+e.pointerId+" which:"+e.which+" isPrimary:"+e.isPrimary+" :: x y "+x+" "+y); - - this.ds.viewport = this.viewport; - var is_inside = !this.viewport || ( this.viewport && x >= this.viewport[0] && x < (this.viewport[0] + this.viewport[2]) && y >= this.viewport[1] && y < (this.viewport[1] + this.viewport[3]) ); - - //move mouse move event to the window in case it drags outside of the canvas - if(!this.options.skip_events) - { - LiteGraph.pointerListenerRemove(this.canvas,"move", this._mousemove_callback); - LiteGraph.pointerListenerAdd(ref_window.document,"move", this._mousemove_callback,true); //catch for the entire window - LiteGraph.pointerListenerAdd(ref_window.document,"up", this._mouseup_callback,true); - } - - if(!is_inside){ - return; - } - - var node = this.graph.getNodeOnPos( e.canvasX, e.canvasY, this.visible_nodes, 5 ); - var skip_dragging = false; - var skip_action = false; - var now = LiteGraph.getTime(); - var is_primary = (e.isPrimary === undefined || !e.isPrimary); - var is_double_click = (now - this.last_mouseclick < 300); - this.mouse[0] = e.clientX; - this.mouse[1] = e.clientY; - this.graph_mouse[0] = e.canvasX; - this.graph_mouse[1] = e.canvasY; - this.last_click_position = [this.mouse[0],this.mouse[1]]; - - if (this.pointer_is_down && is_primary ){ - this.pointer_is_double = true; - //console.log("pointerevents: pointer_is_double start"); - }else{ - this.pointer_is_double = false; - } - this.pointer_is_down = true; - - - this.canvas.focus(); - - LiteGraph.closeAllContextMenus(ref_window); - - if (this.onMouse) - { - if (this.onMouse(e) == true) - return; - } - - //left button mouse / single finger - if (e.which == 1 && !this.pointer_is_double) - { - if (e.ctrlKey) - { - this.dragging_rectangle = new Float32Array(4); - this.dragging_rectangle[0] = e.canvasX; - this.dragging_rectangle[1] = e.canvasY; - this.dragging_rectangle[2] = 1; - this.dragging_rectangle[3] = 1; - skip_action = true; - } - - // clone node ALT dragging - if (LiteGraph.alt_drag_do_clone_nodes && e.altKey && node && this.allow_interaction && !skip_action && !this.read_only) - { - if (cloned = node.clone()){ - cloned.pos[0] += 5; - cloned.pos[1] += 5; - this.graph.add(cloned,false,{doCalcSize: false}); - node = cloned; - skip_action = true; - if (!block_drag_node) { - if (this.allow_dragnodes) { - this.graph.beforeChange(); - this.node_dragged = node; - } - if (!this.selected_nodes[node.id]) { - this.processNodeSelected(node, e); - } - } - } - } - - var clicking_canvas_bg = false; - - //when clicked on top of a node - //and it is not interactive - if (node && (this.allow_interaction || node.flags.allow_interaction) && !skip_action && !this.read_only) { - if (!this.live_mode && !node.flags.pinned) { - this.bringToFront(node); - } //if it wasn't selected? - - //not dragging mouse to connect two slots - if ( this.allow_interaction && !this.connecting_node && !node.flags.collapsed && !this.live_mode ) { - //Search for corner for resize - if ( !skip_action && - node.resizable !== false && node.inResizeCorner(e.canvasX, e.canvasY) - ) { - this.graph.beforeChange(); - this.resizing_node = node; - this.canvas.style.cursor = "se-resize"; - skip_action = true; - } else { - //search for outputs - if (node.outputs) { - for ( var i = 0, l = node.outputs.length; i < l; ++i ) { - var output = node.outputs[i]; - var link_pos = node.getConnectionPos(false, i); - if ( - isInsideRectangle( - e.canvasX, - e.canvasY, - link_pos[0] - 15, - link_pos[1] - 10, - 30, - 20 - ) - ) { - this.connecting_node = node; - this.connecting_output = output; - this.connecting_output.slot_index = i; - this.connecting_pos = node.getConnectionPos( false, i ); - this.connecting_slot = i; - - if (LiteGraph.shift_click_do_break_link_from){ - if (e.shiftKey) { - node.disconnectOutput(i); - } - } - - if (is_double_click) { - if (node.onOutputDblClick) { - node.onOutputDblClick(i, e); - } - } else { - if (node.onOutputClick) { - node.onOutputClick(i, e); - } - } - - skip_action = true; - break; - } - } - } - - //search for inputs - if (node.inputs) { - for ( var i = 0, l = node.inputs.length; i < l; ++i ) { - var input = node.inputs[i]; - var link_pos = node.getConnectionPos(true, i); - if ( - isInsideRectangle( - e.canvasX, - e.canvasY, - link_pos[0] - 15, - link_pos[1] - 10, - 30, - 20 - ) - ) { - if (is_double_click) { - if (node.onInputDblClick) { - node.onInputDblClick(i, e); - } - } else { - if (node.onInputClick) { - node.onInputClick(i, e); - } - } - - if (input.link !== null) { - var link_info = this.graph.links[ - input.link - ]; //before disconnecting - if (LiteGraph.click_do_break_link_to){ - node.disconnectInput(i); - this.dirty_bgcanvas = true; - skip_action = true; - }else{ - // do same action as has not node ? - } - - if ( - this.allow_reconnect_links || - //this.move_destination_link_without_shift || - e.shiftKey - ) { - if (!LiteGraph.click_do_break_link_to){ - node.disconnectInput(i); - } - this.connecting_node = this.graph._nodes_by_id[ - link_info.origin_id - ]; - this.connecting_slot = - link_info.origin_slot; - this.connecting_output = this.connecting_node.outputs[ - this.connecting_slot - ]; - this.connecting_pos = this.connecting_node.getConnectionPos( false, this.connecting_slot ); - - this.dirty_bgcanvas = true; - skip_action = true; - } - - - }else{ - // has not node - } - - if (!skip_action){ - // connect from in to out, from to to from - this.connecting_node = node; - this.connecting_input = input; - this.connecting_input.slot_index = i; - this.connecting_pos = node.getConnectionPos( true, i ); - this.connecting_slot = i; - - this.dirty_bgcanvas = true; - skip_action = true; - } - } - } - } - } //not resizing - } - - //it wasn't clicked on the links boxes - if (!skip_action) { - var block_drag_node = false; - if(node && node.flags && node.flags.pinned) { - block_drag_node = true; - } - var pos = [e.canvasX - node.pos[0], e.canvasY - node.pos[1]]; - - //widgets - var widget = this.processNodeWidgets( node, this.graph_mouse, e ); - if (widget) { - block_drag_node = true; - this.node_widget = [node, widget]; - } - - //double clicking - if (this.allow_interaction && is_double_click && this.selected_nodes[node.id]) { - //double click node - if (node.onDblClick) { - node.onDblClick( e, pos, this ); - } - this.processNodeDblClicked(node); - block_drag_node = true; - } - - //if do not capture mouse - if ( node.onMouseDown && node.onMouseDown( e, pos, this ) ) { - block_drag_node = true; - } else { - //open subgraph button - if(node.subgraph && !node.skip_subgraph_button) - { - if ( !node.flags.collapsed && pos[0] > node.size[0] - LiteGraph.NODE_TITLE_HEIGHT && pos[1] < 0 ) { - var that = this; - setTimeout(function() { - that.openSubgraph(node.subgraph); - }, 10); - } - } - - if (this.live_mode) { - clicking_canvas_bg = true; - block_drag_node = true; - } - } - - if (!block_drag_node) { - if (this.allow_dragnodes) { - this.graph.beforeChange(); - this.node_dragged = node; - } - this.processNodeSelected(node, e); - } else { // double-click - /** - * Don't call the function if the block is already selected. - * Otherwise, it could cause the block to be unselected while its panel is open. - */ - if (!node.is_selected) this.processNodeSelected(node, e); - } - - this.dirty_canvas = true; - } - } //clicked outside of nodes - else { - if (!skip_action){ - //search for link connector - if(!this.read_only) { - for (var i = 0; i < this.visible_links.length; ++i) { - var link = this.visible_links[i]; - var center = link._pos; - if ( - !center || - e.canvasX < center[0] - 4 || - e.canvasX > center[0] + 4 || - e.canvasY < center[1] - 4 || - e.canvasY > center[1] + 4 - ) { - continue; - } - //link clicked - this.showLinkMenu(link, e); - this.over_link_center = null; //clear tooltip - break; - } - } - - this.selected_group = this.graph.getGroupOnPos( e.canvasX, e.canvasY ); - this.selected_group_resizing = false; - if (this.selected_group && !this.read_only ) { - if (e.ctrlKey) { - this.dragging_rectangle = null; - } - - var dist = distance( [e.canvasX, e.canvasY], [ this.selected_group.pos[0] + this.selected_group.size[0], this.selected_group.pos[1] + this.selected_group.size[1] ] ); - if (dist * this.ds.scale < 10) { - this.selected_group_resizing = true; - } else { - this.selected_group.recomputeInsideNodes(); - } - } - - if (is_double_click && !this.read_only && this.allow_searchbox) { - this.showSearchBox(e); - e.preventDefault(); - e.stopPropagation(); - } - - clicking_canvas_bg = true; - } - } - - if (!skip_action && clicking_canvas_bg && this.allow_dragcanvas) { - //console.log("pointerevents: dragging_canvas start"); - this.dragging_canvas = true; - } - - } else if (e.which == 2) { - //middle button - - if (LiteGraph.middle_click_slot_add_default_node){ - if (node && this.allow_interaction && !skip_action && !this.read_only){ - //not dragging mouse to connect two slots - if ( - !this.connecting_node && - !node.flags.collapsed && - !this.live_mode - ) { - var mClikSlot = false; - var mClikSlot_index = false; - var mClikSlot_isOut = false; - //search for outputs - if (node.outputs) { - for ( var i = 0, l = node.outputs.length; i < l; ++i ) { - var output = node.outputs[i]; - var link_pos = node.getConnectionPos(false, i); - if (isInsideRectangle(e.canvasX,e.canvasY,link_pos[0] - 15,link_pos[1] - 10,30,20)) { - mClikSlot = output; - mClikSlot_index = i; - mClikSlot_isOut = true; - break; - } - } - } - - //search for inputs - if (node.inputs) { - for ( var i = 0, l = node.inputs.length; i < l; ++i ) { - var input = node.inputs[i]; - var link_pos = node.getConnectionPos(true, i); - if (isInsideRectangle(e.canvasX,e.canvasY,link_pos[0] - 15,link_pos[1] - 10,30,20)) { - mClikSlot = input; - mClikSlot_index = i; - mClikSlot_isOut = false; - break; - } - } - } - //console.log("middleClickSlots? "+mClikSlot+" & "+(mClikSlot_index!==false)); - if (mClikSlot && mClikSlot_index!==false){ - - var alphaPosY = 0.5-((mClikSlot_index+1)/((mClikSlot_isOut?node.outputs.length:node.inputs.length))); - var node_bounding = node.getBounding(); - // estimate a position: this is a bad semi-bad-working mess .. REFACTOR with a correct autoplacement that knows about the others slots and nodes - var posRef = [ (!mClikSlot_isOut?node_bounding[0]:node_bounding[0]+node_bounding[2])// + node_bounding[0]/this.canvas.width*150 - ,e.canvasY-80// + node_bounding[0]/this.canvas.width*66 // vertical "derive" - ]; - var nodeCreated = this.createDefaultNodeForSlot({ nodeFrom: !mClikSlot_isOut?null:node - ,slotFrom: !mClikSlot_isOut?null:mClikSlot_index - ,nodeTo: !mClikSlot_isOut?node:null - ,slotTo: !mClikSlot_isOut?mClikSlot_index:null - ,position: posRef //,e: e - ,nodeType: "AUTO" //nodeNewType - ,posAdd:[!mClikSlot_isOut?-30:30, -alphaPosY*130] //-alphaPosY*30] - ,posSizeFix:[!mClikSlot_isOut?-1:0, 0] //-alphaPosY*2*/ - }); - skip_action = true; - } - } - } - } - - if (!skip_action && this.allow_dragcanvas) { - //console.log("pointerevents: dragging_canvas start from middle button"); - this.dragging_canvas = true; - } - - - } else if (e.which == 3 || this.pointer_is_double) { - - //right button - if (this.allow_interaction && !skip_action && !this.read_only){ - - // is it hover a node ? - if (node){ - if(Object.keys(this.selected_nodes).length - && (this.selected_nodes[node.id] || e.shiftKey || e.ctrlKey || e.metaKey) - ){ - // is multiselected or using shift to include the now node - if (!this.selected_nodes[node.id]) this.selectNodes([node],true); // add this if not present - }else{ - // update selection - this.selectNodes([node]); - } - } - - // show menu on this node - this.processContextMenu(node, e); - } - - } - - //TODO - //if(this.node_selected != prev_selected) - // this.onNodeSelectionChange(this.node_selected); - - this.last_mouse[0] = e.clientX; - this.last_mouse[1] = e.clientY; - this.last_mouseclick = LiteGraph.getTime(); - this.last_mouse_dragging = true; - - /* - if( (this.dirty_canvas || this.dirty_bgcanvas) && this.rendering_timer_id == null) - this.draw(); - */ - - this.graph.change(); - - //this is to ensure to defocus(blur) if a text input element is on focus - if ( - !ref_window.document.activeElement || - (ref_window.document.activeElement.nodeName.toLowerCase() != - "input" && - ref_window.document.activeElement.nodeName.toLowerCase() != - "textarea") - ) { - e.preventDefault(); - } - e.stopPropagation(); - - if (this.onMouseDown) { - this.onMouseDown(e); - } - - return false; - }; - - /** - * Called when a mouse move event has to be processed - * @method processMouseMove - **/ - LGraphCanvas.prototype.processMouseMove = function(e) { - if (this.autoresize) { - this.resize(); - } - - if( this.set_canvas_dirty_on_mouse_event ) - this.dirty_canvas = true; - - if (!this.graph) { - return; - } - - LGraphCanvas.active_canvas = this; - this.adjustMouseEvent(e); - var mouse = [e.clientX, e.clientY]; - this.mouse[0] = mouse[0]; - this.mouse[1] = mouse[1]; - var delta = [ - mouse[0] - this.last_mouse[0], - mouse[1] - this.last_mouse[1] - ]; - this.last_mouse = mouse; - this.graph_mouse[0] = e.canvasX; - this.graph_mouse[1] = e.canvasY; - - //console.log("pointerevents: processMouseMove "+e.pointerId+" "+e.isPrimary); - - if(this.block_click) - { - //console.log("pointerevents: processMouseMove block_click"); - e.preventDefault(); - return false; - } - - e.dragging = this.last_mouse_dragging; - - if (this.node_widget) { - this.processNodeWidgets( - this.node_widget[0], - this.graph_mouse, - e, - this.node_widget[1] - ); - this.dirty_canvas = true; - } - - //get node over - var node = this.graph.getNodeOnPos(e.canvasX,e.canvasY,this.visible_nodes); - - if (this.dragging_rectangle) - { - this.dragging_rectangle[2] = e.canvasX - this.dragging_rectangle[0]; - this.dragging_rectangle[3] = e.canvasY - this.dragging_rectangle[1]; - this.dirty_canvas = true; - } - else if (this.selected_group && !this.read_only) - { - //moving/resizing a group - if (this.selected_group_resizing) { - this.selected_group.size = [ - e.canvasX - this.selected_group.pos[0], - e.canvasY - this.selected_group.pos[1] - ]; - } else { - var deltax = delta[0] / this.ds.scale; - var deltay = delta[1] / this.ds.scale; - this.selected_group.move(deltax, deltay, e.ctrlKey); - if (this.selected_group._nodes.length) { - this.dirty_canvas = true; - } - } - this.dirty_bgcanvas = true; - } else if (this.dragging_canvas) { - ////console.log("pointerevents: processMouseMove is dragging_canvas"); - this.ds.offset[0] += delta[0] / this.ds.scale; - this.ds.offset[1] += delta[1] / this.ds.scale; - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - } else if ((this.allow_interaction || (node && node.flags.allow_interaction)) && !this.read_only) { - if (this.connecting_node) { - this.dirty_canvas = true; - } - - //remove mouseover flag - for (var i = 0, l = this.graph._nodes.length; i < l; ++i) { - if (this.graph._nodes[i].mouseOver && node != this.graph._nodes[i] ) { - //mouse leave - this.graph._nodes[i].mouseOver = false; - if (this.node_over && this.node_over.onMouseLeave) { - this.node_over.onMouseLeave(e); - } - this.node_over = null; - this.dirty_canvas = true; - } - } - - //mouse over a node - if (node) { - - if(node.redraw_on_mouse) - this.dirty_canvas = true; - - //this.canvas.style.cursor = "move"; - if (!node.mouseOver) { - //mouse enter - node.mouseOver = true; - this.node_over = node; - this.dirty_canvas = true; - - if (node.onMouseEnter) { - node.onMouseEnter(e); - } - } - - //in case the node wants to do something - if (node.onMouseMove) { - node.onMouseMove( e, [e.canvasX - node.pos[0], e.canvasY - node.pos[1]], this ); - } - - //if dragging a link - if (this.connecting_node) { - - if (this.connecting_output){ - - var pos = this._highlight_input || [0, 0]; //to store the output of isOverNodeInput - - //on top of input - if (this.isOverNodeBox(node, e.canvasX, e.canvasY)) { - //mouse on top of the corner box, don't know what to do - } else { - //check if I have a slot below de mouse - var slot = this.isOverNodeInput( node, e.canvasX, e.canvasY, pos ); - if (slot != -1 && node.inputs[slot]) { - var slot_type = node.inputs[slot].type; - if ( LiteGraph.isValidConnection( this.connecting_output.type, slot_type ) ) { - this._highlight_input = pos; - this._highlight_input_slot = node.inputs[slot]; // XXX CHECK THIS - } - } else { - this._highlight_input = null; - this._highlight_input_slot = null; // XXX CHECK THIS - } - } - - }else if(this.connecting_input){ - - var pos = this._highlight_output || [0, 0]; //to store the output of isOverNodeOutput - - //on top of output - if (this.isOverNodeBox(node, e.canvasX, e.canvasY)) { - //mouse on top of the corner box, don't know what to do - } else { - //check if I have a slot below de mouse - var slot = this.isOverNodeOutput( node, e.canvasX, e.canvasY, pos ); - if (slot != -1 && node.outputs[slot]) { - var slot_type = node.outputs[slot].type; - if ( LiteGraph.isValidConnection( this.connecting_input.type, slot_type ) ) { - this._highlight_output = pos; - } - } else { - this._highlight_output = null; - } - } - } - } - - //Search for corner - if (this.canvas) { - if (node.inResizeCorner(e.canvasX, e.canvasY)) { - this.canvas.style.cursor = "se-resize"; - } else { - this.canvas.style.cursor = "crosshair"; - } - } - } else { //not over a node - - //search for link connector - var over_link = null; - for (var i = 0; i < this.visible_links.length; ++i) { - var link = this.visible_links[i]; - var center = link._pos; - if ( - !center || - e.canvasX < center[0] - 4 || - e.canvasX > center[0] + 4 || - e.canvasY < center[1] - 4 || - e.canvasY > center[1] + 4 - ) { - continue; - } - over_link = link; - break; - } - if( over_link != this.over_link_center ) - { - this.over_link_center = over_link; - this.dirty_canvas = true; - } - - if (this.canvas) { - this.canvas.style.cursor = ""; - } - } //end - - //send event to node if capturing input (used with widgets that allow drag outside of the area of the node) - if ( this.node_capturing_input && this.node_capturing_input != node && this.node_capturing_input.onMouseMove ) { - this.node_capturing_input.onMouseMove(e,[e.canvasX - this.node_capturing_input.pos[0],e.canvasY - this.node_capturing_input.pos[1]], this); - } - - //node being dragged - if (this.node_dragged && !this.live_mode) { - //console.log("draggin!",this.selected_nodes); - for (var i in this.selected_nodes) { - var n = this.selected_nodes[i]; - n.pos[0] += delta[0] / this.ds.scale; - n.pos[1] += delta[1] / this.ds.scale; - if (!n.is_selected) this.processNodeSelected(n, e); /* - * Don't call the function if the block is already selected. - * Otherwise, it could cause the block to be unselected while dragging. - */ - } - - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - } - - if (this.resizing_node && !this.live_mode) { - //convert mouse to node space - var desired_size = [ e.canvasX - this.resizing_node.pos[0], e.canvasY - this.resizing_node.pos[1] ]; - var min_size = this.resizing_node.computeSize(); - desired_size[0] = Math.max( min_size[0], desired_size[0] ); - desired_size[1] = Math.max( min_size[1], desired_size[1] ); - this.resizing_node.setSize( desired_size ); - - this.canvas.style.cursor = "se-resize"; - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - } - } - - e.preventDefault(); - return false; - }; - - /** - * Called when a mouse up event has to be processed - * @method processMouseUp - **/ - LGraphCanvas.prototype.processMouseUp = function(e) { - - var is_primary = ( e.isPrimary === undefined || e.isPrimary ); - - //early exit for extra pointer - if(!is_primary){ - /*e.stopPropagation(); - e.preventDefault();*/ - //console.log("pointerevents: processMouseUp pointerN_stop "+e.pointerId+" "+e.isPrimary); - return false; - } - - //console.log("pointerevents: processMouseUp "+e.pointerId+" "+e.isPrimary+" :: "+e.clientX+" "+e.clientY); - - if( this.set_canvas_dirty_on_mouse_event ) - this.dirty_canvas = true; - - if (!this.graph) - return; - - var window = this.getCanvasWindow(); - var document = window.document; - LGraphCanvas.active_canvas = this; - - //restore the mousemove event back to the canvas - if(!this.options.skip_events) - { - //console.log("pointerevents: processMouseUp adjustEventListener"); - LiteGraph.pointerListenerRemove(document,"move", this._mousemove_callback,true); - LiteGraph.pointerListenerAdd(this.canvas,"move", this._mousemove_callback,true); - LiteGraph.pointerListenerRemove(document,"up", this._mouseup_callback,true); - } - - this.adjustMouseEvent(e); - var now = LiteGraph.getTime(); - e.click_time = now - this.last_mouseclick; - this.last_mouse_dragging = false; - this.last_click_position = null; - - if(this.block_click) - { - //console.log("pointerevents: processMouseUp block_clicks"); - this.block_click = false; //used to avoid sending twice a click in a immediate button - } - - //console.log("pointerevents: processMouseUp which: "+e.which); - - if (e.which == 1) { - - if( this.node_widget ) - { - this.processNodeWidgets( this.node_widget[0], this.graph_mouse, e ); - } - - //left button - this.node_widget = null; - - if (this.selected_group) { - var diffx = - this.selected_group.pos[0] - - Math.round(this.selected_group.pos[0]); - var diffy = - this.selected_group.pos[1] - - Math.round(this.selected_group.pos[1]); - this.selected_group.move(diffx, diffy, e.ctrlKey); - this.selected_group.pos[0] = Math.round( - this.selected_group.pos[0] - ); - this.selected_group.pos[1] = Math.round( - this.selected_group.pos[1] - ); - if (this.selected_group._nodes.length) { - this.dirty_canvas = true; - } - this.selected_group = null; - } - this.selected_group_resizing = false; - - var node = this.graph.getNodeOnPos( - e.canvasX, - e.canvasY, - this.visible_nodes - ); - - if (this.dragging_rectangle) { - if (this.graph) { - var nodes = this.graph._nodes; - var node_bounding = new Float32Array(4); - - //compute bounding and flip if left to right - var w = Math.abs(this.dragging_rectangle[2]); - var h = Math.abs(this.dragging_rectangle[3]); - var startx = - this.dragging_rectangle[2] < 0 - ? this.dragging_rectangle[0] - w - : this.dragging_rectangle[0]; - var starty = - this.dragging_rectangle[3] < 0 - ? this.dragging_rectangle[1] - h - : this.dragging_rectangle[1]; - this.dragging_rectangle[0] = startx; - this.dragging_rectangle[1] = starty; - this.dragging_rectangle[2] = w; - this.dragging_rectangle[3] = h; - - // test dragging rect size, if minimun simulate a click - if (!node || (w > 10 && h > 10 )){ - //test against all nodes (not visible because the rectangle maybe start outside - var to_select = []; - for (var i = 0; i < nodes.length; ++i) { - var nodeX = nodes[i]; - nodeX.getBounding(node_bounding); - if ( - !overlapBounding( - this.dragging_rectangle, - node_bounding - ) - ) { - continue; - } //out of the visible area - to_select.push(nodeX); - } - if (to_select.length) { - this.selectNodes(to_select,e.shiftKey); // add to selection with shift - } - }else{ - // will select of update selection - this.selectNodes([node],e.shiftKey||e.ctrlKey); // add to selection add to selection with ctrlKey or shiftKey - } - - } - this.dragging_rectangle = null; - } else if (this.connecting_node) { - //dragging a connection - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - - var connInOrOut = this.connecting_output || this.connecting_input; - var connType = connInOrOut.type; - - //node below mouse - if (node) { - - /* no need to condition on event type.. just another type - if ( - connType == LiteGraph.EVENT && - this.isOverNodeBox(node, e.canvasX, e.canvasY) - ) { - - this.connecting_node.connect( - this.connecting_slot, - node, - LiteGraph.EVENT - ); - - } else {*/ - - //slot below mouse? connect - - if (this.connecting_output){ - - var slot = this.isOverNodeInput( - node, - e.canvasX, - e.canvasY - ); - if (slot != -1) { - this.connecting_node.connect(this.connecting_slot, node, slot); - } else { - //not on top of an input - // look for a good slot - this.connecting_node.connectByType(this.connecting_slot,node,connType); - } - - }else if (this.connecting_input){ - - var slot = this.isOverNodeOutput( - node, - e.canvasX, - e.canvasY - ); - - if (slot != -1) { - node.connect(slot, this.connecting_node, this.connecting_slot); // this is inverted has output-input nature like - } else { - //not on top of an input - // look for a good slot - this.connecting_node.connectByTypeOutput(this.connecting_slot,node,connType); - } - - } - - - //} - - }else{ - - // add menu when releasing link in empty space - if (LiteGraph.release_link_on_empty_shows_menu){ - if (e.shiftKey && this.allow_searchbox){ - if(this.connecting_output){ - this.showSearchBox(e,{node_from: this.connecting_node, slot_from: this.connecting_output, type_filter_in: this.connecting_output.type}); - }else if(this.connecting_input){ - this.showSearchBox(e,{node_to: this.connecting_node, slot_from: this.connecting_input, type_filter_out: this.connecting_input.type}); - } - }else{ - if(this.connecting_output){ - this.showConnectionMenu({nodeFrom: this.connecting_node, slotFrom: this.connecting_output, e: e}); - }else if(this.connecting_input){ - this.showConnectionMenu({nodeTo: this.connecting_node, slotTo: this.connecting_input, e: e}); - } - } - } - } - - this.connecting_output = null; - this.connecting_input = null; - this.connecting_pos = null; - this.connecting_node = null; - this.connecting_slot = -1; - } //not dragging connection - else if (this.resizing_node) { - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - this.graph.afterChange(this.resizing_node); - this.resizing_node = null; - } else if (this.node_dragged) { - //node being dragged? - var node = this.node_dragged; - if ( - node && - e.click_time < 300 && - isInsideRectangle( e.canvasX, e.canvasY, node.pos[0], node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT, LiteGraph.NODE_TITLE_HEIGHT, LiteGraph.NODE_TITLE_HEIGHT ) - ) { - node.collapse(); - } - - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - this.node_dragged.pos[0] = Math.round(this.node_dragged.pos[0]); - this.node_dragged.pos[1] = Math.round(this.node_dragged.pos[1]); - if (this.graph.config.align_to_grid || this.align_to_grid ) { - this.node_dragged.alignToGrid(); - } - if( this.onNodeMoved ) - this.onNodeMoved( this.node_dragged ); - this.graph.afterChange(this.node_dragged); - this.node_dragged = null; - } //no node being dragged - else { - //get node over - var node = this.graph.getNodeOnPos( - e.canvasX, - e.canvasY, - this.visible_nodes - ); - - if (!node && e.click_time < 300) { - this.deselectAllNodes(); - } - - this.dirty_canvas = true; - this.dragging_canvas = false; - - if (this.node_over && this.node_over.onMouseUp) { - this.node_over.onMouseUp( e, [ e.canvasX - this.node_over.pos[0], e.canvasY - this.node_over.pos[1] ], this ); - } - if ( - this.node_capturing_input && - this.node_capturing_input.onMouseUp - ) { - this.node_capturing_input.onMouseUp(e, [ - e.canvasX - this.node_capturing_input.pos[0], - e.canvasY - this.node_capturing_input.pos[1] - ]); - } - } - } else if (e.which == 2) { - //middle button - //trace("middle"); - this.dirty_canvas = true; - this.dragging_canvas = false; - } else if (e.which == 3) { - //right button - //trace("right"); - this.dirty_canvas = true; - this.dragging_canvas = false; - } - - /* - if((this.dirty_canvas || this.dirty_bgcanvas) && this.rendering_timer_id == null) - this.draw(); - */ - - if (is_primary) - { - this.pointer_is_down = false; - this.pointer_is_double = false; - } - - this.graph.change(); - - //console.log("pointerevents: processMouseUp stopPropagation"); - e.stopPropagation(); - e.preventDefault(); - return false; - }; - - /** - * Called when a mouse wheel event has to be processed - * @method processMouseWheel - **/ - LGraphCanvas.prototype.processMouseWheel = function(e) { - if (!this.graph || !this.allow_dragcanvas) { - return; - } - - var delta = e.wheelDeltaY != null ? e.wheelDeltaY : e.detail * -60; - - this.adjustMouseEvent(e); - - var x = e.clientX; - var y = e.clientY; - var is_inside = !this.viewport || ( this.viewport && x >= this.viewport[0] && x < (this.viewport[0] + this.viewport[2]) && y >= this.viewport[1] && y < (this.viewport[1] + this.viewport[3]) ); - if(!is_inside) - return; - - var scale = this.ds.scale; - - if (delta > 0) { - scale *= 1.1; - } else if (delta < 0) { - scale *= 1 / 1.1; - } - - //this.setZoom( scale, [ e.clientX, e.clientY ] ); - this.ds.changeScale(scale, [e.clientX, e.clientY]); - - this.graph.change(); - - e.preventDefault(); - return false; // prevent default - }; - - /** - * returns true if a position (in graph space) is on top of a node little corner box - * @method isOverNodeBox - **/ - LGraphCanvas.prototype.isOverNodeBox = function(node, canvasx, canvasy) { - var title_height = LiteGraph.NODE_TITLE_HEIGHT; - if ( - isInsideRectangle( - canvasx, - canvasy, - node.pos[0] + 2, - node.pos[1] + 2 - title_height, - title_height - 4, - title_height - 4 - ) - ) { - return true; - } - return false; - }; - - /** - * returns the INDEX if a position (in graph space) is on top of a node input slot - * @method isOverNodeInput - **/ - LGraphCanvas.prototype.isOverNodeInput = function( - node, - canvasx, - canvasy, - slot_pos - ) { - if (node.inputs) { - for (var i = 0, l = node.inputs.length; i < l; ++i) { - var input = node.inputs[i]; - var link_pos = node.getConnectionPos(true, i); - var is_inside = false; - if (node.horizontal) { - is_inside = isInsideRectangle( - canvasx, - canvasy, - link_pos[0] - 5, - link_pos[1] - 10, - 10, - 20 - ); - } else { - is_inside = isInsideRectangle( - canvasx, - canvasy, - link_pos[0] - 10, - link_pos[1] - 5, - 40, - 10 - ); - } - if (is_inside) { - if (slot_pos) { - slot_pos[0] = link_pos[0]; - slot_pos[1] = link_pos[1]; - } - return i; - } - } - } - return -1; - }; - - /** - * returns the INDEX if a position (in graph space) is on top of a node output slot - * @method isOverNodeOuput - **/ - LGraphCanvas.prototype.isOverNodeOutput = function( - node, - canvasx, - canvasy, - slot_pos - ) { - if (node.outputs) { - for (var i = 0, l = node.outputs.length; i < l; ++i) { - var output = node.outputs[i]; - var link_pos = node.getConnectionPos(false, i); - var is_inside = false; - if (node.horizontal) { - is_inside = isInsideRectangle( - canvasx, - canvasy, - link_pos[0] - 5, - link_pos[1] - 10, - 10, - 20 - ); - } else { - is_inside = isInsideRectangle( - canvasx, - canvasy, - link_pos[0] - 10, - link_pos[1] - 5, - 40, - 10 - ); - } - if (is_inside) { - if (slot_pos) { - slot_pos[0] = link_pos[0]; - slot_pos[1] = link_pos[1]; - } - return i; - } - } - } - return -1; - }; - - /** - * process a key event - * @method processKey - **/ - LGraphCanvas.prototype.processKey = function(e) { - if (!this.graph) { - return; - } - - var block_default = false; - //console.log(e); //debug - - if (e.target.localName == "input") { - return; - } - - if (e.type == "keydown") { - if (e.keyCode == 32) { - //space - this.dragging_canvas = true; - block_default = true; - } - - if (e.keyCode == 27) { - //esc - if(this.node_panel) this.node_panel.close(); - if(this.options_panel) this.options_panel.close(); - block_default = true; - } - - //select all Control A - if (e.keyCode == 65 && e.ctrlKey) { - this.selectNodes(); - block_default = true; - } - - if ((e.keyCode === 67) && (e.metaKey || e.ctrlKey) && !e.shiftKey) { - //copy - if (this.selected_nodes) { - this.copyToClipboard(); - block_default = true; - } - } - - if ((e.keyCode === 86) && (e.metaKey || e.ctrlKey)) { - //paste - this.pasteFromClipboard(e.shiftKey); - } - - //delete or backspace - if (e.keyCode == 46 || e.keyCode == 8) { - if ( - e.target.localName != "input" && - e.target.localName != "textarea" - ) { - this.deleteSelectedNodes(); - block_default = true; - } - } - - //collapse - //... - - //TODO - if (this.selected_nodes) { - for (var i in this.selected_nodes) { - if (this.selected_nodes[i].onKeyDown) { - this.selected_nodes[i].onKeyDown(e); - } - } - } - } else if (e.type == "keyup") { - if (e.keyCode == 32) { - // space - this.dragging_canvas = false; - } - - if (this.selected_nodes) { - for (var i in this.selected_nodes) { - if (this.selected_nodes[i].onKeyUp) { - this.selected_nodes[i].onKeyUp(e); - } - } - } - } - - this.graph.change(); - - if (block_default) { - e.preventDefault(); - e.stopImmediatePropagation(); - return false; - } - }; - - LGraphCanvas.prototype.copyToClipboard = function(nodes) { - var clipboard_info = { - nodes: [], - links: [] - }; - var index = 0; - var selected_nodes_array = []; - if (!nodes) nodes = this.selected_nodes; - for (var i in nodes) { - var node = nodes[i]; - if (node.clonable === false) - continue; - node._relative_id = index; - selected_nodes_array.push(node); - index += 1; - } - - for (var i = 0; i < selected_nodes_array.length; ++i) { - var node = selected_nodes_array[i]; - var cloned = node.clone(); - if(!cloned) - { - console.warn("node type not found: " + node.type ); - continue; - } - clipboard_info.nodes.push(cloned.serialize()); - if (node.inputs && node.inputs.length) { - for (var j = 0; j < node.inputs.length; ++j) { - var input = node.inputs[j]; - if (!input || input.link == null) { - continue; - } - var link_info = this.graph.links[input.link]; - if (!link_info) { - continue; - } - var target_node = this.graph.getNodeById( - link_info.origin_id - ); - if (!target_node) { - continue; - } - clipboard_info.links.push([ - target_node._relative_id, - link_info.origin_slot, //j, - node._relative_id, - link_info.target_slot, - target_node.id - ]); - } - } - } - localStorage.setItem( - "litegrapheditor_clipboard", - JSON.stringify(clipboard_info) - ); - }; - - LGraphCanvas.prototype.pasteFromClipboard = function(isConnectUnselected = false) { - // if ctrl + shift + v is off, return when isConnectUnselected is true (shift is pressed) to maintain old behavior - if (!LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs && isConnectUnselected) { - return; - } - var data = localStorage.getItem("litegrapheditor_clipboard"); - if (!data) { - return; - } - - this.graph.beforeChange(); - - //create nodes - var clipboard_info = JSON.parse(data); - // calculate top-left node, could work without this processing but using diff with last node pos :: clipboard_info.nodes[clipboard_info.nodes.length-1].pos - var posMin = false; - var posMinIndexes = false; - for (var i = 0; i < clipboard_info.nodes.length; ++i) { - if (posMin){ - if(posMin[0]>clipboard_info.nodes[i].pos[0]){ - posMin[0] = clipboard_info.nodes[i].pos[0]; - posMinIndexes[0] = i; - } - if(posMin[1]>clipboard_info.nodes[i].pos[1]){ - posMin[1] = clipboard_info.nodes[i].pos[1]; - posMinIndexes[1] = i; - } - } - else{ - posMin = [clipboard_info.nodes[i].pos[0], clipboard_info.nodes[i].pos[1]]; - posMinIndexes = [i, i]; - } - } - var nodes = []; - for (var i = 0; i < clipboard_info.nodes.length; ++i) { - var node_data = clipboard_info.nodes[i]; - var node = LiteGraph.createNode(node_data.type); - if (node) { - node.configure(node_data); - - //paste in last known mouse position - node.pos[0] += this.graph_mouse[0] - posMin[0]; //+= 5; - node.pos[1] += this.graph_mouse[1] - posMin[1]; //+= 5; - - this.graph.add(node,{doProcessChange:false}); - - nodes.push(node); - } - } - - //create links - for (var i = 0; i < clipboard_info.links.length; ++i) { - var link_info = clipboard_info.links[i]; - var origin_node = undefined; - var origin_node_relative_id = link_info[0]; - if (origin_node_relative_id != null) { - origin_node = nodes[origin_node_relative_id]; - } else if (LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs && isConnectUnselected) { - var origin_node_id = link_info[4]; - if (origin_node_id) { - origin_node = this.graph.getNodeById(origin_node_id); - } - } - var target_node = nodes[link_info[2]]; - if( origin_node && target_node ) - origin_node.connect(link_info[1], target_node, link_info[3]); - else - console.warn("Warning, nodes missing on pasting"); - } - - this.selectNodes(nodes); - - this.graph.afterChange(); - }; - - /** - * process a item drop event on top the canvas - * @method processDrop - **/ - LGraphCanvas.prototype.processDrop = function(e) { - e.preventDefault(); - this.adjustMouseEvent(e); - var x = e.clientX; - var y = e.clientY; - var is_inside = !this.viewport || ( this.viewport && x >= this.viewport[0] && x < (this.viewport[0] + this.viewport[2]) && y >= this.viewport[1] && y < (this.viewport[1] + this.viewport[3]) ); - if(!is_inside){ - return; - // --- BREAK --- - } - - var pos = [e.canvasX, e.canvasY]; - - - var node = this.graph ? this.graph.getNodeOnPos(pos[0], pos[1]) : null; - - if (!node) { - var r = null; - if (this.onDropItem) { - r = this.onDropItem(event); - } - if (!r) { - this.checkDropItem(e); - } - return; - } - - if (node.onDropFile || node.onDropData) { - var files = e.dataTransfer.files; - if (files && files.length) { - for (var i = 0; i < files.length; i++) { - var file = e.dataTransfer.files[0]; - var filename = file.name; - var ext = LGraphCanvas.getFileExtension(filename); - //console.log(file); - - if (node.onDropFile) { - node.onDropFile(file); - } - - if (node.onDropData) { - //prepare reader - var reader = new FileReader(); - reader.onload = function(event) { - //console.log(event.target); - var data = event.target.result; - node.onDropData(data, filename, file); - }; - - //read data - var type = file.type.split("/")[0]; - if (type == "text" || type == "") { - reader.readAsText(file); - } else if (type == "image") { - reader.readAsDataURL(file); - } else { - reader.readAsArrayBuffer(file); - } - } - } - } - } - - if (node.onDropItem) { - if (node.onDropItem(event)) { - return true; - } - } - - if (this.onDropItem) { - return this.onDropItem(event); - } - - return false; - }; - - //called if the graph doesn't have a default drop item behaviour - LGraphCanvas.prototype.checkDropItem = function(e) { - if (e.dataTransfer.files.length) { - var file = e.dataTransfer.files[0]; - var ext = LGraphCanvas.getFileExtension(file.name).toLowerCase(); - var nodetype = LiteGraph.node_types_by_file_extension[ext]; - if (nodetype) { - this.graph.beforeChange(); - var node = LiteGraph.createNode(nodetype.type); - node.pos = [e.canvasX, e.canvasY]; - this.graph.add(node); - if (node.onDropFile) { - node.onDropFile(file); - } - this.graph.afterChange(); - } - } - }; - - LGraphCanvas.prototype.processNodeDblClicked = function(n) { - if (this.onShowNodePanel) { - this.onShowNodePanel(n); - } - - if (this.onNodeDblClicked) { - this.onNodeDblClicked(n); - } - - this.setDirty(true); - }; - - LGraphCanvas.prototype.processNodeSelected = function(node, e) { - this.selectNode(node, e && (e.shiftKey || e.ctrlKey || this.multi_select)); - if (this.onNodeSelected) { - this.onNodeSelected(node); - } - }; - - /** - * selects a given node (or adds it to the current selection) - * @method selectNode - **/ - LGraphCanvas.prototype.selectNode = function( - node, - add_to_current_selection - ) { - if (node == null) { - this.deselectAllNodes(); - } else { - this.selectNodes([node], add_to_current_selection); - } - }; - - /** - * selects several nodes (or adds them to the current selection) - * @method selectNodes - **/ - LGraphCanvas.prototype.selectNodes = function( nodes, add_to_current_selection ) - { - if (!add_to_current_selection) { - this.deselectAllNodes(); - } - - nodes = nodes || this.graph._nodes; - if (typeof nodes == "string") nodes = [nodes]; - for (var i in nodes) { - var node = nodes[i]; - if (node.is_selected) { - this.deselectNode(node); - continue; - } - - if (!node.is_selected && node.onSelected) { - node.onSelected(); - } - node.is_selected = true; - this.selected_nodes[node.id] = node; - - if (node.inputs) { - for (var j = 0; j < node.inputs.length; ++j) { - this.highlighted_links[node.inputs[j].link] = true; - } - } - if (node.outputs) { - for (var j = 0; j < node.outputs.length; ++j) { - var out = node.outputs[j]; - if (out.links) { - for (var k = 0; k < out.links.length; ++k) { - this.highlighted_links[out.links[k]] = true; - } - } - } - } - } - - if( this.onSelectionChange ) - this.onSelectionChange( this.selected_nodes ); - - this.setDirty(true); - }; - - /** - * removes a node from the current selection - * @method deselectNode - **/ - LGraphCanvas.prototype.deselectNode = function(node) { - if (!node.is_selected) { - return; - } - if (node.onDeselected) { - node.onDeselected(); - } - node.is_selected = false; - - if (this.onNodeDeselected) { - this.onNodeDeselected(node); - } - - //remove highlighted - if (node.inputs) { - for (var i = 0; i < node.inputs.length; ++i) { - delete this.highlighted_links[node.inputs[i].link]; - } - } - if (node.outputs) { - for (var i = 0; i < node.outputs.length; ++i) { - var out = node.outputs[i]; - if (out.links) { - for (var j = 0; j < out.links.length; ++j) { - delete this.highlighted_links[out.links[j]]; - } - } - } - } - }; - - /** - * removes all nodes from the current selection - * @method deselectAllNodes - **/ - LGraphCanvas.prototype.deselectAllNodes = function() { - if (!this.graph) { - return; - } - var nodes = this.graph._nodes; - for (var i = 0, l = nodes.length; i < l; ++i) { - var node = nodes[i]; - if (!node.is_selected) { - continue; - } - if (node.onDeselected) { - node.onDeselected(); - } - node.is_selected = false; - if (this.onNodeDeselected) { - this.onNodeDeselected(node); - } - } - this.selected_nodes = {}; - this.current_node = null; - this.highlighted_links = {}; - if( this.onSelectionChange ) - this.onSelectionChange( this.selected_nodes ); - this.setDirty(true); - }; - - /** - * deletes all nodes in the current selection from the graph - * @method deleteSelectedNodes - **/ - LGraphCanvas.prototype.deleteSelectedNodes = function() { - - this.graph.beforeChange(); - - for (var i in this.selected_nodes) { - var node = this.selected_nodes[i]; - - if(node.block_delete) - continue; - - //autoconnect when possible (very basic, only takes into account first input-output) - if(node.inputs && node.inputs.length && node.outputs && node.outputs.length && LiteGraph.isValidConnection( node.inputs[0].type, node.outputs[0].type ) && node.inputs[0].link && node.outputs[0].links && node.outputs[0].links.length ) - { - var input_link = node.graph.links[ node.inputs[0].link ]; - var output_link = node.graph.links[ node.outputs[0].links[0] ]; - var input_node = node.getInputNode(0); - var output_node = node.getOutputNodes(0)[0]; - if(input_node && output_node) - input_node.connect( input_link.origin_slot, output_node, output_link.target_slot ); - } - this.graph.remove(node); - if (this.onNodeDeselected) { - this.onNodeDeselected(node); - } - } - this.selected_nodes = {}; - this.current_node = null; - this.highlighted_links = {}; - this.setDirty(true); - this.graph.afterChange(); - }; - - /** - * centers the camera on a given node - * @method centerOnNode - **/ - LGraphCanvas.prototype.centerOnNode = function(node) { - this.ds.offset[0] = - -node.pos[0] - - node.size[0] * 0.5 + - (this.canvas.width * 0.5) / this.ds.scale; - this.ds.offset[1] = - -node.pos[1] - - node.size[1] * 0.5 + - (this.canvas.height * 0.5) / this.ds.scale; - this.setDirty(true, true); - }; - - /** - * adds some useful properties to a mouse event, like the position in graph coordinates - * @method adjustMouseEvent - **/ - LGraphCanvas.prototype.adjustMouseEvent = function(e) { - var clientX_rel = 0; - var clientY_rel = 0; - - if (this.canvas) { - var b = this.canvas.getBoundingClientRect(); - clientX_rel = e.clientX - b.left; - clientY_rel = e.clientY - b.top; - } else { - clientX_rel = e.clientX; - clientY_rel = e.clientY; - } - - e.deltaX = clientX_rel - this.last_mouse_position[0]; - e.deltaY = clientY_rel- this.last_mouse_position[1]; - - this.last_mouse_position[0] = clientX_rel; - this.last_mouse_position[1] = clientY_rel; - - e.canvasX = clientX_rel / this.ds.scale - this.ds.offset[0]; - e.canvasY = clientY_rel / this.ds.scale - this.ds.offset[1]; - - //console.log("pointerevents: adjustMouseEvent "+e.clientX+":"+e.clientY+" "+clientX_rel+":"+clientY_rel+" "+e.canvasX+":"+e.canvasY); - }; - - /** - * changes the zoom level of the graph (default is 1), you can pass also a place used to pivot the zoom - * @method setZoom - **/ - LGraphCanvas.prototype.setZoom = function(value, zooming_center) { - this.ds.changeScale(value, zooming_center); - /* - if(!zooming_center && this.canvas) - zooming_center = [this.canvas.width * 0.5,this.canvas.height * 0.5]; - - var center = this.convertOffsetToCanvas( zooming_center ); - - this.ds.scale = value; - - if(this.scale > this.max_zoom) - this.scale = this.max_zoom; - else if(this.scale < this.min_zoom) - this.scale = this.min_zoom; - - var new_center = this.convertOffsetToCanvas( zooming_center ); - var delta_offset = [new_center[0] - center[0], new_center[1] - center[1]]; - - this.offset[0] += delta_offset[0]; - this.offset[1] += delta_offset[1]; - */ - - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - }; - - /** - * converts a coordinate from graph coordinates to canvas2D coordinates - * @method convertOffsetToCanvas - **/ - LGraphCanvas.prototype.convertOffsetToCanvas = function(pos, out) { - return this.ds.convertOffsetToCanvas(pos, out); - }; - - /** - * converts a coordinate from Canvas2D coordinates to graph space - * @method convertCanvasToOffset - **/ - LGraphCanvas.prototype.convertCanvasToOffset = function(pos, out) { - return this.ds.convertCanvasToOffset(pos, out); - }; - - //converts event coordinates from canvas2D to graph coordinates - LGraphCanvas.prototype.convertEventToCanvasOffset = function(e) { - var rect = this.canvas.getBoundingClientRect(); - return this.convertCanvasToOffset([ - e.clientX - rect.left, - e.clientY - rect.top - ]); - }; - - /** - * brings a node to front (above all other nodes) - * @method bringToFront - **/ - LGraphCanvas.prototype.bringToFront = function(node) { - var i = this.graph._nodes.indexOf(node); - if (i == -1) { - return; - } - - this.graph._nodes.splice(i, 1); - this.graph._nodes.push(node); - }; - - /** - * sends a node to the back (below all other nodes) - * @method sendToBack - **/ - LGraphCanvas.prototype.sendToBack = function(node) { - var i = this.graph._nodes.indexOf(node); - if (i == -1) { - return; - } - - this.graph._nodes.splice(i, 1); - this.graph._nodes.unshift(node); - }; - - /* Interaction */ - - /* LGraphCanvas render */ - var temp = new Float32Array(4); - - /** - * checks which nodes are visible (inside the camera area) - * @method computeVisibleNodes - **/ - LGraphCanvas.prototype.computeVisibleNodes = function(nodes, out) { - var visible_nodes = out || []; - visible_nodes.length = 0; - nodes = nodes || this.graph._nodes; - for (var i = 0, l = nodes.length; i < l; ++i) { - var n = nodes[i]; - - //skip rendering nodes in live mode - if (this.live_mode && !n.onDrawBackground && !n.onDrawForeground) { - continue; - } - - if (!overlapBounding(this.visible_area, n.getBounding(temp, true))) { - continue; - } //out of the visible area - - visible_nodes.push(n); - } - return visible_nodes; - }; - - /** - * renders the whole canvas content, by rendering in two separated canvas, one containing the background grid and the connections, and one containing the nodes) - * @method draw - **/ - LGraphCanvas.prototype.draw = function(force_canvas, force_bgcanvas) { - if (!this.canvas || this.canvas.width == 0 || this.canvas.height == 0) { - return; - } - - //fps counting - var now = LiteGraph.getTime(); - this.render_time = (now - this.last_draw_time) * 0.001; - this.last_draw_time = now; - - if (this.graph) { - this.ds.computeVisibleArea(this.viewport); - } - - if ( - this.dirty_bgcanvas || - force_bgcanvas || - this.always_render_background || - (this.graph && - this.graph._last_trigger_time && - now - this.graph._last_trigger_time < 1000) - ) { - this.drawBackCanvas(); - } - - if (this.dirty_canvas || force_canvas) { - this.drawFrontCanvas(); - } - - this.fps = this.render_time ? 1.0 / this.render_time : 0; - this.frame += 1; - }; - - /** - * draws the front canvas (the one containing all the nodes) - * @method drawFrontCanvas - **/ - LGraphCanvas.prototype.drawFrontCanvas = function() { - this.dirty_canvas = false; - - if (!this.ctx) { - this.ctx = this.bgcanvas.getContext("2d"); - } - var ctx = this.ctx; - if (!ctx) { - //maybe is using webgl... - return; - } - - var canvas = this.canvas; - if ( ctx.start2D && !this.viewport ) { - ctx.start2D(); - ctx.restore(); - ctx.setTransform(1, 0, 0, 1, 0, 0); - } - - //clip dirty area if there is one, otherwise work in full canvas - var area = this.viewport || this.dirty_area; - if (area) { - ctx.save(); - ctx.beginPath(); - ctx.rect( area[0],area[1],area[2],area[3] ); - ctx.clip(); - } - - //clear - //canvas.width = canvas.width; - if (this.clear_background) { - if(area) - ctx.clearRect( area[0],area[1],area[2],area[3] ); - else - ctx.clearRect(0, 0, canvas.width, canvas.height); - } - - //draw bg canvas - if (this.bgcanvas == this.canvas) { - this.drawBackCanvas(); - } else { - ctx.drawImage( this.bgcanvas, 0, 0 ); - } - - //rendering - if (this.onRender) { - this.onRender(canvas, ctx); - } - - //info widget - if (this.show_info) { - this.renderInfo(ctx, area ? area[0] : 0, area ? area[1] : 0 ); - } - - if (this.graph) { - //apply transformations - ctx.save(); - this.ds.toCanvasContext(ctx); - - //draw nodes - var drawn_nodes = 0; - var visible_nodes = this.computeVisibleNodes( - null, - this.visible_nodes - ); - - for (var i = 0; i < visible_nodes.length; ++i) { - var node = visible_nodes[i]; - - //transform coords system - ctx.save(); - ctx.translate(node.pos[0], node.pos[1]); - - //Draw - this.drawNode(node, ctx); - drawn_nodes += 1; - - //Restore - ctx.restore(); - } - - //on top (debug) - if (this.render_execution_order) { - this.drawExecutionOrder(ctx); - } - - //connections ontop? - if (this.graph.config.links_ontop) { - if (!this.live_mode) { - this.drawConnections(ctx); - } - } - - //current connection (the one being dragged by the mouse) - if (this.connecting_pos != null) { - ctx.lineWidth = this.connections_width; - var link_color = null; - - var connInOrOut = this.connecting_output || this.connecting_input; - - var connType = connInOrOut.type; - var connDir = connInOrOut.dir; - if(connDir == null) - { - if (this.connecting_output) - connDir = this.connecting_node.horizontal ? LiteGraph.DOWN : LiteGraph.RIGHT; - else - connDir = this.connecting_node.horizontal ? LiteGraph.UP : LiteGraph.LEFT; - } - var connShape = connInOrOut.shape; - - switch (connType) { - case LiteGraph.EVENT: - link_color = LiteGraph.EVENT_LINK_COLOR; - break; - default: - link_color = LiteGraph.CONNECTING_LINK_COLOR; - } - - //the connection being dragged by the mouse - this.renderLink( - ctx, - this.connecting_pos, - [this.graph_mouse[0], this.graph_mouse[1]], - null, - false, - null, - link_color, - connDir, - LiteGraph.CENTER - ); - - ctx.beginPath(); - if ( - connType === LiteGraph.EVENT || - connShape === LiteGraph.BOX_SHAPE - ) { - ctx.rect( - this.connecting_pos[0] - 6 + 0.5, - this.connecting_pos[1] - 5 + 0.5, - 14, - 10 - ); - ctx.fill(); - ctx.beginPath(); - ctx.rect( - this.graph_mouse[0] - 6 + 0.5, - this.graph_mouse[1] - 5 + 0.5, - 14, - 10 - ); - } else if (connShape === LiteGraph.ARROW_SHAPE) { - ctx.moveTo(this.connecting_pos[0] + 8, this.connecting_pos[1] + 0.5); - ctx.lineTo(this.connecting_pos[0] - 4, this.connecting_pos[1] + 6 + 0.5); - ctx.lineTo(this.connecting_pos[0] - 4, this.connecting_pos[1] - 6 + 0.5); - ctx.closePath(); - } - else { - ctx.arc( - this.connecting_pos[0], - this.connecting_pos[1], - 4, - 0, - Math.PI * 2 - ); - ctx.fill(); - ctx.beginPath(); - ctx.arc( - this.graph_mouse[0], - this.graph_mouse[1], - 4, - 0, - Math.PI * 2 - ); - } - ctx.fill(); - - ctx.fillStyle = "#ffcc00"; - if (this._highlight_input) { - ctx.beginPath(); - var shape = this._highlight_input_slot.shape; - if (shape === LiteGraph.ARROW_SHAPE) { - ctx.moveTo(this._highlight_input[0] + 8, this._highlight_input[1] + 0.5); - ctx.lineTo(this._highlight_input[0] - 4, this._highlight_input[1] + 6 + 0.5); - ctx.lineTo(this._highlight_input[0] - 4, this._highlight_input[1] - 6 + 0.5); - ctx.closePath(); - } else { - ctx.arc( - this._highlight_input[0], - this._highlight_input[1], - 6, - 0, - Math.PI * 2 - ); - } - ctx.fill(); - } - if (this._highlight_output) { - ctx.beginPath(); - if (shape === LiteGraph.ARROW_SHAPE) { - ctx.moveTo(this._highlight_output[0] + 8, this._highlight_output[1] + 0.5); - ctx.lineTo(this._highlight_output[0] - 4, this._highlight_output[1] + 6 + 0.5); - ctx.lineTo(this._highlight_output[0] - 4, this._highlight_output[1] - 6 + 0.5); - ctx.closePath(); - } else { - ctx.arc( - this._highlight_output[0], - this._highlight_output[1], - 6, - 0, - Math.PI * 2 - ); - } - ctx.fill(); - } - } - - //the selection rectangle - if (this.dragging_rectangle) { - ctx.strokeStyle = "#FFF"; - ctx.strokeRect( - this.dragging_rectangle[0], - this.dragging_rectangle[1], - this.dragging_rectangle[2], - this.dragging_rectangle[3] - ); - } - - //on top of link center - if(this.over_link_center && this.render_link_tooltip) - this.drawLinkTooltip( ctx, this.over_link_center ); - else - if(this.onDrawLinkTooltip) //to remove - this.onDrawLinkTooltip(ctx,null); - - //custom info - if (this.onDrawForeground) { - this.onDrawForeground(ctx, this.visible_rect); - } - - ctx.restore(); - } - - //draws panel in the corner - if (this._graph_stack && this._graph_stack.length) { - this.drawSubgraphPanel( ctx ); - } - - - if (this.onDrawOverlay) { - this.onDrawOverlay(ctx); - } - - if (area){ - ctx.restore(); - } - - if (ctx.finish2D) { - //this is a function I use in webgl renderer - ctx.finish2D(); - } - }; - - /** - * draws the panel in the corner that shows subgraph properties - * @method drawSubgraphPanel - **/ - LGraphCanvas.prototype.drawSubgraphPanel = function (ctx) { - var subgraph = this.graph; - var subnode = subgraph._subgraph_node; - if (!subnode) { - console.warn("subgraph without subnode"); - return; - } - this.drawSubgraphPanelLeft(subgraph, subnode, ctx) - this.drawSubgraphPanelRight(subgraph, subnode, ctx) - } - - LGraphCanvas.prototype.drawSubgraphPanelLeft = function (subgraph, subnode, ctx) { - var num = subnode.inputs ? subnode.inputs.length : 0; - var w = 200; - var h = Math.floor(LiteGraph.NODE_SLOT_HEIGHT * 1.6); - - ctx.fillStyle = "#111"; - ctx.globalAlpha = 0.8; - ctx.beginPath(); - ctx.roundRect(10, 10, w, (num + 1) * h + 50, [8]); - ctx.fill(); - ctx.globalAlpha = 1; - - ctx.fillStyle = "#888"; - ctx.font = "14px Arial"; - ctx.textAlign = "left"; - ctx.fillText("Graph Inputs", 20, 34); - // var pos = this.mouse; - - if (this.drawButton(w - 20, 20, 20, 20, "X", "#151515")) { - this.closeSubgraph(); - return; - } - - var y = 50; - ctx.font = "14px Arial"; - if (subnode.inputs) - for (var i = 0; i < subnode.inputs.length; ++i) { - var input = subnode.inputs[i]; - if (input.not_subgraph_input) - continue; - - //input button clicked - if (this.drawButton(20, y + 2, w - 20, h - 2)) { - var type = subnode.constructor.input_node_type || "graph/input"; - this.graph.beforeChange(); - var newnode = LiteGraph.createNode(type); - if (newnode) { - subgraph.add(newnode); - this.block_click = false; - this.last_click_position = null; - this.selectNodes([newnode]); - this.node_dragged = newnode; - this.dragging_canvas = false; - newnode.setProperty("name", input.name); - newnode.setProperty("type", input.type); - this.node_dragged.pos[0] = this.graph_mouse[0] - 5; - this.node_dragged.pos[1] = this.graph_mouse[1] - 5; - this.graph.afterChange(); - } - else - console.error("graph input node not found:", type); - } - ctx.fillStyle = "#9C9"; - ctx.beginPath(); - ctx.arc(w - 16, y + h * 0.5, 5, 0, 2 * Math.PI); - ctx.fill(); - ctx.fillStyle = "#AAA"; - ctx.fillText(input.name, 30, y + h * 0.75); - // var tw = ctx.measureText(input.name); - ctx.fillStyle = "#777"; - ctx.fillText(input.type, 130, y + h * 0.75); - y += h; - } - //add + button - if (this.drawButton(20, y + 2, w - 20, h - 2, "+", "#151515", "#222")) { - this.showSubgraphPropertiesDialog(subnode); - } - } - LGraphCanvas.prototype.drawSubgraphPanelRight = function (subgraph, subnode, ctx) { - var num = subnode.outputs ? subnode.outputs.length : 0; - var canvas_w = this.bgcanvas.width - var w = 200; - var h = Math.floor(LiteGraph.NODE_SLOT_HEIGHT * 1.6); - - ctx.fillStyle = "#111"; - ctx.globalAlpha = 0.8; - ctx.beginPath(); - ctx.roundRect(canvas_w - w - 10, 10, w, (num + 1) * h + 50, [8]); - ctx.fill(); - ctx.globalAlpha = 1; - - ctx.fillStyle = "#888"; - ctx.font = "14px Arial"; - ctx.textAlign = "left"; - var title_text = "Graph Outputs" - var tw = ctx.measureText(title_text).width - ctx.fillText(title_text, (canvas_w - tw) - 20, 34); - // var pos = this.mouse; - if (this.drawButton(canvas_w - w, 20, 20, 20, "X", "#151515")) { - this.closeSubgraph(); - return; - } - - var y = 50; - ctx.font = "14px Arial"; - if (subnode.outputs) - for (var i = 0; i < subnode.outputs.length; ++i) { - var output = subnode.outputs[i]; - if (output.not_subgraph_input) - continue; - - //output button clicked - if (this.drawButton(canvas_w - w, y + 2, w - 20, h - 2)) { - var type = subnode.constructor.output_node_type || "graph/output"; - this.graph.beforeChange(); - var newnode = LiteGraph.createNode(type); - if (newnode) { - subgraph.add(newnode); - this.block_click = false; - this.last_click_position = null; - this.selectNodes([newnode]); - this.node_dragged = newnode; - this.dragging_canvas = false; - newnode.setProperty("name", output.name); - newnode.setProperty("type", output.type); - this.node_dragged.pos[0] = this.graph_mouse[0] - 5; - this.node_dragged.pos[1] = this.graph_mouse[1] - 5; - this.graph.afterChange(); - } - else - console.error("graph input node not found:", type); - } - ctx.fillStyle = "#9C9"; - ctx.beginPath(); - ctx.arc(canvas_w - w + 16, y + h * 0.5, 5, 0, 2 * Math.PI); - ctx.fill(); - ctx.fillStyle = "#AAA"; - ctx.fillText(output.name, canvas_w - w + 30, y + h * 0.75); - // var tw = ctx.measureText(input.name); - ctx.fillStyle = "#777"; - ctx.fillText(output.type, canvas_w - w + 130, y + h * 0.75); - y += h; - } - //add + button - if (this.drawButton(canvas_w - w, y + 2, w - 20, h - 2, "+", "#151515", "#222")) { - this.showSubgraphPropertiesDialogRight(subnode); - } - } - //Draws a button into the canvas overlay and computes if it was clicked using the immediate gui paradigm - LGraphCanvas.prototype.drawButton = function( x,y,w,h, text, bgcolor, hovercolor, textcolor ) - { - var ctx = this.ctx; - bgcolor = bgcolor || LiteGraph.NODE_DEFAULT_COLOR; - hovercolor = hovercolor || "#555"; - textcolor = textcolor || LiteGraph.NODE_TEXT_COLOR; - var pos = this.ds.convertOffsetToCanvas(this.graph_mouse); - var hover = LiteGraph.isInsideRectangle( pos[0], pos[1], x,y,w,h ); - pos = this.last_click_position ? [this.last_click_position[0], this.last_click_position[1]] : null; - if(pos) { - var rect = this.canvas.getBoundingClientRect(); - pos[0] -= rect.left; - pos[1] -= rect.top; - } - var clicked = pos && LiteGraph.isInsideRectangle( pos[0], pos[1], x,y,w,h ); - - ctx.fillStyle = hover ? hovercolor : bgcolor; - if(clicked) - ctx.fillStyle = "#AAA"; - ctx.beginPath(); - ctx.roundRect(x,y,w,h,[4] ); - ctx.fill(); - - if(text != null) - { - if(text.constructor == String) - { - ctx.fillStyle = textcolor; - ctx.textAlign = "center"; - ctx.font = ((h * 0.65)|0) + "px Arial"; - ctx.fillText( text, x + w * 0.5,y + h * 0.75 ); - ctx.textAlign = "left"; - } - } - - var was_clicked = clicked && !this.block_click; - if(clicked) - this.blockClick(); - return was_clicked; - } - - LGraphCanvas.prototype.isAreaClicked = function( x,y,w,h, hold_click ) - { - var pos = this.mouse; - var hover = LiteGraph.isInsideRectangle( pos[0], pos[1], x,y,w,h ); - pos = this.last_click_position; - var clicked = pos && LiteGraph.isInsideRectangle( pos[0], pos[1], x,y,w,h ); - var was_clicked = clicked && !this.block_click; - if(clicked && hold_click) - this.blockClick(); - return was_clicked; - } - - /** - * draws some useful stats in the corner of the canvas - * @method renderInfo - **/ - LGraphCanvas.prototype.renderInfo = function(ctx, x, y) { - x = x || 10; - y = y || this.canvas.offsetHeight - 80; - - ctx.save(); - ctx.translate(x, y); - - ctx.font = "10px Arial"; - ctx.fillStyle = "#888"; - ctx.textAlign = "left"; - if (this.graph) { - ctx.fillText( "T: " + this.graph.globaltime.toFixed(2) + "s", 5, 13 * 1 ); - ctx.fillText("I: " + this.graph.iteration, 5, 13 * 2 ); - ctx.fillText("N: " + this.graph._nodes.length + " [" + this.visible_nodes.length + "]", 5, 13 * 3 ); - ctx.fillText("V: " + this.graph._version, 5, 13 * 4); - ctx.fillText("FPS:" + this.fps.toFixed(2), 5, 13 * 5); - } else { - ctx.fillText("No graph selected", 5, 13 * 1); - } - ctx.restore(); - }; - - /** - * draws the back canvas (the one containing the background and the connections) - * @method drawBackCanvas - **/ - LGraphCanvas.prototype.drawBackCanvas = function() { - var canvas = this.bgcanvas; - if ( - canvas.width != this.canvas.width || - canvas.height != this.canvas.height - ) { - canvas.width = this.canvas.width; - canvas.height = this.canvas.height; - } - - if (!this.bgctx) { - this.bgctx = this.bgcanvas.getContext("2d"); - } - var ctx = this.bgctx; - if (ctx.start) { - ctx.start(); - } - - var viewport = this.viewport || [0,0,ctx.canvas.width,ctx.canvas.height]; - - //clear - if (this.clear_background) { - ctx.clearRect( viewport[0], viewport[1], viewport[2], viewport[3] ); - } - - //show subgraph stack header - if (this._graph_stack && this._graph_stack.length) { - ctx.save(); - var parent_graph = this._graph_stack[this._graph_stack.length - 1]; - var subgraph_node = this.graph._subgraph_node; - ctx.strokeStyle = subgraph_node.bgcolor; - ctx.lineWidth = 10; - ctx.strokeRect(1, 1, canvas.width - 2, canvas.height - 2); - ctx.lineWidth = 1; - ctx.font = "40px Arial"; - ctx.textAlign = "center"; - ctx.fillStyle = subgraph_node.bgcolor || "#AAA"; - var title = ""; - for (var i = 1; i < this._graph_stack.length; ++i) { - title += - this._graph_stack[i]._subgraph_node.getTitle() + " >> "; - } - ctx.fillText( - title + subgraph_node.getTitle(), - canvas.width * 0.5, - 40 - ); - ctx.restore(); - } - - var bg_already_painted = false; - if (this.onRenderBackground) { - bg_already_painted = this.onRenderBackground(canvas, ctx); - } - - //reset in case of error - if ( !this.viewport ) - { - ctx.restore(); - ctx.setTransform(1, 0, 0, 1, 0, 0); - } - this.visible_links.length = 0; - - if (this.graph) { - //apply transformations - ctx.save(); - this.ds.toCanvasContext(ctx); - - //render BG - if ( this.ds.scale < 1.5 && !bg_already_painted && this.clear_background_color ) - { - ctx.fillStyle = this.clear_background_color; - ctx.fillRect( - this.visible_area[0], - this.visible_area[1], - this.visible_area[2], - this.visible_area[3] - ); - } - - if ( - this.background_image && - this.ds.scale > 0.5 && - !bg_already_painted - ) { - if (this.zoom_modify_alpha) { - ctx.globalAlpha = - (1.0 - 0.5 / this.ds.scale) * this.editor_alpha; - } else { - ctx.globalAlpha = this.editor_alpha; - } - ctx.imageSmoothingEnabled = ctx.imageSmoothingEnabled = false; // ctx.mozImageSmoothingEnabled = - if ( - !this._bg_img || - this._bg_img.name != this.background_image - ) { - this._bg_img = new Image(); - this._bg_img.name = this.background_image; - this._bg_img.src = this.background_image; - var that = this; - this._bg_img.onload = function() { - that.draw(true, true); - }; - } - - var pattern = null; - if (this._pattern == null && this._bg_img.width > 0) { - pattern = ctx.createPattern(this._bg_img, "repeat"); - this._pattern_img = this._bg_img; - this._pattern = pattern; - } else { - pattern = this._pattern; - } - if (pattern) { - ctx.fillStyle = pattern; - ctx.fillRect( - this.visible_area[0], - this.visible_area[1], - this.visible_area[2], - this.visible_area[3] - ); - ctx.fillStyle = "transparent"; - } - - ctx.globalAlpha = 1.0; - ctx.imageSmoothingEnabled = ctx.imageSmoothingEnabled = true; //= ctx.mozImageSmoothingEnabled - } - - //groups - if (this.graph._groups.length && !this.live_mode) { - this.drawGroups(canvas, ctx); - } - - if (this.onDrawBackground) { - this.onDrawBackground(ctx, this.visible_area); - } - if (this.onBackgroundRender) { - //LEGACY - console.error( - "WARNING! onBackgroundRender deprecated, now is named onDrawBackground " - ); - this.onBackgroundRender = null; - } - - //DEBUG: show clipping area - //ctx.fillStyle = "red"; - //ctx.fillRect( this.visible_area[0] + 10, this.visible_area[1] + 10, this.visible_area[2] - 20, this.visible_area[3] - 20); - - //bg - if (this.render_canvas_border) { - ctx.strokeStyle = "#235"; - ctx.strokeRect(0, 0, canvas.width, canvas.height); - } - - if (this.render_connections_shadows) { - ctx.shadowColor = "#000"; - ctx.shadowOffsetX = 0; - ctx.shadowOffsetY = 0; - ctx.shadowBlur = 6; - } else { - ctx.shadowColor = "rgba(0,0,0,0)"; - } - - //draw connections - if (!this.live_mode) { - this.drawConnections(ctx); - } - - ctx.shadowColor = "rgba(0,0,0,0)"; - - //restore state - ctx.restore(); - } - - if (ctx.finish) { - ctx.finish(); - } - - this.dirty_bgcanvas = false; - this.dirty_canvas = true; //to force to repaint the front canvas with the bgcanvas - }; - - var temp_vec2 = new Float32Array(2); - - /** - * draws the given node inside the canvas - * @method drawNode - **/ - LGraphCanvas.prototype.drawNode = function(node, ctx) { - var glow = false; - this.current_node = node; - - var color = node.color || node.constructor.color || LiteGraph.NODE_DEFAULT_COLOR; - var bgcolor = node.bgcolor || node.constructor.bgcolor || LiteGraph.NODE_DEFAULT_BGCOLOR; - - //shadow and glow - if (node.mouseOver) { - glow = true; - } - - var low_quality = this.ds.scale < 0.6; //zoomed out - - //only render if it forces it to do it - if (this.live_mode) { - if (!node.flags.collapsed) { - ctx.shadowColor = "transparent"; - if (node.onDrawForeground) { - node.onDrawForeground(ctx, this, this.canvas); - } - } - return; - } - - var editor_alpha = this.editor_alpha; - ctx.globalAlpha = editor_alpha; - - if (this.render_shadows && !low_quality) { - ctx.shadowColor = LiteGraph.DEFAULT_SHADOW_COLOR; - ctx.shadowOffsetX = 2 * this.ds.scale; - ctx.shadowOffsetY = 2 * this.ds.scale; - ctx.shadowBlur = 3 * this.ds.scale; - } else { - ctx.shadowColor = "transparent"; - } - - //custom draw collapsed method (draw after shadows because they are affected) - if ( - node.flags.collapsed && - node.onDrawCollapsed && - node.onDrawCollapsed(ctx, this) == true - ) { - return; - } - - //clip if required (mask) - var shape = node._shape || LiteGraph.BOX_SHAPE; - var size = temp_vec2; - temp_vec2.set(node.size); - var horizontal = node.horizontal; // || node.flags.horizontal; - - if (node.flags.collapsed) { - ctx.font = this.inner_text_font; - var title = node.getTitle ? node.getTitle() : node.title; - if (title != null) { - node._collapsed_width = Math.min( - node.size[0], - ctx.measureText(title).width + - LiteGraph.NODE_TITLE_HEIGHT * 2 - ); //LiteGraph.NODE_COLLAPSED_WIDTH; - size[0] = node._collapsed_width; - size[1] = 0; - } - } - - if (node.clip_area) { - //Start clipping - ctx.save(); - ctx.beginPath(); - if (shape == LiteGraph.BOX_SHAPE) { - ctx.rect(0, 0, size[0], size[1]); - } else if (shape == LiteGraph.ROUND_SHAPE) { - ctx.roundRect(0, 0, size[0], size[1], [10]); - } else if (shape == LiteGraph.CIRCLE_SHAPE) { - ctx.arc( - size[0] * 0.5, - size[1] * 0.5, - size[0] * 0.5, - 0, - Math.PI * 2 - ); - } - ctx.clip(); - } - - //draw shape - if (node.has_errors) { - bgcolor = "red"; - } - this.drawNodeShape( - node, - ctx, - size, - color, - bgcolor, - node.is_selected, - node.mouseOver - ); - ctx.shadowColor = "transparent"; - - //draw foreground - if (node.onDrawForeground) { - node.onDrawForeground(ctx, this, this.canvas); - } - - //connection slots - ctx.textAlign = horizontal ? "center" : "left"; - ctx.font = this.inner_text_font; - - var render_text = !low_quality; - - var out_slot = this.connecting_output; - var in_slot = this.connecting_input; - ctx.lineWidth = 1; - - var max_y = 0; - var slot_pos = new Float32Array(2); //to reuse - - //render inputs and outputs - if (!node.flags.collapsed) { - //input connection slots - if (node.inputs) { - for (var i = 0; i < node.inputs.length; i++) { - var slot = node.inputs[i]; - - var slot_type = slot.type; - var slot_shape = slot.shape; - - ctx.globalAlpha = editor_alpha; - //change opacity of incompatible slots when dragging a connection - if ( this.connecting_output && !LiteGraph.isValidConnection( slot.type , out_slot.type) ) { - ctx.globalAlpha = 0.4 * editor_alpha; - } - - ctx.fillStyle = - slot.link != null - ? slot.color_on || - this.default_connection_color_byType[slot_type] || - this.default_connection_color.input_on - : slot.color_off || - this.default_connection_color_byTypeOff[slot_type] || - this.default_connection_color_byType[slot_type] || - this.default_connection_color.input_off; - - var pos = node.getConnectionPos(true, i, slot_pos); - pos[0] -= node.pos[0]; - pos[1] -= node.pos[1]; - if (max_y < pos[1] + LiteGraph.NODE_SLOT_HEIGHT * 0.5) { - max_y = pos[1] + LiteGraph.NODE_SLOT_HEIGHT * 0.5; - } - - ctx.beginPath(); - - if (slot_type == "array"){ - slot_shape = LiteGraph.GRID_SHAPE; // place in addInput? addOutput instead? - } - - var doStroke = true; - - if ( - slot.type === LiteGraph.EVENT || - slot.shape === LiteGraph.BOX_SHAPE - ) { - if (horizontal) { - ctx.rect( - pos[0] - 5 + 0.5, - pos[1] - 8 + 0.5, - 10, - 14 - ); - } else { - ctx.rect( - pos[0] - 6 + 0.5, - pos[1] - 5 + 0.5, - 14, - 10 - ); - } - } else if (slot_shape === LiteGraph.ARROW_SHAPE) { - ctx.moveTo(pos[0] + 8, pos[1] + 0.5); - ctx.lineTo(pos[0] - 4, pos[1] + 6 + 0.5); - ctx.lineTo(pos[0] - 4, pos[1] - 6 + 0.5); - ctx.closePath(); - } else if (slot_shape === LiteGraph.GRID_SHAPE) { - ctx.rect(pos[0] - 4, pos[1] - 4, 2, 2); - ctx.rect(pos[0] - 1, pos[1] - 4, 2, 2); - ctx.rect(pos[0] + 2, pos[1] - 4, 2, 2); - ctx.rect(pos[0] - 4, pos[1] - 1, 2, 2); - ctx.rect(pos[0] - 1, pos[1] - 1, 2, 2); - ctx.rect(pos[0] + 2, pos[1] - 1, 2, 2); - ctx.rect(pos[0] - 4, pos[1] + 2, 2, 2); - ctx.rect(pos[0] - 1, pos[1] + 2, 2, 2); - ctx.rect(pos[0] + 2, pos[1] + 2, 2, 2); - doStroke = false; - } else { - if(low_quality) - ctx.rect(pos[0] - 4, pos[1] - 4, 8, 8 ); //faster - else - ctx.arc(pos[0], pos[1], 4, 0, Math.PI * 2); - } - ctx.fill(); - - //render name - if (render_text) { - var text = slot.label != null ? slot.label : slot.name; - if (text) { - ctx.fillStyle = LiteGraph.NODE_TEXT_COLOR; - if (horizontal || slot.dir == LiteGraph.UP) { - ctx.fillText(text, pos[0], pos[1] - 10); - } else { - ctx.fillText(text, pos[0] + 10, pos[1] + 5); - } - } - } - } - } - - //output connection slots - - ctx.textAlign = horizontal ? "center" : "right"; - ctx.strokeStyle = "black"; - if (node.outputs) { - for (var i = 0; i < node.outputs.length; i++) { - var slot = node.outputs[i]; - - var slot_type = slot.type; - var slot_shape = slot.shape; - - //change opacity of incompatible slots when dragging a connection - if (this.connecting_input && !LiteGraph.isValidConnection( slot_type , in_slot.type) ) { - ctx.globalAlpha = 0.4 * editor_alpha; - } - - var pos = node.getConnectionPos(false, i, slot_pos); - pos[0] -= node.pos[0]; - pos[1] -= node.pos[1]; - if (max_y < pos[1] + LiteGraph.NODE_SLOT_HEIGHT * 0.5) { - max_y = pos[1] + LiteGraph.NODE_SLOT_HEIGHT * 0.5; - } - - ctx.fillStyle = - slot.links && slot.links.length - ? slot.color_on || - this.default_connection_color_byType[slot_type] || - this.default_connection_color.output_on - : slot.color_off || - this.default_connection_color_byTypeOff[slot_type] || - this.default_connection_color_byType[slot_type] || - this.default_connection_color.output_off; - ctx.beginPath(); - //ctx.rect( node.size[0] - 14,i*14,10,10); - - if (slot_type == "array"){ - slot_shape = LiteGraph.GRID_SHAPE; - } - - var doStroke = true; - - if ( - slot_type === LiteGraph.EVENT || - slot_shape === LiteGraph.BOX_SHAPE - ) { - if (horizontal) { - ctx.rect( - pos[0] - 5 + 0.5, - pos[1] - 8 + 0.5, - 10, - 14 - ); - } else { - ctx.rect( - pos[0] - 6 + 0.5, - pos[1] - 5 + 0.5, - 14, - 10 - ); - } - } else if (slot_shape === LiteGraph.ARROW_SHAPE) { - ctx.moveTo(pos[0] + 8, pos[1] + 0.5); - ctx.lineTo(pos[0] - 4, pos[1] + 6 + 0.5); - ctx.lineTo(pos[0] - 4, pos[1] - 6 + 0.5); - ctx.closePath(); - } else if (slot_shape === LiteGraph.GRID_SHAPE) { - ctx.rect(pos[0] - 4, pos[1] - 4, 2, 2); - ctx.rect(pos[0] - 1, pos[1] - 4, 2, 2); - ctx.rect(pos[0] + 2, pos[1] - 4, 2, 2); - ctx.rect(pos[0] - 4, pos[1] - 1, 2, 2); - ctx.rect(pos[0] - 1, pos[1] - 1, 2, 2); - ctx.rect(pos[0] + 2, pos[1] - 1, 2, 2); - ctx.rect(pos[0] - 4, pos[1] + 2, 2, 2); - ctx.rect(pos[0] - 1, pos[1] + 2, 2, 2); - ctx.rect(pos[0] + 2, pos[1] + 2, 2, 2); - doStroke = false; - } else { - if(low_quality) - ctx.rect(pos[0] - 4, pos[1] - 4, 8, 8 ); - else - ctx.arc(pos[0], pos[1], 4, 0, Math.PI * 2); - } - - //trigger - //if(slot.node_id != null && slot.slot == -1) - // ctx.fillStyle = "#F85"; - - //if(slot.links != null && slot.links.length) - ctx.fill(); - if(!low_quality && doStroke) - ctx.stroke(); - - //render output name - if (render_text) { - var text = slot.label != null ? slot.label : slot.name; - if (text) { - ctx.fillStyle = LiteGraph.NODE_TEXT_COLOR; - if (horizontal || slot.dir == LiteGraph.DOWN) { - ctx.fillText(text, pos[0], pos[1] - 8); - } else { - ctx.fillText(text, pos[0] - 10, pos[1] + 5); - } - } - } - } - } - - ctx.textAlign = "left"; - ctx.globalAlpha = 1; - - if (node.widgets) { - var widgets_y = max_y; - if (horizontal || node.widgets_up) { - widgets_y = 2; - } - if( node.widgets_start_y != null ) - widgets_y = node.widgets_start_y; - this.drawNodeWidgets( - node, - widgets_y, - ctx, - this.node_widget && this.node_widget[0] == node - ? this.node_widget[1] - : null - ); - } - } else if (this.render_collapsed_slots) { - //if collapsed - var input_slot = null; - var output_slot = null; - - //get first connected slot to render - if (node.inputs) { - for (var i = 0; i < node.inputs.length; i++) { - var slot = node.inputs[i]; - if (slot.link == null) { - continue; - } - input_slot = slot; - break; - } - } - if (node.outputs) { - for (var i = 0; i < node.outputs.length; i++) { - var slot = node.outputs[i]; - if (!slot.links || !slot.links.length) { - continue; - } - output_slot = slot; - } - } - - if (input_slot) { - var x = 0; - var y = LiteGraph.NODE_TITLE_HEIGHT * -0.5; //center - if (horizontal) { - x = node._collapsed_width * 0.5; - y = -LiteGraph.NODE_TITLE_HEIGHT; - } - ctx.fillStyle = "#686"; - ctx.beginPath(); - if ( - slot.type === LiteGraph.EVENT || - slot.shape === LiteGraph.BOX_SHAPE - ) { - ctx.rect(x - 7 + 0.5, y - 4, 14, 8); - } else if (slot.shape === LiteGraph.ARROW_SHAPE) { - ctx.moveTo(x + 8, y); - ctx.lineTo(x + -4, y - 4); - ctx.lineTo(x + -4, y + 4); - ctx.closePath(); - } else { - ctx.arc(x, y, 4, 0, Math.PI * 2); - } - ctx.fill(); - } - - if (output_slot) { - var x = node._collapsed_width; - var y = LiteGraph.NODE_TITLE_HEIGHT * -0.5; //center - if (horizontal) { - x = node._collapsed_width * 0.5; - y = 0; - } - ctx.fillStyle = "#686"; - ctx.strokeStyle = "black"; - ctx.beginPath(); - if ( - slot.type === LiteGraph.EVENT || - slot.shape === LiteGraph.BOX_SHAPE - ) { - ctx.rect(x - 7 + 0.5, y - 4, 14, 8); - } else if (slot.shape === LiteGraph.ARROW_SHAPE) { - ctx.moveTo(x + 6, y); - ctx.lineTo(x - 6, y - 4); - ctx.lineTo(x - 6, y + 4); - ctx.closePath(); - } else { - ctx.arc(x, y, 4, 0, Math.PI * 2); - } - ctx.fill(); - //ctx.stroke(); - } - } - - if (node.clip_area) { - ctx.restore(); - } - - ctx.globalAlpha = 1.0; - }; - - //used by this.over_link_center - LGraphCanvas.prototype.drawLinkTooltip = function( ctx, link ) - { - var pos = link._pos; - ctx.fillStyle = "black"; - ctx.beginPath(); - ctx.arc( pos[0], pos[1], 3, 0, Math.PI * 2 ); - ctx.fill(); - - if(link.data == null) - return; - - if(this.onDrawLinkTooltip) - if( this.onDrawLinkTooltip(ctx,link,this) == true ) - return; - - var data = link.data; - var text = null; - - if( data.constructor === Number ) - text = data.toFixed(2); - else if( data.constructor === String ) - text = "\"" + data + "\""; - else if( data.constructor === Boolean ) - text = String(data); - else if (data.toToolTip) - text = data.toToolTip(); - else - text = "[" + data.constructor.name + "]"; - - if(text == null) - return; - text = text.substr(0,30); //avoid weird - - ctx.font = "14px Courier New"; - var info = ctx.measureText(text); - var w = info.width + 20; - var h = 24; - ctx.shadowColor = "black"; - ctx.shadowOffsetX = 2; - ctx.shadowOffsetY = 2; - ctx.shadowBlur = 3; - ctx.fillStyle = "#454"; - ctx.beginPath(); - ctx.roundRect( pos[0] - w*0.5, pos[1] - 15 - h, w, h, [3]); - ctx.moveTo( pos[0] - 10, pos[1] - 15 ); - ctx.lineTo( pos[0] + 10, pos[1] - 15 ); - ctx.lineTo( pos[0], pos[1] - 5 ); - ctx.fill(); - ctx.shadowColor = "transparent"; - ctx.textAlign = "center"; - ctx.fillStyle = "#CEC"; - ctx.fillText(text, pos[0], pos[1] - 15 - h * 0.3); - } - - /** - * draws the shape of the given node in the canvas - * @method drawNodeShape - **/ - var tmp_area = new Float32Array(4); - - LGraphCanvas.prototype.drawNodeShape = function( - node, - ctx, - size, - fgcolor, - bgcolor, - selected, - mouse_over - ) { - //bg rect - ctx.strokeStyle = fgcolor; - ctx.fillStyle = bgcolor; - - var title_height = LiteGraph.NODE_TITLE_HEIGHT; - var low_quality = this.ds.scale < 0.5; - - //render node area depending on shape - var shape = - node._shape || node.constructor.shape || LiteGraph.ROUND_SHAPE; - - var title_mode = node.constructor.title_mode; - - var render_title = true; - if (title_mode == LiteGraph.TRANSPARENT_TITLE || title_mode == LiteGraph.NO_TITLE) { - render_title = false; - } else if (title_mode == LiteGraph.AUTOHIDE_TITLE && mouse_over) { - render_title = true; - } - - var area = tmp_area; - area[0] = 0; //x - area[1] = render_title ? -title_height : 0; //y - area[2] = size[0] + 1; //w - area[3] = render_title ? size[1] + title_height : size[1]; //h - - var old_alpha = ctx.globalAlpha; - - //full node shape - //if(node.flags.collapsed) - { - ctx.beginPath(); - if (shape == LiteGraph.BOX_SHAPE || low_quality) { - ctx.fillRect(area[0], area[1], area[2], area[3]); - } else if ( - shape == LiteGraph.ROUND_SHAPE || - shape == LiteGraph.CARD_SHAPE - ) { - ctx.roundRect( - area[0], - area[1], - area[2], - area[3], - shape == LiteGraph.CARD_SHAPE ? [this.round_radius,this.round_radius,0,0] : [this.round_radius] - ); - } else if (shape == LiteGraph.CIRCLE_SHAPE) { - ctx.arc( - size[0] * 0.5, - size[1] * 0.5, - size[0] * 0.5, - 0, - Math.PI * 2 - ); - } - ctx.fill(); - - //separator - if(!node.flags.collapsed && render_title) - { - ctx.shadowColor = "transparent"; - ctx.fillStyle = "rgba(0,0,0,0.2)"; - ctx.fillRect(0, -1, area[2], 2); - } - } - ctx.shadowColor = "transparent"; - - if (node.onDrawBackground) { - node.onDrawBackground(ctx, this, this.canvas, this.graph_mouse ); - } - - //title bg (remember, it is rendered ABOVE the node) - if (render_title || title_mode == LiteGraph.TRANSPARENT_TITLE) { - //title bar - if (node.onDrawTitleBar) { - node.onDrawTitleBar( ctx, title_height, size, this.ds.scale, fgcolor ); - } else if ( - title_mode != LiteGraph.TRANSPARENT_TITLE && - (node.constructor.title_color || this.render_title_colored) - ) { - var title_color = node.constructor.title_color || fgcolor; - - if (node.flags.collapsed) { - ctx.shadowColor = LiteGraph.DEFAULT_SHADOW_COLOR; - } - - //* gradient test - if (this.use_gradients) { - var grad = LGraphCanvas.gradients[title_color]; - if (!grad) { - grad = LGraphCanvas.gradients[ title_color ] = ctx.createLinearGradient(0, 0, 400, 0); - grad.addColorStop(0, title_color); // TODO refactor: validate color !! prevent DOMException - grad.addColorStop(1, "#000"); - } - ctx.fillStyle = grad; - } else { - ctx.fillStyle = title_color; - } - - //ctx.globalAlpha = 0.5 * old_alpha; - ctx.beginPath(); - if (shape == LiteGraph.BOX_SHAPE || low_quality) { - ctx.rect(0, -title_height, size[0] + 1, title_height); - } else if ( shape == LiteGraph.ROUND_SHAPE || shape == LiteGraph.CARD_SHAPE ) { - ctx.roundRect( - 0, - -title_height, - size[0] + 1, - title_height, - node.flags.collapsed ? [this.round_radius] : [this.round_radius,this.round_radius,0,0] - ); - } - ctx.fill(); - ctx.shadowColor = "transparent"; - } - - var colState = false; - if (LiteGraph.node_box_coloured_by_mode){ - if(LiteGraph.NODE_MODES_COLORS[node.mode]){ - colState = LiteGraph.NODE_MODES_COLORS[node.mode]; - } - } - if (LiteGraph.node_box_coloured_when_on){ - colState = node.action_triggered ? "#FFF" : (node.execute_triggered ? "#AAA" : colState); - } - - //title box - var box_size = 10; - if (node.onDrawTitleBox) { - node.onDrawTitleBox(ctx, title_height, size, this.ds.scale); - } else if ( - shape == LiteGraph.ROUND_SHAPE || - shape == LiteGraph.CIRCLE_SHAPE || - shape == LiteGraph.CARD_SHAPE - ) { - if (low_quality) { - ctx.fillStyle = "black"; - ctx.beginPath(); - ctx.arc( - title_height * 0.5, - title_height * -0.5, - box_size * 0.5 + 1, - 0, - Math.PI * 2 - ); - ctx.fill(); - } - - ctx.fillStyle = node.boxcolor || colState || LiteGraph.NODE_DEFAULT_BOXCOLOR; - if(low_quality) - ctx.fillRect( title_height * 0.5 - box_size *0.5, title_height * -0.5 - box_size *0.5, box_size , box_size ); - else - { - ctx.beginPath(); - ctx.arc( - title_height * 0.5, - title_height * -0.5, - box_size * 0.5, - 0, - Math.PI * 2 - ); - ctx.fill(); - } - } else { - if (low_quality) { - ctx.fillStyle = "black"; - ctx.fillRect( - (title_height - box_size) * 0.5 - 1, - (title_height + box_size) * -0.5 - 1, - box_size + 2, - box_size + 2 - ); - } - ctx.fillStyle = node.boxcolor || colState || LiteGraph.NODE_DEFAULT_BOXCOLOR; - ctx.fillRect( - (title_height - box_size) * 0.5, - (title_height + box_size) * -0.5, - box_size, - box_size - ); - } - ctx.globalAlpha = old_alpha; - - //title text - if (node.onDrawTitleText) { - node.onDrawTitleText( - ctx, - title_height, - size, - this.ds.scale, - this.title_text_font, - selected - ); - } - if (!low_quality) { - ctx.font = this.title_text_font; - var title = String(node.getTitle()); - if (title) { - if (selected) { - ctx.fillStyle = LiteGraph.NODE_SELECTED_TITLE_COLOR; - } else { - ctx.fillStyle = - node.constructor.title_text_color || - this.node_title_color; - } - if (node.flags.collapsed) { - ctx.textAlign = "left"; - var measure = ctx.measureText(title); - ctx.fillText( - title.substr(0,20), //avoid urls too long - title_height,// + measure.width * 0.5, - LiteGraph.NODE_TITLE_TEXT_Y - title_height - ); - ctx.textAlign = "left"; - } else { - ctx.textAlign = "left"; - ctx.fillText( - title, - title_height, - LiteGraph.NODE_TITLE_TEXT_Y - title_height - ); - } - } - } - - //subgraph box - if (!node.flags.collapsed && node.subgraph && !node.skip_subgraph_button) { - var w = LiteGraph.NODE_TITLE_HEIGHT; - var x = node.size[0] - w; - var over = LiteGraph.isInsideRectangle( this.graph_mouse[0] - node.pos[0], this.graph_mouse[1] - node.pos[1], x+2, -w+2, w-4, w-4 ); - ctx.fillStyle = over ? "#888" : "#555"; - if( shape == LiteGraph.BOX_SHAPE || low_quality) - ctx.fillRect(x+2, -w+2, w-4, w-4); - else - { - ctx.beginPath(); - ctx.roundRect(x+2, -w+2, w-4, w-4,[4]); - ctx.fill(); - } - ctx.fillStyle = "#333"; - ctx.beginPath(); - ctx.moveTo(x + w * 0.2, -w * 0.6); - ctx.lineTo(x + w * 0.8, -w * 0.6); - ctx.lineTo(x + w * 0.5, -w * 0.3); - ctx.fill(); - } - - //custom title render - if (node.onDrawTitle) { - node.onDrawTitle(ctx); - } - } - - //render selection marker - if (selected) { - if (node.onBounding) { - node.onBounding(area); - } - - if (title_mode == LiteGraph.TRANSPARENT_TITLE) { - area[1] -= title_height; - area[3] += title_height; - } - ctx.lineWidth = 1; - ctx.globalAlpha = 0.8; - ctx.beginPath(); - if (shape == LiteGraph.BOX_SHAPE) { - ctx.rect( - -6 + area[0], - -6 + area[1], - 12 + area[2], - 12 + area[3] - ); - } else if ( - shape == LiteGraph.ROUND_SHAPE || - (shape == LiteGraph.CARD_SHAPE && node.flags.collapsed) - ) { - ctx.roundRect( - -6 + area[0], - -6 + area[1], - 12 + area[2], - 12 + area[3], - [this.round_radius * 2] - ); - } else if (shape == LiteGraph.CARD_SHAPE) { - ctx.roundRect( - -6 + area[0], - -6 + area[1], - 12 + area[2], - 12 + area[3], - [this.round_radius * 2,2,this.round_radius * 2,2] - ); - } else if (shape == LiteGraph.CIRCLE_SHAPE) { - ctx.arc( - size[0] * 0.5, - size[1] * 0.5, - size[0] * 0.5 + 6, - 0, - Math.PI * 2 - ); - } - ctx.strokeStyle = LiteGraph.NODE_BOX_OUTLINE_COLOR; - ctx.stroke(); - ctx.strokeStyle = fgcolor; - ctx.globalAlpha = 1; - } - - // these counter helps in conditioning drawing based on if the node has been executed or an action occurred - if (node.execute_triggered>0) node.execute_triggered--; - if (node.action_triggered>0) node.action_triggered--; - }; - - var margin_area = new Float32Array(4); - var link_bounding = new Float32Array(4); - var tempA = new Float32Array(2); - var tempB = new Float32Array(2); - - /** - * draws every connection visible in the canvas - * OPTIMIZE THIS: pre-catch connections position instead of recomputing them every time - * @method drawConnections - **/ - LGraphCanvas.prototype.drawConnections = function(ctx) { - var now = LiteGraph.getTime(); - var visible_area = this.visible_area; - margin_area[0] = visible_area[0] - 20; - margin_area[1] = visible_area[1] - 20; - margin_area[2] = visible_area[2] + 40; - margin_area[3] = visible_area[3] + 40; - - //draw connections - ctx.lineWidth = this.connections_width; - - ctx.fillStyle = "#AAA"; - ctx.strokeStyle = "#AAA"; - ctx.globalAlpha = this.editor_alpha; - //for every node - var nodes = this.graph._nodes; - for (var n = 0, l = nodes.length; n < l; ++n) { - var node = nodes[n]; - //for every input (we render just inputs because it is easier as every slot can only have one input) - if (!node.inputs || !node.inputs.length) { - continue; - } - - for (var i = 0; i < node.inputs.length; ++i) { - var input = node.inputs[i]; - if (!input || input.link == null) { - continue; - } - var link_id = input.link; - var link = this.graph.links[link_id]; - if (!link) { - continue; - } - - //find link info - var start_node = this.graph.getNodeById(link.origin_id); - if (start_node == null) { - continue; - } - var start_node_slot = link.origin_slot; - var start_node_slotpos = null; - if (start_node_slot == -1) { - start_node_slotpos = [ - start_node.pos[0] + 10, - start_node.pos[1] + 10 - ]; - } else { - start_node_slotpos = start_node.getConnectionPos( - false, - start_node_slot, - tempA - ); - } - var end_node_slotpos = node.getConnectionPos(true, i, tempB); - - //compute link bounding - link_bounding[0] = start_node_slotpos[0]; - link_bounding[1] = start_node_slotpos[1]; - link_bounding[2] = end_node_slotpos[0] - start_node_slotpos[0]; - link_bounding[3] = end_node_slotpos[1] - start_node_slotpos[1]; - if (link_bounding[2] < 0) { - link_bounding[0] += link_bounding[2]; - link_bounding[2] = Math.abs(link_bounding[2]); - } - if (link_bounding[3] < 0) { - link_bounding[1] += link_bounding[3]; - link_bounding[3] = Math.abs(link_bounding[3]); - } - - //skip links outside of the visible area of the canvas - if (!overlapBounding(link_bounding, margin_area)) { - continue; - } - - var start_slot = start_node.outputs[start_node_slot]; - var end_slot = node.inputs[i]; - if (!start_slot || !end_slot) { - continue; - } - var start_dir = - start_slot.dir || - (start_node.horizontal ? LiteGraph.DOWN : LiteGraph.RIGHT); - var end_dir = - end_slot.dir || - (node.horizontal ? LiteGraph.UP : LiteGraph.LEFT); - - this.renderLink( - ctx, - start_node_slotpos, - end_node_slotpos, - link, - false, - 0, - null, - start_dir, - end_dir - ); - - //event triggered rendered on top - if (link && link._last_time && now - link._last_time < 1000) { - var f = 2.0 - (now - link._last_time) * 0.002; - var tmp = ctx.globalAlpha; - ctx.globalAlpha = tmp * f; - this.renderLink( - ctx, - start_node_slotpos, - end_node_slotpos, - link, - true, - f, - "white", - start_dir, - end_dir - ); - ctx.globalAlpha = tmp; - } - } - } - ctx.globalAlpha = 1; - }; - - /** - * draws a link between two points - * @method renderLink - * @param {vec2} a start pos - * @param {vec2} b end pos - * @param {Object} link the link object with all the link info - * @param {boolean} skip_border ignore the shadow of the link - * @param {boolean} flow show flow animation (for events) - * @param {string} color the color for the link - * @param {number} start_dir the direction enum - * @param {number} end_dir the direction enum - * @param {number} num_sublines number of sublines (useful to represent vec3 or rgb) - **/ - LGraphCanvas.prototype.renderLink = function( - ctx, - a, - b, - link, - skip_border, - flow, - color, - start_dir, - end_dir, - num_sublines - ) { - if (link) { - this.visible_links.push(link); - } - - //choose color - if (!color && link) { - color = link.color || LGraphCanvas.link_type_colors[link.type]; - } - if (!color) { - color = this.default_link_color; - } - if (link != null && this.highlighted_links[link.id]) { - color = "#FFF"; - } - - start_dir = start_dir || LiteGraph.RIGHT; - end_dir = end_dir || LiteGraph.LEFT; - - var dist = distance(a, b); - - if (this.render_connections_border && this.ds.scale > 0.6) { - ctx.lineWidth = this.connections_width + 4; - } - ctx.lineJoin = "round"; - num_sublines = num_sublines || 1; - if (num_sublines > 1) { - ctx.lineWidth = 0.5; - } - - //begin line shape - ctx.beginPath(); - for (var i = 0; i < num_sublines; i += 1) { - var offsety = (i - (num_sublines - 1) * 0.5) * 5; - - if (this.links_render_mode == LiteGraph.SPLINE_LINK) { - ctx.moveTo(a[0], a[1] + offsety); - var start_offset_x = 0; - var start_offset_y = 0; - var end_offset_x = 0; - var end_offset_y = 0; - switch (start_dir) { - case LiteGraph.LEFT: - start_offset_x = dist * -0.25; - break; - case LiteGraph.RIGHT: - start_offset_x = dist * 0.25; - break; - case LiteGraph.UP: - start_offset_y = dist * -0.25; - break; - case LiteGraph.DOWN: - start_offset_y = dist * 0.25; - break; - } - switch (end_dir) { - case LiteGraph.LEFT: - end_offset_x = dist * -0.25; - break; - case LiteGraph.RIGHT: - end_offset_x = dist * 0.25; - break; - case LiteGraph.UP: - end_offset_y = dist * -0.25; - break; - case LiteGraph.DOWN: - end_offset_y = dist * 0.25; - break; - } - ctx.bezierCurveTo( - a[0] + start_offset_x, - a[1] + start_offset_y + offsety, - b[0] + end_offset_x, - b[1] + end_offset_y + offsety, - b[0], - b[1] + offsety - ); - } else if (this.links_render_mode == LiteGraph.LINEAR_LINK) { - ctx.moveTo(a[0], a[1] + offsety); - var start_offset_x = 0; - var start_offset_y = 0; - var end_offset_x = 0; - var end_offset_y = 0; - switch (start_dir) { - case LiteGraph.LEFT: - start_offset_x = -1; - break; - case LiteGraph.RIGHT: - start_offset_x = 1; - break; - case LiteGraph.UP: - start_offset_y = -1; - break; - case LiteGraph.DOWN: - start_offset_y = 1; - break; - } - switch (end_dir) { - case LiteGraph.LEFT: - end_offset_x = -1; - break; - case LiteGraph.RIGHT: - end_offset_x = 1; - break; - case LiteGraph.UP: - end_offset_y = -1; - break; - case LiteGraph.DOWN: - end_offset_y = 1; - break; - } - var l = 15; - ctx.lineTo( - a[0] + start_offset_x * l, - a[1] + start_offset_y * l + offsety - ); - ctx.lineTo( - b[0] + end_offset_x * l, - b[1] + end_offset_y * l + offsety - ); - ctx.lineTo(b[0], b[1] + offsety); - } else if (this.links_render_mode == LiteGraph.STRAIGHT_LINK) { - ctx.moveTo(a[0], a[1]); - var start_x = a[0]; - var start_y = a[1]; - var end_x = b[0]; - var end_y = b[1]; - if (start_dir == LiteGraph.RIGHT) { - start_x += 10; - } else { - start_y += 10; - } - if (end_dir == LiteGraph.LEFT) { - end_x -= 10; - } else { - end_y -= 10; - } - ctx.lineTo(start_x, start_y); - ctx.lineTo((start_x + end_x) * 0.5, start_y); - ctx.lineTo((start_x + end_x) * 0.5, end_y); - ctx.lineTo(end_x, end_y); - ctx.lineTo(b[0], b[1]); - } else { - return; - } //unknown - } - - //rendering the outline of the connection can be a little bit slow - if ( - this.render_connections_border && - this.ds.scale > 0.6 && - !skip_border - ) { - ctx.strokeStyle = "rgba(0,0,0,0.5)"; - ctx.stroke(); - } - - ctx.lineWidth = this.connections_width; - ctx.fillStyle = ctx.strokeStyle = color; - ctx.stroke(); - //end line shape - - var pos = this.computeConnectionPoint(a, b, 0.5, start_dir, end_dir); - if (link && link._pos) { - link._pos[0] = pos[0]; - link._pos[1] = pos[1]; - } - - //render arrow in the middle - if ( - this.ds.scale >= 0.6 && - this.highquality_render && - end_dir != LiteGraph.CENTER - ) { - //render arrow - if (this.render_connection_arrows) { - //compute two points in the connection - var posA = this.computeConnectionPoint( - a, - b, - 0.25, - start_dir, - end_dir - ); - var posB = this.computeConnectionPoint( - a, - b, - 0.26, - start_dir, - end_dir - ); - var posC = this.computeConnectionPoint( - a, - b, - 0.75, - start_dir, - end_dir - ); - var posD = this.computeConnectionPoint( - a, - b, - 0.76, - start_dir, - end_dir - ); - - //compute the angle between them so the arrow points in the right direction - var angleA = 0; - var angleB = 0; - if (this.render_curved_connections) { - angleA = -Math.atan2(posB[0] - posA[0], posB[1] - posA[1]); - angleB = -Math.atan2(posD[0] - posC[0], posD[1] - posC[1]); - } else { - angleB = angleA = b[1] > a[1] ? 0 : Math.PI; - } - - //render arrow - ctx.save(); - ctx.translate(posA[0], posA[1]); - ctx.rotate(angleA); - ctx.beginPath(); - ctx.moveTo(-5, -3); - ctx.lineTo(0, +7); - ctx.lineTo(+5, -3); - ctx.fill(); - ctx.restore(); - ctx.save(); - ctx.translate(posC[0], posC[1]); - ctx.rotate(angleB); - ctx.beginPath(); - ctx.moveTo(-5, -3); - ctx.lineTo(0, +7); - ctx.lineTo(+5, -3); - ctx.fill(); - ctx.restore(); - } - - //circle - ctx.beginPath(); - ctx.arc(pos[0], pos[1], 5, 0, Math.PI * 2); - ctx.fill(); - } - - //render flowing points - if (flow) { - ctx.fillStyle = color; - for (var i = 0; i < 5; ++i) { - var f = (LiteGraph.getTime() * 0.001 + i * 0.2) % 1; - var pos = this.computeConnectionPoint( - a, - b, - f, - start_dir, - end_dir - ); - ctx.beginPath(); - ctx.arc(pos[0], pos[1], 5, 0, 2 * Math.PI); - ctx.fill(); - } - } - }; - - //returns the link center point based on curvature - LGraphCanvas.prototype.computeConnectionPoint = function( - a, - b, - t, - start_dir, - end_dir - ) { - start_dir = start_dir || LiteGraph.RIGHT; - end_dir = end_dir || LiteGraph.LEFT; - - var dist = distance(a, b); - var p0 = a; - var p1 = [a[0], a[1]]; - var p2 = [b[0], b[1]]; - var p3 = b; - - switch (start_dir) { - case LiteGraph.LEFT: - p1[0] += dist * -0.25; - break; - case LiteGraph.RIGHT: - p1[0] += dist * 0.25; - break; - case LiteGraph.UP: - p1[1] += dist * -0.25; - break; - case LiteGraph.DOWN: - p1[1] += dist * 0.25; - break; - } - switch (end_dir) { - case LiteGraph.LEFT: - p2[0] += dist * -0.25; - break; - case LiteGraph.RIGHT: - p2[0] += dist * 0.25; - break; - case LiteGraph.UP: - p2[1] += dist * -0.25; - break; - case LiteGraph.DOWN: - p2[1] += dist * 0.25; - break; - } - - var c1 = (1 - t) * (1 - t) * (1 - t); - var c2 = 3 * ((1 - t) * (1 - t)) * t; - var c3 = 3 * (1 - t) * (t * t); - var c4 = t * t * t; - - var x = c1 * p0[0] + c2 * p1[0] + c3 * p2[0] + c4 * p3[0]; - var y = c1 * p0[1] + c2 * p1[1] + c3 * p2[1] + c4 * p3[1]; - return [x, y]; - }; - - LGraphCanvas.prototype.drawExecutionOrder = function(ctx) { - ctx.shadowColor = "transparent"; - ctx.globalAlpha = 0.25; - - ctx.textAlign = "center"; - ctx.strokeStyle = "white"; - ctx.globalAlpha = 0.75; - - var visible_nodes = this.visible_nodes; - for (var i = 0; i < visible_nodes.length; ++i) { - var node = visible_nodes[i]; - ctx.fillStyle = "black"; - ctx.fillRect( - node.pos[0] - LiteGraph.NODE_TITLE_HEIGHT, - node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT, - LiteGraph.NODE_TITLE_HEIGHT, - LiteGraph.NODE_TITLE_HEIGHT - ); - if (node.order == 0) { - ctx.strokeRect( - node.pos[0] - LiteGraph.NODE_TITLE_HEIGHT + 0.5, - node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT + 0.5, - LiteGraph.NODE_TITLE_HEIGHT, - LiteGraph.NODE_TITLE_HEIGHT - ); - } - ctx.fillStyle = "#FFF"; - ctx.fillText( - node.order, - node.pos[0] + LiteGraph.NODE_TITLE_HEIGHT * -0.5, - node.pos[1] - 6 - ); - } - ctx.globalAlpha = 1; - }; - - /** - * draws the widgets stored inside a node - * @method drawNodeWidgets - **/ - LGraphCanvas.prototype.drawNodeWidgets = function( - node, - posY, - ctx, - active_widget - ) { - if (!node.widgets || !node.widgets.length) { - return 0; - } - var width = node.size[0]; - var widgets = node.widgets; - posY += 2; - var H = LiteGraph.NODE_WIDGET_HEIGHT; - var show_text = this.ds.scale > 0.5; - ctx.save(); - ctx.globalAlpha = this.editor_alpha; - var outline_color = LiteGraph.WIDGET_OUTLINE_COLOR; - var background_color = LiteGraph.WIDGET_BGCOLOR; - var text_color = LiteGraph.WIDGET_TEXT_COLOR; - var secondary_text_color = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR; - var margin = 15; - - for (var i = 0; i < widgets.length; ++i) { - var w = widgets[i]; - var y = posY; - if (w.y) { - y = w.y; - } - w.last_y = y; - ctx.strokeStyle = outline_color; - ctx.fillStyle = "#222"; - ctx.textAlign = "left"; - //ctx.lineWidth = 2; - if(w.disabled) - ctx.globalAlpha *= 0.5; - var widget_width = w.width || width; - - switch (w.type) { - case "button": - ctx.fillStyle = background_color; - if (w.clicked) { - ctx.fillStyle = "#AAA"; - w.clicked = false; - this.dirty_canvas = true; - } - ctx.fillRect(margin, y, widget_width - margin * 2, H); - if(show_text && !w.disabled) - ctx.strokeRect( margin, y, widget_width - margin * 2, H ); - if (show_text) { - ctx.textAlign = "center"; - ctx.fillStyle = text_color; - ctx.fillText(w.label || w.name, widget_width * 0.5, y + H * 0.7); - } - break; - case "toggle": - ctx.textAlign = "left"; - ctx.strokeStyle = outline_color; - ctx.fillStyle = background_color; - ctx.beginPath(); - if (show_text) - ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5]); - else - ctx.rect(margin, y, widget_width - margin * 2, H ); - ctx.fill(); - if(show_text && !w.disabled) - ctx.stroke(); - ctx.fillStyle = w.value ? "#89A" : "#333"; - ctx.beginPath(); - ctx.arc( widget_width - margin * 2, y + H * 0.5, H * 0.36, 0, Math.PI * 2 ); - ctx.fill(); - if (show_text) { - ctx.fillStyle = secondary_text_color; - const label = w.label || w.name; - if (label != null) { - ctx.fillText(label, margin * 2, y + H * 0.7); - } - ctx.fillStyle = w.value ? text_color : secondary_text_color; - ctx.textAlign = "right"; - ctx.fillText( - w.value - ? w.options.on || "true" - : w.options.off || "false", - widget_width - 40, - y + H * 0.7 - ); - } - break; - case "slider": - ctx.fillStyle = background_color; - ctx.fillRect(margin, y, widget_width - margin * 2, H); - var range = w.options.max - w.options.min; - var nvalue = (w.value - w.options.min) / range; - if(nvalue < 0.0) nvalue = 0.0; - if(nvalue > 1.0) nvalue = 1.0; - ctx.fillStyle = w.options.hasOwnProperty("slider_color") ? w.options.slider_color : (active_widget == w ? "#89A" : "#678"); - ctx.fillRect(margin, y, nvalue * (widget_width - margin * 2), H); - if(show_text && !w.disabled) - ctx.strokeRect(margin, y, widget_width - margin * 2, H); - if (w.marker) { - var marker_nvalue = (w.marker - w.options.min) / range; - if(marker_nvalue < 0.0) marker_nvalue = 0.0; - if(marker_nvalue > 1.0) marker_nvalue = 1.0; - ctx.fillStyle = w.options.hasOwnProperty("marker_color") ? w.options.marker_color : "#AA9"; - ctx.fillRect( margin + marker_nvalue * (widget_width - margin * 2), y, 2, H ); - } - if (show_text) { - ctx.textAlign = "center"; - ctx.fillStyle = text_color; - ctx.fillText( - w.label || w.name + " " + Number(w.value).toFixed( - w.options.precision != null - ? w.options.precision - : 3 - ), - widget_width * 0.5, - y + H * 0.7 - ); - } - break; - case "number": - case "combo": - ctx.textAlign = "left"; - ctx.strokeStyle = outline_color; - ctx.fillStyle = background_color; - ctx.beginPath(); - if(show_text) - ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5] ); - else - ctx.rect(margin, y, widget_width - margin * 2, H ); - ctx.fill(); - if (show_text) { - if(!w.disabled) - ctx.stroke(); - ctx.fillStyle = text_color; - if(!w.disabled) - { - ctx.beginPath(); - ctx.moveTo(margin + 16, y + 5); - ctx.lineTo(margin + 6, y + H * 0.5); - ctx.lineTo(margin + 16, y + H - 5); - ctx.fill(); - ctx.beginPath(); - ctx.moveTo(widget_width - margin - 16, y + 5); - ctx.lineTo(widget_width - margin - 6, y + H * 0.5); - ctx.lineTo(widget_width - margin - 16, y + H - 5); - ctx.fill(); - } - ctx.fillStyle = secondary_text_color; - ctx.fillText(w.label || w.name, margin * 2 + 5, y + H * 0.7); - ctx.fillStyle = text_color; - ctx.textAlign = "right"; - if (w.type == "number") { - ctx.fillText( - Number(w.value).toFixed( - w.options.precision !== undefined - ? w.options.precision - : 3 - ), - widget_width - margin * 2 - 20, - y + H * 0.7 - ); - } else { - var v = w.value; - if( w.options.values ) - { - var values = w.options.values; - if( values.constructor === Function ) - values = values(); - if(values && values.constructor !== Array) - v = values[ w.value ]; - } - ctx.fillText( - v, - widget_width - margin * 2 - 20, - y + H * 0.7 - ); - } - } - break; - case "string": - case "text": - ctx.textAlign = "left"; - ctx.strokeStyle = outline_color; - ctx.fillStyle = background_color; - ctx.beginPath(); - if (show_text) - ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5]); - else - ctx.rect( margin, y, widget_width - margin * 2, H ); - ctx.fill(); - if (show_text) { - if(!w.disabled) - ctx.stroke(); - ctx.save(); - ctx.beginPath(); - ctx.rect(margin, y, widget_width - margin * 2, H); - ctx.clip(); - - //ctx.stroke(); - ctx.fillStyle = secondary_text_color; - const label = w.label || w.name; - if (label != null) { - ctx.fillText(label, margin * 2, y + H * 0.7); - } - ctx.fillStyle = text_color; - ctx.textAlign = "right"; - ctx.fillText(String(w.value).substr(0,30), widget_width - margin * 2, y + H * 0.7); //30 chars max - ctx.restore(); - } - break; - default: - if (w.draw) { - w.draw(ctx, node, widget_width, y, H); - } - break; - } - posY += (w.computeSize ? w.computeSize(widget_width)[1] : H) + 4; - ctx.globalAlpha = this.editor_alpha; - - } - ctx.restore(); - ctx.textAlign = "left"; - }; - - /** - * process an event on widgets - * @method processNodeWidgets - **/ - LGraphCanvas.prototype.processNodeWidgets = function( - node, - pos, - event, - active_widget - ) { - if (!node.widgets || !node.widgets.length || (!this.allow_interaction && !node.flags.allow_interaction)) { - return null; - } - - var x = pos[0] - node.pos[0]; - var y = pos[1] - node.pos[1]; - var width = node.size[0]; - var that = this; - var ref_window = this.getCanvasWindow(); - - for (var i = 0; i < node.widgets.length; ++i) { - var w = node.widgets[i]; - if(!w || w.disabled) - continue; - var widget_height = w.computeSize ? w.computeSize(width)[1] : LiteGraph.NODE_WIDGET_HEIGHT; - var widget_width = w.width || width; - //outside - if ( w != active_widget && - (x < 6 || x > widget_width - 12 || y < w.last_y || y > w.last_y + widget_height || w.last_y === undefined) ) - continue; - - var old_value = w.value; - - //if ( w == active_widget || (x > 6 && x < widget_width - 12 && y > w.last_y && y < w.last_y + widget_height) ) { - //inside widget - switch (w.type) { - case "button": - if (event.type === LiteGraph.pointerevents_method+"down") { - if (w.callback) { - setTimeout(function() { - w.callback(w, that, node, pos, event); - }, 20); - } - w.clicked = true; - this.dirty_canvas = true; - } - break; - case "slider": - var old_value = w.value; - var nvalue = clamp((x - 15) / (widget_width - 30), 0, 1); - if(w.options.read_only) break; - w.value = w.options.min + (w.options.max - w.options.min) * nvalue; - if (old_value != w.value) { - setTimeout(function() { - inner_value_change(w, w.value); - }, 20); - } - this.dirty_canvas = true; - break; - case "number": - case "combo": - var old_value = w.value; - var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0; - var allow_scroll = true; - if (delta) { - if (x > -3 && x < widget_width + 3) { - allow_scroll = false; - } - } - if (allow_scroll && event.type == LiteGraph.pointerevents_method+"move" && w.type == "number") { - if(event.deltaX) - w.value += event.deltaX * 0.1 * (w.options.step || 1); - if ( w.options.min != null && w.value < w.options.min ) { - w.value = w.options.min; - } - if ( w.options.max != null && w.value > w.options.max ) { - w.value = w.options.max; - } - } else if (event.type == LiteGraph.pointerevents_method+"down") { - var values = w.options.values; - if (values && values.constructor === Function) { - values = w.options.values(w, node); - } - var values_list = null; - - if( w.type != "number") - values_list = values.constructor === Array ? values : Object.keys(values); - - var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0; - if (w.type == "number") { - w.value += delta * 0.1 * (w.options.step || 1); - if ( w.options.min != null && w.value < w.options.min ) { - w.value = w.options.min; - } - if ( w.options.max != null && w.value > w.options.max ) { - w.value = w.options.max; - } - } else if (delta) { //clicked in arrow, used for combos - var index = -1; - this.last_mouseclick = 0; //avoids dobl click event - if(values.constructor === Object) - index = values_list.indexOf( String( w.value ) ) + delta; - else - index = values_list.indexOf( w.value ) + delta; - if (index >= values_list.length) { - index = values_list.length - 1; - } - if (index < 0) { - index = 0; - } - if( values.constructor === Array ) - w.value = values[index]; - else - w.value = index; - } else { //combo clicked - var text_values = values != values_list ? Object.values(values) : values; - var menu = new LiteGraph.ContextMenu(text_values, { - scale: Math.max(1, this.ds.scale), - event: event, - className: "dark", - callback: inner_clicked.bind(w) - }, - ref_window); - function inner_clicked(v, option, event) { - if(values != values_list) - v = text_values.indexOf(v); - this.value = v; - inner_value_change(this, v); - that.dirty_canvas = true; - return false; - } - } - } //end mousedown - else if(event.type == LiteGraph.pointerevents_method+"up" && w.type == "number") - { - var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0; - if (event.click_time < 200 && delta == 0) { - this.prompt("Value",w.value,function(v) { - // check if v is a valid equation or a number - if (/^[0-9+\-*/()\s]+|\d+\.\d+$/.test(v)) { - try {//solve the equation if possible - v = eval(v); - } catch (e) { } - } - this.value = Number(v); - inner_value_change(this, this.value); - }.bind(w), - event); - } - } - - if( old_value != w.value ) - setTimeout( - function() { - inner_value_change(this, this.value); - }.bind(w), - 20 - ); - this.dirty_canvas = true; - break; - case "toggle": - if (event.type == LiteGraph.pointerevents_method+"down") { - w.value = !w.value; - setTimeout(function() { - inner_value_change(w, w.value); - }, 20); - } - break; - case "string": - case "text": - if (event.type == LiteGraph.pointerevents_method+"down") { - this.prompt("Value",w.value,function(v) { - inner_value_change(this, v); - }.bind(w), - event,w.options ? w.options.multiline : false ); - } - break; - default: - if (w.mouse) { - this.dirty_canvas = w.mouse(event, [x, y], node); - } - break; - } //end switch - - //value changed - if( old_value != w.value ) - { - if(node.onWidgetChanged) - node.onWidgetChanged( w.name,w.value,old_value,w ); - node.graph._version++; - } - - return w; - }//end for - - function inner_value_change(widget, value) { - if(widget.type == "number"){ - value = Number(value); - } - widget.value = value; - if ( widget.options && widget.options.property && node.properties[widget.options.property] !== undefined ) { - node.setProperty( widget.options.property, value ); - } - if (widget.callback) { - widget.callback(widget.value, that, node, pos, event); - } - } - - return null; - }; - - /** - * draws every group area in the background - * @method drawGroups - **/ - LGraphCanvas.prototype.drawGroups = function(canvas, ctx) { - if (!this.graph) { - return; - } - - var groups = this.graph._groups; - - ctx.save(); - ctx.globalAlpha = 0.5 * this.editor_alpha; - - for (var i = 0; i < groups.length; ++i) { - var group = groups[i]; - - if (!overlapBounding(this.visible_area, group._bounding)) { - continue; - } //out of the visible area - - ctx.fillStyle = group.color || "#335"; - ctx.strokeStyle = group.color || "#335"; - var pos = group._pos; - var size = group._size; - ctx.globalAlpha = 0.25 * this.editor_alpha; - ctx.beginPath(); - ctx.rect(pos[0] + 0.5, pos[1] + 0.5, size[0], size[1]); - ctx.fill(); - ctx.globalAlpha = this.editor_alpha; - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(pos[0] + size[0], pos[1] + size[1]); - ctx.lineTo(pos[0] + size[0] - 10, pos[1] + size[1]); - ctx.lineTo(pos[0] + size[0], pos[1] + size[1] - 10); - ctx.fill(); - - var font_size = - group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE; - ctx.font = font_size + "px Arial"; - ctx.textAlign = "left"; - ctx.fillText(group.title, pos[0] + 4, pos[1] + font_size); - } - - ctx.restore(); - }; - - LGraphCanvas.prototype.adjustNodesSize = function() { - var nodes = this.graph._nodes; - for (var i = 0; i < nodes.length; ++i) { - nodes[i].size = nodes[i].computeSize(); - } - this.setDirty(true, true); - }; - - /** - * resizes the canvas to a given size, if no size is passed, then it tries to fill the parentNode - * @method resize - **/ - LGraphCanvas.prototype.resize = function(width, height) { - if (!width && !height) { - var parent = this.canvas.parentNode; - width = parent.offsetWidth; - height = parent.offsetHeight; - } - - if (this.canvas.width == width && this.canvas.height == height) { - return; - } - - this.canvas.width = width; - this.canvas.height = height; - this.bgcanvas.width = this.canvas.width; - this.bgcanvas.height = this.canvas.height; - this.setDirty(true, true); - }; - - /** - * switches to live mode (node shapes are not rendered, only the content) - * this feature was designed when graphs where meant to create user interfaces - * @method switchLiveMode - **/ - LGraphCanvas.prototype.switchLiveMode = function(transition) { - if (!transition) { - this.live_mode = !this.live_mode; - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - return; - } - - var self = this; - var delta = this.live_mode ? 1.1 : 0.9; - if (this.live_mode) { - this.live_mode = false; - this.editor_alpha = 0.1; - } - - var t = setInterval(function() { - self.editor_alpha *= delta; - self.dirty_canvas = true; - self.dirty_bgcanvas = true; - - if (delta < 1 && self.editor_alpha < 0.01) { - clearInterval(t); - if (delta < 1) { - self.live_mode = true; - } - } - if (delta > 1 && self.editor_alpha > 0.99) { - clearInterval(t); - self.editor_alpha = 1; - } - }, 1); - }; - - LGraphCanvas.prototype.onNodeSelectionChange = function(node) { - return; //disabled - }; - - /* this is an implementation for touch not in production and not ready - */ - /*LGraphCanvas.prototype.touchHandler = function(event) { - //alert("foo"); - var touches = event.changedTouches, - first = touches[0], - type = ""; - - switch (event.type) { - case "touchstart": - type = "mousedown"; - break; - case "touchmove": - type = "mousemove"; - break; - case "touchend": - type = "mouseup"; - break; - default: - return; - } - - //initMouseEvent(type, canBubble, cancelable, view, clickCount, - // screenX, screenY, clientX, clientY, ctrlKey, - // altKey, shiftKey, metaKey, button, relatedTarget); - - // this is eventually a Dom object, get the LGraphCanvas back - if(typeof this.getCanvasWindow == "undefined"){ - var window = this.lgraphcanvas.getCanvasWindow(); - }else{ - var window = this.getCanvasWindow(); - } - - var document = window.document; - - var simulatedEvent = document.createEvent("MouseEvent"); - simulatedEvent.initMouseEvent( - type, - true, - true, - window, - 1, - first.screenX, - first.screenY, - first.clientX, - first.clientY, - false, - false, - false, - false, - 0, //left - null - ); - first.target.dispatchEvent(simulatedEvent); - event.preventDefault(); - };*/ - - /* CONTEXT MENU ********************/ - - LGraphCanvas.onGroupAdd = function(info, entry, mouse_event) { - var canvas = LGraphCanvas.active_canvas; - var ref_window = canvas.getCanvasWindow(); - - var group = new LiteGraph.LGraphGroup(); - group.pos = canvas.convertEventToCanvasOffset(mouse_event); - canvas.graph.add(group); - }; - - /** - * Determines the furthest nodes in each direction - * @param nodes {LGraphNode[]} the nodes to from which boundary nodes will be extracted - * @return {{left: LGraphNode, top: LGraphNode, right: LGraphNode, bottom: LGraphNode}} - */ - LGraphCanvas.getBoundaryNodes = function(nodes) { - let top = null; - let right = null; - let bottom = null; - let left = null; - for (const nID in nodes) { - const node = nodes[nID]; - const [x, y] = node.pos; - const [width, height] = node.size; - - if (top === null || y < top.pos[1]) { - top = node; - } - if (right === null || x + width > right.pos[0] + right.size[0]) { - right = node; - } - if (bottom === null || y + height > bottom.pos[1] + bottom.size[1]) { - bottom = node; - } - if (left === null || x < left.pos[0]) { - left = node; - } - } - - return { - "top": top, - "right": right, - "bottom": bottom, - "left": left - }; - } - /** - * Determines the furthest nodes in each direction for the currently selected nodes - * @return {{left: LGraphNode, top: LGraphNode, right: LGraphNode, bottom: LGraphNode}} - */ - LGraphCanvas.prototype.boundaryNodesForSelection = function() { - return LGraphCanvas.getBoundaryNodes(Object.values(this.selected_nodes)); - } - - /** - * - * @param {LGraphNode[]} nodes a list of nodes - * @param {"top"|"bottom"|"left"|"right"} direction Direction to align the nodes - * @param {LGraphNode?} align_to Node to align to (if null, align to the furthest node in the given direction) - */ - LGraphCanvas.alignNodes = function (nodes, direction, align_to) { - if (!nodes) { - return; - } - - const canvas = LGraphCanvas.active_canvas; - let boundaryNodes = [] - if (align_to === undefined) { - boundaryNodes = LGraphCanvas.getBoundaryNodes(nodes) - } else { - boundaryNodes = { - "top": align_to, - "right": align_to, - "bottom": align_to, - "left": align_to - } - } - - for (const [_, node] of Object.entries(canvas.selected_nodes)) { - switch (direction) { - case "right": - node.pos[0] = boundaryNodes["right"].pos[0] + boundaryNodes["right"].size[0] - node.size[0]; - break; - case "left": - node.pos[0] = boundaryNodes["left"].pos[0]; - break; - case "top": - node.pos[1] = boundaryNodes["top"].pos[1]; - break; - case "bottom": - node.pos[1] = boundaryNodes["bottom"].pos[1] + boundaryNodes["bottom"].size[1] - node.size[1]; - break; - } - } - - canvas.dirty_canvas = true; - canvas.dirty_bgcanvas = true; - }; - - LGraphCanvas.onNodeAlign = function(value, options, event, prev_menu, node) { - new LiteGraph.ContextMenu(["Top", "Bottom", "Left", "Right"], { - event: event, - callback: inner_clicked, - parentMenu: prev_menu, - }); - - function inner_clicked(value) { - LGraphCanvas.alignNodes(LGraphCanvas.active_canvas.selected_nodes, value.toLowerCase(), node); - } - } - - LGraphCanvas.onGroupAlign = function(value, options, event, prev_menu) { - new LiteGraph.ContextMenu(["Top", "Bottom", "Left", "Right"], { - event: event, - callback: inner_clicked, - parentMenu: prev_menu, - }); - - function inner_clicked(value) { - LGraphCanvas.alignNodes(LGraphCanvas.active_canvas.selected_nodes, value.toLowerCase()); - } - } - - LGraphCanvas.onMenuAdd = function (node, options, e, prev_menu, callback) { - - var canvas = LGraphCanvas.active_canvas; - var ref_window = canvas.getCanvasWindow(); - var graph = canvas.graph; - if (!graph) - return; - - function inner_onMenuAdded(base_category ,prev_menu){ - - var categories = LiteGraph.getNodeTypesCategories(canvas.filter || graph.filter).filter(function(category){return category.startsWith(base_category)}); - var entries = []; - - categories.map(function(category){ - - if (!category) - return; - - var base_category_regex = new RegExp('^(' + base_category + ')'); - var category_name = category.replace(base_category_regex,"").split('/')[0]; - var category_path = base_category === '' ? category_name + '/' : base_category + category_name + '/'; - - var name = category_name; - if(name.indexOf("::") != -1) //in case it has a namespace like "shader::math/rand" it hides the namespace - name = name.split("::")[1]; - - var index = entries.findIndex(function(entry){return entry.value === category_path}); - if (index === -1) { - entries.push({ value: category_path, content: name, has_submenu: true, callback : function(value, event, mouseEvent, contextMenu){ - inner_onMenuAdded(value.value, contextMenu) - }}); - } - - }); - - var nodes = LiteGraph.getNodeTypesInCategory(base_category.slice(0, -1), canvas.filter || graph.filter ); - nodes.map(function(node){ - - if (node.skip_list) - return; - - var entry = { value: node.type, content: node.title, has_submenu: false , callback : function(value, event, mouseEvent, contextMenu){ - - var first_event = contextMenu.getFirstEvent(); - canvas.graph.beforeChange(); - var node = LiteGraph.createNode(value.value); - if (node) { - node.pos = canvas.convertEventToCanvasOffset(first_event); - canvas.graph.add(node); - } - if(callback) - callback(node); - canvas.graph.afterChange(); - - } - } - - entries.push(entry); - - }); - - new LiteGraph.ContextMenu( entries, { event: e, parentMenu: prev_menu }, ref_window ); - - } - - inner_onMenuAdded('',prev_menu); - return false; - - }; - - LGraphCanvas.onMenuCollapseAll = function() {}; - - LGraphCanvas.onMenuNodeEdit = function() {}; - - LGraphCanvas.showMenuNodeOptionalInputs = function( - v, - options, - e, - prev_menu, - node - ) { - if (!node) { - return; - } - - var that = this; - var canvas = LGraphCanvas.active_canvas; - var ref_window = canvas.getCanvasWindow(); - - var options = node.optional_inputs; - if (node.onGetInputs) { - options = node.onGetInputs(); - } - - var entries = []; - if (options) { - for (var i=0; i < options.length; i++) { - var entry = options[i]; - if (!entry) { - entries.push(null); - continue; - } - var label = entry[0]; - if(!entry[2]) - entry[2] = {}; - - if (entry[2].label) { - label = entry[2].label; - } - - entry[2].removable = true; - var data = { content: label, value: entry }; - if (entry[1] == LiteGraph.ACTION) { - data.className = "event"; - } - entries.push(data); - } - } - - if (node.onMenuNodeInputs) { - var retEntries = node.onMenuNodeInputs(entries); - if(retEntries) entries = retEntries; - } - - if (!entries.length) { - console.log("no input entries"); - return; - } - - var menu = new LiteGraph.ContextMenu( - entries, - { - event: e, - callback: inner_clicked, - parentMenu: prev_menu, - node: node - }, - ref_window - ); - - function inner_clicked(v, e, prev) { - if (!node) { - return; - } - - if (v.callback) { - v.callback.call(that, node, v, e, prev); - } - - if (v.value) { - node.graph.beforeChange(); - node.addInput(v.value[0], v.value[1], v.value[2]); - - if (node.onNodeInputAdd) { // callback to the node when adding a slot - node.onNodeInputAdd(v.value); - } - node.setDirtyCanvas(true, true); - node.graph.afterChange(); - } - } - - return false; - }; - - LGraphCanvas.showMenuNodeOptionalOutputs = function( - v, - options, - e, - prev_menu, - node - ) { - if (!node) { - return; - } - - var that = this; - var canvas = LGraphCanvas.active_canvas; - var ref_window = canvas.getCanvasWindow(); - - var options = node.optional_outputs; - if (node.onGetOutputs) { - options = node.onGetOutputs(); - } - - var entries = []; - if (options) { - for (var i=0; i < options.length; i++) { - var entry = options[i]; - if (!entry) { - //separator? - entries.push(null); - continue; - } - - if ( - node.flags && - node.flags.skip_repeated_outputs && - node.findOutputSlot(entry[0]) != -1 - ) { - continue; - } //skip the ones already on - var label = entry[0]; - if(!entry[2]) - entry[2] = {}; - if (entry[2].label) { - label = entry[2].label; - } - entry[2].removable = true; - var data = { content: label, value: entry }; - if (entry[1] == LiteGraph.EVENT) { - data.className = "event"; - } - entries.push(data); - } - } - - if (this.onMenuNodeOutputs) { - entries = this.onMenuNodeOutputs(entries); - } - if (LiteGraph.do_add_triggers_slots){ //canvas.allow_addOutSlot_onExecuted - if (node.findOutputSlot("onExecuted") == -1){ - entries.push({content: "On Executed", value: ["onExecuted", LiteGraph.EVENT, {nameLocked: true}], className: "event"}); //, opts: {} - } - } - // add callback for modifing the menu elements onMenuNodeOutputs - if (node.onMenuNodeOutputs) { - var retEntries = node.onMenuNodeOutputs(entries); - if(retEntries) entries = retEntries; - } - - if (!entries.length) { - return; - } - - var menu = new LiteGraph.ContextMenu( - entries, - { - event: e, - callback: inner_clicked, - parentMenu: prev_menu, - node: node - }, - ref_window - ); - - function inner_clicked(v, e, prev) { - if (!node) { - return; - } - - if (v.callback) { - v.callback.call(that, node, v, e, prev); - } - - if (!v.value) { - return; - } - - var value = v.value[1]; - - if ( - value && - (value.constructor === Object || value.constructor === Array) - ) { - //submenu why? - var entries = []; - for (var i in value) { - entries.push({ content: i, value: value[i] }); - } - new LiteGraph.ContextMenu(entries, { - event: e, - callback: inner_clicked, - parentMenu: prev_menu, - node: node - }); - return false; - } else { - node.graph.beforeChange(); - node.addOutput(v.value[0], v.value[1], v.value[2]); - - if (node.onNodeOutputAdd) { // a callback to the node when adding a slot - node.onNodeOutputAdd(v.value); - } - node.setDirtyCanvas(true, true); - node.graph.afterChange(); - } - } - - return false; - }; - - LGraphCanvas.onShowMenuNodeProperties = function( - value, - options, - e, - prev_menu, - node - ) { - if (!node || !node.properties) { - return; - } - - var that = this; - var canvas = LGraphCanvas.active_canvas; - var ref_window = canvas.getCanvasWindow(); - - var entries = []; - for (var i in node.properties) { - var value = node.properties[i] !== undefined ? node.properties[i] : " "; - if( typeof value == "object" ) - value = JSON.stringify(value); - var info = node.getPropertyInfo(i); - if(info.type == "enum" || info.type == "combo") - value = LGraphCanvas.getPropertyPrintableValue( value, info.values ); - - //value could contain invalid html characters, clean that - value = LGraphCanvas.decodeHTML(value); - entries.push({ - content: - "" + - (info.label ? info.label : i) + - "" + - "" + - value + - "", - value: i - }); - } - if (!entries.length) { - return; - } - - var menu = new LiteGraph.ContextMenu( - entries, - { - event: e, - callback: inner_clicked, - parentMenu: prev_menu, - allow_html: true, - node: node - }, - ref_window - ); - - function inner_clicked(v, options, e, prev) { - if (!node) { - return; - } - var rect = this.getBoundingClientRect(); - canvas.showEditPropertyValue(node, v.value, { - position: [rect.left, rect.top] - }); - } - - return false; - }; - - LGraphCanvas.decodeHTML = function(str) { - var e = document.createElement("div"); - e.innerText = str; - return e.innerHTML; - }; - - LGraphCanvas.onMenuResizeNode = function(value, options, e, menu, node) { - if (!node) { - return; - } - - var fApplyMultiNode = function(node){ - node.size = node.computeSize(); - if (node.onResize) - node.onResize(node.size); - } - - var graphcanvas = LGraphCanvas.active_canvas; - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ - fApplyMultiNode(node); - }else{ - for (var i in graphcanvas.selected_nodes) { - fApplyMultiNode(graphcanvas.selected_nodes[i]); - } - } - - node.setDirtyCanvas(true, true); - }; - - LGraphCanvas.prototype.showLinkMenu = function(link, e) { - var that = this; - // console.log(link); - var node_left = that.graph.getNodeById( link.origin_id ); - var node_right = that.graph.getNodeById( link.target_id ); - var fromType = false; - if (node_left && node_left.outputs && node_left.outputs[link.origin_slot]) fromType = node_left.outputs[link.origin_slot].type; - var destType = false; - if (node_right && node_right.outputs && node_right.outputs[link.target_slot]) destType = node_right.inputs[link.target_slot].type; - - var options = ["Add Node",null,"Delete",null]; - - - var menu = new LiteGraph.ContextMenu(options, { - event: e, - title: link.data != null ? link.data.constructor.name : null, - callback: inner_clicked - }); - - function inner_clicked(v,options,e) { - switch (v) { - case "Add Node": - LGraphCanvas.onMenuAdd(null, null, e, menu, function(node){ - // console.debug("node autoconnect"); - if(!node.inputs || !node.inputs.length || !node.outputs || !node.outputs.length){ - return; - } - // leave the connection type checking inside connectByType - if (node_left.connectByType( link.origin_slot, node, fromType )){ - node.connectByType( link.target_slot, node_right, destType ); - node.pos[0] -= node.size[0] * 0.5; - } - }); - break; - - case "Delete": - that.graph.removeLink(link.id); - break; - default: - /*var nodeCreated = createDefaultNodeForSlot({ nodeFrom: node_left - ,slotFrom: link.origin_slot - ,nodeTo: node - ,slotTo: link.target_slot - ,e: e - ,nodeType: "AUTO" - }); - if(nodeCreated) console.log("new node in beetween "+v+" created");*/ - } - } - - return false; - }; - - LGraphCanvas.prototype.createDefaultNodeForSlot = function(optPass) { // addNodeMenu for connection - var optPass = optPass || {}; - var opts = Object.assign({ nodeFrom: null // input - ,slotFrom: null // input - ,nodeTo: null // output - ,slotTo: null // output - ,position: [] // pass the event coords - ,nodeType: null // choose a nodetype to add, AUTO to set at first good - ,posAdd:[0,0] // adjust x,y - ,posSizeFix:[0,0] // alpha, adjust the position x,y based on the new node size w,h - } - ,optPass - ); - var that = this; - - var isFrom = opts.nodeFrom && opts.slotFrom!==null; - var isTo = !isFrom && opts.nodeTo && opts.slotTo!==null; - - if (!isFrom && !isTo){ - console.warn("No data passed to createDefaultNodeForSlot "+opts.nodeFrom+" "+opts.slotFrom+" "+opts.nodeTo+" "+opts.slotTo); - return false; - } - if (!opts.nodeType){ - console.warn("No type to createDefaultNodeForSlot"); - return false; - } - - var nodeX = isFrom ? opts.nodeFrom : opts.nodeTo; - var slotX = isFrom ? opts.slotFrom : opts.slotTo; - - var iSlotConn = false; - switch (typeof slotX){ - case "string": - iSlotConn = isFrom ? nodeX.findOutputSlot(slotX,false) : nodeX.findInputSlot(slotX,false); - slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX]; - break; - case "object": - // ok slotX - iSlotConn = isFrom ? nodeX.findOutputSlot(slotX.name) : nodeX.findInputSlot(slotX.name); - break; - case "number": - iSlotConn = slotX; - slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX]; - break; - case "undefined": - default: - // bad ? - //iSlotConn = 0; - console.warn("Cant get slot information "+slotX); - return false; - } - - if (slotX===false || iSlotConn===false){ - console.warn("createDefaultNodeForSlot bad slotX "+slotX+" "+iSlotConn); - } - - // check for defaults nodes for this slottype - var fromSlotType = slotX.type==LiteGraph.EVENT?"_event_":slotX.type; - var slotTypesDefault = isFrom ? LiteGraph.slot_types_default_out : LiteGraph.slot_types_default_in; - if(slotTypesDefault && slotTypesDefault[fromSlotType]){ - if (slotX.link !== null) { - // is connected - }else{ - // is not not connected - } - nodeNewType = false; - if(typeof slotTypesDefault[fromSlotType] == "object" || typeof slotTypesDefault[fromSlotType] == "array"){ - for(var typeX in slotTypesDefault[fromSlotType]){ - if (opts.nodeType == slotTypesDefault[fromSlotType][typeX] || opts.nodeType == "AUTO"){ - nodeNewType = slotTypesDefault[fromSlotType][typeX]; - // console.log("opts.nodeType == slotTypesDefault[fromSlotType][typeX] :: "+opts.nodeType); - break; // -------- - } - } - }else{ - if (opts.nodeType == slotTypesDefault[fromSlotType] || opts.nodeType == "AUTO") nodeNewType = slotTypesDefault[fromSlotType]; - } - if (nodeNewType) { - var nodeNewOpts = false; - if (typeof nodeNewType == "object" && nodeNewType.node){ - nodeNewOpts = nodeNewType; - nodeNewType = nodeNewType.node; - } - - //that.graph.beforeChange(); - - var newNode = LiteGraph.createNode(nodeNewType); - if(newNode){ - // if is object pass options - if (nodeNewOpts){ - if (nodeNewOpts.properties) { - for (var i in nodeNewOpts.properties) { - newNode.addProperty( i, nodeNewOpts.properties[i] ); - } - } - if (nodeNewOpts.inputs) { - newNode.inputs = []; - for (var i in nodeNewOpts.inputs) { - newNode.addOutput( - nodeNewOpts.inputs[i][0], - nodeNewOpts.inputs[i][1] - ); - } - } - if (nodeNewOpts.outputs) { - newNode.outputs = []; - for (var i in nodeNewOpts.outputs) { - newNode.addOutput( - nodeNewOpts.outputs[i][0], - nodeNewOpts.outputs[i][1] - ); - } - } - if (nodeNewOpts.title) { - newNode.title = nodeNewOpts.title; - } - if (nodeNewOpts.json) { - newNode.configure(nodeNewOpts.json); - } - - } - - // add the node - that.graph.add(newNode); - newNode.pos = [ opts.position[0]+opts.posAdd[0]+(opts.posSizeFix[0]?opts.posSizeFix[0]*newNode.size[0]:0) - ,opts.position[1]+opts.posAdd[1]+(opts.posSizeFix[1]?opts.posSizeFix[1]*newNode.size[1]:0)]; //that.last_click_position; //[e.canvasX+30, e.canvasX+5];*/ - - //that.graph.afterChange(); - - // connect the two! - if (isFrom){ - opts.nodeFrom.connectByType( iSlotConn, newNode, fromSlotType ); - }else{ - opts.nodeTo.connectByTypeOutput( iSlotConn, newNode, fromSlotType ); - } - - // if connecting in between - if (isFrom && isTo){ - // TODO - } - - return true; - - }else{ - console.log("failed creating "+nodeNewType); - } - } - } - return false; - } - - LGraphCanvas.prototype.showConnectionMenu = function(optPass) { // addNodeMenu for connection - var optPass = optPass || {}; - var opts = Object.assign({ nodeFrom: null // input - ,slotFrom: null // input - ,nodeTo: null // output - ,slotTo: null // output - ,e: null - } - ,optPass - ); - var that = this; - - var isFrom = opts.nodeFrom && opts.slotFrom; - var isTo = !isFrom && opts.nodeTo && opts.slotTo; - - if (!isFrom && !isTo){ - console.warn("No data passed to showConnectionMenu"); - return false; - } - - var nodeX = isFrom ? opts.nodeFrom : opts.nodeTo; - var slotX = isFrom ? opts.slotFrom : opts.slotTo; - - var iSlotConn = false; - switch (typeof slotX){ - case "string": - iSlotConn = isFrom ? nodeX.findOutputSlot(slotX,false) : nodeX.findInputSlot(slotX,false); - slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX]; - break; - case "object": - // ok slotX - iSlotConn = isFrom ? nodeX.findOutputSlot(slotX.name) : nodeX.findInputSlot(slotX.name); - break; - case "number": - iSlotConn = slotX; - slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX]; - break; - default: - // bad ? - //iSlotConn = 0; - console.warn("Cant get slot information "+slotX); - return false; - } - - var options = ["Add Node",null]; - - if (that.allow_searchbox){ - options.push("Search"); - options.push(null); - } - - // get defaults nodes for this slottype - var fromSlotType = slotX.type==LiteGraph.EVENT?"_event_":slotX.type; - var slotTypesDefault = isFrom ? LiteGraph.slot_types_default_out : LiteGraph.slot_types_default_in; - if(slotTypesDefault && slotTypesDefault[fromSlotType]){ - if(typeof slotTypesDefault[fromSlotType] == "object" || typeof slotTypesDefault[fromSlotType] == "array"){ - for(var typeX in slotTypesDefault[fromSlotType]){ - options.push(slotTypesDefault[fromSlotType][typeX]); - } - }else{ - options.push(slotTypesDefault[fromSlotType]); - } - } - - // build menu - var menu = new LiteGraph.ContextMenu(options, { - event: opts.e, - title: (slotX && slotX.name!="" ? (slotX.name + (fromSlotType?" | ":"")) : "")+(slotX && fromSlotType ? fromSlotType : ""), - callback: inner_clicked - }); - - // callback - function inner_clicked(v,options,e) { - //console.log("Process showConnectionMenu selection"); - switch (v) { - case "Add Node": - LGraphCanvas.onMenuAdd(null, null, e, menu, function(node){ - if (isFrom){ - opts.nodeFrom.connectByType( iSlotConn, node, fromSlotType ); - }else{ - opts.nodeTo.connectByTypeOutput( iSlotConn, node, fromSlotType ); - } - }); - break; - case "Search": - if(isFrom){ - that.showSearchBox(e,{node_from: opts.nodeFrom, slot_from: slotX, type_filter_in: fromSlotType}); - }else{ - that.showSearchBox(e,{node_to: opts.nodeTo, slot_from: slotX, type_filter_out: fromSlotType}); - } - break; - default: - // check for defaults nodes for this slottype - var nodeCreated = that.createDefaultNodeForSlot(Object.assign(opts,{ position: [opts.e.canvasX, opts.e.canvasY] - ,nodeType: v - })); - if (nodeCreated){ - // new node created - //console.log("node "+v+" created") - }else{ - // failed or v is not in defaults - } - break; - } - } - - return false; - }; - - // TODO refactor :: this is used fot title but not for properties! - LGraphCanvas.onShowPropertyEditor = function(item, options, e, menu, node) { - var input_html = ""; - var property = item.property || "title"; - var value = node[property]; - - // TODO refactor :: use createDialog ? - - var dialog = document.createElement("div"); - dialog.is_modified = false; - dialog.className = "graphdialog"; - dialog.innerHTML = - ""; - dialog.close = function() { - if (dialog.parentNode) { - dialog.parentNode.removeChild(dialog); - } - }; - var title = dialog.querySelector(".name"); - title.innerText = property; - var input = dialog.querySelector(".value"); - if (input) { - input.value = value; - input.addEventListener("blur", function(e) { - this.focus(); - }); - input.addEventListener("keydown", function(e) { - dialog.is_modified = true; - if (e.keyCode == 27) { - //ESC - dialog.close(); - } else if (e.keyCode == 13) { - inner(); // save - } else if (e.keyCode != 13 && e.target.localName != "textarea") { - return; - } - e.preventDefault(); - e.stopPropagation(); - }); - } - - var graphcanvas = LGraphCanvas.active_canvas; - var canvas = graphcanvas.canvas; - - var rect = canvas.getBoundingClientRect(); - var offsetx = -20; - var offsety = -20; - if (rect) { - offsetx -= rect.left; - offsety -= rect.top; - } - - if (event) { - dialog.style.left = event.clientX + offsetx + "px"; - dialog.style.top = event.clientY + offsety + "px"; - } else { - dialog.style.left = canvas.width * 0.5 + offsetx + "px"; - dialog.style.top = canvas.height * 0.5 + offsety + "px"; - } - - var button = dialog.querySelector("button"); - button.addEventListener("click", inner); - canvas.parentNode.appendChild(dialog); - - if(input) input.focus(); - - var dialogCloseTimer = null; - dialog.addEventListener("mouseleave", function(e) { - if(LiteGraph.dialog_close_on_mouse_leave) - if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave) - dialogCloseTimer = setTimeout(dialog.close, LiteGraph.dialog_close_on_mouse_leave_delay); //dialog.close(); - }); - dialog.addEventListener("mouseenter", function(e) { - if(LiteGraph.dialog_close_on_mouse_leave) - if(dialogCloseTimer) clearTimeout(dialogCloseTimer); - }); - - function inner() { - if(input) setValue(input.value); - } - - function setValue(value) { - if (item.type == "Number") { - value = Number(value); - } else if (item.type == "Boolean") { - value = Boolean(value); - } - node[property] = value; - if (dialog.parentNode) { - dialog.parentNode.removeChild(dialog); - } - node.setDirtyCanvas(true, true); - } - }; - - // refactor: there are different dialogs, some uses createDialog some dont - LGraphCanvas.prototype.prompt = function(title, value, callback, event, multiline) { - var that = this; - var input_html = ""; - title = title || ""; - - var dialog = document.createElement("div"); - dialog.is_modified = false; - dialog.className = "graphdialog rounded"; - if(multiline) - dialog.innerHTML = " "; - else - dialog.innerHTML = " "; - dialog.close = function() { - that.prompt_box = null; - if (dialog.parentNode) { - dialog.parentNode.removeChild(dialog); - } - }; - - var graphcanvas = LGraphCanvas.active_canvas; - var canvas = graphcanvas.canvas; - canvas.parentNode.appendChild(dialog); - - if (this.ds.scale > 1) { - dialog.style.transform = "scale(" + this.ds.scale + ")"; - } - - var dialogCloseTimer = null; - var prevent_timeout = false; - LiteGraph.pointerListenerAdd(dialog,"leave", function(e) { - if (prevent_timeout) - return; - if(LiteGraph.dialog_close_on_mouse_leave) - if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave) - dialogCloseTimer = setTimeout(dialog.close, LiteGraph.dialog_close_on_mouse_leave_delay); //dialog.close(); - }); - LiteGraph.pointerListenerAdd(dialog,"enter", function(e) { - if(LiteGraph.dialog_close_on_mouse_leave) - if(dialogCloseTimer) clearTimeout(dialogCloseTimer); - }); - var selInDia = dialog.querySelectorAll("select"); - if (selInDia){ - // if filtering, check focus changed to comboboxes and prevent closing - selInDia.forEach(function(selIn) { - selIn.addEventListener("click", function(e) { - prevent_timeout++; - }); - selIn.addEventListener("blur", function(e) { - prevent_timeout = 0; - }); - selIn.addEventListener("change", function(e) { - prevent_timeout = -1; - }); - }); - } - - if (that.prompt_box) { - that.prompt_box.close(); - } - that.prompt_box = dialog; - - var first = null; - var timeout = null; - var selected = null; - - var name_element = dialog.querySelector(".name"); - name_element.innerText = title; - var value_element = dialog.querySelector(".value"); - value_element.value = value; - value_element.select(); - - var input = value_element; - input.addEventListener("keydown", function(e) { - dialog.is_modified = true; - if (e.keyCode == 27) { - //ESC - dialog.close(); - } else if (e.keyCode == 13 && e.target.localName != "textarea") { - if (callback) { - callback(this.value); - } - dialog.close(); - } else { - return; - } - e.preventDefault(); - e.stopPropagation(); - }); - - var button = dialog.querySelector("button"); - button.addEventListener("click", function(e) { - if (callback) { - callback(input.value); - } - that.setDirty(true); - dialog.close(); - }); - - var rect = canvas.getBoundingClientRect(); - var offsetx = -20; - var offsety = -20; - if (rect) { - offsetx -= rect.left; - offsety -= rect.top; - } - - if (event) { - dialog.style.left = event.clientX + offsetx + "px"; - dialog.style.top = event.clientY + offsety + "px"; - } else { - dialog.style.left = canvas.width * 0.5 + offsetx + "px"; - dialog.style.top = canvas.height * 0.5 + offsety + "px"; - } - - setTimeout(function() { - input.focus(); - }, 10); - - return dialog; - }; - - LGraphCanvas.search_limit = -1; - LGraphCanvas.prototype.showSearchBox = function(event, options) { - // proposed defaults - var def_options = { slot_from: null - ,node_from: null - ,node_to: null - ,do_type_filter: LiteGraph.search_filter_enabled // TODO check for registered_slot_[in/out]_types not empty // this will be checked for functionality enabled : filter on slot type, in and out - ,type_filter_in: false // these are default: pass to set initially set values - ,type_filter_out: false - ,show_general_if_none_on_typefilter: true - ,show_general_after_typefiltered: true - ,hide_on_mouse_leave: LiteGraph.search_hide_on_mouse_leave - ,show_all_if_empty: true - ,show_all_on_open: LiteGraph.search_show_all_on_open - }; - options = Object.assign(def_options, options || {}); - - //console.log(options); - - var that = this; - var input_html = ""; - var graphcanvas = LGraphCanvas.active_canvas; - var canvas = graphcanvas.canvas; - var root_document = canvas.ownerDocument || document; - - var dialog = document.createElement("div"); - dialog.className = "litegraph litesearchbox graphdialog rounded"; - dialog.innerHTML = "Search "; - if (options.do_type_filter){ - dialog.innerHTML += ""; - dialog.innerHTML += ""; - } - dialog.innerHTML += "
"; - - if( root_document.fullscreenElement ) - root_document.fullscreenElement.appendChild(dialog); - else - { - root_document.body.appendChild(dialog); - root_document.body.style.overflow = "hidden"; - } - // dialog element has been appended - - if (options.do_type_filter){ - var selIn = dialog.querySelector(".slot_in_type_filter"); - var selOut = dialog.querySelector(".slot_out_type_filter"); - } - - dialog.close = function() { - that.search_box = null; - this.blur(); - canvas.focus(); - root_document.body.style.overflow = ""; - - setTimeout(function() { - that.canvas.focus(); - }, 20); //important, if canvas loses focus keys wont be captured - if (dialog.parentNode) { - dialog.parentNode.removeChild(dialog); - } - }; - - if (this.ds.scale > 1) { - dialog.style.transform = "scale(" + this.ds.scale + ")"; - } - - // hide on mouse leave - if(options.hide_on_mouse_leave){ - var prevent_timeout = false; - var timeout_close = null; - LiteGraph.pointerListenerAdd(dialog,"enter", function(e) { - if (timeout_close) { - clearTimeout(timeout_close); - timeout_close = null; - } - }); - LiteGraph.pointerListenerAdd(dialog,"leave", function(e) { - if (prevent_timeout){ - return; - } - timeout_close = setTimeout(function() { - dialog.close(); - }, typeof options.hide_on_mouse_leave === "number" ? options.hide_on_mouse_leave : 500); - }); - // if filtering, check focus changed to comboboxes and prevent closing - if (options.do_type_filter){ - selIn.addEventListener("click", function(e) { - prevent_timeout++; - }); - selIn.addEventListener("blur", function(e) { - prevent_timeout = 0; - }); - selIn.addEventListener("change", function(e) { - prevent_timeout = -1; - }); - selOut.addEventListener("click", function(e) { - prevent_timeout++; - }); - selOut.addEventListener("blur", function(e) { - prevent_timeout = 0; - }); - selOut.addEventListener("change", function(e) { - prevent_timeout = -1; - }); - } - } - - if (that.search_box) { - that.search_box.close(); - } - that.search_box = dialog; - - var helper = dialog.querySelector(".helper"); - - var first = null; - var timeout = null; - var selected = null; - - var input = dialog.querySelector("input"); - if (input) { - input.addEventListener("blur", function(e) { - this.focus(); - }); - input.addEventListener("keydown", function(e) { - if (e.keyCode == 38) { - //UP - changeSelection(false); - } else if (e.keyCode == 40) { - //DOWN - changeSelection(true); - } else if (e.keyCode == 27) { - //ESC - dialog.close(); - } else if (e.keyCode == 13) { - if (selected) { - select(unescape(selected.dataset["type"])); - } else if (first) { - select(first); - } else { - dialog.close(); - } - } else { - if (timeout) { - clearInterval(timeout); - } - timeout = setTimeout(refreshHelper, 10); - return; - } - e.preventDefault(); - e.stopPropagation(); - e.stopImmediatePropagation(); - return true; - }); - } - - // if should filter on type, load and fill selected and choose elements if passed - if (options.do_type_filter){ - if (selIn){ - var aSlots = LiteGraph.slot_types_in; - var nSlots = aSlots.length; // this for object :: Object.keys(aSlots).length; - - if (options.type_filter_in == LiteGraph.EVENT || options.type_filter_in == LiteGraph.ACTION) - options.type_filter_in = "_event_"; - /* this will filter on * .. but better do it manually in case - else if(options.type_filter_in === "" || options.type_filter_in === 0) - options.type_filter_in = "*";*/ - - for (var iK=0; iK (rect.height - 200)) - helper.style.maxHeight = (rect.height - event.layerY - 20) + "px"; - - /* - var offsetx = -20; - var offsety = -20; - if (rect) { - offsetx -= rect.left; - offsety -= rect.top; - } - - if (event) { - dialog.style.left = event.clientX + offsetx + "px"; - dialog.style.top = event.clientY + offsety + "px"; - } else { - dialog.style.left = canvas.width * 0.5 + offsetx + "px"; - dialog.style.top = canvas.height * 0.5 + offsety + "px"; - } - canvas.parentNode.appendChild(dialog); - */ - - input.focus(); - if (options.show_all_on_open) refreshHelper(); - - function select(name) { - if (name) { - if (that.onSearchBoxSelection) { - that.onSearchBoxSelection(name, event, graphcanvas); - } else { - var extra = LiteGraph.searchbox_extras[name.toLowerCase()]; - if (extra) { - name = extra.type; - } - - graphcanvas.graph.beforeChange(); - var node = LiteGraph.createNode(name); - if (node) { - node.pos = graphcanvas.convertEventToCanvasOffset( - event - ); - graphcanvas.graph.add(node, false); - } - - if (extra && extra.data) { - if (extra.data.properties) { - for (var i in extra.data.properties) { - node.addProperty( i, extra.data.properties[i] ); - } - } - if (extra.data.inputs) { - node.inputs = []; - for (var i in extra.data.inputs) { - node.addOutput( - extra.data.inputs[i][0], - extra.data.inputs[i][1] - ); - } - } - if (extra.data.outputs) { - node.outputs = []; - for (var i in extra.data.outputs) { - node.addOutput( - extra.data.outputs[i][0], - extra.data.outputs[i][1] - ); - } - } - if (extra.data.title) { - node.title = extra.data.title; - } - if (extra.data.json) { - node.configure(extra.data.json); - } - - } - - // join node after inserting - if (options.node_from){ - var iS = false; - switch (typeof options.slot_from){ - case "string": - iS = options.node_from.findOutputSlot(options.slot_from); - break; - case "object": - if (options.slot_from.name){ - iS = options.node_from.findOutputSlot(options.slot_from.name); - }else{ - iS = -1; - } - if (iS==-1 && typeof options.slot_from.slot_index !== "undefined") iS = options.slot_from.slot_index; - break; - case "number": - iS = options.slot_from; - break; - default: - iS = 0; // try with first if no name set - } - if (typeof options.node_from.outputs[iS] !== "undefined"){ - if (iS!==false && iS>-1){ - options.node_from.connectByType( iS, node, options.node_from.outputs[iS].type ); - } - }else{ - // console.warn("cant find slot " + options.slot_from); - } - } - if (options.node_to){ - var iS = false; - switch (typeof options.slot_from){ - case "string": - iS = options.node_to.findInputSlot(options.slot_from); - break; - case "object": - if (options.slot_from.name){ - iS = options.node_to.findInputSlot(options.slot_from.name); - }else{ - iS = -1; - } - if (iS==-1 && typeof options.slot_from.slot_index !== "undefined") iS = options.slot_from.slot_index; - break; - case "number": - iS = options.slot_from; - break; - default: - iS = 0; // try with first if no name set - } - if (typeof options.node_to.inputs[iS] !== "undefined"){ - if (iS!==false && iS>-1){ - // try connection - options.node_to.connectByTypeOutput(iS,node,options.node_to.inputs[iS].type); - } - }else{ - // console.warn("cant find slot_nodeTO " + options.slot_from); - } - } - - graphcanvas.graph.afterChange(); - } - } - - dialog.close(); - } - - function changeSelection(forward) { - var prev = selected; - if (selected) { - selected.classList.remove("selected"); - } - if (!selected) { - selected = forward - ? helper.childNodes[0] - : helper.childNodes[helper.childNodes.length]; - } else { - selected = forward - ? selected.nextSibling - : selected.previousSibling; - if (!selected) { - selected = prev; - } - } - if (!selected) { - return; - } - selected.classList.add("selected"); - selected.scrollIntoView({block: "end", behavior: "smooth"}); - } - - function refreshHelper() { - timeout = null; - var str = input.value; - first = null; - helper.innerHTML = ""; - if (!str && !options.show_all_if_empty) { - return; - } - - if (that.onSearchBox) { - var list = that.onSearchBox(helper, str, graphcanvas); - if (list) { - for (var i = 0; i < list.length; ++i) { - addResult(list[i]); - } - } - } else { - var c = 0; - str = str.toLowerCase(); - var filter = graphcanvas.filter || graphcanvas.graph.filter; - - // filter by type preprocess - if(options.do_type_filter && that.search_box){ - var sIn = that.search_box.querySelector(".slot_in_type_filter"); - var sOut = that.search_box.querySelector(".slot_out_type_filter"); - }else{ - var sIn = false; - var sOut = false; - } - - //extras - for (var i in LiteGraph.searchbox_extras) { - var extra = LiteGraph.searchbox_extras[i]; - if ((!options.show_all_if_empty || str) && extra.desc.toLowerCase().indexOf(str) === -1) { - continue; - } - var ctor = LiteGraph.registered_node_types[ extra.type ]; - if( ctor && ctor.filter != filter ) - continue; - if( ! inner_test_filter(extra.type) ) - continue; - addResult( extra.desc, "searchbox_extra" ); - if ( LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit ) { - break; - } - } - - var filtered = null; - if (Array.prototype.filter) { //filter supported - var keys = Object.keys( LiteGraph.registered_node_types ); //types - var filtered = keys.filter( inner_test_filter ); - } else { - filtered = []; - for (var i in LiteGraph.registered_node_types) { - if( inner_test_filter(i) ) - filtered.push(i); - } - } - - for (var i = 0; i < filtered.length; i++) { - addResult(filtered[i]); - if ( LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit ) { - break; - } - } - - // add general type if filtering - if (options.show_general_after_typefiltered - && (sIn.value || sOut.value) - ){ - filtered_extra = []; - for (var i in LiteGraph.registered_node_types) { - if( inner_test_filter(i, {inTypeOverride: sIn&&sIn.value?"*":false, outTypeOverride: sOut&&sOut.value?"*":false}) ) - filtered_extra.push(i); - } - for (var i = 0; i < filtered_extra.length; i++) { - addResult(filtered_extra[i], "generic_type"); - if ( LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit ) { - break; - } - } - } - - // check il filtering gave no results - if ((sIn.value || sOut.value) && - ( (helper.childNodes.length == 0 && options.show_general_if_none_on_typefilter) ) - ){ - filtered_extra = []; - for (var i in LiteGraph.registered_node_types) { - if( inner_test_filter(i, {skipFilter: true}) ) - filtered_extra.push(i); - } - for (var i = 0; i < filtered_extra.length; i++) { - addResult(filtered_extra[i], "not_in_filter"); - if ( LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit ) { - break; - } - } - } - - function inner_test_filter( type, optsIn ) - { - var optsIn = optsIn || {}; - var optsDef = { skipFilter: false - ,inTypeOverride: false - ,outTypeOverride: false - }; - var opts = Object.assign(optsDef,optsIn); - var ctor = LiteGraph.registered_node_types[ type ]; - if(filter && ctor.filter != filter ) - return false; - if ((!options.show_all_if_empty || str) && type.toLowerCase().indexOf(str) === -1 && (!ctor.title || ctor.title.toLowerCase().indexOf(str) === -1)) - return false; - - // filter by slot IN, OUT types - if(options.do_type_filter && !opts.skipFilter){ - var sType = type; - - var sV = sIn.value; - if (opts.inTypeOverride!==false) sV = opts.inTypeOverride; - //if (sV.toLowerCase() == "_event_") sV = LiteGraph.EVENT; // -1 - - if(sIn && sV){ - //console.log("will check filter against "+sV); - if (LiteGraph.registered_slot_in_types[sV] && LiteGraph.registered_slot_in_types[sV].nodes){ // type is stored - //console.debug("check "+sType+" in "+LiteGraph.registered_slot_in_types[sV].nodes); - var doesInc = LiteGraph.registered_slot_in_types[sV].nodes.includes(sType); - if (doesInc!==false){ - //console.log(sType+" HAS "+sV); - }else{ - /*console.debug(LiteGraph.registered_slot_in_types[sV]); - console.log(+" DONT includes "+type);*/ - return false; - } - } - } - - var sV = sOut.value; - if (opts.outTypeOverride!==false) sV = opts.outTypeOverride; - //if (sV.toLowerCase() == "_event_") sV = LiteGraph.EVENT; // -1 - - if(sOut && sV){ - //console.log("search will check filter against "+sV); - if (LiteGraph.registered_slot_out_types[sV] && LiteGraph.registered_slot_out_types[sV].nodes){ // type is stored - //console.debug("check "+sType+" in "+LiteGraph.registered_slot_out_types[sV].nodes); - var doesInc = LiteGraph.registered_slot_out_types[sV].nodes.includes(sType); - if (doesInc!==false){ - //console.log(sType+" HAS "+sV); - }else{ - /*console.debug(LiteGraph.registered_slot_out_types[sV]); - console.log(+" DONT includes "+type);*/ - return false; - } - } - } - } - return true; - } - } - - function addResult(type, className) { - var help = document.createElement("div"); - if (!first) { - first = type; - } - - const nodeType = LiteGraph.registered_node_types[type]; - if (nodeType?.title) { - help.innerText = nodeType?.title; - const typeEl = document.createElement("span"); - typeEl.className = "litegraph lite-search-item-type"; - typeEl.textContent = type; - help.append(typeEl); - } else { - help.innerText = type; - } - - help.dataset["type"] = escape(type); - help.className = "litegraph lite-search-item"; - if (className) { - help.className += " " + className; - } - help.addEventListener("click", function(e) { - select(unescape(this.dataset["type"])); - }); - helper.appendChild(help); - } - } - - return dialog; - }; - - LGraphCanvas.prototype.showEditPropertyValue = function( node, property, options ) { - if (!node || node.properties[property] === undefined) { - return; - } - - options = options || {}; - var that = this; - - var info = node.getPropertyInfo(property); - var type = info.type; - - var input_html = ""; - - if (type == "string" || type == "number" || type == "array" || type == "object") { - input_html = ""; - } else if ( (type == "enum" || type == "combo") && info.values) { - input_html = ""; - } else if (type == "boolean" || type == "toggle") { - input_html = - ""; - } else { - console.warn("unknown type: " + type); - return; - } - - var dialog = this.createDialog( - "" + - (info.label ? info.label : property) + - "" + - input_html + - "", - options - ); - - var input = false; - if ((type == "enum" || type == "combo") && info.values) { - input = dialog.querySelector("select"); - input.addEventListener("change", function(e) { - dialog.modified(); - setValue(e.target.value); - //var index = e.target.value; - //setValue( e.options[e.selectedIndex].value ); - }); - } else if (type == "boolean" || type == "toggle") { - input = dialog.querySelector("input"); - if (input) { - input.addEventListener("click", function(e) { - dialog.modified(); - setValue(!!input.checked); - }); - } - } else { - input = dialog.querySelector("input"); - if (input) { - input.addEventListener("blur", function(e) { - this.focus(); - }); - - var v = node.properties[property] !== undefined ? node.properties[property] : ""; - if (type !== 'string') { - v = JSON.stringify(v); - } - - input.value = v; - input.addEventListener("keydown", function(e) { - if (e.keyCode == 27) { - //ESC - dialog.close(); - } else if (e.keyCode == 13) { - // ENTER - inner(); // save - } else if (e.keyCode != 13) { - dialog.modified(); - return; - } - e.preventDefault(); - e.stopPropagation(); - }); - } - } - if (input) input.focus(); - - var button = dialog.querySelector("button"); - button.addEventListener("click", inner); - - function inner() { - setValue(input.value); - } - - function setValue(value) { - - if(info && info.values && info.values.constructor === Object && info.values[value] != undefined ) - value = info.values[value]; - - if (typeof node.properties[property] == "number") { - value = Number(value); - } - if (type == "array" || type == "object") { - value = JSON.parse(value); - } - node.properties[property] = value; - if (node.graph) { - node.graph._version++; - } - if (node.onPropertyChanged) { - node.onPropertyChanged(property, value); - } - if(options.onclose) - options.onclose(); - dialog.close(); - node.setDirtyCanvas(true, true); - } - - return dialog; - }; - - // TODO refactor, theer are different dialog, some uses createDialog, some dont - LGraphCanvas.prototype.createDialog = function(html, options) { - var def_options = { checkForInput: false, closeOnLeave: true, closeOnLeave_checkModified: true }; - options = Object.assign(def_options, options || {}); - - var dialog = document.createElement("div"); - dialog.className = "graphdialog"; - dialog.innerHTML = html; - dialog.is_modified = false; - - var rect = this.canvas.getBoundingClientRect(); - var offsetx = -20; - var offsety = -20; - if (rect) { - offsetx -= rect.left; - offsety -= rect.top; - } - - if (options.position) { - offsetx += options.position[0]; - offsety += options.position[1]; - } else if (options.event) { - offsetx += options.event.clientX; - offsety += options.event.clientY; - } //centered - else { - offsetx += this.canvas.width * 0.5; - offsety += this.canvas.height * 0.5; - } - - dialog.style.left = offsetx + "px"; - dialog.style.top = offsety + "px"; - - this.canvas.parentNode.appendChild(dialog); - - // acheck for input and use default behaviour: save on enter, close on esc - if (options.checkForInput){ - var aI = []; - var focused = false; - if (aI = dialog.querySelectorAll("input")){ - aI.forEach(function(iX) { - iX.addEventListener("keydown",function(e){ - dialog.modified(); - if (e.keyCode == 27) { - dialog.close(); - } else if (e.keyCode != 13) { - return; - } - // set value ? - e.preventDefault(); - e.stopPropagation(); - }); - if (!focused) iX.focus(); - }); - } - } - - dialog.modified = function(){ - dialog.is_modified = true; - } - dialog.close = function() { - if (dialog.parentNode) { - dialog.parentNode.removeChild(dialog); - } - }; - - var dialogCloseTimer = null; - var prevent_timeout = false; - dialog.addEventListener("mouseleave", function(e) { - if (prevent_timeout) - return; - if(options.closeOnLeave || LiteGraph.dialog_close_on_mouse_leave) - if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave) - dialogCloseTimer = setTimeout(dialog.close, LiteGraph.dialog_close_on_mouse_leave_delay); //dialog.close(); - }); - dialog.addEventListener("mouseenter", function(e) { - if(options.closeOnLeave || LiteGraph.dialog_close_on_mouse_leave) - if(dialogCloseTimer) clearTimeout(dialogCloseTimer); - }); - var selInDia = dialog.querySelectorAll("select"); - if (selInDia){ - // if filtering, check focus changed to comboboxes and prevent closing - selInDia.forEach(function(selIn) { - selIn.addEventListener("click", function(e) { - prevent_timeout++; - }); - selIn.addEventListener("blur", function(e) { - prevent_timeout = 0; - }); - selIn.addEventListener("change", function(e) { - prevent_timeout = -1; - }); - }); - } - - return dialog; - }; - - LGraphCanvas.prototype.createPanel = function(title, options) { - options = options || {}; - - var ref_window = options.window || window; - var root = document.createElement("div"); - root.className = "litegraph dialog"; - root.innerHTML = "
"; - root.header = root.querySelector(".dialog-header"); - - if(options.width) - root.style.width = options.width + (options.width.constructor === Number ? "px" : ""); - if(options.height) - root.style.height = options.height + (options.height.constructor === Number ? "px" : ""); - if(options.closable) - { - var close = document.createElement("span"); - close.innerHTML = "✕"; - close.classList.add("close"); - close.addEventListener("click",function(){ - root.close(); - }); - root.header.appendChild(close); - } - root.title_element = root.querySelector(".dialog-title"); - root.title_element.innerText = title; - root.content = root.querySelector(".dialog-content"); - root.alt_content = root.querySelector(".dialog-alt-content"); - root.footer = root.querySelector(".dialog-footer"); - - root.close = function() - { - if (root.onClose && typeof root.onClose == "function"){ - root.onClose(); - } - if(root.parentNode) - root.parentNode.removeChild(root); - /* XXX CHECK THIS */ - if(this.parentNode){ - this.parentNode.removeChild(this); - } - /* XXX this was not working, was fixed with an IF, check this */ - } - - // function to swap panel content - root.toggleAltContent = function(force){ - if (typeof force != "undefined"){ - var vTo = force ? "block" : "none"; - var vAlt = force ? "none" : "block"; - }else{ - var vTo = root.alt_content.style.display != "block" ? "block" : "none"; - var vAlt = root.alt_content.style.display != "block" ? "none" : "block"; - } - root.alt_content.style.display = vTo; - root.content.style.display = vAlt; - } - - root.toggleFooterVisibility = function(force){ - if (typeof force != "undefined"){ - var vTo = force ? "block" : "none"; - }else{ - var vTo = root.footer.style.display != "block" ? "block" : "none"; - } - root.footer.style.display = vTo; - } - - root.clear = function() - { - this.content.innerHTML = ""; - } - - root.addHTML = function(code, classname, on_footer) - { - var elem = document.createElement("div"); - if(classname) - elem.className = classname; - elem.innerHTML = code; - if(on_footer) - root.footer.appendChild(elem); - else - root.content.appendChild(elem); - return elem; - } - - root.addButton = function( name, callback, options ) - { - var elem = document.createElement("button"); - elem.innerText = name; - elem.options = options; - elem.classList.add("btn"); - elem.addEventListener("click",callback); - root.footer.appendChild(elem); - return elem; - } - - root.addSeparator = function() - { - var elem = document.createElement("div"); - elem.className = "separator"; - root.content.appendChild(elem); - } - - root.addWidget = function( type, name, value, options, callback ) - { - options = options || {}; - var str_value = String(value); - type = type.toLowerCase(); - if(type == "number") - str_value = value.toFixed(3); - - var elem = document.createElement("div"); - elem.className = "property"; - elem.innerHTML = ""; - elem.querySelector(".property_name").innerText = options.label || name; - var value_element = elem.querySelector(".property_value"); - value_element.innerText = str_value; - elem.dataset["property"] = name; - elem.dataset["type"] = options.type || type; - elem.options = options; - elem.value = value; - - if( type == "code" ) - elem.addEventListener("click", function(e){ root.inner_showCodePad( this.dataset["property"] ); }); - else if (type == "boolean") - { - elem.classList.add("boolean"); - if(value) - elem.classList.add("bool-on"); - elem.addEventListener("click", function(){ - //var v = node.properties[this.dataset["property"]]; - //node.setProperty(this.dataset["property"],!v); this.innerText = v ? "true" : "false"; - var propname = this.dataset["property"]; - this.value = !this.value; - this.classList.toggle("bool-on"); - this.querySelector(".property_value").innerText = this.value ? "true" : "false"; - innerChange(propname, this.value ); - }); - } - else if (type == "string" || type == "number") - { - value_element.setAttribute("contenteditable",true); - value_element.addEventListener("keydown", function(e){ - if(e.code == "Enter" && (type != "string" || !e.shiftKey)) // allow for multiline - { - e.preventDefault(); - this.blur(); - } - }); - value_element.addEventListener("blur", function(){ - var v = this.innerText; - var propname = this.parentNode.dataset["property"]; - var proptype = this.parentNode.dataset["type"]; - if( proptype == "number") - v = Number(v); - innerChange(propname, v); - }); - } - else if (type == "enum" || type == "combo") { - var str_value = LGraphCanvas.getPropertyPrintableValue( value, options.values ); - value_element.innerText = str_value; - - value_element.addEventListener("click", function(event){ - var values = options.values || []; - var propname = this.parentNode.dataset["property"]; - var elem_that = this; - var menu = new LiteGraph.ContextMenu(values,{ - event: event, - className: "dark", - callback: inner_clicked - }, - ref_window); - function inner_clicked(v, option, event) { - //node.setProperty(propname,v); - //graphcanvas.dirty_canvas = true; - elem_that.innerText = v; - innerChange(propname,v); - return false; - } - }); - } - - root.content.appendChild(elem); - - function innerChange(name, value) - { - //console.log("change",name,value); - //that.dirty_canvas = true; - if(options.callback) - options.callback(name,value,options); - if(callback) - callback(name,value,options); - } - - return elem; - } - - if (root.onOpen && typeof root.onOpen == "function") root.onOpen(); - - return root; - }; - - LGraphCanvas.getPropertyPrintableValue = function(value, values) - { - if(!values) - return String(value); - - if(values.constructor === Array) - { - return String(value); - } - - if(values.constructor === Object) - { - var desc_value = ""; - for(var k in values) - { - if(values[k] != value) - continue; - desc_value = k; - break; - } - return String(value) + " ("+desc_value+")"; - } - } - - LGraphCanvas.prototype.closePanels = function(){ - var panel = document.querySelector("#node-panel"); - if(panel) - panel.close(); - var panel = document.querySelector("#option-panel"); - if(panel) - panel.close(); - } - - LGraphCanvas.prototype.showShowGraphOptionsPanel = function(refOpts, obEv, refMenu, refMenu2){ - if(this.constructor && this.constructor.name == "HTMLDivElement"){ - // assume coming from the menu event click - if (!obEv || !obEv.event || !obEv.event.target || !obEv.event.target.lgraphcanvas){ - console.warn("Canvas not found"); // need a ref to canvas obj - /*console.debug(event); - console.debug(event.target);*/ - return; - } - var graphcanvas = obEv.event.target.lgraphcanvas; - }else{ - // assume called internally - var graphcanvas = this; - } - graphcanvas.closePanels(); - var ref_window = graphcanvas.getCanvasWindow(); - panel = graphcanvas.createPanel("Options",{ - closable: true - ,window: ref_window - ,onOpen: function(){ - graphcanvas.OPTIONPANEL_IS_OPEN = true; - } - ,onClose: function(){ - graphcanvas.OPTIONPANEL_IS_OPEN = false; - graphcanvas.options_panel = null; - } - }); - graphcanvas.options_panel = panel; - panel.id = "option-panel"; - panel.classList.add("settings"); - - function inner_refresh(){ - - panel.content.innerHTML = ""; //clear - - var fUpdate = function(name, value, options){ - switch(name){ - /*case "Render mode": - // Case "".. - if (options.values && options.key){ - var kV = Object.values(options.values).indexOf(value); - if (kV>=0 && options.values[kV]){ - console.debug("update graph options: "+options.key+": "+kV); - graphcanvas[options.key] = kV; - //console.debug(graphcanvas); - break; - } - } - console.warn("unexpected options"); - console.debug(options); - break;*/ - default: - //console.debug("want to update graph options: "+name+": "+value); - if (options && options.key){ - name = options.key; - } - if (options.values){ - value = Object.values(options.values).indexOf(value); - } - //console.debug("update graph option: "+name+": "+value); - graphcanvas[name] = value; - break; - } - }; - - // panel.addWidget( "string", "Graph name", "", {}, fUpdate); // implement - - var aProps = LiteGraph.availableCanvasOptions; - aProps.sort(); - for(var pI in aProps){ - var pX = aProps[pI]; - panel.addWidget( "boolean", pX, graphcanvas[pX], {key: pX, on: "True", off: "False"}, fUpdate); - } - - var aLinks = [ graphcanvas.links_render_mode ]; - panel.addWidget( "combo", "Render mode", LiteGraph.LINK_RENDER_MODES[graphcanvas.links_render_mode], {key: "links_render_mode", values: LiteGraph.LINK_RENDER_MODES}, fUpdate); - - panel.addSeparator(); - - panel.footer.innerHTML = ""; // clear - - } - inner_refresh(); - - graphcanvas.canvas.parentNode.appendChild( panel ); - } - - LGraphCanvas.prototype.showShowNodePanel = function( node ) - { - this.SELECTED_NODE = node; - this.closePanels(); - var ref_window = this.getCanvasWindow(); - var that = this; - var graphcanvas = this; - var panel = this.createPanel(node.title || "",{ - closable: true - ,window: ref_window - ,onOpen: function(){ - graphcanvas.NODEPANEL_IS_OPEN = true; - } - ,onClose: function(){ - graphcanvas.NODEPANEL_IS_OPEN = false; - graphcanvas.node_panel = null; - } - }); - graphcanvas.node_panel = panel; - panel.id = "node-panel"; - panel.node = node; - panel.classList.add("settings"); - - function inner_refresh() - { - panel.content.innerHTML = ""; //clear - panel.addHTML(""+node.type+""+(node.constructor.desc || "")+""); - - panel.addHTML("

Properties

"); - - var fUpdate = function(name,value){ - graphcanvas.graph.beforeChange(node); - switch(name){ - case "Title": - node.title = value; - break; - case "Mode": - var kV = Object.values(LiteGraph.NODE_MODES).indexOf(value); - if (kV>=0 && LiteGraph.NODE_MODES[kV]){ - node.changeMode(kV); - }else{ - console.warn("unexpected mode: "+value); - } - break; - case "Color": - if (LGraphCanvas.node_colors[value]){ - node.color = LGraphCanvas.node_colors[value].color; - node.bgcolor = LGraphCanvas.node_colors[value].bgcolor; - }else{ - console.warn("unexpected color: "+value); - } - break; - default: - node.setProperty(name,value); - break; - } - graphcanvas.graph.afterChange(); - graphcanvas.dirty_canvas = true; - }; - - panel.addWidget( "string", "Title", node.title, {}, fUpdate); - - panel.addWidget( "combo", "Mode", LiteGraph.NODE_MODES[node.mode], {values: LiteGraph.NODE_MODES}, fUpdate); - - var nodeCol = ""; - if (node.color !== undefined){ - nodeCol = Object.keys(LGraphCanvas.node_colors).filter(function(nK){ return LGraphCanvas.node_colors[nK].color == node.color; }); - } - - panel.addWidget( "combo", "Color", nodeCol, {values: Object.keys(LGraphCanvas.node_colors)}, fUpdate); - - for(var pName in node.properties) - { - var value = node.properties[pName]; - var info = node.getPropertyInfo(pName); - var type = info.type || "string"; - - //in case the user wants control over the side panel widget - if( node.onAddPropertyToPanel && node.onAddPropertyToPanel(pName,panel) ) - continue; - - panel.addWidget( info.widget || info.type, pName, value, info, fUpdate); - } - - panel.addSeparator(); - - if(node.onShowCustomPanelInfo) - node.onShowCustomPanelInfo(panel); - - panel.footer.innerHTML = ""; // clear - panel.addButton("Delete",function(){ - if(node.block_delete) - return; - node.graph.remove(node); - panel.close(); - }).classList.add("delete"); - } - - panel.inner_showCodePad = function( propname ) - { - panel.classList.remove("settings"); - panel.classList.add("centered"); - - - /*if(window.CodeFlask) //disabled for now - { - panel.content.innerHTML = "
"; - var flask = new CodeFlask( "div.code", { language: 'js' }); - flask.updateCode(node.properties[propname]); - flask.onUpdate( function(code) { - node.setProperty(propname, code); - }); - } - else - {*/ - panel.alt_content.innerHTML = ""; - var textarea = panel.alt_content.querySelector("textarea"); - var fDoneWith = function(){ - panel.toggleAltContent(false); //if(node_prop_div) node_prop_div.style.display = "block"; // panel.close(); - panel.toggleFooterVisibility(true); - textarea.parentNode.removeChild(textarea); - panel.classList.add("settings"); - panel.classList.remove("centered"); - inner_refresh(); - } - textarea.value = node.properties[propname]; - textarea.addEventListener("keydown", function(e){ - if(e.code == "Enter" && e.ctrlKey ) - { - node.setProperty(propname, textarea.value); - fDoneWith(); - } - }); - panel.toggleAltContent(true); - panel.toggleFooterVisibility(false); - textarea.style.height = "calc(100% - 40px)"; - /*}*/ - var assign = panel.addButton( "Assign", function(){ - node.setProperty(propname, textarea.value); - fDoneWith(); - }); - panel.alt_content.appendChild(assign); //panel.content.appendChild(assign); - var button = panel.addButton( "Close", fDoneWith); - button.style.float = "right"; - panel.alt_content.appendChild(button); // panel.content.appendChild(button); - } - - inner_refresh(); - - this.canvas.parentNode.appendChild( panel ); - } - - LGraphCanvas.prototype.showSubgraphPropertiesDialog = function(node) - { - console.log("showing subgraph properties dialog"); - - var old_panel = this.canvas.parentNode.querySelector(".subgraph_dialog"); - if(old_panel) - old_panel.close(); - - var panel = this.createPanel("Subgraph Inputs",{closable:true, width: 500}); - panel.node = node; - panel.classList.add("subgraph_dialog"); - - function inner_refresh() - { - panel.clear(); - - //show currents - if(node.inputs) - for(var i = 0; i < node.inputs.length; ++i) - { - var input = node.inputs[i]; - if(input.not_subgraph_input) - continue; - var html = " "; - var elem = panel.addHTML(html,"subgraph_property"); - elem.dataset["name"] = input.name; - elem.dataset["slot"] = i; - elem.querySelector(".name").innerText = input.name; - elem.querySelector(".type").innerText = input.type; - elem.querySelector("button").addEventListener("click",function(e){ - node.removeInput( Number( this.parentNode.dataset["slot"] ) ); - inner_refresh(); - }); - } - } - - //add extra - var html = " + NameType"; - var elem = panel.addHTML(html,"subgraph_property extra", true); - elem.querySelector("button").addEventListener("click", function(e){ - var elem = this.parentNode; - var name = elem.querySelector(".name").value; - var type = elem.querySelector(".type").value; - if(!name || node.findInputSlot(name) != -1) - return; - node.addInput(name,type); - elem.querySelector(".name").value = ""; - elem.querySelector(".type").value = ""; - inner_refresh(); - }); - - inner_refresh(); - this.canvas.parentNode.appendChild(panel); - return panel; - } - LGraphCanvas.prototype.showSubgraphPropertiesDialogRight = function (node) { - - // console.log("showing subgraph properties dialog"); - var that = this; - // old_panel if old_panel is exist close it - var old_panel = this.canvas.parentNode.querySelector(".subgraph_dialog"); - if (old_panel) - old_panel.close(); - // new panel - var panel = this.createPanel("Subgraph Outputs", { closable: true, width: 500 }); - panel.node = node; - panel.classList.add("subgraph_dialog"); - - function inner_refresh() { - panel.clear(); - //show currents - if (node.outputs) - for (var i = 0; i < node.outputs.length; ++i) { - var input = node.outputs[i]; - if (input.not_subgraph_output) - continue; - var html = " "; - var elem = panel.addHTML(html, "subgraph_property"); - elem.dataset["name"] = input.name; - elem.dataset["slot"] = i; - elem.querySelector(".name").innerText = input.name; - elem.querySelector(".type").innerText = input.type; - elem.querySelector("button").addEventListener("click", function (e) { - node.removeOutput(Number(this.parentNode.dataset["slot"])); - inner_refresh(); - }); - } - } - - //add extra - var html = " + NameType"; - var elem = panel.addHTML(html, "subgraph_property extra", true); - elem.querySelector(".name").addEventListener("keydown", function (e) { - if (e.keyCode == 13) { - addOutput.apply(this) - } - }) - elem.querySelector("button").addEventListener("click", function (e) { - addOutput.apply(this) - }); - function addOutput() { - var elem = this.parentNode; - var name = elem.querySelector(".name").value; - var type = elem.querySelector(".type").value; - if (!name || node.findOutputSlot(name) != -1) - return; - node.addOutput(name, type); - elem.querySelector(".name").value = ""; - elem.querySelector(".type").value = ""; - inner_refresh(); - } - - inner_refresh(); - this.canvas.parentNode.appendChild(panel); - return panel; - } - LGraphCanvas.prototype.checkPanels = function() - { - if(!this.canvas) - return; - var panels = this.canvas.parentNode.querySelectorAll(".litegraph.dialog"); - for(var i = 0; i < panels.length; ++i) - { - var panel = panels[i]; - if( !panel.node ) - continue; - if( !panel.node.graph || panel.graph != this.graph ) - panel.close(); - } - } - - LGraphCanvas.onMenuNodeCollapse = function(value, options, e, menu, node) { - node.graph.beforeChange(/*?*/); - - var fApplyMultiNode = function(node){ - node.collapse(); - } - - var graphcanvas = LGraphCanvas.active_canvas; - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ - fApplyMultiNode(node); - }else{ - for (var i in graphcanvas.selected_nodes) { - fApplyMultiNode(graphcanvas.selected_nodes[i]); - } - } - - node.graph.afterChange(/*?*/); - }; - - LGraphCanvas.onMenuNodePin = function(value, options, e, menu, node) { - node.pin(); - }; - - LGraphCanvas.onMenuNodeMode = function(value, options, e, menu, node) { - new LiteGraph.ContextMenu( - LiteGraph.NODE_MODES, - { event: e, callback: inner_clicked, parentMenu: menu, node: node } - ); - - function inner_clicked(v) { - if (!node) { - return; - } - var kV = Object.values(LiteGraph.NODE_MODES).indexOf(v); - var fApplyMultiNode = function(node){ - if (kV>=0 && LiteGraph.NODE_MODES[kV]) - node.changeMode(kV); - else{ - console.warn("unexpected mode: "+v); - node.changeMode(LiteGraph.ALWAYS); - } - } - - var graphcanvas = LGraphCanvas.active_canvas; - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ - fApplyMultiNode(node); - }else{ - for (var i in graphcanvas.selected_nodes) { - fApplyMultiNode(graphcanvas.selected_nodes[i]); - } - } - } - - return false; - }; - - LGraphCanvas.onMenuNodeColors = function(value, options, e, menu, node) { - if (!node) { - throw "no node for color"; - } - - var values = []; - values.push({ - value: null, - content: - "No color" - }); - - for (var i in LGraphCanvas.node_colors) { - var color = LGraphCanvas.node_colors[i]; - var value = { - value: i, - content: - "" + - i + - "" - }; - values.push(value); - } - new LiteGraph.ContextMenu(values, { - event: e, - callback: inner_clicked, - parentMenu: menu, - node: node - }); - - function inner_clicked(v) { - if (!node) { - return; - } - - var color = v.value ? LGraphCanvas.node_colors[v.value] : null; - - var fApplyColor = function(node){ - if (color) { - if (node.constructor === LiteGraph.LGraphGroup) { - node.color = color.groupcolor; - } else { - node.color = color.color; - node.bgcolor = color.bgcolor; - } - } else { - delete node.color; - delete node.bgcolor; - } - } - - var graphcanvas = LGraphCanvas.active_canvas; - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ - fApplyColor(node); - }else{ - for (var i in graphcanvas.selected_nodes) { - fApplyColor(graphcanvas.selected_nodes[i]); - } - } - node.setDirtyCanvas(true, true); - } - - return false; - }; - - LGraphCanvas.onMenuNodeShapes = function(value, options, e, menu, node) { - if (!node) { - throw "no node passed"; - } - - new LiteGraph.ContextMenu(LiteGraph.VALID_SHAPES, { - event: e, - callback: inner_clicked, - parentMenu: menu, - node: node - }); - - function inner_clicked(v) { - if (!node) { - return; - } - node.graph.beforeChange(/*?*/); //node - - var fApplyMultiNode = function(node){ - node.shape = v; - } - - var graphcanvas = LGraphCanvas.active_canvas; - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ - fApplyMultiNode(node); - }else{ - for (var i in graphcanvas.selected_nodes) { - fApplyMultiNode(graphcanvas.selected_nodes[i]); - } - } - - node.graph.afterChange(/*?*/); //node - node.setDirtyCanvas(true); - } - - return false; - }; - - LGraphCanvas.onMenuNodeRemove = function(value, options, e, menu, node) { - if (!node) { - throw "no node passed"; - } - - var graph = node.graph; - graph.beforeChange(); - - - var fApplyMultiNode = function(node){ - if (node.removable === false) { - return; - } - graph.remove(node); - } - - var graphcanvas = LGraphCanvas.active_canvas; - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ - fApplyMultiNode(node); - }else{ - for (var i in graphcanvas.selected_nodes) { - fApplyMultiNode(graphcanvas.selected_nodes[i]); - } - } - - graph.afterChange(); - node.setDirtyCanvas(true, true); - }; - - LGraphCanvas.onMenuNodeToSubgraph = function(value, options, e, menu, node) { - var graph = node.graph; - var graphcanvas = LGraphCanvas.active_canvas; - if(!graphcanvas) //?? - return; - - var nodes_list = Object.values( graphcanvas.selected_nodes || {} ); - if( !nodes_list.length ) - nodes_list = [ node ]; - - var subgraph_node = LiteGraph.createNode("graph/subgraph"); - subgraph_node.pos = node.pos.concat(); - graph.add(subgraph_node); - - subgraph_node.buildFromNodes( nodes_list ); - - graphcanvas.deselectAllNodes(); - node.setDirtyCanvas(true, true); - }; - - LGraphCanvas.onMenuNodeClone = function(value, options, e, menu, node) { - - node.graph.beforeChange(); - - var newSelected = {}; - - var fApplyMultiNode = function(node){ - if (node.clonable === false) { - return; - } - var newnode = node.clone(); - if (!newnode) { - return; - } - newnode.pos = [node.pos[0] + 5, node.pos[1] + 5]; - node.graph.add(newnode); - newSelected[newnode.id] = newnode; - } - - var graphcanvas = LGraphCanvas.active_canvas; - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ - fApplyMultiNode(node); - }else{ - for (var i in graphcanvas.selected_nodes) { - fApplyMultiNode(graphcanvas.selected_nodes[i]); - } - } - - if(Object.keys(newSelected).length){ - graphcanvas.selectNodes(newSelected); - } - - node.graph.afterChange(); - - node.setDirtyCanvas(true, true); - }; - - LGraphCanvas.node_colors = { - red: { color: "#322", bgcolor: "#533", groupcolor: "#A88" }, - brown: { color: "#332922", bgcolor: "#593930", groupcolor: "#b06634" }, - green: { color: "#232", bgcolor: "#353", groupcolor: "#8A8" }, - blue: { color: "#223", bgcolor: "#335", groupcolor: "#88A" }, - pale_blue: { - color: "#2a363b", - bgcolor: "#3f5159", - groupcolor: "#3f789e" - }, - cyan: { color: "#233", bgcolor: "#355", groupcolor: "#8AA" }, - purple: { color: "#323", bgcolor: "#535", groupcolor: "#a1309b" }, - yellow: { color: "#432", bgcolor: "#653", groupcolor: "#b58b2a" }, - black: { color: "#222", bgcolor: "#000", groupcolor: "#444" } - }; - - LGraphCanvas.prototype.getCanvasMenuOptions = function() { - var options = null; - var that = this; - if (this.getMenuOptions) { - options = this.getMenuOptions(); - } else { - options = [ - { - content: "Add Node", - has_submenu: true, - callback: LGraphCanvas.onMenuAdd - }, - { content: "Add Group", callback: LGraphCanvas.onGroupAdd }, - //{ content: "Arrange", callback: that.graph.arrange }, - //{content:"Collapse All", callback: LGraphCanvas.onMenuCollapseAll } - ]; - /*if (LiteGraph.showCanvasOptions){ - options.push({ content: "Options", callback: that.showShowGraphOptionsPanel }); - }*/ - - if (Object.keys(this.selected_nodes).length > 1) { - options.push({ - content: "Align", - has_submenu: true, - callback: LGraphCanvas.onGroupAlign, - }) - } - - if (this._graph_stack && this._graph_stack.length > 0) { - options.push(null, { - content: "Close subgraph", - callback: this.closeSubgraph.bind(this) - }); - } - } - - if (this.getExtraMenuOptions) { - var extra = this.getExtraMenuOptions(this, options); - if (extra) { - options = options.concat(extra); - } - } - - return options; - }; - - //called by processContextMenu to extract the menu list - LGraphCanvas.prototype.getNodeMenuOptions = function(node) { - var options = null; - - if (node.getMenuOptions) { - options = node.getMenuOptions(this); - } else { - options = [ - { - content: "Inputs", - has_submenu: true, - disabled: true, - callback: LGraphCanvas.showMenuNodeOptionalInputs - }, - { - content: "Outputs", - has_submenu: true, - disabled: true, - callback: LGraphCanvas.showMenuNodeOptionalOutputs - }, - null, - { - content: "Properties", - has_submenu: true, - callback: LGraphCanvas.onShowMenuNodeProperties - }, - { - content: "Properties Panel", - callback: function(item, options, e, menu, node) { LGraphCanvas.active_canvas.showShowNodePanel(node) } - }, - null, - { - content: "Title", - callback: LGraphCanvas.onShowPropertyEditor - }, - { - content: "Mode", - has_submenu: true, - callback: LGraphCanvas.onMenuNodeMode - }]; - if(node.resizable !== false){ - options.push({ - content: "Resize", callback: LGraphCanvas.onMenuResizeNode - }); - } - options.push( - { - content: "Collapse", - callback: LGraphCanvas.onMenuNodeCollapse - }, - { content: "Pin", callback: LGraphCanvas.onMenuNodePin }, - { - content: "Colors", - has_submenu: true, - callback: LGraphCanvas.onMenuNodeColors - }, - { - content: "Shapes", - has_submenu: true, - callback: LGraphCanvas.onMenuNodeShapes - }, - null - ); - } - - if (node.onGetInputs) { - var inputs = node.onGetInputs(); - if (inputs && inputs.length) { - options[0].disabled = false; - } - } - - if (node.onGetOutputs) { - var outputs = node.onGetOutputs(); - if (outputs && outputs.length) { - options[1].disabled = false; - } - } - - if (node.getExtraMenuOptions) { - var extra = node.getExtraMenuOptions(this, options); - if (extra) { - extra.push(null); - options = extra.concat(options); - } - } - - if (node.clonable !== false) { - options.push({ - content: "Clone", - callback: LGraphCanvas.onMenuNodeClone - }); - } - - if(0) //TODO - options.push({ - content: "To Subgraph", - callback: LGraphCanvas.onMenuNodeToSubgraph - }); - - if (Object.keys(this.selected_nodes).length > 1) { - options.push({ - content: "Align Selected To", - has_submenu: true, - callback: LGraphCanvas.onNodeAlign, - }) - } - - options.push(null, { - content: "Remove", - disabled: !(node.removable !== false && !node.block_delete ), - callback: LGraphCanvas.onMenuNodeRemove - }); - - if (node.graph && node.graph.onGetNodeMenuOptions) { - node.graph.onGetNodeMenuOptions(options, node); - } - - return options; - }; - - LGraphCanvas.prototype.getGroupMenuOptions = function(node) { - var o = [ - { content: "Title", callback: LGraphCanvas.onShowPropertyEditor }, - { - content: "Color", - has_submenu: true, - callback: LGraphCanvas.onMenuNodeColors - }, - { - content: "Font size", - property: "font_size", - type: "Number", - callback: LGraphCanvas.onShowPropertyEditor - }, - null, - { content: "Remove", callback: LGraphCanvas.onMenuNodeRemove } - ]; - - return o; - }; - - LGraphCanvas.prototype.processContextMenu = function(node, event) { - var that = this; - var canvas = LGraphCanvas.active_canvas; - var ref_window = canvas.getCanvasWindow(); - - var menu_info = null; - var options = { - event: event, - callback: inner_option_clicked, - extra: node - }; - - if(node) - options.title = node.type; - - //check if mouse is in input - var slot = null; - if (node) { - slot = node.getSlotInPosition(event.canvasX, event.canvasY); - LGraphCanvas.active_node = node; - } - - if (slot) { - //on slot - menu_info = []; - if (node.getSlotMenuOptions) { - menu_info = node.getSlotMenuOptions(slot); - } else { - if ( - slot && - slot.output && - slot.output.links && - slot.output.links.length - ) { - menu_info.push({ content: "Disconnect Links", slot: slot }); - } - var _slot = slot.input || slot.output; - if (_slot.removable){ - menu_info.push( - _slot.locked - ? "Cannot remove" - : { content: "Remove Slot", slot: slot } - ); - } - if (!_slot.nameLocked){ - menu_info.push({ content: "Rename Slot", slot: slot }); - } - - } - options.title = - (slot.input ? slot.input.type : slot.output.type) || "*"; - if (slot.input && slot.input.type == LiteGraph.ACTION) { - options.title = "Action"; - } - if (slot.output && slot.output.type == LiteGraph.EVENT) { - options.title = "Event"; - } - } else { - if (node) { - //on node - menu_info = this.getNodeMenuOptions(node); - } else { - menu_info = this.getCanvasMenuOptions(); - var group = this.graph.getGroupOnPos( - event.canvasX, - event.canvasY - ); - if (group) { - //on group - menu_info.push(null, { - content: "Edit Group", - has_submenu: true, - submenu: { - title: "Group", - extra: group, - options: this.getGroupMenuOptions(group) - } - }); - } - } - } - - //show menu - if (!menu_info) { - return; - } - - var menu = new LiteGraph.ContextMenu(menu_info, options, ref_window); - - function inner_option_clicked(v, options, e) { - if (!v) { - return; - } - - if (v.content == "Remove Slot") { - var info = v.slot; - node.graph.beforeChange(); - if (info.input) { - node.removeInput(info.slot); - } else if (info.output) { - node.removeOutput(info.slot); - } - node.graph.afterChange(); - return; - } else if (v.content == "Disconnect Links") { - var info = v.slot; - node.graph.beforeChange(); - if (info.output) { - node.disconnectOutput(info.slot); - } else if (info.input) { - node.disconnectInput(info.slot); - } - node.graph.afterChange(); - return; - } else if (v.content == "Rename Slot") { - var info = v.slot; - var slot_info = info.input - ? node.getInputInfo(info.slot) - : node.getOutputInfo(info.slot); - var dialog = that.createDialog( - "Name", - options - ); - var input = dialog.querySelector("input"); - if (input && slot_info) { - input.value = slot_info.label || ""; - } - var inner = function(){ - node.graph.beforeChange(); - if (input.value) { - if (slot_info) { - slot_info.label = input.value; - } - that.setDirty(true); - } - dialog.close(); - node.graph.afterChange(); - } - dialog.querySelector("button").addEventListener("click", inner); - input.addEventListener("keydown", function(e) { - dialog.is_modified = true; - if (e.keyCode == 27) { - //ESC - dialog.close(); - } else if (e.keyCode == 13) { - inner(); // save - } else if (e.keyCode != 13 && e.target.localName != "textarea") { - return; - } - e.preventDefault(); - e.stopPropagation(); - }); - input.focus(); - } - - //if(v.callback) - // return v.callback.call(that, node, options, e, menu, that, event ); - } - }; - - //API ************************************************* - //like rect but rounded corners - if (typeof(window) != "undefined" && window.CanvasRenderingContext2D && !window.CanvasRenderingContext2D.prototype.roundRect) { - window.CanvasRenderingContext2D.prototype.roundRect = function( - x, - y, - w, - h, - radius, - radius_low - ) { - var top_left_radius = 0; - var top_right_radius = 0; - var bottom_left_radius = 0; - var bottom_right_radius = 0; - - if ( radius === 0 ) - { - this.rect(x,y,w,h); - return; - } - - if(radius_low === undefined) - radius_low = radius; - - //make it compatible with official one - if(radius != null && radius.constructor === Array) - { - if(radius.length == 1) - top_left_radius = top_right_radius = bottom_left_radius = bottom_right_radius = radius[0]; - else if(radius.length == 2) - { - top_left_radius = bottom_right_radius = radius[0]; - top_right_radius = bottom_left_radius = radius[1]; - } - else if(radius.length == 4) - { - top_left_radius = radius[0]; - top_right_radius = radius[1]; - bottom_left_radius = radius[2]; - bottom_right_radius = radius[3]; - } - else - return; - } - else //old using numbers - { - top_left_radius = radius || 0; - top_right_radius = radius || 0; - bottom_left_radius = radius_low || 0; - bottom_right_radius = radius_low || 0; - } - - //top right - this.moveTo(x + top_left_radius, y); - this.lineTo(x + w - top_right_radius, y); - this.quadraticCurveTo(x + w, y, x + w, y + top_right_radius); - - //bottom right - this.lineTo(x + w, y + h - bottom_right_radius); - this.quadraticCurveTo( - x + w, - y + h, - x + w - bottom_right_radius, - y + h - ); - - //bottom left - this.lineTo(x + bottom_right_radius, y + h); - this.quadraticCurveTo(x, y + h, x, y + h - bottom_left_radius); - - //top left - this.lineTo(x, y + bottom_left_radius); - this.quadraticCurveTo(x, y, x + top_left_radius, y); - }; - }//if - - function compareObjects(a, b) { - for (var i in a) { - if (a[i] != b[i]) { - return false; - } - } - return true; - } - LiteGraph.compareObjects = compareObjects; - - function distance(a, b) { - return Math.sqrt( - (b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1]) - ); - } - LiteGraph.distance = distance; - - function colorToString(c) { - return ( - "rgba(" + - Math.round(c[0] * 255).toFixed() + - "," + - Math.round(c[1] * 255).toFixed() + - "," + - Math.round(c[2] * 255).toFixed() + - "," + - (c.length == 4 ? c[3].toFixed(2) : "1.0") + - ")" - ); - } - LiteGraph.colorToString = colorToString; - - function isInsideRectangle(x, y, left, top, width, height) { - if (left < x && left + width > x && top < y && top + height > y) { - return true; - } - return false; - } - LiteGraph.isInsideRectangle = isInsideRectangle; - - //[minx,miny,maxx,maxy] - function growBounding(bounding, x, y) { - if (x < bounding[0]) { - bounding[0] = x; - } else if (x > bounding[2]) { - bounding[2] = x; - } - - if (y < bounding[1]) { - bounding[1] = y; - } else if (y > bounding[3]) { - bounding[3] = y; - } - } - LiteGraph.growBounding = growBounding; - - //point inside bounding box - function isInsideBounding(p, bb) { - if ( - p[0] < bb[0][0] || - p[1] < bb[0][1] || - p[0] > bb[1][0] || - p[1] > bb[1][1] - ) { - return false; - } - return true; - } - LiteGraph.isInsideBounding = isInsideBounding; - - //bounding overlap, format: [ startx, starty, width, height ] - function overlapBounding(a, b) { - var A_end_x = a[0] + a[2]; - var A_end_y = a[1] + a[3]; - var B_end_x = b[0] + b[2]; - var B_end_y = b[1] + b[3]; - - if ( - a[0] > B_end_x || - a[1] > B_end_y || - A_end_x < b[0] || - A_end_y < b[1] - ) { - return false; - } - return true; - } - LiteGraph.overlapBounding = overlapBounding; - - //Convert a hex value to its decimal value - the inputted hex must be in the - // format of a hex triplet - the kind we use for HTML colours. The function - // will return an array with three values. - function hex2num(hex) { - if (hex.charAt(0) == "#") { - hex = hex.slice(1); - } //Remove the '#' char - if there is one. - hex = hex.toUpperCase(); - var hex_alphabets = "0123456789ABCDEF"; - var value = new Array(3); - var k = 0; - var int1, int2; - for (var i = 0; i < 6; i += 2) { - int1 = hex_alphabets.indexOf(hex.charAt(i)); - int2 = hex_alphabets.indexOf(hex.charAt(i + 1)); - value[k] = int1 * 16 + int2; - k++; - } - return value; - } - - LiteGraph.hex2num = hex2num; - - //Give a array with three values as the argument and the function will return - // the corresponding hex triplet. - function num2hex(triplet) { - var hex_alphabets = "0123456789ABCDEF"; - var hex = "#"; - var int1, int2; - for (var i = 0; i < 3; i++) { - int1 = triplet[i] / 16; - int2 = triplet[i] % 16; - - hex += hex_alphabets.charAt(int1) + hex_alphabets.charAt(int2); - } - return hex; - } - - LiteGraph.num2hex = num2hex; - - /* LiteGraph GUI elements used for canvas editing *************************************/ - - /** - * ContextMenu from LiteGUI - * - * @class ContextMenu - * @constructor - * @param {Array} values (allows object { title: "Nice text", callback: function ... }) - * @param {Object} options [optional] Some options:\ - * - title: title to show on top of the menu - * - callback: function to call when an option is clicked, it receives the item information - * - ignore_item_callbacks: ignores the callback inside the item, it just calls the options.callback - * - event: you can pass a MouseEvent, this way the ContextMenu appears in that position - */ - function ContextMenu(values, options) { - options = options || {}; - this.options = options; - var that = this; - - //to link a menu with its parent - if (options.parentMenu) { - if (options.parentMenu.constructor !== this.constructor) { - console.error( - "parentMenu must be of class ContextMenu, ignoring it" - ); - options.parentMenu = null; - } else { - this.parentMenu = options.parentMenu; - this.parentMenu.lock = true; - this.parentMenu.current_submenu = this; - } - } - - var eventClass = null; - if(options.event) //use strings because comparing classes between windows doesnt work - eventClass = options.event.constructor.name; - if ( eventClass !== "MouseEvent" && - eventClass !== "CustomEvent" && - eventClass !== "PointerEvent" - ) { - console.error( - "Event passed to ContextMenu is not of type MouseEvent or CustomEvent. Ignoring it. ("+eventClass+")" - ); - options.event = null; - } - - var root = document.createElement("div"); - root.className = "litegraph litecontextmenu litemenubar-panel"; - if (options.className) { - root.className += " " + options.className; - } - root.style.minWidth = 100; - root.style.minHeight = 100; - root.style.pointerEvents = "none"; - setTimeout(function() { - root.style.pointerEvents = "auto"; - }, 100); //delay so the mouse up event is not caught by this element - - //this prevents the default context browser menu to open in case this menu was created when pressing right button - LiteGraph.pointerListenerAdd(root,"up", - function(e) { - //console.log("pointerevents: ContextMenu up root prevent"); - e.preventDefault(); - return true; - }, - true - ); - root.addEventListener( - "contextmenu", - function(e) { - if (e.button != 2) { - //right button - return false; - } - e.preventDefault(); - return false; - }, - true - ); - - LiteGraph.pointerListenerAdd(root,"down", - function(e) { - //console.log("pointerevents: ContextMenu down"); - if (e.button == 2) { - that.close(); - e.preventDefault(); - return true; - } - }, - true - ); - - function on_mouse_wheel(e) { - var pos = parseInt(root.style.top); - root.style.top = - (pos + e.deltaY * options.scroll_speed).toFixed() + "px"; - e.preventDefault(); - return true; - } - - if (!options.scroll_speed) { - options.scroll_speed = 0.1; - } - - root.addEventListener("wheel", on_mouse_wheel, true); - root.addEventListener("mousewheel", on_mouse_wheel, true); - - this.root = root; - - //title - if (options.title) { - var element = document.createElement("div"); - element.className = "litemenu-title"; - element.innerHTML = options.title; - root.appendChild(element); - } - - //entries - var num = 0; - for (var i=0; i < values.length; i++) { - var name = values.constructor == Array ? values[i] : i; - if (name != null && name.constructor !== String) { - name = name.content === undefined ? String(name) : name.content; - } - var value = values[i]; - this.addItem(name, value, options); - num++; - } - - //close on leave? touch enabled devices won't work TODO use a global device detector and condition on that - /*LiteGraph.pointerListenerAdd(root,"leave", function(e) { - console.log("pointerevents: ContextMenu leave"); - if (that.lock) { - return; - } - if (root.closing_timer) { - clearTimeout(root.closing_timer); - } - root.closing_timer = setTimeout(that.close.bind(that, e), 500); - //that.close(e); - });*/ - - LiteGraph.pointerListenerAdd(root,"enter", function(e) { - //console.log("pointerevents: ContextMenu enter"); - if (root.closing_timer) { - clearTimeout(root.closing_timer); - } - }); - - //insert before checking position - var root_document = document; - if (options.event) { - root_document = options.event.target.ownerDocument; - } - - if (!root_document) { - root_document = document; - } - - if( root_document.fullscreenElement ) - root_document.fullscreenElement.appendChild(root); - else - root_document.body.appendChild(root); - - //compute best position - var left = options.left || 0; - var top = options.top || 0; - if (options.event) { - left = options.event.clientX - 10; - top = options.event.clientY - 10; - if (options.title) { - top -= 20; - } - - if (options.parentMenu) { - var rect = options.parentMenu.root.getBoundingClientRect(); - left = rect.left + rect.width; - } - - var body_rect = document.body.getBoundingClientRect(); - var root_rect = root.getBoundingClientRect(); - if(body_rect.height == 0) - console.error("document.body height is 0. That is dangerous, set html,body { height: 100%; }"); - - if (body_rect.width && left > body_rect.width - root_rect.width - 10) { - left = body_rect.width - root_rect.width - 10; - } - if (body_rect.height && top > body_rect.height - root_rect.height - 10) { - top = body_rect.height - root_rect.height - 10; - } - } - - root.style.left = left + "px"; - root.style.top = top + "px"; - - if (options.scale) { - root.style.transform = "scale(" + options.scale + ")"; - } - } - - ContextMenu.prototype.addItem = function(name, value, options) { - var that = this; - options = options || {}; - - var element = document.createElement("div"); - element.className = "litemenu-entry submenu"; - - var disabled = false; - - if (value === null) { - element.classList.add("separator"); - //element.innerHTML = "
" - //continue; - } else { - element.innerHTML = value && value.title ? value.title : name; - element.value = value; - - if (value) { - if (value.disabled) { - disabled = true; - element.classList.add("disabled"); - } - if (value.submenu || value.has_submenu) { - element.classList.add("has_submenu"); - } - } - - if (typeof value == "function") { - element.dataset["value"] = name; - element.onclick_callback = value; - } else { - element.dataset["value"] = value; - } - - if (value.className) { - element.className += " " + value.className; - } - } - - this.root.appendChild(element); - if (!disabled) { - element.addEventListener("click", inner_onclick); - } - if (!disabled && options.autoopen) { - LiteGraph.pointerListenerAdd(element,"enter",inner_over); - } - - function inner_over(e) { - var value = this.value; - if (!value || !value.has_submenu) { - return; - } - //if it is a submenu, autoopen like the item was clicked - inner_onclick.call(this, e); - } - - //menu option clicked - function inner_onclick(e) { - var value = this.value; - var close_parent = true; - - if (that.current_submenu) { - that.current_submenu.close(e); - } - - //global callback - if (options.callback) { - var r = options.callback.call( - this, - value, - options, - e, - that, - options.node - ); - if (r === true) { - close_parent = false; - } - } - - //special cases - if (value) { - if ( - value.callback && - !options.ignore_item_callbacks && - value.disabled !== true - ) { - //item callback - var r = value.callback.call( - this, - value, - options, - e, - that, - options.extra - ); - if (r === true) { - close_parent = false; - } - } - if (value.submenu) { - if (!value.submenu.options) { - throw "ContextMenu submenu needs options"; - } - var submenu = new that.constructor(value.submenu.options, { - callback: value.submenu.callback, - event: e, - parentMenu: that, - ignore_item_callbacks: - value.submenu.ignore_item_callbacks, - title: value.submenu.title, - extra: value.submenu.extra, - autoopen: options.autoopen - }); - close_parent = false; - } - } - - if (close_parent && !that.lock) { - that.close(); - } - } - - return element; - }; - - ContextMenu.prototype.close = function(e, ignore_parent_menu) { - if (this.root.parentNode) { - this.root.parentNode.removeChild(this.root); - } - if (this.parentMenu && !ignore_parent_menu) { - this.parentMenu.lock = false; - this.parentMenu.current_submenu = null; - if (e === undefined) { - this.parentMenu.close(); - } else if ( - e && - !ContextMenu.isCursorOverElement(e, this.parentMenu.root) - ) { - ContextMenu.trigger(this.parentMenu.root, LiteGraph.pointerevents_method+"leave", e); - } - } - if (this.current_submenu) { - this.current_submenu.close(e, true); - } - - if (this.root.closing_timer) { - clearTimeout(this.root.closing_timer); - } - - // TODO implement : LiteGraph.contextMenuClosed(); :: keep track of opened / closed / current ContextMenu - // on key press, allow filtering/selecting the context menu elements - }; - - //this code is used to trigger events easily (used in the context menu mouseleave - ContextMenu.trigger = function(element, event_name, params, origin) { - var evt = document.createEvent("CustomEvent"); - evt.initCustomEvent(event_name, true, true, params); //canBubble, cancelable, detail - evt.srcElement = origin; - if (element.dispatchEvent) { - element.dispatchEvent(evt); - } else if (element.__events) { - element.__events.dispatchEvent(evt); - } - //else nothing seems binded here so nothing to do - return evt; - }; - - //returns the top most menu - ContextMenu.prototype.getTopMenu = function() { - if (this.options.parentMenu) { - return this.options.parentMenu.getTopMenu(); - } - return this; - }; - - ContextMenu.prototype.getFirstEvent = function() { - if (this.options.parentMenu) { - return this.options.parentMenu.getFirstEvent(); - } - return this.options.event; - }; - - ContextMenu.isCursorOverElement = function(event, element) { - var left = event.clientX; - var top = event.clientY; - var rect = element.getBoundingClientRect(); - if (!rect) { - return false; - } - if ( - top > rect.top && - top < rect.top + rect.height && - left > rect.left && - left < rect.left + rect.width - ) { - return true; - } - return false; - }; - - LiteGraph.ContextMenu = ContextMenu; - - LiteGraph.closeAllContextMenus = function(ref_window) { - ref_window = ref_window || window; - - var elements = ref_window.document.querySelectorAll(".litecontextmenu"); - if (!elements.length) { - return; - } - - var result = []; - for (var i = 0; i < elements.length; i++) { - result.push(elements[i]); - } - - for (var i=0; i < result.length; i++) { - if (result[i].close) { - result[i].close(); - } else if (result[i].parentNode) { - result[i].parentNode.removeChild(result[i]); - } - } - }; - - LiteGraph.extendClass = function(target, origin) { - for (var i in origin) { - //copy class properties - if (target.hasOwnProperty(i)) { - continue; - } - target[i] = origin[i]; - } - - if (origin.prototype) { - //copy prototype properties - for (var i in origin.prototype) { - //only enumerable - if (!origin.prototype.hasOwnProperty(i)) { - continue; - } - - if (target.prototype.hasOwnProperty(i)) { - //avoid overwriting existing ones - continue; - } - - //copy getters - if (origin.prototype.__lookupGetter__(i)) { - target.prototype.__defineGetter__( - i, - origin.prototype.__lookupGetter__(i) - ); - } else { - target.prototype[i] = origin.prototype[i]; - } - - //and setters - if (origin.prototype.__lookupSetter__(i)) { - target.prototype.__defineSetter__( - i, - origin.prototype.__lookupSetter__(i) - ); - } - } - } - }; - - //used by some widgets to render a curve editor - function CurveEditor( points ) - { - this.points = points; - this.selected = -1; - this.nearest = -1; - this.size = null; //stores last size used - this.must_update = true; - this.margin = 5; - } - - CurveEditor.sampleCurve = function(f,points) - { - if(!points) - return; - for(var i = 0; i < points.length - 1; ++i) - { - var p = points[i]; - var pn = points[i+1]; - if(pn[0] < f) - continue; - var r = (pn[0] - p[0]); - if( Math.abs(r) < 0.00001 ) - return p[1]; - var local_f = (f - p[0]) / r; - return p[1] * (1.0 - local_f) + pn[1] * local_f; - } - return 0; - } - - CurveEditor.prototype.draw = function( ctx, size, graphcanvas, background_color, line_color, inactive ) - { - var points = this.points; - if(!points) - return; - this.size = size; - var w = size[0] - this.margin * 2; - var h = size[1] - this.margin * 2; - - line_color = line_color || "#666"; - - ctx.save(); - ctx.translate(this.margin,this.margin); - - if(background_color) - { - ctx.fillStyle = "#111"; - ctx.fillRect(0,0,w,h); - ctx.fillStyle = "#222"; - ctx.fillRect(w*0.5,0,1,h); - ctx.strokeStyle = "#333"; - ctx.strokeRect(0,0,w,h); - } - ctx.strokeStyle = line_color; - if(inactive) - ctx.globalAlpha = 0.5; - ctx.beginPath(); - for(var i = 0; i < points.length; ++i) - { - var p = points[i]; - ctx.lineTo( p[0] * w, (1.0 - p[1]) * h ); - } - ctx.stroke(); - ctx.globalAlpha = 1; - if(!inactive) - for(var i = 0; i < points.length; ++i) - { - var p = points[i]; - ctx.fillStyle = this.selected == i ? "#FFF" : (this.nearest == i ? "#DDD" : "#AAA"); - ctx.beginPath(); - ctx.arc( p[0] * w, (1.0 - p[1]) * h, 2, 0, Math.PI * 2 ); - ctx.fill(); - } - ctx.restore(); - } - - //localpos is mouse in curve editor space - CurveEditor.prototype.onMouseDown = function( localpos, graphcanvas ) - { - var points = this.points; - if(!points) - return; - if( localpos[1] < 0 ) - return; - - //this.captureInput(true); - var w = this.size[0] - this.margin * 2; - var h = this.size[1] - this.margin * 2; - var x = localpos[0] - this.margin; - var y = localpos[1] - this.margin; - var pos = [x,y]; - var max_dist = 30 / graphcanvas.ds.scale; - //search closer one - this.selected = this.getCloserPoint(pos, max_dist); - //create one - if(this.selected == -1) - { - var point = [x / w, 1 - y / h]; - points.push(point); - points.sort(function(a,b){ return a[0] - b[0]; }); - this.selected = points.indexOf(point); - this.must_update = true; - } - if(this.selected != -1) - return true; - } - - CurveEditor.prototype.onMouseMove = function( localpos, graphcanvas ) - { - var points = this.points; - if(!points) - return; - var s = this.selected; - if(s < 0) - return; - var x = (localpos[0] - this.margin) / (this.size[0] - this.margin * 2 ); - var y = (localpos[1] - this.margin) / (this.size[1] - this.margin * 2 ); - var curvepos = [(localpos[0] - this.margin),(localpos[1] - this.margin)]; - var max_dist = 30 / graphcanvas.ds.scale; - this._nearest = this.getCloserPoint(curvepos, max_dist); - var point = points[s]; - if(point) - { - var is_edge_point = s == 0 || s == points.length - 1; - if( !is_edge_point && (localpos[0] < -10 || localpos[0] > this.size[0] + 10 || localpos[1] < -10 || localpos[1] > this.size[1] + 10) ) - { - points.splice(s,1); - this.selected = -1; - return; - } - if( !is_edge_point ) //not edges - point[0] = clamp(x, 0, 1); - else - point[0] = s == 0 ? 0 : 1; - point[1] = 1.0 - clamp(y, 0, 1); - points.sort(function(a,b){ return a[0] - b[0]; }); - this.selected = points.indexOf(point); - this.must_update = true; - } - } - - CurveEditor.prototype.onMouseUp = function( localpos, graphcanvas ) - { - this.selected = -1; - return false; - } - - CurveEditor.prototype.getCloserPoint = function(pos, max_dist) - { - var points = this.points; - if(!points) - return -1; - max_dist = max_dist || 30; - var w = (this.size[0] - this.margin * 2); - var h = (this.size[1] - this.margin * 2); - var num = points.length; - var p2 = [0,0]; - var min_dist = 1000000; - var closest = -1; - var last_valid = -1; - for(var i = 0; i < num; ++i) - { - var p = points[i]; - p2[0] = p[0] * w; - p2[1] = (1.0 - p[1]) * h; - if(p2[0] < pos[0]) - last_valid = i; - var dist = vec2.distance(pos,p2); - if(dist > min_dist || dist > max_dist) - continue; - closest = i; - min_dist = dist; - } - return closest; - } - - LiteGraph.CurveEditor = CurveEditor; - - //used to create nodes from wrapping functions - LiteGraph.getParameterNames = function(func) { - return (func + "") - .replace(/[/][/].*$/gm, "") // strip single-line comments - .replace(/\s+/g, "") // strip white space - .replace(/[/][*][^/*]*[*][/]/g, "") // strip multi-line comments /**/ - .split("){", 1)[0] - .replace(/^[^(]*[(]/, "") // extract the parameters - .replace(/=[^,]+/g, "") // strip any ES6 defaults - .split(",") - .filter(Boolean); // split & filter [""] - }; - - /* helper for interaction: pointer, touch, mouse Listeners - used by LGraphCanvas DragAndScale ContextMenu*/ - LiteGraph.pointerListenerAdd = function(oDOM, sEvIn, fCall, capture=false) { - if (!oDOM || !oDOM.addEventListener || !sEvIn || typeof fCall!=="function"){ - //console.log("cant pointerListenerAdd "+oDOM+", "+sEvent+", "+fCall); - return; // -- break -- - } - - var sMethod = LiteGraph.pointerevents_method; - var sEvent = sEvIn; - - // UNDER CONSTRUCTION - // convert pointerevents to touch event when not available - if (sMethod=="pointer" && !window.PointerEvent){ - console.warn("sMethod=='pointer' && !window.PointerEvent"); - console.log("Converting pointer["+sEvent+"] : down move up cancel enter TO touchstart touchmove touchend, etc .."); - switch(sEvent){ - case "down":{ - sMethod = "touch"; - sEvent = "start"; - break; - } - case "move":{ - sMethod = "touch"; - //sEvent = "move"; - break; - } - case "up":{ - sMethod = "touch"; - sEvent = "end"; - break; - } - case "cancel":{ - sMethod = "touch"; - //sEvent = "cancel"; - break; - } - case "enter":{ - console.log("debug: Should I send a move event?"); // ??? - break; - } - // case "over": case "out": not used at now - default:{ - console.warn("PointerEvent not available in this browser ? The event "+sEvent+" would not be called"); - } - } - } - - switch(sEvent){ - //both pointer and move events - case "down": case "up": case "move": case "over": case "out": case "enter": - { - oDOM.addEventListener(sMethod+sEvent, fCall, capture); - } - // only pointerevents - case "leave": case "cancel": case "gotpointercapture": case "lostpointercapture": - { - if (sMethod!="mouse"){ - return oDOM.addEventListener(sMethod+sEvent, fCall, capture); - } - } - // not "pointer" || "mouse" - default: - return oDOM.addEventListener(sEvent, fCall, capture); - } - } - LiteGraph.pointerListenerRemove = function(oDOM, sEvent, fCall, capture=false) { - if (!oDOM || !oDOM.removeEventListener || !sEvent || typeof fCall!=="function"){ - //console.log("cant pointerListenerRemove "+oDOM+", "+sEvent+", "+fCall); - return; // -- break -- - } - switch(sEvent){ - //both pointer and move events - case "down": case "up": case "move": case "over": case "out": case "enter": - { - if (LiteGraph.pointerevents_method=="pointer" || LiteGraph.pointerevents_method=="mouse"){ - oDOM.removeEventListener(LiteGraph.pointerevents_method+sEvent, fCall, capture); - } - } - // only pointerevents - case "leave": case "cancel": case "gotpointercapture": case "lostpointercapture": - { - if (LiteGraph.pointerevents_method=="pointer"){ - return oDOM.removeEventListener(LiteGraph.pointerevents_method+sEvent, fCall, capture); - } - } - // not "pointer" || "mouse" - default: - return oDOM.removeEventListener(sEvent, fCall, capture); - } - } - - function clamp(v, a, b) { - return a > v ? a : b < v ? b : v; - }; - global.clamp = clamp; - - if (typeof window != "undefined" && !window["requestAnimationFrame"]) { - window.requestAnimationFrame = - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - function(callback) { - window.setTimeout(callback, 1000 / 60); - }; - } -})(this); - -if (typeof exports != "undefined") { - exports.LiteGraph = this.LiteGraph; - exports.LGraph = this.LGraph; - exports.LLink = this.LLink; - exports.LGraphNode = this.LGraphNode; - exports.LGraphGroup = this.LGraphGroup; - exports.DragAndScale = this.DragAndScale; - exports.LGraphCanvas = this.LGraphCanvas; - exports.ContextMenu = this.ContextMenu; -} - - diff --git a/web/lib/litegraph.css b/web/lib/litegraph.css deleted file mode 100644 index 5524e24ba..000000000 --- a/web/lib/litegraph.css +++ /dev/null @@ -1,693 +0,0 @@ -/* this CSS contains only the basic CSS needed to run the app and use it */ - -.lgraphcanvas { - /*cursor: crosshair;*/ - user-select: none; - -moz-user-select: none; - -webkit-user-select: none; - outline: none; - font-family: Tahoma, sans-serif; -} - -.lgraphcanvas * { - box-sizing: border-box; -} - -.litegraph.litecontextmenu { - font-family: Tahoma, sans-serif; - position: fixed; - top: 100px; - left: 100px; - min-width: 100px; - color: #aaf; - padding: 0; - box-shadow: 0 0 10px black !important; - background-color: #2e2e2e !important; - z-index: 10; -} - -.litegraph.litecontextmenu.dark { - background-color: #000 !important; -} - -.litegraph.litecontextmenu .litemenu-title img { - margin-top: 2px; - margin-left: 2px; - margin-right: 4px; -} - -.litegraph.litecontextmenu .litemenu-entry { - margin: 2px; - padding: 2px; -} - -.litegraph.litecontextmenu .litemenu-entry.submenu { - background-color: #2e2e2e !important; -} - -.litegraph.litecontextmenu.dark .litemenu-entry.submenu { - background-color: #000 !important; -} - -.litegraph .litemenubar ul { - font-family: Tahoma, sans-serif; - margin: 0; - padding: 0; -} - -.litegraph .litemenubar li { - font-size: 14px; - color: #999; - display: inline-block; - min-width: 50px; - padding-left: 10px; - padding-right: 10px; - user-select: none; - -moz-user-select: none; - -webkit-user-select: none; - cursor: pointer; -} - -.litegraph .litemenubar li:hover { - background-color: #777; - color: #eee; -} - -.litegraph .litegraph .litemenubar-panel { - position: absolute; - top: 5px; - left: 5px; - min-width: 100px; - background-color: #444; - box-shadow: 0 0 3px black; - padding: 4px; - border-bottom: 2px solid #aaf; - z-index: 10; -} - -.litegraph .litemenu-entry, -.litemenu-title { - font-size: 12px; - color: #aaa; - padding: 0 0 0 4px; - margin: 2px; - padding-left: 2px; - -moz-user-select: none; - -webkit-user-select: none; - user-select: none; - cursor: pointer; -} - -.litegraph .litemenu-entry .icon { - display: inline-block; - width: 12px; - height: 12px; - margin: 2px; - vertical-align: top; -} - -.litegraph .litemenu-entry.checked .icon { - background-color: #aaf; -} - -.litegraph .litemenu-entry .more { - float: right; - padding-right: 5px; -} - -.litegraph .litemenu-entry.disabled { - opacity: 0.5; - cursor: default; -} - -.litegraph .litemenu-entry.separator { - display: block; - border-top: 1px solid #333; - border-bottom: 1px solid #666; - width: 100%; - height: 0px; - margin: 3px 0 2px 0; - background-color: transparent; - padding: 0 !important; - cursor: default !important; -} - -.litegraph .litemenu-entry.has_submenu { - border-right: 2px solid cyan; -} - -.litegraph .litemenu-title { - color: #dde; - background-color: #111; - margin: 0; - padding: 2px; - cursor: default; -} - -.litegraph .litemenu-entry:hover:not(.disabled):not(.separator) { - background-color: #444 !important; - color: #eee; - transition: all 0.2s; -} - -.litegraph .litemenu-entry .property_name { - display: inline-block; - text-align: left; - min-width: 80px; - min-height: 1.2em; -} - -.litegraph .litemenu-entry .property_value { - display: inline-block; - background-color: rgba(0, 0, 0, 0.5); - text-align: right; - min-width: 80px; - min-height: 1.2em; - vertical-align: middle; - padding-right: 10px; -} - -.litegraph.litesearchbox { - font-family: Tahoma, sans-serif; - position: absolute; - background-color: rgba(0, 0, 0, 0.5); - padding-top: 4px; -} - -.litegraph.litesearchbox input, -.litegraph.litesearchbox select { - margin-top: 3px; - min-width: 60px; - min-height: 1.5em; - background-color: black; - border: 0; - color: white; - padding-left: 10px; - margin-right: 5px; - max-width: 300px; -} - -.litegraph.litesearchbox .name { - display: inline-block; - min-width: 60px; - min-height: 1.5em; - padding-left: 10px; -} - -.litegraph.litesearchbox .helper { - overflow: auto; - max-height: 200px; - margin-top: 2px; -} - -.litegraph.lite-search-item { - font-family: Tahoma, sans-serif; - background-color: rgba(0, 0, 0, 0.5); - color: white; - padding-top: 2px; -} - -.litegraph.lite-search-item.not_in_filter{ - /*background-color: rgba(50, 50, 50, 0.5);*/ - /*color: #999;*/ - color: #B99; - font-style: italic; -} - -.litegraph.lite-search-item.generic_type{ - /*background-color: rgba(50, 50, 50, 0.5);*/ - /*color: #DD9;*/ - color: #999; - font-style: italic; -} - -.litegraph.lite-search-item:hover, -.litegraph.lite-search-item.selected { - cursor: pointer; - background-color: white; - color: black; -} - -.litegraph.lite-search-item-type { - display: inline-block; - background: rgba(0,0,0,0.2); - margin-left: 5px; - font-size: 14px; - padding: 2px 5px; - position: relative; - top: -2px; - opacity: 0.8; - border-radius: 4px; - } - -/* DIALOGs ******/ - -.litegraph .dialog { - position: absolute; - top: 50%; - left: 50%; - margin-top: -150px; - margin-left: -200px; - - background-color: #2A2A2A; - - min-width: 400px; - min-height: 200px; - box-shadow: 0 0 4px #111; - border-radius: 6px; -} - -.litegraph .dialog.settings { - left: 10px; - top: 10px; - height: calc( 100% - 20px ); - margin: auto; - max-width: 50%; -} - -.litegraph .dialog.centered { - top: 50px; - left: 50%; - position: absolute; - transform: translateX(-50%); - min-width: 600px; - min-height: 300px; - height: calc( 100% - 100px ); - margin: auto; -} - -.litegraph .dialog .close { - float: right; - margin: 4px; - margin-right: 10px; - cursor: pointer; - font-size: 1.4em; -} - -.litegraph .dialog .close:hover { - color: white; -} - -.litegraph .dialog .dialog-header { - color: #AAA; - border-bottom: 1px solid #161616; -} - -.litegraph .dialog .dialog-header { height: 40px; } -.litegraph .dialog .dialog-footer { height: 50px; padding: 10px; border-top: 1px solid #1a1a1a;} - -.litegraph .dialog .dialog-header .dialog-title { - font: 20px "Arial"; - margin: 4px; - padding: 4px 10px; - display: inline-block; -} - -.litegraph .dialog .dialog-content, .litegraph .dialog .dialog-alt-content { - height: calc(100% - 90px); - width: 100%; - min-height: 100px; - display: inline-block; - color: #AAA; - /*background-color: black;*/ - overflow: auto; -} - -.litegraph .dialog .dialog-content h3 { - margin: 10px; -} - -.litegraph .dialog .dialog-content .connections { - flex-direction: row; -} - -.litegraph .dialog .dialog-content .connections .connections_side { - width: calc(50% - 5px); - min-height: 100px; - background-color: black; - display: flex; -} - -.litegraph .dialog .node_type { - font-size: 1.2em; - display: block; - margin: 10px; -} - -.litegraph .dialog .node_desc { - opacity: 0.5; - display: block; - margin: 10px; -} - -.litegraph .dialog .separator { - display: block; - width: calc( 100% - 4px ); - height: 1px; - border-top: 1px solid #000; - border-bottom: 1px solid #333; - margin: 10px 2px; - padding: 0; -} - -.litegraph .dialog .property { - margin-bottom: 2px; - padding: 4px; -} - -.litegraph .dialog .property:hover { - background: #545454; -} - -.litegraph .dialog .property_name { - color: #737373; - display: inline-block; - text-align: left; - vertical-align: top; - width: 160px; - padding-left: 4px; - overflow: hidden; - margin-right: 6px; -} - -.litegraph .dialog .property:hover .property_name { - color: white; -} - -.litegraph .dialog .property_value { - display: inline-block; - text-align: right; - color: #AAA; - background-color: #1A1A1A; - /*width: calc( 100% - 122px );*/ - max-width: calc( 100% - 162px ); - min-width: 200px; - max-height: 300px; - min-height: 20px; - padding: 4px; - padding-right: 12px; - overflow: hidden; - cursor: pointer; - border-radius: 3px; -} - -.litegraph .dialog .property_value:hover { - color: white; -} - -.litegraph .dialog .property.boolean .property_value { - padding-right: 30px; - color: #A88; - /*width: auto; - float: right;*/ -} - -.litegraph .dialog .property.boolean.bool-on .property_name{ - color: #8A8; -} -.litegraph .dialog .property.boolean.bool-on .property_value{ - color: #8A8; -} - -.litegraph .dialog .btn { - border: 0; - border-radius: 4px; - padding: 4px 20px; - margin-left: 0px; - background-color: #060606; - color: #8e8e8e; -} - -.litegraph .dialog .btn:hover { - background-color: #111; - color: #FFF; -} - -.litegraph .dialog .btn.delete:hover { - background-color: #F33; - color: black; -} - -.litegraph .subgraph_property { - padding: 4px; -} - -.litegraph .subgraph_property:hover { - background-color: #333; -} - -.litegraph .subgraph_property.extra { - margin-top: 8px; -} - -.litegraph .subgraph_property span.name { - font-size: 1.3em; - padding-left: 4px; -} - -.litegraph .subgraph_property span.type { - opacity: 0.5; - margin-right: 20px; - padding-left: 4px; -} - -.litegraph .subgraph_property span.label { - display: inline-block; - width: 60px; - padding: 0px 10px; -} - -.litegraph .subgraph_property input { - width: 140px; - color: #999; - background-color: #1A1A1A; - border-radius: 4px; - border: 0; - margin-right: 10px; - padding: 4px; - padding-left: 10px; -} - -.litegraph .subgraph_property button { - background-color: #1c1c1c; - color: #aaa; - border: 0; - border-radius: 2px; - padding: 4px 10px; - cursor: pointer; -} - -.litegraph .subgraph_property.extra { - color: #ccc; -} - -.litegraph .subgraph_property.extra input { - background-color: #111; -} - -.litegraph .bullet_icon { - margin-left: 10px; - border-radius: 10px; - width: 12px; - height: 12px; - background-color: #666; - display: inline-block; - margin-top: 2px; - margin-right: 4px; - transition: background-color 0.1s ease 0s; - -moz-transition: background-color 0.1s ease 0s; -} - -.litegraph .bullet_icon:hover { - background-color: #698; - cursor: pointer; -} - -/* OLD */ - -.graphcontextmenu { - padding: 4px; - min-width: 100px; -} - -.graphcontextmenu-title { - color: #dde; - background-color: #222; - margin: 0; - padding: 2px; - cursor: default; -} - -.graphmenu-entry { - box-sizing: border-box; - margin: 2px; - padding-left: 20px; - user-select: none; - -moz-user-select: none; - -webkit-user-select: none; - transition: all linear 0.3s; -} - -.graphmenu-entry.event, -.litemenu-entry.event { - border-left: 8px solid orange; - padding-left: 12px; -} - -.graphmenu-entry.disabled { - opacity: 0.3; -} - -.graphmenu-entry.submenu { - border-right: 2px solid #eee; -} - -.graphmenu-entry:hover { - background-color: #555; -} - -.graphmenu-entry.separator { - background-color: #111; - border-bottom: 1px solid #666; - height: 1px; - width: calc(100% - 20px); - -moz-width: calc(100% - 20px); - -webkit-width: calc(100% - 20px); -} - -.graphmenu-entry .property_name { - display: inline-block; - text-align: left; - min-width: 80px; - min-height: 1.2em; -} - -.graphmenu-entry .property_value, -.litemenu-entry .property_value { - display: inline-block; - background-color: rgba(0, 0, 0, 0.5); - text-align: right; - min-width: 80px; - min-height: 1.2em; - vertical-align: middle; - padding-right: 10px; -} - -.graphdialog { - position: absolute; - top: 10px; - left: 10px; - min-height: 2em; - background-color: #333; - font-size: 1.2em; - box-shadow: 0 0 10px black !important; - z-index: 10; -} - -.graphdialog.rounded { - border-radius: 12px; - padding-right: 2px; -} - -.graphdialog .name { - display: inline-block; - min-width: 60px; - min-height: 1.5em; - padding-left: 10px; -} - -.graphdialog input, -.graphdialog textarea, -.graphdialog select { - margin: 3px; - min-width: 60px; - min-height: 1.5em; - background-color: black; - border: 0; - color: white; - padding-left: 10px; - outline: none; -} - -.graphdialog textarea { - min-height: 150px; -} - -.graphdialog button { - margin-top: 3px; - vertical-align: top; - background-color: #999; - border: 0; -} - -.graphdialog button.rounded, -.graphdialog input.rounded { - border-radius: 0 12px 12px 0; -} - -.graphdialog .helper { - overflow: auto; - max-height: 200px; -} - -.graphdialog .help-item { - padding-left: 10px; -} - -.graphdialog .help-item:hover, -.graphdialog .help-item.selected { - cursor: pointer; - background-color: white; - color: black; -} - -.litegraph .dialog { - min-height: 0; -} -.litegraph .dialog .dialog-content { -display: block; -} -.litegraph .dialog .dialog-content .subgraph_property { -padding: 5px; -} -.litegraph .dialog .dialog-footer { -margin: 0; -} -.litegraph .dialog .dialog-footer .subgraph_property { -margin-top: 0; -display: flex; -align-items: center; -padding: 5px; -} -.litegraph .dialog .dialog-footer .subgraph_property .name { -flex: 1; -} -.litegraph .graphdialog { -display: flex; -align-items: center; -border-radius: 20px; -padding: 4px 10px; -position: fixed; -} -.litegraph .graphdialog .name { -padding: 0; -min-height: 0; -font-size: 16px; -vertical-align: middle; -} -.litegraph .graphdialog .value { -font-size: 16px; -min-height: 0; -margin: 0 10px; -padding: 2px 5px; -} -.litegraph .graphdialog input[type="checkbox"] { -width: 16px; -height: 16px; -} -.litegraph .graphdialog button { -padding: 4px 18px; -border-radius: 20px; -cursor: pointer; -} - diff --git a/web/lib/litegraph.extensions.js b/web/lib/litegraph.extensions.js deleted file mode 100644 index 32853fe49..000000000 --- a/web/lib/litegraph.extensions.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Changes the background color of the canvas. - * - * @method updateBackground - * @param {image} String - * @param {clearBackgroundColor} String - * @ - */ -LGraphCanvas.prototype.updateBackground = function (image, clearBackgroundColor) { - this._bg_img = new Image(); - this._bg_img.name = image; - this._bg_img.src = image; - this._bg_img.onload = () => { - this.draw(true, true); - }; - this.background_image = image; - - this.clear_background = true; - this.clear_background_color = clearBackgroundColor; - this._pattern = null -} diff --git a/web/lib/materialdesignicons.min.css b/web/lib/materialdesignicons.min.css deleted file mode 100644 index 459ce9ea8..000000000 --- a/web/lib/materialdesignicons.min.css +++ /dev/null @@ -1,3 +0,0 @@ -@font-face{font-family:"Material Design Icons";src:url("../fonts/materialdesignicons-webfont.eot?v=7.4.47");src:url("../fonts/materialdesignicons-webfont.eot?#iefix&v=7.4.47") format("embedded-opentype"),url("../fonts/materialdesignicons-webfont.woff2?v=7.4.47") format("woff2"),url("../fonts/materialdesignicons-webfont.woff?v=7.4.47") format("woff"),url("../fonts/materialdesignicons-webfont.ttf?v=7.4.47") format("truetype");font-weight:normal;font-style:normal}.mdi:before,.mdi-set{display:inline-block;font:normal normal normal 24px/1 "Material Design Icons";font-size:inherit;text-rendering:auto;line-height:inherit;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.mdi-ab-testing::before{content:"\F01C9"}.mdi-abacus::before{content:"\F16E0"}.mdi-abjad-arabic::before{content:"\F1328"}.mdi-abjad-hebrew::before{content:"\F1329"}.mdi-abugida-devanagari::before{content:"\F132A"}.mdi-abugida-thai::before{content:"\F132B"}.mdi-access-point::before{content:"\F0003"}.mdi-access-point-check::before{content:"\F1538"}.mdi-access-point-minus::before{content:"\F1539"}.mdi-access-point-network::before{content:"\F0002"}.mdi-access-point-network-off::before{content:"\F0BE1"}.mdi-access-point-off::before{content:"\F1511"}.mdi-access-point-plus::before{content:"\F153A"}.mdi-access-point-remove::before{content:"\F153B"}.mdi-account::before{content:"\F0004"}.mdi-account-alert::before{content:"\F0005"}.mdi-account-alert-outline::before{content:"\F0B50"}.mdi-account-arrow-down::before{content:"\F1868"}.mdi-account-arrow-down-outline::before{content:"\F1869"}.mdi-account-arrow-left::before{content:"\F0B51"}.mdi-account-arrow-left-outline::before{content:"\F0B52"}.mdi-account-arrow-right::before{content:"\F0B53"}.mdi-account-arrow-right-outline::before{content:"\F0B54"}.mdi-account-arrow-up::before{content:"\F1867"}.mdi-account-arrow-up-outline::before{content:"\F186A"}.mdi-account-badge::before{content:"\F1B0A"}.mdi-account-badge-outline::before{content:"\F1B0B"}.mdi-account-box::before{content:"\F0006"}.mdi-account-box-edit-outline::before{content:"\F1CC8"}.mdi-account-box-minus-outline::before{content:"\F1CC9"}.mdi-account-box-multiple::before{content:"\F0934"}.mdi-account-box-multiple-outline::before{content:"\F100A"}.mdi-account-box-outline::before{content:"\F0007"}.mdi-account-box-plus-outline::before{content:"\F1CCA"}.mdi-account-cancel::before{content:"\F12DF"}.mdi-account-cancel-outline::before{content:"\F12E0"}.mdi-account-card::before{content:"\F1BA4"}.mdi-account-card-outline::before{content:"\F1BA5"}.mdi-account-cash::before{content:"\F1097"}.mdi-account-cash-outline::before{content:"\F1098"}.mdi-account-check::before{content:"\F0008"}.mdi-account-check-outline::before{content:"\F0BE2"}.mdi-account-child::before{content:"\F0A89"}.mdi-account-child-circle::before{content:"\F0A8A"}.mdi-account-child-outline::before{content:"\F10C8"}.mdi-account-circle::before{content:"\F0009"}.mdi-account-circle-outline::before{content:"\F0B55"}.mdi-account-clock::before{content:"\F0B56"}.mdi-account-clock-outline::before{content:"\F0B57"}.mdi-account-cog::before{content:"\F1370"}.mdi-account-cog-outline::before{content:"\F1371"}.mdi-account-convert::before{content:"\F000A"}.mdi-account-convert-outline::before{content:"\F1301"}.mdi-account-cowboy-hat::before{content:"\F0E9B"}.mdi-account-cowboy-hat-outline::before{content:"\F17F3"}.mdi-account-credit-card::before{content:"\F1BA6"}.mdi-account-credit-card-outline::before{content:"\F1BA7"}.mdi-account-details::before{content:"\F0631"}.mdi-account-details-outline::before{content:"\F1372"}.mdi-account-edit::before{content:"\F06BC"}.mdi-account-edit-outline::before{content:"\F0FFB"}.mdi-account-eye::before{content:"\F0420"}.mdi-account-eye-outline::before{content:"\F127B"}.mdi-account-file::before{content:"\F1CA7"}.mdi-account-file-outline::before{content:"\F1CA8"}.mdi-account-file-text::before{content:"\F1CA9"}.mdi-account-file-text-outline::before{content:"\F1CAA"}.mdi-account-filter::before{content:"\F0936"}.mdi-account-filter-outline::before{content:"\F0F9D"}.mdi-account-group::before{content:"\F0849"}.mdi-account-group-outline::before{content:"\F0B58"}.mdi-account-hard-hat::before{content:"\F05B5"}.mdi-account-hard-hat-outline::before{content:"\F1A1F"}.mdi-account-heart::before{content:"\F0899"}.mdi-account-heart-outline::before{content:"\F0BE3"}.mdi-account-injury::before{content:"\F1815"}.mdi-account-injury-outline::before{content:"\F1816"}.mdi-account-key::before{content:"\F000B"}.mdi-account-key-outline::before{content:"\F0BE4"}.mdi-account-lock::before{content:"\F115E"}.mdi-account-lock-open::before{content:"\F1960"}.mdi-account-lock-open-outline::before{content:"\F1961"}.mdi-account-lock-outline::before{content:"\F115F"}.mdi-account-minus::before{content:"\F000D"}.mdi-account-minus-outline::before{content:"\F0AEC"}.mdi-account-multiple::before{content:"\F000E"}.mdi-account-multiple-check::before{content:"\F08C5"}.mdi-account-multiple-check-outline::before{content:"\F11FE"}.mdi-account-multiple-minus::before{content:"\F05D3"}.mdi-account-multiple-minus-outline::before{content:"\F0BE5"}.mdi-account-multiple-outline::before{content:"\F000F"}.mdi-account-multiple-plus::before{content:"\F0010"}.mdi-account-multiple-plus-outline::before{content:"\F0800"}.mdi-account-multiple-remove::before{content:"\F120A"}.mdi-account-multiple-remove-outline::before{content:"\F120B"}.mdi-account-music::before{content:"\F0803"}.mdi-account-music-outline::before{content:"\F0CE9"}.mdi-account-network::before{content:"\F0011"}.mdi-account-network-off::before{content:"\F1AF1"}.mdi-account-network-off-outline::before{content:"\F1AF2"}.mdi-account-network-outline::before{content:"\F0BE6"}.mdi-account-off::before{content:"\F0012"}.mdi-account-off-outline::before{content:"\F0BE7"}.mdi-account-outline::before{content:"\F0013"}.mdi-account-plus::before{content:"\F0014"}.mdi-account-plus-outline::before{content:"\F0801"}.mdi-account-question::before{content:"\F0B59"}.mdi-account-question-outline::before{content:"\F0B5A"}.mdi-account-reactivate::before{content:"\F152B"}.mdi-account-reactivate-outline::before{content:"\F152C"}.mdi-account-remove::before{content:"\F0015"}.mdi-account-remove-outline::before{content:"\F0AED"}.mdi-account-school::before{content:"\F1A20"}.mdi-account-school-outline::before{content:"\F1A21"}.mdi-account-search::before{content:"\F0016"}.mdi-account-search-outline::before{content:"\F0935"}.mdi-account-settings::before{content:"\F0630"}.mdi-account-settings-outline::before{content:"\F10C9"}.mdi-account-star::before{content:"\F0017"}.mdi-account-star-outline::before{content:"\F0BE8"}.mdi-account-supervisor::before{content:"\F0A8B"}.mdi-account-supervisor-circle::before{content:"\F0A8C"}.mdi-account-supervisor-circle-outline::before{content:"\F14EC"}.mdi-account-supervisor-outline::before{content:"\F112D"}.mdi-account-switch::before{content:"\F0019"}.mdi-account-switch-outline::before{content:"\F04CB"}.mdi-account-sync::before{content:"\F191B"}.mdi-account-sync-outline::before{content:"\F191C"}.mdi-account-tag::before{content:"\F1C1B"}.mdi-account-tag-outline::before{content:"\F1C1C"}.mdi-account-tie::before{content:"\F0CE3"}.mdi-account-tie-hat::before{content:"\F1898"}.mdi-account-tie-hat-outline::before{content:"\F1899"}.mdi-account-tie-outline::before{content:"\F10CA"}.mdi-account-tie-voice::before{content:"\F1308"}.mdi-account-tie-voice-off::before{content:"\F130A"}.mdi-account-tie-voice-off-outline::before{content:"\F130B"}.mdi-account-tie-voice-outline::before{content:"\F1309"}.mdi-account-tie-woman::before{content:"\F1A8C"}.mdi-account-voice::before{content:"\F05CB"}.mdi-account-voice-off::before{content:"\F0ED4"}.mdi-account-wrench::before{content:"\F189A"}.mdi-account-wrench-outline::before{content:"\F189B"}.mdi-adjust::before{content:"\F001A"}.mdi-advertisements::before{content:"\F192A"}.mdi-advertisements-off::before{content:"\F192B"}.mdi-air-conditioner::before{content:"\F001B"}.mdi-air-filter::before{content:"\F0D43"}.mdi-air-horn::before{content:"\F0DAC"}.mdi-air-humidifier::before{content:"\F1099"}.mdi-air-humidifier-off::before{content:"\F1466"}.mdi-air-purifier::before{content:"\F0D44"}.mdi-air-purifier-off::before{content:"\F1B57"}.mdi-airbag::before{content:"\F0BE9"}.mdi-airballoon::before{content:"\F001C"}.mdi-airballoon-outline::before{content:"\F100B"}.mdi-airplane::before{content:"\F001D"}.mdi-airplane-alert::before{content:"\F187A"}.mdi-airplane-check::before{content:"\F187B"}.mdi-airplane-clock::before{content:"\F187C"}.mdi-airplane-cog::before{content:"\F187D"}.mdi-airplane-edit::before{content:"\F187E"}.mdi-airplane-landing::before{content:"\F05D4"}.mdi-airplane-marker::before{content:"\F187F"}.mdi-airplane-minus::before{content:"\F1880"}.mdi-airplane-off::before{content:"\F001E"}.mdi-airplane-plus::before{content:"\F1881"}.mdi-airplane-remove::before{content:"\F1882"}.mdi-airplane-search::before{content:"\F1883"}.mdi-airplane-settings::before{content:"\F1884"}.mdi-airplane-takeoff::before{content:"\F05D5"}.mdi-airport::before{content:"\F084B"}.mdi-alarm::before{content:"\F0020"}.mdi-alarm-bell::before{content:"\F078E"}.mdi-alarm-check::before{content:"\F0021"}.mdi-alarm-light::before{content:"\F078F"}.mdi-alarm-light-off::before{content:"\F171E"}.mdi-alarm-light-off-outline::before{content:"\F171F"}.mdi-alarm-light-outline::before{content:"\F0BEA"}.mdi-alarm-multiple::before{content:"\F0022"}.mdi-alarm-note::before{content:"\F0E71"}.mdi-alarm-note-off::before{content:"\F0E72"}.mdi-alarm-off::before{content:"\F0023"}.mdi-alarm-panel::before{content:"\F15C4"}.mdi-alarm-panel-outline::before{content:"\F15C5"}.mdi-alarm-plus::before{content:"\F0024"}.mdi-alarm-snooze::before{content:"\F068E"}.mdi-album::before{content:"\F0025"}.mdi-alert::before{content:"\F0026"}.mdi-alert-box::before{content:"\F0027"}.mdi-alert-box-outline::before{content:"\F0CE4"}.mdi-alert-circle::before{content:"\F0028"}.mdi-alert-circle-check::before{content:"\F11ED"}.mdi-alert-circle-check-outline::before{content:"\F11EE"}.mdi-alert-circle-outline::before{content:"\F05D6"}.mdi-alert-decagram::before{content:"\F06BD"}.mdi-alert-decagram-outline::before{content:"\F0CE5"}.mdi-alert-minus::before{content:"\F14BB"}.mdi-alert-minus-outline::before{content:"\F14BE"}.mdi-alert-octagon::before{content:"\F0029"}.mdi-alert-octagon-outline::before{content:"\F0CE6"}.mdi-alert-octagram::before{content:"\F0767"}.mdi-alert-octagram-outline::before{content:"\F0CE7"}.mdi-alert-outline::before{content:"\F002A"}.mdi-alert-plus::before{content:"\F14BA"}.mdi-alert-plus-outline::before{content:"\F14BD"}.mdi-alert-remove::before{content:"\F14BC"}.mdi-alert-remove-outline::before{content:"\F14BF"}.mdi-alert-rhombus::before{content:"\F11CE"}.mdi-alert-rhombus-outline::before{content:"\F11CF"}.mdi-alien::before{content:"\F089A"}.mdi-alien-outline::before{content:"\F10CB"}.mdi-align-horizontal-center::before{content:"\F11C3"}.mdi-align-horizontal-distribute::before{content:"\F1962"}.mdi-align-horizontal-left::before{content:"\F11C2"}.mdi-align-horizontal-right::before{content:"\F11C4"}.mdi-align-vertical-bottom::before{content:"\F11C5"}.mdi-align-vertical-center::before{content:"\F11C6"}.mdi-align-vertical-distribute::before{content:"\F1963"}.mdi-align-vertical-top::before{content:"\F11C7"}.mdi-all-inclusive::before{content:"\F06BE"}.mdi-all-inclusive-box::before{content:"\F188D"}.mdi-all-inclusive-box-outline::before{content:"\F188E"}.mdi-allergy::before{content:"\F1258"}.mdi-alpha::before{content:"\F002B"}.mdi-alpha-a::before{content:"\F0AEE"}.mdi-alpha-a-box::before{content:"\F0B08"}.mdi-alpha-a-box-outline::before{content:"\F0BEB"}.mdi-alpha-a-circle::before{content:"\F0BEC"}.mdi-alpha-a-circle-outline::before{content:"\F0BED"}.mdi-alpha-b::before{content:"\F0AEF"}.mdi-alpha-b-box::before{content:"\F0B09"}.mdi-alpha-b-box-outline::before{content:"\F0BEE"}.mdi-alpha-b-circle::before{content:"\F0BEF"}.mdi-alpha-b-circle-outline::before{content:"\F0BF0"}.mdi-alpha-c::before{content:"\F0AF0"}.mdi-alpha-c-box::before{content:"\F0B0A"}.mdi-alpha-c-box-outline::before{content:"\F0BF1"}.mdi-alpha-c-circle::before{content:"\F0BF2"}.mdi-alpha-c-circle-outline::before{content:"\F0BF3"}.mdi-alpha-d::before{content:"\F0AF1"}.mdi-alpha-d-box::before{content:"\F0B0B"}.mdi-alpha-d-box-outline::before{content:"\F0BF4"}.mdi-alpha-d-circle::before{content:"\F0BF5"}.mdi-alpha-d-circle-outline::before{content:"\F0BF6"}.mdi-alpha-e::before{content:"\F0AF2"}.mdi-alpha-e-box::before{content:"\F0B0C"}.mdi-alpha-e-box-outline::before{content:"\F0BF7"}.mdi-alpha-e-circle::before{content:"\F0BF8"}.mdi-alpha-e-circle-outline::before{content:"\F0BF9"}.mdi-alpha-f::before{content:"\F0AF3"}.mdi-alpha-f-box::before{content:"\F0B0D"}.mdi-alpha-f-box-outline::before{content:"\F0BFA"}.mdi-alpha-f-circle::before{content:"\F0BFB"}.mdi-alpha-f-circle-outline::before{content:"\F0BFC"}.mdi-alpha-g::before{content:"\F0AF4"}.mdi-alpha-g-box::before{content:"\F0B0E"}.mdi-alpha-g-box-outline::before{content:"\F0BFD"}.mdi-alpha-g-circle::before{content:"\F0BFE"}.mdi-alpha-g-circle-outline::before{content:"\F0BFF"}.mdi-alpha-h::before{content:"\F0AF5"}.mdi-alpha-h-box::before{content:"\F0B0F"}.mdi-alpha-h-box-outline::before{content:"\F0C00"}.mdi-alpha-h-circle::before{content:"\F0C01"}.mdi-alpha-h-circle-outline::before{content:"\F0C02"}.mdi-alpha-i::before{content:"\F0AF6"}.mdi-alpha-i-box::before{content:"\F0B10"}.mdi-alpha-i-box-outline::before{content:"\F0C03"}.mdi-alpha-i-circle::before{content:"\F0C04"}.mdi-alpha-i-circle-outline::before{content:"\F0C05"}.mdi-alpha-j::before{content:"\F0AF7"}.mdi-alpha-j-box::before{content:"\F0B11"}.mdi-alpha-j-box-outline::before{content:"\F0C06"}.mdi-alpha-j-circle::before{content:"\F0C07"}.mdi-alpha-j-circle-outline::before{content:"\F0C08"}.mdi-alpha-k::before{content:"\F0AF8"}.mdi-alpha-k-box::before{content:"\F0B12"}.mdi-alpha-k-box-outline::before{content:"\F0C09"}.mdi-alpha-k-circle::before{content:"\F0C0A"}.mdi-alpha-k-circle-outline::before{content:"\F0C0B"}.mdi-alpha-l::before{content:"\F0AF9"}.mdi-alpha-l-box::before{content:"\F0B13"}.mdi-alpha-l-box-outline::before{content:"\F0C0C"}.mdi-alpha-l-circle::before{content:"\F0C0D"}.mdi-alpha-l-circle-outline::before{content:"\F0C0E"}.mdi-alpha-m::before{content:"\F0AFA"}.mdi-alpha-m-box::before{content:"\F0B14"}.mdi-alpha-m-box-outline::before{content:"\F0C0F"}.mdi-alpha-m-circle::before{content:"\F0C10"}.mdi-alpha-m-circle-outline::before{content:"\F0C11"}.mdi-alpha-n::before{content:"\F0AFB"}.mdi-alpha-n-box::before{content:"\F0B15"}.mdi-alpha-n-box-outline::before{content:"\F0C12"}.mdi-alpha-n-circle::before{content:"\F0C13"}.mdi-alpha-n-circle-outline::before{content:"\F0C14"}.mdi-alpha-o::before{content:"\F0AFC"}.mdi-alpha-o-box::before{content:"\F0B16"}.mdi-alpha-o-box-outline::before{content:"\F0C15"}.mdi-alpha-o-circle::before{content:"\F0C16"}.mdi-alpha-o-circle-outline::before{content:"\F0C17"}.mdi-alpha-p::before{content:"\F0AFD"}.mdi-alpha-p-box::before{content:"\F0B17"}.mdi-alpha-p-box-outline::before{content:"\F0C18"}.mdi-alpha-p-circle::before{content:"\F0C19"}.mdi-alpha-p-circle-outline::before{content:"\F0C1A"}.mdi-alpha-q::before{content:"\F0AFE"}.mdi-alpha-q-box::before{content:"\F0B18"}.mdi-alpha-q-box-outline::before{content:"\F0C1B"}.mdi-alpha-q-circle::before{content:"\F0C1C"}.mdi-alpha-q-circle-outline::before{content:"\F0C1D"}.mdi-alpha-r::before{content:"\F0AFF"}.mdi-alpha-r-box::before{content:"\F0B19"}.mdi-alpha-r-box-outline::before{content:"\F0C1E"}.mdi-alpha-r-circle::before{content:"\F0C1F"}.mdi-alpha-r-circle-outline::before{content:"\F0C20"}.mdi-alpha-s::before{content:"\F0B00"}.mdi-alpha-s-box::before{content:"\F0B1A"}.mdi-alpha-s-box-outline::before{content:"\F0C21"}.mdi-alpha-s-circle::before{content:"\F0C22"}.mdi-alpha-s-circle-outline::before{content:"\F0C23"}.mdi-alpha-t::before{content:"\F0B01"}.mdi-alpha-t-box::before{content:"\F0B1B"}.mdi-alpha-t-box-outline::before{content:"\F0C24"}.mdi-alpha-t-circle::before{content:"\F0C25"}.mdi-alpha-t-circle-outline::before{content:"\F0C26"}.mdi-alpha-u::before{content:"\F0B02"}.mdi-alpha-u-box::before{content:"\F0B1C"}.mdi-alpha-u-box-outline::before{content:"\F0C27"}.mdi-alpha-u-circle::before{content:"\F0C28"}.mdi-alpha-u-circle-outline::before{content:"\F0C29"}.mdi-alpha-v::before{content:"\F0B03"}.mdi-alpha-v-box::before{content:"\F0B1D"}.mdi-alpha-v-box-outline::before{content:"\F0C2A"}.mdi-alpha-v-circle::before{content:"\F0C2B"}.mdi-alpha-v-circle-outline::before{content:"\F0C2C"}.mdi-alpha-w::before{content:"\F0B04"}.mdi-alpha-w-box::before{content:"\F0B1E"}.mdi-alpha-w-box-outline::before{content:"\F0C2D"}.mdi-alpha-w-circle::before{content:"\F0C2E"}.mdi-alpha-w-circle-outline::before{content:"\F0C2F"}.mdi-alpha-x::before{content:"\F0B05"}.mdi-alpha-x-box::before{content:"\F0B1F"}.mdi-alpha-x-box-outline::before{content:"\F0C30"}.mdi-alpha-x-circle::before{content:"\F0C31"}.mdi-alpha-x-circle-outline::before{content:"\F0C32"}.mdi-alpha-y::before{content:"\F0B06"}.mdi-alpha-y-box::before{content:"\F0B20"}.mdi-alpha-y-box-outline::before{content:"\F0C33"}.mdi-alpha-y-circle::before{content:"\F0C34"}.mdi-alpha-y-circle-outline::before{content:"\F0C35"}.mdi-alpha-z::before{content:"\F0B07"}.mdi-alpha-z-box::before{content:"\F0B21"}.mdi-alpha-z-box-outline::before{content:"\F0C36"}.mdi-alpha-z-circle::before{content:"\F0C37"}.mdi-alpha-z-circle-outline::before{content:"\F0C38"}.mdi-alphabet-aurebesh::before{content:"\F132C"}.mdi-alphabet-cyrillic::before{content:"\F132D"}.mdi-alphabet-greek::before{content:"\F132E"}.mdi-alphabet-latin::before{content:"\F132F"}.mdi-alphabet-piqad::before{content:"\F1330"}.mdi-alphabet-tengwar::before{content:"\F1337"}.mdi-alphabetical::before{content:"\F002C"}.mdi-alphabetical-off::before{content:"\F100C"}.mdi-alphabetical-variant::before{content:"\F100D"}.mdi-alphabetical-variant-off::before{content:"\F100E"}.mdi-altimeter::before{content:"\F05D7"}.mdi-ambulance::before{content:"\F002F"}.mdi-ammunition::before{content:"\F0CE8"}.mdi-ampersand::before{content:"\F0A8D"}.mdi-amplifier::before{content:"\F0030"}.mdi-amplifier-off::before{content:"\F11B5"}.mdi-anchor::before{content:"\F0031"}.mdi-android::before{content:"\F0032"}.mdi-android-studio::before{content:"\F0034"}.mdi-angle-acute::before{content:"\F0937"}.mdi-angle-obtuse::before{content:"\F0938"}.mdi-angle-right::before{content:"\F0939"}.mdi-angular::before{content:"\F06B2"}.mdi-angularjs::before{content:"\F06BF"}.mdi-animation::before{content:"\F05D8"}.mdi-animation-outline::before{content:"\F0A8F"}.mdi-animation-play::before{content:"\F093A"}.mdi-animation-play-outline::before{content:"\F0A90"}.mdi-ansible::before{content:"\F109A"}.mdi-antenna::before{content:"\F1119"}.mdi-anvil::before{content:"\F089B"}.mdi-apache-kafka::before{content:"\F100F"}.mdi-api::before{content:"\F109B"}.mdi-api-off::before{content:"\F1257"}.mdi-apple::before{content:"\F0035"}.mdi-apple-finder::before{content:"\F0036"}.mdi-apple-icloud::before{content:"\F0038"}.mdi-apple-ios::before{content:"\F0037"}.mdi-apple-keyboard-caps::before{content:"\F0632"}.mdi-apple-keyboard-command::before{content:"\F0633"}.mdi-apple-keyboard-control::before{content:"\F0634"}.mdi-apple-keyboard-option::before{content:"\F0635"}.mdi-apple-keyboard-shift::before{content:"\F0636"}.mdi-apple-safari::before{content:"\F0039"}.mdi-application::before{content:"\F08C6"}.mdi-application-array::before{content:"\F10F5"}.mdi-application-array-outline::before{content:"\F10F6"}.mdi-application-braces::before{content:"\F10F7"}.mdi-application-braces-outline::before{content:"\F10F8"}.mdi-application-brackets::before{content:"\F0C8B"}.mdi-application-brackets-outline::before{content:"\F0C8C"}.mdi-application-cog::before{content:"\F0675"}.mdi-application-cog-outline::before{content:"\F1577"}.mdi-application-edit::before{content:"\F00AE"}.mdi-application-edit-outline::before{content:"\F0619"}.mdi-application-export::before{content:"\F0DAD"}.mdi-application-import::before{content:"\F0DAE"}.mdi-application-outline::before{content:"\F0614"}.mdi-application-parentheses::before{content:"\F10F9"}.mdi-application-parentheses-outline::before{content:"\F10FA"}.mdi-application-settings::before{content:"\F0B60"}.mdi-application-settings-outline::before{content:"\F1555"}.mdi-application-variable::before{content:"\F10FB"}.mdi-application-variable-outline::before{content:"\F10FC"}.mdi-approximately-equal::before{content:"\F0F9E"}.mdi-approximately-equal-box::before{content:"\F0F9F"}.mdi-apps::before{content:"\F003B"}.mdi-apps-box::before{content:"\F0D46"}.mdi-arch::before{content:"\F08C7"}.mdi-archive::before{content:"\F003C"}.mdi-archive-alert::before{content:"\F14FD"}.mdi-archive-alert-outline::before{content:"\F14FE"}.mdi-archive-arrow-down::before{content:"\F1259"}.mdi-archive-arrow-down-outline::before{content:"\F125A"}.mdi-archive-arrow-up::before{content:"\F125B"}.mdi-archive-arrow-up-outline::before{content:"\F125C"}.mdi-archive-cancel::before{content:"\F174B"}.mdi-archive-cancel-outline::before{content:"\F174C"}.mdi-archive-check::before{content:"\F174D"}.mdi-archive-check-outline::before{content:"\F174E"}.mdi-archive-clock::before{content:"\F174F"}.mdi-archive-clock-outline::before{content:"\F1750"}.mdi-archive-cog::before{content:"\F1751"}.mdi-archive-cog-outline::before{content:"\F1752"}.mdi-archive-edit::before{content:"\F1753"}.mdi-archive-edit-outline::before{content:"\F1754"}.mdi-archive-eye::before{content:"\F1755"}.mdi-archive-eye-outline::before{content:"\F1756"}.mdi-archive-lock::before{content:"\F1757"}.mdi-archive-lock-open::before{content:"\F1758"}.mdi-archive-lock-open-outline::before{content:"\F1759"}.mdi-archive-lock-outline::before{content:"\F175A"}.mdi-archive-marker::before{content:"\F175B"}.mdi-archive-marker-outline::before{content:"\F175C"}.mdi-archive-minus::before{content:"\F175D"}.mdi-archive-minus-outline::before{content:"\F175E"}.mdi-archive-music::before{content:"\F175F"}.mdi-archive-music-outline::before{content:"\F1760"}.mdi-archive-off::before{content:"\F1761"}.mdi-archive-off-outline::before{content:"\F1762"}.mdi-archive-outline::before{content:"\F120E"}.mdi-archive-plus::before{content:"\F1763"}.mdi-archive-plus-outline::before{content:"\F1764"}.mdi-archive-refresh::before{content:"\F1765"}.mdi-archive-refresh-outline::before{content:"\F1766"}.mdi-archive-remove::before{content:"\F1767"}.mdi-archive-remove-outline::before{content:"\F1768"}.mdi-archive-search::before{content:"\F1769"}.mdi-archive-search-outline::before{content:"\F176A"}.mdi-archive-settings::before{content:"\F176B"}.mdi-archive-settings-outline::before{content:"\F176C"}.mdi-archive-star::before{content:"\F176D"}.mdi-archive-star-outline::before{content:"\F176E"}.mdi-archive-sync::before{content:"\F176F"}.mdi-archive-sync-outline::before{content:"\F1770"}.mdi-arm-flex::before{content:"\F0FD7"}.mdi-arm-flex-outline::before{content:"\F0FD6"}.mdi-arrange-bring-forward::before{content:"\F003D"}.mdi-arrange-bring-to-front::before{content:"\F003E"}.mdi-arrange-send-backward::before{content:"\F003F"}.mdi-arrange-send-to-back::before{content:"\F0040"}.mdi-arrow-all::before{content:"\F0041"}.mdi-arrow-bottom-left::before{content:"\F0042"}.mdi-arrow-bottom-left-bold-box::before{content:"\F1964"}.mdi-arrow-bottom-left-bold-box-outline::before{content:"\F1965"}.mdi-arrow-bottom-left-bold-outline::before{content:"\F09B7"}.mdi-arrow-bottom-left-thick::before{content:"\F09B8"}.mdi-arrow-bottom-left-thin::before{content:"\F19B6"}.mdi-arrow-bottom-left-thin-circle-outline::before{content:"\F1596"}.mdi-arrow-bottom-right::before{content:"\F0043"}.mdi-arrow-bottom-right-bold-box::before{content:"\F1966"}.mdi-arrow-bottom-right-bold-box-outline::before{content:"\F1967"}.mdi-arrow-bottom-right-bold-outline::before{content:"\F09B9"}.mdi-arrow-bottom-right-thick::before{content:"\F09BA"}.mdi-arrow-bottom-right-thin::before{content:"\F19B7"}.mdi-arrow-bottom-right-thin-circle-outline::before{content:"\F1595"}.mdi-arrow-collapse::before{content:"\F0615"}.mdi-arrow-collapse-all::before{content:"\F0044"}.mdi-arrow-collapse-down::before{content:"\F0792"}.mdi-arrow-collapse-horizontal::before{content:"\F084C"}.mdi-arrow-collapse-left::before{content:"\F0793"}.mdi-arrow-collapse-right::before{content:"\F0794"}.mdi-arrow-collapse-up::before{content:"\F0795"}.mdi-arrow-collapse-vertical::before{content:"\F084D"}.mdi-arrow-decision::before{content:"\F09BB"}.mdi-arrow-decision-auto::before{content:"\F09BC"}.mdi-arrow-decision-auto-outline::before{content:"\F09BD"}.mdi-arrow-decision-outline::before{content:"\F09BE"}.mdi-arrow-down::before{content:"\F0045"}.mdi-arrow-down-bold::before{content:"\F072E"}.mdi-arrow-down-bold-box::before{content:"\F072F"}.mdi-arrow-down-bold-box-outline::before{content:"\F0730"}.mdi-arrow-down-bold-circle::before{content:"\F0047"}.mdi-arrow-down-bold-circle-outline::before{content:"\F0048"}.mdi-arrow-down-bold-hexagon-outline::before{content:"\F0049"}.mdi-arrow-down-bold-outline::before{content:"\F09BF"}.mdi-arrow-down-box::before{content:"\F06C0"}.mdi-arrow-down-circle::before{content:"\F0CDB"}.mdi-arrow-down-circle-outline::before{content:"\F0CDC"}.mdi-arrow-down-drop-circle::before{content:"\F004A"}.mdi-arrow-down-drop-circle-outline::before{content:"\F004B"}.mdi-arrow-down-left::before{content:"\F17A1"}.mdi-arrow-down-left-bold::before{content:"\F17A2"}.mdi-arrow-down-right::before{content:"\F17A3"}.mdi-arrow-down-right-bold::before{content:"\F17A4"}.mdi-arrow-down-thick::before{content:"\F0046"}.mdi-arrow-down-thin::before{content:"\F19B3"}.mdi-arrow-down-thin-circle-outline::before{content:"\F1599"}.mdi-arrow-expand::before{content:"\F0616"}.mdi-arrow-expand-all::before{content:"\F004C"}.mdi-arrow-expand-down::before{content:"\F0796"}.mdi-arrow-expand-horizontal::before{content:"\F084E"}.mdi-arrow-expand-left::before{content:"\F0797"}.mdi-arrow-expand-right::before{content:"\F0798"}.mdi-arrow-expand-up::before{content:"\F0799"}.mdi-arrow-expand-vertical::before{content:"\F084F"}.mdi-arrow-horizontal-lock::before{content:"\F115B"}.mdi-arrow-left::before{content:"\F004D"}.mdi-arrow-left-bold::before{content:"\F0731"}.mdi-arrow-left-bold-box::before{content:"\F0732"}.mdi-arrow-left-bold-box-outline::before{content:"\F0733"}.mdi-arrow-left-bold-circle::before{content:"\F004F"}.mdi-arrow-left-bold-circle-outline::before{content:"\F0050"}.mdi-arrow-left-bold-hexagon-outline::before{content:"\F0051"}.mdi-arrow-left-bold-outline::before{content:"\F09C0"}.mdi-arrow-left-bottom::before{content:"\F17A5"}.mdi-arrow-left-bottom-bold::before{content:"\F17A6"}.mdi-arrow-left-box::before{content:"\F06C1"}.mdi-arrow-left-circle::before{content:"\F0CDD"}.mdi-arrow-left-circle-outline::before{content:"\F0CDE"}.mdi-arrow-left-drop-circle::before{content:"\F0052"}.mdi-arrow-left-drop-circle-outline::before{content:"\F0053"}.mdi-arrow-left-right::before{content:"\F0E73"}.mdi-arrow-left-right-bold::before{content:"\F0E74"}.mdi-arrow-left-right-bold-outline::before{content:"\F09C1"}.mdi-arrow-left-thick::before{content:"\F004E"}.mdi-arrow-left-thin::before{content:"\F19B1"}.mdi-arrow-left-thin-circle-outline::before{content:"\F159A"}.mdi-arrow-left-top::before{content:"\F17A7"}.mdi-arrow-left-top-bold::before{content:"\F17A8"}.mdi-arrow-oscillating::before{content:"\F1C91"}.mdi-arrow-oscillating-off::before{content:"\F1C92"}.mdi-arrow-projectile::before{content:"\F1840"}.mdi-arrow-projectile-multiple::before{content:"\F183F"}.mdi-arrow-right::before{content:"\F0054"}.mdi-arrow-right-bold::before{content:"\F0734"}.mdi-arrow-right-bold-box::before{content:"\F0735"}.mdi-arrow-right-bold-box-outline::before{content:"\F0736"}.mdi-arrow-right-bold-circle::before{content:"\F0056"}.mdi-arrow-right-bold-circle-outline::before{content:"\F0057"}.mdi-arrow-right-bold-hexagon-outline::before{content:"\F0058"}.mdi-arrow-right-bold-outline::before{content:"\F09C2"}.mdi-arrow-right-bottom::before{content:"\F17A9"}.mdi-arrow-right-bottom-bold::before{content:"\F17AA"}.mdi-arrow-right-box::before{content:"\F06C2"}.mdi-arrow-right-circle::before{content:"\F0CDF"}.mdi-arrow-right-circle-outline::before{content:"\F0CE0"}.mdi-arrow-right-drop-circle::before{content:"\F0059"}.mdi-arrow-right-drop-circle-outline::before{content:"\F005A"}.mdi-arrow-right-thick::before{content:"\F0055"}.mdi-arrow-right-thin::before{content:"\F19B0"}.mdi-arrow-right-thin-circle-outline::before{content:"\F1598"}.mdi-arrow-right-top::before{content:"\F17AB"}.mdi-arrow-right-top-bold::before{content:"\F17AC"}.mdi-arrow-split-horizontal::before{content:"\F093B"}.mdi-arrow-split-vertical::before{content:"\F093C"}.mdi-arrow-top-left::before{content:"\F005B"}.mdi-arrow-top-left-bold-box::before{content:"\F1968"}.mdi-arrow-top-left-bold-box-outline::before{content:"\F1969"}.mdi-arrow-top-left-bold-outline::before{content:"\F09C3"}.mdi-arrow-top-left-bottom-right::before{content:"\F0E75"}.mdi-arrow-top-left-bottom-right-bold::before{content:"\F0E76"}.mdi-arrow-top-left-thick::before{content:"\F09C4"}.mdi-arrow-top-left-thin::before{content:"\F19B5"}.mdi-arrow-top-left-thin-circle-outline::before{content:"\F1593"}.mdi-arrow-top-right::before{content:"\F005C"}.mdi-arrow-top-right-bold-box::before{content:"\F196A"}.mdi-arrow-top-right-bold-box-outline::before{content:"\F196B"}.mdi-arrow-top-right-bold-outline::before{content:"\F09C5"}.mdi-arrow-top-right-bottom-left::before{content:"\F0E77"}.mdi-arrow-top-right-bottom-left-bold::before{content:"\F0E78"}.mdi-arrow-top-right-thick::before{content:"\F09C6"}.mdi-arrow-top-right-thin::before{content:"\F19B4"}.mdi-arrow-top-right-thin-circle-outline::before{content:"\F1594"}.mdi-arrow-u-down-left::before{content:"\F17AD"}.mdi-arrow-u-down-left-bold::before{content:"\F17AE"}.mdi-arrow-u-down-right::before{content:"\F17AF"}.mdi-arrow-u-down-right-bold::before{content:"\F17B0"}.mdi-arrow-u-left-bottom::before{content:"\F17B1"}.mdi-arrow-u-left-bottom-bold::before{content:"\F17B2"}.mdi-arrow-u-left-top::before{content:"\F17B3"}.mdi-arrow-u-left-top-bold::before{content:"\F17B4"}.mdi-arrow-u-right-bottom::before{content:"\F17B5"}.mdi-arrow-u-right-bottom-bold::before{content:"\F17B6"}.mdi-arrow-u-right-top::before{content:"\F17B7"}.mdi-arrow-u-right-top-bold::before{content:"\F17B8"}.mdi-arrow-u-up-left::before{content:"\F17B9"}.mdi-arrow-u-up-left-bold::before{content:"\F17BA"}.mdi-arrow-u-up-right::before{content:"\F17BB"}.mdi-arrow-u-up-right-bold::before{content:"\F17BC"}.mdi-arrow-up::before{content:"\F005D"}.mdi-arrow-up-bold::before{content:"\F0737"}.mdi-arrow-up-bold-box::before{content:"\F0738"}.mdi-arrow-up-bold-box-outline::before{content:"\F0739"}.mdi-arrow-up-bold-circle::before{content:"\F005F"}.mdi-arrow-up-bold-circle-outline::before{content:"\F0060"}.mdi-arrow-up-bold-hexagon-outline::before{content:"\F0061"}.mdi-arrow-up-bold-outline::before{content:"\F09C7"}.mdi-arrow-up-box::before{content:"\F06C3"}.mdi-arrow-up-circle::before{content:"\F0CE1"}.mdi-arrow-up-circle-outline::before{content:"\F0CE2"}.mdi-arrow-up-down::before{content:"\F0E79"}.mdi-arrow-up-down-bold::before{content:"\F0E7A"}.mdi-arrow-up-down-bold-outline::before{content:"\F09C8"}.mdi-arrow-up-drop-circle::before{content:"\F0062"}.mdi-arrow-up-drop-circle-outline::before{content:"\F0063"}.mdi-arrow-up-left::before{content:"\F17BD"}.mdi-arrow-up-left-bold::before{content:"\F17BE"}.mdi-arrow-up-right::before{content:"\F17BF"}.mdi-arrow-up-right-bold::before{content:"\F17C0"}.mdi-arrow-up-thick::before{content:"\F005E"}.mdi-arrow-up-thin::before{content:"\F19B2"}.mdi-arrow-up-thin-circle-outline::before{content:"\F1597"}.mdi-arrow-vertical-lock::before{content:"\F115C"}.mdi-artboard::before{content:"\F1B9A"}.mdi-artstation::before{content:"\F0B5B"}.mdi-aspect-ratio::before{content:"\F0A24"}.mdi-assistant::before{content:"\F0064"}.mdi-asterisk::before{content:"\F06C4"}.mdi-asterisk-circle-outline::before{content:"\F1A27"}.mdi-at::before{content:"\F0065"}.mdi-atlassian::before{content:"\F0804"}.mdi-atm::before{content:"\F0D47"}.mdi-atom::before{content:"\F0768"}.mdi-atom-variant::before{content:"\F0E7B"}.mdi-attachment::before{content:"\F0066"}.mdi-attachment-check::before{content:"\F1AC1"}.mdi-attachment-lock::before{content:"\F19C4"}.mdi-attachment-minus::before{content:"\F1AC2"}.mdi-attachment-off::before{content:"\F1AC3"}.mdi-attachment-plus::before{content:"\F1AC4"}.mdi-attachment-remove::before{content:"\F1AC5"}.mdi-atv::before{content:"\F1B70"}.mdi-audio-input-rca::before{content:"\F186B"}.mdi-audio-input-stereo-minijack::before{content:"\F186C"}.mdi-audio-input-xlr::before{content:"\F186D"}.mdi-audio-video::before{content:"\F093D"}.mdi-audio-video-off::before{content:"\F11B6"}.mdi-augmented-reality::before{content:"\F0850"}.mdi-aurora::before{content:"\F1BB9"}.mdi-auto-download::before{content:"\F137E"}.mdi-auto-fix::before{content:"\F0068"}.mdi-auto-mode::before{content:"\F1C20"}.mdi-auto-upload::before{content:"\F0069"}.mdi-autorenew::before{content:"\F006A"}.mdi-autorenew-off::before{content:"\F19E7"}.mdi-av-timer::before{content:"\F006B"}.mdi-awning::before{content:"\F1B87"}.mdi-awning-outline::before{content:"\F1B88"}.mdi-aws::before{content:"\F0E0F"}.mdi-axe::before{content:"\F08C8"}.mdi-axe-battle::before{content:"\F1842"}.mdi-axis::before{content:"\F0D48"}.mdi-axis-arrow::before{content:"\F0D49"}.mdi-axis-arrow-info::before{content:"\F140E"}.mdi-axis-arrow-lock::before{content:"\F0D4A"}.mdi-axis-lock::before{content:"\F0D4B"}.mdi-axis-x-arrow::before{content:"\F0D4C"}.mdi-axis-x-arrow-lock::before{content:"\F0D4D"}.mdi-axis-x-rotate-clockwise::before{content:"\F0D4E"}.mdi-axis-x-rotate-counterclockwise::before{content:"\F0D4F"}.mdi-axis-x-y-arrow-lock::before{content:"\F0D50"}.mdi-axis-y-arrow::before{content:"\F0D51"}.mdi-axis-y-arrow-lock::before{content:"\F0D52"}.mdi-axis-y-rotate-clockwise::before{content:"\F0D53"}.mdi-axis-y-rotate-counterclockwise::before{content:"\F0D54"}.mdi-axis-z-arrow::before{content:"\F0D55"}.mdi-axis-z-arrow-lock::before{content:"\F0D56"}.mdi-axis-z-rotate-clockwise::before{content:"\F0D57"}.mdi-axis-z-rotate-counterclockwise::before{content:"\F0D58"}.mdi-babel::before{content:"\F0A25"}.mdi-baby::before{content:"\F006C"}.mdi-baby-bottle::before{content:"\F0F39"}.mdi-baby-bottle-outline::before{content:"\F0F3A"}.mdi-baby-buggy::before{content:"\F13E0"}.mdi-baby-buggy-off::before{content:"\F1AF3"}.mdi-baby-carriage::before{content:"\F068F"}.mdi-baby-carriage-off::before{content:"\F0FA0"}.mdi-baby-face::before{content:"\F0E7C"}.mdi-baby-face-outline::before{content:"\F0E7D"}.mdi-backburger::before{content:"\F006D"}.mdi-backspace::before{content:"\F006E"}.mdi-backspace-outline::before{content:"\F0B5C"}.mdi-backspace-reverse::before{content:"\F0E7E"}.mdi-backspace-reverse-outline::before{content:"\F0E7F"}.mdi-backup-restore::before{content:"\F006F"}.mdi-bacteria::before{content:"\F0ED5"}.mdi-bacteria-outline::before{content:"\F0ED6"}.mdi-badge-account::before{content:"\F0DA7"}.mdi-badge-account-alert::before{content:"\F0DA8"}.mdi-badge-account-alert-outline::before{content:"\F0DA9"}.mdi-badge-account-horizontal::before{content:"\F0E0D"}.mdi-badge-account-horizontal-outline::before{content:"\F0E0E"}.mdi-badge-account-outline::before{content:"\F0DAA"}.mdi-badminton::before{content:"\F0851"}.mdi-bag-carry-on::before{content:"\F0F3B"}.mdi-bag-carry-on-check::before{content:"\F0D65"}.mdi-bag-carry-on-off::before{content:"\F0F3C"}.mdi-bag-checked::before{content:"\F0F3D"}.mdi-bag-personal::before{content:"\F0E10"}.mdi-bag-personal-off::before{content:"\F0E11"}.mdi-bag-personal-off-outline::before{content:"\F0E12"}.mdi-bag-personal-outline::before{content:"\F0E13"}.mdi-bag-personal-plus::before{content:"\F1CA4"}.mdi-bag-personal-plus-outline::before{content:"\F1CA5"}.mdi-bag-personal-tag::before{content:"\F1B0C"}.mdi-bag-personal-tag-outline::before{content:"\F1B0D"}.mdi-bag-suitcase::before{content:"\F158B"}.mdi-bag-suitcase-off::before{content:"\F158D"}.mdi-bag-suitcase-off-outline::before{content:"\F158E"}.mdi-bag-suitcase-outline::before{content:"\F158C"}.mdi-baguette::before{content:"\F0F3E"}.mdi-balcony::before{content:"\F1817"}.mdi-balloon::before{content:"\F0A26"}.mdi-ballot::before{content:"\F09C9"}.mdi-ballot-outline::before{content:"\F09CA"}.mdi-ballot-recount::before{content:"\F0C39"}.mdi-ballot-recount-outline::before{content:"\F0C3A"}.mdi-bandage::before{content:"\F0DAF"}.mdi-bank::before{content:"\F0070"}.mdi-bank-check::before{content:"\F1655"}.mdi-bank-circle::before{content:"\F1C03"}.mdi-bank-circle-outline::before{content:"\F1C04"}.mdi-bank-minus::before{content:"\F0DB0"}.mdi-bank-off::before{content:"\F1656"}.mdi-bank-off-outline::before{content:"\F1657"}.mdi-bank-outline::before{content:"\F0E80"}.mdi-bank-plus::before{content:"\F0DB1"}.mdi-bank-remove::before{content:"\F0DB2"}.mdi-bank-transfer::before{content:"\F0A27"}.mdi-bank-transfer-in::before{content:"\F0A28"}.mdi-bank-transfer-out::before{content:"\F0A29"}.mdi-barcode::before{content:"\F0071"}.mdi-barcode-off::before{content:"\F1236"}.mdi-barcode-scan::before{content:"\F0072"}.mdi-barley::before{content:"\F0073"}.mdi-barley-off::before{content:"\F0B5D"}.mdi-barn::before{content:"\F0B5E"}.mdi-barrel::before{content:"\F0074"}.mdi-barrel-outline::before{content:"\F1A28"}.mdi-baseball::before{content:"\F0852"}.mdi-baseball-bat::before{content:"\F0853"}.mdi-baseball-diamond::before{content:"\F15EC"}.mdi-baseball-diamond-outline::before{content:"\F15ED"}.mdi-baseball-outline::before{content:"\F1C5A"}.mdi-bash::before{content:"\F1183"}.mdi-basket::before{content:"\F0076"}.mdi-basket-check::before{content:"\F18E5"}.mdi-basket-check-outline::before{content:"\F18E6"}.mdi-basket-fill::before{content:"\F0077"}.mdi-basket-minus::before{content:"\F1523"}.mdi-basket-minus-outline::before{content:"\F1524"}.mdi-basket-off::before{content:"\F1525"}.mdi-basket-off-outline::before{content:"\F1526"}.mdi-basket-outline::before{content:"\F1181"}.mdi-basket-plus::before{content:"\F1527"}.mdi-basket-plus-outline::before{content:"\F1528"}.mdi-basket-remove::before{content:"\F1529"}.mdi-basket-remove-outline::before{content:"\F152A"}.mdi-basket-unfill::before{content:"\F0078"}.mdi-basketball::before{content:"\F0806"}.mdi-basketball-hoop::before{content:"\F0C3B"}.mdi-basketball-hoop-outline::before{content:"\F0C3C"}.mdi-bat::before{content:"\F0B5F"}.mdi-bathtub::before{content:"\F1818"}.mdi-bathtub-outline::before{content:"\F1819"}.mdi-battery::before{content:"\F0079"}.mdi-battery-10::before{content:"\F007A"}.mdi-battery-10-bluetooth::before{content:"\F093E"}.mdi-battery-20::before{content:"\F007B"}.mdi-battery-20-bluetooth::before{content:"\F093F"}.mdi-battery-30::before{content:"\F007C"}.mdi-battery-30-bluetooth::before{content:"\F0940"}.mdi-battery-40::before{content:"\F007D"}.mdi-battery-40-bluetooth::before{content:"\F0941"}.mdi-battery-50::before{content:"\F007E"}.mdi-battery-50-bluetooth::before{content:"\F0942"}.mdi-battery-60::before{content:"\F007F"}.mdi-battery-60-bluetooth::before{content:"\F0943"}.mdi-battery-70::before{content:"\F0080"}.mdi-battery-70-bluetooth::before{content:"\F0944"}.mdi-battery-80::before{content:"\F0081"}.mdi-battery-80-bluetooth::before{content:"\F0945"}.mdi-battery-90::before{content:"\F0082"}.mdi-battery-90-bluetooth::before{content:"\F0946"}.mdi-battery-alert::before{content:"\F0083"}.mdi-battery-alert-bluetooth::before{content:"\F0947"}.mdi-battery-alert-variant::before{content:"\F10CC"}.mdi-battery-alert-variant-outline::before{content:"\F10CD"}.mdi-battery-arrow-down::before{content:"\F17DE"}.mdi-battery-arrow-down-outline::before{content:"\F17DF"}.mdi-battery-arrow-up::before{content:"\F17E0"}.mdi-battery-arrow-up-outline::before{content:"\F17E1"}.mdi-battery-bluetooth::before{content:"\F0948"}.mdi-battery-bluetooth-variant::before{content:"\F0949"}.mdi-battery-charging::before{content:"\F0084"}.mdi-battery-charging-10::before{content:"\F089C"}.mdi-battery-charging-100::before{content:"\F0085"}.mdi-battery-charging-20::before{content:"\F0086"}.mdi-battery-charging-30::before{content:"\F0087"}.mdi-battery-charging-40::before{content:"\F0088"}.mdi-battery-charging-50::before{content:"\F089D"}.mdi-battery-charging-60::before{content:"\F0089"}.mdi-battery-charging-70::before{content:"\F089E"}.mdi-battery-charging-80::before{content:"\F008A"}.mdi-battery-charging-90::before{content:"\F008B"}.mdi-battery-charging-high::before{content:"\F12A6"}.mdi-battery-charging-low::before{content:"\F12A4"}.mdi-battery-charging-medium::before{content:"\F12A5"}.mdi-battery-charging-outline::before{content:"\F089F"}.mdi-battery-charging-wireless::before{content:"\F0807"}.mdi-battery-charging-wireless-10::before{content:"\F0808"}.mdi-battery-charging-wireless-20::before{content:"\F0809"}.mdi-battery-charging-wireless-30::before{content:"\F080A"}.mdi-battery-charging-wireless-40::before{content:"\F080B"}.mdi-battery-charging-wireless-50::before{content:"\F080C"}.mdi-battery-charging-wireless-60::before{content:"\F080D"}.mdi-battery-charging-wireless-70::before{content:"\F080E"}.mdi-battery-charging-wireless-80::before{content:"\F080F"}.mdi-battery-charging-wireless-90::before{content:"\F0810"}.mdi-battery-charging-wireless-alert::before{content:"\F0811"}.mdi-battery-charging-wireless-outline::before{content:"\F0812"}.mdi-battery-check::before{content:"\F17E2"}.mdi-battery-check-outline::before{content:"\F17E3"}.mdi-battery-clock::before{content:"\F19E5"}.mdi-battery-clock-outline::before{content:"\F19E6"}.mdi-battery-heart::before{content:"\F120F"}.mdi-battery-heart-outline::before{content:"\F1210"}.mdi-battery-heart-variant::before{content:"\F1211"}.mdi-battery-high::before{content:"\F12A3"}.mdi-battery-lock::before{content:"\F179C"}.mdi-battery-lock-open::before{content:"\F179D"}.mdi-battery-low::before{content:"\F12A1"}.mdi-battery-medium::before{content:"\F12A2"}.mdi-battery-minus::before{content:"\F17E4"}.mdi-battery-minus-outline::before{content:"\F17E5"}.mdi-battery-minus-variant::before{content:"\F008C"}.mdi-battery-negative::before{content:"\F008D"}.mdi-battery-off::before{content:"\F125D"}.mdi-battery-off-outline::before{content:"\F125E"}.mdi-battery-outline::before{content:"\F008E"}.mdi-battery-plus::before{content:"\F17E6"}.mdi-battery-plus-outline::before{content:"\F17E7"}.mdi-battery-plus-variant::before{content:"\F008F"}.mdi-battery-positive::before{content:"\F0090"}.mdi-battery-remove::before{content:"\F17E8"}.mdi-battery-remove-outline::before{content:"\F17E9"}.mdi-battery-sync::before{content:"\F1834"}.mdi-battery-sync-outline::before{content:"\F1835"}.mdi-battery-unknown::before{content:"\F0091"}.mdi-battery-unknown-bluetooth::before{content:"\F094A"}.mdi-beach::before{content:"\F0092"}.mdi-beaker::before{content:"\F0CEA"}.mdi-beaker-alert::before{content:"\F1229"}.mdi-beaker-alert-outline::before{content:"\F122A"}.mdi-beaker-check::before{content:"\F122B"}.mdi-beaker-check-outline::before{content:"\F122C"}.mdi-beaker-minus::before{content:"\F122D"}.mdi-beaker-minus-outline::before{content:"\F122E"}.mdi-beaker-outline::before{content:"\F0690"}.mdi-beaker-plus::before{content:"\F122F"}.mdi-beaker-plus-outline::before{content:"\F1230"}.mdi-beaker-question::before{content:"\F1231"}.mdi-beaker-question-outline::before{content:"\F1232"}.mdi-beaker-remove::before{content:"\F1233"}.mdi-beaker-remove-outline::before{content:"\F1234"}.mdi-bed::before{content:"\F02E3"}.mdi-bed-clock::before{content:"\F1B94"}.mdi-bed-double::before{content:"\F0FD4"}.mdi-bed-double-outline::before{content:"\F0FD3"}.mdi-bed-empty::before{content:"\F08A0"}.mdi-bed-king::before{content:"\F0FD2"}.mdi-bed-king-outline::before{content:"\F0FD1"}.mdi-bed-outline::before{content:"\F0099"}.mdi-bed-queen::before{content:"\F0FD0"}.mdi-bed-queen-outline::before{content:"\F0FDB"}.mdi-bed-single::before{content:"\F106D"}.mdi-bed-single-outline::before{content:"\F106E"}.mdi-bee::before{content:"\F0FA1"}.mdi-bee-flower::before{content:"\F0FA2"}.mdi-beehive-off-outline::before{content:"\F13ED"}.mdi-beehive-outline::before{content:"\F10CE"}.mdi-beekeeper::before{content:"\F14E2"}.mdi-beer::before{content:"\F0098"}.mdi-beer-outline::before{content:"\F130C"}.mdi-bell::before{content:"\F009A"}.mdi-bell-alert::before{content:"\F0D59"}.mdi-bell-alert-outline::before{content:"\F0E81"}.mdi-bell-badge::before{content:"\F116B"}.mdi-bell-badge-outline::before{content:"\F0178"}.mdi-bell-cancel::before{content:"\F13E7"}.mdi-bell-cancel-outline::before{content:"\F13E8"}.mdi-bell-check::before{content:"\F11E5"}.mdi-bell-check-outline::before{content:"\F11E6"}.mdi-bell-circle::before{content:"\F0D5A"}.mdi-bell-circle-outline::before{content:"\F0D5B"}.mdi-bell-cog::before{content:"\F1A29"}.mdi-bell-cog-outline::before{content:"\F1A2A"}.mdi-bell-minus::before{content:"\F13E9"}.mdi-bell-minus-outline::before{content:"\F13EA"}.mdi-bell-off::before{content:"\F009B"}.mdi-bell-off-outline::before{content:"\F0A91"}.mdi-bell-outline::before{content:"\F009C"}.mdi-bell-plus::before{content:"\F009D"}.mdi-bell-plus-outline::before{content:"\F0A92"}.mdi-bell-remove::before{content:"\F13EB"}.mdi-bell-remove-outline::before{content:"\F13EC"}.mdi-bell-ring::before{content:"\F009E"}.mdi-bell-ring-outline::before{content:"\F009F"}.mdi-bell-sleep::before{content:"\F00A0"}.mdi-bell-sleep-outline::before{content:"\F0A93"}.mdi-bench::before{content:"\F1C21"}.mdi-bench-back::before{content:"\F1C22"}.mdi-beta::before{content:"\F00A1"}.mdi-betamax::before{content:"\F09CB"}.mdi-biathlon::before{content:"\F0E14"}.mdi-bicycle::before{content:"\F109C"}.mdi-bicycle-basket::before{content:"\F1235"}.mdi-bicycle-cargo::before{content:"\F189C"}.mdi-bicycle-electric::before{content:"\F15B4"}.mdi-bicycle-penny-farthing::before{content:"\F15E9"}.mdi-bike::before{content:"\F00A3"}.mdi-bike-fast::before{content:"\F111F"}.mdi-bike-pedal::before{content:"\F1C23"}.mdi-bike-pedal-clipless::before{content:"\F1C24"}.mdi-bike-pedal-mountain::before{content:"\F1C25"}.mdi-billboard::before{content:"\F1010"}.mdi-billiards::before{content:"\F0B61"}.mdi-billiards-rack::before{content:"\F0B62"}.mdi-binoculars::before{content:"\F00A5"}.mdi-bio::before{content:"\F00A6"}.mdi-biohazard::before{content:"\F00A7"}.mdi-bird::before{content:"\F15C6"}.mdi-bitbucket::before{content:"\F00A8"}.mdi-bitcoin::before{content:"\F0813"}.mdi-black-mesa::before{content:"\F00A9"}.mdi-blender::before{content:"\F0CEB"}.mdi-blender-outline::before{content:"\F181A"}.mdi-blender-software::before{content:"\F00AB"}.mdi-blinds::before{content:"\F00AC"}.mdi-blinds-horizontal::before{content:"\F1A2B"}.mdi-blinds-horizontal-closed::before{content:"\F1A2C"}.mdi-blinds-open::before{content:"\F1011"}.mdi-blinds-vertical::before{content:"\F1A2D"}.mdi-blinds-vertical-closed::before{content:"\F1A2E"}.mdi-block-helper::before{content:"\F00AD"}.mdi-blood-bag::before{content:"\F0CEC"}.mdi-bluetooth::before{content:"\F00AF"}.mdi-bluetooth-audio::before{content:"\F00B0"}.mdi-bluetooth-connect::before{content:"\F00B1"}.mdi-bluetooth-off::before{content:"\F00B2"}.mdi-bluetooth-settings::before{content:"\F00B3"}.mdi-bluetooth-transfer::before{content:"\F00B4"}.mdi-blur::before{content:"\F00B5"}.mdi-blur-linear::before{content:"\F00B6"}.mdi-blur-off::before{content:"\F00B7"}.mdi-blur-radial::before{content:"\F00B8"}.mdi-bolt::before{content:"\F0DB3"}.mdi-bomb::before{content:"\F0691"}.mdi-bomb-off::before{content:"\F06C5"}.mdi-bone::before{content:"\F00B9"}.mdi-bone-off::before{content:"\F19E0"}.mdi-book::before{content:"\F00BA"}.mdi-book-account::before{content:"\F13AD"}.mdi-book-account-outline::before{content:"\F13AE"}.mdi-book-alert::before{content:"\F167C"}.mdi-book-alert-outline::before{content:"\F167D"}.mdi-book-alphabet::before{content:"\F061D"}.mdi-book-arrow-down::before{content:"\F167E"}.mdi-book-arrow-down-outline::before{content:"\F167F"}.mdi-book-arrow-left::before{content:"\F1680"}.mdi-book-arrow-left-outline::before{content:"\F1681"}.mdi-book-arrow-right::before{content:"\F1682"}.mdi-book-arrow-right-outline::before{content:"\F1683"}.mdi-book-arrow-up::before{content:"\F1684"}.mdi-book-arrow-up-outline::before{content:"\F1685"}.mdi-book-cancel::before{content:"\F1686"}.mdi-book-cancel-outline::before{content:"\F1687"}.mdi-book-check::before{content:"\F14F3"}.mdi-book-check-outline::before{content:"\F14F4"}.mdi-book-clock::before{content:"\F1688"}.mdi-book-clock-outline::before{content:"\F1689"}.mdi-book-cog::before{content:"\F168A"}.mdi-book-cog-outline::before{content:"\F168B"}.mdi-book-cross::before{content:"\F00A2"}.mdi-book-edit::before{content:"\F168C"}.mdi-book-edit-outline::before{content:"\F168D"}.mdi-book-education::before{content:"\F16C9"}.mdi-book-education-outline::before{content:"\F16CA"}.mdi-book-heart::before{content:"\F1A1D"}.mdi-book-heart-outline::before{content:"\F1A1E"}.mdi-book-information-variant::before{content:"\F106F"}.mdi-book-lock::before{content:"\F079A"}.mdi-book-lock-open::before{content:"\F079B"}.mdi-book-lock-open-outline::before{content:"\F168E"}.mdi-book-lock-outline::before{content:"\F168F"}.mdi-book-marker::before{content:"\F1690"}.mdi-book-marker-outline::before{content:"\F1691"}.mdi-book-minus::before{content:"\F05D9"}.mdi-book-minus-multiple::before{content:"\F0A94"}.mdi-book-minus-multiple-outline::before{content:"\F090B"}.mdi-book-minus-outline::before{content:"\F1692"}.mdi-book-multiple::before{content:"\F00BB"}.mdi-book-multiple-outline::before{content:"\F0436"}.mdi-book-music::before{content:"\F0067"}.mdi-book-music-outline::before{content:"\F1693"}.mdi-book-off::before{content:"\F1694"}.mdi-book-off-outline::before{content:"\F1695"}.mdi-book-open::before{content:"\F00BD"}.mdi-book-open-blank-variant::before{content:"\F00BE"}.mdi-book-open-blank-variant-outline::before{content:"\F1CCB"}.mdi-book-open-outline::before{content:"\F0B63"}.mdi-book-open-page-variant::before{content:"\F05DA"}.mdi-book-open-page-variant-outline::before{content:"\F15D6"}.mdi-book-open-variant::before{content:"\F14F7"}.mdi-book-open-variant-outline::before{content:"\F1CCC"}.mdi-book-outline::before{content:"\F0B64"}.mdi-book-play::before{content:"\F0E82"}.mdi-book-play-outline::before{content:"\F0E83"}.mdi-book-plus::before{content:"\F05DB"}.mdi-book-plus-multiple::before{content:"\F0A95"}.mdi-book-plus-multiple-outline::before{content:"\F0ADE"}.mdi-book-plus-outline::before{content:"\F1696"}.mdi-book-refresh::before{content:"\F1697"}.mdi-book-refresh-outline::before{content:"\F1698"}.mdi-book-remove::before{content:"\F0A97"}.mdi-book-remove-multiple::before{content:"\F0A96"}.mdi-book-remove-multiple-outline::before{content:"\F04CA"}.mdi-book-remove-outline::before{content:"\F1699"}.mdi-book-search::before{content:"\F0E84"}.mdi-book-search-outline::before{content:"\F0E85"}.mdi-book-settings::before{content:"\F169A"}.mdi-book-settings-outline::before{content:"\F169B"}.mdi-book-sync::before{content:"\F169C"}.mdi-book-sync-outline::before{content:"\F16C8"}.mdi-book-variant::before{content:"\F00BF"}.mdi-bookmark::before{content:"\F00C0"}.mdi-bookmark-box::before{content:"\F1B75"}.mdi-bookmark-box-multiple::before{content:"\F196C"}.mdi-bookmark-box-multiple-outline::before{content:"\F196D"}.mdi-bookmark-box-outline::before{content:"\F1B76"}.mdi-bookmark-check::before{content:"\F00C1"}.mdi-bookmark-check-outline::before{content:"\F137B"}.mdi-bookmark-minus::before{content:"\F09CC"}.mdi-bookmark-minus-outline::before{content:"\F09CD"}.mdi-bookmark-multiple::before{content:"\F0E15"}.mdi-bookmark-multiple-outline::before{content:"\F0E16"}.mdi-bookmark-music::before{content:"\F00C2"}.mdi-bookmark-music-outline::before{content:"\F1379"}.mdi-bookmark-off::before{content:"\F09CE"}.mdi-bookmark-off-outline::before{content:"\F09CF"}.mdi-bookmark-outline::before{content:"\F00C3"}.mdi-bookmark-plus::before{content:"\F00C5"}.mdi-bookmark-plus-outline::before{content:"\F00C4"}.mdi-bookmark-remove::before{content:"\F00C6"}.mdi-bookmark-remove-outline::before{content:"\F137A"}.mdi-bookshelf::before{content:"\F125F"}.mdi-boom-gate::before{content:"\F0E86"}.mdi-boom-gate-alert::before{content:"\F0E87"}.mdi-boom-gate-alert-outline::before{content:"\F0E88"}.mdi-boom-gate-arrow-down::before{content:"\F0E89"}.mdi-boom-gate-arrow-down-outline::before{content:"\F0E8A"}.mdi-boom-gate-arrow-up::before{content:"\F0E8C"}.mdi-boom-gate-arrow-up-outline::before{content:"\F0E8D"}.mdi-boom-gate-outline::before{content:"\F0E8B"}.mdi-boom-gate-up::before{content:"\F17F9"}.mdi-boom-gate-up-outline::before{content:"\F17FA"}.mdi-boombox::before{content:"\F05DC"}.mdi-boomerang::before{content:"\F10CF"}.mdi-bootstrap::before{content:"\F06C6"}.mdi-border-all::before{content:"\F00C7"}.mdi-border-all-variant::before{content:"\F08A1"}.mdi-border-bottom::before{content:"\F00C8"}.mdi-border-bottom-variant::before{content:"\F08A2"}.mdi-border-color::before{content:"\F00C9"}.mdi-border-horizontal::before{content:"\F00CA"}.mdi-border-inside::before{content:"\F00CB"}.mdi-border-left::before{content:"\F00CC"}.mdi-border-left-variant::before{content:"\F08A3"}.mdi-border-none::before{content:"\F00CD"}.mdi-border-none-variant::before{content:"\F08A4"}.mdi-border-outside::before{content:"\F00CE"}.mdi-border-radius::before{content:"\F1AF4"}.mdi-border-right::before{content:"\F00CF"}.mdi-border-right-variant::before{content:"\F08A5"}.mdi-border-style::before{content:"\F00D0"}.mdi-border-top::before{content:"\F00D1"}.mdi-border-top-variant::before{content:"\F08A6"}.mdi-border-vertical::before{content:"\F00D2"}.mdi-bottle-soda::before{content:"\F1070"}.mdi-bottle-soda-classic::before{content:"\F1071"}.mdi-bottle-soda-classic-outline::before{content:"\F1363"}.mdi-bottle-soda-outline::before{content:"\F1072"}.mdi-bottle-tonic::before{content:"\F112E"}.mdi-bottle-tonic-outline::before{content:"\F112F"}.mdi-bottle-tonic-plus::before{content:"\F1130"}.mdi-bottle-tonic-plus-outline::before{content:"\F1131"}.mdi-bottle-tonic-skull::before{content:"\F1132"}.mdi-bottle-tonic-skull-outline::before{content:"\F1133"}.mdi-bottle-wine::before{content:"\F0854"}.mdi-bottle-wine-outline::before{content:"\F1310"}.mdi-bow-arrow::before{content:"\F1841"}.mdi-bow-tie::before{content:"\F0678"}.mdi-bowl::before{content:"\F028E"}.mdi-bowl-mix::before{content:"\F0617"}.mdi-bowl-mix-outline::before{content:"\F02E4"}.mdi-bowl-outline::before{content:"\F02A9"}.mdi-bowling::before{content:"\F00D3"}.mdi-box::before{content:"\F00D4"}.mdi-box-cutter::before{content:"\F00D5"}.mdi-box-cutter-off::before{content:"\F0B4A"}.mdi-box-shadow::before{content:"\F0637"}.mdi-boxing-glove::before{content:"\F0B65"}.mdi-braille::before{content:"\F09D0"}.mdi-brain::before{content:"\F09D1"}.mdi-bread-slice::before{content:"\F0CEE"}.mdi-bread-slice-outline::before{content:"\F0CEF"}.mdi-bridge::before{content:"\F0618"}.mdi-briefcase::before{content:"\F00D6"}.mdi-briefcase-account::before{content:"\F0CF0"}.mdi-briefcase-account-outline::before{content:"\F0CF1"}.mdi-briefcase-arrow-left-right::before{content:"\F1A8D"}.mdi-briefcase-arrow-left-right-outline::before{content:"\F1A8E"}.mdi-briefcase-arrow-up-down::before{content:"\F1A8F"}.mdi-briefcase-arrow-up-down-outline::before{content:"\F1A90"}.mdi-briefcase-check::before{content:"\F00D7"}.mdi-briefcase-check-outline::before{content:"\F131E"}.mdi-briefcase-clock::before{content:"\F10D0"}.mdi-briefcase-clock-outline::before{content:"\F10D1"}.mdi-briefcase-download::before{content:"\F00D8"}.mdi-briefcase-download-outline::before{content:"\F0C3D"}.mdi-briefcase-edit::before{content:"\F0A98"}.mdi-briefcase-edit-outline::before{content:"\F0C3E"}.mdi-briefcase-eye::before{content:"\F17D9"}.mdi-briefcase-eye-outline::before{content:"\F17DA"}.mdi-briefcase-minus::before{content:"\F0A2A"}.mdi-briefcase-minus-outline::before{content:"\F0C3F"}.mdi-briefcase-off::before{content:"\F1658"}.mdi-briefcase-off-outline::before{content:"\F1659"}.mdi-briefcase-outline::before{content:"\F0814"}.mdi-briefcase-plus::before{content:"\F0A2B"}.mdi-briefcase-plus-outline::before{content:"\F0C40"}.mdi-briefcase-remove::before{content:"\F0A2C"}.mdi-briefcase-remove-outline::before{content:"\F0C41"}.mdi-briefcase-search::before{content:"\F0A2D"}.mdi-briefcase-search-outline::before{content:"\F0C42"}.mdi-briefcase-upload::before{content:"\F00D9"}.mdi-briefcase-upload-outline::before{content:"\F0C43"}.mdi-briefcase-variant::before{content:"\F1494"}.mdi-briefcase-variant-off::before{content:"\F165A"}.mdi-briefcase-variant-off-outline::before{content:"\F165B"}.mdi-briefcase-variant-outline::before{content:"\F1495"}.mdi-brightness-1::before{content:"\F00DA"}.mdi-brightness-2::before{content:"\F00DB"}.mdi-brightness-3::before{content:"\F00DC"}.mdi-brightness-4::before{content:"\F00DD"}.mdi-brightness-5::before{content:"\F00DE"}.mdi-brightness-6::before{content:"\F00DF"}.mdi-brightness-7::before{content:"\F00E0"}.mdi-brightness-auto::before{content:"\F00E1"}.mdi-brightness-percent::before{content:"\F0CF2"}.mdi-broadcast::before{content:"\F1720"}.mdi-broadcast-off::before{content:"\F1721"}.mdi-broom::before{content:"\F00E2"}.mdi-brush::before{content:"\F00E3"}.mdi-brush-off::before{content:"\F1771"}.mdi-brush-outline::before{content:"\F1A0D"}.mdi-brush-variant::before{content:"\F1813"}.mdi-bucket::before{content:"\F1415"}.mdi-bucket-outline::before{content:"\F1416"}.mdi-buffet::before{content:"\F0578"}.mdi-bug::before{content:"\F00E4"}.mdi-bug-check::before{content:"\F0A2E"}.mdi-bug-check-outline::before{content:"\F0A2F"}.mdi-bug-outline::before{content:"\F0A30"}.mdi-bug-pause::before{content:"\F1AF5"}.mdi-bug-pause-outline::before{content:"\F1AF6"}.mdi-bug-play::before{content:"\F1AF7"}.mdi-bug-play-outline::before{content:"\F1AF8"}.mdi-bug-stop::before{content:"\F1AF9"}.mdi-bug-stop-outline::before{content:"\F1AFA"}.mdi-bugle::before{content:"\F0DB4"}.mdi-bulkhead-light::before{content:"\F1A2F"}.mdi-bulldozer::before{content:"\F0B22"}.mdi-bullet::before{content:"\F0CF3"}.mdi-bulletin-board::before{content:"\F00E5"}.mdi-bullhorn::before{content:"\F00E6"}.mdi-bullhorn-outline::before{content:"\F0B23"}.mdi-bullhorn-variant::before{content:"\F196E"}.mdi-bullhorn-variant-outline::before{content:"\F196F"}.mdi-bullseye::before{content:"\F05DD"}.mdi-bullseye-arrow::before{content:"\F08C9"}.mdi-bulma::before{content:"\F12E7"}.mdi-bunk-bed::before{content:"\F1302"}.mdi-bunk-bed-outline::before{content:"\F0097"}.mdi-bus::before{content:"\F00E7"}.mdi-bus-alert::before{content:"\F0A99"}.mdi-bus-articulated-end::before{content:"\F079C"}.mdi-bus-articulated-front::before{content:"\F079D"}.mdi-bus-clock::before{content:"\F08CA"}.mdi-bus-double-decker::before{content:"\F079E"}.mdi-bus-electric::before{content:"\F191D"}.mdi-bus-marker::before{content:"\F1212"}.mdi-bus-multiple::before{content:"\F0F3F"}.mdi-bus-school::before{content:"\F079F"}.mdi-bus-side::before{content:"\F07A0"}.mdi-bus-sign::before{content:"\F1CC1"}.mdi-bus-stop::before{content:"\F1012"}.mdi-bus-stop-covered::before{content:"\F1013"}.mdi-bus-stop-uncovered::before{content:"\F1014"}.mdi-bus-wrench::before{content:"\F1CC2"}.mdi-butterfly::before{content:"\F1589"}.mdi-butterfly-outline::before{content:"\F158A"}.mdi-button-cursor::before{content:"\F1B4F"}.mdi-button-pointer::before{content:"\F1B50"}.mdi-cabin-a-frame::before{content:"\F188C"}.mdi-cable-data::before{content:"\F1394"}.mdi-cached::before{content:"\F00E8"}.mdi-cactus::before{content:"\F0DB5"}.mdi-cake::before{content:"\F00E9"}.mdi-cake-layered::before{content:"\F00EA"}.mdi-cake-variant::before{content:"\F00EB"}.mdi-cake-variant-outline::before{content:"\F17F0"}.mdi-calculator::before{content:"\F00EC"}.mdi-calculator-variant::before{content:"\F0A9A"}.mdi-calculator-variant-outline::before{content:"\F15A6"}.mdi-calendar::before{content:"\F00ED"}.mdi-calendar-account::before{content:"\F0ED7"}.mdi-calendar-account-outline::before{content:"\F0ED8"}.mdi-calendar-alert::before{content:"\F0A31"}.mdi-calendar-alert-outline::before{content:"\F1B62"}.mdi-calendar-arrow-left::before{content:"\F1134"}.mdi-calendar-arrow-right::before{content:"\F1135"}.mdi-calendar-badge::before{content:"\F1B9D"}.mdi-calendar-badge-outline::before{content:"\F1B9E"}.mdi-calendar-blank::before{content:"\F00EE"}.mdi-calendar-blank-multiple::before{content:"\F1073"}.mdi-calendar-blank-outline::before{content:"\F0B66"}.mdi-calendar-check::before{content:"\F00EF"}.mdi-calendar-check-outline::before{content:"\F0C44"}.mdi-calendar-clock::before{content:"\F00F0"}.mdi-calendar-clock-outline::before{content:"\F16E1"}.mdi-calendar-collapse-horizontal::before{content:"\F189D"}.mdi-calendar-collapse-horizontal-outline::before{content:"\F1B63"}.mdi-calendar-cursor::before{content:"\F157B"}.mdi-calendar-cursor-outline::before{content:"\F1B64"}.mdi-calendar-edit::before{content:"\F08A7"}.mdi-calendar-edit-outline::before{content:"\F1B65"}.mdi-calendar-end::before{content:"\F166C"}.mdi-calendar-end-outline::before{content:"\F1B66"}.mdi-calendar-expand-horizontal::before{content:"\F189E"}.mdi-calendar-expand-horizontal-outline::before{content:"\F1B67"}.mdi-calendar-export::before{content:"\F0B24"}.mdi-calendar-export-outline::before{content:"\F1B68"}.mdi-calendar-filter::before{content:"\F1A32"}.mdi-calendar-filter-outline::before{content:"\F1A33"}.mdi-calendar-heart::before{content:"\F09D2"}.mdi-calendar-heart-outline::before{content:"\F1B69"}.mdi-calendar-import::before{content:"\F0B25"}.mdi-calendar-import-outline::before{content:"\F1B6A"}.mdi-calendar-lock::before{content:"\F1641"}.mdi-calendar-lock-open::before{content:"\F1B5B"}.mdi-calendar-lock-open-outline::before{content:"\F1B5C"}.mdi-calendar-lock-outline::before{content:"\F1642"}.mdi-calendar-minus::before{content:"\F0D5C"}.mdi-calendar-minus-outline::before{content:"\F1B6B"}.mdi-calendar-month::before{content:"\F0E17"}.mdi-calendar-month-outline::before{content:"\F0E18"}.mdi-calendar-multiple::before{content:"\F00F1"}.mdi-calendar-multiple-check::before{content:"\F00F2"}.mdi-calendar-multiselect::before{content:"\F0A32"}.mdi-calendar-multiselect-outline::before{content:"\F1B55"}.mdi-calendar-outline::before{content:"\F0B67"}.mdi-calendar-plus::before{content:"\F00F3"}.mdi-calendar-plus-outline::before{content:"\F1B6C"}.mdi-calendar-question::before{content:"\F0692"}.mdi-calendar-question-outline::before{content:"\F1B6D"}.mdi-calendar-range::before{content:"\F0679"}.mdi-calendar-range-outline::before{content:"\F0B68"}.mdi-calendar-refresh::before{content:"\F01E1"}.mdi-calendar-refresh-outline::before{content:"\F0203"}.mdi-calendar-remove::before{content:"\F00F4"}.mdi-calendar-remove-outline::before{content:"\F0C45"}.mdi-calendar-search::before{content:"\F094C"}.mdi-calendar-search-outline::before{content:"\F1B6E"}.mdi-calendar-star::before{content:"\F09D3"}.mdi-calendar-star-four-points::before{content:"\F1C1F"}.mdi-calendar-star-outline::before{content:"\F1B53"}.mdi-calendar-start::before{content:"\F166D"}.mdi-calendar-start-outline::before{content:"\F1B6F"}.mdi-calendar-sync::before{content:"\F0E8E"}.mdi-calendar-sync-outline::before{content:"\F0E8F"}.mdi-calendar-text::before{content:"\F00F5"}.mdi-calendar-text-outline::before{content:"\F0C46"}.mdi-calendar-today::before{content:"\F00F6"}.mdi-calendar-today-outline::before{content:"\F1A30"}.mdi-calendar-week::before{content:"\F0A33"}.mdi-calendar-week-begin::before{content:"\F0A34"}.mdi-calendar-week-begin-outline::before{content:"\F1A31"}.mdi-calendar-week-outline::before{content:"\F1A34"}.mdi-calendar-weekend::before{content:"\F0ED9"}.mdi-calendar-weekend-outline::before{content:"\F0EDA"}.mdi-call-made::before{content:"\F00F7"}.mdi-call-merge::before{content:"\F00F8"}.mdi-call-missed::before{content:"\F00F9"}.mdi-call-received::before{content:"\F00FA"}.mdi-call-split::before{content:"\F00FB"}.mdi-camcorder::before{content:"\F00FC"}.mdi-camcorder-off::before{content:"\F00FF"}.mdi-camera::before{content:"\F0100"}.mdi-camera-account::before{content:"\F08CB"}.mdi-camera-burst::before{content:"\F0693"}.mdi-camera-control::before{content:"\F0B69"}.mdi-camera-document::before{content:"\F1871"}.mdi-camera-document-off::before{content:"\F1872"}.mdi-camera-enhance::before{content:"\F0101"}.mdi-camera-enhance-outline::before{content:"\F0B6A"}.mdi-camera-flip::before{content:"\F15D9"}.mdi-camera-flip-outline::before{content:"\F15DA"}.mdi-camera-front::before{content:"\F0102"}.mdi-camera-front-variant::before{content:"\F0103"}.mdi-camera-gopro::before{content:"\F07A1"}.mdi-camera-image::before{content:"\F08CC"}.mdi-camera-iris::before{content:"\F0104"}.mdi-camera-lock::before{content:"\F1A14"}.mdi-camera-lock-open::before{content:"\F1C0D"}.mdi-camera-lock-open-outline::before{content:"\F1C0E"}.mdi-camera-lock-outline::before{content:"\F1A15"}.mdi-camera-marker::before{content:"\F19A7"}.mdi-camera-marker-outline::before{content:"\F19A8"}.mdi-camera-metering-center::before{content:"\F07A2"}.mdi-camera-metering-matrix::before{content:"\F07A3"}.mdi-camera-metering-partial::before{content:"\F07A4"}.mdi-camera-metering-spot::before{content:"\F07A5"}.mdi-camera-off::before{content:"\F05DF"}.mdi-camera-off-outline::before{content:"\F19BF"}.mdi-camera-outline::before{content:"\F0D5D"}.mdi-camera-party-mode::before{content:"\F0105"}.mdi-camera-plus::before{content:"\F0EDB"}.mdi-camera-plus-outline::before{content:"\F0EDC"}.mdi-camera-rear::before{content:"\F0106"}.mdi-camera-rear-variant::before{content:"\F0107"}.mdi-camera-retake::before{content:"\F0E19"}.mdi-camera-retake-outline::before{content:"\F0E1A"}.mdi-camera-switch::before{content:"\F0108"}.mdi-camera-switch-outline::before{content:"\F084A"}.mdi-camera-timer::before{content:"\F0109"}.mdi-camera-wireless::before{content:"\F0DB6"}.mdi-camera-wireless-outline::before{content:"\F0DB7"}.mdi-campfire::before{content:"\F0EDD"}.mdi-cancel::before{content:"\F073A"}.mdi-candelabra::before{content:"\F17D2"}.mdi-candelabra-fire::before{content:"\F17D3"}.mdi-candle::before{content:"\F05E2"}.mdi-candy::before{content:"\F1970"}.mdi-candy-off::before{content:"\F1971"}.mdi-candy-off-outline::before{content:"\F1972"}.mdi-candy-outline::before{content:"\F1973"}.mdi-candycane::before{content:"\F010A"}.mdi-cannabis::before{content:"\F07A6"}.mdi-cannabis-off::before{content:"\F166E"}.mdi-caps-lock::before{content:"\F0A9B"}.mdi-car::before{content:"\F010B"}.mdi-car-2-plus::before{content:"\F1015"}.mdi-car-3-plus::before{content:"\F1016"}.mdi-car-arrow-left::before{content:"\F13B2"}.mdi-car-arrow-right::before{content:"\F13B3"}.mdi-car-back::before{content:"\F0E1B"}.mdi-car-battery::before{content:"\F010C"}.mdi-car-brake-abs::before{content:"\F0C47"}.mdi-car-brake-alert::before{content:"\F0C48"}.mdi-car-brake-fluid-level::before{content:"\F1909"}.mdi-car-brake-hold::before{content:"\F0D5E"}.mdi-car-brake-low-pressure::before{content:"\F190A"}.mdi-car-brake-parking::before{content:"\F0D5F"}.mdi-car-brake-retarder::before{content:"\F1017"}.mdi-car-brake-temperature::before{content:"\F190B"}.mdi-car-brake-worn-linings::before{content:"\F190C"}.mdi-car-child-seat::before{content:"\F0FA3"}.mdi-car-clock::before{content:"\F1974"}.mdi-car-clutch::before{content:"\F1018"}.mdi-car-cog::before{content:"\F13CC"}.mdi-car-connected::before{content:"\F010D"}.mdi-car-convertible::before{content:"\F07A7"}.mdi-car-coolant-level::before{content:"\F1019"}.mdi-car-cruise-control::before{content:"\F0D60"}.mdi-car-defrost-front::before{content:"\F0D61"}.mdi-car-defrost-rear::before{content:"\F0D62"}.mdi-car-door::before{content:"\F0B6B"}.mdi-car-door-lock::before{content:"\F109D"}.mdi-car-door-lock-open::before{content:"\F1C81"}.mdi-car-electric::before{content:"\F0B6C"}.mdi-car-electric-outline::before{content:"\F15B5"}.mdi-car-emergency::before{content:"\F160F"}.mdi-car-esp::before{content:"\F0C49"}.mdi-car-estate::before{content:"\F07A8"}.mdi-car-hatchback::before{content:"\F07A9"}.mdi-car-info::before{content:"\F11BE"}.mdi-car-key::before{content:"\F0B6D"}.mdi-car-lifted-pickup::before{content:"\F152D"}.mdi-car-light-alert::before{content:"\F190D"}.mdi-car-light-dimmed::before{content:"\F0C4A"}.mdi-car-light-fog::before{content:"\F0C4B"}.mdi-car-light-high::before{content:"\F0C4C"}.mdi-car-limousine::before{content:"\F08CD"}.mdi-car-multiple::before{content:"\F0B6E"}.mdi-car-off::before{content:"\F0E1C"}.mdi-car-outline::before{content:"\F14ED"}.mdi-car-parking-lights::before{content:"\F0D63"}.mdi-car-pickup::before{content:"\F07AA"}.mdi-car-search::before{content:"\F1B8D"}.mdi-car-search-outline::before{content:"\F1B8E"}.mdi-car-seat::before{content:"\F0FA4"}.mdi-car-seat-cooler::before{content:"\F0FA5"}.mdi-car-seat-heater::before{content:"\F0FA6"}.mdi-car-select::before{content:"\F1879"}.mdi-car-settings::before{content:"\F13CD"}.mdi-car-shift-pattern::before{content:"\F0F40"}.mdi-car-side::before{content:"\F07AB"}.mdi-car-speed-limiter::before{content:"\F190E"}.mdi-car-sports::before{content:"\F07AC"}.mdi-car-tire-alert::before{content:"\F0C4D"}.mdi-car-traction-control::before{content:"\F0D64"}.mdi-car-turbocharger::before{content:"\F101A"}.mdi-car-wash::before{content:"\F010E"}.mdi-car-windshield::before{content:"\F101B"}.mdi-car-windshield-outline::before{content:"\F101C"}.mdi-car-wireless::before{content:"\F1878"}.mdi-car-wrench::before{content:"\F1814"}.mdi-carabiner::before{content:"\F14C0"}.mdi-caravan::before{content:"\F07AD"}.mdi-card::before{content:"\F0B6F"}.mdi-card-account-details::before{content:"\F05D2"}.mdi-card-account-details-outline::before{content:"\F0DAB"}.mdi-card-account-details-star::before{content:"\F02A3"}.mdi-card-account-details-star-outline::before{content:"\F06DB"}.mdi-card-account-mail::before{content:"\F018E"}.mdi-card-account-mail-outline::before{content:"\F0E98"}.mdi-card-account-phone::before{content:"\F0E99"}.mdi-card-account-phone-outline::before{content:"\F0E9A"}.mdi-card-bulleted::before{content:"\F0B70"}.mdi-card-bulleted-off::before{content:"\F0B71"}.mdi-card-bulleted-off-outline::before{content:"\F0B72"}.mdi-card-bulleted-outline::before{content:"\F0B73"}.mdi-card-bulleted-settings::before{content:"\F0B74"}.mdi-card-bulleted-settings-outline::before{content:"\F0B75"}.mdi-card-minus::before{content:"\F1600"}.mdi-card-minus-outline::before{content:"\F1601"}.mdi-card-multiple::before{content:"\F17F1"}.mdi-card-multiple-outline::before{content:"\F17F2"}.mdi-card-off::before{content:"\F1602"}.mdi-card-off-outline::before{content:"\F1603"}.mdi-card-outline::before{content:"\F0B76"}.mdi-card-plus::before{content:"\F11FF"}.mdi-card-plus-outline::before{content:"\F1200"}.mdi-card-remove::before{content:"\F1604"}.mdi-card-remove-outline::before{content:"\F1605"}.mdi-card-search::before{content:"\F1074"}.mdi-card-search-outline::before{content:"\F1075"}.mdi-card-text::before{content:"\F0B77"}.mdi-card-text-outline::before{content:"\F0B78"}.mdi-cards::before{content:"\F0638"}.mdi-cards-club::before{content:"\F08CE"}.mdi-cards-club-outline::before{content:"\F189F"}.mdi-cards-diamond::before{content:"\F08CF"}.mdi-cards-diamond-outline::before{content:"\F101D"}.mdi-cards-heart::before{content:"\F08D0"}.mdi-cards-heart-outline::before{content:"\F18A0"}.mdi-cards-outline::before{content:"\F0639"}.mdi-cards-playing::before{content:"\F18A1"}.mdi-cards-playing-club::before{content:"\F18A2"}.mdi-cards-playing-club-multiple::before{content:"\F18A3"}.mdi-cards-playing-club-multiple-outline::before{content:"\F18A4"}.mdi-cards-playing-club-outline::before{content:"\F18A5"}.mdi-cards-playing-diamond::before{content:"\F18A6"}.mdi-cards-playing-diamond-multiple::before{content:"\F18A7"}.mdi-cards-playing-diamond-multiple-outline::before{content:"\F18A8"}.mdi-cards-playing-diamond-outline::before{content:"\F18A9"}.mdi-cards-playing-heart::before{content:"\F18AA"}.mdi-cards-playing-heart-multiple::before{content:"\F18AB"}.mdi-cards-playing-heart-multiple-outline::before{content:"\F18AC"}.mdi-cards-playing-heart-outline::before{content:"\F18AD"}.mdi-cards-playing-outline::before{content:"\F063A"}.mdi-cards-playing-spade::before{content:"\F18AE"}.mdi-cards-playing-spade-multiple::before{content:"\F18AF"}.mdi-cards-playing-spade-multiple-outline::before{content:"\F18B0"}.mdi-cards-playing-spade-outline::before{content:"\F18B1"}.mdi-cards-spade::before{content:"\F08D1"}.mdi-cards-spade-outline::before{content:"\F18B2"}.mdi-cards-variant::before{content:"\F06C7"}.mdi-carrot::before{content:"\F010F"}.mdi-cart::before{content:"\F0110"}.mdi-cart-arrow-down::before{content:"\F0D66"}.mdi-cart-arrow-right::before{content:"\F0C4E"}.mdi-cart-arrow-up::before{content:"\F0D67"}.mdi-cart-check::before{content:"\F15EA"}.mdi-cart-heart::before{content:"\F18E0"}.mdi-cart-minus::before{content:"\F0D68"}.mdi-cart-off::before{content:"\F066B"}.mdi-cart-outline::before{content:"\F0111"}.mdi-cart-percent::before{content:"\F1BAE"}.mdi-cart-plus::before{content:"\F0112"}.mdi-cart-remove::before{content:"\F0D69"}.mdi-cart-variant::before{content:"\F15EB"}.mdi-case-sensitive-alt::before{content:"\F0113"}.mdi-cash::before{content:"\F0114"}.mdi-cash-100::before{content:"\F0115"}.mdi-cash-check::before{content:"\F14EE"}.mdi-cash-clock::before{content:"\F1A91"}.mdi-cash-edit::before{content:"\F1CAB"}.mdi-cash-fast::before{content:"\F185C"}.mdi-cash-lock::before{content:"\F14EA"}.mdi-cash-lock-open::before{content:"\F14EB"}.mdi-cash-marker::before{content:"\F0DB8"}.mdi-cash-minus::before{content:"\F1260"}.mdi-cash-multiple::before{content:"\F0116"}.mdi-cash-off::before{content:"\F1C79"}.mdi-cash-plus::before{content:"\F1261"}.mdi-cash-refund::before{content:"\F0A9C"}.mdi-cash-register::before{content:"\F0CF4"}.mdi-cash-remove::before{content:"\F1262"}.mdi-cash-sync::before{content:"\F1A92"}.mdi-cassette::before{content:"\F09D4"}.mdi-cast::before{content:"\F0118"}.mdi-cast-audio::before{content:"\F101E"}.mdi-cast-audio-variant::before{content:"\F1749"}.mdi-cast-connected::before{content:"\F0119"}.mdi-cast-education::before{content:"\F0E1D"}.mdi-cast-off::before{content:"\F078A"}.mdi-cast-variant::before{content:"\F001F"}.mdi-castle::before{content:"\F011A"}.mdi-cat::before{content:"\F011B"}.mdi-cctv::before{content:"\F07AE"}.mdi-cctv-off::before{content:"\F185F"}.mdi-ceiling-fan::before{content:"\F1797"}.mdi-ceiling-fan-light::before{content:"\F1798"}.mdi-ceiling-light::before{content:"\F0769"}.mdi-ceiling-light-multiple::before{content:"\F18DD"}.mdi-ceiling-light-multiple-outline::before{content:"\F18DE"}.mdi-ceiling-light-outline::before{content:"\F17C7"}.mdi-cellphone::before{content:"\F011C"}.mdi-cellphone-arrow-down::before{content:"\F09D5"}.mdi-cellphone-arrow-down-variant::before{content:"\F19C5"}.mdi-cellphone-basic::before{content:"\F011E"}.mdi-cellphone-charging::before{content:"\F1397"}.mdi-cellphone-check::before{content:"\F17FD"}.mdi-cellphone-cog::before{content:"\F0951"}.mdi-cellphone-dock::before{content:"\F011F"}.mdi-cellphone-information::before{content:"\F0F41"}.mdi-cellphone-key::before{content:"\F094E"}.mdi-cellphone-link::before{content:"\F0121"}.mdi-cellphone-link-off::before{content:"\F0122"}.mdi-cellphone-lock::before{content:"\F094F"}.mdi-cellphone-marker::before{content:"\F183A"}.mdi-cellphone-message::before{content:"\F08D3"}.mdi-cellphone-message-off::before{content:"\F10D2"}.mdi-cellphone-nfc::before{content:"\F0E90"}.mdi-cellphone-nfc-off::before{content:"\F12D8"}.mdi-cellphone-off::before{content:"\F0950"}.mdi-cellphone-play::before{content:"\F101F"}.mdi-cellphone-remove::before{content:"\F094D"}.mdi-cellphone-screenshot::before{content:"\F0A35"}.mdi-cellphone-settings::before{content:"\F0123"}.mdi-cellphone-sound::before{content:"\F0952"}.mdi-cellphone-text::before{content:"\F08D2"}.mdi-cellphone-wireless::before{content:"\F0815"}.mdi-centos::before{content:"\F111A"}.mdi-certificate::before{content:"\F0124"}.mdi-certificate-outline::before{content:"\F1188"}.mdi-chair-rolling::before{content:"\F0F48"}.mdi-chair-school::before{content:"\F0125"}.mdi-chandelier::before{content:"\F1793"}.mdi-charity::before{content:"\F0C4F"}.mdi-charity-search::before{content:"\F1C82"}.mdi-chart-arc::before{content:"\F0126"}.mdi-chart-areaspline::before{content:"\F0127"}.mdi-chart-areaspline-variant::before{content:"\F0E91"}.mdi-chart-bar::before{content:"\F0128"}.mdi-chart-bar-stacked::before{content:"\F076A"}.mdi-chart-bell-curve::before{content:"\F0C50"}.mdi-chart-bell-curve-cumulative::before{content:"\F0FA7"}.mdi-chart-box::before{content:"\F154D"}.mdi-chart-box-multiple::before{content:"\F1CCD"}.mdi-chart-box-multiple-outline::before{content:"\F1CCE"}.mdi-chart-box-outline::before{content:"\F154E"}.mdi-chart-box-plus-outline::before{content:"\F154F"}.mdi-chart-bubble::before{content:"\F05E3"}.mdi-chart-donut::before{content:"\F07AF"}.mdi-chart-donut-variant::before{content:"\F07B0"}.mdi-chart-gantt::before{content:"\F066C"}.mdi-chart-histogram::before{content:"\F0129"}.mdi-chart-line::before{content:"\F012A"}.mdi-chart-line-stacked::before{content:"\F076B"}.mdi-chart-line-variant::before{content:"\F07B1"}.mdi-chart-multiline::before{content:"\F08D4"}.mdi-chart-multiple::before{content:"\F1213"}.mdi-chart-pie::before{content:"\F012B"}.mdi-chart-pie-outline::before{content:"\F1BDF"}.mdi-chart-ppf::before{content:"\F1380"}.mdi-chart-sankey::before{content:"\F11DF"}.mdi-chart-sankey-variant::before{content:"\F11E0"}.mdi-chart-scatter-plot::before{content:"\F0E92"}.mdi-chart-scatter-plot-hexbin::before{content:"\F066D"}.mdi-chart-timeline::before{content:"\F066E"}.mdi-chart-timeline-variant::before{content:"\F0E93"}.mdi-chart-timeline-variant-shimmer::before{content:"\F15B6"}.mdi-chart-tree::before{content:"\F0E94"}.mdi-chart-waterfall::before{content:"\F1918"}.mdi-chat::before{content:"\F0B79"}.mdi-chat-alert::before{content:"\F0B7A"}.mdi-chat-alert-outline::before{content:"\F12C9"}.mdi-chat-minus::before{content:"\F1410"}.mdi-chat-minus-outline::before{content:"\F1413"}.mdi-chat-outline::before{content:"\F0EDE"}.mdi-chat-plus::before{content:"\F140F"}.mdi-chat-plus-outline::before{content:"\F1412"}.mdi-chat-processing::before{content:"\F0B7B"}.mdi-chat-processing-outline::before{content:"\F12CA"}.mdi-chat-question::before{content:"\F1738"}.mdi-chat-question-outline::before{content:"\F1739"}.mdi-chat-remove::before{content:"\F1411"}.mdi-chat-remove-outline::before{content:"\F1414"}.mdi-chat-sleep::before{content:"\F12D1"}.mdi-chat-sleep-outline::before{content:"\F12D2"}.mdi-check::before{content:"\F012C"}.mdi-check-all::before{content:"\F012D"}.mdi-check-bold::before{content:"\F0E1E"}.mdi-check-circle::before{content:"\F05E0"}.mdi-check-circle-outline::before{content:"\F05E1"}.mdi-check-decagram::before{content:"\F0791"}.mdi-check-decagram-outline::before{content:"\F1740"}.mdi-check-network::before{content:"\F0C53"}.mdi-check-network-outline::before{content:"\F0C54"}.mdi-check-outline::before{content:"\F0855"}.mdi-check-underline::before{content:"\F0E1F"}.mdi-check-underline-circle::before{content:"\F0E20"}.mdi-check-underline-circle-outline::before{content:"\F0E21"}.mdi-checkbook::before{content:"\F0A9D"}.mdi-checkbook-arrow-left::before{content:"\F1C1D"}.mdi-checkbook-arrow-right::before{content:"\F1C1E"}.mdi-checkbox-blank::before{content:"\F012E"}.mdi-checkbox-blank-badge::before{content:"\F1176"}.mdi-checkbox-blank-badge-outline::before{content:"\F0117"}.mdi-checkbox-blank-circle::before{content:"\F012F"}.mdi-checkbox-blank-circle-outline::before{content:"\F0130"}.mdi-checkbox-blank-off::before{content:"\F12EC"}.mdi-checkbox-blank-off-outline::before{content:"\F12ED"}.mdi-checkbox-blank-outline::before{content:"\F0131"}.mdi-checkbox-intermediate::before{content:"\F0856"}.mdi-checkbox-intermediate-variant::before{content:"\F1B54"}.mdi-checkbox-marked::before{content:"\F0132"}.mdi-checkbox-marked-circle::before{content:"\F0133"}.mdi-checkbox-marked-circle-auto-outline::before{content:"\F1C26"}.mdi-checkbox-marked-circle-minus-outline::before{content:"\F1C27"}.mdi-checkbox-marked-circle-outline::before{content:"\F0134"}.mdi-checkbox-marked-circle-plus-outline::before{content:"\F1927"}.mdi-checkbox-marked-outline::before{content:"\F0135"}.mdi-checkbox-multiple-blank::before{content:"\F0136"}.mdi-checkbox-multiple-blank-circle::before{content:"\F063B"}.mdi-checkbox-multiple-blank-circle-outline::before{content:"\F063C"}.mdi-checkbox-multiple-blank-outline::before{content:"\F0137"}.mdi-checkbox-multiple-marked::before{content:"\F0138"}.mdi-checkbox-multiple-marked-circle::before{content:"\F063D"}.mdi-checkbox-multiple-marked-circle-outline::before{content:"\F063E"}.mdi-checkbox-multiple-marked-outline::before{content:"\F0139"}.mdi-checkbox-multiple-outline::before{content:"\F0C51"}.mdi-checkbox-outline::before{content:"\F0C52"}.mdi-checkerboard::before{content:"\F013A"}.mdi-checkerboard-minus::before{content:"\F1202"}.mdi-checkerboard-plus::before{content:"\F1201"}.mdi-checkerboard-remove::before{content:"\F1203"}.mdi-cheese::before{content:"\F12B9"}.mdi-cheese-off::before{content:"\F13EE"}.mdi-chef-hat::before{content:"\F0B7C"}.mdi-chemical-weapon::before{content:"\F013B"}.mdi-chess-bishop::before{content:"\F085C"}.mdi-chess-king::before{content:"\F0857"}.mdi-chess-knight::before{content:"\F0858"}.mdi-chess-pawn::before{content:"\F0859"}.mdi-chess-queen::before{content:"\F085A"}.mdi-chess-rook::before{content:"\F085B"}.mdi-chevron-double-down::before{content:"\F013C"}.mdi-chevron-double-left::before{content:"\F013D"}.mdi-chevron-double-right::before{content:"\F013E"}.mdi-chevron-double-up::before{content:"\F013F"}.mdi-chevron-down::before{content:"\F0140"}.mdi-chevron-down-box::before{content:"\F09D6"}.mdi-chevron-down-box-outline::before{content:"\F09D7"}.mdi-chevron-down-circle::before{content:"\F0B26"}.mdi-chevron-down-circle-outline::before{content:"\F0B27"}.mdi-chevron-left::before{content:"\F0141"}.mdi-chevron-left-box::before{content:"\F09D8"}.mdi-chevron-left-box-outline::before{content:"\F09D9"}.mdi-chevron-left-circle::before{content:"\F0B28"}.mdi-chevron-left-circle-outline::before{content:"\F0B29"}.mdi-chevron-right::before{content:"\F0142"}.mdi-chevron-right-box::before{content:"\F09DA"}.mdi-chevron-right-box-outline::before{content:"\F09DB"}.mdi-chevron-right-circle::before{content:"\F0B2A"}.mdi-chevron-right-circle-outline::before{content:"\F0B2B"}.mdi-chevron-triple-down::before{content:"\F0DB9"}.mdi-chevron-triple-left::before{content:"\F0DBA"}.mdi-chevron-triple-right::before{content:"\F0DBB"}.mdi-chevron-triple-up::before{content:"\F0DBC"}.mdi-chevron-up::before{content:"\F0143"}.mdi-chevron-up-box::before{content:"\F09DC"}.mdi-chevron-up-box-outline::before{content:"\F09DD"}.mdi-chevron-up-circle::before{content:"\F0B2C"}.mdi-chevron-up-circle-outline::before{content:"\F0B2D"}.mdi-chili-alert::before{content:"\F17EA"}.mdi-chili-alert-outline::before{content:"\F17EB"}.mdi-chili-hot::before{content:"\F07B2"}.mdi-chili-hot-outline::before{content:"\F17EC"}.mdi-chili-medium::before{content:"\F07B3"}.mdi-chili-medium-outline::before{content:"\F17ED"}.mdi-chili-mild::before{content:"\F07B4"}.mdi-chili-mild-outline::before{content:"\F17EE"}.mdi-chili-off::before{content:"\F1467"}.mdi-chili-off-outline::before{content:"\F17EF"}.mdi-chip::before{content:"\F061A"}.mdi-church::before{content:"\F0144"}.mdi-church-outline::before{content:"\F1B02"}.mdi-cigar::before{content:"\F1189"}.mdi-cigar-off::before{content:"\F141B"}.mdi-circle::before{content:"\F0765"}.mdi-circle-box::before{content:"\F15DC"}.mdi-circle-box-outline::before{content:"\F15DD"}.mdi-circle-double::before{content:"\F0E95"}.mdi-circle-edit-outline::before{content:"\F08D5"}.mdi-circle-expand::before{content:"\F0E96"}.mdi-circle-half::before{content:"\F1395"}.mdi-circle-half-full::before{content:"\F1396"}.mdi-circle-medium::before{content:"\F09DE"}.mdi-circle-multiple::before{content:"\F0B38"}.mdi-circle-multiple-outline::before{content:"\F0695"}.mdi-circle-off-outline::before{content:"\F10D3"}.mdi-circle-opacity::before{content:"\F1853"}.mdi-circle-outline::before{content:"\F0766"}.mdi-circle-slice-1::before{content:"\F0A9E"}.mdi-circle-slice-2::before{content:"\F0A9F"}.mdi-circle-slice-3::before{content:"\F0AA0"}.mdi-circle-slice-4::before{content:"\F0AA1"}.mdi-circle-slice-5::before{content:"\F0AA2"}.mdi-circle-slice-6::before{content:"\F0AA3"}.mdi-circle-slice-7::before{content:"\F0AA4"}.mdi-circle-slice-8::before{content:"\F0AA5"}.mdi-circle-small::before{content:"\F09DF"}.mdi-circular-saw::before{content:"\F0E22"}.mdi-city::before{content:"\F0146"}.mdi-city-switch::before{content:"\F1C28"}.mdi-city-variant::before{content:"\F0A36"}.mdi-city-variant-outline::before{content:"\F0A37"}.mdi-clipboard::before{content:"\F0147"}.mdi-clipboard-account::before{content:"\F0148"}.mdi-clipboard-account-outline::before{content:"\F0C55"}.mdi-clipboard-alert::before{content:"\F0149"}.mdi-clipboard-alert-outline::before{content:"\F0CF7"}.mdi-clipboard-arrow-down::before{content:"\F014A"}.mdi-clipboard-arrow-down-outline::before{content:"\F0C56"}.mdi-clipboard-arrow-left::before{content:"\F014B"}.mdi-clipboard-arrow-left-outline::before{content:"\F0CF8"}.mdi-clipboard-arrow-right::before{content:"\F0CF9"}.mdi-clipboard-arrow-right-outline::before{content:"\F0CFA"}.mdi-clipboard-arrow-up::before{content:"\F0C57"}.mdi-clipboard-arrow-up-outline::before{content:"\F0C58"}.mdi-clipboard-check::before{content:"\F014E"}.mdi-clipboard-check-multiple::before{content:"\F1263"}.mdi-clipboard-check-multiple-outline::before{content:"\F1264"}.mdi-clipboard-check-outline::before{content:"\F08A8"}.mdi-clipboard-clock::before{content:"\F16E2"}.mdi-clipboard-clock-outline::before{content:"\F16E3"}.mdi-clipboard-edit::before{content:"\F14E5"}.mdi-clipboard-edit-outline::before{content:"\F14E6"}.mdi-clipboard-file::before{content:"\F1265"}.mdi-clipboard-file-outline::before{content:"\F1266"}.mdi-clipboard-flow::before{content:"\F06C8"}.mdi-clipboard-flow-outline::before{content:"\F1117"}.mdi-clipboard-list::before{content:"\F10D4"}.mdi-clipboard-list-outline::before{content:"\F10D5"}.mdi-clipboard-minus::before{content:"\F1618"}.mdi-clipboard-minus-outline::before{content:"\F1619"}.mdi-clipboard-multiple::before{content:"\F1267"}.mdi-clipboard-multiple-outline::before{content:"\F1268"}.mdi-clipboard-off::before{content:"\F161A"}.mdi-clipboard-off-outline::before{content:"\F161B"}.mdi-clipboard-outline::before{content:"\F014C"}.mdi-clipboard-play::before{content:"\F0C59"}.mdi-clipboard-play-multiple::before{content:"\F1269"}.mdi-clipboard-play-multiple-outline::before{content:"\F126A"}.mdi-clipboard-play-outline::before{content:"\F0C5A"}.mdi-clipboard-plus::before{content:"\F0751"}.mdi-clipboard-plus-outline::before{content:"\F131F"}.mdi-clipboard-pulse::before{content:"\F085D"}.mdi-clipboard-pulse-outline::before{content:"\F085E"}.mdi-clipboard-remove::before{content:"\F161C"}.mdi-clipboard-remove-outline::before{content:"\F161D"}.mdi-clipboard-search::before{content:"\F161E"}.mdi-clipboard-search-outline::before{content:"\F161F"}.mdi-clipboard-text::before{content:"\F014D"}.mdi-clipboard-text-clock::before{content:"\F18F9"}.mdi-clipboard-text-clock-outline::before{content:"\F18FA"}.mdi-clipboard-text-multiple::before{content:"\F126B"}.mdi-clipboard-text-multiple-outline::before{content:"\F126C"}.mdi-clipboard-text-off::before{content:"\F1620"}.mdi-clipboard-text-off-outline::before{content:"\F1621"}.mdi-clipboard-text-outline::before{content:"\F0A38"}.mdi-clipboard-text-play::before{content:"\F0C5B"}.mdi-clipboard-text-play-outline::before{content:"\F0C5C"}.mdi-clipboard-text-search::before{content:"\F1622"}.mdi-clipboard-text-search-outline::before{content:"\F1623"}.mdi-clippy::before{content:"\F014F"}.mdi-clock::before{content:"\F0954"}.mdi-clock-alert::before{content:"\F0955"}.mdi-clock-alert-outline::before{content:"\F05CE"}.mdi-clock-check::before{content:"\F0FA8"}.mdi-clock-check-outline::before{content:"\F0FA9"}.mdi-clock-digital::before{content:"\F0E97"}.mdi-clock-edit::before{content:"\F19BA"}.mdi-clock-edit-outline::before{content:"\F19BB"}.mdi-clock-end::before{content:"\F0151"}.mdi-clock-fast::before{content:"\F0152"}.mdi-clock-in::before{content:"\F0153"}.mdi-clock-minus::before{content:"\F1863"}.mdi-clock-minus-outline::before{content:"\F1864"}.mdi-clock-out::before{content:"\F0154"}.mdi-clock-outline::before{content:"\F0150"}.mdi-clock-plus::before{content:"\F1861"}.mdi-clock-plus-outline::before{content:"\F1862"}.mdi-clock-remove::before{content:"\F1865"}.mdi-clock-remove-outline::before{content:"\F1866"}.mdi-clock-star-four-points::before{content:"\F1C29"}.mdi-clock-star-four-points-outline::before{content:"\F1C2A"}.mdi-clock-start::before{content:"\F0155"}.mdi-clock-time-eight::before{content:"\F1446"}.mdi-clock-time-eight-outline::before{content:"\F1452"}.mdi-clock-time-eleven::before{content:"\F1449"}.mdi-clock-time-eleven-outline::before{content:"\F1455"}.mdi-clock-time-five::before{content:"\F1443"}.mdi-clock-time-five-outline::before{content:"\F144F"}.mdi-clock-time-four::before{content:"\F1442"}.mdi-clock-time-four-outline::before{content:"\F144E"}.mdi-clock-time-nine::before{content:"\F1447"}.mdi-clock-time-nine-outline::before{content:"\F1453"}.mdi-clock-time-one::before{content:"\F143F"}.mdi-clock-time-one-outline::before{content:"\F144B"}.mdi-clock-time-seven::before{content:"\F1445"}.mdi-clock-time-seven-outline::before{content:"\F1451"}.mdi-clock-time-six::before{content:"\F1444"}.mdi-clock-time-six-outline::before{content:"\F1450"}.mdi-clock-time-ten::before{content:"\F1448"}.mdi-clock-time-ten-outline::before{content:"\F1454"}.mdi-clock-time-three::before{content:"\F1441"}.mdi-clock-time-three-outline::before{content:"\F144D"}.mdi-clock-time-twelve::before{content:"\F144A"}.mdi-clock-time-twelve-outline::before{content:"\F1456"}.mdi-clock-time-two::before{content:"\F1440"}.mdi-clock-time-two-outline::before{content:"\F144C"}.mdi-close::before{content:"\F0156"}.mdi-close-box::before{content:"\F0157"}.mdi-close-box-multiple::before{content:"\F0C5D"}.mdi-close-box-multiple-outline::before{content:"\F0C5E"}.mdi-close-box-outline::before{content:"\F0158"}.mdi-close-circle::before{content:"\F0159"}.mdi-close-circle-multiple::before{content:"\F062A"}.mdi-close-circle-multiple-outline::before{content:"\F0883"}.mdi-close-circle-outline::before{content:"\F015A"}.mdi-close-network::before{content:"\F015B"}.mdi-close-network-outline::before{content:"\F0C5F"}.mdi-close-octagon::before{content:"\F015C"}.mdi-close-octagon-outline::before{content:"\F015D"}.mdi-close-outline::before{content:"\F06C9"}.mdi-close-thick::before{content:"\F1398"}.mdi-closed-caption::before{content:"\F015E"}.mdi-closed-caption-outline::before{content:"\F0DBD"}.mdi-cloud::before{content:"\F015F"}.mdi-cloud-alert::before{content:"\F09E0"}.mdi-cloud-alert-outline::before{content:"\F1BE0"}.mdi-cloud-arrow-down::before{content:"\F1BE1"}.mdi-cloud-arrow-down-outline::before{content:"\F1BE2"}.mdi-cloud-arrow-left::before{content:"\F1BE3"}.mdi-cloud-arrow-left-outline::before{content:"\F1BE4"}.mdi-cloud-arrow-right::before{content:"\F1BE5"}.mdi-cloud-arrow-right-outline::before{content:"\F1BE6"}.mdi-cloud-arrow-up::before{content:"\F1BE7"}.mdi-cloud-arrow-up-outline::before{content:"\F1BE8"}.mdi-cloud-braces::before{content:"\F07B5"}.mdi-cloud-cancel::before{content:"\F1BE9"}.mdi-cloud-cancel-outline::before{content:"\F1BEA"}.mdi-cloud-check::before{content:"\F1BEB"}.mdi-cloud-check-outline::before{content:"\F1BEC"}.mdi-cloud-check-variant::before{content:"\F0160"}.mdi-cloud-check-variant-outline::before{content:"\F12CC"}.mdi-cloud-circle::before{content:"\F0161"}.mdi-cloud-circle-outline::before{content:"\F1BED"}.mdi-cloud-clock::before{content:"\F1BEE"}.mdi-cloud-clock-outline::before{content:"\F1BEF"}.mdi-cloud-cog::before{content:"\F1BF0"}.mdi-cloud-cog-outline::before{content:"\F1BF1"}.mdi-cloud-download::before{content:"\F0162"}.mdi-cloud-download-outline::before{content:"\F0B7D"}.mdi-cloud-key::before{content:"\F1CA1"}.mdi-cloud-key-outline::before{content:"\F1CA2"}.mdi-cloud-lock::before{content:"\F11F1"}.mdi-cloud-lock-open::before{content:"\F1BF2"}.mdi-cloud-lock-open-outline::before{content:"\F1BF3"}.mdi-cloud-lock-outline::before{content:"\F11F2"}.mdi-cloud-minus::before{content:"\F1BF4"}.mdi-cloud-minus-outline::before{content:"\F1BF5"}.mdi-cloud-off::before{content:"\F1BF6"}.mdi-cloud-off-outline::before{content:"\F0164"}.mdi-cloud-outline::before{content:"\F0163"}.mdi-cloud-percent::before{content:"\F1A35"}.mdi-cloud-percent-outline::before{content:"\F1A36"}.mdi-cloud-plus::before{content:"\F1BF7"}.mdi-cloud-plus-outline::before{content:"\F1BF8"}.mdi-cloud-print::before{content:"\F0165"}.mdi-cloud-print-outline::before{content:"\F0166"}.mdi-cloud-question::before{content:"\F0A39"}.mdi-cloud-question-outline::before{content:"\F1BF9"}.mdi-cloud-refresh::before{content:"\F1BFA"}.mdi-cloud-refresh-outline::before{content:"\F1BFB"}.mdi-cloud-refresh-variant::before{content:"\F052A"}.mdi-cloud-refresh-variant-outline::before{content:"\F1BFC"}.mdi-cloud-remove::before{content:"\F1BFD"}.mdi-cloud-remove-outline::before{content:"\F1BFE"}.mdi-cloud-search::before{content:"\F0956"}.mdi-cloud-search-outline::before{content:"\F0957"}.mdi-cloud-sync::before{content:"\F063F"}.mdi-cloud-sync-outline::before{content:"\F12D6"}.mdi-cloud-tags::before{content:"\F07B6"}.mdi-cloud-upload::before{content:"\F0167"}.mdi-cloud-upload-outline::before{content:"\F0B7E"}.mdi-clouds::before{content:"\F1B95"}.mdi-clover::before{content:"\F0816"}.mdi-clover-outline::before{content:"\F1C62"}.mdi-coach-lamp::before{content:"\F1020"}.mdi-coach-lamp-variant::before{content:"\F1A37"}.mdi-coat-rack::before{content:"\F109E"}.mdi-code-array::before{content:"\F0168"}.mdi-code-block-braces::before{content:"\F1C83"}.mdi-code-block-brackets::before{content:"\F1C84"}.mdi-code-block-parentheses::before{content:"\F1C85"}.mdi-code-block-tags::before{content:"\F1C86"}.mdi-code-braces::before{content:"\F0169"}.mdi-code-braces-box::before{content:"\F10D6"}.mdi-code-brackets::before{content:"\F016A"}.mdi-code-equal::before{content:"\F016B"}.mdi-code-greater-than::before{content:"\F016C"}.mdi-code-greater-than-or-equal::before{content:"\F016D"}.mdi-code-json::before{content:"\F0626"}.mdi-code-less-than::before{content:"\F016E"}.mdi-code-less-than-or-equal::before{content:"\F016F"}.mdi-code-not-equal::before{content:"\F0170"}.mdi-code-not-equal-variant::before{content:"\F0171"}.mdi-code-parentheses::before{content:"\F0172"}.mdi-code-parentheses-box::before{content:"\F10D7"}.mdi-code-string::before{content:"\F0173"}.mdi-code-tags::before{content:"\F0174"}.mdi-code-tags-check::before{content:"\F0694"}.mdi-codepen::before{content:"\F0175"}.mdi-coffee::before{content:"\F0176"}.mdi-coffee-maker::before{content:"\F109F"}.mdi-coffee-maker-check::before{content:"\F1931"}.mdi-coffee-maker-check-outline::before{content:"\F1932"}.mdi-coffee-maker-outline::before{content:"\F181B"}.mdi-coffee-off::before{content:"\F0FAA"}.mdi-coffee-off-outline::before{content:"\F0FAB"}.mdi-coffee-outline::before{content:"\F06CA"}.mdi-coffee-to-go::before{content:"\F0177"}.mdi-coffee-to-go-outline::before{content:"\F130E"}.mdi-coffin::before{content:"\F0B7F"}.mdi-cog::before{content:"\F0493"}.mdi-cog-box::before{content:"\F0494"}.mdi-cog-clockwise::before{content:"\F11DD"}.mdi-cog-counterclockwise::before{content:"\F11DE"}.mdi-cog-off::before{content:"\F13CE"}.mdi-cog-off-outline::before{content:"\F13CF"}.mdi-cog-outline::before{content:"\F08BB"}.mdi-cog-pause::before{content:"\F1933"}.mdi-cog-pause-outline::before{content:"\F1934"}.mdi-cog-play::before{content:"\F1935"}.mdi-cog-play-outline::before{content:"\F1936"}.mdi-cog-refresh::before{content:"\F145E"}.mdi-cog-refresh-outline::before{content:"\F145F"}.mdi-cog-stop::before{content:"\F1937"}.mdi-cog-stop-outline::before{content:"\F1938"}.mdi-cog-sync::before{content:"\F1460"}.mdi-cog-sync-outline::before{content:"\F1461"}.mdi-cog-transfer::before{content:"\F105B"}.mdi-cog-transfer-outline::before{content:"\F105C"}.mdi-cogs::before{content:"\F08D6"}.mdi-collage::before{content:"\F0640"}.mdi-collapse-all::before{content:"\F0AA6"}.mdi-collapse-all-outline::before{content:"\F0AA7"}.mdi-color-helper::before{content:"\F0179"}.mdi-comma::before{content:"\F0E23"}.mdi-comma-box::before{content:"\F0E2B"}.mdi-comma-box-outline::before{content:"\F0E24"}.mdi-comma-circle::before{content:"\F0E25"}.mdi-comma-circle-outline::before{content:"\F0E26"}.mdi-comment::before{content:"\F017A"}.mdi-comment-account::before{content:"\F017B"}.mdi-comment-account-outline::before{content:"\F017C"}.mdi-comment-alert::before{content:"\F017D"}.mdi-comment-alert-outline::before{content:"\F017E"}.mdi-comment-arrow-left::before{content:"\F09E1"}.mdi-comment-arrow-left-outline::before{content:"\F09E2"}.mdi-comment-arrow-right::before{content:"\F09E3"}.mdi-comment-arrow-right-outline::before{content:"\F09E4"}.mdi-comment-bookmark::before{content:"\F15AE"}.mdi-comment-bookmark-outline::before{content:"\F15AF"}.mdi-comment-check::before{content:"\F017F"}.mdi-comment-check-outline::before{content:"\F0180"}.mdi-comment-edit::before{content:"\F11BF"}.mdi-comment-edit-outline::before{content:"\F12C4"}.mdi-comment-eye::before{content:"\F0A3A"}.mdi-comment-eye-outline::before{content:"\F0A3B"}.mdi-comment-flash::before{content:"\F15B0"}.mdi-comment-flash-outline::before{content:"\F15B1"}.mdi-comment-minus::before{content:"\F15DF"}.mdi-comment-minus-outline::before{content:"\F15E0"}.mdi-comment-multiple::before{content:"\F085F"}.mdi-comment-multiple-outline::before{content:"\F0181"}.mdi-comment-off::before{content:"\F15E1"}.mdi-comment-off-outline::before{content:"\F15E2"}.mdi-comment-outline::before{content:"\F0182"}.mdi-comment-plus::before{content:"\F09E5"}.mdi-comment-plus-outline::before{content:"\F0183"}.mdi-comment-processing::before{content:"\F0184"}.mdi-comment-processing-outline::before{content:"\F0185"}.mdi-comment-question::before{content:"\F0817"}.mdi-comment-question-outline::before{content:"\F0186"}.mdi-comment-quote::before{content:"\F1021"}.mdi-comment-quote-outline::before{content:"\F1022"}.mdi-comment-remove::before{content:"\F05DE"}.mdi-comment-remove-outline::before{content:"\F0187"}.mdi-comment-search::before{content:"\F0A3C"}.mdi-comment-search-outline::before{content:"\F0A3D"}.mdi-comment-text::before{content:"\F0188"}.mdi-comment-text-multiple::before{content:"\F0860"}.mdi-comment-text-multiple-outline::before{content:"\F0861"}.mdi-comment-text-outline::before{content:"\F0189"}.mdi-compare::before{content:"\F018A"}.mdi-compare-horizontal::before{content:"\F1492"}.mdi-compare-remove::before{content:"\F18B3"}.mdi-compare-vertical::before{content:"\F1493"}.mdi-compass::before{content:"\F018B"}.mdi-compass-off::before{content:"\F0B80"}.mdi-compass-off-outline::before{content:"\F0B81"}.mdi-compass-outline::before{content:"\F018C"}.mdi-compass-rose::before{content:"\F1382"}.mdi-compost::before{content:"\F1A38"}.mdi-cone::before{content:"\F194C"}.mdi-cone-off::before{content:"\F194D"}.mdi-connection::before{content:"\F1616"}.mdi-console::before{content:"\F018D"}.mdi-console-line::before{content:"\F07B7"}.mdi-console-network::before{content:"\F08A9"}.mdi-console-network-outline::before{content:"\F0C60"}.mdi-consolidate::before{content:"\F10D8"}.mdi-contactless-payment::before{content:"\F0D6A"}.mdi-contactless-payment-circle::before{content:"\F0321"}.mdi-contactless-payment-circle-outline::before{content:"\F0408"}.mdi-contacts::before{content:"\F06CB"}.mdi-contacts-outline::before{content:"\F05B8"}.mdi-contain::before{content:"\F0A3E"}.mdi-contain-end::before{content:"\F0A3F"}.mdi-contain-start::before{content:"\F0A40"}.mdi-content-copy::before{content:"\F018F"}.mdi-content-cut::before{content:"\F0190"}.mdi-content-duplicate::before{content:"\F0191"}.mdi-content-paste::before{content:"\F0192"}.mdi-content-save::before{content:"\F0193"}.mdi-content-save-alert::before{content:"\F0F42"}.mdi-content-save-alert-outline::before{content:"\F0F43"}.mdi-content-save-all::before{content:"\F0194"}.mdi-content-save-all-outline::before{content:"\F0F44"}.mdi-content-save-check::before{content:"\F18EA"}.mdi-content-save-check-outline::before{content:"\F18EB"}.mdi-content-save-cog::before{content:"\F145B"}.mdi-content-save-cog-outline::before{content:"\F145C"}.mdi-content-save-edit::before{content:"\F0CFB"}.mdi-content-save-edit-outline::before{content:"\F0CFC"}.mdi-content-save-minus::before{content:"\F1B43"}.mdi-content-save-minus-outline::before{content:"\F1B44"}.mdi-content-save-move::before{content:"\F0E27"}.mdi-content-save-move-outline::before{content:"\F0E28"}.mdi-content-save-off::before{content:"\F1643"}.mdi-content-save-off-outline::before{content:"\F1644"}.mdi-content-save-outline::before{content:"\F0818"}.mdi-content-save-plus::before{content:"\F1B41"}.mdi-content-save-plus-outline::before{content:"\F1B42"}.mdi-content-save-settings::before{content:"\F061B"}.mdi-content-save-settings-outline::before{content:"\F0B2E"}.mdi-contrast::before{content:"\F0195"}.mdi-contrast-box::before{content:"\F0196"}.mdi-contrast-circle::before{content:"\F0197"}.mdi-controller::before{content:"\F02B4"}.mdi-controller-classic::before{content:"\F0B82"}.mdi-controller-classic-outline::before{content:"\F0B83"}.mdi-controller-off::before{content:"\F02B5"}.mdi-cookie::before{content:"\F0198"}.mdi-cookie-alert::before{content:"\F16D0"}.mdi-cookie-alert-outline::before{content:"\F16D1"}.mdi-cookie-check::before{content:"\F16D2"}.mdi-cookie-check-outline::before{content:"\F16D3"}.mdi-cookie-clock::before{content:"\F16E4"}.mdi-cookie-clock-outline::before{content:"\F16E5"}.mdi-cookie-cog::before{content:"\F16D4"}.mdi-cookie-cog-outline::before{content:"\F16D5"}.mdi-cookie-edit::before{content:"\F16E6"}.mdi-cookie-edit-outline::before{content:"\F16E7"}.mdi-cookie-lock::before{content:"\F16E8"}.mdi-cookie-lock-outline::before{content:"\F16E9"}.mdi-cookie-minus::before{content:"\F16DA"}.mdi-cookie-minus-outline::before{content:"\F16DB"}.mdi-cookie-off::before{content:"\F16EA"}.mdi-cookie-off-outline::before{content:"\F16EB"}.mdi-cookie-outline::before{content:"\F16DE"}.mdi-cookie-plus::before{content:"\F16D6"}.mdi-cookie-plus-outline::before{content:"\F16D7"}.mdi-cookie-refresh::before{content:"\F16EC"}.mdi-cookie-refresh-outline::before{content:"\F16ED"}.mdi-cookie-remove::before{content:"\F16D8"}.mdi-cookie-remove-outline::before{content:"\F16D9"}.mdi-cookie-settings::before{content:"\F16DC"}.mdi-cookie-settings-outline::before{content:"\F16DD"}.mdi-coolant-temperature::before{content:"\F03C8"}.mdi-copyleft::before{content:"\F1939"}.mdi-copyright::before{content:"\F05E6"}.mdi-cordova::before{content:"\F0958"}.mdi-corn::before{content:"\F07B8"}.mdi-corn-off::before{content:"\F13EF"}.mdi-cosine-wave::before{content:"\F1479"}.mdi-counter::before{content:"\F0199"}.mdi-countertop::before{content:"\F181C"}.mdi-countertop-outline::before{content:"\F181D"}.mdi-cow::before{content:"\F019A"}.mdi-cow-off::before{content:"\F18FC"}.mdi-cpu-32-bit::before{content:"\F0EDF"}.mdi-cpu-64-bit::before{content:"\F0EE0"}.mdi-cradle::before{content:"\F198B"}.mdi-cradle-outline::before{content:"\F1991"}.mdi-crane::before{content:"\F0862"}.mdi-creation::before{content:"\F0674"}.mdi-creation-outline::before{content:"\F1C2B"}.mdi-creative-commons::before{content:"\F0D6B"}.mdi-credit-card::before{content:"\F0FEF"}.mdi-credit-card-check::before{content:"\F13D0"}.mdi-credit-card-check-outline::before{content:"\F13D1"}.mdi-credit-card-chip::before{content:"\F190F"}.mdi-credit-card-chip-outline::before{content:"\F1910"}.mdi-credit-card-clock::before{content:"\F0EE1"}.mdi-credit-card-clock-outline::before{content:"\F0EE2"}.mdi-credit-card-edit::before{content:"\F17D7"}.mdi-credit-card-edit-outline::before{content:"\F17D8"}.mdi-credit-card-fast::before{content:"\F1911"}.mdi-credit-card-fast-outline::before{content:"\F1912"}.mdi-credit-card-lock::before{content:"\F18E7"}.mdi-credit-card-lock-outline::before{content:"\F18E8"}.mdi-credit-card-marker::before{content:"\F06A8"}.mdi-credit-card-marker-outline::before{content:"\F0DBE"}.mdi-credit-card-minus::before{content:"\F0FAC"}.mdi-credit-card-minus-outline::before{content:"\F0FAD"}.mdi-credit-card-multiple::before{content:"\F0FF0"}.mdi-credit-card-multiple-outline::before{content:"\F019C"}.mdi-credit-card-off::before{content:"\F0FF1"}.mdi-credit-card-off-outline::before{content:"\F05E4"}.mdi-credit-card-outline::before{content:"\F019B"}.mdi-credit-card-plus::before{content:"\F0FF2"}.mdi-credit-card-plus-outline::before{content:"\F0676"}.mdi-credit-card-refresh::before{content:"\F1645"}.mdi-credit-card-refresh-outline::before{content:"\F1646"}.mdi-credit-card-refund::before{content:"\F0FF3"}.mdi-credit-card-refund-outline::before{content:"\F0AA8"}.mdi-credit-card-remove::before{content:"\F0FAE"}.mdi-credit-card-remove-outline::before{content:"\F0FAF"}.mdi-credit-card-scan::before{content:"\F0FF4"}.mdi-credit-card-scan-outline::before{content:"\F019D"}.mdi-credit-card-search::before{content:"\F1647"}.mdi-credit-card-search-outline::before{content:"\F1648"}.mdi-credit-card-settings::before{content:"\F0FF5"}.mdi-credit-card-settings-outline::before{content:"\F08D7"}.mdi-credit-card-sync::before{content:"\F1649"}.mdi-credit-card-sync-outline::before{content:"\F164A"}.mdi-credit-card-wireless::before{content:"\F0802"}.mdi-credit-card-wireless-off::before{content:"\F057A"}.mdi-credit-card-wireless-off-outline::before{content:"\F057B"}.mdi-credit-card-wireless-outline::before{content:"\F0D6C"}.mdi-cricket::before{content:"\F0D6D"}.mdi-crop::before{content:"\F019E"}.mdi-crop-free::before{content:"\F019F"}.mdi-crop-landscape::before{content:"\F01A0"}.mdi-crop-portrait::before{content:"\F01A1"}.mdi-crop-rotate::before{content:"\F0696"}.mdi-crop-square::before{content:"\F01A2"}.mdi-cross::before{content:"\F0953"}.mdi-cross-bolnisi::before{content:"\F0CED"}.mdi-cross-celtic::before{content:"\F0CF5"}.mdi-cross-outline::before{content:"\F0CF6"}.mdi-crosshairs::before{content:"\F01A3"}.mdi-crosshairs-gps::before{content:"\F01A4"}.mdi-crosshairs-off::before{content:"\F0F45"}.mdi-crosshairs-question::before{content:"\F1136"}.mdi-crowd::before{content:"\F1975"}.mdi-crown::before{content:"\F01A5"}.mdi-crown-circle::before{content:"\F17DC"}.mdi-crown-circle-outline::before{content:"\F17DD"}.mdi-crown-outline::before{content:"\F11D0"}.mdi-cryengine::before{content:"\F0959"}.mdi-crystal-ball::before{content:"\F0B2F"}.mdi-cube::before{content:"\F01A6"}.mdi-cube-off::before{content:"\F141C"}.mdi-cube-off-outline::before{content:"\F141D"}.mdi-cube-outline::before{content:"\F01A7"}.mdi-cube-scan::before{content:"\F0B84"}.mdi-cube-send::before{content:"\F01A8"}.mdi-cube-unfolded::before{content:"\F01A9"}.mdi-cup::before{content:"\F01AA"}.mdi-cup-off::before{content:"\F05E5"}.mdi-cup-off-outline::before{content:"\F137D"}.mdi-cup-outline::before{content:"\F130F"}.mdi-cup-water::before{content:"\F01AB"}.mdi-cupboard::before{content:"\F0F46"}.mdi-cupboard-outline::before{content:"\F0F47"}.mdi-cupcake::before{content:"\F095A"}.mdi-curling::before{content:"\F0863"}.mdi-currency-bdt::before{content:"\F0864"}.mdi-currency-brl::before{content:"\F0B85"}.mdi-currency-btc::before{content:"\F01AC"}.mdi-currency-cny::before{content:"\F07BA"}.mdi-currency-eth::before{content:"\F07BB"}.mdi-currency-eur::before{content:"\F01AD"}.mdi-currency-eur-off::before{content:"\F1315"}.mdi-currency-fra::before{content:"\F1A39"}.mdi-currency-gbp::before{content:"\F01AE"}.mdi-currency-ils::before{content:"\F0C61"}.mdi-currency-inr::before{content:"\F01AF"}.mdi-currency-jpy::before{content:"\F07BC"}.mdi-currency-krw::before{content:"\F07BD"}.mdi-currency-kzt::before{content:"\F0865"}.mdi-currency-mnt::before{content:"\F1512"}.mdi-currency-ngn::before{content:"\F01B0"}.mdi-currency-php::before{content:"\F09E6"}.mdi-currency-rial::before{content:"\F0E9C"}.mdi-currency-rub::before{content:"\F01B1"}.mdi-currency-rupee::before{content:"\F1976"}.mdi-currency-sign::before{content:"\F07BE"}.mdi-currency-thb::before{content:"\F1C05"}.mdi-currency-try::before{content:"\F01B2"}.mdi-currency-twd::before{content:"\F07BF"}.mdi-currency-uah::before{content:"\F1B9B"}.mdi-currency-usd::before{content:"\F01C1"}.mdi-currency-usd-off::before{content:"\F067A"}.mdi-current-ac::before{content:"\F1480"}.mdi-current-dc::before{content:"\F095C"}.mdi-cursor-default::before{content:"\F01C0"}.mdi-cursor-default-click::before{content:"\F0CFD"}.mdi-cursor-default-click-outline::before{content:"\F0CFE"}.mdi-cursor-default-gesture::before{content:"\F1127"}.mdi-cursor-default-gesture-outline::before{content:"\F1128"}.mdi-cursor-default-outline::before{content:"\F01BF"}.mdi-cursor-move::before{content:"\F01BE"}.mdi-cursor-pointer::before{content:"\F01BD"}.mdi-cursor-text::before{content:"\F05E7"}.mdi-curtains::before{content:"\F1846"}.mdi-curtains-closed::before{content:"\F1847"}.mdi-cylinder::before{content:"\F194E"}.mdi-cylinder-off::before{content:"\F194F"}.mdi-dance-ballroom::before{content:"\F15FB"}.mdi-dance-pole::before{content:"\F1578"}.mdi-data-matrix::before{content:"\F153C"}.mdi-data-matrix-edit::before{content:"\F153D"}.mdi-data-matrix-minus::before{content:"\F153E"}.mdi-data-matrix-plus::before{content:"\F153F"}.mdi-data-matrix-remove::before{content:"\F1540"}.mdi-data-matrix-scan::before{content:"\F1541"}.mdi-database::before{content:"\F01BC"}.mdi-database-alert::before{content:"\F163A"}.mdi-database-alert-outline::before{content:"\F1624"}.mdi-database-arrow-down::before{content:"\F163B"}.mdi-database-arrow-down-outline::before{content:"\F1625"}.mdi-database-arrow-left::before{content:"\F163C"}.mdi-database-arrow-left-outline::before{content:"\F1626"}.mdi-database-arrow-right::before{content:"\F163D"}.mdi-database-arrow-right-outline::before{content:"\F1627"}.mdi-database-arrow-up::before{content:"\F163E"}.mdi-database-arrow-up-outline::before{content:"\F1628"}.mdi-database-check::before{content:"\F0AA9"}.mdi-database-check-outline::before{content:"\F1629"}.mdi-database-clock::before{content:"\F163F"}.mdi-database-clock-outline::before{content:"\F162A"}.mdi-database-cog::before{content:"\F164B"}.mdi-database-cog-outline::before{content:"\F164C"}.mdi-database-edit::before{content:"\F0B86"}.mdi-database-edit-outline::before{content:"\F162B"}.mdi-database-export::before{content:"\F095E"}.mdi-database-export-outline::before{content:"\F162C"}.mdi-database-eye::before{content:"\F191F"}.mdi-database-eye-off::before{content:"\F1920"}.mdi-database-eye-off-outline::before{content:"\F1921"}.mdi-database-eye-outline::before{content:"\F1922"}.mdi-database-import::before{content:"\F095D"}.mdi-database-import-outline::before{content:"\F162D"}.mdi-database-lock::before{content:"\F0AAA"}.mdi-database-lock-outline::before{content:"\F162E"}.mdi-database-marker::before{content:"\F12F6"}.mdi-database-marker-outline::before{content:"\F162F"}.mdi-database-minus::before{content:"\F01BB"}.mdi-database-minus-outline::before{content:"\F1630"}.mdi-database-off::before{content:"\F1640"}.mdi-database-off-outline::before{content:"\F1631"}.mdi-database-outline::before{content:"\F1632"}.mdi-database-plus::before{content:"\F01BA"}.mdi-database-plus-outline::before{content:"\F1633"}.mdi-database-refresh::before{content:"\F05C2"}.mdi-database-refresh-outline::before{content:"\F1634"}.mdi-database-remove::before{content:"\F0D00"}.mdi-database-remove-outline::before{content:"\F1635"}.mdi-database-search::before{content:"\F0866"}.mdi-database-search-outline::before{content:"\F1636"}.mdi-database-settings::before{content:"\F0D01"}.mdi-database-settings-outline::before{content:"\F1637"}.mdi-database-sync::before{content:"\F0CFF"}.mdi-database-sync-outline::before{content:"\F1638"}.mdi-death-star::before{content:"\F08D8"}.mdi-death-star-variant::before{content:"\F08D9"}.mdi-deathly-hallows::before{content:"\F0B87"}.mdi-debian::before{content:"\F08DA"}.mdi-debug-step-into::before{content:"\F01B9"}.mdi-debug-step-out::before{content:"\F01B8"}.mdi-debug-step-over::before{content:"\F01B7"}.mdi-decagram::before{content:"\F076C"}.mdi-decagram-outline::before{content:"\F076D"}.mdi-decimal::before{content:"\F10A1"}.mdi-decimal-comma::before{content:"\F10A2"}.mdi-decimal-comma-decrease::before{content:"\F10A3"}.mdi-decimal-comma-increase::before{content:"\F10A4"}.mdi-decimal-decrease::before{content:"\F01B6"}.mdi-decimal-increase::before{content:"\F01B5"}.mdi-delete::before{content:"\F01B4"}.mdi-delete-alert::before{content:"\F10A5"}.mdi-delete-alert-outline::before{content:"\F10A6"}.mdi-delete-circle::before{content:"\F0683"}.mdi-delete-circle-outline::before{content:"\F0B88"}.mdi-delete-clock::before{content:"\F1556"}.mdi-delete-clock-outline::before{content:"\F1557"}.mdi-delete-empty::before{content:"\F06CC"}.mdi-delete-empty-outline::before{content:"\F0E9D"}.mdi-delete-forever::before{content:"\F05E8"}.mdi-delete-forever-outline::before{content:"\F0B89"}.mdi-delete-off::before{content:"\F10A7"}.mdi-delete-off-outline::before{content:"\F10A8"}.mdi-delete-outline::before{content:"\F09E7"}.mdi-delete-restore::before{content:"\F0819"}.mdi-delete-sweep::before{content:"\F05E9"}.mdi-delete-sweep-outline::before{content:"\F0C62"}.mdi-delete-variant::before{content:"\F01B3"}.mdi-delta::before{content:"\F01C2"}.mdi-desk::before{content:"\F1239"}.mdi-desk-lamp::before{content:"\F095F"}.mdi-desk-lamp-off::before{content:"\F1B1F"}.mdi-desk-lamp-on::before{content:"\F1B20"}.mdi-deskphone::before{content:"\F01C3"}.mdi-desktop-classic::before{content:"\F07C0"}.mdi-desktop-tower::before{content:"\F01C5"}.mdi-desktop-tower-monitor::before{content:"\F0AAB"}.mdi-details::before{content:"\F01C6"}.mdi-dev-to::before{content:"\F0D6E"}.mdi-developer-board::before{content:"\F0697"}.mdi-deviantart::before{content:"\F01C7"}.mdi-devices::before{content:"\F0FB0"}.mdi-dharmachakra::before{content:"\F094B"}.mdi-diabetes::before{content:"\F1126"}.mdi-dialpad::before{content:"\F061C"}.mdi-diameter::before{content:"\F0C63"}.mdi-diameter-outline::before{content:"\F0C64"}.mdi-diameter-variant::before{content:"\F0C65"}.mdi-diamond::before{content:"\F0B8A"}.mdi-diamond-outline::before{content:"\F0B8B"}.mdi-diamond-stone::before{content:"\F01C8"}.mdi-diaper-outline::before{content:"\F1CCF"}.mdi-dice-1::before{content:"\F01CA"}.mdi-dice-1-outline::before{content:"\F114A"}.mdi-dice-2::before{content:"\F01CB"}.mdi-dice-2-outline::before{content:"\F114B"}.mdi-dice-3::before{content:"\F01CC"}.mdi-dice-3-outline::before{content:"\F114C"}.mdi-dice-4::before{content:"\F01CD"}.mdi-dice-4-outline::before{content:"\F114D"}.mdi-dice-5::before{content:"\F01CE"}.mdi-dice-5-outline::before{content:"\F114E"}.mdi-dice-6::before{content:"\F01CF"}.mdi-dice-6-outline::before{content:"\F114F"}.mdi-dice-d10::before{content:"\F1153"}.mdi-dice-d10-outline::before{content:"\F076F"}.mdi-dice-d12::before{content:"\F1154"}.mdi-dice-d12-outline::before{content:"\F0867"}.mdi-dice-d20::before{content:"\F1155"}.mdi-dice-d20-outline::before{content:"\F05EA"}.mdi-dice-d4::before{content:"\F1150"}.mdi-dice-d4-outline::before{content:"\F05EB"}.mdi-dice-d6::before{content:"\F1151"}.mdi-dice-d6-outline::before{content:"\F05ED"}.mdi-dice-d8::before{content:"\F1152"}.mdi-dice-d8-outline::before{content:"\F05EC"}.mdi-dice-multiple::before{content:"\F076E"}.mdi-dice-multiple-outline::before{content:"\F1156"}.mdi-digital-ocean::before{content:"\F1237"}.mdi-dip-switch::before{content:"\F07C1"}.mdi-directions::before{content:"\F01D0"}.mdi-directions-fork::before{content:"\F0641"}.mdi-disc::before{content:"\F05EE"}.mdi-disc-alert::before{content:"\F01D1"}.mdi-disc-player::before{content:"\F0960"}.mdi-dishwasher::before{content:"\F0AAC"}.mdi-dishwasher-alert::before{content:"\F11B8"}.mdi-dishwasher-off::before{content:"\F11B9"}.mdi-disqus::before{content:"\F01D2"}.mdi-distribute-horizontal-center::before{content:"\F11C9"}.mdi-distribute-horizontal-left::before{content:"\F11C8"}.mdi-distribute-horizontal-right::before{content:"\F11CA"}.mdi-distribute-vertical-bottom::before{content:"\F11CB"}.mdi-distribute-vertical-center::before{content:"\F11CC"}.mdi-distribute-vertical-top::before{content:"\F11CD"}.mdi-diversify::before{content:"\F1877"}.mdi-diving::before{content:"\F1977"}.mdi-diving-flippers::before{content:"\F0DBF"}.mdi-diving-helmet::before{content:"\F0DC0"}.mdi-diving-scuba::before{content:"\F1B77"}.mdi-diving-scuba-flag::before{content:"\F0DC2"}.mdi-diving-scuba-mask::before{content:"\F0DC1"}.mdi-diving-scuba-tank::before{content:"\F0DC3"}.mdi-diving-scuba-tank-multiple::before{content:"\F0DC4"}.mdi-diving-snorkel::before{content:"\F0DC5"}.mdi-division::before{content:"\F01D4"}.mdi-division-box::before{content:"\F01D5"}.mdi-dlna::before{content:"\F0A41"}.mdi-dna::before{content:"\F0684"}.mdi-dns::before{content:"\F01D6"}.mdi-dns-outline::before{content:"\F0B8C"}.mdi-dock-bottom::before{content:"\F10A9"}.mdi-dock-left::before{content:"\F10AA"}.mdi-dock-right::before{content:"\F10AB"}.mdi-dock-top::before{content:"\F1513"}.mdi-dock-window::before{content:"\F10AC"}.mdi-docker::before{content:"\F0868"}.mdi-doctor::before{content:"\F0A42"}.mdi-dog::before{content:"\F0A43"}.mdi-dog-service::before{content:"\F0AAD"}.mdi-dog-side::before{content:"\F0A44"}.mdi-dog-side-off::before{content:"\F16EE"}.mdi-dolby::before{content:"\F06B3"}.mdi-dolly::before{content:"\F0E9E"}.mdi-dolphin::before{content:"\F18B4"}.mdi-domain::before{content:"\F01D7"}.mdi-domain-off::before{content:"\F0D6F"}.mdi-domain-plus::before{content:"\F10AD"}.mdi-domain-remove::before{content:"\F10AE"}.mdi-domain-switch::before{content:"\F1C2C"}.mdi-dome-light::before{content:"\F141E"}.mdi-domino-mask::before{content:"\F1023"}.mdi-donkey::before{content:"\F07C2"}.mdi-door::before{content:"\F081A"}.mdi-door-closed::before{content:"\F081B"}.mdi-door-closed-cancel::before{content:"\F1C93"}.mdi-door-closed-lock::before{content:"\F10AF"}.mdi-door-open::before{content:"\F081C"}.mdi-door-sliding::before{content:"\F181E"}.mdi-door-sliding-lock::before{content:"\F181F"}.mdi-door-sliding-open::before{content:"\F1820"}.mdi-doorbell::before{content:"\F12E6"}.mdi-doorbell-video::before{content:"\F0869"}.mdi-dot-net::before{content:"\F0AAE"}.mdi-dots-circle::before{content:"\F1978"}.mdi-dots-grid::before{content:"\F15FC"}.mdi-dots-hexagon::before{content:"\F15FF"}.mdi-dots-horizontal::before{content:"\F01D8"}.mdi-dots-horizontal-circle::before{content:"\F07C3"}.mdi-dots-horizontal-circle-outline::before{content:"\F0B8D"}.mdi-dots-square::before{content:"\F15FD"}.mdi-dots-triangle::before{content:"\F15FE"}.mdi-dots-vertical::before{content:"\F01D9"}.mdi-dots-vertical-circle::before{content:"\F07C4"}.mdi-dots-vertical-circle-outline::before{content:"\F0B8E"}.mdi-download::before{content:"\F01DA"}.mdi-download-box::before{content:"\F1462"}.mdi-download-box-outline::before{content:"\F1463"}.mdi-download-circle::before{content:"\F1464"}.mdi-download-circle-outline::before{content:"\F1465"}.mdi-download-lock::before{content:"\F1320"}.mdi-download-lock-outline::before{content:"\F1321"}.mdi-download-multiple::before{content:"\F09E9"}.mdi-download-multiple-outline::before{content:"\F1CD0"}.mdi-download-network::before{content:"\F06F4"}.mdi-download-network-outline::before{content:"\F0C66"}.mdi-download-off::before{content:"\F10B0"}.mdi-download-off-outline::before{content:"\F10B1"}.mdi-download-outline::before{content:"\F0B8F"}.mdi-drag::before{content:"\F01DB"}.mdi-drag-horizontal::before{content:"\F01DC"}.mdi-drag-horizontal-variant::before{content:"\F12F0"}.mdi-drag-variant::before{content:"\F0B90"}.mdi-drag-vertical::before{content:"\F01DD"}.mdi-drag-vertical-variant::before{content:"\F12F1"}.mdi-drama-masks::before{content:"\F0D02"}.mdi-draw::before{content:"\F0F49"}.mdi-draw-pen::before{content:"\F19B9"}.mdi-drawing::before{content:"\F01DE"}.mdi-drawing-box::before{content:"\F01DF"}.mdi-dresser::before{content:"\F0F4A"}.mdi-dresser-outline::before{content:"\F0F4B"}.mdi-drone::before{content:"\F01E2"}.mdi-dropbox::before{content:"\F01E3"}.mdi-drupal::before{content:"\F01E4"}.mdi-duck::before{content:"\F01E5"}.mdi-dumbbell::before{content:"\F01E6"}.mdi-dump-truck::before{content:"\F0C67"}.mdi-ear-hearing::before{content:"\F07C5"}.mdi-ear-hearing-loop::before{content:"\F1AEE"}.mdi-ear-hearing-off::before{content:"\F0A45"}.mdi-earbuds::before{content:"\F184F"}.mdi-earbuds-off::before{content:"\F1850"}.mdi-earbuds-off-outline::before{content:"\F1851"}.mdi-earbuds-outline::before{content:"\F1852"}.mdi-earth::before{content:"\F01E7"}.mdi-earth-arrow-down::before{content:"\F1C87"}.mdi-earth-arrow-left::before{content:"\F1C88"}.mdi-earth-arrow-right::before{content:"\F1311"}.mdi-earth-arrow-up::before{content:"\F1C89"}.mdi-earth-box::before{content:"\F06CD"}.mdi-earth-box-minus::before{content:"\F1407"}.mdi-earth-box-off::before{content:"\F06CE"}.mdi-earth-box-plus::before{content:"\F1406"}.mdi-earth-box-remove::before{content:"\F1408"}.mdi-earth-minus::before{content:"\F1404"}.mdi-earth-off::before{content:"\F01E8"}.mdi-earth-plus::before{content:"\F1403"}.mdi-earth-remove::before{content:"\F1405"}.mdi-egg::before{content:"\F0AAF"}.mdi-egg-easter::before{content:"\F0AB0"}.mdi-egg-fried::before{content:"\F184A"}.mdi-egg-off::before{content:"\F13F0"}.mdi-egg-off-outline::before{content:"\F13F1"}.mdi-egg-outline::before{content:"\F13F2"}.mdi-eiffel-tower::before{content:"\F156B"}.mdi-eight-track::before{content:"\F09EA"}.mdi-eject::before{content:"\F01EA"}.mdi-eject-circle::before{content:"\F1B23"}.mdi-eject-circle-outline::before{content:"\F1B24"}.mdi-eject-outline::before{content:"\F0B91"}.mdi-electric-switch::before{content:"\F0E9F"}.mdi-electric-switch-closed::before{content:"\F10D9"}.mdi-electron-framework::before{content:"\F1024"}.mdi-elephant::before{content:"\F07C6"}.mdi-elevation-decline::before{content:"\F01EB"}.mdi-elevation-rise::before{content:"\F01EC"}.mdi-elevator::before{content:"\F01ED"}.mdi-elevator-down::before{content:"\F12C2"}.mdi-elevator-passenger::before{content:"\F1381"}.mdi-elevator-passenger-off::before{content:"\F1979"}.mdi-elevator-passenger-off-outline::before{content:"\F197A"}.mdi-elevator-passenger-outline::before{content:"\F197B"}.mdi-elevator-up::before{content:"\F12C1"}.mdi-ellipse::before{content:"\F0EA0"}.mdi-ellipse-outline::before{content:"\F0EA1"}.mdi-email::before{content:"\F01EE"}.mdi-email-alert::before{content:"\F06CF"}.mdi-email-alert-outline::before{content:"\F0D42"}.mdi-email-arrow-left::before{content:"\F10DA"}.mdi-email-arrow-left-outline::before{content:"\F10DB"}.mdi-email-arrow-right::before{content:"\F10DC"}.mdi-email-arrow-right-outline::before{content:"\F10DD"}.mdi-email-box::before{content:"\F0D03"}.mdi-email-check::before{content:"\F0AB1"}.mdi-email-check-outline::before{content:"\F0AB2"}.mdi-email-edit::before{content:"\F0EE3"}.mdi-email-edit-outline::before{content:"\F0EE4"}.mdi-email-fast::before{content:"\F186F"}.mdi-email-fast-outline::before{content:"\F1870"}.mdi-email-heart-outline::before{content:"\F1C5B"}.mdi-email-lock::before{content:"\F01F1"}.mdi-email-lock-outline::before{content:"\F1B61"}.mdi-email-mark-as-unread::before{content:"\F0B92"}.mdi-email-minus::before{content:"\F0EE5"}.mdi-email-minus-outline::before{content:"\F0EE6"}.mdi-email-multiple::before{content:"\F0EE7"}.mdi-email-multiple-outline::before{content:"\F0EE8"}.mdi-email-newsletter::before{content:"\F0FB1"}.mdi-email-off::before{content:"\F13E3"}.mdi-email-off-outline::before{content:"\F13E4"}.mdi-email-open::before{content:"\F01EF"}.mdi-email-open-heart-outline::before{content:"\F1C5C"}.mdi-email-open-multiple::before{content:"\F0EE9"}.mdi-email-open-multiple-outline::before{content:"\F0EEA"}.mdi-email-open-outline::before{content:"\F05EF"}.mdi-email-outline::before{content:"\F01F0"}.mdi-email-plus::before{content:"\F09EB"}.mdi-email-plus-outline::before{content:"\F09EC"}.mdi-email-remove::before{content:"\F1661"}.mdi-email-remove-outline::before{content:"\F1662"}.mdi-email-seal::before{content:"\F195B"}.mdi-email-seal-outline::before{content:"\F195C"}.mdi-email-search::before{content:"\F0961"}.mdi-email-search-outline::before{content:"\F0962"}.mdi-email-sync::before{content:"\F12C7"}.mdi-email-sync-outline::before{content:"\F12C8"}.mdi-email-variant::before{content:"\F05F0"}.mdi-ember::before{content:"\F0B30"}.mdi-emby::before{content:"\F06B4"}.mdi-emoticon::before{content:"\F0C68"}.mdi-emoticon-angry::before{content:"\F0C69"}.mdi-emoticon-angry-outline::before{content:"\F0C6A"}.mdi-emoticon-confused::before{content:"\F10DE"}.mdi-emoticon-confused-outline::before{content:"\F10DF"}.mdi-emoticon-cool::before{content:"\F0C6B"}.mdi-emoticon-cool-outline::before{content:"\F01F3"}.mdi-emoticon-cry::before{content:"\F0C6C"}.mdi-emoticon-cry-outline::before{content:"\F0C6D"}.mdi-emoticon-dead::before{content:"\F0C6E"}.mdi-emoticon-dead-outline::before{content:"\F069B"}.mdi-emoticon-devil::before{content:"\F0C6F"}.mdi-emoticon-devil-outline::before{content:"\F01F4"}.mdi-emoticon-excited::before{content:"\F0C70"}.mdi-emoticon-excited-outline::before{content:"\F069C"}.mdi-emoticon-frown::before{content:"\F0F4C"}.mdi-emoticon-frown-outline::before{content:"\F0F4D"}.mdi-emoticon-happy::before{content:"\F0C71"}.mdi-emoticon-happy-outline::before{content:"\F01F5"}.mdi-emoticon-kiss::before{content:"\F0C72"}.mdi-emoticon-kiss-outline::before{content:"\F0C73"}.mdi-emoticon-lol::before{content:"\F1214"}.mdi-emoticon-lol-outline::before{content:"\F1215"}.mdi-emoticon-minus::before{content:"\F1CB2"}.mdi-emoticon-minus-outline::before{content:"\F1CB3"}.mdi-emoticon-neutral::before{content:"\F0C74"}.mdi-emoticon-neutral-outline::before{content:"\F01F6"}.mdi-emoticon-outline::before{content:"\F01F2"}.mdi-emoticon-plus::before{content:"\F1CB4"}.mdi-emoticon-plus-outline::before{content:"\F1CB5"}.mdi-emoticon-poop::before{content:"\F01F7"}.mdi-emoticon-poop-outline::before{content:"\F0C75"}.mdi-emoticon-remove::before{content:"\F1CB6"}.mdi-emoticon-remove-outline::before{content:"\F1CB7"}.mdi-emoticon-sad::before{content:"\F0C76"}.mdi-emoticon-sad-outline::before{content:"\F01F8"}.mdi-emoticon-sick::before{content:"\F157C"}.mdi-emoticon-sick-outline::before{content:"\F157D"}.mdi-emoticon-tongue::before{content:"\F01F9"}.mdi-emoticon-tongue-outline::before{content:"\F0C77"}.mdi-emoticon-wink::before{content:"\F0C78"}.mdi-emoticon-wink-outline::before{content:"\F0C79"}.mdi-engine::before{content:"\F01FA"}.mdi-engine-off::before{content:"\F0A46"}.mdi-engine-off-outline::before{content:"\F0A47"}.mdi-engine-outline::before{content:"\F01FB"}.mdi-epsilon::before{content:"\F10E0"}.mdi-equal::before{content:"\F01FC"}.mdi-equal-box::before{content:"\F01FD"}.mdi-equalizer::before{content:"\F0EA2"}.mdi-equalizer-outline::before{content:"\F0EA3"}.mdi-eraser::before{content:"\F01FE"}.mdi-eraser-variant::before{content:"\F0642"}.mdi-escalator::before{content:"\F01FF"}.mdi-escalator-box::before{content:"\F1399"}.mdi-escalator-down::before{content:"\F12C0"}.mdi-escalator-up::before{content:"\F12BF"}.mdi-eslint::before{content:"\F0C7A"}.mdi-et::before{content:"\F0AB3"}.mdi-ethereum::before{content:"\F086A"}.mdi-ethernet::before{content:"\F0200"}.mdi-ethernet-cable::before{content:"\F0201"}.mdi-ethernet-cable-off::before{content:"\F0202"}.mdi-ethernet-off::before{content:"\F1CD1"}.mdi-ev-plug-ccs1::before{content:"\F1519"}.mdi-ev-plug-ccs2::before{content:"\F151A"}.mdi-ev-plug-chademo::before{content:"\F151B"}.mdi-ev-plug-tesla::before{content:"\F151C"}.mdi-ev-plug-type1::before{content:"\F151D"}.mdi-ev-plug-type2::before{content:"\F151E"}.mdi-ev-station::before{content:"\F05F1"}.mdi-evernote::before{content:"\F0204"}.mdi-excavator::before{content:"\F1025"}.mdi-exclamation::before{content:"\F0205"}.mdi-exclamation-thick::before{content:"\F1238"}.mdi-exit-run::before{content:"\F0A48"}.mdi-exit-to-app::before{content:"\F0206"}.mdi-expand-all::before{content:"\F0AB4"}.mdi-expand-all-outline::before{content:"\F0AB5"}.mdi-expansion-card::before{content:"\F08AE"}.mdi-expansion-card-variant::before{content:"\F0FB2"}.mdi-exponent::before{content:"\F0963"}.mdi-exponent-box::before{content:"\F0964"}.mdi-export::before{content:"\F0207"}.mdi-export-variant::before{content:"\F0B93"}.mdi-eye::before{content:"\F0208"}.mdi-eye-arrow-left::before{content:"\F18FD"}.mdi-eye-arrow-left-outline::before{content:"\F18FE"}.mdi-eye-arrow-right::before{content:"\F18FF"}.mdi-eye-arrow-right-outline::before{content:"\F1900"}.mdi-eye-check::before{content:"\F0D04"}.mdi-eye-check-outline::before{content:"\F0D05"}.mdi-eye-circle::before{content:"\F0B94"}.mdi-eye-circle-outline::before{content:"\F0B95"}.mdi-eye-closed::before{content:"\F1CA3"}.mdi-eye-lock::before{content:"\F1C06"}.mdi-eye-lock-open::before{content:"\F1C07"}.mdi-eye-lock-open-outline::before{content:"\F1C08"}.mdi-eye-lock-outline::before{content:"\F1C09"}.mdi-eye-minus::before{content:"\F1026"}.mdi-eye-minus-outline::before{content:"\F1027"}.mdi-eye-off::before{content:"\F0209"}.mdi-eye-off-outline::before{content:"\F06D1"}.mdi-eye-outline::before{content:"\F06D0"}.mdi-eye-plus::before{content:"\F086B"}.mdi-eye-plus-outline::before{content:"\F086C"}.mdi-eye-refresh::before{content:"\F197C"}.mdi-eye-refresh-outline::before{content:"\F197D"}.mdi-eye-remove::before{content:"\F15E3"}.mdi-eye-remove-outline::before{content:"\F15E4"}.mdi-eye-settings::before{content:"\F086D"}.mdi-eye-settings-outline::before{content:"\F086E"}.mdi-eyedropper::before{content:"\F020A"}.mdi-eyedropper-minus::before{content:"\F13DD"}.mdi-eyedropper-off::before{content:"\F13DF"}.mdi-eyedropper-plus::before{content:"\F13DC"}.mdi-eyedropper-remove::before{content:"\F13DE"}.mdi-eyedropper-variant::before{content:"\F020B"}.mdi-face-agent::before{content:"\F0D70"}.mdi-face-man::before{content:"\F0643"}.mdi-face-man-outline::before{content:"\F0B96"}.mdi-face-man-profile::before{content:"\F0644"}.mdi-face-man-shimmer::before{content:"\F15CC"}.mdi-face-man-shimmer-outline::before{content:"\F15CD"}.mdi-face-mask::before{content:"\F1586"}.mdi-face-mask-outline::before{content:"\F1587"}.mdi-face-recognition::before{content:"\F0C7B"}.mdi-face-woman::before{content:"\F1077"}.mdi-face-woman-outline::before{content:"\F1078"}.mdi-face-woman-profile::before{content:"\F1076"}.mdi-face-woman-shimmer::before{content:"\F15CE"}.mdi-face-woman-shimmer-outline::before{content:"\F15CF"}.mdi-facebook::before{content:"\F020C"}.mdi-facebook-gaming::before{content:"\F07DD"}.mdi-facebook-messenger::before{content:"\F020E"}.mdi-facebook-workplace::before{content:"\F0B31"}.mdi-factory::before{content:"\F020F"}.mdi-family-tree::before{content:"\F160E"}.mdi-fan::before{content:"\F0210"}.mdi-fan-alert::before{content:"\F146C"}.mdi-fan-auto::before{content:"\F171D"}.mdi-fan-chevron-down::before{content:"\F146D"}.mdi-fan-chevron-up::before{content:"\F146E"}.mdi-fan-clock::before{content:"\F1A3A"}.mdi-fan-minus::before{content:"\F1470"}.mdi-fan-off::before{content:"\F081D"}.mdi-fan-plus::before{content:"\F146F"}.mdi-fan-remove::before{content:"\F1471"}.mdi-fan-speed-1::before{content:"\F1472"}.mdi-fan-speed-2::before{content:"\F1473"}.mdi-fan-speed-3::before{content:"\F1474"}.mdi-fast-forward::before{content:"\F0211"}.mdi-fast-forward-10::before{content:"\F0D71"}.mdi-fast-forward-15::before{content:"\F193A"}.mdi-fast-forward-30::before{content:"\F0D06"}.mdi-fast-forward-45::before{content:"\F1B12"}.mdi-fast-forward-5::before{content:"\F11F8"}.mdi-fast-forward-60::before{content:"\F160B"}.mdi-fast-forward-outline::before{content:"\F06D2"}.mdi-faucet::before{content:"\F1B29"}.mdi-faucet-variant::before{content:"\F1B2A"}.mdi-fax::before{content:"\F0212"}.mdi-feather::before{content:"\F06D3"}.mdi-feature-search::before{content:"\F0A49"}.mdi-feature-search-outline::before{content:"\F0A4A"}.mdi-fedora::before{content:"\F08DB"}.mdi-fence::before{content:"\F179A"}.mdi-fence-electric::before{content:"\F17F6"}.mdi-fencing::before{content:"\F14C1"}.mdi-ferris-wheel::before{content:"\F0EA4"}.mdi-ferry::before{content:"\F0213"}.mdi-file::before{content:"\F0214"}.mdi-file-account::before{content:"\F073B"}.mdi-file-account-outline::before{content:"\F1028"}.mdi-file-alert::before{content:"\F0A4B"}.mdi-file-alert-outline::before{content:"\F0A4C"}.mdi-file-arrow-left-right::before{content:"\F1A93"}.mdi-file-arrow-left-right-outline::before{content:"\F1A94"}.mdi-file-arrow-up-down::before{content:"\F1A95"}.mdi-file-arrow-up-down-outline::before{content:"\F1A96"}.mdi-file-cabinet::before{content:"\F0AB6"}.mdi-file-cad::before{content:"\F0EEB"}.mdi-file-cad-box::before{content:"\F0EEC"}.mdi-file-cancel::before{content:"\F0DC6"}.mdi-file-cancel-outline::before{content:"\F0DC7"}.mdi-file-certificate::before{content:"\F1186"}.mdi-file-certificate-outline::before{content:"\F1187"}.mdi-file-chart::before{content:"\F0215"}.mdi-file-chart-check::before{content:"\F19C6"}.mdi-file-chart-check-outline::before{content:"\F19C7"}.mdi-file-chart-outline::before{content:"\F1029"}.mdi-file-check::before{content:"\F0216"}.mdi-file-check-outline::before{content:"\F0E29"}.mdi-file-clock::before{content:"\F12E1"}.mdi-file-clock-outline::before{content:"\F12E2"}.mdi-file-cloud::before{content:"\F0217"}.mdi-file-cloud-outline::before{content:"\F102A"}.mdi-file-code::before{content:"\F022E"}.mdi-file-code-outline::before{content:"\F102B"}.mdi-file-cog::before{content:"\F107B"}.mdi-file-cog-outline::before{content:"\F107C"}.mdi-file-compare::before{content:"\F08AA"}.mdi-file-delimited::before{content:"\F0218"}.mdi-file-delimited-outline::before{content:"\F0EA5"}.mdi-file-document::before{content:"\F0219"}.mdi-file-document-alert::before{content:"\F1A97"}.mdi-file-document-alert-outline::before{content:"\F1A98"}.mdi-file-document-arrow-right::before{content:"\F1C0F"}.mdi-file-document-arrow-right-outline::before{content:"\F1C10"}.mdi-file-document-check::before{content:"\F1A99"}.mdi-file-document-check-outline::before{content:"\F1A9A"}.mdi-file-document-edit::before{content:"\F0DC8"}.mdi-file-document-edit-outline::before{content:"\F0DC9"}.mdi-file-document-minus::before{content:"\F1A9B"}.mdi-file-document-minus-outline::before{content:"\F1A9C"}.mdi-file-document-multiple::before{content:"\F1517"}.mdi-file-document-multiple-outline::before{content:"\F1518"}.mdi-file-document-outline::before{content:"\F09EE"}.mdi-file-document-plus::before{content:"\F1A9D"}.mdi-file-document-plus-outline::before{content:"\F1A9E"}.mdi-file-document-refresh::before{content:"\F1C7A"}.mdi-file-document-refresh-outline::before{content:"\F1C7B"}.mdi-file-document-remove::before{content:"\F1A9F"}.mdi-file-document-remove-outline::before{content:"\F1AA0"}.mdi-file-download::before{content:"\F0965"}.mdi-file-download-outline::before{content:"\F0966"}.mdi-file-edit::before{content:"\F11E7"}.mdi-file-edit-outline::before{content:"\F11E8"}.mdi-file-excel::before{content:"\F021B"}.mdi-file-excel-box::before{content:"\F021C"}.mdi-file-excel-box-outline::before{content:"\F102C"}.mdi-file-excel-outline::before{content:"\F102D"}.mdi-file-export::before{content:"\F021D"}.mdi-file-export-outline::before{content:"\F102E"}.mdi-file-eye::before{content:"\F0DCA"}.mdi-file-eye-outline::before{content:"\F0DCB"}.mdi-file-find::before{content:"\F021E"}.mdi-file-find-outline::before{content:"\F0B97"}.mdi-file-gif-box::before{content:"\F0D78"}.mdi-file-hidden::before{content:"\F0613"}.mdi-file-image::before{content:"\F021F"}.mdi-file-image-marker::before{content:"\F1772"}.mdi-file-image-marker-outline::before{content:"\F1773"}.mdi-file-image-minus::before{content:"\F193B"}.mdi-file-image-minus-outline::before{content:"\F193C"}.mdi-file-image-outline::before{content:"\F0EB0"}.mdi-file-image-plus::before{content:"\F193D"}.mdi-file-image-plus-outline::before{content:"\F193E"}.mdi-file-image-remove::before{content:"\F193F"}.mdi-file-image-remove-outline::before{content:"\F1940"}.mdi-file-import::before{content:"\F0220"}.mdi-file-import-outline::before{content:"\F102F"}.mdi-file-jpg-box::before{content:"\F0225"}.mdi-file-key::before{content:"\F1184"}.mdi-file-key-outline::before{content:"\F1185"}.mdi-file-link::before{content:"\F1177"}.mdi-file-link-outline::before{content:"\F1178"}.mdi-file-lock::before{content:"\F0221"}.mdi-file-lock-open::before{content:"\F19C8"}.mdi-file-lock-open-outline::before{content:"\F19C9"}.mdi-file-lock-outline::before{content:"\F1030"}.mdi-file-marker::before{content:"\F1774"}.mdi-file-marker-outline::before{content:"\F1775"}.mdi-file-minus::before{content:"\F1AA1"}.mdi-file-minus-outline::before{content:"\F1AA2"}.mdi-file-move::before{content:"\F0AB9"}.mdi-file-move-outline::before{content:"\F1031"}.mdi-file-multiple::before{content:"\F0222"}.mdi-file-multiple-outline::before{content:"\F1032"}.mdi-file-music::before{content:"\F0223"}.mdi-file-music-outline::before{content:"\F0E2A"}.mdi-file-outline::before{content:"\F0224"}.mdi-file-pdf-box::before{content:"\F0226"}.mdi-file-percent::before{content:"\F081E"}.mdi-file-percent-outline::before{content:"\F1033"}.mdi-file-phone::before{content:"\F1179"}.mdi-file-phone-outline::before{content:"\F117A"}.mdi-file-plus::before{content:"\F0752"}.mdi-file-plus-outline::before{content:"\F0EED"}.mdi-file-png-box::before{content:"\F0E2D"}.mdi-file-powerpoint::before{content:"\F0227"}.mdi-file-powerpoint-box::before{content:"\F0228"}.mdi-file-powerpoint-box-outline::before{content:"\F1034"}.mdi-file-powerpoint-outline::before{content:"\F1035"}.mdi-file-presentation-box::before{content:"\F0229"}.mdi-file-question::before{content:"\F086F"}.mdi-file-question-outline::before{content:"\F1036"}.mdi-file-refresh::before{content:"\F0918"}.mdi-file-refresh-outline::before{content:"\F0541"}.mdi-file-remove::before{content:"\F0B98"}.mdi-file-remove-outline::before{content:"\F1037"}.mdi-file-replace::before{content:"\F0B32"}.mdi-file-replace-outline::before{content:"\F0B33"}.mdi-file-restore::before{content:"\F0670"}.mdi-file-restore-outline::before{content:"\F1038"}.mdi-file-rotate-left::before{content:"\F1A3B"}.mdi-file-rotate-left-outline::before{content:"\F1A3C"}.mdi-file-rotate-right::before{content:"\F1A3D"}.mdi-file-rotate-right-outline::before{content:"\F1A3E"}.mdi-file-search::before{content:"\F0C7C"}.mdi-file-search-outline::before{content:"\F0C7D"}.mdi-file-send::before{content:"\F022A"}.mdi-file-send-outline::before{content:"\F1039"}.mdi-file-settings::before{content:"\F1079"}.mdi-file-settings-outline::before{content:"\F107A"}.mdi-file-sign::before{content:"\F19C3"}.mdi-file-star::before{content:"\F103A"}.mdi-file-star-four-points::before{content:"\F1C2D"}.mdi-file-star-four-points-outline::before{content:"\F1C2E"}.mdi-file-star-outline::before{content:"\F103B"}.mdi-file-swap::before{content:"\F0FB4"}.mdi-file-swap-outline::before{content:"\F0FB5"}.mdi-file-sync::before{content:"\F1216"}.mdi-file-sync-outline::before{content:"\F1217"}.mdi-file-table::before{content:"\F0C7E"}.mdi-file-table-box::before{content:"\F10E1"}.mdi-file-table-box-multiple::before{content:"\F10E2"}.mdi-file-table-box-multiple-outline::before{content:"\F10E3"}.mdi-file-table-box-outline::before{content:"\F10E4"}.mdi-file-table-outline::before{content:"\F0C7F"}.mdi-file-tree::before{content:"\F0645"}.mdi-file-tree-outline::before{content:"\F13D2"}.mdi-file-undo::before{content:"\F08DC"}.mdi-file-undo-outline::before{content:"\F103C"}.mdi-file-upload::before{content:"\F0A4D"}.mdi-file-upload-outline::before{content:"\F0A4E"}.mdi-file-video::before{content:"\F022B"}.mdi-file-video-outline::before{content:"\F0E2C"}.mdi-file-word::before{content:"\F022C"}.mdi-file-word-box::before{content:"\F022D"}.mdi-file-word-box-outline::before{content:"\F103D"}.mdi-file-word-outline::before{content:"\F103E"}.mdi-file-xml-box::before{content:"\F1B4B"}.mdi-film::before{content:"\F022F"}.mdi-filmstrip::before{content:"\F0230"}.mdi-filmstrip-box::before{content:"\F0332"}.mdi-filmstrip-box-multiple::before{content:"\F0D18"}.mdi-filmstrip-off::before{content:"\F0231"}.mdi-filter::before{content:"\F0232"}.mdi-filter-check::before{content:"\F18EC"}.mdi-filter-check-outline::before{content:"\F18ED"}.mdi-filter-cog::before{content:"\F1AA3"}.mdi-filter-cog-outline::before{content:"\F1AA4"}.mdi-filter-menu::before{content:"\F10E5"}.mdi-filter-menu-outline::before{content:"\F10E6"}.mdi-filter-minus::before{content:"\F0EEE"}.mdi-filter-minus-outline::before{content:"\F0EEF"}.mdi-filter-multiple::before{content:"\F1A3F"}.mdi-filter-multiple-outline::before{content:"\F1A40"}.mdi-filter-off::before{content:"\F14EF"}.mdi-filter-off-outline::before{content:"\F14F0"}.mdi-filter-outline::before{content:"\F0233"}.mdi-filter-plus::before{content:"\F0EF0"}.mdi-filter-plus-outline::before{content:"\F0EF1"}.mdi-filter-remove::before{content:"\F0234"}.mdi-filter-remove-outline::before{content:"\F0235"}.mdi-filter-settings::before{content:"\F1AA5"}.mdi-filter-settings-outline::before{content:"\F1AA6"}.mdi-filter-variant::before{content:"\F0236"}.mdi-filter-variant-minus::before{content:"\F1112"}.mdi-filter-variant-plus::before{content:"\F1113"}.mdi-filter-variant-remove::before{content:"\F103F"}.mdi-finance::before{content:"\F081F"}.mdi-find-replace::before{content:"\F06D4"}.mdi-fingerprint::before{content:"\F0237"}.mdi-fingerprint-off::before{content:"\F0EB1"}.mdi-fire::before{content:"\F0238"}.mdi-fire-alert::before{content:"\F15D7"}.mdi-fire-circle::before{content:"\F1807"}.mdi-fire-extinguisher::before{content:"\F0EF2"}.mdi-fire-hydrant::before{content:"\F1137"}.mdi-fire-hydrant-alert::before{content:"\F1138"}.mdi-fire-hydrant-off::before{content:"\F1139"}.mdi-fire-off::before{content:"\F1722"}.mdi-fire-station::before{content:"\F1CC3"}.mdi-fire-truck::before{content:"\F08AB"}.mdi-firebase::before{content:"\F0967"}.mdi-firefox::before{content:"\F0239"}.mdi-fireplace::before{content:"\F0E2E"}.mdi-fireplace-off::before{content:"\F0E2F"}.mdi-firewire::before{content:"\F05BE"}.mdi-firework::before{content:"\F0E30"}.mdi-firework-off::before{content:"\F1723"}.mdi-fish::before{content:"\F023A"}.mdi-fish-off::before{content:"\F13F3"}.mdi-fishbowl::before{content:"\F0EF3"}.mdi-fishbowl-outline::before{content:"\F0EF4"}.mdi-fit-to-page::before{content:"\F0EF5"}.mdi-fit-to-page-outline::before{content:"\F0EF6"}.mdi-fit-to-screen::before{content:"\F18F4"}.mdi-fit-to-screen-outline::before{content:"\F18F5"}.mdi-flag::before{content:"\F023B"}.mdi-flag-checkered::before{content:"\F023C"}.mdi-flag-minus::before{content:"\F0B99"}.mdi-flag-minus-outline::before{content:"\F10B2"}.mdi-flag-off::before{content:"\F18EE"}.mdi-flag-off-outline::before{content:"\F18EF"}.mdi-flag-outline::before{content:"\F023D"}.mdi-flag-plus::before{content:"\F0B9A"}.mdi-flag-plus-outline::before{content:"\F10B3"}.mdi-flag-remove::before{content:"\F0B9B"}.mdi-flag-remove-outline::before{content:"\F10B4"}.mdi-flag-triangle::before{content:"\F023F"}.mdi-flag-variant::before{content:"\F0240"}.mdi-flag-variant-minus::before{content:"\F1BB4"}.mdi-flag-variant-minus-outline::before{content:"\F1BB5"}.mdi-flag-variant-off::before{content:"\F1BB0"}.mdi-flag-variant-off-outline::before{content:"\F1BB1"}.mdi-flag-variant-outline::before{content:"\F023E"}.mdi-flag-variant-plus::before{content:"\F1BB2"}.mdi-flag-variant-plus-outline::before{content:"\F1BB3"}.mdi-flag-variant-remove::before{content:"\F1BB6"}.mdi-flag-variant-remove-outline::before{content:"\F1BB7"}.mdi-flare::before{content:"\F0D72"}.mdi-flash::before{content:"\F0241"}.mdi-flash-alert::before{content:"\F0EF7"}.mdi-flash-alert-outline::before{content:"\F0EF8"}.mdi-flash-auto::before{content:"\F0242"}.mdi-flash-off::before{content:"\F0243"}.mdi-flash-off-outline::before{content:"\F1B45"}.mdi-flash-outline::before{content:"\F06D5"}.mdi-flash-red-eye::before{content:"\F067B"}.mdi-flash-triangle::before{content:"\F1B1D"}.mdi-flash-triangle-outline::before{content:"\F1B1E"}.mdi-flashlight::before{content:"\F0244"}.mdi-flashlight-off::before{content:"\F0245"}.mdi-flask::before{content:"\F0093"}.mdi-flask-empty::before{content:"\F0094"}.mdi-flask-empty-minus::before{content:"\F123A"}.mdi-flask-empty-minus-outline::before{content:"\F123B"}.mdi-flask-empty-off::before{content:"\F13F4"}.mdi-flask-empty-off-outline::before{content:"\F13F5"}.mdi-flask-empty-outline::before{content:"\F0095"}.mdi-flask-empty-plus::before{content:"\F123C"}.mdi-flask-empty-plus-outline::before{content:"\F123D"}.mdi-flask-empty-remove::before{content:"\F123E"}.mdi-flask-empty-remove-outline::before{content:"\F123F"}.mdi-flask-minus::before{content:"\F1240"}.mdi-flask-minus-outline::before{content:"\F1241"}.mdi-flask-off::before{content:"\F13F6"}.mdi-flask-off-outline::before{content:"\F13F7"}.mdi-flask-outline::before{content:"\F0096"}.mdi-flask-plus::before{content:"\F1242"}.mdi-flask-plus-outline::before{content:"\F1243"}.mdi-flask-remove::before{content:"\F1244"}.mdi-flask-remove-outline::before{content:"\F1245"}.mdi-flask-round-bottom::before{content:"\F124B"}.mdi-flask-round-bottom-empty::before{content:"\F124C"}.mdi-flask-round-bottom-empty-outline::before{content:"\F124D"}.mdi-flask-round-bottom-outline::before{content:"\F124E"}.mdi-fleur-de-lis::before{content:"\F1303"}.mdi-flip-horizontal::before{content:"\F10E7"}.mdi-flip-to-back::before{content:"\F0247"}.mdi-flip-to-front::before{content:"\F0248"}.mdi-flip-vertical::before{content:"\F10E8"}.mdi-floor-lamp::before{content:"\F08DD"}.mdi-floor-lamp-dual::before{content:"\F1040"}.mdi-floor-lamp-dual-outline::before{content:"\F17CE"}.mdi-floor-lamp-outline::before{content:"\F17C8"}.mdi-floor-lamp-torchiere::before{content:"\F1747"}.mdi-floor-lamp-torchiere-outline::before{content:"\F17D6"}.mdi-floor-lamp-torchiere-variant::before{content:"\F1041"}.mdi-floor-lamp-torchiere-variant-outline::before{content:"\F17CF"}.mdi-floor-plan::before{content:"\F0821"}.mdi-floppy::before{content:"\F0249"}.mdi-floppy-variant::before{content:"\F09EF"}.mdi-flower::before{content:"\F024A"}.mdi-flower-outline::before{content:"\F09F0"}.mdi-flower-pollen::before{content:"\F1885"}.mdi-flower-pollen-outline::before{content:"\F1886"}.mdi-flower-poppy::before{content:"\F0D08"}.mdi-flower-tulip::before{content:"\F09F1"}.mdi-flower-tulip-outline::before{content:"\F09F2"}.mdi-focus-auto::before{content:"\F0F4E"}.mdi-focus-field::before{content:"\F0F4F"}.mdi-focus-field-horizontal::before{content:"\F0F50"}.mdi-focus-field-vertical::before{content:"\F0F51"}.mdi-folder::before{content:"\F024B"}.mdi-folder-account::before{content:"\F024C"}.mdi-folder-account-outline::before{content:"\F0B9C"}.mdi-folder-alert::before{content:"\F0DCC"}.mdi-folder-alert-outline::before{content:"\F0DCD"}.mdi-folder-arrow-down::before{content:"\F19E8"}.mdi-folder-arrow-down-outline::before{content:"\F19E9"}.mdi-folder-arrow-left::before{content:"\F19EA"}.mdi-folder-arrow-left-outline::before{content:"\F19EB"}.mdi-folder-arrow-left-right::before{content:"\F19EC"}.mdi-folder-arrow-left-right-outline::before{content:"\F19ED"}.mdi-folder-arrow-right::before{content:"\F19EE"}.mdi-folder-arrow-right-outline::before{content:"\F19EF"}.mdi-folder-arrow-up::before{content:"\F19F0"}.mdi-folder-arrow-up-down::before{content:"\F19F1"}.mdi-folder-arrow-up-down-outline::before{content:"\F19F2"}.mdi-folder-arrow-up-outline::before{content:"\F19F3"}.mdi-folder-cancel::before{content:"\F19F4"}.mdi-folder-cancel-outline::before{content:"\F19F5"}.mdi-folder-check::before{content:"\F197E"}.mdi-folder-check-outline::before{content:"\F197F"}.mdi-folder-clock::before{content:"\F0ABA"}.mdi-folder-clock-outline::before{content:"\F0ABB"}.mdi-folder-cog::before{content:"\F107F"}.mdi-folder-cog-outline::before{content:"\F1080"}.mdi-folder-download::before{content:"\F024D"}.mdi-folder-download-outline::before{content:"\F10E9"}.mdi-folder-edit::before{content:"\F08DE"}.mdi-folder-edit-outline::before{content:"\F0DCE"}.mdi-folder-eye::before{content:"\F178A"}.mdi-folder-eye-outline::before{content:"\F178B"}.mdi-folder-file::before{content:"\F19F6"}.mdi-folder-file-outline::before{content:"\F19F7"}.mdi-folder-google-drive::before{content:"\F024E"}.mdi-folder-heart::before{content:"\F10EA"}.mdi-folder-heart-outline::before{content:"\F10EB"}.mdi-folder-hidden::before{content:"\F179E"}.mdi-folder-home::before{content:"\F10B5"}.mdi-folder-home-outline::before{content:"\F10B6"}.mdi-folder-image::before{content:"\F024F"}.mdi-folder-information::before{content:"\F10B7"}.mdi-folder-information-outline::before{content:"\F10B8"}.mdi-folder-key::before{content:"\F08AC"}.mdi-folder-key-network::before{content:"\F08AD"}.mdi-folder-key-network-outline::before{content:"\F0C80"}.mdi-folder-key-outline::before{content:"\F10EC"}.mdi-folder-lock::before{content:"\F0250"}.mdi-folder-lock-open::before{content:"\F0251"}.mdi-folder-lock-open-outline::before{content:"\F1AA7"}.mdi-folder-lock-outline::before{content:"\F1AA8"}.mdi-folder-marker::before{content:"\F126D"}.mdi-folder-marker-outline::before{content:"\F126E"}.mdi-folder-minus::before{content:"\F1B49"}.mdi-folder-minus-outline::before{content:"\F1B4A"}.mdi-folder-move::before{content:"\F0252"}.mdi-folder-move-outline::before{content:"\F1246"}.mdi-folder-multiple::before{content:"\F0253"}.mdi-folder-multiple-image::before{content:"\F0254"}.mdi-folder-multiple-outline::before{content:"\F0255"}.mdi-folder-multiple-plus::before{content:"\F147E"}.mdi-folder-multiple-plus-outline::before{content:"\F147F"}.mdi-folder-music::before{content:"\F1359"}.mdi-folder-music-outline::before{content:"\F135A"}.mdi-folder-network::before{content:"\F0870"}.mdi-folder-network-outline::before{content:"\F0C81"}.mdi-folder-off::before{content:"\F19F8"}.mdi-folder-off-outline::before{content:"\F19F9"}.mdi-folder-open::before{content:"\F0770"}.mdi-folder-open-outline::before{content:"\F0DCF"}.mdi-folder-outline::before{content:"\F0256"}.mdi-folder-play::before{content:"\F19FA"}.mdi-folder-play-outline::before{content:"\F19FB"}.mdi-folder-plus::before{content:"\F0257"}.mdi-folder-plus-outline::before{content:"\F0B9D"}.mdi-folder-pound::before{content:"\F0D09"}.mdi-folder-pound-outline::before{content:"\F0D0A"}.mdi-folder-question::before{content:"\F19CA"}.mdi-folder-question-outline::before{content:"\F19CB"}.mdi-folder-refresh::before{content:"\F0749"}.mdi-folder-refresh-outline::before{content:"\F0542"}.mdi-folder-remove::before{content:"\F0258"}.mdi-folder-remove-outline::before{content:"\F0B9E"}.mdi-folder-search::before{content:"\F0968"}.mdi-folder-search-outline::before{content:"\F0969"}.mdi-folder-settings::before{content:"\F107D"}.mdi-folder-settings-outline::before{content:"\F107E"}.mdi-folder-star::before{content:"\F069D"}.mdi-folder-star-multiple::before{content:"\F13D3"}.mdi-folder-star-multiple-outline::before{content:"\F13D4"}.mdi-folder-star-outline::before{content:"\F0B9F"}.mdi-folder-swap::before{content:"\F0FB6"}.mdi-folder-swap-outline::before{content:"\F0FB7"}.mdi-folder-sync::before{content:"\F0D0B"}.mdi-folder-sync-outline::before{content:"\F0D0C"}.mdi-folder-table::before{content:"\F12E3"}.mdi-folder-table-outline::before{content:"\F12E4"}.mdi-folder-text::before{content:"\F0C82"}.mdi-folder-text-outline::before{content:"\F0C83"}.mdi-folder-upload::before{content:"\F0259"}.mdi-folder-upload-outline::before{content:"\F10ED"}.mdi-folder-wrench::before{content:"\F19FC"}.mdi-folder-wrench-outline::before{content:"\F19FD"}.mdi-folder-zip::before{content:"\F06EB"}.mdi-folder-zip-outline::before{content:"\F07B9"}.mdi-font-awesome::before{content:"\F003A"}.mdi-food::before{content:"\F025A"}.mdi-food-apple::before{content:"\F025B"}.mdi-food-apple-outline::before{content:"\F0C84"}.mdi-food-croissant::before{content:"\F07C8"}.mdi-food-drumstick::before{content:"\F141F"}.mdi-food-drumstick-off::before{content:"\F1468"}.mdi-food-drumstick-off-outline::before{content:"\F1469"}.mdi-food-drumstick-outline::before{content:"\F1420"}.mdi-food-fork-drink::before{content:"\F05F2"}.mdi-food-halal::before{content:"\F1572"}.mdi-food-hot-dog::before{content:"\F184B"}.mdi-food-kosher::before{content:"\F1573"}.mdi-food-off::before{content:"\F05F3"}.mdi-food-off-outline::before{content:"\F1915"}.mdi-food-outline::before{content:"\F1916"}.mdi-food-steak::before{content:"\F146A"}.mdi-food-steak-off::before{content:"\F146B"}.mdi-food-takeout-box::before{content:"\F1836"}.mdi-food-takeout-box-outline::before{content:"\F1837"}.mdi-food-turkey::before{content:"\F171C"}.mdi-food-variant::before{content:"\F025C"}.mdi-food-variant-off::before{content:"\F13E5"}.mdi-foot-print::before{content:"\F0F52"}.mdi-football::before{content:"\F025D"}.mdi-football-australian::before{content:"\F025E"}.mdi-football-helmet::before{content:"\F025F"}.mdi-forest::before{content:"\F1897"}.mdi-forest-outline::before{content:"\F1C63"}.mdi-forklift::before{content:"\F07C9"}.mdi-form-dropdown::before{content:"\F1400"}.mdi-form-select::before{content:"\F1401"}.mdi-form-textarea::before{content:"\F1095"}.mdi-form-textbox::before{content:"\F060E"}.mdi-form-textbox-lock::before{content:"\F135D"}.mdi-form-textbox-password::before{content:"\F07F5"}.mdi-format-align-bottom::before{content:"\F0753"}.mdi-format-align-center::before{content:"\F0260"}.mdi-format-align-justify::before{content:"\F0261"}.mdi-format-align-left::before{content:"\F0262"}.mdi-format-align-middle::before{content:"\F0754"}.mdi-format-align-right::before{content:"\F0263"}.mdi-format-align-top::before{content:"\F0755"}.mdi-format-annotation-minus::before{content:"\F0ABC"}.mdi-format-annotation-plus::before{content:"\F0646"}.mdi-format-bold::before{content:"\F0264"}.mdi-format-clear::before{content:"\F0265"}.mdi-format-color-fill::before{content:"\F0266"}.mdi-format-color-highlight::before{content:"\F0E31"}.mdi-format-color-marker-cancel::before{content:"\F1313"}.mdi-format-color-text::before{content:"\F069E"}.mdi-format-columns::before{content:"\F08DF"}.mdi-format-float-center::before{content:"\F0267"}.mdi-format-float-left::before{content:"\F0268"}.mdi-format-float-none::before{content:"\F0269"}.mdi-format-float-right::before{content:"\F026A"}.mdi-format-font::before{content:"\F06D6"}.mdi-format-font-size-decrease::before{content:"\F09F3"}.mdi-format-font-size-increase::before{content:"\F09F4"}.mdi-format-header-1::before{content:"\F026B"}.mdi-format-header-2::before{content:"\F026C"}.mdi-format-header-3::before{content:"\F026D"}.mdi-format-header-4::before{content:"\F026E"}.mdi-format-header-5::before{content:"\F026F"}.mdi-format-header-6::before{content:"\F0270"}.mdi-format-header-decrease::before{content:"\F0271"}.mdi-format-header-equal::before{content:"\F0272"}.mdi-format-header-increase::before{content:"\F0273"}.mdi-format-header-pound::before{content:"\F0274"}.mdi-format-horizontal-align-center::before{content:"\F061E"}.mdi-format-horizontal-align-left::before{content:"\F061F"}.mdi-format-horizontal-align-right::before{content:"\F0620"}.mdi-format-indent-decrease::before{content:"\F0275"}.mdi-format-indent-increase::before{content:"\F0276"}.mdi-format-italic::before{content:"\F0277"}.mdi-format-letter-case::before{content:"\F0B34"}.mdi-format-letter-case-lower::before{content:"\F0B35"}.mdi-format-letter-case-upper::before{content:"\F0B36"}.mdi-format-letter-ends-with::before{content:"\F0FB8"}.mdi-format-letter-matches::before{content:"\F0FB9"}.mdi-format-letter-spacing::before{content:"\F1956"}.mdi-format-letter-spacing-variant::before{content:"\F1AFB"}.mdi-format-letter-starts-with::before{content:"\F0FBA"}.mdi-format-line-height::before{content:"\F1AFC"}.mdi-format-line-spacing::before{content:"\F0278"}.mdi-format-line-style::before{content:"\F05C8"}.mdi-format-line-weight::before{content:"\F05C9"}.mdi-format-list-bulleted::before{content:"\F0279"}.mdi-format-list-bulleted-square::before{content:"\F0DD0"}.mdi-format-list-bulleted-triangle::before{content:"\F0EB2"}.mdi-format-list-bulleted-type::before{content:"\F027A"}.mdi-format-list-checkbox::before{content:"\F096A"}.mdi-format-list-checks::before{content:"\F0756"}.mdi-format-list-group::before{content:"\F1860"}.mdi-format-list-group-plus::before{content:"\F1B56"}.mdi-format-list-numbered::before{content:"\F027B"}.mdi-format-list-numbered-rtl::before{content:"\F0D0D"}.mdi-format-list-text::before{content:"\F126F"}.mdi-format-overline::before{content:"\F0EB3"}.mdi-format-page-break::before{content:"\F06D7"}.mdi-format-page-split::before{content:"\F1917"}.mdi-format-paint::before{content:"\F027C"}.mdi-format-paragraph::before{content:"\F027D"}.mdi-format-paragraph-spacing::before{content:"\F1AFD"}.mdi-format-pilcrow::before{content:"\F06D8"}.mdi-format-pilcrow-arrow-left::before{content:"\F0286"}.mdi-format-pilcrow-arrow-right::before{content:"\F0285"}.mdi-format-quote-close::before{content:"\F027E"}.mdi-format-quote-close-outline::before{content:"\F11A8"}.mdi-format-quote-open::before{content:"\F0757"}.mdi-format-quote-open-outline::before{content:"\F11A7"}.mdi-format-rotate-90::before{content:"\F06AA"}.mdi-format-section::before{content:"\F069F"}.mdi-format-size::before{content:"\F027F"}.mdi-format-strikethrough::before{content:"\F0280"}.mdi-format-strikethrough-variant::before{content:"\F0281"}.mdi-format-subscript::before{content:"\F0282"}.mdi-format-superscript::before{content:"\F0283"}.mdi-format-text::before{content:"\F0284"}.mdi-format-text-rotation-angle-down::before{content:"\F0FBB"}.mdi-format-text-rotation-angle-up::before{content:"\F0FBC"}.mdi-format-text-rotation-down::before{content:"\F0D73"}.mdi-format-text-rotation-down-vertical::before{content:"\F0FBD"}.mdi-format-text-rotation-none::before{content:"\F0D74"}.mdi-format-text-rotation-up::before{content:"\F0FBE"}.mdi-format-text-rotation-vertical::before{content:"\F0FBF"}.mdi-format-text-variant::before{content:"\F0E32"}.mdi-format-text-variant-outline::before{content:"\F150F"}.mdi-format-text-wrapping-clip::before{content:"\F0D0E"}.mdi-format-text-wrapping-overflow::before{content:"\F0D0F"}.mdi-format-text-wrapping-wrap::before{content:"\F0D10"}.mdi-format-textbox::before{content:"\F0D11"}.mdi-format-title::before{content:"\F05F4"}.mdi-format-underline::before{content:"\F0287"}.mdi-format-underline-wavy::before{content:"\F18E9"}.mdi-format-vertical-align-bottom::before{content:"\F0621"}.mdi-format-vertical-align-center::before{content:"\F0622"}.mdi-format-vertical-align-top::before{content:"\F0623"}.mdi-format-wrap-inline::before{content:"\F0288"}.mdi-format-wrap-square::before{content:"\F0289"}.mdi-format-wrap-tight::before{content:"\F028A"}.mdi-format-wrap-top-bottom::before{content:"\F028B"}.mdi-forum::before{content:"\F028C"}.mdi-forum-minus::before{content:"\F1AA9"}.mdi-forum-minus-outline::before{content:"\F1AAA"}.mdi-forum-outline::before{content:"\F0822"}.mdi-forum-plus::before{content:"\F1AAB"}.mdi-forum-plus-outline::before{content:"\F1AAC"}.mdi-forum-remove::before{content:"\F1AAD"}.mdi-forum-remove-outline::before{content:"\F1AAE"}.mdi-forward::before{content:"\F028D"}.mdi-forwardburger::before{content:"\F0D75"}.mdi-fountain::before{content:"\F096B"}.mdi-fountain-pen::before{content:"\F0D12"}.mdi-fountain-pen-tip::before{content:"\F0D13"}.mdi-fraction-one-half::before{content:"\F1992"}.mdi-freebsd::before{content:"\F08E0"}.mdi-french-fries::before{content:"\F1957"}.mdi-frequently-asked-questions::before{content:"\F0EB4"}.mdi-fridge::before{content:"\F0290"}.mdi-fridge-alert::before{content:"\F11B1"}.mdi-fridge-alert-outline::before{content:"\F11B2"}.mdi-fridge-bottom::before{content:"\F0292"}.mdi-fridge-industrial::before{content:"\F15EE"}.mdi-fridge-industrial-alert::before{content:"\F15EF"}.mdi-fridge-industrial-alert-outline::before{content:"\F15F0"}.mdi-fridge-industrial-off::before{content:"\F15F1"}.mdi-fridge-industrial-off-outline::before{content:"\F15F2"}.mdi-fridge-industrial-outline::before{content:"\F15F3"}.mdi-fridge-off::before{content:"\F11AF"}.mdi-fridge-off-outline::before{content:"\F11B0"}.mdi-fridge-outline::before{content:"\F028F"}.mdi-fridge-top::before{content:"\F0291"}.mdi-fridge-variant::before{content:"\F15F4"}.mdi-fridge-variant-alert::before{content:"\F15F5"}.mdi-fridge-variant-alert-outline::before{content:"\F15F6"}.mdi-fridge-variant-off::before{content:"\F15F7"}.mdi-fridge-variant-off-outline::before{content:"\F15F8"}.mdi-fridge-variant-outline::before{content:"\F15F9"}.mdi-fruit-cherries::before{content:"\F1042"}.mdi-fruit-cherries-off::before{content:"\F13F8"}.mdi-fruit-citrus::before{content:"\F1043"}.mdi-fruit-citrus-off::before{content:"\F13F9"}.mdi-fruit-grapes::before{content:"\F1044"}.mdi-fruit-grapes-outline::before{content:"\F1045"}.mdi-fruit-pear::before{content:"\F1A0E"}.mdi-fruit-pineapple::before{content:"\F1046"}.mdi-fruit-watermelon::before{content:"\F1047"}.mdi-fuel::before{content:"\F07CA"}.mdi-fuel-cell::before{content:"\F18B5"}.mdi-fullscreen::before{content:"\F0293"}.mdi-fullscreen-exit::before{content:"\F0294"}.mdi-function::before{content:"\F0295"}.mdi-function-variant::before{content:"\F0871"}.mdi-furigana-horizontal::before{content:"\F1081"}.mdi-furigana-vertical::before{content:"\F1082"}.mdi-fuse::before{content:"\F0C85"}.mdi-fuse-alert::before{content:"\F142D"}.mdi-fuse-blade::before{content:"\F0C86"}.mdi-fuse-off::before{content:"\F142C"}.mdi-gamepad::before{content:"\F0296"}.mdi-gamepad-circle::before{content:"\F0E33"}.mdi-gamepad-circle-down::before{content:"\F0E34"}.mdi-gamepad-circle-left::before{content:"\F0E35"}.mdi-gamepad-circle-outline::before{content:"\F0E36"}.mdi-gamepad-circle-right::before{content:"\F0E37"}.mdi-gamepad-circle-up::before{content:"\F0E38"}.mdi-gamepad-down::before{content:"\F0E39"}.mdi-gamepad-left::before{content:"\F0E3A"}.mdi-gamepad-outline::before{content:"\F1919"}.mdi-gamepad-right::before{content:"\F0E3B"}.mdi-gamepad-round::before{content:"\F0E3C"}.mdi-gamepad-round-down::before{content:"\F0E3D"}.mdi-gamepad-round-left::before{content:"\F0E3E"}.mdi-gamepad-round-outline::before{content:"\F0E3F"}.mdi-gamepad-round-right::before{content:"\F0E40"}.mdi-gamepad-round-up::before{content:"\F0E41"}.mdi-gamepad-square::before{content:"\F0EB5"}.mdi-gamepad-square-outline::before{content:"\F0EB6"}.mdi-gamepad-up::before{content:"\F0E42"}.mdi-gamepad-variant::before{content:"\F0297"}.mdi-gamepad-variant-outline::before{content:"\F0EB7"}.mdi-gamma::before{content:"\F10EE"}.mdi-gantry-crane::before{content:"\F0DD1"}.mdi-garage::before{content:"\F06D9"}.mdi-garage-alert::before{content:"\F0872"}.mdi-garage-alert-variant::before{content:"\F12D5"}.mdi-garage-lock::before{content:"\F17FB"}.mdi-garage-open::before{content:"\F06DA"}.mdi-garage-open-variant::before{content:"\F12D4"}.mdi-garage-variant::before{content:"\F12D3"}.mdi-garage-variant-lock::before{content:"\F17FC"}.mdi-gas-burner::before{content:"\F1A1B"}.mdi-gas-cylinder::before{content:"\F0647"}.mdi-gas-station::before{content:"\F0298"}.mdi-gas-station-in-use::before{content:"\F1CC4"}.mdi-gas-station-in-use-outline::before{content:"\F1CC5"}.mdi-gas-station-off::before{content:"\F1409"}.mdi-gas-station-off-outline::before{content:"\F140A"}.mdi-gas-station-outline::before{content:"\F0EB8"}.mdi-gate::before{content:"\F0299"}.mdi-gate-alert::before{content:"\F17F8"}.mdi-gate-and::before{content:"\F08E1"}.mdi-gate-arrow-left::before{content:"\F17F7"}.mdi-gate-arrow-right::before{content:"\F1169"}.mdi-gate-buffer::before{content:"\F1AFE"}.mdi-gate-nand::before{content:"\F08E2"}.mdi-gate-nor::before{content:"\F08E3"}.mdi-gate-not::before{content:"\F08E4"}.mdi-gate-open::before{content:"\F116A"}.mdi-gate-or::before{content:"\F08E5"}.mdi-gate-xnor::before{content:"\F08E6"}.mdi-gate-xor::before{content:"\F08E7"}.mdi-gatsby::before{content:"\F0E43"}.mdi-gauge::before{content:"\F029A"}.mdi-gauge-empty::before{content:"\F0873"}.mdi-gauge-full::before{content:"\F0874"}.mdi-gauge-low::before{content:"\F0875"}.mdi-gavel::before{content:"\F029B"}.mdi-gender-female::before{content:"\F029C"}.mdi-gender-male::before{content:"\F029D"}.mdi-gender-male-female::before{content:"\F029E"}.mdi-gender-male-female-variant::before{content:"\F113F"}.mdi-gender-non-binary::before{content:"\F1140"}.mdi-gender-transgender::before{content:"\F029F"}.mdi-generator-mobile::before{content:"\F1C8A"}.mdi-generator-portable::before{content:"\F1C8B"}.mdi-generator-stationary::before{content:"\F1C8C"}.mdi-gentoo::before{content:"\F08E8"}.mdi-gesture::before{content:"\F07CB"}.mdi-gesture-double-tap::before{content:"\F073C"}.mdi-gesture-pinch::before{content:"\F0ABD"}.mdi-gesture-spread::before{content:"\F0ABE"}.mdi-gesture-swipe::before{content:"\F0D76"}.mdi-gesture-swipe-down::before{content:"\F073D"}.mdi-gesture-swipe-horizontal::before{content:"\F0ABF"}.mdi-gesture-swipe-left::before{content:"\F073E"}.mdi-gesture-swipe-right::before{content:"\F073F"}.mdi-gesture-swipe-up::before{content:"\F0740"}.mdi-gesture-swipe-vertical::before{content:"\F0AC0"}.mdi-gesture-tap::before{content:"\F0741"}.mdi-gesture-tap-box::before{content:"\F12A9"}.mdi-gesture-tap-button::before{content:"\F12A8"}.mdi-gesture-tap-hold::before{content:"\F0D77"}.mdi-gesture-two-double-tap::before{content:"\F0742"}.mdi-gesture-two-tap::before{content:"\F0743"}.mdi-ghost::before{content:"\F02A0"}.mdi-ghost-off::before{content:"\F09F5"}.mdi-ghost-off-outline::before{content:"\F165C"}.mdi-ghost-outline::before{content:"\F165D"}.mdi-gift::before{content:"\F0E44"}.mdi-gift-off::before{content:"\F16EF"}.mdi-gift-off-outline::before{content:"\F16F0"}.mdi-gift-open::before{content:"\F16F1"}.mdi-gift-open-outline::before{content:"\F16F2"}.mdi-gift-outline::before{content:"\F02A1"}.mdi-git::before{content:"\F02A2"}.mdi-github::before{content:"\F02A4"}.mdi-gitlab::before{content:"\F0BA0"}.mdi-glass-cocktail::before{content:"\F0356"}.mdi-glass-cocktail-off::before{content:"\F15E6"}.mdi-glass-flute::before{content:"\F02A5"}.mdi-glass-fragile::before{content:"\F1873"}.mdi-glass-mug::before{content:"\F02A6"}.mdi-glass-mug-off::before{content:"\F15E7"}.mdi-glass-mug-variant::before{content:"\F1116"}.mdi-glass-mug-variant-off::before{content:"\F15E8"}.mdi-glass-pint-outline::before{content:"\F130D"}.mdi-glass-stange::before{content:"\F02A7"}.mdi-glass-tulip::before{content:"\F02A8"}.mdi-glass-wine::before{content:"\F0876"}.mdi-glasses::before{content:"\F02AA"}.mdi-globe-light::before{content:"\F066F"}.mdi-globe-light-outline::before{content:"\F12D7"}.mdi-globe-model::before{content:"\F08E9"}.mdi-gmail::before{content:"\F02AB"}.mdi-gnome::before{content:"\F02AC"}.mdi-go-kart::before{content:"\F0D79"}.mdi-go-kart-track::before{content:"\F0D7A"}.mdi-gog::before{content:"\F0BA1"}.mdi-gold::before{content:"\F124F"}.mdi-golf::before{content:"\F0823"}.mdi-golf-cart::before{content:"\F11A4"}.mdi-golf-tee::before{content:"\F1083"}.mdi-gondola::before{content:"\F0686"}.mdi-goodreads::before{content:"\F0D7B"}.mdi-google::before{content:"\F02AD"}.mdi-google-ads::before{content:"\F0C87"}.mdi-google-analytics::before{content:"\F07CC"}.mdi-google-assistant::before{content:"\F07CD"}.mdi-google-cardboard::before{content:"\F02AE"}.mdi-google-chrome::before{content:"\F02AF"}.mdi-google-circles::before{content:"\F02B0"}.mdi-google-circles-communities::before{content:"\F02B1"}.mdi-google-circles-extended::before{content:"\F02B2"}.mdi-google-circles-group::before{content:"\F02B3"}.mdi-google-classroom::before{content:"\F02C0"}.mdi-google-cloud::before{content:"\F11F6"}.mdi-google-downasaur::before{content:"\F1362"}.mdi-google-drive::before{content:"\F02B6"}.mdi-google-earth::before{content:"\F02B7"}.mdi-google-fit::before{content:"\F096C"}.mdi-google-glass::before{content:"\F02B8"}.mdi-google-hangouts::before{content:"\F02C9"}.mdi-google-keep::before{content:"\F06DC"}.mdi-google-lens::before{content:"\F09F6"}.mdi-google-maps::before{content:"\F05F5"}.mdi-google-my-business::before{content:"\F1048"}.mdi-google-nearby::before{content:"\F02B9"}.mdi-google-play::before{content:"\F02BC"}.mdi-google-plus::before{content:"\F02BD"}.mdi-google-podcast::before{content:"\F0EB9"}.mdi-google-spreadsheet::before{content:"\F09F7"}.mdi-google-street-view::before{content:"\F0C88"}.mdi-google-translate::before{content:"\F02BF"}.mdi-gradient-horizontal::before{content:"\F174A"}.mdi-gradient-vertical::before{content:"\F06A0"}.mdi-grain::before{content:"\F0D7C"}.mdi-graph::before{content:"\F1049"}.mdi-graph-outline::before{content:"\F104A"}.mdi-graphql::before{content:"\F0877"}.mdi-grass::before{content:"\F1510"}.mdi-grave-stone::before{content:"\F0BA2"}.mdi-grease-pencil::before{content:"\F0648"}.mdi-greater-than::before{content:"\F096D"}.mdi-greater-than-or-equal::before{content:"\F096E"}.mdi-greenhouse::before{content:"\F002D"}.mdi-grid::before{content:"\F02C1"}.mdi-grid-large::before{content:"\F0758"}.mdi-grid-off::before{content:"\F02C2"}.mdi-grill::before{content:"\F0E45"}.mdi-grill-outline::before{content:"\F118A"}.mdi-group::before{content:"\F02C3"}.mdi-guitar-acoustic::before{content:"\F0771"}.mdi-guitar-electric::before{content:"\F02C4"}.mdi-guitar-pick::before{content:"\F02C5"}.mdi-guitar-pick-outline::before{content:"\F02C6"}.mdi-guy-fawkes-mask::before{content:"\F0825"}.mdi-gymnastics::before{content:"\F1A41"}.mdi-hail::before{content:"\F0AC1"}.mdi-hair-dryer::before{content:"\F10EF"}.mdi-hair-dryer-outline::before{content:"\F10F0"}.mdi-halloween::before{content:"\F0BA3"}.mdi-hamburger::before{content:"\F0685"}.mdi-hamburger-check::before{content:"\F1776"}.mdi-hamburger-minus::before{content:"\F1777"}.mdi-hamburger-off::before{content:"\F1778"}.mdi-hamburger-plus::before{content:"\F1779"}.mdi-hamburger-remove::before{content:"\F177A"}.mdi-hammer::before{content:"\F08EA"}.mdi-hammer-screwdriver::before{content:"\F1322"}.mdi-hammer-sickle::before{content:"\F1887"}.mdi-hammer-wrench::before{content:"\F1323"}.mdi-hand-back-left::before{content:"\F0E46"}.mdi-hand-back-left-off::before{content:"\F1830"}.mdi-hand-back-left-off-outline::before{content:"\F1832"}.mdi-hand-back-left-outline::before{content:"\F182C"}.mdi-hand-back-right::before{content:"\F0E47"}.mdi-hand-back-right-off::before{content:"\F1831"}.mdi-hand-back-right-off-outline::before{content:"\F1833"}.mdi-hand-back-right-outline::before{content:"\F182D"}.mdi-hand-clap::before{content:"\F194B"}.mdi-hand-clap-off::before{content:"\F1A42"}.mdi-hand-coin::before{content:"\F188F"}.mdi-hand-coin-outline::before{content:"\F1890"}.mdi-hand-cycle::before{content:"\F1B9C"}.mdi-hand-extended::before{content:"\F18B6"}.mdi-hand-extended-outline::before{content:"\F18B7"}.mdi-hand-front-left::before{content:"\F182B"}.mdi-hand-front-left-outline::before{content:"\F182E"}.mdi-hand-front-right::before{content:"\F0A4F"}.mdi-hand-front-right-outline::before{content:"\F182F"}.mdi-hand-heart::before{content:"\F10F1"}.mdi-hand-heart-outline::before{content:"\F157E"}.mdi-hand-okay::before{content:"\F0A50"}.mdi-hand-peace::before{content:"\F0A51"}.mdi-hand-peace-variant::before{content:"\F0A52"}.mdi-hand-pointing-down::before{content:"\F0A53"}.mdi-hand-pointing-left::before{content:"\F0A54"}.mdi-hand-pointing-right::before{content:"\F02C7"}.mdi-hand-pointing-up::before{content:"\F0A55"}.mdi-hand-saw::before{content:"\F0E48"}.mdi-hand-wash::before{content:"\F157F"}.mdi-hand-wash-outline::before{content:"\F1580"}.mdi-hand-water::before{content:"\F139F"}.mdi-hand-wave::before{content:"\F1821"}.mdi-hand-wave-outline::before{content:"\F1822"}.mdi-handball::before{content:"\F0F53"}.mdi-handcuffs::before{content:"\F113E"}.mdi-hands-pray::before{content:"\F0579"}.mdi-handshake::before{content:"\F1218"}.mdi-handshake-outline::before{content:"\F15A1"}.mdi-hanger::before{content:"\F02C8"}.mdi-hard-hat::before{content:"\F096F"}.mdi-harddisk::before{content:"\F02CA"}.mdi-harddisk-plus::before{content:"\F104B"}.mdi-harddisk-remove::before{content:"\F104C"}.mdi-hat-fedora::before{content:"\F0BA4"}.mdi-hazard-lights::before{content:"\F0C89"}.mdi-hdmi-port::before{content:"\F1BB8"}.mdi-hdr::before{content:"\F0D7D"}.mdi-hdr-off::before{content:"\F0D7E"}.mdi-head::before{content:"\F135E"}.mdi-head-alert::before{content:"\F1338"}.mdi-head-alert-outline::before{content:"\F1339"}.mdi-head-check::before{content:"\F133A"}.mdi-head-check-outline::before{content:"\F133B"}.mdi-head-cog::before{content:"\F133C"}.mdi-head-cog-outline::before{content:"\F133D"}.mdi-head-dots-horizontal::before{content:"\F133E"}.mdi-head-dots-horizontal-outline::before{content:"\F133F"}.mdi-head-flash::before{content:"\F1340"}.mdi-head-flash-outline::before{content:"\F1341"}.mdi-head-heart::before{content:"\F1342"}.mdi-head-heart-outline::before{content:"\F1343"}.mdi-head-lightbulb::before{content:"\F1344"}.mdi-head-lightbulb-outline::before{content:"\F1345"}.mdi-head-minus::before{content:"\F1346"}.mdi-head-minus-outline::before{content:"\F1347"}.mdi-head-outline::before{content:"\F135F"}.mdi-head-plus::before{content:"\F1348"}.mdi-head-plus-outline::before{content:"\F1349"}.mdi-head-question::before{content:"\F134A"}.mdi-head-question-outline::before{content:"\F134B"}.mdi-head-remove::before{content:"\F134C"}.mdi-head-remove-outline::before{content:"\F134D"}.mdi-head-snowflake::before{content:"\F134E"}.mdi-head-snowflake-outline::before{content:"\F134F"}.mdi-head-sync::before{content:"\F1350"}.mdi-head-sync-outline::before{content:"\F1351"}.mdi-headphones::before{content:"\F02CB"}.mdi-headphones-bluetooth::before{content:"\F0970"}.mdi-headphones-box::before{content:"\F02CC"}.mdi-headphones-off::before{content:"\F07CE"}.mdi-headphones-settings::before{content:"\F02CD"}.mdi-headset::before{content:"\F02CE"}.mdi-headset-dock::before{content:"\F02CF"}.mdi-headset-off::before{content:"\F02D0"}.mdi-heart::before{content:"\F02D1"}.mdi-heart-box::before{content:"\F02D2"}.mdi-heart-box-outline::before{content:"\F02D3"}.mdi-heart-broken::before{content:"\F02D4"}.mdi-heart-broken-outline::before{content:"\F0D14"}.mdi-heart-circle::before{content:"\F0971"}.mdi-heart-circle-outline::before{content:"\F0972"}.mdi-heart-cog::before{content:"\F1663"}.mdi-heart-cog-outline::before{content:"\F1664"}.mdi-heart-flash::before{content:"\F0EF9"}.mdi-heart-half::before{content:"\F06DF"}.mdi-heart-half-full::before{content:"\F06DE"}.mdi-heart-half-outline::before{content:"\F06E0"}.mdi-heart-minus::before{content:"\F142F"}.mdi-heart-minus-outline::before{content:"\F1432"}.mdi-heart-multiple::before{content:"\F0A56"}.mdi-heart-multiple-outline::before{content:"\F0A57"}.mdi-heart-off::before{content:"\F0759"}.mdi-heart-off-outline::before{content:"\F1434"}.mdi-heart-outline::before{content:"\F02D5"}.mdi-heart-plus::before{content:"\F142E"}.mdi-heart-plus-outline::before{content:"\F1431"}.mdi-heart-pulse::before{content:"\F05F6"}.mdi-heart-remove::before{content:"\F1430"}.mdi-heart-remove-outline::before{content:"\F1433"}.mdi-heart-search::before{content:"\F1C8D"}.mdi-heart-settings::before{content:"\F1665"}.mdi-heart-settings-outline::before{content:"\F1666"}.mdi-heat-pump::before{content:"\F1A43"}.mdi-heat-pump-outline::before{content:"\F1A44"}.mdi-heat-wave::before{content:"\F1A45"}.mdi-heating-coil::before{content:"\F1AAF"}.mdi-helicopter::before{content:"\F0AC2"}.mdi-help::before{content:"\F02D6"}.mdi-help-box::before{content:"\F078B"}.mdi-help-box-multiple::before{content:"\F1C0A"}.mdi-help-box-multiple-outline::before{content:"\F1C0B"}.mdi-help-box-outline::before{content:"\F1C0C"}.mdi-help-circle::before{content:"\F02D7"}.mdi-help-circle-outline::before{content:"\F0625"}.mdi-help-network::before{content:"\F06F5"}.mdi-help-network-outline::before{content:"\F0C8A"}.mdi-help-rhombus::before{content:"\F0BA5"}.mdi-help-rhombus-outline::before{content:"\F0BA6"}.mdi-hexadecimal::before{content:"\F12A7"}.mdi-hexagon::before{content:"\F02D8"}.mdi-hexagon-multiple::before{content:"\F06E1"}.mdi-hexagon-multiple-outline::before{content:"\F10F2"}.mdi-hexagon-outline::before{content:"\F02D9"}.mdi-hexagon-slice-1::before{content:"\F0AC3"}.mdi-hexagon-slice-2::before{content:"\F0AC4"}.mdi-hexagon-slice-3::before{content:"\F0AC5"}.mdi-hexagon-slice-4::before{content:"\F0AC6"}.mdi-hexagon-slice-5::before{content:"\F0AC7"}.mdi-hexagon-slice-6::before{content:"\F0AC8"}.mdi-hexagram::before{content:"\F0AC9"}.mdi-hexagram-outline::before{content:"\F0ACA"}.mdi-high-definition::before{content:"\F07CF"}.mdi-high-definition-box::before{content:"\F0878"}.mdi-highway::before{content:"\F05F7"}.mdi-hiking::before{content:"\F0D7F"}.mdi-history::before{content:"\F02DA"}.mdi-hockey-puck::before{content:"\F0879"}.mdi-hockey-sticks::before{content:"\F087A"}.mdi-hololens::before{content:"\F02DB"}.mdi-home::before{content:"\F02DC"}.mdi-home-account::before{content:"\F0826"}.mdi-home-alert::before{content:"\F087B"}.mdi-home-alert-outline::before{content:"\F15D0"}.mdi-home-analytics::before{content:"\F0EBA"}.mdi-home-assistant::before{content:"\F07D0"}.mdi-home-automation::before{content:"\F07D1"}.mdi-home-battery::before{content:"\F1901"}.mdi-home-battery-outline::before{content:"\F1902"}.mdi-home-circle::before{content:"\F07D2"}.mdi-home-circle-outline::before{content:"\F104D"}.mdi-home-city::before{content:"\F0D15"}.mdi-home-city-outline::before{content:"\F0D16"}.mdi-home-clock::before{content:"\F1A12"}.mdi-home-clock-outline::before{content:"\F1A13"}.mdi-home-edit::before{content:"\F1159"}.mdi-home-edit-outline::before{content:"\F115A"}.mdi-home-export-outline::before{content:"\F0F9B"}.mdi-home-flood::before{content:"\F0EFA"}.mdi-home-floor-0::before{content:"\F0DD2"}.mdi-home-floor-1::before{content:"\F0D80"}.mdi-home-floor-2::before{content:"\F0D81"}.mdi-home-floor-3::before{content:"\F0D82"}.mdi-home-floor-a::before{content:"\F0D83"}.mdi-home-floor-b::before{content:"\F0D84"}.mdi-home-floor-g::before{content:"\F0D85"}.mdi-home-floor-l::before{content:"\F0D86"}.mdi-home-floor-negative-1::before{content:"\F0DD3"}.mdi-home-group::before{content:"\F0DD4"}.mdi-home-group-minus::before{content:"\F19C1"}.mdi-home-group-plus::before{content:"\F19C0"}.mdi-home-group-remove::before{content:"\F19C2"}.mdi-home-heart::before{content:"\F0827"}.mdi-home-import-outline::before{content:"\F0F9C"}.mdi-home-lightbulb::before{content:"\F1251"}.mdi-home-lightbulb-outline::before{content:"\F1252"}.mdi-home-lightning-bolt::before{content:"\F1903"}.mdi-home-lightning-bolt-outline::before{content:"\F1904"}.mdi-home-lock::before{content:"\F08EB"}.mdi-home-lock-open::before{content:"\F08EC"}.mdi-home-map-marker::before{content:"\F05F8"}.mdi-home-minus::before{content:"\F0974"}.mdi-home-minus-outline::before{content:"\F13D5"}.mdi-home-modern::before{content:"\F02DD"}.mdi-home-off::before{content:"\F1A46"}.mdi-home-off-outline::before{content:"\F1A47"}.mdi-home-outline::before{content:"\F06A1"}.mdi-home-percent::before{content:"\F1C7C"}.mdi-home-percent-outline::before{content:"\F1C7D"}.mdi-home-plus::before{content:"\F0975"}.mdi-home-plus-outline::before{content:"\F13D6"}.mdi-home-remove::before{content:"\F1247"}.mdi-home-remove-outline::before{content:"\F13D7"}.mdi-home-roof::before{content:"\F112B"}.mdi-home-search::before{content:"\F13B0"}.mdi-home-search-outline::before{content:"\F13B1"}.mdi-home-silo::before{content:"\F1BA0"}.mdi-home-silo-outline::before{content:"\F1BA1"}.mdi-home-sound-in::before{content:"\F1C2F"}.mdi-home-sound-in-outline::before{content:"\F1C30"}.mdi-home-sound-out::before{content:"\F1C31"}.mdi-home-sound-out-outline::before{content:"\F1C32"}.mdi-home-switch::before{content:"\F1794"}.mdi-home-switch-outline::before{content:"\F1795"}.mdi-home-thermometer::before{content:"\F0F54"}.mdi-home-thermometer-outline::before{content:"\F0F55"}.mdi-home-variant::before{content:"\F02DE"}.mdi-home-variant-outline::before{content:"\F0BA7"}.mdi-hook::before{content:"\F06E2"}.mdi-hook-off::before{content:"\F06E3"}.mdi-hoop-house::before{content:"\F0E56"}.mdi-hops::before{content:"\F02DF"}.mdi-horizontal-rotate-clockwise::before{content:"\F10F3"}.mdi-horizontal-rotate-counterclockwise::before{content:"\F10F4"}.mdi-horse::before{content:"\F15BF"}.mdi-horse-human::before{content:"\F15C0"}.mdi-horse-variant::before{content:"\F15C1"}.mdi-horse-variant-fast::before{content:"\F186E"}.mdi-horseshoe::before{content:"\F0A58"}.mdi-hospital::before{content:"\F0FF6"}.mdi-hospital-box::before{content:"\F02E0"}.mdi-hospital-box-outline::before{content:"\F0FF7"}.mdi-hospital-building::before{content:"\F02E1"}.mdi-hospital-marker::before{content:"\F02E2"}.mdi-hot-tub::before{content:"\F0828"}.mdi-hours-12::before{content:"\F1C94"}.mdi-hours-24::before{content:"\F1478"}.mdi-hub::before{content:"\F1C95"}.mdi-hub-outline::before{content:"\F1C96"}.mdi-hubspot::before{content:"\F0D17"}.mdi-hulu::before{content:"\F0829"}.mdi-human::before{content:"\F02E6"}.mdi-human-baby-changing-table::before{content:"\F138B"}.mdi-human-cane::before{content:"\F1581"}.mdi-human-capacity-decrease::before{content:"\F159B"}.mdi-human-capacity-increase::before{content:"\F159C"}.mdi-human-child::before{content:"\F02E7"}.mdi-human-dolly::before{content:"\F1980"}.mdi-human-edit::before{content:"\F14E8"}.mdi-human-female::before{content:"\F0649"}.mdi-human-female-boy::before{content:"\F0A59"}.mdi-human-female-dance::before{content:"\F15C9"}.mdi-human-female-female::before{content:"\F0A5A"}.mdi-human-female-female-child::before{content:"\F1C8E"}.mdi-human-female-girl::before{content:"\F0A5B"}.mdi-human-greeting::before{content:"\F17C4"}.mdi-human-greeting-proximity::before{content:"\F159D"}.mdi-human-greeting-variant::before{content:"\F064A"}.mdi-human-handsdown::before{content:"\F064B"}.mdi-human-handsup::before{content:"\F064C"}.mdi-human-male::before{content:"\F064D"}.mdi-human-male-board::before{content:"\F0890"}.mdi-human-male-board-poll::before{content:"\F0846"}.mdi-human-male-boy::before{content:"\F0A5C"}.mdi-human-male-child::before{content:"\F138C"}.mdi-human-male-female::before{content:"\F02E8"}.mdi-human-male-female-child::before{content:"\F1823"}.mdi-human-male-girl::before{content:"\F0A5D"}.mdi-human-male-height::before{content:"\F0EFB"}.mdi-human-male-height-variant::before{content:"\F0EFC"}.mdi-human-male-male::before{content:"\F0A5E"}.mdi-human-male-male-child::before{content:"\F1C8F"}.mdi-human-non-binary::before{content:"\F1848"}.mdi-human-pregnant::before{content:"\F05CF"}.mdi-human-queue::before{content:"\F1571"}.mdi-human-scooter::before{content:"\F11E9"}.mdi-human-walker::before{content:"\F1B71"}.mdi-human-wheelchair::before{content:"\F138D"}.mdi-human-white-cane::before{content:"\F1981"}.mdi-humble-bundle::before{content:"\F0744"}.mdi-hvac::before{content:"\F1352"}.mdi-hvac-off::before{content:"\F159E"}.mdi-hydraulic-oil-level::before{content:"\F1324"}.mdi-hydraulic-oil-temperature::before{content:"\F1325"}.mdi-hydro-power::before{content:"\F12E5"}.mdi-hydrogen-station::before{content:"\F1894"}.mdi-ice-cream::before{content:"\F082A"}.mdi-ice-cream-off::before{content:"\F0E52"}.mdi-ice-pop::before{content:"\F0EFD"}.mdi-id-card::before{content:"\F0FC0"}.mdi-identifier::before{content:"\F0EFE"}.mdi-ideogram-cjk::before{content:"\F1331"}.mdi-ideogram-cjk-variant::before{content:"\F1332"}.mdi-image::before{content:"\F02E9"}.mdi-image-album::before{content:"\F02EA"}.mdi-image-area::before{content:"\F02EB"}.mdi-image-area-close::before{content:"\F02EC"}.mdi-image-auto-adjust::before{content:"\F0FC1"}.mdi-image-broken::before{content:"\F02ED"}.mdi-image-broken-variant::before{content:"\F02EE"}.mdi-image-check::before{content:"\F1B25"}.mdi-image-check-outline::before{content:"\F1B26"}.mdi-image-edit::before{content:"\F11E3"}.mdi-image-edit-outline::before{content:"\F11E4"}.mdi-image-filter-black-white::before{content:"\F02F0"}.mdi-image-filter-center-focus::before{content:"\F02F1"}.mdi-image-filter-center-focus-strong::before{content:"\F0EFF"}.mdi-image-filter-center-focus-strong-outline::before{content:"\F0F00"}.mdi-image-filter-center-focus-weak::before{content:"\F02F2"}.mdi-image-filter-drama::before{content:"\F02F3"}.mdi-image-filter-drama-outline::before{content:"\F1BFF"}.mdi-image-filter-frames::before{content:"\F02F4"}.mdi-image-filter-hdr::before{content:"\F02F5"}.mdi-image-filter-hdr-outline::before{content:"\F1C64"}.mdi-image-filter-none::before{content:"\F02F6"}.mdi-image-filter-tilt-shift::before{content:"\F02F7"}.mdi-image-filter-vintage::before{content:"\F02F8"}.mdi-image-frame::before{content:"\F0E49"}.mdi-image-lock::before{content:"\F1AB0"}.mdi-image-lock-outline::before{content:"\F1AB1"}.mdi-image-marker::before{content:"\F177B"}.mdi-image-marker-outline::before{content:"\F177C"}.mdi-image-minus::before{content:"\F1419"}.mdi-image-minus-outline::before{content:"\F1B47"}.mdi-image-move::before{content:"\F09F8"}.mdi-image-multiple::before{content:"\F02F9"}.mdi-image-multiple-outline::before{content:"\F02EF"}.mdi-image-off::before{content:"\F082B"}.mdi-image-off-outline::before{content:"\F11D1"}.mdi-image-outline::before{content:"\F0976"}.mdi-image-plus::before{content:"\F087C"}.mdi-image-plus-outline::before{content:"\F1B46"}.mdi-image-refresh::before{content:"\F19FE"}.mdi-image-refresh-outline::before{content:"\F19FF"}.mdi-image-remove::before{content:"\F1418"}.mdi-image-remove-outline::before{content:"\F1B48"}.mdi-image-search::before{content:"\F0977"}.mdi-image-search-outline::before{content:"\F0978"}.mdi-image-size-select-actual::before{content:"\F0C8D"}.mdi-image-size-select-large::before{content:"\F0C8E"}.mdi-image-size-select-small::before{content:"\F0C8F"}.mdi-image-sync::before{content:"\F1A00"}.mdi-image-sync-outline::before{content:"\F1A01"}.mdi-image-text::before{content:"\F160D"}.mdi-import::before{content:"\F02FA"}.mdi-inbox::before{content:"\F0687"}.mdi-inbox-arrow-down::before{content:"\F02FB"}.mdi-inbox-arrow-down-outline::before{content:"\F1270"}.mdi-inbox-arrow-up::before{content:"\F03D1"}.mdi-inbox-arrow-up-outline::before{content:"\F1271"}.mdi-inbox-full::before{content:"\F1272"}.mdi-inbox-full-outline::before{content:"\F1273"}.mdi-inbox-multiple::before{content:"\F08B0"}.mdi-inbox-multiple-outline::before{content:"\F0BA8"}.mdi-inbox-outline::before{content:"\F1274"}.mdi-inbox-remove::before{content:"\F159F"}.mdi-inbox-remove-outline::before{content:"\F15A0"}.mdi-incognito::before{content:"\F05F9"}.mdi-incognito-circle::before{content:"\F1421"}.mdi-incognito-circle-off::before{content:"\F1422"}.mdi-incognito-off::before{content:"\F0075"}.mdi-induction::before{content:"\F184C"}.mdi-infinity::before{content:"\F06E4"}.mdi-information::before{content:"\F02FC"}.mdi-information-box::before{content:"\F1C65"}.mdi-information-box-outline::before{content:"\F1C66"}.mdi-information-off::before{content:"\F178C"}.mdi-information-off-outline::before{content:"\F178D"}.mdi-information-outline::before{content:"\F02FD"}.mdi-information-slab-box::before{content:"\F1C67"}.mdi-information-slab-box-outline::before{content:"\F1C68"}.mdi-information-slab-circle::before{content:"\F1C69"}.mdi-information-slab-circle-outline::before{content:"\F1C6A"}.mdi-information-slab-symbol::before{content:"\F1C6B"}.mdi-information-symbol::before{content:"\F1C6C"}.mdi-information-variant::before{content:"\F064E"}.mdi-information-variant-box::before{content:"\F1C6D"}.mdi-information-variant-box-outline::before{content:"\F1C6E"}.mdi-information-variant-circle::before{content:"\F1C6F"}.mdi-information-variant-circle-outline::before{content:"\F1C70"}.mdi-instagram::before{content:"\F02FE"}.mdi-instrument-triangle::before{content:"\F104E"}.mdi-integrated-circuit-chip::before{content:"\F1913"}.mdi-invert-colors::before{content:"\F0301"}.mdi-invert-colors-off::before{content:"\F0E4A"}.mdi-invoice::before{content:"\F1CD2"}.mdi-invoice-arrow-left::before{content:"\F1CD3"}.mdi-invoice-arrow-left-outline::before{content:"\F1CD4"}.mdi-invoice-arrow-right::before{content:"\F1CD5"}.mdi-invoice-arrow-right-outline::before{content:"\F1CD6"}.mdi-invoice-check::before{content:"\F1CD7"}.mdi-invoice-check-outline::before{content:"\F1CD8"}.mdi-invoice-clock::before{content:"\F1CD9"}.mdi-invoice-clock-outline::before{content:"\F1CDA"}.mdi-invoice-edit::before{content:"\F1CDB"}.mdi-invoice-edit-outline::before{content:"\F1CDC"}.mdi-invoice-export-outline::before{content:"\F1CDD"}.mdi-invoice-fast::before{content:"\F1CDE"}.mdi-invoice-fast-outline::before{content:"\F1CDF"}.mdi-invoice-import::before{content:"\F1CE0"}.mdi-invoice-import-outline::before{content:"\F1CE1"}.mdi-invoice-list::before{content:"\F1CE2"}.mdi-invoice-list-outline::before{content:"\F1CE3"}.mdi-invoice-minus::before{content:"\F1CE4"}.mdi-invoice-minus-outline::before{content:"\F1CE5"}.mdi-invoice-multiple::before{content:"\F1CE6"}.mdi-invoice-multiple-outline::before{content:"\F1CE7"}.mdi-invoice-outline::before{content:"\F1CE8"}.mdi-invoice-plus::before{content:"\F1CE9"}.mdi-invoice-plus-outline::before{content:"\F1CEA"}.mdi-invoice-remove::before{content:"\F1CEB"}.mdi-invoice-remove-outline::before{content:"\F1CEC"}.mdi-invoice-send::before{content:"\F1CED"}.mdi-invoice-send-outline::before{content:"\F1CEE"}.mdi-invoice-text::before{content:"\F1CEF"}.mdi-invoice-text-arrow-left::before{content:"\F1CF0"}.mdi-invoice-text-arrow-left-outline::before{content:"\F1CF1"}.mdi-invoice-text-arrow-right::before{content:"\F1CF2"}.mdi-invoice-text-arrow-right-outline::before{content:"\F1CF3"}.mdi-invoice-text-check::before{content:"\F1CF4"}.mdi-invoice-text-check-outline::before{content:"\F1CF5"}.mdi-invoice-text-clock::before{content:"\F1CF6"}.mdi-invoice-text-clock-outline::before{content:"\F1CF7"}.mdi-invoice-text-edit::before{content:"\F1CF8"}.mdi-invoice-text-edit-outline::before{content:"\F1CF9"}.mdi-invoice-text-fast::before{content:"\F1CFA"}.mdi-invoice-text-fast-outline::before{content:"\F1CFB"}.mdi-invoice-text-minus::before{content:"\F1CFC"}.mdi-invoice-text-minus-outline::before{content:"\F1CFD"}.mdi-invoice-text-multiple::before{content:"\F1CFE"}.mdi-invoice-text-multiple-outline::before{content:"\F1CFF"}.mdi-invoice-text-outline::before{content:"\F1D00"}.mdi-invoice-text-plus::before{content:"\F1D01"}.mdi-invoice-text-plus-outline::before{content:"\F1D02"}.mdi-invoice-text-remove::before{content:"\F1D03"}.mdi-invoice-text-remove-outline::before{content:"\F1D04"}.mdi-invoice-text-send::before{content:"\F1D05"}.mdi-invoice-text-send-outline::before{content:"\F1D06"}.mdi-iobroker::before{content:"\F12E8"}.mdi-ip::before{content:"\F0A5F"}.mdi-ip-network::before{content:"\F0A60"}.mdi-ip-network-outline::before{content:"\F0C90"}.mdi-ip-outline::before{content:"\F1982"}.mdi-ipod::before{content:"\F0C91"}.mdi-iron::before{content:"\F1824"}.mdi-iron-board::before{content:"\F1838"}.mdi-iron-outline::before{content:"\F1825"}.mdi-island::before{content:"\F104F"}.mdi-island-variant::before{content:"\F1CC6"}.mdi-iv-bag::before{content:"\F10B9"}.mdi-jabber::before{content:"\F0DD5"}.mdi-jeepney::before{content:"\F0302"}.mdi-jellyfish::before{content:"\F0F01"}.mdi-jellyfish-outline::before{content:"\F0F02"}.mdi-jira::before{content:"\F0303"}.mdi-jquery::before{content:"\F087D"}.mdi-jsfiddle::before{content:"\F0304"}.mdi-jump-rope::before{content:"\F12FF"}.mdi-kabaddi::before{content:"\F0D87"}.mdi-kangaroo::before{content:"\F1558"}.mdi-karate::before{content:"\F082C"}.mdi-kayaking::before{content:"\F08AF"}.mdi-keg::before{content:"\F0305"}.mdi-kettle::before{content:"\F05FA"}.mdi-kettle-alert::before{content:"\F1317"}.mdi-kettle-alert-outline::before{content:"\F1318"}.mdi-kettle-off::before{content:"\F131B"}.mdi-kettle-off-outline::before{content:"\F131C"}.mdi-kettle-outline::before{content:"\F0F56"}.mdi-kettle-pour-over::before{content:"\F173C"}.mdi-kettle-steam::before{content:"\F1319"}.mdi-kettle-steam-outline::before{content:"\F131A"}.mdi-kettlebell::before{content:"\F1300"}.mdi-key::before{content:"\F0306"}.mdi-key-alert::before{content:"\F1983"}.mdi-key-alert-outline::before{content:"\F1984"}.mdi-key-arrow-right::before{content:"\F1312"}.mdi-key-chain::before{content:"\F1574"}.mdi-key-chain-variant::before{content:"\F1575"}.mdi-key-change::before{content:"\F0307"}.mdi-key-link::before{content:"\F119F"}.mdi-key-minus::before{content:"\F0308"}.mdi-key-outline::before{content:"\F0DD6"}.mdi-key-plus::before{content:"\F0309"}.mdi-key-remove::before{content:"\F030A"}.mdi-key-star::before{content:"\F119E"}.mdi-key-variant::before{content:"\F030B"}.mdi-key-wireless::before{content:"\F0FC2"}.mdi-keyboard::before{content:"\F030C"}.mdi-keyboard-backspace::before{content:"\F030D"}.mdi-keyboard-caps::before{content:"\F030E"}.mdi-keyboard-close::before{content:"\F030F"}.mdi-keyboard-close-outline::before{content:"\F1C00"}.mdi-keyboard-esc::before{content:"\F12B7"}.mdi-keyboard-f1::before{content:"\F12AB"}.mdi-keyboard-f10::before{content:"\F12B4"}.mdi-keyboard-f11::before{content:"\F12B5"}.mdi-keyboard-f12::before{content:"\F12B6"}.mdi-keyboard-f2::before{content:"\F12AC"}.mdi-keyboard-f3::before{content:"\F12AD"}.mdi-keyboard-f4::before{content:"\F12AE"}.mdi-keyboard-f5::before{content:"\F12AF"}.mdi-keyboard-f6::before{content:"\F12B0"}.mdi-keyboard-f7::before{content:"\F12B1"}.mdi-keyboard-f8::before{content:"\F12B2"}.mdi-keyboard-f9::before{content:"\F12B3"}.mdi-keyboard-off::before{content:"\F0310"}.mdi-keyboard-off-outline::before{content:"\F0E4B"}.mdi-keyboard-outline::before{content:"\F097B"}.mdi-keyboard-return::before{content:"\F0311"}.mdi-keyboard-settings::before{content:"\F09F9"}.mdi-keyboard-settings-outline::before{content:"\F09FA"}.mdi-keyboard-space::before{content:"\F1050"}.mdi-keyboard-tab::before{content:"\F0312"}.mdi-keyboard-tab-reverse::before{content:"\F0325"}.mdi-keyboard-variant::before{content:"\F0313"}.mdi-khanda::before{content:"\F10FD"}.mdi-kickstarter::before{content:"\F0745"}.mdi-kite::before{content:"\F1985"}.mdi-kite-outline::before{content:"\F1986"}.mdi-kitesurfing::before{content:"\F1744"}.mdi-klingon::before{content:"\F135B"}.mdi-knife::before{content:"\F09FB"}.mdi-knife-military::before{content:"\F09FC"}.mdi-knob::before{content:"\F1B96"}.mdi-koala::before{content:"\F173F"}.mdi-kodi::before{content:"\F0314"}.mdi-kubernetes::before{content:"\F10FE"}.mdi-label::before{content:"\F0315"}.mdi-label-multiple::before{content:"\F1375"}.mdi-label-multiple-outline::before{content:"\F1376"}.mdi-label-off::before{content:"\F0ACB"}.mdi-label-off-outline::before{content:"\F0ACC"}.mdi-label-outline::before{content:"\F0316"}.mdi-label-percent::before{content:"\F12EA"}.mdi-label-percent-outline::before{content:"\F12EB"}.mdi-label-variant::before{content:"\F0ACD"}.mdi-label-variant-outline::before{content:"\F0ACE"}.mdi-ladder::before{content:"\F15A2"}.mdi-ladybug::before{content:"\F082D"}.mdi-lambda::before{content:"\F0627"}.mdi-lamp::before{content:"\F06B5"}.mdi-lamp-outline::before{content:"\F17D0"}.mdi-lamps::before{content:"\F1576"}.mdi-lamps-outline::before{content:"\F17D1"}.mdi-lan::before{content:"\F0317"}.mdi-lan-check::before{content:"\F12AA"}.mdi-lan-connect::before{content:"\F0318"}.mdi-lan-disconnect::before{content:"\F0319"}.mdi-lan-pending::before{content:"\F031A"}.mdi-land-fields::before{content:"\F1AB2"}.mdi-land-plots::before{content:"\F1AB3"}.mdi-land-plots-circle::before{content:"\F1AB4"}.mdi-land-plots-circle-variant::before{content:"\F1AB5"}.mdi-land-plots-marker::before{content:"\F1C5D"}.mdi-land-rows-horizontal::before{content:"\F1AB6"}.mdi-land-rows-vertical::before{content:"\F1AB7"}.mdi-landslide::before{content:"\F1A48"}.mdi-landslide-outline::before{content:"\F1A49"}.mdi-language-c::before{content:"\F0671"}.mdi-language-cpp::before{content:"\F0672"}.mdi-language-csharp::before{content:"\F031B"}.mdi-language-css3::before{content:"\F031C"}.mdi-language-fortran::before{content:"\F121A"}.mdi-language-go::before{content:"\F07D3"}.mdi-language-haskell::before{content:"\F0C92"}.mdi-language-html5::before{content:"\F031D"}.mdi-language-java::before{content:"\F0B37"}.mdi-language-javascript::before{content:"\F031E"}.mdi-language-kotlin::before{content:"\F1219"}.mdi-language-lua::before{content:"\F08B1"}.mdi-language-markdown::before{content:"\F0354"}.mdi-language-markdown-outline::before{content:"\F0F5B"}.mdi-language-php::before{content:"\F031F"}.mdi-language-python::before{content:"\F0320"}.mdi-language-r::before{content:"\F07D4"}.mdi-language-ruby::before{content:"\F0D2D"}.mdi-language-ruby-on-rails::before{content:"\F0ACF"}.mdi-language-rust::before{content:"\F1617"}.mdi-language-swift::before{content:"\F06E5"}.mdi-language-typescript::before{content:"\F06E6"}.mdi-language-xaml::before{content:"\F0673"}.mdi-laptop::before{content:"\F0322"}.mdi-laptop-account::before{content:"\F1A4A"}.mdi-laptop-off::before{content:"\F06E7"}.mdi-laravel::before{content:"\F0AD0"}.mdi-laser-pointer::before{content:"\F1484"}.mdi-lasso::before{content:"\F0F03"}.mdi-lastpass::before{content:"\F0446"}.mdi-latitude::before{content:"\F0F57"}.mdi-launch::before{content:"\F0327"}.mdi-lava-lamp::before{content:"\F07D5"}.mdi-layers::before{content:"\F0328"}.mdi-layers-edit::before{content:"\F1892"}.mdi-layers-minus::before{content:"\F0E4C"}.mdi-layers-off::before{content:"\F0329"}.mdi-layers-off-outline::before{content:"\F09FD"}.mdi-layers-outline::before{content:"\F09FE"}.mdi-layers-plus::before{content:"\F0E4D"}.mdi-layers-remove::before{content:"\F0E4E"}.mdi-layers-search::before{content:"\F1206"}.mdi-layers-search-outline::before{content:"\F1207"}.mdi-layers-triple::before{content:"\F0F58"}.mdi-layers-triple-outline::before{content:"\F0F59"}.mdi-lead-pencil::before{content:"\F064F"}.mdi-leaf::before{content:"\F032A"}.mdi-leaf-circle::before{content:"\F1905"}.mdi-leaf-circle-outline::before{content:"\F1906"}.mdi-leaf-maple::before{content:"\F0C93"}.mdi-leaf-maple-off::before{content:"\F12DA"}.mdi-leaf-off::before{content:"\F12D9"}.mdi-leak::before{content:"\F0DD7"}.mdi-leak-off::before{content:"\F0DD8"}.mdi-lectern::before{content:"\F1AF0"}.mdi-led-off::before{content:"\F032B"}.mdi-led-on::before{content:"\F032C"}.mdi-led-outline::before{content:"\F032D"}.mdi-led-strip::before{content:"\F07D6"}.mdi-led-strip-variant::before{content:"\F1051"}.mdi-led-strip-variant-off::before{content:"\F1A4B"}.mdi-led-variant-off::before{content:"\F032E"}.mdi-led-variant-on::before{content:"\F032F"}.mdi-led-variant-outline::before{content:"\F0330"}.mdi-leek::before{content:"\F117D"}.mdi-less-than::before{content:"\F097C"}.mdi-less-than-or-equal::before{content:"\F097D"}.mdi-library::before{content:"\F0331"}.mdi-library-outline::before{content:"\F1A22"}.mdi-library-shelves::before{content:"\F0BA9"}.mdi-license::before{content:"\F0FC3"}.mdi-lifebuoy::before{content:"\F087E"}.mdi-light-flood-down::before{content:"\F1987"}.mdi-light-flood-up::before{content:"\F1988"}.mdi-light-recessed::before{content:"\F179B"}.mdi-light-switch::before{content:"\F097E"}.mdi-light-switch-off::before{content:"\F1A24"}.mdi-lightbulb::before{content:"\F0335"}.mdi-lightbulb-alert::before{content:"\F19E1"}.mdi-lightbulb-alert-outline::before{content:"\F19E2"}.mdi-lightbulb-auto::before{content:"\F1800"}.mdi-lightbulb-auto-outline::before{content:"\F1801"}.mdi-lightbulb-cfl::before{content:"\F1208"}.mdi-lightbulb-cfl-off::before{content:"\F1209"}.mdi-lightbulb-cfl-spiral::before{content:"\F1275"}.mdi-lightbulb-cfl-spiral-off::before{content:"\F12C3"}.mdi-lightbulb-fluorescent-tube::before{content:"\F1804"}.mdi-lightbulb-fluorescent-tube-outline::before{content:"\F1805"}.mdi-lightbulb-group::before{content:"\F1253"}.mdi-lightbulb-group-off::before{content:"\F12CD"}.mdi-lightbulb-group-off-outline::before{content:"\F12CE"}.mdi-lightbulb-group-outline::before{content:"\F1254"}.mdi-lightbulb-multiple::before{content:"\F1255"}.mdi-lightbulb-multiple-off::before{content:"\F12CF"}.mdi-lightbulb-multiple-off-outline::before{content:"\F12D0"}.mdi-lightbulb-multiple-outline::before{content:"\F1256"}.mdi-lightbulb-night::before{content:"\F1A4C"}.mdi-lightbulb-night-outline::before{content:"\F1A4D"}.mdi-lightbulb-off::before{content:"\F0E4F"}.mdi-lightbulb-off-outline::before{content:"\F0E50"}.mdi-lightbulb-on::before{content:"\F06E8"}.mdi-lightbulb-on-10::before{content:"\F1A4E"}.mdi-lightbulb-on-20::before{content:"\F1A4F"}.mdi-lightbulb-on-30::before{content:"\F1A50"}.mdi-lightbulb-on-40::before{content:"\F1A51"}.mdi-lightbulb-on-50::before{content:"\F1A52"}.mdi-lightbulb-on-60::before{content:"\F1A53"}.mdi-lightbulb-on-70::before{content:"\F1A54"}.mdi-lightbulb-on-80::before{content:"\F1A55"}.mdi-lightbulb-on-90::before{content:"\F1A56"}.mdi-lightbulb-on-outline::before{content:"\F06E9"}.mdi-lightbulb-outline::before{content:"\F0336"}.mdi-lightbulb-question::before{content:"\F19E3"}.mdi-lightbulb-question-outline::before{content:"\F19E4"}.mdi-lightbulb-spot::before{content:"\F17F4"}.mdi-lightbulb-spot-off::before{content:"\F17F5"}.mdi-lightbulb-variant::before{content:"\F1802"}.mdi-lightbulb-variant-outline::before{content:"\F1803"}.mdi-lighthouse::before{content:"\F09FF"}.mdi-lighthouse-on::before{content:"\F0A00"}.mdi-lightning-bolt::before{content:"\F140B"}.mdi-lightning-bolt-circle::before{content:"\F0820"}.mdi-lightning-bolt-outline::before{content:"\F140C"}.mdi-line-scan::before{content:"\F0624"}.mdi-lingerie::before{content:"\F1476"}.mdi-link::before{content:"\F0337"}.mdi-link-box::before{content:"\F0D1A"}.mdi-link-box-outline::before{content:"\F0D1B"}.mdi-link-box-variant::before{content:"\F0D1C"}.mdi-link-box-variant-outline::before{content:"\F0D1D"}.mdi-link-circle::before{content:"\F1CAC"}.mdi-link-circle-outline::before{content:"\F1CAD"}.mdi-link-edit::before{content:"\F1CAE"}.mdi-link-lock::before{content:"\F10BA"}.mdi-link-off::before{content:"\F0338"}.mdi-link-plus::before{content:"\F0C94"}.mdi-link-variant::before{content:"\F0339"}.mdi-link-variant-minus::before{content:"\F10FF"}.mdi-link-variant-off::before{content:"\F033A"}.mdi-link-variant-plus::before{content:"\F1100"}.mdi-link-variant-remove::before{content:"\F1101"}.mdi-linkedin::before{content:"\F033B"}.mdi-linux::before{content:"\F033D"}.mdi-linux-mint::before{content:"\F08ED"}.mdi-lipstick::before{content:"\F13B5"}.mdi-liquid-spot::before{content:"\F1826"}.mdi-liquor::before{content:"\F191E"}.mdi-list-box::before{content:"\F1B7B"}.mdi-list-box-outline::before{content:"\F1B7C"}.mdi-list-status::before{content:"\F15AB"}.mdi-litecoin::before{content:"\F0A61"}.mdi-loading::before{content:"\F0772"}.mdi-location-enter::before{content:"\F0FC4"}.mdi-location-exit::before{content:"\F0FC5"}.mdi-lock::before{content:"\F033E"}.mdi-lock-alert::before{content:"\F08EE"}.mdi-lock-alert-outline::before{content:"\F15D1"}.mdi-lock-check::before{content:"\F139A"}.mdi-lock-check-outline::before{content:"\F16A8"}.mdi-lock-clock::before{content:"\F097F"}.mdi-lock-minus::before{content:"\F16A9"}.mdi-lock-minus-outline::before{content:"\F16AA"}.mdi-lock-off::before{content:"\F1671"}.mdi-lock-off-outline::before{content:"\F1672"}.mdi-lock-open::before{content:"\F033F"}.mdi-lock-open-alert::before{content:"\F139B"}.mdi-lock-open-alert-outline::before{content:"\F15D2"}.mdi-lock-open-check::before{content:"\F139C"}.mdi-lock-open-check-outline::before{content:"\F16AB"}.mdi-lock-open-minus::before{content:"\F16AC"}.mdi-lock-open-minus-outline::before{content:"\F16AD"}.mdi-lock-open-outline::before{content:"\F0340"}.mdi-lock-open-plus::before{content:"\F16AE"}.mdi-lock-open-plus-outline::before{content:"\F16AF"}.mdi-lock-open-remove::before{content:"\F16B0"}.mdi-lock-open-remove-outline::before{content:"\F16B1"}.mdi-lock-open-variant::before{content:"\F0FC6"}.mdi-lock-open-variant-outline::before{content:"\F0FC7"}.mdi-lock-outline::before{content:"\F0341"}.mdi-lock-pattern::before{content:"\F06EA"}.mdi-lock-percent::before{content:"\F1C12"}.mdi-lock-percent-open::before{content:"\F1C13"}.mdi-lock-percent-open-outline::before{content:"\F1C14"}.mdi-lock-percent-open-variant::before{content:"\F1C15"}.mdi-lock-percent-open-variant-outline::before{content:"\F1C16"}.mdi-lock-percent-outline::before{content:"\F1C17"}.mdi-lock-plus::before{content:"\F05FB"}.mdi-lock-plus-outline::before{content:"\F16B2"}.mdi-lock-question::before{content:"\F08EF"}.mdi-lock-remove::before{content:"\F16B3"}.mdi-lock-remove-outline::before{content:"\F16B4"}.mdi-lock-reset::before{content:"\F0773"}.mdi-lock-smart::before{content:"\F08B2"}.mdi-locker::before{content:"\F07D7"}.mdi-locker-multiple::before{content:"\F07D8"}.mdi-login::before{content:"\F0342"}.mdi-login-variant::before{content:"\F05FC"}.mdi-logout::before{content:"\F0343"}.mdi-logout-variant::before{content:"\F05FD"}.mdi-longitude::before{content:"\F0F5A"}.mdi-looks::before{content:"\F0344"}.mdi-lotion::before{content:"\F1582"}.mdi-lotion-outline::before{content:"\F1583"}.mdi-lotion-plus::before{content:"\F1584"}.mdi-lotion-plus-outline::before{content:"\F1585"}.mdi-loupe::before{content:"\F0345"}.mdi-lumx::before{content:"\F0346"}.mdi-lungs::before{content:"\F1084"}.mdi-mace::before{content:"\F1843"}.mdi-magazine-pistol::before{content:"\F0324"}.mdi-magazine-rifle::before{content:"\F0323"}.mdi-magic-staff::before{content:"\F1844"}.mdi-magnet::before{content:"\F0347"}.mdi-magnet-on::before{content:"\F0348"}.mdi-magnify::before{content:"\F0349"}.mdi-magnify-close::before{content:"\F0980"}.mdi-magnify-expand::before{content:"\F1874"}.mdi-magnify-minus::before{content:"\F034A"}.mdi-magnify-minus-cursor::before{content:"\F0A62"}.mdi-magnify-minus-outline::before{content:"\F06EC"}.mdi-magnify-plus::before{content:"\F034B"}.mdi-magnify-plus-cursor::before{content:"\F0A63"}.mdi-magnify-plus-outline::before{content:"\F06ED"}.mdi-magnify-remove-cursor::before{content:"\F120C"}.mdi-magnify-remove-outline::before{content:"\F120D"}.mdi-magnify-scan::before{content:"\F1276"}.mdi-mail::before{content:"\F0EBB"}.mdi-mailbox::before{content:"\F06EE"}.mdi-mailbox-open::before{content:"\F0D88"}.mdi-mailbox-open-outline::before{content:"\F0D89"}.mdi-mailbox-open-up::before{content:"\F0D8A"}.mdi-mailbox-open-up-outline::before{content:"\F0D8B"}.mdi-mailbox-outline::before{content:"\F0D8C"}.mdi-mailbox-up::before{content:"\F0D8D"}.mdi-mailbox-up-outline::before{content:"\F0D8E"}.mdi-manjaro::before{content:"\F160A"}.mdi-map::before{content:"\F034D"}.mdi-map-check::before{content:"\F0EBC"}.mdi-map-check-outline::before{content:"\F0EBD"}.mdi-map-clock::before{content:"\F0D1E"}.mdi-map-clock-outline::before{content:"\F0D1F"}.mdi-map-legend::before{content:"\F0A01"}.mdi-map-marker::before{content:"\F034E"}.mdi-map-marker-account::before{content:"\F18E3"}.mdi-map-marker-account-outline::before{content:"\F18E4"}.mdi-map-marker-alert::before{content:"\F0F05"}.mdi-map-marker-alert-outline::before{content:"\F0F06"}.mdi-map-marker-check::before{content:"\F0C95"}.mdi-map-marker-check-outline::before{content:"\F12FB"}.mdi-map-marker-circle::before{content:"\F034F"}.mdi-map-marker-distance::before{content:"\F08F0"}.mdi-map-marker-down::before{content:"\F1102"}.mdi-map-marker-left::before{content:"\F12DB"}.mdi-map-marker-left-outline::before{content:"\F12DD"}.mdi-map-marker-minus::before{content:"\F0650"}.mdi-map-marker-minus-outline::before{content:"\F12F9"}.mdi-map-marker-multiple::before{content:"\F0350"}.mdi-map-marker-multiple-outline::before{content:"\F1277"}.mdi-map-marker-off::before{content:"\F0351"}.mdi-map-marker-off-outline::before{content:"\F12FD"}.mdi-map-marker-outline::before{content:"\F07D9"}.mdi-map-marker-path::before{content:"\F0D20"}.mdi-map-marker-plus::before{content:"\F0651"}.mdi-map-marker-plus-outline::before{content:"\F12F8"}.mdi-map-marker-question::before{content:"\F0F07"}.mdi-map-marker-question-outline::before{content:"\F0F08"}.mdi-map-marker-radius::before{content:"\F0352"}.mdi-map-marker-radius-outline::before{content:"\F12FC"}.mdi-map-marker-remove::before{content:"\F0F09"}.mdi-map-marker-remove-outline::before{content:"\F12FA"}.mdi-map-marker-remove-variant::before{content:"\F0F0A"}.mdi-map-marker-right::before{content:"\F12DC"}.mdi-map-marker-right-outline::before{content:"\F12DE"}.mdi-map-marker-star::before{content:"\F1608"}.mdi-map-marker-star-outline::before{content:"\F1609"}.mdi-map-marker-up::before{content:"\F1103"}.mdi-map-minus::before{content:"\F0981"}.mdi-map-outline::before{content:"\F0982"}.mdi-map-plus::before{content:"\F0983"}.mdi-map-search::before{content:"\F0984"}.mdi-map-search-outline::before{content:"\F0985"}.mdi-mapbox::before{content:"\F0BAA"}.mdi-margin::before{content:"\F0353"}.mdi-marker::before{content:"\F0652"}.mdi-marker-cancel::before{content:"\F0DD9"}.mdi-marker-check::before{content:"\F0355"}.mdi-mastodon::before{content:"\F0AD1"}.mdi-material-design::before{content:"\F0986"}.mdi-material-ui::before{content:"\F0357"}.mdi-math-compass::before{content:"\F0358"}.mdi-math-cos::before{content:"\F0C96"}.mdi-math-integral::before{content:"\F0FC8"}.mdi-math-integral-box::before{content:"\F0FC9"}.mdi-math-log::before{content:"\F1085"}.mdi-math-norm::before{content:"\F0FCA"}.mdi-math-norm-box::before{content:"\F0FCB"}.mdi-math-sin::before{content:"\F0C97"}.mdi-math-tan::before{content:"\F0C98"}.mdi-matrix::before{content:"\F0628"}.mdi-medal::before{content:"\F0987"}.mdi-medal-outline::before{content:"\F1326"}.mdi-medical-bag::before{content:"\F06EF"}.mdi-medical-cotton-swab::before{content:"\F1AB8"}.mdi-medication::before{content:"\F1B14"}.mdi-medication-outline::before{content:"\F1B15"}.mdi-meditation::before{content:"\F117B"}.mdi-memory::before{content:"\F035B"}.mdi-memory-arrow-down::before{content:"\F1CA6"}.mdi-menorah::before{content:"\F17D4"}.mdi-menorah-fire::before{content:"\F17D5"}.mdi-menu::before{content:"\F035C"}.mdi-menu-close::before{content:"\F1C90"}.mdi-menu-down::before{content:"\F035D"}.mdi-menu-down-outline::before{content:"\F06B6"}.mdi-menu-left::before{content:"\F035E"}.mdi-menu-left-outline::before{content:"\F0A02"}.mdi-menu-open::before{content:"\F0BAB"}.mdi-menu-right::before{content:"\F035F"}.mdi-menu-right-outline::before{content:"\F0A03"}.mdi-menu-swap::before{content:"\F0A64"}.mdi-menu-swap-outline::before{content:"\F0A65"}.mdi-menu-up::before{content:"\F0360"}.mdi-menu-up-outline::before{content:"\F06B7"}.mdi-merge::before{content:"\F0F5C"}.mdi-message::before{content:"\F0361"}.mdi-message-alert::before{content:"\F0362"}.mdi-message-alert-outline::before{content:"\F0A04"}.mdi-message-arrow-left::before{content:"\F12F2"}.mdi-message-arrow-left-outline::before{content:"\F12F3"}.mdi-message-arrow-right::before{content:"\F12F4"}.mdi-message-arrow-right-outline::before{content:"\F12F5"}.mdi-message-badge::before{content:"\F1941"}.mdi-message-badge-outline::before{content:"\F1942"}.mdi-message-bookmark::before{content:"\F15AC"}.mdi-message-bookmark-outline::before{content:"\F15AD"}.mdi-message-bulleted::before{content:"\F06A2"}.mdi-message-bulleted-off::before{content:"\F06A3"}.mdi-message-check::before{content:"\F1B8A"}.mdi-message-check-outline::before{content:"\F1B8B"}.mdi-message-cog::before{content:"\F06F1"}.mdi-message-cog-outline::before{content:"\F1172"}.mdi-message-draw::before{content:"\F0363"}.mdi-message-fast::before{content:"\F19CC"}.mdi-message-fast-outline::before{content:"\F19CD"}.mdi-message-flash::before{content:"\F15A9"}.mdi-message-flash-outline::before{content:"\F15AA"}.mdi-message-image::before{content:"\F0364"}.mdi-message-image-outline::before{content:"\F116C"}.mdi-message-lock::before{content:"\F0FCC"}.mdi-message-lock-outline::before{content:"\F116D"}.mdi-message-minus::before{content:"\F116E"}.mdi-message-minus-outline::before{content:"\F116F"}.mdi-message-off::before{content:"\F164D"}.mdi-message-off-outline::before{content:"\F164E"}.mdi-message-outline::before{content:"\F0365"}.mdi-message-plus::before{content:"\F0653"}.mdi-message-plus-outline::before{content:"\F10BB"}.mdi-message-processing::before{content:"\F0366"}.mdi-message-processing-outline::before{content:"\F1170"}.mdi-message-question::before{content:"\F173A"}.mdi-message-question-outline::before{content:"\F173B"}.mdi-message-reply::before{content:"\F0367"}.mdi-message-reply-outline::before{content:"\F173D"}.mdi-message-reply-text::before{content:"\F0368"}.mdi-message-reply-text-outline::before{content:"\F173E"}.mdi-message-settings::before{content:"\F06F0"}.mdi-message-settings-outline::before{content:"\F1171"}.mdi-message-star::before{content:"\F069A"}.mdi-message-star-outline::before{content:"\F1250"}.mdi-message-text::before{content:"\F0369"}.mdi-message-text-clock::before{content:"\F1173"}.mdi-message-text-clock-outline::before{content:"\F1174"}.mdi-message-text-fast::before{content:"\F19CE"}.mdi-message-text-fast-outline::before{content:"\F19CF"}.mdi-message-text-lock::before{content:"\F0FCD"}.mdi-message-text-lock-outline::before{content:"\F1175"}.mdi-message-text-outline::before{content:"\F036A"}.mdi-message-video::before{content:"\F036B"}.mdi-meteor::before{content:"\F0629"}.mdi-meter-electric::before{content:"\F1A57"}.mdi-meter-electric-outline::before{content:"\F1A58"}.mdi-meter-gas::before{content:"\F1A59"}.mdi-meter-gas-outline::before{content:"\F1A5A"}.mdi-metronome::before{content:"\F07DA"}.mdi-metronome-tick::before{content:"\F07DB"}.mdi-micro-sd::before{content:"\F07DC"}.mdi-microphone::before{content:"\F036C"}.mdi-microphone-message::before{content:"\F050A"}.mdi-microphone-message-off::before{content:"\F050B"}.mdi-microphone-minus::before{content:"\F08B3"}.mdi-microphone-off::before{content:"\F036D"}.mdi-microphone-outline::before{content:"\F036E"}.mdi-microphone-plus::before{content:"\F08B4"}.mdi-microphone-question::before{content:"\F1989"}.mdi-microphone-question-outline::before{content:"\F198A"}.mdi-microphone-settings::before{content:"\F036F"}.mdi-microphone-variant::before{content:"\F0370"}.mdi-microphone-variant-off::before{content:"\F0371"}.mdi-microscope::before{content:"\F0654"}.mdi-microsoft::before{content:"\F0372"}.mdi-microsoft-access::before{content:"\F138E"}.mdi-microsoft-azure::before{content:"\F0805"}.mdi-microsoft-azure-devops::before{content:"\F0FD5"}.mdi-microsoft-bing::before{content:"\F00A4"}.mdi-microsoft-dynamics-365::before{content:"\F0988"}.mdi-microsoft-edge::before{content:"\F01E9"}.mdi-microsoft-excel::before{content:"\F138F"}.mdi-microsoft-internet-explorer::before{content:"\F0300"}.mdi-microsoft-office::before{content:"\F03C6"}.mdi-microsoft-onedrive::before{content:"\F03CA"}.mdi-microsoft-onenote::before{content:"\F0747"}.mdi-microsoft-outlook::before{content:"\F0D22"}.mdi-microsoft-powerpoint::before{content:"\F1390"}.mdi-microsoft-sharepoint::before{content:"\F1391"}.mdi-microsoft-teams::before{content:"\F02BB"}.mdi-microsoft-visual-studio::before{content:"\F0610"}.mdi-microsoft-visual-studio-code::before{content:"\F0A1E"}.mdi-microsoft-windows::before{content:"\F05B3"}.mdi-microsoft-windows-classic::before{content:"\F0A21"}.mdi-microsoft-word::before{content:"\F1392"}.mdi-microsoft-xbox::before{content:"\F05B9"}.mdi-microsoft-xbox-controller::before{content:"\F05BA"}.mdi-microsoft-xbox-controller-battery-alert::before{content:"\F074B"}.mdi-microsoft-xbox-controller-battery-charging::before{content:"\F0A22"}.mdi-microsoft-xbox-controller-battery-empty::before{content:"\F074C"}.mdi-microsoft-xbox-controller-battery-full::before{content:"\F074D"}.mdi-microsoft-xbox-controller-battery-low::before{content:"\F074E"}.mdi-microsoft-xbox-controller-battery-medium::before{content:"\F074F"}.mdi-microsoft-xbox-controller-battery-unknown::before{content:"\F0750"}.mdi-microsoft-xbox-controller-menu::before{content:"\F0E6F"}.mdi-microsoft-xbox-controller-off::before{content:"\F05BB"}.mdi-microsoft-xbox-controller-view::before{content:"\F0E70"}.mdi-microwave::before{content:"\F0C99"}.mdi-microwave-off::before{content:"\F1423"}.mdi-middleware::before{content:"\F0F5D"}.mdi-middleware-outline::before{content:"\F0F5E"}.mdi-midi::before{content:"\F08F1"}.mdi-midi-port::before{content:"\F08F2"}.mdi-mine::before{content:"\F0DDA"}.mdi-minecraft::before{content:"\F0373"}.mdi-mini-sd::before{content:"\F0A05"}.mdi-minidisc::before{content:"\F0A06"}.mdi-minus::before{content:"\F0374"}.mdi-minus-box::before{content:"\F0375"}.mdi-minus-box-multiple::before{content:"\F1141"}.mdi-minus-box-multiple-outline::before{content:"\F1142"}.mdi-minus-box-outline::before{content:"\F06F2"}.mdi-minus-circle::before{content:"\F0376"}.mdi-minus-circle-multiple::before{content:"\F035A"}.mdi-minus-circle-multiple-outline::before{content:"\F0AD3"}.mdi-minus-circle-off::before{content:"\F1459"}.mdi-minus-circle-off-outline::before{content:"\F145A"}.mdi-minus-circle-outline::before{content:"\F0377"}.mdi-minus-network::before{content:"\F0378"}.mdi-minus-network-outline::before{content:"\F0C9A"}.mdi-minus-thick::before{content:"\F1639"}.mdi-mirror::before{content:"\F11FD"}.mdi-mirror-rectangle::before{content:"\F179F"}.mdi-mirror-variant::before{content:"\F17A0"}.mdi-mixed-martial-arts::before{content:"\F0D8F"}.mdi-mixed-reality::before{content:"\F087F"}.mdi-molecule::before{content:"\F0BAC"}.mdi-molecule-co::before{content:"\F12FE"}.mdi-molecule-co2::before{content:"\F07E4"}.mdi-monitor::before{content:"\F0379"}.mdi-monitor-account::before{content:"\F1A5B"}.mdi-monitor-arrow-down::before{content:"\F19D0"}.mdi-monitor-arrow-down-variant::before{content:"\F19D1"}.mdi-monitor-cellphone::before{content:"\F0989"}.mdi-monitor-cellphone-star::before{content:"\F098A"}.mdi-monitor-dashboard::before{content:"\F0A07"}.mdi-monitor-edit::before{content:"\F12C6"}.mdi-monitor-eye::before{content:"\F13B4"}.mdi-monitor-lock::before{content:"\F0DDB"}.mdi-monitor-multiple::before{content:"\F037A"}.mdi-monitor-off::before{content:"\F0D90"}.mdi-monitor-screenshot::before{content:"\F0E51"}.mdi-monitor-share::before{content:"\F1483"}.mdi-monitor-shimmer::before{content:"\F1104"}.mdi-monitor-small::before{content:"\F1876"}.mdi-monitor-speaker::before{content:"\F0F5F"}.mdi-monitor-speaker-off::before{content:"\F0F60"}.mdi-monitor-star::before{content:"\F0DDC"}.mdi-monitor-vertical::before{content:"\F1C33"}.mdi-moon-first-quarter::before{content:"\F0F61"}.mdi-moon-full::before{content:"\F0F62"}.mdi-moon-last-quarter::before{content:"\F0F63"}.mdi-moon-new::before{content:"\F0F64"}.mdi-moon-waning-crescent::before{content:"\F0F65"}.mdi-moon-waning-gibbous::before{content:"\F0F66"}.mdi-moon-waxing-crescent::before{content:"\F0F67"}.mdi-moon-waxing-gibbous::before{content:"\F0F68"}.mdi-moped::before{content:"\F1086"}.mdi-moped-electric::before{content:"\F15B7"}.mdi-moped-electric-outline::before{content:"\F15B8"}.mdi-moped-outline::before{content:"\F15B9"}.mdi-more::before{content:"\F037B"}.mdi-mortar-pestle::before{content:"\F1748"}.mdi-mortar-pestle-plus::before{content:"\F03F1"}.mdi-mosque::before{content:"\F0D45"}.mdi-mosque-outline::before{content:"\F1827"}.mdi-mother-heart::before{content:"\F1314"}.mdi-mother-nurse::before{content:"\F0D21"}.mdi-motion::before{content:"\F15B2"}.mdi-motion-outline::before{content:"\F15B3"}.mdi-motion-pause::before{content:"\F1590"}.mdi-motion-pause-outline::before{content:"\F1592"}.mdi-motion-play::before{content:"\F158F"}.mdi-motion-play-outline::before{content:"\F1591"}.mdi-motion-sensor::before{content:"\F0D91"}.mdi-motion-sensor-off::before{content:"\F1435"}.mdi-motorbike::before{content:"\F037C"}.mdi-motorbike-electric::before{content:"\F15BA"}.mdi-motorbike-off::before{content:"\F1B16"}.mdi-mouse::before{content:"\F037D"}.mdi-mouse-bluetooth::before{content:"\F098B"}.mdi-mouse-left-click::before{content:"\F1D07"}.mdi-mouse-left-click-outline::before{content:"\F1D08"}.mdi-mouse-move-down::before{content:"\F1550"}.mdi-mouse-move-up::before{content:"\F1551"}.mdi-mouse-move-vertical::before{content:"\F1552"}.mdi-mouse-off::before{content:"\F037E"}.mdi-mouse-outline::before{content:"\F1D09"}.mdi-mouse-right-click::before{content:"\F1D0A"}.mdi-mouse-right-click-outline::before{content:"\F1D0B"}.mdi-mouse-scroll-wheel::before{content:"\F1D0C"}.mdi-mouse-variant::before{content:"\F037F"}.mdi-mouse-variant-off::before{content:"\F0380"}.mdi-move-resize::before{content:"\F0655"}.mdi-move-resize-variant::before{content:"\F0656"}.mdi-movie::before{content:"\F0381"}.mdi-movie-check::before{content:"\F16F3"}.mdi-movie-check-outline::before{content:"\F16F4"}.mdi-movie-cog::before{content:"\F16F5"}.mdi-movie-cog-outline::before{content:"\F16F6"}.mdi-movie-edit::before{content:"\F1122"}.mdi-movie-edit-outline::before{content:"\F1123"}.mdi-movie-filter::before{content:"\F1124"}.mdi-movie-filter-outline::before{content:"\F1125"}.mdi-movie-minus::before{content:"\F16F7"}.mdi-movie-minus-outline::before{content:"\F16F8"}.mdi-movie-off::before{content:"\F16F9"}.mdi-movie-off-outline::before{content:"\F16FA"}.mdi-movie-open::before{content:"\F0FCE"}.mdi-movie-open-check::before{content:"\F16FB"}.mdi-movie-open-check-outline::before{content:"\F16FC"}.mdi-movie-open-cog::before{content:"\F16FD"}.mdi-movie-open-cog-outline::before{content:"\F16FE"}.mdi-movie-open-edit::before{content:"\F16FF"}.mdi-movie-open-edit-outline::before{content:"\F1700"}.mdi-movie-open-minus::before{content:"\F1701"}.mdi-movie-open-minus-outline::before{content:"\F1702"}.mdi-movie-open-off::before{content:"\F1703"}.mdi-movie-open-off-outline::before{content:"\F1704"}.mdi-movie-open-outline::before{content:"\F0FCF"}.mdi-movie-open-play::before{content:"\F1705"}.mdi-movie-open-play-outline::before{content:"\F1706"}.mdi-movie-open-plus::before{content:"\F1707"}.mdi-movie-open-plus-outline::before{content:"\F1708"}.mdi-movie-open-remove::before{content:"\F1709"}.mdi-movie-open-remove-outline::before{content:"\F170A"}.mdi-movie-open-settings::before{content:"\F170B"}.mdi-movie-open-settings-outline::before{content:"\F170C"}.mdi-movie-open-star::before{content:"\F170D"}.mdi-movie-open-star-outline::before{content:"\F170E"}.mdi-movie-outline::before{content:"\F0DDD"}.mdi-movie-play::before{content:"\F170F"}.mdi-movie-play-outline::before{content:"\F1710"}.mdi-movie-plus::before{content:"\F1711"}.mdi-movie-plus-outline::before{content:"\F1712"}.mdi-movie-remove::before{content:"\F1713"}.mdi-movie-remove-outline::before{content:"\F1714"}.mdi-movie-roll::before{content:"\F07DE"}.mdi-movie-search::before{content:"\F11D2"}.mdi-movie-search-outline::before{content:"\F11D3"}.mdi-movie-settings::before{content:"\F1715"}.mdi-movie-settings-outline::before{content:"\F1716"}.mdi-movie-star::before{content:"\F1717"}.mdi-movie-star-outline::before{content:"\F1718"}.mdi-mower::before{content:"\F166F"}.mdi-mower-bag::before{content:"\F1670"}.mdi-mower-bag-on::before{content:"\F1B60"}.mdi-mower-on::before{content:"\F1B5F"}.mdi-muffin::before{content:"\F098C"}.mdi-multicast::before{content:"\F1893"}.mdi-multimedia::before{content:"\F1B97"}.mdi-multiplication::before{content:"\F0382"}.mdi-multiplication-box::before{content:"\F0383"}.mdi-mushroom::before{content:"\F07DF"}.mdi-mushroom-off::before{content:"\F13FA"}.mdi-mushroom-off-outline::before{content:"\F13FB"}.mdi-mushroom-outline::before{content:"\F07E0"}.mdi-music::before{content:"\F075A"}.mdi-music-accidental-double-flat::before{content:"\F0F69"}.mdi-music-accidental-double-sharp::before{content:"\F0F6A"}.mdi-music-accidental-flat::before{content:"\F0F6B"}.mdi-music-accidental-natural::before{content:"\F0F6C"}.mdi-music-accidental-sharp::before{content:"\F0F6D"}.mdi-music-box::before{content:"\F0384"}.mdi-music-box-multiple::before{content:"\F0333"}.mdi-music-box-multiple-outline::before{content:"\F0F04"}.mdi-music-box-outline::before{content:"\F0385"}.mdi-music-circle::before{content:"\F0386"}.mdi-music-circle-outline::before{content:"\F0AD4"}.mdi-music-clef-alto::before{content:"\F0F6E"}.mdi-music-clef-bass::before{content:"\F0F6F"}.mdi-music-clef-treble::before{content:"\F0F70"}.mdi-music-note::before{content:"\F0387"}.mdi-music-note-bluetooth::before{content:"\F05FE"}.mdi-music-note-bluetooth-off::before{content:"\F05FF"}.mdi-music-note-eighth::before{content:"\F0388"}.mdi-music-note-eighth-dotted::before{content:"\F0F71"}.mdi-music-note-half::before{content:"\F0389"}.mdi-music-note-half-dotted::before{content:"\F0F72"}.mdi-music-note-minus::before{content:"\F1B89"}.mdi-music-note-off::before{content:"\F038A"}.mdi-music-note-off-outline::before{content:"\F0F73"}.mdi-music-note-outline::before{content:"\F0F74"}.mdi-music-note-plus::before{content:"\F0DDE"}.mdi-music-note-quarter::before{content:"\F038B"}.mdi-music-note-quarter-dotted::before{content:"\F0F75"}.mdi-music-note-sixteenth::before{content:"\F038C"}.mdi-music-note-sixteenth-dotted::before{content:"\F0F76"}.mdi-music-note-whole::before{content:"\F038D"}.mdi-music-note-whole-dotted::before{content:"\F0F77"}.mdi-music-off::before{content:"\F075B"}.mdi-music-rest-eighth::before{content:"\F0F78"}.mdi-music-rest-half::before{content:"\F0F79"}.mdi-music-rest-quarter::before{content:"\F0F7A"}.mdi-music-rest-sixteenth::before{content:"\F0F7B"}.mdi-music-rest-whole::before{content:"\F0F7C"}.mdi-mustache::before{content:"\F15DE"}.mdi-nail::before{content:"\F0DDF"}.mdi-nas::before{content:"\F08F3"}.mdi-nativescript::before{content:"\F0880"}.mdi-nature::before{content:"\F038E"}.mdi-nature-outline::before{content:"\F1C71"}.mdi-nature-people::before{content:"\F038F"}.mdi-nature-people-outline::before{content:"\F1C72"}.mdi-navigation::before{content:"\F0390"}.mdi-navigation-outline::before{content:"\F1607"}.mdi-navigation-variant::before{content:"\F18F0"}.mdi-navigation-variant-outline::before{content:"\F18F1"}.mdi-near-me::before{content:"\F05CD"}.mdi-necklace::before{content:"\F0F0B"}.mdi-needle::before{content:"\F0391"}.mdi-needle-off::before{content:"\F19D2"}.mdi-netflix::before{content:"\F0746"}.mdi-network::before{content:"\F06F3"}.mdi-network-off::before{content:"\F0C9B"}.mdi-network-off-outline::before{content:"\F0C9C"}.mdi-network-outline::before{content:"\F0C9D"}.mdi-network-pos::before{content:"\F1ACB"}.mdi-network-strength-1::before{content:"\F08F4"}.mdi-network-strength-1-alert::before{content:"\F08F5"}.mdi-network-strength-2::before{content:"\F08F6"}.mdi-network-strength-2-alert::before{content:"\F08F7"}.mdi-network-strength-3::before{content:"\F08F8"}.mdi-network-strength-3-alert::before{content:"\F08F9"}.mdi-network-strength-4::before{content:"\F08FA"}.mdi-network-strength-4-alert::before{content:"\F08FB"}.mdi-network-strength-4-cog::before{content:"\F191A"}.mdi-network-strength-off::before{content:"\F08FC"}.mdi-network-strength-off-outline::before{content:"\F08FD"}.mdi-network-strength-outline::before{content:"\F08FE"}.mdi-new-box::before{content:"\F0394"}.mdi-newspaper::before{content:"\F0395"}.mdi-newspaper-check::before{content:"\F1943"}.mdi-newspaper-minus::before{content:"\F0F0C"}.mdi-newspaper-plus::before{content:"\F0F0D"}.mdi-newspaper-remove::before{content:"\F1944"}.mdi-newspaper-variant::before{content:"\F1001"}.mdi-newspaper-variant-multiple::before{content:"\F1002"}.mdi-newspaper-variant-multiple-outline::before{content:"\F1003"}.mdi-newspaper-variant-outline::before{content:"\F1004"}.mdi-nfc::before{content:"\F0396"}.mdi-nfc-search-variant::before{content:"\F0E53"}.mdi-nfc-tap::before{content:"\F0397"}.mdi-nfc-variant::before{content:"\F0398"}.mdi-nfc-variant-off::before{content:"\F0E54"}.mdi-ninja::before{content:"\F0774"}.mdi-nintendo-game-boy::before{content:"\F1393"}.mdi-nintendo-switch::before{content:"\F07E1"}.mdi-nintendo-wii::before{content:"\F05AB"}.mdi-nintendo-wiiu::before{content:"\F072D"}.mdi-nix::before{content:"\F1105"}.mdi-nodejs::before{content:"\F0399"}.mdi-noodles::before{content:"\F117E"}.mdi-not-equal::before{content:"\F098D"}.mdi-not-equal-variant::before{content:"\F098E"}.mdi-note::before{content:"\F039A"}.mdi-note-alert::before{content:"\F177D"}.mdi-note-alert-outline::before{content:"\F177E"}.mdi-note-check::before{content:"\F177F"}.mdi-note-check-outline::before{content:"\F1780"}.mdi-note-edit::before{content:"\F1781"}.mdi-note-edit-outline::before{content:"\F1782"}.mdi-note-minus::before{content:"\F164F"}.mdi-note-minus-outline::before{content:"\F1650"}.mdi-note-multiple::before{content:"\F06B8"}.mdi-note-multiple-outline::before{content:"\F06B9"}.mdi-note-off::before{content:"\F1783"}.mdi-note-off-outline::before{content:"\F1784"}.mdi-note-outline::before{content:"\F039B"}.mdi-note-plus::before{content:"\F039C"}.mdi-note-plus-outline::before{content:"\F039D"}.mdi-note-remove::before{content:"\F1651"}.mdi-note-remove-outline::before{content:"\F1652"}.mdi-note-search::before{content:"\F1653"}.mdi-note-search-outline::before{content:"\F1654"}.mdi-note-text::before{content:"\F039E"}.mdi-note-text-outline::before{content:"\F11D7"}.mdi-notebook::before{content:"\F082E"}.mdi-notebook-check::before{content:"\F14F5"}.mdi-notebook-check-outline::before{content:"\F14F6"}.mdi-notebook-edit::before{content:"\F14E7"}.mdi-notebook-edit-outline::before{content:"\F14E9"}.mdi-notebook-heart::before{content:"\F1A0B"}.mdi-notebook-heart-outline::before{content:"\F1A0C"}.mdi-notebook-minus::before{content:"\F1610"}.mdi-notebook-minus-outline::before{content:"\F1611"}.mdi-notebook-multiple::before{content:"\F0E55"}.mdi-notebook-outline::before{content:"\F0EBF"}.mdi-notebook-plus::before{content:"\F1612"}.mdi-notebook-plus-outline::before{content:"\F1613"}.mdi-notebook-remove::before{content:"\F1614"}.mdi-notebook-remove-outline::before{content:"\F1615"}.mdi-notification-clear-all::before{content:"\F039F"}.mdi-npm::before{content:"\F06F7"}.mdi-nuke::before{content:"\F06A4"}.mdi-null::before{content:"\F07E2"}.mdi-numeric::before{content:"\F03A0"}.mdi-numeric-0::before{content:"\F0B39"}.mdi-numeric-0-box::before{content:"\F03A1"}.mdi-numeric-0-box-multiple::before{content:"\F0F0E"}.mdi-numeric-0-box-multiple-outline::before{content:"\F03A2"}.mdi-numeric-0-box-outline::before{content:"\F03A3"}.mdi-numeric-0-circle::before{content:"\F0C9E"}.mdi-numeric-0-circle-outline::before{content:"\F0C9F"}.mdi-numeric-1::before{content:"\F0B3A"}.mdi-numeric-1-box::before{content:"\F03A4"}.mdi-numeric-1-box-multiple::before{content:"\F0F0F"}.mdi-numeric-1-box-multiple-outline::before{content:"\F03A5"}.mdi-numeric-1-box-outline::before{content:"\F03A6"}.mdi-numeric-1-circle::before{content:"\F0CA0"}.mdi-numeric-1-circle-outline::before{content:"\F0CA1"}.mdi-numeric-10::before{content:"\F0FE9"}.mdi-numeric-10-box::before{content:"\F0F7D"}.mdi-numeric-10-box-multiple::before{content:"\F0FEA"}.mdi-numeric-10-box-multiple-outline::before{content:"\F0FEB"}.mdi-numeric-10-box-outline::before{content:"\F0F7E"}.mdi-numeric-10-circle::before{content:"\F0FEC"}.mdi-numeric-10-circle-outline::before{content:"\F0FED"}.mdi-numeric-2::before{content:"\F0B3B"}.mdi-numeric-2-box::before{content:"\F03A7"}.mdi-numeric-2-box-multiple::before{content:"\F0F10"}.mdi-numeric-2-box-multiple-outline::before{content:"\F03A8"}.mdi-numeric-2-box-outline::before{content:"\F03A9"}.mdi-numeric-2-circle::before{content:"\F0CA2"}.mdi-numeric-2-circle-outline::before{content:"\F0CA3"}.mdi-numeric-3::before{content:"\F0B3C"}.mdi-numeric-3-box::before{content:"\F03AA"}.mdi-numeric-3-box-multiple::before{content:"\F0F11"}.mdi-numeric-3-box-multiple-outline::before{content:"\F03AB"}.mdi-numeric-3-box-outline::before{content:"\F03AC"}.mdi-numeric-3-circle::before{content:"\F0CA4"}.mdi-numeric-3-circle-outline::before{content:"\F0CA5"}.mdi-numeric-4::before{content:"\F0B3D"}.mdi-numeric-4-box::before{content:"\F03AD"}.mdi-numeric-4-box-multiple::before{content:"\F0F12"}.mdi-numeric-4-box-multiple-outline::before{content:"\F03B2"}.mdi-numeric-4-box-outline::before{content:"\F03AE"}.mdi-numeric-4-circle::before{content:"\F0CA6"}.mdi-numeric-4-circle-outline::before{content:"\F0CA7"}.mdi-numeric-5::before{content:"\F0B3E"}.mdi-numeric-5-box::before{content:"\F03B1"}.mdi-numeric-5-box-multiple::before{content:"\F0F13"}.mdi-numeric-5-box-multiple-outline::before{content:"\F03AF"}.mdi-numeric-5-box-outline::before{content:"\F03B0"}.mdi-numeric-5-circle::before{content:"\F0CA8"}.mdi-numeric-5-circle-outline::before{content:"\F0CA9"}.mdi-numeric-6::before{content:"\F0B3F"}.mdi-numeric-6-box::before{content:"\F03B3"}.mdi-numeric-6-box-multiple::before{content:"\F0F14"}.mdi-numeric-6-box-multiple-outline::before{content:"\F03B4"}.mdi-numeric-6-box-outline::before{content:"\F03B5"}.mdi-numeric-6-circle::before{content:"\F0CAA"}.mdi-numeric-6-circle-outline::before{content:"\F0CAB"}.mdi-numeric-7::before{content:"\F0B40"}.mdi-numeric-7-box::before{content:"\F03B6"}.mdi-numeric-7-box-multiple::before{content:"\F0F15"}.mdi-numeric-7-box-multiple-outline::before{content:"\F03B7"}.mdi-numeric-7-box-outline::before{content:"\F03B8"}.mdi-numeric-7-circle::before{content:"\F0CAC"}.mdi-numeric-7-circle-outline::before{content:"\F0CAD"}.mdi-numeric-8::before{content:"\F0B41"}.mdi-numeric-8-box::before{content:"\F03B9"}.mdi-numeric-8-box-multiple::before{content:"\F0F16"}.mdi-numeric-8-box-multiple-outline::before{content:"\F03BA"}.mdi-numeric-8-box-outline::before{content:"\F03BB"}.mdi-numeric-8-circle::before{content:"\F0CAE"}.mdi-numeric-8-circle-outline::before{content:"\F0CAF"}.mdi-numeric-9::before{content:"\F0B42"}.mdi-numeric-9-box::before{content:"\F03BC"}.mdi-numeric-9-box-multiple::before{content:"\F0F17"}.mdi-numeric-9-box-multiple-outline::before{content:"\F03BD"}.mdi-numeric-9-box-outline::before{content:"\F03BE"}.mdi-numeric-9-circle::before{content:"\F0CB0"}.mdi-numeric-9-circle-outline::before{content:"\F0CB1"}.mdi-numeric-9-plus::before{content:"\F0FEE"}.mdi-numeric-9-plus-box::before{content:"\F03BF"}.mdi-numeric-9-plus-box-multiple::before{content:"\F0F18"}.mdi-numeric-9-plus-box-multiple-outline::before{content:"\F03C0"}.mdi-numeric-9-plus-box-outline::before{content:"\F03C1"}.mdi-numeric-9-plus-circle::before{content:"\F0CB2"}.mdi-numeric-9-plus-circle-outline::before{content:"\F0CB3"}.mdi-numeric-negative-1::before{content:"\F1052"}.mdi-numeric-off::before{content:"\F19D3"}.mdi-numeric-positive-1::before{content:"\F15CB"}.mdi-nut::before{content:"\F06F8"}.mdi-nutrition::before{content:"\F03C2"}.mdi-nuxt::before{content:"\F1106"}.mdi-oar::before{content:"\F067C"}.mdi-ocarina::before{content:"\F0DE0"}.mdi-oci::before{content:"\F12E9"}.mdi-ocr::before{content:"\F113A"}.mdi-octagon::before{content:"\F03C3"}.mdi-octagon-outline::before{content:"\F03C4"}.mdi-octagram::before{content:"\F06F9"}.mdi-octagram-edit::before{content:"\F1C34"}.mdi-octagram-edit-outline::before{content:"\F1C35"}.mdi-octagram-minus::before{content:"\F1C36"}.mdi-octagram-minus-outline::before{content:"\F1C37"}.mdi-octagram-outline::before{content:"\F0775"}.mdi-octagram-plus::before{content:"\F1C38"}.mdi-octagram-plus-outline::before{content:"\F1C39"}.mdi-octahedron::before{content:"\F1950"}.mdi-octahedron-off::before{content:"\F1951"}.mdi-odnoklassniki::before{content:"\F03C5"}.mdi-offer::before{content:"\F121B"}.mdi-office-building::before{content:"\F0991"}.mdi-office-building-cog::before{content:"\F1949"}.mdi-office-building-cog-outline::before{content:"\F194A"}.mdi-office-building-marker::before{content:"\F1520"}.mdi-office-building-marker-outline::before{content:"\F1521"}.mdi-office-building-minus::before{content:"\F1BAA"}.mdi-office-building-minus-outline::before{content:"\F1BAB"}.mdi-office-building-outline::before{content:"\F151F"}.mdi-office-building-plus::before{content:"\F1BA8"}.mdi-office-building-plus-outline::before{content:"\F1BA9"}.mdi-office-building-remove::before{content:"\F1BAC"}.mdi-office-building-remove-outline::before{content:"\F1BAD"}.mdi-oil::before{content:"\F03C7"}.mdi-oil-lamp::before{content:"\F0F19"}.mdi-oil-level::before{content:"\F1053"}.mdi-oil-temperature::before{content:"\F0FF8"}.mdi-om::before{content:"\F0973"}.mdi-omega::before{content:"\F03C9"}.mdi-one-up::before{content:"\F0BAD"}.mdi-onepassword::before{content:"\F0881"}.mdi-opacity::before{content:"\F05CC"}.mdi-open-in-app::before{content:"\F03CB"}.mdi-open-in-new::before{content:"\F03CC"}.mdi-open-source-initiative::before{content:"\F0BAE"}.mdi-openid::before{content:"\F03CD"}.mdi-opera::before{content:"\F03CE"}.mdi-orbit::before{content:"\F0018"}.mdi-orbit-variant::before{content:"\F15DB"}.mdi-order-alphabetical-ascending::before{content:"\F020D"}.mdi-order-alphabetical-descending::before{content:"\F0D07"}.mdi-order-bool-ascending::before{content:"\F02BE"}.mdi-order-bool-ascending-variant::before{content:"\F098F"}.mdi-order-bool-descending::before{content:"\F1384"}.mdi-order-bool-descending-variant::before{content:"\F0990"}.mdi-order-numeric-ascending::before{content:"\F0545"}.mdi-order-numeric-descending::before{content:"\F0546"}.mdi-origin::before{content:"\F0B43"}.mdi-ornament::before{content:"\F03CF"}.mdi-ornament-variant::before{content:"\F03D0"}.mdi-outdoor-lamp::before{content:"\F1054"}.mdi-overscan::before{content:"\F1005"}.mdi-owl::before{content:"\F03D2"}.mdi-pac-man::before{content:"\F0BAF"}.mdi-package::before{content:"\F03D3"}.mdi-package-check::before{content:"\F1B51"}.mdi-package-down::before{content:"\F03D4"}.mdi-package-up::before{content:"\F03D5"}.mdi-package-variant::before{content:"\F03D6"}.mdi-package-variant-closed::before{content:"\F03D7"}.mdi-package-variant-closed-check::before{content:"\F1B52"}.mdi-package-variant-closed-minus::before{content:"\F19D4"}.mdi-package-variant-closed-plus::before{content:"\F19D5"}.mdi-package-variant-closed-remove::before{content:"\F19D6"}.mdi-package-variant-minus::before{content:"\F19D7"}.mdi-package-variant-plus::before{content:"\F19D8"}.mdi-package-variant-remove::before{content:"\F19D9"}.mdi-page-first::before{content:"\F0600"}.mdi-page-last::before{content:"\F0601"}.mdi-page-layout-body::before{content:"\F06FA"}.mdi-page-layout-footer::before{content:"\F06FB"}.mdi-page-layout-header::before{content:"\F06FC"}.mdi-page-layout-header-footer::before{content:"\F0F7F"}.mdi-page-layout-sidebar-left::before{content:"\F06FD"}.mdi-page-layout-sidebar-right::before{content:"\F06FE"}.mdi-page-next::before{content:"\F0BB0"}.mdi-page-next-outline::before{content:"\F0BB1"}.mdi-page-previous::before{content:"\F0BB2"}.mdi-page-previous-outline::before{content:"\F0BB3"}.mdi-pail::before{content:"\F1417"}.mdi-pail-minus::before{content:"\F1437"}.mdi-pail-minus-outline::before{content:"\F143C"}.mdi-pail-off::before{content:"\F1439"}.mdi-pail-off-outline::before{content:"\F143E"}.mdi-pail-outline::before{content:"\F143A"}.mdi-pail-plus::before{content:"\F1436"}.mdi-pail-plus-outline::before{content:"\F143B"}.mdi-pail-remove::before{content:"\F1438"}.mdi-pail-remove-outline::before{content:"\F143D"}.mdi-palette::before{content:"\F03D8"}.mdi-palette-advanced::before{content:"\F03D9"}.mdi-palette-outline::before{content:"\F0E0C"}.mdi-palette-swatch::before{content:"\F08B5"}.mdi-palette-swatch-outline::before{content:"\F135C"}.mdi-palette-swatch-variant::before{content:"\F195A"}.mdi-palm-tree::before{content:"\F1055"}.mdi-pan::before{content:"\F0BB4"}.mdi-pan-bottom-left::before{content:"\F0BB5"}.mdi-pan-bottom-right::before{content:"\F0BB6"}.mdi-pan-down::before{content:"\F0BB7"}.mdi-pan-horizontal::before{content:"\F0BB8"}.mdi-pan-left::before{content:"\F0BB9"}.mdi-pan-right::before{content:"\F0BBA"}.mdi-pan-top-left::before{content:"\F0BBB"}.mdi-pan-top-right::before{content:"\F0BBC"}.mdi-pan-up::before{content:"\F0BBD"}.mdi-pan-vertical::before{content:"\F0BBE"}.mdi-panda::before{content:"\F03DA"}.mdi-pandora::before{content:"\F03DB"}.mdi-panorama::before{content:"\F03DC"}.mdi-panorama-fisheye::before{content:"\F03DD"}.mdi-panorama-horizontal::before{content:"\F1928"}.mdi-panorama-horizontal-outline::before{content:"\F03DE"}.mdi-panorama-outline::before{content:"\F198C"}.mdi-panorama-sphere::before{content:"\F198D"}.mdi-panorama-sphere-outline::before{content:"\F198E"}.mdi-panorama-variant::before{content:"\F198F"}.mdi-panorama-variant-outline::before{content:"\F1990"}.mdi-panorama-vertical::before{content:"\F1929"}.mdi-panorama-vertical-outline::before{content:"\F03DF"}.mdi-panorama-wide-angle::before{content:"\F195F"}.mdi-panorama-wide-angle-outline::before{content:"\F03E0"}.mdi-paper-cut-vertical::before{content:"\F03E1"}.mdi-paper-roll::before{content:"\F1157"}.mdi-paper-roll-outline::before{content:"\F1158"}.mdi-paperclip::before{content:"\F03E2"}.mdi-paperclip-check::before{content:"\F1AC6"}.mdi-paperclip-lock::before{content:"\F19DA"}.mdi-paperclip-minus::before{content:"\F1AC7"}.mdi-paperclip-off::before{content:"\F1AC8"}.mdi-paperclip-plus::before{content:"\F1AC9"}.mdi-paperclip-remove::before{content:"\F1ACA"}.mdi-parachute::before{content:"\F0CB4"}.mdi-parachute-outline::before{content:"\F0CB5"}.mdi-paragliding::before{content:"\F1745"}.mdi-parking::before{content:"\F03E3"}.mdi-party-popper::before{content:"\F1056"}.mdi-passport::before{content:"\F07E3"}.mdi-passport-alert::before{content:"\F1CB8"}.mdi-passport-biometric::before{content:"\F0DE1"}.mdi-passport-cancel::before{content:"\F1CB9"}.mdi-passport-check::before{content:"\F1CBA"}.mdi-passport-minus::before{content:"\F1CBB"}.mdi-passport-plus::before{content:"\F1CBC"}.mdi-passport-remove::before{content:"\F1CBD"}.mdi-pasta::before{content:"\F1160"}.mdi-patio-heater::before{content:"\F0F80"}.mdi-patreon::before{content:"\F0882"}.mdi-pause::before{content:"\F03E4"}.mdi-pause-box::before{content:"\F00BC"}.mdi-pause-box-outline::before{content:"\F1B7A"}.mdi-pause-circle::before{content:"\F03E5"}.mdi-pause-circle-outline::before{content:"\F03E6"}.mdi-pause-octagon::before{content:"\F03E7"}.mdi-pause-octagon-outline::before{content:"\F03E8"}.mdi-paw::before{content:"\F03E9"}.mdi-paw-off::before{content:"\F0657"}.mdi-paw-off-outline::before{content:"\F1676"}.mdi-paw-outline::before{content:"\F1675"}.mdi-peace::before{content:"\F0884"}.mdi-peanut::before{content:"\F0FFC"}.mdi-peanut-off::before{content:"\F0FFD"}.mdi-peanut-off-outline::before{content:"\F0FFF"}.mdi-peanut-outline::before{content:"\F0FFE"}.mdi-pen::before{content:"\F03EA"}.mdi-pen-lock::before{content:"\F0DE2"}.mdi-pen-minus::before{content:"\F0DE3"}.mdi-pen-off::before{content:"\F0DE4"}.mdi-pen-plus::before{content:"\F0DE5"}.mdi-pen-remove::before{content:"\F0DE6"}.mdi-pencil::before{content:"\F03EB"}.mdi-pencil-box::before{content:"\F03EC"}.mdi-pencil-box-multiple::before{content:"\F1144"}.mdi-pencil-box-multiple-outline::before{content:"\F1145"}.mdi-pencil-box-outline::before{content:"\F03ED"}.mdi-pencil-circle::before{content:"\F06FF"}.mdi-pencil-circle-outline::before{content:"\F0776"}.mdi-pencil-lock::before{content:"\F03EE"}.mdi-pencil-lock-outline::before{content:"\F0DE7"}.mdi-pencil-minus::before{content:"\F0DE8"}.mdi-pencil-minus-outline::before{content:"\F0DE9"}.mdi-pencil-off::before{content:"\F03EF"}.mdi-pencil-off-outline::before{content:"\F0DEA"}.mdi-pencil-outline::before{content:"\F0CB6"}.mdi-pencil-plus::before{content:"\F0DEB"}.mdi-pencil-plus-outline::before{content:"\F0DEC"}.mdi-pencil-remove::before{content:"\F0DED"}.mdi-pencil-remove-outline::before{content:"\F0DEE"}.mdi-pencil-ruler::before{content:"\F1353"}.mdi-pencil-ruler-outline::before{content:"\F1C11"}.mdi-penguin::before{content:"\F0EC0"}.mdi-pentagon::before{content:"\F0701"}.mdi-pentagon-outline::before{content:"\F0700"}.mdi-pentagram::before{content:"\F1667"}.mdi-percent::before{content:"\F03F0"}.mdi-percent-box::before{content:"\F1A02"}.mdi-percent-box-outline::before{content:"\F1A03"}.mdi-percent-circle::before{content:"\F1A04"}.mdi-percent-circle-outline::before{content:"\F1A05"}.mdi-percent-outline::before{content:"\F1278"}.mdi-periodic-table::before{content:"\F08B6"}.mdi-perspective-less::before{content:"\F0D23"}.mdi-perspective-more::before{content:"\F0D24"}.mdi-ph::before{content:"\F17C5"}.mdi-phone::before{content:"\F03F2"}.mdi-phone-alert::before{content:"\F0F1A"}.mdi-phone-alert-outline::before{content:"\F118E"}.mdi-phone-bluetooth::before{content:"\F03F3"}.mdi-phone-bluetooth-outline::before{content:"\F118F"}.mdi-phone-cancel::before{content:"\F10BC"}.mdi-phone-cancel-outline::before{content:"\F1190"}.mdi-phone-check::before{content:"\F11A9"}.mdi-phone-check-outline::before{content:"\F11AA"}.mdi-phone-classic::before{content:"\F0602"}.mdi-phone-classic-off::before{content:"\F1279"}.mdi-phone-clock::before{content:"\F19DB"}.mdi-phone-dial::before{content:"\F1559"}.mdi-phone-dial-outline::before{content:"\F155A"}.mdi-phone-forward::before{content:"\F03F4"}.mdi-phone-forward-outline::before{content:"\F1191"}.mdi-phone-hangup::before{content:"\F03F5"}.mdi-phone-hangup-outline::before{content:"\F1192"}.mdi-phone-in-talk::before{content:"\F03F6"}.mdi-phone-in-talk-outline::before{content:"\F1182"}.mdi-phone-incoming::before{content:"\F03F7"}.mdi-phone-incoming-outgoing::before{content:"\F1B3F"}.mdi-phone-incoming-outgoing-outline::before{content:"\F1B40"}.mdi-phone-incoming-outline::before{content:"\F1193"}.mdi-phone-lock::before{content:"\F03F8"}.mdi-phone-lock-outline::before{content:"\F1194"}.mdi-phone-log::before{content:"\F03F9"}.mdi-phone-log-outline::before{content:"\F1195"}.mdi-phone-message::before{content:"\F1196"}.mdi-phone-message-outline::before{content:"\F1197"}.mdi-phone-minus::before{content:"\F0658"}.mdi-phone-minus-outline::before{content:"\F1198"}.mdi-phone-missed::before{content:"\F03FA"}.mdi-phone-missed-outline::before{content:"\F11A5"}.mdi-phone-off::before{content:"\F0DEF"}.mdi-phone-off-outline::before{content:"\F11A6"}.mdi-phone-outgoing::before{content:"\F03FB"}.mdi-phone-outgoing-outline::before{content:"\F1199"}.mdi-phone-outline::before{content:"\F0DF0"}.mdi-phone-paused::before{content:"\F03FC"}.mdi-phone-paused-outline::before{content:"\F119A"}.mdi-phone-plus::before{content:"\F0659"}.mdi-phone-plus-outline::before{content:"\F119B"}.mdi-phone-refresh::before{content:"\F1993"}.mdi-phone-refresh-outline::before{content:"\F1994"}.mdi-phone-remove::before{content:"\F152F"}.mdi-phone-remove-outline::before{content:"\F1530"}.mdi-phone-return::before{content:"\F082F"}.mdi-phone-return-outline::before{content:"\F119C"}.mdi-phone-ring::before{content:"\F11AB"}.mdi-phone-ring-outline::before{content:"\F11AC"}.mdi-phone-rotate-landscape::before{content:"\F0885"}.mdi-phone-rotate-portrait::before{content:"\F0886"}.mdi-phone-settings::before{content:"\F03FD"}.mdi-phone-settings-outline::before{content:"\F119D"}.mdi-phone-sync::before{content:"\F1995"}.mdi-phone-sync-outline::before{content:"\F1996"}.mdi-phone-voip::before{content:"\F03FE"}.mdi-pi::before{content:"\F03FF"}.mdi-pi-box::before{content:"\F0400"}.mdi-pi-hole::before{content:"\F0DF1"}.mdi-piano::before{content:"\F067D"}.mdi-piano-off::before{content:"\F0698"}.mdi-pickaxe::before{content:"\F08B7"}.mdi-picture-in-picture-bottom-right::before{content:"\F0E57"}.mdi-picture-in-picture-bottom-right-outline::before{content:"\F0E58"}.mdi-picture-in-picture-top-right::before{content:"\F0E59"}.mdi-picture-in-picture-top-right-outline::before{content:"\F0E5A"}.mdi-pier::before{content:"\F0887"}.mdi-pier-crane::before{content:"\F0888"}.mdi-pig::before{content:"\F0401"}.mdi-pig-variant::before{content:"\F1006"}.mdi-pig-variant-outline::before{content:"\F1678"}.mdi-piggy-bank::before{content:"\F1007"}.mdi-piggy-bank-outline::before{content:"\F1679"}.mdi-pill::before{content:"\F0402"}.mdi-pill-multiple::before{content:"\F1B4C"}.mdi-pill-off::before{content:"\F1A5C"}.mdi-pillar::before{content:"\F0702"}.mdi-pin::before{content:"\F0403"}.mdi-pin-off::before{content:"\F0404"}.mdi-pin-off-outline::before{content:"\F0930"}.mdi-pin-outline::before{content:"\F0931"}.mdi-pine-tree::before{content:"\F0405"}.mdi-pine-tree-box::before{content:"\F0406"}.mdi-pine-tree-fire::before{content:"\F141A"}.mdi-pine-tree-variant::before{content:"\F1C73"}.mdi-pine-tree-variant-outline::before{content:"\F1C74"}.mdi-pinterest::before{content:"\F0407"}.mdi-pinwheel::before{content:"\F0AD5"}.mdi-pinwheel-outline::before{content:"\F0AD6"}.mdi-pipe::before{content:"\F07E5"}.mdi-pipe-disconnected::before{content:"\F07E6"}.mdi-pipe-leak::before{content:"\F0889"}.mdi-pipe-valve::before{content:"\F184D"}.mdi-pipe-wrench::before{content:"\F1354"}.mdi-pirate::before{content:"\F0A08"}.mdi-pistol::before{content:"\F0703"}.mdi-piston::before{content:"\F088A"}.mdi-pitchfork::before{content:"\F1553"}.mdi-pizza::before{content:"\F0409"}.mdi-plane-car::before{content:"\F1AFF"}.mdi-plane-train::before{content:"\F1B00"}.mdi-play::before{content:"\F040A"}.mdi-play-box::before{content:"\F127A"}.mdi-play-box-edit-outline::before{content:"\F1C3A"}.mdi-play-box-lock::before{content:"\F1A16"}.mdi-play-box-lock-open::before{content:"\F1A17"}.mdi-play-box-lock-open-outline::before{content:"\F1A18"}.mdi-play-box-lock-outline::before{content:"\F1A19"}.mdi-play-box-multiple::before{content:"\F0D19"}.mdi-play-box-multiple-outline::before{content:"\F13E6"}.mdi-play-box-outline::before{content:"\F040B"}.mdi-play-circle::before{content:"\F040C"}.mdi-play-circle-outline::before{content:"\F040D"}.mdi-play-network::before{content:"\F088B"}.mdi-play-network-outline::before{content:"\F0CB7"}.mdi-play-outline::before{content:"\F0F1B"}.mdi-play-pause::before{content:"\F040E"}.mdi-play-protected-content::before{content:"\F040F"}.mdi-play-speed::before{content:"\F08FF"}.mdi-playlist-check::before{content:"\F05C7"}.mdi-playlist-edit::before{content:"\F0900"}.mdi-playlist-minus::before{content:"\F0410"}.mdi-playlist-music::before{content:"\F0CB8"}.mdi-playlist-music-outline::before{content:"\F0CB9"}.mdi-playlist-play::before{content:"\F0411"}.mdi-playlist-plus::before{content:"\F0412"}.mdi-playlist-remove::before{content:"\F0413"}.mdi-playlist-star::before{content:"\F0DF2"}.mdi-plex::before{content:"\F06BA"}.mdi-pliers::before{content:"\F19A4"}.mdi-plus::before{content:"\F0415"}.mdi-plus-box::before{content:"\F0416"}.mdi-plus-box-multiple::before{content:"\F0334"}.mdi-plus-box-multiple-outline::before{content:"\F1143"}.mdi-plus-box-outline::before{content:"\F0704"}.mdi-plus-circle::before{content:"\F0417"}.mdi-plus-circle-multiple::before{content:"\F034C"}.mdi-plus-circle-multiple-outline::before{content:"\F0418"}.mdi-plus-circle-outline::before{content:"\F0419"}.mdi-plus-lock::before{content:"\F1A5D"}.mdi-plus-lock-open::before{content:"\F1A5E"}.mdi-plus-minus::before{content:"\F0992"}.mdi-plus-minus-box::before{content:"\F0993"}.mdi-plus-minus-variant::before{content:"\F14C9"}.mdi-plus-network::before{content:"\F041A"}.mdi-plus-network-outline::before{content:"\F0CBA"}.mdi-plus-outline::before{content:"\F0705"}.mdi-plus-thick::before{content:"\F11EC"}.mdi-pocket::before{content:"\F1CBE"}.mdi-podcast::before{content:"\F0994"}.mdi-podium::before{content:"\F0D25"}.mdi-podium-bronze::before{content:"\F0D26"}.mdi-podium-gold::before{content:"\F0D27"}.mdi-podium-silver::before{content:"\F0D28"}.mdi-point-of-sale::before{content:"\F0D92"}.mdi-pokeball::before{content:"\F041D"}.mdi-pokemon-go::before{content:"\F0A09"}.mdi-poker-chip::before{content:"\F0830"}.mdi-polaroid::before{content:"\F041E"}.mdi-police-badge::before{content:"\F1167"}.mdi-police-badge-outline::before{content:"\F1168"}.mdi-police-station::before{content:"\F1839"}.mdi-poll::before{content:"\F041F"}.mdi-polo::before{content:"\F14C3"}.mdi-polymer::before{content:"\F0421"}.mdi-pool::before{content:"\F0606"}.mdi-pool-thermometer::before{content:"\F1A5F"}.mdi-popcorn::before{content:"\F0422"}.mdi-post::before{content:"\F1008"}.mdi-post-lamp::before{content:"\F1A60"}.mdi-post-outline::before{content:"\F1009"}.mdi-postage-stamp::before{content:"\F0CBB"}.mdi-pot::before{content:"\F02E5"}.mdi-pot-mix::before{content:"\F065B"}.mdi-pot-mix-outline::before{content:"\F0677"}.mdi-pot-outline::before{content:"\F02FF"}.mdi-pot-steam::before{content:"\F065A"}.mdi-pot-steam-outline::before{content:"\F0326"}.mdi-pound::before{content:"\F0423"}.mdi-pound-box::before{content:"\F0424"}.mdi-pound-box-outline::before{content:"\F117F"}.mdi-power::before{content:"\F0425"}.mdi-power-cycle::before{content:"\F0901"}.mdi-power-off::before{content:"\F0902"}.mdi-power-on::before{content:"\F0903"}.mdi-power-plug::before{content:"\F06A5"}.mdi-power-plug-battery::before{content:"\F1C3B"}.mdi-power-plug-battery-outline::before{content:"\F1C3C"}.mdi-power-plug-off::before{content:"\F06A6"}.mdi-power-plug-off-outline::before{content:"\F1424"}.mdi-power-plug-outline::before{content:"\F1425"}.mdi-power-settings::before{content:"\F0426"}.mdi-power-sleep::before{content:"\F0904"}.mdi-power-socket::before{content:"\F0427"}.mdi-power-socket-au::before{content:"\F0905"}.mdi-power-socket-ch::before{content:"\F0FB3"}.mdi-power-socket-de::before{content:"\F1107"}.mdi-power-socket-eu::before{content:"\F07E7"}.mdi-power-socket-fr::before{content:"\F1108"}.mdi-power-socket-it::before{content:"\F14FF"}.mdi-power-socket-jp::before{content:"\F1109"}.mdi-power-socket-uk::before{content:"\F07E8"}.mdi-power-socket-us::before{content:"\F07E9"}.mdi-power-standby::before{content:"\F0906"}.mdi-powershell::before{content:"\F0A0A"}.mdi-prescription::before{content:"\F0706"}.mdi-presentation::before{content:"\F0428"}.mdi-presentation-play::before{content:"\F0429"}.mdi-pretzel::before{content:"\F1562"}.mdi-printer::before{content:"\F042A"}.mdi-printer-3d::before{content:"\F042B"}.mdi-printer-3d-nozzle::before{content:"\F0E5B"}.mdi-printer-3d-nozzle-alert::before{content:"\F11C0"}.mdi-printer-3d-nozzle-alert-outline::before{content:"\F11C1"}.mdi-printer-3d-nozzle-heat::before{content:"\F18B8"}.mdi-printer-3d-nozzle-heat-outline::before{content:"\F18B9"}.mdi-printer-3d-nozzle-off::before{content:"\F1B19"}.mdi-printer-3d-nozzle-off-outline::before{content:"\F1B1A"}.mdi-printer-3d-nozzle-outline::before{content:"\F0E5C"}.mdi-printer-3d-off::before{content:"\F1B0E"}.mdi-printer-alert::before{content:"\F042C"}.mdi-printer-check::before{content:"\F1146"}.mdi-printer-eye::before{content:"\F1458"}.mdi-printer-off::before{content:"\F0E5D"}.mdi-printer-off-outline::before{content:"\F1785"}.mdi-printer-outline::before{content:"\F1786"}.mdi-printer-pos::before{content:"\F1057"}.mdi-printer-pos-alert::before{content:"\F1BBC"}.mdi-printer-pos-alert-outline::before{content:"\F1BBD"}.mdi-printer-pos-cancel::before{content:"\F1BBE"}.mdi-printer-pos-cancel-outline::before{content:"\F1BBF"}.mdi-printer-pos-check::before{content:"\F1BC0"}.mdi-printer-pos-check-outline::before{content:"\F1BC1"}.mdi-printer-pos-cog::before{content:"\F1BC2"}.mdi-printer-pos-cog-outline::before{content:"\F1BC3"}.mdi-printer-pos-edit::before{content:"\F1BC4"}.mdi-printer-pos-edit-outline::before{content:"\F1BC5"}.mdi-printer-pos-minus::before{content:"\F1BC6"}.mdi-printer-pos-minus-outline::before{content:"\F1BC7"}.mdi-printer-pos-network::before{content:"\F1BC8"}.mdi-printer-pos-network-outline::before{content:"\F1BC9"}.mdi-printer-pos-off::before{content:"\F1BCA"}.mdi-printer-pos-off-outline::before{content:"\F1BCB"}.mdi-printer-pos-outline::before{content:"\F1BCC"}.mdi-printer-pos-pause::before{content:"\F1BCD"}.mdi-printer-pos-pause-outline::before{content:"\F1BCE"}.mdi-printer-pos-play::before{content:"\F1BCF"}.mdi-printer-pos-play-outline::before{content:"\F1BD0"}.mdi-printer-pos-plus::before{content:"\F1BD1"}.mdi-printer-pos-plus-outline::before{content:"\F1BD2"}.mdi-printer-pos-refresh::before{content:"\F1BD3"}.mdi-printer-pos-refresh-outline::before{content:"\F1BD4"}.mdi-printer-pos-remove::before{content:"\F1BD5"}.mdi-printer-pos-remove-outline::before{content:"\F1BD6"}.mdi-printer-pos-star::before{content:"\F1BD7"}.mdi-printer-pos-star-outline::before{content:"\F1BD8"}.mdi-printer-pos-stop::before{content:"\F1BD9"}.mdi-printer-pos-stop-outline::before{content:"\F1BDA"}.mdi-printer-pos-sync::before{content:"\F1BDB"}.mdi-printer-pos-sync-outline::before{content:"\F1BDC"}.mdi-printer-pos-wrench::before{content:"\F1BDD"}.mdi-printer-pos-wrench-outline::before{content:"\F1BDE"}.mdi-printer-search::before{content:"\F1457"}.mdi-printer-settings::before{content:"\F0707"}.mdi-printer-wireless::before{content:"\F0A0B"}.mdi-priority-high::before{content:"\F0603"}.mdi-priority-low::before{content:"\F0604"}.mdi-professional-hexagon::before{content:"\F042D"}.mdi-progress-alert::before{content:"\F0CBC"}.mdi-progress-check::before{content:"\F0995"}.mdi-progress-clock::before{content:"\F0996"}.mdi-progress-close::before{content:"\F110A"}.mdi-progress-download::before{content:"\F0997"}.mdi-progress-helper::before{content:"\F1BA2"}.mdi-progress-pencil::before{content:"\F1787"}.mdi-progress-question::before{content:"\F1522"}.mdi-progress-star::before{content:"\F1788"}.mdi-progress-star-four-points::before{content:"\F1C3D"}.mdi-progress-tag::before{content:"\F1D0D"}.mdi-progress-upload::before{content:"\F0998"}.mdi-progress-wrench::before{content:"\F0CBD"}.mdi-projector::before{content:"\F042E"}.mdi-projector-off::before{content:"\F1A23"}.mdi-projector-screen::before{content:"\F042F"}.mdi-projector-screen-off::before{content:"\F180D"}.mdi-projector-screen-off-outline::before{content:"\F180E"}.mdi-projector-screen-outline::before{content:"\F1724"}.mdi-projector-screen-variant::before{content:"\F180F"}.mdi-projector-screen-variant-off::before{content:"\F1810"}.mdi-projector-screen-variant-off-outline::before{content:"\F1811"}.mdi-projector-screen-variant-outline::before{content:"\F1812"}.mdi-propane-tank::before{content:"\F1357"}.mdi-propane-tank-outline::before{content:"\F1358"}.mdi-protocol::before{content:"\F0FD8"}.mdi-publish::before{content:"\F06A7"}.mdi-publish-off::before{content:"\F1945"}.mdi-pulse::before{content:"\F0430"}.mdi-pump::before{content:"\F1402"}.mdi-pump-off::before{content:"\F1B22"}.mdi-pumpkin::before{content:"\F0BBF"}.mdi-purse::before{content:"\F0F1C"}.mdi-purse-outline::before{content:"\F0F1D"}.mdi-puzzle::before{content:"\F0431"}.mdi-puzzle-check::before{content:"\F1426"}.mdi-puzzle-check-outline::before{content:"\F1427"}.mdi-puzzle-edit::before{content:"\F14D3"}.mdi-puzzle-edit-outline::before{content:"\F14D9"}.mdi-puzzle-heart::before{content:"\F14D4"}.mdi-puzzle-heart-outline::before{content:"\F14DA"}.mdi-puzzle-minus::before{content:"\F14D1"}.mdi-puzzle-minus-outline::before{content:"\F14D7"}.mdi-puzzle-outline::before{content:"\F0A66"}.mdi-puzzle-plus::before{content:"\F14D0"}.mdi-puzzle-plus-outline::before{content:"\F14D6"}.mdi-puzzle-remove::before{content:"\F14D2"}.mdi-puzzle-remove-outline::before{content:"\F14D8"}.mdi-puzzle-star::before{content:"\F14D5"}.mdi-puzzle-star-outline::before{content:"\F14DB"}.mdi-pyramid::before{content:"\F1952"}.mdi-pyramid-off::before{content:"\F1953"}.mdi-qi::before{content:"\F0999"}.mdi-qqchat::before{content:"\F0605"}.mdi-qrcode::before{content:"\F0432"}.mdi-qrcode-edit::before{content:"\F08B8"}.mdi-qrcode-minus::before{content:"\F118C"}.mdi-qrcode-plus::before{content:"\F118B"}.mdi-qrcode-remove::before{content:"\F118D"}.mdi-qrcode-scan::before{content:"\F0433"}.mdi-quadcopter::before{content:"\F0434"}.mdi-quality-high::before{content:"\F0435"}.mdi-quality-low::before{content:"\F0A0C"}.mdi-quality-medium::before{content:"\F0A0D"}.mdi-queue-first-in-last-out::before{content:"\F1CAF"}.mdi-quora::before{content:"\F0D29"}.mdi-rabbit::before{content:"\F0907"}.mdi-rabbit-variant::before{content:"\F1A61"}.mdi-rabbit-variant-outline::before{content:"\F1A62"}.mdi-racing-helmet::before{content:"\F0D93"}.mdi-racquetball::before{content:"\F0D94"}.mdi-radar::before{content:"\F0437"}.mdi-radiator::before{content:"\F0438"}.mdi-radiator-disabled::before{content:"\F0AD7"}.mdi-radiator-off::before{content:"\F0AD8"}.mdi-radio::before{content:"\F0439"}.mdi-radio-am::before{content:"\F0CBE"}.mdi-radio-fm::before{content:"\F0CBF"}.mdi-radio-handheld::before{content:"\F043A"}.mdi-radio-off::before{content:"\F121C"}.mdi-radio-tower::before{content:"\F043B"}.mdi-radioactive::before{content:"\F043C"}.mdi-radioactive-circle::before{content:"\F185D"}.mdi-radioactive-circle-outline::before{content:"\F185E"}.mdi-radioactive-off::before{content:"\F0EC1"}.mdi-radiobox-blank::before{content:"\F043D"}.mdi-radiobox-indeterminate-variant::before{content:"\F1C5E"}.mdi-radiobox-marked::before{content:"\F043E"}.mdi-radiology-box::before{content:"\F14C5"}.mdi-radiology-box-outline::before{content:"\F14C6"}.mdi-radius::before{content:"\F0CC0"}.mdi-radius-outline::before{content:"\F0CC1"}.mdi-railroad-light::before{content:"\F0F1E"}.mdi-rake::before{content:"\F1544"}.mdi-raspberry-pi::before{content:"\F043F"}.mdi-raw::before{content:"\F1A0F"}.mdi-raw-off::before{content:"\F1A10"}.mdi-ray-end::before{content:"\F0440"}.mdi-ray-end-arrow::before{content:"\F0441"}.mdi-ray-start::before{content:"\F0442"}.mdi-ray-start-arrow::before{content:"\F0443"}.mdi-ray-start-end::before{content:"\F0444"}.mdi-ray-start-vertex-end::before{content:"\F15D8"}.mdi-ray-vertex::before{content:"\F0445"}.mdi-razor-double-edge::before{content:"\F1997"}.mdi-razor-single-edge::before{content:"\F1998"}.mdi-react::before{content:"\F0708"}.mdi-read::before{content:"\F0447"}.mdi-receipt::before{content:"\F0824"}.mdi-receipt-clock::before{content:"\F1C3E"}.mdi-receipt-clock-outline::before{content:"\F1C3F"}.mdi-receipt-outline::before{content:"\F04F7"}.mdi-receipt-send::before{content:"\F1C40"}.mdi-receipt-send-outline::before{content:"\F1C41"}.mdi-receipt-text::before{content:"\F0449"}.mdi-receipt-text-arrow-left::before{content:"\F1C42"}.mdi-receipt-text-arrow-left-outline::before{content:"\F1C43"}.mdi-receipt-text-arrow-right::before{content:"\F1C44"}.mdi-receipt-text-arrow-right-outline::before{content:"\F1C45"}.mdi-receipt-text-check::before{content:"\F1A63"}.mdi-receipt-text-check-outline::before{content:"\F1A64"}.mdi-receipt-text-clock::before{content:"\F1C46"}.mdi-receipt-text-clock-outline::before{content:"\F1C47"}.mdi-receipt-text-edit::before{content:"\F1C48"}.mdi-receipt-text-edit-outline::before{content:"\F1C49"}.mdi-receipt-text-minus::before{content:"\F1A65"}.mdi-receipt-text-minus-outline::before{content:"\F1A66"}.mdi-receipt-text-outline::before{content:"\F19DC"}.mdi-receipt-text-plus::before{content:"\F1A67"}.mdi-receipt-text-plus-outline::before{content:"\F1A68"}.mdi-receipt-text-remove::before{content:"\F1A69"}.mdi-receipt-text-remove-outline::before{content:"\F1A6A"}.mdi-receipt-text-send::before{content:"\F1C4A"}.mdi-receipt-text-send-outline::before{content:"\F1C4B"}.mdi-record::before{content:"\F044A"}.mdi-record-circle::before{content:"\F0EC2"}.mdi-record-circle-outline::before{content:"\F0EC3"}.mdi-record-player::before{content:"\F099A"}.mdi-record-rec::before{content:"\F044B"}.mdi-rectangle::before{content:"\F0E5E"}.mdi-rectangle-outline::before{content:"\F0E5F"}.mdi-recycle::before{content:"\F044C"}.mdi-recycle-variant::before{content:"\F139D"}.mdi-reddit::before{content:"\F044D"}.mdi-redhat::before{content:"\F111B"}.mdi-redo::before{content:"\F044E"}.mdi-redo-variant::before{content:"\F044F"}.mdi-reflect-horizontal::before{content:"\F0A0E"}.mdi-reflect-vertical::before{content:"\F0A0F"}.mdi-refresh::before{content:"\F0450"}.mdi-refresh-auto::before{content:"\F18F2"}.mdi-refresh-circle::before{content:"\F1377"}.mdi-regex::before{content:"\F0451"}.mdi-registered-trademark::before{content:"\F0A67"}.mdi-reiterate::before{content:"\F1588"}.mdi-relation-many-to-many::before{content:"\F1496"}.mdi-relation-many-to-one::before{content:"\F1497"}.mdi-relation-many-to-one-or-many::before{content:"\F1498"}.mdi-relation-many-to-only-one::before{content:"\F1499"}.mdi-relation-many-to-zero-or-many::before{content:"\F149A"}.mdi-relation-many-to-zero-or-one::before{content:"\F149B"}.mdi-relation-one-or-many-to-many::before{content:"\F149C"}.mdi-relation-one-or-many-to-one::before{content:"\F149D"}.mdi-relation-one-or-many-to-one-or-many::before{content:"\F149E"}.mdi-relation-one-or-many-to-only-one::before{content:"\F149F"}.mdi-relation-one-or-many-to-zero-or-many::before{content:"\F14A0"}.mdi-relation-one-or-many-to-zero-or-one::before{content:"\F14A1"}.mdi-relation-one-to-many::before{content:"\F14A2"}.mdi-relation-one-to-one::before{content:"\F14A3"}.mdi-relation-one-to-one-or-many::before{content:"\F14A4"}.mdi-relation-one-to-only-one::before{content:"\F14A5"}.mdi-relation-one-to-zero-or-many::before{content:"\F14A6"}.mdi-relation-one-to-zero-or-one::before{content:"\F14A7"}.mdi-relation-only-one-to-many::before{content:"\F14A8"}.mdi-relation-only-one-to-one::before{content:"\F14A9"}.mdi-relation-only-one-to-one-or-many::before{content:"\F14AA"}.mdi-relation-only-one-to-only-one::before{content:"\F14AB"}.mdi-relation-only-one-to-zero-or-many::before{content:"\F14AC"}.mdi-relation-only-one-to-zero-or-one::before{content:"\F14AD"}.mdi-relation-zero-or-many-to-many::before{content:"\F14AE"}.mdi-relation-zero-or-many-to-one::before{content:"\F14AF"}.mdi-relation-zero-or-many-to-one-or-many::before{content:"\F14B0"}.mdi-relation-zero-or-many-to-only-one::before{content:"\F14B1"}.mdi-relation-zero-or-many-to-zero-or-many::before{content:"\F14B2"}.mdi-relation-zero-or-many-to-zero-or-one::before{content:"\F14B3"}.mdi-relation-zero-or-one-to-many::before{content:"\F14B4"}.mdi-relation-zero-or-one-to-one::before{content:"\F14B5"}.mdi-relation-zero-or-one-to-one-or-many::before{content:"\F14B6"}.mdi-relation-zero-or-one-to-only-one::before{content:"\F14B7"}.mdi-relation-zero-or-one-to-zero-or-many::before{content:"\F14B8"}.mdi-relation-zero-or-one-to-zero-or-one::before{content:"\F14B9"}.mdi-relative-scale::before{content:"\F0452"}.mdi-reload::before{content:"\F0453"}.mdi-reload-alert::before{content:"\F110B"}.mdi-reminder::before{content:"\F088C"}.mdi-remote::before{content:"\F0454"}.mdi-remote-desktop::before{content:"\F08B9"}.mdi-remote-off::before{content:"\F0EC4"}.mdi-remote-tv::before{content:"\F0EC5"}.mdi-remote-tv-off::before{content:"\F0EC6"}.mdi-rename::before{content:"\F1C18"}.mdi-rename-box::before{content:"\F0455"}.mdi-rename-box-outline::before{content:"\F1C19"}.mdi-rename-outline::before{content:"\F1C1A"}.mdi-reorder-horizontal::before{content:"\F0688"}.mdi-reorder-vertical::before{content:"\F0689"}.mdi-repeat::before{content:"\F0456"}.mdi-repeat-off::before{content:"\F0457"}.mdi-repeat-once::before{content:"\F0458"}.mdi-repeat-variant::before{content:"\F0547"}.mdi-replay::before{content:"\F0459"}.mdi-reply::before{content:"\F045A"}.mdi-reply-all::before{content:"\F045B"}.mdi-reply-all-outline::before{content:"\F0F1F"}.mdi-reply-circle::before{content:"\F11AE"}.mdi-reply-outline::before{content:"\F0F20"}.mdi-reproduction::before{content:"\F045C"}.mdi-resistor::before{content:"\F0B44"}.mdi-resistor-nodes::before{content:"\F0B45"}.mdi-resize::before{content:"\F0A68"}.mdi-resize-bottom-right::before{content:"\F045D"}.mdi-responsive::before{content:"\F045E"}.mdi-restart::before{content:"\F0709"}.mdi-restart-alert::before{content:"\F110C"}.mdi-restart-off::before{content:"\F0D95"}.mdi-restore::before{content:"\F099B"}.mdi-restore-alert::before{content:"\F110D"}.mdi-rewind::before{content:"\F045F"}.mdi-rewind-10::before{content:"\F0D2A"}.mdi-rewind-15::before{content:"\F1946"}.mdi-rewind-30::before{content:"\F0D96"}.mdi-rewind-45::before{content:"\F1B13"}.mdi-rewind-5::before{content:"\F11F9"}.mdi-rewind-60::before{content:"\F160C"}.mdi-rewind-outline::before{content:"\F070A"}.mdi-rhombus::before{content:"\F070B"}.mdi-rhombus-medium::before{content:"\F0A10"}.mdi-rhombus-medium-outline::before{content:"\F14DC"}.mdi-rhombus-outline::before{content:"\F070C"}.mdi-rhombus-split::before{content:"\F0A11"}.mdi-rhombus-split-outline::before{content:"\F14DD"}.mdi-ribbon::before{content:"\F0460"}.mdi-rice::before{content:"\F07EA"}.mdi-rickshaw::before{content:"\F15BB"}.mdi-rickshaw-electric::before{content:"\F15BC"}.mdi-ring::before{content:"\F07EB"}.mdi-rivet::before{content:"\F0E60"}.mdi-road::before{content:"\F0461"}.mdi-road-variant::before{content:"\F0462"}.mdi-robber::before{content:"\F1058"}.mdi-robot::before{content:"\F06A9"}.mdi-robot-angry::before{content:"\F169D"}.mdi-robot-angry-outline::before{content:"\F169E"}.mdi-robot-confused::before{content:"\F169F"}.mdi-robot-confused-outline::before{content:"\F16A0"}.mdi-robot-dead::before{content:"\F16A1"}.mdi-robot-dead-outline::before{content:"\F16A2"}.mdi-robot-excited::before{content:"\F16A3"}.mdi-robot-excited-outline::before{content:"\F16A4"}.mdi-robot-happy::before{content:"\F1719"}.mdi-robot-happy-outline::before{content:"\F171A"}.mdi-robot-industrial::before{content:"\F0B46"}.mdi-robot-industrial-outline::before{content:"\F1A1A"}.mdi-robot-love::before{content:"\F16A5"}.mdi-robot-love-outline::before{content:"\F16A6"}.mdi-robot-mower::before{content:"\F11F7"}.mdi-robot-mower-outline::before{content:"\F11F3"}.mdi-robot-off::before{content:"\F16A7"}.mdi-robot-off-outline::before{content:"\F167B"}.mdi-robot-outline::before{content:"\F167A"}.mdi-robot-vacuum::before{content:"\F070D"}.mdi-robot-vacuum-alert::before{content:"\F1B5D"}.mdi-robot-vacuum-off::before{content:"\F1C01"}.mdi-robot-vacuum-variant::before{content:"\F0908"}.mdi-robot-vacuum-variant-alert::before{content:"\F1B5E"}.mdi-robot-vacuum-variant-off::before{content:"\F1C02"}.mdi-rocket::before{content:"\F0463"}.mdi-rocket-launch::before{content:"\F14DE"}.mdi-rocket-launch-outline::before{content:"\F14DF"}.mdi-rocket-outline::before{content:"\F13AF"}.mdi-rodent::before{content:"\F1327"}.mdi-roller-shade::before{content:"\F1A6B"}.mdi-roller-shade-closed::before{content:"\F1A6C"}.mdi-roller-skate::before{content:"\F0D2B"}.mdi-roller-skate-off::before{content:"\F0145"}.mdi-rollerblade::before{content:"\F0D2C"}.mdi-rollerblade-off::before{content:"\F002E"}.mdi-rollupjs::before{content:"\F0BC0"}.mdi-rolodex::before{content:"\F1AB9"}.mdi-rolodex-outline::before{content:"\F1ABA"}.mdi-roman-numeral-1::before{content:"\F1088"}.mdi-roman-numeral-10::before{content:"\F1091"}.mdi-roman-numeral-2::before{content:"\F1089"}.mdi-roman-numeral-3::before{content:"\F108A"}.mdi-roman-numeral-4::before{content:"\F108B"}.mdi-roman-numeral-5::before{content:"\F108C"}.mdi-roman-numeral-6::before{content:"\F108D"}.mdi-roman-numeral-7::before{content:"\F108E"}.mdi-roman-numeral-8::before{content:"\F108F"}.mdi-roman-numeral-9::before{content:"\F1090"}.mdi-room-service::before{content:"\F088D"}.mdi-room-service-outline::before{content:"\F0D97"}.mdi-rotate-360::before{content:"\F1999"}.mdi-rotate-3d::before{content:"\F0EC7"}.mdi-rotate-3d-variant::before{content:"\F0464"}.mdi-rotate-left::before{content:"\F0465"}.mdi-rotate-left-variant::before{content:"\F0466"}.mdi-rotate-orbit::before{content:"\F0D98"}.mdi-rotate-right::before{content:"\F0467"}.mdi-rotate-right-variant::before{content:"\F0468"}.mdi-rounded-corner::before{content:"\F0607"}.mdi-router::before{content:"\F11E2"}.mdi-router-network::before{content:"\F1087"}.mdi-router-network-wireless::before{content:"\F1C97"}.mdi-router-wireless::before{content:"\F0469"}.mdi-router-wireless-off::before{content:"\F15A3"}.mdi-router-wireless-settings::before{content:"\F0A69"}.mdi-routes::before{content:"\F046A"}.mdi-routes-clock::before{content:"\F1059"}.mdi-rowing::before{content:"\F0608"}.mdi-rss::before{content:"\F046B"}.mdi-rss-box::before{content:"\F046C"}.mdi-rss-off::before{content:"\F0F21"}.mdi-rug::before{content:"\F1475"}.mdi-rugby::before{content:"\F0D99"}.mdi-ruler::before{content:"\F046D"}.mdi-ruler-square::before{content:"\F0CC2"}.mdi-ruler-square-compass::before{content:"\F0EBE"}.mdi-run::before{content:"\F070E"}.mdi-run-fast::before{content:"\F046E"}.mdi-rv-truck::before{content:"\F11D4"}.mdi-sack::before{content:"\F0D2E"}.mdi-sack-outline::before{content:"\F1C4C"}.mdi-sack-percent::before{content:"\F0D2F"}.mdi-safe::before{content:"\F0A6A"}.mdi-safe-square::before{content:"\F127C"}.mdi-safe-square-outline::before{content:"\F127D"}.mdi-safety-goggles::before{content:"\F0D30"}.mdi-sail-boat::before{content:"\F0EC8"}.mdi-sail-boat-sink::before{content:"\F1AEF"}.mdi-sale::before{content:"\F046F"}.mdi-sale-outline::before{content:"\F1A06"}.mdi-salesforce::before{content:"\F088E"}.mdi-sass::before{content:"\F07EC"}.mdi-satellite::before{content:"\F0470"}.mdi-satellite-uplink::before{content:"\F0909"}.mdi-satellite-variant::before{content:"\F0471"}.mdi-sausage::before{content:"\F08BA"}.mdi-sausage-off::before{content:"\F1789"}.mdi-saw-blade::before{content:"\F0E61"}.mdi-sawtooth-wave::before{content:"\F147A"}.mdi-saxophone::before{content:"\F0609"}.mdi-scale::before{content:"\F0472"}.mdi-scale-balance::before{content:"\F05D1"}.mdi-scale-bathroom::before{content:"\F0473"}.mdi-scale-off::before{content:"\F105A"}.mdi-scale-unbalanced::before{content:"\F19B8"}.mdi-scan-helper::before{content:"\F13D8"}.mdi-scanner::before{content:"\F06AB"}.mdi-scanner-off::before{content:"\F090A"}.mdi-scatter-plot::before{content:"\F0EC9"}.mdi-scatter-plot-outline::before{content:"\F0ECA"}.mdi-scent::before{content:"\F1958"}.mdi-scent-off::before{content:"\F1959"}.mdi-school::before{content:"\F0474"}.mdi-school-outline::before{content:"\F1180"}.mdi-scissors-cutting::before{content:"\F0A6B"}.mdi-scooter::before{content:"\F15BD"}.mdi-scooter-electric::before{content:"\F15BE"}.mdi-scoreboard::before{content:"\F127E"}.mdi-scoreboard-outline::before{content:"\F127F"}.mdi-screen-rotation::before{content:"\F0475"}.mdi-screen-rotation-lock::before{content:"\F0478"}.mdi-screw-flat-top::before{content:"\F0DF3"}.mdi-screw-lag::before{content:"\F0DF4"}.mdi-screw-machine-flat-top::before{content:"\F0DF5"}.mdi-screw-machine-round-top::before{content:"\F0DF6"}.mdi-screw-round-top::before{content:"\F0DF7"}.mdi-screwdriver::before{content:"\F0476"}.mdi-script::before{content:"\F0BC1"}.mdi-script-outline::before{content:"\F0477"}.mdi-script-text::before{content:"\F0BC2"}.mdi-script-text-key::before{content:"\F1725"}.mdi-script-text-key-outline::before{content:"\F1726"}.mdi-script-text-outline::before{content:"\F0BC3"}.mdi-script-text-play::before{content:"\F1727"}.mdi-script-text-play-outline::before{content:"\F1728"}.mdi-sd::before{content:"\F0479"}.mdi-seal::before{content:"\F047A"}.mdi-seal-variant::before{content:"\F0FD9"}.mdi-search-web::before{content:"\F070F"}.mdi-seat::before{content:"\F0CC3"}.mdi-seat-flat::before{content:"\F047B"}.mdi-seat-flat-angled::before{content:"\F047C"}.mdi-seat-individual-suite::before{content:"\F047D"}.mdi-seat-legroom-extra::before{content:"\F047E"}.mdi-seat-legroom-normal::before{content:"\F047F"}.mdi-seat-legroom-reduced::before{content:"\F0480"}.mdi-seat-outline::before{content:"\F0CC4"}.mdi-seat-passenger::before{content:"\F1249"}.mdi-seat-recline-extra::before{content:"\F0481"}.mdi-seat-recline-normal::before{content:"\F0482"}.mdi-seatbelt::before{content:"\F0CC5"}.mdi-security::before{content:"\F0483"}.mdi-security-network::before{content:"\F0484"}.mdi-seed::before{content:"\F0E62"}.mdi-seed-off::before{content:"\F13FD"}.mdi-seed-off-outline::before{content:"\F13FE"}.mdi-seed-outline::before{content:"\F0E63"}.mdi-seed-plus::before{content:"\F1A6D"}.mdi-seed-plus-outline::before{content:"\F1A6E"}.mdi-seesaw::before{content:"\F15A4"}.mdi-segment::before{content:"\F0ECB"}.mdi-select::before{content:"\F0485"}.mdi-select-all::before{content:"\F0486"}.mdi-select-arrow-down::before{content:"\F1B59"}.mdi-select-arrow-up::before{content:"\F1B58"}.mdi-select-color::before{content:"\F0D31"}.mdi-select-compare::before{content:"\F0AD9"}.mdi-select-drag::before{content:"\F0A6C"}.mdi-select-group::before{content:"\F0F82"}.mdi-select-inverse::before{content:"\F0487"}.mdi-select-marker::before{content:"\F1280"}.mdi-select-multiple::before{content:"\F1281"}.mdi-select-multiple-marker::before{content:"\F1282"}.mdi-select-off::before{content:"\F0488"}.mdi-select-place::before{content:"\F0FDA"}.mdi-select-remove::before{content:"\F17C1"}.mdi-select-search::before{content:"\F1204"}.mdi-selection::before{content:"\F0489"}.mdi-selection-drag::before{content:"\F0A6D"}.mdi-selection-ellipse::before{content:"\F0D32"}.mdi-selection-ellipse-arrow-inside::before{content:"\F0F22"}.mdi-selection-ellipse-remove::before{content:"\F17C2"}.mdi-selection-marker::before{content:"\F1283"}.mdi-selection-multiple::before{content:"\F1285"}.mdi-selection-multiple-marker::before{content:"\F1284"}.mdi-selection-off::before{content:"\F0777"}.mdi-selection-remove::before{content:"\F17C3"}.mdi-selection-search::before{content:"\F1205"}.mdi-semantic-web::before{content:"\F1316"}.mdi-send::before{content:"\F048A"}.mdi-send-check::before{content:"\F1161"}.mdi-send-check-outline::before{content:"\F1162"}.mdi-send-circle::before{content:"\F0DF8"}.mdi-send-circle-outline::before{content:"\F0DF9"}.mdi-send-clock::before{content:"\F1163"}.mdi-send-clock-outline::before{content:"\F1164"}.mdi-send-lock::before{content:"\F07ED"}.mdi-send-lock-outline::before{content:"\F1166"}.mdi-send-outline::before{content:"\F1165"}.mdi-send-variant::before{content:"\F1C4D"}.mdi-send-variant-clock::before{content:"\F1C7E"}.mdi-send-variant-clock-outline::before{content:"\F1C7F"}.mdi-send-variant-outline::before{content:"\F1C4E"}.mdi-serial-port::before{content:"\F065C"}.mdi-server::before{content:"\F048B"}.mdi-server-minus::before{content:"\F048C"}.mdi-server-minus-outline::before{content:"\F1C98"}.mdi-server-network::before{content:"\F048D"}.mdi-server-network-off::before{content:"\F048E"}.mdi-server-network-outline::before{content:"\F1C99"}.mdi-server-off::before{content:"\F048F"}.mdi-server-outline::before{content:"\F1C9A"}.mdi-server-plus::before{content:"\F0490"}.mdi-server-plus-outline::before{content:"\F1C9B"}.mdi-server-remove::before{content:"\F0491"}.mdi-server-security::before{content:"\F0492"}.mdi-set-all::before{content:"\F0778"}.mdi-set-center::before{content:"\F0779"}.mdi-set-center-right::before{content:"\F077A"}.mdi-set-left::before{content:"\F077B"}.mdi-set-left-center::before{content:"\F077C"}.mdi-set-left-right::before{content:"\F077D"}.mdi-set-merge::before{content:"\F14E0"}.mdi-set-none::before{content:"\F077E"}.mdi-set-right::before{content:"\F077F"}.mdi-set-split::before{content:"\F14E1"}.mdi-set-square::before{content:"\F145D"}.mdi-set-top-box::before{content:"\F099F"}.mdi-settings-helper::before{content:"\F0A6E"}.mdi-shaker::before{content:"\F110E"}.mdi-shaker-outline::before{content:"\F110F"}.mdi-shape::before{content:"\F0831"}.mdi-shape-circle-plus::before{content:"\F065D"}.mdi-shape-outline::before{content:"\F0832"}.mdi-shape-oval-plus::before{content:"\F11FA"}.mdi-shape-plus::before{content:"\F0495"}.mdi-shape-plus-outline::before{content:"\F1C4F"}.mdi-shape-polygon-plus::before{content:"\F065E"}.mdi-shape-rectangle-plus::before{content:"\F065F"}.mdi-shape-square-plus::before{content:"\F0660"}.mdi-shape-square-rounded-plus::before{content:"\F14FA"}.mdi-share::before{content:"\F0496"}.mdi-share-all::before{content:"\F11F4"}.mdi-share-all-outline::before{content:"\F11F5"}.mdi-share-circle::before{content:"\F11AD"}.mdi-share-off::before{content:"\F0F23"}.mdi-share-off-outline::before{content:"\F0F24"}.mdi-share-outline::before{content:"\F0932"}.mdi-share-variant::before{content:"\F0497"}.mdi-share-variant-outline::before{content:"\F1514"}.mdi-shark::before{content:"\F18BA"}.mdi-shark-fin::before{content:"\F1673"}.mdi-shark-fin-outline::before{content:"\F1674"}.mdi-shark-off::before{content:"\F18BB"}.mdi-sheep::before{content:"\F0CC6"}.mdi-shield::before{content:"\F0498"}.mdi-shield-account::before{content:"\F088F"}.mdi-shield-account-outline::before{content:"\F0A12"}.mdi-shield-account-variant::before{content:"\F15A7"}.mdi-shield-account-variant-outline::before{content:"\F15A8"}.mdi-shield-airplane::before{content:"\F06BB"}.mdi-shield-airplane-outline::before{content:"\F0CC7"}.mdi-shield-alert::before{content:"\F0ECC"}.mdi-shield-alert-outline::before{content:"\F0ECD"}.mdi-shield-bug::before{content:"\F13DA"}.mdi-shield-bug-outline::before{content:"\F13DB"}.mdi-shield-car::before{content:"\F0F83"}.mdi-shield-check::before{content:"\F0565"}.mdi-shield-check-outline::before{content:"\F0CC8"}.mdi-shield-cross::before{content:"\F0CC9"}.mdi-shield-cross-outline::before{content:"\F0CCA"}.mdi-shield-crown::before{content:"\F18BC"}.mdi-shield-crown-outline::before{content:"\F18BD"}.mdi-shield-edit::before{content:"\F11A0"}.mdi-shield-edit-outline::before{content:"\F11A1"}.mdi-shield-half::before{content:"\F1360"}.mdi-shield-half-full::before{content:"\F0780"}.mdi-shield-home::before{content:"\F068A"}.mdi-shield-home-outline::before{content:"\F0CCB"}.mdi-shield-key::before{content:"\F0BC4"}.mdi-shield-key-outline::before{content:"\F0BC5"}.mdi-shield-link-variant::before{content:"\F0D33"}.mdi-shield-link-variant-outline::before{content:"\F0D34"}.mdi-shield-lock::before{content:"\F099D"}.mdi-shield-lock-open::before{content:"\F199A"}.mdi-shield-lock-open-outline::before{content:"\F199B"}.mdi-shield-lock-outline::before{content:"\F0CCC"}.mdi-shield-moon::before{content:"\F1828"}.mdi-shield-moon-outline::before{content:"\F1829"}.mdi-shield-off::before{content:"\F099E"}.mdi-shield-off-outline::before{content:"\F099C"}.mdi-shield-outline::before{content:"\F0499"}.mdi-shield-plus::before{content:"\F0ADA"}.mdi-shield-plus-outline::before{content:"\F0ADB"}.mdi-shield-refresh::before{content:"\F00AA"}.mdi-shield-refresh-outline::before{content:"\F01E0"}.mdi-shield-remove::before{content:"\F0ADC"}.mdi-shield-remove-outline::before{content:"\F0ADD"}.mdi-shield-search::before{content:"\F0D9A"}.mdi-shield-star::before{content:"\F113B"}.mdi-shield-star-outline::before{content:"\F113C"}.mdi-shield-sun::before{content:"\F105D"}.mdi-shield-sun-outline::before{content:"\F105E"}.mdi-shield-sword::before{content:"\F18BE"}.mdi-shield-sword-outline::before{content:"\F18BF"}.mdi-shield-sync::before{content:"\F11A2"}.mdi-shield-sync-outline::before{content:"\F11A3"}.mdi-shimmer::before{content:"\F1545"}.mdi-ship-wheel::before{content:"\F0833"}.mdi-shipping-pallet::before{content:"\F184E"}.mdi-shoe-ballet::before{content:"\F15CA"}.mdi-shoe-cleat::before{content:"\F15C7"}.mdi-shoe-formal::before{content:"\F0B47"}.mdi-shoe-heel::before{content:"\F0B48"}.mdi-shoe-print::before{content:"\F0DFA"}.mdi-shoe-sneaker::before{content:"\F15C8"}.mdi-shopping::before{content:"\F049A"}.mdi-shopping-music::before{content:"\F049B"}.mdi-shopping-outline::before{content:"\F11D5"}.mdi-shopping-search::before{content:"\F0F84"}.mdi-shopping-search-outline::before{content:"\F1A6F"}.mdi-shore::before{content:"\F14F9"}.mdi-shovel::before{content:"\F0710"}.mdi-shovel-off::before{content:"\F0711"}.mdi-shower::before{content:"\F09A0"}.mdi-shower-head::before{content:"\F09A1"}.mdi-shredder::before{content:"\F049C"}.mdi-shuffle::before{content:"\F049D"}.mdi-shuffle-disabled::before{content:"\F049E"}.mdi-shuffle-variant::before{content:"\F049F"}.mdi-shuriken::before{content:"\F137F"}.mdi-sickle::before{content:"\F18C0"}.mdi-sigma::before{content:"\F04A0"}.mdi-sigma-lower::before{content:"\F062B"}.mdi-sign-caution::before{content:"\F04A1"}.mdi-sign-direction::before{content:"\F0781"}.mdi-sign-direction-minus::before{content:"\F1000"}.mdi-sign-direction-plus::before{content:"\F0FDC"}.mdi-sign-direction-remove::before{content:"\F0FDD"}.mdi-sign-language::before{content:"\F1B4D"}.mdi-sign-language-outline::before{content:"\F1B4E"}.mdi-sign-pole::before{content:"\F14F8"}.mdi-sign-real-estate::before{content:"\F1118"}.mdi-sign-text::before{content:"\F0782"}.mdi-sign-yield::before{content:"\F1BAF"}.mdi-signal::before{content:"\F04A2"}.mdi-signal-2g::before{content:"\F0712"}.mdi-signal-3g::before{content:"\F0713"}.mdi-signal-4g::before{content:"\F0714"}.mdi-signal-5g::before{content:"\F0A6F"}.mdi-signal-cellular-1::before{content:"\F08BC"}.mdi-signal-cellular-2::before{content:"\F08BD"}.mdi-signal-cellular-3::before{content:"\F08BE"}.mdi-signal-cellular-outline::before{content:"\F08BF"}.mdi-signal-distance-variant::before{content:"\F0E64"}.mdi-signal-hspa::before{content:"\F0715"}.mdi-signal-hspa-plus::before{content:"\F0716"}.mdi-signal-off::before{content:"\F0783"}.mdi-signal-variant::before{content:"\F060A"}.mdi-signature::before{content:"\F0DFB"}.mdi-signature-freehand::before{content:"\F0DFC"}.mdi-signature-image::before{content:"\F0DFD"}.mdi-signature-text::before{content:"\F0DFE"}.mdi-silo::before{content:"\F1B9F"}.mdi-silo-outline::before{content:"\F0B49"}.mdi-silverware::before{content:"\F04A3"}.mdi-silverware-clean::before{content:"\F0FDE"}.mdi-silverware-fork::before{content:"\F04A4"}.mdi-silverware-fork-knife::before{content:"\F0A70"}.mdi-silverware-spoon::before{content:"\F04A5"}.mdi-silverware-variant::before{content:"\F04A6"}.mdi-sim::before{content:"\F04A7"}.mdi-sim-alert::before{content:"\F04A8"}.mdi-sim-alert-outline::before{content:"\F15D3"}.mdi-sim-off::before{content:"\F04A9"}.mdi-sim-off-outline::before{content:"\F15D4"}.mdi-sim-outline::before{content:"\F15D5"}.mdi-simple-icons::before{content:"\F131D"}.mdi-sina-weibo::before{content:"\F0ADF"}.mdi-sine-wave::before{content:"\F095B"}.mdi-sitemap::before{content:"\F04AA"}.mdi-sitemap-outline::before{content:"\F199C"}.mdi-size-l::before{content:"\F13A6"}.mdi-size-m::before{content:"\F13A5"}.mdi-size-s::before{content:"\F13A4"}.mdi-size-xl::before{content:"\F13A7"}.mdi-size-xs::before{content:"\F13A3"}.mdi-size-xxl::before{content:"\F13A8"}.mdi-size-xxs::before{content:"\F13A2"}.mdi-size-xxxl::before{content:"\F13A9"}.mdi-skate::before{content:"\F0D35"}.mdi-skate-off::before{content:"\F0699"}.mdi-skateboard::before{content:"\F14C2"}.mdi-skateboarding::before{content:"\F0501"}.mdi-skew-less::before{content:"\F0D36"}.mdi-skew-more::before{content:"\F0D37"}.mdi-ski::before{content:"\F1304"}.mdi-ski-cross-country::before{content:"\F1305"}.mdi-ski-water::before{content:"\F1306"}.mdi-skip-backward::before{content:"\F04AB"}.mdi-skip-backward-outline::before{content:"\F0F25"}.mdi-skip-forward::before{content:"\F04AC"}.mdi-skip-forward-outline::before{content:"\F0F26"}.mdi-skip-next::before{content:"\F04AD"}.mdi-skip-next-circle::before{content:"\F0661"}.mdi-skip-next-circle-outline::before{content:"\F0662"}.mdi-skip-next-outline::before{content:"\F0F27"}.mdi-skip-previous::before{content:"\F04AE"}.mdi-skip-previous-circle::before{content:"\F0663"}.mdi-skip-previous-circle-outline::before{content:"\F0664"}.mdi-skip-previous-outline::before{content:"\F0F28"}.mdi-skull::before{content:"\F068C"}.mdi-skull-crossbones::before{content:"\F0BC6"}.mdi-skull-crossbones-outline::before{content:"\F0BC7"}.mdi-skull-outline::before{content:"\F0BC8"}.mdi-skull-scan::before{content:"\F14C7"}.mdi-skull-scan-outline::before{content:"\F14C8"}.mdi-skype::before{content:"\F04AF"}.mdi-skype-business::before{content:"\F04B0"}.mdi-slack::before{content:"\F04B1"}.mdi-slash-forward::before{content:"\F0FDF"}.mdi-slash-forward-box::before{content:"\F0FE0"}.mdi-sledding::before{content:"\F041B"}.mdi-sleep::before{content:"\F04B2"}.mdi-sleep-off::before{content:"\F04B3"}.mdi-slide::before{content:"\F15A5"}.mdi-slope-downhill::before{content:"\F0DFF"}.mdi-slope-uphill::before{content:"\F0E00"}.mdi-slot-machine::before{content:"\F1114"}.mdi-slot-machine-outline::before{content:"\F1115"}.mdi-smart-card::before{content:"\F10BD"}.mdi-smart-card-off::before{content:"\F18F7"}.mdi-smart-card-off-outline::before{content:"\F18F8"}.mdi-smart-card-outline::before{content:"\F10BE"}.mdi-smart-card-reader::before{content:"\F10BF"}.mdi-smart-card-reader-outline::before{content:"\F10C0"}.mdi-smog::before{content:"\F0A71"}.mdi-smoke::before{content:"\F1799"}.mdi-smoke-detector::before{content:"\F0392"}.mdi-smoke-detector-alert::before{content:"\F192E"}.mdi-smoke-detector-alert-outline::before{content:"\F192F"}.mdi-smoke-detector-off::before{content:"\F1809"}.mdi-smoke-detector-off-outline::before{content:"\F180A"}.mdi-smoke-detector-outline::before{content:"\F1808"}.mdi-smoke-detector-variant::before{content:"\F180B"}.mdi-smoke-detector-variant-alert::before{content:"\F1930"}.mdi-smoke-detector-variant-off::before{content:"\F180C"}.mdi-smoking::before{content:"\F04B4"}.mdi-smoking-off::before{content:"\F04B5"}.mdi-smoking-pipe::before{content:"\F140D"}.mdi-smoking-pipe-off::before{content:"\F1428"}.mdi-snail::before{content:"\F1677"}.mdi-snake::before{content:"\F150E"}.mdi-snapchat::before{content:"\F04B6"}.mdi-snowboard::before{content:"\F1307"}.mdi-snowflake::before{content:"\F0717"}.mdi-snowflake-alert::before{content:"\F0F29"}.mdi-snowflake-check::before{content:"\F1A70"}.mdi-snowflake-melt::before{content:"\F12CB"}.mdi-snowflake-off::before{content:"\F14E3"}.mdi-snowflake-thermometer::before{content:"\F1A71"}.mdi-snowflake-variant::before{content:"\F0F2A"}.mdi-snowman::before{content:"\F04B7"}.mdi-snowmobile::before{content:"\F06DD"}.mdi-snowshoeing::before{content:"\F1A72"}.mdi-soccer::before{content:"\F04B8"}.mdi-soccer-field::before{content:"\F0834"}.mdi-social-distance-2-meters::before{content:"\F1579"}.mdi-social-distance-6-feet::before{content:"\F157A"}.mdi-sofa::before{content:"\F04B9"}.mdi-sofa-outline::before{content:"\F156D"}.mdi-sofa-single::before{content:"\F156E"}.mdi-sofa-single-outline::before{content:"\F156F"}.mdi-solar-panel::before{content:"\F0D9B"}.mdi-solar-panel-large::before{content:"\F0D9C"}.mdi-solar-power::before{content:"\F0A72"}.mdi-solar-power-variant::before{content:"\F1A73"}.mdi-solar-power-variant-outline::before{content:"\F1A74"}.mdi-soldering-iron::before{content:"\F1092"}.mdi-solid::before{content:"\F068D"}.mdi-sony-playstation::before{content:"\F0414"}.mdi-sort::before{content:"\F04BA"}.mdi-sort-alphabetical-ascending::before{content:"\F05BD"}.mdi-sort-alphabetical-ascending-variant::before{content:"\F1148"}.mdi-sort-alphabetical-descending::before{content:"\F05BF"}.mdi-sort-alphabetical-descending-variant::before{content:"\F1149"}.mdi-sort-alphabetical-variant::before{content:"\F04BB"}.mdi-sort-ascending::before{content:"\F04BC"}.mdi-sort-bool-ascending::before{content:"\F1385"}.mdi-sort-bool-ascending-variant::before{content:"\F1386"}.mdi-sort-bool-descending::before{content:"\F1387"}.mdi-sort-bool-descending-variant::before{content:"\F1388"}.mdi-sort-calendar-ascending::before{content:"\F1547"}.mdi-sort-calendar-descending::before{content:"\F1548"}.mdi-sort-clock-ascending::before{content:"\F1549"}.mdi-sort-clock-ascending-outline::before{content:"\F154A"}.mdi-sort-clock-descending::before{content:"\F154B"}.mdi-sort-clock-descending-outline::before{content:"\F154C"}.mdi-sort-descending::before{content:"\F04BD"}.mdi-sort-numeric-ascending::before{content:"\F1389"}.mdi-sort-numeric-ascending-variant::before{content:"\F090D"}.mdi-sort-numeric-descending::before{content:"\F138A"}.mdi-sort-numeric-descending-variant::before{content:"\F0AD2"}.mdi-sort-numeric-variant::before{content:"\F04BE"}.mdi-sort-reverse-variant::before{content:"\F033C"}.mdi-sort-variant::before{content:"\F04BF"}.mdi-sort-variant-lock::before{content:"\F0CCD"}.mdi-sort-variant-lock-open::before{content:"\F0CCE"}.mdi-sort-variant-off::before{content:"\F1ABB"}.mdi-sort-variant-remove::before{content:"\F1147"}.mdi-soundbar::before{content:"\F17DB"}.mdi-soundcloud::before{content:"\F04C0"}.mdi-source-branch::before{content:"\F062C"}.mdi-source-branch-check::before{content:"\F14CF"}.mdi-source-branch-minus::before{content:"\F14CB"}.mdi-source-branch-plus::before{content:"\F14CA"}.mdi-source-branch-refresh::before{content:"\F14CD"}.mdi-source-branch-remove::before{content:"\F14CC"}.mdi-source-branch-sync::before{content:"\F14CE"}.mdi-source-commit::before{content:"\F0718"}.mdi-source-commit-end::before{content:"\F0719"}.mdi-source-commit-end-local::before{content:"\F071A"}.mdi-source-commit-local::before{content:"\F071B"}.mdi-source-commit-next-local::before{content:"\F071C"}.mdi-source-commit-start::before{content:"\F071D"}.mdi-source-commit-start-next-local::before{content:"\F071E"}.mdi-source-fork::before{content:"\F04C1"}.mdi-source-merge::before{content:"\F062D"}.mdi-source-pull::before{content:"\F04C2"}.mdi-source-repository::before{content:"\F0CCF"}.mdi-source-repository-multiple::before{content:"\F0CD0"}.mdi-soy-sauce::before{content:"\F07EE"}.mdi-soy-sauce-off::before{content:"\F13FC"}.mdi-spa::before{content:"\F0CD1"}.mdi-spa-outline::before{content:"\F0CD2"}.mdi-space-invaders::before{content:"\F0BC9"}.mdi-space-station::before{content:"\F1383"}.mdi-spade::before{content:"\F0E65"}.mdi-speaker::before{content:"\F04C3"}.mdi-speaker-bluetooth::before{content:"\F09A2"}.mdi-speaker-message::before{content:"\F1B11"}.mdi-speaker-multiple::before{content:"\F0D38"}.mdi-speaker-off::before{content:"\F04C4"}.mdi-speaker-pause::before{content:"\F1B73"}.mdi-speaker-play::before{content:"\F1B72"}.mdi-speaker-stop::before{content:"\F1B74"}.mdi-speaker-wireless::before{content:"\F071F"}.mdi-spear::before{content:"\F1845"}.mdi-speedometer::before{content:"\F04C5"}.mdi-speedometer-medium::before{content:"\F0F85"}.mdi-speedometer-slow::before{content:"\F0F86"}.mdi-spellcheck::before{content:"\F04C6"}.mdi-sphere::before{content:"\F1954"}.mdi-sphere-off::before{content:"\F1955"}.mdi-spider::before{content:"\F11EA"}.mdi-spider-outline::before{content:"\F1C75"}.mdi-spider-thread::before{content:"\F11EB"}.mdi-spider-web::before{content:"\F0BCA"}.mdi-spirit-level::before{content:"\F14F1"}.mdi-spoon-sugar::before{content:"\F1429"}.mdi-spotify::before{content:"\F04C7"}.mdi-spotlight::before{content:"\F04C8"}.mdi-spotlight-beam::before{content:"\F04C9"}.mdi-spray::before{content:"\F0665"}.mdi-spray-bottle::before{content:"\F0AE0"}.mdi-sprinkler::before{content:"\F105F"}.mdi-sprinkler-fire::before{content:"\F199D"}.mdi-sprinkler-variant::before{content:"\F1060"}.mdi-sprout::before{content:"\F0E66"}.mdi-sprout-outline::before{content:"\F0E67"}.mdi-square::before{content:"\F0764"}.mdi-square-circle::before{content:"\F1500"}.mdi-square-circle-outline::before{content:"\F1C50"}.mdi-square-edit-outline::before{content:"\F090C"}.mdi-square-medium::before{content:"\F0A13"}.mdi-square-medium-outline::before{content:"\F0A14"}.mdi-square-off::before{content:"\F12EE"}.mdi-square-off-outline::before{content:"\F12EF"}.mdi-square-opacity::before{content:"\F1854"}.mdi-square-outline::before{content:"\F0763"}.mdi-square-root::before{content:"\F0784"}.mdi-square-root-box::before{content:"\F09A3"}.mdi-square-rounded::before{content:"\F14FB"}.mdi-square-rounded-badge::before{content:"\F1A07"}.mdi-square-rounded-badge-outline::before{content:"\F1A08"}.mdi-square-rounded-outline::before{content:"\F14FC"}.mdi-square-small::before{content:"\F0A15"}.mdi-square-wave::before{content:"\F147B"}.mdi-squeegee::before{content:"\F0AE1"}.mdi-ssh::before{content:"\F08C0"}.mdi-stack-exchange::before{content:"\F060B"}.mdi-stack-overflow::before{content:"\F04CC"}.mdi-stackpath::before{content:"\F0359"}.mdi-stadium::before{content:"\F0FF9"}.mdi-stadium-outline::before{content:"\F1B03"}.mdi-stadium-variant::before{content:"\F0720"}.mdi-stairs::before{content:"\F04CD"}.mdi-stairs-box::before{content:"\F139E"}.mdi-stairs-down::before{content:"\F12BE"}.mdi-stairs-up::before{content:"\F12BD"}.mdi-stamper::before{content:"\F0D39"}.mdi-standard-definition::before{content:"\F07EF"}.mdi-star::before{content:"\F04CE"}.mdi-star-box::before{content:"\F0A73"}.mdi-star-box-multiple::before{content:"\F1286"}.mdi-star-box-multiple-outline::before{content:"\F1287"}.mdi-star-box-outline::before{content:"\F0A74"}.mdi-star-check::before{content:"\F1566"}.mdi-star-check-outline::before{content:"\F156A"}.mdi-star-circle::before{content:"\F04CF"}.mdi-star-circle-outline::before{content:"\F09A4"}.mdi-star-cog::before{content:"\F1668"}.mdi-star-cog-outline::before{content:"\F1669"}.mdi-star-crescent::before{content:"\F0979"}.mdi-star-david::before{content:"\F097A"}.mdi-star-face::before{content:"\F09A5"}.mdi-star-four-points::before{content:"\F0AE2"}.mdi-star-four-points-box::before{content:"\F1C51"}.mdi-star-four-points-box-outline::before{content:"\F1C52"}.mdi-star-four-points-circle::before{content:"\F1C53"}.mdi-star-four-points-circle-outline::before{content:"\F1C54"}.mdi-star-four-points-outline::before{content:"\F0AE3"}.mdi-star-four-points-small::before{content:"\F1C55"}.mdi-star-half::before{content:"\F0246"}.mdi-star-half-full::before{content:"\F04D0"}.mdi-star-minus::before{content:"\F1564"}.mdi-star-minus-outline::before{content:"\F1568"}.mdi-star-off::before{content:"\F04D1"}.mdi-star-off-outline::before{content:"\F155B"}.mdi-star-outline::before{content:"\F04D2"}.mdi-star-plus::before{content:"\F1563"}.mdi-star-plus-outline::before{content:"\F1567"}.mdi-star-remove::before{content:"\F1565"}.mdi-star-remove-outline::before{content:"\F1569"}.mdi-star-settings::before{content:"\F166A"}.mdi-star-settings-outline::before{content:"\F166B"}.mdi-star-shooting::before{content:"\F1741"}.mdi-star-shooting-outline::before{content:"\F1742"}.mdi-star-three-points::before{content:"\F0AE4"}.mdi-star-three-points-outline::before{content:"\F0AE5"}.mdi-state-machine::before{content:"\F11EF"}.mdi-steam::before{content:"\F04D3"}.mdi-steering::before{content:"\F04D4"}.mdi-steering-off::before{content:"\F090E"}.mdi-step-backward::before{content:"\F04D5"}.mdi-step-backward-2::before{content:"\F04D6"}.mdi-step-forward::before{content:"\F04D7"}.mdi-step-forward-2::before{content:"\F04D8"}.mdi-stethoscope::before{content:"\F04D9"}.mdi-sticker::before{content:"\F1364"}.mdi-sticker-alert::before{content:"\F1365"}.mdi-sticker-alert-outline::before{content:"\F1366"}.mdi-sticker-check::before{content:"\F1367"}.mdi-sticker-check-outline::before{content:"\F1368"}.mdi-sticker-circle-outline::before{content:"\F05D0"}.mdi-sticker-emoji::before{content:"\F0785"}.mdi-sticker-minus::before{content:"\F1369"}.mdi-sticker-minus-outline::before{content:"\F136A"}.mdi-sticker-outline::before{content:"\F136B"}.mdi-sticker-plus::before{content:"\F136C"}.mdi-sticker-plus-outline::before{content:"\F136D"}.mdi-sticker-remove::before{content:"\F136E"}.mdi-sticker-remove-outline::before{content:"\F136F"}.mdi-sticker-text::before{content:"\F178E"}.mdi-sticker-text-outline::before{content:"\F178F"}.mdi-stocking::before{content:"\F04DA"}.mdi-stomach::before{content:"\F1093"}.mdi-stool::before{content:"\F195D"}.mdi-stool-outline::before{content:"\F195E"}.mdi-stop::before{content:"\F04DB"}.mdi-stop-circle::before{content:"\F0666"}.mdi-stop-circle-outline::before{content:"\F0667"}.mdi-storage-tank::before{content:"\F1A75"}.mdi-storage-tank-outline::before{content:"\F1A76"}.mdi-store::before{content:"\F04DC"}.mdi-store-24-hour::before{content:"\F04DD"}.mdi-store-alert::before{content:"\F18C1"}.mdi-store-alert-outline::before{content:"\F18C2"}.mdi-store-check::before{content:"\F18C3"}.mdi-store-check-outline::before{content:"\F18C4"}.mdi-store-clock::before{content:"\F18C5"}.mdi-store-clock-outline::before{content:"\F18C6"}.mdi-store-cog::before{content:"\F18C7"}.mdi-store-cog-outline::before{content:"\F18C8"}.mdi-store-edit::before{content:"\F18C9"}.mdi-store-edit-outline::before{content:"\F18CA"}.mdi-store-marker::before{content:"\F18CB"}.mdi-store-marker-outline::before{content:"\F18CC"}.mdi-store-minus::before{content:"\F165E"}.mdi-store-minus-outline::before{content:"\F18CD"}.mdi-store-off::before{content:"\F18CE"}.mdi-store-off-outline::before{content:"\F18CF"}.mdi-store-outline::before{content:"\F1361"}.mdi-store-plus::before{content:"\F165F"}.mdi-store-plus-outline::before{content:"\F18D0"}.mdi-store-remove::before{content:"\F1660"}.mdi-store-remove-outline::before{content:"\F18D1"}.mdi-store-search::before{content:"\F18D2"}.mdi-store-search-outline::before{content:"\F18D3"}.mdi-store-settings::before{content:"\F18D4"}.mdi-store-settings-outline::before{content:"\F18D5"}.mdi-storefront::before{content:"\F07C7"}.mdi-storefront-check::before{content:"\F1B7D"}.mdi-storefront-check-outline::before{content:"\F1B7E"}.mdi-storefront-edit::before{content:"\F1B7F"}.mdi-storefront-edit-outline::before{content:"\F1B80"}.mdi-storefront-minus::before{content:"\F1B83"}.mdi-storefront-minus-outline::before{content:"\F1B84"}.mdi-storefront-outline::before{content:"\F10C1"}.mdi-storefront-plus::before{content:"\F1B81"}.mdi-storefront-plus-outline::before{content:"\F1B82"}.mdi-storefront-remove::before{content:"\F1B85"}.mdi-storefront-remove-outline::before{content:"\F1B86"}.mdi-stove::before{content:"\F04DE"}.mdi-strategy::before{content:"\F11D6"}.mdi-stretch-to-page::before{content:"\F0F2B"}.mdi-stretch-to-page-outline::before{content:"\F0F2C"}.mdi-string-lights::before{content:"\F12BA"}.mdi-string-lights-off::before{content:"\F12BB"}.mdi-subdirectory-arrow-left::before{content:"\F060C"}.mdi-subdirectory-arrow-right::before{content:"\F060D"}.mdi-submarine::before{content:"\F156C"}.mdi-subtitles::before{content:"\F0A16"}.mdi-subtitles-outline::before{content:"\F0A17"}.mdi-subway::before{content:"\F06AC"}.mdi-subway-alert-variant::before{content:"\F0D9D"}.mdi-subway-variant::before{content:"\F04DF"}.mdi-summit::before{content:"\F0786"}.mdi-sun-angle::before{content:"\F1B27"}.mdi-sun-angle-outline::before{content:"\F1B28"}.mdi-sun-clock::before{content:"\F1A77"}.mdi-sun-clock-outline::before{content:"\F1A78"}.mdi-sun-compass::before{content:"\F19A5"}.mdi-sun-snowflake::before{content:"\F1796"}.mdi-sun-snowflake-variant::before{content:"\F1A79"}.mdi-sun-thermometer::before{content:"\F18D6"}.mdi-sun-thermometer-outline::before{content:"\F18D7"}.mdi-sun-wireless::before{content:"\F17FE"}.mdi-sun-wireless-outline::before{content:"\F17FF"}.mdi-sunglasses::before{content:"\F04E0"}.mdi-surfing::before{content:"\F1746"}.mdi-surround-sound::before{content:"\F05C5"}.mdi-surround-sound-2-0::before{content:"\F07F0"}.mdi-surround-sound-2-1::before{content:"\F1729"}.mdi-surround-sound-3-1::before{content:"\F07F1"}.mdi-surround-sound-5-1::before{content:"\F07F2"}.mdi-surround-sound-5-1-2::before{content:"\F172A"}.mdi-surround-sound-7-1::before{content:"\F07F3"}.mdi-svg::before{content:"\F0721"}.mdi-swap-horizontal::before{content:"\F04E1"}.mdi-swap-horizontal-bold::before{content:"\F0BCD"}.mdi-swap-horizontal-circle::before{content:"\F0FE1"}.mdi-swap-horizontal-circle-outline::before{content:"\F0FE2"}.mdi-swap-horizontal-hidden::before{content:"\F1D0E"}.mdi-swap-horizontal-variant::before{content:"\F08C1"}.mdi-swap-vertical::before{content:"\F04E2"}.mdi-swap-vertical-bold::before{content:"\F0BCE"}.mdi-swap-vertical-circle::before{content:"\F0FE3"}.mdi-swap-vertical-circle-outline::before{content:"\F0FE4"}.mdi-swap-vertical-variant::before{content:"\F08C2"}.mdi-swim::before{content:"\F04E3"}.mdi-switch::before{content:"\F04E4"}.mdi-sword::before{content:"\F04E5"}.mdi-sword-cross::before{content:"\F0787"}.mdi-syllabary-hangul::before{content:"\F1333"}.mdi-syllabary-hiragana::before{content:"\F1334"}.mdi-syllabary-katakana::before{content:"\F1335"}.mdi-syllabary-katakana-halfwidth::before{content:"\F1336"}.mdi-symbol::before{content:"\F1501"}.mdi-symfony::before{content:"\F0AE6"}.mdi-synagogue::before{content:"\F1B04"}.mdi-synagogue-outline::before{content:"\F1B05"}.mdi-sync::before{content:"\F04E6"}.mdi-sync-alert::before{content:"\F04E7"}.mdi-sync-circle::before{content:"\F1378"}.mdi-sync-off::before{content:"\F04E8"}.mdi-tab::before{content:"\F04E9"}.mdi-tab-minus::before{content:"\F0B4B"}.mdi-tab-plus::before{content:"\F075C"}.mdi-tab-remove::before{content:"\F0B4C"}.mdi-tab-search::before{content:"\F199E"}.mdi-tab-unselected::before{content:"\F04EA"}.mdi-table::before{content:"\F04EB"}.mdi-table-account::before{content:"\F13B9"}.mdi-table-alert::before{content:"\F13BA"}.mdi-table-arrow-down::before{content:"\F13BB"}.mdi-table-arrow-left::before{content:"\F13BC"}.mdi-table-arrow-right::before{content:"\F13BD"}.mdi-table-arrow-up::before{content:"\F13BE"}.mdi-table-border::before{content:"\F0A18"}.mdi-table-cancel::before{content:"\F13BF"}.mdi-table-chair::before{content:"\F1061"}.mdi-table-check::before{content:"\F13C0"}.mdi-table-clock::before{content:"\F13C1"}.mdi-table-cog::before{content:"\F13C2"}.mdi-table-column::before{content:"\F0835"}.mdi-table-column-plus-after::before{content:"\F04EC"}.mdi-table-column-plus-before::before{content:"\F04ED"}.mdi-table-column-remove::before{content:"\F04EE"}.mdi-table-column-width::before{content:"\F04EF"}.mdi-table-edit::before{content:"\F04F0"}.mdi-table-eye::before{content:"\F1094"}.mdi-table-eye-off::before{content:"\F13C3"}.mdi-table-filter::before{content:"\F1B8C"}.mdi-table-furniture::before{content:"\F05BC"}.mdi-table-headers-eye::before{content:"\F121D"}.mdi-table-headers-eye-off::before{content:"\F121E"}.mdi-table-heart::before{content:"\F13C4"}.mdi-table-key::before{content:"\F13C5"}.mdi-table-large::before{content:"\F04F1"}.mdi-table-large-plus::before{content:"\F0F87"}.mdi-table-large-remove::before{content:"\F0F88"}.mdi-table-lock::before{content:"\F13C6"}.mdi-table-merge-cells::before{content:"\F09A6"}.mdi-table-minus::before{content:"\F13C7"}.mdi-table-multiple::before{content:"\F13C8"}.mdi-table-network::before{content:"\F13C9"}.mdi-table-of-contents::before{content:"\F0836"}.mdi-table-off::before{content:"\F13CA"}.mdi-table-picnic::before{content:"\F1743"}.mdi-table-pivot::before{content:"\F183C"}.mdi-table-plus::before{content:"\F0A75"}.mdi-table-question::before{content:"\F1B21"}.mdi-table-refresh::before{content:"\F13A0"}.mdi-table-remove::before{content:"\F0A76"}.mdi-table-row::before{content:"\F0837"}.mdi-table-row-height::before{content:"\F04F2"}.mdi-table-row-plus-after::before{content:"\F04F3"}.mdi-table-row-plus-before::before{content:"\F04F4"}.mdi-table-row-remove::before{content:"\F04F5"}.mdi-table-search::before{content:"\F090F"}.mdi-table-settings::before{content:"\F0838"}.mdi-table-split-cell::before{content:"\F142A"}.mdi-table-star::before{content:"\F13CB"}.mdi-table-sync::before{content:"\F13A1"}.mdi-table-tennis::before{content:"\F0E68"}.mdi-tablet::before{content:"\F04F6"}.mdi-tablet-cellphone::before{content:"\F09A7"}.mdi-tablet-dashboard::before{content:"\F0ECE"}.mdi-taco::before{content:"\F0762"}.mdi-tag::before{content:"\F04F9"}.mdi-tag-arrow-down::before{content:"\F172B"}.mdi-tag-arrow-down-outline::before{content:"\F172C"}.mdi-tag-arrow-left::before{content:"\F172D"}.mdi-tag-arrow-left-outline::before{content:"\F172E"}.mdi-tag-arrow-right::before{content:"\F172F"}.mdi-tag-arrow-right-outline::before{content:"\F1730"}.mdi-tag-arrow-up::before{content:"\F1731"}.mdi-tag-arrow-up-outline::before{content:"\F1732"}.mdi-tag-check::before{content:"\F1A7A"}.mdi-tag-check-outline::before{content:"\F1A7B"}.mdi-tag-edit::before{content:"\F1C9C"}.mdi-tag-edit-outline::before{content:"\F1C9D"}.mdi-tag-faces::before{content:"\F04FA"}.mdi-tag-heart::before{content:"\F068B"}.mdi-tag-heart-outline::before{content:"\F0BCF"}.mdi-tag-hidden::before{content:"\F1C76"}.mdi-tag-minus::before{content:"\F0910"}.mdi-tag-minus-outline::before{content:"\F121F"}.mdi-tag-multiple::before{content:"\F04FB"}.mdi-tag-multiple-outline::before{content:"\F12F7"}.mdi-tag-off::before{content:"\F1220"}.mdi-tag-off-outline::before{content:"\F1221"}.mdi-tag-outline::before{content:"\F04FC"}.mdi-tag-plus::before{content:"\F0722"}.mdi-tag-plus-outline::before{content:"\F1222"}.mdi-tag-remove::before{content:"\F0723"}.mdi-tag-remove-outline::before{content:"\F1223"}.mdi-tag-search::before{content:"\F1907"}.mdi-tag-search-outline::before{content:"\F1908"}.mdi-tag-text::before{content:"\F1224"}.mdi-tag-text-outline::before{content:"\F04FD"}.mdi-tailwind::before{content:"\F13FF"}.mdi-tally-mark-1::before{content:"\F1ABC"}.mdi-tally-mark-2::before{content:"\F1ABD"}.mdi-tally-mark-3::before{content:"\F1ABE"}.mdi-tally-mark-4::before{content:"\F1ABF"}.mdi-tally-mark-5::before{content:"\F1AC0"}.mdi-tangram::before{content:"\F04F8"}.mdi-tank::before{content:"\F0D3A"}.mdi-tanker-truck::before{content:"\F0FE5"}.mdi-tape-drive::before{content:"\F16DF"}.mdi-tape-measure::before{content:"\F0B4D"}.mdi-target::before{content:"\F04FE"}.mdi-target-account::before{content:"\F0BD0"}.mdi-target-variant::before{content:"\F0A77"}.mdi-taxi::before{content:"\F04FF"}.mdi-tea::before{content:"\F0D9E"}.mdi-tea-outline::before{content:"\F0D9F"}.mdi-teamviewer::before{content:"\F0500"}.mdi-teddy-bear::before{content:"\F18FB"}.mdi-telescope::before{content:"\F0B4E"}.mdi-television::before{content:"\F0502"}.mdi-television-ambient-light::before{content:"\F1356"}.mdi-television-box::before{content:"\F0839"}.mdi-television-classic::before{content:"\F07F4"}.mdi-television-classic-off::before{content:"\F083A"}.mdi-television-guide::before{content:"\F0503"}.mdi-television-off::before{content:"\F083B"}.mdi-television-pause::before{content:"\F0F89"}.mdi-television-play::before{content:"\F0ECF"}.mdi-television-shimmer::before{content:"\F1110"}.mdi-television-speaker::before{content:"\F1B1B"}.mdi-television-speaker-off::before{content:"\F1B1C"}.mdi-television-stop::before{content:"\F0F8A"}.mdi-temperature-celsius::before{content:"\F0504"}.mdi-temperature-fahrenheit::before{content:"\F0505"}.mdi-temperature-kelvin::before{content:"\F0506"}.mdi-temple-buddhist::before{content:"\F1B06"}.mdi-temple-buddhist-outline::before{content:"\F1B07"}.mdi-temple-hindu::before{content:"\F1B08"}.mdi-temple-hindu-outline::before{content:"\F1B09"}.mdi-tennis::before{content:"\F0DA0"}.mdi-tennis-ball::before{content:"\F0507"}.mdi-tennis-ball-outline::before{content:"\F1C5F"}.mdi-tent::before{content:"\F0508"}.mdi-terraform::before{content:"\F1062"}.mdi-terrain::before{content:"\F0509"}.mdi-test-tube::before{content:"\F0668"}.mdi-test-tube-empty::before{content:"\F0911"}.mdi-test-tube-off::before{content:"\F0912"}.mdi-text::before{content:"\F09A8"}.mdi-text-account::before{content:"\F1570"}.mdi-text-box::before{content:"\F021A"}.mdi-text-box-check::before{content:"\F0EA6"}.mdi-text-box-check-outline::before{content:"\F0EA7"}.mdi-text-box-edit::before{content:"\F1A7C"}.mdi-text-box-edit-outline::before{content:"\F1A7D"}.mdi-text-box-minus::before{content:"\F0EA8"}.mdi-text-box-minus-outline::before{content:"\F0EA9"}.mdi-text-box-multiple::before{content:"\F0AB7"}.mdi-text-box-multiple-outline::before{content:"\F0AB8"}.mdi-text-box-outline::before{content:"\F09ED"}.mdi-text-box-plus::before{content:"\F0EAA"}.mdi-text-box-plus-outline::before{content:"\F0EAB"}.mdi-text-box-remove::before{content:"\F0EAC"}.mdi-text-box-remove-outline::before{content:"\F0EAD"}.mdi-text-box-search::before{content:"\F0EAE"}.mdi-text-box-search-outline::before{content:"\F0EAF"}.mdi-text-long::before{content:"\F09AA"}.mdi-text-recognition::before{content:"\F113D"}.mdi-text-search::before{content:"\F13B8"}.mdi-text-search-variant::before{content:"\F1A7E"}.mdi-text-shadow::before{content:"\F0669"}.mdi-text-short::before{content:"\F09A9"}.mdi-texture::before{content:"\F050C"}.mdi-texture-box::before{content:"\F0FE6"}.mdi-theater::before{content:"\F050D"}.mdi-theme-light-dark::before{content:"\F050E"}.mdi-thermometer::before{content:"\F050F"}.mdi-thermometer-alert::before{content:"\F0E01"}.mdi-thermometer-auto::before{content:"\F1B0F"}.mdi-thermometer-bluetooth::before{content:"\F1895"}.mdi-thermometer-check::before{content:"\F1A7F"}.mdi-thermometer-chevron-down::before{content:"\F0E02"}.mdi-thermometer-chevron-up::before{content:"\F0E03"}.mdi-thermometer-high::before{content:"\F10C2"}.mdi-thermometer-lines::before{content:"\F0510"}.mdi-thermometer-low::before{content:"\F10C3"}.mdi-thermometer-minus::before{content:"\F0E04"}.mdi-thermometer-off::before{content:"\F1531"}.mdi-thermometer-plus::before{content:"\F0E05"}.mdi-thermometer-probe::before{content:"\F1B2B"}.mdi-thermometer-probe-off::before{content:"\F1B2C"}.mdi-thermometer-water::before{content:"\F1A80"}.mdi-thermostat::before{content:"\F0393"}.mdi-thermostat-auto::before{content:"\F1B17"}.mdi-thermostat-box::before{content:"\F0891"}.mdi-thermostat-box-auto::before{content:"\F1B18"}.mdi-thermostat-cog::before{content:"\F1C80"}.mdi-thought-bubble::before{content:"\F07F6"}.mdi-thought-bubble-outline::before{content:"\F07F7"}.mdi-thumb-down::before{content:"\F0511"}.mdi-thumb-down-outline::before{content:"\F0512"}.mdi-thumb-up::before{content:"\F0513"}.mdi-thumb-up-outline::before{content:"\F0514"}.mdi-thumbs-up-down::before{content:"\F0515"}.mdi-thumbs-up-down-outline::before{content:"\F1914"}.mdi-ticket::before{content:"\F0516"}.mdi-ticket-account::before{content:"\F0517"}.mdi-ticket-confirmation::before{content:"\F0518"}.mdi-ticket-confirmation-outline::before{content:"\F13AA"}.mdi-ticket-outline::before{content:"\F0913"}.mdi-ticket-percent::before{content:"\F0724"}.mdi-ticket-percent-outline::before{content:"\F142B"}.mdi-tie::before{content:"\F0519"}.mdi-tilde::before{content:"\F0725"}.mdi-tilde-off::before{content:"\F18F3"}.mdi-timelapse::before{content:"\F051A"}.mdi-timeline::before{content:"\F0BD1"}.mdi-timeline-alert::before{content:"\F0F95"}.mdi-timeline-alert-outline::before{content:"\F0F98"}.mdi-timeline-check::before{content:"\F1532"}.mdi-timeline-check-outline::before{content:"\F1533"}.mdi-timeline-clock::before{content:"\F11FB"}.mdi-timeline-clock-outline::before{content:"\F11FC"}.mdi-timeline-minus::before{content:"\F1534"}.mdi-timeline-minus-outline::before{content:"\F1535"}.mdi-timeline-outline::before{content:"\F0BD2"}.mdi-timeline-plus::before{content:"\F0F96"}.mdi-timeline-plus-outline::before{content:"\F0F97"}.mdi-timeline-question::before{content:"\F0F99"}.mdi-timeline-question-outline::before{content:"\F0F9A"}.mdi-timeline-remove::before{content:"\F1536"}.mdi-timeline-remove-outline::before{content:"\F1537"}.mdi-timeline-text::before{content:"\F0BD3"}.mdi-timeline-text-outline::before{content:"\F0BD4"}.mdi-timer::before{content:"\F13AB"}.mdi-timer-10::before{content:"\F051C"}.mdi-timer-3::before{content:"\F051D"}.mdi-timer-alert::before{content:"\F1ACC"}.mdi-timer-alert-outline::before{content:"\F1ACD"}.mdi-timer-cancel::before{content:"\F1ACE"}.mdi-timer-cancel-outline::before{content:"\F1ACF"}.mdi-timer-check::before{content:"\F1AD0"}.mdi-timer-check-outline::before{content:"\F1AD1"}.mdi-timer-cog::before{content:"\F1925"}.mdi-timer-cog-outline::before{content:"\F1926"}.mdi-timer-edit::before{content:"\F1AD2"}.mdi-timer-edit-outline::before{content:"\F1AD3"}.mdi-timer-lock::before{content:"\F1AD4"}.mdi-timer-lock-open::before{content:"\F1AD5"}.mdi-timer-lock-open-outline::before{content:"\F1AD6"}.mdi-timer-lock-outline::before{content:"\F1AD7"}.mdi-timer-marker::before{content:"\F1AD8"}.mdi-timer-marker-outline::before{content:"\F1AD9"}.mdi-timer-minus::before{content:"\F1ADA"}.mdi-timer-minus-outline::before{content:"\F1ADB"}.mdi-timer-music::before{content:"\F1ADC"}.mdi-timer-music-outline::before{content:"\F1ADD"}.mdi-timer-off::before{content:"\F13AC"}.mdi-timer-off-outline::before{content:"\F051E"}.mdi-timer-outline::before{content:"\F051B"}.mdi-timer-pause::before{content:"\F1ADE"}.mdi-timer-pause-outline::before{content:"\F1ADF"}.mdi-timer-play::before{content:"\F1AE0"}.mdi-timer-play-outline::before{content:"\F1AE1"}.mdi-timer-plus::before{content:"\F1AE2"}.mdi-timer-plus-outline::before{content:"\F1AE3"}.mdi-timer-refresh::before{content:"\F1AE4"}.mdi-timer-refresh-outline::before{content:"\F1AE5"}.mdi-timer-remove::before{content:"\F1AE6"}.mdi-timer-remove-outline::before{content:"\F1AE7"}.mdi-timer-sand::before{content:"\F051F"}.mdi-timer-sand-complete::before{content:"\F199F"}.mdi-timer-sand-empty::before{content:"\F06AD"}.mdi-timer-sand-full::before{content:"\F078C"}.mdi-timer-sand-paused::before{content:"\F19A0"}.mdi-timer-settings::before{content:"\F1923"}.mdi-timer-settings-outline::before{content:"\F1924"}.mdi-timer-star::before{content:"\F1AE8"}.mdi-timer-star-outline::before{content:"\F1AE9"}.mdi-timer-stop::before{content:"\F1AEA"}.mdi-timer-stop-outline::before{content:"\F1AEB"}.mdi-timer-sync::before{content:"\F1AEC"}.mdi-timer-sync-outline::before{content:"\F1AED"}.mdi-timetable::before{content:"\F0520"}.mdi-tire::before{content:"\F1896"}.mdi-toaster::before{content:"\F1063"}.mdi-toaster-off::before{content:"\F11B7"}.mdi-toaster-oven::before{content:"\F0CD3"}.mdi-toggle-switch::before{content:"\F0521"}.mdi-toggle-switch-off::before{content:"\F0522"}.mdi-toggle-switch-off-outline::before{content:"\F0A19"}.mdi-toggle-switch-outline::before{content:"\F0A1A"}.mdi-toggle-switch-variant::before{content:"\F1A25"}.mdi-toggle-switch-variant-off::before{content:"\F1A26"}.mdi-toilet::before{content:"\F09AB"}.mdi-toolbox::before{content:"\F09AC"}.mdi-toolbox-outline::before{content:"\F09AD"}.mdi-tools::before{content:"\F1064"}.mdi-tooltip::before{content:"\F0523"}.mdi-tooltip-account::before{content:"\F000C"}.mdi-tooltip-cellphone::before{content:"\F183B"}.mdi-tooltip-check::before{content:"\F155C"}.mdi-tooltip-check-outline::before{content:"\F155D"}.mdi-tooltip-edit::before{content:"\F0524"}.mdi-tooltip-edit-outline::before{content:"\F12C5"}.mdi-tooltip-image::before{content:"\F0525"}.mdi-tooltip-image-outline::before{content:"\F0BD5"}.mdi-tooltip-minus::before{content:"\F155E"}.mdi-tooltip-minus-outline::before{content:"\F155F"}.mdi-tooltip-outline::before{content:"\F0526"}.mdi-tooltip-plus::before{content:"\F0BD6"}.mdi-tooltip-plus-outline::before{content:"\F0527"}.mdi-tooltip-question::before{content:"\F1BBA"}.mdi-tooltip-question-outline::before{content:"\F1BBB"}.mdi-tooltip-remove::before{content:"\F1560"}.mdi-tooltip-remove-outline::before{content:"\F1561"}.mdi-tooltip-text::before{content:"\F0528"}.mdi-tooltip-text-outline::before{content:"\F0BD7"}.mdi-tooth::before{content:"\F08C3"}.mdi-tooth-outline::before{content:"\F0529"}.mdi-toothbrush::before{content:"\F1129"}.mdi-toothbrush-electric::before{content:"\F112C"}.mdi-toothbrush-paste::before{content:"\F112A"}.mdi-torch::before{content:"\F1606"}.mdi-tortoise::before{content:"\F0D3B"}.mdi-toslink::before{content:"\F12B8"}.mdi-touch-text-outline::before{content:"\F1C60"}.mdi-tournament::before{content:"\F09AE"}.mdi-tow-truck::before{content:"\F083C"}.mdi-tower-beach::before{content:"\F0681"}.mdi-tower-fire::before{content:"\F0682"}.mdi-town-hall::before{content:"\F1875"}.mdi-toy-brick::before{content:"\F1288"}.mdi-toy-brick-marker::before{content:"\F1289"}.mdi-toy-brick-marker-outline::before{content:"\F128A"}.mdi-toy-brick-minus::before{content:"\F128B"}.mdi-toy-brick-minus-outline::before{content:"\F128C"}.mdi-toy-brick-outline::before{content:"\F128D"}.mdi-toy-brick-plus::before{content:"\F128E"}.mdi-toy-brick-plus-outline::before{content:"\F128F"}.mdi-toy-brick-remove::before{content:"\F1290"}.mdi-toy-brick-remove-outline::before{content:"\F1291"}.mdi-toy-brick-search::before{content:"\F1292"}.mdi-toy-brick-search-outline::before{content:"\F1293"}.mdi-track-light::before{content:"\F0914"}.mdi-track-light-off::before{content:"\F1B01"}.mdi-trackpad::before{content:"\F07F8"}.mdi-trackpad-lock::before{content:"\F0933"}.mdi-tractor::before{content:"\F0892"}.mdi-tractor-variant::before{content:"\F14C4"}.mdi-trademark::before{content:"\F0A78"}.mdi-traffic-cone::before{content:"\F137C"}.mdi-traffic-light::before{content:"\F052B"}.mdi-traffic-light-outline::before{content:"\F182A"}.mdi-train::before{content:"\F052C"}.mdi-train-bus::before{content:"\F1CC7"}.mdi-train-car::before{content:"\F0BD8"}.mdi-train-car-autorack::before{content:"\F1B2D"}.mdi-train-car-box::before{content:"\F1B2E"}.mdi-train-car-box-full::before{content:"\F1B2F"}.mdi-train-car-box-open::before{content:"\F1B30"}.mdi-train-car-caboose::before{content:"\F1B31"}.mdi-train-car-centerbeam::before{content:"\F1B32"}.mdi-train-car-centerbeam-full::before{content:"\F1B33"}.mdi-train-car-container::before{content:"\F1B34"}.mdi-train-car-flatbed::before{content:"\F1B35"}.mdi-train-car-flatbed-car::before{content:"\F1B36"}.mdi-train-car-flatbed-tank::before{content:"\F1B37"}.mdi-train-car-gondola::before{content:"\F1B38"}.mdi-train-car-gondola-full::before{content:"\F1B39"}.mdi-train-car-hopper::before{content:"\F1B3A"}.mdi-train-car-hopper-covered::before{content:"\F1B3B"}.mdi-train-car-hopper-full::before{content:"\F1B3C"}.mdi-train-car-intermodal::before{content:"\F1B3D"}.mdi-train-car-passenger::before{content:"\F1733"}.mdi-train-car-passenger-door::before{content:"\F1734"}.mdi-train-car-passenger-door-open::before{content:"\F1735"}.mdi-train-car-passenger-variant::before{content:"\F1736"}.mdi-train-car-tank::before{content:"\F1B3E"}.mdi-train-variant::before{content:"\F08C4"}.mdi-tram::before{content:"\F052D"}.mdi-tram-side::before{content:"\F0FE7"}.mdi-transcribe::before{content:"\F052E"}.mdi-transcribe-close::before{content:"\F052F"}.mdi-transfer::before{content:"\F1065"}.mdi-transfer-down::before{content:"\F0DA1"}.mdi-transfer-left::before{content:"\F0DA2"}.mdi-transfer-right::before{content:"\F0530"}.mdi-transfer-up::before{content:"\F0DA3"}.mdi-transit-connection::before{content:"\F0D3C"}.mdi-transit-connection-horizontal::before{content:"\F1546"}.mdi-transit-connection-variant::before{content:"\F0D3D"}.mdi-transit-detour::before{content:"\F0F8B"}.mdi-transit-skip::before{content:"\F1515"}.mdi-transit-transfer::before{content:"\F06AE"}.mdi-transition::before{content:"\F0915"}.mdi-transition-masked::before{content:"\F0916"}.mdi-translate::before{content:"\F05CA"}.mdi-translate-off::before{content:"\F0E06"}.mdi-translate-variant::before{content:"\F1B99"}.mdi-transmission-tower::before{content:"\F0D3E"}.mdi-transmission-tower-export::before{content:"\F192C"}.mdi-transmission-tower-import::before{content:"\F192D"}.mdi-transmission-tower-off::before{content:"\F19DD"}.mdi-trash-can::before{content:"\F0A79"}.mdi-trash-can-outline::before{content:"\F0A7A"}.mdi-tray::before{content:"\F1294"}.mdi-tray-alert::before{content:"\F1295"}.mdi-tray-arrow-down::before{content:"\F0120"}.mdi-tray-arrow-up::before{content:"\F011D"}.mdi-tray-full::before{content:"\F1296"}.mdi-tray-minus::before{content:"\F1297"}.mdi-tray-plus::before{content:"\F1298"}.mdi-tray-remove::before{content:"\F1299"}.mdi-treasure-chest::before{content:"\F0726"}.mdi-treasure-chest-outline::before{content:"\F1C77"}.mdi-tree::before{content:"\F0531"}.mdi-tree-outline::before{content:"\F0E69"}.mdi-trello::before{content:"\F0532"}.mdi-trending-down::before{content:"\F0533"}.mdi-trending-neutral::before{content:"\F0534"}.mdi-trending-up::before{content:"\F0535"}.mdi-triangle::before{content:"\F0536"}.mdi-triangle-down::before{content:"\F1C56"}.mdi-triangle-down-outline::before{content:"\F1C57"}.mdi-triangle-outline::before{content:"\F0537"}.mdi-triangle-small-down::before{content:"\F1A09"}.mdi-triangle-small-up::before{content:"\F1A0A"}.mdi-triangle-wave::before{content:"\F147C"}.mdi-triforce::before{content:"\F0BD9"}.mdi-trophy::before{content:"\F0538"}.mdi-trophy-award::before{content:"\F0539"}.mdi-trophy-broken::before{content:"\F0DA4"}.mdi-trophy-outline::before{content:"\F053A"}.mdi-trophy-variant::before{content:"\F053B"}.mdi-trophy-variant-outline::before{content:"\F053C"}.mdi-truck::before{content:"\F053D"}.mdi-truck-alert::before{content:"\F19DE"}.mdi-truck-alert-outline::before{content:"\F19DF"}.mdi-truck-cargo-container::before{content:"\F18D8"}.mdi-truck-check::before{content:"\F0CD4"}.mdi-truck-check-outline::before{content:"\F129A"}.mdi-truck-delivery::before{content:"\F053E"}.mdi-truck-delivery-outline::before{content:"\F129B"}.mdi-truck-fast::before{content:"\F0788"}.mdi-truck-fast-outline::before{content:"\F129C"}.mdi-truck-flatbed::before{content:"\F1891"}.mdi-truck-minus::before{content:"\F19AE"}.mdi-truck-minus-outline::before{content:"\F19BD"}.mdi-truck-off-road::before{content:"\F1C9E"}.mdi-truck-off-road-off::before{content:"\F1C9F"}.mdi-truck-outline::before{content:"\F129D"}.mdi-truck-plus::before{content:"\F19AD"}.mdi-truck-plus-outline::before{content:"\F19BC"}.mdi-truck-remove::before{content:"\F19AF"}.mdi-truck-remove-outline::before{content:"\F19BE"}.mdi-truck-snowflake::before{content:"\F19A6"}.mdi-truck-trailer::before{content:"\F0727"}.mdi-trumpet::before{content:"\F1096"}.mdi-tshirt-crew::before{content:"\F0A7B"}.mdi-tshirt-crew-outline::before{content:"\F053F"}.mdi-tshirt-v::before{content:"\F0A7C"}.mdi-tshirt-v-outline::before{content:"\F0540"}.mdi-tsunami::before{content:"\F1A81"}.mdi-tumble-dryer::before{content:"\F0917"}.mdi-tumble-dryer-alert::before{content:"\F11BA"}.mdi-tumble-dryer-off::before{content:"\F11BB"}.mdi-tune::before{content:"\F062E"}.mdi-tune-variant::before{content:"\F1542"}.mdi-tune-vertical::before{content:"\F066A"}.mdi-tune-vertical-variant::before{content:"\F1543"}.mdi-tunnel::before{content:"\F183D"}.mdi-tunnel-outline::before{content:"\F183E"}.mdi-turbine::before{content:"\F1A82"}.mdi-turkey::before{content:"\F171B"}.mdi-turnstile::before{content:"\F0CD5"}.mdi-turnstile-outline::before{content:"\F0CD6"}.mdi-turtle::before{content:"\F0CD7"}.mdi-twitch::before{content:"\F0543"}.mdi-twitter::before{content:"\F0544"}.mdi-two-factor-authentication::before{content:"\F09AF"}.mdi-typewriter::before{content:"\F0F2D"}.mdi-ubisoft::before{content:"\F0BDA"}.mdi-ubuntu::before{content:"\F0548"}.mdi-ufo::before{content:"\F10C4"}.mdi-ufo-outline::before{content:"\F10C5"}.mdi-ultra-high-definition::before{content:"\F07F9"}.mdi-umbraco::before{content:"\F0549"}.mdi-umbrella::before{content:"\F054A"}.mdi-umbrella-beach::before{content:"\F188A"}.mdi-umbrella-beach-outline::before{content:"\F188B"}.mdi-umbrella-closed::before{content:"\F09B0"}.mdi-umbrella-closed-outline::before{content:"\F13E2"}.mdi-umbrella-closed-variant::before{content:"\F13E1"}.mdi-umbrella-outline::before{content:"\F054B"}.mdi-underwear-outline::before{content:"\F1D0F"}.mdi-undo::before{content:"\F054C"}.mdi-undo-variant::before{content:"\F054D"}.mdi-unfold-less-horizontal::before{content:"\F054E"}.mdi-unfold-less-vertical::before{content:"\F0760"}.mdi-unfold-more-horizontal::before{content:"\F054F"}.mdi-unfold-more-vertical::before{content:"\F0761"}.mdi-ungroup::before{content:"\F0550"}.mdi-unicode::before{content:"\F0ED0"}.mdi-unicorn::before{content:"\F15C2"}.mdi-unicorn-variant::before{content:"\F15C3"}.mdi-unicycle::before{content:"\F15E5"}.mdi-unity::before{content:"\F06AF"}.mdi-unreal::before{content:"\F09B1"}.mdi-update::before{content:"\F06B0"}.mdi-upload::before{content:"\F0552"}.mdi-upload-box::before{content:"\F1D10"}.mdi-upload-box-outline::before{content:"\F1D11"}.mdi-upload-circle::before{content:"\F1D12"}.mdi-upload-circle-outline::before{content:"\F1D13"}.mdi-upload-lock::before{content:"\F1373"}.mdi-upload-lock-outline::before{content:"\F1374"}.mdi-upload-multiple::before{content:"\F083D"}.mdi-upload-multiple-outline::before{content:"\F1D14"}.mdi-upload-network::before{content:"\F06F6"}.mdi-upload-network-outline::before{content:"\F0CD8"}.mdi-upload-off::before{content:"\F10C6"}.mdi-upload-off-outline::before{content:"\F10C7"}.mdi-upload-outline::before{content:"\F0E07"}.mdi-usb::before{content:"\F0553"}.mdi-usb-c-port::before{content:"\F1CBF"}.mdi-usb-flash-drive::before{content:"\F129E"}.mdi-usb-flash-drive-outline::before{content:"\F129F"}.mdi-usb-port::before{content:"\F11F0"}.mdi-vacuum::before{content:"\F19A1"}.mdi-vacuum-outline::before{content:"\F19A2"}.mdi-valve::before{content:"\F1066"}.mdi-valve-closed::before{content:"\F1067"}.mdi-valve-open::before{content:"\F1068"}.mdi-van-passenger::before{content:"\F07FA"}.mdi-van-utility::before{content:"\F07FB"}.mdi-vanish::before{content:"\F07FC"}.mdi-vanish-quarter::before{content:"\F1554"}.mdi-vanity-light::before{content:"\F11E1"}.mdi-variable::before{content:"\F0AE7"}.mdi-variable-box::before{content:"\F1111"}.mdi-vector-arrange-above::before{content:"\F0554"}.mdi-vector-arrange-below::before{content:"\F0555"}.mdi-vector-bezier::before{content:"\F0AE8"}.mdi-vector-circle::before{content:"\F0556"}.mdi-vector-circle-variant::before{content:"\F0557"}.mdi-vector-combine::before{content:"\F0558"}.mdi-vector-curve::before{content:"\F0559"}.mdi-vector-difference::before{content:"\F055A"}.mdi-vector-difference-ab::before{content:"\F055B"}.mdi-vector-difference-ba::before{content:"\F055C"}.mdi-vector-ellipse::before{content:"\F0893"}.mdi-vector-intersection::before{content:"\F055D"}.mdi-vector-line::before{content:"\F055E"}.mdi-vector-link::before{content:"\F0FE8"}.mdi-vector-point::before{content:"\F01C4"}.mdi-vector-point-edit::before{content:"\F09E8"}.mdi-vector-point-minus::before{content:"\F1B78"}.mdi-vector-point-plus::before{content:"\F1B79"}.mdi-vector-point-select::before{content:"\F055F"}.mdi-vector-polygon::before{content:"\F0560"}.mdi-vector-polygon-variant::before{content:"\F1856"}.mdi-vector-polyline::before{content:"\F0561"}.mdi-vector-polyline-edit::before{content:"\F1225"}.mdi-vector-polyline-minus::before{content:"\F1226"}.mdi-vector-polyline-plus::before{content:"\F1227"}.mdi-vector-polyline-remove::before{content:"\F1228"}.mdi-vector-radius::before{content:"\F074A"}.mdi-vector-rectangle::before{content:"\F05C6"}.mdi-vector-selection::before{content:"\F0562"}.mdi-vector-square::before{content:"\F0001"}.mdi-vector-square-close::before{content:"\F1857"}.mdi-vector-square-edit::before{content:"\F18D9"}.mdi-vector-square-minus::before{content:"\F18DA"}.mdi-vector-square-open::before{content:"\F1858"}.mdi-vector-square-plus::before{content:"\F18DB"}.mdi-vector-square-remove::before{content:"\F18DC"}.mdi-vector-triangle::before{content:"\F0563"}.mdi-vector-union::before{content:"\F0564"}.mdi-vhs::before{content:"\F0A1B"}.mdi-vibrate::before{content:"\F0566"}.mdi-vibrate-off::before{content:"\F0CD9"}.mdi-video::before{content:"\F0567"}.mdi-video-2d::before{content:"\F1A1C"}.mdi-video-3d::before{content:"\F07FD"}.mdi-video-3d-off::before{content:"\F13D9"}.mdi-video-3d-variant::before{content:"\F0ED1"}.mdi-video-4k-box::before{content:"\F083E"}.mdi-video-account::before{content:"\F0919"}.mdi-video-box::before{content:"\F00FD"}.mdi-video-box-off::before{content:"\F00FE"}.mdi-video-check::before{content:"\F1069"}.mdi-video-check-outline::before{content:"\F106A"}.mdi-video-high-definition::before{content:"\F152E"}.mdi-video-image::before{content:"\F091A"}.mdi-video-input-antenna::before{content:"\F083F"}.mdi-video-input-component::before{content:"\F0840"}.mdi-video-input-hdmi::before{content:"\F0841"}.mdi-video-input-scart::before{content:"\F0F8C"}.mdi-video-input-svideo::before{content:"\F0842"}.mdi-video-marker::before{content:"\F19A9"}.mdi-video-marker-outline::before{content:"\F19AA"}.mdi-video-minus::before{content:"\F09B2"}.mdi-video-minus-outline::before{content:"\F02BA"}.mdi-video-off::before{content:"\F0568"}.mdi-video-off-outline::before{content:"\F0BDB"}.mdi-video-outline::before{content:"\F0BDC"}.mdi-video-plus::before{content:"\F09B3"}.mdi-video-plus-outline::before{content:"\F01D3"}.mdi-video-stabilization::before{content:"\F091B"}.mdi-video-standard-definition::before{content:"\F1CA0"}.mdi-video-switch::before{content:"\F0569"}.mdi-video-switch-outline::before{content:"\F0790"}.mdi-video-vintage::before{content:"\F0A1C"}.mdi-video-wireless::before{content:"\F0ED2"}.mdi-video-wireless-outline::before{content:"\F0ED3"}.mdi-view-agenda::before{content:"\F056A"}.mdi-view-agenda-outline::before{content:"\F11D8"}.mdi-view-array::before{content:"\F056B"}.mdi-view-array-outline::before{content:"\F1485"}.mdi-view-carousel::before{content:"\F056C"}.mdi-view-carousel-outline::before{content:"\F1486"}.mdi-view-column::before{content:"\F056D"}.mdi-view-column-outline::before{content:"\F1487"}.mdi-view-comfy::before{content:"\F0E6A"}.mdi-view-comfy-outline::before{content:"\F1488"}.mdi-view-compact::before{content:"\F0E6B"}.mdi-view-compact-outline::before{content:"\F0E6C"}.mdi-view-dashboard::before{content:"\F056E"}.mdi-view-dashboard-edit::before{content:"\F1947"}.mdi-view-dashboard-edit-outline::before{content:"\F1948"}.mdi-view-dashboard-outline::before{content:"\F0A1D"}.mdi-view-dashboard-variant::before{content:"\F0843"}.mdi-view-dashboard-variant-outline::before{content:"\F1489"}.mdi-view-day::before{content:"\F056F"}.mdi-view-day-outline::before{content:"\F148A"}.mdi-view-gallery::before{content:"\F1888"}.mdi-view-gallery-outline::before{content:"\F1889"}.mdi-view-grid::before{content:"\F0570"}.mdi-view-grid-compact::before{content:"\F1C61"}.mdi-view-grid-outline::before{content:"\F11D9"}.mdi-view-grid-plus::before{content:"\F0F8D"}.mdi-view-grid-plus-outline::before{content:"\F11DA"}.mdi-view-headline::before{content:"\F0571"}.mdi-view-list::before{content:"\F0572"}.mdi-view-list-outline::before{content:"\F148B"}.mdi-view-module::before{content:"\F0573"}.mdi-view-module-outline::before{content:"\F148C"}.mdi-view-parallel::before{content:"\F0728"}.mdi-view-parallel-outline::before{content:"\F148D"}.mdi-view-quilt::before{content:"\F0574"}.mdi-view-quilt-outline::before{content:"\F148E"}.mdi-view-sequential::before{content:"\F0729"}.mdi-view-sequential-outline::before{content:"\F148F"}.mdi-view-split-horizontal::before{content:"\F0BCB"}.mdi-view-split-vertical::before{content:"\F0BCC"}.mdi-view-stream::before{content:"\F0575"}.mdi-view-stream-outline::before{content:"\F1490"}.mdi-view-week::before{content:"\F0576"}.mdi-view-week-outline::before{content:"\F1491"}.mdi-vimeo::before{content:"\F0577"}.mdi-violin::before{content:"\F060F"}.mdi-virtual-reality::before{content:"\F0894"}.mdi-virus::before{content:"\F13B6"}.mdi-virus-off::before{content:"\F18E1"}.mdi-virus-off-outline::before{content:"\F18E2"}.mdi-virus-outline::before{content:"\F13B7"}.mdi-vlc::before{content:"\F057C"}.mdi-voicemail::before{content:"\F057D"}.mdi-volcano::before{content:"\F1A83"}.mdi-volcano-outline::before{content:"\F1A84"}.mdi-volleyball::before{content:"\F09B4"}.mdi-volume-equal::before{content:"\F1B10"}.mdi-volume-high::before{content:"\F057E"}.mdi-volume-low::before{content:"\F057F"}.mdi-volume-medium::before{content:"\F0580"}.mdi-volume-minus::before{content:"\F075E"}.mdi-volume-mute::before{content:"\F075F"}.mdi-volume-off::before{content:"\F0581"}.mdi-volume-plus::before{content:"\F075D"}.mdi-volume-source::before{content:"\F1120"}.mdi-volume-variant-off::before{content:"\F0E08"}.mdi-volume-vibrate::before{content:"\F1121"}.mdi-vote::before{content:"\F0A1F"}.mdi-vote-outline::before{content:"\F0A20"}.mdi-vpn::before{content:"\F0582"}.mdi-vuejs::before{content:"\F0844"}.mdi-vuetify::before{content:"\F0E6D"}.mdi-walk::before{content:"\F0583"}.mdi-wall::before{content:"\F07FE"}.mdi-wall-fire::before{content:"\F1A11"}.mdi-wall-sconce::before{content:"\F091C"}.mdi-wall-sconce-flat::before{content:"\F091D"}.mdi-wall-sconce-flat-outline::before{content:"\F17C9"}.mdi-wall-sconce-flat-variant::before{content:"\F041C"}.mdi-wall-sconce-flat-variant-outline::before{content:"\F17CA"}.mdi-wall-sconce-outline::before{content:"\F17CB"}.mdi-wall-sconce-round::before{content:"\F0748"}.mdi-wall-sconce-round-outline::before{content:"\F17CC"}.mdi-wall-sconce-round-variant::before{content:"\F091E"}.mdi-wall-sconce-round-variant-outline::before{content:"\F17CD"}.mdi-wallet::before{content:"\F0584"}.mdi-wallet-bifold::before{content:"\F1C58"}.mdi-wallet-bifold-outline::before{content:"\F1C59"}.mdi-wallet-giftcard::before{content:"\F0585"}.mdi-wallet-membership::before{content:"\F0586"}.mdi-wallet-outline::before{content:"\F0BDD"}.mdi-wallet-plus::before{content:"\F0F8E"}.mdi-wallet-plus-outline::before{content:"\F0F8F"}.mdi-wallet-travel::before{content:"\F0587"}.mdi-wallpaper::before{content:"\F0E09"}.mdi-wan::before{content:"\F0588"}.mdi-wardrobe::before{content:"\F0F90"}.mdi-wardrobe-outline::before{content:"\F0F91"}.mdi-warehouse::before{content:"\F0F81"}.mdi-washing-machine::before{content:"\F072A"}.mdi-washing-machine-alert::before{content:"\F11BC"}.mdi-washing-machine-off::before{content:"\F11BD"}.mdi-watch::before{content:"\F0589"}.mdi-watch-export::before{content:"\F058A"}.mdi-watch-export-variant::before{content:"\F0895"}.mdi-watch-import::before{content:"\F058B"}.mdi-watch-import-variant::before{content:"\F0896"}.mdi-watch-variant::before{content:"\F0897"}.mdi-watch-vibrate::before{content:"\F06B1"}.mdi-watch-vibrate-off::before{content:"\F0CDA"}.mdi-water::before{content:"\F058C"}.mdi-water-alert::before{content:"\F1502"}.mdi-water-alert-outline::before{content:"\F1503"}.mdi-water-boiler::before{content:"\F0F92"}.mdi-water-boiler-alert::before{content:"\F11B3"}.mdi-water-boiler-auto::before{content:"\F1B98"}.mdi-water-boiler-off::before{content:"\F11B4"}.mdi-water-check::before{content:"\F1504"}.mdi-water-check-outline::before{content:"\F1505"}.mdi-water-circle::before{content:"\F1806"}.mdi-water-minus::before{content:"\F1506"}.mdi-water-minus-outline::before{content:"\F1507"}.mdi-water-off::before{content:"\F058D"}.mdi-water-off-outline::before{content:"\F1508"}.mdi-water-opacity::before{content:"\F1855"}.mdi-water-outline::before{content:"\F0E0A"}.mdi-water-percent::before{content:"\F058E"}.mdi-water-percent-alert::before{content:"\F1509"}.mdi-water-plus::before{content:"\F150A"}.mdi-water-plus-outline::before{content:"\F150B"}.mdi-water-polo::before{content:"\F12A0"}.mdi-water-pump::before{content:"\F058F"}.mdi-water-pump-off::before{content:"\F0F93"}.mdi-water-remove::before{content:"\F150C"}.mdi-water-remove-outline::before{content:"\F150D"}.mdi-water-sync::before{content:"\F17C6"}.mdi-water-thermometer::before{content:"\F1A85"}.mdi-water-thermometer-outline::before{content:"\F1A86"}.mdi-water-well::before{content:"\F106B"}.mdi-water-well-outline::before{content:"\F106C"}.mdi-waterfall::before{content:"\F1849"}.mdi-watering-can::before{content:"\F1481"}.mdi-watering-can-outline::before{content:"\F1482"}.mdi-watermark::before{content:"\F0612"}.mdi-wave::before{content:"\F0F2E"}.mdi-wave-arrow-down::before{content:"\F1CB0"}.mdi-wave-arrow-up::before{content:"\F1CB1"}.mdi-wave-undercurrent::before{content:"\F1CC0"}.mdi-waveform::before{content:"\F147D"}.mdi-waves::before{content:"\F078D"}.mdi-waves-arrow-left::before{content:"\F1859"}.mdi-waves-arrow-right::before{content:"\F185A"}.mdi-waves-arrow-up::before{content:"\F185B"}.mdi-waze::before{content:"\F0BDE"}.mdi-weather-cloudy::before{content:"\F0590"}.mdi-weather-cloudy-alert::before{content:"\F0F2F"}.mdi-weather-cloudy-arrow-right::before{content:"\F0E6E"}.mdi-weather-cloudy-clock::before{content:"\F18F6"}.mdi-weather-dust::before{content:"\F1B5A"}.mdi-weather-fog::before{content:"\F0591"}.mdi-weather-hail::before{content:"\F0592"}.mdi-weather-hazy::before{content:"\F0F30"}.mdi-weather-hurricane::before{content:"\F0898"}.mdi-weather-hurricane-outline::before{content:"\F1C78"}.mdi-weather-lightning::before{content:"\F0593"}.mdi-weather-lightning-rainy::before{content:"\F067E"}.mdi-weather-moonset::before{content:"\F1D15"}.mdi-weather-moonset-down::before{content:"\F1D16"}.mdi-weather-moonset-up::before{content:"\F1D17"}.mdi-weather-night::before{content:"\F0594"}.mdi-weather-night-partly-cloudy::before{content:"\F0F31"}.mdi-weather-partly-cloudy::before{content:"\F0595"}.mdi-weather-partly-lightning::before{content:"\F0F32"}.mdi-weather-partly-rainy::before{content:"\F0F33"}.mdi-weather-partly-snowy::before{content:"\F0F34"}.mdi-weather-partly-snowy-rainy::before{content:"\F0F35"}.mdi-weather-pouring::before{content:"\F0596"}.mdi-weather-rainy::before{content:"\F0597"}.mdi-weather-snowy::before{content:"\F0598"}.mdi-weather-snowy-heavy::before{content:"\F0F36"}.mdi-weather-snowy-rainy::before{content:"\F067F"}.mdi-weather-sunny::before{content:"\F0599"}.mdi-weather-sunny-alert::before{content:"\F0F37"}.mdi-weather-sunny-off::before{content:"\F14E4"}.mdi-weather-sunset::before{content:"\F059A"}.mdi-weather-sunset-down::before{content:"\F059B"}.mdi-weather-sunset-up::before{content:"\F059C"}.mdi-weather-tornado::before{content:"\F0F38"}.mdi-weather-windy::before{content:"\F059D"}.mdi-weather-windy-variant::before{content:"\F059E"}.mdi-web::before{content:"\F059F"}.mdi-web-box::before{content:"\F0F94"}.mdi-web-cancel::before{content:"\F1790"}.mdi-web-check::before{content:"\F0789"}.mdi-web-clock::before{content:"\F124A"}.mdi-web-minus::before{content:"\F10A0"}.mdi-web-off::before{content:"\F0A8E"}.mdi-web-plus::before{content:"\F0033"}.mdi-web-refresh::before{content:"\F1791"}.mdi-web-remove::before{content:"\F0551"}.mdi-web-sync::before{content:"\F1792"}.mdi-webcam::before{content:"\F05A0"}.mdi-webcam-off::before{content:"\F1737"}.mdi-webhook::before{content:"\F062F"}.mdi-webpack::before{content:"\F072B"}.mdi-webrtc::before{content:"\F1248"}.mdi-wechat::before{content:"\F0611"}.mdi-weight::before{content:"\F05A1"}.mdi-weight-gram::before{content:"\F0D3F"}.mdi-weight-kilogram::before{content:"\F05A2"}.mdi-weight-lifter::before{content:"\F115D"}.mdi-weight-pound::before{content:"\F09B5"}.mdi-whatsapp::before{content:"\F05A3"}.mdi-wheel-barrow::before{content:"\F14F2"}.mdi-wheelchair::before{content:"\F1A87"}.mdi-wheelchair-accessibility::before{content:"\F05A4"}.mdi-whistle::before{content:"\F09B6"}.mdi-whistle-outline::before{content:"\F12BC"}.mdi-white-balance-auto::before{content:"\F05A5"}.mdi-white-balance-incandescent::before{content:"\F05A6"}.mdi-white-balance-iridescent::before{content:"\F05A7"}.mdi-white-balance-sunny::before{content:"\F05A8"}.mdi-widgets::before{content:"\F072C"}.mdi-widgets-outline::before{content:"\F1355"}.mdi-wifi::before{content:"\F05A9"}.mdi-wifi-alert::before{content:"\F16B5"}.mdi-wifi-arrow-down::before{content:"\F16B6"}.mdi-wifi-arrow-left::before{content:"\F16B7"}.mdi-wifi-arrow-left-right::before{content:"\F16B8"}.mdi-wifi-arrow-right::before{content:"\F16B9"}.mdi-wifi-arrow-up::before{content:"\F16BA"}.mdi-wifi-arrow-up-down::before{content:"\F16BB"}.mdi-wifi-cancel::before{content:"\F16BC"}.mdi-wifi-check::before{content:"\F16BD"}.mdi-wifi-cog::before{content:"\F16BE"}.mdi-wifi-lock::before{content:"\F16BF"}.mdi-wifi-lock-open::before{content:"\F16C0"}.mdi-wifi-marker::before{content:"\F16C1"}.mdi-wifi-minus::before{content:"\F16C2"}.mdi-wifi-off::before{content:"\F05AA"}.mdi-wifi-plus::before{content:"\F16C3"}.mdi-wifi-refresh::before{content:"\F16C4"}.mdi-wifi-remove::before{content:"\F16C5"}.mdi-wifi-settings::before{content:"\F16C6"}.mdi-wifi-star::before{content:"\F0E0B"}.mdi-wifi-strength-1::before{content:"\F091F"}.mdi-wifi-strength-1-alert::before{content:"\F0920"}.mdi-wifi-strength-1-lock::before{content:"\F0921"}.mdi-wifi-strength-1-lock-open::before{content:"\F16CB"}.mdi-wifi-strength-2::before{content:"\F0922"}.mdi-wifi-strength-2-alert::before{content:"\F0923"}.mdi-wifi-strength-2-lock::before{content:"\F0924"}.mdi-wifi-strength-2-lock-open::before{content:"\F16CC"}.mdi-wifi-strength-3::before{content:"\F0925"}.mdi-wifi-strength-3-alert::before{content:"\F0926"}.mdi-wifi-strength-3-lock::before{content:"\F0927"}.mdi-wifi-strength-3-lock-open::before{content:"\F16CD"}.mdi-wifi-strength-4::before{content:"\F0928"}.mdi-wifi-strength-4-alert::before{content:"\F0929"}.mdi-wifi-strength-4-lock::before{content:"\F092A"}.mdi-wifi-strength-4-lock-open::before{content:"\F16CE"}.mdi-wifi-strength-alert-outline::before{content:"\F092B"}.mdi-wifi-strength-lock-open-outline::before{content:"\F16CF"}.mdi-wifi-strength-lock-outline::before{content:"\F092C"}.mdi-wifi-strength-off::before{content:"\F092D"}.mdi-wifi-strength-off-outline::before{content:"\F092E"}.mdi-wifi-strength-outline::before{content:"\F092F"}.mdi-wifi-sync::before{content:"\F16C7"}.mdi-wikipedia::before{content:"\F05AC"}.mdi-wind-power::before{content:"\F1A88"}.mdi-wind-power-outline::before{content:"\F1A89"}.mdi-wind-turbine::before{content:"\F0DA5"}.mdi-wind-turbine-alert::before{content:"\F19AB"}.mdi-wind-turbine-check::before{content:"\F19AC"}.mdi-window-close::before{content:"\F05AD"}.mdi-window-closed::before{content:"\F05AE"}.mdi-window-closed-variant::before{content:"\F11DB"}.mdi-window-maximize::before{content:"\F05AF"}.mdi-window-minimize::before{content:"\F05B0"}.mdi-window-open::before{content:"\F05B1"}.mdi-window-open-variant::before{content:"\F11DC"}.mdi-window-restore::before{content:"\F05B2"}.mdi-window-shutter::before{content:"\F111C"}.mdi-window-shutter-alert::before{content:"\F111D"}.mdi-window-shutter-auto::before{content:"\F1BA3"}.mdi-window-shutter-cog::before{content:"\F1A8A"}.mdi-window-shutter-open::before{content:"\F111E"}.mdi-window-shutter-settings::before{content:"\F1A8B"}.mdi-windsock::before{content:"\F15FA"}.mdi-wiper::before{content:"\F0AE9"}.mdi-wiper-wash::before{content:"\F0DA6"}.mdi-wiper-wash-alert::before{content:"\F18DF"}.mdi-wizard-hat::before{content:"\F1477"}.mdi-wordpress::before{content:"\F05B4"}.mdi-wrap::before{content:"\F05B6"}.mdi-wrap-disabled::before{content:"\F0BDF"}.mdi-wrench::before{content:"\F05B7"}.mdi-wrench-check::before{content:"\F1B8F"}.mdi-wrench-check-outline::before{content:"\F1B90"}.mdi-wrench-clock::before{content:"\F19A3"}.mdi-wrench-clock-outline::before{content:"\F1B93"}.mdi-wrench-cog::before{content:"\F1B91"}.mdi-wrench-cog-outline::before{content:"\F1B92"}.mdi-wrench-outline::before{content:"\F0BE0"}.mdi-xamarin::before{content:"\F0845"}.mdi-xml::before{content:"\F05C0"}.mdi-xmpp::before{content:"\F07FF"}.mdi-yahoo::before{content:"\F0B4F"}.mdi-yeast::before{content:"\F05C1"}.mdi-yin-yang::before{content:"\F0680"}.mdi-yoga::before{content:"\F117C"}.mdi-youtube::before{content:"\F05C3"}.mdi-youtube-gaming::before{content:"\F0848"}.mdi-youtube-studio::before{content:"\F0847"}.mdi-youtube-subscription::before{content:"\F0D40"}.mdi-youtube-tv::before{content:"\F0448"}.mdi-yurt::before{content:"\F1516"}.mdi-z-wave::before{content:"\F0AEA"}.mdi-zend::before{content:"\F0AEB"}.mdi-zigbee::before{content:"\F0D41"}.mdi-zip-box::before{content:"\F05C4"}.mdi-zip-box-outline::before{content:"\F0FFA"}.mdi-zip-disk::before{content:"\F0A23"}.mdi-zodiac-aquarius::before{content:"\F0A7D"}.mdi-zodiac-aries::before{content:"\F0A7E"}.mdi-zodiac-cancer::before{content:"\F0A7F"}.mdi-zodiac-capricorn::before{content:"\F0A80"}.mdi-zodiac-gemini::before{content:"\F0A81"}.mdi-zodiac-leo::before{content:"\F0A82"}.mdi-zodiac-libra::before{content:"\F0A83"}.mdi-zodiac-pisces::before{content:"\F0A84"}.mdi-zodiac-sagittarius::before{content:"\F0A85"}.mdi-zodiac-scorpio::before{content:"\F0A86"}.mdi-zodiac-taurus::before{content:"\F0A87"}.mdi-zodiac-virgo::before{content:"\F0A88"}.mdi-blank::before{content:"\F68C";visibility:hidden}.mdi-18px.mdi-set,.mdi-18px.mdi:before{font-size:18px}.mdi-24px.mdi-set,.mdi-24px.mdi:before{font-size:24px}.mdi-36px.mdi-set,.mdi-36px.mdi:before{font-size:36px}.mdi-48px.mdi-set,.mdi-48px.mdi:before{font-size:48px}.mdi-dark:before{color:rgba(0,0,0,0.54)}.mdi-dark.mdi-inactive:before{color:rgba(0,0,0,0.26)}.mdi-light:before{color:#fff}.mdi-light.mdi-inactive:before{color:rgba(255,255,255,0.3)}.mdi-rotate-45:before{-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.mdi-rotate-90:before{-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.mdi-rotate-135:before{-webkit-transform:rotate(135deg);-ms-transform:rotate(135deg);transform:rotate(135deg)}.mdi-rotate-180:before{-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.mdi-rotate-225:before{-webkit-transform:rotate(225deg);-ms-transform:rotate(225deg);transform:rotate(225deg)}.mdi-rotate-270:before{-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.mdi-rotate-315:before{-webkit-transform:rotate(315deg);-ms-transform:rotate(315deg);transform:rotate(315deg)}.mdi-flip-h:before{-webkit-transform:scaleX(-1);transform:scaleX(-1);filter:FlipH;-ms-filter:"FlipH"}.mdi-flip-v:before{-webkit-transform:scaleY(-1);transform:scaleY(-1);filter:FlipV;-ms-filter:"FlipV"}.mdi-spin:before{-webkit-animation:mdi-spin 2s infinite linear;animation:mdi-spin 2s infinite linear}@-webkit-keyframes mdi-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes mdi-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}} - -/*# sourceMappingURL=materialdesignicons.css.map */ \ No newline at end of file diff --git a/web/scripts/api.js b/web/scripts/api.js deleted file mode 100644 index 03c3fb607..000000000 --- a/web/scripts/api.js +++ /dev/null @@ -1,482 +0,0 @@ -class ComfyApi extends EventTarget { - #registered = new Set(); - - constructor() { - super(); - this.api_host = location.host; - this.api_base = location.pathname.split('/').slice(0, -1).join('/'); - this.initialClientId = sessionStorage.getItem("clientId"); - } - - apiURL(route) { - return this.api_base + route; - } - - fetchApi(route, options) { - if (!options) { - options = {}; - } - if (!options.headers) { - options.headers = {}; - } - options.headers["Comfy-User"] = this.user; - return fetch(this.apiURL(route), options); - } - - addEventListener(type, callback, options) { - super.addEventListener(type, callback, options); - this.#registered.add(type); - } - - /** - * Poll status for colab and other things that don't support websockets. - */ - #pollQueue() { - setInterval(async () => { - try { - const resp = await this.fetchApi("/prompt"); - const status = await resp.json(); - this.dispatchEvent(new CustomEvent("status", { detail: status })); - } catch (error) { - this.dispatchEvent(new CustomEvent("status", { detail: null })); - } - }, 1000); - } - - /** - * Creates and connects a WebSocket for realtime updates - * @param {boolean} isReconnect If the socket is connection is a reconnect attempt - */ - #createSocket(isReconnect) { - if (this.socket) { - return; - } - - let opened = false; - let existingSession = window.name; - if (existingSession) { - existingSession = "?clientId=" + existingSession; - } - this.socket = new WebSocket( - `ws${window.location.protocol === "https:" ? "s" : ""}://${this.api_host}${this.api_base}/ws${existingSession}` - ); - this.socket.binaryType = "arraybuffer"; - - this.socket.addEventListener("open", () => { - opened = true; - if (isReconnect) { - this.dispatchEvent(new CustomEvent("reconnected")); - } - }); - - this.socket.addEventListener("error", () => { - if (this.socket) this.socket.close(); - if (!isReconnect && !opened) { - this.#pollQueue(); - } - }); - - this.socket.addEventListener("close", () => { - setTimeout(() => { - this.socket = null; - this.#createSocket(true); - }, 300); - if (opened) { - this.dispatchEvent(new CustomEvent("status", { detail: null })); - this.dispatchEvent(new CustomEvent("reconnecting")); - } - }); - - this.socket.addEventListener("message", (event) => { - try { - if (event.data instanceof ArrayBuffer) { - const view = new DataView(event.data); - const eventType = view.getUint32(0); - const buffer = event.data.slice(4); - switch (eventType) { - case 1: - const view2 = new DataView(event.data); - const imageType = view2.getUint32(0) - let imageMime - switch (imageType) { - case 1: - default: - imageMime = "image/jpeg"; - break; - case 2: - imageMime = "image/png" - } - const imageBlob = new Blob([buffer.slice(4)], { type: imageMime }); - this.dispatchEvent(new CustomEvent("b_preview", { detail: imageBlob })); - break; - default: - throw new Error(`Unknown binary websocket message of type ${eventType}`); - } - } - else { - const msg = JSON.parse(event.data); - switch (msg.type) { - case "status": - if (msg.data.sid) { - this.clientId = msg.data.sid; - window.name = this.clientId; // use window name so it isnt reused when duplicating tabs - sessionStorage.setItem("clientId", this.clientId); // store in session storage so duplicate tab can load correct workflow - } - this.dispatchEvent(new CustomEvent("status", { detail: msg.data.status })); - break; - case "progress": - this.dispatchEvent(new CustomEvent("progress", { detail: msg.data })); - break; - case "executing": - this.dispatchEvent(new CustomEvent("executing", { detail: msg.data.node })); - break; - case "executed": - this.dispatchEvent(new CustomEvent("executed", { detail: msg.data })); - break; - case "execution_start": - this.dispatchEvent(new CustomEvent("execution_start", { detail: msg.data })); - break; - case "execution_success": - this.dispatchEvent(new CustomEvent("execution_success", { detail: msg.data })); - break; - case "execution_error": - this.dispatchEvent(new CustomEvent("execution_error", { detail: msg.data })); - break; - case "execution_cached": - this.dispatchEvent(new CustomEvent("execution_cached", { detail: msg.data })); - break; - default: - if (this.#registered.has(msg.type)) { - this.dispatchEvent(new CustomEvent(msg.type, { detail: msg.data })); - } else { - throw new Error(`Unknown message type ${msg.type}`); - } - } - } - } catch (error) { - console.warn("Unhandled message:", event.data, error); - } - }); - } - - /** - * Initialises sockets and realtime updates - */ - init() { - this.#createSocket(); - } - - /** - * Gets a list of extension urls - * @returns An array of script urls to import - */ - async getExtensions() { - const resp = await this.fetchApi("/extensions", { cache: "no-store" }); - return await resp.json(); - } - - /** - * Gets a list of embedding names - * @returns An array of script urls to import - */ - async getEmbeddings() { - const resp = await this.fetchApi("/embeddings", { cache: "no-store" }); - return await resp.json(); - } - - /** - * Loads node object definitions for the graph - * @returns The node definitions - */ - async getNodeDefs() { - const resp = await this.fetchApi("/object_info", { cache: "no-store" }); - return await resp.json(); - } - - /** - * - * @param {number} number The index at which to queue the prompt, passing -1 will insert the prompt at the front of the queue - * @param {object} prompt The prompt data to queue - */ - async queuePrompt(number, { output, workflow }) { - const body = { - client_id: this.clientId, - prompt: output, - extra_data: { extra_pnginfo: { workflow } }, - }; - - if (number === -1) { - body.front = true; - } else if (number != 0) { - body.number = number; - } - - const res = await this.fetchApi("/prompt", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(body), - }); - - if (res.status !== 200) { - throw { - response: await res.json(), - }; - } - - return await res.json(); - } - - /** - * Loads a list of items (queue or history) - * @param {string} type The type of items to load, queue or history - * @returns The items of the specified type grouped by their status - */ - async getItems(type) { - if (type === "queue") { - return this.getQueue(); - } - return this.getHistory(); - } - - /** - * Gets the current state of the queue - * @returns The currently running and queued items - */ - async getQueue() { - try { - const res = await this.fetchApi("/queue"); - const data = await res.json(); - return { - // Running action uses a different endpoint for cancelling - Running: data.queue_running.map((prompt) => ({ - prompt, - remove: { name: "Cancel", cb: () => api.interrupt() }, - })), - Pending: data.queue_pending.map((prompt) => ({ prompt })), - }; - } catch (error) { - console.error(error); - return { Running: [], Pending: [] }; - } - } - - /** - * Gets the prompt execution history - * @returns Prompt history including node outputs - */ - async getHistory(max_items=200) { - try { - const res = await this.fetchApi(`/history?max_items=${max_items}`); - return { History: Object.values(await res.json()) }; - } catch (error) { - console.error(error); - return { History: [] }; - } - } - - /** - * Gets system & device stats - * @returns System stats such as python version, OS, per device info - */ - async getSystemStats() { - const res = await this.fetchApi("/system_stats"); - return await res.json(); - } - - /** - * Sends a POST request to the API - * @param {*} type The endpoint to post to - * @param {*} body Optional POST data - */ - async #postItem(type, body) { - try { - await this.fetchApi("/" + type, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: body ? JSON.stringify(body) : undefined, - }); - } catch (error) { - console.error(error); - } - } - - /** - * Deletes an item from the specified list - * @param {string} type The type of item to delete, queue or history - * @param {number} id The id of the item to delete - */ - async deleteItem(type, id) { - await this.#postItem(type, { delete: [id] }); - } - - /** - * Clears the specified list - * @param {string} type The type of list to clear, queue or history - */ - async clearItems(type) { - await this.#postItem(type, { clear: true }); - } - - /** - * Interrupts the execution of the running prompt - */ - async interrupt() { - await this.#postItem("interrupt", null); - } - - /** - * Gets user configuration data and where data should be stored - * @returns { Promise<{ storage: "server" | "browser", users?: Promise, migrated?: boolean }> } - */ - async getUserConfig() { - return (await this.fetchApi("/users")).json(); - } - - /** - * Creates a new user - * @param { string } username - * @returns The fetch response - */ - createUser(username) { - return this.fetchApi("/users", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ username }), - }); - } - - /** - * Gets all setting values for the current user - * @returns { Promise } A dictionary of id -> value - */ - async getSettings() { - return (await this.fetchApi("/settings")).json(); - } - - /** - * Gets a setting for the current user - * @param { string } id The id of the setting to fetch - * @returns { Promise } The setting value - */ - async getSetting(id) { - return (await this.fetchApi(`/settings/${encodeURIComponent(id)}`)).json(); - } - - /** - * Stores a dictionary of settings for the current user - * @param { Record } settings Dictionary of setting id -> value to save - * @returns { Promise } - */ - async storeSettings(settings) { - return this.fetchApi(`/settings`, { - method: "POST", - body: JSON.stringify(settings) - }); - } - - /** - * Stores a setting for the current user - * @param { string } id The id of the setting to update - * @param { unknown } value The value of the setting - * @returns { Promise } - */ - async storeSetting(id, value) { - return this.fetchApi(`/settings/${encodeURIComponent(id)}`, { - method: "POST", - body: JSON.stringify(value) - }); - } - - /** - * Gets a user data file for the current user - * @param { string } file The name of the userdata file to load - * @param { RequestInit } [options] - * @returns { Promise } The fetch response object - */ - async getUserData(file, options) { - return this.fetchApi(`/userdata/${encodeURIComponent(file)}`, options); - } - - /** - * Stores a user data file for the current user - * @param { string } file The name of the userdata file to save - * @param { unknown } data The data to save to the file - * @param { RequestInit & { overwrite?: boolean, stringify?: boolean, throwOnError?: boolean } } [options] - * @returns { Promise } - */ - async storeUserData(file, data, options = { overwrite: true, stringify: true, throwOnError: true }) { - const resp = await this.fetchApi(`/userdata/${encodeURIComponent(file)}?overwrite=${options?.overwrite}`, { - method: "POST", - body: options?.stringify ? JSON.stringify(data) : data, - ...options, - }); - if (resp.status !== 200 && options?.throwOnError !== false) { - throw new Error(`Error storing user data file '${file}': ${resp.status} ${(await resp).statusText}`); - } - return resp; - } - - /** - * Deletes a user data file for the current user - * @param { string } file The name of the userdata file to delete - */ - async deleteUserData(file) { - const resp = await this.fetchApi(`/userdata/${encodeURIComponent(file)}`, { - method: "DELETE", - }); - if (resp.status !== 204) { - throw new Error(`Error removing user data file '${file}': ${resp.status} ${(resp).statusText}`); - } - } - - /** - * Move a user data file for the current user - * @param { string } source The userdata file to move - * @param { string } dest The destination for the file - */ - async moveUserData(source, dest, options = { overwrite: false }) { - const resp = await this.fetchApi(`/userdata/${encodeURIComponent(source)}/move/${encodeURIComponent(dest)}?overwrite=${options?.overwrite}`, { - method: "POST", - }); - return resp; - } - - /** - * @overload - * Lists user data files for the current user - * @param { string } dir The directory in which to list files - * @param { boolean } [recurse] If the listing should be recursive - * @param { true } [split] If the paths should be split based on the os path separator - * @returns { Promise> } The list of split file paths in the format [fullPath, ...splitPath] - */ - /** - * @overload - * Lists user data files for the current user - * @param { string } dir The directory in which to list files - * @param { boolean } [recurse] If the listing should be recursive - * @param { false | undefined } [split] If the paths should be split based on the os path separator - * @returns { Promise> } The list of files - */ - async listUserData(dir, recurse, split) { - const resp = await this.fetchApi( - `/userdata?${new URLSearchParams({ - recurse, - dir, - split, - })}` - ); - if (resp.status === 404) return []; - if (resp.status !== 200) { - throw new Error(`Error getting user data list '${dir}': ${resp.status} ${resp.statusText}`); - } - return resp.json(); - } -} - -export const api = new ComfyApi(); diff --git a/web/scripts/app.js b/web/scripts/app.js deleted file mode 100644 index 8b4478a32..000000000 --- a/web/scripts/app.js +++ /dev/null @@ -1,2459 +0,0 @@ -import { ComfyLogging } from "./logging.js"; -import { ComfyWidgets, initWidgets } from "./widgets.js"; -import { ComfyUI, $el } from "./ui.js"; -import { api } from "./api.js"; -import { defaultGraph } from "./defaultGraph.js"; -import { getPngMetadata, getWebpMetadata, getFlacMetadata, importA1111, getLatentMetadata } from "./pnginfo.js"; -import { addDomClippingSetting } from "./domWidget.js"; -import { createImageHost, calculateImageGrid } from "./ui/imagePreview.js"; -import { ComfyAppMenu } from "./ui/menu/index.js"; -import { getStorageValue, setStorageValue } from "./utils.js"; -import { ComfyWorkflowManager } from "./workflows.js"; -export const ANIM_PREVIEW_WIDGET = "$$comfy_animation_preview"; - -function sanitizeNodeName(string) { - let entityMap = { - '&': '', - '<': '', - '>': '', - '"': '', - "'": '', - '`': '', - '=': '' - }; - return String(string).replace(/[&<>"'`=]/g, function fromEntityMap (s) { - return entityMap[s]; - }); -} - -/** - * @typedef {import("types/comfy").ComfyExtension} ComfyExtension - */ - -export class ComfyApp { - /** - * List of entries to queue - * @type {{number: number, batchCount: number}[]} - */ - #queueItems = []; - /** - * If the queue is currently being processed - * @type {boolean} - */ - #processingQueue = false; - - /** - * Content Clipboard - * @type {serialized node object} - */ - static clipspace = null; - static clipspace_invalidate_handler = null; - static open_maskeditor = null; - static clipspace_return_node = null; - - constructor() { - this.ui = new ComfyUI(this); - this.logging = new ComfyLogging(this); - this.workflowManager = new ComfyWorkflowManager(this); - this.bodyTop = $el("div.comfyui-body-top", { parent: document.body }); - this.bodyLeft = $el("div.comfyui-body-left", { parent: document.body }); - this.bodyRight = $el("div.comfyui-body-right", { parent: document.body }); - this.bodyBottom = $el("div.comfyui-body-bottom", { parent: document.body }); - this.menu = new ComfyAppMenu(this); - - /** - * List of extensions that are registered with the app - * @type {ComfyExtension[]} - */ - this.extensions = []; - - /** - * Stores the execution output data for each node - * @type {Record} - */ - this._nodeOutputs = {}; - - /** - * Stores the preview image data for each node - * @type {Record} - */ - this.nodePreviewImages = {}; - - /** - * If the shift key on the keyboard is pressed - * @type {boolean} - */ - this.shiftDown = false; - } - - get nodeOutputs() { - return this._nodeOutputs; - } - - set nodeOutputs(value) { - this._nodeOutputs = value; - this.#invokeExtensions("onNodeOutputsUpdated", value); - } - - getPreviewFormatParam() { - let preview_format = this.ui.settings.getSettingValue("Comfy.PreviewFormat"); - if(preview_format) - return `&preview=${preview_format}`; - else - return ""; - } - - getRandParam() { - return "&rand=" + Math.random(); - } - - static isImageNode(node) { - return node.imgs || (node && node.widgets && node.widgets.findIndex(obj => obj.name === 'image') >= 0); - } - - static onClipspaceEditorSave() { - if(ComfyApp.clipspace_return_node) { - ComfyApp.pasteFromClipspace(ComfyApp.clipspace_return_node); - } - } - - static onClipspaceEditorClosed() { - ComfyApp.clipspace_return_node = null; - } - - static copyToClipspace(node) { - var widgets = null; - if(node.widgets) { - widgets = node.widgets.map(({ type, name, value }) => ({ type, name, value })); - } - - var imgs = undefined; - var orig_imgs = undefined; - if(node.imgs != undefined) { - imgs = []; - orig_imgs = []; - - for (let i = 0; i < node.imgs.length; i++) { - imgs[i] = new Image(); - imgs[i].src = node.imgs[i].src; - orig_imgs[i] = imgs[i]; - } - } - - var selectedIndex = 0; - if(node.imageIndex) { - selectedIndex = node.imageIndex; - } - - ComfyApp.clipspace = { - 'widgets': widgets, - 'imgs': imgs, - 'original_imgs': orig_imgs, - 'images': node.images, - 'selectedIndex': selectedIndex, - 'img_paste_mode': 'selected' // reset to default im_paste_mode state on copy action - }; - - ComfyApp.clipspace_return_node = null; - - if(ComfyApp.clipspace_invalidate_handler) { - ComfyApp.clipspace_invalidate_handler(); - } - } - - static pasteFromClipspace(node) { - if(ComfyApp.clipspace) { - // image paste - if(ComfyApp.clipspace.imgs && node.imgs) { - if(node.images && ComfyApp.clipspace.images) { - if(ComfyApp.clipspace['img_paste_mode'] == 'selected') { - node.images = [ComfyApp.clipspace.images[ComfyApp.clipspace['selectedIndex']]]; - } - else { - node.images = ComfyApp.clipspace.images; - } - - if(app.nodeOutputs[node.id + ""]) - app.nodeOutputs[node.id + ""].images = node.images; - } - - if(ComfyApp.clipspace.imgs) { - // deep-copy to cut link with clipspace - if(ComfyApp.clipspace['img_paste_mode'] == 'selected') { - const img = new Image(); - img.src = ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src; - node.imgs = [img]; - node.imageIndex = 0; - } - else { - const imgs = []; - for(let i=0; i obj.name === 'image'); - if(index >= 0) { - if(node.widgets[index].type != 'image' && typeof node.widgets[index].value == "string" && clip_image.filename) { - node.widgets[index].value = (clip_image.subfolder?clip_image.subfolder+'/':'') + clip_image.filename + (clip_image.type?` [${clip_image.type}]`:''); - } - else { - node.widgets[index].value = clip_image; - } - } - } - if(ComfyApp.clipspace.widgets) { - ComfyApp.clipspace.widgets.forEach(({ type, name, value }) => { - const prop = Object.values(node.widgets).find(obj => obj.type === type && obj.name === name); - if (prop && prop.type != 'button') { - if(prop.type != 'image' && typeof prop.value == "string" && value.filename) { - prop.value = (value.subfolder?value.subfolder+'/':'') + value.filename + (value.type?` [${value.type}]`:''); - } - else { - prop.value = value; - prop.callback(value); - } - } - }); - } - } - - app.graph.setDirtyCanvas(true); - } - } - - /** - * Invoke an extension callback - * @param {keyof ComfyExtension} method The extension callback to execute - * @param {any[]} args Any arguments to pass to the callback - * @returns - */ - #invokeExtensions(method, ...args) { - let results = []; - for (const ext of this.extensions) { - if (method in ext) { - try { - results.push(ext[method](...args, this)); - } catch (error) { - console.error( - `Error calling extension '${ext.name}' method '${method}'`, - { error }, - { extension: ext }, - { args } - ); - } - } - } - return results; - } - - /** - * Invoke an async extension callback - * Each callback will be invoked concurrently - * @param {string} method The extension callback to execute - * @param {...any} args Any arguments to pass to the callback - * @returns - */ - async #invokeExtensionsAsync(method, ...args) { - return await Promise.all( - this.extensions.map(async (ext) => { - if (method in ext) { - try { - return await ext[method](...args, this); - } catch (error) { - console.error( - `Error calling extension '${ext.name}' method '${method}'`, - { error }, - { extension: ext }, - { args } - ); - } - } - }) - ); - } - - #addRestoreWorkflowView() { - const serialize = LGraph.prototype.serialize; - const self = this; - LGraph.prototype.serialize = function() { - const workflow = serialize.apply(this, arguments); - - // Store the drag & scale info in the serialized workflow if the setting is enabled - if (self.enableWorkflowViewRestore.value) { - if (!workflow.extra) { - workflow.extra = {}; - } - workflow.extra.ds = { - scale: self.canvas.ds.scale, - offset: self.canvas.ds.offset, - }; - } else if (workflow.extra?.ds) { - // Clear any old view data - delete workflow.extra.ds; - } - - return workflow; - } - this.enableWorkflowViewRestore = this.ui.settings.addSetting({ - id: "Comfy.EnableWorkflowViewRestore", - name: "Save and restore canvas position and zoom level in workflows", - type: "boolean", - defaultValue: true - }); - } - - /** - * Adds special context menu handling for nodes - * e.g. this adds Open Image functionality for nodes that show images - * @param {*} node The node to add the menu handler - */ - #addNodeContextMenuHandler(node) { - function getCopyImageOption(img) { - if (typeof window.ClipboardItem === "undefined") return []; - return [ - { - content: "Copy Image", - callback: async () => { - const url = new URL(img.src); - url.searchParams.delete("preview"); - - const writeImage = async (blob) => { - await navigator.clipboard.write([ - new ClipboardItem({ - [blob.type]: blob, - }), - ]); - }; - - try { - const data = await fetch(url); - const blob = await data.blob(); - try { - await writeImage(blob); - } catch (error) { - // Chrome seems to only support PNG on write, convert and try again - if (blob.type !== "image/png") { - const canvas = $el("canvas", { - width: img.naturalWidth, - height: img.naturalHeight, - }); - const ctx = canvas.getContext("2d"); - let image; - if (typeof window.createImageBitmap === "undefined") { - image = new Image(); - const p = new Promise((resolve, reject) => { - image.onload = resolve; - image.onerror = reject; - }).finally(() => { - URL.revokeObjectURL(image.src); - }); - image.src = URL.createObjectURL(blob); - await p; - } else { - image = await createImageBitmap(blob); - } - try { - ctx.drawImage(image, 0, 0); - canvas.toBlob(writeImage, "image/png"); - } finally { - if (typeof image.close === "function") { - image.close(); - } - } - - return; - } - throw error; - } - } catch (error) { - alert("Error copying image: " + (error.message ?? error)); - } - }, - }, - ]; - } - - node.prototype.getExtraMenuOptions = function (_, options) { - if (this.imgs) { - // If this node has images then we add an open in new tab item - let img; - if (this.imageIndex != null) { - // An image is selected so select that - img = this.imgs[this.imageIndex]; - } else if (this.overIndex != null) { - // No image is selected but one is hovered - img = this.imgs[this.overIndex]; - } - if (img) { - options.unshift( - { - content: "Open Image", - callback: () => { - let url = new URL(img.src); - url.searchParams.delete("preview"); - window.open(url, "_blank"); - }, - }, - ...getCopyImageOption(img), - { - content: "Save Image", - callback: () => { - const a = document.createElement("a"); - let url = new URL(img.src); - url.searchParams.delete("preview"); - a.href = url; - a.setAttribute("download", new URLSearchParams(url.search).get("filename")); - document.body.append(a); - a.click(); - requestAnimationFrame(() => a.remove()); - }, - } - ); - } - } - - options.push({ - content: "Bypass", - callback: (obj) => { - if (this.mode === 4) this.mode = 0; - else this.mode = 4; - this.graph.change(); - }, - }); - - // prevent conflict of clipspace content - if (!ComfyApp.clipspace_return_node) { - options.push({ - content: "Copy (Clipspace)", - callback: (obj) => { - ComfyApp.copyToClipspace(this); - }, - }); - - if (ComfyApp.clipspace != null) { - options.push({ - content: "Paste (Clipspace)", - callback: () => { - ComfyApp.pasteFromClipspace(this); - }, - }); - } - - if (ComfyApp.isImageNode(this)) { - options.push({ - content: "Open in MaskEditor", - callback: (obj) => { - ComfyApp.copyToClipspace(this); - ComfyApp.clipspace_return_node = this; - ComfyApp.open_maskeditor(); - }, - }); - } - } - }; - } - - #addNodeKeyHandler(node) { - const app = this; - const origNodeOnKeyDown = node.prototype.onKeyDown; - - node.prototype.onKeyDown = function(e) { - if (origNodeOnKeyDown && origNodeOnKeyDown.apply(this, e) === false) { - return false; - } - - if (this.flags.collapsed || !this.imgs || this.imageIndex === null) { - return; - } - - let handled = false; - - if (e.key === "ArrowLeft" || e.key === "ArrowRight") { - if (e.key === "ArrowLeft") { - this.imageIndex -= 1; - } else if (e.key === "ArrowRight") { - this.imageIndex += 1; - } - this.imageIndex %= this.imgs.length; - - if (this.imageIndex < 0) { - this.imageIndex = this.imgs.length + this.imageIndex; - } - handled = true; - } else if (e.key === "Escape") { - this.imageIndex = null; - handled = true; - } - - if (handled === true) { - e.preventDefault(); - e.stopImmediatePropagation(); - return false; - } - } - } - - /** - * Adds Custom drawing logic for nodes - * e.g. Draws images and handles thumbnail navigation on nodes that output images - * @param {*} node The node to add the draw handler - */ - #addDrawBackgroundHandler(node) { - const app = this; - - function getImageTop(node) { - let shiftY; - if (node.imageOffset != null) { - shiftY = node.imageOffset; - } else { - if (node.widgets?.length) { - const w = node.widgets[node.widgets.length - 1]; - shiftY = w.last_y; - if (w.computeSize) { - shiftY += w.computeSize()[1] + 4; - } - else if(w.computedHeight) { - shiftY += w.computedHeight; - } - else { - shiftY += LiteGraph.NODE_WIDGET_HEIGHT + 4; - } - } else { - shiftY = node.computeSize()[1]; - } - } - return shiftY; - } - - node.prototype.setSizeForImage = function (force) { - if(!force && this.animatedImages) return; - - if (this.inputHeight || this.freeWidgetSpace > 210) { - this.setSize(this.size); - return; - } - const minHeight = getImageTop(this) + 220; - if (this.size[1] < minHeight) { - this.setSize([this.size[0], minHeight]); - } - }; - - node.prototype.onDrawBackground = function (ctx) { - if (!this.flags.collapsed) { - let imgURLs = [] - let imagesChanged = false - - const output = app.nodeOutputs[this.id + ""]; - if (output?.images) { - this.animatedImages = output?.animated?.find(Boolean); - if (this.images !== output.images) { - this.images = output.images; - imagesChanged = true; - imgURLs = imgURLs.concat( - output.images.map((params) => { - return api.apiURL( - "/view?" + - new URLSearchParams(params).toString() + - (this.animatedImages ? "" : app.getPreviewFormatParam()) + app.getRandParam() - ); - }) - ); - } - } - - const preview = app.nodePreviewImages[this.id + ""] - if (this.preview !== preview) { - this.preview = preview - imagesChanged = true; - if (preview != null) { - imgURLs.push(preview); - } - } - - if (imagesChanged) { - this.imageIndex = null; - if (imgURLs.length > 0) { - Promise.all( - imgURLs.map((src) => { - return new Promise((r) => { - const img = new Image(); - img.onload = () => r(img); - img.onerror = () => r(null); - img.src = src - }); - }) - ).then((imgs) => { - if ((!output || this.images === output.images) && (!preview || this.preview === preview)) { - this.imgs = imgs.filter(Boolean); - this.setSizeForImage?.(); - app.graph.setDirtyCanvas(true); - } - }); - } - else { - this.imgs = null; - } - } - - function calculateGrid(w, h, n) { - let columns, rows, cellsize; - - if (w > h) { - cellsize = h; - columns = Math.ceil(w / cellsize); - rows = Math.ceil(n / columns); - } else { - cellsize = w; - rows = Math.ceil(h / cellsize); - columns = Math.ceil(n / rows); - } - - while (columns * rows < n) { - cellsize++; - if (w >= h) { - columns = Math.ceil(w / cellsize); - rows = Math.ceil(n / columns); - } else { - rows = Math.ceil(h / cellsize); - columns = Math.ceil(n / rows); - } - } - - const cell_size = Math.min(w/columns, h/rows); - return {cell_size, columns, rows}; - } - - function is_all_same_aspect_ratio(imgs) { - // assume: imgs.length >= 2 - let ratio = imgs[0].naturalWidth/imgs[0].naturalHeight; - - for(let i=1; i w.name === ANIM_PREVIEW_WIDGET); - - if(this.animatedImages) { - // Instead of using the canvas we'll use a IMG - if(widgetIdx > -1) { - // Replace content - const widget = this.widgets[widgetIdx]; - widget.options.host.updateImages(this.imgs); - } else { - const host = createImageHost(this); - this.setSizeForImage(true); - const widget = this.addDOMWidget(ANIM_PREVIEW_WIDGET, "img", host.el, { - host, - getHeight: host.getHeight, - onDraw: host.onDraw, - hideOnZoom: false - }); - widget.serializeValue = () => undefined; - widget.options.host.updateImages(this.imgs); - } - return; - } - - if (widgetIdx > -1) { - this.widgets[widgetIdx].onRemove?.(); - this.widgets.splice(widgetIdx, 1); - } - - const canvas = app.graph.list_of_graphcanvas[0]; - const mouse = canvas.graph_mouse; - if (!canvas.pointer_is_down && this.pointerDown) { - if (mouse[0] === this.pointerDown.pos[0] && mouse[1] === this.pointerDown.pos[1]) { - this.imageIndex = this.pointerDown.index; - } - this.pointerDown = null; - } - - let imageIndex = this.imageIndex; - const numImages = this.imgs.length; - if (numImages === 1 && !imageIndex) { - this.imageIndex = imageIndex = 0; - } - - const top = getImageTop(this); - var shiftY = top; - - let dw = this.size[0]; - let dh = this.size[1]; - dh -= shiftY; - - if (imageIndex == null) { - var cellWidth, cellHeight, shiftX, cell_padding, cols; - - const compact_mode = is_all_same_aspect_ratio(this.imgs); - if(!compact_mode) { - // use rectangle cell style and border line - cell_padding = 2; - const { cell_size, columns, rows } = calculateGrid(dw, dh, numImages); - cols = columns; - - cellWidth = cell_size; - cellHeight = cell_size; - shiftX = (dw-cell_size*cols)/2; - shiftY = (dh-cell_size*rows)/2 + top; - } - else { - cell_padding = 0; - ({ cellWidth, cellHeight, cols, shiftX } = calculateImageGrid(this.imgs, dw, dh)); - } - - let anyHovered = false; - this.imageRects = []; - for (let i = 0; i < numImages; i++) { - const img = this.imgs[i]; - const row = Math.floor(i / cols); - const col = i % cols; - const x = col * cellWidth + shiftX; - const y = row * cellHeight + shiftY; - if (!anyHovered) { - anyHovered = LiteGraph.isInsideRectangle( - mouse[0], - mouse[1], - x + this.pos[0], - y + this.pos[1], - cellWidth, - cellHeight - ); - if (anyHovered) { - this.overIndex = i; - let value = 110; - if (canvas.pointer_is_down) { - if (!this.pointerDown || this.pointerDown.index !== i) { - this.pointerDown = { index: i, pos: [...mouse] }; - } - value = 125; - } - ctx.filter = `contrast(${value}%) brightness(${value}%)`; - canvas.canvas.style.cursor = "pointer"; - } - } - this.imageRects.push([x, y, cellWidth, cellHeight]); - - let wratio = cellWidth/img.width; - let hratio = cellHeight/img.height; - var ratio = Math.min(wratio, hratio); - - let imgHeight = ratio * img.height; - let imgY = row * cellHeight + shiftY + (cellHeight - imgHeight)/2; - let imgWidth = ratio * img.width; - let imgX = col * cellWidth + shiftX + (cellWidth - imgWidth)/2; - - ctx.drawImage(img, imgX+cell_padding, imgY+cell_padding, imgWidth-cell_padding*2, imgHeight-cell_padding*2); - if(!compact_mode) { - // rectangle cell and border line style - ctx.strokeStyle = "#8F8F8F"; - ctx.lineWidth = 1; - ctx.strokeRect(x+cell_padding, y+cell_padding, cellWidth-cell_padding*2, cellHeight-cell_padding*2); - } - - ctx.filter = "none"; - } - - if (!anyHovered) { - this.pointerDown = null; - this.overIndex = null; - } - } else { - // Draw individual - let w = this.imgs[imageIndex].naturalWidth; - let h = this.imgs[imageIndex].naturalHeight; - - const scaleX = dw / w; - const scaleY = dh / h; - const scale = Math.min(scaleX, scaleY, 1); - - w *= scale; - h *= scale; - - let x = (dw - w) / 2; - let y = (dh - h) / 2 + shiftY; - ctx.drawImage(this.imgs[imageIndex], x, y, w, h); - - const drawButton = (x, y, sz, text) => { - const hovered = LiteGraph.isInsideRectangle(mouse[0], mouse[1], x + this.pos[0], y + this.pos[1], sz, sz); - let fill = "#333"; - let textFill = "#fff"; - let isClicking = false; - if (hovered) { - canvas.canvas.style.cursor = "pointer"; - if (canvas.pointer_is_down) { - fill = "#1e90ff"; - isClicking = true; - } else { - fill = "#eee"; - textFill = "#000"; - } - } else { - this.pointerWasDown = null; - } - - ctx.fillStyle = fill; - ctx.beginPath(); - ctx.roundRect(x, y, sz, sz, [4]); - ctx.fill(); - ctx.fillStyle = textFill; - ctx.font = "12px Arial"; - ctx.textAlign = "center"; - ctx.fillText(text, x + 15, y + 20); - - return isClicking; - }; - - if (numImages > 1) { - if (drawButton(dw - 40, dh + top - 40, 30, `${this.imageIndex + 1}/${numImages}`)) { - let i = this.imageIndex + 1 >= numImages ? 0 : this.imageIndex + 1; - if (!this.pointerDown || !this.pointerDown.index === i) { - this.pointerDown = { index: i, pos: [...mouse] }; - } - } - - if (drawButton(dw - 40, top + 10, 30, `x`)) { - if (!this.pointerDown || !this.pointerDown.index === null) { - this.pointerDown = { index: null, pos: [...mouse] }; - } - } - } - } - } - } - }; - } - - /** - * Adds a handler allowing drag+drop of files onto the window to load workflows - */ - #addDropHandler() { - // Get prompt from dropped PNG or json - document.addEventListener("drop", async (event) => { - event.preventDefault(); - event.stopPropagation(); - - const n = this.dragOverNode; - this.dragOverNode = null; - // Node handles file drop, we dont use the built in onDropFile handler as its buggy - // If you drag multiple files it will call it multiple times with the same file - if (n && n.onDragDrop && (await n.onDragDrop(event))) { - return; - } - // Dragging from Chrome->Firefox there is a file but its a bmp, so ignore that - if (event.dataTransfer.files.length && event.dataTransfer.files[0].type !== "image/bmp") { - await this.handleFile(event.dataTransfer.files[0]); - } else { - // Try loading the first URI in the transfer list - const validTypes = ["text/uri-list", "text/x-moz-url"]; - const match = [...event.dataTransfer.types].find((t) => validTypes.find(v => t === v)); - if (match) { - const uri = event.dataTransfer.getData(match)?.split("\n")?.[0]; - if (uri) { - await this.handleFile(await (await fetch(uri)).blob()); - } - } - } - }); - - // Always clear over node on drag leave - this.canvasEl.addEventListener("dragleave", async () => { - if (this.dragOverNode) { - this.dragOverNode = null; - this.graph.setDirtyCanvas(false, true); - } - }); - - // Add handler for dropping onto a specific node - this.canvasEl.addEventListener( - "dragover", - (e) => { - this.canvas.adjustMouseEvent(e); - const node = this.graph.getNodeOnPos(e.canvasX, e.canvasY); - if (node) { - if (node.onDragOver && node.onDragOver(e)) { - this.dragOverNode = node; - - // dragover event is fired very frequently, run this on an animation frame - requestAnimationFrame(() => { - this.graph.setDirtyCanvas(false, true); - }); - return; - } - } - this.dragOverNode = null; - }, - false - ); - } - - /** - * Adds a handler on paste that extracts and loads images or workflows from pasted JSON data - */ - #addPasteHandler() { - document.addEventListener("paste", async (e) => { - // ctrl+shift+v is used to paste nodes with connections - // this is handled by litegraph - if(this.shiftDown) return; - - let data = (e.clipboardData || window.clipboardData); - const items = data.items; - - // Look for image paste data - for (const item of items) { - if (item.type.startsWith('image/')) { - var imageNode = null; - - // If an image node is selected, paste into it - if (this.canvas.current_node && - this.canvas.current_node.is_selected && - ComfyApp.isImageNode(this.canvas.current_node)) { - imageNode = this.canvas.current_node; - } - - // No image node selected: add a new one - if (!imageNode) { - const newNode = LiteGraph.createNode("LoadImage"); - newNode.pos = [...this.canvas.graph_mouse]; - imageNode = this.graph.add(newNode); - this.graph.change(); - } - const blob = item.getAsFile(); - imageNode.pasteFile(blob); - return; - } - } - - // No image found. Look for node data - data = data.getData("text/plain"); - let workflow; - try { - data = data.slice(data.indexOf("{")); - workflow = JSON.parse(data); - } catch (err) { - try { - data = data.slice(data.indexOf("workflow\n")); - data = data.slice(data.indexOf("{")); - workflow = JSON.parse(data); - } catch (error) {} - } - - if (workflow && workflow.version && workflow.nodes && workflow.extra) { - await this.loadGraphData(workflow); - } - else { - if (e.target.type === "text" || e.target.type === "textarea") { - return; - } - - // Litegraph default paste - this.canvas.pasteFromClipboard(); - } - - - }); - } - - - /** - * Adds a handler on copy that serializes selected nodes to JSON - */ - #addCopyHandler() { - document.addEventListener("copy", (e) => { - if (e.target.type === "text" || e.target.type === "textarea") { - // Default system copy - return; - } - - // copy nodes and clear clipboard - if (e.target.className === "litegraph" && this.canvas.selected_nodes) { - this.canvas.copyToClipboard(); - e.clipboardData.setData('text', ' '); //clearData doesn't remove images from clipboard - e.preventDefault(); - e.stopImmediatePropagation(); - return false; - } - }); - } - - - /** - * Handle mouse - * - * Move group by header - */ - #addProcessMouseHandler() { - const self = this; - - const origProcessMouseDown = LGraphCanvas.prototype.processMouseDown; - LGraphCanvas.prototype.processMouseDown = function(e) { - // prepare for ctrl+shift drag: zoom start - if(e.ctrlKey && e.shiftKey && e.buttons) { - self.zoom_drag_start = [e.x, e.y, this.ds.scale]; - return; - } - - const res = origProcessMouseDown.apply(this, arguments); - - this.selected_group_moving = false; - - if (this.selected_group && !this.selected_group_resizing) { - var font_size = - this.selected_group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE; - var height = font_size * 1.4; - - // Move group by header - if (LiteGraph.isInsideRectangle(e.canvasX, e.canvasY, this.selected_group.pos[0], this.selected_group.pos[1], this.selected_group.size[0], height)) { - this.selected_group_moving = true; - } - } - - return res; - } - - const origProcessMouseMove = LGraphCanvas.prototype.processMouseMove; - LGraphCanvas.prototype.processMouseMove = function(e) { - // handle ctrl+shift drag - if(e.ctrlKey && e.shiftKey && self.zoom_drag_start) { - // stop canvas zoom action - if(!e.buttons) { - self.zoom_drag_start = null; - return; - } - - // calculate delta - let deltaY = e.y - self.zoom_drag_start[1]; - let startScale = self.zoom_drag_start[2]; - - let scale = startScale - deltaY/100; - - this.ds.changeScale(scale, [this.ds.element.width/2, this.ds.element.height/2]); - this.graph.change(); - - return; - } - - const orig_selected_group = this.selected_group; - - if (this.selected_group && !this.selected_group_resizing && !this.selected_group_moving) { - this.selected_group = null; - } - - const res = origProcessMouseMove.apply(this, arguments); - - if (orig_selected_group && !this.selected_group_resizing && !this.selected_group_moving) { - this.selected_group = orig_selected_group; - } - - return res; - }; - } - - /** - * Handle keypress - * - * Ctrl + M mute/unmute selected nodes - */ - #addProcessKeyHandler() { - const self = this; - const origProcessKey = LGraphCanvas.prototype.processKey; - LGraphCanvas.prototype.processKey = function(e) { - if (!this.graph) { - return; - } - - var block_default = false; - - if (e.target.localName == "input") { - return; - } - - if (e.type == "keydown" && !e.repeat) { - - // Ctrl + M mute/unmute - if (e.key === 'm' && (e.metaKey || e.ctrlKey)) { - if (this.selected_nodes) { - for (var i in this.selected_nodes) { - if (this.selected_nodes[i].mode === 2) { // never - this.selected_nodes[i].mode = 0; // always - } else { - this.selected_nodes[i].mode = 2; // never - } - } - } - block_default = true; - } - - // Ctrl + B bypass - if (e.key === 'b' && (e.metaKey || e.ctrlKey)) { - if (this.selected_nodes) { - for (var i in this.selected_nodes) { - if (this.selected_nodes[i].mode === 4) { // never - this.selected_nodes[i].mode = 0; // always - } else { - this.selected_nodes[i].mode = 4; // never - } - } - } - block_default = true; - } - - // Alt + C collapse/uncollapse - if (e.key === 'c' && e.altKey) { - if (this.selected_nodes) { - for (var i in this.selected_nodes) { - this.selected_nodes[i].collapse() - } - } - block_default = true; - } - - // Ctrl+C Copy - if ((e.key === 'c') && (e.metaKey || e.ctrlKey)) { - // Trigger onCopy - return true; - } - - // Ctrl+V Paste - if ((e.key === 'v' || e.key == 'V') && (e.metaKey || e.ctrlKey) && !e.shiftKey) { - // Trigger onPaste - return true; - } - - if((e.key === '+') && e.altKey) { - block_default = true; - let scale = this.ds.scale * 1.1; - this.ds.changeScale(scale, [this.ds.element.width/2, this.ds.element.height/2]); - this.graph.change(); - } - - if((e.key === '-') && e.altKey) { - block_default = true; - let scale = this.ds.scale * 1 / 1.1; - this.ds.changeScale(scale, [this.ds.element.width/2, this.ds.element.height/2]); - this.graph.change(); - } - } - - this.graph.change(); - - if (block_default) { - e.preventDefault(); - e.stopImmediatePropagation(); - return false; - } - - // Fall through to Litegraph defaults - return origProcessKey.apply(this, arguments); - }; - } - - /** - * Draws group header bar - */ - #addDrawGroupsHandler() { - const self = this; - - const origDrawGroups = LGraphCanvas.prototype.drawGroups; - LGraphCanvas.prototype.drawGroups = function(canvas, ctx) { - if (!this.graph) { - return; - } - - var groups = this.graph._groups; - - ctx.save(); - ctx.globalAlpha = 0.7 * this.editor_alpha; - - for (var i = 0; i < groups.length; ++i) { - var group = groups[i]; - - if (!LiteGraph.overlapBounding(this.visible_area, group._bounding)) { - continue; - } //out of the visible area - - ctx.fillStyle = group.color || "#335"; - ctx.strokeStyle = group.color || "#335"; - var pos = group._pos; - var size = group._size; - ctx.globalAlpha = 0.25 * this.editor_alpha; - ctx.beginPath(); - var font_size = - group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE; - ctx.rect(pos[0] + 0.5, pos[1] + 0.5, size[0], font_size * 1.4); - ctx.fill(); - ctx.globalAlpha = this.editor_alpha; - } - - ctx.restore(); - - const res = origDrawGroups.apply(this, arguments); - return res; - } - } - - /** - * Draws node highlights (executing, drag drop) and progress bar - */ - #addDrawNodeHandler() { - const origDrawNodeShape = LGraphCanvas.prototype.drawNodeShape; - const self = this; - - LGraphCanvas.prototype.drawNodeShape = function (node, ctx, size, fgcolor, bgcolor, selected, mouse_over) { - const res = origDrawNodeShape.apply(this, arguments); - - const nodeErrors = self.lastNodeErrors?.[node.id]; - - let color = null; - let lineWidth = 1; - if (node.id === +self.runningNodeId) { - color = "#0f0"; - } else if (self.dragOverNode && node.id === self.dragOverNode.id) { - color = "dodgerblue"; - } - else if (nodeErrors?.errors) { - color = "red"; - lineWidth = 2; - } - else if (self.lastExecutionError && +self.lastExecutionError.node_id === node.id) { - color = "#f0f"; - lineWidth = 2; - } - - if (color) { - const shape = node._shape || node.constructor.shape || LiteGraph.ROUND_SHAPE; - ctx.lineWidth = lineWidth; - ctx.globalAlpha = 0.8; - ctx.beginPath(); - if (shape == LiteGraph.BOX_SHAPE) - ctx.rect(-6, -6 - LiteGraph.NODE_TITLE_HEIGHT, 12 + size[0] + 1, 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT); - else if (shape == LiteGraph.ROUND_SHAPE || (shape == LiteGraph.CARD_SHAPE && node.flags.collapsed)) - ctx.roundRect( - -6, - -6 - LiteGraph.NODE_TITLE_HEIGHT, - 12 + size[0] + 1, - 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT, - this.round_radius * 2 - ); - else if (shape == LiteGraph.CARD_SHAPE) - ctx.roundRect( - -6, - -6 - LiteGraph.NODE_TITLE_HEIGHT, - 12 + size[0] + 1, - 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT, - [this.round_radius * 2, this.round_radius * 2, 2, 2] - ); - else if (shape == LiteGraph.CIRCLE_SHAPE) - ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5 + 6, 0, Math.PI * 2); - ctx.strokeStyle = color; - ctx.stroke(); - ctx.strokeStyle = fgcolor; - ctx.globalAlpha = 1; - } - - if (self.progress && node.id === +self.runningNodeId) { - ctx.fillStyle = "green"; - ctx.fillRect(0, 0, size[0] * (self.progress.value / self.progress.max), 6); - ctx.fillStyle = bgcolor; - } - - // Highlight inputs that failed validation - if (nodeErrors) { - ctx.lineWidth = 2; - ctx.strokeStyle = "red"; - for (const error of nodeErrors.errors) { - if (error.extra_info && error.extra_info.input_name) { - const inputIndex = node.findInputSlot(error.extra_info.input_name) - if (inputIndex !== -1) { - let pos = node.getConnectionPos(true, inputIndex); - ctx.beginPath(); - ctx.arc(pos[0] - node.pos[0], pos[1] - node.pos[1], 12, 0, 2 * Math.PI, false) - ctx.stroke(); - } - } - } - } - - return res; - }; - - const origDrawNode = LGraphCanvas.prototype.drawNode; - LGraphCanvas.prototype.drawNode = function (node, ctx) { - var editor_alpha = this.editor_alpha; - var old_color = node.bgcolor; - - if (node.mode === 2) { // never - this.editor_alpha = 0.4; - } - - if (node.mode === 4) { // never - node.bgcolor = "#FF00FF"; - this.editor_alpha = 0.2; - } - - const res = origDrawNode.apply(this, arguments); - - this.editor_alpha = editor_alpha; - node.bgcolor = old_color; - - return res; - }; - } - - /** - * Handles updates from the API socket - */ - #addApiUpdateHandlers() { - api.addEventListener("status", ({ detail }) => { - this.ui.setStatus(detail); - }); - - api.addEventListener("reconnecting", () => { - this.ui.dialog.show("Reconnecting..."); - }); - - api.addEventListener("reconnected", () => { - this.ui.dialog.close(); - }); - - api.addEventListener("progress", ({ detail }) => { - if (this.workflowManager.activePrompt?.workflow - && this.workflowManager.activePrompt.workflow !== this.workflowManager.activeWorkflow) return; - this.progress = detail; - this.graph.setDirtyCanvas(true, false); - }); - - api.addEventListener("executing", ({ detail }) => { - if (this.workflowManager.activePrompt ?.workflow - && this.workflowManager.activePrompt.workflow !== this.workflowManager.activeWorkflow) return; - this.progress = null; - this.runningNodeId = detail; - this.graph.setDirtyCanvas(true, false); - delete this.nodePreviewImages[this.runningNodeId] - }); - - api.addEventListener("executed", ({ detail }) => { - if (this.workflowManager.activePrompt ?.workflow - && this.workflowManager.activePrompt.workflow !== this.workflowManager.activeWorkflow) return; - const output = this.nodeOutputs[detail.node]; - if (detail.merge && output) { - for (const k in detail.output ?? {}) { - const v = output[k]; - if (v instanceof Array) { - output[k] = v.concat(detail.output[k]); - } else { - output[k] = detail.output[k]; - } - } - } else { - this.nodeOutputs[detail.node] = detail.output; - } - const node = this.graph.getNodeById(detail.node); - if (node) { - if (node.onExecuted) - node.onExecuted(detail.output); - } - }); - - api.addEventListener("execution_start", ({ detail }) => { - this.runningNodeId = null; - this.lastExecutionError = null - this.graph._nodes.forEach((node) => { - if (node.onExecutionStart) - node.onExecutionStart() - }) - }); - - api.addEventListener("execution_error", ({ detail }) => { - this.lastExecutionError = detail; - const formattedError = this.#formatExecutionError(detail); - this.ui.dialog.show(formattedError); - this.canvas.draw(true, true); - }); - - api.addEventListener("b_preview", ({ detail }) => { - const id = this.runningNodeId - if (id == null) - return; - - const blob = detail - const blobUrl = URL.createObjectURL(blob) - this.nodePreviewImages[id] = [blobUrl] - }); - - api.init(); - } - - #addKeyboardHandler() { - window.addEventListener("keydown", (e) => { - this.shiftDown = e.shiftKey; - }); - window.addEventListener("keyup", (e) => { - this.shiftDown = e.shiftKey; - }); - } - - #addConfigureHandler() { - const app = this; - const configure = LGraph.prototype.configure; - // Flag that the graph is configuring to prevent nodes from running checks while its still loading - LGraph.prototype.configure = function () { - app.configuringGraph = true; - try { - return configure.apply(this, arguments); - } finally { - app.configuringGraph = false; - } - }; - } - - #addAfterConfigureHandler() { - const app = this; - const onConfigure = app.graph.onConfigure; - app.graph.onConfigure = function () { - // Fire callbacks before the onConfigure, this is used by widget inputs to setup the config - for (const node of app.graph._nodes) { - node.onGraphConfigured?.(); - } - - const r = onConfigure?.apply(this, arguments); - - // Fire after onConfigure, used by primitves to generate widget using input nodes config - for (const node of app.graph._nodes) { - node.onAfterGraphConfigured?.(); - } - - return r; - }; - } - - /** - * Loads all extensions from the API into the window in parallel - */ - async #loadExtensions() { - const extensions = await api.getExtensions(); - this.logging.addEntry("Comfy.App", "debug", { Extensions: extensions }); - - const extensionPromises = extensions.map(async ext => { - try { - await import(api.apiURL(ext)); - } catch (error) { - console.error("Error loading extension", ext, error); - } - }); - - await Promise.all(extensionPromises); - try { - this.menu.workflows.registerExtension(this); - } catch (error) { - console.error(error); - } - } - - async #migrateSettings() { - this.isNewUserSession = true; - // Store all current settings - const settings = Object.keys(this.ui.settings).reduce((p, n) => { - const v = localStorage[`Comfy.Settings.${n}`]; - if (v) { - try { - p[n] = JSON.parse(v); - } catch (error) {} - } - return p; - }, {}); - - await api.storeSettings(settings); - } - - async #setUser() { - const userConfig = await api.getUserConfig(); - this.storageLocation = userConfig.storage; - if (typeof userConfig.migrated == "boolean") { - // Single user mode migrated true/false for if the default user is created - if (!userConfig.migrated && this.storageLocation === "server") { - // Default user not created yet - await this.#migrateSettings(); - } - return; - } - - this.multiUserServer = true; - let user = localStorage["Comfy.userId"]; - const users = userConfig.users ?? {}; - if (!user || !users[user]) { - // This will rarely be hit so move the loading to on demand - const { UserSelectionScreen } = await import("./ui/userSelection.js"); - - this.ui.menuContainer.style.display = "none"; - const { userId, username, created } = await new UserSelectionScreen().show(users, user); - this.ui.menuContainer.style.display = ""; - - user = userId; - localStorage["Comfy.userName"] = username; - localStorage["Comfy.userId"] = user; - - if (created) { - api.user = user; - await this.#migrateSettings(); - } - } - - api.user = user; - - this.ui.settings.addSetting({ - id: "Comfy.SwitchUser", - name: "Switch User", - type: (name) => { - let currentUser = localStorage["Comfy.userName"]; - if (currentUser) { - currentUser = ` (${currentUser})`; - } - return $el("tr", [ - $el("td", [ - $el("label", { - textContent: name, - }), - ]), - $el("td", [ - $el("button", { - textContent: name + (currentUser ?? ""), - onclick: () => { - delete localStorage["Comfy.userId"]; - delete localStorage["Comfy.userName"]; - window.location.reload(); - }, - }), - ]), - ]); - }, - }); - } - - /** - * Set up the app on the page - */ - async setup() { - await this.#setUser(); - - // Create and mount the LiteGraph in the DOM - const mainCanvas = document.createElement("canvas") - mainCanvas.style.touchAction = "none" - const canvasEl = (this.canvasEl = Object.assign(mainCanvas, { id: "graph-canvas" })); - canvasEl.tabIndex = "1"; - document.body.append(canvasEl); - this.resizeCanvas(); - - await Promise.all([this.workflowManager.loadWorkflows(), this.ui.settings.load()]); - await this.#loadExtensions(); - - addDomClippingSetting(); - this.#addProcessMouseHandler(); - this.#addProcessKeyHandler(); - this.#addConfigureHandler(); - this.#addApiUpdateHandlers(); - this.#addRestoreWorkflowView(); - - this.graph = new LGraph(); - - this.#addAfterConfigureHandler(); - - this.canvas = new LGraphCanvas(canvasEl, this.graph); - this.ctx = canvasEl.getContext("2d"); - - LiteGraph.release_link_on_empty_shows_menu = true; - LiteGraph.alt_drag_do_clone_nodes = true; - - this.graph.start(); - - // Ensure the canvas fills the window - this.resizeCanvas(); - window.addEventListener("resize", () => this.resizeCanvas()); - const ro = new ResizeObserver(() => this.resizeCanvas()); - ro.observe(this.bodyTop); - ro.observe(this.bodyLeft); - ro.observe(this.bodyRight); - ro.observe(this.bodyBottom); - - await this.#invokeExtensionsAsync("init"); - await this.registerNodes(); - initWidgets(this); - - // Load previous workflow - let restored = false; - try { - const loadWorkflow = async (json) => { - if (json) { - const workflow = JSON.parse(json); - const workflowName = getStorageValue("Comfy.PreviousWorkflow"); - await this.loadGraphData(workflow, true, true, workflowName); - return true; - } - }; - const clientId = api.initialClientId ?? api.clientId; - restored = - (clientId && (await loadWorkflow(sessionStorage.getItem(`workflow:${clientId}`)))) || - (await loadWorkflow(localStorage.getItem("workflow"))); - } catch (err) { - console.error("Error loading previous workflow", err); - } - - // We failed to restore a workflow so load the default - if (!restored) { - await this.loadGraphData(); - } - - // Save current workflow automatically - setInterval(() => { - const workflow = JSON.stringify(this.graph.serialize()); - localStorage.setItem("workflow", workflow); - if (api.clientId) { - sessionStorage.setItem(`workflow:${api.clientId}`, workflow); - } - }, 1000); - - this.#addDrawNodeHandler(); - this.#addDrawGroupsHandler(); - this.#addDropHandler(); - this.#addCopyHandler(); - this.#addPasteHandler(); - this.#addKeyboardHandler(); - - await this.#invokeExtensionsAsync("setup"); - } - - resizeCanvas() { - // Limit minimal scale to 1, see https://github.com/comfyanonymous/ComfyUI/pull/845 - const scale = Math.max(window.devicePixelRatio, 1); - - // Clear fixed width and height while calculating rect so it uses 100% instead - this.canvasEl.height = this.canvasEl.width = ""; - const { width, height } = this.canvasEl.getBoundingClientRect(); - this.canvasEl.width = Math.round(width * scale); - this.canvasEl.height = Math.round(height * scale); - this.canvasEl.getContext("2d").scale(scale, scale); - this.canvas?.draw(true, true); - } - - /** - * Registers nodes with the graph - */ - async registerNodes() { - const app = this; - // Load node definitions from the backend - const defs = await api.getNodeDefs(); - await this.registerNodesFromDefs(defs); - await this.#invokeExtensionsAsync("registerCustomNodes"); - } - - getWidgetType(inputData, inputName) { - const type = inputData[0]; - - if (Array.isArray(type)) { - return "COMBO"; - } else if (`${type}:${inputName}` in this.widgets) { - return `${type}:${inputName}`; - } else if (type in this.widgets) { - return type; - } else { - return null; - } - } - - async registerNodeDef(nodeId, nodeData) { - const self = this; - const node = Object.assign( - function ComfyNode() { - var inputs = nodeData["input"]["required"]; - if (nodeData["input"]["optional"] != undefined) { - inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"]); - } - const config = { minWidth: 1, minHeight: 1 }; - for (const inputName in inputs) { - const inputData = inputs[inputName]; - const type = inputData[0]; - - let widgetCreated = true; - const widgetType = self.getWidgetType(inputData, inputName); - if(widgetType) { - if(widgetType === "COMBO") { - Object.assign(config, self.widgets.COMBO(this, inputName, inputData, app) || {}); - } else { - Object.assign(config, self.widgets[widgetType](this, inputName, inputData, app) || {}); - } - } else { - // Node connection inputs - this.addInput(inputName, type); - widgetCreated = false; - } - - if(widgetCreated && inputData[1]?.forceInput && config?.widget) { - if (!config.widget.options) config.widget.options = {}; - config.widget.options.forceInput = inputData[1].forceInput; - } - if(widgetCreated && inputData[1]?.defaultInput && config?.widget) { - if (!config.widget.options) config.widget.options = {}; - config.widget.options.defaultInput = inputData[1].defaultInput; - } - } - - for (const o in nodeData["output"]) { - let output = nodeData["output"][o]; - if(output instanceof Array) output = "COMBO"; - const outputName = nodeData["output_name"][o] || output; - const outputShape = nodeData["output_is_list"][o] ? LiteGraph.GRID_SHAPE : LiteGraph.CIRCLE_SHAPE ; - this.addOutput(outputName, output, { shape: outputShape }); - } - - const s = this.computeSize(); - s[0] = Math.max(config.minWidth, s[0] * 1.5); - s[1] = Math.max(config.minHeight, s[1]); - this.size = s; - this.serialize_widgets = true; - - app.#invokeExtensionsAsync("nodeCreated", this); - }, - { - title: nodeData.display_name || nodeData.name, - comfyClass: nodeData.name, - nodeData - } - ); - node.prototype.comfyClass = nodeData.name; - - this.#addNodeContextMenuHandler(node); - this.#addDrawBackgroundHandler(node, app); - this.#addNodeKeyHandler(node); - - await this.#invokeExtensionsAsync("beforeRegisterNodeDef", node, nodeData); - LiteGraph.registerNodeType(nodeId, node); - node.category = nodeData.category; - } - - async registerNodesFromDefs(defs) { - await this.#invokeExtensionsAsync("addCustomNodeDefs", defs); - - // Generate list of known widgets - this.widgets = Object.assign( - {}, - ComfyWidgets, - ...(await this.#invokeExtensionsAsync("getCustomWidgets")).filter(Boolean) - ); - - // Register a node for each definition - for (const nodeId in defs) { - this.registerNodeDef(nodeId, defs[nodeId]); - } - } - - loadTemplateData(templateData) { - if (!templateData?.templates) { - return; - } - - const old = localStorage.getItem("litegrapheditor_clipboard"); - - var maxY, nodeBottom, node; - - for (const template of templateData.templates) { - if (!template?.data) { - continue; - } - - localStorage.setItem("litegrapheditor_clipboard", template.data); - app.canvas.pasteFromClipboard(); - - // Move mouse position down to paste the next template below - - maxY = false; - - for (const i in app.canvas.selected_nodes) { - node = app.canvas.selected_nodes[i]; - - nodeBottom = node.pos[1] + node.size[1]; - - if (maxY === false || nodeBottom > maxY) { - maxY = nodeBottom; - } - } - - app.canvas.graph_mouse[1] = maxY + 50; - } - - localStorage.setItem("litegrapheditor_clipboard", old); - } - - showMissingNodesError(missingNodeTypes, hasAddedNodes = true) { - let seenTypes = new Set(); - - this.ui.dialog.show( - $el("div.comfy-missing-nodes", [ - $el("span", { textContent: "When loading the graph, the following node types were not found: " }), - $el( - "ul", - Array.from(new Set(missingNodeTypes)).map((t) => { - let children = []; - if (typeof t === "object") { - if(seenTypes.has(t.type)) return null; - seenTypes.add(t.type); - children.push($el("span", { textContent: t.type })); - if (t.hint) { - children.push($el("span", { textContent: t.hint })); - } - if (t.action) { - children.push($el("button", { onclick: t.action.callback, textContent: t.action.text })); - } - } else { - if(seenTypes.has(t)) return null; - seenTypes.add(t); - children.push($el("span", { textContent: t })); - } - return $el("li", children); - }).filter(Boolean) - ), - ...(hasAddedNodes - ? [$el("span", { textContent: "Nodes that have failed to load will show as red on the graph." })] - : []), - ]) - ); - this.logging.addEntry("Comfy.App", "warn", { - MissingNodes: missingNodeTypes, - }); - } - - async changeWorkflow(callback, workflow = null) { - try { - this.workflowManager.activeWorkflow?.changeTracker?.store() - } catch (error) { - console.error(error); - } - await callback(); - try { - this.workflowManager.setWorkflow(workflow); - this.workflowManager.activeWorkflow?.track() - } catch (error) { - console.error(error); - } - } - - /** - * Populates the graph with the specified workflow data - * @param {*} graphData A serialized graph object - * @param { boolean } clean If the graph state, e.g. images, should be cleared - * @param { boolean } restore_view If the graph position should be restored - * @param { import("./workflows.js").ComfyWorkflowInstance | null } workflow The workflow - */ - async loadGraphData(graphData, clean = true, restore_view = true, workflow = null) { - if (clean !== false) { - this.clean(); - } - - let reset_invalid_values = false; - if (!graphData) { - graphData = defaultGraph; - reset_invalid_values = true; - } - - if (typeof structuredClone === "undefined") - { - graphData = JSON.parse(JSON.stringify(graphData)); - }else - { - graphData = structuredClone(graphData); - } - - try { - this.workflowManager.setWorkflow(workflow); - } catch (error) { - console.error(error); - } - - const missingNodeTypes = []; - await this.#invokeExtensionsAsync("beforeConfigureGraph", graphData, missingNodeTypes); - for (let n of graphData.nodes) { - // Patch T2IAdapterLoader to ControlNetLoader since they are the same node now - if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader"; - if (n.type == "ConditioningAverage ") n.type = "ConditioningAverage"; //typo fix - if (n.type == "SDV_img2vid_Conditioning") n.type = "SVD_img2vid_Conditioning"; //typo fix - - // Find missing node types - if (!(n.type in LiteGraph.registered_node_types)) { - missingNodeTypes.push(n.type); - n.type = sanitizeNodeName(n.type); - } - } - - try { - this.graph.configure(graphData); - if (restore_view && this.enableWorkflowViewRestore.value && graphData.extra?.ds) { - this.canvas.ds.offset = graphData.extra.ds.offset; - this.canvas.ds.scale = graphData.extra.ds.scale; - } - - try { - this.workflowManager.activeWorkflow?.track() - } catch (error) { - } - } catch (error) { - let errorHint = []; - // Try extracting filename to see if it was caused by an extension script - const filename = error.fileName || (error.stack || "").match(/(\/extensions\/.*\.js)/)?.[1]; - const pos = (filename || "").indexOf("/extensions/"); - if (pos > -1) { - errorHint.push( - $el("span", { textContent: "This may be due to the following script:" }), - $el("br"), - $el("span", { - style: { - fontWeight: "bold", - }, - textContent: filename.substring(pos), - }) - ); - } - - // Show dialog to let the user know something went wrong loading the data - this.ui.dialog.show( - $el("div", [ - $el("p", { textContent: "Loading aborted due to error reloading workflow data" }), - $el("pre", { - style: { padding: "5px", backgroundColor: "rgba(255,0,0,0.2)" }, - textContent: error.toString(), - }), - $el("pre", { - style: { - padding: "5px", - color: "#ccc", - fontSize: "10px", - maxHeight: "50vh", - overflow: "auto", - backgroundColor: "rgba(0,0,0,0.2)", - }, - textContent: error.stack || "No stacktrace available", - }), - ...errorHint, - ]).outerHTML - ); - - return; - } - - for (const node of this.graph._nodes) { - const size = node.computeSize(); - size[0] = Math.max(node.size[0], size[0]); - size[1] = Math.max(node.size[1], size[1]); - node.size = size; - - if (node.widgets) { - // If you break something in the backend and want to patch workflows in the frontend - // This is the place to do this - for (let widget of node.widgets) { - if (node.type == "KSampler" || node.type == "KSamplerAdvanced") { - if (widget.name == "sampler_name") { - if (widget.value.startsWith("sample_")) { - widget.value = widget.value.slice(7); - } - if (widget.value === "euler_pp" || widget.value === "euler_ancestral_pp") { - widget.value = widget.value.slice(0, -3); - for (let w of node.widgets) { - if (w.name == "cfg") { - w.value *= 2.0; - } - } - } - } - } - if (node.type == "KSampler" || node.type == "KSamplerAdvanced" || node.type == "PrimitiveNode") { - if (widget.name == "control_after_generate") { - if (widget.value === true) { - widget.value = "randomize"; - } else if (widget.value === false) { - widget.value = "fixed"; - } - } - } - if (reset_invalid_values) { - if (widget.type == "combo") { - if (!widget.options.values.includes(widget.value) && widget.options.values.length > 0) { - widget.value = widget.options.values[0]; - } - } - } - } - } - - this.#invokeExtensions("loadedGraphNode", node); - } - - if (missingNodeTypes.length) { - this.showMissingNodesError(missingNodeTypes); - } - await this.#invokeExtensionsAsync("afterConfigureGraph", missingNodeTypes); - requestAnimationFrame(() => { - this.graph.setDirtyCanvas(true, true); - }); - } - - /** - * Converts the current graph workflow for sending to the API - * @returns The workflow and node links - */ - async graphToPrompt(graph = this.graph, clean = true) { - for (const outerNode of graph.computeExecutionOrder(false)) { - if (outerNode.widgets) { - for (const widget of outerNode.widgets) { - // Allow widgets to run callbacks before a prompt has been queued - // e.g. random seed before every gen - widget.beforeQueued?.(); - } - } - - const innerNodes = outerNode.getInnerNodes ? outerNode.getInnerNodes() : [outerNode]; - for (const node of innerNodes) { - if (node.isVirtualNode) { - // Don't serialize frontend only nodes but let them make changes - if (node.applyToGraph) { - node.applyToGraph(); - } - } - } - } - - const workflow = graph.serialize(); - const output = {}; - // Process nodes in order of execution - for (const outerNode of graph.computeExecutionOrder(false)) { - const skipNode = outerNode.mode === 2 || outerNode.mode === 4; - const innerNodes = (!skipNode && outerNode.getInnerNodes) ? outerNode.getInnerNodes() : [outerNode]; - for (const node of innerNodes) { - if (node.isVirtualNode) { - continue; - } - - if (node.mode === 2 || node.mode === 4) { - // Don't serialize muted nodes - continue; - } - - const inputs = {}; - const widgets = node.widgets; - - // Store all widget values - if (widgets) { - for (const i in widgets) { - const widget = widgets[i]; - if (!widget.options || widget.options.serialize !== false) { - inputs[widget.name] = widget.serializeValue ? await widget.serializeValue(node, i) : widget.value; - } - } - } - - // Store all node links - for (let i in node.inputs) { - let parent = node.getInputNode(i); - if (parent) { - let link = node.getInputLink(i); - while (parent.mode === 4 || parent.isVirtualNode) { - let found = false; - if (parent.isVirtualNode) { - link = parent.getInputLink(link.origin_slot); - if (link) { - parent = parent.getInputNode(link.target_slot); - if (parent) { - found = true; - } - } - } else if (link && parent.mode === 4) { - let all_inputs = [link.origin_slot]; - if (parent.inputs) { - all_inputs = all_inputs.concat(Object.keys(parent.inputs)) - for (let parent_input in all_inputs) { - parent_input = all_inputs[parent_input]; - if (parent.inputs[parent_input]?.type === node.inputs[i].type) { - link = parent.getInputLink(parent_input); - if (link) { - parent = parent.getInputNode(parent_input); - } - found = true; - break; - } - } - } - } - - if (!found) { - break; - } - } - - if (link) { - if (parent?.updateLink) { - link = parent.updateLink(link); - } - if (link) { - inputs[node.inputs[i].name] = [String(link.origin_id), parseInt(link.origin_slot)]; - } - } - } - } - - let node_data = { - inputs, - class_type: node.comfyClass, - }; - - if (this.ui.settings.getSettingValue("Comfy.DevMode")) { - // Ignored by the backend. - node_data["_meta"] = { - title: node.title, - } - } - - output[String(node.id)] = node_data; - } - } - - // Remove inputs connected to removed nodes - if(clean) { - for (const o in output) { - for (const i in output[o].inputs) { - if (Array.isArray(output[o].inputs[i]) - && output[o].inputs[i].length === 2 - && !output[output[o].inputs[i][0]]) { - delete output[o].inputs[i]; - } - } - } - } - - return { workflow, output }; - } - - #formatPromptError(error) { - if (error == null) { - return "(unknown error)" - } - else if (typeof error === "string") { - return error; - } - else if (error.stack && error.message) { - return error.toString() - } - else if (error.response) { - let message = error.response.error.message; - if (error.response.error.details) - message += ": " + error.response.error.details; - for (const [nodeID, nodeError] of Object.entries(error.response.node_errors)) { - message += "\n" + nodeError.class_type + ":" - for (const errorReason of nodeError.errors) { - message += "\n - " + errorReason.message + ": " + errorReason.details - } - } - return message - } - return "(unknown error)" - } - - #formatExecutionError(error) { - if (error == null) { - return "(unknown error)" - } - - const traceback = error.traceback.join("") - const nodeId = error.node_id - const nodeType = error.node_type - - return `Error occurred when executing ${nodeType}:\n\n${error.exception_message}\n\n${traceback}` - } - - async queuePrompt(number, batchCount = 1) { - this.#queueItems.push({ number, batchCount }); - - // Only have one action process the items so each one gets a unique seed correctly - if (this.#processingQueue) { - return; - } - - this.#processingQueue = true; - this.lastNodeErrors = null; - - try { - while (this.#queueItems.length) { - ({ number, batchCount } = this.#queueItems.pop()); - - for (let i = 0; i < batchCount; i++) { - const p = await this.graphToPrompt(); - - try { - const res = await api.queuePrompt(number, p); - this.lastNodeErrors = res.node_errors; - if (this.lastNodeErrors.length > 0) { - this.canvas.draw(true, true); - } else { - try { - this.workflowManager.storePrompt({ - id: res.prompt_id, - nodes: Object.keys(p.output) - }); - } catch (error) { - } - } - } catch (error) { - const formattedError = this.#formatPromptError(error) - this.ui.dialog.show(formattedError); - if (error.response) { - this.lastNodeErrors = error.response.node_errors; - this.canvas.draw(true, true); - } - break; - } - - for (const n of p.workflow.nodes) { - const node = graph.getNodeById(n.id); - if (node.widgets) { - for (const widget of node.widgets) { - // Allow widgets to run callbacks after a prompt has been queued - // e.g. random seed after every gen - if (widget.afterQueued) { - widget.afterQueued(); - } - } - } - } - - this.canvas.draw(true, true); - await this.ui.queue.update(); - } - } - } finally { - this.#processingQueue = false; - } - api.dispatchEvent(new CustomEvent("promptQueued", { detail: { number, batchCount } })); - return !this.lastNodeErrors; - } - - showErrorOnFileLoad(file) { - this.ui.dialog.show( - $el("div", [ - $el("p", {textContent: `Unable to find workflow in ${file.name}`}) - ]).outerHTML - ); - } - - /** - * Loads workflow data from the specified file - * @param {File} file - */ - async handleFile(file) { - const removeExt = f => { - if(!f) return f; - const p = f.lastIndexOf("."); - if(p === -1) return f; - return f.substring(0, p); - }; - - const fileName = removeExt(file.name); - if (file.type === "image/png") { - const pngInfo = await getPngMetadata(file); - if (pngInfo?.workflow) { - await this.loadGraphData(JSON.parse(pngInfo.workflow), true, true, fileName); - } else if (pngInfo?.prompt) { - this.loadApiJson(JSON.parse(pngInfo.prompt), fileName); - } else if (pngInfo?.parameters) { - this.changeWorkflow(() => { - importA1111(this.graph, pngInfo.parameters); - }, fileName) - } else { - this.showErrorOnFileLoad(file); - } - } else if (file.type === "image/webp") { - const pngInfo = await getWebpMetadata(file); - // Support loading workflows from that webp custom node. - const workflow = pngInfo?.workflow || pngInfo?.Workflow; - const prompt = pngInfo?.prompt || pngInfo?.Prompt; - - if (workflow) { - this.loadGraphData(JSON.parse(workflow), true, true, fileName); - } else if (prompt) { - this.loadApiJson(JSON.parse(prompt), fileName); - } else { - this.showErrorOnFileLoad(file); - } - } else if (file.type === "audio/flac" || file.type === "audio/x-flac") { - const pngInfo = await getFlacMetadata(file); - // Support loading workflows from that webp custom node. - const workflow = pngInfo?.workflow; - const prompt = pngInfo?.prompt; - - if (workflow) { - this.loadGraphData(JSON.parse(workflow), true, true, fileName); - } else if (prompt) { - this.loadApiJson(JSON.parse(prompt), fileName); - } else { - this.showErrorOnFileLoad(file); - } - } else if (file.type === "application/json" || file.name?.endsWith(".json")) { - const reader = new FileReader(); - reader.onload = async () => { - const jsonContent = JSON.parse(reader.result); - if (jsonContent?.templates) { - this.loadTemplateData(jsonContent); - } else if(this.isApiJson(jsonContent)) { - this.loadApiJson(jsonContent, fileName); - } else { - await this.loadGraphData(jsonContent, true, true, fileName); - } - }; - reader.readAsText(file); - } else if (file.name?.endsWith(".latent") || file.name?.endsWith(".safetensors")) { - const info = await getLatentMetadata(file); - if (info.workflow) { - await this.loadGraphData(JSON.parse(info.workflow), true, true, fileName); - } else if (info.prompt) { - this.loadApiJson(JSON.parse(info.prompt)); - } else { - this.showErrorOnFileLoad(file); - } - } else { - this.showErrorOnFileLoad(file); - } - } - - isApiJson(data) { - return Object.values(data).every((v) => v.class_type); - } - - loadApiJson(apiData, fileName) { - const missingNodeTypes = Object.values(apiData).filter((n) => !LiteGraph.registered_node_types[n.class_type]); - if (missingNodeTypes.length) { - this.showMissingNodesError(missingNodeTypes.map(t => t.class_type), false); - return; - } - - const ids = Object.keys(apiData); - app.graph.clear(); - for (const id of ids) { - const data = apiData[id]; - const node = LiteGraph.createNode(data.class_type); - node.id = isNaN(+id) ? id : +id; - node.title = data._meta?.title ?? node.title - app.graph.add(node); - } - - this.changeWorkflow(() => { - for (const id of ids) { - const data = apiData[id]; - const node = app.graph.getNodeById(id); - for (const input in data.inputs ?? {}) { - const value = data.inputs[input]; - if (value instanceof Array) { - const [fromId, fromSlot] = value; - const fromNode = app.graph.getNodeById(fromId); - let toSlot = node.inputs?.findIndex((inp) => inp.name === input); - if (toSlot == null || toSlot === -1) { - try { - // Target has no matching input, most likely a converted widget - const widget = node.widgets?.find((w) => w.name === input); - if (widget && node.convertWidgetToInput?.(widget)) { - toSlot = node.inputs?.length - 1; - } - } catch (error) {} - } - if (toSlot != null || toSlot !== -1) { - fromNode.connect(fromSlot, node, toSlot); - } - } else { - const widget = node.widgets?.find((w) => w.name === input); - if (widget) { - widget.value = value; - widget.callback?.(value); - } - } - } - } - app.graph.arrange(); - }, fileName); - } - - /** - * Registers a Comfy web extension with the app - * @param {ComfyExtension} extension - */ - registerExtension(extension) { - if (!extension.name) { - throw new Error("Extensions must have a 'name' property."); - } - if (this.extensions.find((ext) => ext.name === extension.name)) { - throw new Error(`Extension named '${extension.name}' already registered.`); - } - this.extensions.push(extension); - } - - /** - * Refresh combo list on whole nodes - */ - async refreshComboInNodes() { - const defs = await api.getNodeDefs(); - - for (const nodeId in defs) { - this.registerNodeDef(nodeId, defs[nodeId]); - } - - for(let nodeNum in this.graph._nodes) { - const node = this.graph._nodes[nodeNum]; - const def = defs[node.type]; - - // Allow primitive nodes to handle refresh - node.refreshComboInNode?.(defs); - - if(!def) - continue; - - for(const widgetNum in node.widgets) { - const widget = node.widgets[widgetNum] - if(widget.type == "combo" && def["input"]["required"][widget.name] !== undefined) { - widget.options.values = def["input"]["required"][widget.name][0]; - - if(widget.name != 'image' && !widget.options.values.includes(widget.value)) { - widget.value = widget.options.values[0]; - widget.callback(widget.value); - } - } - } - } - - await this.#invokeExtensionsAsync("refreshComboInNodes", defs); - } - - resetView() { - app.canvas.ds.scale = 1; - app.canvas.ds.offset = [0, 0] - app.graph.setDirtyCanvas(true, true); - } - - /** - * Clean current state - */ - clean() { - this.nodeOutputs = {}; - this.nodePreviewImages = {} - this.lastNodeErrors = null; - this.lastExecutionError = null; - this.runningNodeId = null; - } -} - -export const app = new ComfyApp(); diff --git a/web/scripts/changeTracker.js b/web/scripts/changeTracker.js deleted file mode 100644 index d7798dbe9..000000000 --- a/web/scripts/changeTracker.js +++ /dev/null @@ -1,255 +0,0 @@ -// @ts-check - -import { api } from "./api.js"; -import { clone } from "./utils.js"; - -export class ChangeTracker { - static MAX_HISTORY = 50; - #app; - undo = []; - redo = []; - activeState = null; - isOurLoad = false; - /** @type { import("./workflows").ComfyWorkflow | null } */ - workflow; - - ds; - nodeOutputs; - - get app() { - return this.#app ?? this.workflow.manager.app; - } - - constructor(workflow) { - this.workflow = workflow; - } - - #setApp(app) { - this.#app = app; - } - - store() { - this.ds = { scale: this.app.canvas.ds.scale, offset: [...this.app.canvas.ds.offset] }; - } - - restore() { - if (this.ds) { - this.app.canvas.ds.scale = this.ds.scale; - this.app.canvas.ds.offset = this.ds.offset; - } - if (this.nodeOutputs) { - this.app.nodeOutputs = this.nodeOutputs; - } - } - - checkState() { - if (!this.app.graph) return; - - const currentState = this.app.graph.serialize(); - if (!this.activeState) { - this.activeState = clone(currentState); - return; - } - if (!ChangeTracker.graphEqual(this.activeState, currentState)) { - this.undo.push(this.activeState); - if (this.undo.length > ChangeTracker.MAX_HISTORY) { - this.undo.shift(); - } - this.activeState = clone(currentState); - this.redo.length = 0; - this.workflow.unsaved = true; - api.dispatchEvent(new CustomEvent("graphChanged", { detail: this.activeState })); - } - } - - async updateState(source, target) { - const prevState = source.pop(); - if (prevState) { - target.push(this.activeState); - this.isOurLoad = true; - await this.app.loadGraphData(prevState, false, false, this.workflow); - this.activeState = prevState; - } - } - - async undoRedo(e) { - if (e.ctrlKey || e.metaKey) { - if (e.key === "y") { - this.updateState(this.redo, this.undo); - return true; - } else if (e.key === "z") { - this.updateState(this.undo, this.redo); - return true; - } - } - } - - /** @param { import("./app.js").ComfyApp } app */ - static init(app) { - const changeTracker = () => app.workflowManager.activeWorkflow?.changeTracker ?? globalTracker; - globalTracker.#setApp(app); - - const loadGraphData = app.loadGraphData; - app.loadGraphData = async function () { - const v = await loadGraphData.apply(this, arguments); - const ct = changeTracker(); - if (ct.isOurLoad) { - ct.isOurLoad = false; - } else { - ct.checkState(); - } - return v; - }; - - let keyIgnored = false; - window.addEventListener( - "keydown", - (e) => { - const activeEl = document.activeElement; - requestAnimationFrame(async () => { - let bindInputEl; - // If we are auto queue in change mode then we do want to trigger on inputs - if (!app.ui.autoQueueEnabled || app.ui.autoQueueMode === "instant") { - if (activeEl?.tagName === "INPUT" || activeEl?.["type"] === "textarea") { - // Ignore events on inputs, they have their native history - return; - } - bindInputEl = activeEl; - } - - keyIgnored = e.key === "Control" || e.key === "Shift" || e.key === "Alt" || e.key === "Meta"; - if (keyIgnored) return; - - // Check if this is a ctrl+z ctrl+y - if (await changeTracker().undoRedo(e)) return; - - // If our active element is some type of input then handle changes after they're done - if (ChangeTracker.bindInput(bindInputEl)) return; - changeTracker().checkState(); - }); - }, - true - ); - - window.addEventListener("keyup", (e) => { - if (keyIgnored) { - keyIgnored = false; - changeTracker().checkState(); - } - }); - - // Handle clicking DOM elements (e.g. widgets) - window.addEventListener("mouseup", () => { - changeTracker().checkState(); - }); - - // Handle prompt queue event for dynamic widget changes - api.addEventListener("promptQueued", () => { - changeTracker().checkState(); - }); - - // Handle litegraph clicks - const processMouseUp = LGraphCanvas.prototype.processMouseUp; - LGraphCanvas.prototype.processMouseUp = function (e) { - const v = processMouseUp.apply(this, arguments); - changeTracker().checkState(); - return v; - }; - const processMouseDown = LGraphCanvas.prototype.processMouseDown; - LGraphCanvas.prototype.processMouseDown = function (e) { - const v = processMouseDown.apply(this, arguments); - changeTracker().checkState(); - return v; - }; - - // Handle litegraph context menu for COMBO widgets - const close = LiteGraph.ContextMenu.prototype.close; - LiteGraph.ContextMenu.prototype.close = function (e) { - const v = close.apply(this, arguments); - changeTracker().checkState(); - return v; - }; - - // Detects nodes being added via the node search dialog - const onNodeAdded = LiteGraph.LGraph.prototype.onNodeAdded; - LiteGraph.LGraph.prototype.onNodeAdded = function () { - const v = onNodeAdded?.apply(this, arguments); - if (!app?.configuringGraph) { - const ct = changeTracker(); - if (!ct.isOurLoad) { - ct.checkState(); - } - } - return v; - }; - - // Store node outputs - api.addEventListener("executed", ({ detail }) => { - const prompt = app.workflowManager.queuedPrompts[detail.prompt_id]; - if (!prompt?.workflow) return; - const nodeOutputs = (prompt.workflow.changeTracker.nodeOutputs ??= {}); - const output = nodeOutputs[detail.node]; - if (detail.merge && output) { - for (const k in detail.output ?? {}) { - const v = output[k]; - if (v instanceof Array) { - output[k] = v.concat(detail.output[k]); - } else { - output[k] = detail.output[k]; - } - } - } else { - nodeOutputs[detail.node] = detail.output; - } - }); - } - - static bindInput(app, activeEl) { - if (activeEl && activeEl.tagName !== "CANVAS" && activeEl.tagName !== "BODY") { - for (const evt of ["change", "input", "blur"]) { - if (`on${evt}` in activeEl) { - const listener = () => { - app.workflowManager.activeWorkflow.changeTracker.checkState(); - activeEl.removeEventListener(evt, listener); - }; - activeEl.addEventListener(evt, listener); - return true; - } - } - } - } - - static graphEqual(a, b, path = "") { - if (a === b) return true; - - if (typeof a == "object" && a && typeof b == "object" && b) { - const keys = Object.getOwnPropertyNames(a); - - if (keys.length != Object.getOwnPropertyNames(b).length) { - return false; - } - - for (const key of keys) { - let av = a[key]; - let bv = b[key]; - if (!path && key === "nodes") { - // Nodes need to be sorted as the order changes when selecting nodes - av = [...av].sort((a, b) => a.id - b.id); - bv = [...bv].sort((a, b) => a.id - b.id); - } else if (path === "extra.ds") { - // Ignore view changes - continue; - } - if (!ChangeTracker.graphEqual(av, bv, path + (path ? "." : "") + key)) { - return false; - } - } - - return true; - } - - return false; - } -} - -const globalTracker = new ChangeTracker({}); \ No newline at end of file diff --git a/web/scripts/defaultGraph.js b/web/scripts/defaultGraph.js deleted file mode 100644 index 9b3cb4a7e..000000000 --- a/web/scripts/defaultGraph.js +++ /dev/null @@ -1,119 +0,0 @@ -export const defaultGraph = { - last_node_id: 9, - last_link_id: 9, - nodes: [ - { - id: 7, - type: "CLIPTextEncode", - pos: [413, 389], - size: { 0: 425.27801513671875, 1: 180.6060791015625 }, - flags: {}, - order: 3, - mode: 0, - inputs: [{ name: "clip", type: "CLIP", link: 5 }], - outputs: [{ name: "CONDITIONING", type: "CONDITIONING", links: [6], slot_index: 0 }], - properties: {}, - widgets_values: ["text, watermark"], - }, - { - id: 6, - type: "CLIPTextEncode", - pos: [415, 186], - size: { 0: 422.84503173828125, 1: 164.31304931640625 }, - flags: {}, - order: 2, - mode: 0, - inputs: [{ name: "clip", type: "CLIP", link: 3 }], - outputs: [{ name: "CONDITIONING", type: "CONDITIONING", links: [4], slot_index: 0 }], - properties: {}, - widgets_values: ["beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"], - }, - { - id: 5, - type: "EmptyLatentImage", - pos: [473, 609], - size: { 0: 315, 1: 106 }, - flags: {}, - order: 1, - mode: 0, - outputs: [{ name: "LATENT", type: "LATENT", links: [2], slot_index: 0 }], - properties: {}, - widgets_values: [512, 512, 1], - }, - { - id: 3, - type: "KSampler", - pos: [863, 186], - size: { 0: 315, 1: 262 }, - flags: {}, - order: 4, - mode: 0, - inputs: [ - { name: "model", type: "MODEL", link: 1 }, - { name: "positive", type: "CONDITIONING", link: 4 }, - { name: "negative", type: "CONDITIONING", link: 6 }, - { name: "latent_image", type: "LATENT", link: 2 }, - ], - outputs: [{ name: "LATENT", type: "LATENT", links: [7], slot_index: 0 }], - properties: {}, - widgets_values: [156680208700286, true, 20, 8, "euler", "normal", 1], - }, - { - id: 8, - type: "VAEDecode", - pos: [1209, 188], - size: { 0: 210, 1: 46 }, - flags: {}, - order: 5, - mode: 0, - inputs: [ - { name: "samples", type: "LATENT", link: 7 }, - { name: "vae", type: "VAE", link: 8 }, - ], - outputs: [{ name: "IMAGE", type: "IMAGE", links: [9], slot_index: 0 }], - properties: {}, - }, - { - id: 9, - type: "SaveImage", - pos: [1451, 189], - size: { 0: 210, 1: 26 }, - flags: {}, - order: 6, - mode: 0, - inputs: [{ name: "images", type: "IMAGE", link: 9 }], - properties: {}, - }, - { - id: 4, - type: "CheckpointLoaderSimple", - pos: [26, 474], - size: { 0: 315, 1: 98 }, - flags: {}, - order: 0, - mode: 0, - outputs: [ - { name: "MODEL", type: "MODEL", links: [1], slot_index: 0 }, - { name: "CLIP", type: "CLIP", links: [3, 5], slot_index: 1 }, - { name: "VAE", type: "VAE", links: [8], slot_index: 2 }, - ], - properties: {}, - widgets_values: ["v1-5-pruned-emaonly.ckpt"], - }, - ], - links: [ - [1, 4, 0, 3, 0, "MODEL"], - [2, 5, 0, 3, 3, "LATENT"], - [3, 4, 1, 6, 0, "CLIP"], - [4, 6, 0, 3, 1, "CONDITIONING"], - [5, 4, 1, 7, 0, "CLIP"], - [6, 7, 0, 3, 2, "CONDITIONING"], - [7, 3, 0, 8, 0, "LATENT"], - [8, 4, 2, 8, 1, "VAE"], - [9, 8, 0, 9, 0, "IMAGE"], - ], - groups: [], - config: {}, - extra: {}, - version: 0.4, -}; diff --git a/web/scripts/domWidget.js b/web/scripts/domWidget.js deleted file mode 100644 index d97122f9d..000000000 --- a/web/scripts/domWidget.js +++ /dev/null @@ -1,329 +0,0 @@ -import { app, ANIM_PREVIEW_WIDGET } from "./app.js"; - -const SIZE = Symbol(); - -function intersect(a, b) { - const x = Math.max(a.x, b.x); - const num1 = Math.min(a.x + a.width, b.x + b.width); - const y = Math.max(a.y, b.y); - const num2 = Math.min(a.y + a.height, b.y + b.height); - if (num1 >= x && num2 >= y) return [x, y, num1 - x, num2 - y]; - else return null; -} - -function getClipPath(node, element) { - const selectedNode = Object.values(app.canvas.selected_nodes)[0]; - if (selectedNode && selectedNode !== node) { - const elRect = element.getBoundingClientRect(); - const MARGIN = 7; - const scale = app.canvas.ds.scale; - - const bounding = selectedNode.getBounding(); - const intersection = intersect( - { x: elRect.x / scale, y: elRect.y / scale, width: elRect.width / scale, height: elRect.height / scale }, - { - x: selectedNode.pos[0] + app.canvas.ds.offset[0] - MARGIN, - y: selectedNode.pos[1] + app.canvas.ds.offset[1] - LiteGraph.NODE_TITLE_HEIGHT - MARGIN, - width: bounding[2] + MARGIN + MARGIN, - height: bounding[3] + MARGIN + MARGIN, - } - ); - - if (!intersection) { - return ""; - } - - const widgetRect = element.getBoundingClientRect(); - const clipX = elRect.left + intersection[0] - widgetRect.x / scale + "px"; - const clipY = elRect.top + intersection[1] - widgetRect.y / scale + "px"; - const clipWidth = intersection[2] + "px"; - const clipHeight = intersection[3] + "px"; - const path = `polygon(0% 0%, 0% 100%, ${clipX} 100%, ${clipX} ${clipY}, calc(${clipX} + ${clipWidth}) ${clipY}, calc(${clipX} + ${clipWidth}) calc(${clipY} + ${clipHeight}), ${clipX} calc(${clipY} + ${clipHeight}), ${clipX} 100%, 100% 100%, 100% 0%)`; - return path; - } - return ""; -} - -function computeSize(size) { - if (this.widgets?.[0]?.last_y == null) return; - - let y = this.widgets[0].last_y; - let freeSpace = size[1] - y; - - let widgetHeight = 0; - let dom = []; - for (const w of this.widgets) { - if (w.type === "converted-widget") { - // Ignore - delete w.computedHeight; - } else if (w.computeSize) { - widgetHeight += w.computeSize()[1] + 4; - } else if (w.element) { - // Extract DOM widget size info - const styles = getComputedStyle(w.element); - let minHeight = w.options.getMinHeight?.() ?? parseInt(styles.getPropertyValue("--comfy-widget-min-height")); - let maxHeight = w.options.getMaxHeight?.() ?? parseInt(styles.getPropertyValue("--comfy-widget-max-height")); - - let prefHeight = w.options.getHeight?.() ?? styles.getPropertyValue("--comfy-widget-height"); - if (prefHeight.endsWith?.("%")) { - prefHeight = size[1] * (parseFloat(prefHeight.substring(0, prefHeight.length - 1)) / 100); - } else { - prefHeight = parseInt(prefHeight); - if (isNaN(minHeight)) { - minHeight = prefHeight; - } - } - if (isNaN(minHeight)) { - minHeight = 50; - } - if (!isNaN(maxHeight)) { - if (!isNaN(prefHeight)) { - prefHeight = Math.min(prefHeight, maxHeight); - } else { - prefHeight = maxHeight; - } - } - dom.push({ - minHeight, - prefHeight, - w, - }); - } else { - widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 4; - } - } - - freeSpace -= widgetHeight; - - // Calculate sizes with all widgets at their min height - const prefGrow = []; // Nodes that want to grow to their prefd size - const canGrow = []; // Nodes that can grow to auto size - let growBy = 0; - for (const d of dom) { - freeSpace -= d.minHeight; - if (isNaN(d.prefHeight)) { - canGrow.push(d); - d.w.computedHeight = d.minHeight; - } else { - const diff = d.prefHeight - d.minHeight; - if (diff > 0) { - prefGrow.push(d); - growBy += diff; - d.diff = diff; - } else { - d.w.computedHeight = d.minHeight; - } - } - } - - if (this.imgs && !this.widgets.find((w) => w.name === ANIM_PREVIEW_WIDGET)) { - // Allocate space for image - freeSpace -= 220; - } - - this.freeWidgetSpace = freeSpace; - - if (freeSpace < 0) { - // Not enough space for all widgets so we need to grow - size[1] -= freeSpace; - this.graph.setDirtyCanvas(true); - } else { - // Share the space between each - const growDiff = freeSpace - growBy; - if (growDiff > 0) { - // All pref sizes can be fulfilled - freeSpace = growDiff; - for (const d of prefGrow) { - d.w.computedHeight = d.prefHeight; - } - } else { - // We need to grow evenly - const shared = -growDiff / prefGrow.length; - for (const d of prefGrow) { - d.w.computedHeight = d.prefHeight - shared; - } - freeSpace = 0; - } - - if (freeSpace > 0 && canGrow.length) { - // Grow any that are auto height - const shared = freeSpace / canGrow.length; - for (const d of canGrow) { - d.w.computedHeight += shared; - } - } - } - - // Position each of the widgets - for (const w of this.widgets) { - w.y = y; - if (w.computedHeight) { - y += w.computedHeight; - } else if (w.computeSize) { - y += w.computeSize()[1] + 4; - } else { - y += LiteGraph.NODE_WIDGET_HEIGHT + 4; - } - } -} - -// Override the compute visible nodes function to allow us to hide/show DOM elements when the node goes offscreen -const elementWidgets = new Set(); -const computeVisibleNodes = LGraphCanvas.prototype.computeVisibleNodes; -LGraphCanvas.prototype.computeVisibleNodes = function () { - const visibleNodes = computeVisibleNodes.apply(this, arguments); - for (const node of app.graph._nodes) { - if (elementWidgets.has(node)) { - const hidden = visibleNodes.indexOf(node) === -1; - for (const w of node.widgets) { - if (w.element) { - w.element.hidden = hidden; - w.element.style.display = hidden ? "none" : undefined; - if (hidden) { - w.options.onHide?.(w); - } - } - } - } - } - - return visibleNodes; -}; - -let enableDomClipping = true; - -export function addDomClippingSetting() { - app.ui.settings.addSetting({ - id: "Comfy.DOMClippingEnabled", - name: "Enable DOM element clipping (enabling may reduce performance)", - type: "boolean", - defaultValue: enableDomClipping, - onChange(value) { - enableDomClipping = !!value; - }, - }); -} - -LGraphNode.prototype.addDOMWidget = function (name, type, element, options) { - options = { hideOnZoom: true, selectOn: ["focus", "click"], ...options }; - - if (!element.parentElement) { - document.body.append(element); - } - element.hidden = true; - element.style.display = "none"; - - let mouseDownHandler; - if (element.blur) { - mouseDownHandler = (event) => { - if (!element.contains(event.target)) { - element.blur(); - } - }; - document.addEventListener("mousedown", mouseDownHandler); - } - - const widget = { - type, - name, - get value() { - return options.getValue?.() ?? undefined; - }, - set value(v) { - options.setValue?.(v); - widget.callback?.(widget.value); - }, - draw: function (ctx, node, widgetWidth, y, widgetHeight) { - if (widget.computedHeight == null) { - computeSize.call(node, node.size); - } - - const hidden = - node.flags?.collapsed || - (!!options.hideOnZoom && app.canvas.ds.scale < 0.5) || - widget.computedHeight <= 0 || - widget.type === "converted-widget"|| - widget.type === "hidden"; - element.hidden = hidden; - element.style.display = hidden ? "none" : null; - if (hidden) { - widget.options.onHide?.(widget); - return; - } - - const margin = 10; - const elRect = ctx.canvas.getBoundingClientRect(); - const transform = new DOMMatrix() - .scaleSelf(elRect.width / ctx.canvas.width, elRect.height / ctx.canvas.height) - .multiplySelf(ctx.getTransform()) - .translateSelf(margin, margin + y ); - - const scale = new DOMMatrix().scaleSelf(transform.a, transform.d); - - Object.assign(element.style, { - transformOrigin: "0 0", - transform: scale, - left: `${transform.a + transform.e + elRect.left}px`, - top: `${transform.d + transform.f + elRect.top}px`, - width: `${widgetWidth - margin * 2}px`, - height: `${(widget.computedHeight ?? 50) - margin * 2}px`, - position: "absolute", - zIndex: app.graph._nodes.indexOf(node), - }); - - if (enableDomClipping) { - element.style.clipPath = getClipPath(node, element); - element.style.willChange = "clip-path"; - } - - this.options.onDraw?.(widget); - }, - element, - options, - onRemove() { - if (mouseDownHandler) { - document.removeEventListener("mousedown", mouseDownHandler); - } - element.remove(); - }, - }; - - for (const evt of options.selectOn) { - element.addEventListener(evt, () => { - app.canvas.selectNode(this); - app.canvas.bringToFront(this); - }); - } - - this.addCustomWidget(widget); - elementWidgets.add(this); - - const collapse = this.collapse; - this.collapse = function() { - collapse.apply(this, arguments); - if(this.flags?.collapsed) { - element.hidden = true; - element.style.display = "none"; - } - } - - const onRemoved = this.onRemoved; - this.onRemoved = function () { - element.remove(); - elementWidgets.delete(this); - onRemoved?.apply(this, arguments); - }; - - if (!this[SIZE]) { - this[SIZE] = true; - const onResize = this.onResize; - this.onResize = function (size) { - options.beforeResize?.call(widget, this); - computeSize.call(this, size); - onResize?.apply(this, arguments); - options.afterResize?.call(widget, this); - }; - } - - return widget; -}; diff --git a/web/scripts/logging.js b/web/scripts/logging.js deleted file mode 100644 index 875dd970b..000000000 --- a/web/scripts/logging.js +++ /dev/null @@ -1,370 +0,0 @@ -import { $el, ComfyDialog } from "./ui.js"; -import { api } from "./api.js"; - -$el("style", { - textContent: ` - .comfy-logging-logs { - display: grid; - color: var(--fg-color); - white-space: pre-wrap; - } - .comfy-logging-log { - display: contents; - } - .comfy-logging-title { - background: var(--tr-even-bg-color); - font-weight: bold; - margin-bottom: 5px; - text-align: center; - } - .comfy-logging-log div { - background: var(--row-bg); - padding: 5px; - } - `, - parent: document.body, -}); - -// Stringify function supporting max depth and removal of circular references -// https://stackoverflow.com/a/57193345 -function stringify(val, depth, replacer, space, onGetObjID) { - depth = isNaN(+depth) ? 1 : depth; - var recursMap = new WeakMap(); - function _build(val, depth, o, a, r) { - // (JSON.stringify() has it's own rules, which we respect here by using it for property iteration) - return !val || typeof val != "object" - ? val - : ((r = recursMap.has(val)), - recursMap.set(val, true), - (a = Array.isArray(val)), - r - ? (o = (onGetObjID && onGetObjID(val)) || null) - : JSON.stringify(val, function (k, v) { - if (a || depth > 0) { - if (replacer) v = replacer(k, v); - if (!k) return (a = Array.isArray(v)), (val = v); - !o && (o = a ? [] : {}); - o[k] = _build(v, a ? depth : depth - 1); - } - }), - o === void 0 ? (a ? [] : {}) : o); - } - return JSON.stringify(_build(val, depth), null, space); -} - -const jsonReplacer = (k, v, ui) => { - if (v instanceof Array && v.length === 1) { - v = v[0]; - } - if (v instanceof Date) { - v = v.toISOString(); - if (ui) { - v = v.split("T")[1]; - } - } - if (v instanceof Error) { - let err = ""; - if (v.name) err += v.name + "\n"; - if (v.message) err += v.message + "\n"; - if (v.stack) err += v.stack + "\n"; - if (!err) { - err = v.toString(); - } - v = err; - } - return v; -}; - -const fileInput = $el("input", { - type: "file", - accept: ".json", - style: { display: "none" }, - parent: document.body, -}); - -class ComfyLoggingDialog extends ComfyDialog { - constructor(logging) { - super(); - this.logging = logging; - } - - clear() { - this.logging.clear(); - this.show(); - } - - export() { - const blob = new Blob([stringify([...this.logging.entries], 20, jsonReplacer, "\t")], { - type: "application/json", - }); - const url = URL.createObjectURL(blob); - const a = $el("a", { - href: url, - download: `comfyui-logs-${Date.now()}.json`, - style: { display: "none" }, - parent: document.body, - }); - a.click(); - setTimeout(function () { - a.remove(); - window.URL.revokeObjectURL(url); - }, 0); - } - - import() { - fileInput.onchange = () => { - const reader = new FileReader(); - reader.onload = () => { - fileInput.remove(); - try { - const obj = JSON.parse(reader.result); - if (obj instanceof Array) { - this.show(obj); - } else { - throw new Error("Invalid file selected."); - } - } catch (error) { - alert("Unable to load logs: " + error.message); - } - }; - reader.readAsText(fileInput.files[0]); - }; - fileInput.click(); - } - - createButtons() { - return [ - $el("button", { - type: "button", - textContent: "Clear", - onclick: () => this.clear(), - }), - $el("button", { - type: "button", - textContent: "Export logs...", - onclick: () => this.export(), - }), - $el("button", { - type: "button", - textContent: "View exported logs...", - onclick: () => this.import(), - }), - ...super.createButtons(), - ]; - } - - getTypeColor(type) { - switch (type) { - case "error": - return "red"; - case "warn": - return "orange"; - case "debug": - return "dodgerblue"; - } - } - - show(entries) { - if (!entries) entries = this.logging.entries; - this.element.style.width = "100%"; - const cols = { - source: "Source", - type: "Type", - timestamp: "Timestamp", - message: "Message", - }; - const keys = Object.keys(cols); - const headers = Object.values(cols).map((title) => - $el("div.comfy-logging-title", { - textContent: title, - }) - ); - const rows = entries.map((entry, i) => { - return $el( - "div.comfy-logging-log", - { - $: (el) => el.style.setProperty("--row-bg", `var(--tr-${i % 2 ? "even" : "odd"}-bg-color)`), - }, - keys.map((key) => { - let v = entry[key]; - let color; - if (key === "type") { - color = this.getTypeColor(v); - } else { - v = jsonReplacer(key, v, true); - - if (typeof v === "object") { - v = stringify(v, 5, jsonReplacer, " "); - } - } - - return $el("div", { - style: { - color, - }, - textContent: v, - }); - }) - ); - }); - - const grid = $el( - "div.comfy-logging-logs", - { - style: { - gridTemplateColumns: `repeat(${headers.length}, 1fr)`, - }, - }, - [...headers, ...rows] - ); - const els = [grid]; - if (!this.logging.enabled) { - els.unshift( - $el("h3", { - style: { textAlign: "center" }, - textContent: "Logging is disabled", - }) - ); - } - super.show($el("div", els)); - } -} - -export class ComfyLogging { - /** - * @type Array<{ source: string, type: string, timestamp: Date, message: any }> - */ - entries = []; - - #enabled; - #console = {}; - - get enabled() { - return this.#enabled; - } - - set enabled(value) { - if (value === this.#enabled) return; - if (value) { - this.patchConsole(); - } else { - this.unpatchConsole(); - } - this.#enabled = value; - } - - constructor(app) { - this.app = app; - - this.dialog = new ComfyLoggingDialog(this); - this.addSetting(); - this.catchUnhandled(); - this.addInitData(); - } - - addSetting() { - const settingId = "Comfy.Logging.Enabled"; - const htmlSettingId = settingId.replaceAll(".", "-"); - const setting = this.app.ui.settings.addSetting({ - id: settingId, - name: settingId, - defaultValue: true, - onChange: (value) => { - this.enabled = value; - }, - type: (name, setter, value) => { - return $el("tr", [ - $el("td", [ - $el("label", { - textContent: "Logging", - for: htmlSettingId, - }), - ]), - $el("td", [ - $el("input", { - id: htmlSettingId, - type: "checkbox", - checked: value, - onchange: (event) => { - setter(event.target.checked); - }, - }), - $el("button", { - textContent: "View Logs", - onclick: () => { - this.app.ui.settings.element.close(); - this.dialog.show(); - }, - style: { - fontSize: "14px", - display: "block", - marginTop: "5px", - }, - }), - ]), - ]); - }, - }); - this.enabled = setting.value; - } - - patchConsole() { - // Capture common console outputs - const self = this; - for (const type of ["log", "warn", "error", "debug"]) { - const orig = console[type]; - this.#console[type] = orig; - console[type] = function () { - orig.apply(console, arguments); - self.addEntry("console", type, ...arguments); - }; - } - } - - unpatchConsole() { - // Restore original console functions - for (const type of Object.keys(this.#console)) { - console[type] = this.#console[type]; - } - this.#console = {}; - } - - catchUnhandled() { - // Capture uncaught errors - window.addEventListener("error", (e) => { - this.addEntry("window", "error", e.error ?? "Unknown error"); - return false; - }); - - window.addEventListener("unhandledrejection", (e) => { - this.addEntry("unhandledrejection", "error", e.reason ?? "Unknown error"); - }); - } - - clear() { - this.entries = []; - } - - addEntry(source, type, ...args) { - if (this.enabled) { - this.entries.push({ - source, - type, - timestamp: new Date(), - message: args, - }); - } - } - - log(source, ...args) { - this.addEntry(source, "log", ...args); - } - - async addInitData() { - if (!this.enabled) return; - const source = "ComfyUI.Logging"; - this.addEntry(source, "debug", { UserAgent: navigator.userAgent }); - const systemStats = await api.getSystemStats(); - this.addEntry(source, "debug", systemStats); - } -} diff --git a/web/scripts/pnginfo.js b/web/scripts/pnginfo.js deleted file mode 100644 index 4477ed7a1..000000000 --- a/web/scripts/pnginfo.js +++ /dev/null @@ -1,507 +0,0 @@ -import { api } from "./api.js"; - -export function getPngMetadata(file) { - return new Promise((r) => { - const reader = new FileReader(); - reader.onload = (event) => { - // Get the PNG data as a Uint8Array - const pngData = new Uint8Array(event.target.result); - const dataView = new DataView(pngData.buffer); - - // Check that the PNG signature is present - if (dataView.getUint32(0) !== 0x89504e47) { - console.error("Not a valid PNG file"); - r(); - return; - } - - // Start searching for chunks after the PNG signature - let offset = 8; - let txt_chunks = {}; - // Loop through the chunks in the PNG file - while (offset < pngData.length) { - // Get the length of the chunk - const length = dataView.getUint32(offset); - // Get the chunk type - const type = String.fromCharCode(...pngData.slice(offset + 4, offset + 8)); - if (type === "tEXt" || type == "comf" || type === "iTXt") { - // Get the keyword - let keyword_end = offset + 8; - while (pngData[keyword_end] !== 0) { - keyword_end++; - } - const keyword = String.fromCharCode(...pngData.slice(offset + 8, keyword_end)); - // Get the text - const contentArraySegment = pngData.slice(keyword_end + 1, offset + 8 + length); - const contentJson = new TextDecoder("utf-8").decode(contentArraySegment); - txt_chunks[keyword] = contentJson; - } - - offset += 12 + length; - } - - r(txt_chunks); - }; - - reader.readAsArrayBuffer(file); - }); -} - -function parseExifData(exifData) { - // Check for the correct TIFF header (0x4949 for little-endian or 0x4D4D for big-endian) - const isLittleEndian = String.fromCharCode(...exifData.slice(0, 2)) === "II"; - - // Function to read 16-bit and 32-bit integers from binary data - function readInt(offset, isLittleEndian, length) { - let arr = exifData.slice(offset, offset + length) - if (length === 2) { - return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint16(0, isLittleEndian); - } else if (length === 4) { - return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint32(0, isLittleEndian); - } - } - - // Read the offset to the first IFD (Image File Directory) - const ifdOffset = readInt(4, isLittleEndian, 4); - - function parseIFD(offset) { - const numEntries = readInt(offset, isLittleEndian, 2); - const result = {}; - - for (let i = 0; i < numEntries; i++) { - const entryOffset = offset + 2 + i * 12; - const tag = readInt(entryOffset, isLittleEndian, 2); - const type = readInt(entryOffset + 2, isLittleEndian, 2); - const numValues = readInt(entryOffset + 4, isLittleEndian, 4); - const valueOffset = readInt(entryOffset + 8, isLittleEndian, 4); - - // Read the value(s) based on the data type - let value; - if (type === 2) { - // ASCII string - value = String.fromCharCode(...exifData.slice(valueOffset, valueOffset + numValues - 1)); - } - - result[tag] = value; - } - - return result; - } - - // Parse the first IFD - const ifdData = parseIFD(ifdOffset); - return ifdData; -} - -function splitValues(input) { - var output = {}; - for (var key in input) { - var value = input[key]; - var splitValues = value.split(':', 2); - output[splitValues[0]] = splitValues[1]; - } - return output; -} - -export function getWebpMetadata(file) { - return new Promise((r) => { - const reader = new FileReader(); - reader.onload = (event) => { - const webp = new Uint8Array(event.target.result); - const dataView = new DataView(webp.buffer); - - // Check that the WEBP signature is present - if (dataView.getUint32(0) !== 0x52494646 || dataView.getUint32(8) !== 0x57454250) { - console.error("Not a valid WEBP file"); - r(); - return; - } - - // Start searching for chunks after the WEBP signature - let offset = 12; - let txt_chunks = {}; - // Loop through the chunks in the WEBP file - while (offset < webp.length) { - const chunk_length = dataView.getUint32(offset + 4, true); - const chunk_type = String.fromCharCode(...webp.slice(offset, offset + 4)); - if (chunk_type === "EXIF") { - if (String.fromCharCode(...webp.slice(offset + 8, offset + 8 + 6)) == "Exif\0\0") { - offset += 6; - } - let data = parseExifData(webp.slice(offset + 8, offset + 8 + chunk_length)); - for (var key in data) { - var value = data[key]; - let index = value.indexOf(':'); - txt_chunks[value.slice(0, index)] = value.slice(index + 1); - } - break; - } - - offset += 8 + chunk_length; - } - - r(txt_chunks); - }; - - reader.readAsArrayBuffer(file); - }); -} - -export function getLatentMetadata(file) { - return new Promise((r) => { - const reader = new FileReader(); - reader.onload = (event) => { - const safetensorsData = new Uint8Array(event.target.result); - const dataView = new DataView(safetensorsData.buffer); - let header_size = dataView.getUint32(0, true); - let offset = 8; - let header = JSON.parse(new TextDecoder().decode(safetensorsData.slice(offset, offset + header_size))); - r(header.__metadata__); - }; - - var slice = file.slice(0, 1024 * 1024 * 4); - reader.readAsArrayBuffer(slice); - }); -} - - -function getString(dataView, offset, length) { - let string = ''; - for (let i = 0; i < length; i++) { - string += String.fromCharCode(dataView.getUint8(offset + i)); - } - return string; -} - -// Function to parse the Vorbis Comment block -function parseVorbisComment(dataView) { - let offset = 0; - const vendorLength = dataView.getUint32(offset, true); - offset += 4; - const vendorString = getString(dataView, offset, vendorLength); - offset += vendorLength; - - const userCommentListLength = dataView.getUint32(offset, true); - offset += 4; - const comments = {}; - for (let i = 0; i < userCommentListLength; i++) { - const commentLength = dataView.getUint32(offset, true); - offset += 4; - const comment = getString(dataView, offset, commentLength); - offset += commentLength; - - const ind = comment.indexOf('=') - const key = comment.substring(0, ind); - - comments[key] = comment.substring(ind+1); - } - - return comments; -} - -// Function to read a FLAC file and parse Vorbis comments -export function getFlacMetadata(file) { - return new Promise((r) => { - const reader = new FileReader(); - reader.onload = function(event) { - const arrayBuffer = event.target.result; - const dataView = new DataView(arrayBuffer); - - // Verify the FLAC signature - const signature = String.fromCharCode(...new Uint8Array(arrayBuffer, 0, 4)); - if (signature !== 'fLaC') { - console.error('Not a valid FLAC file'); - return; - } - - // Parse metadata blocks - let offset = 4; - let vorbisComment = null; - while (offset < dataView.byteLength) { - const isLastBlock = dataView.getUint8(offset) & 0x80; - const blockType = dataView.getUint8(offset) & 0x7F; - const blockSize = dataView.getUint32(offset, false) & 0xFFFFFF; - offset += 4; - - if (blockType === 4) { // Vorbis Comment block type - vorbisComment = parseVorbisComment(new DataView(arrayBuffer, offset, blockSize)); - } - - offset += blockSize; - if (isLastBlock) break; - } - - r(vorbisComment); - }; - reader.readAsArrayBuffer(file); - }); -} - -export async function importA1111(graph, parameters) { - const p = parameters.lastIndexOf("\nSteps:"); - if (p > -1) { - const embeddings = await api.getEmbeddings(); - const opts = parameters - .substr(p) - .split("\n")[1] - .match(new RegExp("\\s*([^:]+:\\s*([^\"\\{].*?|\".*?\"|\\{.*?\\}))\\s*(,|$)", "g")) - .reduce((p, n) => { - const s = n.split(":"); - if (s[1].endsWith(',')) { - s[1] = s[1].substr(0, s[1].length -1); - } - p[s[0].trim().toLowerCase()] = s[1].trim(); - return p; - }, {}); - const p2 = parameters.lastIndexOf("\nNegative prompt:", p); - if (p2 > -1) { - let positive = parameters.substr(0, p2).trim(); - let negative = parameters.substring(p2 + 18, p).trim(); - - const ckptNode = LiteGraph.createNode("CheckpointLoaderSimple"); - const clipSkipNode = LiteGraph.createNode("CLIPSetLastLayer"); - const positiveNode = LiteGraph.createNode("CLIPTextEncode"); - const negativeNode = LiteGraph.createNode("CLIPTextEncode"); - const samplerNode = LiteGraph.createNode("KSampler"); - const imageNode = LiteGraph.createNode("EmptyLatentImage"); - const vaeNode = LiteGraph.createNode("VAEDecode"); - const vaeLoaderNode = LiteGraph.createNode("VAELoader"); - const saveNode = LiteGraph.createNode("SaveImage"); - let hrSamplerNode = null; - let hrSteps = null; - - const ceil64 = (v) => Math.ceil(v / 64) * 64; - - function getWidget(node, name) { - return node.widgets.find((w) => w.name === name); - } - - function setWidgetValue(node, name, value, isOptionPrefix) { - const w = getWidget(node, name); - if (isOptionPrefix) { - const o = w.options.values.find((w) => w.startsWith(value)); - if (o) { - w.value = o; - } else { - console.warn(`Unknown value '${value}' for widget '${name}'`, node); - w.value = value; - } - } else { - w.value = value; - } - } - - function createLoraNodes(clipNode, text, prevClip, prevModel) { - const loras = []; - text = text.replace(/]+)>/g, function (m, c) { - const s = c.split(":"); - const weight = parseFloat(s[1]); - if (isNaN(weight)) { - console.warn("Invalid LORA", m); - } else { - loras.push({ name: s[0], weight }); - } - return ""; - }); - - for (const l of loras) { - const loraNode = LiteGraph.createNode("LoraLoader"); - graph.add(loraNode); - setWidgetValue(loraNode, "lora_name", l.name, true); - setWidgetValue(loraNode, "strength_model", l.weight); - setWidgetValue(loraNode, "strength_clip", l.weight); - prevModel.node.connect(prevModel.index, loraNode, 0); - prevClip.node.connect(prevClip.index, loraNode, 1); - prevModel = { node: loraNode, index: 0 }; - prevClip = { node: loraNode, index: 1 }; - } - - prevClip.node.connect(1, clipNode, 0); - prevModel.node.connect(0, samplerNode, 0); - if (hrSamplerNode) { - prevModel.node.connect(0, hrSamplerNode, 0); - } - - return { text, prevModel, prevClip }; - } - - function replaceEmbeddings(text) { - if(!embeddings.length) return text; - return text.replaceAll( - new RegExp( - "\\b(" + embeddings.map((e) => e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\b|\\b") + ")\\b", - "ig" - ), - "embedding:$1" - ); - } - - function popOpt(name) { - const v = opts[name]; - delete opts[name]; - return v; - } - - graph.clear(); - graph.add(ckptNode); - graph.add(clipSkipNode); - graph.add(positiveNode); - graph.add(negativeNode); - graph.add(samplerNode); - graph.add(imageNode); - graph.add(vaeNode); - graph.add(vaeLoaderNode); - graph.add(saveNode); - - ckptNode.connect(1, clipSkipNode, 0); - clipSkipNode.connect(0, positiveNode, 0); - clipSkipNode.connect(0, negativeNode, 0); - ckptNode.connect(0, samplerNode, 0); - positiveNode.connect(0, samplerNode, 1); - negativeNode.connect(0, samplerNode, 2); - imageNode.connect(0, samplerNode, 3); - vaeNode.connect(0, saveNode, 0); - samplerNode.connect(0, vaeNode, 0); - vaeLoaderNode.connect(0, vaeNode, 1); - - const handlers = { - model(v) { - setWidgetValue(ckptNode, "ckpt_name", v, true); - }, - "vae"(v) { - setWidgetValue(vaeLoaderNode, "vae_name", v, true); - }, - "cfg scale"(v) { - setWidgetValue(samplerNode, "cfg", +v); - }, - "clip skip"(v) { - setWidgetValue(clipSkipNode, "stop_at_clip_layer", -v); - }, - sampler(v) { - let name = v.toLowerCase().replace("++", "pp").replaceAll(" ", "_"); - if (name.includes("karras")) { - name = name.replace("karras", "").replace(/_+$/, ""); - setWidgetValue(samplerNode, "scheduler", "karras"); - } else { - setWidgetValue(samplerNode, "scheduler", "normal"); - } - const w = getWidget(samplerNode, "sampler_name"); - const o = w.options.values.find((w) => w === name || w === "sample_" + name); - if (o) { - setWidgetValue(samplerNode, "sampler_name", o); - } - }, - size(v) { - const wxh = v.split("x"); - const w = ceil64(+wxh[0]); - const h = ceil64(+wxh[1]); - const hrUp = popOpt("hires upscale"); - const hrSz = popOpt("hires resize"); - hrSteps = popOpt("hires steps"); - let hrMethod = popOpt("hires upscaler"); - - setWidgetValue(imageNode, "width", w); - setWidgetValue(imageNode, "height", h); - - if (hrUp || hrSz) { - let uw, uh; - if (hrUp) { - uw = w * hrUp; - uh = h * hrUp; - } else { - const s = hrSz.split("x"); - uw = +s[0]; - uh = +s[1]; - } - - let upscaleNode; - let latentNode; - - if (hrMethod.startsWith("Latent")) { - latentNode = upscaleNode = LiteGraph.createNode("LatentUpscale"); - graph.add(upscaleNode); - samplerNode.connect(0, upscaleNode, 0); - - switch (hrMethod) { - case "Latent (nearest-exact)": - hrMethod = "nearest-exact"; - break; - } - setWidgetValue(upscaleNode, "upscale_method", hrMethod, true); - } else { - const decode = LiteGraph.createNode("VAEDecodeTiled"); - graph.add(decode); - samplerNode.connect(0, decode, 0); - vaeLoaderNode.connect(0, decode, 1); - - const upscaleLoaderNode = LiteGraph.createNode("UpscaleModelLoader"); - graph.add(upscaleLoaderNode); - setWidgetValue(upscaleLoaderNode, "model_name", hrMethod, true); - - const modelUpscaleNode = LiteGraph.createNode("ImageUpscaleWithModel"); - graph.add(modelUpscaleNode); - decode.connect(0, modelUpscaleNode, 1); - upscaleLoaderNode.connect(0, modelUpscaleNode, 0); - - upscaleNode = LiteGraph.createNode("ImageScale"); - graph.add(upscaleNode); - modelUpscaleNode.connect(0, upscaleNode, 0); - - const vaeEncodeNode = (latentNode = LiteGraph.createNode("VAEEncodeTiled")); - graph.add(vaeEncodeNode); - upscaleNode.connect(0, vaeEncodeNode, 0); - vaeLoaderNode.connect(0, vaeEncodeNode, 1); - } - - setWidgetValue(upscaleNode, "width", ceil64(uw)); - setWidgetValue(upscaleNode, "height", ceil64(uh)); - - hrSamplerNode = LiteGraph.createNode("KSampler"); - graph.add(hrSamplerNode); - ckptNode.connect(0, hrSamplerNode, 0); - positiveNode.connect(0, hrSamplerNode, 1); - negativeNode.connect(0, hrSamplerNode, 2); - latentNode.connect(0, hrSamplerNode, 3); - hrSamplerNode.connect(0, vaeNode, 0); - } - }, - steps(v) { - setWidgetValue(samplerNode, "steps", +v); - }, - seed(v) { - setWidgetValue(samplerNode, "seed", +v); - }, - }; - - for (const opt in opts) { - if (opt in handlers) { - handlers[opt](popOpt(opt)); - } - } - - if (hrSamplerNode) { - setWidgetValue(hrSamplerNode, "steps", hrSteps? +hrSteps : getWidget(samplerNode, "steps").value); - setWidgetValue(hrSamplerNode, "cfg", getWidget(samplerNode, "cfg").value); - setWidgetValue(hrSamplerNode, "scheduler", getWidget(samplerNode, "scheduler").value); - setWidgetValue(hrSamplerNode, "sampler_name", getWidget(samplerNode, "sampler_name").value); - setWidgetValue(hrSamplerNode, "denoise", +(popOpt("denoising strength") || "1")); - } - - let n = createLoraNodes(positiveNode, positive, { node: clipSkipNode, index: 0 }, { node: ckptNode, index: 0 }); - positive = n.text; - n = createLoraNodes(negativeNode, negative, n.prevClip, n.prevModel); - negative = n.text; - - setWidgetValue(positiveNode, "text", replaceEmbeddings(positive)); - setWidgetValue(negativeNode, "text", replaceEmbeddings(negative)); - - graph.arrange(); - - for (const opt of ["model hash", "ensd", "version", "vae hash", "ti hashes", "lora hashes", "hashes"]) { - delete opts[opt]; - } - - console.warn("Unhandled parameters:", opts); - } - } -} diff --git a/web/scripts/ui.js b/web/scripts/ui.js deleted file mode 100644 index 2c47412c9..000000000 --- a/web/scripts/ui.js +++ /dev/null @@ -1,660 +0,0 @@ -import { api } from "./api.js"; -import { ComfyDialog as _ComfyDialog } from "./ui/dialog.js"; -import { toggleSwitch } from "./ui/toggleSwitch.js"; -import { ComfySettingsDialog } from "./ui/settings.js"; - -export const ComfyDialog = _ComfyDialog; - -/** - * @template { string | (keyof HTMLElementTagNameMap) } K - * @typedef { K extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[K] : HTMLElement } ElementType - */ - -/** - * @template { string | (keyof HTMLElementTagNameMap) } K - * @param { K } tag HTML Element Tag and optional classes e.g. div.class1.class2 - * @param { string | Element | Element[] | ({ - * parent?: Element, - * $?: (el: ElementType) => void, - * dataset?: DOMStringMap, - * style?: Partial, - * for?: string - * } & Omit>, "style">) | undefined } [propsOrChildren] - * @param { string | Element | Element[] | undefined } [children] - * @returns { ElementType } - */ -export function $el(tag, propsOrChildren, children) { - const split = tag.split("."); - const element = document.createElement(split.shift()); - if (split.length > 0) { - element.classList.add(...split); - } - - if (propsOrChildren) { - if (typeof propsOrChildren === "string") { - propsOrChildren = { textContent: propsOrChildren }; - } else if (propsOrChildren instanceof Element) { - propsOrChildren = [propsOrChildren]; - } - if (Array.isArray(propsOrChildren)) { - element.append(...propsOrChildren); - } else { - const {parent, $: cb, dataset, style} = propsOrChildren; - delete propsOrChildren.parent; - delete propsOrChildren.$; - delete propsOrChildren.dataset; - delete propsOrChildren.style; - - if (Object.hasOwn(propsOrChildren, "for")) { - element.setAttribute("for", propsOrChildren.for) - } - - if (style) { - Object.assign(element.style, style); - } - - if (dataset) { - Object.assign(element.dataset, dataset); - } - - Object.assign(element, propsOrChildren); - if (children) { - element.append(...(children instanceof Array ? children.filter(Boolean) : [children])); - } - - if (parent) { - parent.append(element); - } - - if (cb) { - cb(element); - } - } - } - return element; -} - -function dragElement(dragEl, settings) { - var posDiffX = 0, - posDiffY = 0, - posStartX = 0, - posStartY = 0, - newPosX = 0, - newPosY = 0; - if (dragEl.getElementsByClassName("drag-handle")[0]) { - // if present, the handle is where you move the DIV from: - dragEl.getElementsByClassName("drag-handle")[0].onmousedown = dragMouseDown; - } else { - // otherwise, move the DIV from anywhere inside the DIV: - dragEl.onmousedown = dragMouseDown; - } - - // When the element resizes (e.g. view queue) ensure it is still in the windows bounds - const resizeObserver = new ResizeObserver(() => { - ensureInBounds(); - }).observe(dragEl); - - function ensureInBounds() { - try { - newPosX = Math.min(document.body.clientWidth - dragEl.clientWidth, Math.max(0, dragEl.offsetLeft)); - newPosY = Math.min(document.body.clientHeight - dragEl.clientHeight, Math.max(0, dragEl.offsetTop)); - - positionElement(); - } - catch(exception){ - // robust - } - } - - function positionElement() { - if(dragEl.style.display === "none") return; - - const halfWidth = document.body.clientWidth / 2; - const anchorRight = newPosX + dragEl.clientWidth / 2 > halfWidth; - - // set the element's new position: - if (anchorRight) { - dragEl.style.left = "unset"; - dragEl.style.right = document.body.clientWidth - newPosX - dragEl.clientWidth + "px"; - } else { - dragEl.style.left = newPosX + "px"; - dragEl.style.right = "unset"; - } - - dragEl.style.top = newPosY + "px"; - dragEl.style.bottom = "unset"; - - if (savePos) { - localStorage.setItem( - "Comfy.MenuPosition", - JSON.stringify({ - x: dragEl.offsetLeft, - y: dragEl.offsetTop, - }) - ); - } - } - - function restorePos() { - let pos = localStorage.getItem("Comfy.MenuPosition"); - if (pos) { - pos = JSON.parse(pos); - newPosX = pos.x; - newPosY = pos.y; - positionElement(); - ensureInBounds(); - } - } - - let savePos = undefined; - settings.addSetting({ - id: "Comfy.MenuPosition", - name: "Save menu position", - type: "boolean", - defaultValue: savePos, - onChange(value) { - if (savePos === undefined && value) { - restorePos(); - } - savePos = value; - }, - }); - - function dragMouseDown(e) { - e = e || window.event; - e.preventDefault(); - // get the mouse cursor position at startup: - posStartX = e.clientX; - posStartY = e.clientY; - document.onmouseup = closeDragElement; - // call a function whenever the cursor moves: - document.onmousemove = elementDrag; - } - - function elementDrag(e) { - e = e || window.event; - e.preventDefault(); - - dragEl.classList.add("comfy-menu-manual-pos"); - - // calculate the new cursor position: - posDiffX = e.clientX - posStartX; - posDiffY = e.clientY - posStartY; - posStartX = e.clientX; - posStartY = e.clientY; - - newPosX = Math.min(document.body.clientWidth - dragEl.clientWidth, Math.max(0, dragEl.offsetLeft + posDiffX)); - newPosY = Math.min(document.body.clientHeight - dragEl.clientHeight, Math.max(0, dragEl.offsetTop + posDiffY)); - - positionElement(); - } - - window.addEventListener("resize", () => { - ensureInBounds(); - }); - - function closeDragElement() { - // stop moving when mouse button is released: - document.onmouseup = null; - document.onmousemove = null; - } - - return restorePos; -} - -class ComfyList { - #type; - #text; - #reverse; - - constructor(text, type, reverse) { - this.#text = text; - this.#type = type || text.toLowerCase(); - this.#reverse = reverse || false; - this.element = $el("div.comfy-list"); - this.element.style.display = "none"; - } - - get visible() { - return this.element.style.display !== "none"; - } - - async load() { - const items = await api.getItems(this.#type); - this.element.replaceChildren( - ...Object.keys(items).flatMap((section) => [ - $el("h4", { - textContent: section, - }), - $el("div.comfy-list-items", [ - ...(this.#reverse ? items[section].reverse() : items[section]).map((item) => { - // Allow items to specify a custom remove action (e.g. for interrupt current prompt) - const removeAction = item.remove || { - name: "Delete", - cb: () => api.deleteItem(this.#type, item.prompt[1]), - }; - return $el("div", {textContent: item.prompt[0] + ": "}, [ - $el("button", { - textContent: "Load", - onclick: async () => { - await app.loadGraphData(item.prompt[3].extra_pnginfo.workflow, true, false); - if (item.outputs) { - app.nodeOutputs = item.outputs; - } - }, - }), - $el("button", { - textContent: removeAction.name, - onclick: async () => { - await removeAction.cb(); - await this.update(); - }, - }), - ]); - }), - ]), - ]), - $el("div.comfy-list-actions", [ - $el("button", { - textContent: "Clear " + this.#text, - onclick: async () => { - await api.clearItems(this.#type); - await this.load(); - }, - }), - $el("button", {textContent: "Refresh", onclick: () => this.load()}), - ]) - ); - } - - async update() { - if (this.visible) { - await this.load(); - } - } - - async show() { - this.element.style.display = "block"; - this.button.textContent = "Close"; - - await this.load(); - } - - hide() { - this.element.style.display = "none"; - this.button.textContent = "View " + this.#text; - } - - toggle() { - if (this.visible) { - this.hide(); - return false; - } else { - this.show(); - return true; - } - } -} - -export class ComfyUI { - constructor(app) { - this.app = app; - this.dialog = new ComfyDialog(); - this.settings = new ComfySettingsDialog(app); - - this.batchCount = 1; - this.lastQueueSize = 0; - this.queue = new ComfyList("Queue"); - this.history = new ComfyList("History", "history", true); - - api.addEventListener("status", () => { - this.queue.update(); - this.history.update(); - }); - - const confirmClear = this.settings.addSetting({ - id: "Comfy.ConfirmClear", - name: "Require confirmation when clearing workflow", - type: "boolean", - defaultValue: true, - }); - - const promptFilename = this.settings.addSetting({ - id: "Comfy.PromptFilename", - name: "Prompt for filename when saving workflow", - type: "boolean", - defaultValue: true, - }); - - /** - * file format for preview - * - * format;quality - * - * ex) - * webp;50 -> webp, quality 50 - * jpeg;80 -> rgb, jpeg, quality 80 - * - * @type {string} - */ - const previewImage = this.settings.addSetting({ - id: "Comfy.PreviewFormat", - name: "When displaying a preview in the image widget, convert it to a lightweight image, e.g. webp, jpeg, webp;50, etc.", - type: "text", - defaultValue: "", - }); - - this.settings.addSetting({ - id: "Comfy.DisableSliders", - name: "Disable sliders.", - type: "boolean", - defaultValue: false, - }); - - this.settings.addSetting({ - id: "Comfy.DisableFloatRounding", - name: "Disable rounding floats (requires page reload).", - type: "boolean", - defaultValue: false, - }); - - this.settings.addSetting({ - id: "Comfy.FloatRoundingPrecision", - name: "Decimal places [0 = auto] (requires page reload).", - type: "slider", - attrs: { - min: 0, - max: 6, - step: 1, - }, - defaultValue: 0, - }); - - const fileInput = $el("input", { - id: "comfy-file-input", - type: "file", - accept: ".json,image/png,.latent,.safetensors,image/webp,audio/flac", - style: {display: "none"}, - parent: document.body, - onchange: () => { - app.handleFile(fileInput.files[0]); - }, - }); - - this.loadFile = () => fileInput.click(); - - const autoQueueModeEl = toggleSwitch( - "autoQueueMode", - [ - { text: "instant", tooltip: "A new prompt will be queued as soon as the queue reaches 0" }, - { text: "change", tooltip: "A new prompt will be queued when the queue is at 0 and the graph is/has changed" }, - ], - { - onChange: (value) => { - this.autoQueueMode = value.item.value; - }, - } - ); - autoQueueModeEl.style.display = "none"; - - api.addEventListener("graphChanged", () => { - if (this.autoQueueMode === "change" && this.autoQueueEnabled === true) { - if (this.lastQueueSize === 0) { - this.graphHasChanged = false; - app.queuePrompt(0, this.batchCount); - } else { - this.graphHasChanged = true; - } - } - }); - - this.menuHamburger = $el( - "div.comfy-menu-hamburger", - { - parent: document.body, - onclick: () => { - this.menuContainer.style.display = "block"; - this.menuHamburger.style.display = "none"; - }, - }, - [$el("div"), $el("div"), $el("div")] - ); - - this.menuContainer = $el("div.comfy-menu", { parent: document.body }, [ - $el("div.drag-handle.comfy-menu-header", { - style: { - overflow: "hidden", - position: "relative", - width: "100%", - cursor: "default" - } - }, [ - $el("span.drag-handle"), - $el("span.comfy-menu-queue-size", { $: (q) => (this.queueSize = q) }), - $el("div.comfy-menu-actions", [ - $el("button.comfy-settings-btn", { - textContent: "⚙️", - onclick: () => this.settings.show(), - }), - $el("button.comfy-close-menu-btn", { - textContent: "\u00d7", - onclick: () => { - this.menuContainer.style.display = "none"; - this.menuHamburger.style.display = "flex"; - }, - }), - ]), - ]), - $el("button.comfy-queue-btn", { - id: "queue-button", - textContent: "Queue Prompt", - onclick: () => app.queuePrompt(0, this.batchCount), - }), - $el("div", {}, [ - $el("label", {innerHTML: "Extra options"}, [ - $el("input", { - type: "checkbox", - onchange: (i) => { - document.getElementById("extraOptions").style.display = i.srcElement.checked ? "block" : "none"; - this.batchCount = i.srcElement.checked ? document.getElementById("batchCountInputRange").value : 1; - document.getElementById("autoQueueCheckbox").checked = false; - this.autoQueueEnabled = false; - }, - }), - ]), - ]), - $el("div", {id: "extraOptions", style: {width: "100%", display: "none"}}, [ - $el("div",[ - - $el("label", {innerHTML: "Batch count"}), - $el("input", { - id: "batchCountInputNumber", - type: "number", - value: this.batchCount, - min: "1", - style: {width: "35%", "margin-left": "0.4em"}, - oninput: (i) => { - this.batchCount = i.target.value; - document.getElementById("batchCountInputRange").value = this.batchCount; - }, - }), - $el("input", { - id: "batchCountInputRange", - type: "range", - min: "1", - max: "100", - value: this.batchCount, - oninput: (i) => { - this.batchCount = i.srcElement.value; - document.getElementById("batchCountInputNumber").value = i.srcElement.value; - }, - }), - ]), - $el("div",[ - $el("label",{ - for:"autoQueueCheckbox", - innerHTML: "Auto Queue" - }), - $el("input", { - id: "autoQueueCheckbox", - type: "checkbox", - checked: false, - title: "Automatically queue prompt when the queue size hits 0", - onchange: (e) => { - this.autoQueueEnabled = e.target.checked; - autoQueueModeEl.style.display = this.autoQueueEnabled ? "" : "none"; - } - }), - autoQueueModeEl - ]) - ]), - $el("div.comfy-menu-btns", [ - $el("button", { - id: "queue-front-button", - textContent: "Queue Front", - onclick: () => app.queuePrompt(-1, this.batchCount) - }), - $el("button", { - $: (b) => (this.queue.button = b), - id: "comfy-view-queue-button", - textContent: "View Queue", - onclick: () => { - this.history.hide(); - this.queue.toggle(); - }, - }), - $el("button", { - $: (b) => (this.history.button = b), - id: "comfy-view-history-button", - textContent: "View History", - onclick: () => { - this.queue.hide(); - this.history.toggle(); - }, - }), - ]), - this.queue.element, - this.history.element, - $el("button", { - id: "comfy-save-button", - textContent: "Save", - onclick: () => { - let filename = "workflow.json"; - if (promptFilename.value) { - filename = prompt("Save workflow as:", filename); - if (!filename) return; - if (!filename.toLowerCase().endsWith(".json")) { - filename += ".json"; - } - } - app.graphToPrompt().then(p=>{ - const json = JSON.stringify(p.workflow, null, 2); // convert the data to a JSON string - const blob = new Blob([json], {type: "application/json"}); - const url = URL.createObjectURL(blob); - const a = $el("a", { - href: url, - download: filename, - style: {display: "none"}, - parent: document.body, - }); - a.click(); - setTimeout(function () { - a.remove(); - window.URL.revokeObjectURL(url); - }, 0); - }); - }, - }), - $el("button", { - id: "comfy-dev-save-api-button", - textContent: "Save (API Format)", - style: {width: "100%", display: "none"}, - onclick: () => { - let filename = "workflow_api.json"; - if (promptFilename.value) { - filename = prompt("Save workflow (API) as:", filename); - if (!filename) return; - if (!filename.toLowerCase().endsWith(".json")) { - filename += ".json"; - } - } - app.graphToPrompt().then(p=>{ - const json = JSON.stringify(p.output, null, 2); // convert the data to a JSON string - const blob = new Blob([json], {type: "application/json"}); - const url = URL.createObjectURL(blob); - const a = $el("a", { - href: url, - download: filename, - style: {display: "none"}, - parent: document.body, - }); - a.click(); - setTimeout(function () { - a.remove(); - window.URL.revokeObjectURL(url); - }, 0); - }); - }, - }), - $el("button", {id: "comfy-load-button", textContent: "Load", onclick: () => fileInput.click()}), - $el("button", { - id: "comfy-refresh-button", - textContent: "Refresh", - onclick: () => app.refreshComboInNodes() - }), - $el("button", {id: "comfy-clipspace-button", textContent: "Clipspace", onclick: () => app.openClipspace()}), - $el("button", { - id: "comfy-clear-button", textContent: "Clear", onclick: () => { - if (!confirmClear.value || confirm("Clear workflow?")) { - app.clean(); - app.graph.clear(); - app.resetView(); - } - } - }), - $el("button", { - id: "comfy-load-default-button", textContent: "Load Default", onclick: async () => { - if (!confirmClear.value || confirm("Load default workflow?")) { - app.resetView(); - await app.loadGraphData() - } - } - }), - $el("button", { - id: "comfy-reset-view-button", textContent: "Reset View", onclick: async () => { - app.resetView(); - } - }), - ]); - - const devMode = this.settings.addSetting({ - id: "Comfy.DevMode", - name: "Enable Dev mode Options", - type: "boolean", - defaultValue: false, - onChange: function(value) { document.getElementById("comfy-dev-save-api-button").style.display = value ? "flex" : "none"}, - }); - - this.restoreMenuPosition = dragElement(this.menuContainer, this.settings); - - this.setStatus({exec_info: {queue_remaining: "X"}}); - } - - setStatus(status) { - this.queueSize.textContent = "Queue size: " + (status ? status.exec_info.queue_remaining : "ERR"); - if (status) { - if ( - this.lastQueueSize != 0 && - status.exec_info.queue_remaining == 0 && - this.autoQueueEnabled && - (this.autoQueueMode === "instant" || this.graphHasChanged) && - !app.lastExecutionError - ) { - app.queuePrompt(0, this.batchCount); - status.exec_info.queue_remaining += this.batchCount; - this.graphHasChanged = false; - } - this.lastQueueSize = status.exec_info.queue_remaining; - } - } -} diff --git a/web/scripts/ui/components/asyncDialog.js b/web/scripts/ui/components/asyncDialog.js deleted file mode 100644 index 434ce4b30..000000000 --- a/web/scripts/ui/components/asyncDialog.js +++ /dev/null @@ -1,64 +0,0 @@ -import { ComfyDialog } from "../dialog.js"; -import { $el } from "../../ui.js"; - -export class ComfyAsyncDialog extends ComfyDialog { - #resolve; - - constructor(actions) { - super( - "dialog.comfy-dialog.comfyui-dialog", - actions?.map((opt) => { - if (typeof opt === "string") { - opt = { text: opt }; - } - return $el("button.comfyui-button", { - type: "button", - textContent: opt.text, - onclick: () => this.close(opt.value ?? opt.text), - }); - }) - ); - } - - show(html) { - this.element.addEventListener("close", () => { - this.close(); - }); - - super.show(html); - - return new Promise((resolve) => { - this.#resolve = resolve; - }); - } - - showModal(html) { - this.element.addEventListener("close", () => { - this.close(); - }); - - super.show(html); - this.element.showModal(); - - return new Promise((resolve) => { - this.#resolve = resolve; - }); - } - - close(result = null) { - this.#resolve(result); - this.element.close(); - super.close(); - } - - static async prompt({ title = null, message, actions }) { - const dialog = new ComfyAsyncDialog(actions); - const content = [$el("span", message)]; - if (title) { - content.unshift($el("h3", title)); - } - const res = await dialog.showModal(content); - dialog.element.remove(); - return res; - } -} diff --git a/web/scripts/ui/components/button.js b/web/scripts/ui/components/button.js deleted file mode 100644 index 25e5aeebd..000000000 --- a/web/scripts/ui/components/button.js +++ /dev/null @@ -1,163 +0,0 @@ -// @ts-check - -import { $el } from "../../ui.js"; -import { applyClasses, toggleElement } from "../utils.js"; -import { prop } from "../../utils.js"; - -/** - * @typedef {{ - * icon?: string; - * overIcon?: string; - * iconSize?: number; - * content?: string | HTMLElement; - * tooltip?: string; - * enabled?: boolean; - * action?: (e: Event, btn: ComfyButton) => void, - * classList?: import("../utils.js").ClassList, - * visibilitySetting?: { id: string, showValue: any }, - * app?: import("../../app.js").ComfyApp - * }} ComfyButtonProps - */ -export class ComfyButton { - #over = 0; - #popupOpen = false; - isOver = false; - iconElement = $el("i.mdi"); - contentElement = $el("span"); - /** - * @type {import("./popup.js").ComfyPopup} - */ - popup; - - /** - * @param {ComfyButtonProps} opts - */ - constructor({ - icon, - overIcon, - iconSize, - content, - tooltip, - action, - classList = "comfyui-button", - visibilitySetting, - app, - enabled = true, - }) { - this.element = $el("button", { - onmouseenter: () => { - this.isOver = true; - if(this.overIcon) { - this.updateIcon(); - } - }, - onmouseleave: () => { - this.isOver = false; - if(this.overIcon) { - this.updateIcon(); - } - } - - }, [this.iconElement, this.contentElement]); - - this.icon = prop(this, "icon", icon, toggleElement(this.iconElement, { onShow: this.updateIcon })); - this.overIcon = prop(this, "overIcon", overIcon, () => { - if(this.isOver) { - this.updateIcon(); - } - }); - this.iconSize = prop(this, "iconSize", iconSize, this.updateIcon); - this.content = prop( - this, - "content", - content, - toggleElement(this.contentElement, { - onShow: (el, v) => { - if (typeof v === "string") { - el.textContent = v; - } else { - el.replaceChildren(v); - } - }, - }) - ); - - this.tooltip = prop(this, "tooltip", tooltip, (v) => { - if (v) { - this.element.title = v; - } else { - this.element.removeAttribute("title"); - } - }); - this.classList = prop(this, "classList", classList, this.updateClasses); - this.hidden = prop(this, "hidden", false, this.updateClasses); - this.enabled = prop(this, "enabled", enabled, () => { - this.updateClasses(); - this.element.disabled = !this.enabled; - }); - this.action = prop(this, "action", action); - this.element.addEventListener("click", (e) => { - if (this.popup) { - // we are either a touch device or triggered by click not hover - if (!this.#over) { - this.popup.toggle(); - } - } - this.action?.(e, this); - }); - - if (visibilitySetting?.id) { - const settingUpdated = () => { - this.hidden = app.ui.settings.getSettingValue(visibilitySetting.id) !== visibilitySetting.showValue; - }; - app.ui.settings.addEventListener(visibilitySetting.id + ".change", settingUpdated); - settingUpdated(); - } - } - - updateIcon = () => (this.iconElement.className = `mdi mdi-${(this.isOver && this.overIcon) || this.icon}${this.iconSize ? " mdi-" + this.iconSize + "px" : ""}`); - updateClasses = () => { - const internalClasses = []; - if (this.hidden) { - internalClasses.push("hidden"); - } - if (!this.enabled) { - internalClasses.push("disabled"); - } - if (this.popup) { - if (this.#popupOpen) { - internalClasses.push("popup-open"); - } else { - internalClasses.push("popup-closed"); - } - } - applyClasses(this.element, this.classList, ...internalClasses); - }; - - /** - * - * @param { import("./popup.js").ComfyPopup } popup - * @param { "click" | "hover" } mode - */ - withPopup(popup, mode = "click") { - this.popup = popup; - - if (mode === "hover") { - for (const el of [this.element, this.popup.element]) { - el.addEventListener("mouseenter", () => { - this.popup.open = !!++this.#over; - }); - el.addEventListener("mouseleave", () => { - this.popup.open = !!--this.#over; - }); - } - } - - popup.addEventListener("change", () => { - this.#popupOpen = popup.open; - this.updateClasses(); - }); - - return this; - } -} diff --git a/web/scripts/ui/components/buttonGroup.js b/web/scripts/ui/components/buttonGroup.js deleted file mode 100644 index 573572fd0..000000000 --- a/web/scripts/ui/components/buttonGroup.js +++ /dev/null @@ -1,45 +0,0 @@ -// @ts-check - -import { $el } from "../../ui.js"; -import { ComfyButton } from "./button.js"; -import { prop } from "../../utils.js"; - -export class ComfyButtonGroup { - element = $el("div.comfyui-button-group"); - - /** @param {Array} buttons */ - constructor(...buttons) { - this.buttons = prop(this, "buttons", buttons, () => this.update()); - } - - /** - * @param {ComfyButton} button - * @param {number} index - */ - insert(button, index) { - this.buttons.splice(index, 0, button); - this.update(); - } - - /** @param {ComfyButton} button */ - append(button) { - this.buttons.push(button); - this.update(); - } - - /** @param {ComfyButton|number} indexOrButton */ - remove(indexOrButton) { - if (typeof indexOrButton !== "number") { - indexOrButton = this.buttons.indexOf(indexOrButton); - } - if (indexOrButton > -1) { - const r = this.buttons.splice(indexOrButton, 1); - this.update(); - return r; - } - } - - update() { - this.element.replaceChildren(...this.buttons.map((b) => b["element"] ?? b)); - } -} diff --git a/web/scripts/ui/components/popup.js b/web/scripts/ui/components/popup.js deleted file mode 100644 index ee59b35d9..000000000 --- a/web/scripts/ui/components/popup.js +++ /dev/null @@ -1,128 +0,0 @@ -// @ts-check - -import { prop } from "../../utils.js"; -import { $el } from "../../ui.js"; -import { applyClasses } from "../utils.js"; - -export class ComfyPopup extends EventTarget { - element = $el("div.comfyui-popup"); - - /** - * @param {{ - * target: HTMLElement, - * container?: HTMLElement, - * classList?: import("../utils.js").ClassList, - * ignoreTarget?: boolean, - * closeOnEscape?: boolean, - * position?: "absolute" | "relative", - * horizontal?: "left" | "right" - * }} param0 - * @param {...HTMLElement} children - */ - constructor( - { - target, - container = document.body, - classList = "", - ignoreTarget = true, - closeOnEscape = true, - position = "absolute", - horizontal = "left", - }, - ...children - ) { - super(); - this.target = target; - this.ignoreTarget = ignoreTarget; - this.container = container; - this.position = position; - this.closeOnEscape = closeOnEscape; - this.horizontal = horizontal; - - container.append(this.element); - - this.children = prop(this, "children", children, () => { - this.element.replaceChildren(...this.children); - this.update(); - }); - this.classList = prop(this, "classList", classList, () => applyClasses(this.element, this.classList, "comfyui-popup", horizontal)); - this.open = prop(this, "open", false, (v, o) => { - if (v === o) return; - if (v) { - this.#show(); - } else { - this.#hide(); - } - }); - } - - toggle() { - this.open = !this.open; - } - - #hide() { - this.element.classList.remove("open"); - window.removeEventListener("resize", this.update); - window.removeEventListener("click", this.#clickHandler, { capture: true }); - window.removeEventListener("keydown", this.#escHandler, { capture: true }); - - this.dispatchEvent(new CustomEvent("close")); - this.dispatchEvent(new CustomEvent("change")); - } - - #show() { - this.element.classList.add("open"); - this.update(); - - window.addEventListener("resize", this.update); - window.addEventListener("click", this.#clickHandler, { capture: true }); - if (this.closeOnEscape) { - window.addEventListener("keydown", this.#escHandler, { capture: true }); - } - - this.dispatchEvent(new CustomEvent("open")); - this.dispatchEvent(new CustomEvent("change")); - } - - #escHandler = (e) => { - if (e.key === "Escape") { - this.open = false; - e.preventDefault(); - e.stopImmediatePropagation(); - } - }; - - #clickHandler = (e) => { - /** @type {any} */ - const target = e.target; - if (!this.element.contains(target) && this.ignoreTarget && !this.target.contains(target)) { - this.open = false; - } - }; - - update = () => { - const rect = this.target.getBoundingClientRect(); - this.element.style.setProperty("--bottom", "unset"); - if (this.position === "absolute") { - if (this.horizontal === "left") { - this.element.style.setProperty("--left", rect.left + "px"); - } else { - this.element.style.setProperty("--left", rect.right - this.element.clientWidth + "px"); - } - this.element.style.setProperty("--top", rect.bottom + "px"); - this.element.style.setProperty("--limit", rect.bottom + "px"); - } else { - this.element.style.setProperty("--left", 0 + "px"); - this.element.style.setProperty("--top", rect.height + "px"); - this.element.style.setProperty("--limit", rect.height + "px"); - } - - const thisRect = this.element.getBoundingClientRect(); - if (thisRect.height < 30) { - // Move up instead - this.element.style.setProperty("--top", "unset"); - this.element.style.setProperty("--bottom", rect.height + 5 + "px"); - this.element.style.setProperty("--limit", rect.height + 5 + "px"); - } - }; -} diff --git a/web/scripts/ui/components/splitButton.js b/web/scripts/ui/components/splitButton.js deleted file mode 100644 index 2b4e6d9f8..000000000 --- a/web/scripts/ui/components/splitButton.js +++ /dev/null @@ -1,43 +0,0 @@ -// @ts-check - -import { $el } from "../../ui.js"; -import { ComfyButton } from "./button.js"; -import { prop } from "../../utils.js"; -import { ComfyPopup } from "./popup.js"; - -export class ComfySplitButton { - /** - * @param {{ - * primary: ComfyButton, - * mode?: "hover" | "click", - * horizontal?: "left" | "right", - * position?: "relative" | "absolute" - * }} param0 - * @param {Array | Array} items - */ - constructor({ primary, mode, horizontal = "left", position = "relative" }, ...items) { - this.arrow = new ComfyButton({ - icon: "chevron-down", - }); - this.element = $el("div.comfyui-split-button" + (mode === "hover" ? ".hover" : ""), [ - $el("div.comfyui-split-primary", primary.element), - $el("div.comfyui-split-arrow", this.arrow.element), - ]); - this.popup = new ComfyPopup({ - target: this.element, - container: position === "relative" ? this.element : document.body, - classList: "comfyui-split-button-popup" + (mode === "hover" ? " hover" : ""), - closeOnEscape: mode === "click", - position, - horizontal, - }); - - this.arrow.withPopup(this.popup, mode); - - this.items = prop(this, "items", items, () => this.update()); - } - - update() { - this.popup.element.replaceChildren(...this.items.map((b) => b.element ?? b)); - } -} diff --git a/web/scripts/ui/dialog.js b/web/scripts/ui/dialog.js deleted file mode 100644 index 803a97a2b..000000000 --- a/web/scripts/ui/dialog.js +++ /dev/null @@ -1,38 +0,0 @@ -import { $el } from "../ui.js"; - -export class ComfyDialog extends EventTarget { - #buttons; - - constructor(type = "div", buttons = null) { - super(); - this.#buttons = buttons; - this.element = $el(type + ".comfy-modal", { parent: document.body }, [ - $el("div.comfy-modal-content", [$el("p", { $: (p) => (this.textElement = p) }), ...this.createButtons()]), - ]); - } - - createButtons() { - return ( - this.#buttons ?? [ - $el("button", { - type: "button", - textContent: "Close", - onclick: () => this.close(), - }), - ] - ); - } - - close() { - this.element.style.display = "none"; - } - - show(html) { - if (typeof html === "string") { - this.textElement.innerHTML = html; - } else { - this.textElement.replaceChildren(...(html instanceof Array ? html : [html])); - } - this.element.style.display = "flex"; - } -} diff --git a/web/scripts/ui/draggableList.js b/web/scripts/ui/draggableList.js deleted file mode 100644 index d53594886..000000000 --- a/web/scripts/ui/draggableList.js +++ /dev/null @@ -1,287 +0,0 @@ -// @ts-check -/* - Original implementation: - https://github.com/TahaSh/drag-to-reorder - MIT License - - Copyright (c) 2023 Taha Shashtari - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. -*/ - -import { $el } from "../ui.js"; - -$el("style", { - parent: document.head, - textContent: ` - .draggable-item { - position: relative; - will-change: transform; - user-select: none; - } - .draggable-item.is-idle { - transition: 0.25s ease transform; - } - .draggable-item.is-draggable { - z-index: 10; - } - ` -}); - -export class DraggableList extends EventTarget { - listContainer; - draggableItem; - pointerStartX; - pointerStartY; - scrollYMax; - itemsGap = 0; - items = []; - itemSelector; - handleClass = "drag-handle"; - off = []; - offDrag = []; - - constructor(element, itemSelector) { - super(); - this.listContainer = element; - this.itemSelector = itemSelector; - - if (!this.listContainer) return; - - this.off.push(this.on(this.listContainer, "mousedown", this.dragStart)); - this.off.push(this.on(this.listContainer, "touchstart", this.dragStart)); - this.off.push(this.on(document, "mouseup", this.dragEnd)); - this.off.push(this.on(document, "touchend", this.dragEnd)); - } - - getAllItems() { - if (!this.items?.length) { - this.items = Array.from(this.listContainer.querySelectorAll(this.itemSelector)); - this.items.forEach((element) => { - element.classList.add("is-idle"); - }); - } - return this.items; - } - - getIdleItems() { - return this.getAllItems().filter((item) => item.classList.contains("is-idle")); - } - - isItemAbove(item) { - return item.hasAttribute("data-is-above"); - } - - isItemToggled(item) { - return item.hasAttribute("data-is-toggled"); - } - - on(source, event, listener, options) { - listener = listener.bind(this); - source.addEventListener(event, listener, options); - return () => source.removeEventListener(event, listener); - } - - dragStart(e) { - if (e.target.classList.contains(this.handleClass)) { - this.draggableItem = e.target.closest(this.itemSelector); - } - - if (!this.draggableItem) return; - - this.pointerStartX = e.clientX || e.touches[0].clientX; - this.pointerStartY = e.clientY || e.touches[0].clientY; - this.scrollYMax = this.listContainer.scrollHeight - this.listContainer.clientHeight; - - this.setItemsGap(); - this.initDraggableItem(); - this.initItemsState(); - - this.offDrag.push(this.on(document, "mousemove", this.drag)); - this.offDrag.push(this.on(document, "touchmove", this.drag, { passive: false })); - - this.dispatchEvent( - new CustomEvent("dragstart", { - detail: { element: this.draggableItem, position: this.getAllItems().indexOf(this.draggableItem) }, - }) - ); - } - - setItemsGap() { - if (this.getIdleItems().length <= 1) { - this.itemsGap = 0; - return; - } - - const item1 = this.getIdleItems()[0]; - const item2 = this.getIdleItems()[1]; - - const item1Rect = item1.getBoundingClientRect(); - const item2Rect = item2.getBoundingClientRect(); - - this.itemsGap = Math.abs(item1Rect.bottom - item2Rect.top); - } - - initItemsState() { - this.getIdleItems().forEach((item, i) => { - if (this.getAllItems().indexOf(this.draggableItem) > i) { - item.dataset.isAbove = ""; - } - }); - } - - initDraggableItem() { - this.draggableItem.classList.remove("is-idle"); - this.draggableItem.classList.add("is-draggable"); - } - - drag(e) { - if (!this.draggableItem) return; - - e.preventDefault(); - - const clientX = e.clientX || e.touches[0].clientX; - const clientY = e.clientY || e.touches[0].clientY; - - const listRect = this.listContainer.getBoundingClientRect(); - - if (clientY > listRect.bottom) { - if (this.listContainer.scrollTop < this.scrollYMax) { - this.listContainer.scrollBy(0, 10); - this.pointerStartY -= 10; - } - } else if (clientY < listRect.top && this.listContainer.scrollTop > 0) { - this.pointerStartY += 10; - this.listContainer.scrollBy(0, -10); - } - - const pointerOffsetX = clientX - this.pointerStartX; - const pointerOffsetY = clientY - this.pointerStartY; - - this.updateIdleItemsStateAndPosition(); - this.draggableItem.style.transform = `translate(${pointerOffsetX}px, ${pointerOffsetY}px)`; - } - - updateIdleItemsStateAndPosition() { - const draggableItemRect = this.draggableItem.getBoundingClientRect(); - const draggableItemY = draggableItemRect.top + draggableItemRect.height / 2; - - // Update state - this.getIdleItems().forEach((item) => { - const itemRect = item.getBoundingClientRect(); - const itemY = itemRect.top + itemRect.height / 2; - if (this.isItemAbove(item)) { - if (draggableItemY <= itemY) { - item.dataset.isToggled = ""; - } else { - delete item.dataset.isToggled; - } - } else { - if (draggableItemY >= itemY) { - item.dataset.isToggled = ""; - } else { - delete item.dataset.isToggled; - } - } - }); - - // Update position - this.getIdleItems().forEach((item) => { - if (this.isItemToggled(item)) { - const direction = this.isItemAbove(item) ? 1 : -1; - item.style.transform = `translateY(${direction * (draggableItemRect.height + this.itemsGap)}px)`; - } else { - item.style.transform = ""; - } - }); - } - - dragEnd() { - if (!this.draggableItem) return; - - this.applyNewItemsOrder(); - this.cleanup(); - } - - applyNewItemsOrder() { - const reorderedItems = []; - - let oldPosition = -1; - this.getAllItems().forEach((item, index) => { - if (item === this.draggableItem) { - oldPosition = index; - return; - } - if (!this.isItemToggled(item)) { - reorderedItems[index] = item; - return; - } - const newIndex = this.isItemAbove(item) ? index + 1 : index - 1; - reorderedItems[newIndex] = item; - }); - - for (let index = 0; index < this.getAllItems().length; index++) { - const item = reorderedItems[index]; - if (typeof item === "undefined") { - reorderedItems[index] = this.draggableItem; - } - } - - reorderedItems.forEach((item) => { - this.listContainer.appendChild(item); - }); - - this.items = reorderedItems; - - this.dispatchEvent( - new CustomEvent("dragend", { - detail: { element: this.draggableItem, oldPosition, newPosition: reorderedItems.indexOf(this.draggableItem) }, - }) - ); - } - - cleanup() { - this.itemsGap = 0; - this.items = []; - this.unsetDraggableItem(); - this.unsetItemState(); - - this.offDrag.forEach((f) => f()); - this.offDrag = []; - } - - unsetDraggableItem() { - this.draggableItem.style = null; - this.draggableItem.classList.remove("is-draggable"); - this.draggableItem.classList.add("is-idle"); - this.draggableItem = null; - } - - unsetItemState() { - this.getIdleItems().forEach((item, i) => { - delete item.dataset.isAbove; - delete item.dataset.isToggled; - item.style.transform = ""; - }); - } - - dispose() { - this.off.forEach((f) => f()); - } -} diff --git a/web/scripts/ui/imagePreview.js b/web/scripts/ui/imagePreview.js deleted file mode 100644 index 2a7f66b8f..000000000 --- a/web/scripts/ui/imagePreview.js +++ /dev/null @@ -1,97 +0,0 @@ -import { $el } from "../ui.js"; - -export function calculateImageGrid(imgs, dw, dh) { - let best = 0; - let w = imgs[0].naturalWidth; - let h = imgs[0].naturalHeight; - const numImages = imgs.length; - - let cellWidth, cellHeight, cols, rows, shiftX; - // compact style - for (let c = 1; c <= numImages; c++) { - const r = Math.ceil(numImages / c); - const cW = dw / c; - const cH = dh / r; - const scaleX = cW / w; - const scaleY = cH / h; - - const scale = Math.min(scaleX, scaleY, 1); - const imageW = w * scale; - const imageH = h * scale; - const area = imageW * imageH * numImages; - - if (area > best) { - best = area; - cellWidth = imageW; - cellHeight = imageH; - cols = c; - rows = r; - shiftX = c * ((cW - imageW) / 2); - } - } - - return { cellWidth, cellHeight, cols, rows, shiftX }; -} - -export function createImageHost(node) { - const el = $el("div.comfy-img-preview"); - let currentImgs; - let first = true; - - function updateSize() { - let w = null; - let h = null; - - if (currentImgs) { - let elH = el.clientHeight; - if (first) { - first = false; - // On first run, if we are small then grow a bit - if (elH < 190) { - elH = 190; - } - el.style.setProperty("--comfy-widget-min-height", elH); - } else { - el.style.setProperty("--comfy-widget-min-height", null); - } - - const nw = node.size[0]; - ({ cellWidth: w, cellHeight: h } = calculateImageGrid(currentImgs, nw - 20, elH)); - w += "px"; - h += "px"; - - el.style.setProperty("--comfy-img-preview-width", w); - el.style.setProperty("--comfy-img-preview-height", h); - } - } - return { - el, - updateImages(imgs) { - if (imgs !== currentImgs) { - if (currentImgs == null) { - requestAnimationFrame(() => { - updateSize(); - }); - } - el.replaceChildren(...imgs); - currentImgs = imgs; - node.onResize(node.size); - node.graph.setDirtyCanvas(true, true); - } - }, - getHeight() { - updateSize(); - }, - onDraw() { - // Element from point uses a hittest find elements so we need to toggle pointer events - el.style.pointerEvents = "all"; - const over = document.elementFromPoint(app.canvas.mouse[0], app.canvas.mouse[1]); - el.style.pointerEvents = "none"; - - if(!over) return; - // Set the overIndex so Open Image etc work - const idx = currentImgs.indexOf(over); - node.overIndex = idx; - }, - }; -} diff --git a/web/scripts/ui/menu/index.js b/web/scripts/ui/menu/index.js deleted file mode 100644 index 1e00b3d22..000000000 --- a/web/scripts/ui/menu/index.js +++ /dev/null @@ -1,302 +0,0 @@ -// @ts-check - -import { $el } from "../../ui.js"; -import { downloadBlob } from "../../utils.js"; -import { ComfyButton } from "../components/button.js"; -import { ComfyButtonGroup } from "../components/buttonGroup.js"; -import { ComfySplitButton } from "../components/splitButton.js"; -import { ComfyViewHistoryButton } from "./viewHistory.js"; -import { ComfyQueueButton } from "./queueButton.js"; -import { ComfyWorkflowsMenu } from "./workflows.js"; -import { ComfyViewQueueButton } from "./viewQueue.js"; -import { getInteruptButton } from "./interruptButton.js"; - -const collapseOnMobile = (t) => { - (t.element ?? t).classList.add("comfyui-menu-mobile-collapse"); - return t; -}; -const showOnMobile = (t) => { - (t.element ?? t).classList.add("lt-lg-show"); - return t; -}; - -export class ComfyAppMenu { - #sizeBreak = "lg"; - #lastSizeBreaks = { - lg: null, - md: null, - sm: null, - xs: null, - }; - #sizeBreaks = Object.keys(this.#lastSizeBreaks); - #cachedInnerSize = null; - #cacheTimeout = null; - - /** - * @param { import("../../app.js").ComfyApp } app - */ - constructor(app) { - this.app = app; - - this.workflows = new ComfyWorkflowsMenu(app); - const getSaveButton = (t) => - new ComfyButton({ - icon: "content-save", - tooltip: "Save the current workflow", - action: () => app.workflowManager.activeWorkflow.save(), - content: t, - }); - - this.logo = $el("h1.comfyui-logo.nlg-hide", { title: "ComfyUI" }, "ComfyUI"); - this.saveButton = new ComfySplitButton( - { - primary: getSaveButton(), - mode: "hover", - position: "absolute", - }, - getSaveButton("Save"), - new ComfyButton({ - icon: "content-save-edit", - content: "Save As", - tooltip: "Save the current graph as a new workflow", - action: () => app.workflowManager.activeWorkflow.save(true), - }), - new ComfyButton({ - icon: "download", - content: "Export", - tooltip: "Export the current workflow as JSON", - action: () => this.exportWorkflow("workflow", "workflow"), - }), - new ComfyButton({ - icon: "api", - content: "Export (API Format)", - tooltip: "Export the current workflow as JSON for use with the ComfyUI API", - action: () => this.exportWorkflow("workflow_api", "output"), - visibilitySetting: { id: "Comfy.DevMode", showValue: true }, - app, - }) - ); - this.actionsGroup = new ComfyButtonGroup( - new ComfyButton({ - icon: "refresh", - content: "Refresh", - tooltip: "Refresh widgets in nodes to find new models or files", - action: () => app.refreshComboInNodes(), - }), - new ComfyButton({ - icon: "clipboard-edit-outline", - content: "Clipspace", - tooltip: "Open Clipspace window", - action: () => app["openClipspace"](), - }), - new ComfyButton({ - icon: "fit-to-page-outline", - content: "Reset View", - tooltip: "Reset the canvas view", - action: () => app.resetView(), - }), - new ComfyButton({ - icon: "cancel", - content: "Clear", - tooltip: "Clears current workflow", - action: () => { - if (!app.ui.settings.getSettingValue("Comfy.ConfirmClear", true) || confirm("Clear workflow?")) { - app.clean(); - app.graph.clear(); - } - }, - }) - ); - this.settingsGroup = new ComfyButtonGroup( - new ComfyButton({ - icon: "cog", - content: "Settings", - tooltip: "Open settings", - action: () => { - app.ui.settings.show(); - }, - }) - ); - this.viewGroup = new ComfyButtonGroup( - new ComfyViewHistoryButton(app).element, - new ComfyViewQueueButton(app).element, - getInteruptButton("nlg-hide").element - ); - this.mobileMenuButton = new ComfyButton({ - icon: "menu", - action: (_, btn) => { - btn.icon = this.element.classList.toggle("expanded") ? "menu-open" : "menu"; - window.dispatchEvent(new Event("resize")); - }, - classList: "comfyui-button comfyui-menu-button", - }); - - this.element = $el("nav.comfyui-menu.lg", { style: { display: "none" } }, [ - this.logo, - this.workflows.element, - this.saveButton.element, - collapseOnMobile(this.actionsGroup).element, - $el("section.comfyui-menu-push"), - collapseOnMobile(this.settingsGroup).element, - collapseOnMobile(this.viewGroup).element, - - getInteruptButton("lt-lg-show").element, - new ComfyQueueButton(app).element, - showOnMobile(this.mobileMenuButton).element, - ]); - - let resizeHandler; - this.menuPositionSetting = app.ui.settings.addSetting({ - id: "Comfy.UseNewMenu", - defaultValue: "Disabled", - name: "[Beta] Use new menu and workflow management. Note: On small screens the menu will always be at the top.", - type: "combo", - options: ["Disabled", "Top", "Bottom"], - onChange: async (v) => { - if (v && v !== "Disabled") { - if (!resizeHandler) { - resizeHandler = () => { - this.calculateSizeBreak(); - }; - window.addEventListener("resize", resizeHandler); - } - this.updatePosition(v); - } else { - if (resizeHandler) { - window.removeEventListener("resize", resizeHandler); - resizeHandler = null; - } - document.body.style.removeProperty("display"); - app.ui.menuContainer.style.removeProperty("display"); - this.element.style.display = "none"; - app.ui.restoreMenuPosition(); - } - window.dispatchEvent(new Event("resize")); - }, - }); - } - - updatePosition(v) { - document.body.style.display = "grid"; - this.app.ui.menuContainer.style.display = "none"; - this.element.style.removeProperty("display"); - this.position = v; - if (v === "Bottom") { - this.app.bodyBottom.append(this.element); - } else { - this.app.bodyTop.prepend(this.element); - } - this.calculateSizeBreak(); - } - - updateSizeBreak(idx, prevIdx, direction) { - const newSize = this.#sizeBreaks[idx]; - if (newSize === this.#sizeBreak) return; - this.#cachedInnerSize = null; - clearTimeout(this.#cacheTimeout); - - this.#sizeBreak = this.#sizeBreaks[idx]; - for (let i = 0; i < this.#sizeBreaks.length; i++) { - const sz = this.#sizeBreaks[i]; - if (sz === this.#sizeBreak) { - this.element.classList.add(sz); - } else { - this.element.classList.remove(sz); - } - if (i < idx) { - this.element.classList.add("lt-" + sz); - } else { - this.element.classList.remove("lt-" + sz); - } - } - - if (idx) { - // We're on a small screen, force the menu at the top - if (this.position !== "Top") { - this.updatePosition("Top"); - } - } else if (this.position != this.menuPositionSetting.value) { - // Restore user position - this.updatePosition(this.menuPositionSetting.value); - } - - // Allow multiple updates, but prevent bouncing - if (!direction) { - direction = prevIdx - idx; - } else if (direction != prevIdx - idx) { - return; - } - this.calculateSizeBreak(direction); - } - - calculateSizeBreak(direction = 0) { - let idx = this.#sizeBreaks.indexOf(this.#sizeBreak); - const currIdx = idx; - const innerSize = this.calculateInnerSize(idx); - if (window.innerWidth >= this.#lastSizeBreaks[this.#sizeBreaks[idx - 1]]) { - if (idx > 0) { - idx--; - } - } else if (innerSize > this.element.clientWidth) { - this.#lastSizeBreaks[this.#sizeBreak] = Math.max(window.innerWidth, innerSize); - // We need to shrink - if (idx < this.#sizeBreaks.length - 1) { - idx++; - } - } - - this.updateSizeBreak(idx, currIdx, direction); - } - - calculateInnerSize(idx) { - // Cache the inner size to prevent too much calculation when resizing the window - clearTimeout(this.#cacheTimeout); - if (this.#cachedInnerSize) { - // Extend cache time - this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100); - } else { - let innerSize = 0; - let count = 1; - for (const c of this.element.children) { - if (c.classList.contains("comfyui-menu-push")) continue; // ignore right push - if (idx && c.classList.contains("comfyui-menu-mobile-collapse")) continue; // ignore collapse items - innerSize += c.clientWidth; - count++; - } - innerSize += 8 * count; - this.#cachedInnerSize = innerSize; - this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100); - } - return this.#cachedInnerSize; - } - - /** - * @param {string} defaultName - */ - getFilename(defaultName) { - if (this.app.ui.settings.getSettingValue("Comfy.PromptFilename", true)) { - defaultName = prompt("Save workflow as:", defaultName); - if (!defaultName) return; - if (!defaultName.toLowerCase().endsWith(".json")) { - defaultName += ".json"; - } - } - return defaultName; - } - - /** - * @param {string} [filename] - * @param { "workflow" | "output" } [promptProperty] - */ - async exportWorkflow(filename, promptProperty) { - if (this.app.workflowManager.activeWorkflow?.path) { - filename = this.app.workflowManager.activeWorkflow.name; - } - const p = await this.app.graphToPrompt(); - const json = JSON.stringify(p[promptProperty], null, 2); - const blob = new Blob([json], { type: "application/json" }); - const file = this.getFilename(filename); - if (!file) return; - downloadBlob(file, blob); - } -} diff --git a/web/scripts/ui/menu/interruptButton.js b/web/scripts/ui/menu/interruptButton.js deleted file mode 100644 index 4db3328db..000000000 --- a/web/scripts/ui/menu/interruptButton.js +++ /dev/null @@ -1,23 +0,0 @@ -// @ts-check - -import { api } from "../../api.js"; -import { ComfyButton } from "../components/button.js"; - -export function getInteruptButton(visibility) { - const btn = new ComfyButton({ - icon: "close", - tooltip: "Cancel current generation", - enabled: false, - action: () => { - api.interrupt(); - }, - classList: ["comfyui-button", "comfyui-interrupt-button", visibility], - }); - - api.addEventListener("status", ({ detail }) => { - const sz = detail?.exec_info?.queue_remaining; - btn.enabled = sz > 0; - }); - - return btn; -} diff --git a/web/scripts/ui/menu/menu.css b/web/scripts/ui/menu/menu.css deleted file mode 100644 index 1d8b9ad0a..000000000 --- a/web/scripts/ui/menu/menu.css +++ /dev/null @@ -1,710 +0,0 @@ -.relative { - position: relative; -} -.hidden { - display: none !important; -} -.mdi.rotate270::before { - transform: rotate(270deg); -} - -/* Generic */ -.comfyui-button { - display: flex; - align-items: center; - gap: 0.5em; - cursor: pointer; - border: none; - border-radius: 4px; - padding: 4px 8px; - box-sizing: border-box; - margin: 0; - transition: box-shadow 0.1s; -} - -.comfyui-button:active { - box-shadow: inset 1px 1px 10px rgba(0, 0, 0, 0.5); -} -.comfyui-button:disabled { - opacity: 0.5; - cursor: not-allowed; -} -.primary .comfyui-button, -.primary.comfyui-button { - background-color: var(--primary-bg) !important; - color: var(--primary-fg) !important; -} - -.primary .comfyui-button:not(:disabled):hover, -.primary.comfyui-button:not(:disabled):hover { - background-color: var(--primary-hover-bg) !important; - color: var(--primary-hover-fg) !important; -} - -/* Popup */ -.comfyui-popup { - position: absolute; - left: var(--left); - right: var(--right); - top: var(--top); - bottom: var(--bottom); - z-index: 2000; - max-height: calc(100vh - var(--limit) - 10px); - box-shadow: 3px 3px 5px 0px rgba(0, 0, 0, 0.3); -} - -.comfyui-popup:not(.open) { - display: none; -} - -.comfyui-popup.right.open { - border-top-left-radius: 4px; - border-bottom-right-radius: 4px; - border-bottom-left-radius: 4px; - overflow: hidden; -} -/* Split button */ -.comfyui-split-button { - position: relative; - display: flex; -} - -.comfyui-split-primary { - flex: auto; -} - -.comfyui-split-primary .comfyui-button { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - border-right: 1px solid var(--comfy-menu-bg); - width: 100%; -} - -.comfyui-split-arrow .comfyui-button { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - padding-left: 2px; - padding-right: 2px; -} - -.comfyui-split-button-popup { - white-space: nowrap; - background-color: var(--content-bg); - color: var(--content-fg); - display: flex; - flex-direction: column; - overflow: auto; -} - -.comfyui-split-button-popup.hover { - z-index: 2001; -} -.comfyui-split-button-popup > .comfyui-button { - border: none; - background-color: transparent; - color: var(--fg-color); - padding: 8px 12px 8px 8px; -} - -.comfyui-split-button-popup > .comfyui-button:not(:disabled):hover { - background-color: var(--comfy-input-bg); -} - -/* Button group */ -.comfyui-button-group { - display: flex; - border-radius: 4px; - overflow: hidden; -} - -.comfyui-button-group > .comfyui-button, -.comfyui-button-group > .comfyui-button-wrapper > .comfyui-button { - padding: 4px 10px; - border-radius: 0; -} - -/* Menu */ -.comfyui-menu { - width: 100vw; - background: var(--comfy-menu-bg); - color: var(--fg-color); - font-family: Arial, Helvetica, sans-serif; - font-size: 0.8em; - display: flex; - padding: 4px 8px; - align-items: center; - gap: 8px; - box-sizing: border-box; - z-index: 1000; - order: 0; - grid-column: 1/-1; - overflow: auto; - max-height: 90vh; -} - -.comfyui-menu>* { - flex-shrink: 0; -} -.comfyui-menu .mdi::before { - font-size: 18px; -} - -.comfyui-menu .comfyui-button { - background: var(--comfy-input-bg); - color: var(--fg-color); - white-space: nowrap; -} - -.comfyui-menu .comfyui-button:not(:disabled):hover { - background: var(--border-color); - color: var(--content-fg); -} - -.comfyui-menu .comfyui-split-button-popup > .comfyui-button { - border-radius: 0; - background-color: transparent; -} - -.comfyui-menu .comfyui-split-button-popup > .comfyui-button:not(:disabled):hover { - background-color: var(--comfy-input-bg); -} - -.comfyui-menu .comfyui-split-button-popup.left { - border-top-right-radius: 4px; - border-bottom-left-radius: 4px; - border-bottom-right-radius: 4px; -} - -.comfyui-menu .comfyui-button.popup-open { - background-color: var(--content-bg); - color: var(--content-fg); -} - -.comfyui-menu-push { - margin-left: -0.8em; - flex: auto; -} - -.comfyui-logo { - font-size: 1.2em; - margin: 0; - user-select: none; - cursor: default; -} - -/* Workflows */ -.comfyui-workflows-button { - flex-direction: row-reverse; - max-width: 200px; - position: relative; - z-index: 0; -} - -.comfyui-workflows-button.popup-open { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} -.comfyui-workflows-button.unsaved { - font-style: italic; -} -.comfyui-workflows-button-progress { - position: absolute; - top: 0; - left: 0; - background-color: green; - height: 100%; - border-radius: 4px; - z-index: -1; -} - -.comfyui-workflows-button > span { - flex: auto; - text-align: left; - overflow: hidden; -} -.comfyui-workflows-button-inner { - display: flex; - align-items: center; - gap: 7px; - width: 150px; -} -.comfyui-workflows-label { - overflow: hidden; - text-overflow: ellipsis; - direction: rtl; - flex: auto; - position: relative; -} - -.comfyui-workflows-button.unsaved .comfyui-workflows-label { - padding-left: 8px; -} - -.comfyui-workflows-button.unsaved .comfyui-workflows-label:after { - content: "*"; - position: absolute; - top: 0; - left: 0; -} -.comfyui-workflows-button-inner .mdi-graph::before { - transform: rotate(-90deg); -} - -.comfyui-workflows-popup { - font-family: Arial, Helvetica, sans-serif; - font-size: 0.8em; - padding: 10px; - overflow: auto; - background-color: var(--content-bg); - color: var(--content-fg); - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; - border-bottom-left-radius: 4px; - z-index: 400; -} - -.comfyui-workflows-panel { - min-height: 150px; -} - -.comfyui-workflows-panel .lds-ring { - transform: translate(-50%); - position: absolute; - left: 50%; - top: 75px; -} - -.comfyui-workflows-panel h3 { - margin: 10px 0 10px 0; - font-size: 11px; - opacity: 0.8; -} - -.comfyui-workflows-panel section header { - display: flex; - justify-content: space-between; - align-items: center; -} -.comfy-ui-workflows-search .mdi { - position: relative; - top: 2px; - pointer-events: none; -} -.comfy-ui-workflows-search input { - background-color: var(--comfy-input-bg); - color: var(--input-text); - border: none; - border-radius: 4px; - padding: 4px 10px; - margin-left: -24px; - text-indent: 18px; -} -.comfy-ui-workflows-search input:placeholder-shown { - width: 10px; -} -.comfy-ui-workflows-search input:placeholder-shown:focus { - width: auto; -} -.comfyui-workflows-actions { - display: flex; - gap: 10px; - margin-bottom: 10px; -} - -.comfyui-workflows-actions .comfyui-button { - background: var(--comfy-input-bg); - color: var(--input-text); -} - -.comfyui-workflows-actions .comfyui-button:not(:disabled):hover { - background: var(--primary-bg); - color: var(--primary-fg); -} - -.comfyui-workflows-favorites, -.comfyui-workflows-open { - border-bottom: 1px solid var(--comfy-input-bg); - padding-bottom: 5px; - margin-bottom: 5px; -} - -.comfyui-workflows-open .active { - font-weight: bold; - color: var(--primary-fg); -} - -.comfyui-workflows-favorites:empty { - display: none; -} - -.comfyui-workflows-tree { - padding: 0; - margin: 0; -} - -.comfyui-workflows-tree:empty::after { - content: "No saved workflows"; - display: block; - text-align: center; -} -.comfyui-workflows-tree > ul { - padding: 0; -} - -.comfyui-workflows-tree > ul ul { - margin: 0; - padding: 0 0 0 25px; -} - -.comfyui-workflows-tree:not(.filtered) .closed > ul { - display: none; -} - -.comfyui-workflows-tree li, -.comfyui-workflows-tree-file { - --item-height: 32px; - list-style-type: none; - height: var(--item-height); - display: flex; - align-items: center; - gap: 5px; - cursor: pointer; - user-select: none; -} - -.comfyui-workflows-tree-file.active::before, -.comfyui-workflows-tree li:hover::before, -.comfyui-workflows-tree-file:hover::before { - content: ""; - position: absolute; - width: 100%; - left: 0; - height: var(--item-height); - background-color: var(--content-hover-bg); - color: var(--content-hover-fg); - z-index: -1; -} - -.comfyui-workflows-tree-file.active::before { - background-color: var(--primary-bg); - color: var(--primary-fg); -} - -.comfyui-workflows-tree-file.running:not(:hover)::before { - content: ""; - position: absolute; - width: var(--progress, 0); - left: 0; - height: var(--item-height); - background-color: green; - z-index: -1; -} - -.comfyui-workflows-tree-file.unsaved span { - font-style: italic; -} - -.comfyui-workflows-tree-file span { - flex: auto; -} - -.comfyui-workflows-tree-file span + .comfyui-workflows-file-action { - margin-left: 10px; -} - -.comfyui-workflows-tree-file .comfyui-workflows-file-action { - background-color: transparent; - color: var(--fg-color); - padding: 2px 4px; -} - -.comfyui-workflows-tree-file.active .comfyui-workflows-file-action { - color: var(--primary-fg); -} - -.lg ~ .comfyui-workflows-popup .comfyui-workflows-tree-file:not(:hover) .comfyui-workflows-file-action { - opacity: 0; -} - -.comfyui-workflows-tree-file .comfyui-workflows-file-action:hover { - background-color: var(--primary-bg); - color: var(--primary-fg); -} - -.comfyui-workflows-tree-file .comfyui-workflows-file-action-primary { - background-color: transparent; - color: var(--fg-color); - padding: 2px 4px; - margin: 0 -4px; -} - -.comfyui-workflows-file-action-favorite .mdi-star { - color: orange; -} - -/* View List */ -.comfyui-view-list-popup { - padding: 10px; - background-color: var(--content-bg); - color: var(--content-fg); - min-width: 170px; - min-height: 435px; - display: flex; - flex-direction: column; - align-items: center; - box-sizing: border-box; -} -.comfyui-view-list-popup h3 { - margin: 0 0 5px 0; -} -.comfyui-view-list-items { - width: 100%; - background: var(--comfy-menu-bg); - border-radius: 5px; - display: flex; - justify-content: center; - flex: auto; - align-items: center; - flex-direction: column; -} -.comfyui-view-list-items section { - max-height: 400px; - overflow: auto; - width: 100%; - display: grid; - grid-template-columns: auto auto auto; - align-items: center; - justify-content: center; - gap: 5px; - padding: 5px 0; -} -.comfyui-view-list-items section + section { - border-top: 1px solid var(--border-color); - margin-top: 10px; - padding-top: 5px; -} -.comfyui-view-list-items section h5 { - grid-column: 1 / 4; - text-align: center; - margin: 5px; -} -.comfyui-view-list-items span { - text-align: center; - padding: 0 2px; -} -.comfyui-view-list-popup header { - margin-bottom: 10px; - display: flex; - gap: 5px; -} -.comfyui-view-list-popup header .comfyui-button { - border: 1px solid transparent; -} -.comfyui-view-list-popup header .comfyui-button:not(:disabled):hover { - border: 1px solid var(--comfy-menu-bg); -} -/* Queue button */ -.comfyui-queue-button .comfyui-split-primary .comfyui-button { - padding-right: 12px; -} -.comfyui-queue-count { - margin-left: 5px; - border-radius: 10px; - background-color: rgb(8, 80, 153); - padding: 2px 4px; - font-size: 10px; - min-width: 1em; - display: inline-block; -} -/* Queue options*/ -.comfyui-queue-options { - padding: 10px; - font-family: Arial, Helvetica, sans-serif; - font-size: 12px; - display: flex; - gap: 10px; -} - -.comfyui-queue-batch { - display: flex; - flex-direction: column; - border-right: 1px solid var(--comfy-menu-bg); - padding-right: 10px; - gap: 5px; -} - -.comfyui-queue-batch input { - width: 145px; -} - -.comfyui-queue-batch .comfyui-queue-batch-value { - width: 70px; -} - -.comfyui-queue-mode { - display: flex; - flex-direction: column; -} - -.comfyui-queue-mode span { - font-weight: bold; - margin-bottom: 2px; -} - -.comfyui-queue-mode label { - display: flex; - flex-direction: row-reverse; - justify-content: start; - gap: 5px; - padding: 2px 0; -} - -.comfyui-queue-mode label input { - padding: 0; - margin: 0; -} - -/** Send to workflow widget selection dialog */ -.comfy-widget-selection-dialog { - border: none; -} - -.comfy-widget-selection-dialog div { - color: var(--fg-color); - font-family: Arial, Helvetica, sans-serif; -} - -.comfy-widget-selection-dialog h2 { - margin-top: 0; -} - -.comfy-widget-selection-dialog section { - width: fit-content; - display: flex; - flex-direction: column; -} - -.comfy-widget-selection-item { - display: flex; - gap: 10px; - align-items: center; -} - -.comfy-widget-selection-item span { - margin-right: auto; -} - -.comfy-widget-selection-item span::before { - content: '#' attr(data-id); - opacity: 0.5; - margin-right: 5px; -} - -.comfy-modal .comfy-widget-selection-item button { - font-size: 1em; -} - -/***** Responsive *****/ -.lg.comfyui-menu .lt-lg-show { - display: none !important; -} -.comfyui-menu:not(.lg) .nlg-hide { - display: none !important; -} -/** Large screen */ -.lg.comfyui-menu>.comfyui-menu-mobile-collapse .comfyui-button span, -.lg.comfyui-menu>.comfyui-menu-mobile-collapse.comfyui-button span { - display: none; -} -.lg.comfyui-menu>.comfyui-menu-mobile-collapse .comfyui-popup .comfyui-button span { - display: unset; -} - -/** Non large screen */ -.lt-lg.comfyui-menu { - flex-wrap: wrap; -} - -.lt-lg.comfyui-menu > *:not(.comfyui-menu-mobile-collapse) { - order: 1; -} - -.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse { - order: 9999; - width: 100%; -} - -.comfyui-body-bottom .lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse { - order: -1; -} - -.comfyui-body-bottom .lt-lg.comfyui-menu > .comfyui-menu-button { - top: unset; - bottom: 4px; -} - -.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse.comfyui-button-group { - flex-wrap: wrap; -} - -.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse .comfyui-button, -.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse.comfyui-button { - padding: 10px; -} -.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse .comfyui-button, -.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse .comfyui-button-wrapper { - width: 100%; -} - -.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse .comfyui-popup { - position: static; - background-color: var(--comfy-input-bg); - max-width: unset; - max-height: 50vh; - overflow: auto; -} - -.lt-lg.comfyui-menu:not(.expanded) > .comfyui-menu-mobile-collapse { - display: none; -} - -.lt-lg .comfyui-queue-button { - margin-right: 44px; -} - -.lt-lg .comfyui-menu-button { - position: absolute; - top: 4px; - right: 8px; -} - -.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse .comfyui-view-list-popup { - border-radius: 0; -} - -.lt-lg.comfyui-menu .comfyui-workflows-popup { - width: 100vw; -} - -/** Small */ -.lt-md .comfyui-workflows-button-inner { - width: unset !important; -} -.lt-md .comfyui-workflows-label { - display: none; -} - -/** Extra small */ -.lt-sm .comfyui-queue-button { - margin-right: 0; - width: 100%; -} -.lt-sm .comfyui-queue-button .comfyui-button { - justify-content: center; -} -.lt-sm .comfyui-interrupt-button { - margin-right: 45px; -} -.comfyui-body-bottom .lt-sm.comfyui-menu > .comfyui-menu-button{ - bottom: 41px; -} \ No newline at end of file diff --git a/web/scripts/ui/menu/queueButton.js b/web/scripts/ui/menu/queueButton.js deleted file mode 100644 index 608f4cc9b..000000000 --- a/web/scripts/ui/menu/queueButton.js +++ /dev/null @@ -1,93 +0,0 @@ -// @ts-check - -import { ComfyButton } from "../components/button.js"; -import { $el } from "../../ui.js"; -import { api } from "../../api.js"; -import { ComfySplitButton } from "../components/splitButton.js"; -import { ComfyQueueOptions } from "./queueOptions.js"; -import { prop } from "../../utils.js"; - -export class ComfyQueueButton { - element = $el("div.comfyui-queue-button"); - #internalQueueSize = 0; - - queuePrompt = async (e) => { - this.#internalQueueSize += this.queueOptions.batchCount; - // Hold shift to queue front, event is undefined when auto-queue is enabled - await this.app.queuePrompt(e?.shiftKey ? -1 : 0, this.queueOptions.batchCount); - }; - - constructor(app) { - this.app = app; - this.queueSizeElement = $el("span.comfyui-queue-count", { - textContent: "?", - }); - - const queue = new ComfyButton({ - content: $el("div", [ - $el("span", { - textContent: "Queue", - }), - this.queueSizeElement, - ]), - icon: "play", - classList: "comfyui-button", - action: this.queuePrompt, - }); - - this.queueOptions = new ComfyQueueOptions(app); - - const btn = new ComfySplitButton( - { - primary: queue, - mode: "click", - position: "absolute", - horizontal: "right", - }, - this.queueOptions.element - ); - btn.element.classList.add("primary"); - this.element.append(btn.element); - - this.autoQueueMode = prop(this, "autoQueueMode", "", () => { - switch (this.autoQueueMode) { - case "instant": - queue.icon = "infinity"; - break; - case "change": - queue.icon = "auto-mode"; - break; - default: - queue.icon = "play"; - break; - } - }); - - this.queueOptions.addEventListener("autoQueueMode", (e) => (this.autoQueueMode = e["detail"])); - - api.addEventListener("graphChanged", () => { - if (this.autoQueueMode === "change") { - if (this.#internalQueueSize) { - this.graphHasChanged = true; - } else { - this.graphHasChanged = false; - this.queuePrompt(); - } - } - }); - - api.addEventListener("status", ({ detail }) => { - this.#internalQueueSize = detail?.exec_info?.queue_remaining; - if (this.#internalQueueSize != null) { - this.queueSizeElement.textContent = this.#internalQueueSize > 99 ? "99+" : this.#internalQueueSize + ""; - this.queueSizeElement.title = `${this.#internalQueueSize} prompts in queue`; - if (!this.#internalQueueSize && !app.lastExecutionError) { - if (this.autoQueueMode === "instant" || (this.autoQueueMode === "change" && this.graphHasChanged)) { - this.graphHasChanged = false; - this.queuePrompt(); - } - } - } - }); - } -} diff --git a/web/scripts/ui/menu/queueOptions.js b/web/scripts/ui/menu/queueOptions.js deleted file mode 100644 index f3d34e74e..000000000 --- a/web/scripts/ui/menu/queueOptions.js +++ /dev/null @@ -1,77 +0,0 @@ -// @ts-check - -import { $el } from "../../ui.js"; -import { prop } from "../../utils.js"; - -export class ComfyQueueOptions extends EventTarget { - element = $el("div.comfyui-queue-options"); - - constructor(app) { - super(); - this.app = app; - - this.batchCountInput = $el("input", { - className: "comfyui-queue-batch-value", - type: "number", - min: "1", - value: "1", - oninput: () => (this.batchCount = +this.batchCountInput.value), - }); - - this.batchCountRange = $el("input", { - type: "range", - min: "1", - max: "100", - value: "1", - oninput: () => (this.batchCount = +this.batchCountRange.value), - }); - - this.element.append( - $el("div.comfyui-queue-batch", [ - $el( - "label", - { - textContent: "Batch count: ", - }, - this.batchCountInput - ), - this.batchCountRange, - ]) - ); - - const createOption = (text, value, checked = false) => - $el( - "label", - { textContent: text }, - $el("input", { - type: "radio", - name: "AutoQueueMode", - checked, - value, - oninput: (e) => (this.autoQueueMode = e.target["value"]), - }) - ); - - this.autoQueueEl = $el("div.comfyui-queue-mode", [ - $el("span", "Auto Queue:"), - createOption("Disabled", "", true), - createOption("Instant", "instant"), - createOption("On Change", "change"), - ]); - - this.element.append(this.autoQueueEl); - - this.batchCount = prop(this, "batchCount", 1, () => { - this.batchCountInput.value = this.batchCount + ""; - this.batchCountRange.value = this.batchCount + ""; - }); - - this.autoQueueMode = prop(this, "autoQueueMode", "Disabled", () => { - this.dispatchEvent( - new CustomEvent("autoQueueMode", { - detail: this.autoQueueMode, - }) - ); - }); - } -} diff --git a/web/scripts/ui/menu/viewHistory.js b/web/scripts/ui/menu/viewHistory.js deleted file mode 100644 index de6b343dc..000000000 --- a/web/scripts/ui/menu/viewHistory.js +++ /dev/null @@ -1,27 +0,0 @@ -// @ts-check - -import { ComfyButton } from "../components/button.js"; -import { ComfyViewList, ComfyViewListButton } from "./viewList.js"; - -export class ComfyViewHistoryButton extends ComfyViewListButton { - constructor(app) { - super(app, { - button: new ComfyButton({ - content: "View History", - icon: "history", - tooltip: "View history", - classList: "comfyui-button comfyui-history-button", - }), - list: ComfyViewHistoryList, - mode: "History", - }); - } -} - -export class ComfyViewHistoryList extends ComfyViewList { - async loadItems() { - const items = await super.loadItems(); - items["History"].reverse(); - return items; - } -} diff --git a/web/scripts/ui/menu/viewList.js b/web/scripts/ui/menu/viewList.js deleted file mode 100644 index 693ca335c..000000000 --- a/web/scripts/ui/menu/viewList.js +++ /dev/null @@ -1,203 +0,0 @@ -// @ts-check - -import { ComfyButton } from "../components/button.js"; -import { $el } from "../../ui.js"; -import { api } from "../../api.js"; -import { ComfyPopup } from "../components/popup.js"; - -export class ComfyViewListButton { - get open() { - return this.popup.open; - } - - set open(open) { - this.popup.open = open; - } - - constructor(app, { button, list, mode }) { - this.app = app; - this.button = button; - this.element = $el("div.comfyui-button-wrapper", this.button.element); - this.popup = new ComfyPopup({ - target: this.element, - container: this.element, - horizontal: "right", - }); - this.list = new (list ?? ComfyViewList)(app, mode, this.popup); - this.popup.children = [this.list.element]; - this.popup.addEventListener("open", () => { - this.list.update(); - }); - this.popup.addEventListener("close", () => { - this.list.close(); - }); - this.button.withPopup(this.popup); - - api.addEventListener("status", () => { - if (this.popup.open) { - this.popup.update(); - } - }); - } -} - -export class ComfyViewList { - popup; - - constructor(app, mode, popup) { - this.app = app; - this.mode = mode; - this.popup = popup; - this.type = mode.toLowerCase(); - - this.items = $el(`div.comfyui-${this.type}-items.comfyui-view-list-items`); - this.clear = new ComfyButton({ - icon: "cancel", - content: "Clear", - action: async () => { - this.showSpinner(false); - await api.clearItems(this.type); - await this.update(); - }, - }); - - this.refresh = new ComfyButton({ - icon: "refresh", - content: "Refresh", - action: async () => { - await this.update(false); - }, - }); - - this.element = $el(`div.comfyui-${this.type}-popup.comfyui-view-list-popup`, [ - $el("h3", mode), - $el("header", [this.clear.element, this.refresh.element]), - this.items, - ]); - - api.addEventListener("status", () => { - if (this.popup.open) { - this.update(); - } - }); - } - - async close() { - this.items.replaceChildren(); - } - - async update(resize = true) { - this.showSpinner(resize); - const res = await this.loadItems(); - let any = false; - - const names = Object.keys(res); - const sections = names - .map((section) => { - const items = res[section]; - if (items?.length) { - any = true; - } else { - return; - } - - const rows = []; - if (names.length > 1) { - rows.push($el("h5", section)); - } - rows.push(...items.flatMap((item) => this.createRow(item, section))); - return $el("section", rows); - }) - .filter(Boolean); - - if (any) { - this.items.replaceChildren(...sections); - } else { - this.items.replaceChildren($el("h5", "None")); - } - - this.popup.update(); - this.clear.enabled = this.refresh.enabled = true; - this.element.style.removeProperty("height"); - } - - showSpinner(resize = true) { - // if (!this.spinner) { - // this.spinner = createSpinner(); - // } - // if (!resize) { - // this.element.style.height = this.element.clientHeight + "px"; - // } - // this.clear.enabled = this.refresh.enabled = false; - // this.items.replaceChildren( - // $el( - // "div", - // { - // style: { - // fontSize: "18px", - // }, - // }, - // this.spinner - // ) - // ); - // this.popup.update(); - } - - async loadItems() { - return await api.getItems(this.type); - } - - getRow(item, section) { - return { - text: item.prompt[0] + "", - actions: [ - { - text: "Load", - action: async () => { - try { - await this.app.loadGraphData(item.prompt[3].extra_pnginfo.workflow); - if (item.outputs) { - this.app.nodeOutputs = item.outputs; - } - } catch (error) { - alert("Error loading workflow: " + error.message); - console.error(error); - } - }, - }, - { - text: "Delete", - action: async () => { - try { - await api.deleteItem(this.type, item.prompt[1]); - this.update(); - } catch (error) {} - }, - }, - ], - }; - } - - createRow = (item, section) => { - const row = this.getRow(item, section); - return [ - $el("span", row.text), - ...row.actions.map( - (a) => - new ComfyButton({ - content: a.text, - action: async (e, btn) => { - btn.enabled = false; - try { - await a.action(); - } catch (error) { - throw error; - } finally { - btn.enabled = true; - } - }, - }).element - ), - ]; - }; -} diff --git a/web/scripts/ui/menu/viewQueue.js b/web/scripts/ui/menu/viewQueue.js deleted file mode 100644 index 97d012986..000000000 --- a/web/scripts/ui/menu/viewQueue.js +++ /dev/null @@ -1,55 +0,0 @@ -// @ts-check - -import { ComfyButton } from "../components/button.js"; -import { ComfyViewList, ComfyViewListButton } from "./viewList.js"; -import { api } from "../../api.js"; - -export class ComfyViewQueueButton extends ComfyViewListButton { - constructor(app) { - super(app, { - button: new ComfyButton({ - content: "View Queue", - icon: "format-list-numbered", - tooltip: "View queue", - classList: "comfyui-button comfyui-queue-button", - }), - list: ComfyViewQueueList, - mode: "Queue", - }); - } -} - -export class ComfyViewQueueList extends ComfyViewList { - getRow = (item, section) => { - if (section !== "Running") { - return super.getRow(item, section); - } - return { - text: item.prompt[0] + "", - actions: [ - { - text: "Load", - action: async () => { - try { - await this.app.loadGraphData(item.prompt[3].extra_pnginfo.workflow); - if (item.outputs) { - this.app.nodeOutputs = item.outputs; - } - } catch (error) { - alert("Error loading workflow: " + error.message); - console.error(error); - } - }, - }, - { - text: "Cancel", - action: async () => { - try { - await api.interrupt(); - } catch (error) {} - }, - }, - ], - }; - } -} diff --git a/web/scripts/ui/menu/workflows.js b/web/scripts/ui/menu/workflows.js deleted file mode 100644 index 3b904fb4b..000000000 --- a/web/scripts/ui/menu/workflows.js +++ /dev/null @@ -1,770 +0,0 @@ -// @ts-check - -import { ComfyButton } from "../components/button.js"; -import { prop, getStorageValue, setStorageValue } from "../../utils.js"; -import { $el } from "../../ui.js"; -import { api } from "../../api.js"; -import { ComfyPopup } from "../components/popup.js"; -import { createSpinner } from "../spinner.js"; -import { ComfyWorkflow, trimJsonExt } from "../../workflows.js"; -import { ComfyAsyncDialog } from "../components/asyncDialog.js"; - -export class ComfyWorkflowsMenu { - #first = true; - element = $el("div.comfyui-workflows"); - - get open() { - return this.popup.open; - } - - set open(open) { - this.popup.open = open; - } - - /** - * @param {import("../../app.js").ComfyApp} app - */ - constructor(app) { - this.app = app; - this.#bindEvents(); - - const classList = { - "comfyui-workflows-button": true, - "comfyui-button": true, - unsaved: getStorageValue("Comfy.PreviousWorkflowUnsaved") === "true", - running: false, - }; - this.buttonProgress = $el("div.comfyui-workflows-button-progress"); - this.workflowLabel = $el("span.comfyui-workflows-label", ""); - this.button = new ComfyButton({ - content: $el("div.comfyui-workflows-button-inner", [$el("i.mdi.mdi-graph"), this.workflowLabel, this.buttonProgress]), - icon: "chevron-down", - classList, - }); - - this.element.append(this.button.element); - - this.popup = new ComfyPopup({ target: this.element, classList: "comfyui-workflows-popup" }); - this.content = new ComfyWorkflowsContent(app, this.popup); - this.popup.children = [this.content.element]; - this.popup.addEventListener("change", () => { - this.button.icon = "chevron-" + (this.popup.open ? "up" : "down"); - }); - this.button.withPopup(this.popup); - - this.unsaved = prop(this, "unsaved", classList.unsaved, (v) => { - classList.unsaved = v; - this.button.classList = classList; - setStorageValue("Comfy.PreviousWorkflowUnsaved", v); - }); - } - - #updateProgress = () => { - const prompt = this.app.workflowManager.activePrompt; - let percent = 0; - if (this.app.workflowManager.activeWorkflow === prompt?.workflow) { - const total = Object.values(prompt.nodes); - const done = total.filter(Boolean); - percent = (done.length / total.length) * 100; - } - this.buttonProgress.style.width = percent + "%"; - }; - - #updateActive = () => { - const active = this.app.workflowManager.activeWorkflow; - this.button.tooltip = active.path; - this.workflowLabel.textContent = active.name; - this.unsaved = active.unsaved; - - if (this.#first) { - this.#first = false; - this.content.load(); - } - - this.#updateProgress(); - }; - - #bindEvents() { - this.app.workflowManager.addEventListener("changeWorkflow", this.#updateActive); - this.app.workflowManager.addEventListener("rename", this.#updateActive); - this.app.workflowManager.addEventListener("delete", this.#updateActive); - - this.app.workflowManager.addEventListener("save", () => { - this.unsaved = this.app.workflowManager.activeWorkflow.unsaved; - }); - - this.app.workflowManager.addEventListener("execute", (e) => { - this.#updateProgress(); - }); - - api.addEventListener("graphChanged", () => { - this.unsaved = true; - }); - } - - #getMenuOptions(callback) { - const menu = []; - const directories = new Map(); - for (const workflow of this.app.workflowManager.workflows || []) { - const path = workflow.pathParts; - if (!path) continue; - let parent = menu; - let currentPath = ""; - for (let i = 0; i < path.length - 1; i++) { - currentPath += "/" + path[i]; - let newParent = directories.get(currentPath); - if (!newParent) { - newParent = { - title: path[i], - has_submenu: true, - submenu: { - options: [], - }, - }; - parent.push(newParent); - newParent = newParent.submenu.options; - directories.set(currentPath, newParent); - } - parent = newParent; - } - parent.push({ - title: trimJsonExt(path[path.length - 1]), - callback: () => callback(workflow), - }); - } - return menu; - } - - #getFavoriteMenuOptions(callback) { - const menu = []; - for (const workflow of this.app.workflowManager.workflows || []) { - if (workflow.isFavorite) { - menu.push({ - title: "⭐ " + workflow.name, - callback: () => callback(workflow), - }); - } - } - return menu; - } - - /** - * @param {import("../../app.js").ComfyApp} app - */ - registerExtension(app) { - const self = this; - app.registerExtension({ - name: "Comfy.Workflows", - async beforeRegisterNodeDef(nodeType) { - function getImageWidget(node) { - const inputs = { ...node.constructor?.nodeData?.input?.required, ...node.constructor?.nodeData?.input?.optional }; - for (const input in inputs) { - if (inputs[input][0] === "IMAGEUPLOAD") { - const imageWidget = node.widgets.find((w) => w.name === (inputs[input]?.[1]?.widget ?? "image")); - if (imageWidget) return imageWidget; - } - } - } - - function setWidgetImage(node, widget, img) { - const url = new URL(img.src); - const filename = url.searchParams.get("filename"); - const subfolder = url.searchParams.get("subfolder"); - const type = url.searchParams.get("type"); - const imageId = `${subfolder ? subfolder + "/" : ""}${filename} [${type}]`; - widget.value = imageId; - node.imgs = [img]; - app.graph.setDirtyCanvas(true, true); - } - - /** - * @param {HTMLImageElement} img - * @param {ComfyWorkflow} workflow - */ - async function sendToWorkflow(img, workflow) { - const openWorkflow = app.workflowManager.openWorkflows.find((w) => w.path === workflow.path); - if (openWorkflow) { - workflow = openWorkflow; - } - - await workflow.load(); - let options = []; - const nodes = app.graph.computeExecutionOrder(false); - for (const node of nodes) { - const widget = getImageWidget(node); - if (widget == null) continue; - - if (node.title?.toLowerCase().includes("input")) { - options = [{ widget, node }]; - break; - } else { - options.push({ widget, node }); - } - } - - if (!options.length) { - alert("No image nodes have been found in this workflow!"); - return; - } else if (options.length > 1) { - const dialog = new WidgetSelectionDialog(options); - const res = await dialog.show(app); - if (!res) return; - options = [res]; - } - - setWidgetImage(options[0].node, options[0].widget, img); - } - - const getExtraMenuOptions = nodeType.prototype["getExtraMenuOptions"]; - nodeType.prototype["getExtraMenuOptions"] = function (_, options) { - const r = getExtraMenuOptions?.apply?.(this, arguments); - - const setting = app.ui.settings.getSettingValue("Comfy.UseNewMenu", false); - if (setting && setting != "Disabled") { - const t = /** @type { {imageIndex?: number, overIndex?: number, imgs: string[]} } */ /** @type {any} */ (this); - let img; - if (t.imageIndex != null) { - // An image is selected so select that - img = t.imgs?.[t.imageIndex]; - } else if (t.overIndex != null) { - // No image is selected but one is hovered - img = t.img?.s[t.overIndex]; - } - - if (img) { - let pos = options.findIndex((o) => o.content === "Save Image"); - if (pos === -1) { - pos = 0; - } else { - pos++; - } - - options.splice(pos, 0, { - content: "Send to workflow", - has_submenu: true, - submenu: { - options: [ - { - callback: () => sendToWorkflow(img, app.workflowManager.activeWorkflow), - title: "[Current workflow]", - }, - ...self.#getFavoriteMenuOptions(sendToWorkflow.bind(null, img)), - null, - ...self.#getMenuOptions(sendToWorkflow.bind(null, img)), - ], - }, - }); - } - } - - return r; - }; - }, - }); - } -} - -export class ComfyWorkflowsContent { - element = $el("div.comfyui-workflows-panel"); - treeState = {}; - treeFiles = {}; - /** @type { Map } */ - openFiles = new Map(); - /** @type {WorkflowElement} */ - activeElement = null; - - /** - * @param {import("../../app.js").ComfyApp} app - * @param {ComfyPopup} popup - */ - constructor(app, popup) { - this.app = app; - this.popup = popup; - this.actions = $el("div.comfyui-workflows-actions", [ - new ComfyButton({ - content: "Default", - icon: "file-code", - iconSize: 18, - classList: "comfyui-button primary", - tooltip: "Load default workflow", - action: () => { - popup.open = false; - app.loadGraphData(); - app.resetView(); - }, - }).element, - new ComfyButton({ - content: "Browse", - icon: "folder", - iconSize: 18, - tooltip: "Browse for an image or exported workflow", - action: () => { - popup.open = false; - app.ui.loadFile(); - }, - }).element, - new ComfyButton({ - content: "Blank", - icon: "plus-thick", - iconSize: 18, - tooltip: "Create a new blank workflow", - action: () => { - app.workflowManager.setWorkflow(null); - app.clean(); - app.graph.clear(); - app.workflowManager.activeWorkflow.track(); - popup.open = false; - }, - }).element, - ]); - - this.spinner = createSpinner(); - this.element.replaceChildren(this.actions, this.spinner); - - this.popup.addEventListener("open", () => this.load()); - this.popup.addEventListener("close", () => this.element.replaceChildren(this.actions, this.spinner)); - - this.app.workflowManager.addEventListener("favorite", (e) => { - const workflow = e["detail"]; - const button = this.treeFiles[workflow.path]?.primary; - if (!button) return; // Can happen when a workflow is renamed - button.icon = this.#getFavoriteIcon(workflow); - button.overIcon = this.#getFavoriteOverIcon(workflow); - this.updateFavorites(); - }); - - for (const e of ["save", "open", "close", "changeWorkflow"]) { - // TODO: dont be lazy and just update the specific element - app.workflowManager.addEventListener(e, () => this.updateOpen()); - } - this.app.workflowManager.addEventListener("rename", () => this.load()); - this.app.workflowManager.addEventListener("execute", (e) => this.#updateActive()); - } - - async load() { - await this.app.workflowManager.loadWorkflows(); - this.updateTree(); - this.updateFavorites(); - this.updateOpen(); - this.element.replaceChildren(this.actions, this.openElement, this.favoritesElement, this.treeElement); - } - - updateOpen() { - const current = this.openElement; - this.openFiles.clear(); - - this.openElement = $el("div.comfyui-workflows-open", [ - $el("h3", "Open"), - ...this.app.workflowManager.openWorkflows.map((w) => { - const wrapper = new WorkflowElement(this, w, { - primary: { element: $el("i.mdi.mdi-18px.mdi-progress-pencil") }, - buttons: [ - this.#getRenameButton(w), - new ComfyButton({ - icon: "close", - iconSize: 18, - classList: "comfyui-button comfyui-workflows-file-action", - tooltip: "Close workflow", - action: (e) => { - e.stopImmediatePropagation(); - this.app.workflowManager.closeWorkflow(w); - }, - }), - ], - }); - if (w.unsaved) { - wrapper.element.classList.add("unsaved"); - } - if(w === this.app.workflowManager.activeWorkflow) { - wrapper.element.classList.add("active"); - } - - this.openFiles.set(w, wrapper); - return wrapper.element; - }), - ]); - - this.#updateActive(); - current?.replaceWith(this.openElement); - } - - updateFavorites() { - const current = this.favoritesElement; - const favorites = [...this.app.workflowManager.workflows.filter((w) => w.isFavorite)]; - - this.favoritesElement = $el("div.comfyui-workflows-favorites", [ - $el("h3", "Favorites"), - ...favorites - .map((w) => { - return this.#getWorkflowElement(w).element; - }) - .filter(Boolean), - ]); - - current?.replaceWith(this.favoritesElement); - } - - filterTree() { - if (!this.filterText) { - this.treeRoot.classList.remove("filtered"); - // Unfilter whole tree - for (const item of Object.values(this.treeFiles)) { - item.element.parentElement.style.removeProperty("display"); - this.showTreeParents(item.element.parentElement); - } - return; - } - this.treeRoot.classList.add("filtered"); - const searchTerms = this.filterText.toLocaleLowerCase().split(" "); - for (const item of Object.values(this.treeFiles)) { - const parts = item.workflow.pathParts; - let termIndex = 0; - let valid = false; - for (const part of parts) { - let currentIndex = 0; - do { - currentIndex = part.indexOf(searchTerms[termIndex], currentIndex); - if (currentIndex > -1) currentIndex += searchTerms[termIndex].length; - } while (currentIndex !== -1 && ++termIndex < searchTerms.length); - - if (termIndex >= searchTerms.length) { - valid = true; - break; - } - } - if (valid) { - item.element.parentElement.style.removeProperty("display"); - this.showTreeParents(item.element.parentElement); - } else { - item.element.parentElement.style.display = "none"; - this.hideTreeParents(item.element.parentElement); - } - } - } - - hideTreeParents(element) { - // Hide all parents if no children are visible - if (element.parentElement?.classList.contains("comfyui-workflows-tree") === false) { - for (let i = 1; i < element.parentElement.children.length; i++) { - const c = element.parentElement.children[i]; - if (c.style.display !== "none") { - return; - } - } - element.parentElement.style.display = "none"; - this.hideTreeParents(element.parentElement); - } - } - - showTreeParents(element) { - if (element.parentElement?.classList.contains("comfyui-workflows-tree") === false) { - element.parentElement.style.removeProperty("display"); - this.showTreeParents(element.parentElement); - } - } - - updateTree() { - const current = this.treeElement; - const nodes = {}; - let typingTimeout; - - this.treeFiles = {}; - this.treeRoot = $el("ul.comfyui-workflows-tree"); - this.treeElement = $el("section", [ - $el("header", [ - $el("h3", "Browse"), - $el("div.comfy-ui-workflows-search", [ - $el("i.mdi.mdi-18px.mdi-magnify"), - $el("input", { - placeholder: "Search", - value: this.filterText ?? "", - oninput: (e) => { - this.filterText = e.target["value"]?.trim(); - clearTimeout(typingTimeout); - typingTimeout = setTimeout(() => this.filterTree(), 250); - }, - }), - ]), - ]), - this.treeRoot, - ]); - - for (const workflow of this.app.workflowManager.workflows) { - if (!workflow.pathParts) continue; - - let currentPath = ""; - let currentRoot = this.treeRoot; - - for (let i = 0; i < workflow.pathParts.length; i++) { - currentPath += (currentPath ? "\\" : "") + workflow.pathParts[i]; - const parentNode = nodes[currentPath] ?? this.#createNode(currentPath, workflow, i, currentRoot); - - nodes[currentPath] = parentNode; - currentRoot = parentNode; - } - } - - current?.replaceWith(this.treeElement); - this.filterTree(); - } - - #expandNode(el, workflow, thisPath, i) { - const expanded = !el.classList.toggle("closed"); - if (expanded) { - let c = ""; - for (let j = 0; j <= i; j++) { - c += (c ? "\\" : "") + workflow.pathParts[j]; - this.treeState[c] = true; - } - } else { - let c = thisPath; - for (let j = i + 1; j < workflow.pathParts.length; j++) { - c += (c ? "\\" : "") + workflow.pathParts[j]; - delete this.treeState[c]; - } - delete this.treeState[thisPath]; - } - } - - #updateActive() { - this.#removeActive(); - - const active = this.app.workflowManager.activePrompt; - if (!active?.workflow) return; - - const open = this.openFiles.get(active.workflow); - if (!open) return; - - this.activeElement = open; - - const total = Object.values(active.nodes); - const done = total.filter(Boolean); - const percent = done.length / total.length; - open.element.classList.add("running"); - open.element.style.setProperty("--progress", percent * 100 + "%"); - open.primary.element.classList.remove("mdi-progress-pencil"); - open.primary.element.classList.add("mdi-play"); - } - - #removeActive() { - if (!this.activeElement) return; - this.activeElement.element.classList.remove("running"); - this.activeElement.element.style.removeProperty("--progress"); - this.activeElement.primary.element.classList.add("mdi-progress-pencil"); - this.activeElement.primary.element.classList.remove("mdi-play"); - } - - /** @param {ComfyWorkflow} workflow */ - #getFavoriteIcon(workflow) { - return workflow.isFavorite ? "star" : "file-outline"; - } - - /** @param {ComfyWorkflow} workflow */ - #getFavoriteOverIcon(workflow) { - return workflow.isFavorite ? "star-off" : "star-outline"; - } - - /** @param {ComfyWorkflow} workflow */ - #getFavoriteTooltip(workflow) { - return workflow.isFavorite ? "Remove this workflow from your favorites" : "Add this workflow to your favorites"; - } - - /** @param {ComfyWorkflow} workflow */ - #getFavoriteButton(workflow, primary) { - return new ComfyButton({ - icon: this.#getFavoriteIcon(workflow), - overIcon: this.#getFavoriteOverIcon(workflow), - iconSize: 18, - classList: "comfyui-button comfyui-workflows-file-action-favorite" + (primary ? " comfyui-workflows-file-action-primary" : ""), - tooltip: this.#getFavoriteTooltip(workflow), - action: (e) => { - e.stopImmediatePropagation(); - workflow.favorite(!workflow.isFavorite); - }, - }); - } - - /** @param {ComfyWorkflow} workflow */ - #getDeleteButton(workflow) { - const deleteButton = new ComfyButton({ - icon: "delete", - tooltip: "Delete this workflow", - classList: "comfyui-button comfyui-workflows-file-action", - iconSize: 18, - action: async (e, btn) => { - e.stopImmediatePropagation(); - - if (btn.icon === "delete-empty") { - btn.enabled = false; - await workflow.delete(); - await this.load(); - } else { - btn.icon = "delete-empty"; - btn.element.style.background = "red"; - } - }, - }); - deleteButton.element.addEventListener("mouseleave", () => { - deleteButton.icon = "delete"; - deleteButton.element.style.removeProperty("background"); - }); - return deleteButton; - } - - /** @param {ComfyWorkflow} workflow */ - #getInsertButton(workflow) { - return new ComfyButton({ - icon: "file-move-outline", - iconSize: 18, - tooltip: "Insert this workflow into the current workflow", - classList: "comfyui-button comfyui-workflows-file-action", - action: (e) => { - if (!this.app.shiftDown) { - this.popup.open = false; - } - e.stopImmediatePropagation(); - if (!this.app.shiftDown) { - this.popup.open = false; - } - workflow.insert(); - }, - }); - } - - /** @param {ComfyWorkflow} workflow */ - #getRenameButton(workflow) { - return new ComfyButton({ - icon: "pencil", - tooltip: workflow.path ? "Rename this workflow" : "This workflow can't be renamed as it hasn't been saved.", - classList: "comfyui-button comfyui-workflows-file-action", - iconSize: 18, - enabled: !!workflow.path, - action: async (e) => { - e.stopImmediatePropagation(); - const newName = prompt("Enter new name", workflow.path); - if (newName) { - await workflow.rename(newName); - } - }, - }); - } - - /** @param {ComfyWorkflow} workflow */ - #getWorkflowElement(workflow) { - return new WorkflowElement(this, workflow, { - primary: this.#getFavoriteButton(workflow, true), - buttons: [this.#getInsertButton(workflow), this.#getRenameButton(workflow), this.#getDeleteButton(workflow)], - }); - } - - /** @param {ComfyWorkflow} workflow */ - #createLeafNode(workflow) { - const fileNode = this.#getWorkflowElement(workflow); - this.treeFiles[workflow.path] = fileNode; - return fileNode; - } - - #createNode(currentPath, workflow, i, currentRoot) { - const part = workflow.pathParts[i]; - - const parentNode = $el("ul" + (this.treeState[currentPath] ? "" : ".closed"), { - $: (el) => { - el.onclick = (e) => { - this.#expandNode(el, workflow, currentPath, i); - e.stopImmediatePropagation(); - }; - }, - }); - currentRoot.append(parentNode); - - // Create a node for the current part and an inner UL for its children if it isnt a leaf node - const leaf = i === workflow.pathParts.length - 1; - let nodeElement; - if (leaf) { - nodeElement = this.#createLeafNode(workflow).element; - } else { - nodeElement = $el("li", [$el("i.mdi.mdi-18px.mdi-folder"), $el("span", part)]); - } - parentNode.append(nodeElement); - return parentNode; - } -} - -class WorkflowElement { - /** - * @param { ComfyWorkflowsContent } parent - * @param { ComfyWorkflow } workflow - */ - constructor(parent, workflow, { tagName = "li", primary, buttons }) { - this.parent = parent; - this.workflow = workflow; - this.primary = primary; - this.buttons = buttons; - - this.element = $el( - tagName + ".comfyui-workflows-tree-file", - { - onclick: () => { - workflow.load(); - this.parent.popup.open = false; - }, - title: this.workflow.path, - }, - [this.primary?.element, $el("span", workflow.name), ...buttons.map((b) => b.element)] - ); - } -} - -class WidgetSelectionDialog extends ComfyAsyncDialog { - #options; - - /** - * @param {Array<{widget: {name: string}, node: {pos: [number, number], title: string, id: string, type: string}}>} options - */ - constructor(options) { - super(); - this.#options = options; - } - - show(app) { - this.element.classList.add("comfy-widget-selection-dialog"); - return super.show( - $el("div", [ - $el("h2", "Select image target"), - $el( - "p", - "This workflow has multiple image loader nodes, you can rename a node to include 'input' in the title for it to be automatically selected, or select one below." - ), - $el( - "section", - this.#options.map((opt) => { - return $el("div.comfy-widget-selection-item", [ - $el("span", { dataset: { id: opt.node.id } }, `${opt.node.title ?? opt.node.type} ${opt.widget.name}`), - $el( - "button.comfyui-button", - { - onclick: () => { - app.canvas.ds.offset[0] = -opt.node.pos[0] + 50; - app.canvas.ds.offset[1] = -opt.node.pos[1] + 50; - app.canvas.selectNode(opt.node); - app.graph.setDirtyCanvas(true, true); - }, - }, - "Show" - ), - $el( - "button.comfyui-button.primary", - { - onclick: () => { - this.close(opt); - }, - }, - "Select" - ), - ]); - }) - ), - ]) - ); - } -} \ No newline at end of file diff --git a/web/scripts/ui/settings.js b/web/scripts/ui/settings.js deleted file mode 100644 index 819e4e7d6..000000000 --- a/web/scripts/ui/settings.js +++ /dev/null @@ -1,333 +0,0 @@ -import { $el } from "../ui.js"; -import { api } from "../api.js"; -import { ComfyDialog } from "./dialog.js"; - -export class ComfySettingsDialog extends ComfyDialog { - constructor(app) { - super(); - this.app = app; - this.settingsValues = {}; - this.settingsLookup = {}; - this.element = $el( - "dialog", - { - id: "comfy-settings-dialog", - parent: document.body, - }, - [ - $el("table.comfy-modal-content.comfy-table", [ - $el( - "caption", - { textContent: "Settings" }, - $el("button.comfy-btn", { - type: "button", - textContent: "\u00d7", - onclick: () => { - this.element.close(); - }, - }) - ), - $el("tbody", { $: (tbody) => (this.textElement = tbody) }), - $el("button", { - type: "button", - textContent: "Close", - style: { - cursor: "pointer", - }, - onclick: () => { - this.element.close(); - }, - }), - ]), - ] - ); - } - - get settings() { - return Object.values(this.settingsLookup); - } - - #dispatchChange(id, value, oldValue) { - this.dispatchEvent( - new CustomEvent(id + ".change", { - detail: { - value, - oldValue - }, - }) - ); - } - - async load() { - if (this.app.storageLocation === "browser") { - this.settingsValues = localStorage; - } else { - this.settingsValues = await api.getSettings(); - } - - // Trigger onChange for any settings added before load - for (const id in this.settingsLookup) { - const value = this.settingsValues[this.getId(id)]; - this.settingsLookup[id].onChange?.(value); - this.#dispatchChange(id, value); - } - } - - getId(id) { - if (this.app.storageLocation === "browser") { - id = "Comfy.Settings." + id; - } - return id; - } - - getSettingValue(id, defaultValue) { - let value = this.settingsValues[this.getId(id)]; - if(value != null) { - if(this.app.storageLocation === "browser") { - try { - value = JSON.parse(value); - } catch (error) { - } - } - } - return value ?? defaultValue; - } - - async setSettingValueAsync(id, value) { - const json = JSON.stringify(value); - localStorage["Comfy.Settings." + id] = json; // backwards compatibility for extensions keep setting in storage - - let oldValue = this.getSettingValue(id, undefined); - this.settingsValues[this.getId(id)] = value; - - if (id in this.settingsLookup) { - this.settingsLookup[id].onChange?.(value, oldValue); - } - this.#dispatchChange(id, value, oldValue); - - await api.storeSetting(id, value); - } - - setSettingValue(id, value) { - this.setSettingValueAsync(id, value).catch((err) => { - alert(`Error saving setting '${id}'`); - console.error(err); - }); - } - - addSetting({ id, name, type, defaultValue, onChange, attrs = {}, tooltip = "", options = undefined }) { - if (!id) { - throw new Error("Settings must have an ID"); - } - - if (id in this.settingsLookup) { - throw new Error(`Setting ${id} of type ${type} must have a unique ID.`); - } - - let skipOnChange = false; - let value = this.getSettingValue(id); - if (value == null) { - if (this.app.isNewUserSession) { - // Check if we have a localStorage value but not a setting value and we are a new user - const localValue = localStorage["Comfy.Settings." + id]; - if (localValue) { - value = JSON.parse(localValue); - this.setSettingValue(id, value); // Store on the server - } - } - if (value == null) { - value = defaultValue; - } - } - - // Trigger initial setting of value - if (!skipOnChange) { - onChange?.(value, undefined); - } - - this.settingsLookup[id] = { - id, - onChange, - name, - render: () => { - if (type === "hidden") return; - - const setter = (v) => { - if (onChange) { - onChange(v, value); - } - - this.setSettingValue(id, v); - value = v; - }; - value = this.getSettingValue(id, defaultValue); - - let element; - const htmlID = id.replaceAll(".", "-"); - - const labelCell = $el("td", [ - $el("label", { - for: htmlID, - classList: [tooltip !== "" ? "comfy-tooltip-indicator" : ""], - textContent: name, - }), - ]); - - if (typeof type === "function") { - element = type(name, setter, value, attrs); - } else { - switch (type) { - case "boolean": - element = $el("tr", [ - labelCell, - $el("td", [ - $el("input", { - id: htmlID, - type: "checkbox", - checked: value, - onchange: (event) => { - const isChecked = event.target.checked; - if (onChange !== undefined) { - onChange(isChecked); - } - this.setSettingValue(id, isChecked); - }, - }), - ]), - ]); - break; - case "number": - element = $el("tr", [ - labelCell, - $el("td", [ - $el("input", { - type, - value, - id: htmlID, - oninput: (e) => { - setter(e.target.value); - }, - ...attrs, - }), - ]), - ]); - break; - case "slider": - element = $el("tr", [ - labelCell, - $el("td", [ - $el( - "div", - { - style: { - display: "grid", - gridAutoFlow: "column", - }, - }, - [ - $el("input", { - ...attrs, - value, - type: "range", - oninput: (e) => { - setter(e.target.value); - e.target.nextElementSibling.value = e.target.value; - }, - }), - $el("input", { - ...attrs, - value, - id: htmlID, - type: "number", - style: { maxWidth: "4rem" }, - oninput: (e) => { - setter(e.target.value); - e.target.previousElementSibling.value = e.target.value; - }, - }), - ] - ), - ]), - ]); - break; - case "combo": - element = $el("tr", [ - labelCell, - $el("td", [ - $el( - "select", - { - oninput: (e) => { - setter(e.target.value); - }, - }, - (typeof options === "function" ? options(value) : options || []).map((opt) => { - if (typeof opt === "string") { - opt = { text: opt }; - } - const v = opt.value ?? opt.text; - return $el("option", { - value: v, - textContent: opt.text, - selected: value + "" === v + "", - }); - }) - ), - ]), - ]); - break; - case "text": - default: - if (type !== "text") { - console.warn(`Unsupported setting type '${type}, defaulting to text`); - } - - element = $el("tr", [ - labelCell, - $el("td", [ - $el("input", { - value, - id: htmlID, - oninput: (e) => { - setter(e.target.value); - }, - ...attrs, - }), - ]), - ]); - break; - } - } - if (tooltip) { - element.title = tooltip; - } - - return element; - }, - }; - - const self = this; - return { - get value() { - return self.getSettingValue(id, defaultValue); - }, - set value(v) { - self.setSettingValue(id, v); - }, - }; - } - - show() { - this.textElement.replaceChildren( - $el( - "tr", - { - style: { display: "none" }, - }, - [$el("th"), $el("th", { style: { width: "33%" } })] - ), - ...this.settings.sort((a, b) => a.name.localeCompare(b.name)).map((s) => s.render()).filter(Boolean) - ); - this.element.showModal(); - } -} diff --git a/web/scripts/ui/spinner.css b/web/scripts/ui/spinner.css deleted file mode 100644 index 56da6072e..000000000 --- a/web/scripts/ui/spinner.css +++ /dev/null @@ -1,34 +0,0 @@ -.lds-ring { - display: inline-block; - position: relative; - width: 1em; - height: 1em; -} -.lds-ring div { - box-sizing: border-box; - display: block; - position: absolute; - width: 100%; - height: 100%; - border: 0.15em solid #fff; - border-radius: 50%; - animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; - border-color: #fff transparent transparent transparent; -} -.lds-ring div:nth-child(1) { - animation-delay: -0.45s; -} -.lds-ring div:nth-child(2) { - animation-delay: -0.3s; -} -.lds-ring div:nth-child(3) { - animation-delay: -0.15s; -} -@keyframes lds-ring { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} diff --git a/web/scripts/ui/spinner.js b/web/scripts/ui/spinner.js deleted file mode 100644 index d049786f6..000000000 --- a/web/scripts/ui/spinner.js +++ /dev/null @@ -1,9 +0,0 @@ -import { addStylesheet } from "../utils.js"; - -addStylesheet(import.meta.url); - -export function createSpinner() { - const div = document.createElement("div"); - div.innerHTML = `
`; - return div.firstElementChild; -} diff --git a/web/scripts/ui/toggleSwitch.js b/web/scripts/ui/toggleSwitch.js deleted file mode 100644 index 59597ef90..000000000 --- a/web/scripts/ui/toggleSwitch.js +++ /dev/null @@ -1,60 +0,0 @@ -import { $el } from "../ui.js"; - -/** - * @typedef { { text: string, value?: string, tooltip?: string } } ToggleSwitchItem - */ -/** - * Creates a toggle switch element - * @param { string } name - * @param { Array void } [opts.onChange] - */ -export function toggleSwitch(name, items, { onChange } = {}) { - let selectedIndex; - let elements; - - function updateSelected(index) { - if (selectedIndex != null) { - elements[selectedIndex].classList.remove("comfy-toggle-selected"); - } - onChange?.({ item: items[index], prev: selectedIndex == null ? undefined : items[selectedIndex] }); - selectedIndex = index; - elements[selectedIndex].classList.add("comfy-toggle-selected"); - } - - elements = items.map((item, i) => { - if (typeof item === "string") item = { text: item }; - if (!item.value) item.value = item.text; - - const toggle = $el( - "label", - { - textContent: item.text, - title: item.tooltip ?? "", - }, - $el("input", { - name, - type: "radio", - value: item.value ?? item.text, - checked: item.selected, - onchange: () => { - updateSelected(i); - }, - }) - ); - if (item.selected) { - updateSelected(i); - } - return toggle; - }); - - const container = $el("div.comfy-toggle-switch", elements); - - if (selectedIndex == null) { - elements[0].children[0].checked = true; - updateSelected(0); - } - - return container; -} diff --git a/web/scripts/ui/userSelection.css b/web/scripts/ui/userSelection.css deleted file mode 100644 index 35c9d6614..000000000 --- a/web/scripts/ui/userSelection.css +++ /dev/null @@ -1,135 +0,0 @@ -.comfy-user-selection { - width: 100vw; - height: 100vh; - position: absolute; - top: 0; - left: 0; - z-index: 999; - display: flex; - align-items: center; - justify-content: center; - font-family: sans-serif; - background: linear-gradient(var(--tr-even-bg-color), var(--tr-odd-bg-color)); -} - -.comfy-user-selection-inner { - background: var(--comfy-menu-bg); - margin-top: -30vh; - padding: 20px 40px; - border-radius: 10px; - min-width: 365px; - position: relative; - box-shadow: 0 0 20px rgba(0, 0, 0, 0.3); -} - -.comfy-user-selection-inner form { - width: 100%; - display: flex; - flex-direction: column; - align-items: center; -} - -.comfy-user-selection-inner h1 { - margin: 10px 0 30px 0; - font-weight: normal; -} - -.comfy-user-selection-inner label { - display: flex; - flex-direction: column; - width: 100%; -} - -.comfy-user-selection input, -.comfy-user-selection select { - background-color: var(--comfy-input-bg); - color: var(--input-text); - border: 0; - border-radius: 5px; - padding: 5px; - margin-top: 10px; -} - -.comfy-user-selection input::placeholder { - color: var(--descrip-text); - opacity: 1; -} - -.comfy-user-existing { - width: 100%; -} - -.no-users .comfy-user-existing { - display: none; -} - -.comfy-user-selection-inner .or-separator { - margin: 10px 0; - padding: 10px; - display: block; - text-align: center; - width: 100%; - color: var(--descrip-text); -} - -.comfy-user-selection-inner .or-separator { - overflow: hidden; - text-align: center; - margin-left: -10px; -} - -.comfy-user-selection-inner .or-separator::before, -.comfy-user-selection-inner .or-separator::after { - content: ""; - background-color: var(--border-color); - position: relative; - height: 1px; - vertical-align: middle; - display: inline-block; - width: calc(50% - 20px); - top: -1px; -} - -.comfy-user-selection-inner .or-separator::before { - right: 10px; - margin-left: -50%; -} - -.comfy-user-selection-inner .or-separator::after { - left: 10px; - margin-right: -50%; -} - -.comfy-user-selection-inner section { - width: 100%; - padding: 10px; - margin: -10px; - transition: background-color 0.2s; -} - -.comfy-user-selection-inner section.selected { - background: var(--border-color); - border-radius: 5px; -} - -.comfy-user-selection-inner footer { - display: flex; - flex-direction: column; - align-items: center; - margin-top: 20px; -} - -.comfy-user-selection-inner .comfy-user-error { - color: var(--error-text); - margin-bottom: 10px; -} - -.comfy-user-button-next { - font-size: 16px; - padding: 6px 10px; - width: 100px; - display: flex; - gap: 5px; - align-items: center; - justify-content: center; -} \ No newline at end of file diff --git a/web/scripts/ui/userSelection.js b/web/scripts/ui/userSelection.js deleted file mode 100644 index f9f1ca807..000000000 --- a/web/scripts/ui/userSelection.js +++ /dev/null @@ -1,114 +0,0 @@ -import { api } from "../api.js"; -import { $el } from "../ui.js"; -import { addStylesheet } from "../utils.js"; -import { createSpinner } from "./spinner.js"; - -export class UserSelectionScreen { - async show(users, user) { - // This will rarely be hit so move the loading to on demand - await addStylesheet(import.meta.url); - const userSelection = document.getElementById("comfy-user-selection"); - userSelection.style.display = ""; - return new Promise((resolve) => { - const input = userSelection.getElementsByTagName("input")[0]; - const select = userSelection.getElementsByTagName("select")[0]; - const inputSection = input.closest("section"); - const selectSection = select.closest("section"); - const form = userSelection.getElementsByTagName("form")[0]; - const error = userSelection.getElementsByClassName("comfy-user-error")[0]; - const button = userSelection.getElementsByClassName("comfy-user-button-next")[0]; - - let inputActive = null; - input.addEventListener("focus", () => { - inputSection.classList.add("selected"); - selectSection.classList.remove("selected"); - inputActive = true; - }); - select.addEventListener("focus", () => { - inputSection.classList.remove("selected"); - selectSection.classList.add("selected"); - inputActive = false; - select.style.color = ""; - }); - select.addEventListener("blur", () => { - if (!select.value) { - select.style.color = "var(--descrip-text)"; - } - }); - - form.addEventListener("submit", async (e) => { - e.preventDefault(); - if (inputActive == null) { - error.textContent = "Please enter a username or select an existing user."; - } else if (inputActive) { - const username = input.value.trim(); - if (!username) { - error.textContent = "Please enter a username."; - return; - } - - // Create new user - input.disabled = select.disabled = input.readonly = select.readonly = true; - const spinner = createSpinner(); - button.prepend(spinner); - try { - const resp = await api.createUser(username); - if (resp.status >= 300) { - let message = "Error creating user: " + resp.status + " " + resp.statusText; - try { - const res = await resp.json(); - if(res.error) { - message = res.error; - } - } catch (error) { - } - throw new Error(message); - } - - resolve({ username, userId: await resp.json(), created: true }); - } catch (err) { - spinner.remove(); - error.textContent = err.message ?? err.statusText ?? err ?? "An unknown error occurred."; - input.disabled = select.disabled = input.readonly = select.readonly = false; - return; - } - } else if (!select.value) { - error.textContent = "Please select an existing user."; - return; - } else { - resolve({ username: users[select.value], userId: select.value, created: false }); - } - }); - - if (user) { - const name = localStorage["Comfy.userName"]; - if (name) { - input.value = name; - } - } - if (input.value) { - // Focus the input, do this separately as sometimes browsers like to fill in the value - input.focus(); - } - - const userIds = Object.keys(users ?? {}); - if (userIds.length) { - for (const u of userIds) { - $el("option", { textContent: users[u], value: u, parent: select }); - } - select.style.color = "var(--descrip-text)"; - - if (select.value) { - // Focus the select, do this separately as sometimes browsers like to fill in the value - select.focus(); - } - } else { - userSelection.classList.add("no-users"); - input.focus(); - } - }).then((r) => { - userSelection.remove(); - return r; - }); - } -} diff --git a/web/scripts/ui/utils.js b/web/scripts/ui/utils.js deleted file mode 100644 index e37d8b41e..000000000 --- a/web/scripts/ui/utils.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * @typedef { string | string[] | Record } ClassList - */ - -/** - * @param { HTMLElement } element - * @param { ClassList } classList - * @param { string[] } requiredClasses - */ -export function applyClasses(element, classList, ...requiredClasses) { - classList ??= ""; - - let str; - if (typeof classList === "string") { - str = classList; - } else if (classList instanceof Array) { - str = classList.join(" "); - } else { - str = Object.entries(classList).reduce((p, c) => { - if (c[1]) { - p += (p.length ? " " : "") + c[0]; - } - return p; - }, ""); - } - element.className = str; - if (requiredClasses) { - element.classList.add(...requiredClasses); - } -} - -/** - * @param { HTMLElement } element - * @param { { onHide?: (el: HTMLElement) => void, onShow?: (el: HTMLElement, value) => void } } [param1] - * @returns - */ -export function toggleElement(element, { onHide, onShow } = {}) { - let placeholder; - let hidden; - return (value) => { - if (value) { - if (hidden) { - hidden = false; - placeholder.replaceWith(element); - } - onShow?.(element, value); - } else { - if (!placeholder) { - placeholder = document.createComment(""); - } - hidden = true; - element.replaceWith(placeholder); - onHide?.(element); - } - }; -} diff --git a/web/scripts/utils.js b/web/scripts/utils.js deleted file mode 100644 index cda7600f6..000000000 --- a/web/scripts/utils.js +++ /dev/null @@ -1,156 +0,0 @@ -import { $el } from "./ui.js"; -import { api } from "./api.js"; - -// Simple date formatter -const parts = { - d: (d) => d.getDate(), - M: (d) => d.getMonth() + 1, - h: (d) => d.getHours(), - m: (d) => d.getMinutes(), - s: (d) => d.getSeconds(), -}; -const format = - Object.keys(parts) - .map((k) => k + k + "?") - .join("|") + "|yyy?y?"; - -function formatDate(text, date) { - return text.replace(new RegExp(format, "g"), function (text) { - if (text === "yy") return (date.getFullYear() + "").substring(2); - if (text === "yyyy") return date.getFullYear(); - if (text[0] in parts) { - const p = parts[text[0]](date); - return (p + "").padStart(text.length, "0"); - } - return text; - }); -} - - -export function clone(obj) { - try { - if (typeof structuredClone !== "undefined") { - return structuredClone(obj); - } - } catch (error) { - // structuredClone is stricter than using JSON.parse/stringify so fallback to that - } - - return JSON.parse(JSON.stringify(obj)); -} - -export function applyTextReplacements(app, value) { - return value.replace(/%([^%]+)%/g, function (match, text) { - const split = text.split("."); - if (split.length !== 2) { - // Special handling for dates - if (split[0].startsWith("date:")) { - return formatDate(split[0].substring(5), new Date()); - } - - if (text !== "width" && text !== "height") { - // Dont warn on standard replacements - console.warn("Invalid replacement pattern", text); - } - return match; - } - - // Find node with matching S&R property name - let nodes = app.graph._nodes.filter((n) => n.properties?.["Node name for S&R"] === split[0]); - // If we cant, see if there is a node with that title - if (!nodes.length) { - nodes = app.graph._nodes.filter((n) => n.title === split[0]); - } - if (!nodes.length) { - console.warn("Unable to find node", split[0]); - return match; - } - - if (nodes.length > 1) { - console.warn("Multiple nodes matched", split[0], "using first match"); - } - - const node = nodes[0]; - - const widget = node.widgets?.find((w) => w.name === split[1]); - if (!widget) { - console.warn("Unable to find widget", split[1], "on node", split[0], node); - return match; - } - - return ((widget.value ?? "") + "").replaceAll(/\/|\\/g, "_"); - }); -} - -export async function addStylesheet(urlOrFile, relativeTo) { - return new Promise((res, rej) => { - let url; - if (urlOrFile.endsWith(".js")) { - url = urlOrFile.substr(0, urlOrFile.length - 2) + "css"; - } else { - url = new URL(urlOrFile, relativeTo ?? `${window.location.protocol}//${window.location.host}`).toString(); - } - $el("link", { - parent: document.head, - rel: "stylesheet", - type: "text/css", - href: url, - onload: res, - onerror: rej, - }); - }); -} - -/** - * @param { string } filename - * @param { Blob } blob - */ -export function downloadBlob(filename, blob) { - const url = URL.createObjectURL(blob); - const a = $el("a", { - href: url, - download: filename, - style: { display: "none" }, - parent: document.body, - }); - a.click(); - setTimeout(function () { - a.remove(); - window.URL.revokeObjectURL(url); - }, 0); -} - -/** - * @template T - * @param {string} name - * @param {T} [defaultValue] - * @param {(currentValue: any, previousValue: any)=>void} [onChanged] - * @returns {T} - */ -export function prop(target, name, defaultValue, onChanged) { - let currentValue; - Object.defineProperty(target, name, { - get() { - return currentValue; - }, - set(newValue) { - const prevValue = currentValue; - currentValue = newValue; - onChanged?.(currentValue, prevValue, target, name); - }, - }); - return defaultValue; -} - -export function getStorageValue(id) { - const clientId = api.clientId ?? api.initialClientId; - return (clientId && sessionStorage.getItem(`${id}:${clientId}`)) ?? localStorage.getItem(id); -} - -export function setStorageValue(id, value) { - const clientId = api.clientId ?? api.initialClientId; - if (clientId) { - sessionStorage.setItem(`${id}:${clientId}`, value); - } - localStorage.setItem(id, value); -} \ No newline at end of file diff --git a/web/scripts/widgets.js b/web/scripts/widgets.js deleted file mode 100644 index 6a6899705..000000000 --- a/web/scripts/widgets.js +++ /dev/null @@ -1,531 +0,0 @@ -import { api } from "./api.js" -import "./domWidget.js"; - -let controlValueRunBefore = false; -export function updateControlWidgetLabel(widget) { - let replacement = "after"; - let find = "before"; - if (controlValueRunBefore) { - [find, replacement] = [replacement, find] - } - widget.label = (widget.label ?? widget.name).replace(find, replacement); -} - -const IS_CONTROL_WIDGET = Symbol(); -const HAS_EXECUTED = Symbol(); - -function getNumberDefaults(inputData, defaultStep, precision, enable_rounding) { - let defaultVal = inputData[1]["default"]; - let { min, max, step, round} = inputData[1]; - - if (defaultVal == undefined) defaultVal = 0; - if (min == undefined) min = 0; - if (max == undefined) max = 2048; - if (step == undefined) step = defaultStep; - // precision is the number of decimal places to show. - // by default, display the the smallest number of decimal places such that changes of size step are visible. - if (precision == undefined) { - precision = Math.max(-Math.floor(Math.log10(step)),0); - } - - if (enable_rounding && (round == undefined || round === true)) { - // by default, round the value to those decimal places shown. - round = Math.round(1000000*Math.pow(0.1,precision))/1000000; - } - - return { val: defaultVal, config: { min, max, step: 10.0 * step, round, precision } }; -} - -export function addValueControlWidget(node, targetWidget, defaultValue = "randomize", values, widgetName, inputData) { - let name = inputData[1]?.control_after_generate; - if(typeof name !== "string") { - name = widgetName; - } - const widgets = addValueControlWidgets(node, targetWidget, defaultValue, { - addFilterList: false, - controlAfterGenerateName: name - }, inputData); - return widgets[0]; -} - -export function addValueControlWidgets(node, targetWidget, defaultValue = "randomize", options, inputData) { - if (!defaultValue) defaultValue = "randomize"; - if (!options) options = {}; - - const getName = (defaultName, optionName) => { - let name = defaultName; - if (options[optionName]) { - name = options[optionName]; - } else if (typeof inputData?.[1]?.[defaultName] === "string") { - name = inputData?.[1]?.[defaultName]; - } else if (inputData?.[1]?.control_prefix) { - name = inputData?.[1]?.control_prefix + " " + name - } - return name; - } - - const widgets = []; - const valueControl = node.addWidget( - "combo", - getName("control_after_generate", "controlAfterGenerateName"), - defaultValue, - function () {}, - { - values: ["fixed", "increment", "decrement", "randomize"], - serialize: false, // Don't include this in prompt. - } - ); - valueControl[IS_CONTROL_WIDGET] = true; - updateControlWidgetLabel(valueControl); - widgets.push(valueControl); - - const isCombo = targetWidget.type === "combo"; - let comboFilter; - if (isCombo) { - valueControl.options.values.push("increment-wrap"); - } - if (isCombo && options.addFilterList !== false) { - comboFilter = node.addWidget( - "string", - getName("control_filter_list", "controlFilterListName"), - "", - function () {}, - { - serialize: false, // Don't include this in prompt. - } - ); - updateControlWidgetLabel(comboFilter); - - widgets.push(comboFilter); - } - - const applyWidgetControl = () => { - var v = valueControl.value; - - if (isCombo && v !== "fixed") { - let values = targetWidget.options.values; - const filter = comboFilter?.value; - if (filter) { - let check; - if (filter.startsWith("/") && filter.endsWith("/")) { - try { - const regex = new RegExp(filter.substring(1, filter.length - 1)); - check = (item) => regex.test(item); - } catch (error) { - console.error("Error constructing RegExp filter for node " + node.id, filter, error); - } - } - if (!check) { - const lower = filter.toLocaleLowerCase(); - check = (item) => item.toLocaleLowerCase().includes(lower); - } - values = values.filter(item => check(item)); - if (!values.length && targetWidget.options.values.length) { - console.warn("Filter for node " + node.id + " has filtered out all items", filter); - } - } - let current_index = values.indexOf(targetWidget.value); - let current_length = values.length; - - switch (v) { - case "increment": - current_index += 1; - break; - case "increment-wrap": - current_index += 1; - if ( current_index >= current_length ) { - current_index = 0; - } - break; - case "decrement": - current_index -= 1; - break; - case "randomize": - current_index = Math.floor(Math.random() * current_length); - default: - break; - } - current_index = Math.max(0, current_index); - current_index = Math.min(current_length - 1, current_index); - if (current_index >= 0) { - let value = values[current_index]; - targetWidget.value = value; - targetWidget.callback(value); - } - } else { - //number - let min = targetWidget.options.min; - let max = targetWidget.options.max; - // limit to something that javascript can handle - max = Math.min(1125899906842624, max); - min = Math.max(-1125899906842624, min); - let range = (max - min) / (targetWidget.options.step / 10); - - //adjust values based on valueControl Behaviour - switch (v) { - case "fixed": - break; - case "increment": - targetWidget.value += targetWidget.options.step / 10; - break; - case "decrement": - targetWidget.value -= targetWidget.options.step / 10; - break; - case "randomize": - targetWidget.value = Math.floor(Math.random() * range) * (targetWidget.options.step / 10) + min; - default: - break; - } - /*check if values are over or under their respective - * ranges and set them to min or max.*/ - if (targetWidget.value < min) targetWidget.value = min; - - if (targetWidget.value > max) - targetWidget.value = max; - targetWidget.callback(targetWidget.value); - } - }; - - valueControl.beforeQueued = () => { - if (controlValueRunBefore) { - // Don't run on first execution - if (valueControl[HAS_EXECUTED]) { - applyWidgetControl(); - } - } - valueControl[HAS_EXECUTED] = true; - }; - - valueControl.afterQueued = () => { - if (!controlValueRunBefore) { - applyWidgetControl(); - } - }; - - return widgets; -}; - -function seedWidget(node, inputName, inputData, app, widgetName) { - const seed = createIntWidget(node, inputName, inputData, app, true); - const seedControl = addValueControlWidget(node, seed.widget, "randomize", undefined, widgetName, inputData); - - seed.widget.linkedWidgets = [seedControl]; - return seed; -} - -function createIntWidget(node, inputName, inputData, app, isSeedInput) { - const control = inputData[1]?.control_after_generate; - if (!isSeedInput && control) { - return seedWidget(node, inputName, inputData, app, typeof control === "string" ? control : undefined); - } - - let widgetType = isSlider(inputData[1]["display"], app); - const { val, config } = getNumberDefaults(inputData, 1, 0, true); - Object.assign(config, { precision: 0 }); - return { - widget: node.addWidget( - widgetType, - inputName, - val, - function (v) { - const s = this.options.step / 10; - let sh = this.options.min % s; - if (isNaN(sh)) { - sh = 0; - } - this.value = Math.round((v - sh) / s) * s + sh; - }, - config - ), - }; -} - -function addMultilineWidget(node, name, opts, app) { - const inputEl = document.createElement("textarea"); - inputEl.className = "comfy-multiline-input"; - inputEl.value = opts.defaultVal; - inputEl.placeholder = opts.placeholder || name; - - const widget = node.addDOMWidget(name, "customtext", inputEl, { - getValue() { - return inputEl.value; - }, - setValue(v) { - inputEl.value = v; - }, - }); - widget.inputEl = inputEl; - - inputEl.addEventListener("input", () => { - widget.callback?.(widget.value); - }); - - return { minWidth: 400, minHeight: 200, widget }; -} - -function isSlider(display, app) { - if (app.ui.settings.getSettingValue("Comfy.DisableSliders")) { - return "number" - } - - return (display==="slider") ? "slider" : "number" -} - -export function initWidgets(app) { - app.ui.settings.addSetting({ - id: "Comfy.WidgetControlMode", - name: "Widget Value Control Mode", - type: "combo", - defaultValue: "after", - options: ["before", "after"], - tooltip: "Controls when widget values are updated (randomize/increment/decrement), either before the prompt is queued or after.", - onChange(value) { - controlValueRunBefore = value === "before"; - for (const n of app.graph._nodes) { - if (!n.widgets) continue; - for (const w of n.widgets) { - if (w[IS_CONTROL_WIDGET]) { - updateControlWidgetLabel(w); - if (w.linkedWidgets) { - for (const l of w.linkedWidgets) { - updateControlWidgetLabel(l); - } - } - } - } - } - app.graph.setDirtyCanvas(true); - }, - }); -} - -export const ComfyWidgets = { - "INT:seed": seedWidget, - "INT:noise_seed": seedWidget, - FLOAT(node, inputName, inputData, app) { - let widgetType = isSlider(inputData[1]["display"], app); - let precision = app.ui.settings.getSettingValue("Comfy.FloatRoundingPrecision"); - let disable_rounding = app.ui.settings.getSettingValue("Comfy.DisableFloatRounding") - if (precision == 0) precision = undefined; - const { val, config } = getNumberDefaults(inputData, 0.5, precision, !disable_rounding); - return { widget: node.addWidget(widgetType, inputName, val, - function (v) { - if (config.round) { - this.value = Math.round((v + Number.EPSILON)/config.round)*config.round; - if (this.value > config.max) this.value = config.max; - if (this.value < config.min) this.value = config.min; - } else { - this.value = v; - } - }, config) }; - }, - INT(node, inputName, inputData, app) { - return createIntWidget(node, inputName, inputData, app); - }, - BOOLEAN(node, inputName, inputData) { - let defaultVal = false; - let options = {}; - if (inputData[1]) { - if (inputData[1].default) - defaultVal = inputData[1].default; - if (inputData[1].label_on) - options["on"] = inputData[1].label_on; - if (inputData[1].label_off) - options["off"] = inputData[1].label_off; - } - return { - widget: node.addWidget( - "toggle", - inputName, - defaultVal, - () => {}, - options, - ) - }; - }, - STRING(node, inputName, inputData, app) { - const defaultVal = inputData[1].default || ""; - const multiline = !!inputData[1].multiline; - - let res; - if (multiline) { - res = addMultilineWidget(node, inputName, { defaultVal, ...inputData[1] }, app); - } else { - res = { widget: node.addWidget("text", inputName, defaultVal, () => {}, {}) }; - } - - if(inputData[1].dynamicPrompts != undefined) - res.widget.dynamicPrompts = inputData[1].dynamicPrompts; - - return res; - }, - COMBO(node, inputName, inputData) { - const type = inputData[0]; - let defaultValue = type[0]; - if (inputData[1] && inputData[1].default) { - defaultValue = inputData[1].default; - } - const res = { widget: node.addWidget("combo", inputName, defaultValue, () => {}, { values: type }) }; - if (inputData[1]?.control_after_generate) { - res.widget.linkedWidgets = addValueControlWidgets(node, res.widget, undefined, undefined, inputData); - } - return res; - }, - IMAGEUPLOAD(node, inputName, inputData, app) { - const imageWidget = node.widgets.find((w) => w.name === (inputData[1]?.widget ?? "image")); - let uploadWidget; - - function showImage(name) { - const img = new Image(); - img.onload = () => { - node.imgs = [img]; - app.graph.setDirtyCanvas(true); - }; - let folder_separator = name.lastIndexOf("/"); - let subfolder = ""; - if (folder_separator > -1) { - subfolder = name.substring(0, folder_separator); - name = name.substring(folder_separator + 1); - } - img.src = api.apiURL(`/view?filename=${encodeURIComponent(name)}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`); - node.setSizeForImage?.(); - } - - var default_value = imageWidget.value; - Object.defineProperty(imageWidget, "value", { - set : function(value) { - this._real_value = value; - }, - - get : function() { - let value = ""; - if (this._real_value) { - value = this._real_value; - } else { - return default_value; - } - - if (value.filename) { - let real_value = value; - value = ""; - if (real_value.subfolder) { - value = real_value.subfolder + "/"; - } - - value += real_value.filename; - - if(real_value.type && real_value.type !== "input") - value += ` [${real_value.type}]`; - } - return value; - } - }); - - // Add our own callback to the combo widget to render an image when it changes - const cb = node.callback; - imageWidget.callback = function () { - showImage(imageWidget.value); - if (cb) { - return cb.apply(this, arguments); - } - }; - - // On load if we have a value then render the image - // The value isnt set immediately so we need to wait a moment - // No change callbacks seem to be fired on initial setting of the value - requestAnimationFrame(() => { - if (imageWidget.value) { - showImage(imageWidget.value); - } - }); - - async function uploadFile(file, updateNode, pasted = false) { - try { - // Wrap file in formdata so it includes filename - const body = new FormData(); - body.append("image", file); - if (pasted) body.append("subfolder", "pasted"); - const resp = await api.fetchApi("/upload/image", { - method: "POST", - body, - }); - - if (resp.status === 200) { - const data = await resp.json(); - // Add the file to the dropdown list and update the widget value - let path = data.name; - if (data.subfolder) path = data.subfolder + "/" + path; - - if (!imageWidget.options.values.includes(path)) { - imageWidget.options.values.push(path); - } - - if (updateNode) { - showImage(path); - imageWidget.value = path; - } - } else { - alert(resp.status + " - " + resp.statusText); - } - } catch (error) { - alert(error); - } - } - - const fileInput = document.createElement("input"); - Object.assign(fileInput, { - type: "file", - accept: "image/jpeg,image/png,image/webp", - style: "display: none", - onchange: async () => { - if (fileInput.files.length) { - await uploadFile(fileInput.files[0], true); - } - }, - }); - document.body.append(fileInput); - - // Create the button widget for selecting the files - uploadWidget = node.addWidget("button", inputName, "image", () => { - fileInput.click(); - }); - uploadWidget.label = "choose file to upload"; - uploadWidget.serialize = false; - - // Add handler to check if an image is being dragged over our node - node.onDragOver = function (e) { - if (e.dataTransfer && e.dataTransfer.items) { - const image = [...e.dataTransfer.items].find((f) => f.kind === "file"); - return !!image; - } - - return false; - }; - - // On drop upload files - node.onDragDrop = function (e) { - console.log("onDragDrop called"); - let handled = false; - for (const file of e.dataTransfer.files) { - if (file.type.startsWith("image/")) { - uploadFile(file, !handled); // Dont await these, any order is fine, only update on first one - handled = true; - } - } - - return handled; - }; - - node.pasteFile = function(file) { - if (file.type.startsWith("image/")) { - const is_pasted = (file.name === "image.png") && - (file.lastModified - Date.now() < 2000); - uploadFile(file, true, is_pasted); - return true; - } - return false; - } - - return { widget: uploadWidget }; - }, -}; diff --git a/web/scripts/workflows.js b/web/scripts/workflows.js deleted file mode 100644 index d38b6f5fc..000000000 --- a/web/scripts/workflows.js +++ /dev/null @@ -1,450 +0,0 @@ -// @ts-check - -import { api } from "./api.js"; -import { ChangeTracker } from "./changeTracker.js"; -import { ComfyAsyncDialog } from "./ui/components/asyncDialog.js"; -import { getStorageValue, setStorageValue } from "./utils.js"; - -function appendJsonExt(path) { - if (!path.toLowerCase().endsWith(".json")) { - path += ".json"; - } - return path; -} - -export function trimJsonExt(path) { - return path?.replace(/\.json$/, ""); -} - -export class ComfyWorkflowManager extends EventTarget { - /** @type {string | null} */ - #activePromptId = null; - #unsavedCount = 0; - #activeWorkflow; - - /** @type {Record} */ - workflowLookup = {}; - /** @type {Array} */ - workflows = []; - /** @type {Array} */ - openWorkflows = []; - /** @type {Record}>} */ - queuedPrompts = {}; - - get activeWorkflow() { - return this.#activeWorkflow ?? this.openWorkflows[0]; - } - - get activePromptId() { - return this.#activePromptId; - } - - get activePrompt() { - return this.queuedPrompts[this.#activePromptId]; - } - - /** - * @param {import("./app.js").ComfyApp} app - */ - constructor(app) { - super(); - this.app = app; - ChangeTracker.init(app); - - this.#bindExecutionEvents(); - } - - #bindExecutionEvents() { - // TODO: on reload, set active prompt based on the latest ws message - - const emit = () => this.dispatchEvent(new CustomEvent("execute", { detail: this.activePrompt })); - let executing = null; - api.addEventListener("execution_start", (e) => { - this.#activePromptId = e.detail.prompt_id; - - // This event can fire before the event is stored, so put a placeholder - this.queuedPrompts[this.#activePromptId] ??= { nodes: {} }; - emit(); - }); - api.addEventListener("execution_cached", (e) => { - if (!this.activePrompt) return; - for (const n of e.detail.nodes) { - this.activePrompt.nodes[n] = true; - } - emit(); - }); - api.addEventListener("executed", (e) => { - if (!this.activePrompt) return; - this.activePrompt.nodes[e.detail.node] = true; - emit(); - }); - api.addEventListener("executing", (e) => { - if (!this.activePrompt) return; - - if (executing) { - // Seems sometimes nodes that are cached fire executing but not executed - this.activePrompt.nodes[executing] = true; - } - executing = e.detail; - if (!executing) { - delete this.queuedPrompts[this.#activePromptId]; - this.#activePromptId = null; - } - emit(); - }); - } - - async loadWorkflows() { - try { - let favorites; - const resp = await api.getUserData("workflows/.index.json"); - let info; - if (resp.status === 200) { - info = await resp.json(); - favorites = new Set(info?.favorites ?? []); - } else { - favorites = new Set(); - } - - const workflows = (await api.listUserData("workflows", true, true)).map((w) => { - let workflow = this.workflowLookup[w[0]]; - if (!workflow) { - workflow = new ComfyWorkflow(this, w[0], w.slice(1), favorites.has(w[0])); - this.workflowLookup[workflow.path] = workflow; - } - return workflow; - }); - - this.workflows = workflows; - } catch (error) { - alert("Error loading workflows: " + (error.message ?? error)); - this.workflows = []; - } - } - - async saveWorkflowMetadata() { - await api.storeUserData("workflows/.index.json", { - favorites: [...this.workflows.filter((w) => w.isFavorite).map((w) => w.path)], - }); - } - - /** - * @param {string | ComfyWorkflow | null} workflow - */ - setWorkflow(workflow) { - if (workflow && typeof workflow === "string") { - // Selected by path, i.e. on reload of last workflow - const found = this.workflows.find((w) => w.path === workflow); - if (found) { - workflow = found; - workflow.unsaved = !workflow || getStorageValue("Comfy.PreviousWorkflowUnsaved") === "true"; - } - } - - if (!(workflow instanceof ComfyWorkflow)) { - // Still not found, either reloading a deleted workflow or blank - workflow = new ComfyWorkflow(this, workflow || "Unsaved Workflow" + (this.#unsavedCount++ ? ` (${this.#unsavedCount})` : "")); - } - - const index = this.openWorkflows.indexOf(workflow); - if (index === -1) { - // Opening a new workflow - this.openWorkflows.push(workflow); - } - - this.#activeWorkflow = workflow; - - setStorageValue("Comfy.PreviousWorkflow", this.activeWorkflow.path ?? ""); - this.dispatchEvent(new CustomEvent("changeWorkflow")); - } - - storePrompt({ nodes, id }) { - this.queuedPrompts[id] ??= {}; - this.queuedPrompts[id].nodes = { - ...nodes.reduce((p, n) => { - p[n] = false; - return p; - }, {}), - ...this.queuedPrompts[id].nodes, - }; - this.queuedPrompts[id].workflow = this.activeWorkflow; - } - - /** - * @param {ComfyWorkflow} workflow - */ - async closeWorkflow(workflow, warnIfUnsaved = true) { - if (!workflow.isOpen) { - return true; - } - if (workflow.unsaved && warnIfUnsaved) { - const res = await ComfyAsyncDialog.prompt({ - title: "Save Changes?", - message: `Do you want to save changes to "${workflow.path ?? workflow.name}" before closing?`, - actions: ["Yes", "No", "Cancel"], - }); - if (res === "Yes") { - const active = this.activeWorkflow; - if (active !== workflow) { - // We need to switch to the workflow to save it - await workflow.load(); - } - - if (!(await workflow.save())) { - // Save was canceled, restore the previous workflow - if (active !== workflow) { - await active.load(); - } - return; - } - } else if (res === "Cancel") { - return; - } - } - workflow.changeTracker = null; - this.openWorkflows.splice(this.openWorkflows.indexOf(workflow), 1); - if (this.openWorkflows.length) { - this.#activeWorkflow = this.openWorkflows[0]; - await this.#activeWorkflow.load(); - } else { - // Load default - await this.app.loadGraphData(); - } - } -} - -export class ComfyWorkflow { - #name; - #path; - #pathParts; - #isFavorite = false; - /** @type {ChangeTracker | null} */ - changeTracker = null; - unsaved = false; - - get name() { - return this.#name; - } - - get path() { - return this.#path; - } - - get pathParts() { - return this.#pathParts; - } - - get isFavorite() { - return this.#isFavorite; - } - - get isOpen() { - return !!this.changeTracker; - } - - /** - * @overload - * @param {ComfyWorkflowManager} manager - * @param {string} path - */ - /** - * @overload - * @param {ComfyWorkflowManager} manager - * @param {string} path - * @param {string[]} pathParts - * @param {boolean} isFavorite - */ - /** - * @param {ComfyWorkflowManager} manager - * @param {string} path - * @param {string[]} [pathParts] - * @param {boolean} [isFavorite] - */ - constructor(manager, path, pathParts, isFavorite) { - this.manager = manager; - if (pathParts) { - this.#updatePath(path, pathParts); - this.#isFavorite = isFavorite; - } else { - this.#name = path; - this.unsaved = true; - } - } - - /** - * @param {string} path - * @param {string[]} [pathParts] - */ - #updatePath(path, pathParts) { - this.#path = path; - - if (!pathParts) { - if (!path.includes("\\")) { - pathParts = path.split("/"); - } else { - pathParts = path.split("\\"); - } - } - - this.#pathParts = pathParts; - this.#name = trimJsonExt(pathParts[pathParts.length - 1]); - } - - async getWorkflowData() { - const resp = await api.getUserData("workflows/" + this.path); - if (resp.status !== 200) { - alert(`Error loading workflow file '${this.path}': ${resp.status} ${resp.statusText}`); - return; - } - return await resp.json(); - } - - load = async () => { - if (this.isOpen) { - await this.manager.app.loadGraphData(this.changeTracker.activeState, true, true, this); - } else { - const data = await this.getWorkflowData(); - if (!data) return; - await this.manager.app.loadGraphData(data, true, true, this); - } - }; - - async save(saveAs = false) { - if (!this.path || saveAs) { - return !!(await this.#save(null, false)); - } else { - return !!(await this.#save(this.path, true)); - } - } - - /** - * @param {boolean} value - */ - async favorite(value) { - try { - if (this.#isFavorite === value) return; - this.#isFavorite = value; - await this.manager.saveWorkflowMetadata(); - this.manager.dispatchEvent(new CustomEvent("favorite", { detail: this })); - } catch (error) { - alert("Error favoriting workflow " + this.path + "\n" + (error.message ?? error)); - } - } - - /** - * @param {string} path - */ - async rename(path) { - path = appendJsonExt(path); - let resp = await api.moveUserData("workflows/" + this.path, "workflows/" + path); - - if (resp.status === 409) { - if (!confirm(`Workflow '${path}' already exists, do you want to overwrite it?`)) return resp; - resp = await api.moveUserData("workflows/" + this.path, "workflows/" + path, { overwrite: true }); - } - - if (resp.status !== 200) { - alert(`Error renaming workflow file '${this.path}': ${resp.status} ${resp.statusText}`); - return; - } - - const isFav = this.isFavorite; - if (isFav) { - await this.favorite(false); - } - path = (await resp.json()).substring("workflows/".length); - this.#updatePath(path, null); - if (isFav) { - await this.favorite(true); - } - this.manager.dispatchEvent(new CustomEvent("rename", { detail: this })); - setStorageValue("Comfy.PreviousWorkflow", this.path ?? ""); - } - - async insert() { - const data = await this.getWorkflowData(); - if (!data) return; - - const old = localStorage.getItem("litegrapheditor_clipboard"); - const graph = new LGraph(data); - const canvas = new LGraphCanvas(null, graph, { skip_events: true, skip_render: true }); - canvas.selectNodes(); - canvas.copyToClipboard(); - this.manager.app.canvas.pasteFromClipboard(); - localStorage.setItem("litegrapheditor_clipboard", old); - } - - async delete() { - // TODO: fix delete of current workflow - should mark workflow as unsaved and when saving use old name by default - - try { - if (this.isFavorite) { - await this.favorite(false); - } - await api.deleteUserData("workflows/" + this.path); - this.unsaved = true; - this.#path = null; - this.#pathParts = null; - this.manager.workflows.splice(this.manager.workflows.indexOf(this), 1); - this.manager.dispatchEvent(new CustomEvent("delete", { detail: this })); - } catch (error) { - alert(`Error deleting workflow: ${error.message || error}`); - } - } - - track() { - if (this.changeTracker) { - this.changeTracker.restore(); - } else { - this.changeTracker = new ChangeTracker(this); - } - } - - /** - * @param {string|null} path - * @param {boolean} overwrite - */ - async #save(path, overwrite) { - if (!path) { - path = prompt("Save workflow as:", trimJsonExt(this.path) ?? this.name ?? "workflow"); - if (!path) return; - } - - path = appendJsonExt(path); - - const p = await this.manager.app.graphToPrompt(); - const json = JSON.stringify(p.workflow, null, 2); - let resp = await api.storeUserData("workflows/" + path, json, { stringify: false, throwOnError: false, overwrite }); - if (resp.status === 409) { - if (!confirm(`Workflow '${path}' already exists, do you want to overwrite it?`)) return; - resp = await api.storeUserData("workflows/" + path, json, { stringify: false }); - } - - if (resp.status !== 200) { - alert(`Error saving workflow '${this.path}': ${resp.status} ${resp.statusText}`); - return; - } - - path = (await resp.json()).substring("workflows/".length); - - if (!this.path) { - // Saved new workflow, patch this instance - this.#updatePath(path, null); - await this.manager.loadWorkflows(); - this.unsaved = false; - this.manager.dispatchEvent(new CustomEvent("rename", { detail: this })); - setStorageValue("Comfy.PreviousWorkflow", this.path ?? ""); - } else if (path !== this.path) { - // Saved as, open the new copy - await this.manager.loadWorkflows(); - const workflow = this.manager.workflowLookup[path]; - await workflow.load(); - } else { - // Normal save - this.unsaved = false; - this.manager.dispatchEvent(new CustomEvent("save", { detail: this })); - } - - return true; - } -} diff --git a/web/style.css b/web/style.css deleted file mode 100644 index 8ef1d0dd1..000000000 --- a/web/style.css +++ /dev/null @@ -1,647 +0,0 @@ -@import url("scripts/ui/menu/menu.css"); - -:root { - --fg-color: #000; - --bg-color: #fff; - --comfy-menu-bg: #353535; - --comfy-input-bg: #222; - --input-text: #ddd; - --descrip-text: #999; - --drag-text: #ccc; - --error-text: #ff4444; - --border-color: #4e4e4e; - --tr-even-bg-color: #222; - --tr-odd-bg-color: #353535; - --primary-bg: #236692; - --primary-fg: #ffffff; - --primary-hover-bg: #3485bb; - --primary-hover-fg: #ffffff; - --content-bg: #e0e0e0; - --content-fg: #000; - --content-hover-bg: #adadad; - --content-hover-fg: #000; -} - -@media (prefers-color-scheme: dark) { - :root { - --fg-color: #fff; - --bg-color: #202020; - --content-bg: #4e4e4e; - --content-fg: #fff; - --content-hover-bg: #222; - --content-hover-fg: #fff; - } -} - -body { - width: 100vw; - height: 100vh; - margin: 0; - overflow: hidden; - background-color: var(--bg-color); - color: var(--fg-color); - grid-template-columns: auto 1fr auto; - grid-template-rows: auto 1fr auto; - min-height: -webkit-fill-available; - max-height: -webkit-fill-available; - min-width: -webkit-fill-available; - max-width: -webkit-fill-available; -} - -.comfyui-body-top { - order: -5; - grid-column: 1/-1; - z-index: 10; - display: flex; - flex-direction: column; -} - -.comfyui-body-left { - order: -4; - z-index: 10; - display: flex; -} - -#graph-canvas { - width: 100%; - height: 100%; - order: -3; -} - -.comfyui-body-right { - order: -2; - z-index: 10; - display: flex; -} - -.comfyui-body-bottom { - order: -1; - grid-column: 1/-1; - z-index: 10; - display: flex; - flex-direction: column; -} - -.comfy-multiline-input { - background-color: var(--comfy-input-bg); - color: var(--input-text); - overflow: hidden; - overflow-y: auto; - padding: 2px; - resize: none; - border: none; - box-sizing: border-box; - font-size: 10px; -} - -.comfy-modal { - display: none; /* Hidden by default */ - position: fixed; /* Stay in place */ - z-index: 100; /* Sit on top */ - padding: 30px 30px 10px 30px; - background-color: var(--comfy-menu-bg); /* Modal background */ - color: var(--error-text); - box-shadow: 0 0 20px #888888; - border-radius: 10px; - top: 50%; - left: 50%; - max-width: 80vw; - max-height: 80vh; - transform: translate(-50%, -50%); - overflow: hidden; - justify-content: center; - font-family: monospace; - font-size: 15px; -} - -.comfy-modal-content { - display: flex; - flex-direction: column; -} - -.comfy-modal p { - overflow: auto; - white-space: pre-line; /* This will respect line breaks */ - margin-bottom: 20px; /* Add some margin between the text and the close button*/ -} - -.comfy-modal select, -.comfy-modal input[type=button], -.comfy-modal input[type=checkbox] { - margin: 3px 3px 3px 4px; -} - -.comfy-menu-hamburger { - position: fixed; - top: 10px; - z-index: 9999; - right: 10px; - width: 30px; - display: none; - gap: 8px; - flex-direction: column; - cursor: pointer; -} -.comfy-menu-hamburger div { - height: 3px; - width: 100%; - border-radius: 20px; - background-color: white; -} - -.comfy-menu { - font-size: 15px; - position: absolute; - top: 50%; - right: 0; - text-align: center; - z-index: 999; - width: 170px; - display: flex; - flex-direction: column; - align-items: center; - color: var(--descrip-text); - background-color: var(--comfy-menu-bg); - font-family: sans-serif; - padding: 10px; - border-radius: 0 8px 8px 8px; - box-shadow: 3px 3px 8px rgba(0, 0, 0, 0.4); -} - -.comfy-menu-header { - display: flex; -} - -.comfy-menu-actions { - display: flex; - gap: 3px; - align-items: center; - height: 20px; - position: relative; - top: -1px; - font-size: 22px; -} - -.comfy-menu .comfy-menu-actions button { - background-color: rgba(0, 0, 0, 0); - padding: 0; - border: none; - cursor: pointer; - font-size: inherit; -} - -.comfy-menu .comfy-menu-actions .comfy-settings-btn { - font-size: 0.6em; -} - -button.comfy-close-menu-btn { - font-size: 1em; - line-height: 12px; - color: #ccc; - position: relative; - top: -1px; -} - -.comfy-menu-queue-size { - flex: auto; -} - -.comfy-menu button, -.comfy-modal button { - font-size: 20px; -} - -.comfy-menu-btns { - margin-bottom: 10px; - width: 100%; -} - -.comfy-menu-btns button { - font-size: 10px; - width: 50%; - color: var(--descrip-text) !important; -} - -.comfy-menu > button { - width: 100%; -} - -.comfy-btn, -.comfy-menu > button, -.comfy-menu-btns button, -.comfy-menu .comfy-list button, -.comfy-modal button { - color: var(--input-text); - background-color: var(--comfy-input-bg); - border-radius: 8px; - border-color: var(--border-color); - border-style: solid; - margin-top: 2px; -} - -.comfy-btn:hover:not(:disabled), -.comfy-menu > button:hover, -.comfy-menu-btns button:hover, -.comfy-menu .comfy-list button:hover, -.comfy-modal button:hover, -.comfy-menu-actions button:hover { - filter: brightness(1.2); - will-change: transform; - cursor: pointer; -} - -span.drag-handle { - width: 10px; - height: 20px; - display: inline-block; - overflow: hidden; - line-height: 5px; - padding: 3px 4px; - cursor: move; - vertical-align: middle; - margin-top: -.4em; - margin-left: -.2em; - font-size: 12px; - font-family: sans-serif; - letter-spacing: 2px; - color: var(--drag-text); - text-shadow: 1px 0 1px black; -} - -span.drag-handle::after { - content: '.. .. ..'; -} - -.comfy-queue-btn { - width: 100%; -} - -.comfy-list { - color: var(--descrip-text); - background-color: var(--comfy-menu-bg); - margin-bottom: 10px; - border-color: var(--border-color); - border-style: solid; -} - -.comfy-list-items { - overflow-y: scroll; - max-height: 100px; - min-height: 25px; - background-color: var(--comfy-input-bg); - padding: 5px; -} - -.comfy-list h4 { - min-width: 160px; - margin: 0; - padding: 3px; - font-weight: normal; -} - -.comfy-list-items button { - font-size: 10px; -} - -.comfy-list-actions { - margin: 5px; - display: flex; - gap: 5px; - justify-content: center; -} - -.comfy-list-actions button { - font-size: 12px; -} - -button.comfy-queue-btn { - margin: 6px 0 !important; -} - -.comfy-modal.comfy-settings, -.comfy-modal.comfy-manage-templates { - text-align: center; - font-family: sans-serif; - color: var(--descrip-text); - z-index: 99; -} - -.comfy-modal.comfy-settings input[type="range"] { - vertical-align: middle; -} - -.comfy-modal.comfy-settings input[type="range"] + input[type="number"] { - width: 3.5em; -} - -.comfy-modal input, -.comfy-modal select { - color: var(--input-text); - background-color: var(--comfy-input-bg); - border-radius: 8px; - border-color: var(--border-color); - border-style: solid; - font-size: inherit; -} - -.comfy-tooltip-indicator { - text-decoration: underline; - text-decoration-style: dashed; -} - -@media only screen and (max-height: 850px) { - .comfy-menu { - top: 0 !important; - bottom: 0 !important; - left: auto !important; - right: 0 !important; - border-radius: 0; - } - - .comfy-menu span.drag-handle { - display: none; - } - - .comfy-menu-queue-size { - flex: unset; - } - - .comfy-menu-header { - justify-content: space-between; - } - .comfy-menu-actions { - gap: 10px; - font-size: 28px; - } -} - -/* Input popup */ - -.graphdialog { - min-height: 1em; - background-color: var(--comfy-menu-bg); -} - -.graphdialog .name { - font-size: 14px; - font-family: sans-serif; - color: var(--descrip-text); -} - -.graphdialog button { - margin-top: unset; - vertical-align: unset; - height: 1.6em; - padding-right: 8px; -} - -.graphdialog input, .graphdialog textarea, .graphdialog select { - background-color: var(--comfy-input-bg); - border: 2px solid; - border-color: var(--border-color); - color: var(--input-text); - border-radius: 12px 0 0 12px; -} - -/* Dialogs */ - -dialog { - box-shadow: 0 0 20px #888888; -} - -dialog::backdrop { - background: rgba(0, 0, 0, 0.5); -} - -.comfy-dialog.comfyui-dialog.comfy-modal { - top: 0; - left: 0; - right: 0; - bottom: 0; - transform: none; -} - -.comfy-dialog.comfy-modal { - font-family: Arial, sans-serif; - border-color: var(--bg-color); - box-shadow: none; - border: 2px solid var(--border-color); -} - -.comfy-dialog .comfy-modal-content { - flex-direction: row; - flex-wrap: wrap; - gap: 10px; - color: var(--fg-color); -} - -.comfy-dialog .comfy-modal-content h3 { - margin-top: 0; -} - -.comfy-dialog .comfy-modal-content > p { - width: 100%; -} - -.comfy-dialog .comfy-modal-content > .comfyui-button { - flex: 1; - justify-content: center; -} - -#comfy-settings-dialog { - padding: 0; - width: 41rem; -} - -#comfy-settings-dialog tr > td:first-child { - text-align: right; -} - -#comfy-settings-dialog tbody button, #comfy-settings-dialog table > button { - background-color: var(--bg-color); - border: 1px var(--border-color) solid; - border-radius: 0; - color: var(--input-text); - font-size: 1rem; - padding: 0.5rem; -} - -#comfy-settings-dialog button:hover { - background-color: var(--tr-odd-bg-color); -} - -/* General CSS for tables */ - -.comfy-table { - border-collapse: collapse; - color: var(--input-text); - font-family: Arial, sans-serif; - width: 100%; -} - -.comfy-table caption { - position: sticky; - top: 0; - background-color: var(--bg-color); - color: var(--input-text); - font-size: 1rem; - font-weight: bold; - padding: 8px; - text-align: center; - border-bottom: 1px solid var(--border-color); -} - -.comfy-table caption .comfy-btn { - position: absolute; - top: -2px; - right: 0; - bottom: 0; - cursor: pointer; - border: none; - height: 100%; - border-radius: 0; - aspect-ratio: 1/1; - user-select: none; - font-size: 20px; -} - -.comfy-table caption .comfy-btn:focus { - outline: none; -} - -.comfy-table tr:nth-child(even) { - background-color: var(--tr-even-bg-color); -} - -.comfy-table tr:nth-child(odd) { - background-color: var(--tr-odd-bg-color); -} - -.comfy-table td, -.comfy-table th { - border: 1px solid var(--border-color); - padding: 8px; -} - -/* Context menu */ - -.litegraph .dialog { - z-index: 1; - font-family: Arial, sans-serif; -} - -.litegraph .litemenu-entry.has_submenu { - position: relative; - padding-right: 20px; -} - -.litemenu-entry.has_submenu::after { - content: ">"; - position: absolute; - top: 0; - right: 2px; -} - -.litegraph.litecontextmenu, -.litegraph.litecontextmenu.dark { - z-index: 9999 !important; - background-color: var(--comfy-menu-bg) !important; - filter: brightness(95%); - will-change: transform; -} - -.litegraph.litecontextmenu .litemenu-entry:hover:not(.disabled):not(.separator) { - background-color: var(--comfy-menu-bg) !important; - filter: brightness(155%); - will-change: transform; - color: var(--input-text); -} - -.litegraph.litecontextmenu .litemenu-entry.submenu, -.litegraph.litecontextmenu.dark .litemenu-entry.submenu { - background-color: var(--comfy-menu-bg) !important; - color: var(--input-text); -} - -.litegraph.litecontextmenu input { - background-color: var(--comfy-input-bg) !important; - color: var(--input-text) !important; -} - -.comfy-context-menu-filter { - box-sizing: border-box; - border: 1px solid #999; - margin: 0 0 5px 5px; - width: calc(100% - 10px); -} - -.comfy-img-preview { - pointer-events: none; - overflow: hidden; - display: flex; - flex-wrap: wrap; - align-content: flex-start; - justify-content: center; -} - -.comfy-img-preview img { - object-fit: contain; - width: var(--comfy-img-preview-width); - height: var(--comfy-img-preview-height); -} - -.comfy-missing-nodes li button { - font-size: 12px; - margin-left: 5px; -} - -/* Search box */ - -.litegraph.litesearchbox { - z-index: 9999 !important; - background-color: var(--comfy-menu-bg) !important; - overflow: hidden; - display: block; -} - -.litegraph.litesearchbox input, -.litegraph.litesearchbox select { - background-color: var(--comfy-input-bg) !important; - color: var(--input-text); -} - -.litegraph.lite-search-item { - color: var(--input-text); - background-color: var(--comfy-input-bg); - filter: brightness(80%); - will-change: transform; - padding-left: 0.2em; -} - -.litegraph.lite-search-item.generic_type { - color: var(--input-text); - filter: brightness(50%); - will-change: transform; -} - -@media only screen and (max-width: 450px) { - #comfy-settings-dialog .comfy-table tbody { - display: grid; - } - #comfy-settings-dialog .comfy-table tr { - display: grid; - } - #comfy-settings-dialog tr > td:first-child { - text-align: center; - border-bottom: none; - padding-bottom: 0; - } - #comfy-settings-dialog tr > td:not(:first-child) { - text-align: center; - border-top: none; - } -} - -audio.comfy-audio.empty-audio-widget { - display: none; -} diff --git a/web/types/comfy.d.ts b/web/types/comfy.d.ts deleted file mode 100644 index 9a338b349..000000000 --- a/web/types/comfy.d.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { LGraphNode, IWidget } from "./litegraph"; -import { ComfyApp } from "../../scripts/app"; - -export interface ComfyExtension { - /** - * The name of the extension - */ - name: string; - /** - * Allows any initialisation, e.g. loading resources. Called after the canvas is created but before nodes are added - * @param app The ComfyUI app instance - */ - init?(app: ComfyApp): Promise; - /** - * Allows any additonal setup, called after the application is fully set up and running - * @param app The ComfyUI app instance - */ - setup?(app: ComfyApp): Promise; - /** - * Called before nodes are registered with the graph - * @param defs The collection of node definitions, add custom ones or edit existing ones - * @param app The ComfyUI app instance - */ - addCustomNodeDefs?(defs: Record, app: ComfyApp): Promise; - /** - * Allows the extension to add custom widgets - * @param app The ComfyUI app instance - * @returns An array of {[widget name]: widget data} - */ - getCustomWidgets?( - app: ComfyApp - ): Promise< - Record { widget?: IWidget; minWidth?: number; minHeight?: number }> - >; - /** - * Allows the extension to add additional handling to the node before it is registered with LGraph - * @param nodeType The node class (not an instance) - * @param nodeData The original node object info config object - * @param app The ComfyUI app instance - */ - beforeRegisterNodeDef?(nodeType: typeof LGraphNode, nodeData: ComfyObjectInfo, app: ComfyApp): Promise; - /** - * Allows the extension to register additional nodes with LGraph after standard nodes are added - * @param app The ComfyUI app instance - */ - registerCustomNodes?(app: ComfyApp): Promise; - /** - * Allows the extension to modify a node that has been reloaded onto the graph. - * If you break something in the backend and want to patch workflows in the frontend - * This is the place to do this - * @param node The node that has been loaded - * @param app The ComfyUI app instance - */ - loadedGraphNode?(node: LGraphNode, app: ComfyApp); - /** - * Allows the extension to run code after the constructor of the node - * @param node The node that has been created - * @param app The ComfyUI app instance - */ - nodeCreated?(node: LGraphNode, app: ComfyApp); -} - -export type ComfyObjectInfo = { - name: string; - display_name?: string; - description?: string; - category: string; - input?: { - required?: Record; - optional?: Record; - }; - output?: string[]; - output_name: string[]; -}; - -export type ComfyObjectInfoConfig = [string | any[]] | [string | any[], any]; diff --git a/web/types/litegraph.d.ts b/web/types/litegraph.d.ts deleted file mode 100644 index 6629e779f..000000000 --- a/web/types/litegraph.d.ts +++ /dev/null @@ -1,1506 +0,0 @@ -// Type definitions for litegraph.js 0.7.0 -// Project: litegraph.js -// Definitions by: NateScarlet - -export type Vector2 = [number, number]; -export type Vector4 = [number, number, number, number]; -export type widgetTypes = - | "number" - | "slider" - | "combo" - | "text" - | "toggle" - | "button"; -export type SlotShape = - | typeof LiteGraph.BOX_SHAPE - | typeof LiteGraph.CIRCLE_SHAPE - | typeof LiteGraph.ARROW_SHAPE - | typeof LiteGraph.SQUARE_SHAPE - | number; // For custom shapes - -/** https://github.com/jagenjo/litegraph.js/tree/master/guides#node-slots */ -export interface INodeSlot { - name: string; - type: string | -1; - label?: string; - dir?: - | typeof LiteGraph.UP - | typeof LiteGraph.RIGHT - | typeof LiteGraph.DOWN - | typeof LiteGraph.LEFT; - color_on?: string; - color_off?: string; - shape?: SlotShape; - locked?: boolean; - nameLocked?: boolean; -} - -export interface INodeInputSlot extends INodeSlot { - link: LLink["id"] | null; -} -export interface INodeOutputSlot extends INodeSlot { - links: LLink["id"][] | null; -} - -export type WidgetCallback = ( - this: T, - value: T["value"], - graphCanvas: LGraphCanvas, - node: LGraphNode, - pos: Vector2, - event?: MouseEvent -) => void; - -export interface IWidget { - name: string | null; - value: TValue; - options?: TOptions; - type?: widgetTypes; - y?: number; - property?: string; - last_y?: number; - clicked?: boolean; - marker?: boolean; - callback?: WidgetCallback; - /** Called by `LGraphCanvas.drawNodeWidgets` */ - draw?( - ctx: CanvasRenderingContext2D, - node: LGraphNode, - width: number, - posY: number, - height: number - ): void; - /** - * Called by `LGraphCanvas.processNodeWidgets` - * https://github.com/jagenjo/litegraph.js/issues/76 - */ - mouse?( - event: MouseEvent, - pos: Vector2, - node: LGraphNode - ): boolean; - /** Called by `LGraphNode.computeSize` */ - computeSize?(width: number): [number, number]; -} -export interface IButtonWidget extends IWidget { - type: "button"; -} -export interface IToggleWidget - extends IWidget { - type: "toggle"; -} -export interface ISliderWidget - extends IWidget { - type: "slider"; -} -export interface INumberWidget extends IWidget { - type: "number"; -} -export interface IComboWidget - extends IWidget< - string[], - { - values: - | string[] - | ((widget: IComboWidget, node: LGraphNode) => string[]); - } - > { - type: "combo"; -} - -export interface ITextWidget extends IWidget { - type: "text"; -} - -export interface IContextMenuItem { - content: string; - callback?: ContextMenuEventListener; - /** Used as innerHTML for extra child element */ - title?: string; - disabled?: boolean; - has_submenu?: boolean; - submenu?: { - options: ContextMenuItem[]; - } & IContextMenuOptions; - className?: string; -} -export interface IContextMenuOptions { - callback?: ContextMenuEventListener; - ignore_item_callbacks?: Boolean; - event?: MouseEvent | CustomEvent; - parentMenu?: ContextMenu; - autoopen?: boolean; - title?: string; - extra?: any; -} - -export type ContextMenuItem = IContextMenuItem | null; -export type ContextMenuEventListener = ( - value: ContextMenuItem, - options: IContextMenuOptions, - event: MouseEvent, - parentMenu: ContextMenu | undefined, - node: LGraphNode -) => boolean | void; - -export const LiteGraph: { - VERSION: number; - - CANVAS_GRID_SIZE: number; - - NODE_TITLE_HEIGHT: number; - NODE_TITLE_TEXT_Y: number; - NODE_SLOT_HEIGHT: number; - NODE_WIDGET_HEIGHT: number; - NODE_WIDTH: number; - NODE_MIN_WIDTH: number; - NODE_COLLAPSED_RADIUS: number; - NODE_COLLAPSED_WIDTH: number; - NODE_TITLE_COLOR: string; - NODE_TEXT_SIZE: number; - NODE_TEXT_COLOR: string; - NODE_SUBTEXT_SIZE: number; - NODE_DEFAULT_COLOR: string; - NODE_DEFAULT_BGCOLOR: string; - NODE_DEFAULT_BOXCOLOR: string; - NODE_DEFAULT_SHAPE: string; - DEFAULT_SHADOW_COLOR: string; - DEFAULT_GROUP_FONT: number; - - LINK_COLOR: string; - EVENT_LINK_COLOR: string; - CONNECTING_LINK_COLOR: string; - - MAX_NUMBER_OF_NODES: number; //avoid infinite loops - DEFAULT_POSITION: Vector2; //default node position - VALID_SHAPES: ["default", "box", "round", "card"]; //,"circle" - - //shapes are used for nodes but also for slots - BOX_SHAPE: 1; - ROUND_SHAPE: 2; - CIRCLE_SHAPE: 3; - CARD_SHAPE: 4; - ARROW_SHAPE: 5; - SQUARE_SHAPE: 6; - - //enums - INPUT: 1; - OUTPUT: 2; - - EVENT: -1; //for outputs - ACTION: -1; //for inputs - - ALWAYS: 0; - ON_EVENT: 1; - NEVER: 2; - ON_TRIGGER: 3; - - UP: 1; - DOWN: 2; - LEFT: 3; - RIGHT: 4; - CENTER: 5; - - STRAIGHT_LINK: 0; - LINEAR_LINK: 1; - SPLINE_LINK: 2; - - NORMAL_TITLE: 0; - NO_TITLE: 1; - TRANSPARENT_TITLE: 2; - AUTOHIDE_TITLE: 3; - - node_images_path: string; - - debug: boolean; - catch_exceptions: boolean; - throw_errors: boolean; - /** if set to true some nodes like Formula would be allowed to evaluate code that comes from unsafe sources (like node configuration), which could lead to exploits */ - allow_scripts: boolean; - /** node types by string */ - registered_node_types: Record; - /** used for dropping files in the canvas */ - node_types_by_file_extension: Record; - /** node types by class name */ - Nodes: Record; - - /** used to add extra features to the search box */ - searchbox_extras: Record< - string, - { - data: { outputs: string[][]; title: string }; - desc: string; - type: string; - } - >; - - createNode(type: string): T; - /** Register a node class so it can be listed when the user wants to create a new one */ - registerNodeType(type: string, base: { new (): LGraphNode }): void; - /** removes a node type from the system */ - unregisterNodeType(type: string): void; - /** Removes all previously registered node's types. */ - clearRegisteredTypes(): void; - /** - * Create a new node type by passing a function, it wraps it with a proper class and generates inputs according to the parameters of the function. - * Useful to wrap simple methods that do not require properties, and that only process some input to generate an output. - * @param name node name with namespace (p.e.: 'math/sum') - * @param func - * @param param_types an array containing the type of every parameter, otherwise parameters will accept any type - * @param return_type string with the return type, otherwise it will be generic - * @param properties properties to be configurable - */ - wrapFunctionAsNode( - name: string, - func: (...args: any[]) => any, - param_types?: string[], - return_type?: string, - properties?: object - ): void; - - /** - * Adds this method to all node types, existing and to be created - * (You can add it to LGraphNode.prototype but then existing node types wont have it) - */ - addNodeMethod(name: string, func: (...args: any[]) => any): void; - - /** - * Create a node of a given type with a name. The node is not attached to any graph yet. - * @param type full name of the node class. p.e. "math/sin" - * @param name a name to distinguish from other nodes - * @param options to set options - */ - createNode( - type: string, - title: string, - options: object - ): T; - - /** - * Returns a registered node type with a given name - * @param type full name of the node class. p.e. "math/sin" - */ - getNodeType(type: string): LGraphNodeConstructor; - - /** - * Returns a list of node types matching one category - * @method getNodeTypesInCategory - * @param {String} category category name - * @param {String} filter only nodes with ctor.filter equal can be shown - * @return {Array} array with all the node classes - */ - getNodeTypesInCategory( - category: string, - filter: string - ): LGraphNodeConstructor[]; - - /** - * Returns a list with all the node type categories - * @method getNodeTypesCategories - * @param {String} filter only nodes with ctor.filter equal can be shown - * @return {Array} array with all the names of the categories - */ - getNodeTypesCategories(filter: string): string[]; - - /** debug purposes: reloads all the js scripts that matches a wildcard */ - reloadNodes(folder_wildcard: string): void; - - getTime(): number; - LLink: typeof LLink; - LGraph: typeof LGraph; - DragAndScale: typeof DragAndScale; - compareObjects(a: object, b: object): boolean; - distance(a: Vector2, b: Vector2): number; - colorToString(c: string): string; - isInsideRectangle( - x: number, - y: number, - left: number, - top: number, - width: number, - height: number - ): boolean; - growBounding(bounding: Vector4, x: number, y: number): Vector4; - isInsideBounding(p: Vector2, bb: Vector4): boolean; - hex2num(hex: string): [number, number, number]; - num2hex(triplet: [number, number, number]): string; - ContextMenu: typeof ContextMenu; - extendClass(target: A, origin: B): A & B; - getParameterNames(func: string): string[]; -}; - -export type serializedLGraph< - TNode = ReturnType, - // https://github.com/jagenjo/litegraph.js/issues/74 - TLink = [number, number, number, number, number, string], - TGroup = ReturnType -> = { - last_node_id: LGraph["last_node_id"]; - last_link_id: LGraph["last_link_id"]; - nodes: TNode[]; - links: TLink[]; - groups: TGroup[]; - config: LGraph["config"]; - version: typeof LiteGraph.VERSION; -}; - -export declare class LGraph { - static supported_types: string[]; - static STATUS_STOPPED: 1; - static STATUS_RUNNING: 2; - - constructor(o?: object); - - filter: string; - catch_errors: boolean; - /** custom data */ - config: object; - elapsed_time: number; - fixedtime: number; - fixedtime_lapse: number; - globaltime: number; - inputs: any; - iteration: number; - last_link_id: number; - last_node_id: number; - last_update_time: number; - links: Record; - list_of_graphcanvas: LGraphCanvas[]; - outputs: any; - runningtime: number; - starttime: number; - status: typeof LGraph.STATUS_RUNNING | typeof LGraph.STATUS_STOPPED; - - private _nodes: LGraphNode[]; - private _groups: LGraphGroup[]; - private _nodes_by_id: Record; - /** nodes that are executable sorted in execution order */ - private _nodes_executable: - | (LGraphNode & { onExecute: NonNullable }[]) - | null; - /** nodes that contain onExecute */ - private _nodes_in_order: LGraphNode[]; - private _version: number; - - getSupportedTypes(): string[]; - /** Removes all nodes from this graph */ - clear(): void; - /** Attach Canvas to this graph */ - attachCanvas(graphCanvas: LGraphCanvas): void; - /** Detach Canvas to this graph */ - detachCanvas(graphCanvas: LGraphCanvas): void; - /** - * Starts running this graph every interval milliseconds. - * @param interval amount of milliseconds between executions, if 0 then it renders to the monitor refresh rate - */ - start(interval?: number): void; - /** Stops the execution loop of the graph */ - stop(): void; - /** - * Run N steps (cycles) of the graph - * @param num number of steps to run, default is 1 - */ - runStep(num?: number, do_not_catch_errors?: boolean): void; - /** - * Updates the graph execution order according to relevance of the nodes (nodes with only outputs have more relevance than - * nodes with only inputs. - */ - updateExecutionOrder(): void; - /** This is more internal, it computes the executable nodes in order and returns it */ - computeExecutionOrder(only_onExecute: boolean, set_level: any): T; - /** - * Returns all the nodes that could affect this one (ancestors) by crawling all the inputs recursively. - * It doesn't include the node itself - * @return an array with all the LGraphNodes that affect this node, in order of execution - */ - getAncestors(node: LGraphNode): LGraphNode[]; - /** - * Positions every node in a more readable manner - */ - arrange(margin?: number,layout?: string): void; - /** - * Returns the amount of time the graph has been running in milliseconds - * @return number of milliseconds the graph has been running - */ - getTime(): number; - - /** - * Returns the amount of time accumulated using the fixedtime_lapse var. This is used in context where the time increments should be constant - * @return number of milliseconds the graph has been running - */ - getFixedTime(): number; - - /** - * Returns the amount of time it took to compute the latest iteration. Take into account that this number could be not correct - * if the nodes are using graphical actions - * @return number of milliseconds it took the last cycle - */ - getElapsedTime(): number; - /** - * Sends an event to all the nodes, useful to trigger stuff - * @param eventName the name of the event (function to be called) - * @param params parameters in array format - */ - sendEventToAllNodes(eventName: string, params: any[], mode?: any): void; - - sendActionToCanvas(action: any, params: any[]): void; - /** - * Adds a new node instance to this graph - * @param node the instance of the node - */ - add(node: LGraphNode, skip_compute_order?: boolean): void; - /** - * Called when a new node is added - * @param node the instance of the node - */ - onNodeAdded(node: LGraphNode): void; - /** Removes a node from the graph */ - remove(node: LGraphNode): void; - /** Returns a node by its id. */ - getNodeById(id: number): LGraphNode | undefined; - /** - * Returns a list of nodes that matches a class - * @param classObject the class itself (not an string) - * @return a list with all the nodes of this type - */ - findNodesByClass( - classObject: LGraphNodeConstructor - ): T[]; - /** - * Returns a list of nodes that matches a type - * @param type the name of the node type - * @return a list with all the nodes of this type - */ - findNodesByType(type: string): T[]; - /** - * Returns the first node that matches a name in its title - * @param title the name of the node to search - * @return the node or null - */ - findNodeByTitle(title: string): T | null; - /** - * Returns a list of nodes that matches a name - * @param title the name of the node to search - * @return a list with all the nodes with this name - */ - findNodesByTitle(title: string): T[]; - /** - * Returns the top-most node in this position of the canvas - * @param x the x coordinate in canvas space - * @param y the y coordinate in canvas space - * @param nodes_list a list with all the nodes to search from, by default is all the nodes in the graph - * @return the node at this position or null - */ - getNodeOnPos( - x: number, - y: number, - node_list?: LGraphNode[], - margin?: number - ): T | null; - /** - * Returns the top-most group in that position - * @param x the x coordinate in canvas space - * @param y the y coordinate in canvas space - * @return the group or null - */ - getGroupOnPos(x: number, y: number): LGraphGroup | null; - - onAction(action: any, param: any): void; - trigger(action: any, param: any): void; - /** Tell this graph it has a global graph input of this type */ - addInput(name: string, type: string, value?: any): void; - /** Assign a data to the global graph input */ - setInputData(name: string, data: any): void; - /** Returns the current value of a global graph input */ - getInputData(name: string): T; - /** Changes the name of a global graph input */ - renameInput(old_name: string, name: string): false | undefined; - /** Changes the type of a global graph input */ - changeInputType(name: string, type: string): false | undefined; - /** Removes a global graph input */ - removeInput(name: string): boolean; - /** Creates a global graph output */ - addOutput(name: string, type: string, value: any): void; - /** Assign a data to the global output */ - setOutputData(name: string, value: string): void; - /** Returns the current value of a global graph output */ - getOutputData(name: string): T; - - /** Renames a global graph output */ - renameOutput(old_name: string, name: string): false | undefined; - /** Changes the type of a global graph output */ - changeOutputType(name: string, type: string): false | undefined; - /** Removes a global graph output */ - removeOutput(name: string): boolean; - triggerInput(name: string, value: any): void; - setCallback(name: string, func: (...args: any[]) => any): void; - beforeChange(info?: LGraphNode): void; - afterChange(info?: LGraphNode): void; - connectionChange(node: LGraphNode): void; - /** returns if the graph is in live mode */ - isLive(): boolean; - /** clears the triggered slot animation in all links (stop visual animation) */ - clearTriggeredSlots(): void; - /* Called when something visually changed (not the graph!) */ - change(): void; - setDirtyCanvas(fg: boolean, bg: boolean): void; - /** Destroys a link */ - removeLink(link_id: number): void; - /** Creates a Object containing all the info about this graph, it can be serialized */ - serialize(): T; - /** - * Configure a graph from a JSON string - * @param data configure a graph from a JSON string - * @returns if there was any error parsing - */ - configure(data: object, keep_old?: boolean): boolean | undefined; - load(url: string): void; -} - -export type SerializedLLink = [number, string, number, number, number, number]; -export declare class LLink { - id: number; - type: string; - origin_id: number; - origin_slot: number; - target_id: number; - target_slot: number; - constructor( - id: number, - type: string, - origin_id: number, - origin_slot: number, - target_id: number, - target_slot: number - ); - configure(o: LLink | SerializedLLink): void; - serialize(): SerializedLLink; -} - -export type SerializedLGraphNode = { - id: T["id"]; - type: T["type"]; - pos: T["pos"]; - size: T["size"]; - flags: T["flags"]; - mode: T["mode"]; - inputs: T["inputs"]; - outputs: T["outputs"]; - title: T["title"]; - properties: T["properties"]; - widgets_values?: IWidget["value"][]; -}; - -/** https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#lgraphnode */ -export declare class LGraphNode { - static title_color: string; - static title: string; - static type: null | string; - static widgets_up: boolean; - constructor(title?: string); - - title: string; - type: null | string; - size: Vector2; - graph: null | LGraph; - graph_version: number; - pos: Vector2; - is_selected: boolean; - mouseOver: boolean; - - id: number; - - //inputs available: array of inputs - inputs: INodeInputSlot[]; - outputs: INodeOutputSlot[]; - connections: any[]; - - //local data - properties: Record; - properties_info: any[]; - - flags: Partial<{ - collapsed: boolean - }>; - - color: string; - bgcolor: string; - boxcolor: string; - shape: - | typeof LiteGraph.BOX_SHAPE - | typeof LiteGraph.ROUND_SHAPE - | typeof LiteGraph.CIRCLE_SHAPE - | typeof LiteGraph.CARD_SHAPE - | typeof LiteGraph.ARROW_SHAPE; - - serialize_widgets: boolean; - skip_list: boolean; - - /** Used in `LGraphCanvas.onMenuNodeMode` */ - mode?: - | typeof LiteGraph.ON_EVENT - | typeof LiteGraph.ON_TRIGGER - | typeof LiteGraph.NEVER - | typeof LiteGraph.ALWAYS; - - /** If set to true widgets do not start after the slots */ - widgets_up: boolean; - /** widgets start at y distance from the top of the node */ - widgets_start_y: number; - /** if you render outside the node, it will be clipped */ - clip_area: boolean; - /** if set to false it wont be resizable with the mouse */ - resizable: boolean; - /** slots are distributed horizontally */ - horizontal: boolean; - /** if true, the node will show the bgcolor as 'red' */ - has_errors?: boolean; - - /** configure a node from an object containing the serialized info */ - configure(info: SerializedLGraphNode): void; - /** serialize the content */ - serialize(): SerializedLGraphNode; - /** Creates a clone of this node */ - clone(): this; - /** serialize and stringify */ - toString(): string; - /** get the title string */ - getTitle(): string; - /** sets the value of a property */ - setProperty(name: string, value: any): void; - /** sets the output data */ - setOutputData(slot: number, data: any): void; - /** sets the output data */ - setOutputDataType(slot: number, type: string): void; - /** - * Retrieves the input data (data traveling through the connection) from one slot - * @param slot - * @param force_update if set to true it will force the connected node of this slot to output data into this link - * @return data or if it is not connected returns undefined - */ - getInputData(slot: number, force_update?: boolean): T; - /** - * Retrieves the input data type (in case this supports multiple input types) - * @param slot - * @return datatype in string format - */ - getInputDataType(slot: number): string; - /** - * Retrieves the input data from one slot using its name instead of slot number - * @param slot_name - * @param force_update if set to true it will force the connected node of this slot to output data into this link - * @return data or if it is not connected returns null - */ - getInputDataByName(slot_name: string, force_update?: boolean): T; - /** tells you if there is a connection in one input slot */ - isInputConnected(slot: number): boolean; - /** tells you info about an input connection (which node, type, etc) */ - getInputInfo( - slot: number - ): { link: number; name: string; type: string | 0 } | null; - /** returns the node connected in the input slot */ - getInputNode(slot: number): LGraphNode | null; - /** returns the value of an input with this name, otherwise checks if there is a property with that name */ - getInputOrProperty(name: string): T; - /** tells you the last output data that went in that slot */ - getOutputData(slot: number): T | null; - /** tells you info about an output connection (which node, type, etc) */ - getOutputInfo( - slot: number - ): { name: string; type: string; links: number[] } | null; - /** tells you if there is a connection in one output slot */ - isOutputConnected(slot: number): boolean; - /** tells you if there is any connection in the output slots */ - isAnyOutputConnected(): boolean; - /** retrieves all the nodes connected to this output slot */ - getOutputNodes(slot: number): LGraphNode[]; - /** Triggers an event in this node, this will trigger any output with the same name */ - trigger(action: string, param: any): void; - /** - * Triggers an slot event in this node - * @param slot the index of the output slot - * @param param - * @param link_id in case you want to trigger and specific output link in a slot - */ - triggerSlot(slot: number, param: any, link_id?: number): void; - /** - * clears the trigger slot animation - * @param slot the index of the output slot - * @param link_id in case you want to trigger and specific output link in a slot - */ - clearTriggeredSlot(slot: number, link_id?: number): void; - /** - * add a new property to this node - * @param name - * @param default_value - * @param type string defining the output type ("vec3","number",...) - * @param extra_info this can be used to have special properties of the property (like values, etc) - */ - addProperty( - name: string, - default_value: any, - type: string, - extra_info?: object - ): T; - /** - * add a new output slot to use in this node - * @param name - * @param type string defining the output type ("vec3","number",...) - * @param extra_info this can be used to have special properties of an output (label, special color, position, etc) - */ - addOutput( - name: string, - type: string | -1, - extra_info?: Partial - ): INodeOutputSlot; - /** - * add a new output slot to use in this node - * @param array of triplets like [[name,type,extra_info],[...]] - */ - addOutputs( - array: [string, string | -1, Partial | undefined][] - ): void; - /** remove an existing output slot */ - removeOutput(slot: number): void; - /** - * add a new input slot to use in this node - * @param name - * @param type string defining the input type ("vec3","number",...), it its a generic one use 0 - * @param extra_info this can be used to have special properties of an input (label, color, position, etc) - */ - addInput( - name: string, - type: string | -1, - extra_info?: Partial - ): INodeInputSlot; - /** - * add several new input slots in this node - * @param array of triplets like [[name,type,extra_info],[...]] - */ - addInputs( - array: [string, string | -1, Partial | undefined][] - ): void; - /** remove an existing input slot */ - removeInput(slot: number): void; - /** - * add an special connection to this node (used for special kinds of graphs) - * @param name - * @param type string defining the input type ("vec3","number",...) - * @param pos position of the connection inside the node - * @param direction if is input or output - */ - addConnection( - name: string, - type: string, - pos: Vector2, - direction: string - ): { - name: string; - type: string; - pos: Vector2; - direction: string; - links: null; - }; - setValue(v: any): void; - /** computes the size of a node according to its inputs and output slots */ - computeSize(): [number, number]; - /** - * https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#node-widgets - * @return created widget - */ - addWidget( - type: T["type"], - name: string, - value: T["value"], - callback?: WidgetCallback | string, - options?: T["options"] - ): T; - - addCustomWidget(customWidget: T): T; - - /** - * returns the bounding of the object, used for rendering purposes - * @return [x, y, width, height] - */ - getBounding(): Vector4; - /** checks if a point is inside the shape of a node */ - isPointInside( - x: number, - y: number, - margin?: number, - skipTitle?: boolean - ): boolean; - /** checks if a point is inside a node slot, and returns info about which slot */ - getSlotInPosition( - x: number, - y: number - ): { - input?: INodeInputSlot; - output?: INodeOutputSlot; - slot: number; - link_pos: Vector2; - }; - /** - * returns the input slot with a given name (used for dynamic slots), -1 if not found - * @param name the name of the slot - * @return the slot (-1 if not found) - */ - findInputSlot(name: string): number; - /** - * returns the output slot with a given name (used for dynamic slots), -1 if not found - * @param name the name of the slot - * @return the slot (-1 if not found) - */ - findOutputSlot(name: string): number; - /** - * connect this node output to the input of another node - * @param slot (could be the number of the slot or the string with the name of the slot) - * @param targetNode the target node - * @param targetSlot the input slot of the target node (could be the number of the slot or the string with the name of the slot, or -1 to connect a trigger) - * @return {Object} the link_info is created, otherwise null - */ - connect( - slot: number | string, - targetNode: LGraphNode, - targetSlot: number | string - ): T | null; - /** - * disconnect one output to an specific node - * @param slot (could be the number of the slot or the string with the name of the slot) - * @param target_node the target node to which this slot is connected [Optional, if not target_node is specified all nodes will be disconnected] - * @return if it was disconnected successfully - */ - disconnectOutput(slot: number | string, targetNode?: LGraphNode): boolean; - /** - * disconnect one input - * @param slot (could be the number of the slot or the string with the name of the slot) - * @return if it was disconnected successfully - */ - disconnectInput(slot: number | string): boolean; - /** - * returns the center of a connection point in canvas coords - * @param is_input true if if a input slot, false if it is an output - * @param slot (could be the number of the slot or the string with the name of the slot) - * @param out a place to store the output, to free garbage - * @return the position - **/ - getConnectionPos( - is_input: boolean, - slot: number | string, - out?: Vector2 - ): Vector2; - /** Force align to grid */ - alignToGrid(): void; - /** Console output */ - trace(msg: string): void; - /** Forces to redraw or the main canvas (LGraphNode) or the bg canvas (links) */ - setDirtyCanvas(fg: boolean, bg: boolean): void; - loadImage(url: string): void; - /** Allows to get onMouseMove and onMouseUp events even if the mouse is out of focus */ - captureInput(v: any): void; - /** Collapse the node to make it smaller on the canvas */ - collapse(force: boolean): void; - /** Forces the node to do not move or realign on Z */ - pin(v?: boolean): void; - localToScreen(x: number, y: number, graphCanvas: LGraphCanvas): Vector2; - - // https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#custom-node-appearance - onDrawBackground?( - ctx: CanvasRenderingContext2D, - canvas: HTMLCanvasElement - ): void; - onDrawForeground?( - ctx: CanvasRenderingContext2D, - canvas: HTMLCanvasElement - ): void; - - // https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#custom-node-behaviour - onMouseDown?( - event: MouseEvent, - pos: Vector2, - graphCanvas: LGraphCanvas - ): void; - onMouseMove?( - event: MouseEvent, - pos: Vector2, - graphCanvas: LGraphCanvas - ): void; - onMouseUp?( - event: MouseEvent, - pos: Vector2, - graphCanvas: LGraphCanvas - ): void; - onMouseEnter?( - event: MouseEvent, - pos: Vector2, - graphCanvas: LGraphCanvas - ): void; - onMouseLeave?( - event: MouseEvent, - pos: Vector2, - graphCanvas: LGraphCanvas - ): void; - onKey?(event: KeyboardEvent, pos: Vector2, graphCanvas: LGraphCanvas): void; - - /** Called by `LGraphCanvas.selectNodes` */ - onSelected?(): void; - /** Called by `LGraphCanvas.deselectNode` */ - onDeselected?(): void; - /** Called by `LGraph.runStep` `LGraphNode.getInputData` */ - onExecute?(): void; - /** Called by `LGraph.serialize` */ - onSerialize?(o: SerializedLGraphNode): void; - /** Called by `LGraph.configure` */ - onConfigure?(o: SerializedLGraphNode): void; - /** - * when added to graph (warning: this is called BEFORE the node is configured when loading) - * Called by `LGraph.add` - */ - onAdded?(graph: LGraph): void; - /** - * when removed from graph - * Called by `LGraph.remove` `LGraph.clear` - */ - onRemoved?(): void; - /** - * if returns false the incoming connection will be canceled - * Called by `LGraph.connect` - * @param inputIndex target input slot number - * @param outputType type of output slot - * @param outputSlot output slot object - * @param outputNode node containing the output - * @param outputIndex index of output slot - */ - onConnectInput?( - inputIndex: number, - outputType: INodeOutputSlot["type"], - outputSlot: INodeOutputSlot, - outputNode: LGraphNode, - outputIndex: number - ): boolean; - /** - * if returns false the incoming connection will be canceled - * Called by `LGraph.connect` - * @param outputIndex target output slot number - * @param inputType type of input slot - * @param inputSlot input slot object - * @param inputNode node containing the input - * @param inputIndex index of input slot - */ - onConnectOutput?( - outputIndex: number, - inputType: INodeInputSlot["type"], - inputSlot: INodeInputSlot, - inputNode: LGraphNode, - inputIndex: number - ): boolean; - - /** - * Called just before connection (or disconnect - if input is linked). - * A convenient place to switch to another input, or create new one. - * This allow for ability to automatically add slots if needed - * @param inputIndex - * @return selected input slot index, can differ from parameter value - */ - onBeforeConnectInput?( - inputIndex: number - ): number; - - /** a connection changed (new one or removed) (LiteGraph.INPUT or LiteGraph.OUTPUT, slot, true if connected, link_info, input_info or output_info ) */ - onConnectionsChange( - type: number, - slotIndex: number, - isConnected: boolean, - link: LLink, - ioSlot: (INodeOutputSlot | INodeInputSlot) - ): void; - - /** - * if returns false, will abort the `LGraphNode.setProperty` - * Called when a property is changed - * @param property - * @param value - * @param prevValue - */ - onPropertyChanged?(property: string, value: any, prevValue: any): void | boolean; - - /** Called by `LGraphCanvas.processContextMenu` */ - getMenuOptions?(graphCanvas: LGraphCanvas): ContextMenuItem[]; - getSlotMenuOptions?(slot: INodeSlot): ContextMenuItem[]; -} - -export type LGraphNodeConstructor = { - new (): T; -}; - -export type SerializedLGraphGroup = { - title: LGraphGroup["title"]; - bounding: LGraphGroup["_bounding"]; - color: LGraphGroup["color"]; - font: LGraphGroup["font"]; -}; -export declare class LGraphGroup { - title: string; - private _bounding: Vector4; - color: string; - font: string; - - configure(o: SerializedLGraphGroup): void; - serialize(): SerializedLGraphGroup; - move(deltaX: number, deltaY: number, ignoreNodes?: boolean): void; - recomputeInsideNodes(): void; - isPointInside: LGraphNode["isPointInside"]; - setDirtyCanvas: LGraphNode["setDirtyCanvas"]; -} - -export declare class DragAndScale { - constructor(element?: HTMLElement, skipEvents?: boolean); - offset: [number, number]; - scale: number; - max_scale: number; - min_scale: number; - onredraw: Function | null; - enabled: boolean; - last_mouse: Vector2; - element: HTMLElement | null; - visible_area: Vector4; - bindEvents(element: HTMLElement): void; - computeVisibleArea(): void; - onMouse(e: MouseEvent): void; - toCanvasContext(ctx: CanvasRenderingContext2D): void; - convertOffsetToCanvas(pos: Vector2): Vector2; - convertCanvasToOffset(pos: Vector2): Vector2; - mouseDrag(x: number, y: number): void; - changeScale(value: number, zooming_center?: Vector2): void; - changeDeltaScale(value: number, zooming_center?: Vector2): void; - reset(): void; -} - -/** - * This class is in charge of rendering one graph inside a canvas. And provides all the interaction required. - * Valid callbacks are: onNodeSelected, onNodeDeselected, onShowNodePanel, onNodeDblClicked - * - * @param canvas the canvas where you want to render (it accepts a selector in string format or the canvas element itself) - * @param graph - * @param options { skip_rendering, autoresize } - */ -export declare class LGraphCanvas { - static node_colors: Record< - string, - { - color: string; - bgcolor: string; - groupcolor: string; - } - >; - static link_type_colors: Record; - static gradients: object; - static search_limit: number; - - static getFileExtension(url: string): string; - static decodeHTML(str: string): string; - - static onMenuCollapseAll(): void; - static onMenuNodeEdit(): void; - static onShowPropertyEditor( - item: any, - options: any, - e: any, - menu: any, - node: any - ): void; - /** Create menu for `Add Group` */ - static onGroupAdd: ContextMenuEventListener; - /** Create menu for `Add Node` */ - static onMenuAdd: ContextMenuEventListener; - static showMenuNodeOptionalInputs: ContextMenuEventListener; - static showMenuNodeOptionalOutputs: ContextMenuEventListener; - static onShowMenuNodeProperties: ContextMenuEventListener; - static onResizeNode: ContextMenuEventListener; - static onMenuNodeCollapse: ContextMenuEventListener; - static onMenuNodePin: ContextMenuEventListener; - static onMenuNodeMode: ContextMenuEventListener; - static onMenuNodeColors: ContextMenuEventListener; - static onMenuNodeShapes: ContextMenuEventListener; - static onMenuNodeRemove: ContextMenuEventListener; - static onMenuNodeClone: ContextMenuEventListener; - - constructor( - canvas: HTMLCanvasElement | string, - graph?: LGraph, - options?: { - skip_render?: boolean; - autoresize?: boolean; - } - ); - - static active_canvas: HTMLCanvasElement; - - allow_dragcanvas: boolean; - allow_dragnodes: boolean; - /** allow to control widgets, buttons, collapse, etc */ - allow_interaction: boolean; - /** allows to change a connection with having to redo it again */ - allow_reconnect_links: boolean; - /** allow selecting multi nodes without pressing extra keys */ - multi_select: boolean; - /** No effect */ - allow_searchbox: boolean; - always_render_background: boolean; - autoresize?: boolean; - background_image: string; - bgcanvas: HTMLCanvasElement; - bgctx: CanvasRenderingContext2D; - canvas: HTMLCanvasElement; - canvas_mouse: Vector2; - clear_background: boolean; - connecting_node: LGraphNode | null; - connections_width: number; - ctx: CanvasRenderingContext2D; - current_node: LGraphNode | null; - default_connection_color: { - input_off: string; - input_on: string; - output_off: string; - output_on: string; - }; - default_link_color: string; - dirty_area: Vector4 | null; - dirty_bgcanvas?: boolean; - dirty_canvas?: boolean; - drag_mode: boolean; - dragging_canvas: boolean; - dragging_rectangle: Vector4 | null; - ds: DragAndScale; - /** used for transition */ - editor_alpha: number; - filter: any; - fps: number; - frame: number; - graph: LGraph; - highlighted_links: Record; - highquality_render: boolean; - inner_text_font: string; - is_rendering: boolean; - last_draw_time: number; - last_mouse: Vector2; - /** - * Possible duplicated with `last_mouse` - * https://github.com/jagenjo/litegraph.js/issues/70 - */ - last_mouse_position: Vector2; - /** Timestamp of last mouse click, defaults to 0 */ - last_mouseclick: number; - links_render_mode: - | typeof LiteGraph.STRAIGHT_LINK - | typeof LiteGraph.LINEAR_LINK - | typeof LiteGraph.SPLINE_LINK; - live_mode: boolean; - node_capturing_input: LGraphNode | null; - node_dragged: LGraphNode | null; - node_in_panel: LGraphNode | null; - node_over: LGraphNode | null; - node_title_color: string; - node_widget: [LGraphNode, IWidget] | null; - /** Called by `LGraphCanvas.drawBackCanvas` */ - onDrawBackground: - | ((ctx: CanvasRenderingContext2D, visibleArea: Vector4) => void) - | null; - /** Called by `LGraphCanvas.drawFrontCanvas` */ - onDrawForeground: - | ((ctx: CanvasRenderingContext2D, visibleArea: Vector4) => void) - | null; - onDrawOverlay: ((ctx: CanvasRenderingContext2D) => void) | null; - /** Called by `LGraphCanvas.processMouseDown` */ - onMouse: ((event: MouseEvent) => boolean) | null; - /** Called by `LGraphCanvas.drawFrontCanvas` and `LGraphCanvas.drawLinkTooltip` */ - onDrawLinkTooltip: ((ctx: CanvasRenderingContext2D, link: LLink, _this: this) => void) | null; - /** Called by `LGraphCanvas.selectNodes` */ - onNodeMoved: ((node: LGraphNode) => void) | null; - /** Called by `LGraphCanvas.processNodeSelected` */ - onNodeSelected: ((node: LGraphNode) => void) | null; - /** Called by `LGraphCanvas.deselectNode` */ - onNodeDeselected: ((node: LGraphNode) => void) | null; - /** Called by `LGraphCanvas.processNodeDblClicked` */ - onShowNodePanel: ((node: LGraphNode) => void) | null; - /** Called by `LGraphCanvas.processNodeDblClicked` */ - onNodeDblClicked: ((node: LGraphNode) => void) | null; - /** Called by `LGraphCanvas.selectNodes` */ - onSelectionChange: ((nodes: Record) => void) | null; - /** Called by `LGraphCanvas.showSearchBox` */ - onSearchBox: - | (( - helper: Element, - value: string, - graphCanvas: LGraphCanvas - ) => string[]) - | null; - onSearchBoxSelection: - | ((name: string, event: MouseEvent, graphCanvas: LGraphCanvas) => void) - | null; - pause_rendering: boolean; - render_canvas_border: boolean; - render_collapsed_slots: boolean; - render_connection_arrows: boolean; - render_connections_border: boolean; - render_connections_shadows: boolean; - render_curved_connections: boolean; - render_execution_order: boolean; - render_only_selected: boolean; - render_shadows: boolean; - render_title_colored: boolean; - round_radius: number; - selected_group: null | LGraphGroup; - selected_group_resizing: boolean; - selected_nodes: Record; - show_info: boolean; - title_text_font: string; - /** set to true to render title bar with gradients */ - use_gradients: boolean; - visible_area: DragAndScale["visible_area"]; - visible_links: LLink[]; - visible_nodes: LGraphNode[]; - zoom_modify_alpha: boolean; - - /** clears all the data inside */ - clear(): void; - /** assigns a graph, you can reassign graphs to the same canvas */ - setGraph(graph: LGraph, skipClear?: boolean): void; - /** opens a graph contained inside a node in the current graph */ - openSubgraph(graph: LGraph): void; - /** closes a subgraph contained inside a node */ - closeSubgraph(): void; - /** assigns a canvas */ - setCanvas(canvas: HTMLCanvasElement, skipEvents?: boolean): void; - /** binds mouse, keyboard, touch and drag events to the canvas */ - bindEvents(): void; - /** unbinds mouse events from the canvas */ - unbindEvents(): void; - - /** - * this function allows to render the canvas using WebGL instead of Canvas2D - * this is useful if you plant to render 3D objects inside your nodes, it uses litegl.js for webgl and canvas2DtoWebGL to emulate the Canvas2D calls in webGL - **/ - enableWebGL(): void; - - /** - * marks as dirty the canvas, this way it will be rendered again - * @param fg if the foreground canvas is dirty (the one containing the nodes) - * @param bg if the background canvas is dirty (the one containing the wires) - */ - setDirty(fg: boolean, bg: boolean): void; - - /** - * Used to attach the canvas in a popup - * @return the window where the canvas is attached (the DOM root node) - */ - getCanvasWindow(): Window; - /** starts rendering the content of the canvas when needed */ - startRendering(): void; - /** stops rendering the content of the canvas (to save resources) */ - stopRendering(): void; - - processMouseDown(e: MouseEvent): boolean | undefined; - processMouseMove(e: MouseEvent): boolean | undefined; - processMouseUp(e: MouseEvent): boolean | undefined; - processMouseWheel(e: MouseEvent): boolean | undefined; - - /** returns true if a position (in graph space) is on top of a node little corner box */ - isOverNodeBox(node: LGraphNode, canvasX: number, canvasY: number): boolean; - /** returns true if a position (in graph space) is on top of a node input slot */ - isOverNodeInput( - node: LGraphNode, - canvasX: number, - canvasY: number, - slotPos: Vector2 - ): boolean; - - /** process a key event */ - processKey(e: KeyboardEvent): boolean | undefined; - - copyToClipboard(): void; - pasteFromClipboard(): void; - processDrop(e: DragEvent): void; - checkDropItem(e: DragEvent): void; - processNodeDblClicked(n: LGraphNode): void; - processNodeSelected(n: LGraphNode, e: MouseEvent): void; - processNodeDeselected(node: LGraphNode): void; - - /** selects a given node (or adds it to the current selection) */ - selectNode(node: LGraphNode, add?: boolean): void; - /** selects several nodes (or adds them to the current selection) */ - selectNodes(nodes?: LGraphNode[], add?: boolean): void; - /** removes a node from the current selection */ - deselectNode(node: LGraphNode): void; - /** removes all nodes from the current selection */ - deselectAllNodes(): void; - /** deletes all nodes in the current selection from the graph */ - deleteSelectedNodes(): void; - - /** centers the camera on a given node */ - centerOnNode(node: LGraphNode): void; - /** changes the zoom level of the graph (default is 1), you can pass also a place used to pivot the zoom */ - setZoom(value: number, center: Vector2): void; - /** brings a node to front (above all other nodes) */ - bringToFront(node: LGraphNode): void; - /** sends a node to the back (below all other nodes) */ - sendToBack(node: LGraphNode): void; - /** checks which nodes are visible (inside the camera area) */ - computeVisibleNodes(nodes: LGraphNode[]): LGraphNode[]; - /** renders the whole canvas content, by rendering in two separated canvas, one containing the background grid and the connections, and one containing the nodes) */ - draw(forceFG?: boolean, forceBG?: boolean): void; - /** draws the front canvas (the one containing all the nodes) */ - drawFrontCanvas(): void; - /** draws some useful stats in the corner of the canvas */ - renderInfo(ctx: CanvasRenderingContext2D, x: number, y: number): void; - /** draws the back canvas (the one containing the background and the connections) */ - drawBackCanvas(): void; - /** draws the given node inside the canvas */ - drawNode(node: LGraphNode, ctx: CanvasRenderingContext2D): void; - /** draws graphic for node's slot */ - drawSlotGraphic(ctx: CanvasRenderingContext2D, pos: number[], shape: SlotShape, horizontal: boolean): void; - /** draws the shape of the given node in the canvas */ - drawNodeShape( - node: LGraphNode, - ctx: CanvasRenderingContext2D, - size: [number, number], - fgColor: string, - bgColor: string, - selected: boolean, - mouseOver: boolean - ): void; - /** draws every connection visible in the canvas */ - drawConnections(ctx: CanvasRenderingContext2D): void; - /** - * draws a link between two points - * @param a start pos - * @param b end pos - * @param link the link object with all the link info - * @param skipBorder ignore the shadow of the link - * @param flow show flow animation (for events) - * @param color the color for the link - * @param startDir the direction enum - * @param endDir the direction enum - * @param numSublines number of sublines (useful to represent vec3 or rgb) - **/ - renderLink( - a: Vector2, - b: Vector2, - link: object, - skipBorder: boolean, - flow: boolean, - color?: string, - startDir?: number, - endDir?: number, - numSublines?: number - ): void; - - computeConnectionPoint( - a: Vector2, - b: Vector2, - t: number, - startDir?: number, - endDir?: number - ): void; - - drawExecutionOrder(ctx: CanvasRenderingContext2D): void; - /** draws the widgets stored inside a node */ - drawNodeWidgets( - node: LGraphNode, - posY: number, - ctx: CanvasRenderingContext2D, - activeWidget: object - ): void; - /** process an event on widgets */ - processNodeWidgets( - node: LGraphNode, - pos: Vector2, - event: Event, - activeWidget: object - ): void; - /** draws every group area in the background */ - drawGroups(canvas: any, ctx: CanvasRenderingContext2D): void; - adjustNodesSize(): void; - /** resizes the canvas to a given size, if no size is passed, then it tries to fill the parentNode */ - resize(width?: number, height?: number): void; - /** - * switches to live mode (node shapes are not rendered, only the content) - * this feature was designed when graphs where meant to create user interfaces - **/ - switchLiveMode(transition?: boolean): void; - onNodeSelectionChange(): void; - touchHandler(event: TouchEvent): void; - - showLinkMenu(link: LLink, e: any): false; - prompt( - title: string, - value: any, - callback: Function, - event: any - ): HTMLDivElement; - showSearchBox(event?: MouseEvent): void; - showEditPropertyValue(node: LGraphNode, property: any, options: any): void; - createDialog( - html: string, - options?: { position?: Vector2; event?: MouseEvent } - ): void; - - convertOffsetToCanvas: DragAndScale["convertOffsetToCanvas"]; - convertCanvasToOffset: DragAndScale["convertCanvasToOffset"]; - /** converts event coordinates from canvas2D to graph coordinates */ - convertEventToCanvasOffset(e: MouseEvent): Vector2; - /** adds some useful properties to a mouse event, like the position in graph coordinates */ - adjustMouseEvent(e: MouseEvent): void; - - getCanvasMenuOptions(): ContextMenuItem[]; - getNodeMenuOptions(node: LGraphNode): ContextMenuItem[]; - getGroupMenuOptions(): ContextMenuItem[]; - /** Called by `getCanvasMenuOptions`, replace default options */ - getMenuOptions?(): ContextMenuItem[]; - /** Called by `getCanvasMenuOptions`, append to default options */ - getExtraMenuOptions?(): ContextMenuItem[]; - /** Called when mouse right click */ - processContextMenu(node: LGraphNode, event: Event): void; -} - -declare class ContextMenu { - static trigger( - element: HTMLElement, - event_name: string, - params: any, - origin: any - ): void; - static isCursorOverElement(event: MouseEvent, element: HTMLElement): void; - static closeAllContextMenus(window: Window): void; - constructor(values: ContextMenuItem[], options?: IContextMenuOptions, window?: Window); - options: IContextMenuOptions; - parentMenu?: ContextMenu; - lock: boolean; - current_submenu?: ContextMenu; - addItem( - name: string, - value: ContextMenuItem, - options?: IContextMenuOptions - ): void; - close(e?: MouseEvent, ignore_parent_menu?: boolean): void; - getTopMenu(): void; - getFirstEvent(): void; -} - -declare global { - interface CanvasRenderingContext2D { - /** like rect but rounded corners */ - roundRect( - x: number, - y: number, - width: number, - height: number, - radius: number, - radiusLow: number - ): void; - } - - interface Math { - clamp(v: number, min: number, max: number): number; - } -} diff --git a/web/user.css b/web/user.css deleted file mode 100644 index 8b1af3868..000000000 --- a/web/user.css +++ /dev/null @@ -1 +0,0 @@ -/* Put custom styles here */ \ No newline at end of file