From 4fb1e1ee82db96aa9c2d70c05f60ff001efd5db7 Mon Sep 17 00:00:00 2001 From: Ayush Kashyap Date: Thu, 5 Mar 2026 15:03:28 +0530 Subject: [PATCH 1/2] feat: Add the support for the functions and tabs --- package-lock.json | 12 + src/assets/starter.json | 1 + src/index.html | 39 +++ src/js/app.js | 42 ++- src/js/compiler/compiler.js | 116 ++++++- src/js/core/colorMap.js | 3 + src/js/editor/contextMenu.js | 51 ++- src/js/editor/deleteHandler.js | 1 + src/js/editor/nodeGroup.js | 11 +- src/js/editor/orphanOverlay.js | 85 +++++ src/js/editor/tabManager.js | 273 ++++++++++++++++ src/js/nodes/nodeFactory.js | 373 ++++++++++++++++++++- src/js/nodes/wiring.js | 28 +- src/js/persistence/saveAndLoad.js | 194 ++++++----- src/js/ui/dialogs.js | 21 +- src/js/ui/functionPanel.js | 522 ++++++++++++++++++++++++++++++ src/js/ui/variableList.js | 217 ++----------- src/js/utils/docTooltip.js | 46 +++ src/js/utils/wireMismatch.js | 127 ++++++++ src/style.css | 223 ++++++++++++- 20 files changed, 2074 insertions(+), 311 deletions(-) create mode 100644 package-lock.json create mode 100644 src/assets/starter.json create mode 100644 src/js/editor/orphanOverlay.js create mode 100644 src/js/editor/tabManager.js create mode 100644 src/js/ui/functionPanel.js create mode 100644 src/js/utils/docTooltip.js create mode 100644 src/js/utils/wireMismatch.js diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5546b9d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,12 @@ +{ + "name": "codewire", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "codewire", + "version": "1.0.0" + } + } +} diff --git a/src/assets/starter.json b/src/assets/starter.json new file mode 100644 index 0000000..8849bf6 --- /dev/null +++ b/src/assets/starter.json @@ -0,0 +1 @@ +{"variables":[{"name":"quantity","dataType":"Number","value":"100"},{"name":"unit_price","dataType":"Number","value":"1000"}],"nodesData":[{"position":{"x":329.66989447061565,"y":220.91332290742616},"nodeDescription":{"nodeTitle":"Begin","color":"Begin","rows":2,"colums":10,"execIn":false,"pinExecInId":null,"execOut":{"execOut0":{"execOutTitle":null,"pinExecOutId":"10","outOrder":0}}}},{"position":{"x":297.7233644556677,"y":381.88250599193964},"nodeDescription":{"nodeTitle":"Get quantity","outputs":{"output0":{"outputTitle":"Value(Ref)","dataType":"Number","pinOutId":"409","outOrder":0}},"color":"Get","rows":2,"colums":10}},{"position":{"x":481.099434396144,"y":410.6046845633682},"nodeDescription":{"nodeTitle":"Multiply","color":"Math","rows":2,"colums":10,"inputs":{"input0":{"inputTitle":"ValueA","dataType":"Number","defValue":0,"pinInId":"419","isInputBoxRequired":true},"input1":{"inputTitle":"ValueB","dataType":"Number","defValue":0,"pinInId":"424","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"Result","dataType":"Number","pinOutId":"429","outOrder":0}}}},{"position":{"x":288.7184820151915,"y":508.8933750395587},"nodeDescription":{"nodeTitle":"Get unit_price","outputs":{"output0":{"outputTitle":"Value(Ref)","dataType":"Number","pinOutId":"444","outOrder":0}},"color":"Get","rows":2,"colums":10}},{"position":{"x":1084.7283004870192,"y":258.0682468154959},"nodeDescription":{"nodeTitle":"Call display_dollar_price","execIn":true,"pinExecInId":"475","execOut":{"execOut0":{"execOutTitle":null,"pinExecOutId":"476","outOrder":0}},"color":"Call","rows":2,"colums":14,"isCallFunction":true,"calledFunctionName":"display_dollar_price","docString":"Function to fetch USD exchange rate and do the INR to USD conversion","inputs":{"input0":{"inputTitle":"inr_amount","dataType":"Number","defValue":"0","pinInId":"477","isInputBoxRequired":true}}}},{"position":{"x":705.7092310126123,"y":164.6421543315391},"nodeDescription":{"nodeTitle":"Call get_discounted_price","execIn":true,"pinExecInId":"540","execOut":{"execOut0":{"execOutTitle":null,"pinExecOutId":"541","outOrder":0}},"color":"Call","rows":3,"colums":14,"isCallFunction":true,"calledFunctionName":"get_discounted_price","docString":"Function to get the discounted prices.","inputs":{"input0":{"inputTitle":"total_amount","dataType":"Number","defValue":"0","pinInId":"542","isInputBoxRequired":true},"input1":{"inputTitle":"discount_perc","dataType":"Number","defValue":"0","pinInId":"547","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"discounted_amount","dataType":"Number","pinOutId":"552","outOrder":1}}}},{"position":{"x":1784.8503547906926,"y":255.8692215133575},"nodeDescription":{"nodeTitle":"Print","color":"Print","rows":3,"colums":12,"execIn":true,"pinExecInId":"1122","execOut":{"execOut0":{"execOutTitle":null,"pinExecOutId":"1123","outOrder":0}},"inputs":{"input0":{"inputTitle":"Value","dataType":"Data","defValue":"'hello'","pinInId":"1124","isInputBoxRequired":true}}}},{"position":{"x":1447.643880113113,"y":264.1809681973247},"nodeDescription":{"nodeTitle":"Call recursive_factorial","execIn":true,"pinExecInId":"1100","execOut":{"execOut0":{"execOutTitle":null,"pinExecOutId":"1101","outOrder":0}},"color":"Call","rows":2,"colums":14,"isCallFunction":true,"calledFunctionName":"recursive_factorial","docString":"","inputs":{"input0":{"inputTitle":"num","dataType":"Number","defValue":"10","pinInId":"1102","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"factorial","dataType":"Number","pinOutId":"1107","outOrder":1}}}}],"wireData":[{"srcId":"444","destId":"424"},{"srcId":"409","destId":"419"},{"srcId":"10","destId":"540"},{"srcId":"541","destId":"475"},{"srcId":"429","destId":"542"},{"srcId":"552","destId":"477"},{"srcId":"476","destId":"1100"},{"srcId":"1101","destId":"1122"},{"srcId":"1107","destId":"1124"}],"groupsData":[{"position":{"x":1398.019131518612,"y":137.39846411260328},"width":625.3779656250006,"height":381.60818718750033,"name":"Calling a recursive function for test"}],"functions":[{"name":"get_discounted_price","inputParams":[{"name":"total_amount","dataType":"Number","defValue":"0"},{"name":"discount_perc","dataType":"Number","defValue":"0"}],"outputParams":[{"name":"discounted_amount","dataType":"Number"}],"variables":[],"docString":"Function to get the discounted prices.","nodesData":[{"position":{"x":369.2488819263885,"y":218.72366699514882},"nodeDescription":{"nodeTitle":"FunctionBegin","execIn":false,"pinExecInId":null,"execOut":{"execOut0":{"execOutTitle":null,"pinExecOutId":"38","outOrder":0}},"color":"FunctionBegin","rows":3,"colums":12,"isDeletable":false,"outputs":{"output0":{"outputTitle":"total_amount","dataType":"Number","pinOutId":"39","outOrder":1},"output1":{"outputTitle":"discount_perc","dataType":"Number","pinOutId":"41","outOrder":2}}}},{"position":{"x":858.8968414596872,"y":322.2400151803591},"nodeDescription":{"nodeTitle":"Multiply","color":"Math","rows":2,"colums":10,"inputs":{"input0":{"inputTitle":"ValueA","dataType":"Number","defValue":0,"pinInId":"51","isInputBoxRequired":true},"input1":{"inputTitle":"ValueB","dataType":"Number","defValue":0,"pinInId":"56","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"Result","dataType":"Number","pinOutId":"61","outOrder":0}}}},{"position":{"x":787.2415126615012,"y":495.4826455658467},"nodeDescription":{"nodeTitle":"Divide","color":"Math","rows":2,"colums":10,"inputs":{"input0":{"inputTitle":"ValueA","dataType":"Number","defValue":0,"pinInId":"76","isInputBoxRequired":true},"input1":{"inputTitle":"ValueB","dataType":"Number","defValue":"100","pinInId":"81","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"Result","dataType":"Number","pinOutId":"86","outOrder":0}}}},{"position":{"x":563.2052314823627,"y":446.50305372911185},"nodeDescription":{"nodeTitle":"Subtract","color":"Math","rows":2,"colums":10,"inputs":{"input0":{"inputTitle":"ValueA","dataType":"Number","defValue":"100","pinInId":"97","isInputBoxRequired":true},"input1":{"inputTitle":"ValueB","dataType":"Number","defValue":0,"pinInId":"102","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"Result","dataType":"Number","pinOutId":"107","outOrder":0}}}},{"position":{"x":1078.0120126333236,"y":196.95495951215565},"nodeDescription":{"nodeTitle":"Return","execIn":true,"pinExecInId":"129","color":"Return","rows":2,"colums":12,"isDeletable":false,"inputs":{"input0":{"inputTitle":"discounted_amount","dataType":"Number","defValue":null,"pinInId":"130","isInputBoxRequired":false}}}}],"wireData":[{"srcId":"38","destId":"129"},{"srcId":"61","destId":"130"},{"srcId":"86","destId":"56"},{"srcId":"107","destId":"76"},{"srcId":"41","destId":"102"},{"srcId":"39","destId":"51"}],"groupsData":[]},{"name":"display_dollar_price","inputParams":[{"name":"inr_amount","dataType":"Number","defValue":"0"}],"outputParams":[],"variables":[],"docString":"Function to fetch USD exchange rate and do the INR to USD conversion","nodesData":[{"position":{"x":200.40309411510998,"y":116.32079483144673},"nodeDescription":{"nodeTitle":"FunctionBegin","execIn":false,"pinExecInId":null,"execOut":{"execOut0":{"execOutTitle":null,"pinExecOutId":"161","outOrder":0}},"color":"FunctionBegin","rows":2,"colums":12,"isDeletable":false,"outputs":{"output0":{"outputTitle":"inr_amount","dataType":"Number","pinOutId":"162","outOrder":1}}}},{"position":{"x":630.439618606527,"y":595.1399853189833},"nodeDescription":{"nodeTitle":"HttpRequest","color":"Func","rows":2,"colums":12,"execIn":true,"pinExecInId":"177","execOut":{"execOut0":{"execOutTitle":"OnSuccess","pinExecOutId":"178","outOrder":0},"execOut1":{"execOutTitle":"OnFail","pinExecOutId":"180","outOrder":2},"execOut2":{"execOutTitle":"Continue","pinExecOutId":"182","outOrder":3}},"inputs":{"input0":{"inputTitle":"URL","dataType":"String","defValue":"'https://api.frankfurter.app/latest?from=USD&to=INR'","pinInId":"184","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"JSON","dataType":"Data","pinOutId":"189","outOrder":1}}}},{"position":{"x":922.9646427992587,"y":749.5765375036962},"nodeDescription":{"nodeTitle":"GetByName(JSON)","color":"Obj","rows":2,"colums":12,"inputs":{"input0":{"inputTitle":"JSON","dataType":"Data","defValue":null,"pinInId":"230","isInputBoxRequired":false},"input1":{"inputTitle":"Name","dataType":"String","defValue":"'rates'","pinInId":"232","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"Data","dataType":"Data","pinOutId":"237","outOrder":0}}}},{"position":{"x":1173.4523588393488,"y":734.2697894033207},"nodeDescription":{"nodeTitle":"GetByName(JSON)","color":"Obj","rows":2,"colums":12,"inputs":{"input0":{"inputTitle":"JSON","dataType":"Data","defValue":null,"pinInId":"252","isInputBoxRequired":false},"input1":{"inputTitle":"Name","dataType":"String","defValue":"'INR'","pinInId":"254","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"Data","dataType":"Data","pinOutId":"259","outOrder":0}}}},{"position":{"x":1427.9573136257832,"y":746.651492565948},"nodeDescription":{"nodeTitle":"ParseFloat","color":"Str","rows":2,"colums":11,"inputs":{"input0":{"inputTitle":"String","dataType":"String","defValue":"'0.0'","pinInId":"275","isInputBoxRequired":false}},"outputs":{"output0":{"outputTitle":"Number","dataType":"Number","pinOutId":"277","outOrder":0}}}},{"position":{"x":1525.9905834056824,"y":145.19684752858797},"nodeDescription":{"nodeTitle":"Divide","color":"Math","rows":2,"colums":10,"inputs":{"input0":{"inputTitle":"ValueA","dataType":"Number","defValue":0,"pinInId":"297","isInputBoxRequired":true},"input1":{"inputTitle":"ValueB","dataType":"Number","defValue":0,"pinInId":"302","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"Result","dataType":"Number","pinOutId":"307","outOrder":0}}}},{"position":{"x":1750.1563808724957,"y":203.03720934837258},"nodeDescription":{"nodeTitle":"Round","color":"Math","rows":2,"colums":10,"inputs":{"input0":{"inputTitle":"Value","dataType":"Number","defValue":0,"pinInId":"469","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"Result","dataType":"Number","pinOutId":"474","outOrder":0}}}},{"position":{"x":1980.7732783708489,"y":195.18502976456716},"nodeDescription":{"nodeTitle":"ToString","color":"Str","rows":2,"colums":10,"inputs":{"input0":{"inputTitle":"Value","dataType":"Data","defValue":0,"pinInId":"510","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"Result","dataType":"String","pinOutId":"515","outOrder":0}}}},{"position":{"x":2250.8344686723267,"y":211.72541952720098},"nodeDescription":{"nodeTitle":"Concat","color":"Str","rows":2,"colums":10,"inputs":{"input0":{"inputTitle":"A","dataType":"String","defValue":"''","pinInId":"490","isInputBoxRequired":true},"input1":{"inputTitle":"B","dataType":"String","defValue":"' USD'","pinInId":"495","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"Result","dataType":"String","pinOutId":"500","outOrder":0}}}},{"position":{"x":2849.4639972846294,"y":368.03953046698064},"nodeDescription":{"nodeTitle":"Return","execIn":true,"pinExecInId":"155","color":"Return","rows":2,"colums":12,"isDeletable":false}},{"position":{"x":2486.452960358817,"y":392.9088963218603},"nodeDescription":{"nodeTitle":"Print","color":"Print","rows":3,"colums":12,"execIn":true,"pinExecInId":"205","execOut":{"execOut0":{"execOutTitle":null,"pinExecOutId":"206","outOrder":0}},"inputs":{"input0":{"inputTitle":"Value","dataType":"Data","defValue":"'hello'","pinInId":"207","isInputBoxRequired":true}}}}],"wireData":[{"srcId":"161","destId":"177"},{"srcId":"189","destId":"230"},{"srcId":"237","destId":"252"},{"srcId":"259","destId":"275"},{"srcId":"162","destId":"297"},{"srcId":"277","destId":"302"},{"srcId":"307","destId":"469"},{"srcId":"474","destId":"510"},{"srcId":"515","destId":"490"},{"srcId":"178","destId":"205"},{"srcId":"206","destId":"155"},{"srcId":"500","destId":"207"}],"groupsData":[{"position":{"x":460.312324873067,"y":525.8005840089143},"width":1239.5888109776342,"height":428.3992868424674,"name":"Get Data From the API"}]},{"name":"recursive_factorial","inputParams":[{"name":"num","dataType":"Number","defValue":"10"}],"outputParams":[{"name":"factorial","dataType":"Number"}],"variables":[{"name":"ret","dataType":"Number","value":"1"}],"docString":"","nodesData":[{"position":{"x":1023.0066749449601,"y":223.81688341778772},"nodeDescription":{"nodeTitle":"Subtract","color":"Math","rows":2,"colums":10,"inputs":{"input0":{"inputTitle":"ValueA","dataType":"Number","defValue":0,"pinInId":"702","isInputBoxRequired":true},"input1":{"inputTitle":"ValueB","dataType":"Number","defValue":"1","pinInId":"707","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"Result","dataType":"Number","pinOutId":"712","outOrder":0}}}},{"position":{"x":1298.3243883332486,"y":134.7520340063008},"nodeDescription":{"nodeTitle":"Call recursive_factorial","execIn":true,"pinExecInId":"904","execOut":{"execOut0":{"execOutTitle":null,"pinExecOutId":"905","outOrder":0}},"color":"Call","rows":2,"colums":14,"isCallFunction":true,"calledFunctionName":"recursive_factorial","docString":"","inputs":{"input0":{"inputTitle":"num","dataType":"Number","defValue":"10","pinInId":"906","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"factorial","dataType":"Number","pinOutId":"911","outOrder":1}}}},{"position":{"x":1699.9591584998007,"y":182.40218599576008},"nodeDescription":{"nodeTitle":"Set ret","execIn":true,"pinExecInId":"1019","execOut":{"execOut0":{"execOutTitle":null,"pinExecOutId":"1020","outOrder":0}},"inputs":{"input0":{"inputTitle":"Value","dataType":"Number","defValue":0,"pinInId":"1021"}},"outputs":{"output0":{"outputTitle":"Value(Ref)","dataType":"Number","pinOutId":"1026","outOrder":1}},"color":"Func","rows":2,"colums":12}},{"position":{"x":1928.7081075436731,"y":-55.477309898947595},"nodeDescription":{"nodeTitle":"Return","execIn":true,"pinExecInId":"523","color":"Return","rows":2,"colums":12,"isDeletable":false,"inputs":{"input0":{"inputTitle":"factorial","dataType":"Number","defValue":null,"pinInId":"524","isInputBoxRequired":false}}}},{"position":{"x":1574.750390698565,"y":-150.03088000044787},"nodeDescription":{"nodeTitle":"Set ret","execIn":true,"pinExecInId":"1052","execOut":{"execOut0":{"execOutTitle":null,"pinExecOutId":"1053","outOrder":0}},"inputs":{"input0":{"inputTitle":"Value","dataType":"Number","defValue":"1","pinInId":"1054"}},"outputs":{"output0":{"outputTitle":"Value(Ref)","dataType":"Number","pinOutId":"1059","outOrder":1}},"color":"Func","rows":2,"colums":12}},{"position":{"x":1584.2318667750133,"y":72.17595609453402},"nodeDescription":{"nodeTitle":"Get ret","outputs":{"output0":{"outputTitle":"Value(Ref)","dataType":"Number","pinOutId":"1080","outOrder":0}},"color":"Get","rows":2,"colums":10}},{"position":{"x":52.12485457043243,"y":78.97408515991168},"nodeDescription":{"nodeTitle":"FunctionBegin","execIn":false,"pinExecInId":null,"execOut":{"execOut0":{"execOutTitle":null,"pinExecOutId":"515","outOrder":0}},"color":"FunctionBegin","rows":2,"colums":12,"isDeletable":false,"outputs":{"output0":{"outputTitle":"num","dataType":"Number","pinOutId":"516","outOrder":1}}}},{"position":{"x":752.6042405443744,"y":-146.98291402171543},"nodeDescription":{"nodeTitle":"If/Else","color":"Logic","rows":3,"colums":12,"execIn":true,"pinExecInId":"792","execOut":{"execOut0":{"execOutTitle":"True","pinExecOutId":"793","outOrder":0},"execOut1":{"execOutTitle":"False","pinExecOutId":"795","outOrder":1},"execOut2":{"execOutTitle":"Done","pinExecOutId":"797","outOrder":2}},"inputs":{"input0":{"inputTitle":"Bool","dataType":"Boolean","defValue":true,"pinInId":"799","isInputBoxRequired":true}}}},{"position":{"x":572.6646227778107,"y":15.169287303525039},"nodeDescription":{"nodeTitle":"Equals","color":"Logic","rows":2,"colums":10,"inputs":{"input0":{"inputTitle":"ValueA","dataType":"Data","defValue":0,"pinInId":"817","isInputBoxRequired":true},"input1":{"inputTitle":"ValueB","dataType":"Data","defValue":"1","pinInId":"822","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"Result","dataType":"Boolean","pinOutId":"827","outOrder":0}}}},{"position":{"x":1485.3160975104106,"y":460.73643125691024},"nodeDescription":{"nodeTitle":"Multiply","color":"Math","rows":2,"colums":10,"inputs":{"input0":{"inputTitle":"ValueA","dataType":"Number","defValue":0,"pinInId":"606","isInputBoxRequired":true},"input1":{"inputTitle":"ValueB","dataType":"Number","defValue":0,"pinInId":"611","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"Result","dataType":"Number","pinOutId":"616","outOrder":0}}}}],"wireData":[{"srcId":"1080","destId":"524"},{"srcId":"1053","destId":"523"},{"srcId":"905","destId":"1019"},{"srcId":"1020","destId":"523"},{"srcId":"712","destId":"906"},{"srcId":"516","destId":"702"},{"srcId":"515","destId":"792"},{"srcId":"793","destId":"1052"},{"srcId":"795","destId":"904"},{"srcId":"516","destId":"817"},{"srcId":"827","destId":"799"},{"srcId":"911","destId":"606"},{"srcId":"516","destId":"611"},{"srcId":"616","destId":"1021"}],"groupsData":[]}]} \ No newline at end of file diff --git a/src/index.html b/src/index.html index c7fe01e..671d3c6 100644 --- a/src/index.html +++ b/src/index.html @@ -27,6 +27,33 @@
+ + + +
Add Variable
@@ -64,8 +91,20 @@
+
Functions
+
+
+
+
    +
    +
    +
    +
    + +
    diff --git a/src/js/app.js b/src/js/app.js index 1d6ce79..44a5a24 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -12,37 +12,49 @@ import { Export, Import, Save, prompLastSave } from './persistence/saveAndLoad.j import { showAlert, prompRefreshOrStarter } from './ui/dialogs.js' import { refresh } from './compiler/codePreview.js' import { enableNodeGroups } from './editor/nodeGroup.js' -// var width = window.innerWidth; -// var height = window.innerHeight; +import { tabManager } from './editor/tabManager.js' +import { initFunctionPanel } from './ui/functionPanel.js' + let stage = AppStage.getStage(document.getElementById("container").clientWidth, document.getElementById("container").clientHeight, 'container'); var layer = new Konva.Layer({ id: 'main_layer' }); +let wireLayer = new Konva.Layer({ + id: 'wireLayer', +}); let dragLayer = new Konva.Layer({ id: 'dragLayer', }); +stage.add(wireLayer); stage.add(layer); stage.add(dragLayer); +layer.moveToBottom(); stage.container().style.backgroundPosition = `${stage.position().x} ${stage.position().y}`; -// stage.on("wheel", () => { -// if (inputIsFocused) { -// document.getElementById("number-ip").blur(); -// document.getElementById("string-ip").blur(); -// document.getElementById("bool-ip").blur(); -// } -// }); +tabManager.init(stage, layer, wireLayer, dragLayer); + // SelectionBox.setSelectionBox(layer, stage); -enableNodeGroups(stage, layer); +enableNodeGroups(stage); Delete.enableDelete(stage, layer); -// DragAndDrop.DragAndDrop(stage, layer); -Wiring.enableWiring(stage, layer); -ContextMenu.contextMenu(stage, layer); +Wiring.enableWiring(stage); +ContextMenu.contextMenu(stage); let panel = new leftPanel(); variableList.init(layer, stage); -// layer.toggleHitCanvas(); -// document.getElementById("number-ip").value = 12; +tabManager.getTab('main').variables = variableList.variables; +initFunctionPanel(); layer.draw(); + +document.getElementById('add-tab-btn').addEventListener('click', () => { + const name = prompt('Enter function name:'); + if (name && name.trim()) { + tabManager.createTab(name.trim()); + } +}); + +tabManager.on('tabSwitched', ({ to }) => { + variableList.layer = to.layer; + variableList.switchToTab(to.variables); +}); document.getElementById("Run").addEventListener("click", (e) => { try { let script = new VSToJS(stage, layer, "Run").script; diff --git a/src/js/compiler/compiler.js b/src/js/compiler/compiler.js index 27e9ae1..09b0956 100644 --- a/src/js/compiler/compiler.js +++ b/src/js/compiler/compiler.js @@ -2,6 +2,7 @@ import { variableList } from '../ui/variableList.js' import { showAlert } from '../ui/dialogs.js' import { BuilInFunctions } from './builtInFunctions.js' import { runExecCodegen, runExprCodegen, hasType } from '../registry/index.js' +import { tabManager } from '../editor/tabManager.js' export var VSToJS = class { constructor(stage, layer, isRunOrCode) { @@ -10,7 +11,13 @@ export var VSToJS = class { this.nodeCount = 0; this.isRunOrCode = isRunOrCode; this.compileIssues = []; - for (let variable of variableList.variables) { + this._callCounter = 0; + + this.compileFunctionTabs(); + + const mainTab = tabManager.getTab('main'); + const globalVars = mainTab ? mainTab.variables : variableList.variables; + for (let variable of globalVars) { this.script += `let ${variable.name} = ${variable.value};\n`; } let begin = this.getBegin(stage); @@ -88,6 +95,33 @@ export var VSToJS = class { } } } + compileFunctionTabs() { + const funcTabs = tabManager.getAllFunctionTabs(); + for (const tab of funcTabs) { + const funcBegin = tab.layer.findOne('#FunctionBegin'); + if (!funcBegin) continue; + + const params = tab.inputParams.map(p => p.name).join(', '); + const savedScript = this.script; + this.script = ''; + + if (tab.variables && tab.variables.length > 0) { + for (const v of tab.variables) { + this.script += `let ${v.name} = ${v.value};\n`; + } + } + + const execOutPins = this.getExecOut(funcBegin); + for (let each of execOutPins) { + this.coreAlgorithm(each); + } + + const bodyScript = this.script; + this.script = savedScript; + this.script += `function ${tab.name}(${params}) {\n${bodyScript}}\n`; + } + } + getBegin(stage) { let X = stage.find("#Begin"); if (X.length == 0) { @@ -127,7 +161,8 @@ export var VSToJS = class { X.push({ node: aNode.wire.attrs.src.getParent(), isWire: true, wire: aNode.wire, srcOutputPinNumber: this.getSrcOutputPinNumber(aNode.wire.attrs.src.getParent(), aNode.wire) }); } else { - X.push({ node: aNode.textBox.textBox.text(), isWire: false, wire: null, srcOutputPinNumber: null }); + const val = aNode.textBox ? aNode.textBox.textBox.text() : 'undefined'; + X.push({ node: val, isWire: false, wire: null, srcOutputPinNumber: null }); } } return X; @@ -144,6 +179,54 @@ export var VSToJS = class { return; } + const nodeType = node.customClass.type.typeOfNode; + + if (nodeType === 'FunctionBegin') { + let execOutPins = this.getExecOut(node); + for (let each of execOutPins) { + this.coreAlgorithm(each); + } + return; + } + + if (nodeType === 'Return') { + let inputPins = this.getInputPins(node); + if (inputPins.length > 0) { + const nd = node.customClass.nodeDescription; + const outputNames = []; + if (nd.inputs) { + for (const key of Object.keys(nd.inputs)) { + outputNames.push(nd.inputs[key].inputTitle); + } + } + if (inputPins.length === 1) { + this.script += `return ${this.handleInputs(inputPins[0])};\n`; + } else { + const pairs = outputNames.map((name, i) => + `${name}: ${this.handleInputs(inputPins[i])}` + ).join(', '); + this.script += `return { ${pairs} };\n`; + } + } else { + this.script += `return;\n`; + } + return; + } + + if (nodeType.startsWith('Call ')) { + let inputPins = this.getInputPins(node); + let execOutPins = this.getExecOut(node); + const funcName = node.customClass.nodeDescription.calledFunctionName || nodeType.slice(5); + const args = inputPins.map(ip => this.handleInputs(ip)).join(', '); + const varName = `_call_${funcName}_${this._callCounter++}`; + this.script += `let ${varName} = ${funcName}(${args});\n`; + node._callResultVar = varName; + for (let each of execOutPins) { + this.coreAlgorithm(each); + } + return; + } + let execOutPins = this.getExecOut(node); let inputPins = this.getInputPins(node); if (node.customClass.type.isGetSet) { @@ -176,6 +259,35 @@ export var VSToJS = class { return "undefined"; } + const nodeType = inputNode.node.customClass.type.typeOfNode; + + if (nodeType === 'FunctionBegin') { + const nd = inputNode.node.customClass.nodeDescription; + if (nd.outputs) { + const outputKey = `output${inputNode.srcOutputPinNumber}`; + const paramName = nd.outputs[outputKey]?.outputTitle; + if (paramName) return paramName; + } + return 'undefined'; + } + + if (nodeType.startsWith('Call ')) { + const resultVar = inputNode.node._callResultVar; + if (resultVar) { + const nd = inputNode.node.customClass.nodeDescription; + if (nd.outputs) { + const outputKeys = Object.keys(nd.outputs); + if (outputKeys.length === 1) { + return resultVar; + } + const outputName = nd.outputs[`output${inputNode.srcOutputPinNumber}`]?.outputTitle; + if (outputName) return `${resultVar}.${outputName}`; + } + return resultVar; + } + return 'undefined'; + } + let inputPins = this.getInputPins(inputNode.node); if (inputNode.node.customClass.type.isGetSet) { return `${inputNode.node.customClass.type.typeOfNode.slice(4)}`; diff --git a/src/js/core/colorMap.js b/src/js/core/colorMap.js index 7aa9d54..51b7201 100644 --- a/src/js/core/colorMap.js +++ b/src/js/core/colorMap.js @@ -26,6 +26,9 @@ export const colorMap = { 'Get': '#2e7d32', // Array / access – green 'Str': '#ad1457', // String – pink/magenta 'Obj': '#00695c', // Object / Map – teal + 'FunctionBegin': '#ff6d00', // Function entry – warm orange + 'Return': '#d50000', // Function exit – red + 'Call': '#00bfa5', // Function call – teal 'GroupBody': 'rgba(255, 255, 255, 0.31)', 'GroupBorder': 'rgba(255,255,255,0.3)', 'GroupTitleBar': 'rgba(41,68,150,0.8)', diff --git a/src/js/editor/contextMenu.js b/src/js/editor/contextMenu.js index cbfd5d3..b0a95bb 100644 --- a/src/js/editor/contextMenu.js +++ b/src/js/editor/contextMenu.js @@ -4,6 +4,7 @@ import { variableList } from '../ui/variableList.js' import { deleteProgramNode, deleteWire } from './deleteHandler.js' import { getMenuOrderGroupedByCategory, getDefinition } from '../registry/index.js' import { colorMap, lightenHex } from '../core/colorMap.js' +import { tabManager } from './tabManager.js' const CATEGORY_LABELS = { Begin: 'Flow', @@ -19,7 +20,7 @@ const CATEGORY_LABELS = { const DEFAULT_EXPANDED_CATEGORIES = new Set(['Begin', 'Logic']); export var ContextMenu = { - contextMenu: function (stage, layer) { + contextMenu: function (stage) { let contextMenu = document.getElementById("ctx-menu-container"); let contextMenuList = document.getElementById("context-menu"); let deleteCtxMenu = document.getElementById("delete-ctx-container"); @@ -126,6 +127,7 @@ export var ContextMenu = { const category = section.dataset.category; const expanded = DEFAULT_EXPANDED_CATEGORIES.has(category); section.classList.toggle('ctx-menu-section--collapsed', !expanded); + section.classList.remove('hidden'); const arr = section.querySelector('.ctx-menu-section-arrow'); if (arr) arr.textContent = expanded ? '\u25BC' : '\u25B6'; const items = section.querySelectorAll('.context-menu-items'); @@ -133,6 +135,23 @@ export var ContextMenu = { }); contextMenuList.querySelectorAll('.context-menu-items').forEach((el) => el.classList.remove('hidden')); } + ContextMenu.resetFilter = function () { + const bar = document.getElementById('ctx-search-bar'); + const list = document.getElementById('context-menu'); + if (bar) bar.value = ''; + if (list) { + list.querySelectorAll('.ctx-menu-section').forEach((section) => { + const category = section.dataset.category; + const expanded = DEFAULT_EXPANDED_CATEGORIES.has(category); + section.classList.toggle('ctx-menu-section--collapsed', !expanded); + section.classList.remove('hidden'); + const arr = section.querySelector('.ctx-menu-section-arrow'); + if (arr) arr.textContent = expanded ? '\u25BC' : '\u25B6'; + section.querySelectorAll('.context-menu-items').forEach((el) => el.classList.remove('hidden')); + }); + list.querySelectorAll('.context-menu-items').forEach((el) => el.classList.remove('hidden')); + } + }; function toggleDeleteCtxMenu(location, show) { if (show) { deleteCtxMenu.classList.toggle("hidden", false); @@ -157,7 +176,7 @@ export var ContextMenu = { ContextMenu.addEventToCtxMenuItems = function (e) { e.addEventListener('click', function (ev) { if (ev.target.classList.contains('ctx-menu-section-header')) return; - makeNode(e, stage, layer, toggleContextMenu); + makeNode(e, stage, tabManager.getActiveLayer(), toggleContextMenu); }); }; searchBar.addEventListener("input", (e) => { @@ -229,7 +248,7 @@ export var ContextMenu = { stage.draw(); } else if (parentGroup && parentGroup.name() == 'aProgramNodeGroup') { - deleteProgramNode(e, layer, stage); + deleteProgramNode(e, tabManager.getActiveLayer(), stage); stage.draw(); } else if (e.target.name() == "isConnection") { @@ -259,11 +278,12 @@ export var ContextMenu = { let nodeType = e.target.innerHTML + " " + draggedVariableInfo.name; let xx = e.target.parentElement.getBoundingClientRect().x - stage.getContainer().getBoundingClientRect().x; let yy = e.target.parentElement.getBoundingClientRect().y - stage.getContainer().getBoundingClientRect().y; + let activeLayer = tabManager.getActiveLayer(); if (e.target.innerHTML == "Get") { - Nodes.CreateNode(nodeType, { x: xx, y: yy }, layer, stage, "Get", draggedVariableInfo.dataType, null); + Nodes.CreateNode(nodeType, { x: xx, y: yy }, activeLayer, stage, "Get", draggedVariableInfo.dataType, null); } else { - Nodes.CreateNode(nodeType, { x: xx, y: yy }, layer, stage, "Set", draggedVariableInfo.dataType, null); + Nodes.CreateNode(nodeType, { x: xx, y: yy }, activeLayer, stage, "Set", draggedVariableInfo.dataType, null); } }); @@ -278,7 +298,19 @@ export var ContextMenu = { }); stage.getContainer().addEventListener('drop', (e) => { e.preventDefault(); - if (e.dataTransfer.getData("variableName")) { + if (e.dataTransfer.getData("functionTabId")) { + const funcTabId = e.dataTransfer.getData("functionTabId"); + const funcTab = tabManager.getTab(funcTabId); + if (funcTab) { + const containerRect = stage.getContainer().getBoundingClientRect(); + const x = e.clientX - containerRect.x; + const y = e.clientY - containerRect.y; + const activeLayer = tabManager.getActiveLayer(); + Nodes.CreateCallNode(funcTab.name, funcTab.inputParams, funcTab.outputParams, + { x, y }, activeLayer, stage, funcTab.docString || ''); + activeLayer.draw(); + } + } else if (e.dataTransfer.getData("variableName")) { toggleGetSetCtxMenu([e.clientX, e.clientY], true); draggedVariableInfo = { name: e.dataTransfer.getData("variableName"), @@ -287,6 +319,13 @@ export var ContextMenu = { } e.stopPropagation(); }); + + tabManager.on('tabSwitched', () => { + toggleContextMenu([0, 0], false); + toggleDeleteCtxMenu([], false); + toggleGetSetCtxMenu([], false); + ContextMenu.resetFilter(); + }); } } diff --git a/src/js/editor/deleteHandler.js b/src/js/editor/deleteHandler.js index d9d950a..9481894 100644 --- a/src/js/editor/deleteHandler.js +++ b/src/js/editor/deleteHandler.js @@ -5,6 +5,7 @@ export var Delete = { } export function deleteProgramNode(e, layer, stage) { let node = e.target.getParent(); + if (node.customClass && node.customClass.isDeletable === false) return; deleteNodeByGroup(node, stage); } diff --git a/src/js/editor/nodeGroup.js b/src/js/editor/nodeGroup.js index bfd9ba4..b5dce85 100644 --- a/src/js/editor/nodeGroup.js +++ b/src/js/editor/nodeGroup.js @@ -1,4 +1,5 @@ -import { colorMap } from '../core/colorMap.js' +import { colorMap } from '../core/colorMap.js'; +import { tabManager } from './tabManager.js'; let placeLocation = function (location, stage) { return { @@ -183,7 +184,7 @@ function setupGroupDrag(grp, layer, stage) { grp.on('dragstart', (e) => { e.cancelBubble = true; - wireLayer = stage.findOne('#wireLayer'); + wireLayer = tabManager.getActiveWireLayer(); containedNodes = findContainedNodes(grp, layer); nodeOffsets = containedNodes.map((node) => ({ node, @@ -233,6 +234,7 @@ export function createNodeGroup(position, width, height, name, layer, stage) { stroke: colorMap['GroupBorder'], strokeWidth: 1.5, cornerRadius: 6, + listening: false, }); const titleBar = new Konva.Rect({ @@ -333,7 +335,7 @@ export function getGroupsData(layer) { return groups; } -export function enableNodeGroups(stage, layer) { +export function enableNodeGroups(stage) { let isDrawing = false; let startX = 0; let startY = 0; @@ -342,6 +344,7 @@ export function enableNodeGroups(stage, layer) { stage.on('mousedown', (e) => { if (!e.evt.shiftKey || e.target !== stage || e.evt.button !== 0) return; + const layer = tabManager.getActiveLayer(); isDrawing = true; const pos = placeLocation(stage.getPointerPosition(), stage); startX = pos.x; @@ -365,6 +368,7 @@ export function enableNodeGroups(stage, layer) { stage.on('mousemove', () => { if (!isDrawing || !previewRect) return; + const layer = tabManager.getActiveLayer(); const pos = placeLocation(stage.getPointerPosition(), stage); previewRect.setAttrs({ x: Math.min(startX, pos.x), @@ -379,6 +383,7 @@ export function enableNodeGroups(stage, layer) { if (!isDrawing || !previewRect) return; isDrawing = false; + const layer = tabManager.getActiveLayer(); const w = previewRect.width(); const h = previewRect.height(); const x = previewRect.x(); diff --git a/src/js/editor/orphanOverlay.js b/src/js/editor/orphanOverlay.js new file mode 100644 index 0000000..1e99a61 --- /dev/null +++ b/src/js/editor/orphanOverlay.js @@ -0,0 +1,85 @@ +import { deleteNodeByGroup } from './deleteHandler.js'; + +/** + * Apply the same red overlay and "Deleted Var" / "Deleted Func" indicator used + * when a variable or function is deleted but nodes still reference it. + * @param {Konva.Group} grp - The program node group (must have customClass.bodyRect) + * @param {Konva.Layer} layer - Layer for draw() + * @param {Konva.Stage} stage - Stage for deleteNodeByGroup + * @param {string} [label='Deleted Var'] - Label text (e.g. 'Deleted Func' for call nodes) + */ +export function applyOrphanOverlay(grp, layer, stage, label = 'Deleted Var') { + const cc = grp.customClass; + if (!cc) return; + cc.isOrphaned = true; + + const bodyRect = cc.bodyRect; + const overlay = new Konva.Rect({ + x: bodyRect.x(), + y: bodyRect.y(), + width: bodyRect.width(), + height: bodyRect.height(), + fill: 'rgba(255, 0, 0, 0.25)', + cornerRadius: 5, + listening: false, + }); + grp.add(overlay); + + const boxWidth = 120; + const boxHeight = 36; + const boxX = bodyRect.width() / 2 - boxWidth / 2; + const boxY = -boxHeight - 5; + + const indicatorGrp = new Konva.Group({ x: boxX, y: boxY }); + + const bg = new Konva.Rect({ + width: boxWidth, + height: boxHeight, + fill: 'rgba(30, 0, 0, 0.85)', + cornerRadius: 3, + stroke: '#f44', + strokeWidth: 1, + }); + indicatorGrp.add(bg); + + const labelText = new Konva.Text({ + text: label, + fontSize: 9, + fontFamily: 'Verdana', + fill: '#f88', + width: boxWidth, + align: 'center', + y: 3, + }); + indicatorGrp.add(labelText); + + const removeBtn = new Konva.Text({ + text: 'Remove', + fontSize: 10, + fontFamily: 'Verdana', + fill: '#fff', + width: boxWidth, + align: 'center', + y: 18, + }); + removeBtn.on('mouseenter', () => { + removeBtn.fill('#f44'); + document.body.style.cursor = 'pointer'; + if (layer) layer.draw(); + }); + removeBtn.on('mouseleave', () => { + removeBtn.fill('#fff'); + document.body.style.cursor = 'default'; + if (layer) layer.draw(); + }); + removeBtn.on('click', (e) => { + e.cancelBubble = true; + document.body.style.cursor = 'default'; + deleteNodeByGroup(grp, stage); + }); + indicatorGrp.add(removeBtn); + + grp.add(indicatorGrp); + cc._orphanOverlay = overlay; + cc._orphanIndicator = indicatorGrp; +} diff --git a/src/js/editor/tabManager.js b/src/js/editor/tabManager.js new file mode 100644 index 0000000..157e11e --- /dev/null +++ b/src/js/editor/tabManager.js @@ -0,0 +1,273 @@ +import { showAlert, showConfirm } from '../ui/dialogs.js'; + +const JS_RESERVED = new Set([ + 'break','case','catch','continue','debugger','default','delete','do', + 'else','finally','for','function','if','in','instanceof','new','return', + 'switch','this','throw','try','typeof','var','void','while','with', + 'class','const','enum','export','extends','import','super','implements', + 'interface','let','package','private','protected','public','static','yield', + 'await','async','null','undefined','true','false','NaN','Infinity', +]); + +export function isValidFunctionName(name) { + if (!name || name.length === 0) return false; + if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) return false; + if (JS_RESERVED.has(name)) return false; + return true; +} + +class TabManager { + constructor() { + this._stage = null; + this._tabs = new Map(); + this._activeTabId = null; + this._nextId = 1; + this._listeners = {}; + this._sharedDragLayer = null; + } + + init(stage, mainLayer, mainWireLayer, dragLayer) { + this._stage = stage; + this._sharedDragLayer = dragLayer; + const mainTab = { + id: 'main', + name: 'Main', + type: 'main', + layer: mainLayer, + wireLayer: mainWireLayer, + inputParams: [], + outputParams: [], + beginNodeId: null, + returnNodeId: null, + variables: [], + }; + this._tabs.set('main', mainTab); + this._activeTabId = 'main'; + this._renderTabBar(); + } + + getActiveTab() { return this._tabs.get(this._activeTabId); } + getActiveTabId() { return this._activeTabId; } + getActiveLayer() { return this._tabs.get(this._activeTabId).layer; } + getActiveWireLayer() { return this._tabs.get(this._activeTabId).wireLayer; } + getDragLayer() { return this._sharedDragLayer; } + getStage() { return this._stage; } + + getTab(id) { return this._tabs.get(id); } + getAllTabs() { return [...this._tabs.values()]; } + getAllFunctionTabs() { return [...this._tabs.values()].filter(t => t.type === 'function'); } + + createTab(name) { + if (!isValidFunctionName(name)) { + showAlert('Invalid function name. Use letters, numbers, and underscores. Must start with a letter or underscore.'); + return null; + } + if (this._isDuplicateName(name)) { + showAlert(`Function "${name}" already exists.`); + return null; + } + + const id = `func_${this._nextId++}`; + const layer = new Konva.Layer({ id: `layer_${id}` }); + const wireLayer = new Konva.Layer({ id: `wireLayer_${id}` }); + + this._stage.add(wireLayer); + this._stage.add(layer); + layer.moveToBottom(); + + layer.hide(); + wireLayer.hide(); + + const tab = { + id, + name, + type: 'function', + layer, + wireLayer, + inputParams: [], + outputParams: [], + beginNodeId: null, + returnNodeId: null, + variables: [], + docString: '', + }; + + this._tabs.set(id, tab); + this._renderTabBar(); + this.switchTab(id); + this._emit('tabCreated', tab); + return tab; + } + + switchTab(id) { + if (!this._tabs.has(id) || id === this._activeTabId) return; + + const prevTab = this._tabs.get(this._activeTabId); + prevTab.layer.hide(); + prevTab.wireLayer.hide(); + + this._activeTabId = id; + const newTab = this._tabs.get(id); + newTab.layer.show(); + newTab.wireLayer.show(); + + this._stage.draw(); + this._updateTabBarSelection(); + this._emit('tabSwitched', { from: prevTab, to: newTab }); + } + + renameTab(id, newName) { + if (id === 'main') return false; + if (!isValidFunctionName(newName)) { + showAlert('Invalid function name.'); + return false; + } + if (this._isDuplicateName(newName, id)) { + showAlert(`Function "${newName}" already exists.`); + return false; + } + + const tab = this._tabs.get(id); + if (!tab) return false; + const oldName = tab.name; + tab.name = newName; + this._renderTabBar(); + this._emit('tabRenamed', { tab, oldName, newName }); + return true; + } + + closeTab(id) { + if (id === 'main') return false; + const tab = this._tabs.get(id); + if (!tab) return false; + + if (this._activeTabId === id) { + this.switchTab('main'); + } + + tab.layer.destroy(); + tab.wireLayer.destroy(); + this._tabs.delete(id); + + this._renderTabBar(); + this._emit('tabClosed', tab); + this._stage.draw(); + return true; + } + + getFunctionDefinition(tabId) { + const tab = this._tabs.get(tabId); + if (!tab || tab.type !== 'function') return null; + return { + name: tab.name, + inputs: [...tab.inputParams], + outputs: [...tab.outputParams], + }; + } + + on(event, fn) { + if (!this._listeners[event]) this._listeners[event] = []; + this._listeners[event].push(fn); + } + + off(event, fn) { + if (!this._listeners[event]) return; + this._listeners[event] = this._listeners[event].filter(f => f !== fn); + } + + _emit(event, data) { + if (this._listeners[event]) { + this._listeners[event].forEach(fn => fn(data)); + } + } + + _isDuplicateName(name, excludeId) { + for (const [id, tab] of this._tabs) { + if (id !== excludeId && tab.name === name) return true; + } + return false; + } + + _renderTabBar() { + const tabList = document.getElementById('tab-list'); + if (!tabList) return; + tabList.innerHTML = ''; + + for (const [id, tab] of this._tabs) { + const tabEl = document.createElement('div'); + tabEl.className = 'tab-item'; + if (id === this._activeTabId) tabEl.classList.add('tab-active'); + tabEl.dataset.tabId = id; + + const nameEl = document.createElement('span'); + nameEl.className = 'tab-name'; + nameEl.textContent = tab.name; + tabEl.appendChild(nameEl); + + if (tab.type === 'function') { + nameEl.addEventListener('dblclick', (e) => { + e.stopPropagation(); + this._startRename(tabEl, tab); + }); + + const closeBtn = document.createElement('span'); + closeBtn.className = 'tab-close'; + closeBtn.textContent = '\u00D7'; + closeBtn.addEventListener('click', (e) => { + e.stopPropagation(); + showConfirm( + `Close function "${tab.name}"? The function definition and its contents will be removed.`, + () => { this.closeTab(id); } + ); + }); + tabEl.appendChild(closeBtn); + } + + tabEl.addEventListener('click', () => { + this.switchTab(id); + }); + + tabList.appendChild(tabEl); + } + } + + _updateTabBarSelection() { + const tabList = document.getElementById('tab-list'); + if (!tabList) return; + tabList.querySelectorAll('.tab-item').forEach(el => { + el.classList.toggle('tab-active', el.dataset.tabId === this._activeTabId); + }); + } + + _startRename(tabEl, tab) { + const nameEl = tabEl.querySelector('.tab-name'); + const input = document.createElement('input'); + input.type = 'text'; + input.className = 'tab-rename-input'; + input.value = tab.name; + + nameEl.replaceWith(input); + input.focus(); + input.select(); + + let finished = false; + const doFinish = () => { + if (finished) return; + finished = true; + const newName = input.value.trim(); + if (newName && newName !== tab.name) { + this.renameTab(tab.id, newName); + } else { + this._renderTabBar(); + } + }; + + input.addEventListener('blur', doFinish); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { e.preventDefault(); input.blur(); } + if (e.key === 'Escape') { input.value = tab.name; input.blur(); } + }); + } +} + +export const tabManager = new TabManager(); diff --git a/src/js/nodes/nodeFactory.js b/src/js/nodes/nodeFactory.js index 3869127..fac9337 100644 --- a/src/js/nodes/nodeFactory.js +++ b/src/js/nodes/nodeFactory.js @@ -2,7 +2,11 @@ import { InputBox } from './nodeInputBox.js' import { colorMap } from '../core/colorMap.js' import { setLocationOfNode } from './nodePosition.js'; import { buildNodeDescription, hasType } from '../registry/index.js'; -import { deleteNodeByGroup } from '../editor/deleteHandler.js'; +import { deleteNodeByGroup, deleteWire } from '../editor/deleteHandler.js'; +import { tabManager } from '../editor/tabManager.js'; +import { addConnectionWire } from './wiring.js'; +import { applyMismatchToWire, isTypeCompatible } from '../utils/wireMismatch.js'; + let placeLocation = function (location) { //"this" is stage return { @@ -204,13 +208,13 @@ export var Nodes = { return text; }, optimizeDrag: function (grp, stage, layer) { - let dragLayer = stage.findOne('#dragLayer'); - let wireLayer = stage.findOne('#wireLayer'); function moveWireToLayer(aWire, targetLayer) { aWire.moveTo(targetLayer); if (aWire._closeIndicator) aWire._closeIndicator.moveTo(targetLayer); } grp.on('dragstart', () => { + let dragLayer = tabManager.getDragLayer(); + let wireLayer = tabManager.getActiveWireLayer(); grp.moveTo(dragLayer); for (let each of grp.customClass.execInPins) { for (let aWire of each.wire) { @@ -235,6 +239,8 @@ export var Nodes = { layer.draw(); }) grp.on('dragend', () => { + let dragLayer = tabManager.getDragLayer(); + let wireLayer = tabManager.getActiveWireLayer(); grp.moveTo(layer); for (let each of grp.customClass.execInPins) { for (let aWire of each.wire) { @@ -283,7 +289,12 @@ export var Nodes = { }); if (nodeDescription.nodeTitle == 'Begin') { this.grp.id('Begin'); + } else if (nodeDescription.nodeTitle == 'FunctionBegin') { + this.grp.id('FunctionBegin'); + } else if (nodeDescription.nodeTitle == 'Return') { + this.grp.id('Return'); } + this.isDeletable = nodeDescription.isDeletable !== false; this.grp.customClass = this; // this.grp.on('dblclick', (e) => { // console.table(e.currentTarget.customClass); @@ -306,7 +317,7 @@ export var Nodes = { this.grp.add(borderRect); let closeBtn = null; - if (nodeDescription.nodeTitle !== 'Begin') { + if (nodeDescription.nodeTitle !== 'Begin' && nodeDescription.isDeletable !== false) { const btnSize = 16; const btnX = width - btnSize - 4; const btnY = Math.round((23 - btnSize) / 2); @@ -529,7 +540,359 @@ export var Nodes = { } if (!nodeDescription) return; new this.ProgramNode(nodeDescription, location, layer, stage); - } + }, + + buildFunctionBeginDescription: function (inputParams) { + const nd = { + nodeTitle: 'FunctionBegin', + execIn: false, + pinExecInId: null, + execOut: { execOut0: { execOutTitle: null, pinExecOutId: null, outOrder: 0 } }, + color: 'FunctionBegin', + rows: 2, + colums: 12, + isDeletable: false, + }; + if (inputParams && inputParams.length > 0) { + nd.outputs = {}; + inputParams.forEach((p, i) => { + nd.outputs[`output${i}`] = { + outputTitle: p.name, + dataType: p.dataType, + pinOutId: null, + outOrder: i + 1, + }; + }); + nd.rows = Math.max(2, inputParams.length + 1); + } + return nd; + }, + + buildReturnDescription: function (outputParams) { + const nd = { + nodeTitle: 'Return', + execIn: true, + pinExecInId: null, + color: 'Return', + rows: 2, + colums: 12, + isDeletable: false, + }; + if (outputParams && outputParams.length > 0) { + nd.inputs = {}; + outputParams.forEach((p, i) => { + nd.inputs[`input${i}`] = { + inputTitle: p.name, + dataType: p.dataType, + defValue: null, + pinInId: null, + isInputBoxRequired: false, + }; + }); + nd.rows = Math.max(2, outputParams.length + 1); + } + return nd; + }, + + CreateFunctionBeginNode: function (inputParams, location, layer, stage) { + const nd = this.buildFunctionBeginDescription(inputParams); + const node = new this.ProgramNode(nd, location, layer, stage); + node.grp.id('FunctionBegin'); + return node; + }, + + CreateReturnNode: function (outputParams, location, layer, stage) { + const nd = this.buildReturnDescription(outputParams); + const node = new this.ProgramNode(nd, location, layer, stage); + node.grp.id('Return'); + return node; + }, + + rebuildFunctionBeginNode: function (node, inputParams, layer, stage, wireLayer) { + const programNode = node; + const grp = programNode.grp; + const oldNd = programNode.nodeDescription; + const oldOutputs = oldNd.outputs ? Object.keys(oldNd.outputs).sort().map(k => oldNd.outputs[k]) : []; + + const savedOutputs = []; + for (let j = 0; j < programNode.outputPins.length; j++) { + const pin = programNode.outputPins[j]; + const wires = [...(Array.isArray(pin.wire) ? pin.wire : (pin.wire ? [pin.wire] : []))].filter(Boolean); + savedOutputs[j] = wires.map(w => { + const destPin = w.attrs.dest; + const oldType = oldOutputs[j]?.dataType; + deleteWire(w); + return { destPin, oldType }; + }); + } + + const pos = grp.position(); + const grpId = grp.id(); + grp.destroy(); + + const newNd = this.buildFunctionBeginDescription(inputParams); + const newNode = new this.ProgramNode(newNd, { + x: pos.x * stage.scaleX() + stage.x(), + y: pos.y * stage.scaleY() + stage.y() + }, layer, stage); + newNode.grp.id(grpId); + const newCC = newNode.grp.customClass; + + const outPins = newNode.grp.find('.pin').filter(p => p.attrs.pinType === 'outp'); + outPins.sort((a, b) => { + const ai = parseInt(String(a.attrs.helper || '').split('-')[1], 10) || 0; + const bi = parseInt(String(b.attrs.helper || '').split('-')[1], 10) || 0; + return ai - bi; + }); + + for (let j = 0; j < savedOutputs.length && j < (inputParams || []).length; j++) { + const newSrcPin = outPins[j]; + if (!newSrcPin) continue; + for (const { destPin, oldType } of savedOutputs[j]) { + addConnectionWire(destPin, newSrcPin, stage, 1, wireLayer); + const newType = (inputParams || [])[j]?.dataType; + if (!isTypeCompatible(newType, destPin.attrs.pinDataType)) { + const inpIdx = destPin.attrs.helper ? parseInt(String(destPin.attrs.helper).split('-')[1], 10) : 0; + const wireToMark = destPin.getParent().customClass?.inputPins?.[inpIdx]?.wire; + if (wireToMark) applyMismatchToWire(wireToMark, wireLayer, stage); + } + } + } + return newNode; + }, + + buildCallNodeDescription: function (funcName, inputParams, outputParams, docString) { + const nd = { + nodeTitle: `Call ${funcName}`, + execIn: true, + pinExecInId: null, + execOut: { execOut0: { execOutTitle: null, pinExecOutId: null, outOrder: 0 } }, + color: 'Call', + rows: 2, + colums: 14, + isCallFunction: true, + calledFunctionName: funcName, + docString: docString || '', + }; + const maxPins = Math.max( + (inputParams ? inputParams.length : 0), + (outputParams ? outputParams.length : 0) + ); + nd.rows = Math.max(2, maxPins + 1); + if (inputParams && inputParams.length > 0) { + nd.inputs = {}; + const defByType = { Number: 0, Boolean: true, String: "'hello'", Array: '[]' }; + inputParams.forEach((p, i) => { + const defVal = p.defValue != null ? p.defValue : defByType[p.dataType]; + nd.inputs[`input${i}`] = { + inputTitle: p.name, + dataType: p.dataType, + defValue: defVal, + pinInId: null, + isInputBoxRequired: true, + }; + }); + } + if (outputParams && outputParams.length > 0) { + nd.outputs = {}; + outputParams.forEach((p, i) => { + nd.outputs[`output${i}`] = { + outputTitle: p.name, + dataType: p.dataType, + pinOutId: null, + outOrder: i + 1, + }; + }); + } + return nd; + }, + + CreateCallNode: function (funcName, inputParams, outputParams, location, layer, stage, docString) { + const nd = this.buildCallNodeDescription(funcName, inputParams, outputParams, docString); + return new this.ProgramNode(nd, location, layer, stage); + }, + + updateCallNodesDocString: function (funcName, docString) { + const allTabs = tabManager.getAllTabs(); + for (const tab of allTabs) { + const layer = tab.layer; + if (!layer) continue; + layer.find('.aProgramNodeGroup').forEach((grp) => { + const cc = grp.customClass; + if (!cc || !cc.nodeDescription || !cc.nodeDescription.isCallFunction || cc.nodeDescription.calledFunctionName !== funcName) return; + cc.nodeDescription.docString = docString || ''; + }); + } + }, + + /** + * Update all Call nodes that call the given function with the current definition. + * Smart wire matching: preserve wires when params match by index; remove wires for + * deleted params; apply dashed mismatch when type changes. + */ + updateCallNodesToDefinition: function (funcName, inputParams, outputParams, docString) { + const allTabs = tabManager.getAllTabs(); + const stage = tabManager.getStage(); + const inputParamsArr = inputParams || []; + const outputParamsArr = outputParams || []; + + for (const tab of allTabs) { + const layer = tab.layer; + const wireLayer = tab.wireLayer; + if (!layer || !wireLayer) continue; + + const toUpdate = []; + layer.find('.aProgramNodeGroup').forEach((grp) => { + const cc = grp.customClass; + if (!cc || !cc.nodeDescription || !cc.nodeDescription.isCallFunction || cc.nodeDescription.calledFunctionName !== funcName) return; + if (cc.isOrphaned) return; + toUpdate.push({ grp, layer, wireLayer, programNode: cc }); + }); + + for (const { grp, layer, wireLayer, programNode } of toUpdate) { + const nd = programNode.nodeDescription; + const oldInputs = nd.inputs ? Object.keys(nd.inputs).sort().map(k => nd.inputs[k]) : []; + const oldOutputs = nd.outputs ? Object.keys(nd.outputs).sort().map(k => nd.outputs[k]) : []; + + const savedInputs = []; + for (let i = 0; i < programNode.inputPins.length; i++) { + const pin = programNode.inputPins[i]; + if (pin.wire) { + savedInputs[i] = { srcPin: pin.wire.attrs.src, oldType: oldInputs[i]?.dataType }; + deleteWire(pin.wire); + } + } + + const savedOutputs = []; + for (let j = 0; j < programNode.outputPins.length; j++) { + const pin = programNode.outputPins[j]; + const wires = [...(Array.isArray(pin.wire) ? pin.wire : (pin.wire ? [pin.wire] : []))].filter(Boolean); + savedOutputs[j] = wires.map(w => { + const destPin = w.attrs.dest; + const oldType = oldOutputs[j]?.dataType; + deleteWire(w); + return { destPin, oldType }; + }); + } + + const savedExecIn = []; + if (programNode.execInPins && programNode.execInPins[0]) { + const wires = [...(programNode.execInPins[0].wire || [])].filter(Boolean); + for (const w of wires) { + savedExecIn.push({ srcPin: w.attrs.src }); + deleteWire(w); + } + } + + const savedExecOut = []; + if (programNode.execOutPins) { + for (const ep of programNode.execOutPins) { + if (ep.wire) { + savedExecOut.push({ destPin: ep.wire.attrs.dest }); + deleteWire(ep.wire); + } + } + } + + const pos = grp.position(); + grp.destroy(); + + const newNode = Nodes.CreateCallNode(funcName, inputParamsArr, outputParamsArr, pos, layer, stage, docString || ''); + const newCC = newNode.grp.customClass; + + for (let i = 0; i < savedInputs.length && i < inputParamsArr.length; i++) { + const saved = savedInputs[i]; + if (!saved) continue; + const newDestPin = newCC.inputPins[i]?.thisNode; + if (!newDestPin) continue; + addConnectionWire(newDestPin, saved.srcPin, stage, 1, wireLayer); + const newType = inputParamsArr[i]?.dataType; + if (!isTypeCompatible(saved.srcPin.attrs.pinDataType, newType)) { + const wire = newCC.inputPins[i].wire; + if (wire) applyMismatchToWire(wire, wireLayer, stage); + } + } + + const outPins = newNode.grp.find('.pin').filter(p => p.attrs.pinType === 'outp'); + outPins.sort((a, b) => { + const ai = parseInt(String(a.attrs.helper || '').split('-')[1], 10) || 0; + const bi = parseInt(String(b.attrs.helper || '').split('-')[1], 10) || 0; + return ai - bi; + }); + + for (let j = 0; j < savedOutputs.length && j < outputParamsArr.length; j++) { + const newSrcPin = outPins[j]; + if (!newSrcPin) continue; + for (const { destPin, oldType } of savedOutputs[j]) { + addConnectionWire(destPin, newSrcPin, stage, 1, wireLayer); + const newType = outputParamsArr[j]?.dataType; + if (!isTypeCompatible(newType, destPin.attrs.pinDataType)) { + const inpIdx = destPin.attrs.helper ? parseInt(String(destPin.attrs.helper).split('-')[1], 10) : 0; + const wireToMark = destPin.getParent().customClass?.inputPins?.[inpIdx]?.wire; + if (wireToMark) applyMismatchToWire(wireToMark, wireLayer, stage); + } + } + } + + for (const { srcPin } of savedExecIn) { + if (newCC.execInPins && newCC.execInPins[0]) { + const destPin = newCC.execInPins[0].thisNode; + addConnectionWire(destPin, srcPin, stage, 1, wireLayer); + } + } + + for (let k = 0; k < savedExecOut.length && newCC.execOutPins && newCC.execOutPins[k]; k++) { + const { destPin } = savedExecOut[k]; + const srcPin = newCC.execOutPins[k].thisNode; + addConnectionWire(destPin, srcPin, stage, 1, wireLayer); + } + } + } + if (stage) stage.draw(); + }, + + rebuildReturnNode: function (node, outputParams, layer, stage, wireLayer) { + const programNode = node; + const grp = programNode.grp; + const oldNd = programNode.nodeDescription; + const oldInputs = oldNd.inputs ? Object.keys(oldNd.inputs).sort().map(k => oldNd.inputs[k]) : []; + + const savedInputs = []; + for (let i = 0; i < programNode.inputPins.length; i++) { + const pin = programNode.inputPins[i]; + if (pin.wire) { + savedInputs[i] = { srcPin: pin.wire.attrs.src, oldType: oldInputs[i]?.dataType }; + deleteWire(pin.wire); + } + } + + const pos = grp.position(); + const grpId = grp.id(); + grp.destroy(); + + const newNd = this.buildReturnDescription(outputParams); + const newNode = new this.ProgramNode(newNd, { + x: pos.x * stage.scaleX() + stage.x(), + y: pos.y * stage.scaleY() + stage.y() + }, layer, stage); + newNode.grp.id(grpId); + const newCC = newNode.grp.customClass; + + const outputParamsArr = outputParams || []; + for (let i = 0; i < savedInputs.length && i < outputParamsArr.length; i++) { + const saved = savedInputs[i]; + if (!saved) continue; + const newDestPin = newCC.inputPins[i]?.thisNode; + if (!newDestPin) continue; + addConnectionWire(newDestPin, saved.srcPin, stage, 1, wireLayer); + const newType = outputParamsArr[i]?.dataType; + if (!isTypeCompatible(saved.srcPin.attrs.pinDataType, newType)) { + const wire = newCC.inputPins[i].wire; + if (wire) applyMismatchToWire(wire, wireLayer, stage); + } + } + return newNode; + }, }; /* diff --git a/src/js/nodes/wiring.js b/src/js/nodes/wiring.js index 0b0cb72..d200002 100644 --- a/src/js/nodes/wiring.js +++ b/src/js/nodes/wiring.js @@ -1,5 +1,6 @@ import { deleteWire, deleteHalfWire } from '../editor/deleteHandler.js' import { colorMap } from '../core/colorMap.js' +import { tabManager } from '../editor/tabManager.js' let placeLocation = function (location, stage) { return { x: (location.x - stage.x()) / stage.scaleX(), @@ -7,7 +8,7 @@ let placeLocation = function (location, stage) { }; } export var Wiring = { - enableWiring: function (stage, layer) { + enableWiring: function (stage) { let currentPinType = null; let currentPinDataType = null; function isValidMatch(pinType, targetPinDataType) { @@ -22,19 +23,18 @@ export var Wiring = { let isWiring = false; let src = null; let dest = null; - let wireLayer = new Konva.Layer({ - id: 'wireLayer', - }); + let activeWireLayer = null; + let activeNodeLayer = null; let drawWire = null; let potentialTarget = null; let dir = 0; let wireColor = null; let originPreOccupied = null; - stage.add(wireLayer); - wireLayer.zIndex(0); stage.on('mousedown', (e) => { if (e.target.name() == 'pin' && e.evt.button == 0) { + activeWireLayer = tabManager.getActiveWireLayer(); + activeNodeLayer = tabManager.getActiveLayer(); src = e.target; currentPinType = e.target.attrs.pinType; currentPinDataType = e.target.attrs.pinDataType; @@ -61,8 +61,8 @@ export var Wiring = { target: src, }); setWirePoints(destLoc, srcLoc, dir, drawWire); - wireLayer.add(drawWire); - wireLayer.draw(); + activeWireLayer.add(drawWire); + activeWireLayer.draw(); } }); stage.on('mouseup', (e) => { @@ -86,7 +86,7 @@ export var Wiring = { let srcLoc = placeLocation(src.getAbsolutePosition(), stage); let destLoc = placeLocation(stage.getPointerPosition(), stage); setWirePoints(destLoc, srcLoc, dir, drawWire); - wireLayer.draw(); + activeWireLayer.draw(); } }) @@ -97,7 +97,7 @@ export var Wiring = { deleteHalfWire(drawWire, originPreOccupied); if (e.target && src && e.target.name() == 'pin' && src != e.target && src.getParent() !== e.target.getParent() && isValidMatch(e.target.attrs.pinType, e.target.attrs.pinDataType)) { dest = e.target; - addConnectionWire(dest, src, stage, dir, wireLayer); + addConnectionWire(dest, src, stage, dir, activeWireLayer); } src.getParent().draggable(true); src = null; @@ -109,8 +109,8 @@ export var Wiring = { currentPinType = null; currentPinDataType = null; wireColor = null; - wireLayer.draw(); - layer.draw(); + activeWireLayer.draw(); + activeNodeLayer.draw(); } } else { @@ -126,8 +126,8 @@ export var Wiring = { currentPinType = null; currentPinDataType = null; wireColor = null; - wireLayer.draw(); - layer.draw(); + activeWireLayer.draw(); + activeNodeLayer.draw(); } } } diff --git a/src/js/persistence/saveAndLoad.js b/src/js/persistence/saveAndLoad.js index 60218b2..1a06fd6 100644 --- a/src/js/persistence/saveAndLoad.js +++ b/src/js/persistence/saveAndLoad.js @@ -3,6 +3,7 @@ import { addConnectionWire } from '../nodes/wiring.js' import { variableList } from '../ui/variableList.js' import { showAlert, vscriptOnLoad } from '../ui/dialogs.js' import { getGroupsData, createNodeGroup } from '../editor/nodeGroup.js' +import { tabManager } from '../editor/tabManager.js' function writeError(err, msg) { document.getElementById("console-window").classList.toggle("hidden", false); let codeDoc = document.getElementById("console").contentWindow.document; @@ -33,52 +34,81 @@ let placeLocation = function (location) { y: (location.y - this.y()) / this.scaleY() }; } +function serializeLayerData(layer, wireLayer) { + const nodesData = []; + const wireData = []; + layer.find('.aProgramNodeGroup').forEach((node) => { + if (node.name() === 'aProgramNodeGroup') { + nodesData.push({ + position: node.position(), + nodeDescription: node.customClass.nodeDescription, + }); + } + }); + wireLayer.find('.isConnection').forEach((aWire) => { + if (aWire.name() === 'isConnection') { + wireData.push({ + srcId: aWire.attrs.src.id(), + destId: aWire.attrs.dest.id(), + }); + } + }); + return { nodesData, wireData }; +} + +function buildFullExportData(layer, wireLayer) { + const { nodesData, wireData } = serializeLayerData(layer, wireLayer); + const mainTab = tabManager.getTab('main'); + const globalVars = mainTab ? mainTab.variables : variableList.variables; + const funcTabs = tabManager.getAllFunctionTabs(); + const functions = funcTabs.map(ft => { + const ftData = serializeLayerData(ft.layer, ft.wireLayer); + return { + name: ft.name, + inputParams: ft.inputParams.map(p => ({ name: p.name, dataType: p.dataType, defValue: p.defValue })), + outputParams: ft.outputParams.map(p => ({ name: p.name, dataType: p.dataType })), + variables: ft.variables.map(v => ({ name: v.name, dataType: v.dataType, value: v.value })), + docString: ft.docString || '', + nodesData: ftData.nodesData, + wireData: ftData.wireData, + groupsData: getGroupsData(ft.layer), + }; + }); + return { + variables: globalVars, + nodesData, + wireData, + groupsData: getGroupsData(layer), + functions, + }; +} + export class Export { constructor(stage, layer, wireLayer) { document.getElementById('export').addEventListener("click", (e) => { - let exportScript = []; - let nodesData = []; - let wireData = []; - layer.find('.aProgramNodeGroup').forEach((node, index) => { - if (node.name() == 'aProgramNodeGroup') { - let nodeData = { - position: node.position(), - nodeDescription: node.customClass.nodeDescription, - }; - nodesData.push(nodeData); - } - }); - wireLayer.find('.isConnection').forEach((aWire, index) => { - if (aWire.name() == 'isConnection') { - let wireD = { - srcId: aWire.attrs.src.id(), - destId: aWire.attrs.dest.id(), - } - wireData.push(wireD); - } - }) - exportScript = { - variables: variableList.variables, - nodesData: nodesData, - wireData: wireData, - groupsData: getGroupsData(layer), - } + const exportScript = buildFullExportData(layer, wireLayer); let dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(exportScript)); let exportAnchorElem = document.getElementById('exportAnchorElem'); exportAnchorElem.setAttribute("href", dataStr); const ts = new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-'); exportAnchorElem.setAttribute("download", `wireScript_${ts}.json`); exportAnchorElem.click(); - - // console.log(JSON.stringify(exportScript)); - // console.log(JSON.parse(JSON.stringify(exportScript))); }); } } export function refresh(layer, wireLayer) { + const funcTabs = tabManager.getAllFunctionTabs(); + for (const ft of [...funcTabs]) { + tabManager.closeTab(ft.id); + } + if (tabManager.getActiveTabId() !== 'main') { + tabManager.switchTab('main'); + } layer.destroyChildren(); wireLayer.destroyChildren(); variableList.deleteAllVariables(); + const mainTab = tabManager.getTab('main'); + if (mainTab) mainTab.variables = variableList.variables; layer.draw(); wireLayer.draw(); } @@ -100,33 +130,7 @@ export class Import { export class Save { constructor(stage, layer, wireLayer) { document.getElementById('save').addEventListener("click", (e) => { - let exportScript = []; - let nodesData = []; - let wireData = []; - layer.find('.aProgramNodeGroup').forEach((node, index) => { - if (node.name() == 'aProgramNodeGroup') { - let nodeData = { - position: node.position(), - nodeDescription: node.customClass.nodeDescription, - }; - nodesData.push(nodeData); - } - }); - wireLayer.find('.isConnection').forEach((aWire, index) => { - if (aWire.name() == 'isConnection') { - let wireD = { - srcId: aWire.attrs.src.id(), - destId: aWire.attrs.dest.id(), - } - wireData.push(wireD); - } - }) - exportScript = { - variables: variableList.variables, - nodesData: nodesData, - wireData: wireData, - groupsData: getGroupsData(layer), - } + const exportScript = buildFullExportData(layer, wireLayer); localStorage.setItem('lastLoadWireScriptJSON', JSON.stringify(exportScript)); let savingWindow = document.getElementById("saving"); // let importMenu = document.getElementById("import-menu"); @@ -173,49 +177,85 @@ export function prompLastSave(stage, layer, wireLayer) { } } -function printContent(json, stage, layer, wireLayer) { - for (let aNode of json.nodesData) { +function loadLayerContent(nodesData, wireData, targetLayer, wireLayer, stage) { + for (let aNode of nodesData) { try { - new Nodes.ProgramNode(aNode.nodeDescription, { x: aNode.position.x * stage.scaleX() + stage.x(), y: aNode.position.y * stage.scaleY() + stage.y() }, layer, stage); - - } - catch (err) { + new Nodes.ProgramNode(aNode.nodeDescription, + { x: aNode.position.x * stage.scaleX() + stage.x(), y: aNode.position.y * stage.scaleY() + stage.y() }, + targetLayer, stage); + } catch (err) { writeError(err, "Error Occurred In Importing The JSON(Node Description Not Valid)"); } } - // let X = layer.findOne('Group'); - // console.log(layer.children); - // console.log(X); - for (let aWire of json.wireData) { - // console.log(`${aWire.srcId}`, `${aWire.destId}`); - let src = layer.findOne(`#${aWire.srcId}`); - let dest = layer.findOne(`#${aWire.destId}`); - // console.log(src, dest); + for (let aWire of wireData) { + let src = targetLayer.findOne(`#${aWire.srcId}`); + let dest = targetLayer.findOne(`#${aWire.destId}`); try { addConnectionWire(dest, src, stage, 1, wireLayer); - } - catch (err) { + } catch (err) { writeError(err, "Error Occurred In Importing The JSON(Wire Data Not Valid)"); } } +} + +function printContent(json, stage, layer, wireLayer) { + loadLayerContent(json.nodesData, json.wireData, layer, wireLayer, stage); + + const mainTab = tabManager.getTab('main'); for (let aVariable of json.variables) { try { variableList.addVariable(aVariable); - } - catch (err) { + } catch (err) { writeError(err, "Error Occurred In Importing The JSON(Variable Data Not Valid)"); } } + if (mainTab) mainTab.variables = variableList.variables; if (json.groupsData) { for (let g of json.groupsData) { try { createNodeGroup(g.position, g.width, g.height, g.name, layer, stage); - } - catch (err) { + } catch (err) { writeError(err, "Error Occurred In Importing The JSON(Group Data Not Valid)"); } } } + + if (json.functions && json.functions.length > 0) { + for (const funcData of json.functions) { + try { + tabManager._suppressAutoNodes = true; + const tab = tabManager.createTab(funcData.name); + tabManager._suppressAutoNodes = false; + if (!tab) continue; + + tab.inputParams = funcData.inputParams || []; + tab.outputParams = funcData.outputParams || []; + tab.variables = funcData.variables || []; + tab.docString = funcData.docString || ''; + + loadLayerContent(funcData.nodesData, funcData.wireData, tab.layer, tab.wireLayer, stage); + + const beginNode = tab.layer.findOne('#FunctionBegin'); + if (beginNode) tab.beginNodeId = beginNode.id(); + const returnNode = tab.layer.findOne('#Return'); + if (returnNode) tab.returnNodeId = returnNode.id(); + + if (funcData.groupsData) { + for (let g of funcData.groupsData) { + createNodeGroup(g.position, g.width, g.height, g.name, tab.layer, stage); + } + } + + tab.layer.draw(); + tab.wireLayer.draw(); + } catch (err) { + tabManager._suppressAutoNodes = false; + writeError(err, "Error Occurred In Importing The JSON(Function Data Not Valid)"); + } + } + tabManager.switchTab('main'); + } + layer.draw(); wireLayer.draw(); } diff --git a/src/js/ui/dialogs.js b/src/js/ui/dialogs.js index 10bebc3..d7a0b7c 100644 --- a/src/js/ui/dialogs.js +++ b/src/js/ui/dialogs.js @@ -97,11 +97,20 @@ export function prompRefreshOrStarter(type, stage) { }) ; } } -export function vscriptOnLoad(stage) { - // stage.setScale({x: 0.5, y: 0.5}); - // const starterFile = { "variables": [{ "name": "coolStuff", "dataType": "String", "value": "'https://www.youtube.com/watch?v=dQw4w9WgXcQ'" }], "nodesData": [{ "position": { "x": 285.76949478124993, "y": 160.00829895214838 }, "nodeDescription": { "nodeTitle": "Begin", "execIn": false, "pinExecInId": null, "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "10", "outOrder": 0 } }, "color": "Begin", "rows": 2, "colums": 10 } }, { "position": { "x": 501.3872296698097, "y": 161.2761797590843 }, "nodeDescription": { "nodeTitle": "Confirm", "execIn": true, "pinExecInId": "62", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "63", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "Message", "dataType": "String", "defValue": "'u wanna see some cool stuff ?'", "pinInId": "64" } }, "outputs": { "output0": { "outputTitle": "Ok?", "dataType": "Boolean", "pinOutId": "69", "outOrder": 1 } }, "color": "Print", "rows": 3, "colums": 12 } }, { "position": { "x": 1216.4116108593755, "y": 150.01528070898422 }, "nodeDescription": { "nodeTitle": "OpenWindow", "execIn": true, "pinExecInId": "16", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "17", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "URL", "dataType": "String", "defValue": "'link'", "pinInId": "18" } }, "outputs": { "output0": { "outputTitle": "Success?", "dataType": "Boolean", "pinOutId": "23", "outOrder": 1 } }, "color": "Func", "rows": 2, "colums": 12 } }, { "position": { "x": 1203.519704023151, "y": 382.23448287513907 }, "nodeDescription": { "nodeTitle": "Alert", "execIn": true, "pinExecInId": "101", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "102", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "Value", "dataType": "Data", "defValue": "'u missed some nice stuff'", "pinInId": "103" } }, "color": "Print", "rows": 3, "colums": 12 } }, { "position": { "x": 1202.448197106272, "y": 520.6776970135538 }, "nodeDescription": { "nodeTitle": "Alert", "execIn": true, "pinExecInId": "115", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "116", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "Value", "dataType": "Data", "defValue": "'After closing this dialog, right click on the editor to get bunch of new nodes and create your own scripts :)'", "pinInId": "117" } }, "color": "Print", "rows": 3, "colums": 12 } }, { "position": { "x": 773.9416135561129, "y": 211.52728595257065 }, "nodeDescription": { "nodeTitle": "If/Else", "execIn": true, "pinExecInId": "78", "execOut": { "execOut0": { "execOutTitle": "True", "pinExecOutId": "79", "outOrder": 0 }, "execOut1": { "execOutTitle": "False", "pinExecOutId": "81", "outOrder": 1 }, "execOut2": { "execOutTitle": "Done", "pinExecOutId": "83", "outOrder": 2 } }, "inputs": { "input0": { "inputTitle": "Bool", "dataType": "Boolean", "defValue": true, "pinInId": "85" } }, "color": "Logic", "rows": 3, "colums": 12 } }, { "position": { "x": 1162.8208273281257, "y": 280.7125038125001 }, "nodeDescription": { "nodeTitle": "Get coolStuff", "outputs": { "output0": { "outputTitle": "Value(Ref)", "dataType": "String", "pinOutId": "32", "outOrder": 0 } }, "color": "Get", "rows": 2, "colums": 10 } }], "wireData": [{ "srcId": "10", "destId": "62" }, { "srcId": "63", "destId": "78" }, { "srcId": "79", "destId": "16" }, { "srcId": "81", "destId": "101" }, { "srcId": "83", "destId": "115" }, { "srcId": "69", "destId": "85" }, { "srcId": "32", "destId": "18" }] }; - const starterFile = { "variables": [{ "name": "coolStuff", "dataType": "String", "value": "'https://www.youtube.com/watch?v=hOHKltAiKXQ'" }, { "name": "fib0", "dataType": "Number", "value": "0" }, { "name": "fib1", "dataType": "Number", "value": "1" }, { "name": "tmp", "dataType": "Number", "value": "0" }, { "name": "fibArray", "dataType": "Array", "value": "[]" }, { "name": "catFactsApi", "dataType": "String", "value": "'https://catfact.ninja/fact'" }], "nodesData": [{ "position": { "x": 640.1957355310791, "y": 1149.6123184434612 }, "nodeDescription": { "nodeTitle": "Confirm", "execIn": true, "pinExecInId": "62", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "63", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "Message", "dataType": "String", "defValue": "'u wanna see some cool stuff ?'", "pinInId": "64" } }, "outputs": { "output0": { "outputTitle": "Ok?", "dataType": "Boolean", "pinOutId": "69", "outOrder": 1 } }, "color": "Print", "rows": 3, "colums": 12 } }, { "position": { "x": 943.0808918774987, "y": 1154.2878748323233 }, "nodeDescription": { "nodeTitle": "If/Else", "execIn": true, "pinExecInId": "78", "execOut": { "execOut0": { "execOutTitle": "True", "pinExecOutId": "79", "outOrder": 0 }, "execOut1": { "execOutTitle": "False", "pinExecOutId": "81", "outOrder": 1 }, "execOut2": { "execOutTitle": "Done", "pinExecOutId": "83", "outOrder": 2 } }, "inputs": { "input0": { "inputTitle": "Bool", "dataType": "Boolean", "defValue": true, "pinInId": "85" } }, "color": "Logic", "rows": 3, "colums": 12 } }, { "position": { "x": 624.8490952267291, "y": 76.13062296509307 }, "nodeDescription": { "nodeTitle": "ForLoop", "pinExecInId": "168", "execIn": true, "execOut": { "execOut0": { "execOutTitle": "Loop Body", "pinExecOutId": "169", "outOrder": 0 }, "execOut1": { "execOutTitle": "Completed", "pinExecOutId": "171", "outOrder": 2 } }, "inputs": { "input0": { "inputTitle": "From", "dataType": "Number", "defValue": 0, "pinInId": "173" }, "input1": { "inputTitle": "To(Excl)", "dataType": "Number", "defValue": 10, "pinInId": "178" }, "input2": { "inputTitle": "Increment", "dataType": "Number", "defValue": 1, "pinInId": "183" } }, "outputs": { "output0": { "outputTitle": "Index", "dataType": "Number", "pinOutId": "188", "outOrder": 1 } }, "color": "Logic", "rows": 2, "colums": 12 } }, { "position": { "x": 1062.358745795078, "y": 9.64809972519835 }, "nodeDescription": { "nodeTitle": "Print", "execIn": true, "pinExecInId": "197", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "198", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "Value", "dataType": "Data", "defValue": "'hello'", "pinInId": "199" } }, "color": "Print", "rows": 3, "colums": 12 } }, { "position": { "x": 882.7650007538189, "y": 159.85163279515638 }, "nodeDescription": { "nodeTitle": "Get fib0", "outputs": { "output0": { "outputTitle": "Value(Ref)", "dataType": "Number", "pinOutId": "161", "outOrder": 0 } }, "color": "Get", "rows": 2, "colums": 10 } }, { "position": { "x": 1236.2691694516263, "y": 163.34281389620685 }, "nodeDescription": { "nodeTitle": "Add", "inputs": { "input0": { "inputTitle": "ValueA", "dataType": "Number", "defValue": 0, "pinInId": "240" }, "input1": { "inputTitle": "ValueB", "dataType": "Number", "defValue": 0, "pinInId": "245" } }, "outputs": { "output0": { "outputTitle": "Result", "dataType": "Number", "pinOutId": "250", "outOrder": 0 } }, "color": "Math", "rows": 2, "colums": 10 } }, { "position": { "x": 935.1522585885414, "y": 253.74975644182052 }, "nodeDescription": { "nodeTitle": "Get fib1", "outputs": { "output0": { "outputTitle": "Value(Ref)", "dataType": "Number", "pinOutId": "231", "outOrder": 0 } }, "color": "Get", "rows": 2, "colums": 10 } }, { "position": { "x": 1296.8417070314447, "y": 16.433630516210982 }, "nodeDescription": { "nodeTitle": "Set tmp", "execIn": true, "pinExecInId": "213", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "214", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "Value", "dataType": "Number", "defValue": 0, "pinInId": "215" } }, "outputs": { "output0": { "outputTitle": "Value(Ref)", "dataType": "Number", "pinOutId": "220", "outOrder": 1 } }, "color": "Func", "rows": 2, "colums": 12 } }, { "position": { "x": 1833.5365239733978, "y": 18.884241374940608 }, "nodeDescription": { "nodeTitle": "Set fib1", "execIn": true, "pinExecInId": "288", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "289", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "Value", "dataType": "Number", "defValue": 0, "pinInId": "290" } }, "outputs": { "output0": { "outputTitle": "Value(Ref)", "dataType": "Number", "pinOutId": "295", "outOrder": 1 } }, "color": "Func", "rows": 2, "colums": 12 } }, { "position": { "x": 1547.3197219286844, "y": -149.4043843715472 }, "nodeDescription": { "nodeTitle": "Set fib0", "execIn": true, "pinExecInId": "263", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "264", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "Value", "dataType": "Number", "defValue": 0, "pinInId": "265" } }, "outputs": { "output0": { "outputTitle": "Value(Ref)", "dataType": "Number", "pinOutId": "270", "outOrder": 1 } }, "color": "Func", "rows": 2, "colums": 12 } }, { "position": { "x": 360.3458171820312, "y": 74.17517317011703 }, "nodeDescription": { "nodeTitle": "Begin", "execIn": false, "pinExecInId": null, "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "10", "outOrder": 0 } }, "color": "Begin", "rows": 2, "colums": 10 } }, { "position": { "x": 1551.216765448137, "y": 5.548682440772666 }, "nodeDescription": { "nodeTitle": "Get fib1", "outputs": { "output0": { "outputTitle": "Value(Ref)", "dataType": "Number", "pinOutId": "279", "outOrder": 0 } }, "color": "Get", "rows": 2, "colums": 10 } }, { "position": { "x": 1512.771776695504, "y": 1416.9201863915812 }, "nodeDescription": { "nodeTitle": "Alert", "execIn": true, "pinExecInId": "115", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "116", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "Value", "dataType": "Data", "defValue": "'After closing this dialog, right click on the editor to get bunch of new nodes and create your own scripts :)'", "pinInId": "117" } }, "color": "Print", "rows": 3, "colums": 12 } }, { "position": { "x": 1561.0501231968399, "y": 1251.0411440014332 }, "nodeDescription": { "nodeTitle": "Alert", "execIn": true, "pinExecInId": "101", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "102", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "Value", "dataType": "Data", "defValue": "'u missed some nice stuff'", "pinInId": "103" } }, "color": "Print", "rows": 3, "colums": 12 } }, { "position": { "x": 1821.096442061649, "y": 1241.9167263753077 }, "nodeDescription": { "nodeTitle": "Get coolStuff", "outputs": { "output0": { "outputTitle": "Value(Ref)", "dataType": "String", "pinOutId": "32", "outOrder": 0 } }, "color": "Get", "rows": 2, "colums": 10 } }, { "position": { "x": 1902.6031409132486, "y": 1076.0798713887261 }, "nodeDescription": { "nodeTitle": "OpenWindow", "execIn": true, "pinExecInId": "16", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "17", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "URL", "dataType": "String", "defValue": "'link'", "pinInId": "18" } }, "outputs": { "output0": { "outputTitle": "Success?", "dataType": "Boolean", "pinOutId": "23", "outOrder": 1 } }, "color": "Func", "rows": 2, "colums": 12 } }, { "position": { "x": 1338.2444748633134, "y": 543.136740532748 }, "nodeDescription": { "nodeTitle": "Print", "execIn": true, "pinExecInId": "334", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "335", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "Value", "dataType": "Data", "defValue": "'hello'", "pinInId": "336" } }, "color": "Print", "rows": 3, "colums": 12 } }, { "position": { "x": 669.8079691212556, "y": 560.617817270978 }, "nodeDescription": { "nodeTitle": "HttpRequest", "execIn": true, "pinExecInId": "306", "execOut": { "execOut0": { "execOutTitle": "OnSuccess", "pinExecOutId": "307", "outOrder": 0 }, "execOut1": { "execOutTitle": "OnFail", "pinExecOutId": "309", "outOrder": 2 }, "execOut2": { "execOutTitle": "Continue", "pinExecOutId": "311", "outOrder": 3 } }, "inputs": { "input0": { "inputTitle": "URL", "dataType": "String", "defValue": "'link'", "pinInId": "313" } }, "outputs": { "output0": { "outputTitle": "JSON", "dataType": "Data", "pinOutId": "318", "outOrder": 1 } }, "color": "Func", "rows": 2, "colums": 12 } }, { "position": { "x": 445.32242406433573, "y": 657.8024685377316 }, "nodeDescription": { "nodeTitle": "Get catFactsApi", "outputs": { "output0": { "outputTitle": "Value(Ref)", "dataType": "String", "pinOutId": "325", "outOrder": 0 } }, "color": "Get", "rows": 2, "colums": 10 } }, { "position": { "x": 1355.211745865666, "y": 786.6439536440772 }, "nodeDescription": { "nodeTitle": "Print", "execIn": true, "pinExecInId": "352", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "353", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "Value", "dataType": "Data", "defValue": "'API REQUEST UNSUCCESSFUL'", "pinInId": "354" } }, "color": "Print", "rows": 3, "colums": 12 } }, { "position": { "x": 1096.6222103758923, "y": 617.8530961704762 }, "nodeDescription": { "nodeTitle": "GetByName(JSON)", "inputs": { "input0": { "inputTitle": "JSON", "dataType": "Data", "isInputBoxRequired": false, "pinInId": "368" }, "input1": { "inputTitle": "Name", "dataType": "String", "defValue": "'fact'", "pinInId": "370" } }, "outputs": { "output0": { "outputTitle": "Data", "dataType": "Data", "pinOutId": "375", "outOrder": 0 } }, "color": "Get", "rows": 2, "colums": 12 } }, { "position": { "x": 3719.492065534509, "y": 45.67011863451796 }, "nodeDescription": { "nodeTitle": "If/Else", "execIn": true, "pinExecInId": "407", "execOut": { "execOut0": { "execOutTitle": "True", "pinExecOutId": "408", "outOrder": 0 }, "execOut1": { "execOutTitle": "False", "pinExecOutId": "410", "outOrder": 1 }, "execOut2": { "execOutTitle": "Done", "pinExecOutId": "412", "outOrder": 2 } }, "inputs": { "input0": { "inputTitle": "Bool", "dataType": "Boolean", "defValue": true, "pinInId": "414" } }, "color": "Logic", "rows": 3, "colums": 12 } }, { "position": { "x": 3497.0899677450816, "y": 121.65556858361023 }, "nodeDescription": { "nodeTitle": "ForEachLoop", "pinExecInId": "442", "execIn": true, "execOut": { "execOut0": { "execOutTitle": "Loop Body", "pinExecOutId": "443", "outOrder": 0 }, "execOut1": { "execOutTitle": "Completed", "pinExecOutId": "445", "outOrder": 4 } }, "inputs": { "input0": { "inputTitle": "Array", "dataType": "Array", "isInputBoxRequired": false, "pinInId": "447" } }, "outputs": { "output0": { "outputTitle": "Value", "dataType": "Data", "pinOutId": "449", "outOrder": 1 }, "output1": { "outputTitle": "Index", "dataType": "Number", "pinOutId": "451", "outOrder": 2 }, "output2": { "outputTitle": "Array", "dataType": "Array", "pinOutId": "453", "outOrder": 3 } }, "color": "Logic", "rows": 2, "colums": 12 } }, { "position": { "x": 3755.240342745082, "y": 248.99431858361027 }, "nodeDescription": { "nodeTitle": "BinarySearch(Num)", "inputs": { "input0": { "inputTitle": "Value", "dataType": "Number", "defValue": 0, "pinInId": "424" }, "input1": { "inputTitle": "Array", "dataType": "Array", "isInputBoxRequired": false, "pinInId": "429" } }, "outputs": { "output0": { "outputTitle": "Exist", "dataType": "Boolean", "pinOutId": "431", "outOrder": 0 }, "output1": { "outputTitle": "Lower Bound", "dataType": "Number", "pinOutId": "433", "outOrder": 1 }, "output2": { "outputTitle": "Upper Bound", "dataType": "Number", "pinOutId": "435", "outOrder": 2 } }, "color": "Func", "rows": 2, "colums": 13 } }, { "position": { "x": 4023.809342745082, "y": 103.13356858361016 }, "nodeDescription": { "nodeTitle": "Continue", "execIn": true, "pinExecInId": "460", "color": "Logic", "rows": 2, "colums": 10 } }, { "position": { "x": 4030.755092745082, "y": 188.79781858361025 }, "nodeDescription": { "nodeTitle": "Break", "execIn": true, "pinExecInId": "466", "color": "Logic", "rows": 2, "colums": 10 } }, { "position": { "x": 4043.538201260707, "y": 288.14623690444165 }, "nodeDescription": { "nodeTitle": "Random", "outputs": { "output0": { "outputTitle": "Random[0,1)", "dataType": "Number", "pinOutId": "472", "outOrder": 0 } }, "color": "Math", "rows": 2, "colums": 10 } }, { "position": { "x": 4047.9482012607077, "y": 365.32127055007163 }, "nodeDescription": { "nodeTitle": "Swap", "execIn": true, "pinExecInId": "479", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "480", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "Ref1", "dataType": "Data", "isInputBoxRequired": false, "pinInId": "481" }, "input1": { "inputTitle": "Ref2", "dataType": "Data", "isInputBoxRequired": false, "pinInId": "483" } }, "outputs": { "output0": { "outputTitle": "Ref1", "dataType": "Data", "pinOutId": "485", "outOrder": 1 }, "output1": { "outputTitle": "Ref2", "dataType": "Data", "pinOutId": "487", "outOrder": 2 } }, "color": "Func", "rows": 2, "colums": 12 } }], "wireData": [{ "srcId": "10", "destId": "168" }, { "srcId": "63", "destId": "78" }, { "srcId": "69", "destId": "85" }, { "srcId": "169", "destId": "197" }, { "srcId": "161", "destId": "199" }, { "srcId": "161", "destId": "240" }, { "srcId": "231", "destId": "245" }, { "srcId": "198", "destId": "213" }, { "srcId": "250", "destId": "215" }, { "srcId": "220", "destId": "290" }, { "srcId": "214", "destId": "263" }, { "srcId": "264", "destId": "288" }, { "srcId": "279", "destId": "265" }, { "srcId": "83", "destId": "115" }, { "srcId": "81", "destId": "101" }, { "srcId": "79", "destId": "16" }, { "srcId": "32", "destId": "18" }, { "srcId": "307", "destId": "334" }, { "srcId": "325", "destId": "313" }, { "srcId": "309", "destId": "352" }, { "srcId": "318", "destId": "368" }, { "srcId": "375", "destId": "336" }] } - let starterJSON = JSON.stringify(starterFile); - new Import(stage, stage.findOne("#main_layer"), stage.findOne("#wireLayer"), starterJSON); +const STARTER_FILE_PATH = 'assets/starter.json'; + +export async function vscriptOnLoad(stage) { + const layer = stage.findOne('#main_layer'); + const wireLayer = stage.findOne('#wireLayer'); + try { + const res = await fetch(STARTER_FILE_PATH); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const starterJSON = await res.text(); + new Import(stage, layer, wireLayer, starterJSON); + } catch (err) { + console.error('Failed to load starter.json:', err); + showAlert('Could not load starter.json. Using empty project.'); + refresh(layer, wireLayer); + } } diff --git a/src/js/ui/functionPanel.js b/src/js/ui/functionPanel.js new file mode 100644 index 0000000..f8c2334 --- /dev/null +++ b/src/js/ui/functionPanel.js @@ -0,0 +1,522 @@ +import { colorMap } from '../core/colorMap.js'; +import { tabManager, isValidFunctionName } from '../editor/tabManager.js'; +import { applyOrphanOverlay } from '../editor/orphanOverlay.js'; +import { showAlert } from './dialogs.js'; +import { Nodes } from '../nodes/nodeFactory.js'; +import { showDocTooltip, hideDocTooltip } from '../utils/docTooltip.js'; + +const DATA_TYPES = ['Number', 'Boolean', 'String', 'Array']; + +function createDefaultValueInput(dataType, currentValue) { + if (dataType === 'Boolean') { + const sel = document.createElement('select'); + sel.className = 'param-def-value'; + const optT = document.createElement('option'); + optT.value = 'true'; + optT.textContent = 'True'; + const optF = document.createElement('option'); + optF.value = 'false'; + optF.textContent = 'False'; + sel.appendChild(optT); + sel.appendChild(optF); + if (currentValue === 'false') optF.selected = true; + return sel; + } + const inp = document.createElement('input'); + inp.className = 'param-def-value'; + inp.type = dataType === 'Number' ? 'number' : 'text'; + if (dataType === 'Number') inp.value = currentValue != null ? currentValue : 0; + else if (dataType === 'String') { + let display = currentValue != null ? currentValue : ''; + if (typeof display === 'string' && display.startsWith("'") && display.endsWith("'")) display = display.slice(1, -1); + inp.value = display; + } else if (dataType === 'Array') inp.value = currentValue || '[]'; + inp.placeholder = dataType === 'Array' ? '[1, 2, 3]' : 'Default value'; + return inp; +} + +function extractDefValue(dataType, el) { + const raw = el.value; + if (dataType === 'Boolean') return raw === 'true'; + if (dataType === 'Number') return raw.toString(); + if (dataType === 'String') return `'${raw}'`; + if (dataType === 'Array') return raw; + return raw; +} + +function buildParamForm(sectionId, onAdd, options = {}) { + const { includeDefaultValue: includeDef = false } = options; + const content = document.getElementById(sectionId); + if (!content) return; + content.innerHTML = ''; + + const sectionInner = document.createElement('div'); + sectionInner.className = 'left-panel-section-inner'; + + const form = document.createElement('div'); + form.className = 'var-inline-edit'; + + const typeSelect = document.createElement('select'); + for (const t of DATA_TYPES) { + const opt = document.createElement('option'); + opt.value = t; + opt.textContent = t; + typeSelect.appendChild(opt); + } + + const nameInput = document.createElement('input'); + nameInput.type = 'text'; + nameInput.placeholder = 'Name'; + + const defValueContainer = document.createElement('div'); + defValueContainer.className = 'param-def-value-container'; + let defValueEl = null; + if (includeDef) { + defValueEl = createDefaultValueInput(typeSelect.value, null); + defValueContainer.appendChild(defValueEl); + typeSelect.addEventListener('change', () => { + defValueContainer.innerHTML = ''; + defValueEl = createDefaultValueInput(typeSelect.value); + defValueContainer.appendChild(defValueEl); + }); + } + + const actionsDiv = document.createElement('div'); + actionsDiv.classList.add('var-edit-actions'); + const addBtn = document.createElement('button'); + addBtn.classList.add('var-edit-save'); + addBtn.textContent = 'Add'; + addBtn.addEventListener('click', () => { + const name = nameInput.value.trim(); + const dataType = typeSelect.value; + if (!name) { showAlert("Parameter name can't be empty!"); return; } + if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) { + showAlert("Parameter name must be a valid identifier"); return; + } + const param = { name, dataType }; + if (includeDef && defValueEl) { + param.defValue = extractDefValue(dataType, defValueEl); + } + onAdd(param); + nameInput.value = ''; + if (includeDef && defValueEl) { + defValueContainer.innerHTML = ''; + defValueEl = createDefaultValueInput(typeSelect.value, null); + defValueContainer.appendChild(defValueEl); + } + }); + actionsDiv.appendChild(addBtn); + + form.appendChild(typeSelect); + form.appendChild(nameInput); + if (includeDef) form.appendChild(defValueContainer); + form.appendChild(actionsDiv); + sectionInner.appendChild(form); + + const listEl = document.createElement('ul'); + listEl.className = 'param-list'; + sectionInner.appendChild(listEl); + content.appendChild(sectionInner); + + return listEl; +} + +function renderParamList(listEl, params, onRemove, onEdit) { + listEl.innerHTML = ''; + params.forEach((param, idx) => { + const li = document.createElement('li'); + li.className = 'left-panel-variable'; + li.style.borderWidth = '2px'; + li.style.borderStyle = 'solid'; + li.style.borderColor = colorMap[param.dataType] || '#fff'; + li.style.boxShadow = `inset 0px 0px 5px ${colorMap[param.dataType] || '#fff'}`; + li.style.backgroundColor = 'transparent'; + + const nameSpan = document.createElement('span'); + nameSpan.className = 'var-name-text'; + nameSpan.textContent = param.name; + nameSpan.style.overflow = 'hidden'; + nameSpan.style.textOverflow = 'ellipsis'; + nameSpan.style.whiteSpace = 'nowrap'; + nameSpan.style.flex = '1'; + li.appendChild(nameSpan); + + const typeSpan = document.createElement('span'); + typeSpan.style.fontSize = '0.85rem'; + typeSpan.style.opacity = '0.6'; + typeSpan.style.marginLeft = '0.3rem'; + typeSpan.textContent = param.dataType; + li.appendChild(typeSpan); + + if (param.defValue != null && param.defValue !== '') { + const defSpan = document.createElement('span'); + defSpan.style.fontSize = '0.75rem'; + defSpan.style.opacity = '0.5'; + defSpan.style.marginLeft = '0.3rem'; + const display = typeof param.defValue === 'string' && param.defValue.startsWith("'") && param.defValue.endsWith("'") + ? param.defValue.slice(1, -1) : String(param.defValue); + defSpan.textContent = `= ${display}`; + defSpan.title = 'Default value'; + li.appendChild(defSpan); + } + + const actionsDiv = document.createElement('div'); + actionsDiv.classList.add('var-actions'); + + const deleteBtn = document.createElement('button'); + deleteBtn.classList.add('var-delete-btn'); + deleteBtn.innerHTML = ''; + deleteBtn.title = 'Remove parameter'; + deleteBtn.addEventListener('click', (e) => { + e.stopPropagation(); + onRemove(idx); + }); + actionsDiv.appendChild(deleteBtn); + li.appendChild(actionsDiv); + + li.addEventListener('mouseover', () => { + li.style.boxShadow = `inset 0px 0px 30px ${colorMap[param.dataType] || '#fff'}`; + }); + li.addEventListener('mouseleave', () => { + li.style.boxShadow = `inset 0px 0px 5px ${colorMap[param.dataType] || '#fff'}`; + }); + + listEl.appendChild(li); + }); +} + +let inputListEl = null; +let outputListEl = null; + +function refreshInputList() { + const tab = tabManager.getActiveTab(); + if (!tab || tab.type !== 'function' || !inputListEl) return; + renderParamList(inputListEl, tab.inputParams, (idx) => { + tab.inputParams.splice(idx, 1); + refreshInputList(); + tabManager._emit('functionParamsChanged', { tab, paramType: 'input' }); + }); +} + +function refreshOutputList() { + const tab = tabManager.getActiveTab(); + if (!tab || tab.type !== 'function' || !outputListEl) return; + renderParamList(outputListEl, tab.outputParams, (idx) => { + tab.outputParams.splice(idx, 1); + refreshOutputList(); + tabManager._emit('functionParamsChanged', { tab, paramType: 'output' }); + }); +} + +export function initFunctionPanel() { + document.getElementById('input-params').addEventListener('click', () => { + document.getElementById('input-params-content').classList.toggle('hidden'); + document.getElementById('input-params-arrow').classList.toggle('left-panel-tab-arrow-up'); + }); + + document.getElementById('output-params').addEventListener('click', () => { + document.getElementById('output-params-content').classList.toggle('hidden'); + document.getElementById('output-params-arrow').classList.toggle('left-panel-tab-arrow-up'); + }); + + document.getElementById('doc-string').addEventListener('click', () => { + document.getElementById('doc-string-content').classList.toggle('hidden'); + document.getElementById('doc-string-arrow').classList.toggle('left-panel-tab-arrow-up'); + }); + + const docStringInput = document.getElementById('doc-string-input'); + if (docStringInput) { + docStringInput.addEventListener('input', () => { + const tab = tabManager.getActiveTab(); + if (!tab || tab.type !== 'function') return; + tab.docString = docStringInput.value.trim(); + Nodes.updateCallNodesDocString(tab.name, tab.docString); + tabManager._emit('functionDocChanged', { tab }); + }); + } + + const updateCallersBtn = document.getElementById('update-callers-btn'); + if (updateCallersBtn) { + updateCallersBtn.addEventListener('click', () => { + const tab = tabManager.getActiveTab(); + if (!tab || tab.type !== 'function') return; + Nodes.updateCallNodesToDefinition(tab.name, tab.inputParams, tab.outputParams, tab.docString || ''); + }); + } + + document.getElementById('list-of-functions').addEventListener('click', () => { + document.getElementById('list-of-functions-content').classList.toggle('hidden'); + document.getElementById('list-of-functions-down-icon').classList.toggle('left-panel-tab-arrow-up'); + }); + + inputListEl = buildParamForm('input-params-content', (param) => { + const tab = tabManager.getActiveTab(); + if (!tab || tab.type !== 'function') return; + if (tab.inputParams.some(p => p.name === param.name)) { + showAlert('Input parameter already exists'); return; + } + tab.inputParams.push(param); + refreshInputList(); + tabManager._emit('functionParamsChanged', { tab, paramType: 'input' }); + }, { includeDefaultValue: true }); + + outputListEl = buildParamForm('output-params-content', (param) => { + const tab = tabManager.getActiveTab(); + if (!tab || tab.type !== 'function') return; + if (tab.outputParams.some(p => p.name === param.name)) { + showAlert('Output parameter already exists'); return; + } + tab.outputParams.push(param); + refreshOutputList(); + tabManager._emit('functionParamsChanged', { tab, paramType: 'output' }); + }); + + tabManager.on('tabSwitched', ({ to }) => { + updatePanelVisibility(to); + }); + + tabManager.on('tabCreated', (tab) => { + updatePanelVisibility(tab); + if (tab.type === 'function' && !tabManager._suppressAutoNodes) { + createFunctionNodes(tab); + } + refreshFunctionsList(); + refreshContextMenuFunctions(); + }); + + tabManager.on('tabClosed', (tab) => { + if (tab && tab.type === 'function') { + const stage = tabManager.getStage(); + const remainingTabs = tabManager.getAllTabs(); + for (const t of remainingTabs) { + const layer = t.layer; + if (!layer) continue; + layer.find('.aProgramNodeGroup').forEach((grp) => { + const cc = grp.customClass; + if (!cc || !cc.nodeDescription || !cc.nodeDescription.isCallFunction || cc.nodeDescription.calledFunctionName !== tab.name) return; + if (cc.isOrphaned) return; + applyOrphanOverlay(grp, layer, stage, 'Deleted Func'); + }); + } + if (stage) stage.draw(); + } + refreshFunctionsList(); + refreshContextMenuFunctions(); + }); + + tabManager.on('tabRenamed', () => { + refreshFunctionsList(); + refreshContextMenuFunctions(); + }); + + tabManager.on('functionParamsChanged', ({ tab, paramType }) => { + syncFunctionNodes(tab, paramType); + Nodes.updateCallNodesToDefinition(tab.name, tab.inputParams, tab.outputParams, tab.docString || ''); + refreshFunctionsList(); + refreshContextMenuFunctions(); + }); + + tabManager.on('functionDocChanged', () => { + refreshFunctionsList(); + }); +} + +function createFunctionNodes(tab) { + const stage = tabManager.getStage(); + const containerRect = stage.container().getBoundingClientRect(); + const beginX = containerRect.width * 0.2; + const beginY = containerRect.height * 0.3; + const returnX = containerRect.width * 0.7; + const returnY = containerRect.height * 0.3; + + const beginNode = Nodes.CreateFunctionBeginNode( + tab.inputParams, { x: beginX, y: beginY }, tab.layer, stage + ); + tab.beginNodeId = beginNode.grp.id(); + + const returnNode = Nodes.CreateReturnNode( + tab.outputParams, { x: returnX, y: returnY }, tab.layer, stage + ); + tab.returnNodeId = returnNode.grp.id(); +} + +function syncFunctionNodes(tab, paramType) { + const stage = tabManager.getStage(); + const wireLayer = tab.wireLayer; + + if (paramType === 'input') { + const beginGrp = tab.layer.findOne('#FunctionBegin') || tab.layer.findOne(`#${tab.beginNodeId}`); + if (beginGrp && beginGrp.customClass) { + const newNode = Nodes.rebuildFunctionBeginNode(beginGrp.customClass, tab.inputParams, tab.layer, stage, wireLayer); + tab.beginNodeId = newNode.grp.id(); + } + } else if (paramType === 'output') { + const returnGrp = tab.layer.findOne('#Return') || tab.layer.findOne(`#${tab.returnNodeId}`); + if (returnGrp && returnGrp.customClass) { + const newNode = Nodes.rebuildReturnNode(returnGrp.customClass, tab.outputParams, tab.layer, stage, wireLayer); + tab.returnNodeId = newNode.grp.id(); + } + } +} + +function updatePanelVisibility(tab) { + const inputSection = document.getElementById('input-params-section'); + const outputSection = document.getElementById('output-params-section'); + const docSection = document.getElementById('doc-string-section'); + const docStringInput = document.getElementById('doc-string-input'); + const updateCallersSection = document.getElementById('update-callers-section'); + + if (tab.type === 'function') { + inputSection.classList.remove('hidden'); + outputSection.classList.remove('hidden'); + if (docSection) docSection.classList.remove('hidden'); + if (updateCallersSection) updateCallersSection.classList.remove('hidden'); + if (docStringInput) docStringInput.value = tab.docString || ''; + refreshInputList(); + refreshOutputList(); + } else { + inputSection.classList.add('hidden'); + outputSection.classList.add('hidden'); + if (docSection) docSection.classList.add('hidden'); + if (updateCallersSection) updateCallersSection.classList.add('hidden'); + } +} + +function refreshFunctionsList() { + const listEl = document.getElementById('function-list'); + if (!listEl) return; + listEl.innerHTML = ''; + + const funcTabs = tabManager.getAllFunctionTabs(); + for (const ft of funcTabs) { + const li = document.createElement('li'); + li.className = 'left-panel-variable'; + li.style.borderWidth = '2px'; + li.style.borderStyle = 'solid'; + li.style.borderColor = colorMap['Call'] || '#00bfa5'; + li.style.boxShadow = `inset 0px 0px 5px ${colorMap['Call'] || '#00bfa5'}`; + li.style.backgroundColor = 'transparent'; + li.setAttribute('draggable', 'true'); + + const nameSpan = document.createElement('span'); + nameSpan.className = 'var-name-text'; + nameSpan.textContent = ft.name; + nameSpan.style.overflow = 'hidden'; + nameSpan.style.textOverflow = 'ellipsis'; + nameSpan.style.whiteSpace = 'nowrap'; + nameSpan.style.flex = '1'; + li.appendChild(nameSpan); + + let docIcon = null; + if (ft.docString) { + docIcon = document.createElement('span'); + docIcon.className = 'function-list-doc-icon'; + docIcon.textContent = 'i'; + docIcon.title = 'Documentation'; + li.appendChild(docIcon); + } + + const infoSpan = document.createElement('span'); + infoSpan.style.fontSize = '0.8rem'; + infoSpan.style.opacity = '0.5'; + infoSpan.style.marginLeft = '0.3rem'; + const inCount = ft.inputParams.length; + const outCount = ft.outputParams.length; + infoSpan.textContent = `(${inCount}→${outCount})`; + li.appendChild(infoSpan); + + li.addEventListener('mouseover', () => { + li.style.boxShadow = `inset 0px 0px 30px ${colorMap['Call'] || '#00bfa5'}`; + }); + li.addEventListener('mouseleave', () => { + li.style.boxShadow = `inset 0px 0px 5px ${colorMap['Call'] || '#00bfa5'}`; + if (docIcon) hideDocTooltip(docIcon); + }); + + if (docIcon) { + li.addEventListener('mouseenter', (e) => { + showDocTooltip(docIcon, ft.docString); + }); + } + + li.addEventListener('dblclick', () => { + tabManager.switchTab(ft.id); + }); + + li.addEventListener('dragstart', (e) => { + e.dataTransfer.setData('functionTabId', ft.id); + e.dataTransfer.setData('functionName', ft.name); + }); + + listEl.appendChild(li); + } +} + +function refreshContextMenuFunctions() { + let section = document.getElementById('context-menu-functions-section'); + const contextMenu = document.getElementById('context-menu'); + if (!contextMenu) return; + + if (section) { + section.remove(); + } + + const funcTabs = tabManager.getAllFunctionTabs(); + if (funcTabs.length === 0) return; + + section = document.createElement('div'); + section.className = 'ctx-menu-section'; + section.id = 'context-menu-functions-section'; + section.dataset.category = 'Functions'; + + const callColor = colorMap['Call'] || '#00bfa5'; + const header = document.createElement('div'); + header.className = 'ctx-menu-section-header'; + header.style.borderLeftColor = callColor; + header.style.color = callColor; + + const arrow = document.createElement('span'); + arrow.className = 'ctx-menu-section-arrow'; + arrow.textContent = '\u25BC'; + const label = document.createElement('span'); + label.textContent = 'Functions'; + header.appendChild(arrow); + header.appendChild(label); + + header.addEventListener('click', (e) => { + e.stopPropagation(); + const collapsed = section.classList.toggle('ctx-menu-section--collapsed'); + arrow.textContent = collapsed ? '\u25B6' : '\u25BC'; + }); + + const body = document.createElement('div'); + body.className = 'ctx-menu-section-body'; + + for (const ft of funcTabs) { + const item = document.createElement('div'); + item.className = 'context-menu-items'; + item.textContent = `Call ${ft.name}`; + item.dataset.functionTabId = ft.id; + item.style.borderLeftColor = callColor; + item.style.color = callColor; + + item.addEventListener('click', () => { + const stage = tabManager.getStage(); + const activeLayer = tabManager.getActiveLayer(); + const rect = item.getBoundingClientRect(); + const containerRect = stage.container().getBoundingClientRect(); + const x = rect.x - containerRect.x; + const y = rect.y - containerRect.y; + Nodes.CreateCallNode(ft.name, ft.inputParams, ft.outputParams, + { x, y }, activeLayer, stage, ft.docString || ''); + activeLayer.draw(); + document.getElementById('ctx-menu-container').classList.add('hidden'); + }); + + body.appendChild(item); + } + + section.appendChild(header); + section.appendChild(body); + contextMenu.appendChild(section); +} diff --git a/src/js/ui/variableList.js b/src/js/ui/variableList.js index 6659db3..dcf4cb6 100644 --- a/src/js/ui/variableList.js +++ b/src/js/ui/variableList.js @@ -1,7 +1,10 @@ import { colorMap, lightenHex } from '../core/colorMap.js' import { ContextMenu } from '../editor/contextMenu.js' import { deleteNodeByGroup, deleteWire } from '../editor/deleteHandler.js' +import { applyOrphanOverlay } from '../editor/orphanOverlay.js' +import { applyMismatchToWire, removeMismatchFromWire } from '../utils/wireMismatch.js' import { showAlert, showConfirm } from './dialogs.js' +import { tabManager } from '../editor/tabManager.js' class VariableList { @@ -161,78 +164,7 @@ class VariableList { } _applyOrphanOverlay(grp) { - const cc = grp.customClass; - cc.isOrphaned = true; - - const bodyRect = cc.bodyRect; - const overlay = new Konva.Rect({ - x: bodyRect.x(), - y: bodyRect.y(), - width: bodyRect.width(), - height: bodyRect.height(), - fill: 'rgba(255, 0, 0, 0.25)', - cornerRadius: 5, - listening: false, - }); - grp.add(overlay); - - const boxWidth = 120; - const boxHeight = 36; - const boxX = bodyRect.width() / 2 - boxWidth / 2; - const boxY = -boxHeight - 5; - - const indicatorGrp = new Konva.Group({ x: boxX, y: boxY }); - - const bg = new Konva.Rect({ - width: boxWidth, - height: boxHeight, - fill: 'rgba(30, 0, 0, 0.85)', - cornerRadius: 3, - stroke: '#f44', - strokeWidth: 1, - }); - indicatorGrp.add(bg); - - const label = new Konva.Text({ - text: 'Deleted Var', - fontSize: 9, - fontFamily: 'Verdana', - fill: '#f88', - width: boxWidth, - align: 'center', - y: 3, - }); - indicatorGrp.add(label); - - const removeBtn = new Konva.Text({ - text: 'Remove', - fontSize: 10, - fontFamily: 'Verdana', - fill: '#fff', - width: boxWidth, - align: 'center', - y: 18, - }); - removeBtn.on('mouseenter', () => { - removeBtn.fill('#f44'); - document.body.style.cursor = 'pointer'; - this.layer.draw(); - }); - removeBtn.on('mouseleave', () => { - removeBtn.fill('#fff'); - document.body.style.cursor = 'default'; - this.layer.draw(); - }); - removeBtn.on('click', (e) => { - e.cancelBubble = true; - document.body.style.cursor = 'default'; - deleteNodeByGroup(grp, this.stage); - }); - indicatorGrp.add(removeBtn); - - grp.add(indicatorGrp); - cc._orphanOverlay = overlay; - cc._orphanIndicator = indicatorGrp; + applyOrphanOverlay(grp, this.layer, this.stage, 'Deleted Var'); } // ---- EDIT FLOW ---- @@ -516,7 +448,7 @@ class VariableList { if (this.layer) this.layer.draw(); if (this.stage) { - const wireLayer = this.stage.findOne('#wireLayer'); + const wireLayer = tabManager.getActiveWireLayer(); if (wireLayer) wireLayer.draw(); } } @@ -525,7 +457,7 @@ class VariableList { _markMismatchedWires(grp, newDataType) { const cc = grp.customClass; - const wireLayer = this.stage ? this.stage.findOne('#wireLayer') : null; + const wireLayer = this.stage ? tabManager.getActiveWireLayer() : null; if (!wireLayer) return; for (const pin of cc.inputPins) { @@ -533,9 +465,9 @@ class VariableList { const srcPin = pin.wire.attrs.src; const srcType = srcPin && srcPin.attrs.pinDataType; if (srcType && srcType !== 'Data' && newDataType !== 'Data' && srcType !== newDataType) { - this._applyMismatchToWire(pin.wire, wireLayer); + applyMismatchToWire(pin.wire, wireLayer, this.stage); } else { - this._removeMismatchFromWire(pin.wire); + removeMismatchFromWire(pin.wire); } } } @@ -547,127 +479,48 @@ class VariableList { const destPin = w.attrs.dest; const destType = destPin && destPin.attrs.pinDataType; if (destType && destType !== 'Data' && newDataType !== 'Data' && destType !== newDataType) { - this._applyMismatchToWire(w, wireLayer); + applyMismatchToWire(w, wireLayer, this.stage); } else { - this._removeMismatchFromWire(w); + removeMismatchFromWire(w); } } } } } - _applyMismatchToWire(wire, wireLayer) { - if (wire.isMismatched) return; - wire.isMismatched = true; - wire.dash([10, 5]); - - const r = 8; - const indicatorGrp = new Konva.Group({ x: 0, y: 0 }); - - const bg = new Konva.Circle({ - radius: r, - fill: 'rgba(50, 20, 0, 0.9)', - stroke: '#ff8800', - strokeWidth: 1.5, - }); - indicatorGrp.add(bg); - - const cross1 = new Konva.Line({ - points: [-4, -4, 4, 4], - stroke: '#fff', - strokeWidth: 2, - lineCap: 'round', - }); - const cross2 = new Konva.Line({ - points: [-4, 4, 4, -4], - stroke: '#fff', - strokeWidth: 2, - lineCap: 'round', - }); - indicatorGrp.add(cross1); - indicatorGrp.add(cross2); - - const tooltip = new Konva.Label({ x: r + 4, y: -10, visible: false }); - tooltip.add(new Konva.Tag({ - fill: 'rgba(30, 10, 0, 0.9)', - cornerRadius: 3, - stroke: '#ff8800', - strokeWidth: 0.5, - })); - tooltip.add(new Konva.Text({ - text: 'Type Mismatch', - fontSize: 10, - fontFamily: 'Verdana', - fill: '#ffaa44', - padding: 4, - })); - indicatorGrp.add(tooltip); - - indicatorGrp.on('mouseenter', () => { - bg.fill('#ff4400'); - cross1.stroke('#fff'); - cross2.stroke('#fff'); - tooltip.visible(true); - document.body.style.cursor = 'pointer'; - wireLayer.draw(); - }); - indicatorGrp.on('mouseleave', () => { - bg.fill('rgba(50, 20, 0, 0.9)'); - tooltip.visible(false); - document.body.style.cursor = 'default'; - wireLayer.draw(); - }); - indicatorGrp.on('click', (e) => { - e.cancelBubble = true; - document.body.style.cursor = 'default'; - indicatorGrp.destroy(); - deleteWire(wire); - if (this.stage) this.stage.draw(); + deleteAllVariables() { + this.variables.length = 0; + document.getElementById("variable-list").innerHTML = ''; + this.variablesElements.forEach((item) => { + item.el.remove(); }); - - const updatePosition = () => { - const pts = wire.points(); - if (pts.length >= 4) { - indicatorGrp.x((pts[0] + pts[pts.length - 2]) / 2); - indicatorGrp.y((pts[1] + pts[pts.length - 1]) / 2); - } - }; - updatePosition(); - - const srcNode = wire.attrs.src?.getParent(); - const destNode = wire.attrs.dest?.getParent(); - if (srcNode) srcNode.on('dragmove.mismatch', updatePosition); - if (destNode) destNode.on('dragmove.mismatch', updatePosition); - wire._mismatchDragCleanup = () => { - if (srcNode) srcNode.off('dragmove.mismatch'); - if (destNode) destNode.off('dragmove.mismatch'); - }; - - wireLayer.add(indicatorGrp); - wire._mismatchIndicator = indicatorGrp; - } - - _removeMismatchFromWire(wire) { - if (!wire.isMismatched) return; - wire.isMismatched = false; - wire.dash([]); - if (wire._mismatchDragCleanup) { - wire._mismatchDragCleanup(); - wire._mismatchDragCleanup = null; - } - if (wire._mismatchIndicator) { - wire._mismatchIndicator.destroy(); - wire._mismatchIndicator = null; - } + this.variablesElements = []; } - deleteAllVariables() { - this.variables = []; + switchToTab(tabVariables) { document.getElementById("variable-list").innerHTML = ''; this.variablesElements.forEach((item) => { item.el.remove(); }); this.variablesElements = []; + + this.variables = tabVariables; + + for (const variable of this.variables) { + const el = this.makeLeftPanelVariableListItem(variable); + document.getElementById("variable-list").appendChild(el); + + const set = this.makeContextMenuItem(variable, 'set'); + const get = this.makeContextMenuItem(variable, 'get'); + const variablesBody = document.getElementById("context-menu-variables-body"); + const container = variablesBody || document.getElementById("context-menu"); + container.appendChild(get); + container.appendChild(set); + ContextMenu.addEventToCtxMenuItems(set); + ContextMenu.addEventToCtxMenuItems(get); + this.variablesElements.push({ name: variable.name, type: 'get', el: get }); + this.variablesElements.push({ name: variable.name, type: 'set', el: set }); + } } } diff --git a/src/js/utils/docTooltip.js b/src/js/utils/docTooltip.js new file mode 100644 index 0000000..1338cef --- /dev/null +++ b/src/js/utils/docTooltip.js @@ -0,0 +1,46 @@ +/** + * Doc tooltip for DOM elements (e.g. function list info icon). + * Uses the same CSS class as call-node-doc-tooltip. + */ +let _el = null; +let _owner = null; + +function getOrCreateEl() { + if (_el) return _el; + const el = document.createElement('div'); + el.className = 'call-node-doc-tooltip'; + el.setAttribute('aria-hidden', 'true'); + document.body.appendChild(el); + _el = el; + return el; +} + +export function showDocTooltip(anchorEl, docString) { + if (!docString || !anchorEl) return; + const el = getOrCreateEl(); + _owner = anchorEl; + el.textContent = docString; + el.style.display = 'block'; + el.style.visibility = 'hidden'; + + const rect = anchorEl.getBoundingClientRect(); + const tw = el.offsetWidth; + const th = el.offsetHeight; + const gap = 8; + let left = rect.left + (rect.width / 2) - (tw / 2); + let top = rect.top - th - gap; + left = Math.max(6, Math.min(left, document.documentElement.clientWidth - tw - 6)); + top = Math.max(6, Math.min(top, document.documentElement.clientHeight - th - 6)); + el.style.left = Math.round(left) + 'px'; + el.style.top = Math.round(top) + 'px'; + el.style.visibility = 'visible'; +} + +export function hideDocTooltip(anchorEl) { + if (_owner !== anchorEl) return; + _owner = null; + if (_el) { + _el.style.display = 'none'; + _el.textContent = ''; + } +} diff --git a/src/js/utils/wireMismatch.js b/src/js/utils/wireMismatch.js new file mode 100644 index 0000000..d5f0ac3 --- /dev/null +++ b/src/js/utils/wireMismatch.js @@ -0,0 +1,127 @@ +import { deleteWire } from '../editor/deleteHandler.js'; +import { tabManager } from '../editor/tabManager.js'; + +/** + * Check if two data types are compatible (no mismatch). + * Data type is universal; otherwise types must match. + */ +export function isTypeCompatible(srcType, destType) { + if (!srcType || !destType) return true; + if (srcType === 'Data' || destType === 'Data') return true; + return srcType === destType; +} + +/** + * Apply dashed line and type-mismatch indicator to a wire. + * @param {Konva.Line} wire + * @param {Konva.Layer} wireLayer + * @param {Konva.Stage} [stage] - for draw() after delete + */ +export function applyMismatchToWire(wire, wireLayer, stage) { + if (wire.isMismatched) return; + wire.isMismatched = true; + wire.dash([10, 5]); + + const r = 8; + const indicatorGrp = new Konva.Group({ x: 0, y: 0 }); + + const bg = new Konva.Circle({ + radius: r, + fill: 'rgba(50, 20, 0, 0.9)', + stroke: '#ff8800', + strokeWidth: 1.5, + }); + indicatorGrp.add(bg); + + const cross1 = new Konva.Line({ + points: [-4, -4, 4, 4], + stroke: '#fff', + strokeWidth: 2, + lineCap: 'round', + }); + const cross2 = new Konva.Line({ + points: [-4, 4, 4, -4], + stroke: '#fff', + strokeWidth: 2, + lineCap: 'round', + }); + indicatorGrp.add(cross1); + indicatorGrp.add(cross2); + + const tooltip = new Konva.Label({ x: r + 4, y: -10, visible: false }); + tooltip.add(new Konva.Tag({ + fill: 'rgba(30, 10, 0, 0.9)', + cornerRadius: 3, + stroke: '#ff8800', + strokeWidth: 0.5, + })); + tooltip.add(new Konva.Text({ + text: 'Type Mismatch', + fontSize: 10, + fontFamily: 'Verdana', + fill: '#ffaa44', + padding: 4, + })); + indicatorGrp.add(tooltip); + + indicatorGrp.on('mouseenter', () => { + bg.fill('#ff4400'); + cross1.stroke('#fff'); + cross2.stroke('#fff'); + tooltip.visible(true); + document.body.style.cursor = 'pointer'; + wireLayer.draw(); + }); + indicatorGrp.on('mouseleave', () => { + bg.fill('rgba(50, 20, 0, 0.9)'); + tooltip.visible(false); + document.body.style.cursor = 'default'; + wireLayer.draw(); + }); + indicatorGrp.on('click', (e) => { + e.cancelBubble = true; + document.body.style.cursor = 'default'; + indicatorGrp.destroy(); + deleteWire(wire); + const s = stage || (wireLayer && wireLayer.getStage()) || tabManager.getStage(); + if (s) s.draw(); + }); + + const updatePosition = () => { + const pts = wire.points(); + if (pts.length >= 4) { + indicatorGrp.x((pts[0] + pts[pts.length - 2]) / 2); + indicatorGrp.y((pts[1] + pts[pts.length - 1]) / 2); + } + }; + updatePosition(); + + const srcNode = wire.attrs.src?.getParent(); + const destNode = wire.attrs.dest?.getParent(); + if (srcNode) srcNode.on('dragmove.mismatch', updatePosition); + if (destNode) destNode.on('dragmove.mismatch', updatePosition); + wire._mismatchDragCleanup = () => { + if (srcNode) srcNode.off('dragmove.mismatch'); + if (destNode) destNode.off('dragmove.mismatch'); + }; + + wireLayer.add(indicatorGrp); + wire._mismatchIndicator = indicatorGrp; +} + +/** + * Remove mismatch styling from a wire. + */ +export function removeMismatchFromWire(wire) { + if (!wire.isMismatched) return; + wire.isMismatched = false; + wire.dash([]); + if (wire._mismatchDragCleanup) { + wire._mismatchDragCleanup(); + wire._mismatchDragCleanup = null; + } + if (wire._mismatchIndicator) { + wire._mismatchIndicator.destroy(); + wire._mismatchIndicator = null; + } +} diff --git a/src/style.css b/src/style.css index 77a2094..1d7dd21 100644 --- a/src/style.css +++ b/src/style.css @@ -71,10 +71,18 @@ background-size: 10rem 10rem; overflow-y: auto; transition: left 0.5s; } - #create-variables{ + #create-variables, + .left-panel-section-inner{ width: 85%; margin: 0.4rem auto; padding: 0.4rem 0; + box-sizing: border-box; + } + #input-params-section, + #output-params-section, + #doc-string-section, + #update-callers-section{ + width: 100%; } #ctx-menu-container{ position: absolute; @@ -628,6 +636,64 @@ iframe{ .left-panel-variable .var-actions .var-delete-btn:hover{ color: #f44; } +.update-callers-btn{ + width: 100%; + min-width: 0; + max-width: 100%; + box-sizing: border-box; + padding: 0.5rem 0.75rem; + font-size: 0.9rem; + white-space: nowrap; + border: 1px solid #00bfa5; + border-radius: 6px; + background: #00897b; + color: #fff; + cursor: pointer; + transition: background 0.15s, border-color 0.15s; +} +.update-callers-btn:hover{ + background: #00a090; + border-color: #00e5cc; + color: #fff; +} + +.call-node-doc-tooltip{ + position: fixed; + z-index: 10000; + max-width: 260px; + padding: 8px 10px; + font-size: 12px; + font-family: Verdana, sans-serif; + line-height: 1.35; + color: #eee; + background: rgba(30, 30, 35, 0.97); + border: 1px solid #00bfa5; + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0,0,0,0.4); + pointer-events: none; + display: none; + word-wrap: break-word; +} + +.function-list-doc-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + margin-left: 6px; + font-size: 11px; + font-weight: bold; + font-family: Verdana, sans-serif; + color: #00bfa5; + cursor: help; + flex-shrink: 0; +} + +.function-list-doc-icon:hover { + color: #00e5cc; +} + .variableList{ display: flex; width: 100%; @@ -844,4 +910,159 @@ iframe{ display: flex; gap: 0.5rem; justify-content: center; +} +/* ── Tab bar ── */ +#tab-bar{ + position: absolute; + top: 0; + left: 20rem; + height: 3rem; + display: flex; + align-items: center; + z-index: 4; + background: rgba(30, 32, 36, 0.88); + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); + border-bottom: 1px solid rgba(214, 90, 49, 0.3); + user-select: none; + transition: left 0.5s; +} +#left-panel.closed-left-panel ~ #tab-bar{ + left: 3rem; +} +#tab-list{ + display: flex; + align-items: center; + height: 100%; +} +.tab-item{ + display: flex; + align-items: center; + padding: 0 1.2rem; + height: 100%; + color: rgba(255,255,255,0.55); + font-size: 1.1rem; + cursor: pointer; + white-space: nowrap; + border-right: 1px solid rgba(255,255,255,0.08); + transition: background 0.15s, color 0.15s; + position: relative; +} +.tab-item:hover{ + background: rgba(255,255,255,0.07); + color: #fff; +} +.tab-item.tab-active{ + background: rgba(214, 90, 49, 0.18); + color: #fff; + box-shadow: inset 0 -2px 0 #d65a31; +} +.tab-name{ + pointer-events: auto; +} +.tab-close{ + margin-left: 0.6rem; + font-size: 1.3rem; + line-height: 1; + opacity: 0; + transition: opacity 0.15s, color 0.15s; +} +.tab-item:hover .tab-close{ + opacity: 0.5; +} +.tab-close:hover{ + opacity: 1 !important; + color: #f44; +} +#add-tab-btn{ + display: flex; + align-items: center; + justify-content: center; + height: 2.2rem; + width: 2.2rem; + margin-left: 0.4rem; + background: none; + border: 1px solid rgba(255,255,255,0.15); + border-radius: 4px; + color: rgba(255,255,255,0.45); + font-size: 1.5rem; + cursor: pointer; + transition: background 0.15s, color 0.15s, border-color 0.15s; +} +#add-tab-btn:hover{ + background: rgba(214, 90, 49, 0.25); + color: #fff; + border-color: #d65a31; +} +.tab-rename-input{ + background: rgba(255,255,255,0.1); + border: 1px solid #d65a31; + color: #fff; + font-size: 1.1rem; + font-family: inherit; + padding: 0.1rem 0.4rem; + outline: none; + width: 10rem; + border-radius: 2px; +} +/* ── Input/Output Parameters: same alignment as Add Variable, Variable List, Functions ── */ +.left-panel-section-inner .var-inline-edit{ + width: 100%; +} +.param-def-value-container{ + width: 100%; +} +.param-def-value-container .param-def-value{ + width: 100%; + box-sizing: border-box; +} +.left-panel-section-inner .var-edit-actions{ + justify-content: stretch; +} +.left-panel-section-inner .var-edit-save{ + width: 100%; + text-align: center; +} +.left-panel-section-inner .param-list{ + list-style: none; + padding: 0; + margin: 0.5rem 0 0; + display: flex; + flex-direction: column; + width: 100%; +} +.left-panel-section-inner .param-list .left-panel-variable{ + width: 100%; + margin: 0.2rem 0; + box-sizing: border-box; +} + +.doc-string-textarea{ + width: 100%; + box-sizing: border-box; + font-size: 0.9rem; + padding: 0.3rem 0.4rem; + background: rgba(255,255,255,0.1); + border: 1px solid rgba(255,255,255,0.3); + color: white; + outline: none; + border-radius: 2px; + resize: vertical; + min-height: 4rem; + font-family: inherit; +} +.doc-string-textarea:focus{ + border-color: #d65a31; +} +.doc-string-textarea::placeholder{ + color: rgba(255,255,255,0.4); +} + +.param-list{ + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + width: 100%; } \ No newline at end of file From 85095686e01c4e220a5306526eb3e4a9bbb8f748 Mon Sep 17 00:00:00 2001 From: Ayush Kashyap <55059858+ayushk7@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:11:15 +0530 Subject: [PATCH 2/2] Update GitHub Actions workflow for feature branch --- .github/workflows/static.yml | 43 ++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/static.yml diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml new file mode 100644 index 0000000..4582139 --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,43 @@ +# Simple workflow for deploying static content to GitHub Pages +name: Deploy static content to Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["feature/functions"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Single deploy job since we're just deploying + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + # Upload entire repository + path: 'src' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4