├── .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 | 
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 |
2 |
10 |
11 |
12 |
42 |
43 |
68 |
--------------------------------------------------------------------------------
/src/components/NodeEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
97 |
--------------------------------------------------------------------------------
/src/components/connection/ConnectionIndicator.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
17 |
18 |
19 |
20 |
67 |
68 |
--------------------------------------------------------------------------------
/src/components/connection/ConnectionListItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | {{item.configuration.protocol}}
17 | ws://{{item.configuration.address}}:{{item.configuration.port}}/
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
48 |
49 |
69 |
--------------------------------------------------------------------------------
/src/components/connection/ConnectionListWindow.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
13 |
68 |
69 |
90 |
--------------------------------------------------------------------------------
/src/components/option/DeviceFeatureOption.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
{{ value ? value.getText() : '' }}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
{{item.getText()}}
22 |
23 |
24 |
No device with vibrator-support has been connected
25 |
26 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
76 |
77 |
89 |
--------------------------------------------------------------------------------
/src/components/ui/Button.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
17 |
18 |
32 |
--------------------------------------------------------------------------------
/src/components/ui/Popup.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
33 |
34 |
70 |
--------------------------------------------------------------------------------
/src/components/workspace/ComponentBrowser.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
32 |
33 |
41 |
--------------------------------------------------------------------------------
/src/components/workspace/ComponentBrowserList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
6 |
7 | {{item.componentName}}
8 |
9 | {{item.description}}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
64 |
65 |
112 |
--------------------------------------------------------------------------------
/src/components/workspace/ComponentCategoryBrowser.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
33 |
34 |
73 |
--------------------------------------------------------------------------------
/src/components/workspace/ProjectProvider.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
19 |
--------------------------------------------------------------------------------
/src/components/workspace/Workspace.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
55 |
56 |
75 |
--------------------------------------------------------------------------------
/src/components/workspace/WorkspaceFooter.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
23 |
24 |
--------------------------------------------------------------------------------
/src/components/workspace/WorkspaceMenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
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 |
--------------------------------------------------------------------------------