├── src ├── editor │ ├── utils │ │ ├── EditorError.js │ │ ├── Node.js │ │ ├── SelectionBox.js │ │ ├── SettingsManager.js │ │ ├── settings.js │ │ ├── Command.js │ │ ├── Connection.js │ │ └── functions.js │ ├── editor │ │ ├── managers │ │ │ ├── ShortcutManager.js │ │ │ ├── ProjectManager.js │ │ │ ├── ImportManager.js │ │ │ └── ExportManager.js │ │ ├── systems │ │ │ ├── CollapseSystem.js │ │ │ ├── ShortcutSystem.js │ │ │ ├── SelectionSystem.js │ │ │ ├── DragSystem.js │ │ │ ├── CameraSystem.js │ │ │ └── ConnectionSystem.js │ │ └── Editor.js │ ├── namespaces.js │ ├── tree │ │ ├── managers │ │ │ ├── ViewManager.js │ │ │ ├── SelectionManager.js │ │ │ ├── ConnectionManager.js │ │ │ ├── OrganizeManager.js │ │ │ └── EditManager.js │ │ └── Tree.js │ ├── project │ │ ├── managers │ │ │ ├── HistoryManager.js │ │ │ ├── TreeManager.js │ │ │ └── NodeManager.js │ │ └── Project.js │ └── draw │ │ └── symbols.js ├── app │ ├── directives │ │ ├── tab.html │ │ ├── tabset.html │ │ ├── tab.directive.js │ │ ├── tabset.directive.js │ │ ├── dropnode.directive.js │ │ ├── keytable.html │ │ ├── dragnode.directive.js │ │ └── keytable.directive.js │ ├── pages │ │ ├── editor │ │ │ ├── editor.controller.js │ │ │ ├── editor.html │ │ │ ├── modals │ │ │ │ ├── modal.html │ │ │ │ ├── import.html │ │ │ │ ├── export.html │ │ │ │ ├── import.controller.js │ │ │ │ ├── editnode.controller.js │ │ │ │ ├── export.controller.js │ │ │ │ └── editnode.html │ │ │ └── components │ │ │ │ ├── treespanel.html │ │ │ │ ├── propertiespanel.html │ │ │ │ ├── propertiespanel.controller.js │ │ │ │ ├── treespanel.controller.js │ │ │ │ ├── nodespanel.html │ │ │ │ ├── nodespanel.controller.js │ │ │ │ └── menubar.html │ │ ├── home │ │ │ ├── home.controller.js │ │ │ └── home.html │ │ ├── dash │ │ │ ├── dash.controller.js │ │ │ └── dash.html │ │ ├── settings │ │ │ └── settings.controller.js │ │ └── projects │ │ │ ├── projects.html │ │ │ └── projects.controller.js │ ├── services │ │ ├── nodejs.service.js │ │ ├── storage │ │ │ ├── localstorage.service.js │ │ │ ├── filestorage.service.js │ │ │ └── storage.service.js │ │ ├── editor.service.js │ │ ├── system.service.js │ │ ├── dialog.service.js │ │ └── notification.service.js │ ├── validators │ │ └── denylist.directive.js │ ├── app.controller.js │ ├── app.js │ ├── app.routes.js │ └── models │ │ ├── settings.model.js │ │ └── project.model.js ├── assets │ ├── imgs │ │ ├── favicon.ico │ │ └── closedhand.cur │ ├── js │ │ └── preload.js │ ├── less │ │ ├── bootstrap.less │ │ ├── c_properties.less │ │ ├── index.less │ │ ├── c_dash.less │ │ ├── c_keytable.less │ │ ├── c_tabset.less │ │ ├── c_trees.less │ │ ├── c_modal.less │ │ ├── c_page.less │ │ ├── c_nodes.less │ │ ├── c_app.less │ │ ├── animations.less │ │ ├── c_notification.less │ │ ├── c_sidebar.less │ │ ├── variables.less │ │ └── c_menubar.less │ ├── css │ │ └── preload.css │ └── libs │ │ └── mousetrap.min.js ├── start.js ├── package.json ├── index.html └── desktop.js ├── preview.png ├── bower.json ├── .gitignore ├── LICENSE ├── package.json ├── BUILD.md ├── CHANGES.md └── README.md /src/editor/utils/EditorError.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/behavior_tree_editor/HEAD/preview.png -------------------------------------------------------------------------------- /src/app/directives/tab.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /src/assets/imgs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/behavior_tree_editor/HEAD/src/assets/imgs/favicon.ico -------------------------------------------------------------------------------- /src/assets/imgs/closedhand.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/behavior_tree_editor/HEAD/src/assets/imgs/closedhand.cur -------------------------------------------------------------------------------- /src/assets/js/preload.js: -------------------------------------------------------------------------------- 1 | function preloadProgress(message) { 2 | var element = document.getElementById('page-preload-progress'); 3 | element.innerHTML = message; 4 | } -------------------------------------------------------------------------------- /src/editor/editor/managers/ShortcutManager.js: -------------------------------------------------------------------------------- 1 | b3e.editor.ShortcutManager = function(editor) { 2 | "use strict"; 3 | 4 | this._applySettings = function(settings) {}; 5 | }; -------------------------------------------------------------------------------- /src/editor/editor/systems/CollapseSystem.js: -------------------------------------------------------------------------------- 1 | b3e.editor.CollapseSystem = function(editor) { 2 | "use strict"; 3 | 4 | this.update = function(delta) { 5 | 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /src/app/pages/editor/editor.controller.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('app') 3 | .controller('EditorController', EditorController); 4 | 5 | EditorController.$inject = []; 6 | 7 | function EditorController() { 8 | 9 | } -------------------------------------------------------------------------------- /src/start.js: -------------------------------------------------------------------------------- 1 | var editor; 2 | 3 | function startApp() { 4 | var domProgress = document.getElementById('page-preload'); 5 | 6 | editor = new b3e.editor.Editor(); 7 | angular.bootstrap(document, ['app']); 8 | } -------------------------------------------------------------------------------- /src/assets/less/bootstrap.less: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .nav, .pagination, .carousel, .panel-title a { cursor: pointer; } 4 | 5 | .panel { 6 | .title { margin-top: 0px; } 7 | .panel-heading { 8 | h1, h2, h3, h4 { margin: 0px; } 9 | } 10 | } -------------------------------------------------------------------------------- /src/app/directives/tabset.html: -------------------------------------------------------------------------------- 1 |
2 | 7 | 8 | 9 |
-------------------------------------------------------------------------------- /src/assets/less/c_properties.less: -------------------------------------------------------------------------------- 1 | .properties { 2 | padding: @padding-small-2x; 3 | 4 | input:not([type=button]), textarea { 5 | padding: @padding-small; 6 | } 7 | 8 | label { 9 | display: block; 10 | text-transform: uppercase; 11 | color: @color-light3; 12 | .font-base; 13 | } 14 | } -------------------------------------------------------------------------------- /src/editor/editor/systems/ShortcutSystem.js: -------------------------------------------------------------------------------- 1 | b3e.editor.ShortcutSystem = function(editor) { 2 | "use strict"; 3 | 4 | this.update = function(delta) { 5 | var project = editor.project.get(); 6 | if (!project) return; 7 | 8 | var tree = project.trees.getSelected(); 9 | if (!tree) return; 10 | 11 | var kb = editor._game.keyboard; 12 | var k = tine.keys; 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "behaviorTreeEditor", 3 | "version" : "[BUILD_VERSION]", 4 | "main" : "desktop.js", 5 | "window": { 6 | "title" : "Behavior Tree Editor", 7 | "position" : "center", 8 | "toolbar" : false, 9 | "frame" : true, 10 | "width" : 1000, 11 | "height" : 800, 12 | "min_width" : 1000, 13 | "min_height" : 800 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/pages/home/home.controller.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app') 6 | .controller('HomeController', HomeController); 7 | 8 | HomeController.$inject = [ 9 | ]; 10 | 11 | function HomeController() { 12 | var vm = this; 13 | 14 | _active(); 15 | 16 | function _active() { 17 | // var e = document.getElementById('prld'); 18 | // if (e) e.remove(); 19 | } 20 | } 21 | 22 | })(); -------------------------------------------------------------------------------- /src/assets/less/index.less: -------------------------------------------------------------------------------- 1 | @import "variables.less"; 2 | @import "bootstrap.less"; 3 | @import "animations.less"; 4 | 5 | @import "c_app.less"; 6 | @import "c_dash.less"; 7 | @import "c_keytable.less"; 8 | @import "c_menubar.less"; 9 | @import "c_modal.less"; 10 | @import "c_nodes.less"; 11 | @import "c_notification.less"; 12 | @import "c_page.less"; 13 | @import "c_properties.less"; 14 | @import "c_sidebar.less"; 15 | @import "c_tabset.less"; 16 | @import "c_trees.less"; -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "behavior3editor", 3 | "version": "0.2.0dev", 4 | "directory": "src/vendor/", 5 | "devDependencies": { 6 | "angular" : "~1.4", 7 | "bootstrap" : "~3.1.1", 8 | "angular-animate" : "~1.4", 9 | "angular-bootstrap" : "~0.13.0", 10 | "angular-ui-router" : "~0.2.14", 11 | "fontawesome" : "~4.3.0", 12 | "less" : "~2.5.0", 13 | "sweetalert" : "~1.0.0-beta" 14 | }, 15 | "dependencies": {} 16 | } -------------------------------------------------------------------------------- /src/app/services/nodejs.service.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('app') 3 | .factory('nodejsService', nodejsService); 4 | 5 | nodejsService.$inject = ['$window']; 6 | 7 | function nodejsService($window) { 8 | var ok = !!$window.require; 9 | var remote = ok?require('electron').remote:null; 10 | var service = { 11 | ok : ok, 12 | fs : (ok?$window.require('fs'):null), 13 | path : (ok?$window.require('path'):null), 14 | dialog : (ok?remote.dialog:null), 15 | }; 16 | return service; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/app/pages/editor/editor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 |
16 |
17 |
-------------------------------------------------------------------------------- /src/app/pages/dash/dash.controller.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app') 6 | .controller('DashController', DashController); 7 | 8 | DashController.$inject = [ 9 | '$scope', 10 | 'projectModel' 11 | ]; 12 | 13 | function DashController($scope, projectModel) { 14 | var vm = this; 15 | vm.project = null; 16 | _activate(); 17 | 18 | function _activate() { 19 | vm.project = projectModel.getProject(); 20 | } 21 | $scope.$on('dash-projectchanged', function() { 22 | _activate(); 23 | }); 24 | } 25 | })(); -------------------------------------------------------------------------------- /src/assets/less/c_dash.less: -------------------------------------------------------------------------------- 1 | .dash-menu { 2 | list-style: none; 3 | text-align: right; 4 | margin-right: @padding-large; 5 | font-size: 2em; 6 | .font-header; 7 | 8 | li { 9 | margin-bottom: 10px; 10 | 11 | a { 12 | color: @color-light2; 13 | &.active { color: @color-light1; } 14 | &:hover { color: @color-light1; } 15 | } 16 | } 17 | } 18 | 19 | .dash-page { 20 | overflow: auto; 21 | position: absolute; 22 | top: 0px; 23 | left: 0px; 24 | width: 100%; 25 | height: 100%; 26 | z-index: @z-page; 27 | background-color: @color-light1; 28 | } -------------------------------------------------------------------------------- /src/assets/less/c_keytable.less: -------------------------------------------------------------------------------- 1 | .keytable { 2 | 3 | input[type=button] { 4 | width: 40px; 5 | } 6 | 7 | th { 8 | margin: 0px; 9 | padding: 0px 2px !important; 10 | 11 | label { 12 | padding-bottom: 0px; 13 | } 14 | } 15 | 16 | td { 17 | border: 0px !important; 18 | padding: 2px 2px !important; 19 | 20 | input:not([type=button]) { 21 | padding: 0px @padding-small; 22 | margin: 0px; 23 | } 24 | } 25 | 26 | .form-control { 27 | height: 25px; 28 | } 29 | 30 | &.no-border { 31 | th { 32 | border: 0px; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/directives/tab.directive.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app') 6 | .directive('b3Tab', tab); 7 | 8 | function tab() { 9 | var directive = { 10 | require : '^b3Tabset', 11 | restrict : 'EA', 12 | scope : { 13 | active : '=?', 14 | heading : '@' 15 | }, 16 | transclude : true, 17 | templateUrl : 'directives/tab.html', 18 | link : link, 19 | }; 20 | return directive; 21 | 22 | function link(scope, element, attrs, ctrl) { 23 | scope.active = !!scope.active; 24 | ctrl.add(scope); 25 | } 26 | } 27 | 28 | })(); -------------------------------------------------------------------------------- /src/app/pages/dash/dash.html: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 |
17 |
-------------------------------------------------------------------------------- /src/assets/less/c_tabset.less: -------------------------------------------------------------------------------- 1 | .tabset { 2 | height: 100%; 3 | 4 | ul.tabset-header { 5 | list-style: none; 6 | margin: 0px; 7 | padding: @padding-medium 0px; 8 | text-align: center; 9 | font-size: 1.3em; 10 | .font-header; 11 | 12 | li { 13 | display: inline; 14 | margin: 0px 10px; 15 | 16 | a { 17 | color: @color-light2; 18 | &:hover { 19 | color: @color-light1; 20 | } 21 | } 22 | 23 | &.active { 24 | a { 25 | color: @color-blue; 26 | &:hover { 27 | color: @color-blue; 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | .tab-panel { 36 | height: 100%; 37 | } -------------------------------------------------------------------------------- /src/app/pages/editor/modals/modal.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

Node

6 |
7 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Explicabo, animi officia minus iusto neque vitae esse voluptas qui. Laborum culpa quasi nisi fuga rerum, cupiditate quibusdam eos consequuntur! Libero, expedita.

8 |
9 |
10 | 11 |
12 | 13 | 14 |
15 |
16 |
-------------------------------------------------------------------------------- /src/app/services/storage/localstorage.service.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('app') 3 | .factory('localStorageService', localStorageService); 4 | 5 | localStorageService.$inject = ['$window']; 6 | 7 | function localStorageService($window) { 8 | var service = { 9 | ok : !!$window.localStorage, 10 | save : save, 11 | load : load, 12 | remove : remove 13 | }; 14 | return service; 15 | 16 | function save(path, data) { 17 | try { data = JSON.stringify(data); } catch (e) {} 18 | $window.localStorage[path] = data; 19 | } 20 | function load(path) { 21 | var data = $window.localStorage[path]; 22 | try { data = JSON.parse(data); } catch (e) {} 23 | return data; 24 | } 25 | function remove(path) { 26 | delete $window.localStorage[path]; 27 | } 28 | } -------------------------------------------------------------------------------- /src/assets/less/c_trees.less: -------------------------------------------------------------------------------- 1 | .tree-list { 2 | overflow-y: auto; 3 | height: 80%; 4 | 5 | &-content { 6 | padding: 0px @padding-small; 7 | 8 | ul { 9 | list-style: none; 10 | margin: 0px; 11 | padding: 0px; 12 | 13 | li { 14 | .remove { 15 | float: right; 16 | color: @color-light1; 17 | &:hover { 18 | text-decoration: none; 19 | background-color: @color-red; 20 | } 21 | } 22 | a { 23 | padding: 2px @padding-small; 24 | display: block; 25 | color: @color-light1; 26 | &:hover { 27 | background-color: @color-dark2; 28 | } 29 | &.active { 30 | color: @color-blue; 31 | font-weight: 600; 32 | } 33 | } 34 | } 35 | } 36 | } 37 | 38 | 39 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | # IDE configurations 31 | .idea 32 | 33 | # Custom 34 | bower_components 35 | dist 36 | .temp-dist 37 | release/ 38 | build 39 | src/vendor 40 | cache 41 | teste.py 42 | 43 | ignore-* 44 | -------------------------------------------------------------------------------- /src/app/validators/denylist.directive.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | angular.module("app").directive("denylist", denylist); 5 | 6 | function denylist() { 7 | var directive = { 8 | require: "ngModel", 9 | restrict: "A", 10 | link: link 11 | }; 12 | return directive; 13 | 14 | function link(scope, element, attrs, ctrl) { 15 | var denylist = attrs.denylist.split(","); 16 | 17 | //For DOM -> model validation 18 | ctrl.$parsers.unshift(function(value) { 19 | var valid = denylist.indexOf(value) === -1; 20 | ctrl.$setValidity("denylist", valid); 21 | return valid ? value : undefined; 22 | }); 23 | 24 | //For model -> DOM validation 25 | ctrl.$formatters.unshift(function(value) { 26 | ctrl.$setValidity("denylist", denylist.indexOf(value) === -1); 27 | return value; 28 | }); 29 | } 30 | } 31 | })(); 32 | 33 | -------------------------------------------------------------------------------- /src/editor/utils/Node.js: -------------------------------------------------------------------------------- 1 | /** @module b3e */ 2 | (function() { 3 | 'use strict'; 4 | 5 | /** 6 | * A node specification. 7 | * 8 | * @class Node 9 | * @param {Boolean} isDefault Whether the node is provided by default or not. 10 | * @constructor 11 | */ 12 | b3e.Node = function(isDefault) { 13 | this.spec = null; 14 | this.name = null; 15 | this.title = null; 16 | this.category = null; 17 | this.description = null; 18 | this.properties = {}; 19 | this.isDefault = !!isDefault; 20 | 21 | /** 22 | * Copy this node. 23 | * 24 | * @method copy 25 | * @returns {b3e.Node} A copy of this node 26 | */ 27 | this.copy = function() { 28 | var n = new b3e.Node(this.isDefault); 29 | n.spec = this.spec; 30 | n.name = this.name; 31 | n.title = this.title; 32 | n.category = this.category; 33 | n.description = this.description; 34 | n.properties = this.properties; 35 | 36 | return n; 37 | }; 38 | }; 39 | })(); -------------------------------------------------------------------------------- /src/app/services/editor.service.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('app') 3 | .factory('editorService', editorService); 4 | 5 | editorService.$inject = ['$window']; 6 | 7 | function editorService($window) { 8 | var service = { 9 | getDefaultSettings : getDefaultSettings, 10 | applySettings : applySettings, 11 | newProject : newProject, 12 | openProject : openProject, 13 | closeProject : closeProject, 14 | exportProject : exportProject, 15 | }; 16 | return service; 17 | 18 | function getDefaultSettings() { 19 | return $window.b3e.DEFAULT_SETTINGS; 20 | } 21 | function applySettings(settings) { 22 | $window.editor.applySettings(settings); 23 | } 24 | 25 | function newProject() { 26 | $window.editor.project.create(); 27 | } 28 | function openProject(data) { 29 | $window.editor.project.open(data); 30 | } 31 | function closeProject() { 32 | $window.editor.project.close(); 33 | } 34 | 35 | function exportProject() { 36 | return $window.editor.export.projectToData(); 37 | } 38 | } -------------------------------------------------------------------------------- /src/app/services/storage/filestorage.service.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('app') 3 | .factory('fileStorageService', fileStorageService); 4 | 5 | fileStorageService.$inject = ['nodejsService']; 6 | 7 | function fileStorageService(nodejsService) { 8 | var ok = nodejsService.ok; 9 | var fs = nodejsService.fs; 10 | 11 | var service = { 12 | ok : ok, 13 | save : save, 14 | load : load, 15 | remove : remove 16 | }; 17 | return service; 18 | 19 | function save(path, data) { 20 | if (typeof data !== 'string') { 21 | try { data = JSON3.stringify(data); } catch (e) {} 22 | } 23 | 24 | var file = fs.openSync(path, 'w'); 25 | fs.writeSync(file, data); 26 | fs.closeSync(file); 27 | 28 | // Rename must be async to override correctly. 29 | // fs.rename(path+'~', path, function(e) {throw e;}); 30 | } 31 | function load(path) { 32 | var data = fs.readFileSync(path, 'utf-8'); 33 | try { data = JSON3.parse(data); } catch (e) {} 34 | return data; 35 | } 36 | function remove(path) { 37 | 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/pages/editor/modals/import.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |

Import {{import.type}} from {{import.format}}

7 |
8 | 13 |
14 |
15 | 16 |
17 | 22 | 26 | 30 |
31 | 32 |
33 |
-------------------------------------------------------------------------------- /src/app/directives/tabset.directive.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app') 6 | .directive('b3Tabset', tabset); 7 | 8 | function tabset() { 9 | var directive = { 10 | restrict : 'EA', 11 | transclude : true, 12 | replace : true, 13 | scope : {}, 14 | templateUrl : 'directives/tabset.html', 15 | bindToController : true, 16 | controllerAs : 'tabset', 17 | controller : tabsetController, 18 | }; 19 | return directive; 20 | } 21 | 22 | function tabsetController() { 23 | // HEAD // 24 | /* jshint validthis: true */ 25 | var vm = this; 26 | vm.tabs = []; 27 | vm.add = add; 28 | vm.select = select; 29 | 30 | // BODY // 31 | function add(tab) { 32 | vm.tabs.push(tab); 33 | } 34 | 35 | function select(tab) { 36 | angular.forEach(vm.tabs, function(t) { 37 | if (t.active && t !== tab) { 38 | t.active = false; 39 | } 40 | }); 41 | 42 | tab.active = true; 43 | } 44 | } 45 | 46 | 47 | })(); -------------------------------------------------------------------------------- /src/assets/css/preload.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | padding: 0px; 3 | margin: 0px; 4 | overflow: hidden; 5 | line-height: 1.42857143; 6 | } 7 | 8 | .preloading { 9 | position : absolute; 10 | top : 0; 11 | left : 0; 12 | width : 100%; 13 | height : 100%; 14 | color : #999999; 15 | background : #171717; 16 | z-index : 10000000; 17 | text-align : center; 18 | font-size : 16px !important; 19 | font-family : "Raleway", sans-serif !important; 20 | font-weight: 300 !important; 21 | 22 | -webkit-transition : opacity .5s ease-in-out; 23 | -moz-transition : opacity .5s ease-in-out; 24 | -o-transition : opacity .5s ease-in-out; 25 | transition : opacity .5s ease-in-out; 26 | } 27 | .preloading.preload-fade { 28 | opacity: 0; 29 | } 30 | 31 | .preloading-body { 32 | width : 50vw; 33 | margin : 45vh auto; 34 | } 35 | 36 | .preloading-body-title { 37 | font-size : 2.0em; 38 | margin : 0px; 39 | display : block; 40 | } 41 | 42 | .preloading-body-subtitle { 43 | font-size : 0.9em; 44 | opacity : 0.8; 45 | display : block; 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Renato de Pontes Pereira 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. 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Renato de Pontes Pereira", 3 | "name": "behavior3editor", 4 | "version": "0.3.0", 5 | "homepage": "http://behavior3.com", 6 | "license": "MIT", 7 | "bugs": "https://github.com/behavior3/behavior3editor/issues", 8 | "repository": "github:behavior3/behavior3editor", 9 | "scripts": { 10 | "postinstall": "bower i", 11 | "build": "npm install && gulp build", 12 | "deploy-web": "npm run build && gh-pages -d build", 13 | "release": "gulp electron" 14 | }, 15 | "devDependencies": { 16 | "bower": "^1.8.8", 17 | "electron-packager": "^14.1.1", 18 | "electron": "^7.1.3", 19 | "gh-pages": "^2.1.1", 20 | "gulp": "~3.9.0", 21 | "gulp-angular-templatecache": "~1.7.0", 22 | "gulp-concat": "~2.6.0", 23 | "gulp-connect": "~2.2.0", 24 | "gulp-foreach": "^0.1.0", 25 | "gulp-jshint": "~1.11.2", 26 | "gulp-less": "~3.0.3", 27 | "gulp-minify-css": "~1.2.1", 28 | "gulp-minify-html": "~1.0.4", 29 | "gulp-replace": "~0.5.4", 30 | "gulp-uglify": "~1.4.1", 31 | "gulp-zip": "^3.0.2", 32 | "jshint-stylish": "~2.0.1", 33 | "merge-stream": "~1.0.0", 34 | "rimraf": "^2.4.3" 35 | }, 36 | "dependencies": {} 37 | } 38 | -------------------------------------------------------------------------------- /src/app/pages/editor/components/treespanel.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 10 | 11 |
12 |
13 | 29 |
30 |
31 |
-------------------------------------------------------------------------------- /src/app/directives/dropnode.directive.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app') 6 | .directive('b3DropNode', dropNode); 7 | 8 | dropNode.$inject = [ 9 | '$window' 10 | ]; 11 | 12 | function dropNode($window) { 13 | var directive = { 14 | restrict : 'A', 15 | link : link, 16 | }; 17 | return directive; 18 | 19 | function link(scope, element, attrs) { 20 | element.bind('dragover', function(e) { 21 | if (e.preventDefault) { 22 | e.preventDefault(); 23 | } 24 | return false; 25 | }); 26 | element.bind('drop', function(e) { 27 | if (e.preventDefault) { 28 | e.preventDefault(); 29 | } 30 | if (e.stopPropagation) { 31 | e.stopPropagation(); 32 | } 33 | 34 | var name = e.dataTransfer.getData('name'); 35 | 36 | var project = $window.editor.project.get(); 37 | var tree = project.trees.getSelected(); 38 | var point = tree.view.getLocalPoint(e.clientX, e.clientY); 39 | tree.blocks.add(name, point.x, point.y); 40 | 41 | $window.editor._game.canvas.focus(); 42 | }); 43 | } 44 | } 45 | 46 | })(); 47 | -------------------------------------------------------------------------------- /src/app/directives/keytable.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | 13 | 21 | 28 | 35 | 36 | 37 |
5 | 6 | 7 |
20 | 27 | 29 | 34 |
-------------------------------------------------------------------------------- /src/app/services/system.service.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('app') 3 | .factory('systemService', systemService); 4 | 5 | systemService.$inject = ['$window', 'nodejsService']; 6 | 7 | function systemService($window, nodejsService) { 8 | var isDesktop = !!$window.process; 9 | var service = { 10 | isDesktop : isDesktop, 11 | getDataPath : getDataPath, 12 | join : join, 13 | }; 14 | return service; 15 | 16 | function _createIfNonExist(path) { 17 | try { 18 | var s = nodejsService.fs.statSync(path); 19 | } catch (e) { 20 | nodejsService.fs.mkdirSync(path); 21 | } 22 | } 23 | function getDataPath() { 24 | if (isDesktop) { 25 | var datapath = process.env.APPDATA; 26 | if (!datapath) { 27 | datapath = process.env.HOME + '/.behavior3'; 28 | } 29 | 30 | var path = join(datapath, 'b3editor'); 31 | _createIfNonExist(datapath); 32 | _createIfNonExist(path); 33 | return path; 34 | } else { 35 | return 'b3editor'; 36 | } 37 | } 38 | function join() { 39 | if (isDesktop) { 40 | return nodejsService.path.join.apply(nodejsService.path, arguments); 41 | } else { 42 | var s = arguments[0]; 43 | for (var i=1; i 2 |
3 |
4 |
5 |

Export {{export.type}} as {{export.format}}

6 |
7 |
{{export.result||'Loading...'}}
8 |
9 |
10 | 11 |
12 | 17 | 23 | 29 | 34 | 38 |
39 |
40 | -------------------------------------------------------------------------------- /src/assets/less/c_page.less: -------------------------------------------------------------------------------- 1 | 2 | .page { 3 | color: @color-dark1; 4 | overflow-y: auto; 5 | padding-bottom: 100px; 6 | min-width: 850px; 7 | 8 | h1.header { 9 | color: @color-light3; 10 | margin: 0px; 11 | overflow-y: hidden; 12 | padding: 0px; 13 | min-width: @size-sidebar+350px; 14 | padding-left: @padding-large+@size-sidebar; 15 | height: @size-header; 16 | line-height: @size-header*1.1; 17 | font-size: 2.5em; 18 | .font-header; 19 | } 20 | 21 | .content { 22 | max-width: @size-page-content; 23 | min-width: @size-sidebar+350px; 24 | width: 100%; 25 | padding: @padding-large; 26 | padding-left: @padding-large+@size-sidebar; 27 | 28 | &.no-header { padding-top: @size-header; } 29 | .current-project { margin-top: 10px; } 30 | } 31 | 32 | &-operations { 33 | background-color: @color-light1; 34 | border: 1px solid #E7E7E7; 35 | 36 | &-content { 37 | max-width: @size-page-content; 38 | min-width: @size-sidebar+350px; 39 | width: 100%; 40 | padding-right: @padding-large; 41 | padding-left: @size-sidebar; 42 | text-align: right; 43 | ul { 44 | margin: 0px; 45 | padding: 0px; 46 | list-style: none; 47 | li { 48 | display: inline; 49 | a, button { 50 | border: 0px; 51 | display: inline; 52 | // height: 22px; 53 | padding: 3.5px 13px; 54 | cursor: pointer; 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /src/editor/namespaces.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Editor package. 3 | * 4 | * This package is independent from the application, and represent the graph 5 | * (canvas element) and logic part of the editor. For organization purposes, 6 | * the editor is divided into several namespaces: 7 | * 8 | * - **b3e** : contains all base classes, functions and constants; 9 | * - **b3e.draw** : contains functions to draw the block shapes and symbols; 10 | * - **b3e.editor** : contains the editor class, editor-related managers and 11 | * symbols; 12 | * - **b3e.project** : contains the project class and project-related managers; 13 | * - **b3e.tree** : contains the tree class and tree-related managers; 14 | * 15 | * As a general rule, an application has a single editor instance; the editor 16 | * can handle several projects but only a single project can be active in a 17 | * given time; a project can have several trees. 18 | * 19 | * Each project has a set of nodes (default fixed nodes and custom nodes). A 20 | * block is a visual instance of a node. 21 | * 22 | * Also notice that, the Editor, Project, Tree, Block, Connection and 23 | * SelectionBox are all children of `createjs.DisplayObject` or 24 | * `createjs.Container`. 25 | * 26 | */ 27 | 28 | window.b3e = window.b3e || {}; 29 | window.b3e.draw = window.b3e.draw || {}; 30 | window.b3e.editor = window.b3e.editor || {}; 31 | window.b3e.project = window.b3e.project || {}; 32 | window.b3e.tree = window.b3e.tree || {}; 33 | 34 | 35 | window.b3e.VERSION = '[BUILD_VERSION]'; -------------------------------------------------------------------------------- /src/app/services/storage/storage.service.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('app') 3 | .factory('storageService', storageService); 4 | 5 | storageService.$inject = ['$q', 'localStorageService', 'fileStorageService']; 6 | 7 | function storageService($q, localStorageService, fileStorageService) { 8 | var storage = (fileStorageService.ok?fileStorageService:localStorageService); 9 | 10 | var service = { 11 | save : save, 12 | saveAsync : saveAsync, 13 | load : load, 14 | loadAsync : loadAsync, 15 | remove : remove, 16 | removeAsync : removeAsync, 17 | }; 18 | return service; 19 | 20 | function save(path, data) { 21 | storage.save(path, data); 22 | } 23 | function saveAsync(path, data) { 24 | return $q(function(resolve, reject) { 25 | try { 26 | storage.save(path, data); 27 | resolve(); 28 | } catch (e) { 29 | reject(e); 30 | } 31 | }); 32 | } 33 | function load(path) { 34 | return storage.load(path); 35 | } 36 | function loadAsync(path) { 37 | return $q(function(resolve, reject) { 38 | try { 39 | var data = storage.load(path); 40 | resolve(data); 41 | } catch (e) { 42 | reject(e); 43 | } 44 | }); 45 | } 46 | function remove(path) { 47 | storage.remove(path); 48 | } 49 | function removeAsync(path) { 50 | return $q(function(resolve, reject) { 51 | try { 52 | storage.remove(path); 53 | resolve(); 54 | } catch (e) { 55 | reject(e); 56 | } 57 | }); 58 | } 59 | } -------------------------------------------------------------------------------- /src/editor/tree/managers/ViewManager.js: -------------------------------------------------------------------------------- 1 | b3e.tree.ViewManager = function(editor, project, tree) { 2 | "use strict"; 3 | 4 | this.reset = function() { 5 | tree.x = 0; 6 | tree.y = 0; 7 | tree.scaleX = 1; 8 | tree.scaleY = 1; 9 | }; 10 | this.zoom = function(factor) { 11 | tree.scaleX = factor; 12 | tree.scaleY = factor; 13 | }; 14 | this.zoomIn = function() { 15 | var min = editor._settings.get('zoom_min'); 16 | var max = editor._settings.get('zoom_max'); 17 | var step = editor._settings.get('zoom_step'); 18 | 19 | var zoom = tree.scaleX; 20 | this.zoom(tine.clip(zoom+step, min, max)); 21 | }; 22 | this.zoomOut = function() { 23 | var min = editor._settings.get('zoom_min'); 24 | var max = editor._settings.get('zoom_max'); 25 | var step = editor._settings.get('zoom_step'); 26 | 27 | var zoom = tree.scaleX; 28 | this.zoom(tine.clip(zoom-step, min, max)); 29 | }; 30 | this.pan = function(dx, dy) { 31 | tree.x += dx; 32 | tree.y += dy; 33 | }; 34 | this.setCam = function(x, y) { 35 | tree.x = x; 36 | tree.y = y; 37 | }; 38 | this.center = function() { 39 | var canvas = editor._game.canvas; 40 | var hw = canvas.width/2; 41 | var hh = canvas.height/2; 42 | this.setCam(hw, hh); 43 | }; 44 | this.getLocalPoint = function(x, y) { 45 | if (typeof x == 'undefined') x = editor._game.mouse.x; 46 | if (typeof y == 'undefined') y = editor._game.mouse.y; 47 | return tree.globalToLocal(x, y); 48 | }; 49 | 50 | this._applySettings = function(settings) {}; 51 | }; 52 | -------------------------------------------------------------------------------- /BUILD.md: -------------------------------------------------------------------------------- 1 | # Building Behavior3 Editor 2 | 3 | You can build the editor in two different environments: for development and for production. For development you can run a local web server that will build and reload automatically the application for each new modification on the project. The production mode builds and package the editor for different platforms. 4 | 5 | 6 | ## Requirements 7 | 8 | To run the editor you will need the following softwares: 9 | 10 | **required for everything:** 11 | - [NodeJS](https://nodejs.org) 12 | - [Bower](http://bower.io) 13 | 14 | *if you want to run/build the desktop version:* 15 | - [Node-Webkit](http://nwjs.io) 16 | - [Node-Webkit Builder](https://github.com/nwjs/nw-builder) 17 | 18 | 19 | ## Configuration 20 | 21 | Before building, you need to install some 3rd-party libraries. You need to run in console the following commands: 22 | 23 | npm install 24 | 25 | and: 26 | 27 | bower install 28 | 29 | The former installs a bunch of NodeJS modules, which are used on the building system and some dependences of the desktop application. The last installs CSS and Javascript vendor libraries. 30 | 31 | 32 | ## Building during development 33 | 34 | During development you can run the editor in a web browser with automatically building and reloading: 35 | 36 | gulp serve 37 | 38 | which will run a web server hosted on `http://127.0.0.1:8000`. 39 | 40 | To run the desktop version (without automatically building and reload): 41 | 42 | gulp nw 43 | 44 | 45 | ## Building final version 46 | 47 | Just run: 48 | 49 | gulp dist 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/assets/less/c_nodes.less: -------------------------------------------------------------------------------- 1 | .node-list { 2 | .empty { 3 | padding: 0px @padding-small; 4 | color: @color-light2; 5 | } 6 | 7 | &-content { 8 | padding: 0px @padding-small; 9 | } 10 | 11 | &-title { 12 | cursor: pointer; 13 | display: block; 14 | margin-top: 10px; 15 | padding: 2px 5px; 16 | // text-transform: uppercase; 17 | text-transform: capitalize; 18 | color: @color-light3; 19 | // border-bottom: 1px solid @color-dark4; 20 | .font-base; 21 | } 22 | 23 | &-category { 24 | padding: 0px 0px @padding-small-2x 0px; 25 | 26 | ul { 27 | list-style: none; 28 | margin: 0px; 29 | padding: 0px; 30 | 31 | li { 32 | word-break: break-all; 33 | margin-bottom: 5px; 34 | .edit { 35 | float: right; 36 | color: @color-light1; 37 | &:hover { 38 | text-decoration: none; 39 | background-color: @color-yellow; 40 | } 41 | } 42 | .remove { 43 | float: right; 44 | color: @color-light1; 45 | &:hover { 46 | text-decoration: none; 47 | background-color: @color-red; 48 | } 49 | } 50 | a { 51 | padding: 2px @padding-small; 52 | display: block; 53 | color: @color-light1; 54 | &:hover { 55 | background-color: @color-dark2; 56 | } 57 | &.active { 58 | color: @color-blue; 59 | // font-weight: 600; 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/app/pages/settings/settings.controller.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app') 6 | .controller('SettingsController', SettingsController); 7 | 8 | SettingsController.$inject = [ 9 | 'notificationService', 10 | 'settingsModel', 11 | 'dialogService', 12 | ]; 13 | 14 | function SettingsController(notificationService, 15 | settingsModel, 16 | dialogService) { 17 | 18 | // HEADER // 19 | var vm = this; 20 | vm.settings = {}; 21 | vm.saveSettings = saveSettings; 22 | vm.resetSettings = resetSettings; 23 | 24 | _activate(); 25 | 26 | // BODY // 27 | function _activate() { 28 | settingsModel 29 | .getSettings() 30 | .then(function(settings) { 31 | vm.settings = settings; 32 | }); 33 | } 34 | 35 | function saveSettings() { 36 | settingsModel 37 | .saveSettings(vm.settings) 38 | .then(function() { 39 | notificationService.success( 40 | 'Settings saved', 41 | 'The editor settings has been updated.' 42 | ); 43 | }); 44 | } 45 | 46 | function resetSettings() { 47 | dialogService.confirm( 48 | 'Reset Settings?', 49 | 'Are you sure you want to reset to the default settings?' 50 | ).then(function() { 51 | settingsModel 52 | .resetSettings() 53 | .then(function() { 54 | notificationService.success( 55 | 'Settings reseted', 56 | 'The editor settings has been updated to default values.' 57 | ); 58 | _activate(); 59 | }); 60 | }); 61 | } 62 | } 63 | })(); -------------------------------------------------------------------------------- /src/assets/less/c_app.less: -------------------------------------------------------------------------------- 1 | /** 2 | * APP GLOBALS 3 | */ 4 | 5 | html, body { 6 | padding: 0px; 7 | margin: 0px; 8 | overflow: hidden; 9 | font-size: 16px; 10 | color: @color-light1; 11 | background: @color-dark1; 12 | 13 | .font-base; 14 | } 15 | 16 | .grabbing { 17 | cursor: url(../imgs/closedhand.cur); 18 | cursor: url(../imgs/closedhand.cur) 8 8, move; 19 | } 20 | .no-margin { margin: 0px; } 21 | .no-padding { padding: 0px; } 22 | .sweet-alert input { color: @color-dark1; } 23 | .full-height { height: 100%; } 24 | .no-select{ 25 | -webkit-touch-callout: none; 26 | -webkit-user-select: none; 27 | -khtml-user-select: none; 28 | -moz-user-select: none; 29 | -ms-user-select: none; 30 | user-select: none; 31 | } 32 | 33 | a, .btn, .btn-success, .btn-default, .btn-warning, .btn-info, .btn-danger { 34 | .no-select; 35 | } 36 | 37 | .form-control[disabled], 38 | .form-control[readonly], 39 | fieldset[disabled] .form-control { 40 | cursor: inherit; 41 | } 42 | 43 | // LINKS ====================================================================== 44 | a { 45 | color: @color-blue-dark; 46 | transition: color 100ms ease-in-out; 47 | outline: none; 48 | 49 | &:hover, &:visited, &:link, &:active, &:focus { 50 | text-decoration: none; 51 | outline: none; 52 | } 53 | &:hover { 54 | cursor: pointer; 55 | color: @color-blue; 56 | } 57 | } 58 | 59 | // SCROLLBAR ================================================================== 60 | ::-webkit-scrollbar { 61 | width: 10px; 62 | background: none; 63 | } 64 | ::-webkit-scrollbar-track { 65 | background: none; 66 | } 67 | ::-webkit-scrollbar-thumb { 68 | background: @color-blue-light; 69 | } 70 | ::-webkit-scrollbar-thumb:window-inactive { 71 | background: @color-blue-light; 72 | } 73 | 74 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Behavior Tree Visual Editor 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 | Behavior3 Editor 22 | 23 | 24 | Loading... 25 | 26 | 27 |
28 |
29 | 30 | 31 | 32 |
33 |
34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 2 | ## Version 0.3.0 [NEXT] 3 | 4 | - mod: services splitted into models and services. 5 | - mod: new building system and dependence organization. 6 | - mod: version and building date automatically inserted into code. 7 | - mod: minor presentation changes on loading screen and dash home. 8 | - mod: desktop version now uses electron (see #14). 9 | - mod: desktop version working on windows and linux (see #14). 10 | - fix: cut now remove all nodes, and consider connections too (see #9). 11 | - fix: ctrl+z does not affect property inputs anymore (see #10). 12 | - fix: preview not showing in the first drag on Firefox (see #16). 13 | 14 | 15 | ## Version 0.2.0 [Not released] 16 | 17 | **editor**: 18 | 19 | - add: editor import and export custom_node property (see #9/b3js). 20 | - add: editor create custom nodes automatically when importing (see #9/b3js). 21 | - add: interface is using AngularJS now. 22 | - add: editor now support multiple trees, you can change it on tree panel. 23 | - add: history manager (undo/redo feature) (#13). 24 | - add: desktop version using nwjs. 25 | - add: a settings panels (see #6). 26 | - add: node categories can have different colors (see #4). 27 | - add: alt-click to select subtree (see #10). 28 | - add: optional vertical layout (see #5). 29 | - add: editor now can hanble multiple projects (see #15). 30 | - mod: new commands and shortcuts (see #16, #17, #10 and #13). 31 | - mod: new repository. 32 | - mod: new architecture, merging viewer and editor (see #1). 33 | - mod: further updates to the architecture. 34 | - mod: parameters are read-only now, so they cannot be edited (see #2). 35 | - mod: all communication (editor->app) is done by events. 36 | - fix: error when importing json without Display. 37 | - rem: dependence on JQuery and other libraries is removed. 38 | 39 | 40 | ## Version 0.1.0 [Out 27, 2014] 41 | 42 | - initial release. -------------------------------------------------------------------------------- /src/app/pages/editor/components/propertiespanel.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
Properties
4 | 5 |
6 |

Select a single block to change its properties.

7 |

NOTE: The root node represents a tree. Therefore, changes applied to this node will persist on the tree object.

8 |
9 | 10 |
11 |
12 |
13 | 14 | 21 |
22 | 23 |
24 | 25 | 34 |
35 | 36 |
37 | 41 | 42 |
43 |
44 |
45 | 46 |
-------------------------------------------------------------------------------- /src/desktop.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow } = require('electron') 2 | 3 | // Keep a global reference of the window object, if you don't, the window will 4 | // be closed automatically when the JavaScript object is garbage collected. 5 | let mainWindow 6 | 7 | function createWindow () { 8 | // Create the browser window. 9 | mainWindow = new BrowserWindow({ 10 | width: 1400, 11 | height: 1000, 12 | webPreferences: { 13 | nodeIntegration: true 14 | } 15 | }) 16 | 17 | // and load the index.html of the app. 18 | mainWindow.loadFile('index.html') 19 | 20 | // Open the DevTools. 21 | // mainWindow.webContents.openDevTools() 22 | 23 | // Emitted when the window is closed. 24 | mainWindow.on('closed', function () { 25 | // Dereference the window object, usually you would store windows 26 | // in an array if your app supports multi windows, this is the time 27 | // when you should delete the corresponding element. 28 | mainWindow = null 29 | }) 30 | } 31 | 32 | // This method will be called when Electron has finished 33 | // initialization and is ready to create browser windows. 34 | // Some APIs can only be used after this event occurs. 35 | app.on('ready', createWindow) 36 | 37 | // Quit when all windows are closed. 38 | app.on('window-all-closed', function () { 39 | // On macOS it is common for applications and their menu bar 40 | // to stay active until the user quits explicitly with Cmd + Q 41 | if (process.platform !== 'darwin') app.quit() 42 | }) 43 | 44 | app.on('activate', function () { 45 | // On macOS it's common to re-create a window in the app when the 46 | // dock icon is clicked and there are no other windows open. 47 | if (mainWindow === null) createWindow() 48 | }) 49 | 50 | // In this file you can include the rest of your app's specific main process 51 | // code. You can also put them in separate files and require them here. 52 | -------------------------------------------------------------------------------- /src/app/directives/dragnode.directive.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app') 6 | .directive('b3DragNode', dragNode); 7 | 8 | dragNode.$inject = [ 9 | '$window' 10 | ]; 11 | 12 | function dragNode($window) { 13 | var directive = { 14 | restrict : 'A', 15 | link : link, 16 | }; 17 | return directive; 18 | 19 | function link(scope, element, attrs) { 20 | element.attr('draggable', 'true'); 21 | 22 | element.bind('dragstart', function(e) { 23 | var canvas = $window.editor.preview(attrs.name); 24 | var isChrome = navigator.userAgent.toLowerCase().indexOf('chrome')>-1; 25 | 26 | if (isChrome) { 27 | var img = document.createElement('img'); 28 | img.src = canvas.toDataURL(); 29 | 30 | // 10ms delay in order to proper create the image object 31 | // ugly hack =( 32 | var time = (new Date()).getTime(); 33 | var delay = time + 10; 34 | while (time < delay) { 35 | time = (new Date()).getTime(); 36 | } 37 | canvas = img; 38 | } 39 | 40 | e.dataTransfer.setData('name', attrs.name); 41 | e.dataTransfer.setDragImage(canvas, canvas.width/2, canvas.height/2); 42 | }); 43 | } 44 | } 45 | 46 | })(); 47 | 48 | 49 | // .directive('draggableNode', function($window) { 50 | // return { 51 | // restrict: 'A', 52 | // link: function(scope, element, attributes, controller) { 53 | // angular.element(element).attr("draggable", "true"); 54 | // element.bind("dragstart", function(e) { 55 | // var img = $window.app.editor.preview(attributes.id.replace('node-', '')); 56 | 57 | // e.dataTransfer.setData('text', attributes.id); 58 | // e.dataTransfer.setDragImage(img, img.width/2, img.height/2); 59 | // }); 60 | // } 61 | // } 62 | // }) 63 | -------------------------------------------------------------------------------- /src/editor/tree/managers/SelectionManager.js: -------------------------------------------------------------------------------- 1 | b3e.tree.SelectionManager = function(editor, project, tree) { 2 | "use strict"; 3 | 4 | this.select = function(block) { 5 | if (block._isSelected) return; 6 | 7 | block._select(); 8 | tree._selectedBlocks.push(block); 9 | 10 | editor.trigger('blockselected', block); 11 | }; 12 | 13 | this.deselect = function(block) { 14 | if (!block._isSelected) return; 15 | 16 | block._deselect(); 17 | tree._selectedBlocks.remove(block); 18 | 19 | editor.trigger('blockdeselected', block); 20 | }; 21 | 22 | this.selectAll = function() { 23 | tree.blocks.each(function(block) { 24 | this.select(block); 25 | }, this); 26 | }; 27 | 28 | this.deselectAll = function() { 29 | for (var i=tree._selectedBlocks.length-1; i>=0; i--) { 30 | this.deselect(tree._selectedBlocks[i]); 31 | } 32 | }; 33 | 34 | this.invertSelection = function(block) { 35 | var blocks = (block)?[block]:tree.blocks.getAll(); 36 | 37 | blocks.forEach(function(block) { 38 | if (block._isSelected) { 39 | this.deselect(block); 40 | } else { 41 | this.select(block); 42 | } 43 | }, this); 44 | }; 45 | 46 | this.selectSubtree = function(block) { 47 | var blocks = (block)?[block]:tree._selectedBlocks; 48 | var fSelect = function(block) { 49 | blocks.remove(block); 50 | this.select(block); 51 | }; 52 | 53 | while (blocks.length > 0) { 54 | blocks.pop().traversal(fSelect, this); 55 | } 56 | }; 57 | 58 | this.deselectSubtree = function(block) { 59 | var blocks = (block)?[block]:tree._selectedBlocks; 60 | 61 | var fDeselect = function(block) { 62 | blocks.remove(block); 63 | this.deselect(block); 64 | }; 65 | 66 | while (blocks.length > 0) { 67 | blocks.pop().traversal(fDeselect, this); 68 | } 69 | }; 70 | 71 | this._applySettings = function(settings) {}; 72 | 73 | }; 74 | -------------------------------------------------------------------------------- /src/editor/utils/settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Default settings of the editor. 3 | * 4 | * @constant {Object} DEFAULT_SETTINGS 5 | * @memberOf b3e 6 | */ 7 | 8 | (function () { 9 | "use strict"; 10 | 11 | var DEFAULT_SETTINGS = { 12 | // CAMERA 13 | zoom_initial : 1.0, 14 | zoom_min : 0.25, 15 | zoom_max : 2.0, 16 | zoom_step : 0.25, 17 | 18 | // EDITOR 19 | snap_x : 12, 20 | snap_y : 12, 21 | snap_offset_x : 0, 22 | snap_offset_y : 0, 23 | layout : 'horizontal', // vertical 24 | max_history : 100, 25 | 26 | // COLORS 27 | background_color : '#171717', 28 | selection_color : '#4BB2FD', 29 | block_border_color : '#6D6D6D', 30 | block_symbol_color : '#333333', 31 | anchor_background_color : '#EFEFEF', 32 | 33 | connection_color : '#6D6D6D', 34 | root_color : '#AFAFAF', 35 | decorator_color : '#A4CCFB', 36 | composite_color : '#A4CCFB', 37 | tree_color : '#F7CB45', 38 | action_color : '#A3C83F', 39 | condition_color : '#FFFFFF', 40 | 41 | // CONNECTION 42 | connection_width : 2, 43 | 44 | // ANCHOR 45 | anchor_border_width : 2, 46 | anchor_radius : 7, 47 | anchor_offset_x : 4, 48 | anchor_offset_y : 0, 49 | 50 | // BLOCK 51 | block_border_width : 2, 52 | block_root_width : 40, 53 | block_root_height : 40, 54 | block_tree_width : 160, 55 | block_tree_height : 40, 56 | block_composite_width : 40, 57 | block_composite_height : 40, 58 | block_decorator_width : 60, 59 | block_decorator_height : 60, 60 | block_action_width : 160, 61 | block_action_height : 40, 62 | block_condition_width : 160, 63 | block_condition_height : 40, 64 | }; 65 | 66 | b3e.DEFAULT_SETTINGS = DEFAULT_SETTINGS; 67 | })(); 68 | -------------------------------------------------------------------------------- /src/app/app.js: -------------------------------------------------------------------------------- 1 | angular.module('app', [ 2 | 'ui.router', 3 | 'ui.bootstrap', 4 | 'ngAnimate', 5 | 'templates' 6 | ]) 7 | 8 | .run(['$rootScope', '$window', '$state', 9 | function Execute($rootScope, $window, $state) { 10 | $rootScope.isDesktop = !!$window.process && !!$window.require; 11 | 12 | $rootScope.go = function(state, params) { 13 | $state.go(state, params); 14 | }; 15 | } 16 | ]) 17 | 18 | .run(['$window', '$animate', '$location', '$document', '$timeout', 'settingsModel', 'projectModel', 19 | function Execute($window, 20 | $animate, 21 | $location, 22 | $document, 23 | $timeout, 24 | settingsModel, 25 | projectModel) { 26 | 27 | // reset path 28 | $location.path('/'); 29 | 30 | // add drop to canvas 31 | angular 32 | .element($window.editor._game.canvas) 33 | .attr('b3-drop-node', true); 34 | 35 | // initialize editor 36 | settingsModel.getSettings(); 37 | projectModel 38 | .getRecentProjects() 39 | .then(function(projects) { 40 | function closePreload() { 41 | $timeout(function() { 42 | var element = angular.element(document.getElementById('page-preload')); 43 | $animate.addClass(element, 'preload-fade') 44 | .then(function() { 45 | element.remove(); 46 | }); 47 | }, 500); 48 | } 49 | 50 | if (projects.length > 0 && projects[0].isOpen) { 51 | projectModel 52 | .openProject(projects[0].path) 53 | .then(function() { 54 | closePreload(); 55 | }) 56 | .catch(function(e) { 57 | console.error(e); 58 | closePreload(); 59 | }); 60 | } else { 61 | closePreload(); 62 | } 63 | }) 64 | .catch(function(e) { 65 | console.error(e); 66 | closePreload(); 67 | }); 68 | } 69 | ]); 70 | -------------------------------------------------------------------------------- /src/app/app.routes.js: -------------------------------------------------------------------------------- 1 | angular.module('app') 2 | 3 | .config(['$stateProvider', '$urlRouterProvider', 4 | function($stateProvider, $urlRouterProvider) { 5 | $urlRouterProvider.otherwise('/dash/home'); 6 | 7 | $stateProvider 8 | // Dash 9 | .state('dash', { 10 | url: '/dash', 11 | abstract: true, 12 | templateUrl: 'pages/dash/dash.html', 13 | controller: 'DashController', 14 | controllerAs: 'dash', 15 | }) 16 | .state('dash.home', { 17 | url: '/home', 18 | templateUrl: 'pages/home/home.html', 19 | controller: 'HomeController', 20 | controllerAs: 'home', 21 | }) 22 | .state('dash.projects', { 23 | url: "/projects", 24 | templateUrl: 'pages/projects/projects.html', 25 | controller: 'ProjectsController', 26 | controllerAs: 'projects', 27 | }) 28 | .state('dash.settings', { 29 | url: "/settings", 30 | templateUrl: 'pages/settings/settings.html', 31 | controller: 'SettingsController', 32 | controllerAs: 'settings', 33 | }) 34 | 35 | // Editor 36 | .state('editor', { 37 | url: "/editor", 38 | templateUrl: 'pages/editor/editor.html', 39 | controller: 'EditorController', 40 | controllerAs: 'editor', 41 | }) 42 | .state('editor.editnode', { 43 | url: "/node/:name", 44 | templateUrl: 'pages/editor/modals/editnode.html', 45 | controller: 'EditNodeController', 46 | controllerAs: 'editnode', 47 | }) 48 | .state('editor.export', { 49 | url: "/export/:type/:format", 50 | templateUrl: 'pages/editor/modals/export.html', 51 | controller: 'ExportController', 52 | controllerAs: 'export', 53 | }) 54 | .state('editor.import', { 55 | url: "/import/:type/:format", 56 | templateUrl: 'pages/editor/modals/import.html', 57 | controller: 'ImportController', 58 | controllerAs: 'import', 59 | }); 60 | } 61 | ]); -------------------------------------------------------------------------------- /src/app/models/settings.model.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app') 6 | .factory('settingsModel', settingsModel); 7 | 8 | settingsModel.$inject = [ 9 | '$q', 10 | 'storageService', 11 | 'systemService', 12 | 'editorService' 13 | ]; 14 | 15 | function settingsModel($q, 16 | storageService, 17 | systemService, 18 | editorService) { 19 | 20 | // HEADER // 21 | var settingsPath = systemService.join(systemService.getDataPath(), 'settings.json'); 22 | var settingsCache = null; 23 | 24 | var service = { 25 | getSettings : getSettings, 26 | saveSettings : saveSettings, 27 | resetSettings : resetSettings, 28 | }; 29 | return service; 30 | 31 | // BODY // 32 | function getSettings() { 33 | return $q(function(resolve, reject) { 34 | if (!settingsCache) { 35 | var data; 36 | var defaultData = editorService.getDefaultSettings(); 37 | try { 38 | data = storageService.load(settingsPath); 39 | editorService.applySettings(data); 40 | } catch (e) {} 41 | 42 | // Create if storage file does not exist 43 | if (!data) { 44 | data = defaultData; 45 | storageService.save(settingsPath, data); 46 | } 47 | 48 | settingsCache = tine.merge({}, defaultData, data); 49 | } 50 | 51 | resolve(settingsCache); 52 | }); 53 | } 54 | function saveSettings(settings) { 55 | return $q(function(resolve, reject) { 56 | editorService.applySettings(settings); 57 | storageService.save(settingsPath, settings); 58 | settingsCache = settings; 59 | resolve(); 60 | }); 61 | } 62 | function resetSettings() { 63 | return $q(function(resolve, reject) { 64 | var settings = editorService.getDefaultSettings(); 65 | storageService.save(settingsPath, settings); 66 | settingsCache = settings; 67 | editorService.applySettings(settings); 68 | resolve(); 69 | }); 70 | } 71 | } 72 | })(); -------------------------------------------------------------------------------- /src/editor/tree/Tree.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | var Tree = function(editor, project) { 5 | this.Container_constructor(); 6 | 7 | // Variables 8 | this._id = b3.createUUID(); 9 | this._editor = editor; 10 | this._project = project; 11 | this._selectedBlocks = []; 12 | this._selectionBox = null; 13 | this._root = null; 14 | 15 | // Layers 16 | this._blocks = new createjs.Container(); 17 | this._connections = new createjs.Container(); 18 | this._overlay = new createjs.Container(); 19 | 20 | // Managers 21 | this.blocks = null; 22 | this.connections = null; 23 | this.edit = null; 24 | this.selection = null; 25 | this.view = null; 26 | this.organizer = null; 27 | 28 | this._initialize(); 29 | }; 30 | var p = createjs.extend(Tree, createjs.Container); 31 | 32 | p._initialize = function() { 33 | this.blocks = new b3e.tree.BlockManager(this._editor, this._project, this); 34 | this.connections = new b3e.tree.ConnectionManager(this._editor, this._project, this); 35 | this.edit = new b3e.tree.EditManager(this._editor, this._project, this); 36 | this.selection = new b3e.tree.SelectionManager(this._editor, this._project, this); 37 | this.view = new b3e.tree.ViewManager(this._editor, this._project, this); 38 | this.organize = new b3e.tree.OrganizeManager(this._editor, this._project, this); 39 | 40 | this.addChild(this._connections); 41 | this.addChild(this._blocks); 42 | this.addChild(this._overlay); 43 | 44 | this._selectionBox = new b3e.SelectionBox(); 45 | this._overlay.addChild(this._selectionBox); 46 | 47 | this._root = this.blocks.add('root', 0, 0); 48 | this._applySettings(this._editor._settings); 49 | 50 | this.view.center(); 51 | }; 52 | 53 | p._applySettings = function(settings) { 54 | this._selectionBox._applySettings(settings); 55 | 56 | this.blocks._applySettings(settings); 57 | this.connections._applySettings(settings); 58 | this.edit._applySettings(settings); 59 | this.selection._applySettings(settings); 60 | this.view._applySettings(settings); 61 | this.organize._applySettings(settings); 62 | }; 63 | 64 | b3e.tree.Tree = createjs.promote(Tree, 'Container'); 65 | })(); 66 | -------------------------------------------------------------------------------- /src/assets/less/animations.less: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | // NGVIEW 4 | [ui-view].ng-enter, [ui-view].ng-leave { 5 | position:absolute; 6 | height:100%; 7 | width:100%; 8 | .animation-fast; 9 | } 10 | 11 | // APP 12 | [ui-view].app-anim.ng-enter { opacity: 0; } 13 | [ui-view].app-anim.ng-enter-active { opacity: 1; } 14 | [ui-view].app-anim.ng-leave { opacity: 1; } 15 | [ui-view].app-anim.ng-leave-active { opacity: 0; } 16 | 17 | [ui-view].app-anim.ng-enter .sidebar.right, 18 | [ui-view].app-anim.ng-leave .sidebar.right { .animation-fast; } 19 | [ui-view].app-anim.ng-enter .sidebar.right { right: -@size-sidebar; } 20 | [ui-view].app-anim.ng-enter-active .sidebar.right { right: 0; } 21 | [ui-view].app-anim.ng-leave .sidebar.right { right: 0; } 22 | [ui-view].app-anim.ng-leave-active .sidebar.right { right: -@size-sidebar; } 23 | 24 | // DASH 25 | [ui-view].dash-anim.ng-enter { opacity: 0; } 26 | [ui-view].dash-anim.ng-enter-active { opacity: 1; } 27 | [ui-view].dash-anim.ng-leave { opacity: 1; } 28 | [ui-view].dash-anim.ng-leave-active { opacity: 0; } 29 | 30 | // EDITOR 31 | [ui-view].editor-anim.ng-enter .b3modal, 32 | [ui-view].editor-anim.ng-leave .b3modal { .animation-fast; } 33 | [ui-view].editor-anim.ng-enter { opacity: 0; z-index: @z-modal; } 34 | [ui-view].editor-anim.ng-enter-active { opacity: 1; } 35 | [ui-view].editor-anim.ng-leave { opacity: 1; } 36 | [ui-view].editor-anim.ng-leave-active { opacity: 0; } 37 | 38 | [ui-view].editor-anim.ng-enter .b3modal-window, 39 | [ui-view].editor-anim.ng-leave .b3modal-window { 40 | .animation-fast; 41 | transform-origin: left top; 42 | } 43 | // [ui-view].editor-anim.ng-enter .b3modal-window { transform: scale(0.1, 1); } 44 | // [ui-view].editor-anim.ng-enter-active .b3modal-window { transform: scale(1, 1); } 45 | // [ui-view].editor-anim.ng-leave .b3modal-window { transform: scale(1, 1); } 46 | // [ui-view].editor-anim.ng-leave-active .b3modal-window { transform: scale(0.1, 1); } 47 | 48 | 49 | 50 | 51 | // // HIDE 52 | // .anim { .animation } 53 | // .anim.ng-hide { opacity:0; } 54 | 55 | // // INVISIBLE 56 | // .invisible-add, .invisible-remove { .animation-fast } 57 | 58 | // .invisible, .invisible-add.invisible-add-active { opacity: 0; } 59 | // .invisible-remove.css-class-remove-active { opacity: 1; } -------------------------------------------------------------------------------- /src/assets/less/c_notification.less: -------------------------------------------------------------------------------- 1 | @icon-size : 35px; 2 | 3 | .notification { 4 | position: fixed; 5 | bottom: 20px; 6 | right: -@size-sidebar+-@icon-size; 7 | z-index: @z-notification; 8 | opacity: 0; 9 | 10 | width: @size-sidebar+@icon-size; 11 | color: @color-dark1; 12 | transition: all 0.5s ease; 13 | 14 | &:hover { 15 | opacity: 0.7; 16 | } 17 | &.started { 18 | right: 0px; 19 | opacity: 1; 20 | } 21 | &.killed { 22 | opacity: 0; 23 | right: -@size-sidebar+-@icon-size; 24 | } 25 | 26 | &.default { 27 | background-color: @color-light4; 28 | border: 1px solid @color-light4; 29 | border-right: 0px; 30 | .notification-content { 31 | background-color: @color-light2; 32 | } 33 | } 34 | &.error { 35 | background-color: @color-red-dark; 36 | border: 1px solid @color-red-dark; 37 | border-right: 0px; 38 | .notification-content { 39 | background-color: @color-red-light; 40 | } 41 | } 42 | &.success { 43 | background-color: @color-green-dark; 44 | border: 1px solid @color-green-dark; 45 | border-right: 0px; 46 | .notification-content { 47 | background-color: @color-green-light; 48 | } 49 | } 50 | &.warning { 51 | background-color: @color-yellow-dark; 52 | border: 1px solid @color-yellow-dark; 53 | border-right: 0px; 54 | .notification-content { 55 | background-color: @color-yellow-light; 56 | } 57 | } 58 | &.info { 59 | background-color: @color-blue-dark; 60 | border: 1px solid @color-blue-dark; 61 | border-right: 0px; 62 | .notification-content { 63 | background-color: @color-blue-light; 64 | } 65 | } 66 | 67 | 68 | 69 | &-icon { 70 | color: @color-light1; 71 | position: absolute; 72 | top: 50%; 73 | left: 18px; 74 | margin-top: -15px; 75 | font-size: 1.3em; 76 | } 77 | 78 | &-content { 79 | padding: @padding-small @padding-large @padding-small @padding-large; 80 | min-height: 30px; 81 | 82 | &.has-icon { 83 | margin-left: @padding-large+@icon-size; 84 | } 85 | } 86 | 87 | &-title { 88 | font-size: 1em; 89 | font-weight: bold; 90 | margin-bottom: 5px; 91 | } 92 | 93 | &-message { 94 | font-size: 0.85em; 95 | } 96 | } -------------------------------------------------------------------------------- /src/app/pages/editor/modals/import.controller.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app') 6 | .controller('ImportController', ImportController); 7 | 8 | ImportController.$inject = [ 9 | '$scope', 10 | '$window', 11 | '$state', 12 | '$stateParams', 13 | 'dialogService', 14 | 'notificationService', 15 | 'storageService' 16 | ]; 17 | 18 | function ImportController($scope, 19 | $window, 20 | $state, 21 | $stateParams, 22 | dialogService, 23 | notificationService, 24 | storageService) { 25 | var vm = this; 26 | vm.type = null; 27 | vm.format = null; 28 | vm.open = open; 29 | vm.loadFromFile = loadFromFile; 30 | vm.data = ''; 31 | 32 | _active(); 33 | 34 | function _active() { 35 | vm.type = $stateParams.type; 36 | vm.format = $stateParams.format; 37 | } 38 | 39 | function loadFromFile() { 40 | dialogService 41 | .openFile(false, ['.b3', '.json']) 42 | .then(function(path) { 43 | storageService 44 | .loadAsync(path) 45 | .then(function(data) { 46 | vm.data = JSON3.stringify(data, null, 2); 47 | }); 48 | }); 49 | } 50 | function open() { 51 | var i = $window.editor.import; 52 | 53 | var data = JSON3.parse(vm.data); 54 | 55 | try { 56 | if (vm.type === 'project' && vm.format === 'json') { 57 | i.projectAsData(data); 58 | } 59 | else if (vm.type === 'tree' && vm.format === 'json') { 60 | var project = editor.project.get(); 61 | if (!project) throw new Error("cannot find project"); 62 | project.trees.add(data.id); 63 | i.treeAsData(data); 64 | } 65 | else if (vm.type === 'nodes' && vm.format === 'json') { 66 | i.nodesAsData(data); 67 | } 68 | } catch(e) { 69 | notificationService.error( 70 | 'Invalid data', 71 | 'The provided data is invalid.' 72 | ); 73 | } 74 | 75 | $state.go('editor'); 76 | } 77 | } 78 | 79 | })(); -------------------------------------------------------------------------------- /src/app/pages/editor/components/propertiespanel.controller.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app') 6 | .controller('PropertiespanelController', PropertiespanelController); 7 | 8 | PropertiespanelController.$inject = [ 9 | '$scope', 10 | '$window' 11 | ]; 12 | 13 | function PropertiespanelController($scope, 14 | $window) { 15 | var vm = this; 16 | vm.original = null; 17 | vm.block = null; 18 | vm.update = update; 19 | vm.keydown = keydown; 20 | 21 | _create(); 22 | _activate(); 23 | 24 | $scope.$on('$destroy', _destroy); 25 | 26 | function _activate() { 27 | var p = $window.editor.project.get(); 28 | var t = p.trees.getSelected(); 29 | var s = t.blocks.getSelected(); 30 | 31 | if (s.length === 1) { 32 | vm.original = s[0]; 33 | vm.block = { 34 | title : vm.original.title, 35 | description : vm.original.description, 36 | properties : tine.merge({}, vm.original.properties) 37 | }; 38 | } else { 39 | vm.original = false; 40 | vm.block = false; 41 | } 42 | } 43 | function _event(e) { 44 | setTimeout(function() {$scope.$apply(function() { _activate(); });}, 0); 45 | 46 | } 47 | function _create() { 48 | $window.editor.on('blockselected', _event); 49 | $window.editor.on('blockdeselected', _event); 50 | $window.editor.on('blockremoved', _event); 51 | $window.editor.on('treeselected', _event); 52 | $window.editor.on('nodechanged', _event); 53 | } 54 | function _destroy() { 55 | $window.editor.off('blockselected', _event); 56 | $window.editor.off('blockdeselected', _event); 57 | $window.editor.off('blockremoved', _event); 58 | $window.editor.off('treeselected', _event); 59 | $window.editor.off('nodechanged', _event); 60 | } 61 | 62 | function keydown(e) { 63 | if (e.ctrlKey && e.keyCode == 90) { 64 | e.preventDefault(); 65 | } 66 | 67 | return false; 68 | } 69 | 70 | function update() { 71 | var p = $window.editor.project.get(); 72 | var t = p.trees.getSelected(); 73 | t.blocks.update(vm.original, vm.block); 74 | } 75 | } 76 | })(); -------------------------------------------------------------------------------- /src/app/pages/editor/modals/editnode.controller.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | angular.module("app").controller("EditNodeController", EditNodeController); 5 | 6 | EditNodeController.$inject = [ 7 | "$scope", 8 | "$window", 9 | "$state", 10 | "$stateParams", 11 | "dialogService", 12 | "notificationService" 13 | ]; 14 | 15 | function EditNodeController( 16 | $scope, 17 | $window, 18 | $state, 19 | $stateParams, 20 | dialogService, 21 | notificationService 22 | ) { 23 | var vm = this; 24 | vm.action = "New"; 25 | vm.node = null; 26 | vm.denylist = null; 27 | vm.original = null; 28 | vm.save = save; 29 | vm.remove = remove; 30 | 31 | _active(); 32 | 33 | function _active() { 34 | var p = $window.editor.project.get(); 35 | 36 | if ($stateParams.name) { 37 | var node = p.nodes.get($stateParams.name); 38 | vm.node = node.copy(); 39 | vm.original = node; 40 | vm.action = "Update"; 41 | } else { 42 | vm.node = new b3e.Node(); 43 | vm.node.category = "composite"; 44 | } 45 | 46 | var denylist = []; 47 | p.nodes.each(function(node) { 48 | if (node.name !== vm.node.name) { 49 | denylist.push(node.name); 50 | } 51 | }); 52 | vm.denylist = denylist.join(","); 53 | } 54 | 55 | function save() { 56 | var p = $window.editor.project.get(); 57 | 58 | if (vm.original) { 59 | p.nodes.update(vm.original, vm.node); 60 | } else { 61 | p.nodes.add(vm.node); 62 | } 63 | 64 | $state.go("editor"); 65 | notificationService.success( 66 | "Node created", 67 | "Node has been created successfully." 68 | ); 69 | } 70 | 71 | function remove() { 72 | dialogService 73 | .confirm( 74 | "Remove node?", 75 | "Are you sure you want to remove this node?\n\nNote: all blocks using this node will be removed." 76 | ) 77 | .then(function() { 78 | var p = $window.editor.project.get(); 79 | p.nodes.remove(vm.original); 80 | notificationService.success( 81 | "Node removed", 82 | "The node has been removed from this project." 83 | ); 84 | $state.go("editor"); 85 | }); 86 | } 87 | } 88 | })(); 89 | 90 | -------------------------------------------------------------------------------- /src/editor/tree/managers/ConnectionManager.js: -------------------------------------------------------------------------------- 1 | b3e.tree.ConnectionManager = function(editor, project, tree) { 2 | "use strict"; 3 | 4 | /** Needed to history manager */ 5 | this._remove = function(block) { 6 | project.history._lock(); 7 | this.remove(block._inConnection); 8 | project.history._unlock(); 9 | }; 10 | 11 | this.add = function(inBlock, outBlock) { 12 | var connection = new b3e.Connection(); 13 | 14 | if (inBlock) { 15 | connection._inBlock = inBlock; 16 | inBlock._outConnections.push(connection); 17 | 18 | editor.trigger('blockconnected', inBlock, { 19 | connection: connection, 20 | type: 'outConnection', 21 | other: outBlock, 22 | }); 23 | } 24 | 25 | if (outBlock) { 26 | connection._outBlock = outBlock; 27 | outBlock._inConnection = connection; 28 | 29 | editor.trigger('blockconnected', outBlock, { 30 | connection: connection, 31 | type: 'inConnection', 32 | other: inBlock, 33 | }); 34 | } 35 | 36 | if (inBlock && outBlock) { 37 | var _old = [this, this._remove, [outBlock]]; 38 | var _new = [this, this.add, [inBlock, outBlock]]; 39 | project.history._add(new b3e.Command(_old, _new)); 40 | } 41 | 42 | connection._applySettings(editor._settings); 43 | tree._connections.addChild(connection); 44 | 45 | // editor.trigger('connectionadded', connection); 46 | return connection; 47 | }; 48 | 49 | this.remove = function(connection) { 50 | if (connection._inBlock && connection._outBlock) { 51 | var _old = [this, this.add, [connection._inBlock, connection._outBlock]]; 52 | var _new = [this, this._remove, [connection._outBlock]]; 53 | project.history._add(new b3e.Command(_old, _new)); 54 | } 55 | 56 | if (connection._inBlock) { 57 | connection._inBlock._outConnections.remove(connection); 58 | connection._inBlock = null; 59 | } 60 | 61 | if (connection._outBlock) { 62 | connection._outBlock._inConnection = null; 63 | connection._outBlock = null; 64 | } 65 | 66 | tree._connections.removeChild(connection); 67 | editor.trigger('connectionremoved', connection); 68 | }; 69 | this.each = function(callback, thisarg) { 70 | tree._connections.children.forEach(callback, thisarg); 71 | }; 72 | 73 | this._applySettings = function(settings) { 74 | this.each(function(connection) { 75 | connection._applySettings(settings); 76 | }); 77 | }; 78 | }; 79 | -------------------------------------------------------------------------------- /src/app/pages/projects/projects.html: -------------------------------------------------------------------------------- 1 |
2 |

Projects

3 | 4 | 12 | 13 |
14 | 15 | 16 |
17 |

You don't have any project yet.

18 | 19 |
20 | 21 | 22 | 23 | 24 | 36 | 44 | 45 |
25 |
26 | 27 | 28 | 29 | 30 |
31 | 32 | Current project 33 |

{{item.name}}

34 | {{item.path}} 35 |
37 |
38 | 39 | 40 |
41 |

{{item.name}}

42 | {{item.path}} 43 |
46 | 47 |
48 |
49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Behavior Tree Editor 2 | 3 | This tool has been adapted for the [Bot Testing Framework](https://hexdocs.pm/bot_army/1.0.0/readme.html) based on the existing [Behavior3 Editor](https://github.com/behavior3/behavior3editor/). 4 | 5 | > See the [Releases tab](https://github.com/adobe/behavior_tree_editor/releases) to download a native (Mac only) application that will save and load flies from disk (recommended). 6 | > 7 | > You can also access a [web app](https://opensource.adobe.com/behavior_tree_editor/#/dash/home) version. Your data will be saved in your browser's local storage. Use "Project > Import > Project as JSON" and "Project > Export > Project as JSON" to load/save from/to a local source controlled .json file for running the tests. 8 | 9 | ![interface preview](preview.png) 10 | 11 | ## Basic usage 12 | 13 | - Drag nodes from the left sidebar, drag the node "handles" to connect nodes 14 | - Press "a" to auto organize the tree 15 | - Make new trees under "Project/New tree" or hover over "Trees" side bar divider 16 | - You can drag the tree names just like other nodes to nest trees 17 | - You must name one tree "Root" (the name of the tree is set via the title of the tree's root node) 18 | - Shift+click to pan the view (or middle mouse button) 19 | - Del key (fn+delete on a macbook) deletes a node 20 | - Each node has details/instructions in its description 21 | - Be sure to save via the menu icon or cntr+s or "Project/Save project" 22 | - You can create custom action nodes via "Project/New node" or the Nodes sidebar divider. They should have a unique name, the "actions" category, and a title in the same format as the generic function action. 23 | - See [`mix bots.extract_actions`](https://hexdocs.pm/bot_army/1.0.0/Mix.Tasks.Bots.ExtractActions.html) on how to import custom actions from your code base. 24 | - Using "" in a node's title will render the value for that property's key 25 | - You can use "templates" in titles and properties to reference properties on the tree's root node. Templates look like "{{key_name}}". 26 | 27 | ## Main features 28 | 29 | - **Custom Nodes**: you can create your own node types inside one of the three basic categories - _composite_, _decorator_, _action_. 30 | - **Individual Node Properties**: you can modify node titles, description and custom properties. 31 | - **Manual and Auto Organization**: organize by dragging nodes around or just type "a" to auto organize the whole tree. 32 | - **Create and Manage Multiple Trees**: you can create and manage an unlimited number of trees. 33 | - **Import and Export to JSON**: export your project, tree or nodes to JSON format. 34 | - Import them back. Use JSON on your own custom library or tool. You decide. 35 | -------------------------------------------------------------------------------- /src/app/pages/editor/modals/export.controller.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app') 6 | .controller('ExportController', ExportController); 7 | 8 | ExportController.$inject = [ 9 | '$scope', 10 | '$document', 11 | '$window', 12 | '$stateParams', 13 | 'dialogService', 14 | 'notificationService', 15 | 'storageService' 16 | ]; 17 | 18 | function ExportController($scope, 19 | $document, 20 | $window, 21 | $stateParams, 22 | dialogService, 23 | notificationService, 24 | storageService) { 25 | var vm = this; 26 | vm.type = null; 27 | vm.format = null; 28 | vm.compact = ''; 29 | vm.pretty = ''; 30 | vm.result = null; 31 | vm.data = null; 32 | vm.hideCompact = false; 33 | vm.showCompact = showCompact; 34 | vm.showPretty = showPretty; 35 | vm.select = select; 36 | vm.save = save; 37 | 38 | _active(); 39 | 40 | function _active() { 41 | vm.type = $stateParams.type; 42 | vm.format = $stateParams.format; 43 | 44 | var e = $window.editor.export; 45 | 46 | if (vm.type === 'project' && vm.format === 'json') { 47 | _createJson(e.projectToData()); 48 | } 49 | else if (vm.type === 'tree' && vm.format === 'json') { 50 | _createJson(e.treeToData()); 51 | } 52 | else if (vm.type === 'nodes' && vm.format === 'json') { 53 | _createJson(e.nodesToData()); 54 | } 55 | } 56 | 57 | function _createJson(data) { 58 | vm.data = data; 59 | vm.compact = JSON3.stringify(data); 60 | vm.pretty = JSON3.stringify(data, null, 2); 61 | vm.result = vm.pretty; 62 | } 63 | 64 | function select(){ 65 | var range = $document[0].createRange(); 66 | range.selectNodeContents($document[0].getElementById('export-result')); 67 | var sel = $window.getSelection(); 68 | sel.removeAllRanges(); 69 | sel.addRange(range); 70 | } 71 | 72 | function save() { 73 | dialogService 74 | .saveAs(null, ['.json']) 75 | .then(function(path) { 76 | storageService 77 | .saveAsync(path, vm.pretty) 78 | .then(function() { 79 | notificationService.success( 80 | 'File saved', 81 | 'The file has been saved successfully.' 82 | ); 83 | }); 84 | }); 85 | } 86 | 87 | function showCompact() { 88 | vm.result = vm.compact; 89 | } 90 | function showPretty() { 91 | vm.result = vm.pretty; 92 | } 93 | } 94 | 95 | })(); 96 | -------------------------------------------------------------------------------- /src/assets/less/c_sidebar.less: -------------------------------------------------------------------------------- 1 | .sidebar-stub { 2 | position: absolute; 3 | top: 0px; 4 | width: @size-sidebar; 5 | height: 100%; 6 | z-index: 1; 7 | background-color: @color-dark3; 8 | } 9 | 10 | .sidebar { 11 | overflow-y: auto; 12 | position: absolute; 13 | top: 0px; 14 | width: @size-sidebar; 15 | height: 100%; 16 | z-index: @z-sidebar; 17 | color: @color-light3; 18 | background-color: @color-dark3; 19 | .box-shadow(0px 0px 16px 0px); 20 | 21 | &.left { left: 0px; } 22 | &.right { right: 0px; } 23 | &::-webkit-scrollbar { 24 | width: 3px; 25 | background: none; 26 | 27 | &-track { background: none; } 28 | &-thumb { background: @color-blue; } 29 | &-thumb:window-inactive { background: @color-blue; } 30 | } 31 | 32 | .title { 33 | color: @color-dark1; 34 | list-style: none; 35 | background-color: @color-dark2+#222; 36 | margin: @padding-medium 0px @padding-small-2x 0px; 37 | padding: 0px @padding-small-2x; 38 | text-align: left; 39 | text-transform: capitalize; 40 | font-size: 1.2em; 41 | .font-base; 42 | 43 | .new { 44 | margin-top: 3px; 45 | float: right; 46 | color: @color-light1; 47 | &:hover { 48 | text-decoration: none; 49 | background-color: @color-green; 50 | } 51 | } 52 | 53 | a { 54 | color: @color-dark1; 55 | display: block; 56 | } 57 | } 58 | 59 | .header-button { 60 | color: @color-light3; 61 | padding-left: @padding-large; 62 | height: @size-header; 63 | line-height: @size-header*1.1; 64 | font-size: 1.5em; 65 | background-color: @color-dark2; 66 | border-bottom: 1px solid @color-dark4; 67 | .font-header; 68 | 69 | a { 70 | display: block; 71 | color: @color-light2; 72 | } 73 | } 74 | 75 | .content { 76 | padding: @padding-medium 0px; 77 | 78 | &.content.no-header { padding-top: @size-header; } 79 | &.content.has-menubar { padding-top: @size-menubar; } 80 | 81 | } 82 | } 83 | 84 | .side-panel { 85 | .panel-operations { 86 | border-top: 1px solid @color-light4; 87 | border-bottom: 1px solid @color-light4; 88 | position: absolute; 89 | width:100%; 90 | bottom: 0px; 91 | background-color: @color-dark2; 92 | 93 | &-content { 94 | width: 100%; 95 | text-align: right; 96 | ul { 97 | margin: 0px; 98 | padding: 0px; 99 | list-style: none; 100 | li { 101 | display: inline; 102 | a { 103 | display: inline-block; 104 | padding: 3.5px 12px; 105 | cursor: pointer; 106 | } 107 | } 108 | } 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /src/assets/less/variables.less: -------------------------------------------------------------------------------- 1 | // COLORS ===================================================================== 2 | @color-dark1 : #0B0B0B; 3 | @color-dark2 : #333333; 4 | @color-dark3 : #2F2F2F; 5 | @color-dark4 : #454545; 6 | @color-light1 : #FFFFFF; 7 | @color-light2 : #CECECE; 8 | @color-light3 : #999999; 9 | @color-light4 : #545454; 10 | @color-blue : #5bc0de; 11 | @color-red : #D9534F; 12 | @color-yellow : #D58512; 13 | @color-green : #398439; 14 | @color-blue-light : lighten(@color-blue, 10%); 15 | @color-blue-dark : darken(@color-blue, 30%); 16 | @color-red-light : lighten(@color-red, 10%); 17 | @color-red-dark : darken(@color-red, 30%); 18 | @color-yellow-light : lighten(@color-yellow, 10%); 19 | @color-yellow-dark : darken(@color-yellow, 30%); 20 | @color-green-light : lighten(@color-green, 10%); 21 | @color-green-dark : darken(@color-green, 30%); 22 | 23 | // LAYER DEPTH ================================================================ 24 | @z-editor: 0; 25 | @z-page: 10; 26 | @z-sidebar: 20; 27 | @z-menubar: 30; 28 | @z-modal: 40; 29 | @z-notification: 50; 30 | 31 | // SIZES ====================================================================== 32 | @size-sidebar: 250px; 33 | @size-menubar: 35px; 34 | @size-page-content: 700px+@size-sidebar; 35 | 36 | @size-header: 100px; 37 | 38 | // PADDING ==================================================================== 39 | @padding-small: 5px; 40 | @padding-small-2x: 10px; 41 | @padding-medium: 15px; 42 | @padding-large: 25px; 43 | @padding-huge: 35px; 44 | @padding-large-2x: 25px; 45 | @padding-huge-2x: 70px; 46 | 47 | 48 | // FONTS ====================================================================== 49 | .font-base { 50 | font-family: sans-serif; 51 | font-weight: 400; 52 | } 53 | 54 | .font-header { 55 | font-family: sans-serif; 56 | font-weight: 300; 57 | } 58 | 59 | .box-shadow(@style) { 60 | -webkit-box-shadow: @style @color-dark1; 61 | box-shadow: @style @color-dark1; 62 | } 63 | 64 | // ANIMATIONS ================================================================= 65 | .animation-slow { 66 | -webkit-transition : all .50s ease-in-out; 67 | -moz-transition : all .50s ease-in-out; 68 | -o-transition : all .50s ease-in-out; 69 | transition : all .50s ease-in-out; 70 | } 71 | 72 | .animation { 73 | -webkit-transition : all .30s ease-in-out; 74 | -moz-transition : all .30s ease-in-out; 75 | -o-transition : all .30s ease-in-out; 76 | transition : all .30s ease-in-out; 77 | } 78 | 79 | .animation-fast { 80 | -webkit-transition : all .15s ease-in-out; 81 | -moz-transition : all .15s ease-in-out; 82 | -o-transition : all .15s ease-in-out; 83 | transition : all .15s ease-in-out; 84 | } -------------------------------------------------------------------------------- /src/editor/utils/Command.js: -------------------------------------------------------------------------------- 1 | /** @module b3e */ 2 | 3 | (function() { 4 | 'use strict'; 5 | 6 | /** 7 | * Represents a command for the history manager. Each command must have an 8 | * undo and a redo specification. The specification must have the following 9 | * format: 10 | * 11 | * var spec = [thisarg, methodOrFunction, params] 12 | * 13 | * For example: 14 | * 15 | * var undo = [this, this.add, [block]]; 16 | * var redo = [this, this.remove, [block]]; 17 | * var command = new b3e.Command(undo, redo); 18 | * 19 | * @class Command 20 | * @param {Array} undo The undo specification. 21 | * @param {Array} redo The redo specification. 22 | * @constructor 23 | */ 24 | b3e.Command = function(undo, redo) { 25 | 26 | if (undo.length !== 3) throw 'Invalid undo command, must have [target, method, args]'; 27 | if (redo.length !== 3) throw 'Invalid redo command, must have [target, method, args]'; 28 | 29 | function execute(target, method, args) { 30 | method.apply(target, args); 31 | } 32 | 33 | /** 34 | * The tree that is selected in the moment of command is added to the 35 | * history manager. This is set by the manager. 36 | * 37 | * @property {b3e.tree.Tree} context; 38 | */ 39 | this.context = null; 40 | 41 | /** 42 | * Execute the redo command. 43 | * 44 | * @method redo 45 | */ 46 | this.redo = function() { 47 | execute(redo[0], redo[1], redo[2]); 48 | }; 49 | 50 | /** 51 | * Execute the undo command. 52 | * 53 | * @method undo 54 | */ 55 | this.undo = function() { 56 | execute(undo[0], undo[1], undo[2]); 57 | }; 58 | }; 59 | 60 | /** 61 | * A list of commands created by the history manager. 62 | * 63 | * @class Command 64 | * @param {Array} undo The undo specification. 65 | * @param {Array} redo The redo specification. 66 | * @constructor 67 | */ 68 | b3e.Commands = function(commands) { 69 | 70 | /** 71 | * The tree that is selected in the moment of command is added to the 72 | * history manager. This is set by the manager. 73 | * 74 | * @property {b3e.tree.Tree} context; 75 | */ 76 | this.context = null; 77 | 78 | /** 79 | * Execute the redo command. 80 | * 81 | * @method redo 82 | */ 83 | this.redo = function() { 84 | for (var i=0; i=0; i--) { 96 | commands[i].undo(); 97 | } 98 | }; 99 | }; 100 | })(); -------------------------------------------------------------------------------- /src/app/pages/editor/components/treespanel.controller.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app') 6 | .controller('TreespanelController', TreespanelController); 7 | 8 | TreespanelController.$inject = [ 9 | '$scope', 10 | '$window', 11 | 'dialogService', 12 | 'notificationService' 13 | ]; 14 | 15 | function TreespanelController($scope, 16 | $window, 17 | dialogService, 18 | notificationService) { 19 | 20 | // HEAD // 21 | var vm = this; 22 | vm.trees = null; 23 | vm.newTree = newTree; 24 | vm.select = select; 25 | vm.remove = remove; 26 | 27 | _create(); 28 | _activate(); 29 | $scope.$on('$destroy', _destroy); 30 | 31 | // BODY // 32 | function _activate() { 33 | vm.trees = []; 34 | 35 | var p = $window.editor.project.get(); 36 | var selected = p.trees.getSelected(); 37 | p.trees.each(function(tree) { 38 | var root = tree.blocks.getRoot(); 39 | vm.trees.push({ 40 | 'id' : tree._id, 41 | 'name' : root.title || 'A behavior tree', 42 | 'active' : tree===selected, 43 | }); 44 | }); 45 | } 46 | 47 | function _event(e) { 48 | if (e.type !== 'blockchanged' || e._target.category === 'root') { 49 | if (!$scope.$$phase) { 50 | $scope.$apply(function() { _activate(); }); 51 | } else { 52 | _activate(); 53 | } 54 | } 55 | } 56 | 57 | function _create() { 58 | $window.editor.on('blockchanged', _event); 59 | $window.editor.on('treeselected', _event); 60 | $window.editor.on('treeremoved', _event); 61 | $window.editor.on('treeimported', _event); 62 | } 63 | 64 | function _destroy() { 65 | $window.editor.off('blockchanged', _event); 66 | $window.editor.off('treeselected', _event); 67 | $window.editor.off('treeremoved', _event); 68 | $window.editor.off('treeimported', _event); 69 | } 70 | 71 | function newTree() { 72 | var p = $window.editor.project.get(); 73 | p.trees.add(); 74 | } 75 | 76 | function select(id) { 77 | var p = $window.editor.project.get(); 78 | p.trees.select(id); 79 | } 80 | 81 | function remove(id) { 82 | dialogService. 83 | confirm( 84 | 'Remove tree?', 85 | 'Are you sure you want to remove this tree?\n\nNote: all blocks using this tree will be removed.' 86 | ).then(function() { 87 | var p = $window.editor.project.get(); 88 | p.trees.remove(id); 89 | notificationService.success( 90 | 'Tree removed', 91 | 'The tree has been removed from this project.' 92 | ); 93 | }); 94 | 95 | } 96 | } 97 | })(); -------------------------------------------------------------------------------- /src/app/directives/keytable.directive.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app') 6 | .directive('b3KeyTable', keytable) 7 | .controller('KeyTableController', KeyTableController); 8 | 9 | keytable.$inject = ['$parse']; 10 | function keytable($parse) { 11 | var directive = { 12 | require : '^ngModel', 13 | restrict : 'EA', 14 | replace : true, 15 | bindToController : true, 16 | controller : 'KeyTableController', 17 | controllerAs : 'keytable', 18 | templateUrl : 'directives/keytable.html', 19 | link: link 20 | }; 21 | return directive; 22 | 23 | function link(scope, element, attrs) { 24 | // get the value of the `ng-model` attribute 25 | scope.keytable.heading = attrs.heading; 26 | scope.keytable._onChange = $parse(attrs.ngChange); 27 | 28 | var variable = attrs.ngModel; 29 | scope.$watch(variable, function(model) { 30 | scope.keytable.reset(model); 31 | }); 32 | } 33 | } 34 | 35 | KeyTableController.$inject = ['$scope']; 36 | function KeyTableController($scope) { 37 | // HEAD // 38 | var vm = this; 39 | vm._onChange = null; 40 | vm.model = $scope.keytable.model || $scope.model || null; 41 | vm.rows = []; 42 | vm.add = add; 43 | vm.remove = remove; 44 | vm.change = change; 45 | vm.reset = reset; 46 | 47 | _activate(); 48 | 49 | // BODY // 50 | function _activate() { 51 | if (vm.model) { 52 | for (var key in vm.model) { 53 | add(key, vm.model[key], false); 54 | } 55 | } else { 56 | vm.model = {}; 57 | } 58 | } 59 | 60 | function reset(model) { 61 | vm.rows = []; 62 | vm.model = model; 63 | _activate(); 64 | } 65 | 66 | function add(key, value, fixed) { 67 | vm.rows.push({key:key, value:value, fixed:fixed===true}); 68 | } 69 | 70 | function remove(i) { 71 | delete vm.model[vm.rows[i].key]; 72 | vm.rows.splice(i, 1); 73 | 74 | if (vm._onChange) { 75 | vm._onChange($scope); 76 | } 77 | } 78 | 79 | function change() { 80 | for (var key in vm.model){ 81 | if (vm.model.hasOwnProperty(key)){ 82 | delete vm.model[key]; 83 | } 84 | } 85 | for (var i=0; i 2 |

Welcome to the Behavior Tree Visual Editor

3 | 4 |
5 | 6 |

A tool to make it easier to build behavior trees for the Bot Testing Framework.

7 | 8 |

This tool was adapted from the existing Behavior3 Editor

9 | 10 |

Basic usage

11 |
    12 |
  • Drag nodes from the left sidebar, drag the node "handles" to connect nodes
  • 13 |
  • Press "a" to auto organize the tree
  • 14 |
  • Make new trees under "Project/New tree" or hover over "Trees" side bar divider
  • 15 |
  • You can drag the tree names just like other nodes to nest trees
  • 16 |
  • You must name one tree "Root" (the name of the tree is set via the title of the tree's root node)
  • 17 |
  • Shift+click to pan the view (or middle mouse button)
  • 18 |
  • Del key (fn+delete on a macbook) deletes a node
  • 19 |
  • Each node has details/instructions in its description
  • 20 |
  • Be sure to save via the menu icon or cntr+s or "Project/Save project"
  • 21 |
  • You can create custom action nodes via "Project/New node" or the Nodes sidebar divider. They should have a unique name, the "actions" category, and a title in the same format as the generic function action.
  • 22 |
  • See mix bots.extract_actions on how to import custom actions from your code base.
  • 23 |
  • Using "<key_name>" in a node's title will render the value for that property's key
  • 24 |
  • You can use "templates" in titles and properties to reference properties on the tree's root node. Templates look like {{key_name}}.
  • 25 |
26 | 27 | 55 | 56 |
57 | 58 | -------------------------------------------------------------------------------- /src/editor/project/managers/HistoryManager.js: -------------------------------------------------------------------------------- 1 | b3e.project.HistoryManager = function(editor, project) { 2 | "use strict"; 3 | 4 | var queue = []; 5 | var index = 0; 6 | var lockRequests = 0; 7 | var batchRequests = 0; 8 | var commandBuffer = []; 9 | 10 | this.clear = function() { 11 | queue = []; 12 | index = 0; 13 | }; 14 | this.undo = function() { 15 | this._lock(); 16 | if (this.canUndo()) { 17 | index--; 18 | queue[index].undo(); 19 | project.trees.select(queue[index].context); 20 | } 21 | this._unlock(); 22 | 23 | editor._dirty--; 24 | }; 25 | this.redo = function() { 26 | this._lock(); 27 | if (this.canRedo()) { 28 | queue[index].redo(); 29 | project.trees.select(queue[index].context); 30 | index++; 31 | } 32 | this._unlock(); 33 | 34 | editor._dirty++; 35 | }; 36 | this.canUndo = function() { 37 | return index>0; 38 | }; 39 | this.canRedo = function() { 40 | return index 0) return; 50 | 51 | // Crear all after index 52 | if (queue.length > index) { 53 | queue.splice(index, queue.length-index); 54 | } 55 | 56 | // Add instruction 57 | if (batchRequests > 0) { 58 | commandBuffer.push(command); 59 | } else { 60 | index++; 61 | command.context = project.trees.getSelected(); 62 | queue.push(command); 63 | 64 | if (editor._dirty < 0) editor._dirty = 0; 65 | editor._dirty++; 66 | } 67 | 68 | // Clear excess 69 | var max = editor._settings.get('max_history'); 70 | if (queue.length > max) { 71 | queue.splice(0, 1); 72 | } 73 | }; 74 | 75 | /** 76 | * Lock the manager, so it can't receive more commands. 77 | */ 78 | this._lock = function() { 79 | // if (lockRequests===0) console.log('------- LOCK -------'); 80 | lockRequests++; 81 | }; 82 | this._unlock = function() { 83 | lockRequests--; 84 | // if (lockRequests===0) console.log('------- UNLOCK -------'); 85 | }; 86 | 87 | /** 88 | * While in batch, merges all added commands to a single command 89 | */ 90 | this._beginBatch = function() { 91 | batchRequests++; 92 | }; 93 | this._endBatch = function() { 94 | batchRequests = Math.max(0, batchRequests-1); 95 | 96 | if (batchRequests === 0) { 97 | if (commandBuffer.length > 0) { 98 | var command = new b3e.Commands(commandBuffer); 99 | command.context = project.trees.getSelected(); 100 | this._add(command); 101 | } 102 | commandBuffer = []; 103 | } 104 | }; 105 | 106 | 107 | this._applySettings = function(settings) { 108 | var max = settings.get('max_history'); 109 | if (queue.length > max) { 110 | queue.splice(0, queue.length-max); 111 | } 112 | }; 113 | }; -------------------------------------------------------------------------------- /src/editor/editor/systems/SelectionSystem.js: -------------------------------------------------------------------------------- 1 | b3e.editor.SelectionSystem = function(editor) { 2 | "use strict"; 3 | 4 | var isSelecting = false; 5 | var ctrl = false; 6 | var shift = false; 7 | var alt = false; 8 | var x0 = 0; 9 | var y0 = 0; 10 | 11 | this.update = function(delta) {}; 12 | 13 | this.onMouseDown = function(e) { 14 | var project = editor.project.get(); 15 | if (!project) return; 16 | 17 | var tree = project.trees.getSelected(); 18 | if (!tree) return; 19 | 20 | // mouse left 21 | if (e.nativeEvent.which !== 1) return; 22 | ctrl = e.nativeEvent.ctrlKey; 23 | shift = e.nativeEvent.shiftKey; 24 | alt = e.nativeEvent.altKey; 25 | 26 | // if clicked on block 27 | var point = tree.view.getLocalPoint(); 28 | var x = point.x; 29 | var y = point.y; 30 | var block = tree.blocks.getUnderPoint(x, y); 31 | 32 | if (block && block._isSelected && ctrl) { 33 | if (alt) { 34 | tree.selection.deselectSubtree(block); 35 | } else { 36 | tree.selection.deselect(block); 37 | } 38 | } 39 | 40 | else if (block && !block._isSelected && block._hitBody(x, y)) { 41 | if (!ctrl) tree.selection.deselectAll(); 42 | if (alt) { 43 | tree.selection.selectSubtree(block); 44 | } else { 45 | tree.selection.select(block); 46 | } 47 | } 48 | else if (block && block._hitBody(x, y)) { 49 | if (alt) { 50 | tree.selection.selectSubtree(block); 51 | } 52 | } 53 | 54 | else if (!block) { 55 | isSelecting = true; 56 | x0 = x; 57 | y0 = y; 58 | 59 | if (!ctrl) tree.selection.deselectAll(); 60 | } 61 | }; 62 | 63 | this.onMouseMove = function(e) { 64 | if (!isSelecting) return; 65 | 66 | var project = editor.project.get(); 67 | if (!project) return; 68 | 69 | var tree = project.trees.getSelected(); 70 | if (!tree) return; 71 | 72 | var point = tree.view.getLocalPoint(); 73 | var x = point.x; 74 | var y = point.y; 75 | 76 | tree._selectionBox.visible = true; 77 | tree._selectionBox._redraw(x0, y0, x, y); 78 | }; 79 | 80 | this.onMouseUp = function(e) { 81 | if (e.nativeEvent.which !== 1 || !isSelecting) return; 82 | 83 | var project = editor.project.get(); 84 | if (!project) return; 85 | 86 | var tree = project.trees.getSelected(); 87 | if (!tree) return; 88 | 89 | var point = tree.view.getLocalPoint(); 90 | var x = point.x; 91 | var y = point.y; 92 | 93 | var x1 = Math.min(x0, x); 94 | var y1 = Math.min(y0, y); 95 | var x2 = Math.max(x0, x); 96 | var y2 = Math.max(y0, y); 97 | 98 | tree.blocks.each(function(block) { 99 | if (block._isContainedIn(x1, y1, x2, y2)) { 100 | tree.selection.select(block); 101 | } 102 | }); 103 | 104 | tree._selectionBox.visible = false; 105 | isSelecting = false; 106 | }; 107 | 108 | editor._game.stage.on('stagemousedown', this.onMouseDown, this); 109 | editor._game.stage.on('stagemousemove', this.onMouseMove, this); 110 | editor._game.stage.on('stagemouseup', this.onMouseUp, this); 111 | }; 112 | -------------------------------------------------------------------------------- /src/app/pages/editor/modals/editnode.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |

{{editnode.action}} node

7 |
8 | 9 |
10 |
11 | 12 | 22 | 23 |
24 | 25 |
26 | 27 | 31 |
32 | 33 |
34 | 35 | 43 |
44 |
45 | 46 |
47 |
48 | 49 | 50 |
51 | 52 |
53 | 54 |
55 |
56 | 57 |
58 |
59 | 60 |
61 | 62 | 63 | 64 | 65 |
66 |
67 |
68 |
69 | -------------------------------------------------------------------------------- /src/app/services/dialog.service.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('app') 3 | .factory('dialogService', dialogService); 4 | 5 | dialogService.$inject = ['$window', '$q', '$document', 'nodejsService']; 6 | 7 | function dialogService($window, $q, $document, nodejsService) { 8 | var service = { 9 | alert : alert, 10 | confirm : confirm, 11 | prompt : prompt, 12 | saveAs : saveAs, 13 | openFile : openFile 14 | }; 15 | return service; 16 | 17 | function _callFileDialog(dialog) { 18 | return $q(function(resolve) { 19 | dialog.addEventListener('change', function() { 20 | resolve(dialog.value); 21 | }); 22 | dialog.click(); 23 | }); 24 | } 25 | 26 | function alert(title, text, type, options) { 27 | options = options || {}; 28 | options.title = title; 29 | options.text = text; 30 | options.type = type; 31 | options.customClass = type; 32 | 33 | return $q(function(resolve) { swal(options, function() { resolve(); }); }); 34 | } 35 | function confirm(title, text, type, options) { 36 | options = options || {}; 37 | options.title = title; 38 | options.text = text; 39 | options.type = type; 40 | options.customClass = type; 41 | options.showCancelButton = true; 42 | 43 | return $q(function(resolve, reject) { 44 | $window.swal(options, function(ok) { 45 | if (ok) { 46 | resolve(); 47 | } else { 48 | reject(); 49 | } 50 | }); 51 | }); 52 | } 53 | function prompt(title, text, type, placeholder, options) { 54 | options = options || {}; 55 | options.title = title; 56 | options.text = text; 57 | options.type = type || 'input'; 58 | options.inputPlaceholder = placeholder; 59 | options.customClass = type; 60 | options.showCancelButton = true; 61 | 62 | return $q(function(resolve, reject) { 63 | swal(options, function(val) { 64 | if (val!==false) { 65 | resolve(val); 66 | } else { 67 | reject(val); 68 | } 69 | }); 70 | }); 71 | } 72 | function saveAs(placeholder, types) { 73 | return $q(function(resolve, reject) { 74 | var value = nodejsService.dialog.showSaveDialogSync({ 75 | title: 'Save project as...', 76 | defaultPath: placeholder + '.json', 77 | filters : [ 78 | {name: 'JSON', extensions: ['json']}, 79 | {name: 'All Files', extensions: ['*']} 80 | ] 81 | }); 82 | if (value) { 83 | resolve(value); 84 | } else { 85 | reject(); 86 | } 87 | }); 88 | } 89 | function openFile(multiple, types) { 90 | return $q(function(resolve, reject) { 91 | var value = nodejsService.dialog.showOpenDialogSync({ 92 | title: 'Open file...', 93 | properties: ['openFile', 'multiSelections'], 94 | filters : [ 95 | {name: 'JSON', extensions: ['json']}, 96 | {name: 'All Files', extensions: ['*']} 97 | ] 98 | }); 99 | 100 | if (value) { 101 | if (!multiple) { 102 | value = value[0]; 103 | } 104 | resolve(value); 105 | } else { 106 | reject(); 107 | } 108 | }); 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/assets/less/c_menubar.less: -------------------------------------------------------------------------------- 1 | .menubar { 2 | position: absolute; 3 | top: 0px; 4 | width: 100%; 5 | height: @size-menubar; 6 | z-index: @z-menubar; 7 | color: @color-light1; 8 | font-size: 14px; 9 | background-color: @color-dark4; 10 | border-bottom: 1px solid @color-dark2; 11 | 12 | .box-shadow(0px 0px 16px 5px); 13 | 14 | &-left, &-right { 15 | display: inline; 16 | } 17 | 18 | &-right { 19 | float: right; 20 | } 21 | } 22 | 23 | .menubar ul { 24 | padding: 0px; 25 | margin: 0px; 26 | list-style: none; 27 | // position: relative; 28 | display: inline-table; 29 | } 30 | .menubar ul:after { 31 | content: ""; 32 | clear: both; 33 | display: block; 34 | } 35 | .menubar ul li { 36 | float: left; 37 | } 38 | .menubar ul li:hover { 39 | background-color: @color-dark1; 40 | } 41 | .menubar ul li a { 42 | color: #FFFFFF; 43 | display: block; 44 | height: 35px; 45 | padding: 0px 10px; 46 | line-height: 35px; 47 | text-decoration: none; 48 | .no-select; 49 | 50 | .shortcut { 51 | color: #999; 52 | float: right; 53 | font-style: italic; 54 | } 55 | } 56 | .menubar .side { 57 | // width: @size-sidebar; 58 | text-align: left; 59 | display: inline-block; 60 | 61 | a { 62 | display: inline-block; 63 | 64 | &.fastlink { 65 | color: @color-light1; 66 | padding: 0px 6px 0px 7px !important; 67 | border-right: 1px solid @color-dark2; 68 | } 69 | } 70 | 71 | .logo { 72 | color: #4BB2FD; 73 | background-color: #333333; 74 | // border-left: 1px solid @color-dark2; 75 | // border-right: 1px solid @color-dark2; 76 | font-weight: bold; 77 | 78 | .animation-slow; 79 | 80 | &:hover { 81 | color: @color-dark2; 82 | background-color: @color-light2; 83 | } 84 | } 85 | } 86 | 87 | .menubar li.disabled { 88 | a { 89 | color: @color-light3; 90 | cursor: default; 91 | } 92 | &:hover { 93 | background-color: inherit; 94 | } 95 | } 96 | 97 | .menubar ul li:hover > ul { 98 | display: block; 99 | } 100 | .menubar ul ul { 101 | display: none; 102 | background: #454545; 103 | position: absolute; 104 | top: 100%; 105 | min-width: 235px; 106 | border: 1px solid #333333; 107 | } 108 | .menubar .menubar-right ul ul { 109 | right: 0; 110 | } 111 | .menubar ul ul li { 112 | float: none; 113 | position: relative; 114 | } 115 | .menubar ul ul li a { 116 | height: 35px; 117 | padding: 0px 10px; 118 | line-height: 35px; 119 | color: #fff; 120 | text-align: left; 121 | } 122 | .menubar ul ul ul { 123 | position: absolute; 124 | top: 0; 125 | } 126 | .menubar .menubar-left ul ul ul { left: 100%; } 127 | .menubar .menubar-right ul ul ul { right: 100%; } 128 | .menubar .divider { border-top: 1px solid #555; } 129 | 130 | .arrow-right { 131 | width: 0; 132 | height: 0; 133 | margin-top: 12px; 134 | border-top: 5px solid transparent; 135 | border-bottom: 5px solid transparent; 136 | border-left: 5px solid #FFF; 137 | } 138 | 139 | 140 | [ui-view].ng-enter .menubar, 141 | [ui-view].ng-leave .menubar { .animation-fast; } 142 | [ui-view].ng-enter .menubar { top: -@size-menubar; } 143 | [ui-view].ng-enter-active .menubar { top: 0; } 144 | [ui-view].ng-leave .menubar { top: 0; } 145 | [ui-view].ng-leave-active .menubar { top: -@size-menubar; } -------------------------------------------------------------------------------- /src/editor/editor/systems/DragSystem.js: -------------------------------------------------------------------------------- 1 | b3e.editor.DragSystem = function(editor) { 2 | "use strict"; 3 | 4 | var isDragging = false; 5 | var dragX0 = 0; 6 | var dragY0 = 0; 7 | 8 | this.update = function(delta) {}; 9 | 10 | this.onMouseDown = function(e) { 11 | if (e.nativeEvent.which !== 1 || 12 | e.nativeEvent.ctrlKey || 13 | isDragging) return; 14 | 15 | var project = editor.project.get(); 16 | if (!project) return; 17 | 18 | var tree = project.trees.getSelected(); 19 | if (!tree) return; 20 | 21 | var point = tree.view.getLocalPoint(); 22 | var x = point.x; 23 | var y = point.y; 24 | var block = tree.blocks.getUnderPoint(x, y); 25 | 26 | // if mouse not on block 27 | if (!block) return; 28 | 29 | // if no block selected 30 | if (!block._isSelected) return; 31 | 32 | // if mouse in anchor 33 | if (!block._hitBody(x, y)) return; 34 | 35 | // start dragging 36 | isDragging = true; 37 | dragX0 = x; 38 | dragY0 = y; 39 | 40 | for (var i=0; i 0) { 96 | tree.view.zoomIn(); 97 | } else { 98 | tree.view.zoomOut(); 99 | } 100 | } 101 | }; 102 | 103 | 104 | var self = this; 105 | editor._game.stage.on('stagemousedown', this.onMouseDown, this); 106 | editor._game.stage.on('stagemousemove', this.onMouseMove, this); 107 | editor._game.stage.on('stagemouseup', this.onMouseUp, this); 108 | editor._game.canvas.addEventListener('wheel', function(e) { 109 | self.onMouseWheel(e); 110 | }, false); 111 | editor._game.canvas.addEventListener('mousewheel', function(e) { 112 | self.onMouseWheel(e); 113 | }, false); 114 | editor._game.canvas.addEventListener('DOMMouseScroll ', function(e) { 115 | self.onMouseWheel(e); 116 | }, false); 117 | }; 118 | -------------------------------------------------------------------------------- /src/app/services/notification.service.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('app') 3 | .factory('notificationService', notificationService); 4 | 5 | notificationService.$inject = ['$window', '$timeout', '$compile', '$rootScope', '$sce']; 6 | 7 | function notificationService($window, $timeout, $compile, $rootScope, $sce) { 8 | var elementBuffer = []; 9 | var service = { 10 | notify : notify, 11 | simple : simple, 12 | success : success, 13 | error : error, 14 | info : info, 15 | warning : warning, 16 | }; 17 | return service; 18 | 19 | function _reposite() { 20 | var verticalSpacing = 10; 21 | var lastBottom = 20; 22 | 23 | for(var i=elementBuffer.length-1; i>=0; i--) { 24 | var element = elementBuffer[i]; 25 | var height = parseInt(element[0].offsetHeight); 26 | 27 | var bottom = lastBottom; 28 | lastBottom = bottom+height+verticalSpacing; 29 | 30 | element.css('bottom', bottom+'px'); 31 | } 32 | } 33 | 34 | function _note(config) { 35 | var TEMPLATE = ''+ 36 | '
'+ 37 | '
'+ 38 | '
' + 39 | '
'+ 40 | '
'+ 41 | '
' + 42 | '
'; 43 | 44 | var DEFAULT = { 45 | type : 'default', 46 | title : '', 47 | message : '', 48 | icon : false, 49 | delay : 3000, 50 | }; 51 | 52 | // Default parameters 53 | config = tine.merge({}, DEFAULT, config); 54 | 55 | // Set scope variables to fill the template 56 | var scope = $rootScope.$new(); 57 | scope.type = config.type; 58 | scope.title = $sce.trustAsHtml(config.title); 59 | scope.message = $sce.trustAsHtml(config.message); 60 | scope.icon = config.icon; 61 | scope.delay = config.delay; 62 | 63 | // Create the DOM element and add events 64 | var element = $compile(TEMPLATE)(scope); 65 | element.bind('webkitTransitionEnd oTransitionEnd otransitionend transitionend msTransitionEnd click', function(e) { 66 | e = e.originalEvent || e; 67 | if (e.type==='click' || element.hasClass('killed')) { 68 | element.remove(); 69 | elementBuffer.remove(element); 70 | _reposite(); 71 | } 72 | }); 73 | 74 | if (angular.isNumber(config.delay)) { 75 | $timeout(function() { 76 | element.addClass('killed'); 77 | }, config.delay); 78 | } 79 | 80 | $timeout(function() { 81 | element.addClass('started'); 82 | _reposite(); 83 | }, 0); 84 | 85 | elementBuffer.push(element); 86 | angular.element(document.getElementsByTagName('body')).append(element); 87 | } 88 | 89 | function notify(config) { 90 | _note(config); 91 | } 92 | function simple(title, message) { 93 | _note({title:title, message:message, type:'default'}); 94 | } 95 | function success(title, message) { 96 | _note({title:title, message:message, icon:'fa-check', type:'success'}); 97 | } 98 | function error(title, message) { 99 | _note({title:title, message:message, icon:'fa-close', type:'error'}); 100 | } 101 | function info(title, message) { 102 | _note({title:title, message:message, icon:'fa-info', type:'info'}); 103 | } 104 | function warning(title, message) { 105 | _note({title:title, message:message, icon:'fa-warning', type:'warning'}); 106 | } 107 | } -------------------------------------------------------------------------------- /src/editor/project/managers/TreeManager.js: -------------------------------------------------------------------------------- 1 | b3e.project.TreeManager = function(editor, project) { 2 | "use strict"; 3 | 4 | /** 5 | * Adds a new tree to the project. 6 | */ 7 | this.add = function(_id) { 8 | var tree; 9 | 10 | if (_id instanceof b3e.tree.Tree) { 11 | tree = _id; 12 | project.addChild(tree); 13 | editor.trigger('treeadded', tree); 14 | this.select(tree); 15 | 16 | } else { 17 | project.history._beginBatch(); 18 | tree = new b3e.tree.Tree(editor, project); 19 | var root = tree.blocks.getRoot(); 20 | project.addChild(tree); 21 | editor.trigger('treeadded', tree); 22 | 23 | if (_id) tree._id = _id; 24 | 25 | var node = { 26 | name : tree._id, 27 | title : root.title, 28 | category : 'tree', 29 | description: 'An instance of the tree. You can add properties to this node and they will be available in action nodes in this tree (using syntax like `{{key}}`). Properties defined on this node will overwrite the same property defined on the tree\'s root node.' 30 | }; 31 | project.nodes.add(node, true); 32 | 33 | // select if this is the only tree 34 | this.select(tree); 35 | 36 | 37 | var _old = [this, this.remove, [tree]]; 38 | var _new = [this, this.add, [tree]]; 39 | project.history._add(new b3e.Command(_old, _new)); 40 | project.history._endBatch(); 41 | } 42 | 43 | return tree; 44 | }; 45 | 46 | /** 47 | * Gets a tree by id. 48 | */ 49 | this.get = function(tree) { 50 | if (typeof tree === 'string') { 51 | for (var i=0; i 2 | 12 | 13 | 14 |
15 |
16 | 21 | 22 | 24 | Trees 25 | 26 |
27 | 28 |
29 |
30 | 31 | 49 |
50 |
51 | 52 |
53 | 58 | 59 | 61 | Nodes 62 | 63 |
64 | 65 |
66 |
67 |
{{category}}s
68 | 87 |
    88 |
  • empty
  • 89 |
90 |
91 | 92 |
93 |
94 | 95 | -------------------------------------------------------------------------------- /src/editor/editor/managers/ImportManager.js: -------------------------------------------------------------------------------- 1 | b3e.editor.ImportManager = function(editor) { 2 | "use strict"; 3 | 4 | this.projectAsData = function(data) { 5 | var project = editor.project.get(); 6 | if (!project) return; 7 | 8 | if (data.custom_nodes) this.nodesAsData(data.custom_nodes); 9 | if (data.trees) this.treesAsData(data.trees); 10 | if (data.selectedTree) { 11 | project.trees.select(data.selectedTree); 12 | } 13 | editor.trigger('projectimported'); 14 | }; 15 | 16 | this.treeAsData = function(data) { 17 | var project = editor.project.get(); 18 | if (!project) return; 19 | 20 | var tree = project.trees.get(data.id); 21 | var root = tree.blocks.getRoot(); 22 | var first = null; 23 | 24 | // Tree data 25 | var display = data.display||{}; 26 | tree.x = display.camera_x || 0; 27 | tree.y = display.camera_y || 0; 28 | tree.scaleX = display.camera_z || 1; 29 | tree.scaleY = display.camera_z || 1; 30 | var treeNode = project.nodes.get(tree._id); 31 | treeNode.title = data.title; 32 | 33 | root.title = data.title; 34 | root.description = data.description; 35 | root.properties = data.properties; 36 | root.x = display.x || 0; 37 | root.y = display.y || 0; 38 | 39 | // Custom nodes 40 | if (data.custom_nodes) this.nodesAsData(data.custom_nodes); 41 | 42 | var id, spec; 43 | 44 | // Add blocks 45 | for (id in data.nodes) { 46 | spec = data.nodes[id]; 47 | var block = null; 48 | display = spec.display || {}; 49 | 50 | block = tree.blocks.add(spec.name, spec.display.x, spec.display.y); 51 | block.id = spec.id; 52 | block.title = spec.title; 53 | block.description = spec.description; 54 | block.properties = tine.merge({}, block.properties, spec.properties); 55 | block._redraw(); 56 | 57 | if (spec.id === data.root) { 58 | first = block; 59 | } 60 | } 61 | 62 | // Add connections 63 | for (id in data.nodes) { 64 | spec = data.nodes[id]; 65 | var inBlock = tree.blocks.get(id); 66 | 67 | var children = null; 68 | if (inBlock.category === 'composite' && spec.children) { 69 | children = spec.children; 70 | } 71 | else if (spec.child && (inBlock.category == 'decorator' || 72 | inBlock.category == 'root')) { 73 | children = [spec.child]; 74 | } 75 | 76 | if (children) { 77 | for (var i=0; i 1) { 103 | 104 | c = connection._inBlock._outConnections[0]; 105 | tree.connections.remove(c); 106 | } 107 | 108 | connection._outBlock = block; 109 | block._inConnection = connection; 110 | 111 | var _old = [tree.connections, tree.connections._remove, [block]]; 112 | var _new = [tree.connections, tree.connections.add, [connection._inBlock, block]]; 113 | project.history._add(new b3e.Command(_old, _new)); 114 | 115 | connection._redraw(); 116 | } 117 | project.history._endBatch(); 118 | 119 | connection = null; 120 | }; 121 | 122 | editor._game.stage.on('stagemousedown', this.onMouseDown, this); 123 | editor._game.stage.on('stagemousemove', this.onMouseMove, this); 124 | editor._game.stage.on('stagemouseup', this.onMouseUp, this); 125 | }; 126 | -------------------------------------------------------------------------------- /src/app/pages/editor/components/nodespanel.controller.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app') 6 | .controller('NodespanelController', NodespanelController); 7 | 8 | NodespanelController.$inject = [ 9 | '$scope', 10 | '$window', 11 | 'dialogService', 12 | 'notificationService' 13 | ]; 14 | 15 | function NodespanelController($scope, 16 | $window, 17 | dialogService, 18 | notificationService) { 19 | 20 | // HEAD // 21 | var vm = this; 22 | vm.nodes = null; 23 | vm.newTree = newTree; 24 | vm.select = select; 25 | vm.remove = remove; 26 | 27 | _create(); 28 | _activate(); 29 | $scope.$on('$destroy', _destroy); 30 | 31 | // BODY // 32 | function _activate() { 33 | vm.trees = []; 34 | vm.nodes = { 35 | composite : [], 36 | decorator : [], 37 | action : [], 38 | // condition : [], 39 | }; 40 | 41 | var p = $window.editor.project.get(); 42 | p.nodes.each(function(node) { 43 | if (node.category === 'tree') return; 44 | 45 | var list = vm.nodes[node.category]; 46 | if (!list) return; 47 | list.push({ 48 | name: node.name, 49 | title: _getTitle(node), 50 | isDefault: node.isDefault 51 | }); 52 | }); 53 | 54 | var selected = p.trees.getSelected(); 55 | p.trees.each(function(tree) { 56 | var root = tree.blocks.getRoot(); 57 | vm.trees.push({ 58 | 'id' : tree._id, 59 | 'name' : root.title || 'My tree', 60 | 'active' : tree===selected, 61 | }); 62 | }); 63 | } 64 | 65 | function _event(e) { 66 | setTimeout(function() {$scope.$apply(function() { _activate(); });}, 0); 67 | } 68 | 69 | function _create() { 70 | $window.editor.on('nodechanged', _event); 71 | $window.editor.on('noderemoved', _event); 72 | $window.editor.on('nodeadded', _event); 73 | $window.editor.on('treeadded', _event); 74 | $window.editor.on('blockchanged', _event); 75 | $window.editor.on('treeselected', _event); 76 | $window.editor.on('treeremoved', _event); 77 | $window.editor.on('treeimported', _event); 78 | } 79 | 80 | function _destroy() { 81 | $window.editor.off('nodechanged', _event); 82 | $window.editor.off('noderemoved', _event); 83 | $window.editor.off('nodeadded', _event); 84 | $window.editor.off('treeadded', _event); 85 | $window.editor.off('blockchanged', _event); 86 | $window.editor.off('treeselected', _event); 87 | $window.editor.off('treeremoved', _event); 88 | $window.editor.off('treeimported', _event); 89 | } 90 | 91 | function _getTitle(node) { 92 | var title = node.title || node.name; 93 | title = title.replace(/(<\w+>)/g, function(match, key) { return '@'; }); 94 | return title; 95 | } 96 | 97 | function newTree() { 98 | var p = $window.editor.project.get(); 99 | p.trees.add(); 100 | } 101 | 102 | function select(id) { 103 | var p = $window.editor.project.get(); 104 | p.trees.select(id); 105 | } 106 | 107 | function remove(id) { 108 | dialogService. 109 | confirm( 110 | 'Remove tree?', 111 | 'Are you sure you want to remove this tree?\n\nNote: all blocks using this tree will be removed.' 112 | ).then(function() { 113 | var p = $window.editor.project.get(); 114 | p.trees.remove(id); 115 | notificationService.success( 116 | 'Tree removed', 117 | 'The tree has been removed from this project.' 118 | ); 119 | }); 120 | } 121 | } 122 | })(); 123 | -------------------------------------------------------------------------------- /src/editor/editor/managers/ExportManager.js: -------------------------------------------------------------------------------- 1 | b3e.editor.ExportManager = function(editor) { 2 | "use strict"; 3 | 4 | function getBlockChildrenIds(block) { 5 | var conns = block._outConnections.slice(0); 6 | if (editor._settings.get('layout') === 'horizontal') { 7 | conns.sort(function(a, b) { 8 | return a._outBlock.y - 9 | b._outBlock.y; 10 | }); 11 | } else { 12 | conns.sort(function(a, b) { 13 | return a._outBlock.x - 14 | b._outBlock.x; 15 | }); 16 | } 17 | 18 | var nodes = []; 19 | for (var i=0; i>> 0; 63 | 64 | // 4. If IsCallable(callback) is false, throw a TypeError exception. 65 | // See: http://es5.github.com/#x9.11 66 | if (typeof callback !== "function") { 67 | throw new TypeError(callback + ' is not a function'); 68 | } 69 | 70 | // 5. If thisArg was supplied, let T be thisArg; else let T be undefined. 71 | if (arguments.length > 1) { 72 | T = thisArg; 73 | } 74 | 75 | // 6. Let k be 0 76 | k = 0; 77 | 78 | // 7. Repeat, while k < len 79 | while (k < len) { 80 | 81 | var kValue; 82 | 83 | // a. Let Pk be ToString(k). 84 | // This is implicit for LHS operands of the in operator 85 | // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk. 86 | // This step can be combined with c 87 | // c. If kPresent is true, then 88 | if (k in O) { 89 | 90 | // i. Let kValue be the result of calling the Get internal method of O with argument Pk. 91 | kValue = O[k]; 92 | 93 | // ii. Call the Call internal method of callback with T as the this value and 94 | // argument list containing kValue, k, and O. 95 | callback.call(T, kValue, k, O); 96 | } 97 | // d. Increase k by 1. 98 | k++; 99 | } 100 | // 8. return undefined 101 | }; 102 | } 103 | 104 | /** 105 | * Object.keys function 106 | * From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys 107 | */ 108 | if (!Object.keys) { 109 | Object.keys = (function() { 110 | 'use strict'; 111 | var hasOwnProperty = Object.prototype.hasOwnProperty, 112 | hasDontEnumBug = !({ toString: null }).propertyIsEnumerable('toString'), 113 | dontEnums = [ 114 | 'toString', 115 | 'toLocaleString', 116 | 'valueOf', 117 | 'hasOwnProperty', 118 | 'isPrototypeOf', 119 | 'propertyIsEnumerable', 120 | 'constructor' 121 | ], 122 | dontEnumsLength = dontEnums.length; 123 | 124 | return function(obj) { 125 | if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) { 126 | throw new TypeError('Object.keys called on non-object'); 127 | } 128 | 129 | var result = [], prop, i; 130 | 131 | for (prop in obj) { 132 | if (hasOwnProperty.call(obj, prop)) { 133 | result.push(prop); 134 | } 135 | } 136 | 137 | if (hasDontEnumBug) { 138 | for (i = 0; i < dontEnumsLength; i++) { 139 | if (hasOwnProperty.call(obj, dontEnums[i])) { 140 | result.push(dontEnums[i]); 141 | } 142 | } 143 | } 144 | return result; 145 | }; 146 | }()); 147 | } -------------------------------------------------------------------------------- /src/editor/project/managers/NodeManager.js: -------------------------------------------------------------------------------- 1 | b3e.project.NodeManager = function(editor, project) { 2 | "use strict"; 3 | 4 | /** 5 | * Register a node to the node list. You can provide: 6 | * 7 | * - a `b3.BaseNode` instance. 8 | * - a `b3e.Node` instance. 9 | * - a generic object containing the node prototype. 10 | */ 11 | this.add = function(node, isDefault) { 12 | if (node.prototype) node = node.prototype; 13 | 14 | if (project._nodes[node.name]) { 15 | return false; 16 | } 17 | 18 | if (!(node instanceof b3e.Node)) { 19 | var n = new b3e.Node(isDefault); 20 | n.name = node.name; 21 | n.category = node.category; 22 | n.title = node.title; 23 | n.description = node.description; 24 | n.properties = tine.merge({}, node.properties||node.parameters); 25 | 26 | node = n; 27 | } 28 | 29 | project._nodes[node.name] = node; 30 | if (isDefault !== true) editor.trigger('nodeadded', node); 31 | 32 | var _old = [this, this.remove, [node]]; 33 | var _new = [this, this.add, [node]]; 34 | project.history._add(new b3e.Command(_old, _new)); 35 | 36 | return node; 37 | }; 38 | 39 | /** 40 | * 41 | */ 42 | this.get = function(node) { 43 | if (typeof node !== 'string') return node; 44 | return project._nodes[node]; 45 | }; 46 | 47 | /** 48 | * 49 | */ 50 | this.update = function(node, template) { 51 | node = this.get(node); 52 | var oldName = node.name; 53 | 54 | delete project._nodes[node.name]; 55 | 56 | if (node.name !== template.name && this.get(template.name)) return false; 57 | 58 | 59 | var _oldValues = { 60 | name : node.name, 61 | title : node.title, 62 | description : node.description, 63 | category : node.category, 64 | properties : node.properties, 65 | }; 66 | 67 | if (typeof template.name !== 'undefined') { 68 | node.name = template.name; 69 | } 70 | if (typeof template.title !== 'undefined') { 71 | node.title = template.title; 72 | } 73 | if (typeof template.category !== 'undefined') { 74 | node.category = template.category; 75 | } 76 | if (typeof template.description !== 'undefined') { 77 | node.description = template.description; 78 | } 79 | if (typeof template.properties !== 'undefined') { 80 | node.properties = tine.merge({}, template.properties); 81 | } 82 | 83 | var _newValues = { 84 | name : node.name, 85 | title : node.title, 86 | description : node.description, 87 | category : node.category, 88 | properties : node.properties, 89 | }; 90 | 91 | project.history._beginBatch(); 92 | 93 | project.trees.each(function(tree) { 94 | var blocks = tree.blocks.getAll(); 95 | for (var i=blocks.length-1; i>=0; i--) { 96 | if (blocks[i].name === oldName) { 97 | tree.blocks.update(blocks[i]); 98 | } 99 | } 100 | }); 101 | 102 | project._nodes[node.name] = node; 103 | 104 | var _old = [this, this.update, [node, _oldValues]]; 105 | var _new = [this, this.update, [node, _newValues]]; 106 | project.history._add(new b3e.Command(_old, _new)); 107 | project.history._endBatch(); 108 | 109 | editor.trigger('nodechanged', node); 110 | }; 111 | 112 | /** 113 | * 114 | */ 115 | this.remove = function(node) { 116 | project.history._beginBatch(); 117 | 118 | var name = node.name||node; 119 | delete project._nodes[name]; 120 | 121 | project.trees.each(function(tree) { 122 | var blocks = tree.blocks.getAll(); 123 | for (var i=blocks.length-1; i>=0; i--) { 124 | if (blocks[i].name === name) { 125 | tree.blocks.remove(blocks[i]); 126 | } 127 | } 128 | }); 129 | 130 | var _old = [this, this.add, [node]]; 131 | var _new = [this, this.remove, [node]]; 132 | project.history._add(new b3e.Command(_old, _new)); 133 | 134 | project.history._endBatch(); 135 | 136 | editor.trigger('noderemoved', node); 137 | }; 138 | 139 | /** 140 | * Iterates over node list. 141 | */ 142 | this.each = function(callback, thisarg) { 143 | Object.keys(project._nodes).forEach(function(key) { 144 | callback.call(thisarg, project._nodes[key]); 145 | }); 146 | }; 147 | 148 | this._applySettings = function(settings) {}; 149 | }; -------------------------------------------------------------------------------- /src/assets/libs/mousetrap.min.js: -------------------------------------------------------------------------------- 1 | /* mousetrap v1.5.2 craig.is/killing/mice */ 2 | (function(C,r,g){function t(a,b,h){a.addEventListener?a.addEventListener(b,h,!1):a.attachEvent("on"+b,h)}function x(a){if("keypress"==a.type){var b=String.fromCharCode(a.which);a.shiftKey||(b=b.toLowerCase());return b}return l[a.which]?l[a.which]:p[a.which]?p[a.which]:String.fromCharCode(a.which).toLowerCase()}function D(a){var b=[];a.shiftKey&&b.push("shift");a.altKey&&b.push("alt");a.ctrlKey&&b.push("ctrl");a.metaKey&&b.push("meta");return b}function u(a){return"shift"==a||"ctrl"==a||"alt"==a|| 3 | "meta"==a}function y(a,b){var h,c,e,g=[];h=a;"+"===h?h=["+"]:(h=h.replace(/\+{2}/g,"+plus"),h=h.split("+"));for(e=0;em||l.hasOwnProperty(m)&&(k[l[m]]=m)}e=k[h]?"keydown":"keypress"}"keypress"==e&&g.length&&(e="keydown");return{key:c,modifiers:g,action:e}}function B(a,b){return a===r?!1:a===b?!0:B(a.parentNode,b)}function c(a){function b(a){a=a||{}; 4 | var b=!1,n;for(n in q)a[n]?b=!0:q[n]=0;b||(v=!1)}function h(a,b,n,f,c,h){var g,e,l=[],m=n.type;if(!d._callbacks[a])return[];"keyup"==m&&u(a)&&(b=[a]);for(g=0;g":".","?":"/","|":"\\"},z={option:"alt",command:"meta","return":"enter",escape:"esc", 9 | plus:"+",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},k;for(g=1;20>g;++g)l[111+g]="f"+g;for(g=0;9>=g;++g)l[g+96]=g;c.prototype.bind=function(a,b,c){a=a instanceof Array?a:[a];this._bindMultiple.call(this,a,b,c);return this};c.prototype.unbind=function(a,b){return this.bind.call(this,a,function(){},b)};c.prototype.trigger=function(a,b){if(this._directMap[a+":"+b])this._directMap[a+":"+b]({},a);return this};c.prototype.reset=function(){this._callbacks={};this._directMap={};return this}; 10 | c.prototype.stopCallback=function(a,b){return-1<(" "+b.className+" ").indexOf(" mousetrap ")||B(b,this.target)?!1:"INPUT"==b.tagName||"SELECT"==b.tagName||"TEXTAREA"==b.tagName||b.isContentEditable};c.prototype.handleKey=function(){return this._handleKey.apply(this,arguments)};c.init=function(){var a=c(r),b;for(b in a)"_"!==b.charAt(0)&&(c[b]=function(b){return function(){return a[b].apply(a,arguments)}}(b))};c.init();C.Mousetrap=c;"undefined"!==typeof module&&module.exports&&(module.exports=c);"function"=== 11 | typeof define&&define.amd&&define(function(){return c})})(window,document); -------------------------------------------------------------------------------- /src/editor/draw/symbols.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | b3e.draw.rootSymbol = function(block, settings) { 5 | // var shape = block.displayObject; 6 | var shape = new createjs.Shape(); 7 | 8 | var w = block._width; 9 | var h = block._height; 10 | var swidth = h/20; 11 | var ssize = h/5; 12 | var scolor = settings.get('block_symbol_color'); 13 | 14 | shape.graphics.setStrokeStyle(swidth, 'round'); 15 | shape.graphics.beginStroke(scolor); 16 | shape.graphics.drawCircle(0, 0, ssize); 17 | shape.graphics.moveTo(-ssize, ssize); 18 | shape.graphics.lineTo(ssize, -ssize); 19 | shape.graphics.endStroke(); 20 | 21 | return shape; 22 | }; 23 | 24 | b3e.draw.sequenceSymbol = function(block, settings) { 25 | // var shape = block.displayObject; 26 | // var shape = block._shapeObject; 27 | var shape = new createjs.Shape(); 28 | 29 | var w = block._width; 30 | var h = block._height; 31 | var swidth = h/20; 32 | var ssize = h/4; 33 | var scolor = settings.get('block_symbol_color'); 34 | 35 | shape.graphics.setStrokeStyle(swidth, 'round'); 36 | shape.graphics.beginStroke(scolor); 37 | shape.graphics.beginFill(scolor); 38 | shape.graphics.moveTo(-ssize, 0); 39 | shape.graphics.lineTo(ssize, 0); 40 | shape.graphics.drawPolyStar(ssize/2, 0, ssize/2, 3, 0, 0); 41 | shape.graphics.endFill(); 42 | shape.graphics.endStroke(); 43 | 44 | return shape; 45 | }; 46 | 47 | b3e.draw.memsequenceSymbol = function(block, settings) { 48 | var shape = new createjs.Shape(); 49 | 50 | var w = block._width; 51 | var h = block._height; 52 | var swidth = h/20; 53 | var ssize = h/4; 54 | var scolor = settings.get('block_symbol_color'); 55 | 56 | shape.graphics.setStrokeStyle(swidth, 'round'); 57 | shape.graphics.beginStroke(scolor); 58 | shape.graphics.beginFill(scolor); 59 | shape.graphics.drawPolyStar(0, -ssize*0.75, ssize/2, 6, ssize/10, 0); 60 | 61 | shape.graphics.setStrokeStyle(swidth, 'round'); 62 | shape.graphics.beginStroke(scolor); 63 | shape.graphics.beginFill(scolor); 64 | shape.graphics.moveTo(-ssize, ssize/2); 65 | shape.graphics.lineTo(ssize, ssize/2); 66 | shape.graphics.drawPolyStar(ssize/2, ssize/2, ssize/2, 3, 0, 0); 67 | shape.graphics.endFill(); 68 | shape.graphics.endStroke(); 69 | 70 | return shape; 71 | }; 72 | 73 | b3e.draw.prioritySymbol = function(block, settings) { 74 | // var shape = block.displayObject; 75 | // var shape = block._shapeObject; 76 | var shape = new createjs.Shape(); 77 | 78 | var w = block._width; 79 | var h = block._height; 80 | var swidth = h/20; 81 | var ssize = h/8; 82 | var scolor = settings.get('block_symbol_color'); 83 | 84 | shape.graphics.setStrokeStyle(swidth, 'round'); 85 | shape.graphics.beginStroke(scolor); 86 | shape.graphics.arc(0, -ssize, ssize, 3.141561, 1.570796, false); 87 | shape.graphics.lineTo(0, ssize); 88 | shape.graphics.beginFill(scolor); 89 | shape.graphics.drawCircle(0, ssize*2, swidth/2); 90 | 91 | shape.graphics.endFill(); 92 | shape.graphics.endStroke(); 93 | 94 | return shape; 95 | }; 96 | 97 | b3e.draw.memprioritySymbol = function(block, settings) { 98 | var shape = new createjs.Shape(); 99 | 100 | var w = block._width; 101 | var h = block._height; 102 | var swidth = h/20; 103 | var ssize = h/8; 104 | var scolor = settings.get('block_symbol_color'); 105 | 106 | shape.graphics.setStrokeStyle(swidth, 'round'); 107 | shape.graphics.beginStroke(scolor); 108 | shape.graphics.arc(-ssize, -ssize, ssize, 3.141561, 1.570796, false); 109 | shape.graphics.lineTo(-ssize, ssize); 110 | shape.graphics.beginFill(scolor); 111 | shape.graphics.drawCircle(-ssize, ssize*2, swidth/2); 112 | shape.graphics.drawPolyStar(ssize*1.5, 0, ssize/2, 6, ssize/10, 0); 113 | 114 | shape.graphics.endFill(); 115 | shape.graphics.endStroke(); 116 | 117 | return shape; 118 | }; 119 | 120 | b3e.draw.textSymbol = function(block, settings) { 121 | var text = new createjs.Text( 122 | block.getTitle(), 123 | '18px Arial', 124 | settings.get('block_symbol_color') 125 | ); 126 | text.textAlign = 'center'; 127 | 128 | var bounds = text.getBounds(); 129 | text.regY = bounds.height/2; 130 | 131 | // text.x = -block._width/2; 132 | // text.y = -block._height/2; 133 | 134 | return text; 135 | }; 136 | 137 | 138 | b3e.draw.SYMBOLS = { 139 | 'root' : b3e.draw.rootSymbol, 140 | 'sequence' : b3e.draw.sequenceSymbol, 141 | 'select' : b3e.draw.prioritySymbol, 142 | }; 143 | 144 | }()); 145 | -------------------------------------------------------------------------------- /src/editor/tree/managers/OrganizeManager.js: -------------------------------------------------------------------------------- 1 | b3e.tree.OrganizeManager = function(editor, project, tree) { 2 | "use strict"; 3 | 4 | var lastLayout = null; 5 | var depth = 0; 6 | var leafCount = 0; 7 | var horizontalSpacing = 208; 8 | var verticalSpacing = 88; 9 | var verticalCompensation = 42; 10 | var orderByIndex = false; 11 | var connections = []; // to redraw connections 12 | var blocks = []; // to reposition blocks 13 | 14 | function stepH(block) { 15 | var x, y; 16 | blocks.push(block); 17 | 18 | // leaf 19 | if (block._outConnections.length === 0) { 20 | leafCount++; 21 | 22 | // leaf nodes have the position accord. to the depth and leaf cont. 23 | x = depth*horizontalSpacing; 24 | y = leafCount*verticalSpacing; 25 | } 26 | 27 | // internal node 28 | else { 29 | // internal nodes have the position acord. to the depth and the 30 | // mean position of its children 31 | var ySum = 0; 32 | var conns; 33 | 34 | if (orderByIndex) { 35 | conns = block._outConnections; 36 | } else { 37 | // get connections ordered by y position 38 | conns = block._outConnections.slice(0); 39 | conns.sort(function(a, b) { 40 | return a._outBlock.y - b._outBlock.y; 41 | }); 42 | } 43 | 44 | for (var i=0; i=0; i--) { 49 | if (recentCache[i].path === project.path) { 50 | recentCache.splice(i, 1); 51 | } else { 52 | recentCache[i].isOpen = false; 53 | } 54 | } 55 | 56 | var data = { 57 | name : project.name, 58 | description : project.description, 59 | path : project.path, 60 | isOpen : true, 61 | }; 62 | 63 | recentCache.splice(0, 0, data); 64 | } else { 65 | for (var j=0; j=0; i--) { 55 | var block = tree._selectedBlocks[i]; 56 | 57 | if (block.category != 'root') { 58 | tree.blocks.remove(tree._selectedBlocks[i]); 59 | } 60 | } 61 | project.history._endBatch(); 62 | tree._selectedBlocks = []; 63 | 64 | console.log(project._clipboard); 65 | }; 66 | 67 | this.paste = function() { 68 | if (project._clipboard === null) return; 69 | 70 | var i; 71 | var table = {}; 72 | var blocks = []; 73 | 74 | project.history._beginBatch(); 75 | 76 | // copy blocks 77 | for (var key in project._clipboard.blocks) { 78 | var spec = project._clipboard.blocks[key]; 79 | var block = new b3e.Block(spec); 80 | 81 | spec.x += 50; 82 | spec.y += 50; 83 | block._applySettings(spec._settings); 84 | block.x = spec.x; 85 | block.y = spec.y; 86 | 87 | tree.blocks.add(block); 88 | table[key] = block; 89 | blocks.push(block); 90 | } 91 | 92 | // copy connections 93 | for (i=0; i=0; i--) { 122 | if (tree._selectedBlocks[i].category === 'root') { 123 | root = tree._selectedBlocks[i]; 124 | } else { 125 | tree.blocks.remove(tree._selectedBlocks[i]); 126 | } 127 | } 128 | 129 | // tree.selection.deselectAll(); 130 | // if (root) { 131 | // tree.selection.select(root); 132 | // } 133 | project.history._endBatch(); 134 | }; 135 | 136 | this.removeConnections = function() { 137 | project.history._beginBatch(); 138 | for (var i=0; i 0) { 146 | for (var j=block._outConnections.length-1; j>=0; j--) { 147 | tree.connections.remove(block._outConnections[j]); 148 | } 149 | } 150 | } 151 | project.history._endBatch(); 152 | }; 153 | 154 | this.removeInConnections = function() { 155 | project.history._beginBatch(); 156 | for (var i=0; i 0) { 172 | for (var j=block._outConnections.length-1; j>=0; j--) { 173 | tree.connections.remove(block._outConnections[j]); 174 | } 175 | } 176 | } 177 | project.history._endBatch(); 178 | }; 179 | 180 | this._applySettings = function(settings) { 181 | }; 182 | }; 183 | -------------------------------------------------------------------------------- /src/editor/project/Project.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | var Project = function(editor) { 5 | this.Container_constructor(); 6 | 7 | // Variables 8 | this._id = b3.createUUID(); 9 | this._editor = editor; 10 | this._selectedTree = null; 11 | this._clipboard = null; 12 | this._nodes = {}; 13 | 14 | // Managers 15 | this.trees = null; 16 | this.nodes = null; 17 | this.history = null; 18 | 19 | this._initialize(); 20 | }; 21 | var p = createjs.extend(Project, createjs.Container); 22 | 23 | p._initialize = function() { 24 | this.trees = new b3e.project.TreeManager(this._editor, this); 25 | this.nodes = new b3e.project.NodeManager(this._editor, this); 26 | this.history = new b3e.project.HistoryManager(this._editor, this); 27 | 28 | var default_nodes = [ 29 | { 30 | name : 'root', 31 | category : 'root', 32 | title : 'My tree', 33 | description : 'The root of this tree. The title of this node sets the title of the tree. You must have one tree called "Root". You can set tree-wide properties on this node and reference them in other places with the following template syntax: `{{key_name}}`.' , 34 | }, 35 | { 36 | name : 'sequence', 37 | category : 'composite', 38 | title : 'Sequence', 39 | description : 'Takes multiple children and runs them from top to bottom (or left to right). If any fail, this node fails, if all succeed, this node succeeds.', 40 | }, 41 | { 42 | name : 'select', 43 | category : 'composite', 44 | title : 'Select', 45 | description : 'Takes multiple children and runs them from top to bottom (or left to right), succeeding when any one succeeds. Fails if all fail.', 46 | }, 47 | { 48 | name : 'random', 49 | category : 'composite', 50 | title : 'Random', 51 | description : 'Takes multiple children and runs one of them at random.', 52 | }, 53 | { 54 | name : 'random_weighted', 55 | category : 'composite', 56 | title : 'Random weighted', 57 | description : 'Takes multiple children and runs one of them at random based on their weightings. Each child node MUST have a "weight" property with a value greater than 0.', 58 | }, 59 | { 60 | name : 'repeat_n', 61 | category : 'decorator', 62 | title : 'Repeat x', 63 | description : 'Takes one child and runs it "n" times, where "n" is defined in this node\'s properties.', 64 | properties : {n: 2} 65 | }, 66 | { 67 | name : 'repeat_until_fail', 68 | category : 'decorator', 69 | title : 'Repeat until fail', 70 | description : 'Takes one child which it repeats until it fails. This node always succeeds.', 71 | }, 72 | { 73 | name : 'repeat_until_succeed', 74 | category : 'decorator', 75 | title : 'Repeat until succeed', 76 | description : 'Takes one child which it repeats until it succeeds. This node always succeeds.', 77 | }, 78 | { 79 | name : 'negate', 80 | category : 'decorator', 81 | title : 'Negate', 82 | description : 'Takes one child. If that child succeeds, this node fails, and vice versa.', 83 | }, 84 | { 85 | name : 'always_fail', 86 | category : 'decorator', 87 | title : 'Always fail', 88 | description : 'Takes one child and fails regardless of its outcome.', 89 | }, 90 | { 91 | name : 'always_succeed', 92 | category : 'decorator', 93 | title : 'Always succeed', 94 | description : 'Takes one child and succeeds regardless of its outcome.', 95 | }, 96 | { 97 | name : 'runner', 98 | category : 'action', 99 | title : 'Module.function(1, 2, 3)', 100 | description : 'An action that calls the function specified in the title (must be in valid Elixir terms). The title can contain "template variables" (like `{{mod}}.rename({{new_name}}, true)`) which will be replaced with corresponding values looked up on the parent tree.', 101 | }, 102 | { 103 | name : 'wait', 104 | category : 'action', 105 | title : 'wait(1)', 106 | description : '"Pauses" the bot for the specified number of seconds. You can specify two numbers (like `wait(1,10)`) to wait a random number of seconds between those numbers.', 107 | }, 108 | { 109 | name : 'error', 110 | category : 'action', 111 | title : 'error("Oops...")', 112 | description : 'Raises an error with the supplied message.', 113 | }, 114 | { 115 | name : 'log', 116 | category : 'action', 117 | title : 'log("Info...")', 118 | description : 'Logs the specified message.', 119 | }, 120 | { 121 | name : 'succeed_rate', 122 | category : 'action', 123 | title : 'succeed_rate(0.5)', 124 | description : 'Succeeds randomly at the specified rate, expressed as a number between 0 and 1. For example, a rate of 0.25 will succeed one out of every 4 times on average.', 125 | }, 126 | { 127 | name : 'done', 128 | category : 'action', 129 | title : 'done()', 130 | description : 'Stops the behavior tree.', 131 | }, 132 | ]; 133 | 134 | default_nodes.forEach(function(node_spec) {this.nodes.add(node_spec, true);}, this); 135 | 136 | this._applySettings(this._editor._settings); 137 | this.history.clear(); 138 | this._editor.clearDirty(); 139 | }; 140 | 141 | p._applySettings = function(settings) { 142 | this.trees._applySettings(settings); 143 | this.nodes._applySettings(settings); 144 | this.history._applySettings(settings); 145 | }; 146 | 147 | b3e.project.Project = createjs.promote(Project, 'Container'); 148 | })(); 149 | -------------------------------------------------------------------------------- /src/app/pages/projects/projects.controller.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app') 6 | .controller('ProjectsController', ProjectsController); 7 | 8 | ProjectsController.$inject = [ 9 | '$state', 10 | '$window', 11 | 'dialogService', 12 | 'systemService', 13 | 'notificationService', 14 | 'projectModel' 15 | ]; 16 | 17 | function ProjectsController($state, 18 | $window, 19 | dialogService, 20 | systemService, 21 | notificationService, 22 | projectModel) { 23 | 24 | // HEAD // 25 | var vm = this; 26 | vm.recentProjects = []; 27 | vm.isDesktop = null; 28 | 29 | vm.newProject = newProject; 30 | vm.openProject = openProject; 31 | vm.editProject = editProject; 32 | vm.saveProject = saveProject; 33 | vm.closeProject = closeProject; 34 | vm.removeProject = removeProject; 35 | 36 | _activate(); 37 | 38 | // BODY // 39 | function _activate() { 40 | vm.isDesktop = systemService.isDesktop; 41 | projectModel 42 | .getRecentProjects() 43 | .then(function(recents) { 44 | vm.recentProjects = recents; 45 | }); 46 | } 47 | 48 | function _newProject(path, name) { 49 | projectModel 50 | .newProject(path, name) 51 | .then(function() { 52 | $state.go('editor'); 53 | }); 54 | } 55 | 56 | function newProject() { 57 | function doNew() { 58 | // Get project name 59 | dialogService 60 | .prompt('New project', null, 'input', 'Project name') 61 | .then(function(name) { 62 | // If no name provided, abort 63 | if (!name) { 64 | notificationService.error( 65 | 'Invalid name', 66 | 'You must provide a name for the project.' 67 | ); 68 | return; 69 | } 70 | 71 | // If desktop, open file dialog 72 | if (vm.isDesktop) { 73 | var placeholder = name.replace(/\s+/g, "_").toLowerCase(); 74 | 75 | dialogService 76 | .saveAs(placeholder, ['.json']) 77 | .then(function(path) { 78 | _newProject(path, name); 79 | }); 80 | } else { 81 | var path = 'b3projects-'+b3.createUUID(); 82 | _newProject(path, name); 83 | } 84 | }); 85 | } 86 | 87 | if ($window.editor.isDirty()) { 88 | dialogService 89 | .confirm( 90 | 'Leave without saving?', 91 | 'If you proceed you will lose all unsaved modifications.', 92 | null, {closeOnConfirm: false}) 93 | .then(doNew); 94 | } else { 95 | doNew(); 96 | } 97 | } 98 | 99 | function _openProject(path) { 100 | projectModel 101 | .openProject(path) 102 | .then(function() { 103 | $state.go('editor'); 104 | }, function(e) { 105 | console.error(e); 106 | notificationService.error( 107 | 'Invalid file', 108 | 'Couldn\'t open the project file.' 109 | ); 110 | }); 111 | } 112 | function openProject(path) { 113 | function doOpen() { 114 | if (path) { 115 | _openProject(path); 116 | } else { 117 | dialogService 118 | .openFile(false, ['.json']) 119 | .then(function(path) { 120 | _openProject(path); 121 | }) 122 | .catch(function(e) {throw e;}); 123 | } 124 | } 125 | 126 | if ($window.editor.isDirty()) { 127 | dialogService 128 | .confirm( 129 | 'Leave without saving?', 130 | 'If you proceed you will lose all unsaved modifications.') 131 | .then(doOpen); 132 | } else { 133 | doOpen(); 134 | } 135 | } 136 | 137 | function editProject() { 138 | var project = projectModel.getProject(); 139 | 140 | dialogService 141 | .prompt('Rename project', null, 'input', project.name) 142 | .then(function(name) { 143 | // If no name provided, abort 144 | if (!name) { 145 | notificationService.error( 146 | 'Invalid name', 147 | 'You must provide a name for the project.' 148 | ); 149 | return; 150 | } 151 | 152 | project.name = name; 153 | projectModel 154 | .saveProject(project) 155 | .then(function() { 156 | _activate(); 157 | notificationService.success( 158 | 'Project renamed', 159 | 'The project has been renamed successfully.' 160 | ); 161 | }); 162 | }); 163 | } 164 | 165 | function saveProject() { 166 | projectModel 167 | .saveProject() 168 | .then(function() { 169 | notificationService.success( 170 | 'Project saved', 171 | 'The project has been saved' 172 | ); 173 | }, function() { 174 | notificationService.error( 175 | 'Error', 176 | 'Project couldn\'t be saved' 177 | ); 178 | }); 179 | } 180 | 181 | function closeProject() { 182 | function doClose() { 183 | projectModel.closeProject(); 184 | } 185 | 186 | if ($window.editor.isDirty()) { 187 | dialogService 188 | .confirm( 189 | 'Leave without saving?', 190 | 'If you proceed you will lose all unsaved modifications.', 191 | null) 192 | .then(doClose); 193 | } else { 194 | doClose(); 195 | } 196 | } 197 | 198 | function removeProject(path) { 199 | dialogService. 200 | confirm( 201 | 'Remove project?', 202 | 'Are you sure you want to remove this project?' 203 | ).then(function() { 204 | projectModel 205 | .removeProject(path) 206 | .then(function() { 207 | _activate(); 208 | notificationService.success( 209 | 'Project removed', 210 | 'The project has been removed from editor.' 211 | ); 212 | }); 213 | }); 214 | } 215 | } 216 | })(); 217 | -------------------------------------------------------------------------------- /src/app/pages/editor/components/menubar.html: -------------------------------------------------------------------------------- 1 | 169 | --------------------------------------------------------------------------------