├── .browserslistrc ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── docs └── editor.jpg ├── examples ├── nogasm.json ├── simple-vibrator.json └── wave-types.json ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── components │ ├── NodeEditor.vue │ ├── connection │ │ ├── ConnectionIndicator.vue │ │ ├── ConnectionListItem.vue │ │ └── ConnectionListWindow.vue │ ├── option │ │ ├── DeviceFeatureOption.vue │ │ ├── DeviceFeatureOptionValue.ts │ │ └── VisualizeOption.vue │ ├── ui │ │ ├── Button.vue │ │ └── Popup.vue │ └── workspace │ │ ├── ComponentBrowser.vue │ │ ├── ComponentBrowserList.vue │ │ ├── ComponentCategoryBrowser.vue │ │ ├── ProjectProvider.vue │ │ ├── Workspace.vue │ │ ├── WorkspaceFooter.vue │ │ └── WorkspaceMenu.vue ├── connection │ ├── ButtplugIoConnection.ts │ ├── Connection.ts │ ├── ConnectionConfiguration.ts │ ├── ConnectionManager.ts │ ├── DevIo.ts │ └── DevIoConnection.ts ├── device │ ├── ButtplugIoDevice.ts │ ├── DevIoDevice.ts │ └── Device.ts ├── main.ts ├── nodes │ ├── Node.ts │ ├── device │ │ ├── CustomDeviceNode.ts │ │ ├── CustomInputDeviceNode.ts │ │ └── Vibrator.ts │ ├── math │ │ ├── Average.ts │ │ ├── Calculation.ts │ │ ├── Clamp.ts │ │ ├── Conditional.ts │ │ ├── Memory.ts │ │ └── StateMachine.ts │ └── signal │ │ ├── Constant.ts │ │ ├── Random.ts │ │ ├── RemoteSignal.ts │ │ ├── Time.ts │ │ ├── VisualizeWave.ts │ │ └── WaveGenerator.ts ├── project │ ├── NodeRegistry.ts │ ├── Project.ts │ ├── ProjectFileInterface.ts │ ├── ProjectLoaderV1.ts │ ├── ProjectWorkspace.ts │ └── ProjectWorkspaceBoard.ts ├── runtime │ ├── Runtime.ts │ └── RuntimeData.ts ├── shims-tsx.d.ts ├── shims-vue.d.ts ├── style.scss └── utils │ └── AsyncCache.ts ├── tsconfig.json └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | 'eslint:recommended', 9 | '@vue/typescript/recommended' 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 2020 13 | }, 14 | rules: { 15 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 16 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 MaidKun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Buttplug.io Interactive Editor 2 | 3 | ![Editor screenshot](./docs/editor.jpg) 4 | 5 | Graph editor for [Buttplug.IO](https://www.buttplug.io). Use this editor to design your own sessions simply by drag and drop. 6 | 7 | Automatically connects to ws://127.0.0.1:12345/ on startup. Reload page if the server was not running at the moment. 8 | 9 | This project is currently **highly WIP**, feel free to test it, but don't expect everything to work yet :) 10 | 11 | ## Project setup 12 | ``` 13 | yarn install 14 | ``` 15 | 16 | ### Compiles and hot-reloads for development 17 | ``` 18 | yarn serve 19 | ``` 20 | 21 | ### Compiles and minifies for production 22 | ``` 23 | yarn build 24 | ``` 25 | 26 | ### Lints and fixes files 27 | ``` 28 | yarn lint 29 | ``` -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /docs/editor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaidKun/buttplug-editor/6fca47d92db3940b72505c702e60e199b78ff6c1/docs/editor.jpg -------------------------------------------------------------------------------- /examples/nogasm.json: -------------------------------------------------------------------------------- 1 | {"identifier":"maidkun/buttplug-editor/project","version":1,"workspace":{"boards":[{"state":{"nodes":[{"type":"Constant","id":"node_16096162179727","name":"Sensitivity","options":[],"state":{},"interfaces":[["Number",{"id":"ni_16096162179728","value":0.1}],["Value",{"id":"ni_16096162179729","value":0.1}]],"position":{"x":49,"y":38},"width":200,"twoColumn":false},{"type":"Average","id":"node_160961624872320","name":"Average","options":[["Duration",25]],"state":{},"interfaces":[["Value",{"id":"ni_160961624872321","value":0}],["Average",{"id":"ni_160961624872422","value":0}]],"position":{"x":306,"y":132},"width":200,"twoColumn":false},{"type":"Calculation","id":"node_160961626177126","name":"Trigger","options":[["Operator",1]],"state":{},"interfaces":[["Operand A",{"id":"ni_160961626177127","value":0.1}],["Operand B",{"id":"ni_160961626177128","value":0}],["Result",{"id":"ni_160961626177129","value":0.1}]],"position":{"x":564,"y":2},"width":200,"twoColumn":false},{"type":"Conditional","id":"node_1609668644886266","name":"StopCondition","options":[["Test",4]],"state":{},"interfaces":[["Operand A",{"id":"ni_1609668644886267","value":0.1}],["Operand B",{"id":"ni_1609668644886268","value":0}],["True",{"id":"ni_1609668644886269","value":false}],["False",{"id":"ni_1609668644886270","value":true}]],"position":{"x":850,"y":185},"width":200,"twoColumn":false},{"type":"StateMachine","id":"node_1609668691022282","name":"State Machine","options":[],"state":{},"interfaces":[["Trigger 1",{"id":"ni_1609668691022283","value":false}],["State 1",{"id":"ni_1609668691022284","value":false}],["Trigger 2",{"id":"ni_1609668691022285","value":true}],["State 2",{"id":"ni_1609668691022286","value":true}],["Trigger 3",{"id":"ni_1609668691022287","value":false}],["State 3",{"id":"ni_1609668691022288","value":null}],["Trigger 4",{"id":"ni_1609668691022289","value":false}],["State 4",{"id":"ni_1609668691023290","value":null}],["Trigger 5",{"id":"ni_1609668691023291","value":false}],["State 5",{"id":"ni_1609668691023292","value":null}],["Trigger 6",{"id":"ni_1609668691023293","value":false}],["State 6",{"id":"ni_1609668691023294","value":null}],["Trigger 7",{"id":"ni_1609668691023295","value":false}],["State 7",{"id":"ni_1609668691023296","value":null}],["Trigger 8",{"id":"ni_1609668691023297","value":false}],["State 8",{"id":"ni_1609668691023298","value":null}],["Trigger 9",{"id":"ni_1609668691023299","value":false}],["State 9",{"id":"ni_1609668691023300","value":null}],["Trigger 10",{"id":"ni_1609668691023301","value":false}],["State 10",{"id":"ni_1609668691023302","value":null}]],"position":{"x":1258.3032851082344,"y":505.10718026110567},"width":200,"twoColumn":true},{"type":"Time","id":"node_1609668732468306","name":"Time","options":[],"state":{},"interfaces":[["Time",{"id":"ni_1609668732468307","value":41.73300004005432}]],"position":{"x":812.3296519381242,"y":-207.13795245100502},"width":200,"twoColumn":false},{"type":"Memory","id":"node_1609668734860308","name":"StopTime","options":[["On positive Edge only",true]],"state":{},"interfaces":[["Input",{"id":"ni_1609668734860309","value":41.73300004005432}],["Trigger",{"id":"ni_1609668734861310","value":false}],["Output",{"id":"ni_1609668734861311","value":0}]],"position":{"x":1101.3831615855395,"y":-52.17764562388498},"width":200,"twoColumn":false},{"type":"Calculation","id":"node_1609668778787320","name":"Calculation","options":[["Operator",2]],"state":{},"interfaces":[["Operand A",{"id":"ni_1609668778788321","value":41.73300004005432}],["Operand B",{"id":"ni_1609668778788322","value":0}],["Result",{"id":"ni_1609668778788323","value":41.73300004005432}]],"position":{"x":1379.3306135429116,"y":-171.2120852272152},"width":200,"twoColumn":false},{"type":"Conditional","id":"node_1609668800866333","name":"15SecondsPassed","options":[["Test",5]],"state":{},"interfaces":[["Operand A",{"id":"ni_1609668800866334","value":41.73300004005432}],["Operand B",{"id":"ni_1609668800867335","value":15}],["True",{"id":"ni_1609668800867336","value":true}],["False",{"id":"ni_1609668800867337","value":false}]],"position":{"x":1653.4927925478294,"y":-167.00129236669432},"width":200,"twoColumn":false},{"type":"Time","id":"node_1609668986142345","name":"Time","options":[],"state":{},"interfaces":[["Time",{"id":"ni_1609668986143346","value":41.73300004005432}]],"position":{"x":1664.1202258452663,"y":582.060959094971},"width":200,"twoColumn":false},{"type":"Memory","id":"node_1609668989934347","name":"RampUpStart","options":[["On positive Edge only",true]],"state":{},"interfaces":[["Input",{"id":"ni_1609668989934348","value":41.73300004005432}],["Trigger",{"id":"ni_1609668989934349","value":true}],["Output",{"id":"ni_1609668989934350","value":15.004999876022339}]],"position":{"x":1907.31439617554,"y":675.3949379784823},"width":200,"twoColumn":false},{"type":"Calculation","id":"node_1609669021941357","name":"RampUpDuration","options":[["Operator",2]],"state":{},"interfaces":[["Operand A",{"id":"ni_1609669021941358","value":41.73300004005432}],["Operand B",{"id":"ni_1609669021941359","value":15.004999876022339}],["Result",{"id":"ni_1609669021941360","value":26.728000164031982}]],"position":{"x":2158.3959450030084,"y":546.5677558575802},"width":200,"twoColumn":false},{"type":"Calculation","id":"node_1609669037997367","name":"Calculation","options":[["Operator",4]],"state":{},"interfaces":[["Operand A",{"id":"ni_1609669037997368","value":26.728000164031982}],["Operand B",{"id":"ni_1609669037997369","value":30}],["Result",{"id":"ni_1609669037997370","value":0.8909333388010661}]],"position":{"x":2401.590115333282,"y":539.9949404432488},"width":200,"twoColumn":false},{"type":"Clamp","id":"node_1609669050149374","name":"Clamp","options":[],"state":{},"interfaces":[["Value",{"id":"ni_1609669050149375","value":0.8909333388010661}],["Minimum",{"id":"ni_1609669050149376","value":0}],["Maximum",{"id":"ni_1609669050149377","value":1}],["Result",{"id":"ni_1609669050149378","value":0.8909333388010661}]],"position":{"x":2698.680693869229,"y":677.2826514150481},"width":200,"twoColumn":false},{"type":"Clamp","id":"node_1609669103283382","name":"Clamp","options":[],"state":{},"interfaces":[["Value",{"id":"ni_1609669103283383","value":1}],["Minimum",{"id":"ni_1609669103283384","value":0}],["Maximum",{"id":"ni_1609669103283385","value":1}],["Result",{"id":"ni_1609669103283386","value":1}]],"position":{"x":1906.9547063287514,"y":873.3780841065194},"width":200,"twoColumn":false},{"type":"RemoteSignal","id":"node_1609691269408259","name":"Pressure Sensor","options":[["Signal","Device1@ws://127.0.0.1:12355///pressure"]],"state":{},"interfaces":[["Value",{"id":"ni_1609691269408260","value":0}]],"position":{"x":37.395720413929226,"y":269.2812883131203},"width":200,"twoColumn":false},{"type":"Vibrator","id":"node_1609691297895267","name":"Vibrator","options":[["Vibrator","Device1@ws://127.0.0.1:12355///vibrator0"]],"state":{},"interfaces":[["Value",{"id":"ni_1609691297895268","value":0.8909333388010661}]],"position":{"x":3007.417242602179,"y":647.5023795265034},"width":200,"twoColumn":false}],"connections":[{"id":"1609691262860220","from":"ni_160961624872422","to":"ni_160961626177128"},{"id":"1609691262861222","from":"ni_16096162179729","to":"ni_160961626177127"},{"id":"1609691262861226","from":"ni_160961626177129","to":"ni_1609668644886267"},{"id":"1609691262862228","from":"ni_1609668644886269","to":"ni_1609668691022283"},{"id":"1609691262862230","from":"ni_1609668644886269","to":"ni_1609668734861310"},{"id":"1609691262862232","from":"ni_1609668732468307","to":"ni_1609668734860309"},{"id":"1609691262862234","from":"ni_1609668732468307","to":"ni_1609668778788321"},{"id":"1609691262862236","from":"ni_1609668734861311","to":"ni_1609668778788322"},{"id":"1609691262862238","from":"ni_1609668778788323","to":"ni_1609668800866334"},{"id":"1609691262862240","from":"ni_1609668800867336","to":"ni_1609668691022285"},{"id":"1609691262863242","from":"ni_1609668691022286","to":"ni_1609668989934349"},{"id":"1609691262863244","from":"ni_1609668986143346","to":"ni_1609668989934348"},{"id":"1609691262864246","from":"ni_1609668986143346","to":"ni_1609669021941358"},{"id":"1609691262864248","from":"ni_1609668989934350","to":"ni_1609669021941359"},{"id":"1609691262864250","from":"ni_1609669021941360","to":"ni_1609669037997368"},{"id":"1609691262864252","from":"ni_1609669037997370","to":"ni_1609669050149375"},{"id":"1609691262864254","from":"ni_1609668691022286","to":"ni_1609669103283383"},{"id":"1609691262864256","from":"ni_1609669103283386","to":"ni_1609669050149377"},{"id":"1609691271817263","from":"ni_1609691269408260","to":"ni_160961624872321"},{"id":"1609691290400266","from":"ni_1609691269408260","to":"ni_1609668644886268"},{"id":"1609691300467271","from":"ni_1609669050149378","to":"ni_1609691297895268"}],"panning":{"x":-2519.6863253710776,"y":-194.21259768074165},"scaling":0.9000864708192956}}]},"components":[{"origin":"core","name":"Constant","inputPorts":[],"outputPorts":[]},{"origin":"core","name":"Average","inputPorts":[],"outputPorts":[]},{"origin":"core","name":"Calculation","inputPorts":[],"outputPorts":[]},{"origin":"core","name":"Conditional","inputPorts":[],"outputPorts":[]},{"origin":"core","name":"StateMachine","inputPorts":[],"outputPorts":[]},{"origin":"core","name":"Time","inputPorts":[],"outputPorts":[]},{"origin":"core","name":"Memory","inputPorts":[],"outputPorts":[]},{"origin":"core","name":"Clamp","inputPorts":[],"outputPorts":[]},{"origin":"core","name":"RemoteSignal","inputPorts":[],"outputPorts":[]},{"origin":"core","name":"Vibrator","inputPorts":[],"outputPorts":[]}]} -------------------------------------------------------------------------------- /examples/simple-vibrator.json: -------------------------------------------------------------------------------- 1 | {"identifier":"maidkun/buttplug-editor/project","version":1,"workspace":{"boards":[{"state":{"nodes":[{"type":"WaveGenerator","id":"node_160967437089010","name":"Wave Generator","options":[["Wave Type",1],["Only Positive Wave",true]],"state":{},"interfaces":[["Time",{"id":"ni_160967437089111","value":0}],["Amplitude",{"id":"ni_160967437089112","value":0.5}],["Frequency",{"id":"ni_160967437089113","value":0.2}],["Output",{"id":"ni_160967437089114","value":0.01217908401155321}]],"position":{"x":32,"y":308},"width":200,"twoColumn":false},{"type":"Vibrator","id":"node_160967437589015","name":"Vibrator","options":[["Vibrator","Device1@ws://127.0.0.1:12355///vibrator0"]],"state":{},"interfaces":[["Value",{"id":"ni_160967437589016","value":0.01217908401155321}]],"position":{"x":437,"y":425},"width":200,"twoColumn":false},{"type":"VisualizeWave","id":"node_160967437817917","name":"Wave Visualizer","options":[["Result",{"value":0.01217908401155321,"time":53.926000118255615}]],"state":{},"interfaces":[["Value",{"id":"ni_160967437817918","value":0.01217908401155321}]],"position":{"x":438,"y":248},"width":200,"twoColumn":false}],"connections":[{"id":"1609693046727373","from":"ni_160967437089114","to":"ni_160967437589016"},{"id":"1609693046728375","from":"ni_160967437089114","to":"ni_160967437817918"}],"panning":{"x":72,"y":-20},"scaling":1}}]},"components":[{"origin":"core","name":"WaveGenerator","inputPorts":[],"outputPorts":[]},{"origin":"core","name":"Vibrator","inputPorts":[],"outputPorts":[]},{"origin":"core","name":"VisualizeWave","inputPorts":[],"outputPorts":[]}]} -------------------------------------------------------------------------------- /examples/wave-types.json: -------------------------------------------------------------------------------- 1 | {"identifier":"maidkun/buttplug-editor/project","version":1,"workspace":{"boards":[{"state":{"nodes":[{"type":"WaveGenerator","id":"node_16096700313500","name":"Wave Generator","options":[["Wave Type",1],["Only Positive Wave",false]],"state":{},"interfaces":[["Time",{"id":"ni_16096700313521","value":0}],["Amplitude",{"id":"ni_16096700313522","value":1}],["Frequency",{"id":"ni_16096700313523","value":0.1}],["Output",{"id":"ni_16096700313524","value":-0.9458816441080752}]],"position":{"x":207,"y":70},"width":200,"twoColumn":false},{"type":"WaveGenerator","id":"node_16096700338945","name":"Wave Generator","options":[["Wave Type",2],["Only Positive Wave",false]],"state":{},"interfaces":[["Time",{"id":"ni_16096700338946","value":0}],["Amplitude",{"id":"ni_16096700338947","value":1}],["Frequency",{"id":"ni_16096700338948","value":0.1}],["Output",{"id":"ni_16096700338949","value":-1}]],"position":{"x":206,"y":407},"width":200,"twoColumn":false},{"type":"WaveGenerator","id":"node_160967003815810","name":"Wave Generator","options":[["Wave Type",3],["Only Positive Wave",false]],"state":{},"interfaces":[["Time",{"id":"ni_160967003815911","value":0}],["Amplitude",{"id":"ni_160967003815912","value":1}],["Frequency",{"id":"ni_160967003815913","value":0.1}],["Output",{"id":"ni_160967003815914","value":0.6129070299794879}]],"position":{"x":697,"y":64},"width":200,"twoColumn":false},{"type":"WaveGenerator","id":"node_160967004093415","name":"Wave Generator","options":[["Wave Type",4],["Only Positive Wave",false]],"state":{},"interfaces":[["Time",{"id":"ni_160967004093416","value":0}],["Amplitude",{"id":"ni_160967004093417","value":1}],["Frequency",{"id":"ni_160967004093418","value":0.1}],["Output",{"id":"ni_160967004093419","value":-0.46292480045736734}]],"position":{"x":702,"y":412},"width":200,"twoColumn":false},{"type":"VisualizeWave","id":"node_160967004347820","name":"Wave Visualizer","options":[["Result",{"value":-0.9458816441080752,"time":6.973999977111816}]],"state":{},"interfaces":[["Value",{"id":"ni_160967004347821","value":-0.9458816441080752}]],"position":{"x":435,"y":105},"width":200,"twoColumn":false},{"type":"VisualizeWave","id":"node_160967005082925","name":"Wave Visualizer","options":[["Result",{"value":0.6129070299794879,"time":6.973999977111816}]],"state":{},"interfaces":[["Value",{"id":"ni_160967005082926","value":0.6129070299794879}]],"position":{"x":921,"y":85},"width":200,"twoColumn":false},{"type":"VisualizeWave","id":"node_160967005557330","name":"Wave Visualizer","options":[["Result",{"value":-1,"time":6.973999977111816}]],"state":{},"interfaces":[["Value",{"id":"ni_160967005557431","value":-1}]],"position":{"x":433,"y":441},"width":200,"twoColumn":false},{"type":"VisualizeWave","id":"node_160967005992535","name":"Wave Visualizer","options":[["Result",{"value":-0.46292480045736734,"time":6.973999977111816}]],"state":{},"interfaces":[["Value",{"id":"ni_160967005992536","value":-0.46292480045736734}]],"position":{"x":927,"y":434},"width":200,"twoColumn":false}],"connections":[{"id":"160967009250569","from":"ni_16096700313524","to":"ni_160967004347821"},{"id":"160967009250571","from":"ni_160967003815914","to":"ni_160967005082926"},{"id":"160967009250573","from":"ni_16096700338949","to":"ni_160967005557431"},{"id":"160967009250575","from":"ni_160967004093419","to":"ni_160967005992536"}],"panning":{"x":-33,"y":76},"scaling":1}}]},"components":[{"origin":"core","name":"WaveGenerator","inputPorts":[],"outputPorts":[]},{"origin":"core","name":"VisualizeWave","inputPorts":[],"outputPorts":[]}]} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "buttplug-editor", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "baklavajs": "^1.8.2", 12 | "buttplug": "^1.0.0", 13 | "core-js": "^3.6.5", 14 | "protobufjs": "^6.10.2", 15 | "v-click-outside": "^3.1.2", 16 | "vue": "^2.6.11", 17 | "vue-class-component": "^7.2.3", 18 | "vue-drag-drop": "^1.1.4", 19 | "vue-property-decorator": "^8.4.2" 20 | }, 21 | "devDependencies": { 22 | "@typescript-eslint/eslint-plugin": "^2.33.0", 23 | "@typescript-eslint/parser": "^2.33.0", 24 | "@vue/cli-plugin-babel": "~4.5.0", 25 | "@vue/cli-plugin-eslint": "~4.5.0", 26 | "@vue/cli-plugin-typescript": "~4.5.0", 27 | "@vue/cli-service": "~4.5.0", 28 | "@vue/eslint-config-typescript": "^5.0.2", 29 | "eslint": "^6.7.2", 30 | "eslint-plugin-vue": "^6.2.2", 31 | "sass": "^1.26.5", 32 | "sass-loader": "^8.0.2", 33 | "typescript": "~3.9.3", 34 | "vue-template-compiler": "^2.6.11" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaidKun/buttplug-editor/6fca47d92db3940b72505c702e60e199b78ff6c1/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Buttplug Interactive Editor 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 42 | 43 | 68 | -------------------------------------------------------------------------------- /src/components/NodeEditor.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 97 | -------------------------------------------------------------------------------- /src/components/connection/ConnectionIndicator.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 67 | 68 | -------------------------------------------------------------------------------- /src/components/connection/ConnectionListItem.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 48 | 49 | 69 | -------------------------------------------------------------------------------- /src/components/connection/ConnectionListWindow.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 68 | 69 | 90 | -------------------------------------------------------------------------------- /src/components/option/DeviceFeatureOption.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 122 | 123 | 125 | -------------------------------------------------------------------------------- /src/components/option/DeviceFeatureOptionValue.ts: -------------------------------------------------------------------------------- 1 | import Device, { DevicePort } from '@/device/Device'; 2 | 3 | export default class DeviceFeatureOptionValue { 4 | private _device: Device; 5 | private _port: DevicePort; 6 | 7 | constructor(device: Device, port: DevicePort) { 8 | this._device = device; 9 | this._port = port; 10 | } 11 | 12 | getHash() { 13 | return `${this._device.id}//${this._port.id}`; 14 | } 15 | 16 | getText() { 17 | return `${this._device.name}: ${this._port.name}`; 18 | } 19 | 20 | setInputValue(value: number) { 21 | this._device.setInputValue(this._port, value); 22 | } 23 | 24 | getSensorValue() { 25 | const data = this._device.getSensorValues().find(data => data.name === this._port.name); 26 | if (!data) { 27 | return 0; 28 | } 29 | 30 | return data.value; 31 | } 32 | } -------------------------------------------------------------------------------- /src/components/option/VisualizeOption.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 76 | 77 | 89 | -------------------------------------------------------------------------------- /src/components/ui/Button.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | 32 | -------------------------------------------------------------------------------- /src/components/ui/Popup.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 33 | 34 | 70 | -------------------------------------------------------------------------------- /src/components/workspace/ComponentBrowser.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 32 | 33 | 41 | -------------------------------------------------------------------------------- /src/components/workspace/ComponentBrowserList.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 64 | 65 | 112 | -------------------------------------------------------------------------------- /src/components/workspace/ComponentCategoryBrowser.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 33 | 34 | 73 | -------------------------------------------------------------------------------- /src/components/workspace/ProjectProvider.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | -------------------------------------------------------------------------------- /src/components/workspace/Workspace.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 55 | 56 | 75 | -------------------------------------------------------------------------------- /src/components/workspace/WorkspaceFooter.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | 24 | -------------------------------------------------------------------------------- /src/components/workspace/WorkspaceMenu.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 70 | 71 | -------------------------------------------------------------------------------- /src/connection/ButtplugIoConnection.ts: -------------------------------------------------------------------------------- 1 | import Connection from './Connection'; 2 | import * as Buttplug from 'buttplug'; 3 | import AsyncCache from '@/utils/AsyncCache'; 4 | import ButtplugIoDevice from '@/device/ButtplugIoDevice'; 5 | import Device from '@/device/Device'; 6 | 7 | export default class ButtplugIoConnection extends Connection { 8 | static initialize = new AsyncCache(async () => { 9 | await Buttplug.buttplugInit(); 10 | console.log('Buttplug initialized.'); 11 | }); 12 | 13 | private connector: Buttplug.ButtplugWebsocketConnectorOptions; 14 | private client: Buttplug.ButtplugClient; 15 | private devices: {[index: number]: ButtplugIoDevice} = {} 16 | 17 | constructor(address='127.0.0.1', port=12345) { 18 | super(); 19 | 20 | this.connector = new Buttplug.ButtplugWebsocketConnectorOptions(); 21 | this.connector.Address = `ws://${address}:${port}/`; 22 | 23 | this.client = new Buttplug.ButtplugClient("Buttplug Interactive Editor"); 24 | 25 | this.client.addListener('deviceadded', this.addDevice); 26 | this.client.addListener('deviceremoved', this.removeDevice); 27 | this.client.addListener("disconnect", this.disconnected); 28 | } 29 | 30 | getDevices(): Device[] { 31 | return Object.values(this.devices); 32 | } 33 | 34 | triggerDeviceEventsAgain() { 35 | for (const detail of Object.values(this.devices)) { 36 | this.dispatchEvent(new CustomEvent('deviceadded', {detail})) 37 | } 38 | } 39 | 40 | disconnected=() => { 41 | for (const device of Object.values(this.devices)) { 42 | this.removeDevice(device.device); 43 | } 44 | 45 | this.devices = {} 46 | } 47 | 48 | addDevice=(device: Buttplug.ButtplugClientDevice) => { 49 | if (this.devices[device.Index]) { 50 | return; 51 | } 52 | 53 | this.devices[device.Index] = new ButtplugIoDevice(`Device${device.Index}@${this.connector.Address}`, device); 54 | this.dispatchEvent(new CustomEvent('deviceadded', {detail: this.devices[device.Index]})); 55 | } 56 | 57 | removeDevice=(device: Buttplug.ButtplugClientDevice) => { 58 | if (!this.devices[device.Index]) { 59 | return; 60 | } 61 | 62 | this.dispatchEvent(new CustomEvent('deviceremoved', {detail: this.devices[device.Index]})); 63 | delete this.devices[device.Index]; 64 | } 65 | 66 | async connect() { 67 | await this.client.connect(this.connector); 68 | await this.client.startScanning(); 69 | } 70 | 71 | async disconnect() { 72 | return this.client.disconnect(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/connection/Connection.ts: -------------------------------------------------------------------------------- 1 | import Device from '@/device/Device'; 2 | 3 | export default abstract class Connection extends EventTarget { 4 | abstract connect(): Promise; 5 | abstract disconnect(): Promise; 6 | abstract triggerDeviceEventsAgain(): void; 7 | abstract getDevices(): Device[]; 8 | } -------------------------------------------------------------------------------- /src/connection/ConnectionConfiguration.ts: -------------------------------------------------------------------------------- 1 | export default interface ConnectionConfiguration { 2 | protocol: 'buttplug' | 'dev'; 3 | address: string; 4 | port: number; 5 | } -------------------------------------------------------------------------------- /src/connection/ConnectionManager.ts: -------------------------------------------------------------------------------- 1 | import AsyncCache from '@/utils/AsyncCache'; 2 | import ButtplugIoConnection from './ButtplugIoConnection'; 3 | import Connection from './Connection'; 4 | import ConnectionConfiguration from './ConnectionConfiguration'; 5 | import DevIoConnection from './DevIoConnection'; 6 | 7 | export type ConnectionManagerState = 'disconnected' | 'connecting' | 'connected'; 8 | 9 | export interface ConnectionConfigurationStatus { 10 | configuration: ConnectionConfiguration; 11 | status: ConnectionManagerState; 12 | connect?: AsyncCache; 13 | connection?: Connection; 14 | } 15 | 16 | export default class ConnectionManager extends EventTarget { 17 | private _state: ConnectionManagerState = 'disconnected'; 18 | private _connections: Connection[] = []; 19 | private _configurations: ConnectionConfigurationStatus[] = []; 20 | 21 | addDefaultConfigurations() { 22 | console.log((new Error)); 23 | this.addConfiguration({protocol: 'buttplug', address: '127.0.0.1', port: 12345}); 24 | this.addConfiguration({protocol: 'buttplug', address: '127.0.0.1', port: 12346}); 25 | this.addConfiguration({protocol: 'dev', address: '127.0.0.1', port: 12355}); 26 | } 27 | 28 | addConfiguration(configuration: ConnectionConfiguration) { 29 | this._configurations.push({status: 'disconnected', configuration}); 30 | } 31 | 32 | async findConnections() { 33 | this.state = 'connecting'; 34 | 35 | await Promise.all(this._configurations.map(configuration => this.connect(configuration))); 36 | } 37 | 38 | disconnect(status: ConnectionConfigurationStatus) { 39 | if (!status.connection) { 40 | return; 41 | } 42 | 43 | if (!status.connect) { 44 | return; 45 | } 46 | 47 | this._connections = this._connections.filter(item => item !== status.connection); 48 | 49 | const connection = status.connection; 50 | status.status = 'disconnected'; 51 | status.connect = undefined; 52 | status.connection = undefined; 53 | connection.disconnect(); 54 | 55 | if (this._connections.length === 0) { 56 | this.state = 'disconnected'; 57 | } else { 58 | this.state = 'connected'; 59 | } 60 | this.dispatchEvent(new CustomEvent('changed')); 61 | } 62 | 63 | async connect(status: ConnectionConfigurationStatus) { 64 | if (status.connection) { 65 | return status.connection; 66 | } 67 | 68 | const connection = await this.createConnection(status.configuration); 69 | 70 | if (!status.connect) { 71 | status.connect = new AsyncCache(async () => { 72 | status.status = 'connecting'; 73 | this.dispatchEvent(new CustomEvent('changed')); 74 | 75 | this.connectEvents(connection); 76 | await connection.connect(); 77 | status.connection = connection; 78 | this._connections.push(connection); 79 | 80 | status.status = 'connected'; 81 | 82 | if (this._connections.length === 0) { 83 | this.state = 'disconnected'; 84 | } else { 85 | this.state = 'connected'; 86 | } 87 | 88 | this.dispatchEvent(new CustomEvent('changed')); 89 | }) 90 | } 91 | 92 | try { 93 | await status.connect.invoke(); 94 | } catch(error) { 95 | status.status = 'disconnected'; 96 | status.connect = undefined; 97 | this.dispatchEvent(new CustomEvent('changed')); 98 | return null; 99 | } 100 | } 101 | 102 | async createConnection(config: ConnectionConfiguration) { 103 | switch (config.protocol) { 104 | case 'buttplug': 105 | await ButtplugIoConnection.initialize.invoke(); 106 | return new ButtplugIoConnection(config.address, config.port); 107 | 108 | case 'dev': 109 | return new DevIoConnection(config.address, config.port); 110 | 111 | default: 112 | throw new Error(`Invalid protocol: ${config.protocol}`) 113 | } 114 | } 115 | 116 | private connectEvents(connection: Connection) { 117 | connection.addEventListener('deviceadded', (e) => { 118 | this.dispatchEvent(new CustomEvent('deviceadded', {detail: (e as CustomEvent).detail})) 119 | }); 120 | connection.addEventListener('deviceremoved', (e) => { 121 | this.dispatchEvent(new CustomEvent('deviceremoved', {detail: (e as CustomEvent).detail})) 122 | }); 123 | } 124 | 125 | triggerDeviceEventsAgain() { 126 | for (const connection of this._connections) { 127 | connection.triggerDeviceEventsAgain(); 128 | } 129 | } 130 | 131 | get connections() { 132 | return this._connections; 133 | } 134 | 135 | get configurations() { 136 | return this._configurations; 137 | } 138 | 139 | get state() { 140 | return this._state; 141 | } 142 | 143 | set state(state: ConnectionManagerState) { 144 | this._state = state; 145 | this.dispatchEvent(new CustomEvent('changed')); 146 | } 147 | } -------------------------------------------------------------------------------- /src/connection/DevIo.ts: -------------------------------------------------------------------------------- 1 | export interface DevIoDeviceMessage { 2 | FeatureCount?: number; 3 | Description?: string[]; 4 | Identifier?: string[]; 5 | } 6 | 7 | export type DevIoVibrateDeviceMessage = DevIoDeviceMessage; 8 | 9 | export type DevIoSensorReadMessage = DevIoDeviceMessage; 10 | 11 | export interface DevIoDeviceInfo { 12 | DeviceName: string; 13 | DeviceIndex: number; 14 | DeviceMessages: { 15 | StopDeviceCmd?: DevIoDeviceMessage; 16 | SensorReadCmd?: DevIoSensorReadMessage; 17 | VibrateCmd?: DevIoVibrateDeviceMessage; 18 | }; 19 | } 20 | 21 | 22 | export interface DevIoMessage { 23 | Id: number; 24 | } 25 | 26 | export interface DevIoRequestServerInfoMessage extends DevIoMessage { 27 | ClientName: string; 28 | } 29 | 30 | export type DevIoRequestDeviceList = DevIoMessage; 31 | 32 | export interface DevIoServerInfoMessage extends DevIoMessage { 33 | ServerName: string; 34 | MessageVersion: number; 35 | MajorVersion: number; 36 | BuildVersion: number; 37 | MinorVersion: number; 38 | MaxPingTime: number; 39 | } 40 | 41 | export interface DevIoDeviceListMessage extends DevIoMessage { 42 | Devices: DevIoDeviceInfo[]; 43 | } 44 | 45 | export interface DevIoVibrateCmdMsg extends DevIoMessage { 46 | DeviceIndex: number; 47 | Speeds: Array<{Index: number, Speed: number}>; 48 | } 49 | 50 | export interface DevIoSensorSubscribeCmdMsg extends DevIoMessage { 51 | DeviceIndex: number; 52 | Sensor: number; 53 | } 54 | 55 | export interface DevIoSensorUpdateMessage extends DevIoMessage { 56 | DeviceIndex: number; 57 | Sensor: number; 58 | Value: number; 59 | } 60 | 61 | export interface DevIoClientMessage { 62 | RequestServerInfo?: DevIoRequestServerInfoMessage; 63 | RequestDeviceList?: DevIoRequestDeviceList; 64 | VibrateCmd?: DevIoVibrateCmdMsg; 65 | SensorSubscribeCmd?: DevIoSensorSubscribeCmdMsg; 66 | } 67 | 68 | export interface DevIoServerMessage { 69 | ServerInfo?: DevIoServerInfoMessage; 70 | DeviceList?: DevIoDeviceListMessage; 71 | Ok?: DevIoMessage; 72 | SensorUpdate?: DevIoSensorUpdateMessage; 73 | } 74 | 75 | export interface DevIoMessageWaitItem { 76 | resolve: (item: DevIoMessage) => unknown; 77 | reject: (error: Error) => unknown; 78 | } -------------------------------------------------------------------------------- /src/connection/DevIoConnection.ts: -------------------------------------------------------------------------------- 1 | import Device from '@/device/Device'; 2 | import DevIoDevice from '@/device/DevIoDevice'; 3 | import Connection from './Connection'; 4 | import * as DevIo from './DevIo'; 5 | 6 | export default class DevIoConnection extends Connection { 7 | private url: string; 8 | private socket?: WebSocket; 9 | nextId = 1; 10 | private devices: {[index: number]: DevIoDevice} = {} 11 | 12 | private messageResponseMap: {[id: number]: DevIo.DevIoMessageWaitItem} = {}; 13 | 14 | constructor(address='127.0.0.1', port=12345) { 15 | super(); 16 | 17 | this.url = `ws://${address}:${port}/`; 18 | } 19 | 20 | getDevices(): Device[] { 21 | return Object.values(this.devices); 22 | } 23 | 24 | send(message: DevIo.DevIoClientMessage) { 25 | if (!this.socket) { 26 | throw new Error(`Can not send message when socket is not connected`); 27 | } 28 | this.socket.send(JSON.stringify([message])); 29 | } 30 | 31 | async sendAndWait(message: DevIo.DevIoClientMessage): Promise { 32 | const key = Object.keys(message)[0] as keyof DevIo.DevIoClientMessage; 33 | if (!key) { 34 | throw new Error('message must contain one message'); 35 | } 36 | const id = (message[key] as DevIo.DevIoMessage).Id; 37 | 38 | return new Promise((resolve, reject) => { 39 | this.messageResponseMap[id] = {resolve: resolve as (item: DevIo.DevIoMessage) => unknown, reject}; 40 | this.send(message); 41 | }) 42 | } 43 | 44 | loadDeviceList(message: DevIo.DevIoDeviceListMessage) { 45 | console.log(`Receives device list with ${message.Devices.length} devices`); 46 | 47 | for (const device of message.Devices) { 48 | this.addDevice(device); 49 | } 50 | } 51 | 52 | updateSensor(message: DevIo.DevIoSensorUpdateMessage) { 53 | const device = this.devices[message.DeviceIndex]; 54 | device.setSensorValue(message.Sensor, message.Value); 55 | } 56 | 57 | addDevice(device: DevIo.DevIoDeviceInfo) { 58 | if (this.devices[device.DeviceIndex]) { 59 | return; 60 | } 61 | 62 | this.devices[device.DeviceIndex] = new DevIoDevice(`Device${device.DeviceIndex}@${this.url}`, this, device); 63 | this.dispatchEvent(new CustomEvent('deviceadded', {detail: this.devices[device.DeviceIndex]})); 64 | } 65 | 66 | triggerDeviceEventsAgain() { 67 | for (const detail of Object.values(this.devices)) { 68 | this.dispatchEvent(new CustomEvent('deviceadded', {detail})) 69 | } 70 | } 71 | 72 | onMessage(message: DevIo.DevIoServerMessage) { 73 | const key = Object.keys(message)[0] as keyof DevIo.DevIoServerMessage; 74 | if (!key) { 75 | console.error('Server sent empty message'); 76 | return; 77 | } 78 | 79 | const id = (message[key] as DevIo.DevIoMessage).Id; 80 | if (this.messageResponseMap[id]) { 81 | this.messageResponseMap[id].resolve(message[key] as DevIo.DevIoMessage); 82 | return; 83 | } 84 | 85 | switch (key) { 86 | case 'Ok': 87 | break; 88 | case 'DeviceList': 89 | this.loadDeviceList(message[key] as DevIo.DevIoDeviceListMessage) 90 | break; 91 | case 'SensorUpdate': 92 | this.updateSensor(message[key] as DevIo.DevIoSensorUpdateMessage); 93 | break; 94 | 95 | default: 96 | console.error(`Unknown server message: ${key}`); 97 | } 98 | } 99 | 100 | disconnected=() => { 101 | for (const device of Object.values(this.devices)) { 102 | this.removeDevice(device.device); 103 | } 104 | 105 | this.devices = {} 106 | } 107 | 108 | removeDevice=(device: DevIo.DevIoDeviceInfo) => { 109 | if (!this.devices[device.DeviceIndex]) { 110 | return; 111 | } 112 | 113 | this.dispatchEvent(new CustomEvent('deviceremoved', {detail: this.devices[device.DeviceIndex]})); 114 | delete this.devices[device.DeviceIndex]; 115 | } 116 | 117 | connect() { 118 | return new Promise((resolve) => { 119 | let isConnected = false; 120 | 121 | this.socket = new WebSocket(this.url); 122 | 123 | this.socket.onopen = async () => { 124 | isConnected = true; 125 | console.log(`Connected to server. Wait for server info.`); 126 | const serverInfo = await this.sendAndWait({RequestServerInfo: {Id: this.nextId++, ClientName: 'Buttplug Editor'}}); 127 | console.log(`Server name is: ${serverInfo.ServerName}`); 128 | this.send({RequestDeviceList: {Id: this.nextId++}}); 129 | resolve(); 130 | } 131 | 132 | this.socket.onmessage = (data) => { 133 | for (const row of JSON.parse(data.data)) { 134 | this.onMessage(row); 135 | } 136 | } 137 | 138 | this.socket.onclose = () => { 139 | this.disconnected(); 140 | } 141 | 142 | this.socket.onerror = (err) => { 143 | if (isConnected) { 144 | console.error(err); 145 | } else { 146 | throw err; 147 | } 148 | } 149 | }); 150 | } 151 | 152 | async disconnect() { 153 | if (!this.socket) { 154 | return; 155 | } 156 | 157 | this.socket.close(); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/device/ButtplugIoDevice.ts: -------------------------------------------------------------------------------- 1 | import * as Buttplug from 'buttplug'; 2 | import { ButtplugDeviceMessageType } from 'buttplug'; 3 | import Device, { DevicePort } from './Device'; 4 | 5 | export interface ButtplugIoPort extends DevicePort { 6 | buttplugType: 'vibrate'; 7 | buttplugIndex: number; 8 | } 9 | 10 | export default class ButtplugIoDevice extends Device { 11 | public readonly device: Buttplug.ButtplugClientDevice; 12 | 13 | protected _inputPorts: ButtplugIoPort[]; 14 | protected vibrators: number[] = []; 15 | protected _updatedPorts: ButtplugIoPort[] = [] 16 | 17 | constructor(id: string, device: Buttplug.ButtplugClientDevice) { 18 | super(id, device.Name); 19 | 20 | this.device = device; 21 | this.description = `Buttplug device ${device.Index}`; 22 | 23 | this._inputPorts = []; 24 | 25 | this.addVibrate(); 26 | } 27 | 28 | private addVibrate() { 29 | const attributes = this.device.messageAttributes(ButtplugDeviceMessageType.VibrateCmd); 30 | if (!attributes) { 31 | return; 32 | } 33 | for (let index=0; index<(attributes.featureCount || 1); index++) { 34 | this._inputPorts.push({ 35 | type: 'number', 36 | tags: ['vibrate'], 37 | id: `vibrate${index}`, 38 | name: `Motor ${index + 1}`, 39 | buttplugType: 'vibrate', 40 | buttplugIndex: index 41 | }) 42 | } 43 | } 44 | 45 | inputPorts() { 46 | return this._inputPorts; 47 | } 48 | 49 | outputPorts() { 50 | return []; 51 | } 52 | 53 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 54 | setInputValue(port: ButtplugIoPort, value: number) { 55 | if (this.vibrators[port.buttplugIndex] === value) { 56 | return; 57 | } 58 | 59 | this.vibrators[port.buttplugIndex] = value; 60 | this._updatedPorts.push(port); 61 | this.triggerUpdate(); 62 | } 63 | 64 | sendUpdates() { 65 | const ports = this._updatedPorts; 66 | this._updatedPorts = []; 67 | 68 | const vibrates = ports.filter(port => port.buttplugType === 'vibrate').map(port => new Buttplug.VibrationCmd(port.buttplugIndex, Math.min(1, Math.max(0, this.vibrators[port.buttplugIndex])))); 69 | if (vibrates.length) { 70 | this.device.vibrate(vibrates); 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /src/device/DevIoDevice.ts: -------------------------------------------------------------------------------- 1 | import Device, { DevicePort } from './Device'; 2 | import * as DevIo from '../connection/DevIo'; 3 | import DevIoConnection from '@/connection/DevIoConnection'; 4 | 5 | export interface DevIoPort extends DevicePort { 6 | buttplugType: 'vibrate' | 'sensor'; 7 | buttplugIndex: number; 8 | } 9 | 10 | export default class DevIoDevice extends Device { 11 | public readonly device: DevIo.DevIoDeviceInfo; 12 | protected _inputPorts: DevIoPort[]; 13 | protected _outputPorts: DevIoPort[]; 14 | protected vibrators: number[] = []; 15 | protected _updatedPorts: DevIoPort[] = [] 16 | protected socket: DevIoConnection; 17 | protected sensors: number[] = []; 18 | 19 | constructor(id: string, socket: DevIoConnection, device: DevIo.DevIoDeviceInfo) { 20 | super(id, device.DeviceName); 21 | 22 | this.device = device; 23 | this.description = `DevIo device ${device.DeviceIndex}`; 24 | 25 | this._inputPorts = []; 26 | this._outputPorts = []; 27 | this.socket = socket; 28 | 29 | this.addVibrate(); 30 | this.addSensors(); 31 | } 32 | 33 | private addVibrate() { 34 | const attributes = this.device.DeviceMessages.VibrateCmd; 35 | if (!attributes) { 36 | return; 37 | } 38 | 39 | for (let index=0; index<(attributes.FeatureCount || 1); index++) { 40 | this._inputPorts.push({ 41 | type: 'number', 42 | tags: ['vibrate'], 43 | id: attributes.Identifier ? attributes.Identifier[index] : `vibrate${index}`, 44 | name: attributes.Description ? attributes.Description[index] : `Motor ${index + 1}`, 45 | buttplugType: 'vibrate', 46 | buttplugIndex: index 47 | }) 48 | } 49 | } 50 | 51 | private addSensors() { 52 | const attributes = this.device.DeviceMessages.SensorReadCmd; 53 | if (!attributes) { 54 | return; 55 | } 56 | 57 | for (let index=0; index<(attributes.FeatureCount || 1); index++) { 58 | this._outputPorts.push({ 59 | type: 'number', 60 | tags: ['sensor'], 61 | id: attributes.Identifier ? attributes.Identifier[index] : `sensor${index}`, 62 | name: attributes.Description ? attributes.Description[index] : `Sensor ${index + 1}`, 63 | buttplugType: 'sensor', 64 | buttplugIndex: index 65 | }) 66 | 67 | this.socket.send({SensorSubscribeCmd: {Id: this.socket.nextId++, DeviceIndex: this.device.DeviceIndex, Sensor: index}}); 68 | this.sensors.push(0); 69 | } 70 | } 71 | 72 | getSensorValues() { 73 | return Object.keys(this.sensors).map(index => ({ 74 | name: this._outputPorts[parseInt(index)].name, 75 | value: this.sensors[parseInt(index)] 76 | })) 77 | } 78 | 79 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 80 | setSensorValue(sensor: number, value: number) { 81 | this.sensors[sensor] = value; 82 | } 83 | 84 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 85 | setInputValue(port: DevIoPort, value: number) { 86 | if (this.vibrators[port.buttplugIndex] === value) { 87 | return; 88 | } 89 | 90 | this.vibrators[port.buttplugIndex] = value; 91 | this._updatedPorts.push(port); 92 | this.triggerUpdate(); 93 | } 94 | 95 | sendUpdates() { 96 | const ports = this._updatedPorts; 97 | this._updatedPorts = []; 98 | 99 | const vibrates = ports.filter(port => port.buttplugType === 'vibrate').map(port => ({Index: port.buttplugIndex, Speed: this.vibrators[port.buttplugIndex]})); 100 | if (vibrates.length) { 101 | this.socket.send({VibrateCmd: {Id: this.socket.nextId++, DeviceIndex: this.device.DeviceIndex, Speeds: vibrates}}); 102 | } 103 | } 104 | 105 | sendVibrates(devices: DevIoPort[]) { 106 | //this.socket.send() 107 | console.log('SEND VIBRATE', devices); 108 | } 109 | 110 | inputPorts() { 111 | return this._inputPorts; 112 | } 113 | 114 | outputPorts() { 115 | return this._outputPorts; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/device/Device.ts: -------------------------------------------------------------------------------- 1 | export interface DevicePort { 2 | type: 'number'; 3 | id: string; 4 | name: string; 5 | tags: string[]; 6 | } 7 | 8 | export interface DeviceSignalData { 9 | name: string; 10 | value: number; 11 | } 12 | 13 | export default abstract class Device { 14 | public readonly id: string; 15 | public readonly name: string; 16 | protected _immediate?: NodeJS.Immediate; 17 | public description = ''; 18 | 19 | constructor(id: string, name: string) { 20 | this.id = id; 21 | this.name = name; 22 | } 23 | 24 | protected triggerUpdate() { 25 | if (this._immediate) { 26 | return; 27 | } 28 | 29 | this._immediate = setImmediate(() => { 30 | this._immediate = undefined; 31 | 32 | this.sendUpdates(); 33 | }); 34 | } 35 | 36 | abstract inputPorts(): Port[]; 37 | abstract outputPorts(): Port[]; 38 | 39 | sendUpdates() { 40 | // empty default call 41 | } 42 | 43 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 44 | setInputValue(port: Port, value: number) { 45 | // empty default call 46 | } 47 | 48 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 49 | getSensorValues(): DeviceSignalData[] { 50 | return []; 51 | } 52 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import { BaklavaVuePlugin } from "@baklavajs/plugin-renderer-vue"; 4 | import "@baklavajs/plugin-renderer-vue/dist/styles.css"; 5 | import "./style.scss"; 6 | 7 | Vue.config.productionTip = false 8 | Vue.use(BaklavaVuePlugin); 9 | 10 | new Vue({ 11 | render: h => h(App), 12 | }).$mount('#app') 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/nodes/Node.ts: -------------------------------------------------------------------------------- 1 | import { ProjectFileComponentInterface } from '@/project/ProjectFileInterface'; 2 | import RuntimeData from '@/runtime/RuntimeData'; 3 | import {Node as BaklavaNode} from "@baklavajs/core"; 4 | 5 | export default abstract class Node extends BaklavaNode { 6 | // Category 7 | static componentId = ""; 8 | static componentName = "Component"; 9 | static category = 'all'; 10 | static description = ''; 11 | 12 | name = (this.constructor as NodeConstructor).componentName; 13 | type = (this.constructor as NodeConstructor).componentId; 14 | 15 | // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function 16 | send(data: RuntimeData) {} 17 | 18 | //! Returns a hash if this component might be serialized 19 | hash(): string | null { 20 | const serialized = this.serializeType(); 21 | if (serialized === null) { 22 | return serialized; 23 | } 24 | 25 | return JSON.stringify(serialized); 26 | } 27 | 28 | // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function 29 | serializeType(): ProjectFileComponentInterface { 30 | return { 31 | origin: 'core', 32 | name: this.type, 33 | inputPorts: [], 34 | outputPorts: [] 35 | }; 36 | } 37 | } 38 | 39 | export interface NodeConstructor { 40 | new (): Node; 41 | category: string; 42 | componentId: string; 43 | componentName: string; 44 | description: string; 45 | } -------------------------------------------------------------------------------- /src/nodes/device/CustomDeviceNode.ts: -------------------------------------------------------------------------------- 1 | import Device from '@/device/Device'; 2 | import { ProjectFileComponentInterface } from '@/project/ProjectFileInterface'; 3 | import Node from '../Node'; 4 | 5 | export default class CustomDeviceNode extends Node { 6 | static category = "device"; 7 | public readonly device: Device; 8 | 9 | constructor(device: Device) { 10 | super(); 11 | 12 | this.device = device; 13 | 14 | for (const input of this.device.inputPorts()) { 15 | switch (input.type) { 16 | case 'number': 17 | this.addInputInterface(input.name, 'NumberOption', 0, {type: 'number'}) 18 | } 19 | } 20 | } 21 | 22 | send() { 23 | for (const input of this.device.inputPorts()) { 24 | this.device.setInputValue(input, this.getInterface(input.name).value); 25 | } 26 | } 27 | 28 | serializeType(): ProjectFileComponentInterface { 29 | return { 30 | origin: 'device', 31 | name: this.type, 32 | inputPorts: this.device.inputPorts().map(port => ({ 33 | id: port.id, 34 | name: port.name, 35 | type: port.type 36 | })), 37 | outputPorts: [] 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/nodes/device/CustomInputDeviceNode.ts: -------------------------------------------------------------------------------- 1 | import Device from '@/device/Device'; 2 | import { ProjectFileComponentInterface } from '@/project/ProjectFileInterface'; 3 | import Node from '../Node'; 4 | 5 | export default class CustomInputDeviceNode extends Node { 6 | static category = "device"; 7 | public readonly device: Device; 8 | 9 | constructor(device: Device) { 10 | super(); 11 | 12 | this.device = device; 13 | 14 | for (const output of this.device.outputPorts()) { 15 | switch (output.type) { 16 | case 'number': 17 | this.addOutputInterface(output.name, {type: 'number'}); 18 | break; 19 | } 20 | } 21 | } 22 | 23 | calculate() { 24 | for (const {name, value} of this.device.getSensorValues()) { 25 | this.getInterface(name).value = value; 26 | } 27 | } 28 | 29 | serializeType(): ProjectFileComponentInterface { 30 | return { 31 | origin: 'device', 32 | name: this.type, 33 | outputPorts: this.device.outputPorts().map(port => ({ 34 | id: port.id, 35 | name: port.name, 36 | type: port.type 37 | })), 38 | inputPorts: [] 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/nodes/device/Vibrator.ts: -------------------------------------------------------------------------------- 1 | import DeviceFeatureOptionValue from '@/components/option/DeviceFeatureOptionValue'; 2 | import Node from '../Node'; 3 | 4 | export default class Vibrator extends Node { 5 | static componentId = "Vibrator"; 6 | static componentName = "Vibrator"; 7 | static category = "device"; 8 | static description = "Connected to a remote vibrator" 9 | 10 | constructor() { 11 | super(); 12 | 13 | this.addOption("Vibrator", "DeviceFeatureOption", undefined, undefined, {allowedTags: ['vibrate']}); 14 | this.addInputInterface("Value", "NumberOption", 0, {type: "number"}); 15 | } 16 | 17 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 18 | load(state: any) { 19 | // TODO: Support loading selected device 20 | state.options = [] 21 | super.load(state); 22 | } 23 | 24 | save() { 25 | const state = super.save(); 26 | 27 | const signal = this.getOptionValue('Vibrator') as DeviceFeatureOptionValue | undefined; 28 | state.options = [['Vibrator', signal ? signal.getHash() : null]]; 29 | 30 | return state; 31 | } 32 | 33 | send() { 34 | const signal = this.getOptionValue('Vibrator') as DeviceFeatureOptionValue | undefined; 35 | if (!signal) { 36 | return; 37 | } 38 | 39 | signal.setInputValue(this.getInterface('Value').value); 40 | } 41 | } -------------------------------------------------------------------------------- /src/nodes/math/Average.ts: -------------------------------------------------------------------------------- 1 | import RuntimeData from '@/runtime/RuntimeData'; 2 | import Node from '../Node'; 3 | 4 | export default class Average extends Node { 5 | static componentId = "Average"; 6 | static componentName = "Average"; 7 | static category = "math"; 8 | static description = "Provides the average of an input value" 9 | 10 | private lastTime = 0; 11 | private lastTick = 0; 12 | private duration = 0; 13 | private probesPerSecond = 10; 14 | private tickDuration = 1; 15 | 16 | private records: number[] = [] 17 | private sum = 0; 18 | private maxSize = 0; 19 | private count = 0; 20 | private index = 0; 21 | 22 | constructor() { 23 | super(); 24 | 25 | this.addOption("Duration", "NumberOption", 10); 26 | this.addInputInterface("Value", "NumberOption", 0, {type: "number"}); 27 | this.addOutputInterface("Average", {type: "number"}); 28 | } 29 | 30 | getDuration() { 31 | return Math.max(0.1, this.getOptionValue('Duration')) 32 | } 33 | 34 | reset() { 35 | const duration = this.getDuration(); 36 | 37 | this.lastTime = 0; 38 | this.lastTick = 0; 39 | this.index = 0; 40 | this.count = 0; 41 | this.sum = 0; 42 | 43 | this.tickDuration = 1 / this.probesPerSecond; 44 | this.maxSize = duration * this.probesPerSecond; 45 | } 46 | 47 | calculate(data: RuntimeData) { 48 | const duration = this.getDuration(); 49 | 50 | if (data.time < this.lastTime 51 | || duration !== this.duration 52 | ) { 53 | this.reset(); 54 | this.duration = duration; 55 | } 56 | 57 | this.lastTick += data.time - this.lastTime; 58 | this.lastTime = data.time; 59 | 60 | const value = this.getInterface('Value').value; 61 | while (this.lastTick >= this.tickDuration) { 62 | this.addTick(value); 63 | this.lastTick -= this.tickDuration; 64 | } 65 | 66 | if (this.count === 0) { 67 | this.getInterface('Average').value = value; 68 | } else { 69 | this.getInterface('Average').value = this.sum / this.count; 70 | } 71 | } 72 | 73 | private addTick(value: number) { 74 | if (this.index < this.count) { 75 | this.sum -= this.records[this.index]; 76 | } 77 | 78 | this.records[this.index++] = value; 79 | this.sum += value; 80 | this.count = Math.max(this.count, this.index); 81 | this.index = this.index % this.maxSize; 82 | } 83 | } -------------------------------------------------------------------------------- /src/nodes/math/Calculation.ts: -------------------------------------------------------------------------------- 1 | import Node from '../Node'; 2 | 3 | enum CalculationType { 4 | Add = 1, 5 | Sub = 2, 6 | Mul = 3, 7 | Div = 4, 8 | Mod = 5 9 | } 10 | 11 | export default class Calculation extends Node { 12 | static componentId = "Calculation"; 13 | static componentName = "Calculation"; 14 | static category = "math"; 15 | static description = "Performs simple calculation" 16 | 17 | constructor() { 18 | super(); 19 | 20 | this.addOption('Operator', 'SelectOption', CalculationType.Add, undefined, {items: [ 21 | {value: CalculationType.Add, text: 'Add'}, 22 | {value: CalculationType.Sub, text: 'Subtract'}, 23 | {value: CalculationType.Mul, text: 'Multiply'}, 24 | {value: CalculationType.Div, text: 'Divide'}, 25 | {value: CalculationType.Mod, text: 'Modulo'} 26 | ]}) 27 | this.addInputInterface("Operand A", "NumberOption", 0, {type: "number"}); 28 | this.addInputInterface("Operand B", "NumberOption", 0, {type: "number"}); 29 | this.addOutputInterface("Result", {type: "number"}); 30 | } 31 | 32 | calculate() { 33 | const a = this.getInterface('Operand A').value; 34 | const b = this.getInterface('Operand B').value; 35 | const out = this.getInterface('Result'); 36 | 37 | switch (this.getOptionValue('Operator') as CalculationType) { 38 | case CalculationType.Add: out.value = a + b; break; 39 | case CalculationType.Sub: out.value = a - b; break; 40 | case CalculationType.Mul: out.value = a * b; break; 41 | 42 | case CalculationType.Div: 43 | if (b === 0) { 44 | out.value = Infinity; 45 | } else { 46 | out.value = a / b; 47 | } 48 | break; 49 | 50 | case CalculationType.Mod: 51 | if (b === 0) { 52 | out.value = Infinity; 53 | } else { 54 | out.value = a % b; 55 | } 56 | break; 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/nodes/math/Clamp.ts: -------------------------------------------------------------------------------- 1 | import Node from '../Node'; 2 | 3 | export default class Clamp extends Node { 4 | static componentId = "Clamp"; 5 | static componentName = "Clamp"; 6 | static category = "math"; 7 | static description = "Ensures that a value is in a range" 8 | 9 | constructor() { 10 | super(); 11 | 12 | this.addInputInterface("Value", "NumberOption", 0, {type: "number"}); 13 | this.addInputInterface("Minimum", "NumberOption", 0, {type: "number"}); 14 | this.addInputInterface("Maximum", "NumberOption", 1, {type: "number"}); 15 | this.addOutputInterface("Result", {type: "number"}); 16 | } 17 | 18 | calculate() { 19 | const value = this.getInterface('Value').value; 20 | const min = this.getInterface('Minimum').value; 21 | const max = this.getInterface('Maximum').value; 22 | 23 | this.getInterface('Result').value = Math.min(max, Math.max(min, value)); 24 | } 25 | } -------------------------------------------------------------------------------- /src/nodes/math/Conditional.ts: -------------------------------------------------------------------------------- 1 | import Node from '../Node'; 2 | 3 | enum CalculationType { 4 | Equal = 1, 5 | NotEqual = 2, 6 | LessThan = 3, 7 | LessThanOrEqual = 4, 8 | GreaterThan = 5, 9 | GreaterThanOrEqual = 6 10 | } 11 | 12 | export default class Conditional extends Node { 13 | static componentId = "Conditional"; 14 | static componentName = "Conditional"; 15 | static category = "math"; 16 | static description = "Performs condition tests" 17 | 18 | constructor() { 19 | super(); 20 | 21 | this.addOption('Test', 'SelectOption', CalculationType.GreaterThan, undefined, {items: [ 22 | {value: CalculationType.Equal, text: 'Equal (=)'}, 23 | {value: CalculationType.NotEqual, text: 'Not equal (!=)'}, 24 | {value: CalculationType.LessThan, text: 'Less than (<)'}, 25 | {value: CalculationType.LessThanOrEqual, text: 'Less than or equal (<=)'}, 26 | {value: CalculationType.GreaterThan, text: 'Greater than (>)'}, 27 | {value: CalculationType.GreaterThanOrEqual, text: 'Greater than or equal (>=)'}, 28 | ]}) 29 | this.addInputInterface("Operand A", "NumberOption", 0, {type: "number"}); 30 | this.addInputInterface("Operand B", "NumberOption", 0, {type: "number"}); 31 | this.addOutputInterface("True", {type: "boolean"}); 32 | this.addOutputInterface("False", {type: "boolean"}); 33 | } 34 | 35 | calculate() { 36 | const a = this.getInterface('Operand A').value; 37 | const b = this.getInterface('Operand B').value; 38 | const out = this.getInterface('True'); 39 | 40 | switch (this.getOptionValue('Test') as CalculationType) { 41 | case CalculationType.Equal: out.value = a == b; break; 42 | case CalculationType.NotEqual: out.value = a != b; break; 43 | case CalculationType.LessThan: out.value = a < b; break; 44 | case CalculationType.LessThanOrEqual: out.value = a <= b; break; 45 | case CalculationType.GreaterThan: out.value = a > b; break; 46 | case CalculationType.GreaterThanOrEqual: out.value = a >= b; break; 47 | } 48 | 49 | this.getInterface('False').value = !out.value; 50 | } 51 | } -------------------------------------------------------------------------------- /src/nodes/math/Memory.ts: -------------------------------------------------------------------------------- 1 | import Node from '../Node'; 2 | 3 | export default class Memory extends Node { 4 | static componentId = "Memory"; 5 | static componentName = "Memory"; 6 | static category = "math"; 7 | static description = "Remembers the value of the last trigger" 8 | 9 | private lastValue = 0; 10 | private lastTrigger = false; 11 | 12 | constructor() { 13 | super(); 14 | 15 | this.addOption("On positive Edge only", "CheckboxOption", false); 16 | this.addInputInterface("Input", "NumberOption", 0, {type: "number"}); 17 | this.addInputInterface("Trigger", "CheckboxOption", false, {type: "boolean"}); 18 | this.addOutputInterface("Output", {type: "number"}); 19 | } 20 | 21 | calculate() { 22 | const triggerPositive = this.getOptionValue('On positive Edge only'); 23 | const trigger = this.getInterface('Trigger').value; 24 | 25 | if (trigger !== this.lastTrigger) { 26 | if (!triggerPositive || trigger) { 27 | this.lastValue = this.getInterface('Input').value; 28 | } 29 | this.lastTrigger = trigger; 30 | } 31 | 32 | this.getInterface('Output').value = this.lastValue; 33 | } 34 | } -------------------------------------------------------------------------------- /src/nodes/math/StateMachine.ts: -------------------------------------------------------------------------------- 1 | import Node from '../Node'; 2 | 3 | const NUM_STATES = 10; 4 | 5 | export default class StateMachine extends Node { 6 | width = 200; 7 | twoColumn = true; 8 | 9 | static componentId = "StateMachine"; 10 | static componentName = "State Machine"; 11 | static category = "math"; 12 | static description = "Allows to control several states" 13 | 14 | protected activeState = 1; 15 | protected lastInputValues = [false, false, false, false, false, false, false, false, false, false] 16 | 17 | constructor() { 18 | super(); 19 | 20 | for (let i=1; i<=NUM_STATES; i++) { 21 | this.addInputInterface(`Trigger ${i}`, "CheckboxOption", false, {type: "boolean"}); 22 | this.addOutputInterface(`State ${i}`, {type: 'boolean'}); 23 | } 24 | 25 | this.getInterface(`State 1`).value = true; 26 | } 27 | 28 | calculate() { 29 | let newState = this.activeState; 30 | 31 | for (let i=1; i<=NUM_STATES; i++) { 32 | const value = this.getInterface(`Trigger ${i}`).value; 33 | if (value && !this.lastInputValues[i - 1]) { 34 | newState = i; 35 | } 36 | this.lastInputValues[i - 1] = value; 37 | } 38 | 39 | if (newState !== this.activeState) { 40 | this.getInterface(`State ${this.activeState}`).value = false; 41 | this.activeState = newState; 42 | this.getInterface(`State ${this.activeState}`).value = true; 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/nodes/signal/Constant.ts: -------------------------------------------------------------------------------- 1 | import Node from '../Node'; 2 | 3 | export default class Constant extends Node { 4 | static componentId = "Constant"; 5 | static componentName = "Constant"; 6 | static category = "signal"; 7 | static description = "Provides a constant value" 8 | 9 | constructor() { 10 | super(); 11 | 12 | this.addInputInterface("Number", "NumberOption", 0, {type: "number"}); 13 | this.addOutputInterface("Value", {type: "number"}); 14 | } 15 | 16 | calculate() { 17 | this.getInterface("Value").value = this.getInterface('Number').value; 18 | } 19 | } -------------------------------------------------------------------------------- /src/nodes/signal/Random.ts: -------------------------------------------------------------------------------- 1 | import Node from '../Node'; 2 | 3 | export default class Random extends Node { 4 | static componentId = "Random"; 5 | static componentName = "Random Generator"; 6 | static category = "signal"; 7 | static description = "Generates a random signal"; 8 | 9 | constructor() { 10 | super(); 11 | 12 | this.addInputInterface("Minimum", "NumberOption", -1, {type: "number"}); 13 | this.addInputInterface("Maximum", "NumberOption", 1, {type: "number"}); 14 | this.addOutputInterface("Output", {type: "number"}); 15 | } 16 | 17 | calculate() { 18 | const minimum = this.getInterface("Minimum").value; 19 | const maximum = this.getInterface("Maximum").value; 20 | 21 | this.getInterface("Output").value = (Math.random() * (maximum - minimum)) + minimum; 22 | } 23 | } -------------------------------------------------------------------------------- /src/nodes/signal/RemoteSignal.ts: -------------------------------------------------------------------------------- 1 | import DeviceFeatureOptionValue from '@/components/option/DeviceFeatureOptionValue'; 2 | import Node from '../Node'; 3 | 4 | export default class RemoteSignal extends Node { 5 | static componentId = "RemoteSignal"; 6 | static componentName = "Remote Signal"; 7 | static category = "signal"; 8 | static description = "Provides a signal from an external device" 9 | 10 | constructor() { 11 | super(); 12 | 13 | this.addOption("Signal", "DeviceFeatureOption", undefined, undefined, {allowedTags: ['sensor']}); 14 | this.addOutputInterface("Value", {type: "number"}); 15 | } 16 | 17 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 18 | load(state: any) { 19 | // TODO: Support loading selected device 20 | state.options = [] 21 | super.load(state); 22 | } 23 | 24 | save() { 25 | const state = super.save(); 26 | 27 | const signal = this.getOptionValue('Signal') as DeviceFeatureOptionValue | undefined; 28 | state.options = [['Signal', signal ? signal.getHash() : null]]; 29 | 30 | return state; 31 | } 32 | 33 | calculate() { 34 | const signal = this.getOptionValue('Signal') as DeviceFeatureOptionValue | undefined; 35 | if (!signal) { 36 | this.getInterface("Value").value = 0; 37 | return; 38 | } 39 | 40 | this.getInterface('Value').value = signal.getSensorValue(); 41 | } 42 | } -------------------------------------------------------------------------------- /src/nodes/signal/Time.ts: -------------------------------------------------------------------------------- 1 | import RuntimeData from '@/runtime/RuntimeData'; 2 | import Node from '../Node'; 3 | 4 | export default class Time extends Node { 5 | static componentId = "Time"; 6 | static componentName = "Time"; 7 | static category = "signal"; 8 | static description = "Session duration starting at zero" 9 | 10 | constructor() { 11 | super(); 12 | 13 | this.addOutputInterface("Time", {type: "time"}); 14 | } 15 | 16 | calculate(data: RuntimeData) { 17 | this.getInterface("Time").value = data.time; 18 | } 19 | } -------------------------------------------------------------------------------- /src/nodes/signal/VisualizeWave.ts: -------------------------------------------------------------------------------- 1 | import RuntimeData from '@/runtime/RuntimeData'; 2 | import Node from '../Node'; 3 | 4 | export default class VisualizeWave extends Node { 5 | static componentId = "VisualizeWave"; 6 | static componentName = "Wave Visualizer"; 7 | static category = "signal"; 8 | static description = "Visualizes the input wave"; 9 | 10 | constructor() { 11 | super(); 12 | 13 | this.addInputInterface("Value", "NumberOption", 0, {type: "number"}); 14 | this.addOption('Result', 'VisualizeOption', {value: 0, time: 0}) 15 | } 16 | 17 | send(data: RuntimeData) { 18 | this.setOptionValue('Result', {value: this.getInterface('Value').value, time: data.time}); 19 | } 20 | } -------------------------------------------------------------------------------- /src/nodes/signal/WaveGenerator.ts: -------------------------------------------------------------------------------- 1 | import RuntimeData from '@/runtime/RuntimeData'; 2 | import Node from '../Node'; 3 | 4 | enum WaveType { 5 | Sine = 1, 6 | Square = 2, 7 | Sawtooth = 3, 8 | Triangle = 4 9 | } 10 | 11 | export default class WaveGenerator extends Node { 12 | static componentId = "WaveGenerator"; 13 | static componentName = "Wave Generator"; 14 | static category = "signal"; 15 | static description = "Generates a wave signal"; 16 | 17 | constructor() { 18 | super(); 19 | 20 | this.addOption("Wave Type", "SelectOption", WaveType.Sine, undefined, {items: [ 21 | {value: WaveType.Sine, text: 'Sine'}, 22 | {value: WaveType.Square, text: 'Square'}, 23 | {value: WaveType.Sawtooth, text: 'Sawtooth'}, 24 | {value: WaveType.Triangle, text: 'Triangle'}, 25 | ]}); 26 | this.addOption("Only Positive Wave", "CheckboxOption", false); 27 | this.addInputInterface("Time", "TimeOption", 0, {type: "time"}); 28 | this.addInputInterface("Amplitude", "NumberOption", 1, {type: "number"}); 29 | this.addInputInterface("Frequency", "NumberOption", 1, {type: "number"}); 30 | this.addOutputInterface("Output", {type: "number"}); 31 | } 32 | 33 | calculate(data: RuntimeData) { 34 | const timeInterface = this.getInterface("Time"); 35 | const time = timeInterface.connectionCount > 0 ? timeInterface.value : data.time; 36 | 37 | const amplitude = this.getInterface("Amplitude").value; 38 | const frequency = this.getInterface("Frequency").value; 39 | const isPositive = this.getOptionValue('Only Positive Wave'); 40 | const min = (isPositive ? amplitude : 0); 41 | 42 | const out = this.getInterface("Output"); 43 | 44 | switch (this.getOptionValue('Wave Type')) { 45 | case WaveType.Sine: 46 | out.value = Math.sin(time * 2 * Math.PI * frequency) * amplitude + min; 47 | break; 48 | 49 | case WaveType.Square: 50 | out.value = (((time * frequency) % 1) >= 0.5 ? -amplitude : amplitude) + min; 51 | break; 52 | 53 | case WaveType.Sawtooth: 54 | out.value = ((((time * frequency) + 0.75) % 1) * amplitude * 2) + min - amplitude; 55 | break; 56 | 57 | case WaveType.Triangle: { 58 | const value = ((((time * frequency * 2) + 0.75) % 2) * amplitude * 2) - amplitude; 59 | if (value > amplitude) { 60 | out.value = amplitude * 2 - value + min; 61 | } else { 62 | out.value = value + min; 63 | } 64 | break; 65 | } 66 | } 67 | 68 | } 69 | } -------------------------------------------------------------------------------- /src/project/NodeRegistry.ts: -------------------------------------------------------------------------------- 1 | import Device from '@/device/Device'; 2 | import Calculation from '@/nodes/math/Calculation'; 3 | import Clamp from '@/nodes/math/Clamp'; 4 | import CustomDeviceNode from '@/nodes/device/CustomDeviceNode'; 5 | import { NodeConstructor } from '@/nodes/Node'; 6 | import Time from '@/nodes/signal/Time'; 7 | import VisualizeWave from '@/nodes/signal/VisualizeWave'; 8 | import WaveGenerator from '@/nodes/signal/WaveGenerator'; 9 | import Constant from '@/nodes/signal/Constant'; 10 | import RemoteSignal from '@/nodes/signal/RemoteSignal'; 11 | import Average from '@/nodes/math/Average'; 12 | import Memory from '@/nodes/math/Memory'; 13 | import Random from '@/nodes/signal/Random'; 14 | import CustomInputDeviceNode from '@/nodes/device/CustomInputDeviceNode'; 15 | import Conditional from '@/nodes/math/Conditional'; 16 | import StateMachine from '@/nodes/math/StateMachine'; 17 | import Vibrator from '@/nodes/device/Vibrator'; 18 | 19 | export interface NodeRegistryCategory{ 20 | id: string; 21 | name: string; 22 | icon: string; 23 | } 24 | 25 | export default class NodeRegistry extends EventTarget { 26 | protected nodes: NodeConstructor[] = []; 27 | public readonly categories: NodeRegistryCategory[] = []; 28 | 29 | protected deviceNodes: {[id: string]: NodeConstructor[]} = {} 30 | 31 | get all() { 32 | return this.nodes; 33 | } 34 | 35 | filtered(category: string) { 36 | if (category === 'all') { 37 | return this.nodes; 38 | } 39 | 40 | return this.nodes.filter(node => node.category === category); 41 | } 42 | 43 | addCategory(id: string, name: string, icon: string) { 44 | this.categories.push({id, name, icon}); 45 | } 46 | 47 | add(node: NodeConstructor) { 48 | const oldItem = this.nodes.findIndex(item => item.componentId === node.componentId); 49 | if (oldItem >= 0) { 50 | this.nodes.splice(oldItem, 1); 51 | } 52 | 53 | this.dispatchEvent(new CustomEvent('nodetypeadded', {detail: node})); 54 | 55 | this.nodes.push(node); 56 | this.nodes.sort((a, b) => (a.componentName < b.componentName) ? -1 : 1); 57 | } 58 | 59 | remove(node: NodeConstructor) { 60 | const oldItem = this.nodes.findIndex(item => item.componentId === node.componentId); 61 | if (oldItem >= 0) { 62 | this.nodes.splice(oldItem, 1); 63 | } 64 | 65 | this.dispatchEvent(new CustomEvent('nodetyperemoved', {detail: node})); 66 | } 67 | 68 | addDeviceNode(device: Device, node: NodeConstructor) { 69 | if (!this.deviceNodes[device.id]) { 70 | this.deviceNodes[device.id] = []; 71 | } 72 | 73 | this.deviceNodes[device.id].push(node); 74 | 75 | this.add(node); 76 | } 77 | 78 | addCustomDevice(device: Device) { 79 | class CustomDevice extends CustomDeviceNode { 80 | static componentId = device.id; 81 | static componentName = device.name; 82 | static description = device.description; 83 | 84 | constructor() { 85 | super(device); 86 | } 87 | } 88 | 89 | this.addDeviceNode(device, CustomDevice); 90 | 91 | if (device.outputPorts().length) { 92 | this.addCustomDeviceInput(device); 93 | } 94 | } 95 | 96 | addCustomDeviceInput(device: Device) { 97 | class CustomInputDevice extends CustomInputDeviceNode { 98 | static componentId = device.id + 'Input'; 99 | static componentName = device.name + ' (Signals)'; 100 | static description = device.description + ' (Signals)'; 101 | 102 | constructor() { 103 | super(device); 104 | } 105 | } 106 | 107 | this.addDeviceNode(device, CustomInputDevice); 108 | } 109 | 110 | removeCustomDevice(device: Device) { 111 | if (!this.deviceNodes[device.id]) { 112 | return; 113 | } 114 | 115 | for (const node of this.deviceNodes[device.id]) { 116 | this.remove(node) 117 | } 118 | 119 | this.deviceNodes[device.id] = []; 120 | } 121 | 122 | create(id: string) { 123 | const node = this.nodes.find(node => node.componentId === id); 124 | if (!node) { 125 | return null; 126 | } 127 | 128 | return new node(); 129 | } 130 | 131 | registerDefaultNodes() { 132 | this.addCategory('all', 'All', 'fas fa-folder'); 133 | this.addCategory('device', 'Devices', 'fas fa-charging-station'); 134 | this.addCategory('signal', 'Signals', 'fas fa-wave-square'); 135 | this.addCategory('math', 'Math', 'fas fa-square-root-alt'); 136 | 137 | this.add(Time); 138 | this.add(WaveGenerator); 139 | this.add(VisualizeWave); 140 | this.add(Calculation); 141 | this.add(Conditional); 142 | this.add(StateMachine); 143 | this.add(Clamp); 144 | this.add(RemoteSignal); 145 | this.add(Constant); 146 | this.add(Average); 147 | this.add(Memory); 148 | this.add(Vibrator); 149 | this.add(Random); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/project/Project.ts: -------------------------------------------------------------------------------- 1 | import ConnectionManager from '@/connection/ConnectionManager'; 2 | import NodeRegistry from './NodeRegistry'; 3 | import ProjectFileInterface, { ProjectFileComponentInterface, ProjectFileIdentifier, ProjectFileVersion } from './ProjectFileInterface'; 4 | import ProjectLoaderV1 from './ProjectLoaderV1'; 5 | import ProjectWorkspace from './ProjectWorkspace'; 6 | 7 | export default class Project { 8 | public readonly nodes: NodeRegistry = new NodeRegistry(); 9 | public readonly connections: ConnectionManager; 10 | 11 | protected workspace = new ProjectWorkspace; 12 | 13 | constructor(manager: ConnectionManager) { 14 | this.connections = manager; 15 | 16 | this.connections.addEventListener('deviceadded', this.onDeviceAdded); 17 | this.connections.addEventListener('deviceremoved', this.onDevicesRemoved); 18 | this.nodes.addEventListener('nodetypeadded', this.onNodeTypeAdded); 19 | } 20 | 21 | unload() { 22 | this.connections.removeEventListener('deviceadded', this.onDeviceAdded); 23 | this.connections.removeEventListener('deviceremoved', this.onDevicesRemoved); 24 | this.nodes.removeEventListener('nodetypeadded', this.onNodeTypeAdded); 25 | } 26 | 27 | private onDeviceAdded = (event: Event) => { 28 | this.nodes.addCustomDevice((event as CustomEvent).detail) 29 | } 30 | 31 | private onDevicesRemoved = (event: Event) => { 32 | this.nodes.removeCustomDevice((event as CustomEvent).detail) 33 | } 34 | 35 | private onNodeTypeAdded = (event: Event) => { 36 | this.workspace.registerNodeType((event as CustomEvent).detail) 37 | } 38 | 39 | async initialize() { 40 | this.nodes.registerDefaultNodes(); 41 | this.connections.triggerDeviceEventsAgain(); 42 | } 43 | 44 | get currentWorkspace() { 45 | return this.workspace; 46 | } 47 | 48 | static async load(json: ProjectFileInterface, manager: ConnectionManager): Promise { 49 | if (json.identifier !== ProjectFileIdentifier) { 50 | throw new Error(`Project identifier is invalid`); 51 | } 52 | 53 | if (json.version !== ProjectFileVersion) { 54 | throw new Error(`Project version is invalid`); 55 | } 56 | 57 | const loader = new ProjectLoaderV1(manager); 58 | return loader.load(json); 59 | } 60 | 61 | save() { 62 | const element = document.createElement('a'); 63 | const data = JSON.stringify(this.serialize()); 64 | element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(data)); 65 | element.setAttribute('download', 'buttplugeditor-project.json'); 66 | 67 | element.style.display = 'none'; 68 | document.body.appendChild(element); 69 | element.click(); 70 | setImmediate(() => { 71 | document.body.removeChild(element); 72 | }); 73 | } 74 | 75 | serialize(): ProjectFileInterface { 76 | return { 77 | identifier: ProjectFileIdentifier, 78 | version: ProjectFileVersion, 79 | workspace: this.workspace.serialize(), 80 | components: this.serializeNodeTypes() 81 | } 82 | } 83 | 84 | serializeNodeTypes(): ProjectFileComponentInterface[] { 85 | const allNodes = this.workspace.allNodes().map(node => node.serializeType()); 86 | const allNodesMap = allNodes.reduce((a, b) => { 87 | a[b.name] = b; 88 | return a; 89 | }, {} as {[id: string]: ProjectFileComponentInterface}) 90 | 91 | return Object.values(allNodesMap); 92 | } 93 | } -------------------------------------------------------------------------------- /src/project/ProjectFileInterface.ts: -------------------------------------------------------------------------------- 1 | type IState = unknown; 2 | 3 | export const ProjectFileIdentifier = 'maidkun/buttplug-editor/project'; 4 | export const ProjectFileVersion = 1; 5 | 6 | export interface ProjectFileComponentPortInterface { 7 | id: string; 8 | name: string; 9 | type: 'number'; 10 | } 11 | 12 | export interface ProjectFileComponentInterface { 13 | origin: 'device' | 'core'; 14 | name: string; 15 | inputPorts: ProjectFileComponentPortInterface[]; 16 | outputPorts: ProjectFileComponentPortInterface[]; 17 | } 18 | 19 | export interface ProjectFileBoardInterface { 20 | state: IState; 21 | } 22 | 23 | export interface ProjectFileWorkspaceInterface { 24 | boards: ProjectFileBoardInterface[]; 25 | } 26 | 27 | export default interface ProjectFileInterface { 28 | identifier: typeof ProjectFileIdentifier; 29 | version: number; 30 | 31 | workspace: ProjectFileWorkspaceInterface; 32 | components: ProjectFileComponentInterface[]; 33 | } -------------------------------------------------------------------------------- /src/project/ProjectLoaderV1.ts: -------------------------------------------------------------------------------- 1 | import ConnectionManager from '@/connection/ConnectionManager'; 2 | import Project from './Project'; 3 | import ProjectFileInterface, { ProjectFileBoardInterface } from './ProjectFileInterface'; 4 | 5 | export default class ProjectLoaderV1 { 6 | public readonly project: Project; 7 | 8 | constructor(manager: ConnectionManager) { 9 | this.project = new Project(manager); 10 | } 11 | 12 | async load(json: ProjectFileInterface) { 13 | await this.project.initialize(); 14 | await Promise.all(json.workspace.boards.map(board => this.restoreBoard(board))); 15 | 16 | return this.project; 17 | } 18 | 19 | protected async restoreBoard(board: ProjectFileBoardInterface) { 20 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 21 | this.project.currentWorkspace.currentBoard.editor.load(board.state as any); 22 | } 23 | } -------------------------------------------------------------------------------- /src/project/ProjectWorkspace.ts: -------------------------------------------------------------------------------- 1 | import Node, { NodeConstructor } from '@/nodes/Node'; 2 | import { ProjectFileWorkspaceInterface } from './ProjectFileInterface'; 3 | import ProjectWorkspaceBoard from './ProjectWorkspaceBoard'; 4 | 5 | export default class ProjectWorkspace { 6 | public readonly currentBoard = new ProjectWorkspaceBoard(); 7 | 8 | serialize(): ProjectFileWorkspaceInterface { 9 | return { 10 | boards: [ 11 | this.currentBoard.serialize() 12 | ] 13 | } 14 | } 15 | 16 | allNodes(): Node[] { 17 | return this.currentBoard.allNodes(); 18 | } 19 | 20 | registerNodeType(node: NodeConstructor) { 21 | this.currentBoard.editor.registerNodeType(node.componentId, node); 22 | } 23 | } -------------------------------------------------------------------------------- /src/project/ProjectWorkspaceBoard.ts: -------------------------------------------------------------------------------- 1 | import { Editor } from "@baklavajs/core"; 2 | import { ViewPlugin } from "@baklavajs/plugin-renderer-vue"; 3 | import { OptionPlugin } from "@baklavajs/plugin-options-vue"; 4 | import { InterfaceTypePlugin } from "@baklavajs/plugin-interface-types"; 5 | import { Engine } from "@baklavajs/plugin-engine"; 6 | import VisualizeOption from '@/components/option/VisualizeOption.vue'; 7 | import DeviceFeatureOption from '@/components/option/DeviceFeatureOption.vue'; 8 | import { ProjectFileBoardInterface } from './ProjectFileInterface'; 9 | import Node from '@/nodes/Node'; 10 | 11 | export default class ProjectWorkspaceBoard { 12 | public readonly editor = new Editor(); 13 | public readonly viewPlugin = new ViewPlugin(); 14 | public readonly optionPlugin = new OptionPlugin(); 15 | public readonly intfTypePlugin = new InterfaceTypePlugin(); 16 | public readonly engine = new Engine(false); 17 | 18 | constructor() { 19 | this.editor.use(this.optionPlugin); 20 | this.editor.use(this.viewPlugin); 21 | this.editor.use(this.intfTypePlugin); 22 | this.editor.use(this.engine); 23 | 24 | this.viewPlugin.registerOption('VisualizeOption', VisualizeOption); 25 | this.viewPlugin.registerOption('DeviceFeatureOption', DeviceFeatureOption); 26 | 27 | this.intfTypePlugin.addType("time", "#88ff00"); 28 | this.intfTypePlugin.addType("number", "#888888"); 29 | this.intfTypePlugin.addType("boolean", "#0088ff"); 30 | this.intfTypePlugin.addConversion("time", "number", v => v); 31 | this.intfTypePlugin.addConversion("number", "time", v => v); 32 | this.intfTypePlugin.addConversion("boolean", "number", v => v ? 1 : 0); 33 | this.intfTypePlugin.addConversion("number", "boolean", v => v > 0.01 ? true : false); 34 | } 35 | 36 | serialize(): ProjectFileBoardInterface { 37 | return { 38 | state: this.editor.save() 39 | } 40 | } 41 | 42 | allNodes(): Node[] { 43 | return this.editor.nodes as Node[]; 44 | } 45 | } -------------------------------------------------------------------------------- /src/runtime/Runtime.ts: -------------------------------------------------------------------------------- 1 | export default class Runtime { 2 | protected startTime: number; 3 | protected time = 0; 4 | 5 | constructor() { 6 | this.startTime = this.getSystemTime(); 7 | } 8 | 9 | reset() { 10 | this.startTime = this.getSystemTime(); 11 | } 12 | 13 | tick() { 14 | this.time = this.getSystemTime() - this.startTime; 15 | } 16 | 17 | getTime() { 18 | return this.time; 19 | } 20 | 21 | getSystemTime() { 22 | return (new Date()).getTime() / 1000; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/runtime/RuntimeData.ts: -------------------------------------------------------------------------------- 1 | export default interface RuntimeData { 2 | time: number; 3 | } -------------------------------------------------------------------------------- /src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /src/style.scss: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, 6 | body { 7 | width: 100%; 8 | height: 100%; 9 | margin: 0; 10 | padding: 0; 11 | overflow: hidden; 12 | } 13 | 14 | .flex-rows { 15 | display: flex; 16 | flex: 1; 17 | flex-direction: row; 18 | max-width: 100%; 19 | max-height: 100%; 20 | } 21 | 22 | .flex-columns { 23 | display: flex; 24 | flex: 1; 25 | flex-direction: column; 26 | max-width: 100%; 27 | max-height: 100%; 28 | } 29 | 30 | .flex-stretch { 31 | flex: 1; 32 | } 33 | 34 | .text-danger { color: #f04; } 35 | .text-warning { color: #fc4; } 36 | .text-success { color: #8c4; } 37 | 38 | .scroll-container { 39 | flex: 1; 40 | overflow-x: hidden; 41 | overflow-y: auto; 42 | } -------------------------------------------------------------------------------- /src/utils/AsyncCache.ts: -------------------------------------------------------------------------------- 1 | export default class AsyncCache { 2 | private callback: () => Promise; 3 | 4 | private isExecuting = false; 5 | private hasResult = false; 6 | 7 | private result?: T; 8 | private error?: Error; 9 | 10 | private resolve: Array<(value: T) => unknown> = []; 11 | private reject: Array<(error: Error) => unknown> = []; 12 | 13 | constructor(callback: () => Promise) { 14 | this.callback = callback; 15 | } 16 | 17 | invoke() { 18 | if (this.isExecuting) { 19 | return this.listenToCurrentCall(); 20 | } 21 | 22 | return this.reload(); 23 | } 24 | 25 | reload() { 26 | this.hasResult = false; 27 | this.isExecuting = true; 28 | 29 | return new Promise((resolve, reject) => { 30 | this.resolve.push(resolve); 31 | this.reject.push(reject); 32 | 33 | this.callback().then(result => { 34 | this.setResult(result); 35 | }).catch(err => { 36 | this.setError(err); 37 | }) 38 | }) 39 | } 40 | 41 | setResult(result: T) { 42 | this.hasResult = true; 43 | this.isExecuting = false; 44 | this.error = undefined; 45 | 46 | const resolve = this.resolve; 47 | this.resolve = []; 48 | this.reject = []; 49 | 50 | for (const call of resolve) { 51 | call(result); 52 | } 53 | } 54 | 55 | setError(result: Error) { 56 | this.hasResult = true; 57 | this.isExecuting = false; 58 | 59 | const reject = this.reject; 60 | this.resolve = []; 61 | this.reject = []; 62 | 63 | for (const call of reject) { 64 | call(result); 65 | } 66 | } 67 | 68 | private listenToCurrentCall() { 69 | return new Promise((resolve, reject) => { 70 | if (!this.hasResult) { 71 | this.resolve.push(resolve); 72 | this.reject.push(reject); 73 | return; 74 | } 75 | 76 | if (this.error) { 77 | reject(this.error); 78 | } else { 79 | resolve(this.result); 80 | } 81 | }) 82 | } 83 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "sourceMap": true, 14 | "baseUrl": ".", 15 | "types": [ 16 | "webpack-env" 17 | ], 18 | "paths": { 19 | "@/*": [ 20 | "src/*" 21 | ] 22 | }, 23 | "lib": [ 24 | "esnext", 25 | "dom", 26 | "dom.iterable", 27 | "scripthost" 28 | ] 29 | }, 30 | "include": [ 31 | "src/**/*.ts", 32 | "src/**/*.tsx", 33 | "src/**/*.vue", 34 | "tests/**/*.ts", 35 | "tests/**/*.tsx" 36 | ], 37 | "exclude": [ 38 | "node_modules" 39 | ] 40 | } 41 | --------------------------------------------------------------------------------