From 79c53e0095c5fe5dc1d15409de553a2a4af782d7 Mon Sep 17 00:00:00 2001 From: dmx Date: Sun, 3 Nov 2024 09:20:57 +0400 Subject: [PATCH] (prettier formatting) --- .prettierrc | 10 +- public/css/litegraph.css | 786 +- src/ContextMenu.ts | 692 +- src/CurveEditor.ts | 319 +- src/DragAndScale.ts | 407 +- src/LGraph.ts | 2564 +++--- src/LGraphBadge.ts | 100 +- src/LGraphCanvas.ts | 14608 +++++++++++++++++------------------ src/LGraphGroup.ts | 443 +- src/LGraphNode.ts | 4123 +++++----- src/LLink.ts | 185 +- src/LiteGraphGlobal.ts | 1691 ++-- src/MapProxyHandler.ts | 87 +- src/draw.ts | 110 +- src/interfaces.ts | 172 +- src/litegraph.ts | 165 +- src/measure.ts | 239 +- src/polyfills.ts | 157 +- src/strings.ts | 4 +- src/types/events.ts | 95 +- src/types/globalEnums.ts | 64 +- src/types/serialisation.ts | 141 +- src/types/widgets.ts | 165 +- src/utils/arrange.ts | 133 +- test/LGraph.test.ts | 36 +- test/LGraphNode.test.ts | 13 +- 26 files changed, 13503 insertions(+), 14006 deletions(-) diff --git a/.prettierrc b/.prettierrc index ef6f9008a..f36bb1cd2 100755 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,7 @@ { - "singleQuote": false, - "semi": true, - "tabWidth": 2 -} + "singleQuote": true, + "tabWidth": 2, + "semi": false, + "trailingComma": "none", + "printWidth": 80 +} \ No newline at end of file diff --git a/public/css/litegraph.css b/public/css/litegraph.css index 5524e24ba..ebaacaaa7 100644 --- a/public/css/litegraph.css +++ b/public/css/litegraph.css @@ -1,693 +1,699 @@ /* 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; + /*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; + 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; + 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; + background-color: #000 !important; } .litegraph.litecontextmenu .litemenu-title img { - margin-top: 2px; - margin-left: 2px; - margin-right: 4px; + margin-top: 2px; + margin-left: 2px; + margin-right: 4px; } .litegraph.litecontextmenu .litemenu-entry { - margin: 2px; - padding: 2px; + margin: 2px; + padding: 2px; } .litegraph.litecontextmenu .litemenu-entry.submenu { - background-color: #2e2e2e !important; + background-color: #2e2e2e !important; } .litegraph.litecontextmenu.dark .litemenu-entry.submenu { - background-color: #000 !important; + background-color: #000 !important; } .litegraph .litemenubar ul { - font-family: Tahoma, sans-serif; - margin: 0; - padding: 0; + 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; + 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; + 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; + 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; + 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; + display: inline-block; + width: 12px; + height: 12px; + margin: 2px; + vertical-align: top; } .litegraph .litemenu-entry.checked .icon { - background-color: #aaf; + background-color: #aaf; } .litegraph .litemenu-entry .more { - float: right; - padding-right: 5px; + float: right; + padding-right: 5px; } .litegraph .litemenu-entry.disabled { - opacity: 0.5; - cursor: default; + 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; + 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; + border-right: 2px solid cyan; } .litegraph .litemenu-title { - color: #dde; - background-color: #111; - margin: 0; - padding: 2px; - cursor: default; + 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; + 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; + 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; + 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; + 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; + 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; + display: inline-block; + min-width: 60px; + min-height: 1.5em; + padding-left: 10px; } .litegraph.litesearchbox .helper { - overflow: auto; - max-height: 200px; - margin-top: 2px; + 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; + 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.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.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; + 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; - } + 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; + position: absolute; + top: 50%; + left: 50%; + margin-top: -150px; + margin-left: -200px; - background-color: #2A2A2A; + background-color: #2a2a2a; - min-width: 400px; - min-height: 200px; - box-shadow: 0 0 4px #111; - border-radius: 6px; + 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%; + 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; + 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; + float: right; + margin: 4px; + margin-right: 10px; + cursor: pointer; + font-size: 1.4em; } .litegraph .dialog .close:hover { - color: white; + color: white; } .litegraph .dialog .dialog-header { - color: #AAA; - border-bottom: 1px solid #161616; + 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 { + 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; + 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, +.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; + margin: 10px; } .litegraph .dialog .dialog-content .connections { - flex-direction: row; + flex-direction: row; } .litegraph .dialog .dialog-content .connections .connections_side { - width: calc(50% - 5px); - min-height: 100px; - background-color: black; - display: flex; + width: calc(50% - 5px); + min-height: 100px; + background-color: black; + display: flex; } .litegraph .dialog .node_type { - font-size: 1.2em; - display: block; - margin: 10px; + font-size: 1.2em; + display: block; + margin: 10px; } .litegraph .dialog .node_desc { - opacity: 0.5; - display: block; - margin: 10px; + 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; + 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; + margin-bottom: 2px; + padding: 4px; } .litegraph .dialog .property:hover { - background: #545454; + 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; + 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; + 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; + 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; + color: white; } .litegraph .dialog .property.boolean .property_value { - padding-right: 30px; - color: #A88; - /*width: auto; + 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_name { + color: #8a8; } -.litegraph .dialog .property.boolean.bool-on .property_value{ - 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; + 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; + background-color: #111; + color: #fff; } .litegraph .dialog .btn.delete:hover { - background-color: #F33; - color: black; + background-color: #f33; + color: black; } .litegraph .subgraph_property { - padding: 4px; + padding: 4px; } .litegraph .subgraph_property:hover { - background-color: #333; + background-color: #333; } .litegraph .subgraph_property.extra { - margin-top: 8px; + margin-top: 8px; } .litegraph .subgraph_property span.name { - font-size: 1.3em; - padding-left: 4px; + font-size: 1.3em; + padding-left: 4px; } .litegraph .subgraph_property span.type { - opacity: 0.5; - margin-right: 20px; - padding-left: 4px; + opacity: 0.5; + margin-right: 20px; + padding-left: 4px; } .litegraph .subgraph_property span.label { - display: inline-block; - width: 60px; - padding: 0px 10px; + 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; + 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; + background-color: #1c1c1c; + color: #aaa; + border: 0; + border-radius: 2px; + padding: 4px 10px; + cursor: pointer; } .litegraph .subgraph_property.extra { - color: #ccc; + color: #ccc; } .litegraph .subgraph_property.extra input { - background-color: #111; + 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; + 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; -} + background-color: #698; + cursor: pointer; +} /* OLD */ .graphcontextmenu { - padding: 4px; - min-width: 100px; + padding: 4px; + min-width: 100px; } .graphcontextmenu-title { - color: #dde; - background-color: #222; - margin: 0; - padding: 2px; - cursor: default; + 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; + 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; + border-left: 8px solid orange; + padding-left: 12px; } .graphmenu-entry.disabled { - opacity: 0.3; + opacity: 0.3; } .graphmenu-entry.submenu { - border-right: 2px solid #eee; + border-right: 2px solid #eee; } .graphmenu-entry:hover { - background-color: #555; + 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); + 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; + 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; + 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; + 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; + border-radius: 12px; + padding-right: 2px; } .graphdialog .name { - display: inline-block; - min-width: 60px; - min-height: 1.5em; - padding-left: 10px; + 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; + 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; + min-height: 150px; } .graphdialog button { - margin-top: 3px; - vertical-align: top; - background-color: #999; - border: 0; + margin-top: 3px; + vertical-align: top; + background-color: #999; + border: 0; } .graphdialog button.rounded, .graphdialog input.rounded { - border-radius: 0 12px 12px 0; + border-radius: 0 12px 12px 0; } .graphdialog .helper { - overflow: auto; - max-height: 200px; + overflow: auto; + max-height: 200px; } .graphdialog .help-item { - padding-left: 10px; + padding-left: 10px; } .graphdialog .help-item:hover, .graphdialog .help-item.selected { - cursor: pointer; - background-color: white; - color: black; + cursor: pointer; + background-color: white; + color: black; } .litegraph .dialog { - min-height: 0; + min-height: 0; } .litegraph .dialog .dialog-content { -display: block; + display: block; } .litegraph .dialog .dialog-content .subgraph_property { -padding: 5px; + padding: 5px; } .litegraph .dialog .dialog-footer { -margin: 0; + margin: 0; } .litegraph .dialog .dialog-footer .subgraph_property { -margin-top: 0; -display: flex; -align-items: center; -padding: 5px; + margin-top: 0; + display: flex; + align-items: center; + padding: 5px; } .litegraph .dialog .dialog-footer .subgraph_property .name { -flex: 1; + flex: 1; } .litegraph .graphdialog { -display: flex; -align-items: center; -border-radius: 20px; -padding: 4px 10px; -position: fixed; + 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; + 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; + font-size: 16px; + min-height: 0; + margin: 0 10px; + padding: 2px 5px; } .litegraph .graphdialog input[type="checkbox"] { -width: 16px; -height: 16px; + width: 16px; + height: 16px; } .litegraph .graphdialog button { -padding: 4px 18px; -border-radius: 20px; -cursor: pointer; + padding: 4px 18px; + border-radius: 20px; + cursor: pointer; } - diff --git a/src/ContextMenu.ts b/src/ContextMenu.ts index 39bc1e0df..7d6f0a92e 100644 --- a/src/ContextMenu.ts +++ b/src/ContextMenu.ts @@ -1,14 +1,14 @@ -import type { IContextMenuOptions, IContextMenuValue } from "./interfaces" -import { LiteGraph } from "./litegraph" +import type { IContextMenuOptions, IContextMenuValue } from './interfaces' +import { LiteGraph } from './litegraph' interface ContextMenuDivElement extends HTMLDivElement { - value?: IContextMenuValue | string - onclick_callback?: never - closing_timer?: number + value?: IContextMenuValue | string + onclick_callback?: never + closing_timer?: number } export interface ContextMenu { - constructor: new (...args: ConstructorParameters) => ContextMenu + constructor: new (...args: ConstructorParameters) => ContextMenu } /** @@ -24,354 +24,352 @@ export interface ContextMenu { * - event: you can pass a MouseEvent, this way the ContextMenu appears in that position */ export class ContextMenu { - options?: IContextMenuOptions - parentMenu?: ContextMenu - root: ContextMenuDivElement - current_submenu?: ContextMenu - lock?: boolean + options?: IContextMenuOptions + parentMenu?: ContextMenu + root: ContextMenuDivElement + current_submenu?: ContextMenu + lock?: boolean - // TODO: Interface for values requires functionality change - currently accepts an array of strings, functions, objects, nulls, or undefined. - constructor(values: (IContextMenuValue | string)[], options: IContextMenuOptions) { - options ||= {} - this.options = options + // TODO: Interface for values requires functionality change - currently accepts an array of strings, functions, objects, nulls, or undefined. + constructor(values: (IContextMenuValue | string)[], options: IContextMenuOptions) { + options ||= {} + this.options = options - //to link a menu with its parent - const parent = options.parentMenu - if (parent) { - if (!(parent instanceof ContextMenu)) { - console.error("parentMenu must be of class ContextMenu, ignoring it") - options.parentMenu = null - } else { - this.parentMenu = parent - this.parentMenu.lock = true - this.parentMenu.current_submenu = this - } - if (parent.options?.className === "dark") { - options.className = "dark" - } - } - - //use strings because comparing classes between windows doesnt work - const eventClass = options.event - ? options.event.constructor.name - : null - 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 - } - - const root: ContextMenuDivElement = document.createElement("div") - let classes = "litegraph litecontextmenu litemenubar-panel" - if (options.className) classes += " " + options.className - root.className = classes - root.style.minWidth = "100" - root.style.minHeight = "100" - // TODO: Fix use of timer in place of events - 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: MouseEvent) { - //console.log("pointerevents: ContextMenu up root prevent"); - e.preventDefault() - return true - }, - true - ) - root.addEventListener( - "contextmenu", - function (e: MouseEvent) { - //right button - if (e.button != 2) return false - e.preventDefault() - return false - }, - true - ) - - LiteGraph.pointerListenerAdd(root, "down", - (e: MouseEvent) => { - //console.log("pointerevents: ContextMenu down"); - if (e.button == 2) { - this.close() - e.preventDefault() - return true - } - }, - true - ) - - function on_mouse_wheel(e: WheelEvent) { - const 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) - - this.root = root - - //title - if (options.title) { - const element = document.createElement("div") - element.className = "litemenu-title" - element.innerHTML = options.title - root.appendChild(element) - } - - //entries - for (let i = 0; i < values.length; i++) { - const value = values[i] - let name = Array.isArray(values) ? value : String(i) - - if (typeof name !== "string") { - name = name != null - ? name.content === undefined ? String(name) : name.content - : name as null | undefined - } - - this.addItem(name, value, options) - } - - LiteGraph.pointerListenerAdd(root, "enter", function () { - if (root.closing_timer) { - clearTimeout(root.closing_timer) - } - }) - - //insert before checking position - const ownerDocument = (options.event?.target as Node).ownerDocument - const root_document = ownerDocument || document - - if (root_document.fullscreenElement) - root_document.fullscreenElement.appendChild(root) - else - root_document.body.appendChild(root) - - //compute best position - let left = options.left || 0 - let top = options.top || 0 - if (options.event) { - left = options.event.clientX - 10 - top = options.event.clientY - 10 - if (options.title) top -= 20 - - if (parent) { - const rect = parent.root.getBoundingClientRect() - left = rect.left + rect.width - } - - const body_rect = document.body.getBoundingClientRect() - const 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})` + //to link a menu with its parent + const parent = options.parentMenu + if (parent) { + if (!(parent instanceof ContextMenu)) { + console.error('parentMenu must be of class ContextMenu, ignoring it') + options.parentMenu = null + } else { + this.parentMenu = parent + this.parentMenu.lock = true + this.parentMenu.current_submenu = this + } + if (parent.options?.className === 'dark') { + options.className = 'dark' + } } - addItem(name: string, value: IContextMenuValue | string, options: IContextMenuOptions): HTMLElement { - options ||= {} - - const element: ContextMenuDivElement = document.createElement("div") - element.className = "litemenu-entry submenu" - - let disabled = false - - if (value === null) { - element.classList.add("separator") - } else { - if (typeof value === "string") { - element.innerHTML = name - } else { - element.innerHTML = value?.title ?? name - - if (value.disabled) { - disabled = true - element.classList.add("disabled") - element.setAttribute("aria-disabled", "true") - } - if (value.submenu || value.has_submenu) { - element.classList.add("has_submenu") - element.setAttribute("aria-haspopup", "true") - element.setAttribute("aria-expanded", "false") - } - if (value.className) - element.className += " " + value.className - } - element.value = value - element.setAttribute("role", "menuitem") - - if (typeof value === "function") { - element.dataset["value"] = name - element.onclick_callback = value - } else { - element.dataset["value"] = String(value) - } - } - - this.root.appendChild(element) - if (!disabled) element.addEventListener("click", inner_onclick) - if (!disabled && options.autoopen) - LiteGraph.pointerListenerAdd(element, "enter", inner_over) - - const setAriaExpanded = () => { - const entries = this.root.querySelectorAll("div.litemenu-entry.has_submenu") - if (entries) { - for (let i = 0; i < entries.length; i++) { - entries[i].setAttribute("aria-expanded", "false") - } - } - element.setAttribute("aria-expanded", "true") - } - - function inner_over(this: ContextMenuDivElement, e: MouseEvent) { - const value = this.value - if (!value || !(value as IContextMenuValue).has_submenu) return - - //if it is a submenu, autoopen like the item was clicked - inner_onclick.call(this, e) - setAriaExpanded() - } - - //menu option clicked - const that = this - function inner_onclick(this: ContextMenuDivElement, e: MouseEvent) { - const value = this.value - let close_parent = true - - that.current_submenu?.close(e) - if ((value as IContextMenuValue)?.has_submenu || (value as IContextMenuValue)?.submenu) setAriaExpanded() - - //global callback - if (options.callback) { - const r = options.callback.call( - this, - value, - options, - e, - that, - options.node - ) - if (r === true) close_parent = false - } - - //special cases - if (typeof value === "object") { - if (value.callback && - !options.ignore_item_callbacks && - value.disabled !== true) { - //item callback - const 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" - - 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 + //use strings because comparing classes between windows doesnt work + const eventClass = options.event ? options.event.constructor.name : null + 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 } - close(e?: MouseEvent, ignore_parent_menu?: boolean): void { - 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) - } - } - this.current_submenu?.close(e, true) + const root: ContextMenuDivElement = document.createElement('div') + let classes = 'litegraph litecontextmenu litemenubar-panel' + if (options.className) classes += ' ' + options.className + root.className = classes + root.style.minWidth = '100' + root.style.minHeight = '100' + // TODO: Fix use of timer in place of events + root.style.pointerEvents = 'none' + setTimeout(function () { + root.style.pointerEvents = 'auto' + }, 100) //delay so the mouse up event is not caught by this element - if (this.root.closing_timer) - clearTimeout(this.root.closing_timer) - } - - //this code is used to trigger events easily (used in the context menu mouseleave - static trigger(element: HTMLDivElement, event_name: string, params: MouseEvent, origin?: unknown): CustomEvent { - const evt = document.createEvent("CustomEvent") - evt.initCustomEvent(event_name, true, true, params) //canBubble, cancelable, detail - // @ts-expect-error - evt.srcElement = origin - if (element.dispatchEvent) element.dispatchEvent(evt) - // @ts-expect-error - else if (element.__events) element.__events.dispatchEvent(evt) - //else nothing seems binded here so nothing to do - return evt - } - - //returns the top most menu - getTopMenu(): ContextMenu { - return this.options.parentMenu - ? this.options.parentMenu.getTopMenu() - : this - } - - getFirstEvent(): MouseEvent { - return this.options.parentMenu - ? this.options.parentMenu.getFirstEvent() - : this.options.event - } - - static isCursorOverElement(event: MouseEvent, element: HTMLDivElement): boolean { - const left = event.clientX - const top = event.clientY - const 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 - } + //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: MouseEvent) { + //console.log("pointerevents: ContextMenu up root prevent"); + e.preventDefault() + return true + }, + true, + ) + root.addEventListener( + 'contextmenu', + function (e: MouseEvent) { + //right button + if (e.button != 2) return false + e.preventDefault() return false + }, + true + ) + + LiteGraph.pointerListenerAdd( + root, + 'down', + (e: MouseEvent) => { + //console.log("pointerevents: ContextMenu down"); + if (e.button == 2) { + this.close() + e.preventDefault() + return true + } + }, + true + ) + + function on_mouse_wheel(e: WheelEvent) { + const 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) + + this.root = root + + //title + if (options.title) { + const element = document.createElement('div') + element.className = 'litemenu-title' + element.innerHTML = options.title + root.appendChild(element) + } + + //entries + for (let i = 0; i < values.length; i++) { + const value = values[i] + let name = Array.isArray(values) ? value : String(i) + + if (typeof name !== 'string') { + name = name != null + ? (name.content === undefined ? String(name) : name.content) + : (name as null | undefined) + } + + this.addItem(name, value, options) + } + + LiteGraph.pointerListenerAdd(root, 'enter', function () { + if (root.closing_timer) { + clearTimeout(root.closing_timer) + } + }) + + //insert before checking position + const ownerDocument = (options.event?.target as Node).ownerDocument + const root_document = ownerDocument || document + + if (root_document.fullscreenElement) { + root_document.fullscreenElement.appendChild(root) + } else { + root_document.body.appendChild(root) + } + + //compute best position + let left = options.left || 0 + let top = options.top || 0 + if (options.event) { + left = options.event.clientX - 10 + top = options.event.clientY - 10 + if (options.title) top -= 20 + + if (parent) { + const rect = parent.root.getBoundingClientRect() + left = rect.left + rect.width + } + + const body_rect = document.body.getBoundingClientRect() + const 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})` + } + } + + addItem( + name: string, + value: IContextMenuValue | string, + options: IContextMenuOptions, + ): HTMLElement { + options ||= {} + + const element: ContextMenuDivElement = document.createElement('div') + element.className = 'litemenu-entry submenu' + + let disabled = false + + if (value === null) { + element.classList.add('separator') + } else { + if (typeof value === 'string') { + element.innerHTML = name + } else { + element.innerHTML = value?.title ?? name + + if (value.disabled) { + disabled = true + element.classList.add('disabled') + element.setAttribute('aria-disabled', 'true') + } + if (value.submenu || value.has_submenu) { + element.classList.add('has_submenu') + element.setAttribute('aria-haspopup', 'true') + element.setAttribute('aria-expanded', 'false') + } + if (value.className) element.className += ' ' + value.className + } + element.value = value + element.setAttribute('role', 'menuitem') + + if (typeof value === 'function') { + element.dataset['value'] = name + element.onclick_callback = value + } else { + element.dataset['value'] = String(value) + } + } + + this.root.appendChild(element) + if (!disabled) element.addEventListener('click', inner_onclick) + if (!disabled && options.autoopen) LiteGraph.pointerListenerAdd(element, 'enter', inner_over) + + const setAriaExpanded = () => { + const entries = this.root.querySelectorAll('div.litemenu-entry.has_submenu') + if (entries) { + for (let i = 0; i < entries.length; i++) { + entries[i].setAttribute('aria-expanded', 'false') + } + } + element.setAttribute('aria-expanded', 'true') + } + + function inner_over(this: ContextMenuDivElement, e: MouseEvent) { + const value = this.value + if (!value || !(value as IContextMenuValue).has_submenu) return + + //if it is a submenu, autoopen like the item was clicked + inner_onclick.call(this, e) + setAriaExpanded() + } + + //menu option clicked + const that = this + function inner_onclick(this: ContextMenuDivElement, e: MouseEvent) { + const value = this.value + let close_parent = true + + that.current_submenu?.close(e) + if ((value as IContextMenuValue)?.has_submenu || (value as IContextMenuValue)?.submenu) + setAriaExpanded() + + //global callback + if (options.callback) { + const r = options.callback.call(this, value, options, e, that, options.node) + if (r === true) close_parent = false + } + + //special cases + if (typeof value === 'object') { + if (value.callback && !options.ignore_item_callbacks && value.disabled !== true) { + //item callback + const 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' + + 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 + } + + close(e?: MouseEvent, ignore_parent_menu?: boolean): void { + 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) + } + } + this.current_submenu?.close(e, true) + + if (this.root.closing_timer) { + clearTimeout(this.root.closing_timer) + } + } + + //this code is used to trigger events easily (used in the context menu mouseleave + static trigger( + element: HTMLDivElement, + event_name: string, + params: MouseEvent, + origin?: unknown, + ): CustomEvent { + const evt = document.createEvent('CustomEvent') + evt.initCustomEvent(event_name, true, true, params) //canBubble, cancelable, detail + // @ts-expect-error + evt.srcElement = origin + if (element.dispatchEvent) element.dispatchEvent(evt) + // @ts-expect-error + else if (element.__events) element.__events.dispatchEvent(evt) + //else nothing seems binded here so nothing to do + return evt + } + + //returns the top most menu + getTopMenu(): ContextMenu { + return this.options.parentMenu ? this.options.parentMenu.getTopMenu() : this + } + + getFirstEvent(): MouseEvent { + return this.options.parentMenu ? this.options.parentMenu.getFirstEvent() : this.options.event + } + + static isCursorOverElement(event: MouseEvent, element: HTMLDivElement): boolean { + const left = event.clientX + const top = event.clientY + const 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 + } } diff --git a/src/CurveEditor.ts b/src/CurveEditor.ts index fb252d88a..302d5e2a3 100644 --- a/src/CurveEditor.ts +++ b/src/CurveEditor.ts @@ -1,173 +1,178 @@ -import type { Point, Rect } from "./interfaces" -import { clamp, LGraphCanvas } from "./litegraph" -import { distance } from "./measure" +import type { Point, Rect } from './interfaces' +import { clamp, LGraphCanvas } from './litegraph' +import { distance } from './measure' //used by some widgets to render a curve editor export class CurveEditor { - points: Point[] - selected: number - nearest: number - size: Rect - must_update: boolean - margin: number - _nearest: number + points: Point[] + selected: number + nearest: number + size: Rect + must_update: boolean + margin: number + _nearest: number - constructor(points: Point[]) { - this.points = points - this.selected = -1 - this.nearest = -1 - this.size = null //stores last size used - this.must_update = true - this.margin = 5 + constructor(points: Point[]) { + this.points = points + this.selected = -1 + this.nearest = -1 + this.size = null //stores last size used + this.must_update = true + this.margin = 5 + } + + static sampleCurve(f: number, points: Point[]): number { + if (!points) return + for (let i = 0; i < points.length - 1; ++i) { + const p = points[i] + const pn = points[i + 1] + if (pn[0] < f) continue + const r = pn[0] - p[0] + if (Math.abs(r) < 0.00001) return p[1] + const local_f = (f - p[0]) / r + return p[1] * (1.0 - local_f) + pn[1] * local_f } + return 0 + } - static sampleCurve(f: number, points: Point[]): number { - if (!points) - return - for (let i = 0; i < points.length - 1; ++i) { - const p = points[i] - const pn = points[i + 1] - if (pn[0] < f) - continue - const r = (pn[0] - p[0]) - if (Math.abs(r) < 0.00001) - return p[1] - const local_f = (f - p[0]) / r - return p[1] * (1.0 - local_f) + pn[1] * local_f - } - return 0 + draw( + ctx: CanvasRenderingContext2D, + size: Rect, + graphcanvas?: LGraphCanvas, + background_color?: string, + line_color?: string, + inactive = false, + ): void { + const points = this.points + if (!points) return + this.size = size + const w = size[0] - this.margin * 2 + const 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) } - - draw(ctx: CanvasRenderingContext2D, size: Rect, graphcanvas?: LGraphCanvas, background_color?: string, line_color?: string, inactive = false): void { - const points = this.points - if (!points) - return - this.size = size - const w = size[0] - this.margin * 2 - const 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.strokeStyle = line_color + if (inactive) ctx.globalAlpha = 0.5 + ctx.beginPath() + for (let i = 0; i < points.length; ++i) { + const p = points[i] + ctx.lineTo(p[0] * w, (1.0 - p[1]) * h) + } + ctx.stroke() + ctx.globalAlpha = 1 + if (!inactive) + for (let i = 0; i < points.length; ++i) { + const p = points[i] + ctx.fillStyle = this.selected == i ? '#FFF' : this.nearest == i ? '#DDD' : '#AAA' ctx.beginPath() - for (let i = 0; i < points.length; ++i) { - const p = points[i] - ctx.lineTo(p[0] * w, (1.0 - p[1]) * h) - } - ctx.stroke() - ctx.globalAlpha = 1 - if (!inactive) - for (let i = 0; i < points.length; ++i) { - const 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() + 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 + onMouseDown(localpos: Point, graphcanvas: LGraphCanvas): boolean { + const points = this.points + if (!points) return + if (localpos[1] < 0) return + + //this.captureInput(true); + const w = this.size[0] - this.margin * 2 + const h = this.size[1] - this.margin * 2 + const x = localpos[0] - this.margin + const y = localpos[1] - this.margin + const pos: Point = [x, y] + const max_dist = 30 / graphcanvas.ds.scale + //search closer one + this.selected = this.getCloserPoint(pos, max_dist) + //create one + if (this.selected == -1) { + const point: 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 + } - //localpos is mouse in curve editor space - onMouseDown(localpos: Point, graphcanvas: LGraphCanvas): boolean { - const points = this.points - if (!points) - return - if (localpos[1] < 0) - return - - //this.captureInput(true); - const w = this.size[0] - this.margin * 2 - const h = this.size[1] - this.margin * 2 - const x = localpos[0] - this.margin - const y = localpos[1] - this.margin - const pos: Point = [x, y] - const max_dist = 30 / graphcanvas.ds.scale - //search closer one - this.selected = this.getCloserPoint(pos, max_dist) - //create one - if (this.selected == -1) { - const point: 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 - } - - onMouseMove(localpos: Point, graphcanvas: LGraphCanvas): void { - const points = this.points - if (!points) - return - const s = this.selected - if (s < 0) - return - const x = (localpos[0] - this.margin) / (this.size[0] - this.margin * 2) - const y = (localpos[1] - this.margin) / (this.size[1] - this.margin * 2) - const curvepos: Point = [(localpos[0] - this.margin), (localpos[1] - this.margin)] - const max_dist = 30 / graphcanvas.ds.scale - this._nearest = this.getCloserPoint(curvepos, max_dist) - const point = points[s] - if (point) { - const 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 - } - } - - // Former params: localpos, graphcanvas - onMouseUp(): boolean { + onMouseMove(localpos: Point, graphcanvas: LGraphCanvas): void { + const points = this.points + if (!points) return + const s = this.selected + if (s < 0) return + const x = (localpos[0] - this.margin) / (this.size[0] - this.margin * 2) + const y = (localpos[1] - this.margin) / (this.size[1] - this.margin * 2) + const curvepos: Point = [localpos[0] - this.margin, localpos[1] - this.margin] + const max_dist = 30 / graphcanvas.ds.scale + this._nearest = this.getCloserPoint(curvepos, max_dist) + const point = points[s] + if (point) { + const 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 false + 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 } + } - getCloserPoint(pos: Point, max_dist: number): number { - const points = this.points - if (!points) - return -1 - max_dist = max_dist || 30 - const w = (this.size[0] - this.margin * 2) - const h = (this.size[1] - this.margin * 2) - const num = points.length - const p2: Point = [0, 0] - let min_dist = 1000000 - let closest = -1 - for (let i = 0; i < num; ++i) { - const p = points[i] - p2[0] = p[0] * w - p2[1] = (1.0 - p[1]) * h - const dist = distance(pos, p2) - if (dist > min_dist || dist > max_dist) - continue - closest = i - min_dist = dist - } - return closest + // Former params: localpos, graphcanvas + onMouseUp(): boolean { + this.selected = -1 + return false + } + + getCloserPoint(pos: Point, max_dist: number): number { + const points = this.points + if (!points) return -1 + max_dist = max_dist || 30 + const w = this.size[0] - this.margin * 2 + const h = this.size[1] - this.margin * 2 + const num = points.length + const p2: Point = [0, 0] + let min_dist = 1000000 + let closest = -1 + for (let i = 0; i < num; ++i) { + const p = points[i] + p2[0] = p[0] * w + p2[1] = (1.0 - p[1]) * h + const dist = distance(pos, p2) + if (dist > min_dist || dist > max_dist) continue + closest = i + min_dist = dist } + return closest + } } diff --git a/src/DragAndScale.ts b/src/DragAndScale.ts index ff8f8c97a..3733f9aeb 100644 --- a/src/DragAndScale.ts +++ b/src/DragAndScale.ts @@ -1,228 +1,221 @@ -import type { Point, Rect, Rect32 } from "./interfaces" -import type { CanvasMouseEvent } from "./types/events" -import { LiteGraph } from "./litegraph" +import type { Point, Rect, Rect32 } from './interfaces' +import type { CanvasMouseEvent } from './types/events' +import { LiteGraph } from './litegraph' export class DragAndScale { - /** Maximum scale (zoom in) */ - max_scale: number - /** Minimum scale (zoom out) */ - min_scale: number - offset: Point - scale: number - enabled: boolean - last_mouse: Point - element?: HTMLCanvasElement - visible_area: Rect32 - _binded_mouse_callback - dragging?: boolean - viewport?: Rect + /** Maximum scale (zoom in) */ + max_scale: number + /** Minimum scale (zoom out) */ + min_scale: number + offset: Point + scale: number + enabled: boolean + last_mouse: Point + element?: HTMLCanvasElement + visible_area: Rect32 + _binded_mouse_callback + dragging?: boolean + viewport?: Rect - onredraw?(das: DragAndScale): void - /** @deprecated */ - onmouse?(e: unknown): boolean + onredraw?(das: DragAndScale): void + /** @deprecated */ + onmouse?(e: unknown): boolean - constructor(element?: HTMLCanvasElement, skip_events?: boolean) { - 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) + constructor(element?: HTMLCanvasElement, skip_events?: boolean) { + 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) - } + if (element) { + this.element = element + if (!skip_events) { + this.bindEvents(element) + } + } + } + + /** @deprecated Has not been kept up to date */ + bindEvents(element: Node): void { + 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) + } + + computeVisibleArea(viewport: Rect): void { + if (!this.element) { + this.visible_area[0] = this.visible_area[1] = this.visible_area[2] = this.visible_area[3] = 0 + return + } + let width = this.element.width + let height = this.element.height + let startx = -this.offset[0] + let starty = -this.offset[1] + if (viewport) { + startx += viewport[0] / this.scale + starty += viewport[1] / this.scale + width = viewport[2] + height = viewport[3] + } + const endx = startx + width / this.scale + const 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 + } + + /** @deprecated Has not been kept up to date */ + onMouse(e: CanvasMouseEvent) { + if (!this.enabled) { + return + } + + const canvas = this.element + const rect = canvas.getBoundingClientRect() + const x = e.clientX - rect.left + const y = e.clientY - rect.top + // FIXME: "canvasx" / y are not referenced anywhere - wrong case + // @ts-expect-error Incorrect case + e.canvasx = x + // @ts-expect-error Incorrect case + e.canvasy = y + e.dragging = this.dragging + + const 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]) + + let 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) { + const deltax = x - this.last_mouse[0] + const 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') + ) { + // @ts-expect-error Deprecated + e.eventType = 'mousewheel' + // @ts-expect-error Deprecated + if (e.type == 'wheel') e.wheel = -e.deltaY + // @ts-expect-error Deprecated + else e.wheel = e.wheelDeltaY != null ? e.wheelDeltaY : e.detail * -60 + + //from stack overflow + // @ts-expect-error Deprecated + e.delta = e.wheelDelta + ? // @ts-expect-error Deprecated + e.wheelDelta / 40 + : e.deltaY + ? -e.deltaY / 3 + : 0 + // @ts-expect-error Deprecated + this.changeDeltaScale(1.0 + e.delta * 0.05) } - /** @deprecated Has not been kept up to date */ - bindEvents(element: Node): void { - this.last_mouse = new Float32Array(2) + this.last_mouse[0] = x + this.last_mouse[1] = y - this._binded_mouse_callback = this.onMouse.bind(this) + if (is_inside) { + e.preventDefault() + e.stopPropagation() + return false + } + } - LiteGraph.pointerListenerAdd(element, "down", this._binded_mouse_callback) - LiteGraph.pointerListenerAdd(element, "move", this._binded_mouse_callback) - LiteGraph.pointerListenerAdd(element, "up", this._binded_mouse_callback) + toCanvasContext(ctx: CanvasRenderingContext2D): void { + ctx.scale(this.scale, this.scale) + ctx.translate(this.offset[0], this.offset[1]) + } - element.addEventListener( - "mousewheel", - this._binded_mouse_callback, - false - ) - element.addEventListener("wheel", this._binded_mouse_callback, false) + convertOffsetToCanvas(pos: Point): Point { + return [(pos[0] + this.offset[0]) * this.scale, (pos[1] + this.offset[1]) * this.scale] + } + + convertCanvasToOffset(pos: Point, out?: Point): Point { + out = out || [0, 0] + out[0] = pos[0] / this.scale - this.offset[0] + out[1] = pos[1] / this.scale - this.offset[1] + return out + } + + /** @deprecated Has not been kept up to date */ + mouseDrag(x: number, y: number): void { + this.offset[0] += x / this.scale + this.offset[1] += y / this.scale + + this.onredraw?.(this) + } + + changeScale(value: number, zooming_center?: Point): void { + if (value < this.min_scale) { + value = this.min_scale + } else if (value > this.max_scale) { + value = this.max_scale } - computeVisibleArea(viewport: Rect): void { - if (!this.element) { - this.visible_area[0] = this.visible_area[1] = this.visible_area[2] = this.visible_area[3] = 0 - return - } - let width = this.element.width - let height = this.element.height - let startx = -this.offset[0] - let starty = -this.offset[1] - if (viewport) { - startx += viewport[0] / this.scale - starty += viewport[1] / this.scale - width = viewport[2] - height = viewport[3] - } - const endx = startx + width / this.scale - const 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 - } + if (value == this.scale) return + if (!this.element) return - /** @deprecated Has not been kept up to date */ - onMouse(e: CanvasMouseEvent) { - if (!this.enabled) { - return - } + const rect = this.element.getBoundingClientRect() + if (!rect) return - const canvas = this.element - const rect = canvas.getBoundingClientRect() - const x = e.clientX - rect.left - const y = e.clientY - rect.top - // FIXME: "canvasx" / y are not referenced anywhere - wrong case - // @ts-expect-error Incorrect case - e.canvasx = x - // @ts-expect-error Incorrect case - e.canvasy = y - e.dragging = this.dragging + zooming_center = zooming_center || [rect.width * 0.5, rect.height * 0.5] + const center = this.convertCanvasToOffset(zooming_center) + this.scale = value + if (Math.abs(this.scale - 1) < 0.01) this.scale = 1 - const 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])) + const new_center = this.convertCanvasToOffset(zooming_center) + const delta_offset = [new_center[0] - center[0], new_center[1] - center[1]] - let ignore = false - if (this.onmouse) { - ignore = this.onmouse(e) - } + this.offset[0] += delta_offset[0] + this.offset[1] += delta_offset[1] - 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) { - const deltax = x - this.last_mouse[0] - const 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")) { - // @ts-expect-error Deprecated - e.eventType = "mousewheel" - // @ts-expect-error Deprecated - if (e.type == "wheel") e.wheel = -e.deltaY - // @ts-expect-error Deprecated - else e.wheel = e.wheelDeltaY != null ? e.wheelDeltaY : e.detail * -60 + this.onredraw?.(this) + } - //from stack overflow - // @ts-expect-error Deprecated - e.delta = e.wheelDelta - // @ts-expect-error Deprecated - ? e.wheelDelta / 40 - : e.deltaY - ? -e.deltaY / 3 - : 0 - // @ts-expect-error Deprecated - this.changeDeltaScale(1.0 + e.delta * 0.05) - } + changeDeltaScale(value: number, zooming_center?: Point): void { + this.changeScale(this.scale * value, zooming_center) + } - this.last_mouse[0] = x - this.last_mouse[1] = y - - if (is_inside) { - e.preventDefault() - e.stopPropagation() - return false - } - } - - toCanvasContext(ctx: CanvasRenderingContext2D): void { - ctx.scale(this.scale, this.scale) - ctx.translate(this.offset[0], this.offset[1]) - } - - convertOffsetToCanvas(pos: Point): Point { - return [ - (pos[0] + this.offset[0]) * this.scale, - (pos[1] + this.offset[1]) * this.scale - ] - } - - convertCanvasToOffset(pos: Point, out?: Point): Point { - out = out || [0, 0] - out[0] = pos[0] / this.scale - this.offset[0] - out[1] = pos[1] / this.scale - this.offset[1] - return out - } - - /** @deprecated Has not been kept up to date */ - mouseDrag(x: number, y: number): void { - this.offset[0] += x / this.scale - this.offset[1] += y / this.scale - - this.onredraw?.(this) - } - - changeScale(value: number, zooming_center?: Point): void { - 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 - - const rect = this.element.getBoundingClientRect() - if (!rect) return - - zooming_center = zooming_center || [ - rect.width * 0.5, - rect.height * 0.5 - ] - const center = this.convertCanvasToOffset(zooming_center) - this.scale = value - if (Math.abs(this.scale - 1) < 0.01) this.scale = 1 - - const new_center = this.convertCanvasToOffset(zooming_center) - const 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.onredraw?.(this) - } - - changeDeltaScale(value: number, zooming_center?: Point): void { - this.changeScale(this.scale * value, zooming_center) - } - - reset(): void { - this.scale = 1 - this.offset[0] = 0 - this.offset[1] = 0 - } + reset(): void { + this.scale = 1 + this.offset[0] = 0 + this.offset[1] = 0 + } } diff --git a/src/LGraph.ts b/src/LGraph.ts index 3a7d8f6c2..6a72bb602 100644 --- a/src/LGraph.ts +++ b/src/LGraph.ts @@ -1,20 +1,24 @@ -import type { Dictionary, IContextMenuValue, ISlotType, MethodNames, Point } from "./interfaces" -import type { ISerialisedGraph } from "@/types/serialisation" -import { LGraphEventMode, TitleMode } from "./types/globalEnums" -import { LiteGraph } from "./litegraph" -import { LGraphCanvas } from "./LGraphCanvas" -import { LGraphGroup } from "./LGraphGroup" -import { type NodeId, LGraphNode } from "./LGraphNode" -import { type LinkId, LLink, type SerialisedLLinkArray } from "./LLink" -import { MapProxyHandler } from "./MapProxyHandler" +import type { Dictionary, IContextMenuValue, ISlotType, MethodNames, Point } from './interfaces' +import type { ISerialisedGraph } from '@/types/serialisation' +import { LGraphEventMode, TitleMode } from './types/globalEnums' +import { LiteGraph } from './litegraph' +import { LGraphCanvas } from './LGraphCanvas' +import { LGraphGroup } from './LGraphGroup' +import { type NodeId, LGraphNode } from './LGraphNode' +import { type LinkId, LLink, type SerialisedLLinkArray } from './LLink' +import { MapProxyHandler } from './MapProxyHandler' interface IGraphInput { - name: string - type: string - value?: unknown + name: string + type: string + value?: unknown } -type ParamsArray, K extends MethodNames> = Parameters[1] extends undefined ? Parameters | Parameters[0] : Parameters +type ParamsArray, K extends MethodNames> = Parameters< + T[K] +>[1] extends undefined + ? Parameters | Parameters[0] + : Parameters /** * 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. @@ -29,1306 +33,1298 @@ type ParamsArray, K extends MethodNames> = Paramet */ export class LGraph { - //default supported types - static supported_types = ["number", "string", "boolean"] - static STATUS_STOPPED = 1 - static STATUS_RUNNING = 2 + //default supported types + static supported_types = ['number', 'string', 'boolean'] + static STATUS_STOPPED = 1 + static STATUS_RUNNING = 2 - _version: number - /** The backing store for links. Keys are wrapped in String() */ - _links: Map = new Map() - /** - * Indexed property access is deprecated. - * Backwards compatibility with a Proxy has been added, but will eventually be removed. - * - * Use {@link Map} methods: - * ``` - * const linkId = 123 - * const link = graph.links.get(linkId) - * // Deprecated: const link = graph.links[linkId] - * ``` - */ - links: Map & Record - list_of_graphcanvas: LGraphCanvas[] | null - status: number - last_node_id: number - last_link_id: number - /** The largest ID created by this graph */ - last_reroute_id: number - _nodes: LGraphNode[] - _nodes_by_id: Record - _nodes_in_order: LGraphNode[] - _nodes_executable: LGraphNode[] | null - _groups: LGraphGroup[] - iteration: number - globaltime: number - runningtime: number - fixedtime: number - fixedtime_lapse: number - elapsed_time: number - last_update_time: number - starttime: number - catch_errors: boolean - execution_timer_id: number | null - errors_in_execution: boolean - execution_time: number - _last_trigger_time?: number - filter?: string - _subgraph_node?: LGraphNode - config: { align_to_grid?: any; links_ontop?: any } - vars: Dictionary - nodes_executing: boolean[] - nodes_actioning: (string | boolean)[] - nodes_executedAction: string[] - extra: Record - inputs: Dictionary - outputs: Dictionary - onInputsOutputsChange?(): void - onInputAdded?(name: string, type: string): void - onAfterStep?(): void - onBeforeStep?(): void - onPlayEvent?(): void - onStopEvent?(): void - onAfterExecute?(): void - onExecuteStep?(): void - onNodeAdded?(node: LGraphNode): void - onNodeRemoved?(node: LGraphNode): void - onTrigger?(action: string, param: unknown): void - onInputRenamed?(old_name: string, name: string): void - onInputTypeChanged?(name: string, type: string): void - onInputRemoved?(name: string): void - onOutputAdded?(name: string, type: string): void - onOutputRenamed?(old_name: string, name: string): void - onOutputTypeChanged?(name: string, type: string): void - onOutputRemoved?(name: string): void - onBeforeChange?(graph: LGraph, info?: LGraphNode): void - onAfterChange?(graph: LGraph, info?: LGraphNode): void - onConnectionChange?(node: LGraphNode): void - on_change?(graph: LGraph): void - onSerialize?(data: ISerialisedGraph): void - onConfigure?(data: ISerialisedGraph): void - onGetNodeMenuOptions?(options: IContextMenuValue[], node: LGraphNode): void - onNodeConnectionChange?(nodeSlotType: ISlotType, targetNode: LGraphNode, slotIndex: number, sourceNode?: LGraphNode, sourceSlotIndex?: number): void + _version: number + /** The backing store for links. Keys are wrapped in String() */ + _links: Map = new Map() + /** + * Indexed property access is deprecated. + * Backwards compatibility with a Proxy has been added, but will eventually be removed. + * + * Use {@link Map} methods: + * ``` + * const linkId = 123 + * const link = graph.links.get(linkId) + * // Deprecated: const link = graph.links[linkId] + * ``` + */ + links: Map & Record + list_of_graphcanvas: LGraphCanvas[] | null + status: number + last_node_id: number + last_link_id: number + /** The largest ID created by this graph */ + last_reroute_id: number + _nodes: LGraphNode[] + _nodes_by_id: Record + _nodes_in_order: LGraphNode[] + _nodes_executable: LGraphNode[] | null + _groups: LGraphGroup[] + iteration: number + globaltime: number + runningtime: number + fixedtime: number + fixedtime_lapse: number + elapsed_time: number + last_update_time: number + starttime: number + catch_errors: boolean + execution_timer_id: number | null + errors_in_execution: boolean + execution_time: number + _last_trigger_time?: number + filter?: string + _subgraph_node?: LGraphNode + config: { align_to_grid?: any; links_ontop?: any } + vars: Dictionary + nodes_executing: boolean[] + nodes_actioning: (string | boolean)[] + nodes_executedAction: string[] + extra: Record + inputs: Dictionary + outputs: Dictionary + onInputsOutputsChange?(): void + onInputAdded?(name: string, type: string): void + onAfterStep?(): void + onBeforeStep?(): void + onPlayEvent?(): void + onStopEvent?(): void + onAfterExecute?(): void + onExecuteStep?(): void + onNodeAdded?(node: LGraphNode): void + onNodeRemoved?(node: LGraphNode): void + onNodeUpdated?(node: LGraphNode): void + onTrigger?(action: string, param: unknown): void + onInputRenamed?(old_name: string, name: string): void + onInputTypeChanged?(name: string, type: string): void + onInputRemoved?(name: string): void + onOutputAdded?(name: string, type: string): void + onOutputRenamed?(old_name: string, name: string): void + onOutputTypeChanged?(name: string, type: string): void + onOutputRemoved?(name: string): void + onBeforeChange?(graph: LGraph, info?: LGraphNode): void + onAfterChange?(graph: LGraph, info?: LGraphNode): void + onConnectionChange?(node: LGraphNode): void + on_change?(graph: LGraph): void + onSerialize?(data: ISerialisedGraph): void + onConfigure?(data: ISerialisedGraph): void + onGetNodeMenuOptions?(options: IContextMenuValue[], node: LGraphNode): void + onNodeConnectionChange?( + nodeSlotType: ISlotType, + targetNode: LGraphNode, + slotIndex: number, + sourceNode?: LGraphNode, + sourceSlotIndex?: number, + ): void - private _input_nodes?: LGraphNode[] + private _input_nodes?: LGraphNode[] - constructor(o?: ISerialisedGraph) { - if (LiteGraph.debug) console.log("Graph created") + constructor(o?: ISerialisedGraph) { + if (LiteGraph.debug) console.log('Graph created') - /** @see MapProxyHandler */ - const links = this._links - MapProxyHandler.bindAllMethods(links) - const handler = new MapProxyHandler() - this.links = new Proxy(links, handler) as Map & Record + /** @see MapProxyHandler */ + const links = this._links + MapProxyHandler.bindAllMethods(links) + const handler = new MapProxyHandler() + this.links = new Proxy(links, handler) as Map & Record - this.list_of_graphcanvas = null - this.clear() + this.list_of_graphcanvas = null + this.clear() - if (o) this.configure(o) + if (o) this.configure(o) + } + + // TODO: Remove + //used to know which types of connections support this graph (some graphs do not allow certain types) + getSupportedTypes(): string[] { + // @ts-expect-error + return this.supported_types || LGraph.supported_types + } + /** + * Removes all nodes from this graph + */ + clear(): void { + 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 (let i = 0; i < this._nodes.length; ++i) { + this._nodes[i].onRemoved?.() + } } - // TODO: Remove - //used to know which types of connections support this graph (some graphs do not allow certain types) - getSupportedTypes(): string[] { - // @ts-expect-error - return this.supported_types || LGraph.supported_types + //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 = [] + + //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') + } + + get nodes() { + return this._nodes + } + + get groups() { + return this._groups + } + + /** + * Attach Canvas to this graph + * @param {GraphCanvas} graph_canvas + */ + attachCanvas(graphcanvas: LGraphCanvas): void { + if (graphcanvas.constructor != LGraphCanvas) + throw 'attachCanvas expects a LGraphCanvas instance' + if (graphcanvas.graph != this) graphcanvas.graph?.detachCanvas(graphcanvas) + + graphcanvas.graph = this + + this.list_of_graphcanvas ||= [] + this.list_of_graphcanvas.push(graphcanvas) + } + /** + * Detach Canvas from this graph + * @param {GraphCanvas} graph_canvas + */ + detachCanvas(graphcanvas: LGraphCanvas): void { + if (!this.list_of_graphcanvas) return + + const 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. + * @param {number} interval amount of milliseconds between executions, if 0 then it renders to the monitor refresh rate + */ + start(interval?: number): void { + if (this.status == LGraph.STATUS_RUNNING) return + this.status = LGraph.STATUS_RUNNING + + this.onPlayEvent?.() + this.sendEventToAllNodes('onStart') + + //launch + this.starttime = LiteGraph.getTime() + this.last_update_time = this.starttime + interval ||= 0 + const 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) + that.onBeforeStep?.() + that.runStep(1, !that.catch_errors) + that.onAfterStep?.() + } + this.execution_timer_id = -1 + on_frame() + } else { + //execute every 'interval' ms + + this.execution_timer_id = setInterval(function () { + //execute + that.onBeforeStep?.() + that.runStep(1, !that.catch_errors) + that.onAfterStep?.() + }, interval) } - /** - * Removes all nodes from this graph - */ - clear(): void { - this.stop() - this.status = LGraph.STATUS_STOPPED + } + /** + * Stops the execution loop of the graph + */ + stop(): void { + if (this.status == LGraph.STATUS_STOPPED) return - this.last_node_id = 0 - this.last_link_id = 0 + this.status = LGraph.STATUS_STOPPED - this._version = -1 //used to detect changes + this.onStopEvent?.() - //safe clear - if (this._nodes) { - for (let i = 0; i < this._nodes.length; ++i) { - this._nodes[i].onRemoved?.() - } + 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 + * @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) + */ + runStep(num: number, do_not_catch_errors: boolean, limit?: number): void { + num = num || 1 + + const start = LiteGraph.getTime() + this.globaltime = 0.001 * (start - this.starttime) + + const nodes = this._nodes_executable ? this._nodes_executable : this._nodes + if (!nodes) return + + limit = limit || nodes.length + + if (do_not_catch_errors) { + //iterations + for (let i = 0; i < num; i++) { + for (let j = 0; j < limit; ++j) { + const node = nodes[j] + // FIXME: Looks like copy/paste broken logic - checks for "on", executes "do" + if (node.mode == LGraphEventMode.ALWAYS && node.onExecute) { + //wrap node.onExecute(); + node.doExecute?.() + } } - //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 = [] + this.fixedtime += this.fixedtime_lapse + this.onExecuteStep?.() + } + this.onAfterExecute?.() + } else { + try { //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") - } - - get nodes() { - return this._nodes - } - - get groups() { - return this._groups - } - - /** - * Attach Canvas to this graph - * @param {GraphCanvas} graph_canvas - */ - attachCanvas(graphcanvas: LGraphCanvas): void { - if (graphcanvas.constructor != LGraphCanvas) - throw "attachCanvas expects a LGraphCanvas instance" - if (graphcanvas.graph != this) - graphcanvas.graph?.detachCanvas(graphcanvas) - - graphcanvas.graph = this - - this.list_of_graphcanvas ||= [] - this.list_of_graphcanvas.push(graphcanvas) - } - /** - * Detach Canvas from this graph - * @param {GraphCanvas} graph_canvas - */ - detachCanvas(graphcanvas: LGraphCanvas): void { - if (!this.list_of_graphcanvas) return - - const 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. - * @param {number} interval amount of milliseconds between executions, if 0 then it renders to the monitor refresh rate - */ - start(interval?: number): void { - if (this.status == LGraph.STATUS_RUNNING) return - this.status = LGraph.STATUS_RUNNING - - this.onPlayEvent?.() - this.sendEventToAllNodes("onStart") - - //launch - this.starttime = LiteGraph.getTime() - this.last_update_time = this.starttime - interval ||= 0 - const 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) - that.onBeforeStep?.() - that.runStep(1, !that.catch_errors) - that.onAfterStep?.() - } - this.execution_timer_id = -1 - on_frame() - } else { //execute every 'interval' ms - // @ts-expect-error - this.execution_timer_id = setInterval(function () { - //execute - that.onBeforeStep?.() - that.runStep(1, !that.catch_errors) - that.onAfterStep?.() - }, interval) - } - } - /** - * Stops the execution loop of the graph - */ - stop(): void { - if (this.status == LGraph.STATUS_STOPPED) return - - this.status = LGraph.STATUS_STOPPED - - 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 - * @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) - */ - runStep(num: number, do_not_catch_errors: boolean, limit?: number): void { - num = num || 1 - - const start = LiteGraph.getTime() - this.globaltime = 0.001 * (start - this.starttime) - - const nodes = this._nodes_executable - ? this._nodes_executable - : this._nodes - if (!nodes) return - - limit = limit || nodes.length - - if (do_not_catch_errors) { - //iterations - for (let i = 0; i < num; i++) { - for (let j = 0; j < limit; ++j) { - const node = nodes[j] - // FIXME: Looks like copy/paste broken logic - checks for "on", executes "do" - if (node.mode == LGraphEventMode.ALWAYS && node.onExecute) { - //wrap node.onExecute(); - node.doExecute?.() - } - } - - this.fixedtime += this.fixedtime_lapse - this.onExecuteStep?.() - } - - this.onAfterExecute?.() - } else { - try { - //iterations - for (let i = 0; i < num; i++) { - for (let j = 0; j < limit; ++j) { - const node = nodes[j] - if (node.mode == LGraphEventMode.ALWAYS) { - node.onExecute?.() - } - } - - this.fixedtime += this.fixedtime_lapse - this.onExecuteStep?.() - } - - 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() - } - } - - const now = LiteGraph.getTime() - let 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. - */ - updateExecutionOrder(): void { - this._nodes_in_order = this.computeExecutionOrder(false) - this._nodes_executable = [] - for (let 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 - computeExecutionOrder(only_onExecute: boolean, set_level?: boolean): LGraphNode[] { - const L: LGraphNode[] = [] - const S: LGraphNode[] = [] - const M: Dictionary = {} - const visited_links: Record = {} //to avoid repeating links - const remaining_links: Record = {} //to a - - //search for the nodes without inputs (starting nodes) - for (let i = 0, l = this._nodes.length; i < l; ++i) { - const node = this._nodes[i] - if (only_onExecute && !node.onExecute) { - continue - } - - M[node.id] = node //add to pending nodes - - let num = 0 //num of input connections - if (node.inputs) { - for (let j = 0, l2 = node.inputs.length; j < l2; j++) { - if (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) { - //get an starting node - const node = S.shift() - if (node === undefined) break - - L.push(node) //add to ordered list - delete M[node.id] //remove from the pending nodes - - if (!node.outputs) continue - - //for every output - for (let i = 0; i < node.outputs.length; i++) { - const output = node.outputs[i] - //not connected - // TODO: Confirm functionality, clean condition - if (output?.links == null || output.links.length == 0) - continue - - //for every connection - for (let j = 0; j < output.links.length; j++) { - const link_id = output.links[j] - const link = this._links.get(link_id) - if (!link) continue - - //already visited link (ignore it) - if (visited_links[link.id]) continue - - const 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 - } - - //mark as visited - visited_links[link.id] = true - //reduce the number of links remaining - remaining_links[target_node.id] -= 1 - - //if no more links, then add to starters array - if (remaining_links[target_node.id] == 0) S.push(target_node) - } - } - } - - //the remaining ones (loops) - for (const i in M) { - L.push(M[i]) - } - - if (L.length != this._nodes.length && LiteGraph.debug) - console.warn("something went wrong, nodes missing") - - const l = L.length - - /** Ensure type is set */ - type OrderedLGraphNode = LGraphNode & { order: number } - - /** Sets the order property of each provided node to its index in {@link nodes}. */ - function setOrder(nodes: LGraphNode[]): asserts nodes is OrderedLGraphNode[] { - const l = nodes.length - for (let i = 0; i < l; ++i) { - nodes[i].order = i - } - } - - //save order number in the node - setOrder(L) - - //sort now by priority - L.sort(function (A, B) { - // @ts-expect-error ctor props - const Ap = A.constructor.priority || A.priority || 0 - // @ts-expect-error ctor props - const Bp = B.constructor.priority || B.priority || 0 - //if same priority, sort by order - - return Ap == Bp - ? A.order - B.order - : Ap - Bp - }) - - //save order number in the node, again... - setOrder(L) - - 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 - * @return {Array} an array with all the LGraphNodes that affect this node, in order of execution - */ - getAncestors(node: LGraphNode): LGraphNode[] { - const ancestors: LGraphNode[] = [] - const pending = [node] - const visited: Dictionary = {} - - while (pending.length) { - const current = pending.shift() - if (!current?.inputs) continue - - if (!visited[current.id] && current != node) { - visited[current.id] = true - ancestors.push(current) - } - - for (let i = 0; i < current.inputs.length; ++i) { - const 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 - */ - arrange(margin?: number, layout?: string): void { - margin = margin || 100 - - const nodes = this.computeExecutionOrder(false, true) - const columns: LGraphNode[][] = [] - for (let i = 0; i < nodes.length; ++i) { - const node = nodes[i] - const col = node._level || 1 - 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 - * @return {number} number of milliseconds the graph has been running - */ - getTime(): number { - 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 - * @return {number} number of milliseconds the graph has been running - */ - getFixedTime(): number { - 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 - * @return {number} number of milliseconds it took the last cycle - */ - getElapsedTime(): number { - return this.elapsed_time - } - /** - * Sends an event to all the nodes, useful to trigger stuff - * @param {String} eventname the name of the event (function to be called) - * @param {Array} params parameters in array format - */ - sendEventToAllNodes(eventname: string, params?: object | object[], mode?: LGraphEventMode): void { - mode = mode || LGraphEventMode.ALWAYS - - const nodes = this._nodes_in_order ? this._nodes_in_order : this._nodes - if (!nodes) return - - for (let j = 0, l = nodes.length; j < l; ++j) { + for (let i = 0; i < num; i++) { + for (let j = 0; j < limit; ++j) { const node = nodes[j] - - // @ts-expect-error - if (node.constructor === LiteGraph.Subgraph && eventname != "onExecute") { - if (node.mode == mode) { - // @ts-expect-error Subgraph - not currently in use - node.sendEventToAllNodes(eventname, params, mode) - } - continue + if (node.mode == LGraphEventMode.ALWAYS) { + node.onExecute?.() } + } - 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) - } - } - } - sendActionToCanvas>(action: T, params?: ParamsArray): void { - if (!this.list_of_graphcanvas) return - - for (let i = 0; i < this.list_of_graphcanvas.length; ++i) { - const c = this.list_of_graphcanvas[i] - c[action]?.apply(c, params) - } - } - /** - * Adds a new node instance to this graph - * @param {LGraphNode} node the instance of the node - */ - add(node: LGraphNode | LGraphGroup, skip_compute_order?: boolean): LGraphNode | null | undefined { - if (!node) return - - // LEGACY: This was changed from constructor === LGraphGroup - //groups - if (node instanceof LGraphGroup) { - this._groups.push(node) - this.setDirtyCanvas(true) - this.change() - node.graph = this - this._version++ - return + this.fixedtime += this.fixedtime_lapse + this.onExecuteStep?.() } - //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" - ) - node.id = LiteGraph.use_uuids - ? LiteGraph.uuidv4() - : ++this.last_node_id + 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() + } + } + + const now = LiteGraph.getTime() + let 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. + */ + updateExecutionOrder(): void { + this._nodes_in_order = this.computeExecutionOrder(false) + this._nodes_executable = [] + for (let 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 + computeExecutionOrder(only_onExecute: boolean, set_level?: boolean): LGraphNode[] { + const L: LGraphNode[] = [] + const S: LGraphNode[] = [] + const M: Dictionary = {} + const visited_links: Record = {} //to avoid repeating links + const remaining_links: Record = {} //to a + + //search for the nodes without inputs (starting nodes) + for (let i = 0, l = this._nodes.length; i < l; ++i) { + const node = this._nodes[i] + if (only_onExecute && !node.onExecute) { + continue + } + + M[node.id] = node //add to pending nodes + + let num = 0 //num of input connections + if (node.inputs) { + for (let j = 0, l2 = node.inputs.length; j < l2; j++) { + if (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) { + //get an starting node + const node = S.shift() + if (node === undefined) break + + L.push(node) //add to ordered list + delete M[node.id] //remove from the pending nodes + + if (!node.outputs) continue + + //for every output + for (let i = 0; i < node.outputs.length; i++) { + const output = node.outputs[i] + //not connected + // TODO: Confirm functionality, clean condition + if (output?.links == null || output.links.length == 0) continue + + //for every connection + for (let j = 0; j < output.links.length; j++) { + const link_id = output.links[j] + const link = this._links.get(link_id) + if (!link) continue + + //already visited link (ignore it) + if (visited_links[link.id]) continue + + const 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 + } + + //mark as visited + visited_links[link.id] = true + //reduce the number of links remaining + remaining_links[target_node.id] -= 1 + + //if no more links, then add to starters array + if (remaining_links[target_node.id] == 0) S.push(target_node) + } + } + } + + //the remaining ones (loops) + for (const i in M) { + L.push(M[i]) + } + + if (L.length != this._nodes.length && LiteGraph.debug) + console.warn('something went wrong, nodes missing') + + const l = L.length + + /** Ensure type is set */ + type OrderedLGraphNode = LGraphNode & { order: number } + + /** Sets the order property of each provided node to its index in {@link nodes}. */ + function setOrder(nodes: LGraphNode[]): asserts nodes is OrderedLGraphNode[] { + const l = nodes.length + for (let i = 0; i < l; ++i) { + nodes[i].order = i + } + } + + //save order number in the node + setOrder(L) + + //sort now by priority + L.sort(function (A, B) { + // @ts-expect-error ctor props + const Ap = A.constructor.priority || A.priority || 0 + // @ts-expect-error ctor props + const Bp = B.constructor.priority || B.priority || 0 + //if same priority, sort by order + + return Ap == Bp ? A.order - B.order : Ap - Bp + }) + + //save order number in the node, again... + setOrder(L) + + 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 + * @return {Array} an array with all the LGraphNodes that affect this node, in order of execution + */ + getAncestors(node: LGraphNode): LGraphNode[] { + const ancestors: LGraphNode[] = [] + const pending = [node] + const visited: Dictionary = {} + + while (pending.length) { + const current = pending.shift() + if (!current?.inputs) continue + + if (!visited[current.id] && current != node) { + visited[current.id] = true + ancestors.push(current) + } + + for (let i = 0; i < current.inputs.length; ++i) { + const 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 + */ + arrange(margin?: number, layout?: string): void { + margin = margin || 100 + + const nodes = this.computeExecutionOrder(false, true) + const columns: LGraphNode[][] = [] + for (let i = 0; i < nodes.length; ++i) { + const node = nodes[i] + const col = node._level || 1 + 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 + * @return {number} number of milliseconds the graph has been running + */ + getTime(): number { + 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 + * @return {number} number of milliseconds the graph has been running + */ + getFixedTime(): number { + 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 + * @return {number} number of milliseconds it took the last cycle + */ + getElapsedTime(): number { + return this.elapsed_time + } + /** + * Sends an event to all the nodes, useful to trigger stuff + * @param {String} eventname the name of the event (function to be called) + * @param {Array} params parameters in array format + */ + sendEventToAllNodes(eventname: string, params?: object | object[], mode?: LGraphEventMode): void { + mode = mode || LGraphEventMode.ALWAYS + + const nodes = this._nodes_in_order ? this._nodes_in_order : this._nodes + if (!nodes) return + + for (let j = 0, l = nodes.length; j < l; ++j) { + const node = nodes[j] + + // @ts-expect-error + if (node.constructor === LiteGraph.Subgraph && eventname != 'onExecute') { + if (node.mode == mode) { + // @ts-expect-error Subgraph - not currently in use + 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) + } + } + } + sendActionToCanvas>( + action: T, + params?: ParamsArray, + ): void { + if (!this.list_of_graphcanvas) return + + for (let i = 0; i < this.list_of_graphcanvas.length; ++i) { + const c = this.list_of_graphcanvas[i] + c[action]?.apply(c, params) + } + } + /** + * Adds a new node instance to this graph + * @param {LGraphNode} node the instance of the node + */ + add(node: LGraphNode | LGraphGroup, skip_compute_order?: boolean): LGraphNode | null | undefined { + if (!node) return + + // LEGACY: This was changed from constructor === LGraphGroup + //groups + if (node instanceof 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') + node.id = LiteGraph.use_uuids ? LiteGraph.uuidv4() : ++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 (typeof node.id === 'number' && 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 + + node.onAdded?.(this) + + if (this.config.align_to_grid) node.alignToGrid() + + if (!skip_compute_order) this.updateExecutionOrder() + + this.onNodeAdded?.(node) + + this.setDirtyCanvas(true) + this.change() + + return node //to chain actions + } + /** + * Removes a node from the graph + * @param {LGraphNode} node the instance of the node + */ + remove(node: LGraphNode | LGraphGroup): void { + // LEGACY: This was changed from constructor === LiteGraph.LGraphGroup + if (node instanceof LGraphGroup) { + const 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 + } + + //not found + if (this._nodes_by_id[node.id] == null) return + //cannot be removed + if (node.ignore_remove) return + + this.beforeChange() //sure? - almost sure is wrong + + //disconnect inputs + if (node.inputs) { + for (let i = 0; i < node.inputs.length; i++) { + const slot = node.inputs[i] + if (slot.link != null) node.disconnectInput(i) + } + } + + //disconnect outputs + if (node.outputs) { + for (let i = 0; i < node.outputs.length; i++) { + const slot = node.outputs[i] + if (slot.links?.length) node.disconnectOutput(i) + } + } + + //callback + node.onRemoved?.() + + node.graph = null + this._version++ + + //remove from canvas render + if (this.list_of_graphcanvas) { + for (let i = 0; i < this.list_of_graphcanvas.length; ++i) { + const 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 + const pos = this._nodes.indexOf(node) + if (pos != -1) this._nodes.splice(pos, 1) + + delete this._nodes_by_id[node.id] + + 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. + * @param {Number} id + */ + getNodeById(id: NodeId): LGraphNode | null { + return id != null ? this._nodes_by_id[id] : null + } + /** + * Returns a list of nodes that matches a class + * @param {Class} classObject the class itself (not an string) + * @return {Array} a list with all the nodes of this type + */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + findNodesByClass(classObject: Function, result?: LGraphNode[]): LGraphNode[] { + result = result || [] + result.length = 0 + for (let 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 + * @param {String} type the name of the node type + * @return {Array} a list with all the nodes of this type + */ + findNodesByType(type: string, result: LGraphNode[]): LGraphNode[] { + const matchType = type.toLowerCase() + result = result || [] + result.length = 0 + for (let i = 0, l = this._nodes.length; i < l; ++i) { + if (this._nodes[i].type?.toLowerCase() == matchType) result.push(this._nodes[i]) + } + return result + } + /** + * Returns the first node that matches a name in its title + * @param {String} name the name of the node to search + * @return {Node} the node or null + */ + findNodeByTitle(title: string): LGraphNode | null { + for (let 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 + * @param {String} name the name of the node to search + * @return {Array} a list with all the nodes with this name + */ + findNodesByTitle(title: string): LGraphNode[] { + const result: LGraphNode[] = [] + for (let 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 + * @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 + */ + getNodeOnPos( + x: number, + y: number, + nodes_list?: LGraphNode[], + margin?: number, + ): LGraphNode | null { + nodes_list = nodes_list || this._nodes + const nRet = null + for (let i = nodes_list.length - 1; i >= 0; i--) { + const n = nodes_list[i] + const skip_title = n.constructor.title_mode == TitleMode.NO_TITLE + if (n.isPointInside(x, y, margin, skip_title)) return n + } + return nRet + } + /** + * 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, { margin = 2 } = {}): LGraphGroup | undefined { + return this._groups.toReversed().find((g) => g.isPointInside(x, y, margin, true)) + } + + /** + * 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 + */ + checkNodeTypes() { + for (let i = 0; i < this._nodes.length; i++) { + const node = this._nodes[i] + const ctor = LiteGraph.registered_node_types[node.type] + if (node.constructor == ctor) continue + + console.log('node being replaced by newer version: ' + node.type) + const newnode = LiteGraph.createNode(node.type) + 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 ***************** + onAction(action: string, param: unknown, options: { action_call?: string }): void { + this._input_nodes = this.findNodesByClass( + // @ts-expect-error Never impl. + LiteGraph.GraphInput, + this._input_nodes, + ) + for (let i = 0; i < this._input_nodes.length; ++i) { + const node = this._input_nodes[i] + if (node.properties.name != action) continue + + //wrap node.onAction(action, param); + node.actionDo(action, param, options) + break + } + } + trigger(action: string, param: unknown) { + this.onTrigger?.(action, param) + } + /** + * Tell this graph it has a global graph input of this type + * @param {String} name + * @param {String} type + * @param {*} value [optional] + */ + addInput(name: string, type: string, value?: unknown): void { + const input = this.inputs[name] + //already exist + if (input) return + + this.beforeChange() + this.inputs[name] = { name: name, type: type, value: value } + this._version++ + this.afterChange() + + this.onInputAdded?.(name, type) + this.onInputsOutputsChange?.() + } + /** + * Assign a data to the global graph input + * @param {String} name + * @param {*} data + */ + setInputData(name: string, data: unknown): void { + const input = this.inputs[name] + if (!input) return + input.value = data + } + /** + * Returns the current value of a global graph input + * @param {String} name + * @return {*} the data + */ + getInputData(name: string): unknown { + const input = this.inputs[name] + return input ? input.value : null + } + /** + * Changes the name of a global graph input + * @param {String} old_name + * @param {String} new_name + */ + renameInput(old_name: string, name: string): boolean | undefined { + 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++ + + this.onInputRenamed?.(old_name, name) + this.onInputsOutputsChange?.() + } + /** + * Changes the type of a global graph input + * @param {String} name + * @param {String} type + */ + changeInputType(name: string, type: string): boolean | undefined { + 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++ + this.onInputTypeChanged?.(name, type) + } + /** + * Removes a global graph input + * @param {String} name + * @param {String} type + */ + removeInput(name: string): boolean { + if (!this.inputs[name]) return false + + delete this.inputs[name] + this._version++ + + this.onInputRemoved?.(name) + this.onInputsOutputsChange?.() + return true + } + /** + * Creates a global graph output + * @param {String} name + * @param {String} type + * @param {*} value + */ + addOutput(name: string, type: string, value: unknown): void { + this.outputs[name] = { name: name, type: type, value: value } + this._version++ + + this.onOutputAdded?.(name, type) + + this.onInputsOutputsChange?.() + } + /** + * Assign a data to the global output + * @param {String} name + * @param {String} value + */ + setOutputData(name: string, value: unknown): void { + const output = this.outputs[name] + if (!output) return + output.value = value + } + /** + * Returns the current value of a global graph output + * @param {String} name + * @return {*} the data + */ + getOutputData(name: string): unknown { + const output = this.outputs[name] + if (!output) return null + return output.value + } + /** + * Renames a global graph output + * @param {String} old_name + * @param {String} new_name + */ + renameOutput(old_name: string, name: string): boolean | undefined { + 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++ + + this.onOutputRenamed?.(old_name, name) + + this.onInputsOutputsChange?.() + } + /** + * Changes the type of a global graph output + * @param {String} name + * @param {String} type + */ + changeOutputType(name: string, type: string): boolean | undefined { + 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++ + this.onOutputTypeChanged?.(name, type) + } + /** + * Removes a global graph output + * @param {String} name + */ + removeOutput(name: string): boolean { + if (!this.outputs[name]) return false + + delete this.outputs[name] + this._version++ + + this.onOutputRemoved?.(name) + + this.onInputsOutputsChange?.() + return true + } + /** @todo Clean up - never implemented. */ + triggerInput(name: string, value: any): void { + const nodes = this.findNodesByTitle(name) + for (let i = 0; i < nodes.length; ++i) { + // @ts-expect-error + nodes[i].onTrigger(value) + } + } + /** @todo Clean up - never implemented. */ + setCallback(name: string, func: any): void { + const nodes = this.findNodesByTitle(name) + for (let i = 0; i < nodes.length; ++i) { + // @ts-expect-error + nodes[i].setTrigger(func) + } + } + //used for undo, called before any change is made to the graph + beforeChange(info?: LGraphNode): void { + this.onBeforeChange?.(this, info) + this.sendActionToCanvas('onBeforeChange', this) + } + //used to resend actions, called after any change is made to the graph + afterChange(info?: LGraphNode): void { + this.onAfterChange?.(this, info) + this.sendActionToCanvas('onAfterChange', this) + } + connectionChange(node: LGraphNode): void { + this.updateExecutionOrder() + this.onConnectionChange?.(node) + this._version++ + // TODO: Interface never implemented - any consumers? + // @ts-expect-error + this.sendActionToCanvas('onConnectionChange') + } + /** + * returns if the graph is in live mode + */ + isLive(): boolean { + if (!this.list_of_graphcanvas) return false + + for (let i = 0; i < this.list_of_graphcanvas.length; ++i) { + const 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) + */ + clearTriggeredSlots(): void { + for (const link_info of this._links.values()) { + if (!link_info) continue + + if (link_info._last_time) link_info._last_time = 0 + } + } + /* Called when something visually changed (not the graph!) */ + change(): void { + if (LiteGraph.debug) { + console.log('Graph changed') + } + this.sendActionToCanvas('setDirty', [true, true]) + this.on_change?.(this) + } + setDirtyCanvas(fg: boolean, bg?: boolean): void { + this.sendActionToCanvas('setDirty', [fg, bg]) + } + /** + * Destroys a link + * @param {Number} link_id + */ + removeLink(link_id: LinkId): void { + const link = this._links.get(link_id) + if (!link) return + + const node = this.getNodeById(link.target_id) + node?.disconnectInput(link.target_slot) + } + //save and recover app state *************************************** + /** + * Creates a Object containing all the info about this graph, it can be serialized + * @return {Object} value of the node + */ + serialize(option?: { sortNodes: boolean }): ISerialisedGraph { + const nodes = + !LiteGraph.use_uuids && option?.sortNodes + ? // @ts-expect-error If LiteGraph.use_uuids is false, ids are numbers. + [...this._nodes].sort((a, b) => a.id - b.id) + : this._nodes + const nodes_info = nodes.map((node) => node.serialize()) + + //pack link info into a non-verbose format + const links: SerialisedLLinkArray[] = [] + for (const link of this._links.values()) { + links.push(link.serialize()) + } + + const groups_info = [] + for (let i = 0; i < this._groups.length; ++i) { + groups_info.push(this._groups[i].serialize()) + } + + const data: ISerialisedGraph = { + 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, + } + + this.onSerialize?.(data) + return data + } + /** + * Configure a graph from a JSON string + * @param {String} str configure a graph from a JSON string + * @param {Boolean} returns if there was any error parsing + */ + configure(data: ISerialisedGraph, keep_old?: boolean): boolean | undefined { + // TODO: Finish typing configure() + if (!data) return + + if (!keep_old) this.clear() + + const nodesData = data.nodes + + // LEGACY: This was changed from constructor === Array + //decode links info (they are very verbose) + if (Array.isArray(data.links)) { + this._links.clear() + for (const link_data of data.links) { + const link = LLink.createFromArray(link_data) + this._links.set(link.id, link) + } + } + + //copy all stored fields + for (const i in data) { + //links must be accepted + if (i == 'nodes' || i == 'groups' || i == 'links') continue + this[i] = data[i] + } + + let error = false + + //create nodes + this._nodes = [] + if (nodesData) { + for (let i = 0, l = nodesData.length; i < l; ++i) { + const n_info = nodesData[i] //stored info + let 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(undefined) + node.last_serialization = n_info + node.has_errors = true + error = true + //continue; } - if (this._nodes.length >= LiteGraph.MAX_NUMBER_OF_NODES) { - throw "LiteGraph: max number of nodes in a graph reached" - } + 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 + } - //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 (typeof node.id === "number" && 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 - - node.onAdded?.(this) - - if (this.config.align_to_grid) node.alignToGrid() - - if (!skip_compute_order) this.updateExecutionOrder() - - this.onNodeAdded?.(node) - - this.setDirtyCanvas(true) - this.change() - - return node //to chain actions - } - /** - * Removes a node from the graph - * @param {LGraphNode} node the instance of the node - */ - remove(node: LGraphNode | LGraphGroup): void { - // LEGACY: This was changed from constructor === LiteGraph.LGraphGroup - if (node instanceof LGraphGroup) { - const 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 - } - - //not found - if (this._nodes_by_id[node.id] == null) return - //cannot be removed - if (node.ignore_remove) return - - this.beforeChange() //sure? - almost sure is wrong - - //disconnect inputs - if (node.inputs) { - for (let i = 0; i < node.inputs.length; i++) { - const slot = node.inputs[i] - if (slot.link != null) - node.disconnectInput(i) - } - } - - //disconnect outputs - if (node.outputs) { - for (let i = 0; i < node.outputs.length; i++) { - const slot = node.outputs[i] - if (slot.links?.length) - node.disconnectOutput(i) - } - } - - //callback - node.onRemoved?.() - - node.graph = null - this._version++ - - //remove from canvas render - if (this.list_of_graphcanvas) { - for (let i = 0; i < this.list_of_graphcanvas.length; ++i) { - const 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 - const pos = this._nodes.indexOf(node) - if (pos != -1) this._nodes.splice(pos, 1) - - delete this._nodes_by_id[node.id] - - 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. - * @param {Number} id - */ - getNodeById(id: NodeId): LGraphNode | null { - return id != null - ? this._nodes_by_id[id] - : null - } - /** - * Returns a list of nodes that matches a class - * @param {Class} classObject the class itself (not an string) - * @return {Array} a list with all the nodes of this type - */ - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - findNodesByClass(classObject: Function, result?: LGraphNode[]): LGraphNode[] { - result = result || [] - result.length = 0 - for (let 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 - * @param {String} type the name of the node type - * @return {Array} a list with all the nodes of this type - */ - findNodesByType(type: string, result: LGraphNode[]): LGraphNode[] { - const matchType = type.toLowerCase() - result = result || [] - result.length = 0 - for (let i = 0, l = this._nodes.length; i < l; ++i) { - if (this._nodes[i].type?.toLowerCase() == matchType) - result.push(this._nodes[i]) - } - return result - } - /** - * Returns the first node that matches a name in its title - * @param {String} name the name of the node to search - * @return {Node} the node or null - */ - findNodeByTitle(title: string): LGraphNode | null { - for (let 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 - * @param {String} name the name of the node to search - * @return {Array} a list with all the nodes with this name - */ - findNodesByTitle(title: string): LGraphNode[] { - const result: LGraphNode[] = [] - for (let 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 - * @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 - */ - getNodeOnPos(x: number, y: number, nodes_list?: LGraphNode[], margin?: number): LGraphNode | null { - nodes_list = nodes_list || this._nodes - const nRet = null - for (let i = nodes_list.length - 1; i >= 0; i--) { - const n = nodes_list[i] - const skip_title = n.constructor.title_mode == TitleMode.NO_TITLE - if (n.isPointInside(x, y, margin, skip_title)) - return n - } - return nRet - } - /** - * 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, { margin = 2 } = {}): LGraphGroup | undefined { - return this._groups.toReversed().find(g => g.isPointInside(x, y, margin, true)) + //configure nodes afterwards so they can reach each other + for (let i = 0, l = nodesData.length; i < l; ++i) { + const n_info = nodesData[i] + const node = this.getNodeById(n_info.id) + node?.configure(n_info) + } } - /** - * 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 - */ - checkNodeTypes() { - for (let i = 0; i < this._nodes.length; i++) { - const node = this._nodes[i] - const ctor = LiteGraph.registered_node_types[node.type] - if (node.constructor == ctor) continue - - console.log("node being replaced by newer version: " + node.type) - const newnode = LiteGraph.createNode(node.type) - 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() + //groups + this._groups.length = 0 + if (data.groups) { + for (let i = 0; i < data.groups.length; ++i) { + // TODO: Search/remove these global object refs + const group = new LiteGraph.LGraphGroup() + group.configure(data.groups[i]) + this.add(group) + } } - // ********** GLOBALS ***************** - onAction(action: string, param: unknown, options: { action_call?: string }): void { - this._input_nodes = this.findNodesByClass( - // @ts-expect-error Never impl. - LiteGraph.GraphInput, - this._input_nodes - ) - for (let i = 0; i < this._input_nodes.length; ++i) { - const node = this._input_nodes[i] - if (node.properties.name != action) continue - //wrap node.onAction(action, param); - node.actionDo(action, param, options) - break - } + this.updateExecutionOrder() + + this.extra = data.extra || {} + + this.onConfigure?.(data) + this._version++ + this.setDirtyCanvas(true, true) + return error + } + load(url: string | Blob | URL | File, callback: () => void) { + const that = this + + // LEGACY: This was changed from constructor === File/Blob + //from file + if (url instanceof Blob || url instanceof File) { + const reader = new FileReader() + reader.addEventListener('load', function (event) { + const data = JSON.parse(event.target.result.toString()) + that.configure(data) + callback?.() + }) + + reader.readAsText(url) + return } - trigger(action: string, param: unknown) { - this.onTrigger?.(action, param) + + //is a string, then an URL + const req = new XMLHttpRequest() + req.open('GET', url, true) + req.send(null) + req.onload = function () { + if (req.status !== 200) { + console.error('Error loading graph:', req.status, req.response) + return + } + const data = JSON.parse(req.response) + that.configure(data) + callback?.() } - /** - * Tell this graph it has a global graph input of this type - * @param {String} name - * @param {String} type - * @param {*} value [optional] - */ - addInput(name: string, type: string, value?: unknown): void { - const input = this.inputs[name] - //already exist - if (input) return - - this.beforeChange() - this.inputs[name] = { name: name, type: type, value: value } - this._version++ - this.afterChange() - - this.onInputAdded?.(name, type) - this.onInputsOutputsChange?.() - } - /** - * Assign a data to the global graph input - * @param {String} name - * @param {*} data - */ - setInputData(name: string, data: unknown): void { - const input = this.inputs[name] - if (!input) return - input.value = data - } - /** - * Returns the current value of a global graph input - * @param {String} name - * @return {*} the data - */ - getInputData(name: string): unknown { - const input = this.inputs[name] - return input - ? input.value - : null - } - /** - * Changes the name of a global graph input - * @param {String} old_name - * @param {String} new_name - */ - renameInput(old_name: string, name: string): boolean | undefined { - 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++ - - this.onInputRenamed?.(old_name, name) - this.onInputsOutputsChange?.() - } - /** - * Changes the type of a global graph input - * @param {String} name - * @param {String} type - */ - changeInputType(name: string, type: string): boolean | undefined { - 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++ - this.onInputTypeChanged?.(name, type) - } - /** - * Removes a global graph input - * @param {String} name - * @param {String} type - */ - removeInput(name: string): boolean { - if (!this.inputs[name]) return false - - delete this.inputs[name] - this._version++ - - this.onInputRemoved?.(name) - this.onInputsOutputsChange?.() - return true - } - /** - * Creates a global graph output - * @param {String} name - * @param {String} type - * @param {*} value - */ - addOutput(name: string, type: string, value: unknown): void { - this.outputs[name] = { name: name, type: type, value: value } - this._version++ - - this.onOutputAdded?.(name, type) - - this.onInputsOutputsChange?.() - } - /** - * Assign a data to the global output - * @param {String} name - * @param {String} value - */ - setOutputData(name: string, value: unknown): void { - const output = this.outputs[name] - if (!output) return - output.value = value - } - /** - * Returns the current value of a global graph output - * @param {String} name - * @return {*} the data - */ - getOutputData(name: string): unknown { - const output = this.outputs[name] - if (!output) return null - return output.value - } - /** - * Renames a global graph output - * @param {String} old_name - * @param {String} new_name - */ - renameOutput(old_name: string, name: string): boolean | undefined { - 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++ - - this.onOutputRenamed?.(old_name, name) - - this.onInputsOutputsChange?.() - } - /** - * Changes the type of a global graph output - * @param {String} name - * @param {String} type - */ - changeOutputType(name: string, type: string): boolean | undefined { - 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++ - this.onOutputTypeChanged?.(name, type) - } - /** - * Removes a global graph output - * @param {String} name - */ - removeOutput(name: string): boolean { - if (!this.outputs[name]) return false - - delete this.outputs[name] - this._version++ - - this.onOutputRemoved?.(name) - - this.onInputsOutputsChange?.() - return true - } - /** @todo Clean up - never implemented. */ - triggerInput(name: string, value: any): void { - const nodes = this.findNodesByTitle(name) - for (let i = 0; i < nodes.length; ++i) { - // @ts-expect-error - nodes[i].onTrigger(value) - } - } - /** @todo Clean up - never implemented. */ - setCallback(name: string, func: any): void { - const nodes = this.findNodesByTitle(name) - for (let i = 0; i < nodes.length; ++i) { - // @ts-expect-error - nodes[i].setTrigger(func) - } - } - //used for undo, called before any change is made to the graph - beforeChange(info?: LGraphNode): void { - this.onBeforeChange?.(this, info) - this.sendActionToCanvas("onBeforeChange", this) - } - //used to resend actions, called after any change is made to the graph - afterChange(info?: LGraphNode): void { - this.onAfterChange?.(this, info) - this.sendActionToCanvas("onAfterChange", this) - } - connectionChange(node: LGraphNode): void { - this.updateExecutionOrder() - this.onConnectionChange?.(node) - this._version++ - // TODO: Interface never implemented - any consumers? - // @ts-expect-error - this.sendActionToCanvas("onConnectionChange") - } - /** - * returns if the graph is in live mode - */ - isLive(): boolean { - if (!this.list_of_graphcanvas) return false - - for (let i = 0; i < this.list_of_graphcanvas.length; ++i) { - const 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) - */ - clearTriggeredSlots(): void { - for (const link_info of this._links.values()) { - if (!link_info) continue - - if (link_info._last_time) - link_info._last_time = 0 - } - } - /* Called when something visually changed (not the graph!) */ - change(): void { - if (LiteGraph.debug) { - console.log("Graph changed") - } - this.sendActionToCanvas("setDirty", [true, true]) - this.on_change?.(this) - } - setDirtyCanvas(fg: boolean, bg?: boolean): void { - this.sendActionToCanvas("setDirty", [fg, bg]) - } - /** - * Destroys a link - * @param {Number} link_id - */ - removeLink(link_id: LinkId): void { - const link = this._links.get(link_id) - if (!link) return - - const node = this.getNodeById(link.target_id) - node?.disconnectInput(link.target_slot) - } - //save and recover app state *************************************** - /** - * Creates a Object containing all the info about this graph, it can be serialized - * @return {Object} value of the node - */ - serialize(option?: { sortNodes: boolean }): ISerialisedGraph { - const nodes = !LiteGraph.use_uuids && option?.sortNodes - // @ts-expect-error If LiteGraph.use_uuids is false, ids are numbers. - ? [...this._nodes].sort((a, b) => a.id - b.id) - : this._nodes - const nodes_info = nodes.map(node => node.serialize()) - - //pack link info into a non-verbose format - const links: SerialisedLLinkArray[] = [] - for (const link of this._links.values()) { - links.push(link.serialize()) - } - - const groups_info = [] - for (let i = 0; i < this._groups.length; ++i) { - groups_info.push(this._groups[i].serialize()) - } - - const data: ISerialisedGraph = { - 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 - } - - this.onSerialize?.(data) - return data - } - /** - * Configure a graph from a JSON string - * @param {String} str configure a graph from a JSON string - * @param {Boolean} returns if there was any error parsing - */ - configure(data: ISerialisedGraph, keep_old?: boolean): boolean | undefined { - // TODO: Finish typing configure() - if (!data) return - - if (!keep_old) this.clear() - - const nodesData = data.nodes - - // LEGACY: This was changed from constructor === Array - //decode links info (they are very verbose) - if (Array.isArray(data.links)) { - this._links.clear() - for (const link_data of data.links) { - const link = LLink.createFromArray(link_data) - this._links.set(link.id, link) - } - } - - //copy all stored fields - for (const i in data) { - //links must be accepted - if (i == "nodes" || i == "groups" || i == "links") - continue - this[i] = data[i] - } - - let error = false - - //create nodes - this._nodes = [] - if (nodesData) { - for (let i = 0, l = nodesData.length; i < l; ++i) { - const n_info = nodesData[i] //stored info - let 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(undefined) - 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 (let i = 0, l = nodesData.length; i < l; ++i) { - const n_info = nodesData[i] - const node = this.getNodeById(n_info.id) - node?.configure(n_info) - } - } - - //groups - this._groups.length = 0 - if (data.groups) { - for (let i = 0; i < data.groups.length; ++i) { - // TODO: Search/remove these global object refs - const group = new LiteGraph.LGraphGroup() - group.configure(data.groups[i]) - this.add(group) - } - } - - this.updateExecutionOrder() - - this.extra = data.extra || {} - - this.onConfigure?.(data) - this._version++ - this.setDirtyCanvas(true, true) - return error - } - load(url: string | Blob | URL | File, callback: () => void) { - const that = this - - // LEGACY: This was changed from constructor === File/Blob - //from file - if (url instanceof Blob || url instanceof File) { - const reader = new FileReader() - reader.addEventListener('load', function (event) { - const data = JSON.parse(event.target.result.toString()) - that.configure(data) - callback?.() - }) - - reader.readAsText(url) - return - } - - //is a string, then an URL - const req = new XMLHttpRequest() - req.open("GET", url, true) - req.send(null) - req.onload = function () { - if (req.status !== 200) { - console.error("Error loading graph:", req.status, req.response) - return - } - const data = JSON.parse(req.response) - that.configure(data) - callback?.() - } - req.onerror = function (err) { - console.error("Error loading graph:", err) - } - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - onNodeTrace(node?: LGraphNode, msg?: string) { - //TODO + req.onerror = function (err) { + console.error('Error loading graph:', err) } + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onNodeTrace(node?: LGraphNode, msg?: string) { + //TODO + } } diff --git a/src/LGraphBadge.ts b/src/LGraphBadge.ts index 0d0c3b0a3..c394ccca3 100644 --- a/src/LGraphBadge.ts +++ b/src/LGraphBadge.ts @@ -1,90 +1,82 @@ export enum BadgePosition { - TopLeft = "top-left", - TopRight = "top-right", + TopLeft = 'top-left', + TopRight = 'top-right', } export interface LGraphBadgeOptions { - text: string; - fgColor?: string; - bgColor?: string; - fontSize?: number; - padding?: number; - height?: number; - cornerRadius?: number; + text: string + fgColor?: string + bgColor?: string + fontSize?: number + padding?: number + height?: number + cornerRadius?: number } export class LGraphBadge { - text: string; - fgColor: string; - bgColor: string; - fontSize: number; - padding: number; - height: number; - cornerRadius: number; + text: string + fgColor: string + bgColor: string + fontSize: number + padding: number + height: number + cornerRadius: number constructor({ text, - fgColor = "white", - bgColor = "#0F1F0F", + fgColor = 'white', + bgColor = '#0F1F0F', fontSize = 12, padding = 6, height = 20, cornerRadius = 5, }: LGraphBadgeOptions) { - this.text = text; - this.fgColor = fgColor; - this.bgColor = bgColor; - this.fontSize = fontSize; - this.padding = padding; - this.height = height; - this.cornerRadius = cornerRadius; + this.text = text + this.fgColor = fgColor + this.bgColor = bgColor + this.fontSize = fontSize + this.padding = padding + this.height = height + this.cornerRadius = cornerRadius } get visible() { - return this.text.length > 0; + return this.text.length > 0 } getWidth(ctx: CanvasRenderingContext2D) { - if (!this.visible) return 0; + if (!this.visible) return 0 - ctx.save(); - ctx.font = `${this.fontSize}px sans-serif`; - const textWidth = ctx.measureText(this.text).width; - ctx.restore(); - return textWidth + this.padding * 2; + ctx.save() + ctx.font = `${this.fontSize}px sans-serif` + const textWidth = ctx.measureText(this.text).width + ctx.restore() + return textWidth + this.padding * 2 } - draw( - ctx: CanvasRenderingContext2D, - x: number, - y: number, - ): void { - if (!this.visible) return; + draw(ctx: CanvasRenderingContext2D, x: number, y: number): void { + if (!this.visible) return - ctx.save(); - ctx.font = `${this.fontSize}px sans-serif`; - const badgeWidth = this.getWidth(ctx); - const badgeX = 0; + ctx.save() + ctx.font = `${this.fontSize}px sans-serif` + const badgeWidth = this.getWidth(ctx) + const badgeX = 0 // Draw badge background - ctx.fillStyle = this.bgColor; - ctx.beginPath(); + ctx.fillStyle = this.bgColor + ctx.beginPath() if (ctx.roundRect) { - ctx.roundRect(x + badgeX, y, badgeWidth, this.height, this.cornerRadius); + ctx.roundRect(x + badgeX, y, badgeWidth, this.height, this.cornerRadius) } else { // Fallback for browsers that don't support roundRect - ctx.rect(x + badgeX, y, badgeWidth, this.height); + ctx.rect(x + badgeX, y, badgeWidth, this.height) } - ctx.fill(); + ctx.fill() // Draw badge text - ctx.fillStyle = this.fgColor; - ctx.fillText( - this.text, - x + badgeX + this.padding, - y + this.height - this.padding - ); + ctx.fillStyle = this.fgColor + ctx.fillText(this.text, x + badgeX + this.padding, y + this.height - this.padding) - ctx.restore(); + ctx.restore() } } diff --git a/src/LGraphCanvas.ts b/src/LGraphCanvas.ts index 70d8d36f3..f579063bd 100644 --- a/src/LGraphCanvas.ts +++ b/src/LGraphCanvas.ts @@ -1,100 +1,118 @@ -import type { CanvasColour, Dictionary, Direction, IBoundaryNodes, IContextMenuOptions, INodeSlot, INodeInputSlot, INodeOutputSlot, IOptionalSlotData, Point, Rect, Rect32, Size, IContextMenuValue, ISlotType, ConnectingLink, NullableProperties } from "./interfaces" -import type { IWidget, TWidgetValue } from "./types/widgets" -import type { LGraphNode, NodeId } from "./LGraphNode" -import type { CanvasDragEvent, CanvasMouseEvent, CanvasWheelEvent, CanvasEventDetail, CanvasPointerEvent } from "./types/events" -import type { IClipboardContents } from "./types/serialisation" -import type { LLink } from "./LLink" -import type { LGraph } from "./LGraph" -import type { ContextMenu } from "./ContextMenu" -import { LGraphEventMode, LinkDirection, LinkRenderType, RenderShape, TitleMode } from "./types/globalEnums" -import { LGraphGroup } from "./LGraphGroup" -import { isInsideRectangle, distance, overlapBounding, isPointInRectangle } from "./measure" -import { drawSlot, LabelPosition } from "./draw" -import { DragAndScale } from "./DragAndScale" -import { LinkReleaseContextExtended, LiteGraph, clamp } from "./litegraph" -import { stringOrEmpty, stringOrNull } from "./strings" -import { alignNodes, distributeNodes, getBoundaryNodes } from "./utils/arrange" +import type { + CanvasColour, + Dictionary, + Direction, + IBoundaryNodes, + IContextMenuOptions, + INodeSlot, + INodeInputSlot, + INodeOutputSlot, + IOptionalSlotData, + Point, + Rect, + Rect32, + Size, + IContextMenuValue, + ISlotType, + ConnectingLink, + NullableProperties, +} from './interfaces' +import type { IWidget, TWidgetValue } from './types/widgets' +import type { LGraphNode, NodeId } from './LGraphNode' +import type { CanvasDragEvent, CanvasMouseEvent, CanvasWheelEvent, CanvasEventDetail, CanvasPointerEvent } from './types/events' +import type { IClipboardContents } from './types/serialisation' +import type { LLink } from './LLink' +import type { LGraph } from './LGraph' +import type { ContextMenu } from './ContextMenu' +import { LGraphEventMode, LinkDirection, LinkRenderType, RenderShape, TitleMode } from './types/globalEnums' +import { LGraphGroup } from './LGraphGroup' +import { isInsideRectangle, distance, overlapBounding, isPointInRectangle } from './measure' +import { drawSlot, LabelPosition } from './draw' +import { DragAndScale } from './DragAndScale' +import { LinkReleaseContextExtended, LiteGraph, clamp } from './litegraph' +import { stringOrEmpty, stringOrNull } from './strings' +import { alignNodes, distributeNodes, getBoundaryNodes } from './utils/arrange' interface IShowSearchOptions { - node_to?: LGraphNode - node_from?: LGraphNode - slot_from: number | INodeOutputSlot | INodeInputSlot - type_filter_in?: ISlotType - type_filter_out?: ISlotType | false + node_to?: LGraphNode + node_from?: LGraphNode + slot_from: number | INodeOutputSlot | INodeInputSlot + type_filter_in?: ISlotType + type_filter_out?: ISlotType | false - // TODO check for registered_slot_[in/out]_types not empty // this will be checked for functionality enabled : filter on slot type, in and out - do_type_filter?: boolean - show_general_if_none_on_typefilter?: boolean - show_general_after_typefiltered?: boolean - hide_on_mouse_leave?: boolean - show_all_if_empty?: boolean - show_all_on_open?: boolean + // TODO check for registered_slot_[in/out]_types not empty // this will be checked for functionality enabled : filter on slot type, in and out + do_type_filter?: boolean + show_general_if_none_on_typefilter?: boolean + show_general_after_typefiltered?: boolean + hide_on_mouse_leave?: boolean + show_all_if_empty?: boolean + show_all_on_open?: boolean } interface INodeFromTo { - /** input */ - nodeFrom?: LGraphNode - /** input */ - slotFrom?: number | INodeOutputSlot | INodeInputSlot - /** output */ - nodeTo?: LGraphNode - /** output */ - slotTo?: number | INodeOutputSlot | INodeInputSlot - /** pass the event coords */ + /** input */ + nodeFrom?: LGraphNode + /** input */ + slotFrom?: number | INodeOutputSlot | INodeInputSlot + /** output */ + nodeTo?: LGraphNode + /** output */ + slotTo?: number | INodeOutputSlot | INodeInputSlot + /** pass the event coords */ } interface ICreateNodeOptions extends INodeFromTo { - // FIXME: Should not be optional - /** Position of new node */ - position?: Point + // FIXME: Should not be optional + /** Position of new node */ + position?: Point - // FIXME: Should not be optional - /** choose a nodetype to add, AUTO to set at first good */ - nodeType?: string //nodeNewType - /** adjust x,y */ - posAdd?: Point //-alphaPosY*30] - /** alpha, adjust the position x,y based on the new node size w,h */ - posSizeFix?: Point //-alphaPosY*2*/ - e?: CanvasMouseEvent - allow_searchbox?: boolean - showSearchBox?: LGraphCanvas["showSearchBox"] + // FIXME: Should not be optional + /** choose a nodetype to add, AUTO to set at first good */ + nodeType?: string //nodeNewType + /** adjust x,y */ + posAdd?: Point //-alphaPosY*30] + /** alpha, adjust the position x,y based on the new node size w,h */ + posSizeFix?: Point //-alphaPosY*2*/ + e?: CanvasMouseEvent + allow_searchbox?: boolean + showSearchBox?: LGraphCanvas['showSearchBox'] } interface ICloseableDiv extends HTMLDivElement { - close?(): void + close?(): void } interface IDialog extends ICloseableDiv { - modified?(): void - close?(): void - is_modified?: boolean + modified?(): void + close?(): void + is_modified?: boolean } interface IDialogOptions { - position?: Point - event?: MouseEvent - checkForInput?: boolean - closeOnLeave?: boolean - onclose?(): void + position?: Point + event?: MouseEvent + checkForInput?: boolean + closeOnLeave?: boolean + onclose?(): void } interface IDrawSelectionBoundingOptions { - shape?: RenderShape - title_height?: number - title_mode?: TitleMode - fgcolor?: CanvasColour - padding?: number - collapsed?: boolean + shape?: RenderShape + title_height?: number + title_mode?: TitleMode + fgcolor?: CanvasColour + padding?: number + collapsed?: boolean } /** @inheritdoc {@link LGraphCanvas.state} */ export interface LGraphCanvasState { - /** {@link Positionable} items are being dragged on the canvas. */ - draggingItems: boolean - /** The canvas itself is being dragged. */ - draggingCanvas: boolean - /** The canvas is read-only, preventing changes to nodes, disconnecting links, moving items, etc. */ - readOnly: boolean + /** {@link Positionable} items are being dragged on the canvas. */ + draggingItems: boolean + /** The canvas itself is being dragged. */ + draggingCanvas: boolean + /** The canvas is read-only, preventing changes to nodes, disconnecting links, moving items, etc. */ + readOnly: boolean } /** @@ -106,7765 +124,7263 @@ export interface LGraphCanvasState { * @param {Object} options [optional] { skip_rendering, autoresize, viewport } */ export class LGraphCanvas { + /* Interaction */ + static #temp = new Float32Array(4) + static #temp_vec2 = new Float32Array(2) + static #tmp_area = new Float32Array(4) + static #margin_area = new Float32Array(4) + static #link_bounding = new Float32Array(4) + static #tempA = new Float32Array(2) + static #tempB = new Float32Array(2) - /* Interaction */ - static #temp = new Float32Array(4) - static #temp_vec2 = new Float32Array(2) - static #tmp_area = new Float32Array(4) - static #margin_area = new Float32Array(4) - static #link_bounding = new Float32Array(4) - static #tempA = new Float32Array(2) - static #tempB = new Float32Array(2) + static DEFAULT_BACKGROUND_IMAGE = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=' - static DEFAULT_BACKGROUND_IMAGE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=" + /** Initialised from LiteGraphGlobal static block to avoid circular dependency. */ + static link_type_colors: Record + static gradients: Record = {} //cache of gradients - /** Initialised from LiteGraphGlobal static block to avoid circular dependency. */ - static link_type_colors: Record - static gradients: Record = {} //cache of gradients + static search_limit = -1 + static 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' }, + } - static search_limit = -1 - static 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" } + /** + * The state of this canvas, e.g. whether it is being dragged, or read-only. + * + * Implemented as a POCO that can be proxied without side-effects. + */ + state: LGraphCanvasState = { + draggingItems: false, + draggingCanvas: false, + readOnly: false, + } + + /** @inheritdoc {@link LGraphCanvasState.draggingCanvas} */ + get dragging_canvas(): boolean { + return this.state.draggingCanvas + } + set dragging_canvas(value: boolean) { + this.state.draggingCanvas = value + } + + // Whether the canvas was previously being dragged prior to pressing space key. + // null if space key is not pressed. + private _previously_dragging_canvas: boolean | null = null + + /** @inheritdoc {@link LGraphCanvasState.readOnly} */ + get read_only(): boolean { + return this.state.readOnly + } + set read_only(value: boolean) { + this.state.readOnly = value + } + + get isDragging(): boolean { + return this.state.draggingItems + } + set isDragging(value: boolean) { + this.state.draggingItems = value + } + options: { skip_events?: any; viewport?: any; skip_render?: any; autoresize?: any } + background_image: string + ds: DragAndScale + zoom_modify_alpha: boolean + zoom_speed: number + title_text_font: string + inner_text_font: string + node_title_color: string + default_link_color: string + default_connection_color: { + input_off: string + input_on: string //"#BBD" + output_off: string + output_on: string //"#BBD" + } + default_connection_color_byType: Dictionary + default_connection_color_byTypeOff: Dictionary + highquality_render: boolean + use_gradients: boolean + editor_alpha: number + pause_rendering: boolean + clear_background: boolean + clear_background_color: string + render_only_selected: boolean + live_mode: boolean + show_info: boolean + allow_dragcanvas: boolean + allow_dragnodes: boolean + allow_interaction: boolean + multi_select: boolean + allow_searchbox: boolean + allow_reconnect_links: boolean + align_to_grid: boolean + drag_mode: boolean + dragging_rectangle: Rect | null + filter?: string | null + set_canvas_dirty_on_mouse_event: boolean + always_render_background: boolean + render_shadows: boolean + render_canvas_border: boolean + render_connections_shadows: boolean + render_connections_border: boolean + render_curved_connections: boolean + render_connection_arrows: boolean + render_collapsed_slots: boolean + render_execution_order: boolean + render_title_colored: boolean + render_link_tooltip: boolean + links_render_mode: number + /** mouse in canvas coordinates, where 0,0 is the top-left corner of the blue rectangle */ + mouse: Point + /** mouse in graph coordinates, where 0,0 is the top-left corner of the blue rectangle */ + graph_mouse: Point + /** @deprecated LEGACY: REMOVE THIS, USE {@link graph_mouse} INSTEAD */ + canvas_mouse: Point + /** to personalize the search box */ + onSearchBox?: (helper: Element, str: string, canvas: LGraphCanvas) => any + onSearchBoxSelection?: (name: any, event: any, canvas: LGraphCanvas) => void + onMouse?: (e: CanvasMouseEvent) => boolean + /** to render background objects (behind nodes and connections) in the canvas affected by transform */ + onDrawBackground?: (ctx: CanvasRenderingContext2D, visible_area: any) => void + /** to render foreground objects (above nodes and connections) in the canvas affected by transform */ + onDrawForeground?: (arg0: CanvasRenderingContext2D, arg1: any) => void + connections_width: number + round_radius: number + current_node: LGraphNode | null + /** used for widgets */ + node_widget?: [LGraphNode, IWidget] | null + over_link_center: LLink | null + last_mouse_position: Point + visible_area?: Rect32 + visible_links?: LLink[] + connecting_links: ConnectingLink[] | null + viewport?: Rect + autoresize: boolean + static active_canvas: LGraphCanvas + static onMenuNodeOutputs?(entries: IOptionalSlotData[]): IOptionalSlotData[] + frame = 0 + last_draw_time = 0 + render_time = 0 + fps = 0 + selected_nodes: Dictionary = {} + /** @deprecated Temporary implementation only - will be replaced with `selectedItems: Set`. */ + selectedGroups: Set = new Set() + selected_group: LGraphGroup | null = null + visible_nodes: LGraphNode[] = [] + node_dragged?: LGraphNode + node_over?: LGraphNode + node_capturing_input?: LGraphNode + highlighted_links: Dictionary = {} + link_over_widget?: IWidget + link_over_widget_type?: string + + dirty_canvas: boolean = true + dirty_bgcanvas: boolean = true + /** A map of nodes that require selective-redraw */ + dirty_nodes = new Map() + dirty_area?: Rect + // Unused + node_in_panel?: LGraphNode + last_mouse: Point = [0, 0] + last_mouseclick: number = 0 + pointer_is_down: boolean = false + pointer_is_double: boolean = false + graph!: LGraph + _graph_stack: LGraph[] | null = null + canvas: HTMLCanvasElement + bgcanvas: HTMLCanvasElement + ctx?: CanvasRenderingContext2D + _events_binded?: boolean + _mousedown_callback?(e: CanvasMouseEvent): boolean + _mousewheel_callback?(e: CanvasMouseEvent): boolean + _mousemove_callback?(e: CanvasMouseEvent): boolean + _mouseup_callback?(e: CanvasMouseEvent): boolean + _mouseout_callback?(e: CanvasMouseEvent): boolean + _key_callback?(e: KeyboardEvent): boolean + _ondrop_callback?(e: CanvasDragEvent): unknown + gl?: never + bgctx?: CanvasRenderingContext2D + is_rendering?: boolean + block_click?: boolean + last_click_position?: Point + resizing_node?: LGraphNode + selected_group_resizing?: boolean + last_mouse_dragging: boolean + onMouseDown: (arg0: CanvasMouseEvent) => void + _highlight_pos?: Point + _highlight_input?: INodeInputSlot + // TODO: Check if panels are used + node_panel + options_panel + onDropItem: (e: Event) => any + _bg_img: HTMLImageElement + _pattern?: CanvasPattern + _pattern_img: HTMLImageElement + // TODO: This looks like another panel thing + prompt_box: IDialog + search_box: HTMLDivElement + SELECTED_NODE: LGraphNode + NODEPANEL_IS_OPEN: boolean + getMenuOptions?(): IContextMenuValue[] + getExtraMenuOptions?(canvas: LGraphCanvas, options: IContextMenuValue[]): IContextMenuValue[] + static active_node: LGraphNode + /** called before modifying the graph */ + onBeforeChange?(graph: LGraph): void + /** called after modifying the graph */ + onAfterChange?(graph: LGraph): void + onClear?: () => void + /** called after moving a node */ + onNodeMoved?: (node_dragged: LGraphNode) => void + /** called if the selection changes */ + onSelectionChange?: (selected_nodes: Dictionary) => void + /** called when rendering a tooltip */ + onDrawLinkTooltip?: (ctx: CanvasRenderingContext2D, link: LLink, canvas?: LGraphCanvas) => boolean + /** to render foreground objects not affected by transform (for GUIs) */ + onDrawOverlay?: (ctx: CanvasRenderingContext2D) => void + onRenderBackground?: (canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) => boolean + onNodeDblClicked?: (n: LGraphNode) => void + onShowNodePanel?: (n: LGraphNode) => void + onNodeSelected?: (node: LGraphNode) => void + onNodeDeselected?: (node: LGraphNode) => void + onRender?: (canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) => void + /** Implement this function to allow conversion of widget types to input types, e.g. number -> INT or FLOAT for widget link validation checks */ + getWidgetLinkType?: (widget: IWidget, node: LGraphNode) => string | null | undefined + + /** + * Creates a new instance of LGraphCanvas. + * @param canvas The canvas HTML element (or its id) to use, or null / undefined to leave blank. + * @param graph The graph that owns this canvas. + * @param options + */ + constructor(canvas: HTMLCanvasElement, graph: LGraph, options?: LGraphCanvas['options']) { + options ||= {} + this.options = options + + //if(graph === undefined) + // throw ("No graph assigned"); + this.background_image = LGraphCanvas.DEFAULT_BACKGROUND_IMAGE + + this.ds = new DragAndScale() + this.zoom_modify_alpha = true //otherwise it generates ugly patterns when scaling down too much + this.zoom_speed = 1.1 // in range (1.01, 2.5). Less than 1 will invert the zoom direction + + 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" } - - /** - * The state of this canvas, e.g. whether it is being dragged, or read-only. - * - * Implemented as a POCO that can be proxied without side-effects. - */ - state: LGraphCanvasState = { - draggingItems: false, - draggingCanvas: false, - readOnly: false, - } - - /** @inheritdoc {@link LGraphCanvasState.draggingCanvas} */ - get dragging_canvas(): boolean { - return this.state.draggingCanvas - } - set dragging_canvas(value: boolean) { - this.state.draggingCanvas = value - } - - // Whether the canvas was previously being dragged prior to pressing space key. - // null if space key is not pressed. - private _previously_dragging_canvas: boolean | null = null - - /** @inheritdoc {@link LGraphCanvasState.readOnly} */ - get read_only(): boolean { - return this.state.readOnly - } - set read_only(value: boolean) { - this.state.readOnly = value - } - - get isDragging(): boolean { - return this.state.draggingItems - } - set isDragging(value: boolean) { - this.state.draggingItems = value - } - options: { skip_events?: any; viewport?: any; skip_render?: any; autoresize?: any } - background_image: string - ds: DragAndScale - zoom_modify_alpha: boolean - zoom_speed: number - title_text_font: string - inner_text_font: string - node_title_color: string - default_link_color: string - default_connection_color: { - input_off: string; input_on: string //"#BBD" - output_off: string; output_on: string //"#BBD" - } - default_connection_color_byType: Dictionary - default_connection_color_byTypeOff: Dictionary - highquality_render: boolean - use_gradients: boolean - editor_alpha: number - pause_rendering: boolean - clear_background: boolean - clear_background_color: string - render_only_selected: boolean - live_mode: boolean - show_info: boolean - allow_dragcanvas: boolean - allow_dragnodes: boolean - allow_interaction: boolean - multi_select: boolean - allow_searchbox: boolean - allow_reconnect_links: boolean - align_to_grid: boolean - drag_mode: boolean - dragging_rectangle: Rect | null - filter?: string | null - set_canvas_dirty_on_mouse_event: boolean - always_render_background: boolean - render_shadows: boolean - render_canvas_border: boolean - render_connections_shadows: boolean - render_connections_border: boolean - render_curved_connections: boolean - render_connection_arrows: boolean - render_collapsed_slots: boolean - render_execution_order: boolean - render_title_colored: boolean - render_link_tooltip: boolean - links_render_mode: number - /** mouse in canvas coordinates, where 0,0 is the top-left corner of the blue rectangle */ - mouse: Point - /** mouse in graph coordinates, where 0,0 is the top-left corner of the blue rectangle */ - graph_mouse: Point - /** @deprecated LEGACY: REMOVE THIS, USE {@link graph_mouse} INSTEAD */ - canvas_mouse: Point - /** to personalize the search box */ - onSearchBox?: (helper: Element, str: string, canvas: LGraphCanvas) => any - onSearchBoxSelection?: (name: any, event: any, canvas: LGraphCanvas) => void - onMouse?: (e: CanvasMouseEvent) => boolean - /** to render background objects (behind nodes and connections) in the canvas affected by transform */ - onDrawBackground?: (ctx: CanvasRenderingContext2D, visible_area: any) => void - /** to render foreground objects (above nodes and connections) in the canvas affected by transform */ - onDrawForeground?: (arg0: CanvasRenderingContext2D, arg1: any) => void - connections_width: number - round_radius: number - current_node: LGraphNode | null - /** used for widgets */ - node_widget?: [LGraphNode, IWidget] | null - over_link_center: LLink | null - last_mouse_position: Point - visible_area?: Rect32 - visible_links?: LLink[] - connecting_links: ConnectingLink[] | null - viewport?: Rect - autoresize: boolean - static active_canvas: LGraphCanvas - static onMenuNodeOutputs?(entries: IOptionalSlotData[]): IOptionalSlotData[] - frame = 0 - last_draw_time = 0 - render_time = 0 - fps = 0 - selected_nodes: Dictionary = {} - /** @deprecated Temporary implementation only - will be replaced with `selectedItems: Set`. */ - selectedGroups: Set = new Set() - selected_group: LGraphGroup | null = null - visible_nodes: LGraphNode[] = [] - node_dragged?: LGraphNode - node_over?: LGraphNode - node_capturing_input?: LGraphNode - highlighted_links: Dictionary = {} - link_over_widget?: IWidget - link_over_widget_type?: string - - dirty_canvas: boolean = true - dirty_bgcanvas: boolean = true - /** A map of nodes that require selective-redraw */ - dirty_nodes = new Map() - dirty_area?: Rect - // Unused - node_in_panel?: LGraphNode - last_mouse: Point = [0, 0] - last_mouseclick: number = 0 - pointer_is_down: boolean = false - pointer_is_double: boolean = false - graph!: LGraph - _graph_stack: LGraph[] | null = null - canvas: HTMLCanvasElement - bgcanvas: HTMLCanvasElement - ctx?: CanvasRenderingContext2D - _events_binded?: boolean - _mousedown_callback?(e: CanvasMouseEvent): boolean - _mousewheel_callback?(e: CanvasMouseEvent): boolean - _mousemove_callback?(e: CanvasMouseEvent): boolean - _mouseup_callback?(e: CanvasMouseEvent): boolean - _mouseout_callback?(e: CanvasMouseEvent): boolean - _key_callback?(e: KeyboardEvent): boolean - _ondrop_callback?(e: CanvasDragEvent): unknown - gl?: never - bgctx?: CanvasRenderingContext2D - is_rendering?: boolean - block_click?: boolean - last_click_position?: Point - resizing_node?: LGraphNode - selected_group_resizing?: boolean - last_mouse_dragging: boolean - onMouseDown: (arg0: CanvasMouseEvent) => void - _highlight_pos?: Point - _highlight_input?: INodeInputSlot - // TODO: Check if panels are used - node_panel - options_panel - onDropItem: (e: Event) => any - _bg_img: HTMLImageElement - _pattern?: CanvasPattern - _pattern_img: HTMLImageElement - // TODO: This looks like another panel thing - prompt_box: IDialog - search_box: HTMLDivElement - SELECTED_NODE: LGraphNode - NODEPANEL_IS_OPEN: boolean - getMenuOptions?(): IContextMenuValue[] - getExtraMenuOptions?(canvas: LGraphCanvas, options: IContextMenuValue[]): IContextMenuValue[] - static active_node: LGraphNode - /** called before modifying the graph */ - onBeforeChange?(graph: LGraph): void - /** called after modifying the graph */ - onAfterChange?(graph: LGraph): void - onClear?: () => void - /** called after moving a node */ - onNodeMoved?: (node_dragged: LGraphNode) => void - /** called if the selection changes */ - onSelectionChange?: (selected_nodes: Dictionary) => void - /** called when rendering a tooltip */ - onDrawLinkTooltip?: (ctx: CanvasRenderingContext2D, link: LLink, canvas?: LGraphCanvas) => boolean - /** to render foreground objects not affected by transform (for GUIs) */ - onDrawOverlay?: (ctx: CanvasRenderingContext2D) => void - onRenderBackground?: (canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) => boolean - onNodeDblClicked?: (n: LGraphNode) => void - onShowNodePanel?: (n: LGraphNode) => void - onNodeSelected?: (node: LGraphNode) => void - onNodeDeselected?: (node: LGraphNode) => void - onRender?: (canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) => void - /** Implement this function to allow conversion of widget types to input types, e.g. number -> INT or FLOAT for widget link validation checks */ - getWidgetLinkType?: (widget: IWidget, node: LGraphNode) => string | null | undefined - - /** - * Creates a new instance of LGraphCanvas. - * @param canvas The canvas HTML element (or its id) to use, or null / undefined to leave blank. - * @param graph The graph that owns this canvas. - * @param options - */ - constructor(canvas: HTMLCanvasElement, graph: LGraph, options?: LGraphCanvas["options"]) { - options ||= {} - this.options = options - - //if(graph === undefined) - // throw ("No graph assigned"); - this.background_image = LGraphCanvas.DEFAULT_BACKGROUND_IMAGE - - this.ds = new DragAndScale() - this.zoom_modify_alpha = true //otherwise it generates ugly patterns when scaling down too much - this.zoom_speed = 1.1 // in range (1.01, 2.5). Less than 1 will invert the zoom direction - - 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", + this.default_connection_color_byType = { + /*number: "#7F7", string: "#77F", boolean: "#F77",*/ - } - this.default_connection_color_byTypeOff = { - /*number: "#474", + } + 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.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 on mouse events (except move) - 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 = LinkRenderType.SPLINE_LINK - - this.mouse = [0, 0] - this.graph_mouse = [0, 0] - this.canvas_mouse = this.graph_mouse - - //to personalize the search box - this.onSearchBox = null - this.onSearchBoxSelection = null - - //callbacks - this.onMouse = null - this.onDrawBackground = null - this.onDrawForeground = null - this.onDrawOverlay = null - this.onDrawLinkTooltip = null - this.onNodeMoved = null - this.onSelectionChange = null - // FIXME: Typo, does nothing - //called before any link changes - // @ts-expect-error - this.onConnectingChange = null - this.onBeforeChange = null - this.onAfterChange = null - - this.connections_width = 3 - this.round_radius = 8 - - this.current_node = null - this.node_widget = null - this.over_link_center = null - this.last_mouse_position = [0, 0] - this.visible_area = this.ds.visible_area - this.visible_links = [] - this.connecting_links = null // Explicitly null-checked - - this.viewport = options.viewport || null //to constraint render area to a portion of the canvas - - //link canvas and graph - graph?.attachCanvas(this) - - this.setCanvas(canvas, options.skip_events) - this.clear() - - if (!options.skip_render) { - this.startRendering() - } - - this.autoresize = options.autoresize } - static getFileExtension(url: string): string { - const question = url.indexOf("?") - if (question !== -1) url = url.substring(0, question) + 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' - const point = url.lastIndexOf(".") - return point === -1 - ? "" - : url.substring(point + 1).toLowerCase() + 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 on mouse events (except move) + 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 = LinkRenderType.SPLINE_LINK + + this.mouse = [0, 0] + this.graph_mouse = [0, 0] + this.canvas_mouse = this.graph_mouse + + //to personalize the search box + this.onSearchBox = null + this.onSearchBoxSelection = null + + //callbacks + this.onMouse = null + this.onDrawBackground = null + this.onDrawForeground = null + this.onDrawOverlay = null + this.onDrawLinkTooltip = null + this.onNodeMoved = null + this.onSelectionChange = null + // FIXME: Typo, does nothing + //called before any link changes + // @ts-expect-error + this.onConnectingChange = null + this.onBeforeChange = null + this.onAfterChange = null + + this.connections_width = 3 + this.round_radius = 8 + + this.current_node = null + this.node_widget = null + this.over_link_center = null + this.last_mouse_position = [0, 0] + this.visible_area = this.ds.visible_area + this.visible_links = [] + this.connecting_links = null // Explicitly null-checked + + this.viewport = options.viewport || null //to constraint render area to a portion of the canvas + + //link canvas and graph + graph?.attachCanvas(this) + + this.setCanvas(canvas, options.skip_events) + this.clear() + + if (!options.skip_render) { + this.startRendering() } - static onGroupAdd(info: unknown, entry: unknown, mouse_event: MouseEvent): void { - const canvas = LGraphCanvas.active_canvas + this.autoresize = options.autoresize + } - const group = new LiteGraph.LGraphGroup() - group.pos = canvas.convertEventToCanvasOffset(mouse_event) - canvas.graph.add(group) + static getFileExtension(url: string): string { + const question = url.indexOf('?') + if (question !== -1) url = url.substring(0, question) + + const point = url.lastIndexOf('.') + return point === -1 ? '' : url.substring(point + 1).toLowerCase() + } + + static onGroupAdd(info: unknown, entry: unknown, mouse_event: MouseEvent): void { + const canvas = LGraphCanvas.active_canvas + + const group = new LiteGraph.LGraphGroup() + group.pos = canvas.convertEventToCanvasOffset(mouse_event) + canvas.graph.add(group) + } + /** + * @deprecated Functionality moved to {@link getBoundaryNodes}. The new function returns null on failure, instead of an object with all null properties. + * Determines the furthest nodes in each direction + * @param {Dictionary} nodes the nodes to from which boundary nodes will be extracted + * @return {{left: LGraphNode, top: LGraphNode, right: LGraphNode, bottom: LGraphNode}} + */ + static getBoundaryNodes(nodes: LGraphNode[] | Dictionary): NullableProperties { + const _nodes = Array.isArray(nodes) ? nodes : Object.values(nodes) + return ( + getBoundaryNodes(_nodes) ?? { + top: null, + right: null, + bottom: null, + left: null, + } + ) + } + /** + * @deprecated Functionality moved to {@link alignNodes}. The new function does not set dirty canvas. + * @param {Dictionary} 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) + */ + static alignNodes(nodes: Dictionary, direction: Direction, align_to?: LGraphNode): void { + alignNodes(Object.values(nodes), direction, align_to) + LGraphCanvas.active_canvas.setDirty(true, true) + } + static onNodeAlign(value: IContextMenuValue, options: IContextMenuOptions, event: MouseEvent, prev_menu: ContextMenu, node: LGraphNode): void { + new LiteGraph.ContextMenu(['Top', 'Bottom', 'Left', 'Right'], { + event: event, + callback: inner_clicked, + parentMenu: prev_menu, + }) + + function inner_clicked(value: string) { + alignNodes(Object.values(LGraphCanvas.active_canvas.selected_nodes), value.toLowerCase() as Direction, node) + LGraphCanvas.active_canvas.setDirty(true, true) } - /** - * @deprecated Functionality moved to {@link getBoundaryNodes}. The new function returns null on failure, instead of an object with all null properties. - * Determines the furthest nodes in each direction - * @param {Dictionary} nodes the nodes to from which boundary nodes will be extracted - * @return {{left: LGraphNode, top: LGraphNode, right: LGraphNode, bottom: LGraphNode}} - */ - static getBoundaryNodes(nodes: LGraphNode[] | Dictionary): NullableProperties { - const _nodes = Array.isArray(nodes) ? nodes : Object.values(nodes) - return getBoundaryNodes(_nodes) ?? { - top: null, - right: null, - bottom: null, - left: null - } + } + static onGroupAlign(value: IContextMenuValue, options: IContextMenuOptions, event: MouseEvent, prev_menu: ContextMenu): void { + new LiteGraph.ContextMenu(['Top', 'Bottom', 'Left', 'Right'], { + event: event, + callback: inner_clicked, + parentMenu: prev_menu, + }) + + function inner_clicked(value: string) { + alignNodes(Object.values(LGraphCanvas.active_canvas.selected_nodes), value.toLowerCase() as Direction) + LGraphCanvas.active_canvas.setDirty(true, true) } - /** - * @deprecated Functionality moved to {@link alignNodes}. The new function does not set dirty canvas. - * @param {Dictionary} 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) - */ - static alignNodes(nodes: Dictionary, direction: Direction, align_to?: LGraphNode): void { - alignNodes(Object.values(nodes), direction, align_to) - LGraphCanvas.active_canvas.setDirty(true, true) + } + static createDistributeMenu( + value: IContextMenuValue, + options: IContextMenuOptions, + event: MouseEvent, + prev_menu: ContextMenu, + node: LGraphNode, + ): void { + new LiteGraph.ContextMenu(['Vertically', 'Horizontally'], { + event, + callback: inner_clicked, + parentMenu: prev_menu, + }) + + function inner_clicked(value: string) { + const canvas = LGraphCanvas.active_canvas + distributeNodes(Object.values(canvas.selected_nodes), value === 'Horizontally') + canvas.setDirty(true, true) } - static onNodeAlign(value: IContextMenuValue, options: IContextMenuOptions, event: MouseEvent, prev_menu: ContextMenu, node: LGraphNode): void { - new LiteGraph.ContextMenu(["Top", "Bottom", "Left", "Right"], { - event: event, - callback: inner_clicked, - parentMenu: prev_menu, + } + static onMenuAdd( + node: LGraphNode, + options: IContextMenuOptions, + e: MouseEvent, + prev_menu: ContextMenu, + callback?: (node: LGraphNode) => void, + ): boolean { + const canvas = LGraphCanvas.active_canvas + const ref_window = canvas.getCanvasWindow() + const graph = canvas.graph + if (!graph) return + + function inner_onMenuAdded(base_category: string, prev_menu: ContextMenu): void { + const categories = LiteGraph.getNodeTypesCategories(canvas.filter || graph.filter).filter(function (category) { + return category.startsWith(base_category) + }) + const entries = [] + + categories.map(function (category) { + if (!category) return + + const base_category_regex = new RegExp('^(' + base_category + ')') + const category_name = category.replace(base_category_regex, '').split('/')[0] + const category_path = base_category === '' ? category_name + '/' : base_category + category_name + '/' + + let 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] + + const index = entries.findIndex(function (entry) { + return entry.value === category_path }) - - function inner_clicked(value: string) { - alignNodes(Object.values(LGraphCanvas.active_canvas.selected_nodes), (value.toLowerCase() as Direction), node) - LGraphCanvas.active_canvas.setDirty(true, true) - } - } - static onGroupAlign(value: IContextMenuValue, options: IContextMenuOptions, event: MouseEvent, prev_menu: ContextMenu): void { - new LiteGraph.ContextMenu(["Top", "Bottom", "Left", "Right"], { - event: event, - callback: inner_clicked, - parentMenu: prev_menu, - }) - - function inner_clicked(value: string) { - alignNodes(Object.values(LGraphCanvas.active_canvas.selected_nodes), (value.toLowerCase() as Direction)) - LGraphCanvas.active_canvas.setDirty(true, true) - } - } - static createDistributeMenu(value: IContextMenuValue, options: IContextMenuOptions, event: MouseEvent, prev_menu: ContextMenu, node: LGraphNode): void { - new LiteGraph.ContextMenu(["Vertically", "Horizontally"], { - event, - callback: inner_clicked, - parentMenu: prev_menu, - }) - - function inner_clicked(value: string) { - const canvas = LGraphCanvas.active_canvas - distributeNodes(Object.values(canvas.selected_nodes), value === "Horizontally") - canvas.setDirty(true, true) - } - } - static onMenuAdd(node: LGraphNode, options: IContextMenuOptions, e: MouseEvent, prev_menu: ContextMenu, callback?: (node: LGraphNode) => void): boolean { - - const canvas = LGraphCanvas.active_canvas - const ref_window = canvas.getCanvasWindow() - const graph = canvas.graph - if (!graph) - return - - function inner_onMenuAdded(base_category: string, prev_menu: ContextMenu): void { - - const categories = LiteGraph.getNodeTypesCategories(canvas.filter || graph.filter).filter(function (category) { return category.startsWith(base_category) }) - const entries = [] - - categories.map(function (category) { - - if (!category) - return - - const base_category_regex = new RegExp('^(' + base_category + ')') - const category_name = category.replace(base_category_regex, "").split('/')[0] - const category_path = base_category === '' ? category_name + '/' : base_category + category_name + '/' - - let 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] - - const 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) - } - }) - } - - }) - - const nodes = LiteGraph.getNodeTypesInCategory(base_category.slice(0, -1), canvas.filter || graph.filter) - nodes.map(function (node) { - - if (node.skip_list) - return - - const entry = { - value: node.type, content: node.title, has_submenu: false, callback: function (value, event, mouseEvent, contextMenu) { - - const first_event = contextMenu.getFirstEvent() - canvas.graph.beforeChange() - const node = LiteGraph.createNode(value.value) - if (node) { - node.pos = canvas.convertEventToCanvasOffset(first_event) - canvas.graph.add(node) - } - - callback?.(node) - canvas.graph.afterChange() - - } - } - - entries.push(entry) - - }) - - // @ts-expect-error Remove param ref_window - unused - new LiteGraph.ContextMenu(entries, { event: e, parentMenu: prev_menu }, ref_window) - - } - - inner_onMenuAdded('', prev_menu) - return false - - } - - static onMenuCollapseAll() { } - static onMenuNodeEdit() { } - - /** @param options Parameter is never used */ - static showMenuNodeOptionalInputs(v: unknown, options: INodeInputSlot[], e: MouseEvent, prev_menu: ContextMenu, node: LGraphNode): boolean { - if (!node) return - - // FIXME: Static function this - const that = this - const canvas = LGraphCanvas.active_canvas - const ref_window = canvas.getCanvasWindow() - - options = node.onGetInputs - ? node.onGetInputs() - : node.optional_inputs - - let entries: IOptionalSlotData[] = [] - if (options) { - for (let i = 0; i < options.length; i++) { - const entry = options[i] - if (!entry) { - entries.push(null) - continue - } - let label = entry[0] - entry[2] ||= {} - - if (entry[2].label) { - label = entry[2].label - } - - entry[2].removable = true - const data: IOptionalSlotData = { content: label, value: entry } - if (entry[1] == LiteGraph.ACTION) { - data.className = "event" - } - entries.push(data) - } - } - - const retEntries = node.onMenuNodeInputs?.(entries) - if (retEntries) entries = retEntries - - if (!entries.length) { - console.log("no input entries") - return - } - - new LiteGraph.ContextMenu( - entries, - { - event: e, - callback: inner_clicked, - parentMenu: prev_menu, - node: node + if (index === -1) { + entries.push({ + value: category_path, + content: name, + has_submenu: true, + callback: function (value, event, mouseEvent, contextMenu) { + inner_onMenuAdded(value.value, contextMenu) }, - // @ts-expect-error Unused param - ref_window - ) + }) + } + }) - function inner_clicked(v, e, prev) { - if (!node) return + const nodes = LiteGraph.getNodeTypesInCategory(base_category.slice(0, -1), canvas.filter || graph.filter) + nodes.map(function (node) { + if (node.skip_list) return - v.callback?.call(that, node, v, e, prev) + const entry = { + value: node.type, + content: node.title, + has_submenu: false, + callback: function (value, event, mouseEvent, contextMenu) { + const first_event = contextMenu.getFirstEvent() + canvas.graph.beforeChange() + const node = LiteGraph.createNode(value.value) + if (node) { + node.pos = canvas.convertEventToCanvasOffset(first_event) + canvas.graph.add(node) + } - if (!v.value) return - node.graph.beforeChange() - node.addInput(v.value[0], v.value[1], v.value[2]) - - // callback to the node when adding a slot - node.onNodeInputAdd?.(v.value) - canvas.setDirty(true, true) - node.graph.afterChange() + callback?.(node) + canvas.graph.afterChange() + }, } - return false + entries.push(entry) + }) + + // @ts-expect-error Remove param ref_window - unused + new LiteGraph.ContextMenu(entries, { event: e, parentMenu: prev_menu }, ref_window) } - /** @param options Parameter is never used */ - static showMenuNodeOptionalOutputs(v: unknown, options: INodeOutputSlot[], e: unknown, prev_menu: ContextMenu, node: LGraphNode): boolean { - if (!node) return + inner_onMenuAdded('', prev_menu) + return false + } - const that = this - const canvas = LGraphCanvas.active_canvas - const ref_window = canvas.getCanvasWindow() + static onMenuCollapseAll() {} + static onMenuNodeEdit() {} - options = node.onGetOutputs - ? node.onGetOutputs() - : node.optional_outputs + /** @param options Parameter is never used */ + static showMenuNodeOptionalInputs(v: unknown, options: INodeInputSlot[], e: MouseEvent, prev_menu: ContextMenu, node: LGraphNode): boolean { + if (!node) return - let entries: IOptionalSlotData[] = [] - if (options) { - for (let i = 0; i < options.length; i++) { - const entry = options[i] - if (!entry) { - //separator? - entries.push(null) - continue - } + // FIXME: Static function this + const that = this + const canvas = LGraphCanvas.active_canvas + const ref_window = canvas.getCanvasWindow() - if (node.flags && - node.flags.skip_repeated_outputs && - node.findOutputSlot(entry[0]) != -1) { - continue - } //skip the ones already on - let label = entry[0] - entry[2] ||= {} - if (entry[2].label) { - label = entry[2].label - } - entry[2].removable = true - const data: IOptionalSlotData = { content: label, value: entry } - if (entry[1] == LiteGraph.EVENT) { - data.className = "event" - } - entries.push(data) - } + options = node.onGetInputs ? node.onGetInputs() : node.optional_inputs + + let entries: IOptionalSlotData[] = [] + if (options) { + for (let i = 0; i < options.length; i++) { + const entry = options[i] + if (!entry) { + entries.push(null) + continue + } + let label = entry[0] + entry[2] ||= {} + + if (entry[2].label) { + label = entry[2].label } - if (this.onMenuNodeOutputs) entries = this.onMenuNodeOutputs(entries) - if (LiteGraph.do_add_triggers_slots) { //canvas.allow_addOutSlot_onExecuted - if (node.findOutputSlot("onExecuted") == -1) { - // @ts-expect-error Events - entries.push({ content: "On Executed", value: ["onExecuted", LiteGraph.EVENT, { nameLocked: true }], className: "event" }) //, opts: {} - } + entry[2].removable = true + const data: IOptionalSlotData = { content: label, value: entry } + if (entry[1] == LiteGraph.ACTION) { + data.className = 'event' } - // add callback for modifing the menu elements onMenuNodeOutputs - const retEntries = node.onMenuNodeOutputs?.(entries) - if (retEntries) entries = retEntries - - if (!entries.length) return - - new LiteGraph.ContextMenu( - entries, - { - event: e, - callback: inner_clicked, - parentMenu: prev_menu, - node: node - }, - // @ts-expect-error Unused - ref_window - ) - - function inner_clicked(v, e, prev) { - if (!node) return - - // TODO: This is a static method, so the below "that" appears broken. - if (v.callback) v.callback.call(that, node, v, e, prev) - - if (!v.value) return - - const value = v.value[1] - - if (value && - (typeof value === "object" || Array.isArray(value))) { - //submenu why? - const entries = [] - for (const 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 - } - - const graph = node.graph - graph.beforeChange() - node.addOutput(v.value[0], v.value[1], v.value[2]) - - // a callback to the node when adding a slot - node.onNodeOutputAdd?.(v.value) - canvas.setDirty(true, true) - graph.afterChange() - } - - return false + entries.push(data) + } } - /** @param value Parameter is never used */ - static onShowMenuNodeProperties(value: unknown, options: unknown, e: MouseEvent, prev_menu: ContextMenu, node: LGraphNode): boolean { - if (!node || !node.properties) return + const retEntries = node.onMenuNodeInputs?.(entries) + if (retEntries) entries = retEntries - const canvas = LGraphCanvas.active_canvas - const ref_window = canvas.getCanvasWindow() + if (!entries.length) { + console.log('no input entries') + return + } + new LiteGraph.ContextMenu( + entries, + { + event: e, + callback: inner_clicked, + parentMenu: prev_menu, + node: node, + }, + // @ts-expect-error Unused param + ref_window, + ) + + function inner_clicked(v, e, prev) { + if (!node) return + + v.callback?.call(that, node, v, e, prev) + + if (!v.value) return + node.graph.beforeChange() + node.addInput(v.value[0], v.value[1], v.value[2]) + + // callback to the node when adding a slot + node.onNodeInputAdd?.(v.value) + canvas.setDirty(true, true) + node.graph.afterChange() + } + + return false + } + + /** @param options Parameter is never used */ + static showMenuNodeOptionalOutputs(v: unknown, options: INodeOutputSlot[], e: unknown, prev_menu: ContextMenu, node: LGraphNode): boolean { + if (!node) return + + const that = this + const canvas = LGraphCanvas.active_canvas + const ref_window = canvas.getCanvasWindow() + + options = node.onGetOutputs ? node.onGetOutputs() : node.optional_outputs + + let entries: IOptionalSlotData[] = [] + if (options) { + for (let i = 0; i < options.length; i++) { + const 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 + let label = entry[0] + entry[2] ||= {} + if (entry[2].label) { + label = entry[2].label + } + entry[2].removable = true + const data: IOptionalSlotData = { 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) { + // @ts-expect-error Events + entries.push({ content: 'On Executed', value: ['onExecuted', LiteGraph.EVENT, { nameLocked: true }], className: 'event' }) //, opts: {} + } + } + // add callback for modifing the menu elements onMenuNodeOutputs + const retEntries = node.onMenuNodeOutputs?.(entries) + if (retEntries) entries = retEntries + + if (!entries.length) return + + new LiteGraph.ContextMenu( + entries, + { + event: e, + callback: inner_clicked, + parentMenu: prev_menu, + node: node, + }, + // @ts-expect-error Unused + ref_window, + ) + + function inner_clicked(v, e, prev) { + if (!node) return + + // TODO: This is a static method, so the below "that" appears broken. + if (v.callback) v.callback.call(that, node, v, e, prev) + + if (!v.value) return + + const value = v.value[1] + + if (value && (typeof value === 'object' || Array.isArray(value))) { + //submenu why? const entries = [] - for (const i in node.properties) { - value = node.properties[i] !== undefined ? node.properties[i] : " " - if (typeof value == "object") - value = JSON.stringify(value) - const 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(stringOrNull(value)) - entries.push({ - content: "" + - (info.label || i) + - "" + - "" + - value + - "", - value: i - }) + for (const i in value) { + entries.push({ content: i, value: value[i] }) } - if (!entries.length) { - return - } - - new LiteGraph.ContextMenu( - entries, - { - event: e, - callback: inner_clicked, - parentMenu: prev_menu, - allow_html: true, - node: node - }, - // @ts-expect-error Unused - ref_window - ) - - function inner_clicked(v: { value: any }) { - if (!node) return - - const rect = this.getBoundingClientRect() - canvas.showEditPropertyValue(node, v.value, { - position: [rect.left, rect.top] - }) - } - + new LiteGraph.ContextMenu(entries, { + event: e, + callback: inner_clicked, + parentMenu: prev_menu, + node: node, + }) return false - } - static decodeHTML(str: string): string { - const e = document.createElement("div") - e.innerText = str - return e.innerHTML - } - static onMenuResizeNode(value: IContextMenuValue, options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, node: LGraphNode): void { - if (!node) return + } - const fApplyMultiNode = function (node: LGraphNode) { - node.size = node.computeSize() - node.onResize?.(node.size) - } + const graph = node.graph + graph.beforeChange() + node.addOutput(v.value[0], v.value[1], v.value[2]) - const canvas = LGraphCanvas.active_canvas - if (!canvas.selected_nodes || Object.keys(canvas.selected_nodes).length <= 1) { - fApplyMultiNode(node) - } else { - for (const i in canvas.selected_nodes) { - fApplyMultiNode(canvas.selected_nodes[i]) - } - } - - canvas.setDirty(true, true) - } - // TODO refactor :: this is used fot title but not for properties! - static onShowPropertyEditor(item: { property: string; type: string }, options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, node: LGraphNode): void { - const property = item.property || "title" - const value = node[property] - - // TODO: Remove "any" kludge - // TODO refactor :: use createDialog ? - const dialog: any = document.createElement("div") - dialog.is_modified = false - dialog.className = "graphdialog" - dialog.innerHTML = - "" - dialog.close = function () { - dialog.parentNode?.removeChild(dialog) - } - const title = dialog.querySelector(".name") - title.innerText = property - const input = dialog.querySelector(".value") - if (input) { - input.value = value - input.addEventListener("blur", function () { - this.focus() - }) - input.addEventListener("keydown", function (e: KeyboardEvent) { - dialog.is_modified = true - if (e.keyCode == 27) { - //ESC - dialog.close() - } else if (e.keyCode == 13) { - inner() // save - // @ts-expect-error Intentional - undefined if not present - } else if (e.keyCode != 13 && e.target.localName != "textarea") { - return - } - e.preventDefault() - e.stopPropagation() - }) - } - - const canvas = LGraphCanvas.active_canvas - const canvasEl = canvas.canvas - - const rect = canvasEl.getBoundingClientRect() - let offsetx = -20 - let offsety = -20 - if (rect) { - offsetx -= rect.left - offsety -= rect.top - } - - if (e) { - dialog.style.left = e.clientX + offsetx + "px" - dialog.style.top = e.clientY + offsety + "px" - } else { - dialog.style.left = canvasEl.width * 0.5 + offsetx + "px" - dialog.style.top = canvasEl.height * 0.5 + offsety + "px" - } - - const button = dialog.querySelector("button") - button.addEventListener("click", inner) - canvasEl.parentNode.appendChild(dialog) - - input?.focus() - - let dialogCloseTimer = null - dialog.addEventListener("mouseleave", function () { - 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 () { - 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 - dialog.parentNode?.removeChild(dialog) - canvas.setDirty(true, true) - } - } - static getPropertyPrintableValue(value: unknown, values: unknown[] | object): string { - if (!values) - return String(value) - - if (Array.isArray(values)) { - return String(value) - } - - if (typeof values === "object") { - let desc_value = "" - for (const k in values) { - if (values[k] != value) - continue - desc_value = k - break - } - return String(value) + " (" + desc_value + ")" - } - } - static onMenuNodeCollapse(value: IContextMenuValue, options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, node: LGraphNode): void { - node.graph.beforeChange( /*?*/) - - const fApplyMultiNode = function (node) { - node.collapse() - } - - const graphcanvas = LGraphCanvas.active_canvas - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1) { - fApplyMultiNode(node) - } else { - for (const i in graphcanvas.selected_nodes) { - fApplyMultiNode(graphcanvas.selected_nodes[i]) - } - } - - node.graph.afterChange( /*?*/) - } - static onMenuToggleAdvanced(value: IContextMenuValue, options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, node: LGraphNode): void { - node.graph.beforeChange( /*?*/) - const fApplyMultiNode = function (node: LGraphNode) { - node.toggleAdvanced() - } - - const graphcanvas = LGraphCanvas.active_canvas - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1) { - fApplyMultiNode(node) - } else { - for (const i in graphcanvas.selected_nodes) { - fApplyMultiNode(graphcanvas.selected_nodes[i]) - } - } - node.graph.afterChange( /*?*/) - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - static onMenuNodePin(value: IContextMenuValue, options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, node: LGraphNode): void { - } - static onMenuNodeMode(value: IContextMenuValue, options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, node: LGraphNode): boolean { - new LiteGraph.ContextMenu( - LiteGraph.NODE_MODES, - { event: e, callback: inner_clicked, parentMenu: menu, node: node } - ) - - function inner_clicked(v) { - if (!node) return - - const kV = Object.values(LiteGraph.NODE_MODES).indexOf(v) - const fApplyMultiNode = function (node) { - if (kV >= 0 && LiteGraph.NODE_MODES[kV]) - node.changeMode(kV) - else { - console.warn("unexpected mode: " + v) - node.changeMode(LGraphEventMode.ALWAYS) - } - } - - const graphcanvas = LGraphCanvas.active_canvas - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1) { - fApplyMultiNode(node) - } else { - for (const i in graphcanvas.selected_nodes) { - fApplyMultiNode(graphcanvas.selected_nodes[i]) - } - } - } - - return false + // a callback to the node when adding a slot + node.onNodeOutputAdd?.(v.value) + canvas.setDirty(true, true) + graph.afterChange() } - /** @param value Parameter is never used */ - static onMenuNodeColors(value: IContextMenuValue, options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, node: LGraphNode): boolean { - if (!node) throw "no node for color" + return false + } - const values: IContextMenuValue[] = [] - values.push({ - value: null, - content: "No color" - }) + /** @param value Parameter is never used */ + static onShowMenuNodeProperties(value: unknown, options: unknown, e: MouseEvent, prev_menu: ContextMenu, node: LGraphNode): boolean { + if (!node || !node.properties) return - for (const i in LGraphCanvas.node_colors) { - const color = LGraphCanvas.node_colors[i] - value = { - value: i, - content: "" + - i + - "" - } - values.push(value) - } - new LiteGraph.ContextMenu(values, { - event: e, - callback: inner_clicked, - parentMenu: menu, - node: node - }) + const canvas = LGraphCanvas.active_canvas + const ref_window = canvas.getCanvasWindow() - function inner_clicked(v: { value: string | number }) { - if (!node) return + const entries = [] + for (const i in node.properties) { + value = node.properties[i] !== undefined ? node.properties[i] : ' ' + if (typeof value == 'object') value = JSON.stringify(value) + const info = node.getPropertyInfo(i) + if (info.type == 'enum' || info.type == 'combo') value = LGraphCanvas.getPropertyPrintableValue(value, info.values) - const color = v.value ? LGraphCanvas.node_colors[v.value] : null - - const fApplyColor = function (node: LGraphNode) { - if (color) { - if (node instanceof LGraphGroup) { - node.color = color.groupcolor - } else { - node.color = color.color - node.bgcolor = color.bgcolor - } - } else { - delete node.color - delete node.bgcolor - } - } - - const canvas = LGraphCanvas.active_canvas - if (!canvas.selected_nodes || Object.keys(canvas.selected_nodes).length <= 1) { - fApplyColor(node) - } else { - for (const i in canvas.selected_nodes) { - fApplyColor(canvas.selected_nodes[i]) - } - } - canvas.setDirty(true, true) - } - - return false + //value could contain invalid html characters, clean that + value = LGraphCanvas.decodeHTML(stringOrNull(value)) + entries.push({ + content: "" + (info.label || i) + '' + "" + value + '', + value: i, + }) } - static onMenuNodeShapes(value: IContextMenuValue, options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, node: LGraphNode): boolean { - 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 - - const fApplyMultiNode = function (node) { - node.shape = v - } - - const canvas = LGraphCanvas.active_canvas - if (!canvas.selected_nodes || Object.keys(canvas.selected_nodes).length <= 1) { - fApplyMultiNode(node) - } else { - for (const i in canvas.selected_nodes) { - fApplyMultiNode(canvas.selected_nodes[i]) - } - } - - node.graph.afterChange( /*?*/) //node - canvas.setDirty(true) - } - - return false + if (!entries.length) { + return } - static onMenuNodeRemove(value: IContextMenuValue, options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, node: LGraphNode): void { - if (!node) throw "no node passed" - const graph = node.graph - graph.beforeChange() + new LiteGraph.ContextMenu( + entries, + { + event: e, + callback: inner_clicked, + parentMenu: prev_menu, + allow_html: true, + node: node, + }, + // @ts-expect-error Unused + ref_window, + ) - const fApplyMultiNode = function (node: LGraphNode) { - if (node.removable === false) return + function inner_clicked(v: { value: any }) { + if (!node) return - graph.remove(node) - } - - const canvas = LGraphCanvas.active_canvas - if (!canvas.selected_nodes || Object.keys(canvas.selected_nodes).length <= 1) { - fApplyMultiNode(node) - } else { - for (const i in canvas.selected_nodes) { - fApplyMultiNode(canvas.selected_nodes[i]) - } - } - - graph.afterChange() - canvas.setDirty(true, true) + const rect = this.getBoundingClientRect() + canvas.showEditPropertyValue(node, v.value, { + position: [rect.left, rect.top], + }) } - static onMenuNodeToSubgraph(value: IContextMenuValue, options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, node: LGraphNode): void { - const graph = node.graph - const canvas = LGraphCanvas.active_canvas - if (!canvas) return - let nodes_list = Object.values(canvas.selected_nodes || {}) - if (!nodes_list.length) - nodes_list = [node] + return false + } + static decodeHTML(str: string): string { + const e = document.createElement('div') + e.innerText = str + return e.innerHTML + } + static onMenuResizeNode(value: IContextMenuValue, options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, node: LGraphNode): void { + if (!node) return - const subgraph_node = LiteGraph.createNode("graph/subgraph") - // @ts-expect-error Refactor this to use typed array. - subgraph_node.pos = node.pos.concat() - graph.add(subgraph_node) - - // @ts-expect-error Doesn't exist anywhere... - subgraph_node.buildFromNodes(nodes_list) - - canvas.deselectAllNodes() - canvas.setDirty(true, true) + const fApplyMultiNode = function (node: LGraphNode) { + node.size = node.computeSize() + node.onResize?.(node.size) } - static onMenuNodeClone(value: IContextMenuValue, options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, node: LGraphNode): void { - const graph = node.graph - graph.beforeChange() - - const newSelected: Dictionary = {} - - const fApplyMultiNode = function (node) { - if (node.clonable === false) return - - const newnode = node.clone() - if (!newnode) return - - newnode.pos = [node.pos[0] + 5, node.pos[1] + 5] - node.graph.add(newnode) - newSelected[newnode.id] = newnode - } - - const canvas = LGraphCanvas.active_canvas - if (!canvas.selected_nodes || Object.keys(canvas.selected_nodes).length <= 1) { - fApplyMultiNode(node) - } else { - for (const i in canvas.selected_nodes) { - fApplyMultiNode(canvas.selected_nodes[i]) - } - } - - if (Object.keys(newSelected).length) { - canvas.selectNodes(newSelected) - } - - graph.afterChange() - - canvas.setDirty(true, true) + const canvas = LGraphCanvas.active_canvas + if (!canvas.selected_nodes || Object.keys(canvas.selected_nodes).length <= 1) { + fApplyMultiNode(node) + } else { + for (const i in canvas.selected_nodes) { + fApplyMultiNode(canvas.selected_nodes[i]) + } } - /** - * clears all the data inside - * - */ - clear(): void { - 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 + canvas.setDirty(true, true) + } + // TODO refactor :: this is used fot title but not for properties! + static onShowPropertyEditor( + item: { property: string; type: string }, + options: IContextMenuOptions, + e: MouseEvent, + menu: ContextMenu, + node: LGraphNode, + ): void { + const property = item.property || 'title' + const value = node[property] - this.selected_nodes = {} - /** All selected groups */ - this.selectedGroups = null - /** The group currently being resized */ - this.selected_group = null - - this.visible_nodes = [] - this.node_dragged = null - this.node_over = null - this.node_capturing_input = null - this.connecting_links = 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]) - - this.onClear?.() + // TODO: Remove "any" kludge + // TODO refactor :: use createDialog ? + const dialog: any = document.createElement('div') + dialog.is_modified = false + dialog.className = 'graphdialog' + dialog.innerHTML = "" + dialog.close = function () { + dialog.parentNode?.removeChild(dialog) } - /** - * assigns a graph, you can reassign graphs to the same canvas - * - * @param {LGraph} graph - */ - setGraph(graph: LGraph, skip_clear: boolean): void { - if (this.graph == graph) return - - if (!skip_clear) this.clear() - - if (!graph && this.graph) { - this.graph.detachCanvas(this) - return + const title = dialog.querySelector('.name') + title.innerText = property + const input = dialog.querySelector('.value') + if (input) { + input.value = value + input.addEventListener('blur', function () { + this.focus() + }) + input.addEventListener('keydown', function (e: KeyboardEvent) { + dialog.is_modified = true + if (e.keyCode == 27) { + //ESC + dialog.close() + } else if (e.keyCode == 13) { + inner() // save + // @ts-expect-error Intentional - undefined if not present + } else if (e.keyCode != 13 && e.target.localName != 'textarea') { + return } - - graph.attachCanvas(this) - - //remove the graph stack in case a subgraph was open - this._graph_stack &&= null - - this.setDirty(true, true) - } - /** - * returns the top level graph (in case there are subgraphs open on the canvas) - * - * @return {LGraph} graph - */ - getTopGraph(): LGraph { - return this._graph_stack.length - ? this._graph_stack[0] - : this.graph - } - /** - * opens a graph contained inside a node in the current graph - * - * @param {LGraph} graph - */ - openSubgraph(graph: LGraph): void { - if (!graph) throw "graph cannot be null" - - if (this.graph == graph) throw "graph cannot be the same" - - this.clear() - - if (this.graph) { - 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 - * - * @param {LGraph} assigns a graph - */ - closeSubgraph(): void { - if (!this._graph_stack || this._graph_stack.length == 0) return - - const subgraph_node = this.graph._subgraph_node - const 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) - * @return {LGraph} the active graph - */ - getCurrentGraph(): LGraph { - return this.graph - } - /** - * Finds the canvas if required, throwing on failure. - * @param canvas Canvas element, or its element ID - * @returns The canvas element - * @throws If {@link canvas} is an element ID that does not belong to a valid HTML canvas element - */ - #validateCanvas(canvas: string | HTMLCanvasElement): HTMLCanvasElement & { data?: LGraphCanvas } { - if (typeof canvas === "string") { - const el = document.getElementById(canvas) - if (!(el instanceof HTMLCanvasElement)) throw "Error validating LiteGraph canvas: Canvas element not found" - return el - } - return canvas - } - /** - * Sets the current HTML canvas element. - * Calls bindEvents to add input event listeners, and (re)creates the background canvas. - * - * @param canvas The canvas element to assign, or its HTML element ID. If null or undefined, the current reference is cleared. - * @param skip_events If true, events on the previous canvas will not be removed. Has no effect on the first invocation. - */ - setCanvas(canvas: string | HTMLCanvasElement, skip_events?: boolean) { - const element = this.#validateCanvas(canvas) - if (element === this.canvas) return - //maybe detach events from old_canvas - if (!element && this.canvas && !skip_events) this.unbindEvents() - - this.canvas = element - this.ds.element = element - - if (!element) return - - // TODO: classList.add - element.className += " lgraphcanvas" - element.data = this - // @ts-expect-error Likely safe to remove. A decent default, but expectation is to be configured by calling app. - element.tabindex = "1" //to allow key events - - // Background canvas: To render objects behind nodes (background, links, groups) - this.bgcanvas = null - if (!this.bgcanvas) { - this.bgcanvas = document.createElement("canvas") - this.bgcanvas.width = this.canvas.width - this.bgcanvas.height = this.canvas.height - } - if (element.getContext == null) { - if (element.localName != "canvas") { - throw "Element supplied for LGraphCanvas must be a element, you passed a " + - element.localName - } - throw "This browser doesn't support Canvas" - } - - const ctx = (this.ctx = element.getContext("2d")) - if (ctx == null) { - // @ts-expect-error WebGL - if (!element.webgl_enabled) { - console.warn( - "This canvas seems to be WebGL, enabling WebGL renderer" - ) - } - this.enableWebGL() - } - - if (!skip_events) this.bindEvents() - } - //used in some events to capture them - _doNothing(e: Event) { - //console.log("pointerevents: _doNothing "+e.type); e.preventDefault() - return false + e.stopPropagation() + }) } - _doReturnTrue(e: Event) { - e.preventDefault() - return true + + const canvas = LGraphCanvas.active_canvas + const canvasEl = canvas.canvas + + const rect = canvasEl.getBoundingClientRect() + let offsetx = -20 + let offsety = -20 + if (rect) { + offsetx -= rect.left + offsety -= rect.top } - /** - * binds mouse, keyboard, touch and drag events to the canvas - **/ - bindEvents(): void { - if (this._events_binded) { - console.warn("LGraphCanvas: events already binded") - return - } - //console.log("pointerevents: bindEvents"); - const canvas = this.canvas - - const ref_window = this.getCanvasWindow() - const 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) - this._mouseout_callback = this.processMouseOut.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("pointerout", this._mouseout_callback) - - canvas.addEventListener("contextmenu", this._doNothing) - canvas.addEventListener( - "DOMMouseScroll", - this._mousewheel_callback, - false - ) - - //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 + if (e) { + dialog.style.left = e.clientX + offsetx + 'px' + dialog.style.top = e.clientY + offsety + 'px' + } else { + dialog.style.left = canvasEl.width * 0.5 + offsetx + 'px' + dialog.style.top = canvasEl.height * 0.5 + offsety + 'px' } - /** - * unbinds mouse events from the canvas - **/ - unbindEvents(): void { - if (!this._events_binded) { - console.warn("LGraphCanvas: no events binded") - return - } - //console.log("pointerevents: unbindEvents"); - const ref_window = this.getCanvasWindow() - const document = ref_window.document + const button = dialog.querySelector('button') + button.addEventListener('click', inner) + canvasEl.parentNode.appendChild(dialog) - this.canvas.removeEventListener("pointerout", this._mouseout_callback) - LiteGraph.pointerListenerRemove(this.canvas, "move", this._mousemove_callback) - LiteGraph.pointerListenerRemove(this.canvas, "up", this._mouseup_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) + input?.focus() - this._mousedown_callback = null - this._mousewheel_callback = null - this._key_callback = null - this._ondrop_callback = null + let dialogCloseTimer = null + dialog.addEventListener('mouseleave', function () { + 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 () { + if (LiteGraph.dialog_close_on_mouse_leave) if (dialogCloseTimer) clearTimeout(dialogCloseTimer) + }) - this._events_binded = false + function inner() { + if (input) setValue(input.value) } - /** - * 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 { - // TODO: Delete or move all webgl to a module and never load it. - // @ts-expect-error - if (typeof GL === "undefined") { - throw "litegl.js must be included to use a WebGL canvas" - } - // @ts-expect-error - if (typeof enableWebGLCanvas === "undefined") { - throw "webglCanvas.js must be included to use this feature" - } - // @ts-expect-error - this.gl = this.ctx = enableWebGLCanvas(this.canvas) - // @ts-expect-error - this.ctx.webgl = true - this.bgcanvas = this.canvas - this.bgctx = this.gl - // @ts-expect-error - this.canvas.webgl_enabled = true + function setValue(value) { + if (item.type == 'Number') { + value = Number(value) + } else if (item.type == 'Boolean') { + value = Boolean(value) + } + node[property] = value + dialog.parentNode?.removeChild(dialog) + canvas.setDirty(true, true) + } + } + static getPropertyPrintableValue(value: unknown, values: unknown[] | object): string { + if (!values) return String(value) - /* + if (Array.isArray(values)) { + return String(value) + } + + if (typeof values === 'object') { + let desc_value = '' + for (const k in values) { + if (values[k] != value) continue + desc_value = k + break + } + return String(value) + ' (' + desc_value + ')' + } + } + static onMenuNodeCollapse(value: IContextMenuValue, options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, node: LGraphNode): void { + node.graph.beforeChange(/*?*/) + + const fApplyMultiNode = function (node) { + node.collapse() + } + + const graphcanvas = LGraphCanvas.active_canvas + if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1) { + fApplyMultiNode(node) + } else { + for (const i in graphcanvas.selected_nodes) { + fApplyMultiNode(graphcanvas.selected_nodes[i]) + } + } + + node.graph.afterChange(/*?*/) + } + static onMenuToggleAdvanced(value: IContextMenuValue, options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, node: LGraphNode): void { + node.graph.beforeChange(/*?*/) + const fApplyMultiNode = function (node: LGraphNode) { + node.toggleAdvanced() + } + + const graphcanvas = LGraphCanvas.active_canvas + if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1) { + fApplyMultiNode(node) + } else { + for (const i in graphcanvas.selected_nodes) { + fApplyMultiNode(graphcanvas.selected_nodes[i]) + } + } + node.graph.afterChange(/*?*/) + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + static onMenuNodePin(value: IContextMenuValue, options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, node: LGraphNode): void {} + static onMenuNodeMode(value: IContextMenuValue, options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, node: LGraphNode): boolean { + new LiteGraph.ContextMenu(LiteGraph.NODE_MODES, { event: e, callback: inner_clicked, parentMenu: menu, node: node }) + + function inner_clicked(v) { + if (!node) return + + const kV = Object.values(LiteGraph.NODE_MODES).indexOf(v) + const fApplyMultiNode = function (node) { + if (kV >= 0 && LiteGraph.NODE_MODES[kV]) node.changeMode(kV) + else { + console.warn('unexpected mode: ' + v) + node.changeMode(LGraphEventMode.ALWAYS) + } + } + + const graphcanvas = LGraphCanvas.active_canvas + if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1) { + fApplyMultiNode(node) + } else { + for (const i in graphcanvas.selected_nodes) { + fApplyMultiNode(graphcanvas.selected_nodes[i]) + } + } + } + + return false + } + + /** @param value Parameter is never used */ + static onMenuNodeColors(value: IContextMenuValue, options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, node: LGraphNode): boolean { + if (!node) throw 'no node for color' + + const values: IContextMenuValue[] = [] + values.push({ + value: null, + content: "No color", + }) + + for (const i in LGraphCanvas.node_colors) { + const color = LGraphCanvas.node_colors[i] + 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: { value: string | number }) { + if (!node) return + + const color = v.value ? LGraphCanvas.node_colors[v.value] : null + + const fApplyColor = function (node: LGraphNode) { + if (color) { + if (node instanceof LGraphGroup) { + node.color = color.groupcolor + } else { + node.color = color.color + node.bgcolor = color.bgcolor + } + } else { + delete node.color + delete node.bgcolor + } + } + + const canvas = LGraphCanvas.active_canvas + if (!canvas.selected_nodes || Object.keys(canvas.selected_nodes).length <= 1) { + fApplyColor(node) + } else { + for (const i in canvas.selected_nodes) { + fApplyColor(canvas.selected_nodes[i]) + } + } + canvas.setDirty(true, true) + } + + return false + } + static onMenuNodeShapes(value: IContextMenuValue, options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, node: LGraphNode): boolean { + 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 + + const fApplyMultiNode = function (node) { + node.shape = v + } + + const canvas = LGraphCanvas.active_canvas + if (!canvas.selected_nodes || Object.keys(canvas.selected_nodes).length <= 1) { + fApplyMultiNode(node) + } else { + for (const i in canvas.selected_nodes) { + fApplyMultiNode(canvas.selected_nodes[i]) + } + } + + node.graph.afterChange(/*?*/) //node + canvas.setDirty(true) + } + + return false + } + static onMenuNodeRemove(value: IContextMenuValue, options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, node: LGraphNode): void { + if (!node) throw 'no node passed' + + const graph = node.graph + graph.beforeChange() + + const fApplyMultiNode = function (node: LGraphNode) { + if (node.removable === false) return + + graph.remove(node) + } + + const canvas = LGraphCanvas.active_canvas + if (!canvas.selected_nodes || Object.keys(canvas.selected_nodes).length <= 1) { + fApplyMultiNode(node) + } else { + for (const i in canvas.selected_nodes) { + fApplyMultiNode(canvas.selected_nodes[i]) + } + } + + graph.afterChange() + canvas.setDirty(true, true) + } + static onMenuNodeToSubgraph(value: IContextMenuValue, options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, node: LGraphNode): void { + const graph = node.graph + const canvas = LGraphCanvas.active_canvas + if (!canvas) return + + let nodes_list = Object.values(canvas.selected_nodes || {}) + if (!nodes_list.length) nodes_list = [node] + + const subgraph_node = LiteGraph.createNode('graph/subgraph') + // @ts-expect-error Refactor this to use typed array. + subgraph_node.pos = node.pos.concat() + graph.add(subgraph_node) + + // @ts-expect-error Doesn't exist anywhere... + subgraph_node.buildFromNodes(nodes_list) + + canvas.deselectAllNodes() + canvas.setDirty(true, true) + } + static onMenuNodeClone(value: IContextMenuValue, options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, node: LGraphNode): void { + const graph = node.graph + graph.beforeChange() + + const newSelected: Dictionary = {} + + const fApplyMultiNode = function (node) { + if (node.clonable === false) return + + const newnode = node.clone() + if (!newnode) return + + newnode.pos = [node.pos[0] + 5, node.pos[1] + 5] + node.graph.add(newnode) + newSelected[newnode.id] = newnode + } + + const canvas = LGraphCanvas.active_canvas + if (!canvas.selected_nodes || Object.keys(canvas.selected_nodes).length <= 1) { + fApplyMultiNode(node) + } else { + for (const i in canvas.selected_nodes) { + fApplyMultiNode(canvas.selected_nodes[i]) + } + } + + if (Object.keys(newSelected).length) { + canvas.selectNodes(newSelected) + } + + graph.afterChange() + + canvas.setDirty(true, true) + } + /** + * clears all the data inside + * + */ + clear(): void { + 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 = {} + /** All selected groups */ + this.selectedGroups = null + /** The group currently being resized */ + this.selected_group = null + + this.visible_nodes = [] + this.node_dragged = null + this.node_over = null + this.node_capturing_input = null + this.connecting_links = 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]) + + this.onClear?.() + } + /** + * assigns a graph, you can reassign graphs to the same canvas + * + * @param {LGraph} graph + */ + setGraph(graph: LGraph, skip_clear: boolean): void { + 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 + this._graph_stack &&= null + + this.setDirty(true, true) + } + /** + * returns the top level graph (in case there are subgraphs open on the canvas) + * + * @return {LGraph} graph + */ + getTopGraph(): LGraph { + return this._graph_stack.length ? this._graph_stack[0] : this.graph + } + /** + * opens a graph contained inside a node in the current graph + * + * @param {LGraph} graph + */ + openSubgraph(graph: LGraph): void { + if (!graph) throw 'graph cannot be null' + + if (this.graph == graph) throw 'graph cannot be the same' + + this.clear() + + if (this.graph) { + 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 + * + * @param {LGraph} assigns a graph + */ + closeSubgraph(): void { + if (!this._graph_stack || this._graph_stack.length == 0) return + + const subgraph_node = this.graph._subgraph_node + const 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) + * @return {LGraph} the active graph + */ + getCurrentGraph(): LGraph { + return this.graph + } + /** + * Finds the canvas if required, throwing on failure. + * @param canvas Canvas element, or its element ID + * @returns The canvas element + * @throws If {@link canvas} is an element ID that does not belong to a valid HTML canvas element + */ + #validateCanvas(canvas: string | HTMLCanvasElement): HTMLCanvasElement & { data?: LGraphCanvas } { + if (typeof canvas === 'string') { + const el = document.getElementById(canvas) + if (!(el instanceof HTMLCanvasElement)) throw 'Error validating LiteGraph canvas: Canvas element not found' + return el + } + return canvas + } + /** + * Sets the current HTML canvas element. + * Calls bindEvents to add input event listeners, and (re)creates the background canvas. + * + * @param canvas The canvas element to assign, or its HTML element ID. If null or undefined, the current reference is cleared. + * @param skip_events If true, events on the previous canvas will not be removed. Has no effect on the first invocation. + */ + setCanvas(canvas: string | HTMLCanvasElement, skip_events?: boolean) { + const element = this.#validateCanvas(canvas) + if (element === this.canvas) return + //maybe detach events from old_canvas + if (!element && this.canvas && !skip_events) this.unbindEvents() + + this.canvas = element + this.ds.element = element + + if (!element) return + + // TODO: classList.add + element.className += ' lgraphcanvas' + element.data = this + // @ts-expect-error Likely safe to remove. A decent default, but expectation is to be configured by calling app. + element.tabindex = '1' //to allow key events + + // Background canvas: To render objects behind nodes (background, links, groups) + this.bgcanvas = null + if (!this.bgcanvas) { + this.bgcanvas = document.createElement('canvas') + this.bgcanvas.width = this.canvas.width + this.bgcanvas.height = this.canvas.height + } + if (element.getContext == null) { + if (element.localName != 'canvas') { + throw 'Element supplied for LGraphCanvas must be a element, you passed a ' + element.localName + } + throw "This browser doesn't support Canvas" + } + + const ctx = (this.ctx = element.getContext('2d')) + if (ctx == null) { + // @ts-expect-error WebGL + if (!element.webgl_enabled) { + console.warn('This canvas seems to be WebGL, enabling WebGL renderer') + } + this.enableWebGL() + } + + if (!skip_events) this.bindEvents() + } + //used in some events to capture them + _doNothing(e: Event) { + //console.log("pointerevents: _doNothing "+e.type); + e.preventDefault() + return false + } + _doReturnTrue(e: Event) { + e.preventDefault() + return true + } + /** + * binds mouse, keyboard, touch and drag events to the canvas + **/ + bindEvents(): void { + if (this._events_binded) { + console.warn('LGraphCanvas: events already binded') + return + } + + //console.log("pointerevents: bindEvents"); + const canvas = this.canvas + + const ref_window = this.getCanvasWindow() + const 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) + this._mouseout_callback = this.processMouseOut.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('pointerout', this._mouseout_callback) + + canvas.addEventListener('contextmenu', this._doNothing) + canvas.addEventListener('DOMMouseScroll', this._mousewheel_callback, false) + + //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 + **/ + unbindEvents(): void { + if (!this._events_binded) { + console.warn('LGraphCanvas: no events binded') + return + } + + //console.log("pointerevents: unbindEvents"); + const ref_window = this.getCanvasWindow() + const document = ref_window.document + + this.canvas.removeEventListener('pointerout', this._mouseout_callback) + LiteGraph.pointerListenerRemove(this.canvas, 'move', this._mousemove_callback) + LiteGraph.pointerListenerRemove(this.canvas, 'up', this._mouseup_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) + + this._mousedown_callback = null + this._mousewheel_callback = null + this._key_callback = null + this._ondrop_callback = null + + this._events_binded = false + } + /** + * 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 { + // TODO: Delete or move all webgl to a module and never load it. + // @ts-expect-error + if (typeof GL === 'undefined') { + throw 'litegl.js must be included to use a WebGL canvas' + } + // @ts-expect-error + if (typeof enableWebGLCanvas === 'undefined') { + throw 'webglCanvas.js must be included to use this feature' + } + + // @ts-expect-error + this.gl = this.ctx = enableWebGLCanvas(this.canvas) + // @ts-expect-error + this.ctx.webgl = true + this.bgcanvas = this.canvas + this.bgctx = this.gl + // @ts-expect-error + 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 - * @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) - */ - setDirty(fgcanvas: boolean, bgcanvas?: boolean): void { - if (fgcanvas) this.dirty_canvas = true - if (bgcanvas) this.dirty_bgcanvas = true - } - /** - * Used to attach the canvas in a popup - * - * @return {window} returns the window where the canvas is attached (the DOM root node) - */ - getCanvasWindow(): Window { - if (!this.canvas) return window + } + /** + * marks as dirty the canvas, this way it will be rendered again + * + * @class LGraphCanvas + * @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) + */ + setDirty(fgcanvas: boolean, bgcanvas?: boolean): void { + if (fgcanvas) this.dirty_canvas = true + if (bgcanvas) this.dirty_bgcanvas = true + } + /** + * Used to attach the canvas in a popup + * + * @return {window} returns the window where the canvas is attached (the DOM root node) + */ + getCanvasWindow(): Window { + if (!this.canvas) return window - const doc = this.canvas.ownerDocument - // @ts-expect-error Check if required - return doc.defaultView || doc.parentWindow + const doc = this.canvas.ownerDocument + // @ts-expect-error Check if required + return doc.defaultView || doc.parentWindow + } + /** + * starts rendering the content of the canvas when needed + * + */ + startRendering(): void { + //already rendering + if (this.is_rendering) return + + this.is_rendering = true + renderFrame.call(this) + + function renderFrame(this: LGraphCanvas) { + if (!this.pause_rendering) { + this.draw() + } + + const window = this.getCanvasWindow() + if (this.is_rendering) { + window.requestAnimationFrame(renderFrame.bind(this)) + } } - /** - * starts rendering the content of the canvas when needed - * - */ - startRendering(): void { - //already rendering - if (this.is_rendering) return - - this.is_rendering = true - renderFrame.call(this) - - function renderFrame(this: LGraphCanvas) { - if (!this.pause_rendering) { - this.draw() - } - - const window = this.getCanvasWindow() - if (this.is_rendering) { - window.requestAnimationFrame(renderFrame.bind(this)) - } - } - } - /** - * stops rendering the content of the canvas (to save resources) - * - */ - stopRendering(): void { - this.is_rendering = false - /* + } + /** + * stops rendering the content of the canvas (to save resources) + * + */ + stopRendering(): void { + 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) - blockClick(): void { - this.block_click = true - this.last_mouseclick = 0 + } + /* LiteGraphCanvas input */ + //used to block future mouse events (because of im gui) + blockClick(): void { + this.block_click = true + this.last_mouseclick = 0 + } + + /** + * Gets the widget at the current cursor position + * @param node Optional node to check for widgets under cursor + * @returns The widget located at the current cursor position or null + */ + getWidgetAtCursor(node?: LGraphNode): IWidget | null { + node ??= this.node_over + + if (!node.widgets) return null + + const graphPos = this.graph_mouse + const x = graphPos[0] - node.pos[0] + const y = graphPos[1] - node.pos[1] + + for (const widget of node.widgets) { + if (widget.hidden || (widget.advanced && !node.showAdvanced)) continue + + let widgetWidth, widgetHeight + if (widget.computeSize) { + ;[widgetWidth, widgetHeight] = widget.computeSize(node.size[0]) + } else { + widgetWidth = widget.width || node.size[0] + widgetHeight = LiteGraph.NODE_WIDGET_HEIGHT + } + + if (widget.last_y !== undefined && x >= 6 && x <= widgetWidth - 12 && y >= widget.last_y && y <= widget.last_y + widgetHeight) { + return widget + } } - /** - * Gets the widget at the current cursor position - * @param node Optional node to check for widgets under cursor - * @returns The widget located at the current cursor position or null - */ - getWidgetAtCursor(node?: LGraphNode): IWidget | null { - node ??= this.node_over + return null + } - if (!node.widgets) return null + /** + * Clears highlight and mouse-over information from nodes that should not have it. + * + * Intended to be called when the pointer moves away from a node. + * @param {LGraphNode} node The node that the mouse is now over + * @param {MouseEvent} e MouseEvent that is triggering this + */ + updateMouseOverNodes(node: LGraphNode, e: CanvasMouseEvent): void { + const nodes = this.graph._nodes + const l = nodes.length + for (let i = 0; i < l; ++i) { + if (nodes[i].mouseOver && node != nodes[i]) { + //mouse leave + nodes[i].mouseOver = null + this._highlight_input = null + this._highlight_pos = null + this.link_over_widget = null - const graphPos = this.graph_mouse - const x = graphPos[0] - node.pos[0] - const y = graphPos[1] - node.pos[1] + // Hover transitions + // TODO: Implement single lerp ease factor for current progress on hover in/out. In drawNode, multiply by ease factor and differential value (e.g. bg alpha +0.5). + nodes[i].lostFocusAt = LiteGraph.getTime() - for (const widget of node.widgets) { - if(widget.hidden || (widget.advanced && !node.showAdvanced)) continue; + this.node_over?.onMouseLeave?.(e) + this.node_over = null + this.dirty_canvas = true + } + } + } - let widgetWidth, widgetHeight - if (widget.computeSize) { - ([widgetWidth, widgetHeight] = widget.computeSize(node.size[0])) - } else { - widgetWidth = (widget).width || node.size[0] - widgetHeight = LiteGraph.NODE_WIDGET_HEIGHT - } + processMouseDown(e: CanvasPointerEvent): boolean { + if (this.set_canvas_dirty_on_mouse_event) this.dirty_canvas = true - if ( - widget.last_y !== undefined && - x >= 6 && - x <= widgetWidth - 12 && - y >= widget.last_y && - y <= widget.last_y + widgetHeight - ) { - return widget - } + if (!this.graph) return + + this.adjustMouseEvent(e) + + const ref_window = this.getCanvasWindow() + LGraphCanvas.active_canvas = this + + const x = e.clientX + const y = e.clientY + this.ds.viewport = this.viewport + const 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) + //catch for the entire window + LiteGraph.pointerListenerAdd(ref_window.document, 'move', this._mousemove_callback, true) + LiteGraph.pointerListenerAdd(ref_window.document, 'up', this._mouseup_callback, true) + } + + if (!is_inside) return + + let node = this.graph.getNodeOnPos(e.canvasX, e.canvasY, this.visible_nodes, 5) + let skip_action = false + const now = LiteGraph.getTime() + const is_primary = e.isPrimary === undefined || !e.isPrimary + const 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]] + + this.pointer_is_double = this.pointer_is_down && is_primary + this.pointer_is_down = true + + this.canvas.focus() + + LiteGraph.closeAllContextMenus(ref_window) + + if (this.onMouse?.(e) == true) return + + //left button mouse / single finger + if (e.which == 1 && !this.pointer_is_double) { + if ((e.metaKey || e.ctrlKey) && !e.altKey) { + 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 && !e.ctrlKey && node && this.allow_interaction && !skip_action && !this.read_only) { + const node_data = node.clone()?.serialize() + const cloned = LiteGraph.createNode(node_data.type) + if (cloned) { + cloned.configure(node_data) + cloned.pos[0] += 5 + cloned.pos[1] += 5 + + this.graph.add(cloned, false) + node = cloned + skip_action = true + if (this.allow_dragnodes) { + this.graph.beforeChange() + this.node_dragged = node + this.isDragging = true + } + if (!this.selected_nodes[node.id]) { + this.processNodeSelected(node, e) + } + } + } + + let 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 it wasn't selected? + if (!this.live_mode && !node.flags.pinned) { + this.bringToFront(node) } - return null - } + //not dragging mouse to connect two slots + if (this.allow_interaction && !this.connecting_links && !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 (let i = 0, l = node.outputs.length; i < l; ++i) { + const output = node.outputs[i] + const link_pos = node.getConnectionPos(false, i) + if (isInsideRectangle(e.canvasX, e.canvasY, link_pos[0] - 15, link_pos[1] - 10, 30, 20)) { + // Drag multiple output links + if (e.shiftKey) { + if (output.links?.length > 0) { + this.connecting_links = [] + for (const linkId of output.links) { + const link = this.graph._links.get(linkId) + const slot = link.target_slot + const linked_node = this.graph._nodes_by_id[link.target_id] + const input = linked_node.inputs[slot] + const pos = linked_node.getConnectionPos(true, slot) - /** - * Clears highlight and mouse-over information from nodes that should not have it. - * - * Intended to be called when the pointer moves away from a node. - * @param {LGraphNode} node The node that the mouse is now over - * @param {MouseEvent} e MouseEvent that is triggering this - */ - updateMouseOverNodes(node: LGraphNode, e: CanvasMouseEvent): void { - const nodes = this.graph._nodes - const l = nodes.length - for (let i = 0; i < l; ++i) { - if (nodes[i].mouseOver && node != nodes[i]) { - //mouse leave - nodes[i].mouseOver = null - this._highlight_input = null - this._highlight_pos = null - this.link_over_widget = null - - // Hover transitions - // TODO: Implement single lerp ease factor for current progress on hover in/out. In drawNode, multiply by ease factor and differential value (e.g. bg alpha +0.5). - nodes[i].lostFocusAt = LiteGraph.getTime() - - this.node_over?.onMouseLeave?.(e) - this.node_over = null - this.dirty_canvas = true - } - } - } - - processMouseDown(e: CanvasPointerEvent): boolean { - - if (this.set_canvas_dirty_on_mouse_event) - this.dirty_canvas = true - - if (!this.graph) return - - this.adjustMouseEvent(e) - - const ref_window = this.getCanvasWindow() - LGraphCanvas.active_canvas = this - - const x = e.clientX - const y = e.clientY - this.ds.viewport = this.viewport - const 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) - //catch for the entire window - LiteGraph.pointerListenerAdd(ref_window.document, "move", this._mousemove_callback, true) - LiteGraph.pointerListenerAdd(ref_window.document, "up", this._mouseup_callback, true) - } - - if (!is_inside) return - - let node = this.graph.getNodeOnPos(e.canvasX, e.canvasY, this.visible_nodes, 5) - let skip_action = false - const now = LiteGraph.getTime() - const is_primary = (e.isPrimary === undefined || !e.isPrimary) - const 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]] - - this.pointer_is_double = this.pointer_is_down && is_primary - this.pointer_is_down = true - - this.canvas.focus() - - LiteGraph.closeAllContextMenus(ref_window) - - if (this.onMouse?.(e) == true) return - - //left button mouse / single finger - if (e.which == 1 && !this.pointer_is_double) { - if ((e.metaKey || e.ctrlKey) && !e.altKey) { - 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 && !e.ctrlKey && node && this.allow_interaction && !skip_action && !this.read_only) { - const node_data = node.clone()?.serialize() - const cloned = LiteGraph.createNode(node_data.type) - if (cloned) { - cloned.configure(node_data) - cloned.pos[0] += 5 - cloned.pos[1] += 5 - - this.graph.add(cloned, false) - node = cloned - skip_action = true - if (this.allow_dragnodes) { - this.graph.beforeChange() - this.node_dragged = node - this.isDragging = true - } - if (!this.selected_nodes[node.id]) { - this.processNodeSelected(node, e) - } - } - } - - let 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 it wasn't selected? - if (!this.live_mode && !node.flags.pinned) { - this.bringToFront(node) - } - - //not dragging mouse to connect two slots - if (this.allow_interaction && !this.connecting_links && !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 (let i = 0, l = node.outputs.length; i < l; ++i) { - const output = node.outputs[i] - const link_pos = node.getConnectionPos(false, i) - if (isInsideRectangle( - e.canvasX, - e.canvasY, - link_pos[0] - 15, - link_pos[1] - 10, - 30, - 20 - )) { - // Drag multiple output links - if (e.shiftKey) { - if (output.links?.length > 0) { - - this.connecting_links = [] - for (const linkId of output.links) { - const link = this.graph._links.get(linkId) - const slot = link.target_slot - const linked_node = this.graph._nodes_by_id[link.target_id] - const input = linked_node.inputs[slot] - const pos = linked_node.getConnectionPos(true, slot) - - this.connecting_links.push({ - node: linked_node, - slot: slot, - input: input, - output: null, - pos: pos, - direction: node.horizontal !== true ? LinkDirection.RIGHT : LinkDirection.CENTER, - }) - } - - skip_action = true - break - } - } - - output.slot_index = i - this.connecting_links = [ - { - node: node, - slot: i, - input: null, - output: output, - pos: link_pos, - } - ] - - if (LiteGraph.shift_click_do_break_link_from) { - if (e.shiftKey) { - node.disconnectOutput(i) - } - } else if (LiteGraph.ctrl_alt_click_do_break_link) { - if (e.ctrlKey && e.altKey && !e.shiftKey) { - node.disconnectOutput(i) - } - } - - if (is_double_click) { - node.onOutputDblClick?.(i, e) - } else { - node.onOutputClick?.(i, e) - } - - skip_action = true - break - } - } - } - - //search for inputs - if (node.inputs) { - for (let i = 0, l = node.inputs.length; i < l; ++i) { - const input = node.inputs[i] - const 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) { - node.onInputDblClick?.(i, e) - } else { - node.onInputClick?.(i, e) - } - - if (input.link !== null) { - //before disconnecting - const link_info = this.graph._links.get(input.link) - const slot = link_info.origin_slot - const linked_node = this.graph._nodes_by_id[link_info.origin_id] - if (LiteGraph.click_do_break_link_to || (LiteGraph.ctrl_alt_click_do_break_link && e.ctrlKey && e.altKey && !e.shiftKey)) { - node.disconnectInput(i) - } else if (e.shiftKey) { - this.connecting_links = [{ - node: linked_node, - slot, - output: linked_node.outputs[slot], - pos: linked_node.getConnectionPos(false, slot), - }] - - this.dirty_bgcanvas = true - skip_action = true - } else if (this.allow_reconnect_links) { - if (!LiteGraph.click_do_break_link_to) { - node.disconnectInput(i) - } - this.connecting_links = [ - { - node: linked_node, - slot: slot, - input: null, - output: linked_node.outputs[slot], - pos: linked_node.getConnectionPos(false, slot), - } - ] - - this.dirty_bgcanvas = true - skip_action = true - } else { - // do same action as has not node ? - } - - } else { - // has not node - } - - if (!skip_action) { - // connect from in to out, from to to from - this.connecting_links = [ - { - node: node, - slot: i, - input: input, - output: null, - pos: link_pos, - } - ] - - this.dirty_bgcanvas = true - skip_action = true - } - - break - } - } - } - } - } - - //it wasn't clicked on the links boxes - if (!skip_action) { - let block_drag_node = node?.pinned ? true : false - const pos: Point = [e.canvasX - node.pos[0], e.canvasY - node.pos[1]] - - //widgets - const 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]) { - // Check if it's a double click on the title bar - // Note: pos[1] is the y-coordinate of the node's body - // If clicking on node header (title), pos[1] is negative - if (pos[1] < 0) { - node.onNodeTitleDblClick?.(e, pos, this) - } - //double click node - node.onDblClick?.(e, pos, this) - this.processNodeDblClicked(node) - block_drag_node = true - } - - //if do not capture mouse - if (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) { - const 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.isDragging = true - } - // Account for shift + click + drag - if (!(e.shiftKey && !e.ctrlKey && !e.altKey) || !node.is_selected) { - 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) { - // Set the width of the line for isPointInStroke checks - const lineWidth = this.ctx.lineWidth - this.ctx.lineWidth = this.connections_width + 7 - for (let i = 0; i < this.visible_links.length; ++i) { - const link = this.visible_links[i] - const center = link._pos - let overLink: LLink = null - if (!center || - e.canvasX < center[0] - 4 || - e.canvasX > center[0] + 4 || - e.canvasY < center[1] - 4 || - e.canvasY > center[1] + 4) { - // If we shift click on a link then start a link from that input - if (e.shiftKey && link.path && this.ctx.isPointInStroke(link.path, e.canvasX, e.canvasY)) { - overLink = link - } else { - continue - } - } - if (overLink) { - const slot = overLink.origin_slot - const originNode = this.graph._nodes_by_id[overLink.origin_id] - - this.connecting_links ??= [] - this.connecting_links.push({ - node: originNode, - slot, - output: originNode.outputs[slot], - pos: originNode.getConnectionPos(false, slot), - }) - skip_action = true - } else { - //link clicked - this.showLinkMenu(link, e) - this.over_link_center = null //clear tooltip - } - break - } - - // Restore line width - this.ctx.lineWidth = lineWidth - } - - this.selected_group = this.graph.getGroupOnPos(e.canvasX, e.canvasY) - this.selected_group_resizing = false - - const group = this.selected_group - if (this.selected_group && !this.read_only) { - if (e.ctrlKey) { - this.dragging_rectangle = null - } - - const 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 { - const f = group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE - const headerHeight = f * 1.4 - if (isInsideRectangle(e.canvasX, e.canvasY, group.pos[0], group.pos[1], group.size[0], headerHeight)) { - this.selected_group.recomputeInsideNodes() - if (!e.shiftKey && !e.ctrlKey && !e.metaKey) this.deselectAllNodes() - this.selectedGroups ??= new Set() - this.selectedGroups.add(group) - group.selected = true - - this.isDragging = true - skip_action = true - } - } - - if (is_double_click) { - this.emitEvent({ - subType: "group-double-click", - originalEvent: e, - group: this.selected_group, - }) - } - } else if (is_double_click && !this.read_only) { - // Double click within group should not trigger the searchbox. - if (this.allow_searchbox) { - this.showSearchBox(e) - e.preventDefault() - e.stopPropagation() - } - this.emitEvent({ - subType: "empty-double-click", - originalEvent: e, + this.connecting_links.push({ + node: linked_node, + slot: slot, + input: input, + output: null, + pos: pos, + direction: node.horizontal !== true ? LinkDirection.RIGHT : LinkDirection.CENTER, }) - } + } - clicking_canvas_bg = true + skip_action = true + break + } + } + + output.slot_index = i + this.connecting_links = [ + { + node: node, + slot: i, + input: null, + output: output, + pos: link_pos, + }, + ] + + if (LiteGraph.shift_click_do_break_link_from) { + if (e.shiftKey) { + node.disconnectOutput(i) + } + } else if (LiteGraph.ctrl_alt_click_do_break_link) { + if (e.ctrlKey && e.altKey && !e.shiftKey) { + node.disconnectOutput(i) + } + } + + if (is_double_click) { + node.onOutputDblClick?.(i, e) + } else { + node.onOutputClick?.(i, e) + } + + skip_action = true + break } + } } - if (!skip_action && clicking_canvas_bg && this.allow_dragcanvas) { - 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_links && - !node.flags.collapsed && - !this.live_mode) { - let mClikSlot: INodeSlot | false = false - let mClikSlot_index: number | false = false - let mClikSlot_isOut: boolean = false - //search for outputs - if (node.outputs) { - for (let i = 0, l = node.outputs.length; i < l; ++i) { - const output = node.outputs[i] - const 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 (let i = 0, l = node.inputs.length; i < l; ++i) { - const input = node.inputs[i] - const 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 - } - } - } - // Middle clicked a slot - if (mClikSlot && mClikSlot_index !== false) { - - const alphaPosY = 0.5 - ((mClikSlot_index + 1) / ((mClikSlot_isOut ? node.outputs.length : node.inputs.length))) - const 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 - const posRef: Point = [ - (!mClikSlot_isOut ? node_bounding[0] : node_bounding[0] + node_bounding[2]), - e.canvasY - 80 - ] - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const 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, - nodeType: "AUTO", - posAdd: [!mClikSlot_isOut ? -30 : 30, -alphaPosY * 130], - posSizeFix: [!mClikSlot_isOut ? -1 : 0, 0] - }) - skip_action = true - } - } - } - } - - // Drag canvas using middle mouse button - if (!skip_action && this.allow_dragcanvas) { - 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 context menu for the node or group under the pointer - this.processContextMenu(node, e) - } - - } - - this.last_mouse[0] = e.clientX - this.last_mouse[1] = e.clientY - this.last_mouseclick = LiteGraph.getTime() - this.last_mouse_dragging = true - - 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() - - this.onMouseDown?.(e) - - return false - } - /** - * Called when a mouse move event has to be processed - **/ - processMouseMove(e: CanvasMouseEvent): boolean { - 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) - const mouse: Point = [e.clientX, e.clientY] - this.mouse[0] = mouse[0] - this.mouse[1] = mouse[1] - const 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 - - if (this.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 - const 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_resizing && !this.read_only) { - //moving/resizing a group - this.selected_group.resize( - e.canvasX - this.selected_group.pos[0], - e.canvasY - this.selected_group.pos[1] - ) - this.dirty_bgcanvas = true - } else if (this.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_links) this.dirty_canvas = true - - //remove mouseover flag - this.updateMouseOverNodes(node, e) - - //mouse over a node - if (node) { - - if (node.redraw_on_mouse) - this.dirty_canvas = true - - // For input/output hovering - //to store the output of isOverNodeInput - const pos: Point = [0, 0] - const inputId = this.isOverNodeInput(node, e.canvasX, e.canvasY, pos) - const outputId = this.isOverNodeOutput(node, e.canvasX, e.canvasY, pos) - const overWidget = this.getWidgetAtCursor(node) - - if (!node.mouseOver) { - //mouse enter - node.mouseOver = { - inputId: null, - outputId: null, - overWidget: null, - } - this.node_over = node - this.dirty_canvas = true - - node.onMouseEnter?.(e) - } - - //in case the node wants to do something - node.onMouseMove?.(e, [e.canvasX - node.pos[0], e.canvasY - node.pos[1]], this) - - // The input the mouse is over has changed - if (node.mouseOver.inputId !== inputId || node.mouseOver.outputId !== outputId || node.mouseOver.overWidget !== overWidget) { - node.mouseOver.inputId = inputId - node.mouseOver.outputId = outputId - node.mouseOver.overWidget = overWidget - - // Check if link is over anything it could connect to - record position of valid target for snap / highlight - if (this.connecting_links) { - const firstLink = this.connecting_links[0] - - // Default: nothing highlighted - let highlightPos: Point = null - let highlightInput: INodeInputSlot = null - let linkOverWidget: IWidget = null - - if (firstLink.node === node) { - // Cannot connect link from a node to itself - } else if (firstLink.output) { - - // Connecting from an output to an input - - if (inputId === -1 && outputId === -1) { - // Allow support for linking to widgets, handled externally to LiteGraph - if (this.getWidgetLinkType && overWidget) { - const widgetLinkType = this.getWidgetLinkType(overWidget, node) - if (widgetLinkType && LiteGraph.isValidConnection(firstLink.output.type, widgetLinkType)) { - if (firstLink.node.isValidWidgetLink?.(firstLink.output.slot_index, node, overWidget) !== false) { - linkOverWidget = overWidget - this.link_over_widget_type = widgetLinkType - } - } - } - // Node background / title under the pointer - if (!linkOverWidget) { - const targetSlotId = firstLink.node.findConnectByTypeSlot(true, node, firstLink.output.type) - if (targetSlotId !== null && targetSlotId >= 0) { - node.getConnectionPos(true, targetSlotId, pos) - highlightPos = pos - highlightInput = node.inputs[targetSlotId] - } - } - } else 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 - if (inputId != -1 && node.inputs[inputId] && LiteGraph.isValidConnection(firstLink.output.type, node.inputs[inputId].type)) { - highlightPos = pos - highlightInput = node.inputs[inputId] // XXX CHECK THIS - } - } - - } else if (firstLink.input) { - - // Connecting from an input to an output - if (inputId === -1 && outputId === -1) { - const targetSlotId = firstLink.node.findConnectByTypeSlot(false, node, firstLink.input.type) - if (targetSlotId !== null && targetSlotId >= 0) { - node.getConnectionPos(false, targetSlotId, pos) - highlightPos = pos - } - } else 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 - if (outputId != -1 && node.outputs[outputId] && LiteGraph.isValidConnection(firstLink.input.type, node.outputs[outputId].type)) { - highlightPos = pos - } - } - } - this._highlight_pos = highlightPos - this._highlight_input = highlightInput - this.link_over_widget = linkOverWidget - } - - this.dirty_canvas = true - } - - //Search for corner - if (this.canvas) { - this.canvas.style.cursor = node.inResizeCorner(e.canvasX, e.canvasY) - ? "se-resize" - : "crosshair" - } - } else { //not over a node - //search for link connector - let over_link: LLink = null - for (let i = 0; i < this.visible_links.length; ++i) { - const link = this.visible_links[i] - const 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?.(e, [e.canvasX - this.node_capturing_input.pos[0], e.canvasY - this.node_capturing_input.pos[1]], this) - } - - //node being dragged - if (this.isDragging && !this.live_mode) { - //console.log("draggin!",this.selected_nodes); - const nodes = new Set() - const deltax = delta[0] / this.ds.scale - const deltay = delta[1] / this.ds.scale - for (const i in this.selected_nodes) { - const n = this.selected_nodes[i] - nodes.add(n) - n.pos[0] += delta[0] / this.ds.scale - n.pos[1] += delta[1] / this.ds.scale - /* - * Don't call the function if the block is already selected. - * Otherwise, it could cause the block to be unselected while dragging. - */ - if (!n.is_selected) this.processNodeSelected(n, e) - - } - - if (this.selectedGroups) { - for (const group of this.selectedGroups) { - group.move(deltax, deltay, true) - if (!e.ctrlKey) { - for (const node of group._nodes) { - if (!nodes.has(node)) { - node.pos[0] += deltax - node.pos[1] += deltay - } - } - } - } - } - - this.dirty_canvas = true - this.dirty_bgcanvas = true - } - - if (this.resizing_node && !this.live_mode) { - //convert mouse to node space - const desired_size: Size = [e.canvasX - this.resizing_node.pos[0], e.canvasY - this.resizing_node.pos[1]] - const 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 - **/ - processMouseUp(e: CanvasPointerEvent): boolean { - //early exit for extra pointer - if (e.isPrimary === false) return false - if (!this.graph) return - - const window = this.getCanvasWindow() - const document = window.document - LGraphCanvas.active_canvas = this - - //restore the mousemove event back to the canvas - if (!this.options.skip_events) { - 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) - const now = LiteGraph.getTime() - e.click_time = now - this.last_mouseclick - this.last_mouse_dragging = false - this.last_click_position = null - - //used to avoid sending twice a click in an immediate button - this.block_click &&= false - - 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) { - const diffx = this.selected_group.pos[0] - - Math.round(this.selected_group.pos[0]) - const 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 - this.isDragging = false - - let node = this.graph.getNodeOnPos( - e.canvasX, - e.canvasY, - this.visible_nodes - ) - - if (this.dragging_rectangle) { - if (this.graph) { - const nodes = this.graph._nodes - const node_bounding = new Float32Array(4) - - //compute bounding and flip if left to right - const w = Math.abs(this.dragging_rectangle[2]) - const h = Math.abs(this.dragging_rectangle[3]) - const startx = this.dragging_rectangle[2] < 0 - ? this.dragging_rectangle[0] - w - : this.dragging_rectangle[0] - const 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 - const to_select = [] - for (let i = 0; i < nodes.length; ++i) { - const 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 - } - - // Select groups - if (!e.shiftKey) this.deselectGroups() - this.selectedGroups ??= new Set() - - const groups = this.graph.groups - for (const group of groups) { - const r = this.dragging_rectangle - const pos = group.pos - const size = group.size - if (!isInsideRectangle(pos[0], pos[1], r[0], r[1], r[2], r[3]) || !isInsideRectangle(pos[0] + size[0], pos[1] + size[1], r[0], r[1], r[2], r[3])) continue - this.selectedGroups.add(group) - group.recomputeInsideNodes() - group.selected = true - } - } else { - // will select of update selection - this.selectNodes([node], e.shiftKey || e.ctrlKey || e.metaKey) // add to selection add to selection with ctrlKey or shiftKey - } - - } - this.dragging_rectangle = null - } else if (this.connecting_links) { - - //node below mouse - if (node) { - for (const link of this.connecting_links) { - - //dragging a connection - this.dirty_canvas = true - this.dirty_bgcanvas = true - - //slot below mouse? connect - if (link.output) { - - const slot = this.isOverNodeInput( - node, - e.canvasX, - e.canvasY - ) - if (slot != -1) { - link.node.connect(link.slot, node, slot) - } else if (this.link_over_widget) { - this.emitEvent({ - subType: "connectingWidgetLink", - link, - node, - widget: this.link_over_widget - }) - this.link_over_widget = null - } else { - //not on top of an input - // look for a good slot - link.node.connectByType(link.slot, node, link.output.type) - } - } else if (link.input) { - const slot = this.isOverNodeOutput( - node, - e.canvasX, - e.canvasY - ) - - if (slot != -1) { - node.connect(slot, link.node, link.slot) // this is inverted has output-input nature like - } else { - //not on top of an input - // look for a good slot - link.node.connectByTypeOutput(link.slot, node, link.input.type) - } - } - } - } else { - const firstLink = this.connecting_links[0] - const linkReleaseContext = firstLink.output ? { - node_from: firstLink.node, - slot_from: firstLink.output, - type_filter_in: firstLink.output.type - } : { - node_to: firstLink.node, - slot_from: firstLink.input, - type_filter_out: firstLink.input.type - } - // For external event only. - const linkReleaseContextExtended: LinkReleaseContextExtended = { - links: this.connecting_links, - } - this.emitEvent({ - subType: "empty-release", - originalEvent: e, - linkReleaseContext: linkReleaseContextExtended, - }) - // add menu when releasing link in empty space - if (LiteGraph.release_link_on_empty_shows_menu) { - if (e.shiftKey) { - if (this.allow_searchbox) { - this.showSearchBox(e, linkReleaseContext) - } - } else { - if (firstLink.output) { - this.showConnectionMenu({ nodeFrom: firstLink.node, slotFrom: firstLink.output, e: e }) - } else if (firstLink.input) { - this.showConnectionMenu({ nodeTo: firstLink.node, slotTo: firstLink.input, e: e }) - } - } - } - } - - this.connecting_links = null - } //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? - 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() - } - this.onNodeMoved?.(this.node_dragged) - this.graph.afterChange(this.node_dragged) - this.node_dragged = null - } //no node being dragged - else { - //get node over - node = this.graph.getNodeOnPos( - e.canvasX, - e.canvasY, - this.visible_nodes - ) - - if (!node && e.click_time < 300 && !this.graph.groups.some(x => x.isPointInTitlebar(e.canvasX, e.canvasY))) { - this.deselectAllNodes() - } - - this.dirty_canvas = true - this.dragging_canvas = false - - // @ts-expect-error Unused param - this.node_over?.onMouseUp?.(e, [e.canvasX - this.node_over.pos[0], e.canvasY - this.node_over.pos[1]], this) - 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 - this.dirty_canvas = true - this.dragging_canvas = false - } else if (e.which == 3) { - //right button - this.dirty_canvas = true - this.dragging_canvas = false - } - - this.pointer_is_down = false - this.pointer_is_double = false - - this.graph.change() - - e.stopPropagation() - e.preventDefault() - return false - } - - /** - * Called when the mouse moves off the canvas. Clears all node hover states. - * @param e - */ - processMouseOut(e: CanvasMouseEvent): void { - // TODO: Check if document.contains(e.relatedTarget) - handle mouseover node textarea etc. - this.updateMouseOverNodes(null, e) - } - - /** - * Called when a mouse wheel event has to be processed - **/ - processMouseWheel(e: CanvasWheelEvent): boolean { - if (!this.graph || !this.allow_dragcanvas) return - - // TODO: Mouse wheel zoom rewrite - // @ts-expect-error - const delta = e.wheelDeltaY ?? e.detail * -60 - - this.adjustMouseEvent(e) - - const pos: Point = [e.clientX, e.clientY] - if (this.viewport && !isPointInRectangle(pos, this.viewport)) return - - let scale = this.ds.scale - - if (delta > 0) scale *= this.zoom_speed - else if (delta < 0) scale *= 1 / this.zoom_speed - - this.ds.changeScale(scale, [e.clientX, e.clientY]) - - this.graph.change() - - e.preventDefault() - return false - } - /** - * 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 { - const title_height = LiteGraph.NODE_TITLE_HEIGHT - return Boolean(isInsideRectangle( - canvasx, - canvasy, - node.pos[0] + 2, - node.pos[1] + 2 - title_height, - title_height - 4, - title_height - 4 - )) - } - /** - * returns the INDEX if a position (in graph space) is on top of a node input slot - **/ - isOverNodeInput(node: LGraphNode, canvasx: number, canvasy: number, slot_pos?: Point): number { - if (node.inputs) { - for (let i = 0, l = node.inputs.length; i < l; ++i) { + //search for inputs + if (node.inputs) { + for (let i = 0, l = node.inputs.length; i < l; ++i) { const input = node.inputs[i] const link_pos = node.getConnectionPos(true, i) - let is_inside = false - if (node.horizontal) { - is_inside = isInsideRectangle( - canvasx, - canvasy, - link_pos[0] - 5, - link_pos[1] - 10, - 10, - 20 - ) - } else { - // TODO: Find a cheap way to measure text, and do it on node label change instead of here - // Input icon width + text approximation - const width = 20 + (((input.label?.length ?? input.name?.length) || 3) * 7) - is_inside = isInsideRectangle( - canvasx, - canvasy, - link_pos[0] - 10, - link_pos[1] - 10, - width, - 20 - ) - } - 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 - **/ - isOverNodeOutput(node: LGraphNode, canvasx: number, canvasy: number, slot_pos?: Point): number { - if (node.outputs) { - for (let i = 0, l = node.outputs.length; i < l; ++i) { - const link_pos = node.getConnectionPos(false, i) - let 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] - 10, - 40, - 20 - ) - } - 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 - **/ - processKey(e: KeyboardEvent): boolean | null { - if (!this.graph) return - - let block_default = false - //console.log(e); //debug - // @ts-expect-error - if (e.target.localName == "input") return - - if (e.type == "keydown") { - // TODO: Switch - if (e.keyCode == 32) { - // space - this.read_only = true - if (this._previously_dragging_canvas === null) { - this._previously_dragging_canvas = this.dragging_canvas - } - this.dragging_canvas = this.pointer_is_down - block_default = true - } - - else if (e.keyCode == 27) { - //esc - this.node_panel?.close() - this.options_panel?.close() - block_default = true - } - - //select all Control A - else if (e.keyCode == 65 && e.ctrlKey) { - this.selectNodes() - block_default = true - } - - else if ((e.keyCode === 67) && (e.metaKey || e.ctrlKey) && !e.shiftKey) { - //copy - if (this.selected_nodes) { - this.copyToClipboard() - block_default = true - } - } - - else if ((e.keyCode === 86) && (e.metaKey || e.ctrlKey)) { - //paste - this.pasteFromClipboard(e.shiftKey) - } - - //delete or backspace - else if (e.keyCode == 46 || e.keyCode == 8) { - // @ts-expect-error - if (e.target.localName != "input" && e.target.localName != "textarea") { - this.deleteSelectedNodes() - block_default = true - } - } - - //collapse - //... - //TODO - if (this.selected_nodes) { - for (const i in this.selected_nodes) { - this.selected_nodes[i].onKeyDown?.(e) - } - } - } else if (e.type == "keyup") { - if (e.keyCode == 32) { - // space - this.read_only = false - this.dragging_canvas = this._previously_dragging_canvas ?? false - this._previously_dragging_canvas = null - } - - if (this.selected_nodes) { - for (const i in this.selected_nodes) { - this.selected_nodes[i].onKeyUp?.(e) - } - } - } - - // TODO: Do we need to remeasure and recalculate everything on every key down/up? - this.graph.change() - - if (block_default) { - e.preventDefault() - e.stopImmediatePropagation() - return false - } - } - copyToClipboard(nodes?: Dictionary): void { - const clipboard_info: IClipboardContents = { - nodes: [], - links: [] - } - let index = 0 - const selected_nodes_array: LGraphNode[] = [] - if (!nodes) nodes = this.selected_nodes - for (const i in nodes) { - const node = nodes[i] - if (node.clonable === false) continue - - node._relative_id = index - selected_nodes_array.push(node) - index += 1 - } - - for (let i = 0; i < selected_nodes_array.length; ++i) { - const node = selected_nodes_array[i] - const cloned = node.clone() - if (!cloned) { - console.warn("node type not found: " + node.type) - continue - } - clipboard_info.nodes.push(cloned.serialize()) - if (node.inputs?.length) { - for (let j = 0; j < node.inputs.length; ++j) { - const input = node.inputs[j] - if (!input || input.link == null) continue + if (isInsideRectangle(e.canvasX, e.canvasY, link_pos[0] - 15, link_pos[1] - 10, 30, 20)) { + if (is_double_click) { + node.onInputDblClick?.(i, e) + } else { + node.onInputClick?.(i, e) + } + if (input.link !== null) { + //before disconnecting const link_info = this.graph._links.get(input.link) - if (!link_info) continue + const slot = link_info.origin_slot + const linked_node = this.graph._nodes_by_id[link_info.origin_id] + if (LiteGraph.click_do_break_link_to || (LiteGraph.ctrl_alt_click_do_break_link && e.ctrlKey && e.altKey && !e.shiftKey)) { + node.disconnectInput(i) + } else if (e.shiftKey) { + this.connecting_links = [ + { + node: linked_node, + slot, + output: linked_node.outputs[slot], + pos: linked_node.getConnectionPos(false, slot), + }, + ] - const target_node = this.graph.getNodeById(link_info.origin_id) - if (!target_node) continue + this.dirty_bgcanvas = true + skip_action = true + } else if (this.allow_reconnect_links) { + if (!LiteGraph.click_do_break_link_to) { + node.disconnectInput(i) + } + this.connecting_links = [ + { + node: linked_node, + slot: slot, + input: null, + output: linked_node.outputs[slot], + pos: linked_node.getConnectionPos(false, slot), + }, + ] - 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) - ) - } - - emitEvent(detail: CanvasEventDetail): void { - this.canvas.dispatchEvent(new CustomEvent( - "litegraph:canvas", - { - bubbles: true, - detail - } - )) - } - - emitBeforeChange(): void { - this.emitEvent({ - subType: "before-change", - }) - } - - emitAfterChange(): void { - this.emitEvent({ - subType: "after-change", - }) - } - - _pasteFromClipboard(isConnectUnselected = false): void { - // 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 - const data = localStorage.getItem("litegrapheditor_clipboard") - if (!data) return - - this.graph.beforeChange() - - //create nodes - const clipboard_info: IClipboardContents = 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 - let posMin: false | [number, number] = false - let posMinIndexes: false | [number, number] = false - for (let 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] - } - } - const nodes: LGraphNode[] = [] - for (let i = 0; i < clipboard_info.nodes.length; ++i) { - const node_data = clipboard_info.nodes[i] - const 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, true) - - nodes.push(node) - } - } - - //create links - for (let i = 0; i < clipboard_info.links.length; ++i) { - const link_info = clipboard_info.links[i] - let origin_node: LGraphNode = undefined - const 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) { - const origin_node_id = link_info[4] - if (origin_node_id) { - origin_node = this.graph.getNodeById(origin_node_id) - } - } - const 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() - } - - pasteFromClipboard(isConnectUnselected = false): void { - this.emitBeforeChange() - try { - this._pasteFromClipboard(isConnectUnselected) - } finally { - this.emitAfterChange() - } - } - /** - * process a item drop event on top the canvas - **/ - processDrop(e: CanvasDragEvent): boolean { - e.preventDefault() - this.adjustMouseEvent(e) - const x = e.clientX - const y = e.clientY - const 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 - - const pos = [e.canvasX, e.canvasY] - const node = this.graph ? this.graph.getNodeOnPos(pos[0], pos[1]) : null - - if (!node) { - const r = this.onDropItem?.(e) - if (!r) this.checkDropItem(e) - return - } - - if (node.onDropFile || node.onDropData) { - const files = e.dataTransfer.files - if (files && files.length) { - for (let i = 0; i < files.length; i++) { - const file = e.dataTransfer.files[0] - const filename = file.name - node.onDropFile?.(file) - - if (node.onDropData) { - //prepare reader - const reader = new FileReader() - reader.onload = function (event) { - const data = event.target.result - node.onDropData(data, filename, file) - } - - //read data - const type = file.type.split("/")[0] - if (type == "text" || type == "") { - reader.readAsText(file) - } else if (type == "image") { - reader.readAsDataURL(file) - } else { - reader.readAsArrayBuffer(file) - } + this.dirty_bgcanvas = true + skip_action = true + } else { + // do same action as has not node ? } + } else { + // has not node + } + + if (!skip_action) { + // connect from in to out, from to to from + this.connecting_links = [ + { + node: node, + slot: i, + input: input, + output: null, + pos: link_pos, + }, + ] + + this.dirty_bgcanvas = true + skip_action = true + } + + break } + } } + } } - if (node.onDropItem?.(e)) return true + //it wasn't clicked on the links boxes + if (!skip_action) { + let block_drag_node = node?.pinned ? true : false + const pos: Point = [e.canvasX - node.pos[0], e.canvasY - node.pos[1]] - return this.onDropItem - ? this.onDropItem(e) - : false - } - //called if the graph doesn't have a default drop item behaviour - checkDropItem(e: CanvasDragEvent): void { - if (!e.dataTransfer.files.length) return + //widgets + const widget = this.processNodeWidgets(node, this.graph_mouse, e) + if (widget) { + block_drag_node = true + this.node_widget = [node, widget] + } - const file = e.dataTransfer.files[0] - const ext = LGraphCanvas.getFileExtension(file.name).toLowerCase() - const nodetype = LiteGraph.node_types_by_file_extension[ext] - if (!nodetype) return + //double clicking + if (this.allow_interaction && is_double_click && this.selected_nodes[node.id]) { + // Check if it's a double click on the title bar + // Note: pos[1] is the y-coordinate of the node's body + // If clicking on node header (title), pos[1] is negative + if (pos[1] < 0) { + node.onNodeTitleDblClick?.(e, pos, this) + } + //double click node + node.onDblClick?.(e, pos, this) + this.processNodeDblClicked(node) + block_drag_node = true + } - this.graph.beforeChange() - const node = LiteGraph.createNode(nodetype.type) - node.pos = [e.canvasX, e.canvasY] - this.graph.add(node) - node.onDropFile?.(file) - this.graph.afterChange() - } - processNodeDblClicked(n: LGraphNode): void { - this.onShowNodePanel?.(n) - this.onNodeDblClicked?.(n) - - this.setDirty(true) - } - processNodeSelected(node: LGraphNode, e: CanvasMouseEvent): void { - this.selectNode(node, e && (e.shiftKey || e.metaKey || e.ctrlKey || this.multi_select)) - this.onNodeSelected?.(node) - } - /** - * selects a given node (or adds it to the current selection) - **/ - selectNode(node: LGraphNode, add_to_current_selection?: boolean): void { - if (node == null) { - this.deselectAllNodes() - } else { - this.selectNodes([node], add_to_current_selection) - } - } - /** - * selects several nodes (or adds them to the current selection) - **/ - selectNodes(nodes?: LGraphNode[] | Dictionary, add_to_current_selection?: boolean): void { - if (!add_to_current_selection) { - this.deselectAllNodes() - } - - nodes = nodes || this.graph._nodes - if (typeof nodes == "string") nodes = [nodes] - for (const i in nodes) { - const node: LGraphNode = nodes[i] - if (node.is_selected) { - this.deselectNode(node) - continue + //if do not capture mouse + if (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) { + const that = this + setTimeout(function () { + that.openSubgraph(node.subgraph) + }, 10) + } } - if (!node.is_selected) { - node.onSelected?.() + if (this.live_mode) { + clicking_canvas_bg = true + block_drag_node = true } - node.is_selected = true - this.selected_nodes[node.id] = node + } - if (node.inputs) { - for (let j = 0; j < node.inputs.length; ++j) { - this.highlighted_links[node.inputs[j].link] = true + if (!block_drag_node) { + if (this.allow_dragnodes) { + this.graph.beforeChange() + this.node_dragged = node + this.isDragging = true + } + // Account for shift + click + drag + if (!(e.shiftKey && !e.ctrlKey && !e.altKey) || !node.is_selected) { + 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) { + // Set the width of the line for isPointInStroke checks + const lineWidth = this.ctx.lineWidth + this.ctx.lineWidth = this.connections_width + 7 + for (let i = 0; i < this.visible_links.length; ++i) { + const link = this.visible_links[i] + const center = link._pos + let overLink: LLink = null + if (!center || e.canvasX < center[0] - 4 || e.canvasX > center[0] + 4 || e.canvasY < center[1] - 4 || e.canvasY > center[1] + 4) { + // If we shift click on a link then start a link from that input + if (e.shiftKey && link.path && this.ctx.isPointInStroke(link.path, e.canvasX, e.canvasY)) { + overLink = link + } else { + continue } + } + if (overLink) { + const slot = overLink.origin_slot + const originNode = this.graph._nodes_by_id[overLink.origin_id] + + this.connecting_links ??= [] + this.connecting_links.push({ + node: originNode, + slot, + output: originNode.outputs[slot], + pos: originNode.getConnectionPos(false, slot), + }) + skip_action = true + } else { + //link clicked + this.showLinkMenu(link, e) + this.over_link_center = null //clear tooltip + } + break } + + // Restore line width + this.ctx.lineWidth = lineWidth + } + + this.selected_group = this.graph.getGroupOnPos(e.canvasX, e.canvasY) + this.selected_group_resizing = false + + const group = this.selected_group + if (this.selected_group && !this.read_only) { + if (e.ctrlKey) { + this.dragging_rectangle = null + } + + const 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 { + const f = group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE + const headerHeight = f * 1.4 + if (isInsideRectangle(e.canvasX, e.canvasY, group.pos[0], group.pos[1], group.size[0], headerHeight)) { + this.selected_group.recomputeInsideNodes() + if (!e.shiftKey && !e.ctrlKey && !e.metaKey) this.deselectAllNodes() + this.selectedGroups ??= new Set() + this.selectedGroups.add(group) + group.selected = true + + this.isDragging = true + skip_action = true + } + } + + if (is_double_click) { + this.emitEvent({ + subType: 'group-double-click', + originalEvent: e, + group: this.selected_group, + }) + } + } else if (is_double_click && !this.read_only) { + // Double click within group should not trigger the searchbox. + if (this.allow_searchbox) { + this.showSearchBox(e) + e.preventDefault() + e.stopPropagation() + } + this.emitEvent({ + subType: 'empty-double-click', + originalEvent: e, + }) + } + + clicking_canvas_bg = true + } + } + + if (!skip_action && clicking_canvas_bg && this.allow_dragcanvas) { + 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_links && !node.flags.collapsed && !this.live_mode) { + let mClikSlot: INodeSlot | false = false + let mClikSlot_index: number | false = false + let mClikSlot_isOut: boolean = false + //search for outputs if (node.outputs) { - for (let j = 0; j < node.outputs.length; ++j) { - const out = node.outputs[j] - if (out.links) { - for (let k = 0; k < out.links.length; ++k) { - this.highlighted_links[out.links[k]] = true - } - } + for (let i = 0, l = node.outputs.length; i < l; ++i) { + const output = node.outputs[i] + const 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 } + } } - } - this.onSelectionChange?.(this.selected_nodes) - - this.setDirty(true) - } - /** - * removes a node from the current selection - **/ - deselectNode(node: LGraphNode): void { - if (!node.is_selected) return - node.onDeselected?.() - node.is_selected = false - delete this.selected_nodes[node.id] - - this.onNodeDeselected?.(node) - - //remove highlighted - if (node.inputs) { - for (let i = 0; i < node.inputs.length; ++i) { - delete this.highlighted_links[node.inputs[i].link] - } - } - if (node.outputs) { - for (let i = 0; i < node.outputs.length; ++i) { - const out = node.outputs[i] - if (out.links) { - for (let j = 0; j < out.links.length; ++j) { - delete this.highlighted_links[out.links[j]] - } + //search for inputs + if (node.inputs) { + for (let i = 0, l = node.inputs.length; i < l; ++i) { + const input = node.inputs[i] + const 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 } + } } - } - } - /** - * removes all nodes from the current selection - **/ - deselectAllNodes(): void { - if (!this.graph) return - const nodes = this.graph._nodes - for (let i = 0, l = nodes.length; i < l; ++i) { - const node = nodes[i] - if (!node.is_selected) { - continue + // Middle clicked a slot + if (mClikSlot && mClikSlot_index !== false) { + const alphaPosY = 0.5 - (mClikSlot_index + 1) / (mClikSlot_isOut ? node.outputs.length : node.inputs.length) + const 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 + const posRef: Point = [!mClikSlot_isOut ? node_bounding[0] : node_bounding[0] + node_bounding[2], e.canvasY - 80] + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const 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, + nodeType: 'AUTO', + posAdd: [!mClikSlot_isOut ? -30 : 30, -alphaPosY * 130], + posSizeFix: [!mClikSlot_isOut ? -1 : 0, 0], + }) + skip_action = true } - node.onDeselected?.() - node.is_selected = false - this.onNodeDeselected?.(node) + } } - this.selected_nodes = {} - this.current_node = null - this.highlighted_links = {} - this.deselectGroups() + } - this.onSelectionChange?.(this.selected_nodes) - this.setDirty(true) + // Drag canvas using middle mouse button + if (!skip_action && this.allow_dragcanvas) { + 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 context menu for the node or group under the pointer + this.processContextMenu(node, e) + } } - deselectGroups() { - if (!this.selectedGroups) return - for (const group of this.selectedGroups) { - delete group.selected - } - this.selectedGroups = null + this.last_mouse[0] = e.clientX + this.last_mouse[1] = e.clientY + this.last_mouseclick = LiteGraph.getTime() + this.last_mouse_dragging = true + + 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() + + this.onMouseDown?.(e) + + return false + } + /** + * Called when a mouse move event has to be processed + **/ + processMouseMove(e: CanvasMouseEvent): boolean { + 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) + const mouse: Point = [e.clientX, e.clientY] + this.mouse[0] = mouse[0] + this.mouse[1] = mouse[1] + const 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 + + if (this.block_click) { + e.preventDefault() + return false } - /** - * deletes all nodes in the current selection from the graph - **/ - deleteSelectedNodes(): void { + e.dragging = this.last_mouse_dragging - this.graph.beforeChange() + if (this.node_widget) { + this.processNodeWidgets(this.node_widget[0], this.graph_mouse, e, this.node_widget[1]) + this.dirty_canvas = true + } - for (const i in this.selected_nodes) { - const node = this.selected_nodes[i] + //get node over + const node = this.graph.getNodeOnPos(e.canvasX, e.canvasY, this.visible_nodes) - if (node.block_delete) continue + 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_resizing && !this.read_only) { + //moving/resizing a group + this.selected_group.resize(e.canvasX - this.selected_group.pos[0], e.canvasY - this.selected_group.pos[1]) + this.dirty_bgcanvas = true + } else if (this.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_links) this.dirty_canvas = true - //autoconnect when possible (very basic, only takes into account first input-output) - if (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) { - const input_link = node.graph._links.get(node.inputs[0].link) - const output_link = node.graph._links.get(node.outputs[0].links[0]) - const input_node = node.getInputNode(0) - const output_node = node.getOutputNodes(0)[0] - if (input_node && output_node) - input_node.connect(input_link.origin_slot, output_node, output_link.target_slot) + //remove mouseover flag + this.updateMouseOverNodes(node, e) + + //mouse over a node + if (node) { + if (node.redraw_on_mouse) this.dirty_canvas = true + + // For input/output hovering + //to store the output of isOverNodeInput + const pos: Point = [0, 0] + const inputId = this.isOverNodeInput(node, e.canvasX, e.canvasY, pos) + const outputId = this.isOverNodeOutput(node, e.canvasX, e.canvasY, pos) + const overWidget = this.getWidgetAtCursor(node) + + if (!node.mouseOver) { + //mouse enter + node.mouseOver = { + inputId: null, + outputId: null, + overWidget: null, + } + this.node_over = node + this.dirty_canvas = true + + node.onMouseEnter?.(e) + } + + //in case the node wants to do something + node.onMouseMove?.(e, [e.canvasX - node.pos[0], e.canvasY - node.pos[1]], this) + + // The input the mouse is over has changed + if (node.mouseOver.inputId !== inputId || node.mouseOver.outputId !== outputId || node.mouseOver.overWidget !== overWidget) { + node.mouseOver.inputId = inputId + node.mouseOver.outputId = outputId + node.mouseOver.overWidget = overWidget + + // Check if link is over anything it could connect to - record position of valid target for snap / highlight + if (this.connecting_links) { + const firstLink = this.connecting_links[0] + + // Default: nothing highlighted + let highlightPos: Point = null + let highlightInput: INodeInputSlot = null + let linkOverWidget: IWidget = null + + if (firstLink.node === node) { + // Cannot connect link from a node to itself + } else if (firstLink.output) { + // Connecting from an output to an input + + if (inputId === -1 && outputId === -1) { + // Allow support for linking to widgets, handled externally to LiteGraph + if (this.getWidgetLinkType && overWidget) { + const widgetLinkType = this.getWidgetLinkType(overWidget, node) + if (widgetLinkType && LiteGraph.isValidConnection(firstLink.output.type, widgetLinkType)) { + if (firstLink.node.isValidWidgetLink?.(firstLink.output.slot_index, node, overWidget) !== false) { + linkOverWidget = overWidget + this.link_over_widget_type = widgetLinkType + } + } + } + // Node background / title under the pointer + if (!linkOverWidget) { + const targetSlotId = firstLink.node.findConnectByTypeSlot(true, node, firstLink.output.type) + if (targetSlotId !== null && targetSlotId >= 0) { + node.getConnectionPos(true, targetSlotId, pos) + highlightPos = pos + highlightInput = node.inputs[targetSlotId] + } + } + } else 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 + if (inputId != -1 && node.inputs[inputId] && LiteGraph.isValidConnection(firstLink.output.type, node.inputs[inputId].type)) { + highlightPos = pos + highlightInput = node.inputs[inputId] // XXX CHECK THIS + } + } + } else if (firstLink.input) { + // Connecting from an input to an output + if (inputId === -1 && outputId === -1) { + const targetSlotId = firstLink.node.findConnectByTypeSlot(false, node, firstLink.input.type) + if (targetSlotId !== null && targetSlotId >= 0) { + node.getConnectionPos(false, targetSlotId, pos) + highlightPos = pos + } + } else 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 + if (outputId != -1 && node.outputs[outputId] && LiteGraph.isValidConnection(firstLink.input.type, node.outputs[outputId].type)) { + highlightPos = pos + } + } } - this.graph.remove(node) - this.onNodeDeselected?.(node) + this._highlight_pos = highlightPos + this._highlight_input = highlightInput + this.link_over_widget = linkOverWidget + } + + this.dirty_canvas = true + } + + //Search for corner + if (this.canvas) { + this.canvas.style.cursor = node.inResizeCorner(e.canvasX, e.canvasY) ? 'se-resize' : 'crosshair' + } + } else { + //not over a node + //search for link connector + let over_link: LLink = null + for (let i = 0; i < this.visible_links.length; ++i) { + const link = this.visible_links[i] + const 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 } - this.selected_nodes = {} - this.current_node = null - this.highlighted_links = {} - this.setDirty(true) - this.graph.afterChange() - } - /** - * centers the camera on a given node - **/ - centerOnNode(node: LGraphNode): void { - const dpi = window?.devicePixelRatio || 1 - this.ds.offset[0] = - -node.pos[0] - - node.size[0] * 0.5 + - (this.canvas.width * 0.5) / (this.ds.scale * dpi) - this.ds.offset[1] = - -node.pos[1] - - node.size[1] * 0.5 + - (this.canvas.height * 0.5) / (this.ds.scale * dpi) - this.setDirty(true, true) - } - /** - * adds some useful properties to a mouse event, like the position in graph coordinates - **/ - adjustMouseEvent(e: CanvasMouseEvent | CanvasDragEvent | CanvasWheelEvent): asserts e is CanvasMouseEvent { - let clientX_rel = e.clientX - let clientY_rel = e.clientY if (this.canvas) { - const b = this.canvas.getBoundingClientRect() - clientX_rel -= b.left - clientY_rel -= b.top + 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?.(e, [e.canvasX - this.node_capturing_input.pos[0], e.canvasY - this.node_capturing_input.pos[1]], this) + } + + //node being dragged + if (this.isDragging && !this.live_mode) { + //console.log("draggin!",this.selected_nodes); + const nodes = new Set() + const deltax = delta[0] / this.ds.scale + const deltay = delta[1] / this.ds.scale + for (const i in this.selected_nodes) { + const n = this.selected_nodes[i] + nodes.add(n) + n.pos[0] += delta[0] / this.ds.scale + n.pos[1] += delta[1] / this.ds.scale + /* + * Don't call the function if the block is already selected. + * Otherwise, it could cause the block to be unselected while dragging. + */ + if (!n.is_selected) this.processNodeSelected(n, e) } - // TODO: Find a less brittle way to do this + if (this.selectedGroups) { + for (const group of this.selectedGroups) { + group.move(deltax, deltay, true) + if (!e.ctrlKey) { + for (const node of group._nodes) { + if (!nodes.has(node)) { + node.pos[0] += deltax + node.pos[1] += deltay + } + } + } + } + } - // Only set deltaX and deltaY if not already set. - // If deltaX and deltaY are already present, they are read-only. - // Setting them would result browser error => zoom in/out feature broken. - // @ts-expect-error This behaviour is not guaranteed but for now works on all browsers - if (e.deltaX === undefined) e.deltaX = clientX_rel - this.last_mouse_position[0] - // @ts-expect-error This behaviour is not guaranteed but for now works on all browsers - if (e.deltaY === undefined) 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] - } - /** - * changes the zoom level of the graph (default is 1), you can pass also a place used to pivot the zoom - **/ - setZoom(value: number, zooming_center: Point) { - this.ds.changeScale(value, zooming_center) this.dirty_canvas = true this.dirty_bgcanvas = true - } - /** - * converts a coordinate from graph coordinates to canvas2D coordinates - **/ - convertOffsetToCanvas(pos: Point, out: Point): Point { - // @ts-expect-error Unused param - return this.ds.convertOffsetToCanvas(pos, out) - } - /** - * converts a coordinate from Canvas2D coordinates to graph space - **/ - convertCanvasToOffset(pos: Point, out?: Point): Point { - return this.ds.convertCanvasToOffset(pos, out) - } - //converts event coordinates from canvas2D to graph coordinates - convertEventToCanvasOffset(e: MouseEvent): Point { - const rect = this.canvas.getBoundingClientRect() - // TODO: -> this.ds.convertCanvasToOffset - return this.convertCanvasToOffset([ - e.clientX - rect.left, - e.clientY - rect.top - ]) - } - /** - * brings a node to front (above all other nodes) - **/ - bringToFront(node: LGraphNode): void { - const 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) - **/ - sendToBack(node: LGraphNode): void { - const i = this.graph._nodes.indexOf(node) - if (i == -1) return + if (this.resizing_node && !this.live_mode) { + //convert mouse to node space + const desired_size: Size = [e.canvasX - this.resizing_node.pos[0], e.canvasY - this.resizing_node.pos[1]] + const 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.graph._nodes.splice(i, 1) - this.graph._nodes.unshift(node) - } - - /** - * Determines which nodes are visible and populates {@link out} with the results. - * @param nodes The list of nodes to check - if falsy, all nodes in the graph will be checked - * @param out Array to write visible nodes into - if falsy, a new array is created instead - * @returns {LGraphNode[]} Array passed ({@link out}), or a new array containing all visible nodes - */ - computeVisibleNodes(nodes?: LGraphNode[], out?: LGraphNode[]): LGraphNode[] { - const visible_nodes = out || [] - visible_nodes.length = 0 - nodes ||= this.graph._nodes - for (let i = 0, l = nodes.length; i < l; ++i) { - const n = nodes[i] - - //skip rendering nodes in live mode - if (this.live_mode && !n.onDrawBackground && !n.onDrawForeground) continue - // Not in visible area - if (!overlapBounding(this.visible_area, n.getBounding(LGraphCanvas.#temp, true))) continue - - 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) - **/ - draw(force_canvas?: boolean, force_bgcanvas?: boolean): void { - if (!this.canvas || this.canvas.width == 0 || this.canvas.height == 0) return - - //fps counting - const 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) - **/ - drawFrontCanvas(): void { - this.dirty_canvas = false - - if (!this.ctx) { - this.ctx = this.bgcanvas.getContext("2d") - } - const ctx = this.ctx - //maybe is using webgl... - if (!ctx) return - - const canvas = this.canvas - // @ts-expect-error - if (ctx.start2D && !this.viewport) { - // @ts-expect-error - ctx.start2D() - ctx.restore() - ctx.setTransform(1, 0, 0, 1, 0, 0) - } - - //clip dirty area if there is one, otherwise work in full canvas - const 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 { - const scale = window.devicePixelRatio - ctx.drawImage(this.bgcanvas, 0, 0, this.bgcanvas.width / scale, this.bgcanvas.height / scale) - } - - //rendering - 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 - const visible_nodes = this.computeVisibleNodes( - null, - this.visible_nodes - ) - - for (let i = 0; i < visible_nodes.length; ++i) { - const node = visible_nodes[i] - - //transform coords system - ctx.save() - ctx.translate(node.pos[0], node.pos[1]) - - //Draw - this.drawNode(node, ctx) - - //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) - } - } - - if (this.connecting_links) { - //current connection (the one being dragged by the mouse) - for (const link of this.connecting_links) { - ctx.lineWidth = this.connections_width - let link_color = null - - const connInOrOut = link.output || link.input - - const connType = connInOrOut.type - let connDir = connInOrOut.dir - if (connDir == null) { - if (link.output) - connDir = link.node.horizontal ? LinkDirection.DOWN : LinkDirection.RIGHT - - else - connDir = link.node.horizontal ? LinkDirection.UP : LinkDirection.LEFT - } - const connShape = connInOrOut.shape - - switch (connType) { - case LiteGraph.EVENT: - link_color = LiteGraph.EVENT_LINK_COLOR - break - default: - link_color = LiteGraph.CONNECTING_LINK_COLOR - } - - const highlightPos: Point = this.#getHighlightPosition() - //the connection being dragged by the mouse - this.renderLink( - ctx, - link.pos, - highlightPos, - null, - false, - null, - link_color, - connDir, - link.direction ?? LinkDirection.CENTER - ) - - ctx.beginPath() - if (connType === LiteGraph.EVENT || - connShape === RenderShape.BOX) { - ctx.rect( - link.pos[0] - 6 + 0.5, - link.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 === RenderShape.ARROW) { - ctx.moveTo(link.pos[0] + 8, link.pos[1] + 0.5) - ctx.lineTo(link.pos[0] - 4, link.pos[1] + 6 + 0.5) - ctx.lineTo(link.pos[0] - 4, link.pos[1] - 6 + 0.5) - ctx.closePath() - } - else { - ctx.arc( - link.pos[0], - link.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() - - // Gradient half-border over target node - this.#renderSnapHighlight(ctx, highlightPos) - } - } - - //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) - - //to remove - else - this.onDrawLinkTooltip?.(ctx, null) - - //custom info - this.onDrawForeground?.(ctx, this.visible_area) - - ctx.restore() - } - - //draws panel in the corner - if (this._graph_stack?.length) { - this.drawSubgraphPanel(ctx) - } - - this.onDrawOverlay?.(ctx) - - if (area) ctx.restore() - - // FIXME: Remove this hook - //this is a function I use in webgl renderer - // @ts-expect-error - if (ctx.finish2D) ctx.finish2D() - } - - /** Get the target snap / highlight point in graph space */ - #getHighlightPosition(): Point { - return LiteGraph.snaps_for_comfy - ? this._highlight_pos ?? this.graph_mouse - : this.graph_mouse - } - - /** - * Renders indicators showing where a link will connect if released. - * Partial border over target node and a highlight over the slot itself. - * @param ctx Canvas 2D context - */ - #renderSnapHighlight(ctx: CanvasRenderingContext2D, highlightPos: Point): void { - if (!this._highlight_pos) return - - ctx.fillStyle = "#ffcc00" - ctx.beginPath() - const shape = this._highlight_input?.shape - - if (shape === RenderShape.ARROW) { - ctx.moveTo(highlightPos[0] + 8, highlightPos[1] + 0.5) - ctx.lineTo(highlightPos[0] - 4, highlightPos[1] + 6 + 0.5) - ctx.lineTo(highlightPos[0] - 4, highlightPos[1] - 6 + 0.5) - ctx.closePath() - } else { - ctx.arc( - highlightPos[0], - highlightPos[1], - 6, - 0, - Math.PI * 2 - ) - } - ctx.fill() - - if (!LiteGraph.snap_highlights_node) return - - // Ensure we're mousing over a node and connecting a link - const node = this.node_over - if (!(node && this.connecting_links?.[0])) return - - const { strokeStyle, lineWidth } = ctx - - const area = LGraphCanvas.#tmp_area - node.measure(area) - node.onBounding?.(area) - const gap = 3 - const radius = this.round_radius + gap - - const x = area[0] - gap - const y = area[1] - gap - const width = area[2] + (gap * 2) - const height = area[3] + (gap * 2) - - ctx.beginPath() - ctx.roundRect(x, y, width, height, radius) - - // TODO: Currently works on LTR slots only. Add support for other directions. - const start = this.connecting_links[0].output === null ? 0 : 1 - const inverter = start ? -1 : 1 - - // Radial highlight centred on highlight pos - const hx = highlightPos[0] - const hy = highlightPos[1] - const gRadius = width < height - ? width - : width * Math.max(height / width, 0.5) - - const gradient = ctx.createRadialGradient(hx, hy, 0, hx, hy, gRadius) - gradient.addColorStop(1, "#00000000") - gradient.addColorStop(0, "#ffcc00aa") - - // Linear gradient over half the node. - const linearGradient = ctx.createLinearGradient(x, y, x + width, y) - linearGradient.addColorStop(0.5, "#00000000") - linearGradient.addColorStop(start + (0.67 * inverter), "#ddeeff33") - linearGradient.addColorStop(start + inverter, "#ffcc0055") - - /** - * Workaround for a canvas render issue. - * In Chromium 129 (2024-10-15), rounded corners can be rendered with the wrong part of a gradient colour. - * Occurs only at certain thicknesses / arc sizes. - */ - ctx.setLineDash([radius, radius * 0.001]) - - ctx.lineWidth = 1 - ctx.strokeStyle = linearGradient - ctx.stroke() - - ctx.strokeStyle = gradient - ctx.stroke() - - ctx.setLineDash([]) - ctx.lineWidth = lineWidth - ctx.strokeStyle = strokeStyle - } - - /** - * draws the panel in the corner that shows subgraph properties - **/ - drawSubgraphPanel(ctx: CanvasRenderingContext2D): void { - const subgraph = this.graph - const subnode = subgraph._subgraph_node - if (!subnode) { - console.warn("subgraph without subnode") - return - } - this.drawSubgraphPanelLeft(subgraph, subnode, ctx) - this.drawSubgraphPanelRight(subgraph, subnode, ctx) - } - drawSubgraphPanelLeft(subgraph: LGraph, subnode: LGraphNode, ctx: CanvasRenderingContext2D): void { - const num = subnode.inputs ? subnode.inputs.length : 0 - const w = 200 - const 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 - } - - let y = 50 - ctx.font = "14px Arial" - if (subnode.inputs) - for (let i = 0; i < subnode.inputs.length; ++i) { - const input = subnode.inputs[i] - if (input.not_subgraph_input) continue - - //input button clicked - if (this.drawButton(20, y + 2, w - 20, h - 2)) { - // @ts-expect-error ctor props - const type = subnode.constructor.input_node_type || "graph/input" - this.graph.beforeChange() - const 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" - // @ts-expect-error FIXME: Should be a string? Should be a number? - 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) - } - } - drawSubgraphPanelRight(subgraph: LGraph, subnode: LGraphNode, ctx: CanvasRenderingContext2D): void { - const num = subnode.outputs ? subnode.outputs.length : 0 - const canvas_w = this.bgcanvas.width - const w = 200 - const 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" - const title_text = "Graph Outputs" - const 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 - } - - let y = 50 - ctx.font = "14px Arial" - if (subnode.outputs) - for (let i = 0; i < subnode.outputs.length; ++i) { - const 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)) { - // @ts-expect-error ctor props - const type = subnode.constructor.output_node_type || "graph/output" - this.graph.beforeChange() - const 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" - // @ts-expect-error slot type issue - 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 - drawButton(x: number, y: number, w: number, h: number, text?: string, bgcolor?: CanvasColour, hovercolor?: CanvasColour, textcolor?: CanvasColour): boolean { - const ctx = this.ctx - bgcolor = bgcolor || LiteGraph.NODE_DEFAULT_COLOR - hovercolor = hovercolor || "#555" - textcolor = textcolor || LiteGraph.NODE_TEXT_COLOR - let pos = this.ds.convertOffsetToCanvas(this.graph_mouse) - const 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) { - const rect = this.canvas.getBoundingClientRect() - pos[0] -= rect.left - pos[1] -= rect.top - } - const 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" - } - } - - const was_clicked = clicked && !this.block_click - if (clicked) this.blockClick() - return was_clicked - } - isAreaClicked(x: number, y: number, w: number, h: number, hold_click: boolean): boolean { - const clickPos = this.last_click_position - const clicked = clickPos && LiteGraph.isInsideRectangle(clickPos[0], clickPos[1], x, y, w, h) - const 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 - **/ - renderInfo(ctx: CanvasRenderingContext2D, x: number, y: number): void { - 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) - **/ - drawBackCanvas(): void { - const 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") - } - const ctx = this.bgctx - // TODO: Remove this - // @ts-expect-error - if (ctx.start) ctx.start() - - const 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?.length) { - ctx.save() - const 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" - let title = "" - for (let 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() - } - - const bg_already_painted = this.onRenderBackground - ? this.onRenderBackground(canvas, ctx) - : false - - //reset in case of error - if (!this.viewport) { - const scale = window.devicePixelRatio - ctx.restore() - ctx.setTransform(scale, 0, 0, scale, 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 = false - 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 - const that = this - this._bg_img.onload = function () { - that.draw(true, true) - } - } - - let pattern = this._pattern - if (pattern == null && this._bg_img.width > 0) { - pattern = ctx.createPattern(this._bg_img, "repeat") - this._pattern_img = this._bg_img - this._pattern = pattern - } - - // NOTE: This ridiculous kludge provides a significant performance increase when rendering many large (> canvas width) paths in HTML canvas. - // I could find no documentation or explanation. Requires that the BG image is set. - 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 = true - } - - //groups - if (this.graph._groups.length && !this.live_mode) { - this.drawGroups(canvas, ctx) - } - - this.onDrawBackground?.(ctx, this.visible_area) - - //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() - } - - // TODO: Remove this - // @ts-expect-error - ctx.finish?.() - - this.dirty_bgcanvas = false - //to force to repaint the front canvas with the bgcanvas - // But why would you actually want to do this? + this.canvas.style.cursor = 'se-resize' this.dirty_canvas = true + this.dirty_bgcanvas = true + } } - /** - * draws the given node inside the canvas - **/ - drawNode(node: LGraphNode, ctx: CanvasRenderingContext2D): void { - this.current_node = node - const color = node.color || node.constructor.color || LiteGraph.NODE_DEFAULT_COLOR - let bgcolor = node.bgcolor || node.constructor.bgcolor || LiteGraph.NODE_DEFAULT_BGCOLOR + e.preventDefault() + return false + } + /** + * Called when a mouse up event has to be processed + **/ + processMouseUp(e: CanvasPointerEvent): boolean { + //early exit for extra pointer + if (e.isPrimary === false) return false + if (!this.graph) return - const low_quality = this.ds.scale < 0.6 //zoomed out + const window = this.getCanvasWindow() + const document = window.document + LGraphCanvas.active_canvas = this - //only render if it forces it to do it - if (this.live_mode) { - if (!node.flags.collapsed) { - ctx.shadowColor = "transparent" - node.onDrawForeground?.(ctx, this, this.canvas) - } - return + //restore the mousemove event back to the canvas + if (!this.options.skip_events) { + 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) + const now = LiteGraph.getTime() + e.click_time = now - this.last_mouseclick + this.last_mouse_dragging = false + this.last_click_position = null + + //used to avoid sending twice a click in an immediate button + this.block_click &&= false + + 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) { + const diffx = this.selected_group.pos[0] - Math.round(this.selected_group.pos[0]) + const 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 + this.isDragging = false - const editor_alpha = this.editor_alpha - ctx.globalAlpha = editor_alpha + let node = this.graph.getNodeOnPos(e.canvasX, e.canvasY, this.visible_nodes) - 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 + if (this.dragging_rectangle) { + if (this.graph) { + const nodes = this.graph._nodes + const node_bounding = new Float32Array(4) + + //compute bounding and flip if left to right + const w = Math.abs(this.dragging_rectangle[2]) + const h = Math.abs(this.dragging_rectangle[3]) + const startx = this.dragging_rectangle[2] < 0 ? this.dragging_rectangle[0] - w : this.dragging_rectangle[0] + const 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 + const to_select = [] + for (let i = 0; i < nodes.length; ++i) { + const 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 + } + + // Select groups + if (!e.shiftKey) this.deselectGroups() + this.selectedGroups ??= new Set() + + const groups = this.graph.groups + for (const group of groups) { + const r = this.dragging_rectangle + const pos = group.pos + const size = group.size + if ( + !isInsideRectangle(pos[0], pos[1], r[0], r[1], r[2], r[3]) || + !isInsideRectangle(pos[0] + size[0], pos[1] + size[1], r[0], r[1], r[2], r[3]) + ) + continue + this.selectedGroups.add(group) + group.recomputeInsideNodes() + group.selected = true + } + } else { + // will select of update selection + this.selectNodes([node], e.shiftKey || e.ctrlKey || e.metaKey) // add to selection add to selection with ctrlKey or shiftKey + } + } + this.dragging_rectangle = null + } else if (this.connecting_links) { + //node below mouse + if (node) { + for (const link of this.connecting_links) { + //dragging a connection + this.dirty_canvas = true + this.dirty_bgcanvas = true + + //slot below mouse? connect + if (link.output) { + const slot = this.isOverNodeInput(node, e.canvasX, e.canvasY) + if (slot != -1) { + link.node.connect(link.slot, node, slot) + } else if (this.link_over_widget) { + this.emitEvent({ + subType: 'connectingWidgetLink', + link, + node, + widget: this.link_over_widget, + }) + this.link_over_widget = null + } else { + //not on top of an input + // look for a good slot + link.node.connectByType(link.slot, node, link.output.type) + } + } else if (link.input) { + const slot = this.isOverNodeOutput(node, e.canvasX, e.canvasY) + + if (slot != -1) { + node.connect(slot, link.node, link.slot) // this is inverted has output-input nature like + } else { + //not on top of an input + // look for a good slot + link.node.connectByTypeOutput(link.slot, node, link.input.type) + } + } + } } else { - ctx.shadowColor = "transparent" - } - - //custom draw collapsed method (draw after shadows because they are affected) - if (node.flags.collapsed && node.onDrawCollapsed?.(ctx, this) == true) - return - - //clip if required (mask) - const shape = node._shape || RenderShape.BOX - const size = LGraphCanvas.#temp_vec2 - LGraphCanvas.#temp_vec2.set(node.size) - const horizontal = node.horizontal // || node.flags.horizontal; - - if (node.flags.collapsed) { - ctx.font = this.inner_text_font - const 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 + const firstLink = this.connecting_links[0] + const linkReleaseContext = firstLink.output + ? { + node_from: firstLink.node, + slot_from: firstLink.output, + type_filter_in: firstLink.output.type, + } + : { + node_to: firstLink.node, + slot_from: firstLink.input, + type_filter_out: firstLink.input.type, + } + // For external event only. + const linkReleaseContextExtended: LinkReleaseContextExtended = { + links: this.connecting_links, + } + this.emitEvent({ + subType: 'empty-release', + originalEvent: e, + linkReleaseContext: linkReleaseContextExtended, + }) + // add menu when releasing link in empty space + if (LiteGraph.release_link_on_empty_shows_menu) { + if (e.shiftKey) { + if (this.allow_searchbox) { + this.showSearchBox(e, linkReleaseContext) + } + } else { + if (firstLink.output) { + this.showConnectionMenu({ nodeFrom: firstLink.node, slotFrom: firstLink.output, e: e }) + } else if (firstLink.input) { + this.showConnectionMenu({ nodeTo: firstLink.node, slotTo: firstLink.input, e: e }) + } } + } } - if (node.clip_area) { - //Start clipping - ctx.save() + this.connecting_links = null + } //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? + 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() + } + this.onNodeMoved?.(this.node_dragged) + this.graph.afterChange(this.node_dragged) + this.node_dragged = null + } //no node being dragged + else { + //get node over + node = this.graph.getNodeOnPos(e.canvasX, e.canvasY, this.visible_nodes) + + if (!node && e.click_time < 300 && !this.graph.groups.some((x) => x.isPointInTitlebar(e.canvasX, e.canvasY))) { + this.deselectAllNodes() + } + + this.dirty_canvas = true + this.dragging_canvas = false + + // @ts-expect-error Unused param + this.node_over?.onMouseUp?.(e, [e.canvasX - this.node_over.pos[0], e.canvasY - this.node_over.pos[1]], this) + 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 + this.dirty_canvas = true + this.dragging_canvas = false + } else if (e.which == 3) { + //right button + this.dirty_canvas = true + this.dragging_canvas = false + } + + this.pointer_is_down = false + this.pointer_is_double = false + + this.graph.change() + + e.stopPropagation() + e.preventDefault() + return false + } + + /** + * Called when the mouse moves off the canvas. Clears all node hover states. + * @param e + */ + processMouseOut(e: CanvasMouseEvent): void { + // TODO: Check if document.contains(e.relatedTarget) - handle mouseover node textarea etc. + this.updateMouseOverNodes(null, e) + } + + /** + * Called when a mouse wheel event has to be processed + **/ + processMouseWheel(e: CanvasWheelEvent): boolean { + if (!this.graph || !this.allow_dragcanvas) return + + // TODO: Mouse wheel zoom rewrite + // @ts-expect-error + const delta = e.wheelDeltaY ?? e.detail * -60 + + this.adjustMouseEvent(e) + + const pos: Point = [e.clientX, e.clientY] + if (this.viewport && !isPointInRectangle(pos, this.viewport)) return + + let scale = this.ds.scale + + if (delta > 0) scale *= this.zoom_speed + else if (delta < 0) scale *= 1 / this.zoom_speed + + this.ds.changeScale(scale, [e.clientX, e.clientY]) + + this.graph.change() + + e.preventDefault() + return false + } + /** + * 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 { + const title_height = LiteGraph.NODE_TITLE_HEIGHT + return Boolean(isInsideRectangle(canvasx, canvasy, node.pos[0] + 2, node.pos[1] + 2 - title_height, title_height - 4, title_height - 4)) + } + /** + * returns the INDEX if a position (in graph space) is on top of a node input slot + **/ + isOverNodeInput(node: LGraphNode, canvasx: number, canvasy: number, slot_pos?: Point): number { + if (node.inputs) { + for (let i = 0, l = node.inputs.length; i < l; ++i) { + const input = node.inputs[i] + const link_pos = node.getConnectionPos(true, i) + let is_inside = false + if (node.horizontal) { + is_inside = isInsideRectangle(canvasx, canvasy, link_pos[0] - 5, link_pos[1] - 10, 10, 20) + } else { + // TODO: Find a cheap way to measure text, and do it on node label change instead of here + // Input icon width + text approximation + const width = 20 + ((input.label?.length ?? input.name?.length) || 3) * 7 + is_inside = isInsideRectangle(canvasx, canvasy, link_pos[0] - 10, link_pos[1] - 10, width, 20) + } + 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 + **/ + isOverNodeOutput(node: LGraphNode, canvasx: number, canvasy: number, slot_pos?: Point): number { + if (node.outputs) { + for (let i = 0, l = node.outputs.length; i < l; ++i) { + const link_pos = node.getConnectionPos(false, i) + let 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] - 10, 40, 20) + } + 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 + **/ + processKey(e: KeyboardEvent): boolean | null { + if (!this.graph) return + + let block_default = false + //console.log(e); //debug + // @ts-expect-error + if (e.target.localName == 'input') return + + if (e.type == 'keydown') { + // TODO: Switch + if (e.keyCode == 32) { + // space + this.read_only = true + if (this._previously_dragging_canvas === null) { + this._previously_dragging_canvas = this.dragging_canvas + } + this.dragging_canvas = this.pointer_is_down + block_default = true + } else if (e.keyCode == 27) { + //esc + this.node_panel?.close() + this.options_panel?.close() + block_default = true + } + + //select all Control A + else if (e.keyCode == 65 && e.ctrlKey) { + this.selectNodes() + block_default = true + } else if (e.keyCode === 67 && (e.metaKey || e.ctrlKey) && !e.shiftKey) { + //copy + if (this.selected_nodes) { + this.copyToClipboard() + block_default = true + } + } else if (e.keyCode === 86 && (e.metaKey || e.ctrlKey)) { + //paste + this.pasteFromClipboard(e.shiftKey) + } + + //delete or backspace + else if (e.keyCode == 46 || e.keyCode == 8) { + // @ts-expect-error + if (e.target.localName != 'input' && e.target.localName != 'textarea') { + this.deleteSelectedNodes() + block_default = true + } + } + + //collapse + //... + //TODO + if (this.selected_nodes) { + for (const i in this.selected_nodes) { + this.selected_nodes[i].onKeyDown?.(e) + } + } + } else if (e.type == 'keyup') { + if (e.keyCode == 32) { + // space + this.read_only = false + this.dragging_canvas = this._previously_dragging_canvas ?? false + this._previously_dragging_canvas = null + } + + if (this.selected_nodes) { + for (const i in this.selected_nodes) { + this.selected_nodes[i].onKeyUp?.(e) + } + } + } + + // TODO: Do we need to remeasure and recalculate everything on every key down/up? + this.graph.change() + + if (block_default) { + e.preventDefault() + e.stopImmediatePropagation() + return false + } + } + copyToClipboard(nodes?: Dictionary): void { + const clipboard_info: IClipboardContents = { + nodes: [], + links: [], + } + let index = 0 + const selected_nodes_array: LGraphNode[] = [] + if (!nodes) nodes = this.selected_nodes + for (const i in nodes) { + const node = nodes[i] + if (node.clonable === false) continue + + node._relative_id = index + selected_nodes_array.push(node) + index += 1 + } + + for (let i = 0; i < selected_nodes_array.length; ++i) { + const node = selected_nodes_array[i] + const cloned = node.clone() + if (!cloned) { + console.warn('node type not found: ' + node.type) + continue + } + clipboard_info.nodes.push(cloned.serialize()) + if (node.inputs?.length) { + for (let j = 0; j < node.inputs.length; ++j) { + const input = node.inputs[j] + if (!input || input.link == null) continue + + const link_info = this.graph._links.get(input.link) + if (!link_info) continue + + const 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)) + } + + emitEvent(detail: CanvasEventDetail): void { + this.canvas.dispatchEvent( + new CustomEvent('litegraph:canvas', { + bubbles: true, + detail, + }), + ) + } + + emitBeforeChange(): void { + this.emitEvent({ + subType: 'before-change', + }) + } + + emitAfterChange(): void { + this.emitEvent({ + subType: 'after-change', + }) + } + + _pasteFromClipboard(isConnectUnselected = false): void { + // 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 + const data = localStorage.getItem('litegrapheditor_clipboard') + if (!data) return + + this.graph.beforeChange() + + //create nodes + const clipboard_info: IClipboardContents = 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 + let posMin: false | [number, number] = false + let posMinIndexes: false | [number, number] = false + for (let 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] + } + } + const nodes: LGraphNode[] = [] + for (let i = 0; i < clipboard_info.nodes.length; ++i) { + const node_data = clipboard_info.nodes[i] + const 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, true) + + nodes.push(node) + } + } + + //create links + for (let i = 0; i < clipboard_info.links.length; ++i) { + const link_info = clipboard_info.links[i] + let origin_node: LGraphNode = undefined + const 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) { + const origin_node_id = link_info[4] + if (origin_node_id) { + origin_node = this.graph.getNodeById(origin_node_id) + } + } + const 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() + } + + pasteFromClipboard(isConnectUnselected = false): void { + this.emitBeforeChange() + try { + this._pasteFromClipboard(isConnectUnselected) + } finally { + this.emitAfterChange() + } + } + /** + * process a item drop event on top the canvas + **/ + processDrop(e: CanvasDragEvent): boolean { + e.preventDefault() + this.adjustMouseEvent(e) + const x = e.clientX + const y = e.clientY + const 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 + + const pos = [e.canvasX, e.canvasY] + const node = this.graph ? this.graph.getNodeOnPos(pos[0], pos[1]) : null + + if (!node) { + const r = this.onDropItem?.(e) + if (!r) this.checkDropItem(e) + return + } + + if (node.onDropFile || node.onDropData) { + const files = e.dataTransfer.files + if (files && files.length) { + for (let i = 0; i < files.length; i++) { + const file = e.dataTransfer.files[0] + const filename = file.name + node.onDropFile?.(file) + + if (node.onDropData) { + //prepare reader + const reader = new FileReader() + reader.onload = function (event) { + const data = event.target.result + node.onDropData(data, filename, file) + } + + //read data + const 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?.(e)) return true + + return this.onDropItem ? this.onDropItem(e) : false + } + //called if the graph doesn't have a default drop item behaviour + checkDropItem(e: CanvasDragEvent): void { + if (!e.dataTransfer.files.length) return + + const file = e.dataTransfer.files[0] + const ext = LGraphCanvas.getFileExtension(file.name).toLowerCase() + const nodetype = LiteGraph.node_types_by_file_extension[ext] + if (!nodetype) return + + this.graph.beforeChange() + const node = LiteGraph.createNode(nodetype.type) + node.pos = [e.canvasX, e.canvasY] + this.graph.add(node) + node.onDropFile?.(file) + this.graph.afterChange() + } + processNodeDblClicked(n: LGraphNode): void { + this.onShowNodePanel?.(n) + this.onNodeDblClicked?.(n) + + this.setDirty(true) + } + processNodeSelected(node: LGraphNode, e: CanvasMouseEvent): void { + this.selectNode(node, e && (e.shiftKey || e.metaKey || e.ctrlKey || this.multi_select)) + this.onNodeSelected?.(node) + } + /** + * selects a given node (or adds it to the current selection) + **/ + selectNode(node: LGraphNode, add_to_current_selection?: boolean): void { + if (node == null) { + this.deselectAllNodes() + } else { + this.selectNodes([node], add_to_current_selection) + } + } + /** + * selects several nodes (or adds them to the current selection) + **/ + selectNodes(nodes?: LGraphNode[] | Dictionary, add_to_current_selection?: boolean): void { + if (!add_to_current_selection) { + this.deselectAllNodes() + } + + nodes = nodes || this.graph._nodes + if (typeof nodes == 'string') nodes = [nodes] + for (const i in nodes) { + const node: LGraphNode = nodes[i] + if (node.is_selected) { + this.deselectNode(node) + continue + } + + if (!node.is_selected) { + node.onSelected?.() + } + node.is_selected = true + this.selected_nodes[node.id] = node + + if (node.inputs) { + for (let j = 0; j < node.inputs.length; ++j) { + this.highlighted_links[node.inputs[j].link] = true + } + } + if (node.outputs) { + for (let j = 0; j < node.outputs.length; ++j) { + const out = node.outputs[j] + if (out.links) { + for (let k = 0; k < out.links.length; ++k) { + this.highlighted_links[out.links[k]] = true + } + } + } + } + } + + this.onSelectionChange?.(this.selected_nodes) + + this.setDirty(true) + } + /** + * removes a node from the current selection + **/ + deselectNode(node: LGraphNode): void { + if (!node.is_selected) return + node.onDeselected?.() + node.is_selected = false + delete this.selected_nodes[node.id] + + this.onNodeDeselected?.(node) + + //remove highlighted + if (node.inputs) { + for (let i = 0; i < node.inputs.length; ++i) { + delete this.highlighted_links[node.inputs[i].link] + } + } + if (node.outputs) { + for (let i = 0; i < node.outputs.length; ++i) { + const out = node.outputs[i] + if (out.links) { + for (let j = 0; j < out.links.length; ++j) { + delete this.highlighted_links[out.links[j]] + } + } + } + } + } + /** + * removes all nodes from the current selection + **/ + deselectAllNodes(): void { + if (!this.graph) return + const nodes = this.graph._nodes + for (let i = 0, l = nodes.length; i < l; ++i) { + const node = nodes[i] + if (!node.is_selected) { + continue + } + node.onDeselected?.() + node.is_selected = false + this.onNodeDeselected?.(node) + } + this.selected_nodes = {} + this.current_node = null + this.highlighted_links = {} + this.deselectGroups() + + this.onSelectionChange?.(this.selected_nodes) + this.setDirty(true) + } + + deselectGroups() { + if (!this.selectedGroups) return + for (const group of this.selectedGroups) { + delete group.selected + } + this.selectedGroups = null + } + + /** + * deletes all nodes in the current selection from the graph + **/ + deleteSelectedNodes(): void { + this.graph.beforeChange() + + for (const i in this.selected_nodes) { + const 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?.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 + ) { + const input_link = node.graph._links.get(node.inputs[0].link) + const output_link = node.graph._links.get(node.outputs[0].links[0]) + const input_node = node.getInputNode(0) + const 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) + 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 + **/ + centerOnNode(node: LGraphNode): void { + const dpi = window?.devicePixelRatio || 1 + this.ds.offset[0] = -node.pos[0] - node.size[0] * 0.5 + (this.canvas.width * 0.5) / (this.ds.scale * dpi) + this.ds.offset[1] = -node.pos[1] - node.size[1] * 0.5 + (this.canvas.height * 0.5) / (this.ds.scale * dpi) + this.setDirty(true, true) + } + /** + * adds some useful properties to a mouse event, like the position in graph coordinates + **/ + adjustMouseEvent(e: CanvasMouseEvent | CanvasDragEvent | CanvasWheelEvent): asserts e is CanvasMouseEvent { + let clientX_rel = e.clientX + let clientY_rel = e.clientY + + if (this.canvas) { + const b = this.canvas.getBoundingClientRect() + clientX_rel -= b.left + clientY_rel -= b.top + } + + // TODO: Find a less brittle way to do this + + // Only set deltaX and deltaY if not already set. + // If deltaX and deltaY are already present, they are read-only. + // Setting them would result browser error => zoom in/out feature broken. + // @ts-expect-error This behaviour is not guaranteed but for now works on all browsers + if (e.deltaX === undefined) e.deltaX = clientX_rel - this.last_mouse_position[0] + // @ts-expect-error This behaviour is not guaranteed but for now works on all browsers + if (e.deltaY === undefined) 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] + } + /** + * changes the zoom level of the graph (default is 1), you can pass also a place used to pivot the zoom + **/ + setZoom(value: number, zooming_center: Point) { + this.ds.changeScale(value, zooming_center) + this.dirty_canvas = true + this.dirty_bgcanvas = true + } + /** + * converts a coordinate from graph coordinates to canvas2D coordinates + **/ + convertOffsetToCanvas(pos: Point, out: Point): Point { + // @ts-expect-error Unused param + return this.ds.convertOffsetToCanvas(pos, out) + } + /** + * converts a coordinate from Canvas2D coordinates to graph space + **/ + convertCanvasToOffset(pos: Point, out?: Point): Point { + return this.ds.convertCanvasToOffset(pos, out) + } + //converts event coordinates from canvas2D to graph coordinates + convertEventToCanvasOffset(e: MouseEvent): Point { + const rect = this.canvas.getBoundingClientRect() + // TODO: -> this.ds.convertCanvasToOffset + return this.convertCanvasToOffset([e.clientX - rect.left, e.clientY - rect.top]) + } + /** + * brings a node to front (above all other nodes) + **/ + bringToFront(node: LGraphNode): void { + const 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) + **/ + sendToBack(node: LGraphNode): void { + const i = this.graph._nodes.indexOf(node) + if (i == -1) return + + this.graph._nodes.splice(i, 1) + this.graph._nodes.unshift(node) + } + + /** + * Determines which nodes are visible and populates {@link out} with the results. + * @param nodes The list of nodes to check - if falsy, all nodes in the graph will be checked + * @param out Array to write visible nodes into - if falsy, a new array is created instead + * @returns {LGraphNode[]} Array passed ({@link out}), or a new array containing all visible nodes + */ + computeVisibleNodes(nodes?: LGraphNode[], out?: LGraphNode[]): LGraphNode[] { + const visible_nodes = out || [] + visible_nodes.length = 0 + nodes ||= this.graph._nodes + for (let i = 0, l = nodes.length; i < l; ++i) { + const n = nodes[i] + + //skip rendering nodes in live mode + if (this.live_mode && !n.onDrawBackground && !n.onDrawForeground) continue + // Not in visible area + if (!overlapBounding(this.visible_area, n.getBounding(LGraphCanvas.#temp, true))) continue + + 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) + **/ + draw(force_canvas?: boolean, force_bgcanvas?: boolean): void { + if (!this.canvas || this.canvas.width == 0 || this.canvas.height == 0) return + + //fps counting + const 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) + **/ + drawFrontCanvas(): void { + this.dirty_canvas = false + + if (!this.ctx) { + this.ctx = this.bgcanvas.getContext('2d') + } + const ctx = this.ctx + //maybe is using webgl... + if (!ctx) return + + const canvas = this.canvas + // @ts-expect-error + if (ctx.start2D && !this.viewport) { + // @ts-expect-error + ctx.start2D() + ctx.restore() + ctx.setTransform(1, 0, 0, 1, 0, 0) + } + + //clip dirty area if there is one, otherwise work in full canvas + const 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 { + const scale = window.devicePixelRatio + ctx.drawImage(this.bgcanvas, 0, 0, this.bgcanvas.width / scale, this.bgcanvas.height / scale) + } + + //rendering + 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 + const visible_nodes = this.computeVisibleNodes(null, this.visible_nodes) + + for (let i = 0; i < visible_nodes.length; ++i) { + const node = visible_nodes[i] + + //transform coords system + ctx.save() + ctx.translate(node.pos[0], node.pos[1]) + + //Draw + this.drawNode(node, ctx) + + //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) + } + } + + if (this.connecting_links) { + //current connection (the one being dragged by the mouse) + for (const link of this.connecting_links) { + ctx.lineWidth = this.connections_width + let link_color = null + + const connInOrOut = link.output || link.input + + const connType = connInOrOut.type + let connDir = connInOrOut.dir + if (connDir == null) { + if (link.output) connDir = link.node.horizontal ? LinkDirection.DOWN : LinkDirection.RIGHT + else connDir = link.node.horizontal ? LinkDirection.UP : LinkDirection.LEFT + } + const connShape = connInOrOut.shape + + switch (connType) { + case LiteGraph.EVENT: + link_color = LiteGraph.EVENT_LINK_COLOR + break + default: + link_color = LiteGraph.CONNECTING_LINK_COLOR + } + + const highlightPos: Point = this.#getHighlightPosition() + //the connection being dragged by the mouse + this.renderLink(ctx, link.pos, highlightPos, null, false, null, link_color, connDir, link.direction ?? LinkDirection.CENTER) + + ctx.beginPath() + if (connType === LiteGraph.EVENT || connShape === RenderShape.BOX) { + ctx.rect(link.pos[0] - 6 + 0.5, link.pos[1] - 5 + 0.5, 14, 10) + ctx.fill() ctx.beginPath() - if (shape == RenderShape.BOX) { - ctx.rect(0, 0, size[0], size[1]) - } else if (shape == RenderShape.ROUND) { - ctx.roundRect(0, 0, size[0], size[1], [10]) - } else if (shape == RenderShape.CIRCLE) { - ctx.arc( - size[0] * 0.5, - size[1] * 0.5, - size[0] * 0.5, - 0, - Math.PI * 2 - ) - } - ctx.clip() + ctx.rect(this.graph_mouse[0] - 6 + 0.5, this.graph_mouse[1] - 5 + 0.5, 14, 10) + } else if (connShape === RenderShape.ARROW) { + ctx.moveTo(link.pos[0] + 8, link.pos[1] + 0.5) + ctx.lineTo(link.pos[0] - 4, link.pos[1] + 6 + 0.5) + ctx.lineTo(link.pos[0] - 4, link.pos[1] - 6 + 0.5) + ctx.closePath() + } else { + ctx.arc(link.pos[0], link.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() + + // Gradient half-border over target node + this.#renderSnapHighlight(ctx, highlightPos) + } + } + + //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) + //to remove + else this.onDrawLinkTooltip?.(ctx, null) + + //custom info + this.onDrawForeground?.(ctx, this.visible_area) + + ctx.restore() + } + + //draws panel in the corner + if (this._graph_stack?.length) { + this.drawSubgraphPanel(ctx) + } + + this.onDrawOverlay?.(ctx) + + if (area) ctx.restore() + + // FIXME: Remove this hook + //this is a function I use in webgl renderer + // @ts-expect-error + if (ctx.finish2D) ctx.finish2D() + } + + /** Get the target snap / highlight point in graph space */ + #getHighlightPosition(): Point { + return LiteGraph.snaps_for_comfy ? (this._highlight_pos ?? this.graph_mouse) : this.graph_mouse + } + + /** + * Renders indicators showing where a link will connect if released. + * Partial border over target node and a highlight over the slot itself. + * @param ctx Canvas 2D context + */ + #renderSnapHighlight(ctx: CanvasRenderingContext2D, highlightPos: Point): void { + if (!this._highlight_pos) return + + ctx.fillStyle = '#ffcc00' + ctx.beginPath() + const shape = this._highlight_input?.shape + + if (shape === RenderShape.ARROW) { + ctx.moveTo(highlightPos[0] + 8, highlightPos[1] + 0.5) + ctx.lineTo(highlightPos[0] - 4, highlightPos[1] + 6 + 0.5) + ctx.lineTo(highlightPos[0] - 4, highlightPos[1] - 6 + 0.5) + ctx.closePath() + } else { + ctx.arc(highlightPos[0], highlightPos[1], 6, 0, Math.PI * 2) + } + ctx.fill() + + if (!LiteGraph.snap_highlights_node) return + + // Ensure we're mousing over a node and connecting a link + const node = this.node_over + if (!(node && this.connecting_links?.[0])) return + + const { strokeStyle, lineWidth } = ctx + + const area = LGraphCanvas.#tmp_area + node.measure(area) + node.onBounding?.(area) + const gap = 3 + const radius = this.round_radius + gap + + const x = area[0] - gap + const y = area[1] - gap + const width = area[2] + gap * 2 + const height = area[3] + gap * 2 + + ctx.beginPath() + ctx.roundRect(x, y, width, height, radius) + + // TODO: Currently works on LTR slots only. Add support for other directions. + const start = this.connecting_links[0].output === null ? 0 : 1 + const inverter = start ? -1 : 1 + + // Radial highlight centred on highlight pos + const hx = highlightPos[0] + const hy = highlightPos[1] + const gRadius = width < height ? width : width * Math.max(height / width, 0.5) + + const gradient = ctx.createRadialGradient(hx, hy, 0, hx, hy, gRadius) + gradient.addColorStop(1, '#00000000') + gradient.addColorStop(0, '#ffcc00aa') + + // Linear gradient over half the node. + const linearGradient = ctx.createLinearGradient(x, y, x + width, y) + linearGradient.addColorStop(0.5, '#00000000') + linearGradient.addColorStop(start + 0.67 * inverter, '#ddeeff33') + linearGradient.addColorStop(start + inverter, '#ffcc0055') + + /** + * Workaround for a canvas render issue. + * In Chromium 129 (2024-10-15), rounded corners can be rendered with the wrong part of a gradient colour. + * Occurs only at certain thicknesses / arc sizes. + */ + ctx.setLineDash([radius, radius * 0.001]) + + ctx.lineWidth = 1 + ctx.strokeStyle = linearGradient + ctx.stroke() + + ctx.strokeStyle = gradient + ctx.stroke() + + ctx.setLineDash([]) + ctx.lineWidth = lineWidth + ctx.strokeStyle = strokeStyle + } + + /** + * draws the panel in the corner that shows subgraph properties + **/ + drawSubgraphPanel(ctx: CanvasRenderingContext2D): void { + const subgraph = this.graph + const subnode = subgraph._subgraph_node + if (!subnode) { + console.warn('subgraph without subnode') + return + } + this.drawSubgraphPanelLeft(subgraph, subnode, ctx) + this.drawSubgraphPanelRight(subgraph, subnode, ctx) + } + drawSubgraphPanelLeft(subgraph: LGraph, subnode: LGraphNode, ctx: CanvasRenderingContext2D): void { + const num = subnode.inputs ? subnode.inputs.length : 0 + const w = 200 + const 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 + } + + let y = 50 + ctx.font = '14px Arial' + if (subnode.inputs) + for (let i = 0; i < subnode.inputs.length; ++i) { + const input = subnode.inputs[i] + if (input.not_subgraph_input) continue + + //input button clicked + if (this.drawButton(20, y + 2, w - 20, h - 2)) { + // @ts-expect-error ctor props + const type = subnode.constructor.input_node_type || 'graph/input' + this.graph.beforeChange() + const 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' + // @ts-expect-error FIXME: Should be a string? Should be a number? + 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) + } + } + drawSubgraphPanelRight(subgraph: LGraph, subnode: LGraphNode, ctx: CanvasRenderingContext2D): void { + const num = subnode.outputs ? subnode.outputs.length : 0 + const canvas_w = this.bgcanvas.width + const w = 200 + const 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' + const title_text = 'Graph Outputs' + const 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 + } + + let y = 50 + ctx.font = '14px Arial' + if (subnode.outputs) + for (let i = 0; i < subnode.outputs.length; ++i) { + const 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)) { + // @ts-expect-error ctor props + const type = subnode.constructor.output_node_type || 'graph/output' + this.graph.beforeChange() + const 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' + // @ts-expect-error slot type issue + 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 + drawButton( + x: number, + y: number, + w: number, + h: number, + text?: string, + bgcolor?: CanvasColour, + hovercolor?: CanvasColour, + textcolor?: CanvasColour, + ): boolean { + const ctx = this.ctx + bgcolor = bgcolor || LiteGraph.NODE_DEFAULT_COLOR + hovercolor = hovercolor || '#555' + textcolor = textcolor || LiteGraph.NODE_TEXT_COLOR + let pos = this.ds.convertOffsetToCanvas(this.graph_mouse) + const 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) { + const rect = this.canvas.getBoundingClientRect() + pos[0] -= rect.left + pos[1] -= rect.top + } + const 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' + } + } + + const was_clicked = clicked && !this.block_click + if (clicked) this.blockClick() + return was_clicked + } + isAreaClicked(x: number, y: number, w: number, h: number, hold_click: boolean): boolean { + const clickPos = this.last_click_position + const clicked = clickPos && LiteGraph.isInsideRectangle(clickPos[0], clickPos[1], x, y, w, h) + const 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 + **/ + renderInfo(ctx: CanvasRenderingContext2D, x: number, y: number): void { + 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) + **/ + drawBackCanvas(): void { + const 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') + } + const ctx = this.bgctx + // TODO: Remove this + // @ts-expect-error + if (ctx.start) ctx.start() + + const 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?.length) { + ctx.save() + const 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' + let title = '' + for (let 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() + } + + const bg_already_painted = this.onRenderBackground ? this.onRenderBackground(canvas, ctx) : false + + //reset in case of error + if (!this.viewport) { + const scale = window.devicePixelRatio + ctx.restore() + ctx.setTransform(scale, 0, 0, scale, 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 = false + 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 + const that = this + this._bg_img.onload = function () { + that.draw(true, true) + } } - //draw shape - if (node.has_errors) { - bgcolor = "red" - } - this.drawNodeShape( - node, - ctx, - size, - color, - bgcolor, - node.is_selected - ) - - if (!low_quality) { - node.drawBadges(ctx) + let pattern = this._pattern + if (pattern == null && this._bg_img.width > 0) { + pattern = ctx.createPattern(this._bg_img, 'repeat') + this._pattern_img = this._bg_img + this._pattern = pattern } - ctx.shadowColor = "transparent" - - //draw foreground - node.onDrawForeground?.(ctx, this, this.canvas) - - //connection slots - ctx.textAlign = horizontal ? "center" : "left" - ctx.font = this.inner_text_font - - const render_text = !low_quality - const highlightColour = LiteGraph.NODE_TEXT_HIGHLIGHT_COLOR ?? LiteGraph.NODE_SELECTED_TITLE_COLOR ?? LiteGraph.NODE_TEXT_COLOR - - const out_slot = this.connecting_links ? this.connecting_links[0].output : null - const in_slot = this.connecting_links ? this.connecting_links[0].input : null - ctx.lineWidth = 1 - - let max_y = 0 - const slot_pos = new Float32Array(2) //to reuse - - //render inputs and outputs - if (!node.flags.collapsed) { - //input connection slots - if (node.inputs) { - for (let i = 0; i < node.inputs.length; i++) { - const slot = node.inputs[i] - - const slot_type = slot.type - - //change opacity of incompatible slots when dragging a connection - const isValid = !this.connecting_links || (out_slot && LiteGraph.isValidConnection(slot.type, out_slot.type)) - const highlight = isValid && node.mouseOver?.inputId === i - const label_color = highlight ? highlightColour : LiteGraph.NODE_TEXT_COLOR - ctx.globalAlpha = isValid ? editor_alpha : 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 - - const 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 - } - - drawSlot(ctx, slot, pos, { - horizontal, - low_quality, - render_text, - label_color, - label_position: LabelPosition.Right, - // Input slot is not stroked. - do_stroke: false, - highlight, - }) - } - } - - //output connection slots - ctx.textAlign = horizontal ? "center" : "right" - ctx.strokeStyle = "black" - if (node.outputs) { - for (let i = 0; i < node.outputs.length; i++) { - const slot = node.outputs[i] - - const slot_type = slot.type - - //change opacity of incompatible slots when dragging a connection - const isValid = !this.connecting_links || (in_slot && LiteGraph.isValidConnection(slot_type, in_slot.type)) - const highlight = isValid && node.mouseOver?.outputId === i - const label_color = highlight ? highlightColour : LiteGraph.NODE_TEXT_COLOR - ctx.globalAlpha = isValid ? editor_alpha : 0.4 * editor_alpha - - const 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 - - drawSlot(ctx, slot, pos, { - horizontal, - low_quality, - render_text, - label_color, - label_position: LabelPosition.Left, - do_stroke: true, - highlight, - }) - } - } - - ctx.textAlign = "left" - ctx.globalAlpha = 1 - - if (node.widgets) { - let 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 - let input_slot = null - let output_slot = null - let slot - - //get first connected slot to render - if (node.inputs) { - for (let i = 0; i < node.inputs.length; i++) { - slot = node.inputs[i] - if (slot.link == null) { - continue - } - input_slot = slot - break - } - } - if (node.outputs) { - for (let i = 0; i < node.outputs.length; i++) { - slot = node.outputs[i] - if (!slot.links || !slot.links.length) { - continue - } - output_slot = slot - } - } - - if (input_slot) { - let x = 0 - let 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 === RenderShape.BOX) { - ctx.rect(x - 7 + 0.5, y - 4, 14, 8) - } else if (slot.shape === RenderShape.ARROW) { - 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) { - let x = node._collapsed_width - let 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 === RenderShape.BOX) { - ctx.rect(x - 7 + 0.5, y - 4, 14, 8) - } else if (slot.shape === RenderShape.ARROW) { - 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() + // NOTE: This ridiculous kludge provides a significant performance increase when rendering many large (> canvas width) paths in HTML canvas. + // I could find no documentation or explanation. Requires that the BG image is set. + 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 = true + } + + //groups + if (this.graph._groups.length && !this.live_mode) { + this.drawGroups(canvas, ctx) + } + + this.onDrawBackground?.(ctx, this.visible_area) + + //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() } - //used by this.over_link_center - drawLinkTooltip(ctx: CanvasRenderingContext2D, link: LLink): void { - const 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 + // TODO: Remove this + // @ts-expect-error + ctx.finish?.() - if (this.onDrawLinkTooltip?.(ctx, link, this) == true) - return + this.dirty_bgcanvas = false + //to force to repaint the front canvas with the bgcanvas + // But why would you actually want to do this? + this.dirty_canvas = true + } + /** + * draws the given node inside the canvas + **/ + drawNode(node: LGraphNode, ctx: CanvasRenderingContext2D): void { + this.current_node = node - // TODO: Better value typing - const data = link.data - let text: string = null + const color = node.color || node.constructor.color || LiteGraph.NODE_DEFAULT_COLOR + let bgcolor = node.bgcolor || node.constructor.bgcolor || LiteGraph.NODE_DEFAULT_BGCOLOR - if (typeof data === "number") - text = data.toFixed(2) - else if (typeof data === "string") - text = "\"" + data + "\"" - else if (typeof data === "boolean") - text = String(data) - else if (data.toToolTip) - text = data.toToolTip() - else - text = "[" + data.constructor.name + "]" + const low_quality = this.ds.scale < 0.6 //zoomed out - if (text == null) return - - // Hard-coded tooltip limit - text = text.substring(0, 30) - - ctx.font = "14px Courier New" - const info = ctx.measureText(text) - const w = info.width + 20 - const 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) + //only render if it forces it to do it + if (this.live_mode) { + if (!node.flags.collapsed) { + ctx.shadowColor = 'transparent' + node.onDrawForeground?.(ctx, this, this.canvas) + } + return } - /** - * Draws the shape of the given node on the canvas - * @param node The node to draw - * @param ctx 2D canvas rendering context used to draw - * @param size Size of the background to draw, in graph units. Differs from node size if collapsed, etc. - * @param fgcolor Foreground colour - used for text - * @param bgcolor Background colour of the node - * @param selected Whether to render the node as selected. Likely to be removed in future, as current usage is simply the is_selected property of the node. - * @param mouse_over Deprecated - */ - drawNodeShape( - node: LGraphNode, - ctx: CanvasRenderingContext2D, - size: Size, - fgcolor: CanvasColour, - bgcolor: CanvasColour, - selected: boolean - ): void { - //bg rect - ctx.strokeStyle = fgcolor - ctx.fillStyle = bgcolor - const title_height = LiteGraph.NODE_TITLE_HEIGHT - const low_quality = this.ds.scale < 0.5 + const editor_alpha = this.editor_alpha + ctx.globalAlpha = editor_alpha - //render node area depending on shape - const shape = node._shape || node.constructor.shape || RenderShape.ROUND - const title_mode = node.constructor.title_mode + 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' + } - const render_title = title_mode == TitleMode.TRANSPARENT_TITLE || title_mode == TitleMode.NO_TITLE - ? false - : true + //custom draw collapsed method (draw after shadows because they are affected) + if (node.flags.collapsed && node.onDrawCollapsed?.(ctx, this) == true) return - // Normalised node dimensions - const area = LGraphCanvas.#tmp_area - node.measure(area) - area[0] -= node.pos[0] - area[1] -= node.pos[1] - area[2]++ + //clip if required (mask) + const shape = node._shape || RenderShape.BOX + const size = LGraphCanvas.#temp_vec2 + LGraphCanvas.#temp_vec2.set(node.size) + const horizontal = node.horizontal // || node.flags.horizontal; - const old_alpha = ctx.globalAlpha + if (node.flags.collapsed) { + ctx.font = this.inner_text_font + const 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 + } + } - //full node shape - //if(node.flags.collapsed) - { - ctx.beginPath() - if (shape == RenderShape.BOX || low_quality) { - ctx.fillRect(area[0], area[1], area[2], area[3]) - } else if (shape == RenderShape.ROUND || - shape == RenderShape.CARD) { - ctx.roundRect( - area[0], - area[1], - area[2], - area[3], - shape == RenderShape.CARD ? [this.round_radius, this.round_radius, 0, 0] : [this.round_radius] - ) - } else if (shape == RenderShape.CIRCLE) { - ctx.arc( - size[0] * 0.5, - size[1] * 0.5, - size[0] * 0.5, - 0, - Math.PI * 2 - ) - } - ctx.fill() + if (node.clip_area) { + //Start clipping + ctx.save() + ctx.beginPath() + if (shape == RenderShape.BOX) { + ctx.rect(0, 0, size[0], size[1]) + } else if (shape == RenderShape.ROUND) { + ctx.roundRect(0, 0, size[0], size[1], [10]) + } else if (shape == RenderShape.CIRCLE) { + ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5, 0, Math.PI * 2) + } + ctx.clip() + } - //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) - } + //draw shape + if (node.has_errors) { + bgcolor = 'red' + } + this.drawNodeShape(node, ctx, size, color, bgcolor, node.is_selected) + + if (!low_quality) { + node.drawBadges(ctx) + } + + ctx.shadowColor = 'transparent' + + //draw foreground + node.onDrawForeground?.(ctx, this, this.canvas) + + //connection slots + ctx.textAlign = horizontal ? 'center' : 'left' + ctx.font = this.inner_text_font + + const render_text = !low_quality + const highlightColour = LiteGraph.NODE_TEXT_HIGHLIGHT_COLOR ?? LiteGraph.NODE_SELECTED_TITLE_COLOR ?? LiteGraph.NODE_TEXT_COLOR + + const out_slot = this.connecting_links ? this.connecting_links[0].output : null + const in_slot = this.connecting_links ? this.connecting_links[0].input : null + ctx.lineWidth = 1 + + let max_y = 0 + const slot_pos = new Float32Array(2) //to reuse + + //render inputs and outputs + if (!node.flags.collapsed) { + //input connection slots + if (node.inputs) { + for (let i = 0; i < node.inputs.length; i++) { + const slot = node.inputs[i] + + const slot_type = slot.type + + //change opacity of incompatible slots when dragging a connection + const isValid = !this.connecting_links || (out_slot && LiteGraph.isValidConnection(slot.type, out_slot.type)) + const highlight = isValid && node.mouseOver?.inputId === i + const label_color = highlight ? highlightColour : LiteGraph.NODE_TEXT_COLOR + ctx.globalAlpha = isValid ? editor_alpha : 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 + + const 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 + } + + drawSlot(ctx, slot, pos, { + horizontal, + low_quality, + render_text, + label_color, + label_position: LabelPosition.Right, + // Input slot is not stroked. + do_stroke: false, + highlight, + }) } - ctx.shadowColor = "transparent" + } - node.onDrawBackground?.(ctx, this, this.canvas, this.graph_mouse) + //output connection slots + ctx.textAlign = horizontal ? 'center' : 'right' + ctx.strokeStyle = 'black' + if (node.outputs) { + for (let i = 0; i < node.outputs.length; i++) { + const slot = node.outputs[i] - //title bg (remember, it is rendered ABOVE the node) - if (render_title || title_mode == TitleMode.TRANSPARENT_TITLE) { - //title bar - if (node.onDrawTitleBar) { - node.onDrawTitleBar(ctx, title_height, size, this.ds.scale, fgcolor) - } else if ( - title_mode != TitleMode.TRANSPARENT_TITLE && - (node.constructor.title_color || this.render_title_colored) - ) { - const title_color = node.constructor.title_color || fgcolor + const slot_type = slot.type - if (node.flags.collapsed) { - ctx.shadowColor = LiteGraph.DEFAULT_SHADOW_COLOR - } + //change opacity of incompatible slots when dragging a connection + const isValid = !this.connecting_links || (in_slot && LiteGraph.isValidConnection(slot_type, in_slot.type)) + const highlight = isValid && node.mouseOver?.outputId === i + const label_color = highlight ? highlightColour : LiteGraph.NODE_TEXT_COLOR + ctx.globalAlpha = isValid ? editor_alpha : 0.4 * editor_alpha - //* gradient test - if (this.use_gradients) { - // TODO: This feature may not have been completed. Could finish or remove. - // Original impl. may cause CanvasColour to be used as index key. Also, colour requires validation before blindly passing on. - // @ts-expect-error Fix or remove gradient feature - let grad = LGraphCanvas.gradients[title_color] - if (!grad) { - // @ts-expect-error Fix or remove gradient feature - grad = LGraphCanvas.gradients[title_color] = ctx.createLinearGradient(0, 0, 400, 0) - grad.addColorStop(0, title_color) - grad.addColorStop(1, "#000") - } - ctx.fillStyle = grad - } else { - ctx.fillStyle = title_color - } + const 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.globalAlpha = 0.5 * old_alpha; - ctx.beginPath() - if (shape == RenderShape.BOX || low_quality) { - ctx.rect(0, -title_height, size[0] + 1, title_height) - } else if (shape == RenderShape.ROUND || shape == RenderShape.CARD) { - 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" - } + 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 - let colState: string | boolean = 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) - } + drawSlot(ctx, slot, pos, { + horizontal, + low_quality, + render_text, + label_color, + label_position: LabelPosition.Left, + do_stroke: true, + highlight, + }) + } + } - //title box - const box_size = 10 - if (node.onDrawTitleBox) { - node.onDrawTitleBox(ctx, title_height, size, this.ds.scale) - } else if (shape == RenderShape.ROUND || - shape == RenderShape.CIRCLE || - shape == RenderShape.CARD) { - 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.textAlign = 'left' + ctx.globalAlpha = 1 - 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) + if (node.widgets) { + let 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 + let input_slot = null + let output_slot = null + let slot - 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 + //get first connected slot to render + if (node.inputs) { + for (let i = 0; i < node.inputs.length; i++) { + slot = node.inputs[i] + if (slot.link == null) { + continue + } + input_slot = slot + break + } + } + if (node.outputs) { + for (let i = 0; i < node.outputs.length; i++) { + slot = node.outputs[i] + if (!slot.links || !slot.links.length) { + continue + } + output_slot = slot + } + } - //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 - const title = String(node.getTitle()) + (node.pinned ? "📌" : "") - 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" - // const 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 - ) - } - } - } + if (input_slot) { + let x = 0 + let 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 === RenderShape.BOX) { + ctx.rect(x - 7 + 0.5, y - 4, 14, 8) + } else if (slot.shape === RenderShape.ARROW) { + 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() + } - //subgraph box - if (!node.flags.collapsed && node.subgraph && !node.skip_subgraph_button) { - const w = LiteGraph.NODE_TITLE_HEIGHT - const x = node.size[0] - w - const 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 == RenderShape.BOX || 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() - } + if (output_slot) { + let x = node._collapsed_width + let 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 === RenderShape.BOX) { + ctx.rect(x - 7 + 0.5, y - 4, 14, 8) + } else if (slot.shape === RenderShape.ARROW) { + 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(); + } + } - //custom title render - node.onDrawTitle?.(ctx) + if (node.clip_area) { + ctx.restore() + } + + ctx.globalAlpha = 1.0 + } + //used by this.over_link_center + drawLinkTooltip(ctx: CanvasRenderingContext2D, link: LLink): void { + const 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?.(ctx, link, this) == true) return + + // TODO: Better value typing + const data = link.data + let text: string = null + + if (typeof data === 'number') text = data.toFixed(2) + else if (typeof data === 'string') text = '"' + data + '"' + else if (typeof data === 'boolean') text = String(data) + else if (data.toToolTip) text = data.toToolTip() + else text = '[' + data.constructor.name + ']' + + if (text == null) return + + // Hard-coded tooltip limit + text = text.substring(0, 30) + + ctx.font = '14px Courier New' + const info = ctx.measureText(text) + const w = info.width + 20 + const 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 on the canvas + * @param node The node to draw + * @param ctx 2D canvas rendering context used to draw + * @param size Size of the background to draw, in graph units. Differs from node size if collapsed, etc. + * @param fgcolor Foreground colour - used for text + * @param bgcolor Background colour of the node + * @param selected Whether to render the node as selected. Likely to be removed in future, as current usage is simply the is_selected property of the node. + * @param mouse_over Deprecated + */ + drawNodeShape(node: LGraphNode, ctx: CanvasRenderingContext2D, size: Size, fgcolor: CanvasColour, bgcolor: CanvasColour, selected: boolean): void { + //bg rect + ctx.strokeStyle = fgcolor + ctx.fillStyle = bgcolor + + const title_height = LiteGraph.NODE_TITLE_HEIGHT + const low_quality = this.ds.scale < 0.5 + + //render node area depending on shape + const shape = node._shape || node.constructor.shape || RenderShape.ROUND + const title_mode = node.constructor.title_mode + + const render_title = title_mode == TitleMode.TRANSPARENT_TITLE || title_mode == TitleMode.NO_TITLE ? false : true + + // Normalised node dimensions + const area = LGraphCanvas.#tmp_area + node.measure(area) + area[0] -= node.pos[0] + area[1] -= node.pos[1] + area[2]++ + + const old_alpha = ctx.globalAlpha + + //full node shape + //if(node.flags.collapsed) + { + ctx.beginPath() + if (shape == RenderShape.BOX || low_quality) { + ctx.fillRect(area[0], area[1], area[2], area[3]) + } else if (shape == RenderShape.ROUND || shape == RenderShape.CARD) { + ctx.roundRect( + area[0], + area[1], + area[2], + area[3], + shape == RenderShape.CARD ? [this.round_radius, this.round_radius, 0, 0] : [this.round_radius], + ) + } else if (shape == RenderShape.CIRCLE) { + 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' + + node.onDrawBackground?.(ctx, this, this.canvas, this.graph_mouse) + + //title bg (remember, it is rendered ABOVE the node) + if (render_title || title_mode == TitleMode.TRANSPARENT_TITLE) { + //title bar + if (node.onDrawTitleBar) { + node.onDrawTitleBar(ctx, title_height, size, this.ds.scale, fgcolor) + } else if (title_mode != TitleMode.TRANSPARENT_TITLE && (node.constructor.title_color || this.render_title_colored)) { + const title_color = node.constructor.title_color || fgcolor + + if (node.flags.collapsed) { + ctx.shadowColor = LiteGraph.DEFAULT_SHADOW_COLOR } - //render selection marker - if (selected) { - node.onBounding?.(area) + //* gradient test + if (this.use_gradients) { + // TODO: This feature may not have been completed. Could finish or remove. + // Original impl. may cause CanvasColour to be used as index key. Also, colour requires validation before blindly passing on. + // @ts-expect-error Fix or remove gradient feature + let grad = LGraphCanvas.gradients[title_color] + if (!grad) { + // @ts-expect-error Fix or remove gradient feature + grad = LGraphCanvas.gradients[title_color] = ctx.createLinearGradient(0, 0, 400, 0) + grad.addColorStop(0, title_color) + grad.addColorStop(1, '#000') + } + ctx.fillStyle = grad + } else { + ctx.fillStyle = title_color + } - this.drawSelectionBounding( - ctx, - area, - { - shape, - title_height, - title_mode, - fgcolor, - collapsed: node.flags?.collapsed - } + //ctx.globalAlpha = 0.5 * old_alpha; + ctx.beginPath() + if (shape == RenderShape.BOX || low_quality) { + ctx.rect(0, -title_height, size[0] + 1, title_height) + } else if (shape == RenderShape.ROUND || shape == RenderShape.CARD) { + 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' + } + + let colState: string | boolean = 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 + const box_size = 10 + if (node.onDrawTitleBox) { + node.onDrawTitleBox(ctx, title_height, size, this.ds.scale) + } else if (shape == RenderShape.ROUND || shape == RenderShape.CIRCLE || shape == RenderShape.CARD) { + 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 + const title = String(node.getTitle()) + (node.pinned ? '📌' : '') + 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' + // const 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) + } } + } - // 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-- - } - - /** - * Draws the selection bounding of an area. - * @param {CanvasRenderingContext2D} ctx - * @param {Vector4} area - * @param {{ - * shape: LiteGraph.Shape, - * title_height: number, - * title_mode: LiteGraph.TitleMode, - * fgcolor: string, - * padding: number, - * }} options - */ - drawSelectionBounding( - ctx: CanvasRenderingContext2D, - area: Rect, - { - shape = RenderShape.BOX, - title_height = LiteGraph.NODE_TITLE_HEIGHT, - title_mode = TitleMode.NORMAL_TITLE, - fgcolor = LiteGraph.NODE_BOX_OUTLINE_COLOR, - padding = 6, - collapsed = false, - }: IDrawSelectionBoundingOptions = {} - ) { - // Adjust area if title is transparent - if (title_mode === TitleMode.TRANSPARENT_TITLE) { - area[1] -= title_height - area[3] += title_height + //subgraph box + if (!node.flags.collapsed && node.subgraph && !node.skip_subgraph_button) { + const w = LiteGraph.NODE_TITLE_HEIGHT + const x = node.size[0] - w + const 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 == RenderShape.BOX || 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() } - - // Set up context - ctx.lineWidth = 1 - ctx.globalAlpha = 0.8 + 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() + } - // Draw shape based on type - const [x, y, width, height] = area - switch (shape) { - case RenderShape.BOX: { - ctx.rect(x - padding, y - padding, width + 2 * padding, height + 2 * padding) - break - } - case RenderShape.ROUND: - case RenderShape.CARD: { - const radius = this.round_radius * 2 - const isCollapsed = shape === RenderShape.CARD && collapsed - const cornerRadii = isCollapsed || shape === RenderShape.ROUND ? [radius] : [radius, 2, radius, 2] - ctx.roundRect(x - padding, y - padding, width + 2 * padding, height + 2 * padding, cornerRadii) - break - } - case RenderShape.CIRCLE: { - const centerX = x + width / 2 - const centerY = y + height / 2 - const radius = Math.max(width, height) / 2 + padding - ctx.arc(centerX, centerY, radius, 0, Math.PI * 2) - break - } - } - - // Stroke the shape - ctx.strokeStyle = LiteGraph.NODE_BOX_OUTLINE_COLOR - ctx.stroke() - - // Reset context - ctx.strokeStyle = fgcolor - ctx.globalAlpha = 1 + //custom title render + node.onDrawTitle?.(ctx) } - drawConnections(ctx: CanvasRenderingContext2D): void { - const now = LiteGraph.getTime() - const visible_area = this.visible_area - LGraphCanvas.#margin_area[0] = visible_area[0] - 20 - LGraphCanvas.#margin_area[1] = visible_area[1] - 20 - LGraphCanvas.#margin_area[2] = visible_area[2] + 40 - LGraphCanvas.#margin_area[3] = visible_area[3] + 40 + //render selection marker + if (selected) { + node.onBounding?.(area) - //draw connections - ctx.lineWidth = this.connections_width - - ctx.fillStyle = "#AAA" - ctx.strokeStyle = "#AAA" - ctx.globalAlpha = this.editor_alpha - //for every node - const nodes = this.graph._nodes - for (let n = 0, l = nodes.length; n < l; ++n) { - const 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 (let i = 0; i < node.inputs.length; ++i) { - const input = node.inputs[i] - if (!input || input.link == null) continue - - const link_id = input.link - const link = this.graph._links.get(link_id) - if (!link) continue - - //find link info - const start_node = this.graph.getNodeById(link.origin_id) - if (start_node == null) continue - - const start_node_slot = link.origin_slot - let start_node_slotpos: Point = 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, - LGraphCanvas.#tempA - ) - } - const end_node_slotpos = node.getConnectionPos(true, i, LGraphCanvas.#tempB) - - //compute link bounding - LGraphCanvas.#link_bounding[0] = start_node_slotpos[0] - LGraphCanvas.#link_bounding[1] = start_node_slotpos[1] - LGraphCanvas.#link_bounding[2] = end_node_slotpos[0] - start_node_slotpos[0] - LGraphCanvas.#link_bounding[3] = end_node_slotpos[1] - start_node_slotpos[1] - if (LGraphCanvas.#link_bounding[2] < 0) { - LGraphCanvas.#link_bounding[0] += LGraphCanvas.#link_bounding[2] - LGraphCanvas.#link_bounding[2] = Math.abs(LGraphCanvas.#link_bounding[2]) - } - if (LGraphCanvas.#link_bounding[3] < 0) { - LGraphCanvas.#link_bounding[1] += LGraphCanvas.#link_bounding[3] - LGraphCanvas.#link_bounding[3] = Math.abs(LGraphCanvas.#link_bounding[3]) - } - - //skip links outside of the visible area of the canvas - if (!overlapBounding(LGraphCanvas.#link_bounding, LGraphCanvas.#margin_area)) - continue - - const start_slot = start_node.outputs[start_node_slot] - const end_slot = node.inputs[i] - if (!start_slot || !end_slot) - continue - const start_dir = start_slot.dir || - (start_node.horizontal ? LinkDirection.DOWN : LinkDirection.RIGHT) - const end_dir = end_slot.dir || - (node.horizontal ? LinkDirection.UP : LinkDirection.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) { - const f = 2.0 - (now - link._last_time) * 0.002 - const 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 + this.drawSelectionBounding(ctx, area, { + shape, + title_height, + title_mode, + fgcolor, + collapsed: node.flags?.collapsed, + }) } - /** - * draws a link between two points - * @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 {LinkDirection} start_dir the direction enum - * @param {LinkDirection} end_dir the direction enum - * @param {number} num_sublines number of sublines (useful to represent vec3 or rgb) - **/ - renderLink(ctx: CanvasRenderingContext2D, - a: Point, - b: Point, - link: LLink, - skip_border: boolean, - flow: number, - color: CanvasColour, - start_dir: LinkDirection, - end_dir: LinkDirection, - num_sublines?: number): void { + // 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-- + } - if (link) { - this.visible_links.push(link) - } - - //choose color - if (!color && link) { - color = link.color || LGraphCanvas.link_type_colors[link.type] - } - color ||= this.default_link_color - if (link != null && this.highlighted_links[link.id]) { - color = "#FFF" - } - - start_dir = start_dir || LinkDirection.RIGHT - end_dir = end_dir || LinkDirection.LEFT - - const dist = distance(a, b) - - // TODO: Subline code below was inserted in the wrong place - should be before this statement - if (this.render_connections_border && this.ds.scale > 0.6) { - ctx.lineWidth = this.connections_width + 4 - } - ctx.lineJoin = "round" - num_sublines ||= 1 - if (num_sublines > 1) { - ctx.lineWidth = 0.5 - } - - //begin line shape - const path = new Path2D() - if (link) { - // Store the path on the link for hittests - link.path = path - } - for (let i = 0; i < num_sublines; i += 1) { - const offsety = (i - (num_sublines - 1) * 0.5) * 5 - - if (this.links_render_mode == LinkRenderType.SPLINE_LINK) { - path.moveTo(a[0], a[1] + offsety) - let start_offset_x = 0 - let start_offset_y = 0 - let end_offset_x = 0 - let end_offset_y = 0 - switch (start_dir) { - case LinkDirection.LEFT: - start_offset_x = dist * -0.25 - break - case LinkDirection.RIGHT: - start_offset_x = dist * 0.25 - break - case LinkDirection.UP: - start_offset_y = dist * -0.25 - break - case LinkDirection.DOWN: - start_offset_y = dist * 0.25 - break - } - switch (end_dir) { - case LinkDirection.LEFT: - end_offset_x = dist * -0.25 - break - case LinkDirection.RIGHT: - end_offset_x = dist * 0.25 - break - case LinkDirection.UP: - end_offset_y = dist * -0.25 - break - case LinkDirection.DOWN: - end_offset_y = dist * 0.25 - break - } - path.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 == LinkRenderType.LINEAR_LINK) { - path.moveTo(a[0], a[1] + offsety) - let start_offset_x = 0 - let start_offset_y = 0 - let end_offset_x = 0 - let end_offset_y = 0 - switch (start_dir) { - case LinkDirection.LEFT: - start_offset_x = -1 - break - case LinkDirection.RIGHT: - start_offset_x = 1 - break - case LinkDirection.UP: - start_offset_y = -1 - break - case LinkDirection.DOWN: - start_offset_y = 1 - break - } - switch (end_dir) { - case LinkDirection.LEFT: - end_offset_x = -1 - break - case LinkDirection.RIGHT: - end_offset_x = 1 - break - case LinkDirection.UP: - end_offset_y = -1 - break - case LinkDirection.DOWN: - end_offset_y = 1 - break - } - const l = 15 - path.lineTo( - a[0] + start_offset_x * l, - a[1] + start_offset_y * l + offsety - ) - path.lineTo( - b[0] + end_offset_x * l, - b[1] + end_offset_y * l + offsety - ) - path.lineTo(b[0], b[1] + offsety) - } else if (this.links_render_mode == LinkRenderType.STRAIGHT_LINK) { - path.moveTo(a[0], a[1]) - let start_x = a[0] - let start_y = a[1] - let end_x = b[0] - let end_y = b[1] - if (start_dir == LinkDirection.RIGHT) { - start_x += 10 - } else { - start_y += 10 - } - if (end_dir == LinkDirection.LEFT) { - end_x -= 10 - } else { - end_y -= 10 - } - path.lineTo(start_x, start_y) - path.lineTo((start_x + end_x) * 0.5, start_y) - path.lineTo((start_x + end_x) * 0.5, end_y) - path.lineTo(end_x, end_y) - path.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(path) - } - - ctx.lineWidth = this.connections_width - ctx.fillStyle = ctx.strokeStyle = color - ctx.stroke(path) - //end line shape - const pos = this.computeConnectionPoint(a, b, 0.5, start_dir, end_dir) - if (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 != LinkDirection.CENTER) { - //render arrow - if (this.render_connection_arrows) { - //compute two points in the connection - const posA = this.computeConnectionPoint( - a, - b, - 0.25, - start_dir, - end_dir - ) - const posB = this.computeConnectionPoint( - a, - b, - 0.26, - start_dir, - end_dir - ) - const posC = this.computeConnectionPoint( - a, - b, - 0.75, - start_dir, - end_dir - ) - const posD = this.computeConnectionPoint( - a, - b, - 0.76, - start_dir, - end_dir - ) - - //compute the angle between them so the arrow points in the right direction - let angleA = 0 - let 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 (let i = 0; i < 5; ++i) { - const f = (LiteGraph.getTime() * 0.001 + i * 0.2) % 1 - const flowPos = this.computeConnectionPoint( - a, - b, - f, - start_dir, - end_dir - ) - ctx.beginPath() - ctx.arc(flowPos[0], flowPos[1], 5, 0, 2 * Math.PI) - ctx.fill() - } - } + /** + * Draws the selection bounding of an area. + * @param {CanvasRenderingContext2D} ctx + * @param {Vector4} area + * @param {{ + * shape: LiteGraph.Shape, + * title_height: number, + * title_mode: LiteGraph.TitleMode, + * fgcolor: string, + * padding: number, + * }} options + */ + drawSelectionBounding( + ctx: CanvasRenderingContext2D, + area: Rect, + { + shape = RenderShape.BOX, + title_height = LiteGraph.NODE_TITLE_HEIGHT, + title_mode = TitleMode.NORMAL_TITLE, + fgcolor = LiteGraph.NODE_BOX_OUTLINE_COLOR, + padding = 6, + collapsed = false, + }: IDrawSelectionBoundingOptions = {}, + ) { + // Adjust area if title is transparent + if (title_mode === TitleMode.TRANSPARENT_TITLE) { + area[1] -= title_height + area[3] += title_height } - //returns the link center point based on curvature - computeConnectionPoint(a: Point, - b: Point, - t: number, - start_dir: number, - end_dir: number): number[] { - start_dir ||= LinkDirection.RIGHT - end_dir ||= LinkDirection.LEFT - const dist = distance(a, b) - const p0 = a - const p1 = [a[0], a[1]] - const p2 = [b[0], b[1]] - const p3 = b + // Set up context + ctx.lineWidth = 1 + ctx.globalAlpha = 0.8 + ctx.beginPath() + // Draw shape based on type + const [x, y, width, height] = area + switch (shape) { + case RenderShape.BOX: { + ctx.rect(x - padding, y - padding, width + 2 * padding, height + 2 * padding) + break + } + case RenderShape.ROUND: + case RenderShape.CARD: { + const radius = this.round_radius * 2 + const isCollapsed = shape === RenderShape.CARD && collapsed + const cornerRadii = isCollapsed || shape === RenderShape.ROUND ? [radius] : [radius, 2, radius, 2] + ctx.roundRect(x - padding, y - padding, width + 2 * padding, height + 2 * padding, cornerRadii) + break + } + case RenderShape.CIRCLE: { + const centerX = x + width / 2 + const centerY = y + height / 2 + const radius = Math.max(width, height) / 2 + padding + ctx.arc(centerX, centerY, radius, 0, Math.PI * 2) + break + } + } + + // Stroke the shape + ctx.strokeStyle = LiteGraph.NODE_BOX_OUTLINE_COLOR + ctx.stroke() + + // Reset context + ctx.strokeStyle = fgcolor + ctx.globalAlpha = 1 + } + + drawConnections(ctx: CanvasRenderingContext2D): void { + const now = LiteGraph.getTime() + const visible_area = this.visible_area + LGraphCanvas.#margin_area[0] = visible_area[0] - 20 + LGraphCanvas.#margin_area[1] = visible_area[1] - 20 + LGraphCanvas.#margin_area[2] = visible_area[2] + 40 + LGraphCanvas.#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 + const nodes = this.graph._nodes + for (let n = 0, l = nodes.length; n < l; ++n) { + const 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 (let i = 0; i < node.inputs.length; ++i) { + const input = node.inputs[i] + if (!input || input.link == null) continue + + const link_id = input.link + const link = this.graph._links.get(link_id) + if (!link) continue + + //find link info + const start_node = this.graph.getNodeById(link.origin_id) + if (start_node == null) continue + + const start_node_slot = link.origin_slot + let start_node_slotpos: Point = 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, LGraphCanvas.#tempA) + } + const end_node_slotpos = node.getConnectionPos(true, i, LGraphCanvas.#tempB) + + //compute link bounding + LGraphCanvas.#link_bounding[0] = start_node_slotpos[0] + LGraphCanvas.#link_bounding[1] = start_node_slotpos[1] + LGraphCanvas.#link_bounding[2] = end_node_slotpos[0] - start_node_slotpos[0] + LGraphCanvas.#link_bounding[3] = end_node_slotpos[1] - start_node_slotpos[1] + if (LGraphCanvas.#link_bounding[2] < 0) { + LGraphCanvas.#link_bounding[0] += LGraphCanvas.#link_bounding[2] + LGraphCanvas.#link_bounding[2] = Math.abs(LGraphCanvas.#link_bounding[2]) + } + if (LGraphCanvas.#link_bounding[3] < 0) { + LGraphCanvas.#link_bounding[1] += LGraphCanvas.#link_bounding[3] + LGraphCanvas.#link_bounding[3] = Math.abs(LGraphCanvas.#link_bounding[3]) + } + + //skip links outside of the visible area of the canvas + if (!overlapBounding(LGraphCanvas.#link_bounding, LGraphCanvas.#margin_area)) continue + + const start_slot = start_node.outputs[start_node_slot] + const end_slot = node.inputs[i] + if (!start_slot || !end_slot) continue + const start_dir = start_slot.dir || (start_node.horizontal ? LinkDirection.DOWN : LinkDirection.RIGHT) + const end_dir = end_slot.dir || (node.horizontal ? LinkDirection.UP : LinkDirection.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) { + const f = 2.0 - (now - link._last_time) * 0.002 + const 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 + * @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 {LinkDirection} start_dir the direction enum + * @param {LinkDirection} end_dir the direction enum + * @param {number} num_sublines number of sublines (useful to represent vec3 or rgb) + **/ + renderLink( + ctx: CanvasRenderingContext2D, + a: Point, + b: Point, + link: LLink, + skip_border: boolean, + flow: number, + color: CanvasColour, + start_dir: LinkDirection, + end_dir: LinkDirection, + num_sublines?: number, + ): void { + if (link) { + this.visible_links.push(link) + } + + //choose color + if (!color && link) { + color = link.color || LGraphCanvas.link_type_colors[link.type] + } + color ||= this.default_link_color + if (link != null && this.highlighted_links[link.id]) { + color = '#FFF' + } + + start_dir = start_dir || LinkDirection.RIGHT + end_dir = end_dir || LinkDirection.LEFT + + const dist = distance(a, b) + + // TODO: Subline code below was inserted in the wrong place - should be before this statement + if (this.render_connections_border && this.ds.scale > 0.6) { + ctx.lineWidth = this.connections_width + 4 + } + ctx.lineJoin = 'round' + num_sublines ||= 1 + if (num_sublines > 1) { + ctx.lineWidth = 0.5 + } + + //begin line shape + const path = new Path2D() + if (link) { + // Store the path on the link for hittests + link.path = path + } + for (let i = 0; i < num_sublines; i += 1) { + const offsety = (i - (num_sublines - 1) * 0.5) * 5 + + if (this.links_render_mode == LinkRenderType.SPLINE_LINK) { + path.moveTo(a[0], a[1] + offsety) + let start_offset_x = 0 + let start_offset_y = 0 + let end_offset_x = 0 + let end_offset_y = 0 switch (start_dir) { - case LinkDirection.LEFT: - p1[0] += dist * -0.25 - break - case LinkDirection.RIGHT: - p1[0] += dist * 0.25 - break - case LinkDirection.UP: - p1[1] += dist * -0.25 - break - case LinkDirection.DOWN: - p1[1] += dist * 0.25 - break + case LinkDirection.LEFT: + start_offset_x = dist * -0.25 + break + case LinkDirection.RIGHT: + start_offset_x = dist * 0.25 + break + case LinkDirection.UP: + start_offset_y = dist * -0.25 + break + case LinkDirection.DOWN: + start_offset_y = dist * 0.25 + break } switch (end_dir) { - case LinkDirection.LEFT: - p2[0] += dist * -0.25 - break - case LinkDirection.RIGHT: - p2[0] += dist * 0.25 - break - case LinkDirection.UP: - p2[1] += dist * -0.25 - break - case LinkDirection.DOWN: - p2[1] += dist * 0.25 - break + case LinkDirection.LEFT: + end_offset_x = dist * -0.25 + break + case LinkDirection.RIGHT: + end_offset_x = dist * 0.25 + break + case LinkDirection.UP: + end_offset_y = dist * -0.25 + break + case LinkDirection.DOWN: + end_offset_y = dist * 0.25 + break } - - const c1 = (1 - t) * (1 - t) * (1 - t) - const c2 = 3 * ((1 - t) * (1 - t)) * t - const c3 = 3 * (1 - t) * (t * t) - const c4 = t * t * t - - const x = c1 * p0[0] + c2 * p1[0] + c3 * p2[0] + c4 * p3[0] - const y = c1 * p0[1] + c2 * p1[1] + c3 * p2[1] + c4 * p3[1] - return [x, y] - } - drawExecutionOrder(ctx: CanvasRenderingContext2D): void { - ctx.shadowColor = "transparent" - ctx.globalAlpha = 0.25 - - ctx.textAlign = "center" - ctx.strokeStyle = "white" - ctx.globalAlpha = 0.75 - - const visible_nodes = this.visible_nodes - for (let i = 0; i < visible_nodes.length; ++i) { - const 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( - stringOrEmpty(node.order), - node.pos[0] + LiteGraph.NODE_TITLE_HEIGHT * -0.5, - node.pos[1] - 6 - ) + path.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 == LinkRenderType.LINEAR_LINK) { + path.moveTo(a[0], a[1] + offsety) + let start_offset_x = 0 + let start_offset_y = 0 + let end_offset_x = 0 + let end_offset_y = 0 + switch (start_dir) { + case LinkDirection.LEFT: + start_offset_x = -1 + break + case LinkDirection.RIGHT: + start_offset_x = 1 + break + case LinkDirection.UP: + start_offset_y = -1 + break + case LinkDirection.DOWN: + start_offset_y = 1 + break } - ctx.globalAlpha = 1 - } - /** - * draws the widgets stored inside a node - **/ - drawNodeWidgets(node: LGraphNode, - posY: number, - ctx: CanvasRenderingContext2D, - active_widget: IWidget) { - if (!node.widgets || !node.widgets.length) return 0 - const width = node.size[0] - const widgets = node.widgets - posY += 2 - const H = LiteGraph.NODE_WIDGET_HEIGHT - const show_text = this.ds.scale > 0.5 - ctx.save() - ctx.globalAlpha = this.editor_alpha - const outline_color = LiteGraph.WIDGET_OUTLINE_COLOR - const background_color = LiteGraph.WIDGET_BGCOLOR - const text_color = LiteGraph.WIDGET_TEXT_COLOR - const secondary_text_color = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR - const margin = 15 - - for (let i = 0; i < widgets.length; ++i) { - const w = widgets[i] - if(w.hidden || (w.advanced && !node.showAdvanced)) continue; - const y = w.y || posY - - if (w === this.link_over_widget) { - ctx.fillStyle = this.default_connection_color_byType[this.link_over_widget_type] || - this.default_connection_color.input_on - - // Manually draw a slot next to the widget simulating an input - drawSlot(ctx, {}, [10, y + 10], {}) - } - - w.last_y = y - ctx.strokeStyle = outline_color - ctx.fillStyle = "#222" - ctx.textAlign = "left" - //ctx.lineWidth = 2; - if (w.disabled) - ctx.globalAlpha *= 0.5 - const 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) - const range = w.options.max - w.options.min - let 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) { - let 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 { - let v = typeof w.value === "number" ? String(w.value) : w.value - if (w.options.values) { - let values = w.options.values - if (typeof values === "function") - // @ts-expect-error - values = values() - if (values && !Array.isArray(values)) - v = values[w.value] - } - const labelWidth = ctx.measureText(w.label || w.name).width + margin * 2 - const inputWidth = widget_width - margin * 4 - const availableWidth = inputWidth - labelWidth - const textWidth = ctx.measureText(v).width - if (textWidth > availableWidth) { - const ELLIPSIS = "\u2026" - const ellipsisWidth = ctx.measureText(ELLIPSIS).width - const charWidthAvg = ctx.measureText("a").width - if (availableWidth <= ellipsisWidth) { - v = "\u2024" // One dot leader - } else { - v = `${v}` - const overflowWidth = (textWidth + ellipsisWidth) - availableWidth - // Only first 3 characters need to be measured precisely - if (overflowWidth + charWidthAvg * 3 > availableWidth) { - const preciseRange = availableWidth + charWidthAvg * 3 - const preTruncateCt = Math.floor((preciseRange - ellipsisWidth) / charWidthAvg) - v = v.substr(0, preTruncateCt) - } - while (ctx.measureText(v).width + ellipsisWidth > availableWidth) { - v = v.substr(0, v.length - 1) - } - v += ELLIPSIS - } - } - 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 - // Custom widgets - default: - w.draw?.(ctx, node, widget_width, y, H) - break - } - posY += (w.computeSize ? w.computeSize(widget_width)[1] : H) + 4 - ctx.globalAlpha = this.editor_alpha - + switch (end_dir) { + case LinkDirection.LEFT: + end_offset_x = -1 + break + case LinkDirection.RIGHT: + end_offset_x = 1 + break + case LinkDirection.UP: + end_offset_y = -1 + break + case LinkDirection.DOWN: + end_offset_y = 1 + break } - ctx.restore() - ctx.textAlign = "left" - } - /** - * process an event on widgets - **/ - processNodeWidgets(node: LGraphNode, - // TODO: Hitting enter does not trigger onWidgetChanged - may require a separate value processor for processKey - pos: Point, - event: CanvasMouseEvent, - active_widget?: IWidget): IWidget { - if (!node.widgets || !node.widgets.length || (!this.allow_interaction && !node.flags.allow_interaction)) { - return null - } - - const x = pos[0] - node.pos[0] - const y = pos[1] - node.pos[1] - const width = node.size[0] - const that = this - const ref_window = this.getCanvasWindow() - - let values - let values_list - for (let i = 0; i < node.widgets.length; ++i) { - const w = node.widgets[i] - if (!w || w.disabled || w.hidden || (w.advanced && !node.showAdvanced)) - continue - const widget_height = w.computeSize ? w.computeSize(width)[1] : LiteGraph.NODE_WIDGET_HEIGHT - const 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 - - const 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": { - // FIXME: This one-function-to-rule-them-all pattern is nuts. Split events into manageable chunks. - 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": { - const 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": { - let delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0 - const allow_scroll = delta && (x > -3 && x < widget_width + 3) - ? false - : true - // TODO: Type checks on widget values - 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") { - values = w.options.values - if (typeof values === "function") { - // @ts-expect-error - values = w.options.values(w, node) - } - values_list = null - - if (w.type != "number") - values_list = Array.isArray(values) ? values : Object.keys(values) - - 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 - let index = -1 - this.last_mouseclick = 0 //avoids dobl click event - index = typeof values === "object" - ? values_list.indexOf(String(w.value)) + delta - : values_list.indexOf(w.value) + delta - - if (index >= values_list.length) index = values_list.length - 1 - if (index < 0) index = 0 - - w.value = Array.isArray(values) - ? values[index] - : index - } else { //combo clicked - const text_values = values != values_list ? Object.values(values) : values - new LiteGraph.ContextMenu(text_values, { - scale: Math.max(1, this.ds.scale), - event: event, - className: "dark", - callback: inner_clicked.bind(w) - }, - // @ts-expect-error Not impl - harmless - ref_window) - function inner_clicked(v) { - 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") { - 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 { } - } - 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: any) { - 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) { - node.onWidgetChanged?.(w.name, w.value, old_value, w) - node.graph._version++ - } - - return w - } //end for - - function inner_value_change(widget: IWidget, value: TWidgetValue) { - const v = widget.type === "number" ? Number(value) : value - widget.value = v - if (widget.options?.property && node.properties[widget.options.property] !== undefined) { - node.setProperty(widget.options.property, v) - } - widget.callback?.(widget.value, that, node, pos, event) - } - - return null - } - - /** - * draws every group area in the background - **/ - drawGroups(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D): void { - if (!this.graph) return - - const groups = this.graph._groups - - ctx.save() - ctx.globalAlpha = 0.5 * this.editor_alpha - - for (let i = 0; i < groups.length; ++i) { - const group = groups[i] - - if (!overlapBounding(this.visible_area, group._bounding)) { - continue - } //out of the visible area - - group.draw(this, ctx) - } - - ctx.restore() - } - adjustNodesSize(): void { - const nodes = this.graph._nodes - for (let 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 - * @todo Remove or rewrite - **/ - resize(width?: number, height?: number): void { - if (!width && !height) { - const parent = this.canvas.parentElement - 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 - **/ - switchLiveMode(transition: boolean): void { - if (!transition) { - this.live_mode = !this.live_mode - this.dirty_canvas = true - this.dirty_bgcanvas = true - return - } - - const self = this - const delta = this.live_mode ? 1.1 : 0.9 - if (this.live_mode) { - this.live_mode = false - this.editor_alpha = 0.1 - } - - const 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) - } - - onNodeSelectionChange(): void { } - - /** - * Determines the furthest nodes in each direction for the currently selected nodes - * @return {{left: LGraphNode, top: LGraphNode, right: LGraphNode, bottom: LGraphNode}} - */ - boundaryNodesForSelection(): NullableProperties { - return LGraphCanvas.getBoundaryNodes(this.selected_nodes) - } - showLinkMenu(link: LLink, e: CanvasMouseEvent): boolean { - const graph = this.graph - const node_left = graph.getNodeById(link.origin_id) - const node_right = graph.getNodeById(link.target_id) - // TODO: Replace ternary with ?? "" - const fromType = node_left?.outputs?.[link.origin_slot] - ? node_left.outputs[link.origin_slot].type - : false - const destType = node_right?.outputs?.[link.target_slot] - ? node_right.inputs[link.target_slot].type - : false - - const options = ["Add Node", null, "Delete", null] - - const menu = new LiteGraph.ContextMenu(options, { - event: e, - title: link.data != null ? link.data.constructor.name : null, - callback: inner_clicked - }) - - function inner_clicked(v: string, options: unknown, e: MouseEvent) { - switch (v) { - case "Add Node": - LGraphCanvas.onMenuAdd(null, null, e, menu, function (node) { - if (!node.inputs?.length || !node.outputs?.length) return - - // leave the connection type checking inside connectByType - // @ts-expect-error Assigning from check to false results in the type being treated as "*". This should fail. - if (node_left.connectByType(link.origin_slot, node, fromType)) { - // @ts-expect-error Assigning from check to false results in the type being treated as "*". This should fail. - node.connectByType(link.target_slot, node_right, destType) - node.pos[0] -= node.size[0] * 0.5 - } - }) - break - - case "Delete": - graph.removeLink(link.id) - break - default: - } - } - - return false - } - createDefaultNodeForSlot(optPass: ICreateNodeOptions): boolean { - const opts = Object.assign({ - nodeFrom: null, - slotFrom: null, - nodeTo: null, - slotTo: null, - position: [0, 0], - nodeType: null, - posAdd: [0, 0], - posSizeFix: [0, 0] - }, optPass || {}) - - const isFrom = opts.nodeFrom && opts.slotFrom !== null - const 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 - } - - const nodeX = isFrom ? opts.nodeFrom : opts.nodeTo - let slotX = isFrom ? opts.slotFrom : opts.slotTo - - let iSlotConn: number | false = 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: - console.warn("Cant get slot information " + slotX) - return false - } - - // check for defaults nodes for this slottype - const fromSlotType = slotX.type == LiteGraph.EVENT ? "_event_" : slotX.type - const slotTypesDefault = isFrom ? LiteGraph.slot_types_default_out : LiteGraph.slot_types_default_in - if (slotTypesDefault?.[fromSlotType]) { - // TODO: Remove "any" kludge - let nodeNewType: any = false - if (typeof slotTypesDefault[fromSlotType] == "object") { - for (const typeX in slotTypesDefault[fromSlotType]) { - if (opts.nodeType == slotTypesDefault[fromSlotType][typeX] || opts.nodeType == "AUTO") { - nodeNewType = slotTypesDefault[fromSlotType][typeX] - break - } - } - } else if (opts.nodeType == slotTypesDefault[fromSlotType] || opts.nodeType == "AUTO") { - nodeNewType = slotTypesDefault[fromSlotType] - } - if (nodeNewType) { - // TODO: Remove "any" kludge - let nodeNewOpts: any = false - if (typeof nodeNewType == "object" && nodeNewType.node) { - nodeNewOpts = nodeNewType - nodeNewType = nodeNewType.node - } - - //that.graph.beforeChange(); - const newNode = LiteGraph.createNode(nodeNewType) - if (newNode) { - // if is object pass options - if (nodeNewOpts) { - if (nodeNewOpts.properties) { - for (const i in nodeNewOpts.properties) { - newNode.addProperty(i, nodeNewOpts.properties[i]) - } - } - if (nodeNewOpts.inputs) { - newNode.inputs = [] - for (const i in nodeNewOpts.inputs) { - newNode.addOutput( - nodeNewOpts.inputs[i][0], - nodeNewOpts.inputs[i][1] - ) - } - } - if (nodeNewOpts.outputs) { - newNode.outputs = [] - for (const 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 - this.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) - ] - - // 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 - } - console.log("failed creating " + nodeNewType) - } - } - return false - } - showConnectionMenu(optPass: Partial): void { - const opts = Object.assign({ - nodeFrom: null, - slotFrom: null, - nodeTo: null, - slotTo: null, - e: null, - allow_searchbox: this.allow_searchbox, - showSearchBox: this.showSearchBox, - }, optPass || {}) - const that = this - - const isFrom = opts.nodeFrom && opts.slotFrom - const isTo = !isFrom && opts.nodeTo && opts.slotTo - - if (!isFrom && !isTo) { - console.warn("No data passed to showConnectionMenu") - return - } - - const nodeX = isFrom ? opts.nodeFrom : opts.nodeTo - let slotX = isFrom ? opts.slotFrom : opts.slotTo - - let iSlotConn: number - 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: - console.warn("Cant get slot information " + slotX) - return - } - - const options = ["Add Node", null] - - if (opts.allow_searchbox) { - options.push("Search") - options.push(null) - } - - // get defaults nodes for this slottype - const fromSlotType = slotX.type == LiteGraph.EVENT ? "_event_" : slotX.type - const slotTypesDefault = isFrom ? LiteGraph.slot_types_default_out : LiteGraph.slot_types_default_in - if (slotTypesDefault?.[fromSlotType]) { - if (typeof slotTypesDefault[fromSlotType] == "object") { - for (const typeX in slotTypesDefault[fromSlotType]) { - options.push(slotTypesDefault[fromSlotType][typeX]) - } - } else { - options.push(slotTypesDefault[fromSlotType]) - } - } - - // build menu - const 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: string, options: unknown, e: MouseEvent) { - //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) { - opts.showSearchBox(e, { node_from: opts.nodeFrom, slot_from: slotX, type_filter_in: fromSlotType }) - } else { - opts.showSearchBox(e, { node_to: opts.nodeTo, slot_from: slotX, type_filter_out: fromSlotType }) - } - break - default: { - // check for defaults nodes for this slottype - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const nodeCreated = that.createDefaultNodeForSlot(Object.assign(opts, { - position: [opts.e.canvasX, opts.e.canvasY], - nodeType: v - })) - break - } - } - } - } - // refactor: there are different dialogs, some uses createDialog some dont - prompt(title: string, value: any, callback: (arg0: any) => void, event: CanvasMouseEvent, multiline?: boolean): HTMLDivElement { - const that = this - title = title || "" - - const dialog: IDialog = document.createElement("div") - dialog.is_modified = false - dialog.className = "graphdialog rounded" - dialog.innerHTML = multiline - ? " " - : " " - dialog.close = function () { - that.prompt_box = null - if (dialog.parentNode) { - dialog.parentNode.removeChild(dialog) - } - } - - const graphcanvas = LGraphCanvas.active_canvas - const canvas = graphcanvas.canvas - canvas.parentNode.appendChild(dialog) - - if (this.ds.scale > 1) dialog.style.transform = "scale(" + this.ds.scale + ")" - - let dialogCloseTimer = null - let prevent_timeout = 0 - LiteGraph.pointerListenerAdd(dialog, "leave", function () { - 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 () { - if (LiteGraph.dialog_close_on_mouse_leave && dialogCloseTimer) - clearTimeout(dialogCloseTimer) - }) - const selInDia = dialog.querySelectorAll("select") - if (selInDia) { - // if filtering, check focus changed to comboboxes and prevent closing - for (const selIn of selInDia) { - selIn.addEventListener("click", function () { prevent_timeout++ }) - selIn.addEventListener("blur", function () { prevent_timeout = 0 }) - selIn.addEventListener("change", function () { prevent_timeout = -1 }) - } - } - this.prompt_box?.close() - this.prompt_box = dialog - - const name_element: HTMLSpanElement = dialog.querySelector(".name") - name_element.innerText = title - const value_element: HTMLTextAreaElement | HTMLInputElement = dialog.querySelector(".value") - value_element.value = value - value_element.select() - - const input = value_element - input.addEventListener("keydown", function (e: KeyboardEvent) { - dialog.is_modified = true - if (e.keyCode == 27) { - //ESC - dialog.close() - } else if (e.keyCode == 13 && (e.target as Element).localName != "textarea") { - if (callback) { - callback(this.value) - } - dialog.close() - } else { - return - } - e.preventDefault() - e.stopPropagation() - }) - - const button = dialog.querySelector("button") - button.addEventListener("click", function () { - callback?.(input.value) - that.setDirty(true) - dialog.close() - }) - - const rect = canvas.getBoundingClientRect() - let offsetx = -20 - let 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" + const l = 15 + path.lineTo(a[0] + start_offset_x * l, a[1] + start_offset_y * l + offsety) + path.lineTo(b[0] + end_offset_x * l, b[1] + end_offset_y * l + offsety) + path.lineTo(b[0], b[1] + offsety) + } else if (this.links_render_mode == LinkRenderType.STRAIGHT_LINK) { + path.moveTo(a[0], a[1]) + let start_x = a[0] + let start_y = a[1] + let end_x = b[0] + let end_y = b[1] + if (start_dir == LinkDirection.RIGHT) { + start_x += 10 } else { - dialog.style.left = canvas.width * 0.5 + offsetx + "px" - dialog.style.top = canvas.height * 0.5 + offsety + "px" + start_y += 10 } - - setTimeout(function () { - input.focus() - const clickTime = Date.now() - function handleOutsideClick(e: MouseEvent) { - if (e.target === canvas && Date.now() - clickTime > 256) { - dialog.close() - canvas.parentNode.removeEventListener("click", handleOutsideClick) - canvas.parentNode.removeEventListener("touchend", handleOutsideClick) - } - } - canvas.parentNode.addEventListener("click", handleOutsideClick) - canvas.parentNode.addEventListener("touchend", handleOutsideClick) - }, 10) - - return dialog + if (end_dir == LinkDirection.LEFT) { + end_x -= 10 + } else { + end_y -= 10 + } + path.lineTo(start_x, start_y) + path.lineTo((start_x + end_x) * 0.5, start_y) + path.lineTo((start_x + end_x) * 0.5, end_y) + path.lineTo(end_x, end_y) + path.lineTo(b[0], b[1]) + } else { + return + } //unknown } - showSearchBox(event: CanvasMouseEvent, options?: IShowSearchOptions): HTMLDivElement { - // proposed defaults - const def_options: IShowSearchOptions = { - 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 - , - // @ts-expect-error - type_filter_in: false // these are default: pass to set initially set values - , + //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(path) + } - 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 || {}) + ctx.lineWidth = this.connections_width + ctx.fillStyle = ctx.strokeStyle = color + ctx.stroke(path) + //end line shape + const pos = this.computeConnectionPoint(a, b, 0.5, start_dir, end_dir) + if (link?._pos) { + link._pos[0] = pos[0] + link._pos[1] = pos[1] + } - //console.log(options); - const that = this - const graphcanvas = LGraphCanvas.active_canvas - const canvas = graphcanvas.canvas - const root_document = canvas.ownerDocument || document + //render arrow in the middle + if (this.ds.scale >= 0.6 && this.highquality_render && end_dir != LinkDirection.CENTER) { + //render arrow + if (this.render_connection_arrows) { + //compute two points in the connection + const posA = this.computeConnectionPoint(a, b, 0.25, start_dir, end_dir) + const posB = this.computeConnectionPoint(a, b, 0.26, start_dir, end_dir) + const posC = this.computeConnectionPoint(a, b, 0.75, start_dir, end_dir) + const posD = this.computeConnectionPoint(a, b, 0.76, start_dir, end_dir) - const 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 - let selIn - let selOut - if (options.do_type_filter) { - selIn = dialog.querySelector(".slot_in_type_filter") - selOut = dialog.querySelector(".slot_out_type_filter") + //compute the angle between them so the arrow points in the right direction + let angleA = 0 + let 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 } - // @ts-expect-error Panel? - dialog.close = function () { - that.search_box = null - this.blur() - canvas.focus() - root_document.body.style.overflow = "" + //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 (let i = 0; i < 5; ++i) { + const f = (LiteGraph.getTime() * 0.001 + i * 0.2) % 1 + const flowPos = this.computeConnectionPoint(a, b, f, start_dir, end_dir) + ctx.beginPath() + ctx.arc(flowPos[0], flowPos[1], 5, 0, 2 * Math.PI) + ctx.fill() + } + } + } + //returns the link center point based on curvature + computeConnectionPoint(a: Point, b: Point, t: number, start_dir: number, end_dir: number): number[] { + start_dir ||= LinkDirection.RIGHT + end_dir ||= LinkDirection.LEFT + + const dist = distance(a, b) + const p0 = a + const p1 = [a[0], a[1]] + const p2 = [b[0], b[1]] + const p3 = b + + switch (start_dir) { + case LinkDirection.LEFT: + p1[0] += dist * -0.25 + break + case LinkDirection.RIGHT: + p1[0] += dist * 0.25 + break + case LinkDirection.UP: + p1[1] += dist * -0.25 + break + case LinkDirection.DOWN: + p1[1] += dist * 0.25 + break + } + switch (end_dir) { + case LinkDirection.LEFT: + p2[0] += dist * -0.25 + break + case LinkDirection.RIGHT: + p2[0] += dist * 0.25 + break + case LinkDirection.UP: + p2[1] += dist * -0.25 + break + case LinkDirection.DOWN: + p2[1] += dist * 0.25 + break + } + + const c1 = (1 - t) * (1 - t) * (1 - t) + const c2 = 3 * ((1 - t) * (1 - t)) * t + const c3 = 3 * (1 - t) * (t * t) + const c4 = t * t * t + + const x = c1 * p0[0] + c2 * p1[0] + c3 * p2[0] + c4 * p3[0] + const y = c1 * p0[1] + c2 * p1[1] + c3 * p2[1] + c4 * p3[1] + return [x, y] + } + drawExecutionOrder(ctx: CanvasRenderingContext2D): void { + ctx.shadowColor = 'transparent' + ctx.globalAlpha = 0.25 + + ctx.textAlign = 'center' + ctx.strokeStyle = 'white' + ctx.globalAlpha = 0.75 + + const visible_nodes = this.visible_nodes + for (let i = 0; i < visible_nodes.length; ++i) { + const 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(stringOrEmpty(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 + **/ + drawNodeWidgets(node: LGraphNode, posY: number, ctx: CanvasRenderingContext2D, active_widget: IWidget) { + if (!node.widgets || !node.widgets.length) return 0 + const width = node.size[0] + const widgets = node.widgets + posY += 2 + const H = LiteGraph.NODE_WIDGET_HEIGHT + const show_text = this.ds.scale > 0.5 + ctx.save() + ctx.globalAlpha = this.editor_alpha + const outline_color = LiteGraph.WIDGET_OUTLINE_COLOR + const background_color = LiteGraph.WIDGET_BGCOLOR + const text_color = LiteGraph.WIDGET_TEXT_COLOR + const secondary_text_color = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR + const margin = 15 + + for (let i = 0; i < widgets.length; ++i) { + const w = widgets[i] + if (w.hidden || (w.advanced && !node.showAdvanced)) continue + const y = w.y || posY + + if (w === this.link_over_widget) { + ctx.fillStyle = this.default_connection_color_byType[this.link_over_widget_type] || this.default_connection_color.input_on + + // Manually draw a slot next to the widget simulating an input + drawSlot(ctx, {}, [10, y + 10], {}) + } + + w.last_y = y + ctx.strokeStyle = outline_color + ctx.fillStyle = '#222' + ctx.textAlign = 'left' + //ctx.lineWidth = 2; + if (w.disabled) ctx.globalAlpha *= 0.5 + const 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) + const range = w.options.max - w.options.min + let 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) { + let 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 { + let v = typeof w.value === 'number' ? String(w.value) : w.value + if (w.options.values) { + let values = w.options.values + if (typeof values === 'function') + // @ts-expect-error + values = values() + if (values && !Array.isArray(values)) v = values[w.value] + } + const labelWidth = ctx.measureText(w.label || w.name).width + margin * 2 + const inputWidth = widget_width - margin * 4 + const availableWidth = inputWidth - labelWidth + const textWidth = ctx.measureText(v).width + if (textWidth > availableWidth) { + const ELLIPSIS = '\u2026' + const ellipsisWidth = ctx.measureText(ELLIPSIS).width + const charWidthAvg = ctx.measureText('a').width + if (availableWidth <= ellipsisWidth) { + v = '\u2024' // One dot leader + } else { + v = `${v}` + const overflowWidth = textWidth + ellipsisWidth - availableWidth + // Only first 3 characters need to be measured precisely + if (overflowWidth + charWidthAvg * 3 > availableWidth) { + const preciseRange = availableWidth + charWidthAvg * 3 + const preTruncateCt = Math.floor((preciseRange - ellipsisWidth) / charWidthAvg) + v = v.substr(0, preTruncateCt) + } + while (ctx.measureText(v).width + ellipsisWidth > availableWidth) { + v = v.substr(0, v.length - 1) + } + v += ELLIPSIS + } + } + 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 + // Custom widgets + default: + 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 + **/ + processNodeWidgets( + node: LGraphNode, + // TODO: Hitting enter does not trigger onWidgetChanged - may require a separate value processor for processKey + pos: Point, + event: CanvasMouseEvent, + active_widget?: IWidget, + ): IWidget { + if (!node.widgets || !node.widgets.length || (!this.allow_interaction && !node.flags.allow_interaction)) { + return null + } + + const x = pos[0] - node.pos[0] + const y = pos[1] - node.pos[1] + const width = node.size[0] + const that = this + const ref_window = this.getCanvasWindow() + + let values + let values_list + for (let i = 0; i < node.widgets.length; ++i) { + const w = node.widgets[i] + if (!w || w.disabled || w.hidden || (w.advanced && !node.showAdvanced)) continue + const widget_height = w.computeSize ? w.computeSize(width)[1] : LiteGraph.NODE_WIDGET_HEIGHT + const 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 + + const 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': { + // FIXME: This one-function-to-rule-them-all pattern is nuts. Split events into manageable chunks. + 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': { + const 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 () { - that.canvas.focus() - }, 20) //important, if canvas loses focus keys wont be captured - dialog.parentNode?.removeChild(dialog) + inner_value_change(w, w.value) + }, 20) + } + this.dirty_canvas = true + break } - - if (this.ds.scale > 1) { - dialog.style.transform = "scale(" + this.ds.scale + ")" - } - - // hide on mouse leave - if (options.hide_on_mouse_leave) { - // FIXME: Remove "any" kludge - let prevent_timeout: any = false - let timeout_close = null - LiteGraph.pointerListenerAdd(dialog, "enter", function () { - if (timeout_close) { - clearTimeout(timeout_close) - timeout_close = null - } - }) - LiteGraph.pointerListenerAdd(dialog, "leave", function () { - if (prevent_timeout) - return - timeout_close = setTimeout(function () { - // @ts-expect-error Panel? - 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 () { - prevent_timeout++ - }) - selIn.addEventListener("blur", function () { - prevent_timeout = 0 - }) - selIn.addEventListener("change", function () { - prevent_timeout = -1 - }) - selOut.addEventListener("click", function () { - prevent_timeout++ - }) - selOut.addEventListener("blur", function () { - prevent_timeout = 0 - }) - selOut.addEventListener("change", function () { - prevent_timeout = -1 - }) + case 'number': + case 'combo': { + let delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0 + const allow_scroll = delta && x > -3 && x < widget_width + 3 ? false : true + // TODO: Type checks on widget values + 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 } - } - - // @ts-expect-error Panel? - that.search_box?.close() - that.search_box = dialog - - const helper = dialog.querySelector(".helper") - - let first = null - let timeout = null - let selected = null - - const input = dialog.querySelector("input") - if (input) { - input.addEventListener("blur", function () { - 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 - // @ts-expect-error Panel? - dialog.close() - } else if (e.keyCode == 13) { - if (selected) { - select(unescape(selected.dataset["type"])) - } else if (first) { - select(first) - } else { - // @ts-expect-error Panel? - 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) { - const aSlots = LiteGraph.slot_types_in - const 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 (let iK = 0; iK < nSlots; iK++) { - const opt = document.createElement('option') - opt.value = aSlots[iK] - opt.innerHTML = aSlots[iK] - selIn.appendChild(opt) - // @ts-expect-error - if (options.type_filter_in !== false && (options.type_filter_in + "").toLowerCase() == (aSlots[iK] + "").toLowerCase()) { - //selIn.selectedIndex .. - opt.selected = true - //console.log("comparing IN "+options.type_filter_in+" :: "+aSlots[iK]); - } else { - //console.log("comparing OUT "+options.type_filter_in+" :: "+aSlots[iK]); - } - } - selIn.addEventListener("change", function () { - refreshHelper() - }) + if (w.options.max != null && w.value > w.options.max) { + w.value = w.options.max } - if (selOut) { - const aSlots = LiteGraph.slot_types_out - const nSlots = aSlots.length // this for object :: Object.keys(aSlots).length; - - if (options.type_filter_out == LiteGraph.EVENT || options.type_filter_out == LiteGraph.ACTION) - options.type_filter_out = "_event_" - /* this will filter on * .. but better do it manually in case - else if(options.type_filter_out === "" || options.type_filter_out === 0) - options.type_filter_out = "*";*/ - for (let iK = 0; iK < nSlots; iK++) { - const opt = document.createElement('option') - opt.value = aSlots[iK] - opt.innerHTML = aSlots[iK] - selOut.appendChild(opt) - if (options.type_filter_out !== false && (options.type_filter_out + "").toLowerCase() == (aSlots[iK] + "").toLowerCase()) - opt.selected = true - } - selOut.addEventListener("change", function () { - refreshHelper() - }) + } else if (event.type == LiteGraph.pointerevents_method + 'down') { + values = w.options.values + if (typeof values === 'function') { + // @ts-expect-error + values = w.options.values(w, node) } + values_list = null + + if (w.type != 'number') values_list = Array.isArray(values) ? values : Object.keys(values) + + 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 + let index = -1 + this.last_mouseclick = 0 //avoids dobl click event + index = typeof values === 'object' ? values_list.indexOf(String(w.value)) + delta : values_list.indexOf(w.value) + delta + + if (index >= values_list.length) index = values_list.length - 1 + if (index < 0) index = 0 + + w.value = Array.isArray(values) ? values[index] : index + } else { + //combo clicked + const text_values = values != values_list ? Object.values(values) : values + new LiteGraph.ContextMenu( + text_values, + { + scale: Math.max(1, this.ds.scale), + event: event, + className: 'dark', + callback: inner_clicked.bind(w), + }, + // @ts-expect-error Not impl - harmless + ref_window, + ) + function inner_clicked(v) { + 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') { + 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 {} + } + 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: any) { + 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) { + node.onWidgetChanged?.(w.name, w.value, old_value, w) + node.graph._version++ + } + + return w + } //end for + + function inner_value_change(widget: IWidget, value: TWidgetValue) { + const v = widget.type === 'number' ? Number(value) : value + widget.value = v + if (widget.options?.property && node.properties[widget.options.property] !== undefined) { + node.setProperty(widget.options.property, v) + } + widget.callback?.(widget.value, that, node, pos, event) + } + + return null + } + + /** + * draws every group area in the background + **/ + drawGroups(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D): void { + if (!this.graph) return + + const groups = this.graph._groups + + ctx.save() + ctx.globalAlpha = 0.5 * this.editor_alpha + + for (let i = 0; i < groups.length; ++i) { + const group = groups[i] + + if (!overlapBounding(this.visible_area, group._bounding)) { + continue + } //out of the visible area + + group.draw(this, ctx) + } + + ctx.restore() + } + adjustNodesSize(): void { + const nodes = this.graph._nodes + for (let 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 + * @todo Remove or rewrite + **/ + resize(width?: number, height?: number): void { + if (!width && !height) { + const parent = this.canvas.parentElement + 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 + **/ + switchLiveMode(transition: boolean): void { + if (!transition) { + this.live_mode = !this.live_mode + this.dirty_canvas = true + this.dirty_bgcanvas = true + return + } + + const self = this + const delta = this.live_mode ? 1.1 : 0.9 + if (this.live_mode) { + this.live_mode = false + this.editor_alpha = 0.1 + } + + const 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) + } + + onNodeSelectionChange(): void {} + + /** + * Determines the furthest nodes in each direction for the currently selected nodes + * @return {{left: LGraphNode, top: LGraphNode, right: LGraphNode, bottom: LGraphNode}} + */ + boundaryNodesForSelection(): NullableProperties { + return LGraphCanvas.getBoundaryNodes(this.selected_nodes) + } + showLinkMenu(link: LLink, e: CanvasMouseEvent): boolean { + const graph = this.graph + const node_left = graph.getNodeById(link.origin_id) + const node_right = graph.getNodeById(link.target_id) + // TODO: Replace ternary with ?? "" + const fromType = node_left?.outputs?.[link.origin_slot] ? node_left.outputs[link.origin_slot].type : false + const destType = node_right?.outputs?.[link.target_slot] ? node_right.inputs[link.target_slot].type : false + + const options = ['Add Node', null, 'Delete', null] + + const menu = new LiteGraph.ContextMenu(options, { + event: e, + title: link.data != null ? link.data.constructor.name : null, + callback: inner_clicked, + }) + + function inner_clicked(v: string, options: unknown, e: MouseEvent) { + switch (v) { + case 'Add Node': + LGraphCanvas.onMenuAdd(null, null, e, menu, function (node) { + if (!node.inputs?.length || !node.outputs?.length) return + + // leave the connection type checking inside connectByType + // @ts-expect-error Assigning from check to false results in the type being treated as "*". This should fail. + if (node_left.connectByType(link.origin_slot, node, fromType)) { + // @ts-expect-error Assigning from check to false results in the type being treated as "*". This should fail. + node.connectByType(link.target_slot, node_right, destType) + node.pos[0] -= node.size[0] * 0.5 + } + }) + break + + case 'Delete': + graph.removeLink(link.id) + break + default: + } + } + + return false + } + createDefaultNodeForSlot(optPass: ICreateNodeOptions): boolean { + const opts = Object.assign( + { + nodeFrom: null, + slotFrom: null, + nodeTo: null, + slotTo: null, + position: [0, 0], + nodeType: null, + posAdd: [0, 0], + posSizeFix: [0, 0], + }, + optPass || {}, + ) + + const isFrom = opts.nodeFrom && opts.slotFrom !== null + const 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 + } + + const nodeX = isFrom ? opts.nodeFrom : opts.nodeTo + let slotX = isFrom ? opts.slotFrom : opts.slotTo + + let iSlotConn: number | false = 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: + console.warn('Cant get slot information ' + slotX) + return false + } + + // check for defaults nodes for this slottype + const fromSlotType = slotX.type == LiteGraph.EVENT ? '_event_' : slotX.type + const slotTypesDefault = isFrom ? LiteGraph.slot_types_default_out : LiteGraph.slot_types_default_in + if (slotTypesDefault?.[fromSlotType]) { + // TODO: Remove "any" kludge + let nodeNewType: any = false + if (typeof slotTypesDefault[fromSlotType] == 'object') { + for (const typeX in slotTypesDefault[fromSlotType]) { + if (opts.nodeType == slotTypesDefault[fromSlotType][typeX] || opts.nodeType == 'AUTO') { + nodeNewType = slotTypesDefault[fromSlotType][typeX] + break + } + } + } else if (opts.nodeType == slotTypesDefault[fromSlotType] || opts.nodeType == 'AUTO') { + nodeNewType = slotTypesDefault[fromSlotType] + } + if (nodeNewType) { + // TODO: Remove "any" kludge + let nodeNewOpts: any = false + if (typeof nodeNewType == 'object' && nodeNewType.node) { + nodeNewOpts = nodeNewType + nodeNewType = nodeNewType.node } - //compute best position - const rect = canvas.getBoundingClientRect() + //that.graph.beforeChange(); + const newNode = LiteGraph.createNode(nodeNewType) + if (newNode) { + // if is object pass options + if (nodeNewOpts) { + if (nodeNewOpts.properties) { + for (const i in nodeNewOpts.properties) { + newNode.addProperty(i, nodeNewOpts.properties[i]) + } + } + if (nodeNewOpts.inputs) { + newNode.inputs = [] + for (const i in nodeNewOpts.inputs) { + newNode.addOutput(nodeNewOpts.inputs[i][0], nodeNewOpts.inputs[i][1]) + } + } + if (nodeNewOpts.outputs) { + newNode.outputs = [] + for (const 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) + } + } - const left = (event ? event.clientX : (rect.left + rect.width * 0.5)) - 80 - const top = (event ? event.clientY : (rect.top + rect.height * 0.5)) - 20 - dialog.style.left = left + "px" - dialog.style.top = top + "px" + // add the node + this.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), + ] - //To avoid out of screen problems - if (event.layerY > (rect.height - 200)) - // @ts-expect-error - helper.style.maxHeight = (rect.height - event.layerY - 20) + "px" + // connect the two! + if (isFrom) { + opts.nodeFrom.connectByType(iSlotConn, newNode, fromSlotType) + } else { + opts.nodeTo.connectByTypeOutput(iSlotConn, newNode, fromSlotType) + } - requestAnimationFrame(function () { - input.focus() + // if connecting in between + if (isFrom && isTo) { + // TODO + } + + return true + } + console.log('failed creating ' + nodeNewType) + } + } + return false + } + showConnectionMenu(optPass: Partial): void { + const opts = Object.assign( + { + nodeFrom: null, + slotFrom: null, + nodeTo: null, + slotTo: null, + e: null, + allow_searchbox: this.allow_searchbox, + showSearchBox: this.showSearchBox, + }, + optPass || {}, + ) + const that = this + + const isFrom = opts.nodeFrom && opts.slotFrom + const isTo = !isFrom && opts.nodeTo && opts.slotTo + + if (!isFrom && !isTo) { + console.warn('No data passed to showConnectionMenu') + return + } + + const nodeX = isFrom ? opts.nodeFrom : opts.nodeTo + let slotX = isFrom ? opts.slotFrom : opts.slotTo + + let iSlotConn: number + 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: + console.warn('Cant get slot information ' + slotX) + return + } + + const options = ['Add Node', null] + + if (opts.allow_searchbox) { + options.push('Search') + options.push(null) + } + + // get defaults nodes for this slottype + const fromSlotType = slotX.type == LiteGraph.EVENT ? '_event_' : slotX.type + const slotTypesDefault = isFrom ? LiteGraph.slot_types_default_out : LiteGraph.slot_types_default_in + if (slotTypesDefault?.[fromSlotType]) { + if (typeof slotTypesDefault[fromSlotType] == 'object') { + for (const typeX in slotTypesDefault[fromSlotType]) { + options.push(slotTypesDefault[fromSlotType][typeX]) + } + } else { + options.push(slotTypesDefault[fromSlotType]) + } + } + + // build menu + const 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: string, options: unknown, e: MouseEvent) { + //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) { + opts.showSearchBox(e, { node_from: opts.nodeFrom, slot_from: slotX, type_filter_in: fromSlotType }) + } else { + opts.showSearchBox(e, { node_to: opts.nodeTo, slot_from: slotX, type_filter_out: fromSlotType }) + } + break + default: { + // check for defaults nodes for this slottype + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const nodeCreated = that.createDefaultNodeForSlot( + Object.assign(opts, { + position: [opts.e.canvasX, opts.e.canvasY], + nodeType: v, + }), + ) + break + } + } + } + } + // refactor: there are different dialogs, some uses createDialog some dont + prompt(title: string, value: any, callback: (arg0: any) => void, event: CanvasMouseEvent, multiline?: boolean): HTMLDivElement { + const that = this + title = title || '' + + const dialog: IDialog = document.createElement('div') + dialog.is_modified = false + dialog.className = 'graphdialog rounded' + dialog.innerHTML = multiline + ? " " + : " " + dialog.close = function () { + that.prompt_box = null + if (dialog.parentNode) { + dialog.parentNode.removeChild(dialog) + } + } + + const graphcanvas = LGraphCanvas.active_canvas + const canvas = graphcanvas.canvas + canvas.parentNode.appendChild(dialog) + + if (this.ds.scale > 1) dialog.style.transform = 'scale(' + this.ds.scale + ')' + + let dialogCloseTimer = null + let prevent_timeout = 0 + LiteGraph.pointerListenerAdd(dialog, 'leave', function () { + 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 () { + if (LiteGraph.dialog_close_on_mouse_leave && dialogCloseTimer) clearTimeout(dialogCloseTimer) + }) + const selInDia = dialog.querySelectorAll('select') + if (selInDia) { + // if filtering, check focus changed to comboboxes and prevent closing + for (const selIn of selInDia) { + selIn.addEventListener('click', function () { + prevent_timeout++ }) - if (options.show_all_on_open) refreshHelper() + selIn.addEventListener('blur', function () { + prevent_timeout = 0 + }) + selIn.addEventListener('change', function () { + prevent_timeout = -1 + }) + } + } + this.prompt_box?.close() + this.prompt_box = dialog - function select(name) { - if (name) { - if (that.onSearchBoxSelection) { - that.onSearchBoxSelection(name, event, graphcanvas) - } else { - const extra = LiteGraph.searchbox_extras[name.toLowerCase()] - if (extra) - name = extra.type + const name_element: HTMLSpanElement = dialog.querySelector('.name') + name_element.innerText = title + const value_element: HTMLTextAreaElement | HTMLInputElement = dialog.querySelector('.value') + value_element.value = value + value_element.select() - graphcanvas.graph.beforeChange() - const node = LiteGraph.createNode(name) - if (node) { - node.pos = graphcanvas.convertEventToCanvasOffset( - event - ) - graphcanvas.graph.add(node, false) - } + const input = value_element + input.addEventListener('keydown', function (e: KeyboardEvent) { + dialog.is_modified = true + if (e.keyCode == 27) { + //ESC + dialog.close() + } else if (e.keyCode == 13 && (e.target as Element).localName != 'textarea') { + if (callback) { + callback(this.value) + } + dialog.close() + } else { + return + } + e.preventDefault() + e.stopPropagation() + }) - if (extra?.data) { - if (extra.data.properties) { - for (const i in extra.data.properties) { - node.addProperty(i, extra.data.properties[i]) - } - } - if (extra.data.inputs) { - node.inputs = [] - for (const i in extra.data.inputs) { - node.addOutput( - extra.data.inputs[i][0], - extra.data.inputs[i][1] - ) - } - } - if (extra.data.outputs) { - node.outputs = [] - for (const 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) - } + const button = dialog.querySelector('button') + button.addEventListener('click', function () { + callback?.(input.value) + that.setDirty(true) + dialog.close() + }) - } + const rect = canvas.getBoundingClientRect() + let offsetx = -20 + let offsety = -20 + if (rect) { + offsetx -= rect.left + offsety -= rect.top + } - // join node after inserting - if (options.node_from) { - // FIXME: any - let iS: any = false - switch (typeof options.slot_from) { - case "string": - iS = options.node_from.findOutputSlot(options.slot_from) - break - case "object": - iS = options.slot_from.name - ? options.node_from.findOutputSlot(options.slot_from.name) - : -1 - // @ts-expect-error change interface check - 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) { - // FIXME: any - let iS: any = false - switch (typeof options.slot_from) { - case "string": - iS = options.node_to.findInputSlot(options.slot_from) - break - case "object": - iS = options.slot_from.name - ? options.node_to.findInputSlot(options.slot_from.name) - : -1 - // @ts-expect-error change interface check - 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); - } - } + 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' + } - graphcanvas.graph.afterChange() - } - } + setTimeout(function () { + input.focus() + const clickTime = Date.now() + function handleOutsideClick(e: MouseEvent) { + if (e.target === canvas && Date.now() - clickTime > 256) { + dialog.close() + canvas.parentNode.removeEventListener('click', handleOutsideClick) + canvas.parentNode.removeEventListener('touchend', handleOutsideClick) + } + } + canvas.parentNode.addEventListener('click', handleOutsideClick) + canvas.parentNode.addEventListener('touchend', handleOutsideClick) + }, 10) + return dialog + } + showSearchBox(event: CanvasMouseEvent, options?: IShowSearchOptions): HTMLDivElement { + // proposed defaults + const def_options: IShowSearchOptions = { + 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 + // @ts-expect-error + 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); + const that = this + const graphcanvas = LGraphCanvas.active_canvas + const canvas = graphcanvas.canvas + const root_document = canvas.ownerDocument || document + + const 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 + let selIn + let selOut + if (options.do_type_filter) { + selIn = dialog.querySelector('.slot_in_type_filter') + selOut = dialog.querySelector('.slot_out_type_filter') + } + + // @ts-expect-error Panel? + 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 + 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) { + // FIXME: Remove "any" kludge + let prevent_timeout: any = false + let timeout_close = null + LiteGraph.pointerListenerAdd(dialog, 'enter', function () { + if (timeout_close) { + clearTimeout(timeout_close) + timeout_close = null + } + }) + LiteGraph.pointerListenerAdd(dialog, 'leave', function () { + if (prevent_timeout) return + timeout_close = setTimeout( + function () { // @ts-expect-error Panel? dialog.close() - } - - function changeSelection(forward) { - const prev = selected - if (!selected) { - selected = forward - ? helper.childNodes[0] - : helper.childNodes[helper.childNodes.length] - } else { - selected.classList.remove("selected") - selected = forward - ? selected.nextSibling - : selected.previousSibling - selected ||= prev - } - if (!selected) return - - selected.classList.add("selected") - selected.scrollIntoView({ block: "end", behavior: "smooth" }) - } - - function refreshHelper() { - timeout = null - let str = input.value - first = null - helper.innerHTML = "" - if (!str && !options.show_all_if_empty) return - - if (that.onSearchBox) { - const list = that.onSearchBox(helper, str, graphcanvas) - if (list) { - for (let i = 0; i < list.length; ++i) { - addResult(list[i]) - } - } - } else { - let c = 0 - str = str.toLowerCase() - const filter = graphcanvas.filter || graphcanvas.graph.filter - - // FIXME: any - // filter by type preprocess - let sIn: any = false - let sOut: any = false - if (options.do_type_filter && that.search_box) { - sIn = that.search_box.querySelector(".slot_in_type_filter") - sOut = that.search_box.querySelector(".slot_out_type_filter") - } - - //extras - for (const i in LiteGraph.searchbox_extras) { - const extra = LiteGraph.searchbox_extras[i] - if ((!options.show_all_if_empty || str) && extra.desc.toLowerCase().indexOf(str) === -1) - continue - const 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 - } - } - - let filtered = null - if (Array.prototype.filter) { //filter supported - const keys = Object.keys(LiteGraph.registered_node_types) //types - filtered = keys.filter(inner_test_filter) - } else { - filtered = [] - for (const i in LiteGraph.registered_node_types) { - if (inner_test_filter(i)) - filtered.push(i) - } - } - - for (let 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)) { - // FIXME: Undeclared variable again - // @ts-expect-error - filtered_extra = [] - for (const i in LiteGraph.registered_node_types) { - if (inner_test_filter(i, { inTypeOverride: sIn && sIn.value ? "*" : false, outTypeOverride: sOut && sOut.value ? "*" : false })) - // @ts-expect-error - filtered_extra.push(i) - } - // @ts-expect-error - for (let i = 0; i < filtered_extra.length; i++) { - // @ts-expect-error - 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))) { - // @ts-expect-error - filtered_extra = [] - for (const i in LiteGraph.registered_node_types) { - if (inner_test_filter(i, { skipFilter: true })) - // @ts-expect-error - filtered_extra.push(i) - } - // @ts-expect-error - for (let i = 0; i < filtered_extra.length; i++) { - // @ts-expect-error - addResult(filtered_extra[i], "not_in_filter") - if (LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit) - break - } - } - - function inner_test_filter(type: string, optsIn?: number | { inTypeOverride?: string | boolean; outTypeOverride?: string | boolean; skipFilter?: boolean }): boolean { - optsIn = optsIn || {} - const optsDef = { - skipFilter: false, - inTypeOverride: false, - outTypeOverride: false - } - const opts = Object.assign(optsDef, optsIn) - const 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) { - const sType = type - - let sV = opts.inTypeOverride !== false - ? opts.inTypeOverride - : sIn.value - // type is stored - if (sIn && sV && LiteGraph.registered_slot_in_types[sV]?.nodes) { - const doesInc = LiteGraph.registered_slot_in_types[sV].nodes.includes(sType) - if (doesInc === false) return false - } - - sV = sOut.value - if (opts.outTypeOverride !== false) sV = opts.outTypeOverride - // type is stored - if (sOut && sV && LiteGraph.registered_slot_out_types[sV]?.nodes) { - const doesInc = LiteGraph.registered_slot_out_types[sV].nodes.includes(sType) - if (doesInc === false) return false - } - } - return true - } - } - - function addResult(type: string, className?: string): void { - const help = document.createElement("div") - 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 () { - select(unescape(this.dataset["type"])) - }) - helper.appendChild(help) - } - } - - return dialog - } - showEditPropertyValue(node: LGraphNode, property: string, options: IDialogOptions): IDialog { - if (!node || node.properties[property] === undefined) return - - options = options || {} - - const info = node.getPropertyInfo(property) - const type = info.type - - let 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 - } - - const dialog = this.createDialog( - "" + - (info.label || property) + - "" + - input_html + - "", - options + }, + 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 () { + prevent_timeout++ + }) + selIn.addEventListener('blur', function () { + prevent_timeout = 0 + }) + selIn.addEventListener('change', function () { + prevent_timeout = -1 + }) + selOut.addEventListener('click', function () { + prevent_timeout++ + }) + selOut.addEventListener('blur', function () { + prevent_timeout = 0 + }) + selOut.addEventListener('change', function () { + prevent_timeout = -1 + }) + } + } - let input: HTMLInputElement | HTMLSelectElement - if ((type == "enum" || type == "combo") && info.values) { - input = dialog.querySelector("select") - input.addEventListener("change", function (e) { - dialog.modified() - setValue((e.target as HTMLSelectElement)?.value) - }) - } else if (type == "boolean" || type == "toggle") { - input = dialog.querySelector("input") - input?.addEventListener("click", function () { - dialog.modified() - // @ts-expect-error - setValue(!!input.checked) - }) - } else { - input = dialog.querySelector("input") - if (input) { - input.addEventListener("blur", function () { - this.focus() - }) + // @ts-expect-error Panel? + that.search_box?.close() + that.search_box = dialog - let v = node.properties[property] !== undefined ? node.properties[property] : "" - if (type !== 'string') { - v = JSON.stringify(v) - } + const helper = dialog.querySelector('.helper') - // @ts-expect-error - 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() - }) - } - } - input?.focus() + let first = null + let timeout = null + let selected = null - const button = dialog.querySelector("button") - button.addEventListener("click", inner) - - function inner() { - setValue(input.value) - } - - function setValue(value: string | number) { - - if (info?.values && typeof info.values === "object" && info.values[value] != undefined) - value = info.values[value] - - if (typeof node.properties[property] == "number") { - value = Number(value) - } - if (type == "array" || type == "object") { - // @ts-expect-error JSON.parse doesn't care. - value = JSON.parse(value) - } - node.properties[property] = value - if (node.graph) { - node.graph._version++ - } - node.onPropertyChanged?.(property, value) - options.onclose?.() + const input = dialog.querySelector('input') + if (input) { + input.addEventListener('blur', function () { + 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 + // @ts-expect-error Panel? + dialog.close() + } else if (e.keyCode == 13) { + if (selected) { + select(unescape(selected.dataset['type'])) + } else if (first) { + select(first) + } else { + // @ts-expect-error Panel? dialog.close() - this.setDirty(true, true) - } - - return dialog - } - // TODO refactor, theer are different dialog, some uses createDialog, some dont - createDialog(html: string, options: IDialogOptions): IDialog { - const def_options = { checkForInput: false, closeOnLeave: true, closeOnLeave_checkModified: true } - options = Object.assign(def_options, options || {}) - const dialog: IDialog = document.createElement("div") - dialog.className = "graphdialog" - dialog.innerHTML = html - dialog.is_modified = false - - const rect = this.canvas.getBoundingClientRect() - let offsetx = -20 - let 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) { - const aI = dialog.querySelectorAll("input") - const focused = false - 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 () { - dialog.parentNode?.removeChild(dialog) - } - - let dialogCloseTimer = null - let prevent_timeout = 0 - dialog.addEventListener("mouseleave", function () { - if (prevent_timeout) - return - 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 () { - if (options.closeOnLeave || LiteGraph.dialog_close_on_mouse_leave) - if (dialogCloseTimer) clearTimeout(dialogCloseTimer) - }) - const selInDia = dialog.querySelectorAll("select") - // if filtering, check focus changed to comboboxes and prevent closing - selInDia?.forEach(function (selIn) { - selIn.addEventListener("click", function () { prevent_timeout++ }) - selIn.addEventListener("blur", function () { prevent_timeout = 0 }) - selIn.addEventListener("change", function () { prevent_timeout = -1 }) - }) - - return dialog - } - createPanel(title, options) { - options = options || {} - - const ref_window = options.window || window - // TODO: any kludge - const root: any = document.createElement("div") - root.className = "litegraph dialog" - root.innerHTML = "
" - root.header = root.querySelector(".dialog-header") - - if (options.width) - root.style.width = options.width + (typeof options.width === "number" ? "px" : "") - if (options.height) - root.style.height = options.height + (typeof options.height === "number" ? "px" : "") - if (options.closable) { - const 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 (typeof root.onClose == "function") root.onClose() - root.parentNode?.removeChild(root) - /* XXX CHECK THIS */ - 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: unknown) { - let vTo: string - let vAlt: string - if (typeof force != "undefined") { - vTo = force ? "block" : "none" - vAlt = force ? "none" : "block" - } else { - vTo = root.alt_content.style.display != "block" ? "block" : "none" - vAlt = root.alt_content.style.display != "block" ? "none" : "block" - } - root.alt_content.style.display = vTo - root.content.style.display = vAlt - } - - root.toggleFooterVisibility = function (force: unknown) { - let vTo: string - if (typeof force != "undefined") { - vTo = force ? "block" : "none" - } else { - 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) { - const 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) { - // TODO: any kludge - const elem: any = 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 () { - const elem = document.createElement("div") - elem.className = "separator" - root.content.appendChild(elem) - } - - root.addWidget = function (type, name, value, options, callback) { - options = options || {} - let str_value = String(value) - type = type.toLowerCase() - if (type == "number") - str_value = value.toFixed(3) - - // FIXME: any kludge - const elem: any = document.createElement("div") - elem.className = "property" - elem.innerHTML = "" - elem.querySelector(".property_name").innerText = options.label || name - // TODO: any kludge - const value_element: any = 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 () { 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 () { - const 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) { - // allow for multiline - if (e.code == "Enter" && (type != "string" || !e.shiftKey)) { - e.preventDefault() - this.blur() - } - }) - value_element.addEventListener("blur", function () { - let v = this.innerText - const propname = this.parentNode.dataset["property"] - const proptype = this.parentNode.dataset["type"] - if (proptype == "number") - v = Number(v) - innerChange(propname, v) - }) - } - else if (type == "enum" || type == "combo") { - const str_value = LGraphCanvas.getPropertyPrintableValue(value, options.values) - value_element.innerText = str_value - - value_element.addEventListener("click", function (event) { - const values = options.values || [] - const propname = this.parentNode.dataset["property"] - const elem_that = this - new LiteGraph.ContextMenu(values, { - event: event, - className: "dark", - callback: inner_clicked - }, - // @ts-expect-error - ref_window) - function inner_clicked(v) { - //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) { - options.callback?.(name, value, options) - callback?.(name, value, options) - } - - return elem - } - - if (root.onOpen && typeof root.onOpen == "function") root.onOpen() - - return root - } - closePanels(): void { - document.querySelector("#node-panel")?.close() - document.querySelector("#option-panel")?.close() - } - showShowNodePanel(node: LGraphNode): void { - this.SELECTED_NODE = node - this.closePanels() - const ref_window = this.getCanvasWindow() - const graphcanvas = this - const 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() { - //clear - panel.content.innerHTML = "" - // @ts-expect-error ctor props - panel.addHTML(`${node.type}${node.constructor.desc || ""}`) - - panel.addHTML("

Properties

") - - const fUpdate = function (name, value) { - graphcanvas.graph.beforeChange(node) - switch (name) { - case "Title": - node.title = value - break - case "Mode": { - const 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) - - const nodeCol = node.color !== undefined - ? 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 (const pName in node.properties) { - const value = node.properties[pName] - const info = node.getPropertyInfo(pName) - - //in case the user wants control over the side panel widget - if (node.onAddPropertyToPanel?.(pName, panel)) - continue - - panel.addWidget(info.widget || info.type, pName, value, info, fUpdate) - } - - panel.addSeparator() - - 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") - - panel.alt_content.innerHTML = "" - const textarea = panel.alt_content.querySelector("textarea") - const fDoneWith = function () { - panel.toggleAltContent(false) - 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)" - - const assign = panel.addButton("Assign", function () { - node.setProperty(propname, textarea.value) - fDoneWith() - }) - panel.alt_content.appendChild(assign) - const button = panel.addButton("Close", fDoneWith) - button.style.float = "right" - panel.alt_content.appendChild(button) - } - - inner_refresh() - - this.canvas.parentNode.appendChild(panel) - } - showSubgraphPropertiesDialog(node: LGraphNode) { - console.log("showing subgraph properties dialog") - - const old_panel = this.canvas.parentNode.querySelector(".subgraph_dialog") - old_panel?.close() - - const 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 (let i = 0; i < node.inputs.length; ++i) { - const input = node.inputs[i] - if (input.not_subgraph_input) - continue - const html = " " - const 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 () { - node.removeInput(Number(this.parentNode.dataset["slot"])) - inner_refresh() - }) - } - } - - //add extra - const html = " + NameType" - const elem = panel.addHTML(html, "subgraph_property extra", true) - elem.querySelector("button").addEventListener("click", function () { - const elem = this.parentNode - const name = elem.querySelector(".name").value - const 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 - } - showSubgraphPropertiesDialogRight(node: LGraphNode): any { - - // console.log("showing subgraph properties dialog"); - // old_panel if old_panel is exist close it - const old_panel = this.canvas.parentNode.querySelector(".subgraph_dialog") - old_panel?.close() - // new panel - const 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 (let i = 0; i < node.outputs.length; ++i) { - // FIXME: Rename - it's an output - const input = node.outputs[i] - if (input.not_subgraph_output) - continue - const html = " " - const 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 () { - node.removeOutput(Number(this.parentNode.dataset["slot"])) - inner_refresh() - }) - } - } - - //add extra - const html = " + NameType" - const 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 () { - addOutput.apply(this) - }) - function addOutput() { - const elem = this.parentNode - const name = elem.querySelector(".name").value - const 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 - } - checkPanels(): void { - if (!this.canvas) return - - const panels = this.canvas.parentNode.querySelectorAll(".litegraph.dialog") - for (let i = 0; i < panels.length; ++i) { - const panel = panels[i] - // @ts-expect-error Panel - if (!panel.node) continue - // @ts-expect-error Panel - if (!panel.node.graph || panel.graph != this.graph) panel.close() - } - } - getCanvasMenuOptions(): IContextMenuValue[] { - let options: IContextMenuValue[] = null - if (this.getMenuOptions) { - options = this.getMenuOptions() + } } else { - options = [ - { - content: "Add Node", - has_submenu: true, - // @ts-expect-error Might be broken? Or just param overlap - callback: LGraphCanvas.onMenuAdd - }, - { content: "Add Group", callback: LGraphCanvas.onGroupAdd }, - //{ content: "Arrange", callback: that.graph.arrange }, - //{content:"Collapse All", callback: LGraphCanvas.onMenuCollapseAll } - ] - 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 (timeout) { + clearInterval(timeout) + } + timeout = setTimeout(refreshHelper, 10) + return } - - const extra = this.getExtraMenuOptions?.(this, options) - return extra - ? options.concat(extra) - : options + e.preventDefault() + e.stopPropagation() + e.stopImmediatePropagation() + return true + }) } - //called by processContextMenu to extract the menu list - getNodeMenuOptions(node: LGraphNode): IContextMenuValue[] { - let options: IContextMenuValue[] = null - if (node.getMenuOptions) { - options = node.getMenuOptions(this) + // if should filter on type, load and fill selected and choose elements if passed + if (options.do_type_filter) { + if (selIn) { + const aSlots = LiteGraph.slot_types_in + const 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 (let iK = 0; iK < nSlots; iK++) { + const opt = document.createElement('option') + opt.value = aSlots[iK] + opt.innerHTML = aSlots[iK] + selIn.appendChild(opt) + // @ts-expect-error + if (options.type_filter_in !== false && (options.type_filter_in + '').toLowerCase() == (aSlots[iK] + '').toLowerCase()) { + //selIn.selectedIndex .. + opt.selected = true + //console.log("comparing IN "+options.type_filter_in+" :: "+aSlots[iK]); + } else { + //console.log("comparing OUT "+options.type_filter_in+" :: "+aSlots[iK]); + } + } + selIn.addEventListener('change', function () { + refreshHelper() + }) + } + if (selOut) { + const aSlots = LiteGraph.slot_types_out + const nSlots = aSlots.length // this for object :: Object.keys(aSlots).length; + + if (options.type_filter_out == LiteGraph.EVENT || options.type_filter_out == LiteGraph.ACTION) options.type_filter_out = '_event_' + /* this will filter on * .. but better do it manually in case + else if(options.type_filter_out === "" || options.type_filter_out === 0) + options.type_filter_out = "*";*/ + for (let iK = 0; iK < nSlots; iK++) { + const opt = document.createElement('option') + opt.value = aSlots[iK] + opt.innerHTML = aSlots[iK] + selOut.appendChild(opt) + if (options.type_filter_out !== false && (options.type_filter_out + '').toLowerCase() == (aSlots[iK] + '').toLowerCase()) + opt.selected = true + } + selOut.addEventListener('change', function () { + refreshHelper() + }) + } + } + + //compute best position + const rect = canvas.getBoundingClientRect() + + const left = (event ? event.clientX : rect.left + rect.width * 0.5) - 80 + const top = (event ? event.clientY : rect.top + rect.height * 0.5) - 20 + dialog.style.left = left + 'px' + dialog.style.top = top + 'px' + + //To avoid out of screen problems + if (event.layerY > rect.height - 200) + // @ts-expect-error + helper.style.maxHeight = rect.height - event.layerY - 20 + 'px' + + requestAnimationFrame(function () { + input.focus() + }) + if (options.show_all_on_open) refreshHelper() + + function select(name) { + if (name) { + if (that.onSearchBoxSelection) { + that.onSearchBoxSelection(name, event, graphcanvas) } 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 - }) + const extra = LiteGraph.searchbox_extras[name.toLowerCase()] + if (extra) name = extra.type + + graphcanvas.graph.beforeChange() + const node = LiteGraph.createNode(name) + if (node) { + node.pos = graphcanvas.convertEventToCanvasOffset(event) + graphcanvas.graph.add(node, false) + } + + if (extra?.data) { + if (extra.data.properties) { + for (const i in extra.data.properties) { + node.addProperty(i, extra.data.properties[i]) + } } - if (node.collapsible) { - options.push({ - content: node.collapsed ? "Expand" : "Collapse", - callback: LGraphCanvas.onMenuNodeCollapse - }) + if (extra.data.inputs) { + node.inputs = [] + for (const i in extra.data.inputs) { + node.addOutput(extra.data.inputs[i][0], extra.data.inputs[i][1]) + } } - if (node.widgets?.some(w => w.advanced)) { - options.push({ - content: node.showAdvanced ? "Hide Advanced" : "Show Advanced", - callback: LGraphCanvas.onMenuToggleAdvanced - }) + if (extra.data.outputs) { + node.outputs = [] + for (const i in extra.data.outputs) { + node.addOutput(extra.data.outputs[i][0], extra.data.outputs[i][1]) + } } - options.push( - { - content: node.pinned ? "Unpin" : "Pin", - callback: (...args) => { - // @ts-expect-error Not impl. - LGraphCanvas.onMenuNodePin(...args) - for (const i in this.selected_nodes) { - const node = this.selected_nodes[i] - node.pin() - } - this.setDirty(true, true) - } - }, - { - content: "Colors", - has_submenu: true, - callback: LGraphCanvas.onMenuNodeColors - }, - { - content: "Shapes", - has_submenu: true, - callback: LGraphCanvas.onMenuNodeShapes - }, - null - ) + 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) { + // FIXME: any + let iS: any = false + switch (typeof options.slot_from) { + case 'string': + iS = options.node_from.findOutputSlot(options.slot_from) + break + case 'object': + iS = options.slot_from.name ? options.node_from.findOutputSlot(options.slot_from.name) : -1 + // @ts-expect-error change interface check + 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) { + // FIXME: any + let iS: any = false + switch (typeof options.slot_from) { + case 'string': + iS = options.node_to.findInputSlot(options.slot_from) + break + case 'object': + iS = options.slot_from.name ? options.node_to.findInputSlot(options.slot_from.name) : -1 + // @ts-expect-error change interface check + 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() + } + } + + // @ts-expect-error Panel? + dialog.close() + } + + function changeSelection(forward) { + const prev = selected + if (!selected) { + selected = forward ? helper.childNodes[0] : helper.childNodes[helper.childNodes.length] + } else { + selected.classList.remove('selected') + selected = forward ? selected.nextSibling : selected.previousSibling + selected ||= prev + } + if (!selected) return + + selected.classList.add('selected') + selected.scrollIntoView({ block: 'end', behavior: 'smooth' }) + } + + function refreshHelper() { + timeout = null + let str = input.value + first = null + helper.innerHTML = '' + if (!str && !options.show_all_if_empty) return + + if (that.onSearchBox) { + const list = that.onSearchBox(helper, str, graphcanvas) + if (list) { + for (let i = 0; i < list.length; ++i) { + addResult(list[i]) + } + } + } else { + let c = 0 + str = str.toLowerCase() + const filter = graphcanvas.filter || graphcanvas.graph.filter + + // FIXME: any + // filter by type preprocess + let sIn: any = false + let sOut: any = false + if (options.do_type_filter && that.search_box) { + sIn = that.search_box.querySelector('.slot_in_type_filter') + sOut = that.search_box.querySelector('.slot_out_type_filter') } - const inputs = node.onGetInputs?.() - if (inputs?.length) - options[0].disabled = false - - const outputs = node.onGetOutputs?.() - if (outputs?.length) - options[1].disabled = false - - const extra = node.getExtraMenuOptions?.(this, options) - if (extra) { - extra.push(null) - options = extra.concat(options) + //extras + for (const i in LiteGraph.searchbox_extras) { + const extra = LiteGraph.searchbox_extras[i] + if ((!options.show_all_if_empty || str) && extra.desc.toLowerCase().indexOf(str) === -1) continue + const 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 + } } - if (node.clonable !== false) { - options.push({ - content: "Clone", - callback: LGraphCanvas.onMenuNodeClone - }) + let filtered = null + if (Array.prototype.filter) { + //filter supported + const keys = Object.keys(LiteGraph.registered_node_types) //types + filtered = keys.filter(inner_test_filter) + } else { + filtered = [] + for (const i in LiteGraph.registered_node_types) { + if (inner_test_filter(i)) filtered.push(i) + } } - // TODO: Subgraph code never implemented. - // 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({ - content: "Distribute Nodes", - has_submenu: true, - callback: LGraphCanvas.createDistributeMenu, - }) + for (let i = 0; i < filtered.length; i++) { + addResult(filtered[i]) + if (LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit) break } - options.push(null, { - content: "Remove", - disabled: !(node.removable !== false && !node.block_delete), - callback: LGraphCanvas.onMenuNodeRemove + // add general type if filtering + if (options.show_general_after_typefiltered && (sIn.value || sOut.value)) { + // FIXME: Undeclared variable again + // @ts-expect-error + filtered_extra = [] + for (const i in LiteGraph.registered_node_types) { + if (inner_test_filter(i, { inTypeOverride: sIn && sIn.value ? '*' : false, outTypeOverride: sOut && sOut.value ? '*' : false })) + // @ts-expect-error + filtered_extra.push(i) + } + // @ts-expect-error + for (let i = 0; i < filtered_extra.length; i++) { + // @ts-expect-error + 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) { + // @ts-expect-error + filtered_extra = [] + for (const i in LiteGraph.registered_node_types) { + if (inner_test_filter(i, { skipFilter: true })) + // @ts-expect-error + filtered_extra.push(i) + } + // @ts-expect-error + for (let i = 0; i < filtered_extra.length; i++) { + // @ts-expect-error + addResult(filtered_extra[i], 'not_in_filter') + if (LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit) break + } + } + + function inner_test_filter( + type: string, + optsIn?: number | { inTypeOverride?: string | boolean; outTypeOverride?: string | boolean; skipFilter?: boolean }, + ): boolean { + optsIn = optsIn || {} + const optsDef = { + skipFilter: false, + inTypeOverride: false, + outTypeOverride: false, + } + const opts = Object.assign(optsDef, optsIn) + const 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) { + const sType = type + + let sV = opts.inTypeOverride !== false ? opts.inTypeOverride : sIn.value + // type is stored + if (sIn && sV && LiteGraph.registered_slot_in_types[sV]?.nodes) { + const doesInc = LiteGraph.registered_slot_in_types[sV].nodes.includes(sType) + if (doesInc === false) return false + } + + sV = sOut.value + if (opts.outTypeOverride !== false) sV = opts.outTypeOverride + // type is stored + if (sOut && sV && LiteGraph.registered_slot_out_types[sV]?.nodes) { + const doesInc = LiteGraph.registered_slot_out_types[sV].nodes.includes(sType) + if (doesInc === false) return false + } + } + return true + } + } + + function addResult(type: string, className?: string): void { + const help = document.createElement('div') + 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 () { + select(unescape(this.dataset['type'])) + }) + helper.appendChild(help) + } + } + + return dialog + } + showEditPropertyValue(node: LGraphNode, property: string, options: IDialogOptions): IDialog { + if (!node || node.properties[property] === undefined) return + + options = options || {} + + const info = node.getPropertyInfo(property) + const type = info.type + + let 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 + } + + const dialog = this.createDialog("" + (info.label || property) + '' + input_html + '', options) + + let input: HTMLInputElement | HTMLSelectElement + if ((type == 'enum' || type == 'combo') && info.values) { + input = dialog.querySelector('select') + input.addEventListener('change', function (e) { + dialog.modified() + setValue((e.target as HTMLSelectElement)?.value) + }) + } else if (type == 'boolean' || type == 'toggle') { + input = dialog.querySelector('input') + input?.addEventListener('click', function () { + dialog.modified() + // @ts-expect-error + setValue(!!input.checked) + }) + } else { + input = dialog.querySelector('input') + if (input) { + input.addEventListener('blur', function () { + this.focus() }) - node.graph?.onGetNodeMenuOptions?.(options, node) - - return options - } - getGroupMenuOptions(group: LGraphGroup): IContextMenuValue[] { - console.warn("LGraphCanvas.getGroupMenuOptions is deprecated, use LGraphGroup.getMenuOptions instead") - return group.getMenuOptions() - } - processContextMenu(node: LGraphNode, event: CanvasMouseEvent): void { - const that = this - const canvas = LGraphCanvas.active_canvas - const ref_window = canvas.getCanvasWindow() - - // TODO: Remove type kludge - let menu_info: (IContextMenuValue | string)[] = null - const options: IContextMenuOptions = { - event: event, - callback: inner_option_clicked, - extra: node + let v = node.properties[property] !== undefined ? node.properties[property] : '' + if (type !== 'string') { + v = JSON.stringify(v) } - if (node) options.title = node.type - - //check if mouse is in input - let slot: ReturnType = 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?.output?.links?.length) - menu_info.push({ content: "Disconnect Links", slot: slot }) - - const _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 }) - - } - // @ts-expect-error Slot type can be number and has number checks - 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() - const 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: group.getMenuOptions() - } - }) - } - } - - //show menu - if (!menu_info) + // @ts-expect-error + 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() + }) + } + } + input?.focus() - // @ts-expect-error Remove param ref_window - unused - new LiteGraph.ContextMenu(menu_info, options, ref_window) + const button = dialog.querySelector('button') + button.addEventListener('click', inner) - function inner_option_clicked(v, options) { - if (!v) return + function inner() { + setValue(input.value) + } - if (v.content == "Remove Slot") { - const 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") { - const 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") { - const info = v.slot - const slot_info = info.input - ? node.getInputInfo(info.slot) - : node.getOutputInfo(info.slot) - const dialog = that.createDialog( - "Name", - options - ) - const input = dialog.querySelector("input") - if (input && slot_info) { - input.value = slot_info.label || "" - } - const 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 as Element).localName != "textarea") { - return - } - e.preventDefault() - e.stopPropagation() - }) - input.focus() + function setValue(value: string | number) { + if (info?.values && typeof info.values === 'object' && info.values[value] != undefined) value = info.values[value] + + if (typeof node.properties[property] == 'number') { + value = Number(value) + } + if (type == 'array' || type == 'object') { + // @ts-expect-error JSON.parse doesn't care. + value = JSON.parse(value) + } + node.properties[property] = value + if (node.graph) { + node.graph._version++ + } + node.onPropertyChanged?.(property, value) + options.onclose?.() + dialog.close() + this.setDirty(true, true) + } + + return dialog + } + // TODO refactor, theer are different dialog, some uses createDialog, some dont + createDialog(html: string, options: IDialogOptions): IDialog { + const def_options = { checkForInput: false, closeOnLeave: true, closeOnLeave_checkModified: true } + options = Object.assign(def_options, options || {}) + const dialog: IDialog = document.createElement('div') + dialog.className = 'graphdialog' + dialog.innerHTML = html + dialog.is_modified = false + + const rect = this.canvas.getBoundingClientRect() + let offsetx = -20 + let 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) { + const aI = dialog.querySelectorAll('input') + const focused = false + 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 () { + dialog.parentNode?.removeChild(dialog) + } + + let dialogCloseTimer = null + let prevent_timeout = 0 + dialog.addEventListener('mouseleave', function () { + if (prevent_timeout) return + 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 () { + if (options.closeOnLeave || LiteGraph.dialog_close_on_mouse_leave) if (dialogCloseTimer) clearTimeout(dialogCloseTimer) + }) + const selInDia = dialog.querySelectorAll('select') + // if filtering, check focus changed to comboboxes and prevent closing + selInDia?.forEach(function (selIn) { + selIn.addEventListener('click', function () { + prevent_timeout++ + }) + selIn.addEventListener('blur', function () { + prevent_timeout = 0 + }) + selIn.addEventListener('change', function () { + prevent_timeout = -1 + }) + }) + + return dialog + } + createPanel(title, options) { + options = options || {} + + const ref_window = options.window || window + // TODO: any kludge + const root: any = document.createElement('div') + root.className = 'litegraph dialog' + root.innerHTML = + "
" + root.header = root.querySelector('.dialog-header') + + if (options.width) root.style.width = options.width + (typeof options.width === 'number' ? 'px' : '') + if (options.height) root.style.height = options.height + (typeof options.height === 'number' ? 'px' : '') + if (options.closable) { + const 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 (typeof root.onClose == 'function') root.onClose() + root.parentNode?.removeChild(root) + /* XXX CHECK THIS */ + 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: unknown) { + let vTo: string + let vAlt: string + if (typeof force != 'undefined') { + vTo = force ? 'block' : 'none' + vAlt = force ? 'none' : 'block' + } else { + vTo = root.alt_content.style.display != 'block' ? 'block' : 'none' + vAlt = root.alt_content.style.display != 'block' ? 'none' : 'block' + } + root.alt_content.style.display = vTo + root.content.style.display = vAlt + } + + root.toggleFooterVisibility = function (force: unknown) { + let vTo: string + if (typeof force != 'undefined') { + vTo = force ? 'block' : 'none' + } else { + 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) { + const 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) { + // TODO: any kludge + const elem: any = 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 () { + const elem = document.createElement('div') + elem.className = 'separator' + root.content.appendChild(elem) + } + + root.addWidget = function (type, name, value, options, callback) { + options = options || {} + let str_value = String(value) + type = type.toLowerCase() + if (type == 'number') str_value = value.toFixed(3) + + // FIXME: any kludge + const elem: any = document.createElement('div') + elem.className = 'property' + elem.innerHTML = "" + elem.querySelector('.property_name').innerText = options.label || name + // TODO: any kludge + const value_element: any = 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 () { + 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 () { + const 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) { + // allow for multiline + if (e.code == 'Enter' && (type != 'string' || !e.shiftKey)) { + e.preventDefault() + this.blur() + } + }) + value_element.addEventListener('blur', function () { + let v = this.innerText + const propname = this.parentNode.dataset['property'] + const proptype = this.parentNode.dataset['type'] + if (proptype == 'number') v = Number(v) + innerChange(propname, v) + }) + } else if (type == 'enum' || type == 'combo') { + const str_value = LGraphCanvas.getPropertyPrintableValue(value, options.values) + value_element.innerText = str_value + + value_element.addEventListener('click', function (event) { + const values = options.values || [] + const propname = this.parentNode.dataset['property'] + const elem_that = this + new LiteGraph.ContextMenu( + values, + { + event: event, + className: 'dark', + callback: inner_clicked, + }, + // @ts-expect-error + ref_window, + ) + function inner_clicked(v) { + //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) { + options.callback?.(name, value, options) + callback?.(name, value, options) + } + + return elem + } + + if (root.onOpen && typeof root.onOpen == 'function') root.onOpen() + + return root + } + closePanels(): void { + document.querySelector('#node-panel')?.close() + document.querySelector('#option-panel')?.close() + } + showShowNodePanel(node: LGraphNode): void { + this.SELECTED_NODE = node + this.closePanels() + const ref_window = this.getCanvasWindow() + const graphcanvas = this + const 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() { + //clear + panel.content.innerHTML = '' + + panel.addHTML( + `${node.type}${node.constructor.desc || ''}`, + ) + + panel.addHTML('

Properties

') + + const fUpdate = function (name, value) { + graphcanvas.graph.beforeChange(node) + switch (name) { + case 'Title': + node.title = value + break + case 'Mode': { + const 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 + } - //if(v.callback) - // return v.callback.call(that, node, options, e, menu, that, event ); + panel.addWidget('string', 'Title', node.title, {}, fUpdate) + + panel.addWidget('combo', 'Mode', LiteGraph.NODE_MODES[node.mode], { values: LiteGraph.NODE_MODES }, fUpdate) + + const nodeCol = + node.color !== undefined + ? 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 (const pName in node.properties) { + const value = node.properties[pName] + const info = node.getPropertyInfo(pName) + + //in case the user wants control over the side panel widget + if (node.onAddPropertyToPanel?.(pName, panel)) continue + + panel.addWidget(info.widget || info.type, pName, value, info, fUpdate) + } + + panel.addSeparator() + + 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') + + panel.alt_content.innerHTML = "" + const textarea = panel.alt_content.querySelector('textarea') + const fDoneWith = function () { + panel.toggleAltContent(false) + 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)' + + const assign = panel.addButton('Assign', function () { + node.setProperty(propname, textarea.value) + fDoneWith() + }) + panel.alt_content.appendChild(assign) + const button = panel.addButton('Close', fDoneWith) + button.style.float = 'right' + panel.alt_content.appendChild(button) + } + + inner_refresh() + + this.canvas.parentNode.appendChild(panel) + } + showSubgraphPropertiesDialog(node: LGraphNode) { + console.log('showing subgraph properties dialog') + + const old_panel = this.canvas.parentNode.querySelector('.subgraph_dialog') + old_panel?.close() + + const 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 (let i = 0; i < node.inputs.length; ++i) { + const input = node.inputs[i] + if (input.not_subgraph_input) continue + const html = " " + const 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 () { + node.removeInput(Number(this.parentNode.dataset['slot'])) + inner_refresh() + }) } } + + //add extra + const html = + " + NameType" + const elem = panel.addHTML(html, 'subgraph_property extra', true) + elem.querySelector('button').addEventListener('click', function () { + const elem = this.parentNode + const name = elem.querySelector('.name').value + const 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 + } + showSubgraphPropertiesDialogRight(node: LGraphNode): any { + // console.log("showing subgraph properties dialog"); + // old_panel if old_panel is exist close it + const old_panel = this.canvas.parentNode.querySelector('.subgraph_dialog') + old_panel?.close() + // new panel + const 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 (let i = 0; i < node.outputs.length; ++i) { + // FIXME: Rename - it's an output + const input = node.outputs[i] + if (input.not_subgraph_output) continue + const html = " " + const 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 () { + node.removeOutput(Number(this.parentNode.dataset['slot'])) + inner_refresh() + }) + } + } + + //add extra + const html = + " + NameType" + const 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 () { + addOutput.apply(this) + }) + function addOutput() { + const elem = this.parentNode + const name = elem.querySelector('.name').value + const 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 + } + checkPanels(): void { + if (!this.canvas) return + + const panels = this.canvas.parentNode.querySelectorAll('.litegraph.dialog') + for (let i = 0; i < panels.length; ++i) { + const panel = panels[i] + // @ts-expect-error Panel + if (!panel.node) continue + // @ts-expect-error Panel + if (!panel.node.graph || panel.graph != this.graph) panel.close() + } + } + getCanvasMenuOptions(): IContextMenuValue[] { + let options: IContextMenuValue[] = null + if (this.getMenuOptions) { + options = this.getMenuOptions() + } else { + options = [ + { + content: 'Add Node', + has_submenu: true, + // @ts-expect-error Might be broken? Or just param overlap + callback: LGraphCanvas.onMenuAdd, + }, + { content: 'Add Group', callback: LGraphCanvas.onGroupAdd }, + //{ content: "Arrange", callback: that.graph.arrange }, + //{content:"Collapse All", callback: LGraphCanvas.onMenuCollapseAll } + ] + 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), + }) + } + } + + const extra = this.getExtraMenuOptions?.(this, options) + return extra ? options.concat(extra) : options + } + //called by processContextMenu to extract the menu list + getNodeMenuOptions(node: LGraphNode): IContextMenuValue[] { + let options: IContextMenuValue[] = 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, + }) + } + if (node.collapsible) { + options.push({ + content: node.collapsed ? 'Expand' : 'Collapse', + callback: LGraphCanvas.onMenuNodeCollapse, + }) + } + if (node.widgets?.some((w) => w.advanced)) { + options.push({ + content: node.showAdvanced ? 'Hide Advanced' : 'Show Advanced', + callback: LGraphCanvas.onMenuToggleAdvanced, + }) + } + options.push( + { + content: node.pinned ? 'Unpin' : 'Pin', + callback: (...args) => { + // @ts-expect-error Not impl. + LGraphCanvas.onMenuNodePin(...args) + for (const i in this.selected_nodes) { + const node = this.selected_nodes[i] + node.pin() + } + this.setDirty(true, true) + }, + }, + { + content: 'Colors', + has_submenu: true, + callback: LGraphCanvas.onMenuNodeColors, + }, + { + content: 'Shapes', + has_submenu: true, + callback: LGraphCanvas.onMenuNodeShapes, + }, + null, + ) + } + + const inputs = node.onGetInputs?.() + if (inputs?.length) options[0].disabled = false + + const outputs = node.onGetOutputs?.() + if (outputs?.length) options[1].disabled = false + + const 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, + }) + } + + // TODO: Subgraph code never implemented. + // 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({ + content: 'Distribute Nodes', + has_submenu: true, + callback: LGraphCanvas.createDistributeMenu, + }) + } + + options.push(null, { + content: 'Remove', + disabled: !(node.removable !== false && !node.block_delete), + callback: LGraphCanvas.onMenuNodeRemove, + }) + + node.graph?.onGetNodeMenuOptions?.(options, node) + + return options + } + getGroupMenuOptions(group: LGraphGroup): IContextMenuValue[] { + console.warn('LGraphCanvas.getGroupMenuOptions is deprecated, use LGraphGroup.getMenuOptions instead') + return group.getMenuOptions() + } + processContextMenu(node: LGraphNode, event: CanvasMouseEvent): void { + const that = this + const canvas = LGraphCanvas.active_canvas + const ref_window = canvas.getCanvasWindow() + + // TODO: Remove type kludge + let menu_info: (IContextMenuValue | string)[] = null + const options: IContextMenuOptions = { + event: event, + callback: inner_option_clicked, + extra: node, + } + + if (node) options.title = node.type + + //check if mouse is in input + let slot: ReturnType = 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?.output?.links?.length) menu_info.push({ content: 'Disconnect Links', slot: slot }) + + const _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 }) + } + // @ts-expect-error Slot type can be number and has number checks + 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() + const 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: group.getMenuOptions(), + }, + }) + } + } + + //show menu + if (!menu_info) return + + // @ts-expect-error Remove param ref_window - unused + new LiteGraph.ContextMenu(menu_info, options, ref_window) + + function inner_option_clicked(v, options) { + if (!v) return + + if (v.content == 'Remove Slot') { + const 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') { + const 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') { + const info = v.slot + const slot_info = info.input ? node.getInputInfo(info.slot) : node.getOutputInfo(info.slot) + const dialog = that.createDialog("Name", options) + const input = dialog.querySelector('input') + if (input && slot_info) { + input.value = slot_info.label || '' + } + const 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 as Element).localName != 'textarea') { + return + } + e.preventDefault() + e.stopPropagation() + }) + input.focus() + } + + //if(v.callback) + // return v.callback.call(that, node, options, e, menu, that, event ); + } + } } diff --git a/src/LGraphGroup.ts b/src/LGraphGroup.ts index eec7a0d47..5b661de16 100644 --- a/src/LGraphGroup.ts +++ b/src/LGraphGroup.ts @@ -1,253 +1,242 @@ -import type { IContextMenuValue, Point, Size } from "./interfaces" -import type { LGraph } from "./LGraph" -import type { ISerialisedGroup } from "./types/serialisation" -import { LiteGraph } from "./litegraph" -import { LGraphCanvas } from "./LGraphCanvas" -import { isInsideRectangle, overlapBounding } from "./measure" -import { LGraphNode } from "./LGraphNode" -import { RenderShape, TitleMode } from "./types/globalEnums" +import type { IContextMenuValue, Point, Size } from './interfaces' +import type { LGraph } from './LGraph' +import type { ISerialisedGroup } from './types/serialisation' +import { LiteGraph } from './litegraph' +import { LGraphCanvas } from './LGraphCanvas' +import { isInsideRectangle, overlapBounding } from './measure' +import { LGraphNode } from './LGraphNode' +import { RenderShape, TitleMode } from './types/globalEnums' export interface IGraphGroupFlags extends Record { - pinned?: true + pinned?: true } export class LGraphGroup { - color: string - title: string - font?: string - font_size: number = LiteGraph.DEFAULT_GROUP_FONT || 24 - _bounding: Float32Array = new Float32Array([10, 10, 140, 80]) - _pos: Point = this._bounding.subarray(0, 2) - _size: Size = this._bounding.subarray(2, 4) - _nodes: LGraphNode[] = [] - graph: LGraph | null = null - flags: IGraphGroupFlags = {} - selected?: boolean + color: string + title: string + font?: string + font_size: number = LiteGraph.DEFAULT_GROUP_FONT || 24 + _bounding: Float32Array = new Float32Array([10, 10, 140, 80]) + _pos: Point = this._bounding.subarray(0, 2) + _size: Size = this._bounding.subarray(2, 4) + _nodes: LGraphNode[] = [] + graph: LGraph | null = null + flags: IGraphGroupFlags = {} + selected?: boolean - constructor(title?: string) { - this.title = title || "Group" - this.color = LGraphCanvas.node_colors.pale_blue - ? LGraphCanvas.node_colors.pale_blue.groupcolor - : "#AAA" + constructor(title?: string) { + this.title = title || 'Group' + this.color = LGraphCanvas.node_colors.pale_blue ? LGraphCanvas.node_colors.pale_blue.groupcolor : '#AAA' + } + + /** Position of the group, as x,y co-ordinates in graph space */ + get pos() { + return this._pos + } + set pos(v) { + if (!v || v.length < 2) return + + this._pos[0] = v[0] + this._pos[1] = v[1] + } + + /** Size of the group, as width,height in graph units */ + get size() { + return this._size + } + set size(v) { + if (!v || v.length < 2) return + + this._size[0] = Math.max(140, v[0]) + this._size[1] = Math.max(80, v[1]) + } + + get nodes() { + return this._nodes + } + + get titleHeight() { + return this.font_size * 1.4 + } + + get pinned() { + return !!this.flags.pinned + } + + pin(): void { + this.flags.pinned = true + } + + unpin(): void { + delete this.flags.pinned + } + + configure(o: ISerialisedGroup): void { + this.title = o.title + this._bounding.set(o.bounding) + this.color = o.color + this.flags = o.flags || this.flags + if (o.font_size) this.font_size = o.font_size + } + + serialize(): ISerialisedGroup { + const 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, + flags: this.flags, } + } - /** Position of the group, as x,y co-ordinates in graph space */ - get pos() { - return this._pos + /** + * Draws the group on the canvas + * @param {LGraphCanvas} graphCanvas + * @param {CanvasRenderingContext2D} ctx + */ + draw(graphCanvas: LGraphCanvas, ctx: CanvasRenderingContext2D): void { + const padding = 4 + + ctx.fillStyle = this.color + ctx.strokeStyle = this.color + const [x, y] = this._pos + const [width, height] = this._size + ctx.globalAlpha = 0.25 * graphCanvas.editor_alpha + ctx.beginPath() + ctx.rect(x + 0.5, y + 0.5, width, height) + ctx.fill() + ctx.globalAlpha = graphCanvas.editor_alpha + ctx.stroke() + + ctx.beginPath() + ctx.moveTo(x + width, y + height) + ctx.lineTo(x + width - 10, y + height) + ctx.lineTo(x + width, y + height - 10) + ctx.fill() + + const font_size = this.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE + ctx.font = font_size + 'px Arial' + ctx.textAlign = 'left' + ctx.fillText(this.title + (this.pinned ? '📌' : ''), x + padding, y + font_size) + + if (LiteGraph.highlight_selected_group && this.selected) { + graphCanvas.drawSelectionBounding(ctx, this._bounding, { + shape: RenderShape.BOX, + title_height: this.titleHeight, + title_mode: TitleMode.NORMAL_TITLE, + fgcolor: this.color, + padding, + }) } - set pos(v) { - if (!v || v.length < 2) return + } - this._pos[0] = v[0] - this._pos[1] = v[1] + resize(width: number, height: number): void { + if (this.pinned) return + + this._size[0] = width + this._size[1] = height + } + + move(deltax: number, deltay: number, ignore_nodes = false): void { + if (this.pinned) return + + this._pos[0] += deltax + this._pos[1] += deltay + if (ignore_nodes) return + + for (let i = 0; i < this._nodes.length; ++i) { + const node = this._nodes[i] + node.pos[0] += deltax + node.pos[1] += deltay } + } - /** Size of the group, as width,height in graph units */ - get size() { - return this._size + recomputeInsideNodes(): void { + this._nodes.length = 0 + const nodes = this.graph._nodes + const node_bounding = new Float32Array(4) + + for (let i = 0; i < nodes.length; ++i) { + const node = nodes[i] + node.getBounding(node_bounding) + //out of the visible area + if (!overlapBounding(this._bounding, node_bounding)) continue + + this._nodes.push(node) } - set size(v) { - if (!v || v.length < 2) return + } - this._size[0] = Math.max(140, v[0]) - this._size[1] = Math.max(80, v[1]) - } + /** + * Add nodes to the group and adjust the group's position and size accordingly + * @param {LGraphNode[]} nodes - The nodes to add to the group + * @param {number} [padding=10] - The padding around the group + * @returns {void} + */ + addNodes(nodes: LGraphNode[], padding: number = 10): void { + if (!this._nodes && nodes.length === 0) return - get nodes() { - return this._nodes - } + const allNodes = [...(this._nodes || []), ...nodes] - get titleHeight() { - return this.font_size * 1.4 - } + const bounds = allNodes.reduce( + (acc, node) => { + const [x, y] = node.pos + const [width, height] = node.size + const isReroute = node.type === 'Reroute' + const isCollapsed = node.flags?.collapsed - get pinned() { - return !!this.flags.pinned - } + const top = y - (isReroute ? 0 : LiteGraph.NODE_TITLE_HEIGHT) + const bottom = isCollapsed ? top + LiteGraph.NODE_TITLE_HEIGHT : y + height + const right = isCollapsed && node._collapsed_width ? x + Math.round(node._collapsed_width) : x + width - pin(): void { - this.flags.pinned = true - } - - unpin(): void { - delete this.flags.pinned - } - - configure(o: ISerialisedGroup): void { - this.title = o.title - this._bounding.set(o.bounding) - this.color = o.color - this.flags = o.flags || this.flags - if (o.font_size) this.font_size = o.font_size - } - - serialize(): ISerialisedGroup { - const 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, - flags: this.flags, + left: Math.min(acc.left, x), + top: Math.min(acc.top, top), + right: Math.max(acc.right, right), + bottom: Math.max(acc.bottom, bottom), } - } + }, + { left: Infinity, top: Infinity, right: -Infinity, bottom: -Infinity }, + ) - /** - * Draws the group on the canvas - * @param {LGraphCanvas} graphCanvas - * @param {CanvasRenderingContext2D} ctx - */ - draw(graphCanvas: LGraphCanvas, ctx: CanvasRenderingContext2D): void { - const padding = 4 + this.pos = [bounds.left - padding, bounds.top - padding - this.titleHeight] - ctx.fillStyle = this.color - ctx.strokeStyle = this.color - const [x, y] = this._pos - const [width, height] = this._size - ctx.globalAlpha = 0.25 * graphCanvas.editor_alpha - ctx.beginPath() - ctx.rect(x + 0.5, y + 0.5, width, height) - ctx.fill() - ctx.globalAlpha = graphCanvas.editor_alpha - ctx.stroke() + this.size = [bounds.right - bounds.left + padding * 2, bounds.bottom - bounds.top + padding * 2 + this.titleHeight] + } - ctx.beginPath() - ctx.moveTo(x + width, y + height) - ctx.lineTo(x + width - 10, y + height) - ctx.lineTo(x + width, y + height - 10) - ctx.fill() + getMenuOptions(): IContextMenuValue[] { + return [ + { + content: this.pinned ? 'Unpin' : 'Pin', + callback: () => { + if (this.pinned) this.unpin() + else this.pin() + this.setDirtyCanvas(false, true) + }, + }, + null, + { 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 }, + ] + } - const font_size = this.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE - ctx.font = font_size + "px Arial" - ctx.textAlign = "left" - ctx.fillText(this.title + (this.pinned ? "📌" : ""), x + padding, y + font_size) + isPointInTitlebar(x: number, y: number): boolean { + const b = this._bounding + return isInsideRectangle(x, y, b[0], b[1], b[2], this.titleHeight) + } - if (LiteGraph.highlight_selected_group && this.selected) { - graphCanvas.drawSelectionBounding(ctx, this._bounding, { - shape: RenderShape.BOX, - title_height: this.titleHeight, - title_mode: TitleMode.NORMAL_TITLE, - fgcolor: this.color, - padding, - }) - } - } - - resize(width: number, height: number): void { - if (this.pinned) return - - this._size[0] = width - this._size[1] = height - } - - move(deltax: number, deltay: number, ignore_nodes = false): void { - if (this.pinned) return - - this._pos[0] += deltax - this._pos[1] += deltay - if (ignore_nodes) return - - for (let i = 0; i < this._nodes.length; ++i) { - const node = this._nodes[i] - node.pos[0] += deltax - node.pos[1] += deltay - } - } - - recomputeInsideNodes(): void { - this._nodes.length = 0 - const nodes = this.graph._nodes - const node_bounding = new Float32Array(4) - - for (let i = 0; i < nodes.length; ++i) { - const node = nodes[i] - node.getBounding(node_bounding) - //out of the visible area - if (!overlapBounding(this._bounding, node_bounding)) - continue - - this._nodes.push(node) - } - } - - /** - * Add nodes to the group and adjust the group's position and size accordingly - * @param {LGraphNode[]} nodes - The nodes to add to the group - * @param {number} [padding=10] - The padding around the group - * @returns {void} - */ - addNodes(nodes: LGraphNode[], padding: number = 10): void { - if (!this._nodes && nodes.length === 0) return - - const allNodes = [...(this._nodes || []), ...nodes] - - const bounds = allNodes.reduce((acc, node) => { - const [x, y] = node.pos - const [width, height] = node.size - const isReroute = node.type === "Reroute" - const isCollapsed = node.flags?.collapsed - - const top = y - (isReroute ? 0 : LiteGraph.NODE_TITLE_HEIGHT) - const bottom = isCollapsed ? top + LiteGraph.NODE_TITLE_HEIGHT : y + height - const right = isCollapsed && node._collapsed_width ? x + Math.round(node._collapsed_width) : x + width - - return { - left: Math.min(acc.left, x), - top: Math.min(acc.top, top), - right: Math.max(acc.right, right), - bottom: Math.max(acc.bottom, bottom) - } - }, { left: Infinity, top: Infinity, right: -Infinity, bottom: -Infinity }) - - this.pos = [ - bounds.left - padding, - bounds.top - padding - this.titleHeight - ] - - this.size = [ - bounds.right - bounds.left + padding * 2, - bounds.bottom - bounds.top + padding * 2 + this.titleHeight - ] - } - - getMenuOptions(): IContextMenuValue[] { - return [ - { - content: this.pinned ? "Unpin" : "Pin", - callback: () => { - if (this.pinned) this.unpin() - else this.pin() - this.setDirtyCanvas(false, true) - }, - }, - null, - { 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 } - ] - } - - isPointInTitlebar(x: number, y: number): boolean { - const b = this._bounding - return isInsideRectangle(x, y, b[0], b[1], b[2], this.titleHeight) - } - - isPointInside = LGraphNode.prototype.isPointInside - setDirtyCanvas = LGraphNode.prototype.setDirtyCanvas + isPointInside = LGraphNode.prototype.isPointInside + setDirtyCanvas = LGraphNode.prototype.setDirtyCanvas } diff --git a/src/LGraphNode.ts b/src/LGraphNode.ts index afccfb962..4a192b46e 100644 --- a/src/LGraphNode.ts +++ b/src/LGraphNode.ts @@ -1,54 +1,66 @@ -import type { Dictionary, IContextMenuValue, IFoundSlot, INodeFlags, INodeInputSlot, INodeOutputSlot, IOptionalSlotData, ISlotType, Point, Rect, Size } from "./interfaces" -import type { LGraph } from "./LGraph" -import type { IWidget, TWidgetValue } from "./types/widgets" -import type { ISerialisedNode } from "./types/serialisation" -import type { LGraphCanvas } from "./LGraphCanvas" -import type { CanvasMouseEvent } from "./types/events" -import type { DragAndScale } from "./DragAndScale" -import { LGraphEventMode, NodeSlotType, TitleMode, RenderShape } from "./types/globalEnums" -import { BadgePosition, LGraphBadge } from "./LGraphBadge" -import { type LGraphNodeConstructor, LiteGraph } from "./litegraph" -import { isInsideRectangle } from "./measure" -import { LLink } from "./LLink" +import type { + Dictionary, + IContextMenuValue, + IFoundSlot, + INodeFlags, + INodeInputSlot, + INodeOutputSlot, + IOptionalSlotData, + ISlotType, + Point, + Rect, + Size, +} from './interfaces' +import type { LGraph } from './LGraph' +import type { IWidget, TWidgetValue } from './types/widgets' +import type { ISerialisedNode } from './types/serialisation' +import type { LGraphCanvas } from './LGraphCanvas' +import type { CanvasMouseEvent } from './types/events' +import type { DragAndScale } from './DragAndScale' +import { LGraphEventMode, NodeSlotType, TitleMode, RenderShape } from './types/globalEnums' +import { BadgePosition, LGraphBadge } from './LGraphBadge' +import { type LGraphNodeConstructor, LiteGraph } from './litegraph' +import { isInsideRectangle } from './measure' +import { LLink } from './LLink' export type NodeId = number | string export interface INodePropertyInfo { - name: string - type: string - default_value: unknown + name: string + type: string + default_value: unknown } export type INodeProperties = Dictionary & { - horizontal?: boolean + horizontal?: boolean } interface IMouseOverData { - inputId: number | null - outputId: number | null - overWidget: IWidget | null + inputId: number | null + outputId: number | null + overWidget: IWidget | null } interface ConnectByTypeOptions { - /** @deprecated Events */ - createEventInCase?: boolean - /** Allow our wildcard slot to connect to typed slots on remote node. Default: true */ - wildcardToTyped?: boolean - /** Allow our typed slot to connect to wildcard slots on remote node. Default: true */ - typedToWildcard?: boolean + /** @deprecated Events */ + createEventInCase?: boolean + /** Allow our wildcard slot to connect to typed slots on remote node. Default: true */ + wildcardToTyped?: boolean + /** Allow our typed slot to connect to wildcard slots on remote node. Default: true */ + typedToWildcard?: boolean } /** Internal type used for type safety when implementing generic checks for inputs & outputs */ interface IGenericLinkOrLinks { - links?: INodeOutputSlot["links"] - link?: INodeInputSlot["link"] + links?: INodeOutputSlot['links'] + link?: INodeInputSlot['link'] } interface FindFreeSlotOptions { - /** Slots matching these types will be ignored. Default: [] */ - typesNotAccepted?: ISlotType[] - /** If true, the slot itself is returned instead of the index. Default: false */ - returnObj?: boolean + /** Slots matching these types will be ignored. Default: [] */ + typesNotAccepted?: ISlotType[] + /** If true, the slot itself is returned instead of the index. Default: false */ + returnObj?: boolean } /* @@ -104,7 +116,7 @@ supported callbacks: */ export interface LGraphNode { - constructor: LGraphNodeConstructor + constructor: LGraphNodeConstructor } /** @@ -112,2223 +124,2128 @@ export interface LGraphNode { * @param {String} name a name for the node */ export class LGraphNode { - // Static properties used by dynamic child classes - static title?: string - static MAX_CONSOLE?: number - static type?: string - static category?: string - static supported_extensions?: string[] - static filter?: string - static skip_list?: boolean + // Static properties used by dynamic child classes + static title?: string + static MAX_CONSOLE?: number + static type?: string + static category?: string + static supported_extensions?: string[] + static filter?: string + static skip_list?: boolean - title: string - graph: LGraph | null = null - id: NodeId - type: string | null = null - inputs: INodeInputSlot[] = [] - outputs: INodeOutputSlot[] = [] - // Not used - connections: unknown[] = [] - properties: INodeProperties = {} - properties_info: INodePropertyInfo[] = [] - flags: INodeFlags = {} - widgets?: IWidget[] + title: string + graph: LGraph | null = null + id: NodeId + type: string | null = null + inputs: INodeInputSlot[] = [] + outputs: INodeOutputSlot[] = [] + // Not used + connections: unknown[] = [] + properties: INodeProperties = {} + properties_info: INodePropertyInfo[] = [] + flags: INodeFlags = {} + widgets?: IWidget[] - size: Size - locked?: boolean + size: Size + locked?: boolean - // Execution order, automatically computed during run - order?: number - mode: LGraphEventMode - last_serialization?: ISerialisedNode - serialize_widgets?: boolean - color: string - bgcolor: string - boxcolor: string - exec_version: number - action_call?: string - execute_triggered: number - action_triggered: number - widgets_up?: boolean - widgets_start_y?: number - lostFocusAt?: number - gotFocusAt?: number - badges: (LGraphBadge | (() => LGraphBadge))[] = [] - badgePosition: BadgePosition = BadgePosition.TopLeft - onOutputRemoved?(this: LGraphNode, slot: number): void - onInputRemoved?(this: LGraphNode, slot: number, input: INodeInputSlot): void - _collapsed_width: number - onBounding?(this: LGraphNode, out: Rect): void - horizontal?: boolean - console?: string[] - _level: number - _shape?: RenderShape - subgraph?: LGraph - skip_subgraph_button?: boolean - mouseOver?: IMouseOverData - is_selected?: boolean - redraw_on_mouse?: boolean - // Appears unused - optional_inputs? - // Appears unused - optional_outputs? - resizable?: boolean - clonable?: boolean - _relative_id?: number - clip_area?: boolean - ignore_remove?: boolean - has_errors?: boolean - removable?: boolean - block_delete?: boolean - showAdvanced?: boolean + // Execution order, automatically computed during run + order?: number + mode: LGraphEventMode + last_serialization?: ISerialisedNode + serialize_widgets?: boolean + color: string + bgcolor: string + boxcolor: string + exec_version: number + action_call?: string + execute_triggered: number + action_triggered: number + widgets_up?: boolean + widgets_start_y?: number + lostFocusAt?: number + gotFocusAt?: number + badges: (LGraphBadge | (() => LGraphBadge))[] = [] + badgePosition: BadgePosition = BadgePosition.TopLeft + onOutputRemoved?(this: LGraphNode, slot: number): void + onInputRemoved?(this: LGraphNode, slot: number, input: INodeInputSlot): void + _collapsed_width: number + onBounding?(this: LGraphNode, out: Rect): void + horizontal?: boolean + console?: string[] + _level: number + _shape?: RenderShape + subgraph?: LGraph + skip_subgraph_button?: boolean + mouseOver?: IMouseOverData + is_selected?: boolean + redraw_on_mouse?: boolean + // Appears unused + optional_inputs? + // Appears unused + optional_outputs? + resizable?: boolean + clonable?: boolean + _relative_id?: number + clip_area?: boolean + ignore_remove?: boolean + has_errors?: boolean + removable?: boolean + block_delete?: boolean + showAdvanced?: boolean - _pos: Point = new Float32Array([10, 10]) - public get pos() { - return this._pos + _pos: Point = new Float32Array([10, 10]) + public get pos() { + return this._pos + } + public set pos(value) { + if (!value || value.length < 2) return + + this._pos[0] = value[0] + this._pos[1] = value[1] + } + + get shape(): RenderShape { + return this._shape + } + set shape(v: RenderShape | 'default' | 'box' | 'round' | 'circle' | 'card') { + switch (v) { + case 'default': + delete this._shape + break + case 'box': + this._shape = RenderShape.BOX + break + case 'round': + this._shape = RenderShape.ROUND + break + case 'circle': + this._shape = RenderShape.CIRCLE + break + case 'card': + this._shape = RenderShape.CARD + break + default: + this._shape = v } - public set pos(value) { - if (!value || value.length < 2) return + } - this._pos[0] = value[0] - this._pos[1] = value[1] + // Used in group node + setInnerNodes?(this: LGraphNode, nodes: LGraphNode[]): void + + onConnectInput?(this: LGraphNode, target_slot: number, type: unknown, output: INodeOutputSlot, node: LGraphNode, slot: number): boolean + onConnectOutput?( + this: LGraphNode, + slot: number, + type: unknown, + input: INodeInputSlot, + target_node: number | LGraphNode, + target_slot: number, + ): boolean + onResize?(this: LGraphNode, size: Size): void + onPropertyChanged?(this: LGraphNode, name: string, value: unknown, prev_value?: unknown): boolean + onConnectionsChange?( + this: LGraphNode, + type: ISlotType, + index: number, + isConnected: boolean, + link_info: LLink, + inputOrOutput: INodeInputSlot | INodeOutputSlot, + ): void + onInputAdded?(this: LGraphNode, input: INodeInputSlot): void + onOutputAdded?(this: LGraphNode, output: INodeOutputSlot): void + onConfigure?(this: LGraphNode, serialisedNode: ISerialisedNode): void + onSerialize?(this: LGraphNode, serialised: ISerialisedNode): any + onExecute?(this: LGraphNode, param?: unknown, options?: { action_call?: any }): void + onAction?(this: LGraphNode, action: string, param: unknown, options: { action_call?: string }): void + onDrawBackground?( + this: LGraphNode, + ctx: CanvasRenderingContext2D, + canvas: LGraphCanvas, + canvasElement: HTMLCanvasElement, + mousePosition: Point, + ): void + onNodeCreated?(this: LGraphNode): void + /** + * Callback invoked by {@link connect} to override the target slot index. Its return value overrides the target index selection. + * @param target_slot The current input slot index + * @param requested_slot The originally requested slot index - could be negative, or if using (deprecated) name search, a string + * @returns {number | null} If a number is returned, the connection will be made to that input index. + * If an invalid index or non-number (false, null, NaN etc) is returned, the connection will be cancelled. + */ + onBeforeConnectInput?(this: LGraphNode, target_slot: number, requested_slot?: number | string): number | false | null + onShowCustomPanelInfo?(this: LGraphNode, panel: any): void + onAddPropertyToPanel?(this: LGraphNode, pName: string, panel: any): boolean + onWidgetChanged?(this: LGraphNode, name: string, value: unknown, old_value: unknown, w: IWidget): void + onDeselected?(this: LGraphNode): void + onKeyUp?(this: LGraphNode, e: KeyboardEvent): void + onKeyDown?(this: LGraphNode, e: KeyboardEvent): void + onSelected?(this: LGraphNode): void + getExtraMenuOptions?(this: LGraphNode, canvas: LGraphCanvas, options: IContextMenuValue[]): IContextMenuValue[] + getMenuOptions?(this: LGraphNode, canvas: LGraphCanvas): IContextMenuValue[] + onAdded?(this: LGraphNode, graph: LGraph): void + onDrawCollapsed?(this: LGraphNode, ctx: CanvasRenderingContext2D, cavnas: LGraphCanvas): boolean + onDrawForeground?(this: LGraphNode, ctx: CanvasRenderingContext2D, canvas: LGraphCanvas, canvasElement: HTMLCanvasElement): void + onMouseLeave?(this: LGraphNode, e: CanvasMouseEvent): void + getSlotMenuOptions?(this: LGraphNode, slot: IFoundSlot): IContextMenuValue[] + // FIXME: Re-typing + onDropItem?(this: LGraphNode, event: Event): boolean + onDropData?(this: LGraphNode, data: string | ArrayBuffer, filename: any, file: any): void + onDropFile?(this: LGraphNode, file: any): void + onInputClick?(this: LGraphNode, index: number, e: CanvasMouseEvent): void + onInputDblClick?(this: LGraphNode, index: number, e: CanvasMouseEvent): void + onOutputClick?(this: LGraphNode, index: number, e: CanvasMouseEvent): void + onOutputDblClick?(this: LGraphNode, index: number, e: CanvasMouseEvent): void + // TODO: Return type + onGetPropertyInfo?(this: LGraphNode, property: string): any + onNodeOutputAdd?(this: LGraphNode, value): void + onNodeInputAdd?(this: LGraphNode, value): void + onMenuNodeInputs?(this: LGraphNode, entries: IOptionalSlotData[]): IOptionalSlotData[] + onMenuNodeOutputs?(this: LGraphNode, entries: IOptionalSlotData[]): IOptionalSlotData[] + onGetInputs?(this: LGraphNode): INodeInputSlot[] + onGetOutputs?(this: LGraphNode): INodeOutputSlot[] + onMouseUp?(this: LGraphNode, e: CanvasMouseEvent, pos: Point): void + onMouseEnter?(this: LGraphNode, e: CanvasMouseEvent): void + onMouseDown?(this: LGraphNode, e: CanvasMouseEvent, pos: Point, canvas: LGraphCanvas): boolean + onDblClick?(this: LGraphNode, e: CanvasMouseEvent, pos: Point, canvas: LGraphCanvas): void + onNodeTitleDblClick?(this: LGraphNode, e: CanvasMouseEvent, pos: Point, canvas: LGraphCanvas): void + onDrawTitle?(this: LGraphNode, ctx: CanvasRenderingContext2D): void + onDrawTitleText?( + this: LGraphNode, + ctx: CanvasRenderingContext2D, + title_height: number, + size: Size, + scale: number, + title_text_font: string, + selected: boolean, + ): void + onDrawTitleBox?(this: LGraphNode, ctx: CanvasRenderingContext2D, title_height: number, size: Size, scale: number): void + onDrawTitleBar?(this: LGraphNode, ctx: CanvasRenderingContext2D, title_height: number, size: Size, scale: number, fgcolor: any): void + onRemoved?(this: LGraphNode): void + onMouseMove?(this: LGraphNode, e: MouseEvent, pos: Point, arg2: LGraphCanvas): void + onPropertyChange?(this: LGraphNode): void + updateOutputData?(this: LGraphNode, origin_slot: number): void + isValidWidgetLink?(slot_index: number, node: LGraphNode, overWidget: IWidget): boolean | undefined + + constructor(title: string) { + this.id = LiteGraph.use_uuids ? LiteGraph.uuidv4() : -1 + this.title = title || 'Unnamed' + this.size = [LiteGraph.NODE_WIDTH, 60] + } + + /** + * configure a node from an object containing the serialized info + */ + configure(info: ISerialisedNode): void { + if (this.graph) { + this.graph._version++ } - - get shape(): RenderShape { - return this._shape - } - set shape(v: RenderShape | "default" | "box" | "round" | "circle" | "card") { - switch (v) { - case "default": - delete this._shape - break - case "box": - this._shape = RenderShape.BOX - break - case "round": - this._shape = RenderShape.ROUND - break - case "circle": - this._shape = RenderShape.CIRCLE - break - case "card": - this._shape = RenderShape.CARD - break - default: - this._shape = v + for (const j in info) { + if (j == 'properties') { + //i don't want to clone properties, I want to reuse the old container + for (const k in info.properties) { + this.properties[k] = info.properties[k] + this.onPropertyChanged?.(k, info.properties[k]) } - } + continue + } - // Used in group node - setInnerNodes?(this: LGraphNode, nodes: LGraphNode[]): void - - onConnectInput?(this: LGraphNode, target_slot: number, type: unknown, output: INodeOutputSlot, node: LGraphNode, slot: number): boolean - onConnectOutput?(this: LGraphNode, slot: number, type: unknown, input: INodeInputSlot, target_node: number | LGraphNode, target_slot: number): boolean - onResize?(this: LGraphNode, size: Size): void - onPropertyChanged?(this: LGraphNode, name: string, value: unknown, prev_value?: unknown): boolean - onConnectionsChange?(this: LGraphNode, type: ISlotType, index: number, isConnected: boolean, link_info: LLink, inputOrOutput: INodeInputSlot | INodeOutputSlot): void - onInputAdded?(this: LGraphNode, input: INodeInputSlot): void - onOutputAdded?(this: LGraphNode, output: INodeOutputSlot): void - onConfigure?(this: LGraphNode, serialisedNode: ISerialisedNode): void - onSerialize?(this: LGraphNode, serialised: ISerialisedNode): any - onExecute?(this: LGraphNode, param?: unknown, options?: { action_call?: any }): void - onAction?(this: LGraphNode, action: string, param: unknown, options: { action_call?: string }): void - onDrawBackground?(this: LGraphNode, ctx: CanvasRenderingContext2D, canvas: LGraphCanvas, canvasElement: HTMLCanvasElement, mousePosition: Point): void - onNodeCreated?(this: LGraphNode): void - /** - * Callback invoked by {@link connect} to override the target slot index. Its return value overrides the target index selection. - * @param target_slot The current input slot index - * @param requested_slot The originally requested slot index - could be negative, or if using (deprecated) name search, a string - * @returns {number | null} If a number is returned, the connection will be made to that input index. - * If an invalid index or non-number (false, null, NaN etc) is returned, the connection will be cancelled. - */ - onBeforeConnectInput?(this: LGraphNode, target_slot: number, requested_slot?: number | string): number | false | null - onShowCustomPanelInfo?(this: LGraphNode, panel: any): void - onAddPropertyToPanel?(this: LGraphNode, pName: string, panel: any): boolean - onWidgetChanged?(this: LGraphNode, name: string, value: unknown, old_value: unknown, w: IWidget): void - onDeselected?(this: LGraphNode): void - onKeyUp?(this: LGraphNode, e: KeyboardEvent): void - onKeyDown?(this: LGraphNode, e: KeyboardEvent): void - onSelected?(this: LGraphNode): void - getExtraMenuOptions?(this: LGraphNode, canvas: LGraphCanvas, options: IContextMenuValue[]): IContextMenuValue[] - getMenuOptions?(this: LGraphNode, canvas: LGraphCanvas): IContextMenuValue[] - onAdded?(this: LGraphNode, graph: LGraph): void - onDrawCollapsed?(this: LGraphNode, ctx: CanvasRenderingContext2D, cavnas: LGraphCanvas): boolean - onDrawForeground?(this: LGraphNode, ctx: CanvasRenderingContext2D, canvas: LGraphCanvas, canvasElement: HTMLCanvasElement): void - onMouseLeave?(this: LGraphNode, e: CanvasMouseEvent): void - getSlotMenuOptions?(this: LGraphNode, slot: IFoundSlot): IContextMenuValue[] - // FIXME: Re-typing - onDropItem?(this: LGraphNode, event: Event): boolean - onDropData?(this: LGraphNode, data: string | ArrayBuffer, filename: any, file: any): void - onDropFile?(this: LGraphNode, file: any): void - onInputClick?(this: LGraphNode, index: number, e: CanvasMouseEvent): void - onInputDblClick?(this: LGraphNode, index: number, e: CanvasMouseEvent): void - onOutputClick?(this: LGraphNode, index: number, e: CanvasMouseEvent): void - onOutputDblClick?(this: LGraphNode, index: number, e: CanvasMouseEvent): void - // TODO: Return type - onGetPropertyInfo?(this: LGraphNode, property: string): any - onNodeOutputAdd?(this: LGraphNode, value): void - onNodeInputAdd?(this: LGraphNode, value): void - onMenuNodeInputs?(this: LGraphNode, entries: IOptionalSlotData[]): IOptionalSlotData[] - onMenuNodeOutputs?(this: LGraphNode, entries: IOptionalSlotData[]): IOptionalSlotData[] - onGetInputs?(this: LGraphNode): INodeInputSlot[] - onGetOutputs?(this: LGraphNode): INodeOutputSlot[] - onMouseUp?(this: LGraphNode, e: CanvasMouseEvent, pos: Point): void - onMouseEnter?(this: LGraphNode, e: CanvasMouseEvent): void - onMouseDown?(this: LGraphNode, e: CanvasMouseEvent, pos: Point, canvas: LGraphCanvas): boolean - onDblClick?(this: LGraphNode, e: CanvasMouseEvent, pos: Point, canvas: LGraphCanvas): void - onNodeTitleDblClick?(this: LGraphNode, e: CanvasMouseEvent, pos: Point, canvas: LGraphCanvas): void - onDrawTitle?(this: LGraphNode, ctx: CanvasRenderingContext2D): void - onDrawTitleText?(this: LGraphNode, ctx: CanvasRenderingContext2D, title_height: number, size: Size, scale: number, title_text_font: string, selected: boolean): void - onDrawTitleBox?(this: LGraphNode, ctx: CanvasRenderingContext2D, title_height: number, size: Size, scale: number): void - onDrawTitleBar?(this: LGraphNode, ctx: CanvasRenderingContext2D, title_height: number, size: Size, scale: number, fgcolor: any): void - onRemoved?(this: LGraphNode): void - onMouseMove?(this: LGraphNode, e: MouseEvent, pos: Point, arg2: LGraphCanvas): void - onPropertyChange?(this: LGraphNode): void - updateOutputData?(this: LGraphNode, origin_slot: number): void - isValidWidgetLink?(slot_index: number, node: LGraphNode, overWidget: IWidget): boolean | undefined - - constructor(title: string) { - this.id = LiteGraph.use_uuids ? LiteGraph.uuidv4() : -1 - this.title = title || "Unnamed" - this.size = [LiteGraph.NODE_WIDTH, 60] - } - - /** - * configure a node from an object containing the serialized info - */ - configure(info: ISerialisedNode): void { - if (this.graph) { - this.graph._version++ - } - for (const j in info) { - if (j == "properties") { - //i don't want to clone properties, I want to reuse the old container - for (const k in info.properties) { - this.properties[k] = info.properties[k] - this.onPropertyChanged?.(k, info.properties[k]) - } - continue - } - - if (info[j] == null) { - continue - } else if (typeof info[j] == "object") { - //object - if (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 (let i = 0; i < this.inputs.length; ++i) { - const input = this.inputs[i] - const link = this.graph ? this.graph._links.get(input.link) : null - this.onConnectionsChange?.(NodeSlotType.INPUT, i, true, link, input) - this.onInputAdded?.(input) - } - } - - if (this.outputs) { - for (let i = 0; i < this.outputs.length; ++i) { - const output = this.outputs[i] - if (!output.links) { - continue - } - for (let j = 0; j < output.links.length; ++j) { - const link = this.graph ? this.graph._links.get(output.links[j]) : null - this.onConnectionsChange?.(NodeSlotType.OUTPUT, i, true, link, output) - } - this.onOutputAdded?.(output) - } - } - - if (this.widgets) { - for (let i = 0; i < this.widgets.length; ++i) { - const w = this.widgets[i] - if (!w) - continue - if (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 (let i = 0; i < info.widgets_values.length; ++i) { - if (this.widgets[i]) { - this.widgets[i].value = info.widgets_values[i] - } - } - } - } - - // Sync the state of this.resizable. - if (this.pinned) this.pin(true) - - this.onConfigure?.(info) - } - - /** - * serialize the content - */ - serialize(): ISerialisedNode { - //create serialization object - const o: ISerialisedNode = { - id: this.id, - type: this.type, - pos: this.pos, - size: this.size, - flags: LiteGraph.cloneObject(this.flags), - order: this.order, - mode: this.mode, - showAdvanced: this.showAdvanced - } - - //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 (let 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 (let 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?.(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 */ - clone(): LGraphNode { - const node = LiteGraph.createNode(this.type) - if (!node) return null - - //we clone it because serialize returns shared containers - const data = LiteGraph.cloneObject(this.serialize()) - - //remove links - if (data.inputs) { - for (let i = 0; i < data.inputs.length; ++i) { - data.inputs[i].link = null - } - } - - if (data.outputs) { - for (let 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 - */ - toString(): string { - return JSON.stringify(this.serialize()) - } - - /** - * get the title string - */ - getTitle(): string { - return this.title || this.constructor.title - } - - /** - * sets the value of a property - * @param {String} name - * @param {*} value - */ - setProperty(name: string, value: TWidgetValue): void { - this.properties ||= {} - if (value === this.properties[name]) - return - - const prev_value = this.properties[name] - this.properties[name] = value - //abort change - if (this.onPropertyChanged?.(name, value, prev_value) === false) - this.properties[name] = prev_value - - if (this.widgets) //widgets could be linked to properties - for (let i = 0; i < this.widgets.length; ++i) { - const w = this.widgets[i] - if (!w) - continue - if (w.options.property == name) { - w.value = value - break - } - } - } - - /** - * sets the output data - * @param {number} slot - * @param {*} data - */ - setOutputData(slot: number, data: unknown): void { - 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 - - const 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 (let i = 0; i < this.outputs[slot].links.length; i++) { - const link_id = this.outputs[slot].links[i] - const link = this.graph._links.get(link_id) - if (link) - link.data = data - } - } - } - - /** - * sets the output data type, useful when you want to be able to overwrite the data type - * @param {number} slot - * @param {String} datatype - */ - setOutputDataType(slot: number, type: ISlotType): void { - if (!this.outputs) return - if (slot == -1 || slot >= this.outputs.length) return - const 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 (let i = 0; i < this.outputs[slot].links.length; i++) { - const link_id = this.outputs[slot].links[i] - this.graph._links.get(link_id).type = type - } - } - } - - /** - * Retrieves the input data (data traveling through the connection) from one slot - * @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 - */ - getInputData(slot: number, force_update?: boolean): unknown { - if (!this.inputs) return - - if (slot >= this.inputs.length || this.inputs[slot].link == null) return - - const link_id = this.inputs[slot].link - const link = this.graph._links.get(link_id) - //bug: weird case but it happens sometimes - if (!link) return null - - if (!force_update) return link.data - - //special case: used to extract data from the incoming connection before the graph has been executed - const node = this.graph.getNodeById(link.origin_id) - if (!node) return link.data - - if (node.updateOutputData) { - node.updateOutputData(link.origin_slot) + if (info[j] == null) { + continue + } else if (typeof info[j] == 'object') { + //object + if (this[j]?.configure) { + this[j]?.configure(info[j]) } else { - node.onExecute?.() + this[j] = LiteGraph.cloneObject(info[j], this[j]) } - - return link.data + } //value + else { + this[j] = info[j] + } } - /** - * Retrieves the input data type (in case this supports multiple input types) - * @param {number} slot - * @return {String} datatype in string format - */ - getInputDataType(slot: number): ISlotType { - if (!this.inputs) return null - - if (slot >= this.inputs.length || this.inputs[slot].link == null) return null - const link_id = this.inputs[slot].link - const link = this.graph._links.get(link_id) - //bug: weird case but it happens sometimes - if (!link) return null - - const node = this.graph.getNodeById(link.origin_id) - if (!node) return link.type - - const output_info = node.outputs[link.origin_slot] - return output_info - ? output_info.type - : null + if (!info.title) { + this.title = this.constructor.title } - /** - * Retrieves the input data from one slot using its name instead of slot number - * @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 - */ - getInputDataByName(slot_name: string, force_update: boolean): unknown { - const slot = this.findInputSlot(slot_name) - return slot == -1 - ? null - : this.getInputData(slot, force_update) - } - - /** - * tells you if there is a connection in one input slot - * @param {number} slot - * @return {boolean} - */ - isInputConnected(slot: number): boolean { - 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) - * @param {number} slot - * @return {Object} object or null { link: id, name: string, type: string or 0 } - */ - getInputInfo(slot: number): INodeInputSlot { - return !this.inputs || !(slot < this.inputs.length) - ? null - : this.inputs[slot] - } - - /** - * Returns the link info in the connection of an input slot - * @param {number} slot - * @return {LLink} object or null - */ - getInputLink(slot: number): LLink | null { - if (!this.inputs) return null - if (slot < this.inputs.length) { - const slot_info = this.inputs[slot] - return this.graph._links.get(slot_info.link) - } - return null - } - - /** - * returns the node connected in the input slot - * @param {number} slot - * @return {LGraphNode} node or null - */ - getInputNode(slot: number): LGraphNode { - if (!this.inputs) return null - if (slot >= this.inputs.length) return null - - const input = this.inputs[slot] - if (!input || input.link === null) return null - - const link_info = this.graph._links.get(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 - * @param {string} name - * @return {*} value - */ - getInputOrProperty(name: string): unknown { - if (!this.inputs || !this.inputs.length) { - return this.properties ? this.properties[name] : null - } - - for (let i = 0, l = this.inputs.length; i < l; ++i) { - const input_info = this.inputs[i] - if (name == input_info.name && input_info.link != null) { - const link = this.graph._links.get(input_info.link) - if (link) return link.data - } - } - return this.properties[name] - } - - /** - * tells you the last output data that went in that slot - * @param {number} slot - * @return {Object} object or null - */ - getOutputData(slot: number): unknown { - if (!this.outputs) return null - if (slot >= this.outputs.length) return null - - const info = this.outputs[slot] - return info._data - } - - /** - * tells you info about an output connection (which node, type, etc) - * @param {number} slot - * @return {Object} object or null { name: string, type: string, links: [ ids of links in number ] } - */ - getOutputInfo(slot: number): INodeOutputSlot { - return !this.outputs || !(slot < this.outputs.length) - ? null - : this.outputs[slot] - } - - /** - * tells you if there is a connection in one output slot - * @param {number} slot - * @return {boolean} - */ - isOutputConnected(slot: number): boolean { - if (!this.outputs) return false - return slot < this.outputs.length && this.outputs[slot].links?.length > 0 - } - - /** - * tells you if there is any connection in the output slots - * @return {boolean} - */ - isAnyOutputConnected(): boolean { - if (!this.outputs) return false - - for (let 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 - * @param {number} slot - * @return {array} - */ - getOutputNodes(slot: number): LGraphNode[] { - if (!this.outputs || this.outputs.length == 0) return null - - if (slot >= this.outputs.length) return null - - const output = this.outputs[slot] - if (!output.links || output.links.length == 0) return null - - const r: LGraphNode[] = [] - for (let i = 0; i < output.links.length; i++) { - const link_id = output.links[i] - const link = this.graph._links.get(link_id) - if (link) { - const target_node = this.graph.getNodeById(link.target_id) - if (target_node) { - r.push(target_node) - } - } - } - return r - } - - addOnTriggerInput(): number { - const trigS = this.findInputSlot("onTrigger") - if (trigS == -1) { //!trigS || - const input = this.addInput("onTrigger", LiteGraph.EVENT, { optional: true, nameLocked: true }) - return this.findInputSlot("onTrigger") - } - return trigS - } - - addOnExecutedOutput(): number { - const trigS = this.findOutputSlot("onExecuted") - if (trigS == -1) { //!trigS || - const output = this.addOutput("onExecuted", LiteGraph.ACTION, { optional: true, nameLocked: true }) - return this.findOutputSlot("onExecuted") - } - return trigS - } - - onAfterExecuteNode(param: unknown, options?: { action_call?: any }) { - const 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) - - } - } - - changeMode(modeTo: number): boolean { - switch (modeTo) { - case LGraphEventMode.ON_EVENT: - // this.addOnExecutedOutput(); - break - - case LGraphEventMode.ON_TRIGGER: - this.addOnTriggerInput() - this.addOnExecutedOutput() - break - - case LGraphEventMode.NEVER: - break - - case LGraphEventMode.ALWAYS: - break - - // @ts-expect-error Not impl. - 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 - * @param {*} param - * @param {*} options - */ - doExecute(param?: unknown, options?: { action_call?: any }): void { - options = options || {} - if (this.onExecute) { - - // enable this to give the event an ID - 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?.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 - this.onAfterExecuteNode?.(param, options) // callback - } - - /** - * Triggers an action, wrapped by logics to control execution flow - * @param {String} action name - * @param {*} param - */ - actionDo(action: string, param: unknown, options: { action_call?: string }): void { - options = options || {} - if (this.onAction) { - - // enable this to give the event an ID - 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?.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 - this.onAfterExecuteNode?.(param, options) - } - - /** - * Triggers an event in this node, this will trigger any output with the same name - * @param {String} event name ( "on_play", ... ) if action is equivalent to false then the event is send to all - * @param {*} param - */ - trigger(action: string, param: unknown, options: { action_call?: any }): void { - if (!this.outputs || !this.outputs.length) { - return - } - - if (this.graph) - this.graph._last_trigger_time = LiteGraph.getTime() - - for (let i = 0; i < this.outputs.length; ++i) { - const 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 - * @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 - */ - triggerSlot(slot: number, param: unknown, link_id: number, options: { action_call?: any }): void { - options = options || {} - if (!this.outputs) return - - if (slot == null) { - console.error("slot must be a number") - return - } - - if (typeof slot !== "number") - console.warn("slot must be a number, use node.trigger('name') if you want to use a string") - - const output = this.outputs[slot] - if (!output) return - - const links = output.links - if (!links || !links.length) return - - if (this.graph) - this.graph._last_trigger_time = LiteGraph.getTime() - - //for every link attached here - for (let k = 0; k < links.length; ++k) { - const id = links[k] - //to skip links - if (link_id != null && link_id != id) continue - - const link_info = this.graph._links.get(id) - //not connected - if (!link_info) continue - - link_info._last_time = LiteGraph.getTime() - const node = this.graph.getNodeById(link_info.target_id) - //node not found? - if (!node) continue - - if (node.mode === LGraphEventMode.ON_TRIGGER) { - // generate unique trigger ID if not present - if (!options.action_call) options.action_call = this.id + "_trigg_" + Math.floor(Math.random() * 9999) - // -- 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 - const 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 - * @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 - */ - clearTriggeredSlot(slot: number, link_id: number): void { - if (!this.outputs) return - - const output = this.outputs[slot] - if (!output) return - - const links = output.links - if (!links || !links.length) return - - //for every link attached here - for (let k = 0; k < links.length; ++k) { - const id = links[k] - //to skip links - if (link_id != null && link_id != id) continue - - const link_info = this.graph._links.get(id) - //not connected - if (!link_info) continue - - link_info._last_time = 0 - } - } - - /** - * changes node size and triggers callback - * @param {vec2} size - */ - setSize(size: Size): void { - this.size = size - this.onResize?.(this.size) - } - - /** - * add a new property to this node - * @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) - */ - addProperty(name: string, - default_value: unknown, - type?: string, - extra_info?: Dictionary): INodePropertyInfo { - const o: INodePropertyInfo = { name: name, type: type, default_value: default_value } - if (extra_info) { - for (const i in extra_info) { - o[i] = extra_info[i] - } - } - this.properties_info ||= [] - this.properties_info.push(o) - this.properties ||= {} - this.properties[name] = default_value - return o - } - - /** - * add a new output slot to use in this node - * @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) - */ - addOutput(name?: string, type?: ISlotType, extra_info?: object): INodeOutputSlot { - const output = { name: name, type: type, links: null } - if (extra_info) { - for (const i in extra_info) { - output[i] = extra_info[i] - } - } - - this.outputs ||= [] - this.outputs.push(output) - 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 - * @param {Array} array of triplets like [[name,type,extra_info],[...]] - */ - addOutputs(array: [string, ISlotType, Record][]): void { - for (let i = 0; i < array.length; ++i) { - const info = array[i] - const o = { name: info[0], type: info[1], links: null } - if (array[2]) { - for (const j in info[2]) { - o[j] = info[2][j] - } - } - - this.outputs ||= [] - this.outputs.push(o) - 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 - * @param {number} slot - */ - removeOutput(slot: number): void { - this.disconnectOutput(slot) - this.outputs.splice(slot, 1) - for (let i = slot; i < this.outputs.length; ++i) { - if (!this.outputs[i] || !this.outputs[i].links) - continue - const links = this.outputs[i].links - for (let j = 0; j < links.length; ++j) { - const link = this.graph._links.get(links[j]) - if (!link) continue - - link.origin_slot -= 1 - } - } - - this.setSize(this.computeSize()) - this.onOutputRemoved?.(slot) - this.setDirtyCanvas(true, true) - } - - /** - * add a new input slot to use in this node - * @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) - */ - addInput(name: string, type: ISlotType, extra_info?: object): INodeInputSlot { - type = type || 0 - const input: INodeInputSlot = { name: name, type: type, link: null } - if (extra_info) { - for (const i in extra_info) { - input[i] = extra_info[i] - } - } - - this.inputs ||= [] - this.inputs.push(input) - this.setSize(this.computeSize()) - + if (this.inputs) { + for (let i = 0; i < this.inputs.length; ++i) { + const input = this.inputs[i] + const link = this.graph ? this.graph._links.get(input.link) : null + this.onConnectionsChange?.(NodeSlotType.INPUT, i, true, link, input) this.onInputAdded?.(input) - LiteGraph.registerNodeAndSlotType(this, type) - - this.setDirtyCanvas(true, true) - return input + } } - /** - * add several new input slots in this node - * @param {Array} array of triplets like [[name,type,extra_info],[...]] - */ - addInputs(array: [string, ISlotType, Record][]): void { - for (let i = 0; i < array.length; ++i) { - const info = array[i] - const o: INodeInputSlot = { name: info[0], type: info[1], link: null } - // TODO: Checking the wrong variable here - confirm no downstream consumers, then remove. - if (array[2]) { - for (const j in info[2]) { - o[j] = info[2][j] - } - } - - this.inputs ||= [] - this.inputs.push(o) - this.onInputAdded?.(o) - - LiteGraph.registerNodeAndSlotType(this, info[1]) + if (this.outputs) { + for (let i = 0; i < this.outputs.length; ++i) { + const output = this.outputs[i] + if (!output.links) { + continue } - - this.setSize(this.computeSize()) - this.setDirtyCanvas(true, true) + for (let j = 0; j < output.links.length; ++j) { + const link = this.graph ? this.graph._links.get(output.links[j]) : null + this.onConnectionsChange?.(NodeSlotType.OUTPUT, i, true, link, output) + } + this.onOutputAdded?.(output) + } } - /** - * remove an existing input slot - * @param {number} slot - */ - removeInput(slot: number): void { - this.disconnectInput(slot) - const slot_info = this.inputs.splice(slot, 1) - for (let i = slot; i < this.inputs.length; ++i) { - if (!this.inputs[i]) continue + if (this.widgets) { + for (let i = 0; i < this.widgets.length; ++i) { + const w = this.widgets[i] - const link = this.graph._links.get(this.inputs[i].link) - if (!link) continue - - link.target_slot -= 1 + if (!w) continue + if (w.options?.property && this.properties[w.options.property] != undefined) { + w.value = JSON.parse(JSON.stringify(this.properties[w.options.property])) } - this.setSize(this.computeSize()) - this.onInputRemoved?.(slot, slot_info[0]) - this.setDirtyCanvas(true, true) + } + + if (info.widgets_values) { + for (let i = 0; i < info.widgets_values.length; ++i) { + if (this.widgets[i]) { + this.widgets[i].value = info.widgets_values[i] + } + } + } } - /** - * add an special connection to this node (used for special kinds of graphs) - * @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 - */ - addConnection(name: string, type: string, pos: Point, direction: string) { - const o = { - name: name, - type: type, - pos: pos, - direction: direction, - links: null - } - this.connections.push(o) - return o + // Sync the state of this.resizable. + if (this.pinned) this.pin(true) + + this.onConfigure?.(info) + } + + /** + * serialize the content + */ + serialize(): ISerialisedNode { + //create serialization object + const o: ISerialisedNode = { + id: this.id, + type: this.type, + pos: this.pos, + size: this.size, + flags: LiteGraph.cloneObject(this.flags), + order: this.order, + mode: this.mode, + showAdvanced: this.showAdvanced, } - /** - * computes the minimum size of a node according to its inputs and output slots - * @param out - * @return the total size - */ - computeSize(out?: Size): Size { - const ctorSize = this.constructor.size - if (ctorSize) return [ctorSize[0], ctorSize[1]] + //special case for when there were errors + if (this.constructor === LGraphNode && this.last_serialization) return this.last_serialization - let rows = Math.max( - this.inputs ? this.inputs.length : 1, - this.outputs ? this.outputs.length : 1 - ) - const size = out || new Float32Array([0, 0]) - rows = Math.max(rows, 1) - const font_size = LiteGraph.NODE_TEXT_SIZE //although it should be graphcanvas.inner_text_font size + if (this.inputs) o.inputs = this.inputs - const title_width = compute_text_size(this.title) - let input_width = 0 - let output_width = 0 - - if (this.inputs) { - for (let i = 0, l = this.inputs.length; i < l; ++i) { - const input = this.inputs[i] - const text = input.label || input.name || "" - const text_width = compute_text_size(text) - if (input_width < text_width) - input_width = text_width - } - } - - if (this.outputs) { - for (let i = 0, l = this.outputs.length; i < l; ++i) { - const output = this.outputs[i] - const text = output.label || output.name || "" - const 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?.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 - - let widgets_height = 0 - if (this.widgets?.length) { - for (let i = 0, l = this.widgets.length; i < l; ++i) { - const widget = this.widgets[i] - if (widget.hidden || (widget.advanced && !this.showAdvanced)) continue; - - widgets_height += widget.computeSize - ? widget.computeSize(size[0])[1] + 4 - : 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: string) { - return text - ? font_size * text.length * 0.6 - : 0 - } - - if (this.constructor.min_height && size[1] < this.constructor.min_height) { - size[1] = this.constructor.min_height - } - - //margin - size[1] += 6 - - return size + if (this.outputs) { + //clear outputs last data (because data in connections is never serialized but stored inside the outputs info) + for (let i = 0; i < this.outputs.length; i++) { + delete this.outputs[i]._data + } + o.outputs = this.outputs } - inResizeCorner(canvasX: number, canvasY: number): boolean { - const rows = this.outputs ? this.outputs.length : 1 - const 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 - ) + 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 (let 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 + } } - /** - * returns all the info available about a property of this node. - * - * @param {String} property name of the property - * @return {Object} the object with all the available info - */ - getPropertyInfo(property: string) { - let info = null + if (!o.type) o.type = this.constructor.type - //there are several ways to define info about a property - //legacy mode - if (this.properties_info) { - for (let 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.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.constructor.widgets_info?.[property]) - info = this.constructor.widgets_info[property] + if (this.onSerialize?.(o)) + console.warn('node onSerialize shouldnt return anything, data should be stored in the object pass in the first parameter') - //litescene mode using the constructor - if (!info && this.onGetPropertyInfo) { - info = this.onGetPropertyInfo(property) - } + return o + } - info ||= {} - info.type ||= typeof this.properties[property] - if (info.widget == "combo") - info.type = "enum" + /* Creates a clone of this node */ + clone(): LGraphNode { + const node = LiteGraph.createNode(this.type) + if (!node) return null - return info + //we clone it because serialize returns shared containers + const data = LiteGraph.cloneObject(this.serialize()) + + //remove links + if (data.inputs) { + for (let i = 0; i < data.inputs.length; ++i) { + data.inputs[i].link = null + } } - /** - * Defines a widget inside the node, it will be rendered on top of the node, you can control lots of properties - * - * @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 - */ - addWidget(type: string, name: string, value: any, callback: IWidget["callback"], options?: any): IWidget { - this.widgets ||= [] - - if (!options && callback && typeof callback === "object") { - options = callback - callback = null + if (data.outputs) { + for (let i = 0; i < data.outputs.length; ++i) { + if (data.outputs[i].links) { + data.outputs[i].links.length = 0 } - - //options can be the property name - if (options && typeof options === "string") - options = { property: options } - - //callback can be the property name - if (callback && typeof callback === "string") { - options ||= {} - options.property = callback - callback = null - } - - if (callback && typeof callback !== "function") { - console.warn("addWidget: callback must be a function") - callback = null - } - - const w: IWidget = { - // @ts-expect-error Type check or just assert? - 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 + } } - addCustomWidget(custom_widget: IWidget): IWidget { - this.widgets ||= [] - this.widgets.push(custom_widget) - return custom_widget + delete data.id + + if (LiteGraph.use_uuids) data.id = LiteGraph.uuidv4() + + //remove links + node.configure(data) + + return node + } + + /** + * serialize and stringify + */ + toString(): string { + return JSON.stringify(this.serialize()) + } + + /** + * get the title string + */ + getTitle(): string { + return this.title || this.constructor.title + } + + /** + * sets the value of a property + * @param {String} name + * @param {*} value + */ + setProperty(name: string, value: TWidgetValue): void { + this.properties ||= {} + if (value === this.properties[name]) return + + const prev_value = this.properties[name] + this.properties[name] = value + //abort change + if (this.onPropertyChanged?.(name, value, prev_value) === false) this.properties[name] = prev_value + + if (this.widgets) + //widgets could be linked to properties + for (let i = 0; i < this.widgets.length; ++i) { + const w = this.widgets[i] + if (!w) continue + if (w.options.property == name) { + w.value = value + break + } + } + } + + /** + * sets the output data + * @param {number} slot + * @param {*} data + */ + setOutputData(slot: number, data: unknown): void { + 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 + + const 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 (let i = 0; i < this.outputs[slot].links.length; i++) { + const link_id = this.outputs[slot].links[i] + const link = this.graph._links.get(link_id) + if (link) link.data = data + } + } + } + + /** + * sets the output data type, useful when you want to be able to overwrite the data type + * @param {number} slot + * @param {String} datatype + */ + setOutputDataType(slot: number, type: ISlotType): void { + if (!this.outputs) return + if (slot == -1 || slot >= this.outputs.length) return + const 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 (let i = 0; i < this.outputs[slot].links.length; i++) { + const link_id = this.outputs[slot].links[i] + this.graph._links.get(link_id).type = type + } + } + } + + /** + * Retrieves the input data (data traveling through the connection) from one slot + * @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 + */ + getInputData(slot: number, force_update?: boolean): unknown { + if (!this.inputs) return + + if (slot >= this.inputs.length || this.inputs[slot].link == null) return + + const link_id = this.inputs[slot].link + const link = this.graph._links.get(link_id) + //bug: weird case but it happens sometimes + if (!link) return null + + if (!force_update) return link.data + + //special case: used to extract data from the incoming connection before the graph has been executed + const node = this.graph.getNodeById(link.origin_id) + if (!node) return link.data + + if (node.updateOutputData) { + node.updateOutputData(link.origin_slot) + } else { + node.onExecute?.() } - /** - * Measures the node for rendering, populating {@link out} with the results in graph space. - * @param out Results (x, y, width, height) are inserted into this array. - * @param pad Expands the area by this amount on each side. Default: 0 - */ - measure(out: Rect, pad = 0): void { - const titleMode = this.constructor.title_mode - const renderTitle = titleMode != TitleMode.TRANSPARENT_TITLE && titleMode != TitleMode.NO_TITLE - const titleHeight = renderTitle ? LiteGraph.NODE_TITLE_HEIGHT : 0 + return link.data + } - out[0] = this.pos[0] - pad - out[1] = this.pos[1] + -titleHeight - pad - if (!this.flags?.collapsed) { - out[2] = this.size[0] + (2 * pad) - out[3] = this.size[1] + titleHeight + (2 * pad) - } else { - out[2] = (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) + (2 * pad) - out[3] = LiteGraph.NODE_TITLE_HEIGHT + (2 * pad) - } + /** + * Retrieves the input data type (in case this supports multiple input types) + * @param {number} slot + * @return {String} datatype in string format + */ + getInputDataType(slot: number): ISlotType { + if (!this.inputs) return null + + if (slot >= this.inputs.length || this.inputs[slot].link == null) return null + const link_id = this.inputs[slot].link + const link = this.graph._links.get(link_id) + //bug: weird case but it happens sometimes + if (!link) return null + + const node = this.graph.getNodeById(link.origin_id) + if (!node) return link.type + + const output_info = node.outputs[link.origin_slot] + return output_info ? output_info.type : null + } + + /** + * Retrieves the input data from one slot using its name instead of slot number + * @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 + */ + getInputDataByName(slot_name: string, force_update: boolean): unknown { + const slot = this.findInputSlot(slot_name) + return slot == -1 ? null : this.getInputData(slot, force_update) + } + + /** + * tells you if there is a connection in one input slot + * @param {number} slot + * @return {boolean} + */ + isInputConnected(slot: number): boolean { + 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) + * @param {number} slot + * @return {Object} object or null { link: id, name: string, type: string or 0 } + */ + getInputInfo(slot: number): INodeInputSlot { + return !this.inputs || !(slot < this.inputs.length) ? null : this.inputs[slot] + } + + /** + * Returns the link info in the connection of an input slot + * @param {number} slot + * @return {LLink} object or null + */ + getInputLink(slot: number): LLink | null { + if (!this.inputs) return null + if (slot < this.inputs.length) { + const slot_info = this.inputs[slot] + return this.graph._links.get(slot_info.link) + } + return null + } + + /** + * returns the node connected in the input slot + * @param {number} slot + * @return {LGraphNode} node or null + */ + getInputNode(slot: number): LGraphNode { + if (!this.inputs) return null + if (slot >= this.inputs.length) return null + + const input = this.inputs[slot] + if (!input || input.link === null) return null + + const link_info = this.graph._links.get(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 + * @param {string} name + * @return {*} value + */ + getInputOrProperty(name: string): unknown { + if (!this.inputs || !this.inputs.length) { + return this.properties ? this.properties[name] : null } - /** - * returns the bounding of the object, used for rendering purposes - * @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] - */ - getBounding(out?: Float32Array, compute_outer?: boolean): Float32Array { - out = out || new Float32Array(4) - this.measure(out) - if (compute_outer) { - // 4 offset for collapsed node connection points - out[0] -= 4 - out[1] -= 4 - // Add shadow & left offset - out[2] += 6 + 4 - // Add shadow & top offsets - out[3] += 5 + 4 - } - this.onBounding?.(out) - return out + for (let i = 0, l = this.inputs.length; i < l; ++i) { + const input_info = this.inputs[i] + if (name == input_info.name && input_info.link != null) { + const link = this.graph._links.get(input_info.link) + if (link) return link.data + } } + return this.properties[name] + } - /** - * checks if a point is inside the shape of a node - * @param {number} x - * @param {number} y - * @return {boolean} - */ - isPointInside(x: number, y: number, margin?: number, skip_title?: boolean): boolean { - margin ||= 0 + /** + * tells you the last output data that went in that slot + * @param {number} slot + * @return {Object} object or null + */ + getOutputData(slot: number): unknown { + if (!this.outputs) return null + if (slot >= this.outputs.length) return null - const margin_top = skip_title || this.graph?.isLive() - ? 0 - : LiteGraph.NODE_TITLE_HEIGHT + const info = this.outputs[slot] + return info._data + } - if (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 + /** + * tells you info about an output connection (which node, type, etc) + * @param {number} slot + * @return {Object} object or null { name: string, type: string, links: [ ids of links in number ] } + */ + getOutputInfo(slot: number): INodeOutputSlot { + return !this.outputs || !(slot < this.outputs.length) ? null : this.outputs[slot] + } + + /** + * tells you if there is a connection in one output slot + * @param {number} slot + * @return {boolean} + */ + isOutputConnected(slot: number): boolean { + if (!this.outputs) return false + return slot < this.outputs.length && this.outputs[slot].links?.length > 0 + } + + /** + * tells you if there is any connection in the output slots + * @return {boolean} + */ + isAnyOutputConnected(): boolean { + if (!this.outputs) return false + + for (let 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 + * @param {number} slot + * @return {array} + */ + getOutputNodes(slot: number): LGraphNode[] { + if (!this.outputs || this.outputs.length == 0) return null + + if (slot >= this.outputs.length) return null + + const output = this.outputs[slot] + if (!output.links || output.links.length == 0) return null + + const r: LGraphNode[] = [] + for (let i = 0; i < output.links.length; i++) { + const link_id = output.links[i] + const link = this.graph._links.get(link_id) + if (link) { + const target_node = this.graph.getNodeById(link.target_id) + if (target_node) { + r.push(target_node) } + } + } + return r + } + + addOnTriggerInput(): number { + const trigS = this.findInputSlot('onTrigger') + if (trigS == -1) { + //!trigS || + const input = this.addInput('onTrigger', LiteGraph.EVENT, { optional: true, nameLocked: true }) + return this.findInputSlot('onTrigger') + } + return trigS + } + + addOnExecutedOutput(): number { + const trigS = this.findOutputSlot('onExecuted') + if (trigS == -1) { + //!trigS || + const output = this.addOutput('onExecuted', LiteGraph.ACTION, { optional: true, nameLocked: true }) + return this.findOutputSlot('onExecuted') + } + return trigS + } + + onAfterExecuteNode(param: unknown, options?: { action_call?: any }) { + const 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) + } + } + + changeMode(modeTo: number): boolean { + switch (modeTo) { + case LGraphEventMode.ON_EVENT: + // this.addOnExecutedOutput(); + break + + case LGraphEventMode.ON_TRIGGER: + this.addOnTriggerInput() + this.addOnExecutedOutput() + break + + case LGraphEventMode.NEVER: + break + + case LGraphEventMode.ALWAYS: + break + + // @ts-expect-error Not impl. + 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 + * @param {*} param + * @param {*} options + */ + doExecute(param?: unknown, options?: { action_call?: any }): void { + options = options || {} + if (this.onExecute) { + // enable this to give the event an ID + 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?.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 + this.onAfterExecuteNode?.(param, options) // callback + } + + /** + * Triggers an action, wrapped by logics to control execution flow + * @param {String} action name + * @param {*} param + */ + actionDo(action: string, param: unknown, options: { action_call?: string }): void { + options = options || {} + if (this.onAction) { + // enable this to give the event an ID + 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?.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 + this.onAfterExecuteNode?.(param, options) + } + + /** + * Triggers an event in this node, this will trigger any output with the same name + * @param {String} event name ( "on_play", ... ) if action is equivalent to false then the event is send to all + * @param {*} param + */ + trigger(action: string, param: unknown, options: { action_call?: any }): void { + if (!this.outputs || !this.outputs.length) { + return } - /** - * checks if a point is inside a node slot, and returns info about which slot - * @param x - * @param y - * @returns if found the object contains { input|output: slot object, slot: number, link_pos: [x,y] } - */ - getSlotInPosition(x: number, y: number): IFoundSlot | null { - //search for inputs - const link_pos = new Float32Array(2) - if (this.inputs) { - for (let i = 0, l = this.inputs.length; i < l; ++i) { - const 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, slot: i, link_pos } - } - } - } + if (this.graph) this.graph._last_trigger_time = LiteGraph.getTime() - if (this.outputs) { - for (let i = 0, l = this.outputs.length; i < l; ++i) { - const 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, slot: i, link_pos } - } - } - } + for (let i = 0; i < this.outputs.length; ++i) { + const output = this.outputs[i] + if (!output || output.type !== LiteGraph.EVENT || (action && output.name != action)) continue + this.triggerSlot(i, param, null, options) + } + } - return null + /** + * Triggers a slot event in this node: cycle output slots and launch execute/action on connected nodes + * @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 + */ + triggerSlot(slot: number, param: unknown, link_id: number, options: { action_call?: any }): void { + options = options || {} + if (!this.outputs) return + + if (slot == null) { + console.error('slot must be a number') + return } - /** - * Returns the input slot with a given name (used for dynamic slots), -1 if not found - * @param name the name of the slot - * @param returnObj if the obj itself wanted - * @returns the slot (-1 if not found) - */ - findInputSlot(name: string, returnObj?: TReturn): number - findInputSlot(name: string, returnObj?: TReturn): INodeInputSlot - findInputSlot(name: string, returnObj: boolean = false) { - if (!this.inputs) return -1 + if (typeof slot !== 'number') console.warn("slot must be a number, use node.trigger('name') if you want to use a string") - for (let i = 0, l = this.inputs.length; i < l; ++i) { - if (name == this.inputs[i].name) { - return !returnObj ? i : this.inputs[i] - } - } - return -1 + const output = this.outputs[slot] + if (!output) return + + const links = output.links + if (!links || !links.length) return + + if (this.graph) this.graph._last_trigger_time = LiteGraph.getTime() + + //for every link attached here + for (let k = 0; k < links.length; ++k) { + const id = links[k] + //to skip links + if (link_id != null && link_id != id) continue + + const link_info = this.graph._links.get(id) + //not connected + if (!link_info) continue + + link_info._last_time = LiteGraph.getTime() + const node = this.graph.getNodeById(link_info.target_id) + //node not found? + if (!node) continue + + if (node.mode === LGraphEventMode.ON_TRIGGER) { + // generate unique trigger ID if not present + if (!options.action_call) options.action_call = this.id + '_trigg_' + Math.floor(Math.random() * 9999) + // -- 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 + const 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 + * @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 + */ + clearTriggeredSlot(slot: number, link_id: number): void { + if (!this.outputs) return + + const output = this.outputs[slot] + if (!output) return + + const links = output.links + if (!links || !links.length) return + + //for every link attached here + for (let k = 0; k < links.length; ++k) { + const id = links[k] + //to skip links + if (link_id != null && link_id != id) continue + + const link_info = this.graph._links.get(id) + //not connected + if (!link_info) continue + + link_info._last_time = 0 + } + } + + /** + * changes node size and triggers callback + * @param {vec2} size + */ + setSize(size: Size): void { + this.size = size + this.onResize?.(this.size) + } + + /** + * add a new property to this node + * @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) + */ + addProperty(name: string, default_value: unknown, type?: string, extra_info?: Dictionary): INodePropertyInfo { + const o: INodePropertyInfo = { name: name, type: type, default_value: default_value } + if (extra_info) { + for (const i in extra_info) { + o[i] = extra_info[i] + } + } + this.properties_info ||= [] + this.properties_info.push(o) + this.properties ||= {} + this.properties[name] = default_value + return o + } + + /** + * add a new output slot to use in this node + * @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) + */ + addOutput(name?: string, type?: ISlotType, extra_info?: object): INodeOutputSlot { + const output = { name: name, type: type, links: null } + if (extra_info) { + for (const i in extra_info) { + output[i] = extra_info[i] + } } - /** - * returns the output slot with a given name (used for dynamic slots), -1 if not found - * @param {string} name the name of the slot - * @param {boolean} returnObj if the obj itself wanted - * @return {number | INodeOutputSlot} the slot (-1 if not found) - */ - findOutputSlot(name: string, returnObj?: TReturn): number - findOutputSlot(name: string, returnObj?: TReturn): INodeOutputSlot - findOutputSlot(name: string, returnObj: boolean = false) { - if (!this.outputs) return -1 + this.outputs ||= [] + this.outputs.push(output) + this.onOutputAdded?.(output) - for (let i = 0, l = this.outputs.length; i < l; ++i) { - if (name == this.outputs[i].name) { - return !returnObj ? i : this.outputs[i] - } + 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 + * @param {Array} array of triplets like [[name,type,extra_info],[...]] + */ + addOutputs(array: [string, ISlotType, Record][]): void { + for (let i = 0; i < array.length; ++i) { + const info = array[i] + const o = { name: info[0], type: info[1], links: null } + if (array[2]) { + for (const j in info[2]) { + o[j] = info[2][j] } - return -1 + } + + this.outputs ||= [] + this.outputs.push(o) + this.onOutputAdded?.(o) + + if (LiteGraph.auto_load_slot_types) LiteGraph.registerNodeAndSlotType(this, info[1], true) } - /** - * Finds the first free input slot. - * @param {object} optsIn - * @return The index of the first matching slot, the slot itself if returnObj is true, or -1 if not found. - */ - findInputSlotFree(optsIn?: FindFreeSlotOptions & { returnObj?: TReturn }): number - findInputSlotFree(optsIn?: FindFreeSlotOptions & { returnObj?: TReturn }): INodeInputSlot - findInputSlotFree(optsIn?: FindFreeSlotOptions) { - return this.#findFreeSlot(this.inputs, optsIn) + this.setSize(this.computeSize()) + this.setDirtyCanvas(true, true) + } + + /** + * remove an existing output slot + * @param {number} slot + */ + removeOutput(slot: number): void { + this.disconnectOutput(slot) + this.outputs.splice(slot, 1) + for (let i = slot; i < this.outputs.length; ++i) { + if (!this.outputs[i] || !this.outputs[i].links) continue + const links = this.outputs[i].links + for (let j = 0; j < links.length; ++j) { + const link = this.graph._links.get(links[j]) + if (!link) continue + + link.origin_slot -= 1 + } } - /** - * Finds the first free output slot. - * @param {object} optsIn - * @return The index of the first matching slot, the slot itself if returnObj is true, or -1 if not found. - */ - findOutputSlotFree(optsIn?: FindFreeSlotOptions & { returnObj?: TReturn }): number - findOutputSlotFree(optsIn?: FindFreeSlotOptions & { returnObj?: TReturn }): INodeOutputSlot - findOutputSlotFree(optsIn?: FindFreeSlotOptions) { - return this.#findFreeSlot(this.outputs, optsIn) + this.setSize(this.computeSize()) + this.onOutputRemoved?.(slot) + this.setDirtyCanvas(true, true) + } + + /** + * add a new input slot to use in this node + * @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) + */ + addInput(name: string, type: ISlotType, extra_info?: object): INodeInputSlot { + type = type || 0 + const input: INodeInputSlot = { name: name, type: type, link: null } + if (extra_info) { + for (const i in extra_info) { + input[i] = extra_info[i] + } } - /** - * Finds the next free slot - * @param slots The slots to search, i.e. this.inputs or this.outputs - * @param options Options - */ - #findFreeSlot(slots: TSlot[], options?: FindFreeSlotOptions): TSlot | number { - const defaults = { - returnObj: false, - typesNotAccepted: [] - } - const opts = Object.assign(defaults, options || {}) - const length = slots?.length - if (!(length > 0)) return -1 + this.inputs ||= [] + this.inputs.push(input) + this.setSize(this.computeSize()) - for (let i = 0; i < length; ++i) { - const slot: TSlot & IGenericLinkOrLinks = slots[i] - if (!slot || slot.link || slot.links?.length) continue - if (opts.typesNotAccepted?.includes?.(slot.type)) continue - return !opts.returnObj ? i : slot + this.onInputAdded?.(input) + LiteGraph.registerNodeAndSlotType(this, type) + + this.setDirtyCanvas(true, true) + return input + } + + /** + * add several new input slots in this node + * @param {Array} array of triplets like [[name,type,extra_info],[...]] + */ + addInputs(array: [string, ISlotType, Record][]): void { + for (let i = 0; i < array.length; ++i) { + const info = array[i] + const o: INodeInputSlot = { name: info[0], type: info[1], link: null } + // TODO: Checking the wrong variable here - confirm no downstream consumers, then remove. + if (array[2]) { + for (const j in info[2]) { + o[j] = info[2][j] } - return -1 + } + + this.inputs ||= [] + this.inputs.push(o) + this.onInputAdded?.(o) + + LiteGraph.registerNodeAndSlotType(this, info[1]) } - /** - * findSlotByType for INPUTS - */ - findInputSlotByType(type: ISlotType, returnObj?: TReturn, preferFreeSlot?: boolean, doNotUseOccupied?: boolean): number - findInputSlotByType(type: ISlotType, returnObj?: TReturn, preferFreeSlot?: boolean, doNotUseOccupied?: boolean): INodeInputSlot - findInputSlotByType(type: ISlotType, returnObj?: boolean, preferFreeSlot?: boolean, doNotUseOccupied?: boolean) { - return this.#findSlotByType(this.inputs, type, returnObj, preferFreeSlot, doNotUseOccupied) + this.setSize(this.computeSize()) + this.setDirtyCanvas(true, true) + } + + /** + * remove an existing input slot + * @param {number} slot + */ + removeInput(slot: number): void { + this.disconnectInput(slot) + const slot_info = this.inputs.splice(slot, 1) + for (let i = slot; i < this.inputs.length; ++i) { + if (!this.inputs[i]) continue + + const link = this.graph._links.get(this.inputs[i].link) + if (!link) continue + + link.target_slot -= 1 + } + this.setSize(this.computeSize()) + this.onInputRemoved?.(slot, slot_info[0]) + this.setDirtyCanvas(true, true) + } + + /** + * add an special connection to this node (used for special kinds of graphs) + * @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 + */ + addConnection(name: string, type: string, pos: Point, direction: string) { + const 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 + * @param out + * @return the total size + */ + computeSize(out?: Size): Size { + const ctorSize = this.constructor.size + if (ctorSize) return [ctorSize[0], ctorSize[1]] + + let rows = Math.max(this.inputs ? this.inputs.length : 1, this.outputs ? this.outputs.length : 1) + const size = out || new Float32Array([0, 0]) + rows = Math.max(rows, 1) + const font_size = LiteGraph.NODE_TEXT_SIZE //although it should be graphcanvas.inner_text_font size + + const title_width = compute_text_size(this.title) + let input_width = 0 + let output_width = 0 + + if (this.inputs) { + for (let i = 0, l = this.inputs.length; i < l; ++i) { + const input = this.inputs[i] + const text = input.label || input.name || '' + const text_width = compute_text_size(text) + if (input_width < text_width) input_width = text_width + } } - /** - * findSlotByType for OUTPUTS - */ - findOutputSlotByType(type: ISlotType, returnObj?: TReturn, preferFreeSlot?: boolean, doNotUseOccupied?: boolean): number - findOutputSlotByType(type: ISlotType, returnObj?: TReturn, preferFreeSlot?: boolean, doNotUseOccupied?: boolean): INodeOutputSlot - findOutputSlotByType(type: ISlotType, returnObj?: boolean, preferFreeSlot?: boolean, doNotUseOccupied?: boolean) { - return this.#findSlotByType(this.outputs, type, returnObj, preferFreeSlot, doNotUseOccupied) + if (this.outputs) { + for (let i = 0, l = this.outputs.length; i < l; ++i) { + const output = this.outputs[i] + const text = output.label || output.name || '' + const text_width = compute_text_size(text) + if (output_width < text_width) output_width = text_width + } } - /** - * returns the output (or input) slot with a given type, -1 if not found - * @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) - */ - findSlotByType(input: TSlot, type: ISlotType, returnObj?: TReturn, preferFreeSlot?: boolean, doNotUseOccupied?: boolean): number - findSlotByType(input: TSlot, type: ISlotType, returnObj?: TReturn, preferFreeSlot?: boolean, doNotUseOccupied?: boolean): INodeInputSlot - findSlotByType(input: TSlot, type: ISlotType, returnObj?: TReturn, preferFreeSlot?: boolean, doNotUseOccupied?: boolean): INodeOutputSlot - findSlotByType(input: boolean, type: ISlotType, returnObj?: boolean, preferFreeSlot?: boolean, doNotUseOccupied?: boolean) { - return input - ? this.#findSlotByType(this.inputs, type, returnObj, preferFreeSlot, doNotUseOccupied) - : this.#findSlotByType(this.outputs, type, returnObj, preferFreeSlot, doNotUseOccupied) + size[0] = Math.max(input_width + output_width + 10, title_width) + size[0] = Math.max(size[0], LiteGraph.NODE_WIDTH) + if (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 + + let widgets_height = 0 + if (this.widgets?.length) { + for (let i = 0, l = this.widgets.length; i < l; ++i) { + const widget = this.widgets[i] + if (widget.hidden || (widget.advanced && !this.showAdvanced)) continue + + widgets_height += widget.computeSize ? widget.computeSize(size[0])[1] + 4 : LiteGraph.NODE_WIDGET_HEIGHT + 4 + } + widgets_height += 8 } - /** - * Finds a matching slot from those provided, returning the slot itself or its index in {@link slots}. - * @param slots Slots to search (this.inputs or this.outputs) - * @param type Type of slot to look for - * @param returnObj If true, returns the slot itself. Otherwise, the index. - * @param preferFreeSlot Prefer a free slot, but if none are found, fall back to an occupied slot. - * @param doNotUseOccupied Do not fall back to occupied slots. - * @see {findSlotByType} - * @see {findOutputSlotByType} - * @see {findInputSlotByType} - * @returns If a match is found, the slot if returnObj is true, otherwise the index. If no matches are found, -1 - */ - #findSlotByType(slots: TSlot[], type: ISlotType, returnObj?: boolean, preferFreeSlot?: boolean, doNotUseOccupied?: boolean): TSlot | number { - const length = slots?.length - if (!length) return -1 + //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 - // !! empty string type is considered 0, * !! - if (type == "" || type == "*") type = 0 - const sourceTypes = String(type).toLowerCase().split(",") - - // Run the search - let occupiedSlot: number | TSlot | null = null - for (let i = 0; i < length; ++i) { - const slot: TSlot & IGenericLinkOrLinks = slots[i] - const destTypes = slot.type == "0" || slot.type == "*" - ? ["0"] - : String(slot.type).toLowerCase().split(",") - - for (const sourceType of sourceTypes) { - // TODO: Remove _event_ entirely. - const source = sourceType == "_event_" ? LiteGraph.EVENT : sourceType - - for (const destType of destTypes) { - const dest = destType == "_event_" ? LiteGraph.EVENT : destType - - if (source == dest || source === "*" || dest === "*") { - if (preferFreeSlot && (slot.links?.length || slot.link != null)) { - // In case we can't find a free slot. - occupiedSlot ??= returnObj ? slot : i - continue - } - return returnObj ? slot : i - } - } - } - } - - return doNotUseOccupied ? -1 : occupiedSlot ?? -1 + function compute_text_size(text: string) { + return text ? font_size * text.length * 0.6 : 0 } - /** - * Determines the slot index to connect to when attempting to connect by type. - * - * @param findInputs If true, searches for an input. Otherwise, an output. - * @param node The node at the other end of the connection. - * @param slotType The type of slot at the other end of the connection. - * @param options Search restrictions to adhere to. - * @see {connectByType} - * @see {connectByTypeOutput} - */ - findConnectByTypeSlot( - findInputs: boolean, - node: LGraphNode, - slotType: ISlotType, - options?: ConnectByTypeOptions - ): number | null { - // LEGACY: Old options names - if (options && typeof options === "object") { - if ("firstFreeIfInputGeneralInCase" in options) options.wildcardToTyped = !!options.firstFreeIfInputGeneralInCase - if ("firstFreeIfOutputGeneralInCase" in options) options.wildcardToTyped = !!options.firstFreeIfOutputGeneralInCase - if ("generalTypeInCase" in options) options.typedToWildcard = !!options.generalTypeInCase - } - const optsDef: ConnectByTypeOptions = { - createEventInCase: true, - wildcardToTyped: true, - typedToWildcard: true - } - const opts = Object.assign(optsDef, options) - - if (node && typeof node === "number") { - node = this.graph.getNodeById(node) - } - const slot = node.findSlotByType(findInputs, slotType, false, true) - if (slot >= 0 && slot !== null) return slot - - // TODO: Remove or reimpl. events. WILL CREATE THE onTrigger IN SLOT - if (opts.createEventInCase && slotType == LiteGraph.EVENT) { - if (findInputs) return -1 - if (LiteGraph.do_add_triggers_slots) return node.addOnExecutedOutput() - } - - // connect to the first general output slot if not found a specific type and - if (opts.typedToWildcard) { - const generalSlot = node.findSlotByType(findInputs, 0, false, true, true) - if (generalSlot >= 0) return generalSlot - } - // connect to the first free input slot if not found a specific type and this output is general - if (opts.wildcardToTyped && (slotType == 0 || slotType == "*" || slotType == "")) { - const opt = { typesNotAccepted: [LiteGraph.EVENT] } - const nonEventSlot = findInputs - ? node.findInputSlotFree(opt) - : node.findOutputSlotFree(opt) - if (nonEventSlot >= 0) return nonEventSlot - } - return null + if (this.constructor.min_height && size[1] < this.constructor.min_height) { + size[1] = this.constructor.min_height } - /** - * connect this node output to the input of another node BY TYPE - * @param {number} slot (could be the number of the slot or the string with the name of the slot) - * @param {LGraphNode} target_node the target node - * @param {string} target_slotType the input slot type of the target node - * @return {Object} the link_info is created, otherwise null - */ - connectByType(slot: number | string, target_node: LGraphNode, target_slotType: ISlotType, optsIn?: ConnectByTypeOptions): LLink | null { - const slotIndex = this.findConnectByTypeSlot(true, target_node, target_slotType, optsIn) - if (slotIndex !== null) return this.connect(slot, target_node, slotIndex) + //margin + size[1] += 6 - console.debug("[connectByType]: no way to connect type: ", target_slotType, " to node: ", target_node) - return null + return size + } + + inResizeCorner(canvasX: number, canvasY: number): boolean { + const rows = this.outputs ? this.outputs.length : 1 + const 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. + * + * @param {String} property name of the property + * @return {Object} the object with all the available info + */ + getPropertyInfo(property: string) { + let info = null + + //there are several ways to define info about a property + //legacy mode + if (this.properties_info) { + for (let 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?.[property]) info = this.constructor.widgets_info[property] + + //litescene mode using the constructor + if (!info && this.onGetPropertyInfo) { + info = this.onGetPropertyInfo(property) } - /** - * connect this node input to the output of another node BY TYPE - * @method connectByType - * @param {number | string} slot (could be the number of the slot or the string with the name of the slot) - * @param {LGraphNode} source_node the target node - * @param {string} source_slotType the output slot type of the target node - * @return {Object} the link_info is created, otherwise null - */ - connectByTypeOutput(slot: number | string, source_node: LGraphNode, source_slotType: ISlotType, optsIn?: ConnectByTypeOptions): LLink | null { - // LEGACY: Old options names - if (typeof optsIn === "object") { - if ("firstFreeIfInputGeneralInCase" in optsIn) optsIn.wildcardToTyped = !!optsIn.firstFreeIfInputGeneralInCase - if ("generalTypeInCase" in optsIn) optsIn.typedToWildcard = !!optsIn.generalTypeInCase - } - const slotIndex = this.findConnectByTypeSlot(false, source_node, source_slotType, optsIn) - if (slotIndex !== null) return source_node.connect(slotIndex, this, slot) + info ||= {} + info.type ||= typeof this.properties[property] + if (info.widget == 'combo') info.type = 'enum' - console.debug("[connectByType]: no way to connect type: ", source_slotType, " to node: ", source_node) - return null + return info + } + + /** + * Defines a widget inside the node, it will be rendered on top of the node, you can control lots of properties + * + * @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 + */ + addWidget(type: string, name: string, value: any, callback: IWidget['callback'], options?: any): IWidget { + this.widgets ||= [] + + if (!options && callback && typeof callback === 'object') { + options = callback + callback = null } - /** - * Connect an output of this node to an input of another node - * @param {number | 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 - * @param {number | 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 - */ - connect(slot: number | string, target_node: LGraphNode, target_slot: ISlotType): LLink | null { - // Allow legacy API support for searching target_slot by string, without mutating the input variables - let targetIndex: number + //options can be the property name + if (options && typeof options === 'string') options = { property: options } - const graph = this.graph - if (!graph) { - //could be connected before adding it to a graph - //due to link ids being associated with graphs - console.log("Connect: Error, node doesn't belong to any graph. Nodes must be added first to a graph before connecting them.") - return null - } + //callback can be the property name + if (callback && typeof callback === 'string') { + options ||= {} + options.property = callback + callback = null + } - //seek for the output slot - if (typeof slot === "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 (callback && typeof callback !== 'function') { + console.warn('addWidget: callback must be a function') + callback = null + } - if (target_node && typeof target_node === "number") { - target_node = graph.getNodeById(target_node) - } - if (!target_node) throw "target node is null" + const w: IWidget = { + // @ts-expect-error Type check or just assert? + type: type.toLowerCase(), + name: name, + value: value, + callback: callback, + options: options || {}, + } - //avoid loopback - if (target_node == this) return null + if (w.options.y !== undefined) { + w.y = w.options.y + } - //you can specify the slot by name - if (typeof target_slot === "string") { - targetIndex = target_node.findInputSlot(target_slot) - if (targetIndex == -1) { - if (LiteGraph.debug) console.log("Connect: Error, no slot of name " + targetIndex) - return null - } - } else if (target_slot === LiteGraph.EVENT) { - // TODO: Events - if (LiteGraph.do_add_triggers_slots) { - target_node.changeMode(LGraphEventMode.ON_TRIGGER) - targetIndex = target_node.findInputSlot("onTrigger") - } else { - return null - } - } else if (typeof target_slot === "number") { - targetIndex = target_slot - } else { - targetIndex = 0 - } + 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 + } - // Allow target node to change slot - if (target_node.onBeforeConnectInput) { - // This way node can choose another slot (or make a new one?) - const requestedIndex: false | number | null = target_node.onBeforeConnectInput(targetIndex, target_slot) - targetIndex = typeof requestedIndex === "number" ? requestedIndex : null - } + addCustomWidget(custom_widget: IWidget): IWidget { + this.widgets ||= [] + this.widgets.push(custom_widget) + return custom_widget + } - if (targetIndex === null || !target_node.inputs || targetIndex >= target_node.inputs.length) { - if (LiteGraph.debug) console.log("Connect: Error, slot number not found") - return null - } + /** + * Measures the node for rendering, populating {@link out} with the results in graph space. + * @param out Results (x, y, width, height) are inserted into this array. + * @param pad Expands the area by this amount on each side. Default: 0 + */ + measure(out: Rect, pad = 0): void { + const titleMode = this.constructor.title_mode + const renderTitle = titleMode != TitleMode.TRANSPARENT_TITLE && titleMode != TitleMode.NO_TITLE + const titleHeight = renderTitle ? LiteGraph.NODE_TITLE_HEIGHT : 0 - let changed = false + out[0] = this.pos[0] - pad + out[1] = this.pos[1] + -titleHeight - pad + if (!this.flags?.collapsed) { + out[2] = this.size[0] + 2 * pad + out[3] = this.size[1] + titleHeight + 2 * pad + } else { + out[2] = (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) + 2 * pad + out[3] = LiteGraph.NODE_TITLE_HEIGHT + 2 * pad + } + } - const input = target_node.inputs[targetIndex] - let link_info: LLink = null - const output = this.outputs[slot] + /** + * returns the bounding of the object, used for rendering purposes + * @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] + */ + getBounding(out?: Float32Array, compute_outer?: boolean): Float32Array { + out = out || new Float32Array(4) + this.measure(out) + if (compute_outer) { + // 4 offset for collapsed node connection points + out[0] -= 4 + out[1] -= 4 + // Add shadow & left offset + out[2] += 6 + 4 + // Add shadow & top offsets + out[3] += 5 + 4 + } + this.onBounding?.(out) + return out + } - if (!this.outputs[slot]) return null + /** + * checks if a point is inside the shape of a node + * @param {number} x + * @param {number} y + * @return {boolean} + */ + isPointInside(x: number, y: number, margin?: number, skip_title?: boolean): boolean { + margin ||= 0 - //check targetSlot and check connection types - if (!LiteGraph.isValidConnection(output.type, input.type)) { - this.setDirtyCanvas(false, true) - // @ts-expect-error Unused param - if (changed) graph.connectionChange(this, link_info) - return null - } + const margin_top = skip_title || this.graph?.isLive() ? 0 : LiteGraph.NODE_TITLE_HEIGHT - // Allow nodes to block connection - if (target_node.onConnectInput?.(targetIndex, output.type, output, this, slot) === false) - return null - if (this.onConnectOutput?.(slot, input.type, input, target_node, targetIndex) === false) - return null - - //if there is something already plugged there, disconnect - if (target_node.inputs[targetIndex]?.link != null) { - graph.beforeChange() - target_node.disconnectInput(targetIndex) - changed = true - } - if (output.links?.length) { - if (output.type === LiteGraph.EVENT && !LiteGraph.allow_multi_output_for_events) { - graph.beforeChange() - // @ts-expect-error Unused param - this.disconnectOutput(slot, false, { doProcessChange: false }) - changed = true - } - } - - const nextId = LiteGraph.use_uuids - ? LiteGraph.uuidv4() - : ++graph.last_link_id - - //create link class - link_info = new LLink( - nextId, - input.type || output.type, - this.id, - slot, - target_node.id, - targetIndex + if (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 + } - //add to graph links list - graph._links.set(link_info.id, link_info) + /** + * checks if a point is inside a node slot, and returns info about which slot + * @param x + * @param y + * @returns if found the object contains { input|output: slot object, slot: number, link_pos: [x,y] } + */ + getSlotInPosition(x: number, y: number): IFoundSlot | null { + //search for inputs + const link_pos = new Float32Array(2) + if (this.inputs) { + for (let i = 0, l = this.inputs.length; i < l; ++i) { + const 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, slot: i, link_pos } + } + } + } - //connect in output - output.links ??= [] - output.links.push(link_info.id) - //connect in input - target_node.inputs[targetIndex].link = link_info.id + if (this.outputs) { + for (let i = 0, l = this.outputs.length; i < l; ++i) { + const 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, slot: i, link_pos } + } + } + } + + return null + } + + /** + * Returns the input slot with a given name (used for dynamic slots), -1 if not found + * @param name the name of the slot + * @param returnObj if the obj itself wanted + * @returns the slot (-1 if not found) + */ + findInputSlot(name: string, returnObj?: TReturn): number + findInputSlot(name: string, returnObj?: TReturn): INodeInputSlot + findInputSlot(name: string, returnObj: boolean = false) { + if (!this.inputs) return -1 + + for (let 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 + * @param {string} name the name of the slot + * @param {boolean} returnObj if the obj itself wanted + * @return {number | INodeOutputSlot} the slot (-1 if not found) + */ + findOutputSlot(name: string, returnObj?: TReturn): number + findOutputSlot(name: string, returnObj?: TReturn): INodeOutputSlot + findOutputSlot(name: string, returnObj: boolean = false) { + if (!this.outputs) return -1 + + for (let i = 0, l = this.outputs.length; i < l; ++i) { + if (name == this.outputs[i].name) { + return !returnObj ? i : this.outputs[i] + } + } + return -1 + } + + /** + * Finds the first free input slot. + * @param {object} optsIn + * @return The index of the first matching slot, the slot itself if returnObj is true, or -1 if not found. + */ + findInputSlotFree(optsIn?: FindFreeSlotOptions & { returnObj?: TReturn }): number + findInputSlotFree(optsIn?: FindFreeSlotOptions & { returnObj?: TReturn }): INodeInputSlot + findInputSlotFree(optsIn?: FindFreeSlotOptions) { + return this.#findFreeSlot(this.inputs, optsIn) + } + + /** + * Finds the first free output slot. + * @param {object} optsIn + * @return The index of the first matching slot, the slot itself if returnObj is true, or -1 if not found. + */ + findOutputSlotFree(optsIn?: FindFreeSlotOptions & { returnObj?: TReturn }): number + findOutputSlotFree(optsIn?: FindFreeSlotOptions & { returnObj?: TReturn }): INodeOutputSlot + findOutputSlotFree(optsIn?: FindFreeSlotOptions) { + return this.#findFreeSlot(this.outputs, optsIn) + } + + /** + * Finds the next free slot + * @param slots The slots to search, i.e. this.inputs or this.outputs + * @param options Options + */ + #findFreeSlot(slots: TSlot[], options?: FindFreeSlotOptions): TSlot | number { + const defaults = { + returnObj: false, + typesNotAccepted: [], + } + const opts = Object.assign(defaults, options || {}) + const length = slots?.length + if (!(length > 0)) return -1 + + for (let i = 0; i < length; ++i) { + const slot: TSlot & IGenericLinkOrLinks = slots[i] + if (!slot || slot.link || slot.links?.length) continue + if (opts.typesNotAccepted?.includes?.(slot.type)) continue + return !opts.returnObj ? i : slot + } + return -1 + } + + /** + * findSlotByType for INPUTS + */ + findInputSlotByType(type: ISlotType, returnObj?: TReturn, preferFreeSlot?: boolean, doNotUseOccupied?: boolean): number + findInputSlotByType( + type: ISlotType, + returnObj?: TReturn, + preferFreeSlot?: boolean, + doNotUseOccupied?: boolean, + ): INodeInputSlot + findInputSlotByType(type: ISlotType, returnObj?: boolean, preferFreeSlot?: boolean, doNotUseOccupied?: boolean) { + return this.#findSlotByType(this.inputs, type, returnObj, preferFreeSlot, doNotUseOccupied) + } + + /** + * findSlotByType for OUTPUTS + */ + findOutputSlotByType(type: ISlotType, returnObj?: TReturn, preferFreeSlot?: boolean, doNotUseOccupied?: boolean): number + findOutputSlotByType( + type: ISlotType, + returnObj?: TReturn, + preferFreeSlot?: boolean, + doNotUseOccupied?: boolean, + ): INodeOutputSlot + findOutputSlotByType(type: ISlotType, returnObj?: boolean, preferFreeSlot?: boolean, doNotUseOccupied?: boolean) { + return this.#findSlotByType(this.outputs, type, returnObj, preferFreeSlot, doNotUseOccupied) + } + + /** + * returns the output (or input) slot with a given type, -1 if not found + * @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) + */ + findSlotByType( + input: TSlot, + type: ISlotType, + returnObj?: TReturn, + preferFreeSlot?: boolean, + doNotUseOccupied?: boolean, + ): number + findSlotByType( + input: TSlot, + type: ISlotType, + returnObj?: TReturn, + preferFreeSlot?: boolean, + doNotUseOccupied?: boolean, + ): INodeInputSlot + findSlotByType( + input: TSlot, + type: ISlotType, + returnObj?: TReturn, + preferFreeSlot?: boolean, + doNotUseOccupied?: boolean, + ): INodeOutputSlot + findSlotByType(input: boolean, type: ISlotType, returnObj?: boolean, preferFreeSlot?: boolean, doNotUseOccupied?: boolean) { + return input + ? this.#findSlotByType(this.inputs, type, returnObj, preferFreeSlot, doNotUseOccupied) + : this.#findSlotByType(this.outputs, type, returnObj, preferFreeSlot, doNotUseOccupied) + } + + /** + * Finds a matching slot from those provided, returning the slot itself or its index in {@link slots}. + * @param slots Slots to search (this.inputs or this.outputs) + * @param type Type of slot to look for + * @param returnObj If true, returns the slot itself. Otherwise, the index. + * @param preferFreeSlot Prefer a free slot, but if none are found, fall back to an occupied slot. + * @param doNotUseOccupied Do not fall back to occupied slots. + * @see {findSlotByType} + * @see {findOutputSlotByType} + * @see {findInputSlotByType} + * @returns If a match is found, the slot if returnObj is true, otherwise the index. If no matches are found, -1 + */ + #findSlotByType( + slots: TSlot[], + type: ISlotType, + returnObj?: boolean, + preferFreeSlot?: boolean, + doNotUseOccupied?: boolean, + ): TSlot | number { + const length = slots?.length + if (!length) return -1 + + // !! empty string type is considered 0, * !! + if (type == '' || type == '*') type = 0 + const sourceTypes = String(type).toLowerCase().split(',') + + // Run the search + let occupiedSlot: number | TSlot | null = null + for (let i = 0; i < length; ++i) { + const slot: TSlot & IGenericLinkOrLinks = slots[i] + const destTypes = slot.type == '0' || slot.type == '*' ? ['0'] : String(slot.type).toLowerCase().split(',') + + for (const sourceType of sourceTypes) { + // TODO: Remove _event_ entirely. + const source = sourceType == '_event_' ? LiteGraph.EVENT : sourceType + + for (const destType of destTypes) { + const dest = destType == '_event_' ? LiteGraph.EVENT : destType + + if (source == dest || source === '*' || dest === '*') { + if (preferFreeSlot && (slot.links?.length || slot.link != null)) { + // In case we can't find a free slot. + occupiedSlot ??= returnObj ? slot : i + continue + } + return returnObj ? slot : i + } + } + } + } + + return doNotUseOccupied ? -1 : (occupiedSlot ?? -1) + } + + /** + * Determines the slot index to connect to when attempting to connect by type. + * + * @param findInputs If true, searches for an input. Otherwise, an output. + * @param node The node at the other end of the connection. + * @param slotType The type of slot at the other end of the connection. + * @param options Search restrictions to adhere to. + * @see {connectByType} + * @see {connectByTypeOutput} + */ + findConnectByTypeSlot(findInputs: boolean, node: LGraphNode, slotType: ISlotType, options?: ConnectByTypeOptions): number | null { + // LEGACY: Old options names + if (options && typeof options === 'object') { + if ('firstFreeIfInputGeneralInCase' in options) options.wildcardToTyped = !!options.firstFreeIfInputGeneralInCase + if ('firstFreeIfOutputGeneralInCase' in options) options.wildcardToTyped = !!options.firstFreeIfOutputGeneralInCase + if ('generalTypeInCase' in options) options.typedToWildcard = !!options.generalTypeInCase + } + const optsDef: ConnectByTypeOptions = { + createEventInCase: true, + wildcardToTyped: true, + typedToWildcard: true, + } + const opts = Object.assign(optsDef, options) + + if (node && typeof node === 'number') { + node = this.graph.getNodeById(node) + } + const slot = node.findSlotByType(findInputs, slotType, false, true) + if (slot >= 0 && slot !== null) return slot + + // TODO: Remove or reimpl. events. WILL CREATE THE onTrigger IN SLOT + if (opts.createEventInCase && slotType == LiteGraph.EVENT) { + if (findInputs) return -1 + if (LiteGraph.do_add_triggers_slots) return node.addOnExecutedOutput() + } + + // connect to the first general output slot if not found a specific type and + if (opts.typedToWildcard) { + const generalSlot = node.findSlotByType(findInputs, 0, false, true, true) + if (generalSlot >= 0) return generalSlot + } + // connect to the first free input slot if not found a specific type and this output is general + if (opts.wildcardToTyped && (slotType == 0 || slotType == '*' || slotType == '')) { + const opt = { typesNotAccepted: [LiteGraph.EVENT] } + const nonEventSlot = findInputs ? node.findInputSlotFree(opt) : node.findOutputSlotFree(opt) + if (nonEventSlot >= 0) return nonEventSlot + } + return null + } + + /** + * connect this node output to the input of another node BY TYPE + * @param {number} slot (could be the number of the slot or the string with the name of the slot) + * @param {LGraphNode} target_node the target node + * @param {string} target_slotType the input slot type of the target node + * @return {Object} the link_info is created, otherwise null + */ + connectByType(slot: number | string, target_node: LGraphNode, target_slotType: ISlotType, optsIn?: ConnectByTypeOptions): LLink | null { + const slotIndex = this.findConnectByTypeSlot(true, target_node, target_slotType, optsIn) + if (slotIndex !== null) return this.connect(slot, target_node, slotIndex) + + console.debug('[connectByType]: no way to connect type: ', target_slotType, ' to node: ', target_node) + return null + } + + /** + * connect this node input to the output of another node BY TYPE + * @method connectByType + * @param {number | string} slot (could be the number of the slot or the string with the name of the slot) + * @param {LGraphNode} source_node the target node + * @param {string} source_slotType the output slot type of the target node + * @return {Object} the link_info is created, otherwise null + */ + connectByTypeOutput(slot: number | string, source_node: LGraphNode, source_slotType: ISlotType, optsIn?: ConnectByTypeOptions): LLink | null { + // LEGACY: Old options names + if (typeof optsIn === 'object') { + if ('firstFreeIfInputGeneralInCase' in optsIn) optsIn.wildcardToTyped = !!optsIn.firstFreeIfInputGeneralInCase + if ('generalTypeInCase' in optsIn) optsIn.typedToWildcard = !!optsIn.generalTypeInCase + } + const slotIndex = this.findConnectByTypeSlot(false, source_node, source_slotType, optsIn) + if (slotIndex !== null) return source_node.connect(slotIndex, this, slot) + + console.debug('[connectByType]: no way to connect type: ', source_slotType, ' to node: ', source_node) + return null + } + + /** + * Connect an output of this node to an input of another node + * @param {number | 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 + * @param {number | 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 + */ + connect(slot: number | string, target_node: LGraphNode, target_slot: ISlotType): LLink | null { + // Allow legacy API support for searching target_slot by string, without mutating the input variables + let targetIndex: number + + const graph = this.graph + if (!graph) { + //could be connected before adding it to a graph + //due to link ids being associated with graphs + console.log("Connect: Error, node doesn't belong to any graph. Nodes must be added first to a graph before connecting them.") + return null + } + + //seek for the output slot + if (typeof slot === '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 && typeof target_node === 'number') { + target_node = 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 (typeof target_slot === 'string') { + targetIndex = target_node.findInputSlot(target_slot) + if (targetIndex == -1) { + if (LiteGraph.debug) console.log('Connect: Error, no slot of name ' + targetIndex) + return null + } + } else if (target_slot === LiteGraph.EVENT) { + // TODO: Events + if (LiteGraph.do_add_triggers_slots) { + target_node.changeMode(LGraphEventMode.ON_TRIGGER) + targetIndex = target_node.findInputSlot('onTrigger') + } else { + return null + } + } else if (typeof target_slot === 'number') { + targetIndex = target_slot + } else { + targetIndex = 0 + } + + // Allow target node to change slot + if (target_node.onBeforeConnectInput) { + // This way node can choose another slot (or make a new one?) + const requestedIndex: false | number | null = target_node.onBeforeConnectInput(targetIndex, target_slot) + targetIndex = typeof requestedIndex === 'number' ? requestedIndex : null + } + + if (targetIndex === null || !target_node.inputs || targetIndex >= target_node.inputs.length) { + if (LiteGraph.debug) console.log('Connect: Error, slot number not found') + return null + } + + let changed = false + + const input = target_node.inputs[targetIndex] + let link_info: LLink = null + const output = this.outputs[slot] + + if (!this.outputs[slot]) return null + + //check targetSlot and check connection types + if (!LiteGraph.isValidConnection(output.type, input.type)) { + this.setDirtyCanvas(false, true) + // @ts-expect-error Unused param + if (changed) graph.connectionChange(this, link_info) + return null + } + + // Allow nodes to block connection + if (target_node.onConnectInput?.(targetIndex, output.type, output, this, slot) === false) return null + if (this.onConnectOutput?.(slot, input.type, input, target_node, targetIndex) === false) return null + + //if there is something already plugged there, disconnect + if (target_node.inputs[targetIndex]?.link != null) { + graph.beforeChange() + target_node.disconnectInput(targetIndex) + changed = true + } + if (output.links?.length) { + if (output.type === LiteGraph.EVENT && !LiteGraph.allow_multi_output_for_events) { + graph.beforeChange() + // @ts-expect-error Unused param + this.disconnectOutput(slot, false, { doProcessChange: false }) + changed = true + } + } + + const nextId = LiteGraph.use_uuids ? LiteGraph.uuidv4() : ++graph.last_link_id + + //create link class + link_info = new LLink(nextId, input.type || output.type, this.id, slot, target_node.id, targetIndex) + + //add to graph links list + graph._links.set(link_info.id, link_info) + + //connect in output + output.links ??= [] + output.links.push(link_info.id) + //connect in input + target_node.inputs[targetIndex].link = link_info.id + graph._version++ + + //link_info has been created now, so its updated + this.onConnectionsChange?.(NodeSlotType.OUTPUT, slot, true, link_info, output) + + target_node.onConnectionsChange?.(NodeSlotType.INPUT, targetIndex, true, link_info, input) + graph.onNodeConnectionChange?.(NodeSlotType.INPUT, target_node, targetIndex, this, slot) + graph.onNodeConnectionChange?.(NodeSlotType.OUTPUT, this, slot, target_node, targetIndex) + + this.setDirtyCanvas(false, true) + graph.afterChange() + graph.connectionChange(this) + + return link_info + } + + /** + * disconnect one output to an specific node + * @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 + */ + disconnectOutput(slot: string | number, target_node?: LGraphNode): boolean { + if (typeof slot === '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 + const output = this.outputs[slot] + if (!output || !output.links || output.links.length == 0) return false + + //one of the output links in this slot + const graph = this.graph + if (target_node) { + if (typeof target_node === 'number') target_node = graph.getNodeById(target_node) + if (!target_node) throw 'Target Node not found' + + for (let i = 0, l = output.links.length; i < l; i++) { + const link_id = output.links[i] + const link_info = graph._links.get(link_id) + + //is the link we are searching for... + if (link_info.target_id == target_node.id) { + output.links.splice(i, 1) //remove here + const input = target_node.inputs[link_info.target_slot] + input.link = null //remove there + + //remove the link from the links pool + graph._links.delete(link_id) + graph._version++ + + //link_info hasn't been modified so its ok + target_node.onConnectionsChange?.(NodeSlotType.INPUT, link_info.target_slot, false, link_info, input) + this.onConnectionsChange?.(NodeSlotType.OUTPUT, slot, false, link_info, output) + + graph.onNodeConnectionChange?.(NodeSlotType.OUTPUT, this, slot) + graph.onNodeConnectionChange?.(NodeSlotType.INPUT, target_node, link_info.target_slot) + break + } + } + } //all the links in this output slot + else { + for (let i = 0, l = output.links.length; i < l; i++) { + const link_id = output.links[i] + const link_info = graph._links.get(link_id) + //bug: it happens sometimes + if (!link_info) continue + + target_node = graph.getNodeById(link_info.target_id) graph._version++ - //link_info has been created now, so its updated - this.onConnectionsChange?.( - NodeSlotType.OUTPUT, - slot, - true, - link_info, - output - ) - - target_node.onConnectionsChange?.( - NodeSlotType.INPUT, - targetIndex, - true, - link_info, - input - ) - graph.onNodeConnectionChange?.( - NodeSlotType.INPUT, - target_node, - targetIndex, - this, - slot - ) - graph.onNodeConnectionChange?.( - NodeSlotType.OUTPUT, - this, - slot, - target_node, - targetIndex - ) - - this.setDirtyCanvas(false, true) - graph.afterChange() - graph.connectionChange(this) - - return link_info - } - - /** - * disconnect one output to an specific node - * @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 - */ - disconnectOutput(slot: string | number, target_node?: LGraphNode): boolean { - if (typeof slot === "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 - const output = this.outputs[slot] - if (!output || !output.links || output.links.length == 0) - return false - - //one of the output links in this slot - const graph = this.graph if (target_node) { - if (typeof target_node === "number") - target_node = graph.getNodeById(target_node) - if (!target_node) - throw "Target Node not found" + const input = target_node.inputs[link_info.target_slot] + //remove other side link + input.link = null - for (let i = 0, l = output.links.length; i < l; i++) { - const link_id = output.links[i] - const link_info = graph._links.get(link_id) + //link_info hasn't been modified so its ok + target_node.onConnectionsChange?.(NodeSlotType.INPUT, link_info.target_slot, false, link_info, input) + } + //remove the link from the links pool + graph._links.delete(link_id) - //is the link we are searching for... - if (link_info.target_id == target_node.id) { - output.links.splice(i, 1) //remove here - const input = target_node.inputs[link_info.target_slot] - input.link = null //remove there + this.onConnectionsChange?.(NodeSlotType.OUTPUT, slot, false, link_info, output) + graph.onNodeConnectionChange?.(NodeSlotType.OUTPUT, this, slot) + graph.onNodeConnectionChange?.(NodeSlotType.INPUT, target_node, link_info.target_slot) + } + output.links = null + } - //remove the link from the links pool - graph._links.delete(link_id) - graph._version++ + this.setDirtyCanvas(false, true) + graph.connectionChange(this) + return true + } - //link_info hasn't been modified so its ok - target_node.onConnectionsChange?.( - NodeSlotType.INPUT, - link_info.target_slot, - false, - link_info, - input - ) - this.onConnectionsChange?.( - NodeSlotType.OUTPUT, - slot, - false, - link_info, - output - ) + /** + * Disconnect one input + * @param slot Input slot index, or the name of the slot + * @return true if disconnected successfully or already disconnected, otherwise false + */ + disconnectInput(slot: number | string): boolean { + // Allow search by string + if (typeof slot === '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 + } - graph.onNodeConnectionChange?.(NodeSlotType.OUTPUT, this, slot) - graph.onNodeConnectionChange?.(NodeSlotType.INPUT, target_node, link_info.target_slot) - break - } - } - } //all the links in this output slot - else { - for (let i = 0, l = output.links.length; i < l; i++) { - const link_id = output.links[i] - const link_info = graph._links.get(link_id) - //bug: it happens sometimes - if (!link_info) continue + const input = this.inputs[slot] + if (!input) return false - target_node = graph.getNodeById(link_info.target_id) - graph._version++ + const link_id = this.inputs[slot].link + if (link_id != null) { + this.inputs[slot].link = null - if (target_node) { - const input = target_node.inputs[link_info.target_slot] - //remove other side link - input.link = null + //remove other side + const link_info = this.graph._links.get(link_id) + if (link_info) { + const target_node = this.graph.getNodeById(link_info.origin_id) + if (!target_node) return false - //link_info hasn't been modified so its ok - target_node.onConnectionsChange?.( - NodeSlotType.INPUT, - link_info.target_slot, - false, - link_info, - input - ) - } - //remove the link from the links pool - graph._links.delete(link_id) + const output = target_node.outputs[link_info.origin_slot] + if (!(output?.links?.length > 0)) return false - this.onConnectionsChange?.( - NodeSlotType.OUTPUT, - slot, - false, - link_info, - output - ) - graph.onNodeConnectionChange?.(NodeSlotType.OUTPUT, this, slot) - graph.onNodeConnectionChange?.(NodeSlotType.INPUT, target_node, link_info.target_slot) - } - output.links = null + //search in the inputs list for this link + let i = 0 + for (const l = output.links.length; i < l; i++) { + if (output.links[i] == link_id) { + output.links.splice(i, 1) + break + } } - this.setDirtyCanvas(false, true) - graph.connectionChange(this) - return true + this.graph._links.delete(link_id) + if (this.graph) this.graph._version++ + + this.onConnectionsChange?.(NodeSlotType.INPUT, slot, false, link_info, input) + target_node.onConnectionsChange?.(NodeSlotType.OUTPUT, i, false, link_info, output) + this.graph?.onNodeConnectionChange?.(NodeSlotType.OUTPUT, target_node, i) + this.graph?.onNodeConnectionChange?.(NodeSlotType.INPUT, this, slot) + } } - /** - * Disconnect one input - * @param slot Input slot index, or the name of the slot - * @return true if disconnected successfully or already disconnected, otherwise false - */ - disconnectInput(slot: number | string): boolean { - // Allow search by string - if (typeof slot === "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 - } + this.setDirtyCanvas(false, true) + this.graph?.connectionChange(this) + return true + } - const input = this.inputs[slot] - if (!input) return false + /** + * returns the center of a connection point in canvas coords + * @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 + **/ + getConnectionPos(is_input: boolean, slot_number: number, out?: Point): Point { + out ||= new Float32Array(2) - const link_id = this.inputs[slot].link - if (link_id != null) { - this.inputs[slot].link = null + const num_slots = is_input ? (this.inputs?.length ?? 0) : (this.outputs?.length ?? 0) - //remove other side - const link_info = this.graph._links.get(link_id) - if (link_info) { - const target_node = this.graph.getNodeById(link_info.origin_id) - if (!target_node) return false + const offset = LiteGraph.NODE_SLOT_HEIGHT * 0.5 - const output = target_node.outputs[link_info.origin_slot] - if (!(output?.links?.length > 0)) return false - - //search in the inputs list for this link - let i = 0 - for (const l = output.links.length; i < l; i++) { - if (output.links[i] == link_id) { - output.links.splice(i, 1) - break - } - } - - this.graph._links.delete(link_id) - if (this.graph) this.graph._version++ - - this.onConnectionsChange?.( - NodeSlotType.INPUT, - slot, - false, - link_info, - input - ) - target_node.onConnectionsChange?.( - NodeSlotType.OUTPUT, - i, - false, - link_info, - output - ) - this.graph?.onNodeConnectionChange?.(NodeSlotType.OUTPUT, target_node, i) - this.graph?.onNodeConnectionChange?.(NodeSlotType.INPUT, this, slot) - } - } - - this.setDirtyCanvas(false, true) - this.graph?.connectionChange(this) - return true + if (this.flags.collapsed) { + const w = this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH + if (this.horizontal) { + out[0] = this.pos[0] + w * 0.5 + out[1] = is_input ? this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT : this.pos[1] + } else { + out[0] = is_input ? this.pos[0] : this.pos[0] + w + out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5 + } + return out } - /** - * returns the center of a connection point in canvas coords - * @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 - **/ - getConnectionPos(is_input: boolean, slot_number: number, out?: Point): Point { - out ||= new Float32Array(2) - - const num_slots = is_input - ? this.inputs?.length ?? 0 - : this.outputs?.length ?? 0 - - const offset = LiteGraph.NODE_SLOT_HEIGHT * 0.5 - - if (this.flags.collapsed) { - const w = this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH - if (this.horizontal) { - out[0] = this.pos[0] + w * 0.5 - out[1] = is_input - ? this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT - : this.pos[1] - } else { - out[0] = is_input - ? this.pos[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) - out[1] = is_input - ? this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT - : this.pos[1] + this.size[1] - return out - } - - //default vertical slots - out[0] = is_input - ? this.pos[0] + offset - : 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 + //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 } - /* Force align to grid */ - alignToGrid(): void { - 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) + //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 } - /* Console output */ - trace(msg?: string): void { - this.console ||= [] - this.console.push(msg) - if (this.console.length > LGraphNode.MAX_CONSOLE) - this.console.shift() - - this.graph.onNodeTrace?.(this, msg) + //horizontal distributed slots + if (this.horizontal) { + out[0] = this.pos[0] + (slot_number + 0.5) * (this.size[0] / num_slots) + out[1] = is_input ? this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT : this.pos[1] + this.size[1] + return out } - /* Forces to redraw or the main canvas (LGraphNode) or the bg canvas (links) */ - setDirtyCanvas(dirty_foreground: boolean, dirty_background?: boolean): void { - this.graph?.sendActionToCanvas("setDirty", [ - dirty_foreground, - dirty_background - ]) + //default vertical slots + out[0] = is_input ? this.pos[0] + offset : 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 */ + alignToGrid(): void { + 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 */ + trace(msg?: string): void { + this.console ||= [] + this.console.push(msg) + if (this.console.length > LGraphNode.MAX_CONSOLE) this.console.shift() + + this.graph.onNodeTrace?.(this, msg) + } + + /* Forces to redraw or the main canvas (LGraphNode) or the bg canvas (links) */ + setDirtyCanvas(dirty_foreground: boolean, dirty_background?: boolean): void { + this.graph?.sendActionToCanvas('setDirty', [dirty_foreground, dirty_background]) + } + + loadImage(url: string): HTMLImageElement { + interface AsyncImageElement extends HTMLImageElement { + ready?: boolean } - loadImage(url: string): HTMLImageElement { - interface AsyncImageElement extends HTMLImageElement { ready?: boolean } + const img: AsyncImageElement = new Image() + img.src = LiteGraph.node_images_path + url + img.ready = false - const img: AsyncImageElement = new Image() - img.src = LiteGraph.node_images_path + url - img.ready = false - - const that = this - img.onload = function (this: AsyncImageElement) { - this.ready = true - that.setDirtyCanvas(true) - } - return img + const that = this + img.onload = function (this: AsyncImageElement) { + this.ready = true + that.setDirtyCanvas(true) } + return img + } - /* Allows to get onMouseMove and onMouseUp events even if the mouse is out of focus */ - captureInput(v: boolean): void { - if (!this.graph || !this.graph.list_of_graphcanvas) - return + /* Allows to get onMouseMove and onMouseUp events even if the mouse is out of focus */ + captureInput(v: boolean): void { + if (!this.graph || !this.graph.list_of_graphcanvas) return - const list = this.graph.list_of_graphcanvas + const list = this.graph.list_of_graphcanvas - for (let i = 0; i < list.length; ++i) { - const c = list[i] - //releasing somebody elses capture?! - if (!v && c.node_capturing_input != this) - continue + for (let i = 0; i < list.length; ++i) { + const c = list[i] + //releasing somebody elses capture?! + if (!v && c.node_capturing_input != this) continue - //change - c.node_capturing_input = v ? this : null - } + //change + c.node_capturing_input = v ? this : null } + } - get collapsed() { - return !!this.flags.collapsed + get collapsed() { + return !!this.flags.collapsed + } + + get collapsible() { + return !this.pinned && this.constructor.collapsable !== false + } + + /** + * Collapse the node to make it smaller on the canvas + **/ + collapse(force?: boolean): void { + if (!this.collapsible && !force) return + this.graph._version++ + this.flags.collapsed = !this.flags.collapsed + this.setDirtyCanvas(true, true) + } + + /** + * Toggles advanced mode of the node, showing advanced widgets + */ + toggleAdvanced() { + if (!this.widgets?.some((w) => w.advanced)) return + this.graph._version++ + this.showAdvanced = !this.showAdvanced + const prefSize = this.computeSize() + if (this.size[0] < prefSize[0] || this.size[1] < prefSize[1]) { + this.setSize([Math.max(this.size[0], prefSize[0]), Math.max(this.size[1], prefSize[1])]) } + this.setDirtyCanvas(true, true) + } - get collapsible() { - return !this.pinned && (this.constructor.collapsable !== false) - } - - /** - * Collapse the node to make it smaller on the canvas - **/ - collapse(force?: boolean): void { - if (!this.collapsible && !force) return - this.graph._version++ - this.flags.collapsed = !this.flags.collapsed - this.setDirtyCanvas(true, true) - } - - /** - * Toggles advanced mode of the node, showing advanced widgets - */ - toggleAdvanced() { - if (!this.widgets?.some(w => w.advanced)) return - this.graph._version++ - this.showAdvanced = !this.showAdvanced - const prefSize = this.computeSize() - if (this.size[0] < prefSize[0] || this.size[1] < prefSize[1]) { - this.setSize([Math.max(this.size[0], prefSize[0]), Math.max(this.size[1], prefSize[1])]) - } - this.setDirtyCanvas(true, true) - } - - get pinned() { - return !!this.flags.pinned - } - - /** - * Prevents the node being accidentally moved or resized by mouse interaction. - **/ - pin(v?: boolean): void { - this.graph._version++ - this.flags.pinned = v === undefined - ? !this.flags.pinned - : v - this.resizable = !this.pinned - // Delete the flag if unpinned, so that we don't get unnecessary - // flags.pinned = false in serialized object. - if (!this.pinned) - delete this.flags.pinned - } - - localToScreen(x: number, y: number, dragAndScale: DragAndScale): Point { - return [ - (x + this.pos[0]) * dragAndScale.scale + dragAndScale.offset[0], - (y + this.pos[1]) * dragAndScale.scale + dragAndScale.offset[1] - ] - } - - get width() { - return this.collapsed ? this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH : this.size[0] - } - - get height() { - // @ts-expect-error Not impl. - return this.collapsed ? LiteGraph.NODE_COLLAPSED_HEIGHT : this.size[1] - } - - drawBadges(ctx: CanvasRenderingContext2D, { gap = 2 } = {}): void { - const badgeInstances = this.badges.map(badge => badge instanceof LGraphBadge ? badge : badge()) - const isLeftAligned = this.badgePosition === BadgePosition.TopLeft - - let currentX = isLeftAligned ? 0 : this.width - badgeInstances.reduce((acc, badge) => acc + badge.getWidth(ctx) + gap, 0) - const y = -(LiteGraph.NODE_TITLE_HEIGHT + gap) - - for (const badge of badgeInstances) { - badge.draw(ctx, currentX, y - badge.height) - currentX += badge.getWidth(ctx) + gap - } + get pinned() { + return !!this.flags.pinned + } + + /** + * Prevents the node being accidentally moved or resized by mouse interaction. + **/ + pin(v?: boolean): void { + this.graph._version++ + this.flags.pinned = v === undefined ? !this.flags.pinned : v + this.resizable = !this.pinned + // Delete the flag if unpinned, so that we don't get unnecessary + // flags.pinned = false in serialized object. + if (!this.pinned) delete this.flags.pinned + } + + localToScreen(x: number, y: number, dragAndScale: DragAndScale): Point { + return [(x + this.pos[0]) * dragAndScale.scale + dragAndScale.offset[0], (y + this.pos[1]) * dragAndScale.scale + dragAndScale.offset[1]] + } + + get width() { + return this.collapsed ? this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH : this.size[0] + } + + get height() { + // @ts-expect-error Not impl. + return this.collapsed ? LiteGraph.NODE_COLLAPSED_HEIGHT : this.size[1] + } + + drawBadges(ctx: CanvasRenderingContext2D, { gap = 2 } = {}): void { + const badgeInstances = this.badges.map((badge) => (badge instanceof LGraphBadge ? badge : badge())) + const isLeftAligned = this.badgePosition === BadgePosition.TopLeft + + let currentX = isLeftAligned ? 0 : this.width - badgeInstances.reduce((acc, badge) => acc + badge.getWidth(ctx) + gap, 0) + const y = -(LiteGraph.NODE_TITLE_HEIGHT + gap) + + for (const badge of badgeInstances) { + badge.draw(ctx, currentX, y - badge.height) + currentX += badge.getWidth(ctx) + gap } + } } diff --git a/src/LLink.ts b/src/LLink.ts index 353f6317b..b13604372 100644 --- a/src/LLink.ts +++ b/src/LLink.ts @@ -1,6 +1,6 @@ -import type { CanvasColour, ISlotType } from "./interfaces" -import type { NodeId } from "./LGraphNode" -import type { Serialisable, SerialisableLLink } from "./types/serialisation" +import type { CanvasColour, ISlotType } from './interfaces' +import type { NodeId } from './LGraphNode' +import type { Serialisable, SerialisableLLink } from './types/serialisation' export type LinkId = number | string @@ -8,101 +8,96 @@ export type SerialisedLLinkArray = [LinkId, NodeId, number, NodeId, number, ISlo //this is the class in charge of storing link information export class LLink implements Serialisable { - /** Link ID */ - id: LinkId - type: ISlotType - /** Output node ID */ - origin_id: NodeId - /** Output slot index */ - origin_slot: number - /** Input node ID */ - target_id: NodeId - /** Input slot index */ - target_slot: number - data?: number | string | boolean | { toToolTip?(): string } - _data?: unknown - /** Centre point of the link, calculated during render only - can be inaccurate */ - _pos: Float32Array - /** @todo Clean up - never implemented in comfy. */ - _last_time?: number - /** The last canvas 2D path that was used to render this link */ - path?: Path2D + /** Link ID */ + id: LinkId + type: ISlotType + /** Output node ID */ + origin_id: NodeId + /** Output slot index */ + origin_slot: number + /** Input node ID */ + target_id: NodeId + /** Input slot index */ + target_slot: number + data?: number | string | boolean | { toToolTip?(): string } + _data?: unknown + /** Centre point of the link, calculated during render only - can be inaccurate */ + _pos: Float32Array + /** @todo Clean up - never implemented in comfy. */ + _last_time?: number + /** The last canvas 2D path that was used to render this link */ + path?: Path2D - #color?: CanvasColour - /** Custom colour for this link only */ - public get color(): CanvasColour { return this.#color } - public set color(value: CanvasColour) { - this.#color = value === "" ? null : value + #color?: CanvasColour + /** Custom colour for this link only */ + public get color(): CanvasColour { + return this.#color + } + public set color(value: CanvasColour) { + this.#color = value === '' ? null : value + } + + constructor(id: LinkId, type: ISlotType, origin_id: NodeId, origin_slot: number, target_id: NodeId, target_slot: number) { + 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 + } + + /** @deprecated Use {@link LLink.create} */ + static createFromArray(data: SerialisedLLinkArray): LLink { + return new LLink(data[0], data[5], data[1], data[2], data[3], data[4]) + } + + /** + * LLink static factory: creates a new LLink from the provided data. + * @param data Serialised LLink data to create the link from + * @returns A new LLink + */ + static create(data: SerialisableLLink): LLink { + return new LLink(data.id, data.type, data.origin_id, data.origin_slot, data.target_id, data.target_slot) + } + + configure(o: LLink | SerialisedLLinkArray) { + if (Array.isArray(o)) { + 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 } + } - constructor(id: LinkId, type: ISlotType, origin_id: NodeId, origin_slot: number, target_id: NodeId, target_slot: number) { - 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 + /** + * @deprecated Prefer {@link LLink.asSerialisable} (returns an object, not an array) + * @returns An array representing this LLink + */ + serialize(): SerialisedLLinkArray { + return [this.id, this.origin_id, this.origin_slot, this.target_id, this.target_slot, this.type] + } - this._data = null - this._pos = new Float32Array(2) //center - } - - /** @deprecated Use {@link LLink.create} */ - static createFromArray(data: SerialisedLLinkArray): LLink { - return new LLink(data[0], data[5], data[1], data[2], data[3], data[4]) - } - - /** - * LLink static factory: creates a new LLink from the provided data. - * @param data Serialised LLink data to create the link from - * @returns A new LLink - */ - static create(data: SerialisableLLink): LLink { - return new LLink(data.id, data.type, data.origin_id, data.origin_slot, data.target_id, data.target_slot) - } - - configure(o: LLink | SerialisedLLinkArray) { - if (Array.isArray(o)) { - 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 - } - } - - /** - * @deprecated Prefer {@link LLink.asSerialisable} (returns an object, not an array) - * @returns An array representing this LLink - */ - serialize(): SerialisedLLinkArray { - return [ - this.id, - this.origin_id, - this.origin_slot, - this.target_id, - this.target_slot, - this.type - ] - } - - asSerialisable(): SerialisableLLink { - const copy: SerialisableLLink = { - id: this.id, - origin_id: this.origin_id, - origin_slot: this.origin_slot, - target_id: this.target_id, - target_slot: this.target_slot, - type: this.type - } - return copy + asSerialisable(): SerialisableLLink { + const copy: SerialisableLLink = { + id: this.id, + origin_id: this.origin_id, + origin_slot: this.origin_slot, + target_id: this.target_id, + target_slot: this.target_slot, + type: this.type, } + return copy + } } diff --git a/src/LiteGraphGlobal.ts b/src/LiteGraphGlobal.ts index e844d6769..825968208 100644 --- a/src/LiteGraphGlobal.ts +++ b/src/LiteGraphGlobal.ts @@ -1,946 +1,923 @@ -import { LGraph } from "./LGraph" -import { LLink } from "./LLink" -import { LGraphGroup } from "./LGraphGroup" -import { DragAndScale } from "./DragAndScale" -import { LGraphCanvas } from "./LGraphCanvas" -import { ContextMenu } from "./ContextMenu" -import { CurveEditor } from "./CurveEditor" -import { LGraphEventMode, LinkDirection, LinkRenderType, NodeSlotType, RenderShape, TitleMode } from "./types/globalEnums" -import { LGraphNode } from "./LGraphNode" -import { SlotShape, SlotDirection, SlotType, LabelPosition } from "./draw" -import type { Dictionary, ISlotType, Rect } from "./interfaces" -import { distance, isInsideRectangle, overlapBounding } from "./measure" +import { LGraph } from './LGraph' +import { LLink } from './LLink' +import { LGraphGroup } from './LGraphGroup' +import { DragAndScale } from './DragAndScale' +import { LGraphCanvas } from './LGraphCanvas' +import { ContextMenu } from './ContextMenu' +import { CurveEditor } from './CurveEditor' +import { LGraphEventMode, LinkDirection, LinkRenderType, NodeSlotType, RenderShape, TitleMode } from './types/globalEnums' +import { LGraphNode } from './LGraphNode' +import { SlotShape, SlotDirection, SlotType, LabelPosition } from './draw' +import type { Dictionary, ISlotType, Rect } from './interfaces' +import { distance, isInsideRectangle, overlapBounding } from './measure' /** * The Global Scope. It contains all the registered node classes. */ export class LiteGraphGlobal { - // Enums - SlotShape = SlotShape - SlotDirection = SlotDirection - SlotType = SlotType - LabelPosition = LabelPosition + // Enums + SlotShape = SlotShape + SlotDirection = SlotDirection + SlotType = SlotType + LabelPosition = LabelPosition - VERSION = 0.4 + VERSION = 0.4 - CANVAS_GRID_SIZE = 10 + 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_TEXT_HIGHLIGHT_COLOR = "#EEE" - 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 - DEFAULT_GROUP_FONT_SIZE?: any + 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_TEXT_HIGHLIGHT_COLOR = '#EEE' + 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 + DEFAULT_GROUP_FONT_SIZE?: any - WIDGET_BGCOLOR = "#222" - WIDGET_OUTLINE_COLOR = "#666" - WIDGET_TEXT_COLOR = "#DDD" - WIDGET_SECONDARY_TEXT_COLOR = "#999" + WIDGET_BGCOLOR = '#222' + WIDGET_OUTLINE_COLOR = '#666' + WIDGET_TEXT_COLOR = '#DDD' + WIDGET_SECONDARY_TEXT_COLOR = '#999' - LINK_COLOR = "#9A9" - // TODO: This is a workaround until LGraphCanvas.link_type_colors is no longer static. - static DEFAULT_EVENT_LINK_COLOR = "#A86" - EVENT_LINK_COLOR = "#A86" - CONNECTING_LINK_COLOR = "#AFA" + LINK_COLOR = '#9A9' + // TODO: This is a workaround until LGraphCanvas.link_type_colors is no longer static. + static DEFAULT_EVENT_LINK_COLOR = '#A86' + 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" + 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 = RenderShape.BOX - ROUND_SHAPE = RenderShape.ROUND - CIRCLE_SHAPE = RenderShape.CIRCLE - CARD_SHAPE = RenderShape.CARD - ARROW_SHAPE = RenderShape.ARROW - GRID_SHAPE = RenderShape.GRID // intended for slot arrays + //shapes are used for nodes but also for slots + BOX_SHAPE = RenderShape.BOX + ROUND_SHAPE = RenderShape.ROUND + CIRCLE_SHAPE = RenderShape.CIRCLE + CARD_SHAPE = RenderShape.CARD + ARROW_SHAPE = RenderShape.ARROW + GRID_SHAPE = RenderShape.GRID // intended for slot arrays - //enums - INPUT = NodeSlotType.INPUT - OUTPUT = NodeSlotType.OUTPUT + //enums + INPUT = NodeSlotType.INPUT + OUTPUT = NodeSlotType.OUTPUT - // TODO: -1 can lead to ambiguity in JS; these should be updated to a more explicit constant or Symbol. - EVENT = -1 as const //for outputs - ACTION = -1 as const //for inputs + // TODO: -1 can lead to ambiguity in JS; these should be updated to a more explicit constant or Symbol. + EVENT = -1 as const //for outputs + ACTION = -1 as const //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 = LGraphEventMode.ALWAYS - ON_EVENT = LGraphEventMode.ON_EVENT - NEVER = LGraphEventMode.NEVER - ON_TRIGGER = LGraphEventMode.ON_TRIGGER + 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 = LGraphEventMode.ALWAYS + ON_EVENT = LGraphEventMode.ON_EVENT + NEVER = LGraphEventMode.NEVER + ON_TRIGGER = LGraphEventMode.ON_TRIGGER - UP = LinkDirection.UP - DOWN = LinkDirection.DOWN - LEFT = LinkDirection.LEFT - RIGHT = LinkDirection.RIGHT - CENTER = LinkDirection.CENTER + UP = LinkDirection.UP + DOWN = LinkDirection.DOWN + LEFT = LinkDirection.LEFT + RIGHT = LinkDirection.RIGHT + CENTER = LinkDirection.CENTER - LINK_RENDER_MODES = ["Straight", "Linear", "Spline"] // helper - HIDDEN_LINK = LinkRenderType.HIDDEN_LINK - STRAIGHT_LINK = LinkRenderType.STRAIGHT_LINK - LINEAR_LINK = LinkRenderType.LINEAR_LINK - SPLINE_LINK = LinkRenderType.SPLINE_LINK + LINK_RENDER_MODES = ['Straight', 'Linear', 'Spline'] // helper + HIDDEN_LINK = LinkRenderType.HIDDEN_LINK + STRAIGHT_LINK = LinkRenderType.STRAIGHT_LINK + LINEAR_LINK = LinkRenderType.LINEAR_LINK + SPLINE_LINK = LinkRenderType.SPLINE_LINK - NORMAL_TITLE = TitleMode.NORMAL_TITLE - NO_TITLE = TitleMode.NO_TITLE - TRANSPARENT_TITLE = TitleMode.TRANSPARENT_TITLE - AUTOHIDE_TITLE = TitleMode.AUTOHIDE_TITLE + NORMAL_TITLE = TitleMode.NORMAL_TITLE + NO_TITLE = TitleMode.NO_TITLE + TRANSPARENT_TITLE = TitleMode.TRANSPARENT_TITLE + AUTOHIDE_TITLE = TitleMode.AUTOHIDE_TITLE - VERTICAL_LAYOUT = "vertical" // arrange nodes vertically + VERTICAL_LAYOUT = 'vertical' // arrange nodes vertically - proxy = null //used to redirect calls - node_images_path = "" + 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: Record = {} //nodetypes by string - node_types_by_file_extension = {} //used for dropping files in the canvas - Nodes: Record = {} //node types by classname - Globals = {} //used to store vars between graphs + 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: Record = {} //nodetypes by string + node_types_by_file_extension = {} //used for dropping files in the canvas + Nodes: Record = {} //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 + 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 + 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 + 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 - ctrl_alt_click_do_break_link = true // [true!] who accidentally ctrl-alt-clicks on an in/output? nobody! that's who! - snaps_for_comfy = true // [true!] snaps links when dragging connections over valid targets - snap_highlights_node = true // [true!] renders a partial border to highlight when a dragged link is snapped to a node + 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 + ctrl_alt_click_do_break_link = true // [true!] who accidentally ctrl-alt-clicks on an in/output? nobody! that's who! + snaps_for_comfy = true // [true!] snaps links when dragging connections over valid targets + snap_highlights_node = true // [true!] renders a partial border to highlight when a dragged link is snapped to a node - 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 + 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] + 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: Record = {} // slot types for nodeclass - registered_slot_out_types: Record = {} // slot types for nodeclass - slot_types_in: string[] = [] // slot types IN - slot_types_out: string[] = [] // slot types OUT - slot_types_default_in: Record = {} // 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: Record = {} // specify for each OUT slot type a(/many) default node(s), use single string, array, or object (with node, title, parameters, ..) like for search + // set these values if not using auto_load_slot_types + registered_slot_in_types: Record = {} // slot types for nodeclass + registered_slot_out_types: Record = {} // slot types for nodeclass + slot_types_in: string[] = [] // slot types IN + slot_types_out: string[] = [] // slot types OUT + slot_types_default_in: Record = {} // 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: Record = {} // 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 + 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 + 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 + 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) + 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 + 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) + 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 + // 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 + // 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 - // Whether to highlight the bounding box of selected groups - highlight_selected_group = true + // Whether to highlight the bounding box of selected groups + highlight_selected_group = true - // TODO: Remove legacy accessors - LGraph = LGraph - LLink = LLink - LGraphNode = LGraphNode - LGraphGroup = LGraphGroup - DragAndScale = DragAndScale - LGraphCanvas = LGraphCanvas - ContextMenu = ContextMenu - CurveEditor = CurveEditor + // TODO: Remove legacy accessors + LGraph = LGraph + LLink = LLink + LGraphNode = LGraphNode + LGraphGroup = LGraphGroup + DragAndScale = DragAndScale + LGraphCanvas = LGraphCanvas + ContextMenu = ContextMenu + CurveEditor = CurveEditor - onNodeTypeRegistered?(type: string, base_class: typeof LGraphNode): void - onNodeTypeReplaced?(type: string, base_class: typeof LGraphNode, prev: unknown): void + onNodeTypeRegistered?(type: string, base_class: typeof LGraphNode): void + onNodeTypeReplaced?(type: string, base_class: typeof LGraphNode, prev: unknown): void - // Avoid circular dependency from original single-module - static { - LGraphCanvas.link_type_colors = { - "-1": LiteGraphGlobal.DEFAULT_EVENT_LINK_COLOR, - number: "#AAA", - node: "#DCA" - } + // Avoid circular dependency from original single-module + static { + LGraphCanvas.link_type_colors = { + '-1': LiteGraphGlobal.DEFAULT_EVENT_LINK_COLOR, + number: '#AAA', + node: '#DCA', + } + } + + constructor() { + //timer that works everywhere + if (typeof performance != 'undefined') { + this.getTime = performance.now.bind(performance) + } else if (typeof Date != 'undefined' && Date.now) { + this.getTime = Date.now.bind(Date) + } else if (typeof process !== 'undefined') { + this.getTime = function () { + const t = process.hrtime() + return t[0] * 0.001 + t[1] * 1e-6 + } + } else { + this.getTime = function () { + return new Date().getTime() + } + } + } + + /** + * Register a node class so it can be listed when the user wants to create a new one + * @param {String} type name of the node and path + * @param {Class} base_class class containing the structure of a node + */ + registerNodeType(type: string, base_class: typeof LGraphNode): void { + if (!base_class.prototype) throw 'Cannot register a simple object, it must be a class with a prototype' + base_class.type = type + + if (this.debug) console.log('Node registered: ' + type) + + const classname = base_class.name + + const pos = type.lastIndexOf('/') + base_class.category = type.substring(0, pos) + + base_class.title ||= classname + + //extend class + for (const i in LGraphNode.prototype) { + base_class.prototype[i] ||= LGraphNode.prototype[i] } - constructor() { - //timer that works everywhere - if (typeof performance != "undefined") { - this.getTime = performance.now.bind(performance) - } else if (typeof Date != "undefined" && Date.now) { - this.getTime = Date.now.bind(Date) - } else if (typeof process != "undefined") { - this.getTime = function () { - const t = process.hrtime() - return t[0] * 0.001 + t[1] * 1e-6 - } - } else { - this.getTime = function () { - return new Date().getTime() - } + 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(this: LGraphNode, v: RenderShape | 'default' | 'box' | 'round' | 'circle' | 'card') { + switch (v) { + case 'default': + delete this._shape + break + case 'box': + this._shape = RenderShape.BOX + break + case 'round': + this._shape = RenderShape.ROUND + break + case 'circle': + this._shape = RenderShape.CIRCLE + break + case 'card': + this._shape = RenderShape.CARD + break + default: + this._shape = v + } + }, + get() { + 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 (const i in base_class.supported_extensions) { + const ext = base_class.supported_extensions[i] + if (ext && typeof ext === 'string') { + this.node_types_by_file_extension[ext.toLowerCase()] = base_class + } } + } } - /** - * Register a node class so it can be listed when the user wants to create a new one - * @param {String} type name of the node and path - * @param {Class} base_class class containing the structure of a node - */ - registerNodeType(type: string, base_class: typeof LGraphNode): void { - if (!base_class.prototype) - throw "Cannot register a simple object, it must be a class with a prototype" - base_class.type = type + this.registered_node_types[type] = base_class + if (base_class.constructor.name) this.Nodes[classname] = base_class - if (this.debug) console.log("Node registered: " + type) + this.onNodeTypeRegistered?.(type, base_class) + if (prev) this.onNodeTypeReplaced?.(type, base_class, prev) - const classname = base_class.name + //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`) - const pos = type.lastIndexOf("/") - base_class.category = type.substring(0, pos) + // 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') + } - base_class.title ||= classname + /** + * removes a node type from the system + * @param {String|Object} type name of the node or the node constructor itself + */ + unregisterNodeType(type: string | typeof LGraphNode): void { + const base_class = typeof type === 'string' ? this.registered_node_types[type] : type + if (!base_class) throw 'node type not found: ' + type - //extend class - for (const i in LGraphNode.prototype) { - base_class.prototype[i] ||= LGraphNode.prototype[i] - } + delete this.registered_node_types[base_class.type] - 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(this: LGraphNode, v: RenderShape | "default" | "box" | "round" | "circle" | "card") { - switch (v) { - case "default": - delete this._shape - break - case "box": - this._shape = RenderShape.BOX - break - case "round": - this._shape = RenderShape.ROUND - break - case "circle": - this._shape = RenderShape.CIRCLE - break - case "card": - this._shape = RenderShape.CARD - break - default: - this._shape = v - } - }, - get() { - return this._shape - }, - enumerable: true, - configurable: true - }) + const name = base_class.constructor.name + if (name) delete this.Nodes[name] + } - //used to know which nodes to create when dragging files to the canvas - if (base_class.supported_extensions) { - for (const i in base_class.supported_extensions) { - const ext = base_class.supported_extensions[i] - if (ext && typeof ext === "string") { - this.node_types_by_file_extension[ext.toLowerCase()] = base_class - } - } - } - } + /** + * Save a slot type and his node + * @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(type: LGraphNode, slot_type: ISlotType, out?: boolean): void { + out ||= false + // @ts-expect-error Confirm this function no longer supports string types - base_class should always be an instance not a constructor. + const base_class = typeof type === 'string' && this.registered_node_types[type] !== 'anonymous' ? this.registered_node_types[type] : type - this.registered_node_types[type] = base_class - if (base_class.constructor.name) this.Nodes[classname] = base_class + // @ts-expect-error Confirm this function no longer supports string types - base_class should always be an instance not a constructor. + const class_type = base_class.constructor.type - this.onNodeTypeRegistered?.(type, base_class) - if (prev) this.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") + 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 = ['*'] } - /** - * removes a node type from the system - * @param {String|Object} type name of the node or the node constructor itself - */ - unregisterNodeType(type: string | typeof LGraphNode): void { - const base_class = typeof type === "string" - ? this.registered_node_types[type] - : type - if (!base_class) throw "node type not found: " + type + for (let i = 0; i < allTypes.length; ++i) { + let slotType = allTypes[i] + if (slotType === '') slotType = '*' - delete this.registered_node_types[base_class.type] + 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) - const name = base_class.constructor.name - if (name) delete this.Nodes[name] + // check if is a new type + const types = out ? this.slot_types_out : this.slot_types_in + if (!types.includes(slotType.toLowerCase())) { + types.push(slotType.toLowerCase()) + types.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. + * @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(name: string, func: (...args: any) => any, param_types: string[], return_type: string, properties: unknown) { + const params = Array(func.length) + let code = '' + const names = this.getParameterNames(func) + for (let 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` + + const classobj = Function(code) + // @ts-ignore + classobj.title = name.split('/').pop() + // @ts-ignore + classobj.desc = 'Generated from ' + func.name + classobj.prototype.onExecute = function onExecute() { + for (let i = 0; i < params.length; ++i) { + params[i] = this.getInputData(i) + } + const r = func.apply(this, params) + this.setOutputData(0, r) + } + // @ts-expect-error Required to make this kludge work + this.registerNodeType(name, classobj) + } + + /** + * Removes all previously registered node's types + */ + clearRegisteredTypes(): void { + 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) + * @param {Function} func + */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + addNodeMethod(name: string, func: Function): void { + LGraphNode.prototype[name] = func + for (const i in this.registered_node_types) { + const type = this.registered_node_types[i] + //keep old in case of replacing + if (type.prototype[name]) type.prototype['_' + name] = type.prototype[name] + type.prototype[name] = func + } + } + + /** + * Create a node of a given type with a name. The node is not attached to any graph yet. + * @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(type: string, title?: string, options?: Dictionary): LGraphNode { + const base_class = this.registered_node_types[type] + if (!base_class) { + if (this.debug) console.log(`GraphNode type "${type}" not registered.`) + return null } - /** - * Save a slot type and his node - * @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(type: LGraphNode, slot_type: ISlotType, out?: boolean): void { - out ||= false - // @ts-expect-error Confirm this function no longer supports string types - base_class should always be an instance not a constructor. - const base_class = typeof type === "string" && this.registered_node_types[type] !== "anonymous" - ? this.registered_node_types[type] - : type + title = title || base_class.title || type - // @ts-expect-error Confirm this function no longer supports string types - base_class should always be an instance not a constructor. - const class_type = base_class.constructor.type + let node = null - 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 - const types = out - ? this.slot_types_out - : this.slot_types_in - if (!types.includes(slotType.toLowerCase())) { - types.push(slotType.toLowerCase()) - types.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. - * @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( - name: string, - func: (...args: any) => any, - param_types: string[], - return_type: string, - properties: unknown - ) { - const params = Array(func.length) - let code = "" - const names = this.getParameterNames(func) - for (let 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` - - const classobj = Function(code) - // @ts-ignore - classobj.title = name.split("/").pop() - // @ts-ignore - classobj.desc = "Generated from " + func.name - classobj.prototype.onExecute = function onExecute() { - for (let i = 0; i < params.length; ++i) { - params[i] = this.getInputData(i) - } - const r = func.apply(this, params) - this.setOutputData(0, r) - } - // @ts-expect-error Required to make this kludge work - this.registerNodeType(name, classobj) - } - - /** - * Removes all previously registered node's types - */ - clearRegisteredTypes(): void { - 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) - * @param {Function} func - */ - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - addNodeMethod(name: string, func: Function): void { - LGraphNode.prototype[name] = func - for (const i in this.registered_node_types) { - const type = this.registered_node_types[i] - //keep old in case of replacing - if (type.prototype[name]) type.prototype["_" + name] = type.prototype[name] - type.prototype[name] = func - } - } - - /** - * Create a node of a given type with a name. The node is not attached to any graph yet. - * @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(type: string, title?: string, options?: Dictionary): LGraphNode { - const base_class = this.registered_node_types[type] - if (!base_class) { - if (this.debug) console.log(`GraphNode type "${type}" not registered.`) - return null - } - - title = title || base_class.title || type - - let node = null - - if (this.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 - node.properties ||= {} - node.properties_info ||= [] - node.flags ||= {} - //call onresize? - node.size ||= node.computeSize() - node.pos ||= this.DEFAULT_POSITION.concat() - node.mode ||= LGraphEventMode.ALWAYS - - //extra options - if (options) { - for (const i in options) { - node[i] = options[i] - } - } - - // callback - node.onNodeCreated?.() - return node - } - - /** - * Returns a registered node type with a given name - * @param {String} type full name of the node class. p.e. "math/sin" - * @return {Class} the node class - */ - getNodeType(type: string): typeof LGraphNode { - return this.registered_node_types[type] - } - - /** - * Returns a list of node types matching one category - * @param {String} category category name - * @return {Array} array with all the node classes - */ - getNodeTypesInCategory(category: string, filter: any) { - const r = [] - for (const i in this.registered_node_types) { - const 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 - * @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[] { - const categories = { "": 1 } - for (const i in this.registered_node_types) { - const type = this.registered_node_types[i] - if (type.category && !type.skip_list) { - if (type.filter != filter) - continue - categories[type.category] = 1 - } - } - const result = [] - for (const 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(folder_wildcard: string): void { - const tmp = document.getElementsByTagName("script") - //weird, this array changes by its own, so we use a copy - const script_files = [] - for (let i = 0; i < tmp.length; i++) { - script_files.push(tmp[i]) - } - - const docHeadObj = document.getElementsByTagName("head")[0] - folder_wildcard = document.location.href + folder_wildcard - - for (let i = 0; i < script_files.length; i++) { - const src = script_files[i].src - if (!src || src.substr(0, folder_wildcard.length) != folder_wildcard) - continue - - try { - if (this.debug) console.log("Reloading: " + src) - const dynamicScript = document.createElement("script") - dynamicScript.type = "text/javascript" - dynamicScript.src = src - docHeadObj.appendChild(dynamicScript) - docHeadObj.removeChild(script_files[i]) - } catch (err) { - if (this.throw_errors) throw err - if (this.debug) console.log("Error while reloading " + src) - } - } - - if (this.debug) console.log("Nodes reloaded") - } - - //separated just to improve if it doesn't work - cloneObject(obj: T, target?: T): T { - if (obj == null) return null - - const r = JSON.parse(JSON.stringify(obj)) - if (!target) return r - - for (const i in r) { - target[i] = r[i] - } - return target - } - - /* - * https://gist.github.com/jed/982883?permalink_comment_id=852670#gistcomment-852670 - */ - uuidv4(): string { - // @ts-ignore - 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) - * @param {String} type_a output - * @param {String} type_b input - * @return {Boolean} true if they can be connected - */ - isValidConnection(type_a: ISlotType, type_b: ISlotType): boolean { - if (type_a == "" || type_a === "*") type_a = 0 - if (type_b == "" || type_b === "*") type_b = 0 - // If generic in/output, matching types (valid for triggers), or event/action types - if (!type_a || !type_b || type_a == type_b || (type_a == this.EVENT && type_b == this.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 - const supported_types_a = type_a.split(",") - const supported_types_b = type_b.split(",") - for (let i = 0; i < supported_types_a.length; ++i) { - for (let j = 0; j < supported_types_b.length; ++j) { - if (this.isValidConnection(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 - * @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(node_type: any, description: string, data: any): void { - 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) - * @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(url: string | URL | Request | Blob, type: string, on_complete: (data: string | ArrayBuffer) => void, on_error: (error: unknown) => void): void | Promise { - if (!url) return null - - type = type || "text" - if (typeof url === "string") { - if (url.substr(0, 4) == "http" && this.proxy) - url = this.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: string | ArrayBuffer): void { - on_complete?.(data) - }) - .catch(function (error) { - console.error("error fetching file:", url) - on_error?.(error) - }) - } else if (url instanceof File || url instanceof Blob) { - const reader = new FileReader() - reader.onload = function (e) { - let v = e.target.result - if (type == "json") - // @ts-ignore - v = JSON.parse(v) - 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) - } + if (this.catch_exceptions) { + try { + node = new base_class(title) + } catch (err) { + console.error(err) return null + } + } else { + node = new base_class(title) } - //used to create nodes from wrapping functions - getParameterNames(func: (...args: any) => any): string[] { - 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 [""] + node.type = type + + if (!node.title && title) node.title = title + node.properties ||= {} + node.properties_info ||= [] + node.flags ||= {} + //call onresize? + node.size ||= node.computeSize() + node.pos ||= this.DEFAULT_POSITION.concat() + node.mode ||= LGraphEventMode.ALWAYS + + //extra options + if (options) { + for (const i in options) { + node[i] = options[i] + } } - /* helper for interaction: pointer, touch, mouse Listeners + // callback + node.onNodeCreated?.() + return node + } + + /** + * Returns a registered node type with a given name + * @param {String} type full name of the node class. p.e. "math/sin" + * @return {Class} the node class + */ + getNodeType(type: string): typeof LGraphNode { + return this.registered_node_types[type] + } + + /** + * Returns a list of node types matching one category + * @param {String} category category name + * @return {Array} array with all the node classes + */ + getNodeTypesInCategory(category: string, filter: any) { + const r = [] + for (const i in this.registered_node_types) { + const 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 + * @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[] { + const categories = { '': 1 } + for (const i in this.registered_node_types) { + const type = this.registered_node_types[i] + if (type.category && !type.skip_list) { + if (type.filter != filter) continue + categories[type.category] = 1 + } + } + const result = [] + for (const 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(folder_wildcard: string): void { + const tmp = document.getElementsByTagName('script') + //weird, this array changes by its own, so we use a copy + const script_files = [] + for (let i = 0; i < tmp.length; i++) { + script_files.push(tmp[i]) + } + + const docHeadObj = document.getElementsByTagName('head')[0] + folder_wildcard = document.location.href + folder_wildcard + + for (let i = 0; i < script_files.length; i++) { + const src = script_files[i].src + if (!src || src.substr(0, folder_wildcard.length) != folder_wildcard) continue + + try { + if (this.debug) console.log('Reloading: ' + src) + const dynamicScript = document.createElement('script') + dynamicScript.type = 'text/javascript' + dynamicScript.src = src + docHeadObj.appendChild(dynamicScript) + docHeadObj.removeChild(script_files[i]) + } catch (err) { + if (this.throw_errors) throw err + if (this.debug) console.log('Error while reloading ' + src) + } + } + + if (this.debug) console.log('Nodes reloaded') + } + + //separated just to improve if it doesn't work + cloneObject(obj: T, target?: T): T { + if (obj == null) return null + + const r = JSON.parse(JSON.stringify(obj)) + if (!target) return r + + for (const i in r) { + target[i] = r[i] + } + return target + } + + /* + * https://gist.github.com/jed/982883?permalink_comment_id=852670#gistcomment-852670 + */ + uuidv4(): string { + // @ts-ignore + 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) + * @param {String} type_a output + * @param {String} type_b input + * @return {Boolean} true if they can be connected + */ + isValidConnection(type_a: ISlotType, type_b: ISlotType): boolean { + if (type_a == '' || type_a === '*') type_a = 0 + if (type_b == '' || type_b === '*') type_b = 0 + // If generic in/output, matching types (valid for triggers), or event/action types + if (!type_a || !type_b || type_a == type_b || (type_a == this.EVENT && type_b == this.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 + const supported_types_a = type_a.split(',') + const supported_types_b = type_b.split(',') + for (let i = 0; i < supported_types_a.length; ++i) { + for (let j = 0; j < supported_types_b.length; ++j) { + if (this.isValidConnection(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 + * @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(node_type: any, description: string, data: any): void { + 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) + * @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( + url: string | URL | Request | Blob, + type: string, + on_complete: (data: string | ArrayBuffer) => void, + on_error: (error: unknown) => void, + ): void | Promise { + if (!url) return null + + type = type || 'text' + if (typeof url === 'string') { + if (url.substr(0, 4) == 'http' && this.proxy) url = this.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: string | ArrayBuffer): void { + on_complete?.(data) + }) + .catch(function (error) { + console.error('error fetching file:', url) + on_error?.(error) + }) + } else if (url instanceof File || url instanceof Blob) { + const reader = new FileReader() + reader.onload = function (e) { + let v = e.target.result + if (type == 'json') + // @ts-ignore + v = JSON.parse(v) + 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 + } + + //used to create nodes from wrapping functions + getParameterNames(func: (...args: any) => any): string[] { + 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*/ - pointerListenerAdd(oDOM: Node, sEvIn: string, fCall: (e: Event) => boolean | void, capture = false): void { - if (!oDOM || !oDOM.addEventListener || !sEvIn || typeof fCall !== "function") return + pointerListenerAdd(oDOM: Node, sEvIn: string, fCall: (e: Event) => boolean | void, capture = false): void { + if (!oDOM || !oDOM.addEventListener || !sEvIn || typeof fCall !== 'function') return - let sMethod = this.pointerevents_method - let sEvent = sEvIn + let sMethod = this.pointerevents_method + let 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") - } - } + // 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 } - - switch (sEvent) { - // @ts-expect-error - //both pointer and move events - case "down": case "up": case "move": case "over": case "out": case "enter": - { - oDOM.addEventListener(sMethod + sEvent, fCall, capture) - } - // @ts-expect-error - // 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) + case 'move': { + sMethod = 'touch' + //sEvent = "move"; + break } - } - pointerListenerRemove(oDOM: Node, sEvent: string, fCall: (e: Event) => boolean | void, capture = false): void { - if (!oDOM || !oDOM.removeEventListener || !sEvent || typeof fCall !== "function") return - - switch (sEvent) { - // @ts-expect-error - //both pointer and move events - case "down": case "up": case "move": case "over": case "out": case "enter": - { - if (this.pointerevents_method == "pointer" || this.pointerevents_method == "mouse") { - oDOM.removeEventListener(this.pointerevents_method + sEvent, fCall, capture) - } - } - // @ts-expect-error - // only pointerevents - case "leave": case "cancel": case "gotpointercapture": case "lostpointercapture": - { - if (this.pointerevents_method == "pointer") { - return oDOM.removeEventListener(this.pointerevents_method + sEvent, fCall, capture) - } - } - // not "pointer" || "mouse" - default: - return oDOM.removeEventListener(sEvent, fCall, capture) + 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') + } + } } - getTime: () => number + 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) + return; // Add return to prevent fallthrough + } - compareObjects(a: object, b: object): boolean { - for (const i in a) { - if (a[i] != b[i]) return false + // only pointerevents + case 'leave': + case 'cancel': + case 'gotpointercapture': + case 'lostpointercapture': { + if (sMethod != 'mouse') { + return oDOM.addEventListener(sMethod + sEvent, fCall, capture) } - return true + return; // Add return to prevent fallthrough + } + // not "pointer" || "mouse" + default: + return oDOM.addEventListener(sEvent, fCall, capture) + } + } + pointerListenerRemove(oDOM: Node, sEvent: string, fCall: (e: Event) => boolean | void, capture = false): void { + if (!oDOM || !oDOM.removeEventListener || !sEvent || typeof fCall !== 'function') return + + switch (sEvent) { + //both pointer and move events + case 'down': + case 'up': + case 'move': + case 'over': + case 'out': + case 'enter': { + if (this.pointerevents_method == 'pointer' || this.pointerevents_method == 'mouse') { + oDOM.removeEventListener(this.pointerevents_method + sEvent, fCall, capture) + } + return; // Add return to prevent fallthrough + } + + // only pointerevents + case 'leave': + case 'cancel': + case 'gotpointercapture': + case 'lostpointercapture': { + if (this.pointerevents_method == 'pointer') { + return oDOM.removeEventListener(this.pointerevents_method + sEvent, fCall, capture) + } + return; // Add return to prevent fallthrough + } + // not "pointer" || "mouse" + default: + return oDOM.removeEventListener(sEvent, fCall, capture) + } + } + + getTime: () => number + + compareObjects(a: object, b: object): boolean { + for (const i in a) { + if (a[i] != b[i]) return false + } + return true + } + + distance = distance + + colorToString(c: [number, number, number, number]): string { + 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') + + ')' + ) + } + + isInsideRectangle = isInsideRectangle + + //[minx,miny,maxx,maxy] + growBounding(bounding: Rect, x: number, y: number): void { + if (x < bounding[0]) { + bounding[0] = x + } else if (x > bounding[2]) { + bounding[2] = x } - distance = distance + if (y < bounding[1]) { + bounding[1] = y + } else if (y > bounding[3]) { + bounding[3] = y + } + } - colorToString(c: [number, number, number, number]): string { - 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") + - ")" - ) + overlapBounding = overlapBounding + + //point inside bounding box + isInsideBounding(p: number[], bb: number[][]): boolean { + 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 + } + + //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. + hex2num(hex: string): number[] { + if (hex.charAt(0) == '#') { + hex = hex.slice(1) + } //Remove the '#' char - if there is one. + hex = hex.toUpperCase() + const hex_alphabets = '0123456789ABCDEF' + const value = new Array(3) + let k = 0 + let int1, int2 + for (let 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 + } + + //Give a array with three values as the argument and the function will return + // the corresponding hex triplet. + num2hex(triplet: number[]): string { + const hex_alphabets = '0123456789ABCDEF' + let hex = '#' + let int1, int2 + for (let i = 0; i < 3; i++) { + int1 = triplet[i] / 16 + int2 = triplet[i] % 16 + + hex += hex_alphabets.charAt(int1) + hex_alphabets.charAt(int2) + } + return hex + } + + closeAllContextMenus(ref_window: Window): void { + ref_window = ref_window || window + + const elements = ref_window.document.querySelectorAll('.litecontextmenu') + if (!elements.length) return + + const result = [] + for (let i = 0; i < elements.length; i++) { + result.push(elements[i]) } - isInsideRectangle = isInsideRectangle + for (let i = 0; i < result.length; i++) { + if (result[i].close) { + result[i].close() + } else if (result[i].parentNode) { + result[i].parentNode.removeChild(result[i]) + } + } + } - //[minx,miny,maxx,maxy] - growBounding(bounding: Rect, x: number, y: number): void { - 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 - } + extendClass(target: any, origin: any): void { + for (const i in origin) { + //copy class properties + if (target.hasOwnProperty(i)) continue + target[i] = origin[i] } - overlapBounding = overlapBounding + if (origin.prototype) { + //copy prototype properties + for (const i in origin.prototype) { + //only enumerable + if (!origin.prototype.hasOwnProperty(i)) continue - //point inside bounding box - isInsideBounding(p: number[], bb: number[][]): boolean { - 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 - } - - //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. - hex2num(hex: string): number[] { - if (hex.charAt(0) == "#") { - hex = hex.slice(1) - } //Remove the '#' char - if there is one. - hex = hex.toUpperCase() - const hex_alphabets = "0123456789ABCDEF" - const value = new Array(3) - let k = 0 - let int1, int2 - for (let 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 - } - - //Give a array with three values as the argument and the function will return - // the corresponding hex triplet. - num2hex(triplet: number[]): string { - const hex_alphabets = "0123456789ABCDEF" - let hex = "#" - let int1, int2 - for (let i = 0; i < 3; i++) { - int1 = triplet[i] / 16 - int2 = triplet[i] % 16 - - hex += hex_alphabets.charAt(int1) + hex_alphabets.charAt(int2) - } - return hex - } - - closeAllContextMenus(ref_window: Window): void { - ref_window = ref_window || window - - const elements = ref_window.document.querySelectorAll(".litecontextmenu") - if (!elements.length) return - - const result = [] - for (let i = 0; i < elements.length; i++) { - result.push(elements[i]) - } - - for (let i = 0; i < result.length; i++) { - if (result[i].close) { - result[i].close() - } else if (result[i].parentNode) { - result[i].parentNode.removeChild(result[i]) - } - } - } - - extendClass(target: any, origin: any): void { - for (const i in origin) { - //copy class properties - if (target.hasOwnProperty(i)) continue - target[i] = origin[i] - } - - if (origin.prototype) { - //copy prototype properties - for (const i in origin.prototype) { - //only enumerable - if (!origin.prototype.hasOwnProperty(i)) continue - - //avoid overwriting existing ones - if (target.prototype.hasOwnProperty(i)) 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) - ) - } - } + //avoid overwriting existing ones + if (target.prototype.hasOwnProperty(i)) 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)) + } + } } + } } diff --git a/src/MapProxyHandler.ts b/src/MapProxyHandler.ts index 68c59bb83..65f7f1786 100644 --- a/src/MapProxyHandler.ts +++ b/src/MapProxyHandler.ts @@ -1,55 +1,56 @@ /** Temporary workaround until downstream consumers migrate to Map. A brittle wrapper with many flaws, but should be fine for simple maps using int indexes. */ export class MapProxyHandler implements ProxyHandler> { - getOwnPropertyDescriptor(target: Map, p: string | symbol): PropertyDescriptor | undefined { - const value = this.get(target, p) - if (value) return { - configurable: true, - enumerable: true, - value - } - } + getOwnPropertyDescriptor(target: Map, p: string | symbol): PropertyDescriptor | undefined { + const value = this.get(target, p) + if (value) + return { + configurable: true, + enumerable: true, + value + } + } - has(target: Map, p: string | symbol): boolean { - if (typeof p === "symbol") return false + has(target: Map, p: string | symbol): boolean { + if (typeof p === 'symbol') return false - const int = parseInt(p, 10) - return target.has(!isNaN(int) ? int : p) - } + const int = parseInt(p, 10) + return target.has(!isNaN(int) ? int : p) + } - ownKeys(target: Map): ArrayLike { - return [...target.keys()].map(x => String(x)) - } + ownKeys(target: Map): ArrayLike { + return [...target.keys()].map((x) => String(x)) + } - get(target: Map, p: string | symbol): any { - // Workaround does not support link IDs of "values", "entries", "constructor", etc. - if (p in target) return Reflect.get(target, p, target) - if (typeof p === "symbol") return + get(target: Map, p: string | symbol): any { + // Workaround does not support link IDs of "values", "entries", "constructor", etc. + if (p in target) return Reflect.get(target, p, target) + if (typeof p === 'symbol') return - const int = parseInt(p, 10) - return target.get(!isNaN(int) ? int : p) - } + const int = parseInt(p, 10) + return target.get(!isNaN(int) ? int : p) + } - set(target: Map, p: string | symbol, newValue: any): boolean { - if (typeof p === "symbol") return false + set(target: Map, p: string | symbol, newValue: any): boolean { + if (typeof p === 'symbol') return false - const int = parseInt(p, 10) - target.set(!isNaN(int) ? int : p, newValue) - return true - } + const int = parseInt(p, 10) + target.set(!isNaN(int) ? int : p, newValue) + return true + } - deleteProperty(target: Map, p: string | symbol): boolean { - return target.delete(p as number | string) - } + deleteProperty(target: Map, p: string | symbol): boolean { + return target.delete(p as number | string) + } - static bindAllMethods(map: Map): void { - map.clear = map.clear.bind(map) - map.delete = map.delete.bind(map) - map.forEach = map.forEach.bind(map) - map.get = map.get.bind(map) - map.has = map.has.bind(map) - map.set = map.set.bind(map) - map.entries = map.entries.bind(map) - map.keys = map.keys.bind(map) - map.values = map.values.bind(map) - } + static bindAllMethods(map: Map): void { + map.clear = map.clear.bind(map) + map.delete = map.delete.bind(map) + map.forEach = map.forEach.bind(map) + map.get = map.get.bind(map) + map.has = map.has.bind(map) + map.set = map.set.bind(map) + map.entries = map.entries.bind(map) + map.keys = map.keys.bind(map) + map.values = map.values.bind(map) + } } diff --git a/src/draw.ts b/src/draw.ts index 8a6e74ed6..5c28ec8ae 100644 --- a/src/draw.ts +++ b/src/draw.ts @@ -1,9 +1,9 @@ -import type { Vector2 } from "./litegraph"; -import type { INodeSlot } from "./interfaces" -import { LinkDirection, RenderShape } from "./types/globalEnums" +import type { Vector2 } from './litegraph' +import type { INodeSlot } from './interfaces' +import { LinkDirection, RenderShape } from './types/globalEnums' export enum SlotType { - Array = "array", + Array = 'array', Event = -1, } @@ -25,8 +25,8 @@ export enum SlotDirection { } export enum LabelPosition { - Left = "left", - Right = "right", + Left = 'left', + Right = 'right', } export function drawSlot( @@ -34,7 +34,7 @@ export function drawSlot( slot: Partial, pos: Vector2, { - label_color = "#AAA", + label_color = '#AAA', label_position = LabelPosition.Right, horizontal = false, low_quality = false, @@ -42,44 +42,42 @@ export function drawSlot( do_stroke = false, highlight = false, }: { - label_color?: string; - label_position?: LabelPosition; - horizontal?: boolean; - low_quality?: boolean; - render_text?: boolean; - do_stroke?: boolean; - highlight?: boolean; - } = {} + label_color?: string + label_position?: LabelPosition + horizontal?: boolean + low_quality?: boolean + render_text?: boolean + do_stroke?: boolean + highlight?: boolean + } = {}, ) { // Save the current fillStyle and strokeStyle - const originalFillStyle = ctx.fillStyle; - const originalStrokeStyle = ctx.strokeStyle; - const originalLineWidth = ctx.lineWidth; + const originalFillStyle = ctx.fillStyle + const originalStrokeStyle = ctx.strokeStyle + const originalLineWidth = ctx.lineWidth - const slot_type = slot.type as SlotType; - const slot_shape = ( - slot_type === SlotType.Array ? SlotShape.Grid : slot.shape - ) as SlotShape; + const slot_type = slot.type as SlotType + const slot_shape = (slot_type === SlotType.Array ? SlotShape.Grid : slot.shape) as SlotShape - ctx.beginPath(); - let doStroke = do_stroke; - let doFill = true; + ctx.beginPath() + let doStroke = do_stroke + let doFill = true if (slot_type === SlotType.Event || slot_shape === SlotShape.Box) { if (horizontal) { - ctx.rect(pos[0] - 5 + 0.5, pos[1] - 8 + 0.5, 10, 14); + 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); + ctx.rect(pos[0] - 6 + 0.5, pos[1] - 5 + 0.5, 14, 10) } } else if (slot_shape === SlotShape.Arrow) { - 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(); + 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 === SlotShape.Grid) { - const gridSize = 3; - const cellSize = 2; - const spacing = 3; + const gridSize = 3 + const cellSize = 2 + const spacing = 3 for (let x = 0; x < gridSize; x++) { for (let y = 0; y < gridSize; y++) { @@ -88,58 +86,58 @@ export function drawSlot( pos[1] - 4 + y * spacing, cellSize, cellSize - ); + ) } } - doStroke = false; + doStroke = false } else { // Default rendering for circle, hollow circle. if (low_quality) { - ctx.rect(pos[0] - 4, pos[1] - 4, 8, 8); + ctx.rect(pos[0] - 4, pos[1] - 4, 8, 8) } else { - let radius: number; + let radius: number if (slot_shape === SlotShape.HollowCircle) { - doFill = false; - doStroke = true; - ctx.lineWidth = 3; - ctx.strokeStyle = ctx.fillStyle; - radius = highlight ? 4 : 3; + doFill = false + doStroke = true + ctx.lineWidth = 3 + ctx.strokeStyle = ctx.fillStyle + radius = highlight ? 4 : 3 } else { // Normal circle - radius = highlight ? 5 : 4; + radius = highlight ? 5 : 4 } - ctx.arc(pos[0], pos[1], radius, 0, Math.PI * 2); + ctx.arc(pos[0], pos[1], radius, 0, Math.PI * 2) } } - if (doFill) ctx.fill(); - if (!low_quality && doStroke) ctx.stroke(); + if (doFill) ctx.fill() + if (!low_quality && doStroke) ctx.stroke() // render slot label if (render_text) { - const text = slot.label != null ? slot.label : slot.name; + const text = slot.label != null ? slot.label : slot.name if (text) { // TODO: Finish impl. Highlight text on mouseover unless we're connecting links. - ctx.fillStyle = label_color; + ctx.fillStyle = label_color if (label_position === LabelPosition.Right) { if (horizontal || slot.dir == LinkDirection.UP) { - ctx.fillText(text, pos[0], pos[1] - 10); + ctx.fillText(text, pos[0], pos[1] - 10) } else { - ctx.fillText(text, pos[0] + 10, pos[1] + 5); + ctx.fillText(text, pos[0] + 10, pos[1] + 5) } } else { if (horizontal || slot.dir == LinkDirection.DOWN) { - ctx.fillText(text, pos[0], pos[1] - 8); + ctx.fillText(text, pos[0], pos[1] - 8) } else { - ctx.fillText(text, pos[0] - 10, pos[1] + 5); + ctx.fillText(text, pos[0] - 10, pos[1] + 5) } } } } // Restore the original fillStyle and strokeStyle - ctx.fillStyle = originalFillStyle; - ctx.strokeStyle = originalStrokeStyle; - ctx.lineWidth = originalLineWidth; + ctx.fillStyle = originalFillStyle + ctx.strokeStyle = originalStrokeStyle + ctx.lineWidth = originalLineWidth } diff --git a/src/interfaces.ts b/src/interfaces.ts index 68036b500..5af796836 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1,29 +1,29 @@ -import type { ContextMenu } from "./ContextMenu" -import type { LGraphNode } from "./LGraphNode" -import type { LinkDirection, RenderShape } from "./types/globalEnums" -import type { LinkId } from "./LLink" +import type { ContextMenu } from './ContextMenu' +import type { LGraphNode } from './LGraphNode' +import type { LinkDirection, RenderShape } from './types/globalEnums' +import type { LinkId } from './LLink' export type Dictionary = { [key: string]: T } /** Allows all properties to be null. The same as `Partial`, but adds null instead of undefined. */ export type NullableProperties = { - [P in keyof T]: T[P] | null + [P in keyof T]: T[P] | null } export type CanvasColour = string | CanvasGradient | CanvasPattern export interface IInputOrOutput { - // If an input, this will be defined - input?: INodeInputSlot - // If an output, this will be defined - output?: INodeOutputSlot + // If an input, this will be defined + input?: INodeInputSlot + // If an output, this will be defined + output?: INodeOutputSlot } export interface IFoundSlot extends IInputOrOutput { - // Slot index - slot: number - // Centre point of the rendered slot connection - link_pos: Point + // Slot index + slot: number + // Centre point of the rendered slot connection + link_pos: Point } /** A point represented as `[x, y]` co-ordinates */ @@ -42,13 +42,31 @@ export type Rect = ArRect | Float32Array | Float64Array export type Rect32 = Float32Array /** A point represented as `[x, y]` co-ordinates that will not be modified */ -export type ReadOnlyPoint = readonly [x: number, y: number] | ReadOnlyTypedArray | ReadOnlyTypedArray +export type ReadOnlyPoint = + | readonly [x: number, y: number] + | ReadOnlyTypedArray + | ReadOnlyTypedArray /** A rectangle starting at top-left coordinates `[x, y, width, height]` that will not be modified */ -export type ReadOnlyRect = readonly [x: number, y: number, width: number, height: number] | ReadOnlyTypedArray | ReadOnlyTypedArray +export type ReadOnlyRect = + | readonly [x: number, y: number, width: number, height: number] + | ReadOnlyTypedArray + | ReadOnlyTypedArray -type TypedArrays = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array +type TypedArrays = + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | Float32Array + | Float64Array type TypedBigIntArrays = BigInt64Array | BigUint64Array -type ReadOnlyTypedArray = Omit +type ReadOnlyTypedArray = Omit< + T, + 'fill' | 'copyWithin' | 'reverse' | 'set' | 'sort' | 'subarray' +> /** Union of property names that are of type Match */ export type KeysOfType = { [P in keyof T]: T[P] extends Match ? P : never }[keyof T] @@ -60,96 +78,102 @@ export type PickByType = { [P in keyof T]: Extract } export type MethodNames = KeysOfType any) | undefined> export interface IBoundaryNodes { - top: LGraphNode - right: LGraphNode - bottom: LGraphNode - left: LGraphNode + top: LGraphNode + right: LGraphNode + bottom: LGraphNode + left: LGraphNode } -export type Direction = "top" | "bottom" | "left" | "right" +export type Direction = 'top' | 'bottom' | 'left' | 'right' export interface IOptionalSlotData { - content: string - value: TSlot - className?: string + content: string + value: TSlot + className?: string } export type ISlotType = number | string export interface INodeSlot { - name: string - type: ISlotType - dir?: LinkDirection - removable?: boolean - shape?: RenderShape - not_subgraph_input?: boolean - color_off?: CanvasColour - color_on?: CanvasColour - label?: string - locked?: boolean - nameLocked?: boolean - pos?: Point - widget?: unknown + name: string + type: ISlotType + dir?: LinkDirection + removable?: boolean + shape?: RenderShape + not_subgraph_input?: boolean + color_off?: CanvasColour + color_on?: CanvasColour + label?: string + locked?: boolean + nameLocked?: boolean + pos?: Point + widget?: unknown } export interface INodeFlags { - skip_repeated_outputs?: boolean - allow_interaction?: boolean - pinned?: boolean - collapsed?: boolean + skip_repeated_outputs?: boolean + allow_interaction?: boolean + pinned?: boolean + collapsed?: boolean } export interface INodeInputSlot extends INodeSlot { - link: LinkId | null - not_subgraph_input?: boolean + link: LinkId | null + not_subgraph_input?: boolean } export interface INodeOutputSlot extends INodeSlot { - links: LinkId[] | null - _data?: unknown - slot_index?: number - not_subgraph_output?: boolean + links: LinkId[] | null + _data?: unknown + slot_index?: number + not_subgraph_output?: boolean } /** Links */ export interface ConnectingLink extends IInputOrOutput { - node: LGraphNode - slot: number - pos: Point - direction?: LinkDirection + node: LGraphNode + slot: number + pos: Point + direction?: LinkDirection } interface IContextMenuBase { - title?: string - className?: string - callback?(value?: unknown, options?: unknown, event?: MouseEvent, previous_menu?: ContextMenu, node?: LGraphNode): void | boolean + title?: string + className?: string + callback?( + value?: unknown, + options?: unknown, + event?: MouseEvent, + previous_menu?: ContextMenu, + node?: LGraphNode, + ): void | boolean } /** ContextMenu */ export interface IContextMenuOptions extends IContextMenuBase { - ignore_item_callbacks?: boolean - parentMenu?: ContextMenu - event?: MouseEvent - extra?: unknown - scroll_speed?: number - left?: number - top?: number - scale?: string - node?: LGraphNode - autoopen?: boolean + ignore_item_callbacks?: boolean + parentMenu?: ContextMenu + event?: MouseEvent + extra?: unknown + scroll_speed?: number + left?: number + top?: number + scale?: string + node?: LGraphNode + autoopen?: boolean } export interface IContextMenuValue extends IContextMenuBase { - value?: string - content: string - has_submenu?: boolean - disabled?: boolean - submenu?: IContextMenuSubmenu - property?: string - type?: string - slot?: IFoundSlot + value?: string + content: string + has_submenu?: boolean + disabled?: boolean + submenu?: IContextMenuSubmenu + property?: string + type?: string + slot?: IFoundSlot } export interface IContextMenuSubmenu extends IContextMenuOptions { - options: ConstructorParameters[0] + options: ConstructorParameters[0] } diff --git a/src/litegraph.ts b/src/litegraph.ts index 0a27d69c3..1185578de 100644 --- a/src/litegraph.ts +++ b/src/litegraph.ts @@ -1,32 +1,73 @@ -import type { Point, ConnectingLink } from "./interfaces" -import type { INodeSlot, INodeInputSlot, INodeOutputSlot, CanvasColour, Direction, IBoundaryNodes, IContextMenuOptions, IContextMenuValue, IFoundSlot, IInputOrOutput, INodeFlags, IOptionalSlotData, ISlotType, KeysOfType, MethodNames, PickByType, Rect, Rect32, Size } from "./interfaces" -import type { SlotShape, LabelPosition, SlotDirection, SlotType } from "./draw" -import type { IWidget } from "./types/widgets" -import type { RenderShape, TitleMode } from "./types/globalEnums" -import type { CanvasEventDetail } from "./types/events" -import { LiteGraphGlobal } from "./LiteGraphGlobal" -import { loadPolyfills } from "./polyfills" +import type { Point, ConnectingLink } from './interfaces' +import type { + INodeSlot, + INodeInputSlot, + INodeOutputSlot, + CanvasColour, + Direction, + IBoundaryNodes, + IContextMenuOptions, + IContextMenuValue, + IFoundSlot, + IInputOrOutput, + INodeFlags, + IOptionalSlotData, + ISlotType, + KeysOfType, + MethodNames, + PickByType, + Rect, + Rect32, + Size, +} from './interfaces' +import type { SlotShape, LabelPosition, SlotDirection, SlotType } from './draw' +import type { IWidget } from './types/widgets' +import type { RenderShape, TitleMode } from './types/globalEnums' +import type { CanvasEventDetail } from './types/events' +import { LiteGraphGlobal } from './LiteGraphGlobal' +import { loadPolyfills } from './polyfills' -import { LGraph } from "./LGraph" -import { LGraphCanvas, type LGraphCanvasState } from "./LGraphCanvas" -import { DragAndScale } from "./DragAndScale" -import { LGraphNode } from "./LGraphNode" -import { LGraphGroup } from "./LGraphGroup" -import { LLink } from "./LLink" -import { ContextMenu } from "./ContextMenu" -import { CurveEditor } from "./CurveEditor" -import { LGraphBadge, BadgePosition } from "./LGraphBadge" +import { LGraph } from './LGraph' +import { LGraphCanvas, type LGraphCanvasState } from './LGraphCanvas' +import { DragAndScale } from './DragAndScale' +import { LGraphNode } from './LGraphNode' +import { LGraphGroup } from './LGraphGroup' +import { LLink } from './LLink' +import { ContextMenu } from './ContextMenu' +import { CurveEditor } from './CurveEditor' +import { LGraphBadge, BadgePosition } from './LGraphBadge' export const LiteGraph = new LiteGraphGlobal() export { LGraph, LGraphCanvas, LGraphCanvasState, DragAndScale, LGraphNode, LGraphGroup, LLink, ContextMenu, CurveEditor } -export { INodeSlot, INodeInputSlot, INodeOutputSlot, ConnectingLink, CanvasColour, Direction, IBoundaryNodes, IContextMenuOptions, IContextMenuValue, IFoundSlot, IInputOrOutput, INodeFlags, IOptionalSlotData, ISlotType, KeysOfType, MethodNames, PickByType, Rect, Rect32, Size } +export { + INodeSlot, + INodeInputSlot, + INodeOutputSlot, + ConnectingLink, + CanvasColour, + Direction, + IBoundaryNodes, + IContextMenuOptions, + IContextMenuValue, + IFoundSlot, + IInputOrOutput, + INodeFlags, + IOptionalSlotData, + ISlotType, + KeysOfType, + MethodNames, + PickByType, + Rect, + Rect32, + Size, +} export { IWidget } export { LGraphBadge, BadgePosition } export { SlotShape, LabelPosition, SlotDirection, SlotType } export function clamp(v: number, a: number, b: number): number { - return a > v ? a : b < v ? b : v -}; + return a > v ? a : b < v ? b : v +} // Load legacy polyfills loadPolyfills() @@ -40,67 +81,69 @@ export type Vector2 = Point export type Vector4 = [number, number, number, number] export interface IContextMenuItem { - content: string - callback?: ContextMenuEventListener - /** Used as innerHTML for extra child element */ - title?: string - disabled?: boolean - has_submenu?: boolean - submenu?: { - options: IContextMenuItem[] - } & IContextMenuOptions - className?: string + content: string + callback?: ContextMenuEventListener + /** Used as innerHTML for extra child element */ + title?: string + disabled?: boolean + has_submenu?: boolean + submenu?: { + options: IContextMenuItem[] + } & IContextMenuOptions + className?: string } export type ContextMenuEventListener = ( - value: IContextMenuItem, - options: IContextMenuOptions, - event: MouseEvent, - parentMenu: ContextMenu | undefined, - node: LGraphNode + value: IContextMenuItem, + options: IContextMenuOptions, + event: MouseEvent, + parentMenu: ContextMenu | undefined, + node: LGraphNode, ) => boolean | void export interface LinkReleaseContext { - node_to?: LGraphNode - node_from?: LGraphNode - slot_from: INodeSlot - type_filter_in?: string - type_filter_out?: string + node_to?: LGraphNode + node_from?: LGraphNode + slot_from: INodeSlot + type_filter_in?: string + type_filter_out?: string } export interface LinkReleaseContextExtended { - links: ConnectingLink[] + links: ConnectingLink[] } /** @deprecated Confirm no downstream consumers, then remove. */ -export type LiteGraphCanvasEventType = "empty-release" | "empty-double-click" | "group-double-click" +export type LiteGraphCanvasEventType = 'empty-release' | 'empty-double-click' | 'group-double-click' -export interface LiteGraphCanvasEvent extends CustomEvent { } +export interface LiteGraphCanvasEvent extends CustomEvent {} -export interface LiteGraphCanvasGroupEvent extends CustomEvent<{ - subType: "group-double-click" +export interface LiteGraphCanvasGroupEvent + extends CustomEvent<{ + subType: 'group-double-click' originalEvent: MouseEvent group: LGraphGroup -}> { } + }> {} /** https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#lgraphnode */ export interface LGraphNodeConstructor { - title?: string - type?: string - size?: Size - min_height?: number - slot_start_y?: number - widgets_info?: any - collapsable?: boolean - color?: string - bgcolor?: string - shape?: RenderShape - title_mode?: TitleMode - title_color?: string - title_text_color?: string - nodeData: any - new(): T + title?: string + type?: string + size?: Size + min_height?: number + slot_start_y?: number + widgets_info?: any + collapsable?: boolean + color?: string + bgcolor?: string + shape?: RenderShape + title_mode?: TitleMode + title_color?: string + title_text_color?: string + desc?: string + nodeData: any + new (): T } // End backwards compat diff --git a/src/measure.ts b/src/measure.ts index d02fd6964..5842c94a6 100644 --- a/src/measure.ts +++ b/src/measure.ts @@ -1,5 +1,5 @@ -import type { Point, ReadOnlyPoint, ReadOnlyRect } from "./interfaces" -import { LinkDirection } from "./types/globalEnums" +import type { Point, ReadOnlyPoint, ReadOnlyRect } from './interfaces' +import { LinkDirection } from './types/globalEnums' /** * Calculates the distance between two points (2D vector) @@ -8,9 +8,9 @@ import { LinkDirection } from "./types/globalEnums" * @returns Distance between point {@link a} & {@link b} */ export function distance(a: ReadOnlyPoint, b: ReadOnlyPoint): number { - return Math.sqrt( - (b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1]) - ) + return Math.sqrt( + (b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1]) + ) } /** @@ -21,7 +21,9 @@ export function distance(a: ReadOnlyPoint, b: ReadOnlyPoint): number { * @returns Distance2 (squared) between point {@link a} & {@link b} */ export function dist2(a: ReadOnlyPoint, b: ReadOnlyPoint): number { - return ((b[0] - a[0]) * (b[0] - a[0])) + ((b[1] - a[1]) * (b[1] - a[1])) + return ( + (b[0] - a[0]) * (b[0] - a[0])) + ((b[1] - a[1]) * (b[1] - a[1]) + ) } /** @@ -31,10 +33,12 @@ export function dist2(a: ReadOnlyPoint, b: ReadOnlyPoint): number { * @returns `true` if the point is inside the rect, otherwise `false` */ export function isPointInRectangle(point: ReadOnlyPoint, rect: ReadOnlyRect): boolean { - return rect[0] < point[0] - && rect[0] + rect[2] > point[0] - && rect[1] < point[1] - && rect[1] + rect[3] > point[1] + return ( + rect[0] < point[0] && + rect[0] + rect[2] > point[0] && + rect[1] < point[1] && + rect[1] + rect[3] > point[1] + ) } /** @@ -48,10 +52,7 @@ export function isPointInRectangle(point: ReadOnlyPoint, rect: ReadOnlyRect): bo * @returns `true` if the point is inside the rect, otherwise `false` */ export function isInsideRectangle(x: number, y: number, left: number, top: number, width: number, height: number): boolean { - return left < x - && left + width > x - && top < y - && top + height > y + return left < x && left + width > x && top < y && top + height > y } /** @@ -62,8 +63,8 @@ export function isInsideRectangle(x: number, y: number, left: number, top: numbe * @returns `true` if the point is roughly inside the octagon centred on 0,0 with specified radius */ export function isSortaInsideOctagon(x: number, y: number, radius: number): boolean { - const sum = Math.min(radius, Math.abs(x)) + Math.min(radius, Math.abs(y)) - return sum < radius * 0.75 + const sum = Math.min(radius, Math.abs(x)) + Math.min(radius, Math.abs(y)) + return sum < radius * 0.75 } /** @@ -73,17 +74,17 @@ export function isSortaInsideOctagon(x: number, y: number, radius: number): bool * @returns `true` if rectangles overlap, otherwise `false` */ export function overlapBounding(a: ReadOnlyRect, b: ReadOnlyRect): boolean { - const aRight = a[0] + a[2] - const aBottom = a[1] + a[3] - const bRight = b[0] + b[2] - const bBottom = b[1] + b[3] + const aRight = a[0] + a[2] + const aBottom = a[1] + a[3] + const bRight = b[0] + b[2] + const bBottom = b[1] + b[3] - return a[0] > bRight - || a[1] > bBottom - || aRight < b[0] - || aBottom < b[1] - ? false - : true + return ( + a[0] > bRight || + a[1] > bBottom || + aRight < b[0] || + aBottom < b[1] + ) ? false : true } /** @@ -93,9 +94,9 @@ export function overlapBounding(a: ReadOnlyRect, b: ReadOnlyRect): boolean { * @returns `true` if {@link a} contains most of {@link b}, otherwise `false` */ export function containsCentre(a: ReadOnlyRect, b: ReadOnlyRect): boolean { - const centreX = b[0] + (b[2] * 0.5) - const centreY = b[1] + (b[3] * 0.5) - return isInsideRectangle(centreX, centreY, a[0], a[1], a[2], a[3]) + const centreX = b[0] + (b[2] * 0.5) + const centreY = b[1] + (b[3] * 0.5) + return isInsideRectangle(centreX, centreY, a[0], a[1], a[2], a[3]) } /** @@ -105,15 +106,17 @@ export function containsCentre(a: ReadOnlyRect, b: ReadOnlyRect): boolean { * @returns `true` if {@link a} wholly contains {@link b}, otherwise `false` */ export function containsRect(a: ReadOnlyRect, b: ReadOnlyRect): boolean { - const aRight = a[0] + a[2] - const aBottom = a[1] + a[3] - const bRight = b[0] + b[2] - const bBottom = b[1] + b[3] + const aRight = a[0] + a[2] + const aBottom = a[1] + a[3] + const bRight = b[0] + b[2] + const bBottom = b[1] + b[3] - return a[0] < b[0] - && a[1] < b[1] - && aRight > bRight - && aBottom > bBottom + return ( + a[0] < b[0] && + a[1] < b[1] && + aRight > bRight && + aBottom > bBottom + ) } /** @@ -123,85 +126,85 @@ export function containsRect(a: ReadOnlyRect, b: ReadOnlyRect): boolean { * @param out The {@link Point} to add the offset to */ export function addDirectionalOffset(amount: number, direction: LinkDirection, out: Point): void { - switch (direction) { - case LinkDirection.LEFT: - out[0] -= amount - return - case LinkDirection.RIGHT: - out[0] += amount - return - case LinkDirection.UP: - out[1] -= amount - return - case LinkDirection.DOWN: - out[1] += amount - return - // LinkDirection.CENTER: Nothing to do. - } + switch (direction) { + case LinkDirection.LEFT: + out[0] -= amount + return + case LinkDirection.RIGHT: + out[0] += amount + return + case LinkDirection.UP: + out[1] -= amount + return + case LinkDirection.DOWN: + out[1] += amount + return + // LinkDirection.CENTER: Nothing to do. + } } /** * Rotates an offset in 90° increments. - * + * * Swaps/flips axis values of a 2D vector offset - effectively rotating {@link offset} by 90° * @param offset The zero-based offset to rotate * @param from Direction to rotate from * @param to Direction to rotate to */ export function rotateLink(offset: Point, from: LinkDirection, to: LinkDirection): void { - let x: number - let y: number + let x: number + let y: number - // Normalise to left - switch (from) { - case to: - case LinkDirection.CENTER: - case LinkDirection.NONE: - // Nothing to do - return + // Normalise to left + switch (from) { + case to: + case LinkDirection.CENTER: + case LinkDirection.NONE: + // Nothing to do + return - case LinkDirection.LEFT: - x = offset[0] - y = offset[1] - break - case LinkDirection.RIGHT: - x = -offset[0] - y = -offset[1] - break - case LinkDirection.UP: - x = -offset[1] - y = offset[0] - break - case LinkDirection.DOWN: - x = offset[1] - y = -offset[0] - break - } + case LinkDirection.LEFT: + x = offset[0] + y = offset[1] + break + case LinkDirection.RIGHT: + x = -offset[0] + y = -offset[1] + break + case LinkDirection.UP: + x = -offset[1] + y = offset[0] + break + case LinkDirection.DOWN: + x = offset[1] + y = -offset[0] + break + } - // Apply new direction - switch (to) { - case LinkDirection.CENTER: - case LinkDirection.NONE: - // Nothing to do - return + // Apply new direction + switch (to) { + case LinkDirection.CENTER: + case LinkDirection.NONE: + // Nothing to do + return - case LinkDirection.LEFT: - offset[0] = x - offset[1] = y - break - case LinkDirection.RIGHT: - offset[0] = -x - offset[1] = -y - break - case LinkDirection.UP: - offset[0] = y - offset[1] = -x - break - case LinkDirection.DOWN: - offset[0] = -y - offset[1] = x - break - } + case LinkDirection.LEFT: + offset[0] = x + offset[1] = y + break + case LinkDirection.RIGHT: + offset[0] = -x + offset[1] = -y + break + case LinkDirection.UP: + offset[0] = y + offset[1] = -x + break + case LinkDirection.DOWN: + offset[0] = -y + offset[1] = x + break + } } /** @@ -214,11 +217,13 @@ export function rotateLink(offset: Point, from: LinkDirection, to: LinkDirection * @returns 0 if all three points are in a straight line, a negative value if point is to the left of the projected line, or positive if the point is to the right */ export function getOrientation(lineStart: ReadOnlyPoint, lineEnd: ReadOnlyPoint, x: number, y: number): number { - return ((lineEnd[1] - lineStart[1]) * (x - lineEnd[0])) - ((lineEnd[0] - lineStart[0]) * (y - lineEnd[1])) + return ( + (lineEnd[1] - lineStart[1]) * (x - lineEnd[0])) - ((lineEnd[0] - lineStart[0]) * (y - lineEnd[1]) + ) } /** - * + * * @param out The array to store the point in * @param a Start point * @param b End point @@ -227,20 +232,20 @@ export function getOrientation(lineStart: ReadOnlyPoint, lineEnd: ReadOnlyPoint, * @param t Time: factor of distance to travel along the curve (e.g 0.25 is 25% along the curve) */ export function findPointOnCurve( - out: Point, - a: ReadOnlyPoint, - b: ReadOnlyPoint, - controlA: ReadOnlyPoint, - controlB: ReadOnlyPoint, - t: number = 0.5, + out: Point, + a: ReadOnlyPoint, + b: ReadOnlyPoint, + controlA: ReadOnlyPoint, + controlB: ReadOnlyPoint, + t: number = 0.5 ): void { - const iT = 1 - t + const iT = 1 - t - const c1 = iT * iT * iT - const c2 = 3 * (iT * iT) * t - const c3 = 3 * iT * (t * t) - const c4 = t * t * t + const c1 = iT * iT * iT + const c2 = 3 * (iT * iT) * t + const c3 = 3 * iT * (t * t) + const c4 = t * t * t - out[0] = (c1 * a[0]) + (c2 * controlA[0]) + (c3 * controlB[0]) + (c4 * b[0]) - out[1] = (c1 * a[1]) + (c2 * controlA[1]) + (c3 * controlB[1]) + (c4 * b[1]) + out[0] = (c1 * a[0]) + (c2 * controlA[0]) + (c3 * controlB[0]) + (c4 * b[0]) + out[1] = (c1 * a[1]) + (c2 * controlA[1]) + (c3 * controlB[1]) + (c4 * b[1]) } diff --git a/src/polyfills.ts b/src/polyfills.ts index 758e3d1a8..e33e2f422 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -1,85 +1,80 @@ - //API ************************************************* //like rect but rounded corners -export function loadPolyfills() { -if (typeof (window) != "undefined" && window.CanvasRenderingContext2D && !window.CanvasRenderingContext2D.prototype.roundRect) { - // @ts-expect-error Slightly broken polyfill - radius_low not impl. anywhere - window.CanvasRenderingContext2D.prototype.roundRect = function ( - x, - y, - w, - h, - radius, - radius_low - ) { - let top_left_radius = 0; - let top_right_radius = 0; - let bottom_left_radius = 0; - let 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 - -if (typeof window != "undefined" && !window["requestAnimationFrame"]) { - window.requestAnimationFrame = - // @ts-expect-error Legacy code - window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || - function (callback) { - window.setTimeout(callback, 1000 / 60); - }; +declare global { + interface Window { + webkitRequestAnimationFrame?: (callback: FrameRequestCallback) => number + mozRequestAnimationFrame?: (callback: FrameRequestCallback) => number + } +} + +export function loadPolyfills() { + if (typeof window != 'undefined' && window.CanvasRenderingContext2D && !window.CanvasRenderingContext2D.prototype.roundRect) { + // @ts-expect-error Slightly broken polyfill - radius_low not impl. anywhere + window.CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, radius, radius_low) { + let top_left_radius = 0 + let top_right_radius = 0 + let bottom_left_radius = 0 + let 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 + } //old using numbers + else { + 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 + + if (typeof window != 'undefined' && !window['requestAnimationFrame']) { + const RAF = ( + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + function (callback) { + window.setTimeout(callback, 1000 / 60) + } + ) as typeof window.requestAnimationFrame + + window.requestAnimationFrame = RAF + } } -} \ No newline at end of file diff --git a/src/strings.ts b/src/strings.ts index 31699266d..78b7f41d3 100644 --- a/src/strings.ts +++ b/src/strings.ts @@ -4,7 +4,7 @@ * @returns String(value) or null */ export function stringOrNull(value: unknown): string | null { - return value == null ? null : String(value) + return value == null ? null : String(value) } /** @@ -13,5 +13,5 @@ export function stringOrNull(value: unknown): string | null { * @returns String(value) or "" */ export function stringOrEmpty(value: unknown): string { - return value == null ? "" : String(value) + return value == null ? '' : String(value) } diff --git a/src/types/events.ts b/src/types/events.ts index c4d3c6d1c..23904d110 100644 --- a/src/types/events.ts +++ b/src/types/events.ts @@ -2,93 +2,102 @@ * Event interfaces for event extension */ -import type { ConnectingLink, LinkReleaseContextExtended } from "@/litegraph" -import type { IWidget } from "@/types/widgets" -import type { LGraphNode } from "@/LGraphNode" -import type { LGraphGroup } from "@/LGraphGroup" +import type { ConnectingLink, LinkReleaseContextExtended } from '@/litegraph' +import type { IWidget } from '@/types/widgets' +import type { LGraphNode } from '@/LGraphNode' +import type { LGraphGroup } from '@/LGraphGroup' /** For Canvas*Event - adds graph space co-ordinates (property names are shipped) */ export interface ICanvasPosition { - /** X co-ordinate of the event, in graph space (NOT canvas space) */ - canvasX?: number - /** Y co-ordinate of the event, in graph space (NOT canvas space) */ - canvasY?: number + /** X co-ordinate of the event, in graph space (NOT canvas space) */ + canvasX?: number + /** Y co-ordinate of the event, in graph space (NOT canvas space) */ + canvasY?: number } /** For Canvas*Event */ export interface IDeltaPosition { - deltaX?: number - deltaY?: number + deltaX?: number + deltaY?: number } /** PointerEvent with canvasX/Y and deltaX/Y properties */ -export interface CanvasPointerEvent extends PointerEvent, CanvasMouseEvent { } +export interface CanvasPointerEvent extends PointerEvent, CanvasMouseEvent {} /** MouseEvent with canvasX/Y and deltaX/Y properties */ -export interface CanvasMouseEvent extends MouseEvent, ICanvasPosition, IDeltaPosition { - /** @deprecated Part of DragAndScale mouse API - incomplete / not maintained */ - dragging?: boolean - click_time?: number - dataTransfer?: unknown +export interface CanvasMouseEvent + extends MouseEvent, + ICanvasPosition, + IDeltaPosition { + /** @deprecated Part of DragAndScale mouse API - incomplete / not maintained */ + dragging?: boolean + click_time?: number + dataTransfer?: unknown } /** WheelEvent with canvasX/Y properties */ export interface CanvasWheelEvent extends WheelEvent, ICanvasPosition { - dragging?: boolean - click_time?: number - dataTransfer?: unknown + dragging?: boolean + click_time?: number + dataTransfer?: unknown } /** DragEvent with canvasX/Y and deltaX/Y properties */ -export interface CanvasDragEvent extends DragEvent, ICanvasPosition, IDeltaPosition { } +export interface CanvasDragEvent + extends DragEvent, + ICanvasPosition, + IDeltaPosition {} /** TouchEvent with canvasX/Y and deltaX/Y properties */ -export interface CanvasTouchEvent extends TouchEvent, ICanvasPosition, IDeltaPosition { } +export interface CanvasTouchEvent + extends TouchEvent, + ICanvasPosition, + IDeltaPosition {} export type CanvasEventDetail = - GenericEventDetail - | DragggingCanvasEventDetail - | ReadOnlyEventDetail - | GroupDoubleClickEventDetail - | EmptyDoubleClickEventDetail - | ConnectingWidgetLinkEventDetail - | EmptyReleaseEventDetail + | GenericEventDetail + | DragggingCanvasEventDetail + | ReadOnlyEventDetail + | GroupDoubleClickEventDetail + | EmptyDoubleClickEventDetail + | ConnectingWidgetLinkEventDetail + | EmptyReleaseEventDetail export interface GenericEventDetail { - subType: "before-change" | "after-change" + subType: 'before-change' | 'after-change' } export interface OriginalEvent { - originalEvent: CanvasPointerEvent, + originalEvent: CanvasPointerEvent } export interface EmptyReleaseEventDetail extends OriginalEvent { - subType: "empty-release", - linkReleaseContext: LinkReleaseContextExtended, + subType: 'empty-release' + linkReleaseContext: LinkReleaseContextExtended } export interface ConnectingWidgetLinkEventDetail { - subType: "connectingWidgetLink" - link: ConnectingLink - node: LGraphNode - widget: IWidget + subType: 'connectingWidgetLink' + link: ConnectingLink + node: LGraphNode + widget: IWidget } export interface EmptyDoubleClickEventDetail extends OriginalEvent { - subType: "empty-double-click" + subType: 'empty-double-click' } export interface GroupDoubleClickEventDetail extends OriginalEvent { - subType: "group-double-click" - group: LGraphGroup + subType: 'group-double-click' + group: LGraphGroup } export interface DragggingCanvasEventDetail { - subType: "dragging-canvas" - draggingCanvas: boolean + subType: 'dragging-canvas' + draggingCanvas: boolean } export interface ReadOnlyEventDetail { - subType: "read-only" - readOnly: boolean + subType: 'read-only' + readOnly: boolean } diff --git a/src/types/globalEnums.ts b/src/types/globalEnums.ts index f43a16c50..72067f532 100644 --- a/src/types/globalEnums.ts +++ b/src/types/globalEnums.ts @@ -1,53 +1,53 @@ /** Node slot type - input or output */ export enum NodeSlotType { - INPUT = 1, - OUTPUT = 2, + INPUT = 1, + OUTPUT = 2, } /** Shape that an object will render as - used by nodes and slots */ export enum RenderShape { - BOX = 1, - ROUND = 2, - CIRCLE = 3, - CARD = 4, - ARROW = 5, - /** intended for slot arrays */ - GRID = 6, - HollowCircle = 7, + BOX = 1, + ROUND = 2, + CIRCLE = 3, + CARD = 4, + ARROW = 5, + /** intended for slot arrays */ + GRID = 6, + HollowCircle = 7, } /** The direction that a link point will flow towards - e.g. horizontal outputs are right by default */ export enum LinkDirection { - NONE = 0, - UP = 1, - DOWN = 2, - LEFT = 3, - RIGHT = 4, - CENTER = 5, + NONE = 0, + UP = 1, + DOWN = 2, + LEFT = 3, + RIGHT = 4, + CENTER = 5, } /** The path calculation that links follow */ export enum LinkRenderType { - HIDDEN_LINK = -1, - /** Juts out from the input & output a little @see LinkDirection, then a straight line between them */ - STRAIGHT_LINK = 0, - /** 90° angles, clean and box-like */ - LINEAR_LINK = 1, - /** Smooth curved links - default */ - SPLINE_LINK = 2, + HIDDEN_LINK = -1, + /** Juts out from the input & output a little @see LinkDirection, then a straight line between them */ + STRAIGHT_LINK = 0, + /** 90° angles, clean and box-like */ + LINEAR_LINK = 1, + /** Smooth curved links - default */ + SPLINE_LINK = 2, } export enum TitleMode { - NORMAL_TITLE = 0, - NO_TITLE = 1, - TRANSPARENT_TITLE = 2, - AUTOHIDE_TITLE = 3, + NORMAL_TITLE = 0, + NO_TITLE = 1, + TRANSPARENT_TITLE = 2, + AUTOHIDE_TITLE = 3, } export enum LGraphEventMode { - ALWAYS = 0, - ON_EVENT = 1, - NEVER = 2, - ON_TRIGGER = 3, - BYPASS = 4, + ALWAYS = 0, + ON_EVENT = 1, + NEVER = 2, + ON_TRIGGER = 3, + BYPASS = 4, } diff --git a/src/types/serialisation.ts b/src/types/serialisation.ts index 24ae6b885..ea4652932 100644 --- a/src/types/serialisation.ts +++ b/src/types/serialisation.ts @@ -1,89 +1,104 @@ -import type { ISlotType, Dictionary, INodeFlags, INodeInputSlot, INodeOutputSlot, Point, Rect, Size } from "@/interfaces" -import type { LGraph } from "@/LGraph" -import type { IGraphGroupFlags, LGraphGroup } from "@/LGraphGroup" -import type { LGraphNode, NodeId } from "@/LGraphNode" -import type { LiteGraph } from "@/litegraph" -import type { LinkId, LLink } from "@/LLink" -import type { TWidgetValue } from "@/types/widgets" -import { RenderShape } from "./globalEnums" +import type { + ISlotType, + Dictionary, + INodeFlags, + INodeInputSlot, + INodeOutputSlot, + Point, + Rect, + Size, +} from '@/interfaces' +import type { LGraph } from '@/LGraph' +import type { IGraphGroupFlags, LGraphGroup } from '@/LGraphGroup' +import type { LGraphNode, NodeId } from '@/LGraphNode' +import type { LiteGraph } from '@/litegraph' +import type { LinkId, LLink } from '@/LLink' +import type { TWidgetValue } from '@/types/widgets' +import { RenderShape } from './globalEnums' /** * An object that implements custom pre-serialization logic via {@link Serialisable.asSerialisable}. */ export interface Serialisable { - /** - * Prepares this object for serialization. - * Creates a partial shallow copy of itself, with only the properties that should be serialised. - * @returns An object that can immediately be serialized to JSON. - */ - asSerialisable(): SerialisableObject + /** + * Prepares this object for serialization. + * Creates a partial shallow copy of itself, with only the properties that should be serialised. + * @returns An object that can immediately be serialized to JSON. + */ + asSerialisable(): SerialisableObject } /** Serialised LGraphNode */ export interface ISerialisedNode { - title?: string - id: NodeId - type?: string - pos?: Point - size?: Size - flags?: INodeFlags - order?: number - mode?: number - outputs?: INodeOutputSlot[] - inputs?: INodeInputSlot[] - properties?: Dictionary - shape?: RenderShape - boxcolor?: string - color?: string - bgcolor?: string - showAdvanced?: boolean - widgets_values?: TWidgetValue[] + title?: string + id: NodeId + type?: string + pos?: Point + size?: Size + flags?: INodeFlags + order?: number + mode?: number + outputs?: INodeOutputSlot[] + inputs?: INodeInputSlot[] + properties?: Dictionary + shape?: RenderShape + boxcolor?: string + color?: string + bgcolor?: string + showAdvanced?: boolean + widgets_values?: TWidgetValue[] } /** Contains serialised graph elements */ export type ISerialisedGraph< - TNode = ReturnType, - TLink = ReturnType, - TGroup = ReturnType + TNode = ReturnType, + TLink = ReturnType, + TGroup = ReturnType, > = { - last_node_id: LGraph["last_node_id"] - last_link_id: LGraph["last_link_id"] - last_reroute_id?: LGraph["last_reroute_id"] - nodes: TNode[] - links: TLink[] - groups: TGroup[] - config: LGraph["config"] - version: typeof LiteGraph.VERSION - extra?: unknown + last_node_id: LGraph['last_node_id'] + last_link_id: LGraph['last_link_id'] + last_reroute_id?: LGraph['last_reroute_id'] + nodes: TNode[] + links: TLink[] + groups: TGroup[] + config: LGraph['config'] + version: typeof LiteGraph.VERSION + extra?: unknown } /** Serialised LGraphGroup */ export interface ISerialisedGroup { - title: string - bounding: number[] - color: string - font_size: number - flags?: IGraphGroupFlags + title: string + bounding: number[] + color: string + font_size: number + flags?: IGraphGroupFlags } -export type TClipboardLink = [targetRelativeIndex: number, originSlot: number, nodeRelativeIndex: number, targetSlot: number, targetNodeId: NodeId] +export type TClipboardLink = [ + targetRelativeIndex: number, + originSlot: number, + nodeRelativeIndex: number, + targetSlot: number, + targetNodeId: NodeId, +] /** */ export interface IClipboardContents { - nodes?: ISerialisedNode[] - links?: TClipboardLink[] + nodes?: ISerialisedNode[] + links?: TClipboardLink[] } export interface SerialisableLLink { - /** Link ID */ - id: LinkId - /** Output node ID */ - origin_id: NodeId - /** Output slot index */ - origin_slot: number - /** Input node ID */ - target_id: NodeId - /** Input slot index */ - target_slot: number - /** Data type of the link */ - type: ISlotType + /** Link ID */ + id: LinkId + /** Output node ID */ + origin_id: NodeId + /** Output slot index */ + origin_slot: number + /** Input node ID */ + target_id: NodeId + /** Input slot index */ + target_slot: number + /** Data type of the link */ + type: ISlotType } diff --git a/src/types/widgets.ts b/src/types/widgets.ts index 326287842..c4d6fe2df 100644 --- a/src/types/widgets.ts +++ b/src/types/widgets.ts @@ -1,28 +1,29 @@ -import { CanvasColour, Point, Size } from "@/interfaces" -import type { LGraphCanvas, LGraphNode } from "@/litegraph" -import type { CanvasMouseEvent } from "./events" +import { CanvasColour, Point, Size } from '@/interfaces' +import type { LGraphCanvas, LGraphNode } from '@/litegraph' +import type { CanvasMouseEvent } from './events' -export interface IWidgetOptions extends Record { - on?: string - off?: string - max?: number - min?: number - slider_color?: CanvasColour - marker_color?: CanvasColour - precision?: number - read_only?: boolean - step?: number - y?: number - multiline?: boolean - // TODO: Confirm this - property?: string +export interface IWidgetOptions + extends Record { + on?: string + off?: string + max?: number + min?: number + slider_color?: CanvasColour + marker_color?: CanvasColour + precision?: number + read_only?: boolean + step?: number + y?: number + multiline?: boolean + // TODO: Confirm this + property?: string - hasOwnProperty?(arg0: string): any - // values?(widget?: IWidget, node?: LGraphNode): any - values?: TValue[] - callback?: IWidget["callback"] + hasOwnProperty?(arg0: string): any + // values?(widget?: IWidget, node?: LGraphNode): any + values?: TValue[] + callback?: IWidget['callback'] - onHide?(widget: IWidget): void + onHide?(widget: IWidget): void } /** @@ -34,94 +35,116 @@ export interface IWidgetOptions extends Record + type?: 'combo' + value: string | number + options: IWidgetOptions } -export type IStringWidgetType = IStringWidget["type"] | IMultilineStringWidget["type"] +export type IStringWidgetType = + | IStringWidget['type'] + | IMultilineStringWidget['type'] /** A widget with a string value */ export interface IStringWidget extends IBaseWidget { - type?: "string" | "text" | "button" - value: string + type?: 'string' | 'text' | 'button' + value: string } /** A widget with a string value and a multiline text input */ -export interface IMultilineStringWidget extends IBaseWidget { - type?: "multiline" - value: string +export interface IMultilineStringWidget< + TElement extends HTMLElement = HTMLTextAreaElement, +> extends IBaseWidget { + type?: 'multiline' + value: string - /** HTML textarea element */ - element?: TElement + /** HTML textarea element */ + element?: TElement } /** A custom widget - accepts any value and has no built-in special handling */ -export interface ICustomWidget extends IBaseWidget { - type?: "custom" - value: string | object +export interface ICustomWidget + extends IBaseWidget { + type?: 'custom' + value: string | object - element?: TElement + element?: TElement } - /** * Valid widget types. TS cannot provide easily extensible type safety for this at present. * Override linkedWidgets[] * Values not in this list will not result in litegraph errors, however they will be treated the same as "custom". */ -export type TWidgetType = IWidget["type"] -export type TWidgetValue = IWidget["value"] +export type TWidgetType = IWidget['type'] +export type TWidgetValue = IWidget['value'] /** * The base type for all widgets. Should not be implemented directly. * @see IWidget */ export interface IBaseWidget { - linkedWidgets?: IWidget[] + linkedWidgets?: IWidget[] - options: IWidgetOptions - marker?: number - label?: string - clicked?: boolean - name?: string - /** Widget type (see {@link TWidgetType}) */ - type?: TWidgetType - value?: TWidgetValue - y?: number - last_y?: number - width?: number - disabled?: boolean - - hidden?: boolean - advanced?: boolean + options: IWidgetOptions + marker?: number + label?: string + clicked?: boolean + name?: string + /** Widget type (see {@link TWidgetType}) */ + type?: TWidgetType + value?: TWidgetValue + y?: number + last_y?: number + width?: number + disabled?: boolean - tooltip?: string + hidden?: boolean + advanced?: boolean - /** HTML widget element */ - element?: TElement + tooltip?: string - // TODO: Confirm this format - callback?(value: any, canvas?: LGraphCanvas, node?: LGraphNode, pos?: Point, e?: CanvasMouseEvent): void - onRemove?(): void - beforeQueued?(): void + /** HTML widget element */ + element?: TElement - mouse?(event: CanvasMouseEvent, arg1: number[], node: LGraphNode): boolean - draw?(ctx: CanvasRenderingContext2D, node: LGraphNode, widget_width: number, y: number, H: number): void - computeSize?(width: number): Size + // TODO: Confirm this format + callback?( + value: any, + canvas?: LGraphCanvas, + node?: LGraphNode, + pos?: Point, + e?: CanvasMouseEvent, + ): void + onRemove?(): void + beforeQueued?(): void + + mouse?(event: CanvasMouseEvent, arg1: number[], node: LGraphNode): boolean + draw?( + ctx: CanvasRenderingContext2D, + node: LGraphNode, + widget_width: number, + y: number, + H: number, + ): void + computeSize?(width: number): Size } diff --git a/src/utils/arrange.ts b/src/utils/arrange.ts index 909c5c2d7..69793cb4f 100644 --- a/src/utils/arrange.ts +++ b/src/utils/arrange.ts @@ -1,5 +1,5 @@ -import type { Dictionary, Direction, IBoundaryNodes } from "@/interfaces" -import type { LGraphNode } from "@/LGraphNode" +import type { Dictionary, Direction, IBoundaryNodes } from '@/interfaces' +import type { LGraphNode } from '@/LGraphNode' /** * Finds the nodes that are farthest in all four directions, representing the boundary of the nodes. @@ -7,31 +7,31 @@ import type { LGraphNode } from "@/LGraphNode" * @returns An object listing the furthest node (edge) in all four directions. `null` if no nodes were supplied or the first node was falsy. */ export function getBoundaryNodes(nodes: LGraphNode[]): IBoundaryNodes | null { - const valid = nodes?.find(x => x) - if (!valid) return null + const valid = nodes?.find((x) => x) + if (!valid) return null - let top = valid - let right = valid - let bottom = valid - let left = valid + let top = valid + let right = valid + let bottom = valid + let left = valid - for (const node of nodes) { - if (!node) continue - const [x, y] = node.pos - const [width, height] = node.size + for (const node of nodes) { + if (!node) continue + const [x, y] = node.pos + const [width, height] = node.size - if (y < top.pos[1]) top = node - if (x + width > right.pos[0] + right.size[0]) right = node - if (y + height > bottom.pos[1] + bottom.size[1]) bottom = node - if (x < left.pos[0]) left = node - } + if (y < top.pos[1]) top = node + if (x + width > right.pos[0] + right.size[0]) right = node + if (y + height > bottom.pos[1] + bottom.size[1]) bottom = node + if (x < left.pos[0]) left = node + } - return { - top, - right, - bottom, - left - } + return { + top, + right, + bottom, + left, + } } /** @@ -40,30 +40,30 @@ export function getBoundaryNodes(nodes: LGraphNode[]): IBoundaryNodes | null { * @param horizontal If true, distributes along the horizontal plane. Otherwise, the vertical plane. */ export function distributeNodes(nodes: LGraphNode[], horizontal?: boolean): void { - const nodeCount = nodes?.length - if (!(nodeCount > 1)) return + const nodeCount = nodes?.length + if (!(nodeCount > 1)) return - const index = horizontal ? 0 : 1 + const index = horizontal ? 0 : 1 - let total = 0 - let highest = -Infinity + let total = 0 + let highest = -Infinity - for (const node of nodes) { - total += node.size[index] + for (const node of nodes) { + total += node.size[index] - const high = node.pos[index] + node.size[index] - if (high > highest) highest = high - } - const sorted = [...nodes].sort((a, b) => a.pos[index] - b.pos[index]) - const lowest = sorted[0].pos[index] + const high = node.pos[index] + node.size[index] + if (high > highest) highest = high + } + const sorted = [...nodes].sort((a, b) => a.pos[index] - b.pos[index]) + const lowest = sorted[0].pos[index] - const gap = ((highest - lowest) - total) / (nodeCount - 1) - let startAt = lowest - for (let i = 0; i < nodeCount; i++) { - const node = sorted[i] - node.pos[index] = startAt + (gap * i) - startAt += node.size[index] - } + const gap = (highest - lowest - total) / (nodeCount - 1) + let startAt = lowest + for (let i = 0; i < nodeCount; i++) { + const node = sorted[i] + node.pos[index] = startAt + gap * i + startAt += node.size[index] + } } /** @@ -73,33 +73,34 @@ export function distributeNodes(nodes: LGraphNode[], horizontal?: boolean): void * @param align_to The node to align all other nodes to. If undefined, the farthest node will be used. */ export function alignNodes(nodes: LGraphNode[], direction: Direction, align_to?: LGraphNode): void { - if (!nodes) return + if (!nodes) return - const boundary = align_to === undefined - ? getBoundaryNodes(nodes) - : { - top: align_to, - right: align_to, - bottom: align_to, - left: align_to + const boundary = + align_to === undefined + ? getBoundaryNodes(nodes) + : { + top: align_to, + right: align_to, + bottom: align_to, + left: align_to, } - if (boundary === null) return + if (boundary === null) return - for (const node of nodes) { - switch (direction) { - case "right": - node.pos[0] = boundary.right.pos[0] + boundary.right.size[0] - node.size[0] - break - case "left": - node.pos[0] = boundary.left.pos[0] - break - case "top": - node.pos[1] = boundary.top.pos[1] - break - case "bottom": - node.pos[1] = boundary.bottom.pos[1] + boundary.bottom.size[1] - node.size[1] - break - } + for (const node of nodes) { + switch (direction) { + case 'right': + node.pos[0] = boundary.right.pos[0] + boundary.right.size[0] - node.size[0] + break + case 'left': + node.pos[0] = boundary.left.pos[0] + break + case 'top': + node.pos[1] = boundary.top.pos[1] + break + case 'bottom': + node.pos[1] = boundary.bottom.pos[1] + boundary.bottom.size[1] - node.size[1] + break } + } } diff --git a/test/LGraph.test.ts b/test/LGraph.test.ts index 8a1680665..12f1112a2 100644 --- a/test/LGraph.test.ts +++ b/test/LGraph.test.ts @@ -1,44 +1,44 @@ -import { LGraph, LGraphGroup, LGraphNode, LiteGraph } from "../src/litegraph" -import { LiteGraphGlobal } from "../src/LiteGraphGlobal" +import { LGraph, LGraphGroup, LGraphNode, LiteGraph } from '../src/litegraph' +import { LiteGraphGlobal } from '../src/LiteGraphGlobal' function makeGraph() { const LiteGraph = new LiteGraphGlobal() - LiteGraph.registerNodeType("TestNode", LGraphNode) - LiteGraph.registerNodeType("OtherNode", LGraphNode) - LiteGraph.registerNodeType("", LGraphNode) + LiteGraph.registerNodeType('TestNode', LGraphNode) + LiteGraph.registerNodeType('OtherNode', LGraphNode) + LiteGraph.registerNodeType('', LGraphNode) return new LGraph() } -describe("LGraph", () => { - it("can be instantiated", () => { +describe('LGraph', () => { + it('can be instantiated', () => { // @ts-ignore TODO: Remove once relative imports fix goes in. - const graph = new LGraph({ extra: "TestGraph" }) + const graph = new LGraph({ extra: 'TestGraph' }) expect(graph).toBeInstanceOf(LGraph) - expect(graph.extra).toBe("TestGraph") + expect(graph.extra).toBe('TestGraph') }) }) -describe("Legacy LGraph Compatibility Layer", () => { - it("can be extended via prototype", () => { +describe('Legacy LGraph Compatibility Layer', () => { + it('can be extended via prototype', () => { const graph = new LGraph() // @ts-expect-error Should always be an error. LGraph.prototype.newMethod = function () { - return "New method added via prototype" + return 'New method added via prototype' } // @ts-expect-error Should always be an error. - expect(graph.newMethod()).toBe("New method added via prototype") + expect(graph.newMethod()).toBe('New method added via prototype') }) - it("is correctly assigned to LiteGraph", () => { + it('is correctly assigned to LiteGraph', () => { expect(LiteGraph.LGraph).toBe(LGraph) }) }) -describe("LGraph Serialisation", () => { - it("should serialise", () => { +describe('LGraph Serialisation', () => { + it('should serialise', () => { const graph = new LGraph() - graph.add(new LGraphNode("Test Node")) - graph.add(new LGraphGroup("Test Group")) + graph.add(new LGraphNode('Test Node')) + graph.add(new LGraphGroup('Test Group')) expect(graph.nodes.length).toBe(1) expect(graph.groups.length).toBe(1) }) diff --git a/test/LGraphNode.test.ts b/test/LGraphNode.test.ts index a846dc637..a2a9de82c 100644 --- a/test/LGraphNode.test.ts +++ b/test/LGraphNode.test.ts @@ -1,13 +1,10 @@ -import { - LGraphNode, -} from "../src/litegraph" +import { LGraphNode } from '../src/litegraph' -describe("LGraphNode", () => { - it("should serialize position correctly", () => { - const node = new LGraphNode("TestNode") +describe('LGraphNode', () => { + it('should serialize position correctly', () => { + const node = new LGraphNode('TestNode') node.pos = [10, 10] expect(node.pos).toEqual(new Float32Array([10, 10])) expect(node.serialize().pos).toEqual(new Float32Array([10, 10])) }) - -}) \ No newline at end of file +})