From 7c0240857cf3fb7cfc338ab95cc5b5599d8390fc Mon Sep 17 00:00:00 2001 From: Chenlei Hu Date: Sun, 3 Nov 2024 09:12:47 -0500 Subject: [PATCH] Revert branch to cb6e80a64571f2d4ff2e9fb19c81f88f6473186c (#257) --- .prettierrc | 10 +- package.json | 1 - public/css/litegraph.css | 786 +- src/ContextMenu.ts | 692 +- src/CurveEditor.ts | 331 +- src/DragAndScale.ts | 407 +- src/LGraph.ts | 2584 +++--- src/LGraphBadge.ts | 100 +- src/LGraphCanvas.ts | 14887 ++++++++++++++++++----------------- src/LGraphGroup.ts | 443 +- src/LGraphNode.ts | 4235 +++++----- src/LLink.ts | 185 +- src/LiteGraphGlobal.ts | 1771 +++-- src/MapProxyHandler.ts | 87 +- src/draw.ts | 110 +- src/interfaces.ts | 172 +- src/litegraph.ts | 165 +- src/measure.ts | 239 +- src/polyfills.ts | 139 +- 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 +- 27 files changed, 14195 insertions(+), 13800 deletions(-) diff --git a/.prettierrc b/.prettierrc index f36bb1cd2..ef6f9008a 100755 --- a/.prettierrc +++ b/.prettierrc @@ -1,7 +1,5 @@ { - "singleQuote": true, - "tabWidth": 2, - "semi": false, - "trailingComma": "none", - "printWidth": 80 -} \ No newline at end of file + "singleQuote": false, + "semi": true, + "tabWidth": 2 +} diff --git a/package.json b/package.json index 026c23cd1..2cdcceda3 100755 --- a/package.json +++ b/package.json @@ -23,7 +23,6 @@ "build": "tsc && vite build", "dev": "vite", "preview": "vite preview", - "watch": "vite build --watch", "release": "node scripts/release.js", "test": "jest", "deprecated-test:allVersions": "./utils/test.sh", diff --git a/public/css/litegraph.css b/public/css/litegraph.css index ebaacaaa7..5524e24ba 100644 --- a/public/css/litegraph.css +++ b/public/css/litegraph.css @@ -1,699 +1,693 @@ /* 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 7d6f0a92e..39bc1e0df 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,352 +24,354 @@ 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' - } + //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})` } - //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 + 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 } - 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 + 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) - //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() + 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 - }, - 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 302d5e2a3..fb252d88a 100644 --- a/src/CurveEditor.ts +++ b/src/CurveEditor.ts @@ -1,178 +1,173 @@ -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 - } - - 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) - } - 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() - 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 - } - - 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) + constructor(points: Point[]) { + this.points = points 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 + this.nearest = -1 + this.size = null //stores last size used + this.must_update = true + this.margin = 5 } - } - // 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 + 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) + } + 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() + 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 + } + + 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 { + 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 } - return closest - } } diff --git a/src/DragAndScale.ts b/src/DragAndScale.ts index 3733f9aeb..ff8f8c97a 100644 --- a/src/DragAndScale.ts +++ b/src/DragAndScale.ts @@ -1,221 +1,228 @@ -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) - } - } - } - - /** @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) + if (element) { + this.element = element + if (!skip_events) { + this.bindEvents(element) + } } - } - } 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) } - this.last_mouse[0] = x - this.last_mouse[1] = y + /** @deprecated Has not been kept up to date */ + bindEvents(element: Node): void { + this.last_mouse = new Float32Array(2) - if (is_inside) { - e.preventDefault() - e.stopPropagation() - return false - } - } + this._binded_mouse_callback = this.onMouse.bind(this) - toCanvasContext(ctx: CanvasRenderingContext2D): void { - ctx.scale(this.scale, this.scale) - ctx.translate(this.offset[0], this.offset[1]) - } + LiteGraph.pointerListenerAdd(element, "down", this._binded_mouse_callback) + LiteGraph.pointerListenerAdd(element, "move", this._binded_mouse_callback) + LiteGraph.pointerListenerAdd(element, "up", this._binded_mouse_callback) - 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 + element.addEventListener( + "mousewheel", + this._binded_mouse_callback, + false + ) + element.addEventListener("wheel", this._binded_mouse_callback, false) } - if (value == this.scale) return - if (!this.element) return + 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 + } - const rect = this.element.getBoundingClientRect() - if (!rect) return + /** @deprecated Has not been kept up to date */ + onMouse(e: CanvasMouseEvent) { + if (!this.enabled) { + 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 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 new_center = this.convertCanvasToOffset(zooming_center) - const delta_offset = [new_center[0] - center[0], new_center[1] - center[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])) - this.offset[0] += delta_offset[0] - this.offset[1] += delta_offset[1] + let ignore = false + if (this.onmouse) { + ignore = this.onmouse(e) + } - this.onredraw?.(this) - } + 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 - changeDeltaScale(value: number, zooming_center?: Point): void { - this.changeScale(this.scale * value, zooming_center) - } + //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) + } - reset(): void { - this.scale = 1 - this.offset[0] = 0 - this.offset[1] = 0 - } + 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 + } } diff --git a/src/LGraph.ts b/src/LGraph.ts index 2f3140a2d..3a7d8f6c2 100644 --- a/src/LGraph.ts +++ b/src/LGraph.ts @@ -1,24 +1,20 @@ -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< - T[K] ->[1] extends undefined - ? Parameters | Parameters[0] - : Parameters +type ParamsArray, K extends MethodNames> = Parameters[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. @@ -33,1304 +29,1306 @@ 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 | ReturnType | 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, node?: LGraphNode): void - onAfterChange?(graph: LGraph, node?: 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 + 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) - } - - // 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?.() - } + if (o) this.configure(o) } - //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) + // 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 } - } - /** - * 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) + /** + * Removes all nodes from this graph + */ + clear(): void { this.stop() - } - } + this.status = LGraph.STATUS_STOPPED - const now = LiteGraph.getTime() - let elapsed = now - start - if (elapsed == 0) elapsed = 1 + this.last_node_id = 0 + this.last_link_id = 0 - 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 + this._version = -1 //used to detect changes - //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(node?: LGraphNode): void { - this.onBeforeChange?.(this, node) - this.sendActionToCanvas('onBeforeChange', this) - } - //used to resend actions, called after any change is made to the graph - afterChange(node?: LGraphNode): void { - this.onAfterChange?.(this, node) - this.sendActionToCanvas('onAfterChange', this) - } - - nodeUpdate(node?: LGraphNode): void { - this.onNodeUpdated?.(node) - this.sendActionToCanvas('onNodeUpdated', node) - } - - 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; + //safe clear + if (this._nodes) { + for (let i = 0; i < this._nodes.length; ++i) { + this._nodes[i].onRemoved?.() + } } - 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 - } + //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 - //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) - } + //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") } - //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) - } + get nodes() { + return this._nodes } - 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 + get groups() { + return this._groups } - //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?.() + /** + * 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) } - req.onerror = function (err) { - console.error('Error loading graph:', err) + /** + * 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) { + 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; + } + + 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 } - } - // 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 c394ccca3..0d0c3b0a3 100644 --- a/src/LGraphBadge.ts +++ b/src/LGraphBadge.ts @@ -1,82 +1,90 @@ 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 3aab9fcdb..70d8d36f3 100644 --- a/src/LGraphCanvas.ts +++ b/src/LGraphCanvas.ts @@ -1,126 +1,100 @@ -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 -} - -/** Easing functions for canvas animations */ -const easeFunctions = { - linear: (t: number) => t, - easeInQuad: (t: number) => t * t, - easeOutQuad: (t: number) => t * (2 - t), - easeInOutQuad: (t: number) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t) + /** {@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 } /** @@ -132,7356 +106,7765 @@ const easeFunctions = { * @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) - static DEFAULT_BACKGROUND_IMAGE = - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=' + /* 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) - /** Initialised from LiteGraphGlobal static block to avoid circular dependency. */ - static link_type_colors: Record - static gradients: Record = {} //cache of gradients + static DEFAULT_BACKGROUND_IMAGE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=" - 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' }, - } + /** Initialised from LiteGraphGlobal static block to avoid circular dependency. */ + static link_type_colors: Record + static gradients: Record = {} //cache of gradients - /** - * 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 - onNodeUpdated?: (node: LGraphNode) => void - onPositionChanged?: (position: { x: number, y: number }) => void - onZoomChanged?: (scale: number) => 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" + 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" } } - this.default_connection_color_byType = { - /*number: "#7F7", + + /** + * 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", 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) - - 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) - } - } - 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 + 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 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) + 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) + + 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) + } + } + 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 + }, + // @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 value) { + entries.push({ content: i, value: value[i] }) + } + new LiteGraph.ContextMenu(entries, { + event: e, + callback: inner_clicked, + parentMenu: prev_menu, + node: node + }) + return false } - callback?.(node) - canvas.graph.afterChange() - }, + 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() } - 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, - }, - // @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 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 - } + /** @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 - /** @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 canvas = LGraphCanvas.active_canvas + const ref_window = canvas.getCanvasWindow() - const canvas = LGraphCanvas.active_canvas - const ref_window = canvas.getCanvasWindow() + 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 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, - }) - } - 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], - }) - } - - 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 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 + //value could contain invalid html characters, clean that + value = LGraphCanvas.decodeHTML(stringOrNull(value)) + entries.push({ + content: "" + + (info.label || i) + + "" + + "" + + value + + "", + value: i + }) } - 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) + if (!entries.length) { + return } - } - 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]) + 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] + }) } - } + + return false } - - 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) + static decodeHTML(str: string): string { + const e = document.createElement("div") + e.innerText = str + return e.innerHTML } - new LiteGraph.ContextMenu(values, { - event: e, - callback: inner_clicked, - parentMenu: menu, - node: node, - }) + static onMenuResizeNode(value: IContextMenuValue, options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, node: LGraphNode): void { + if (!node) return - function inner_clicked(v: { value: string | number }) { - if (!node) return + const fApplyMultiNode = function (node: LGraphNode) { + node.size = node.computeSize() + node.onResize?.(node.size) + } - 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 - } + const canvas = LGraphCanvas.active_canvas + if (!canvas.selected_nodes || Object.keys(canvas.selected_nodes).length <= 1) { + fApplyMultiNode(node) } else { - delete node.color - delete node.bgcolor + for (const i in canvas.selected_nodes) { + fApplyMultiNode(canvas.selected_nodes[i]) + } } - } - 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) + } + // 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) } - } - 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]) + 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() + }) } - } - node.graph.afterChange(/*?*/) //node - canvas.setDirty(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 + } + + 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 } - return false - } - static onMenuNodeRemove(value: IContextMenuValue, options: IContextMenuOptions, e: MouseEvent, menu: ContextMenu, node: LGraphNode): void { - if (!node) throw 'no node passed' + /** @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 graph = node.graph - graph.beforeChange() + const values: IContextMenuValue[] = [] + values.push({ + value: null, + content: "No color" + }) - const fApplyMultiNode = function (node: LGraphNode) { - if (node.removable === false) 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 + }) - graph.remove(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" - 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]) - } + 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" - 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 + const graph = node.graph + graph.beforeChange() - let nodes_list = Object.values(canvas.selected_nodes || {}) - if (!nodes_list.length) nodes_list = [node] + const fApplyMultiNode = function (node: LGraphNode) { + if (node.removable === false) 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) + graph.remove(node) + } - // @ts-expect-error Doesn't exist anywhere... - subgraph_node.buildFromNodes(nodes_list) + 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.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 + 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 - 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]) - } + 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 { - if (Object.keys(newSelected).length) { - canvas.selectNodes(newSelected) + 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 - graph.afterChange() + //this.scale = 1; + //this.offset = [0,0]; + this.dragging_rectangle = null - 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.selected_nodes = {} + /** All selected groups */ + this.selectedGroups = null + /** The group currently being resized */ + this.selected_group = null - //this.scale = 1; - //this.offset = [0,0]; - this.dragging_rectangle = 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.selected_nodes = {} - /** All selected groups */ - this.selectedGroups = null - /** The group currently being resized */ - this.selected_group = null + this.dragging_canvas = false - this.visible_nodes = [] - this.node_dragged = null - this.node_over = null - this.node_capturing_input = null - this.connecting_links = null - this.highlighted_links = {} + this.dirty_canvas = true + this.dirty_bgcanvas = true + this.dirty_area = null - this.dragging_canvas = false + this.node_in_panel = null + this.node_widget = null - this.dirty_canvas = true - this.dirty_bgcanvas = true - this.dirty_area = 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.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 + 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 - graph.attachCanvas(this) + if (!skip_clear) this.clear() - //remove the graph stack in case a subgraph was open - this._graph_stack &&= null + if (!graph && this.graph) { + this.graph.detachCanvas(this) + return + } - 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' + graph.attachCanvas(this) - if (this.graph == graph) throw 'graph cannot be the same' + //remove the graph stack in case a subgraph was open + this._graph_stack &&= null - this.clear() - - if (this.graph) { - this._graph_stack ||= [] - this._graph_stack.push(this.graph) + this.setDirty(true, true) } - - 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]) + /** + * 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 } - // 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 + /** + * 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) } - 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() + /** + * closes a subgraph contained inside a node + * + * @param {LGraph} assigns a graph + */ + closeSubgraph(): void { + if (!this._graph_stack || this._graph_stack.length == 0) return - 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 + 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 } - 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" + /** + * returns the visually active graph (in case there are more in the stack) + * @return {LGraph} the active graph + */ + getCurrentGraph(): LGraph { + return this.graph } - - 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() + /** + * 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() - 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 + 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() } - - //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 + //used in some events to capture them + _doNothing(e: Event) { + //console.log("pointerevents: _doNothing "+e.type); + e.preventDefault() + return false } - - //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' + _doReturnTrue(e: Event) { + e.preventDefault() + return true } - // @ts-expect-error - if (typeof enableWebGLCanvas === 'undefined') { - throw 'webglCanvas.js must be included to use this feature' + /** + * 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 + } - // @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 + //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 - - 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)) - } } - } - /** - * stops rendering the content of the canvas (to save resources) - * - */ - stopRendering(): void { - this.is_rendering = false - /* + /** + * 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 + } + /** + * 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 + /* 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 - } - - /** - * 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 - } } - - 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 - - // 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 - } + /* LiteGraphCanvas input */ + //used to block future mouse events (because of im gui) + blockClick(): void { + this.block_click = true + this.last_mouseclick = 0 } - } - - 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, - }) - } - - 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 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 - - if (this.onPositionChanged) { - this.onPositionChanged({ - x: this.ds.offset[0], - y: this.ds.offset[1] - }) - } - } 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]) - - if (this.onZoomChanged) { - this.onZoomChanged(scale) - } - - if (this.onPositionChanged) { - this.onPositionChanged({ - x: this.ds.offset[0], - y: this.ds.offset[1] - }) - } - - 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) - - if (this.onPositionChanged) { - this.onPositionChanged({ - x: this.ds.offset[0], - y: this.ds.offset[1] - }) - } - } - - /** - * Centers the camera on a given node (animated version) - * @method animateToNode - **/ - animateToNode(node: LGraphNode, duration: number = 350, zoom: number = 0.75, easing: string = 'easeInOutQuad') { - let animationId = null - const startTimestamp = performance.now() - const startX = this.ds.offset[0] - const startY = this.ds.offset[1] - const startScale = this.ds.scale - const cw = this.canvas.width / window.devicePixelRatio - const ch = this.canvas.height / window.devicePixelRatio - - let targetScale = startScale - let targetX = startX - let targetY = startY - - if (zoom > 0) { - const targetScaleX = (zoom * cw) / Math.max(node.size[0], 300) - const targetScaleY = (zoom * ch) / Math.max(node.size[1], 300) - - // Choose the smaller scale to ensure the node fits into the viewport - // Ensure we don't go over the max scale - targetScale = Math.min(targetScaleX, targetScaleY, this.ds.max_scale) - targetX = -node.pos[0] - node.size[0] * 0.5 + (cw * 0.5) / targetScale - targetY = -node.pos[1] - node.size[1] * 0.5 + (ch * 0.5) / targetScale - } else { - targetX = -node.pos[0] - node.size[0] * 0.5 + (cw * 0.5) / targetScale - targetY = -node.pos[1] - node.size[1] * 0.5 + (ch * 0.5) / targetScale - } - - const easeFunction = easeFunctions[easing] || easeFunctions.linear - - const animate = (timestamp: number) => { - const elapsed = timestamp - startTimestamp - const progress = Math.min(elapsed / duration, 1) - const easedProgress = easeFunction(progress) - - this.ds.offset[0] = startX + (targetX - startX) * easedProgress - this.ds.offset[1] = startY + (targetY - startY) * easedProgress - - if (zoom > 0) { - this.ds.scale = startScale + (targetScale - startScale) * easedProgress - } - - this.setDirty(true, true) - - if (this.onPositionChanged) { - this.onPositionChanged({ - x: this.ds.offset[0], - y: this.ds.offset[1] - }) - } - - if (progress < 1) { - animationId = requestAnimationFrame(animate) - } else { - cancelAnimationFrame(animationId) - } - } - - animationId = requestAnimationFrame(animate) - } - - /** - * 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() - 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. + * 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 */ - ctx.setLineDash([radius, radius * 0.001]) + getWidgetAtCursor(node?: LGraphNode): IWidget | null { + node ??= this.node_over - ctx.lineWidth = 1 - ctx.strokeStyle = linearGradient - ctx.stroke() + if (!node.widgets) return null - ctx.strokeStyle = gradient - ctx.stroke() + const graphPos = this.graph_mouse + const x = graphPos[0] - node.pos[0] + const y = graphPos[1] - node.pos[1] - ctx.setLineDash([]) - ctx.lineWidth = lineWidth - ctx.strokeStyle = strokeStyle - } + for (const widget of node.widgets) { + if(widget.hidden || (widget.advanced && !node.showAdvanced)) continue; - /** - * 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.dirty_canvas = 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 - - const low_quality = this.ds.scale < 0.6 //zoomed out - - //only render if it forces it to do it - if (this.live_mode) { - if (!node.flags.collapsed) { - ctx.shadowColor = 'transparent' - node.onDrawForeground?.(ctx, this, this.canvas) - } - return - } - - const editor_alpha = this.editor_alpha - ctx.globalAlpha = editor_alpha - - if (this.render_shadows && !low_quality) { - ctx.shadowColor = LiteGraph.DEFAULT_SHADOW_COLOR - ctx.shadowOffsetX = 2 * this.ds.scale - ctx.shadowOffsetY = 2 * this.ds.scale - ctx.shadowBlur = 3 * this.ds.scale - } else { - ctx.shadowColor = 'transparent' - } - - //custom draw collapsed method (draw after shadows because they are affected) - if (node.flags.collapsed && node.onDrawCollapsed?.(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 - } - } - - 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() - } - - //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, - }) - } - } - - //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() - } - - 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 - } - - //* 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 - } - - //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) - } - } - } - - //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() - } - - //custom title render - node.onDrawTitle?.(ctx) - } - - //render selection marker - if (selected) { - node.onBounding?.(area) - - this.drawSelectionBounding(ctx, area, { - shape, - title_height, - title_mode, - fgcolor, - collapsed: node.flags?.collapsed, - }) - } - - // 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 - } - - // 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: - 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() - } - } - } - //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, - ) + let widgetWidth, widgetHeight + if (widget.computeSize) { + ([widgetWidth, widgetHeight] = widget.computeSize(node.size[0])) } 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 + 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 + } + } + + 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 + + // 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) + } } - } - 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() + let clicking_canvas_bg = false - //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 - } + //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) + } - 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() + //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) { - 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 + 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) - const old_value = w.value + this.connecting_links.push({ + node: linked_node, + slot: slot, + input: input, + output: null, + pos: pos, + direction: node.horizontal !== true ? LinkDirection.RIGHT : LinkDirection.CENTER, + }) + } - //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) + 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, + }) + } + + clicking_canvas_bg = true + } } - w.clicked = 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 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 - } - break + + 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 } - 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) + e.dragging = this.last_mouse_dragging - 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, + if (this.node_widget) { + this.processNodeWidgets( + this.node_widget[0], + this.graph_mouse, + e, + this.node_widget[1] ) - this.dirty_canvas = true - break + this.dirty_canvas = true } - 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, + + //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] ) - } - break - default: - if (w.mouse) this.dirty_canvas = w.mouse(event, [x, y], node) - break - } //end switch + 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 - //value changed - if (old_value != w.value) { - node.onWidgetChanged?.(w.name, w.value, old_value, w) - node.graph._version++ - } + //remove mouseover flag + this.updateMouseOverNodes(node, e) - return w - } //end for + //mouse over a node + if (node) { - 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) - } + if (node.redraw_on_mouse) + this.dirty_canvas = true - return null - } + // 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) - /** - * draws every group area in the background - **/ - drawGroups(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D): void { - if (!this.graph) return + if (!node.mouseOver) { + //mouse enter + node.mouseOver = { + inputId: null, + outputId: null, + overWidget: null, + } + this.node_over = node + this.dirty_canvas = true - const groups = this.graph._groups + node.onMouseEnter?.(e) + } - ctx.save() - ctx.globalAlpha = 0.5 * this.editor_alpha + //in case the node wants to do something + node.onMouseMove?.(e, [e.canvasX - node.pos[0], e.canvasY - node.pos[1]], this) - for (let i = 0; i < groups.length; ++i) { - const group = groups[i] + // 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 - if (!overlapBounding(this.visible_area, group._bounding)) { - continue - } //out of the visible area + // 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] - group.draw(this, ctx) - } + // Default: nothing highlighted + let highlightPos: Point = null + let highlightInput: INodeInputSlot = null + let linkOverWidget: IWidget = null - 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 (firstLink.node === node) { + // Cannot connect link from a node to itself + } else if (firstLink.output) { - if (this.canvas.width == width && this.canvas.height == height) return + // Connecting from an output to an input - 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 - } + 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 + } + } - 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 - } + } else if (firstLink.input) { - const t = setInterval(function () { - self.editor_alpha *= delta - self.dirty_canvas = true - self.dirty_bgcanvas = true + // 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 + } - 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) - } + this.dirty_canvas = true + } - onNodeSelectionChange(): void {} + //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 + } - /** - * 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 + if (this.canvas) { + this.canvas.style.cursor = "" + } + } //end - 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 + //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) } - }) - break - case 'Delete': - graph.removeLink(link.id) - break - default: - } + //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 - 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 window = this.getCanvasWindow() + const document = window.document + LGraphCanvas.active_canvas = this - const isFrom = opts.nodeFrom && opts.slotFrom !== null - const isTo = !isFrom && opts.nodeTo && opts.slotTo !== null + //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) + } - 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 - } + this.adjustMouseEvent(e) + const now = LiteGraph.getTime() + e.click_time = now - this.last_mouseclick + this.last_mouse_dragging = false + this.last_click_position = null - const nodeX = isFrom ? opts.nodeFrom : opts.nodeTo - let slotX = isFrom ? opts.slotFrom : opts.slotTo + //used to avoid sending twice a click in an immediate button + this.block_click &&= false - 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) + 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 } - // 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 + /** + * 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) } - const nodeX = isFrom ? opts.nodeFrom : opts.nodeTo - let slotX = isFrom ? opts.slotFrom : opts.slotTo + /** + * Called when a mouse wheel event has to be processed + **/ + processMouseWheel(e: CanvasWheelEvent): boolean { + if (!this.graph || !this.allow_dragcanvas) return - 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 - } + // TODO: Mouse wheel zoom rewrite + // @ts-expect-error + const delta = e.wheelDeltaY ?? e.detail * -60 - const options = ['Add Node', null] + this.adjustMouseEvent(e) - if (opts.allow_searchbox) { - options.push('Search') - options.push(null) - } + const pos: Point = [e.clientX, e.clientY] + if (this.viewport && !isPointInRectangle(pos, this.viewport)) return - // 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]) - } - } + let scale = this.ds.scale - // build menu - const menu = new LiteGraph.ContextMenu(options, { - event: opts.e, - title: (slotX && slotX.name != '' ? slotX.name + (fromSlotType ? ' | ' : '') : '') + (slotX && fromSlotType ? fromSlotType : ''), - callback: inner_clicked, - }) + if (delta > 0) scale *= this.zoom_speed + else if (delta < 0) scale *= 1 / this.zoom_speed - // 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 || '' + this.ds.changeScale(scale, [e.clientX, e.clientY]) - 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) - } - } + this.graph.change() - 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' - } else { - dialog.style.left = canvas.width * 0.5 + offsetx + 'px' - dialog.style.top = canvas.height * 0.5 + offsety + 'px' - } - - 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() - }, - 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 - }) - } - } - - // @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 - }) + 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) + ) } - // 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; + emitEvent(detail: CanvasEventDetail): void { + this.canvas.dispatchEvent(new CustomEvent( + "litegraph:canvas", + { + bubbles: true, + detail + } + )) + } - 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 + 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() + 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.dirty_canvas = 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 + + const low_quality = this.ds.scale < 0.6 //zoomed out + + //only render if it forces it to do it + if (this.live_mode) { + if (!node.flags.collapsed) { + ctx.shadowColor = "transparent" + node.onDrawForeground?.(ctx, this, this.canvas) + } + return + } + + const editor_alpha = this.editor_alpha + ctx.globalAlpha = editor_alpha + + if (this.render_shadows && !low_quality) { + ctx.shadowColor = LiteGraph.DEFAULT_SHADOW_COLOR + ctx.shadowOffsetX = 2 * this.ds.scale + ctx.shadowOffsetY = 2 * this.ds.scale + ctx.shadowBlur = 3 * this.ds.scale + } else { + ctx.shadowColor = "transparent" + } + + //custom draw collapsed method (draw after shadows because they are affected) + if (node.flags.collapsed && node.onDrawCollapsed?.(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 + } + } + + 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() + } + + //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, + }) + } + } + + //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() + } + + 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 + } + + //* 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 + } + + //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 + ) + } + } + } + + //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() + } + + //custom title render + node.onDrawTitle?.(ctx) + } + + //render selection marker + if (selected) { + node.onBounding?.(area) + + this.drawSelectionBounding( + ctx, + area, + { + shape, + title_height, + title_mode, + fgcolor, + collapsed: node.flags?.collapsed + } + ) + } + + // 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 + } + + // 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: + 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() + } + } + } + //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 () { + 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" + } else { + dialog.style.left = canvas.width * 0.5 + offsetx + "px" + dialog.style.top = canvas.height * 0.5 + offsety + "px" + } + + 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() + }, 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 + }) + } + } + + // @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 (selOut) { - const aSlots = LiteGraph.slot_types_out - const nSlots = aSlots.length // this for object :: Object.keys(aSlots).length; + 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 + 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 { - 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]) - } + 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() + }) } - 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) - } - } - - // 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') } - //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 - } - } + //compute best position + const rect = canvas.getBoundingClientRect() - 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) - } - } + 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" - 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++) { + //To avoid out of screen problems + if (event.layerY > (rect.height - 200)) // @ts-expect-error - addResult(filtered_extra[i], 'generic_type') - if (LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit) break - } - } + helper.style.maxHeight = (rect.height - event.layerY - 20) + "px" - // 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 - } - } + requestAnimationFrame(function () { + input.focus() + }) + if (options.show_all_on_open) refreshHelper() - 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 + 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 - // filter by slot IN, OUT types - if (options.do_type_filter && !opts.skipFilter) { - const sType = type + graphcanvas.graph.beforeChange() + const node = LiteGraph.createNode(name) + if (node) { + node.pos = graphcanvas.convertEventToCanvasOffset( + event + ) + graphcanvas.graph.add(node, false) + } - 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 + 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) + } + + } + + // 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() + } } - 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() - }) - - let v = node.properties[property] !== undefined ? node.properties[property] : '' - if (type !== 'string') { - v = JSON.stringify(v) - } - - // @ts-expect-error - input.value = v - input.addEventListener('keydown', function (e) { - if (e.keyCode == 27) { - //ESC + // @ts-expect-error Panel? dialog.close() - } else if (e.keyCode == 13) { - // ENTER - inner() // save - } else if (e.keyCode != 13) { - dialog.modified() - return - } - e.preventDefault() - e.stopPropagation() - }) - } - } - input?.focus() - - 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?.() - 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 - } - panel.addWidget('string', 'Title', node.title, {}, fUpdate) + 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 - panel.addWidget('combo', 'Mode', LiteGraph.NODE_MODES[node.mode], { values: LiteGraph.NODE_MODES }, fUpdate) + selected.classList.add("selected") + selected.scrollIntoView({ block: "end", behavior: "smooth" }) + } - const nodeCol = - node.color !== undefined - ? Object.keys(LGraphCanvas.node_colors).filter(function (nK) { - return LGraphCanvas.node_colors[nK].color == node.color + 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 + ) + + 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() + }) - panel.addWidget('combo', 'Color', nodeCol, { values: Object.keys(LGraphCanvas.node_colors) }, fUpdate) + let v = node.properties[property] !== undefined ? node.properties[property] : "" + if (type !== 'string') { + v = JSON.stringify(v) + } - 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() + // @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() + }) } - 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' + input?.focus() - 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(), - }, - }) - } - } + const button = dialog.querySelector("button") + button.addEventListener("click", inner) - //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) + function inner() { + setValue(input.value) } - 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 + + 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) } - 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 + 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() - } 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() - } + this.setDirty(true, true) + } - //if(v.callback) - // return v.callback.call(that, node, options, e, menu, that, event ); + 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) + }) + } + } + + 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 5b661de16..eec7a0d47 100644 --- a/src/LGraphGroup.ts +++ b/src/LGraphGroup.ts @@ -1,242 +1,253 @@ -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' - } - - /** 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, + constructor(title?: string) { + this.title = title || "Group" + this.color = LGraphCanvas.node_colors.pale_blue + ? LGraphCanvas.node_colors.pale_blue.groupcolor + : "#AAA" } - } - /** - * 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, - }) + /** 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 - 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 + this._pos[0] = v[0] + this._pos[1] = v[1] } - } - 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) + /** Size of the group, as width,height in graph units */ + get size() { + return this._size } - } + set size(v) { + if (!v || v.length < 2) return - /** - * 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 + this._size[0] = Math.max(140, v[0]) + this._size[1] = Math.max(80, v[1]) + } - const allNodes = [...(this._nodes || []), ...nodes] + get nodes() { + return this._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 + get titleHeight() { + return this.font_size * 1.4 + } - 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 + 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 { - left: Math.min(acc.left, x), - top: Math.min(acc.top, top), - right: Math.max(acc.right, right), - bottom: Math.max(acc.bottom, bottom), + 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: Infinity, top: Infinity, right: -Infinity, bottom: -Infinity }, - ) + } - this.pos = [bounds.left - padding, bounds.top - padding - this.titleHeight] + /** + * Draws the group on the canvas + * @param {LGraphCanvas} graphCanvas + * @param {CanvasRenderingContext2D} ctx + */ + draw(graphCanvas: LGraphCanvas, ctx: CanvasRenderingContext2D): void { + const padding = 4 - this.size = [bounds.right - bounds.left + padding * 2, bounds.bottom - bounds.top + padding * 2 + 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() - 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 }, - ] - } + ctx.beginPath() + ctx.moveTo(x + width, y + height) + ctx.lineTo(x + width - 10, y + height) + ctx.lineTo(x + width, y + height - 10) + ctx.fill() - isPointInTitlebar(x: number, y: number): boolean { - const b = this._bounding - return isInsideRectangle(x, y, b[0], b[1], b[2], this.titleHeight) - } + 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) - isPointInside = LGraphNode.prototype.isPointInside - setDirtyCanvas = LGraphNode.prototype.setDirtyCanvas + 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 } diff --git a/src/LGraphNode.ts b/src/LGraphNode.ts index 4a192b46e..afccfb962 100644 --- a/src/LGraphNode.ts +++ b/src/LGraphNode.ts @@ -1,66 +1,54 @@ -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 } /* @@ -116,7 +104,7 @@ supported callbacks: */ export interface LGraphNode { - constructor: LGraphNodeConstructor + constructor: LGraphNodeConstructor } /** @@ -124,2128 +112,2223 @@ 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 - } - 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 + _pos: Point = new Float32Array([10, 10]) + public get pos() { + return this._pos } - } + public set pos(value) { + if (!value || value.length < 2) return - // 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++ + this._pos[0] = value[0] + this._pos[1] = value[1] } - 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]) + + 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 } - 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]) + // 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++ } - } //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) - } else { - node.onExecute?.() - } - - return link.data - } - - /** - * 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 - } - - 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()) - - 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]) - } - - 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 - } - } - - 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 - } - - 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) - } - - info ||= {} - info.type ||= typeof this.properties[property] - if (info.widget == 'combo') info.type = 'enum' - - return info - } - - /** - * Defines a widget inside the node, it will be rendered on top of the node, you can control lots of properties - * - * @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 - } - - //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 - } - - /** - * 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 - - 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 - } - } - - /** - * 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 - } - - /** - * 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 - - const margin_top = skip_title || this.graph?.isLive() ? 0 : LiteGraph.NODE_TITLE_HEIGHT - - 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 - } - - /** - * 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.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 + 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] } - return returnObj ? slot : i - } } - } + + 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) } - return doNotUseOccupied ? -1 : (occupiedSlot ?? -1) - } + /** + * 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 + } - /** - * 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) + //special case for when there were errors + if (this.constructor === LGraphNode && this.last_serialization) + return this.last_serialization - 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 + if (this.inputs) o.inputs = this.inputs - // 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() + 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 } - // 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 - } + /* Creates a clone of this node */ + clone(): LGraphNode { + const node = LiteGraph.createNode(this.type) + if (!node) 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) + //we clone it because serialize returns shared containers + const data = LiteGraph.cloneObject(this.serialize()) - console.debug('[connectByType]: no way to connect type: ', target_slotType, ' to node: ', target_node) - return null - } + //remove links + if (data.inputs) { + for (let i = 0; i < data.inputs.length; ++i) { + data.inputs[i].link = 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) + if (data.outputs) { + for (let i = 0; i < data.outputs.length; ++i) { + if (data.outputs[i].links) { + data.outputs[i].links.length = 0 + } + } + } - console.debug('[connectByType]: no way to connect type: ', source_slotType, ' to node: ', source_node) - return null - } + delete data.id - /** - * 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 + if (LiteGraph.use_uuids) data.id = LiteGraph.uuidv4() - 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 + //remove links + node.configure(data) + + return node } - //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) + /** + * 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?.() + } + + return link.data + } + + /** + * 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 - } - } 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' + /** + * 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 - //avoid loopback - if (target_node == this) return null + const input = this.inputs[slot] + if (!input || input.link === null) 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 + const link_info = this.graph._links.get(input.link) + if (!link_info) return null + + return this.graph.getNodeById(link_info.origin_id) } - // 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 + /** + * 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] } - if (targetIndex === null || !target_node.inputs || targetIndex >= target_node.inputs.length) { - if (LiteGraph.debug) console.log('Connect: Error, slot number not found') - return null + /** + * 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 } - 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 + /** + * 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] } - // 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 - } + /** + * 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 } - const nextId = LiteGraph.use_uuids ? LiteGraph.uuidv4() : ++graph.last_link_id + /** + * tells you if there is any connection in the output slots + * @return {boolean} + */ + isAnyOutputConnected(): boolean { + if (!this.outputs) return false - //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) + for (let i = 0; i < this.outputs.length; ++i) { + if (this.outputs[i].links && this.outputs[i].links.length) { + return true + } + } 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 + /** + * 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 - //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' + if (slot >= this.outputs.length) return 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) + const output = this.outputs[slot] + if (!output.links || output.links.length == 0) return null - //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 + 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) + } + } } - } - } //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 + return r + } - target_node = graph.getNodeById(link_info.target_id) + 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()) + + 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]) + } + + 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 + } + } + + 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 + } + + 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) + } + + info ||= {} + info.type ||= typeof this.properties[property] + if (info.widget == "combo") + info.type = "enum" + + return info + } + + /** + * Defines a widget inside the node, it will be rendered on top of the node, you can control lots of properties + * + * @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 + } + + //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 + } + + /** + * 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 + + 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) + } + } + + /** + * 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 + } + + /** + * 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 + + const margin_top = skip_title || this.graph?.isLive() + ? 0 + : LiteGraph.NODE_TITLE_HEIGHT + + 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 + } + + /** + * 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.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) { - const input = target_node.inputs[link_info.target_slot] - //remove other side link - input.link = null + if (typeof target_node === "number") + target_node = graph.getNodeById(target_node) + if (!target_node) + throw "Target Node not found" - //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) + 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) - 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 - } + //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.setDirtyCanvas(false, true) - graph.connectionChange(this) - return true - } + //remove the link from the links pool + graph._links.delete(link_id) + graph._version++ - /** - * 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 - } + //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 + ) - const input = this.inputs[slot] - if (!input) 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 link_id = this.inputs[slot].link - if (link_id != null) { - this.inputs[slot].link = null + target_node = graph.getNodeById(link_info.target_id) + graph._version++ - //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 + if (target_node) { + const input = target_node.inputs[link_info.target_slot] + //remove other side link + input.link = null - const output = target_node.outputs[link_info.origin_slot] - if (!(output?.links?.length > 0)) 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) - //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.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 } - 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) + graph.connectionChange(this) + return true } - this.setDirtyCanvas(false, true) - this.graph?.connectionChange(this) - return true - } + /** + * 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 + } - /** - * 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 input = this.inputs[slot] + if (!input) return false - const num_slots = is_input ? (this.inputs?.length ?? 0) : (this.outputs?.length ?? 0) + const link_id = this.inputs[slot].link + if (link_id != null) { + this.inputs[slot].link = null - const offset = LiteGraph.NODE_SLOT_HEIGHT * 0.5 + //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 - 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 + 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 } - //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 + /** + * 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 } - //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 + /* 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) } - //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 + /* 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) } - //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 + /* 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 + ]) } - const img: AsyncImageElement = new Image() - img.src = LiteGraph.node_images_path + url - img.ready = false + loadImage(url: string): HTMLImageElement { + interface AsyncImageElement extends HTMLImageElement { ready?: boolean } - const that = this - img.onload = function (this: AsyncImageElement) { - this.ready = true - that.setDirtyCanvas(true) + 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 } - 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 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])]) + get collapsed() { + return !!this.flags.collapsed } - 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 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 + } } - } } diff --git a/src/LLink.ts b/src/LLink.ts index b13604372..353f6317b 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,96 +8,101 @@ 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 - } - - 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 + #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 } - } - /** - * @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] - } + 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 - 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, + 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 } - return copy - } } diff --git a/src/LiteGraphGlobal.ts b/src/LiteGraphGlobal.ts index 825968208..e844d6769 100644 --- a/src/LiteGraphGlobal.ts +++ b/src/LiteGraphGlobal.ts @@ -1,923 +1,946 @@ -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', - } - } - - 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] - } - - 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 - } + // Avoid circular dependency from original single-module + static { + LGraphCanvas.link_type_colors = { + "-1": LiteGraphGlobal.DEFAULT_EVENT_LINK_COLOR, + number: "#AAA", + node: "#DCA" } - } } - this.registered_node_types[type] = base_class - if (base_class.constructor.name) this.Nodes[classname] = base_class - - 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') - } - - /** - * 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 - - delete this.registered_node_types[base_class.type] - - const name = base_class.constructor.name - if (name) delete this.Nodes[name] - } - - /** - * 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 - - // @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 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) - } - 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 - - 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') - } - } - } - - 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 - } - - // only pointerevents - case 'leave': - case 'cancel': - case 'gotpointercapture': - case 'lostpointercapture': { - if (sMethod != 'mouse') { - return oDOM.addEventListener(sMethod + sEvent, fCall, capture) - } - 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 - } - - if (y < bounding[1]) { - bounding[1] = y - } else if (y > bounding[3]) { - bounding[3] = y - } - } - - 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]) - } - - 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)) + 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 { - target.prototype[i] = origin.prototype[i] + 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] } - //and setters - if (origin.prototype.__lookupSetter__(i)) { - target.prototype.__defineSetter__(i, origin.prototype.__lookupSetter__(i)) + const prev = this.registered_node_types[type] + if (prev) { + console.log("replacing node type: " + type) + } + if (!Object.prototype.hasOwnProperty.call(base_class.prototype, "shape")) { + Object.defineProperty(base_class.prototype, "shape", { + set(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 + } + } + } + } + + this.registered_node_types[type] = base_class + if (base_class.constructor.name) this.Nodes[classname] = base_class + + 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") + } + + /** + * 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 + + delete this.registered_node_types[base_class.type] + + const name = base_class.constructor.name + if (name) delete this.Nodes[name] + } + + /** + * 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 + + // @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 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) + } + 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 + + 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") + } + } + } + + 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) + } + } + 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) + } + } + + 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 + } + + if (y < bounding[1]) { + bounding[1] = y + } else if (y > bounding[3]) { + bounding[3] = y + } + } + + 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]) + } + + 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) + ) + } + } } - } } - } } diff --git a/src/MapProxyHandler.ts b/src/MapProxyHandler.ts index 65f7f1786..68c59bb83 100644 --- a/src/MapProxyHandler.ts +++ b/src/MapProxyHandler.ts @@ -1,56 +1,55 @@ /** 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 5c28ec8ae..8a6e74ed6 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,42 +42,44 @@ 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++) { @@ -86,58 +88,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 5af796836..68036b500 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,31 +42,13 @@ 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< - T, - 'fill' | 'copyWithin' | 'reverse' | 'set' | 'sort' | 'subarray' -> +type ReadOnlyTypedArray = Omit /** Union of property names that are of type Match */ export type KeysOfType = { [P in keyof T]: T[P] extends Match ? P : never }[keyof T] @@ -78,102 +60,96 @@ 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 1185578de..0a27d69c3 100644 --- a/src/litegraph.ts +++ b/src/litegraph.ts @@ -1,73 +1,32 @@ -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() @@ -81,69 +40,67 @@ 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 - desc?: 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 + nodeData: any + new(): T } // End backwards compat diff --git a/src/measure.ts b/src/measure.ts index 5842c94a6..d02fd6964 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,9 +21,7 @@ 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])) } /** @@ -33,12 +31,10 @@ 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] } /** @@ -52,7 +48,10 @@ 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 } /** @@ -63,8 +62,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 } /** @@ -74,17 +73,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 } /** @@ -94,9 +93,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]) } /** @@ -106,17 +105,15 @@ 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 } /** @@ -126,85 +123,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 + } } /** @@ -217,13 +214,11 @@ 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 @@ -232,20 +227,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 e33e2f422..758e3d1a8 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -1,80 +1,85 @@ + //API ************************************************* //like rect but rounded corners - -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) { +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 + 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 === 0) { + this.rect(x, y, w, h); + return; + } - if (radius_low === undefined) { - radius_low = radius - } + 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 - } + //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) + //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 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) + //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 + //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 - } +if (typeof window != "undefined" && !window["requestAnimationFrame"]) { + window.requestAnimationFrame = + // @ts-expect-error Legacy code + window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || + function (callback) { + window.setTimeout(callback, 1000 / 60); + }; } +} \ No newline at end of file diff --git a/src/strings.ts b/src/strings.ts index 78b7f41d3..31699266d 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 23904d110..c4d3c6d1c 100644 --- a/src/types/events.ts +++ b/src/types/events.ts @@ -2,102 +2,93 @@ * 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 72067f532..f43a16c50 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 ea4652932..24ae6b885 100644 --- a/src/types/serialisation.ts +++ b/src/types/serialisation.ts @@ -1,104 +1,89 @@ -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 c4d6fe2df..326287842 100644 --- a/src/types/widgets.ts +++ b/src/types/widgets.ts @@ -1,29 +1,28 @@ -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 } /** @@ -35,116 +34,94 @@ export interface IWidgetOptions * Recommend declaration merging any properties that use IWidget (e.g. {@link LGraphNode.widgets}) with a new type alias. * @see ICustomWidget */ -export type IWidget = - | IBooleanWidget - | INumericWidget - | IStringWidget - | IMultilineStringWidget - | IComboWidget - | ICustomWidget +export type IWidget = IBooleanWidget | INumericWidget | IStringWidget | IMultilineStringWidget | IComboWidget | ICustomWidget export interface IBooleanWidget extends IBaseWidget { - type?: 'toggle' - value: boolean + type?: "toggle" + value: boolean } /** Any widget that uses a numeric backing */ export interface INumericWidget extends IBaseWidget { - type?: 'slider' | 'number' - value: number + type?: "slider" | "number" + value: number } /** A combo-box widget (dropdown, select, etc) */ export interface IComboWidget extends IBaseWidget { - type?: 'combo' - value: string | number - options: IWidgetOptions + 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< - TElement extends HTMLElement = HTMLTextAreaElement, -> extends IBaseWidget { - type?: 'multiline' - value: string +export interface IMultilineStringWidget 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 + 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 - hidden?: boolean - advanced?: boolean + tooltip?: string - tooltip?: string + /** HTML widget element */ + element?: TElement - /** HTML widget element */ - element?: TElement + // TODO: Confirm this format + callback?(value: any, canvas?: LGraphCanvas, node?: LGraphNode, pos?: Point, e?: CanvasMouseEvent): void + onRemove?(): void + beforeQueued?(): void - // 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 + 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 69793cb4f..909c5c2d7 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,34 +73,33 @@ 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 12f1112a2..8a1680665 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 a2a9de82c..a846dc637 100644 --- a/test/LGraphNode.test.ts +++ b/test/LGraphNode.test.ts @@ -1,10 +1,13 @@ -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