Quellcode durchsuchen

cdb-view: base implementation

lviguier vor 1 Jahr
Ursprung
Commit
f876f68f89
62 geänderte Dateien mit 6032 neuen und 2079 gelöschten Zeilen
  1. 69 0
      bin/cdb.css
  2. 84 0
      bin/cdb.less
  3. 2 0
      bin/defaultProps.json
  4. 7 1
      bin/libs/goldenlayout.js
  5. BIN
      bin/res/Inter-Medium.ttf
  6. 203 111
      bin/style.css
  7. 293 168
      bin/style.less
  8. 8 0
      hide/comp/CurveEditor.hx
  9. 21 0
      hide/comp/SVG.hx
  10. 21 8
      hide/comp/Scene.hx
  11. 16 9
      hide/comp/SceneEditor.hx
  12. 4 0
      hide/comp/Toolbar.hx
  13. 9 1
      hide/comp/cdb/Cell.hx
  14. 141 17
      hide/comp/cdb/Editor.hx
  15. 642 0
      hide/comp/cdb/SheetView.hx
  16. 19 5
      hide/comp/cdb/Table.hx
  17. 1 5
      hide/prefab/ContextShared.hx
  18. 0 782
      hide/view/Graph.hx
  19. 1866 0
      hide/view/GraphEditor.hx
  20. 108 0
      hide/view/GraphInterface.hx
  21. 33 4
      hide/view/Image.hx
  22. 161 143
      hide/view/shadereditor/Box.hx
  23. 291 612
      hide/view/shadereditor/ShaderEditor.hx
  24. 459 0
      hide/view/substanceeditor/SubstanceEditor.hx
  25. 3 0
      hrt/prefab/Light.hx
  26. 67 30
      hrt/prefab/Material.hx
  27. 7 7
      hrt/prefab/Prefab.hx
  28. 4 3
      hrt/prefab/Reference.hx
  29. 1 2
      hrt/prefab/Shader.hx
  30. 1 1
      hrt/prefab/fx/Emitter.hx
  31. 1 1
      hrt/prefab/l3d/Instance.hx
  32. 2 2
      hrt/prefab/rfx/ScreenShaderGraph.hx
  33. 215 0
      hrt/sbsgraph/Macros.hx
  34. 251 0
      hrt/sbsgraph/SubstanceGraph.hx
  35. 217 0
      hrt/sbsgraph/SubstanceNode.hx
  36. 98 0
      hrt/sbsgraph/nodes/BnWNoise.hx
  37. 34 0
      hrt/sbsgraph/nodes/Comment.hx
  38. 17 0
      hrt/sbsgraph/nodes/Disc.hx
  39. 44 0
      hrt/sbsgraph/nodes/Multiply.hx
  40. 52 0
      hrt/sbsgraph/nodes/RGBAMerge.hx
  41. 64 0
      hrt/sbsgraph/nodes/RGBASplit.hx
  42. 41 0
      hrt/sbsgraph/nodes/SubstanceOutput.hx
  43. 67 0
      hrt/sbsgraph/nodes/WhiteNoise.hx
  44. 12 8
      hrt/shgraph/NodeGenContext.hx
  45. 10 9
      hrt/shgraph/Random.hx
  46. 3 2
      hrt/shgraph/ShaderGlobalInput.hx
  47. 69 73
      hrt/shgraph/ShaderGraph.hx
  48. 1 0
      hrt/shgraph/ShaderInput.hx
  49. 126 5
      hrt/shgraph/ShaderNode.hx
  50. 1 0
      hrt/shgraph/ShaderOutput.hx
  51. 13 59
      hrt/shgraph/ShaderParam.hx
  52. 2 2
      hrt/shgraph/Variables.hx
  53. 0 1
      hrt/shgraph/nodes/Add.hx
  54. 20 0
      hrt/shgraph/nodes/AlphaOver.hx
  55. 1 1
      hrt/shgraph/nodes/CombineAlpha.hx
  56. 24 3
      hrt/shgraph/nodes/Comment.hx
  57. 3 4
      hrt/shgraph/nodes/Dissolve.hx
  58. 1 0
      hrt/shgraph/nodes/Vec2.hx
  59. 1 0
      hrt/shgraph/nodes/Vec3.hx
  60. 1 0
      hrt/shgraph/nodes/Vec4.hx
  61. 11 0
      hrt/tools/Gizmo.hx
  62. 89 0
      hrt/tools/OneToMany.hx

+ 69 - 0
bin/cdb.css

@@ -577,6 +577,75 @@
   color: red;
   text-align: center;
 }
+.cdb .content-modal .sheet-view {
+  width: 100%;
+}
+.cdb .content-modal .sheet-view #separators-picker {
+  max-height: 200px;
+  width: 100%;
+  overflow-x: hidden;
+  overflow-y: scroll;
+  background-color: red;
+}
+.cdb .content-modal .sheet-view #separators-picker .sep {
+  display: flex;
+  width: 100%;
+  padding: 2px 2px 2px 2px;
+  background-color: #111;
+}
+.cdb .content-modal .sheet-view #separators-picker .sep.level-0 {
+  background-color: #3d3d3d;
+  padding-left: 0;
+}
+.cdb .content-modal .sheet-view #separators-picker .sep.level-1 {
+  background-color: #383838;
+  padding-left: 20px;
+}
+.cdb .content-modal .sheet-view #separators-picker .sep.level-2 {
+  background-color: #343434;
+  padding-left: 40px;
+}
+.cdb .content-modal .sheet-view #separators-picker .sep.level-3 {
+  background-color: #303030;
+  padding-left: 60px;
+}
+.cdb .content-modal .sheet-view #separators-picker .sep.level-4 {
+  background-color: #303030;
+  padding-left: 80px;
+}
+.cdb .content-modal .sheet-view #separators-picker .sep.level-5 {
+  background-color: #303030;
+  padding-left: 100px;
+}
+.cdb .content-modal .sheet-view #separators-picker .sep p {
+  margin: auto;
+  margin-top: 1px;
+  margin-left: 3px;
+}
+.cdb .content-modal .sheet-view #separators-picker .sep input {
+  margin: auto;
+  margin-left: 5px;
+  margin-right: 5px;
+}
+.cdb .content-modal .sheet-view #name {
+  display: flex;
+}
+.cdb .content-modal .sheet-view #name p {
+  margin: 0;
+  margin-right: 5px;
+}
+.cdb .content-modal .sheet-view #buttons {
+  width: 100%;
+  display: inline-block;
+  text-align: center;
+  margin-top: 20px;
+}
+.cdb .content-modal .sheet-view #buttons input[type="button"] {
+  margin: auto;
+  margin-left: 5px;
+  margin-right: 5px;
+  padding: 2px 5px 2px 5px;
+}
 body.fullScreenMode .lm_content,
 body.fullScreenMode .lm_header,
 body.fullScreenMode .lm_splitter {

+ 84 - 0
bin/cdb.less

@@ -628,6 +628,90 @@
 			color: red;
 			text-align: center;
 		}
+
+		.sheet-view {
+			width: 100%;
+
+			#separators-picker {
+				max-height: 200px;
+				width: 100%;
+				overflow-x: hidden;
+				overflow-y: scroll;
+				background-color: red;
+
+				.sep {
+					display: flex;
+					width: 100%;
+					padding: 2px 2px 2px 2px;
+					background-color: #111;
+
+					&.level-0 {
+						background-color: #3d3d3d;
+						padding-left: 0;
+					}
+
+					&.level-1 {
+						background-color: #383838;
+						padding-left: 20px;
+					}
+
+					&.level-2 {
+						background-color: #343434;
+						padding-left: 40px;
+					}
+
+					&.level-3 {
+						background-color: #303030;
+						padding-left: 60px;
+					}
+
+					&.level-4 {
+						background-color: #303030;
+						padding-left: 80px;
+					}
+
+					&.level-5 {
+						background-color: #303030;
+						padding-left: 100px;
+					}
+
+					p {
+						margin: auto;
+						margin-top: 1px;
+						margin-left: 3px;
+					}
+
+					input {
+						margin: auto;
+						margin-left: 5px;
+						margin-right: 5px;
+					}
+				}
+			}
+
+			#name {
+				display: flex;
+
+				p {
+					margin: 0;
+					margin-right: 5px;
+				}
+			}
+
+			#buttons {
+				width: 100%;
+				display: inline-block;
+				text-align: center;
+				margin-top: 20px;
+
+				input[type="button"] {
+					margin: auto;
+					margin-left: 5px;
+					margin-right: 5px;
+					padding: 2px 5px 2px 5px;
+				}
+			}
+		}
 	}
 }
 

+ 2 - 0
bin/defaultProps.json

@@ -86,9 +86,11 @@
 	"key.sceneeditor.rotationMode": "E",
 	"key.sceneeditor.scalingMode": "R",
 	"key.sceneeditor.toggleSnap": "X",
+	"key.sceneeditor.switchMode" : "Space",
 
 	"key.shadergraph.hide" : "H",
 	"key.shadergraph.comment" : "C",
+	"key.graph.openAddMenu" : "Space",
 
 
 

+ 7 - 1
bin/libs/goldenlayout.js

@@ -4399,15 +4399,21 @@ lm.utils.copy( lm.items.Stack.prototype, {
 
 	removeChild: function( contentItem, keepChild ) {
 		var index = lm.utils.indexOf( contentItem, this.contentItems );
+		var curIndex = lm.utils.indexOf( this._activeContentItem, this.contentItems );
 		lm.items.AbstractContentItem.prototype.removeChild.call( this, contentItem, keepChild );
 		this.header.removeTab( contentItem );
 
 		if( this.contentItems.length > 0 ) {
-			this.setActiveContentItem( this.contentItems[ Math.max( index - 1, 0 ) ] );
+			var indexToFocus = index > curIndex ? curIndex : curIndex - 1;
+			if (curIndex == index)
+				indexToFocus = Math.max( index - 1, 0 );
+
+			this.setActiveContentItem( this.contentItems[ indexToFocus ] );
 		} else {
 			this._activeContentItem = null;
 		}
 
+
 		this._$validateClosability();
 		this.emitBubblingEvent( 'stateChanged' );
 	},

BIN
bin/res/Inter-Medium.ttf


+ 203 - 111
bin/style.css

@@ -5,6 +5,12 @@
   font-weight: normal;
   font-style: normal;
 }
+@font-face {
+  font-family: 'Inter';
+  src: url('res/Inter-Medium.ttf');
+  font-weight: normal;
+  font-style: normal;
+}
 body {
   margin: 0;
   padding: 0;
@@ -817,14 +823,19 @@ input[type=checkbox]:checked:after {
   border-top-right-radius: 4px;
   border-bottom-right-radius: 4px;
 }
+.tb-group > *.button2.transparent {
+  opacity: 0.5;
+}
 .tb-group > *.button2:hover {
   border-color: #3d3d3d;
   background: #656565;
+  opacity: 1;
 }
 .tb-group > *.button2:active,
 .tb-group > *.button2[checked],
 .tb-group > *.menu:active {
   background: #969696;
+  opacity: 1;
 }
 .tb-group > *.menu {
   background: #282828;
@@ -1929,81 +1940,91 @@ input[type=checkbox]:checked:after {
   visibility: visible;
 }
 /* Shader Editor */
-.graph-view {
-  outline: none !important;
+.shader-editor {
+  display: flex;
+  flex-direction: row;
 }
-.graph-view .mini-preview {
+.shader-editor #preview {
+  z-index: 3;
   position: absolute;
-  top: 0px;
-  left: 0px;
-  z-index: 0;
+  width: 300px;
+  height: 300px;
+  background-color: #111;
+  border: 1px solid #444;
+  right: 35px;
+  bottom: 35px;
 }
-.graph-view #graph-root {
+.shader-editor #preview .hide-toolbar2 {
   position: absolute;
-  top: 0px;
-  left: 0px;
-  z-index: 0;
+  right: 8px;
+  top: 8px;
 }
-.graph-view .tabs {
+.shader-editor .flex.vertical {
+  position: relative;
+}
+.shader-editor #rightPanel {
+  flex: 0;
+  display: flex;
+  flex-direction: column;
+  min-width: 270px;
   width: 280px;
   padding-top: 10px;
   border-left: 1px solid #444444;
-  z-index: 205;
   background: #222222;
-  height: 100%;
   user-select: none;
+  height: 100%;
 }
-.graph-view .tabs span {
+.shader-editor #rightPanel span {
   margin-left: 5px;
   font-size: 15px;
   font-weight: bold;
 }
-.graph-view .tabs .tab {
-  height: 100%;
+.shader-editor #rightPanel > div {
+  flex: 1;
 }
-.graph-view .tabs .tab .hide-block {
-  height: 600px;
+.shader-editor #rightPanel .hide-block {
+  height: 100%;
 }
-.graph-view .tabs .tab .hide-block #parametersList {
+.shader-editor #rightPanel .hide-block #parametersList {
   height: 100%;
 }
-.graph-view .tabs .tab .hide-block #parametersList .parameter {
+.shader-editor #rightPanel .hide-block #parametersList .parameter {
   border: 1px solid;
   margin: 5px;
   background-color: #222;
   position: relative;
   /*.content {
-							background-color: #b3b3b3;
-							padding: 5px;
-							box-shadow: inset 0px 13px 6px -15px black;
+						background-color: #b3b3b3;
+						padding: 5px;
+						box-shadow: inset 0px 13px 6px -15px black;
 
-							div {
-								width: 100%;
-								height: 20px;
+						div {
+							width: 100%;
+							height: 20px;
 
-								span {
-									float: left;
-									color: black;
-									font-size: 13px;
-									padding-right: 5px;
-									font-weight: normal;
-								}
+							span {
+								float: left;
+								color: black;
+								font-size: 13px;
+								padding-right: 5px;
+								font-weight: normal;
 							}
-							.texture-preview {
-								background-repeat: no-repeat;
-								background-size: 20px 20px!important;
-								border: 2px #444444 solid;
-								width: 20px;
+						}
+						.texture-preview {
+							background-repeat: no-repeat;
+							background-size: 20px 20px!important;
+							border: 2px #444444 solid;
+							width: 20px;
+						}
+						.action-btns {
+							input {
+								float: right;
 							}
-							.action-btns {
-								input {
-									float: right;
-								}
-							}
-						}*/
+						}
+					}*/
 }
-.graph-view .tabs .tab .hide-block #parametersList .parameter.hovertop:before,
-.graph-view .tabs .tab .hide-block #parametersList .parameter.hoverbot:after {
+.shader-editor #rightPanel .hide-block #parametersList .parameter.hovertop:before,
+.shader-editor #rightPanel .hide-block #parametersList .parameter.hoverbot:after {
   display: block;
   position: absolute;
   z-index: 100;
@@ -2011,59 +2032,161 @@ input[type=checkbox]:checked:after {
   width: 100%;
   content: "";
 }
-.graph-view .tabs .tab .hide-block #parametersList .parameter:before {
+.shader-editor #rightPanel .hide-block #parametersList .parameter:before {
   border-top: 5px solid red;
   top: -10px;
 }
-.graph-view .tabs .tab .hide-block #parametersList .parameter:after {
-  border-bottom: 5px solid red;
+.shader-editor #rightPanel .hide-block #parametersList .parameter:after {
+  border-bottom: 5px solid purple;
   bottom: -10px;
 }
-.graph-view .tabs .tab .hide-block #parametersList .parameter .header {
+.shader-editor #rightPanel .hide-block #parametersList .parameter .header {
   background-color: #d6d6d6;
   height: 20px;
   padding: 5px;
   color: black;
   cursor: pointer;
 }
-.graph-view .tabs .tab .hide-block #parametersList .parameter .header .title {
+.shader-editor #rightPanel .hide-block #parametersList .parameter .header .title {
   float: left;
 }
-.graph-view .tabs .tab .hide-block #parametersList .parameter .header .title input {
+.shader-editor #rightPanel .hide-block #parametersList .parameter .header .title input {
   width: 130px;
 }
-.graph-view .tabs .tab .hide-block #parametersList .parameter .header .type {
+.shader-editor #rightPanel .hide-block #parametersList .parameter .header .type {
   float: right;
 }
-.graph-view .tabs .tab .hide-block #parametersList .parameter .header .type span {
+.shader-editor #rightPanel .hide-block #parametersList .parameter .header .type span {
   color: black;
   font-style: italic;
   font-size: 13px;
   padding-right: 5px;
 }
-.graph-view .tabs .tab .hide-block #parametersList .parameter .content {
+.shader-editor #rightPanel .hide-block #parametersList .parameter .content {
   padding: 4px;
 }
-.graph-view .tabs .tab .hide-block #parametersList .parameter .content > div {
+.shader-editor #rightPanel .hide-block #parametersList .parameter .content > div {
   margin-top: 2px;
 }
-.graph-view .tabs .tab .hide-block #parametersList .parameter > span {
+.shader-editor #rightPanel .hide-block #parametersList .parameter > span {
   font-size: 12px;
 }
-.graph-view .tabs .tab .options-block {
+.shader-editor #rightPanel .options-block {
+  flex: 0;
   height: max(250px, 25%);
 }
-.graph-view .tabs .tab .options-block > * {
+.shader-editor #rightPanel .options-block > * {
   display: block;
   margin-bottom: 10px;
   width: 100%;
   text-align: center;
   box-sizing: border-box;
 }
-.graph-view .tabs .tab .options-block > div {
+.shader-editor #rightPanel .options-block > div {
   border: 1px solid #666;
   padding: 2px;
 }
+/* Substance Editor */
+.substance-editor {
+  display: flex;
+  flex-direction: row;
+}
+.substance-editor .flex.vertical {
+  position: relative;
+}
+.substance-editor #right-panel {
+  flex: 0;
+  display: flex;
+  flex-direction: column;
+  min-width: 270px;
+  width: 280px;
+  border-left: 1px solid #444444;
+  background: #222222;
+  height: 100%;
+  user-select: none;
+}
+.substance-editor #right-panel .group .header {
+  display: flex;
+  background-color: #444444;
+  padding: 5px;
+}
+.substance-editor #right-panel .group .header:hover {
+  background-color: #666666;
+}
+.substance-editor #right-panel .group .header .title {
+  text-transform: uppercase;
+  font-weight: bold;
+  margin-left: 7px;
+  font-family: Verdana, serif;
+  font-size: 9pt;
+  color: #aaa;
+}
+.substance-editor #right-panel .group .content {
+  display: block;
+  padding: 10px 20px 10px 20px;
+}
+.substance-editor #right-panel .group .content .title {
+  font-weight: bold;
+  font-family: Verdana, serif;
+  font-size: 9pt;
+  color: #aaa;
+  margin: 0 0 5px 0;
+}
+.substance-editor #right-panel .group .content .fields {
+  display: grid;
+  grid-template-columns: auto auto;
+  gap: 5px 5px;
+  padding-bottom: 10px;
+  border-bottom: 1px solid;
+  margin-bottom: 10px;
+  border-color: #373737;
+}
+.substance-editor #right-panel .group .content .fields.grid-3 {
+  grid-template-columns: auto auto 2%;
+}
+.substance-editor #right-panel .group .content .fields .reset {
+  padding-top: 3px;
+}
+.substance-editor #right-panel .group .content .fields .reset:hover {
+  color: #ffffff;
+}
+.substance-editor #tex-preview {
+  z-index: 3;
+  position: absolute;
+  width: 300px;
+  height: 300px;
+  background-color: #111;
+  border: 1px solid #444;
+  right: 35px;
+  bottom: 35px;
+}
+.substance-editor #tex-preview .hide-toolbar2 {
+  position: absolute;
+  right: 8px;
+  top: 8px;
+}
+.graph-view {
+  outline: none !important;
+  position: relative;
+}
+.graph-view .hide-toolbar2 {
+  position: absolute;
+  top: 10px;
+  left: 10px;
+  z-index: 999;
+}
+.graph-view .mini-preview {
+  position: absolute;
+  top: 0px;
+  left: 0px;
+  z-index: 0;
+}
+.graph-view #graph-root {
+  position: absolute;
+  top: 0px;
+  left: 0px;
+  z-index: 1;
+}
 .graph-view #add-menu {
   position: absolute;
   width: 400px;
@@ -2196,42 +2319,8 @@ input[type=checkbox]:checked:after {
 .graph-view .heaps-scene {
   outline: none !important;
 }
-.graph-view .heaps-scene #preview {
-  z-index: 3;
-  position: absolute;
-  width: 300px;
-  height: 300px;
-  background-color: #111;
-  border: 1px solid #444;
-  right: 290px;
-  bottom: 35px;
-}
-.graph-view .heaps-scene #status-bar {
-  z-index: 2;
-  position: absolute;
-  width: 84%;
-  min-height: 20px;
-  background-color: #111;
-  border: 1px solid #444;
-  left: -1px;
-  bottom: -1px;
-  max-height: 60%;
-  overflow: auto;
-}
-.graph-view .heaps-scene #status-bar pre {
-  padding: 10px;
-  vertical-align: sub;
-}
-.graph-view .heaps-scene #status-bar pre.error {
-  color: #c74848;
-}
-.graph-view .heaps-scene #status-bar #close {
-  width: 100%;
-  text-align: center;
-  cursor: pointer;
-}
-.graph-view .heaps-scene svg g {
-  stroke: #202020;
+.graph-view .heaps-scene svg {
+  font-family: 'Inter';
 }
 .graph-view .heaps-scene svg g.selected .outline {
   stroke: #668fff;
@@ -2242,15 +2331,22 @@ input[type=checkbox]:checked:after {
 .graph-view .heaps-scene svg .outline {
   fill: none;
 }
+.graph-view .heaps-scene svg .background {
+  fill: #393939;
+}
+.graph-view .heaps-scene svg .separator {
+  stroke: #272727;
+  stroke-width: 1px;
+}
 .graph-view .heaps-scene svg .resize {
   fill: rgba(0, 0, 0, 0);
   stroke: none;
 }
 .graph-view .heaps-scene svg .comment .head-box {
   fill: rgba(80, 80, 80, 0.5);
-  stroke-width: 3;
 }
 .graph-view .heaps-scene svg .comment .outline {
+  stroke: rgba(80, 80, 80, 0.5);
   stroke-width: 3;
 }
 .graph-view .heaps-scene svg .comment-title {
@@ -2267,22 +2363,15 @@ input[type=checkbox]:checked:after {
 .graph-view .heaps-scene svg .comment-title br {
   display: none;
 }
-.graph-view .heaps-scene svg .head-box {
-  fill: #155358;
-}
 .graph-view .heaps-scene svg .title-box {
-  fill: #ffffff;
+  text-anchor: start;
+  fill: #c3c3c3;
   stroke: none;
   user-select: none;
 }
 .graph-view .heaps-scene svg .title-box::selection {
   background: none;
 }
-.graph-view .heaps-scene svg .nodes,
-.graph-view .heaps-scene svg .properties {
-  fill: #737373;
-  opacity: 0.75;
-}
 .graph-view .heaps-scene svg .hasLink .input-field {
   display: none;
 }
@@ -2305,7 +2394,7 @@ input[type=checkbox]:checked:after {
   fill: #5ca4ab;
 }
 .graph-view .heaps-scene svg .title-node {
-  fill: #ffffff;
+  fill: #c3c3c3;
   stroke: none;
   user-select: none;
 }
@@ -2373,11 +2462,14 @@ input[type=checkbox]:checked:after {
   stroke-width: 2;
   stroke: #c8c8c8;
   fill: transparent;
+  pointer-events: stroke;
+}
+.graph-view .heaps-scene svg .edge.hitbox {
+  stroke-width: 10;
+  stroke: transparent;
 }
-.graph-view .heaps-scene svg .edge:not(.draft):hover,
-.graph-view .heaps-scene svg .edge:not(.draft).selected {
-  stroke-width: 5;
-  stroke: #72b4ff;
+.graph-view .heaps-scene svg .edge.hitbox:hover {
+  stroke: rgba(114, 180, 255, 0.5);
 }
 .graph-view .heaps-scene svg .rect-selection {
   stroke: rgba(0, 0, 255, 0.7);

+ 293 - 168
bin/style.less

@@ -8,6 +8,13 @@
 	font-style: normal;
 }
 
+@font-face {
+	font-family: 'Inter';
+	src: url('res/Inter-Medium.ttf');
+	font-weight: normal;
+	font-style: normal;
+}
+
 body {
 	margin : 0;
 	padding : 0;
@@ -871,13 +878,19 @@ input[type=checkbox] {
 			border-bottom-right-radius: 4px;
 		}
 
+		&.button2.transparent {
+			opacity: 0.5;
+		}
+
 		&.button2:hover {
 			border-color: @color-border-hover;
 			background: @color-tool-bg-hover;
+			opacity: 1.0;
 		}
 
 		&.button2:active, &.button2[checked], &.menu:active {
 			background: @color-highlight-gray;
+			opacity: 1.0;
 		}
 
 		&.menu {
@@ -2150,28 +2163,43 @@ input[type=checkbox] {
 }
 
 /* Shader Editor */
-.graph-view {
-	outline: none !important;
+.shader-editor {
+	display: flex;
+	flex-direction: row;
 
-	.mini-preview {
+	#preview {
+		z-index: 3;
 		position: absolute;
-		top: 0px;
-		left: 0px;
-		z-index: 0;
+
+		width: 300px;
+		height: 300px;
+
+		background-color: #111;
+		border: 1px solid #444;
+
+		right: 35px;
+		bottom: 35px;
+
+		.hide-toolbar2 {
+			position: absolute;
+			right: 8px;
+			top: 8px;
+		}
 	}
 
-	#graph-root {
-		position: absolute;
-		top: 0px;
-		left: 0px;
-		z-index: 0;
+	.flex.vertical {
+		position:relative;
 	}
 
-	.tabs {
+	#rightPanel {
+		flex: 0;
+		display: flex;
+		flex-direction: column;
+		min-width: 270px;
+
 		width: 280px;
 		padding-top: 10px;
 		border-left: 1px solid #444444;
-		z-index: @default-layer + 5;
     	background: #222222;
 		height: 100%;
 		user-select: none;
@@ -2181,131 +2209,256 @@ input[type=checkbox] {
 			font-size: 15px;
 			font-weight: bold;
 		}
+		>div {
+			flex: 1;
+		}
 
+		height: 100%;
+		@options-height: max(250px, 25%);
+		.hide-block {
+			height: 100%;
+			#parametersList {
+				height: 100%;
 
+				.parameter {
+					border: 1px solid;
+					margin: 5px;
+					background-color: #222;
+					position: relative;
 
-		.tab {
-			height: 100%;
-			@options-height: max(250px, 25%);
-			.hide-block {
-				height: 600px;
-
-				#parametersList {
-					height: 100%;
-
-					.parameter {
-						border: 1px solid;
-						margin: 5px;
-						background-color: #222;
-						position: relative;
-
-						&.hovertop:before, &.hoverbot:after {
-							display: block;
-							position: absolute;
-							z-index: 100;
-							margin: 0 auto;
-							width: 100%;
-							content: "";
-						}
+					&.hovertop:before, &.hoverbot:after {
+						display: block;
+						position: absolute;
+						z-index: 100;
+						margin: 0 auto;
+						width: 100%;
+						content: "";
+					}
 
-						&:before {
-							border-top: 5px solid red;
-							top: -10px;
-						}
+					&:before {
+						border-top: 5px solid red;
+						top: -10px;
+					}
 
-						&:after {
-							border-bottom: 5px solid red;
-							bottom: -10px;
-						}
+					&:after {
+						border-bottom: 5px solid purple;
+						bottom: -10px;
+					}
 
 
-						.header {
-							background-color: #d6d6d6;
-							height: 20px;
-							padding: 5px;
-							color: black;
-							cursor: pointer;
+					.header {
+						background-color: #d6d6d6;
+						height: 20px;
+						padding: 5px;
+						color: black;
+						cursor: pointer;
 
-							.title {
-								float: left;
-								input {
-									width: 130px;
-								}
+						.title {
+							float: left;
+							input {
+								width: 130px;
 							}
+						}
 
-							.type {
-								float: right;
+						.type {
+							float: right;
 
-								span {
-									color: black;
-									font-style: italic;
-									font-size: 13px;
-									padding-right: 5px;
-								}
+							span {
+								color: black;
+								font-style: italic;
+								font-size: 13px;
+								padding-right: 5px;
 							}
 						}
+					}
 
-						.content {
-							padding: 4px;
+					.content {
+						padding: 4px;
 
-							>div {
-								margin-top: 2px;
-							}
+						>div {
+							margin-top: 2px;
 						}
+					}
 
-						/*.content {
-							background-color: #b3b3b3;
-							padding: 5px;
-							box-shadow: inset 0px 13px 6px -15px black;
-
-							div {
-								width: 100%;
-								height: 20px;
-
-								span {
-									float: left;
-									color: black;
-									font-size: 13px;
-									padding-right: 5px;
-									font-weight: normal;
-								}
-							}
-							.texture-preview {
-								background-repeat: no-repeat;
-								background-size: 20px 20px!important;
-								border: 2px #444444 solid;
-								width: 20px;
+					/*.content {
+						background-color: #b3b3b3;
+						padding: 5px;
+						box-shadow: inset 0px 13px 6px -15px black;
+
+						div {
+							width: 100%;
+							height: 20px;
+
+							span {
+								float: left;
+								color: black;
+								font-size: 13px;
+								padding-right: 5px;
+								font-weight: normal;
 							}
-							.action-btns {
-								input {
-									float: right;
-								}
+						}
+						.texture-preview {
+							background-repeat: no-repeat;
+							background-size: 20px 20px!important;
+							border: 2px #444444 solid;
+							width: 20px;
+						}
+						.action-btns {
+							input {
+								float: right;
 							}
-						}*/
-
-						& > span {
-							font-size: 12px;
 						}
+					}*/
+
+					& > span {
+						font-size: 12px;
 					}
 				}
 			}
-			.options-block {
-				height: @options-height;
-				& > * {
-					display: block;
-					margin-bottom: 10px;
-					width: 100%;
-					text-align: center;
-					box-sizing: border-box;
+		}
+		.options-block {
+			flex: 0;
+			height: @options-height;
+			& > * {
+				display: block;
+				margin-bottom: 10px;
+				width: 100%;
+				text-align: center;
+				box-sizing: border-box;
+			}
+
+			& > div {
+				border: 1px solid #666;
+				padding: 2px;
+			}
+		}
+	}
+}
+
+/* Substance Editor */
+.substance-editor {
+	display: flex;
+	flex-direction: row;
+
+	.flex.vertical {
+		position:relative;
+	}
+
+	#right-panel {
+		flex: 0;
+		display: flex;
+		flex-direction: column;
+		min-width: 270px;
+
+		width: 280px;
+		border-left: 1px solid #444444;
+    	background: #222222;
+		height: 100%;
+		user-select: none;
+
+		.group {
+			.header {
+				display: flex;
+				background-color: #444444;
+				padding: 5px;
+
+				&:hover {
+					background-color: #666666;
 				}
 
-				& > div {
-					border: 1px solid #666;
-					padding: 2px;
+				.title {
+					text-transform: uppercase;
+					font-weight: bold;
+					margin-left: 7px;
+					font-family: Verdana, serif;
+					font-size: 9pt;
+					color: #aaa;
+				}
+			}
+
+			.content {
+				display: block;
+				padding: 10px 20px 10px 20px;
+
+				.title {
+					font-weight: bold;
+					font-family: Verdana, serif;
+					font-size: 9pt;
+					color: #aaa;
+					margin: 0 0 5px 0;
+				}
+
+				.fields {
+					display: grid;
+					grid-template-columns: auto auto;
+					gap: 5px 5px;
+					padding-bottom: 10px;
+					border-bottom: 1px solid;
+					margin-bottom: 10px;
+					border-color: #373737;
+
+					&.grid-3 {
+						grid-template-columns: auto auto 2%;
+					}
+
+					.reset {
+						&:hover {
+							color: #ffffff;
+						}
+
+						padding-top: 3px;
+					}
 				}
 			}
 		}
 	}
+
+	#tex-preview {
+		z-index: 3;
+		position: absolute;
+
+		width: 300px;
+		height: 300px;
+
+		background-color: #111;
+		border: 1px solid #444;
+
+		right: 35px;
+		bottom: 35px;
+
+		.hide-toolbar2 {
+			position: absolute;
+			right: 8px;
+			top: 8px;
+		}
+	}
+}
+
+.graph-view {
+	outline: none !important;
+	position: relative;
+
+	.hide-toolbar2 {
+		position: absolute;
+		top: 10px;
+		left: 10px;
+		z-index: 999;
+	}
+
+	.mini-preview {
+		position: absolute;
+		top: 0px;
+		left: 0px;
+		z-index: 0;
+	}
+
+	#graph-root {
+		position: absolute;
+		top: 0px;
+		left: 0px;
+		z-index: 1;
+	}
+
 	#add-menu {
 		position: absolute;
 		width: 400px;
@@ -2458,56 +2611,13 @@ input[type=checkbox] {
 		}
 
 	}
+
 	.heaps-scene {
 		outline: none !important;
-		#preview {
-			z-index: 3;
-			position: absolute;
-
-			width: 300px;
-			height: 300px;
-
-			background-color: #111;
-   			border: 1px solid #444;
-
-			right: 290px;
-			bottom: 35px;
-		}
-		#status-bar {
-			z-index: 2;
-			position: absolute;
-
-			width: 84%;
-			min-height: 20px;
-
-			background-color: #111;
-			border: 1px solid #444;
-
-			left: -1px;
-			bottom: -1px;
-			max-height: 60%;
-			overflow: auto;
-
-			pre {
-				padding: 10px;
-				vertical-align: sub;
-
-				&.error {
-					color: #c74848;
-				}
-			}
-
-			#close {
-				width: 100%;
-				text-align: center;
-				cursor: pointer;
-			}
-		}
 		svg {
 
+			font-family: 'Inter';
 			g {
-				stroke: #202020;
-
 				&.selected {
 					.outline {
 						stroke: #668fff;
@@ -2523,6 +2633,15 @@ input[type=checkbox] {
 				fill: none;
 			}
 
+			.background {
+				fill: #393939;
+			}
+
+			.separator {
+				stroke: #272727;
+				stroke-width: 1px;
+			}
+
 			.resize {
 				fill: rgba(0, 0, 0, 0.0);
 				stroke: none;
@@ -2531,10 +2650,11 @@ input[type=checkbox] {
 			.comment {
 				.head-box {
 					fill: rgba(80,80,80,0.5);
-					stroke-width: 3;
+					//stroke-width: 3;
 				}
 
 				.outline {
+					stroke: rgba(80,80,80,0.5);
 					stroke-width: 3;
 				}
 			}
@@ -2554,12 +2674,13 @@ input[type=checkbox] {
 				}
 			}
 
-			.head-box {
-				fill: #155358;
-			}
+			// .head-box {
+			// 	fill: #155358;
+			// }
 
 			.title-box {
-				fill: #ffffff;
+				text-anchor: start;
+				fill: #c3c3c3;
 				stroke: none;
 				user-select: none;
 				&::selection {
@@ -2567,10 +2688,10 @@ input[type=checkbox] {
 				}
 			}
 
-			.nodes, .properties {
-				fill: #737373;
-				opacity: 0.75;
-			}
+			// .nodes, .properties {
+			// 	fill: #737373;
+			// 	opacity: 0.75;
+			// }
 
 			.hasLink {
 				.input-field {
@@ -2600,7 +2721,7 @@ input[type=checkbox] {
 			}
 
 			.title-node {
-				fill: #ffffff;
+				fill: #c3c3c3;
 				stroke: none;
 				user-select: none;
 				&::selection {
@@ -2678,11 +2799,15 @@ input[type=checkbox] {
 				stroke-width: 2;
 				stroke: rgb(200, 200, 200);
 				fill: transparent;
-			}
+				pointer-events: stroke;
 
-			.edge:not(.draft):hover, .edge:not(.draft).selected {
-				stroke-width: 5;
-				stroke: rgb(114, 180, 255);
+				&.hitbox {
+					stroke-width: 10;
+					stroke: transparent;
+					&:hover {
+						stroke: rgba(114, 180, 255, 0.5);
+					}
+				}
 			}
 
 			.rect-selection {

+ 8 - 0
hide/comp/CurveEditor.hx

@@ -1180,6 +1180,14 @@ class CurveEditor extends hide.comp.Component {
 		svg.line(markersGroup, xt(this.currentTime), svg.element.height(), xt(this.currentTime), labelHeight / 2.0, { stroke:'#426dae', 'stroke-width':'2px' });
 		drawLabel(markersGroup, xt(this.currentTime), labelHeight / 2.0 + (tlHeight - labelHeight) / 2.0, labelWidth, labelHeight, { fill:'#426dae', stroke: '#426dae', 'stroke-width':'5px', 'stroke-linejoin':'round'});
 		svg.text(markersGroup, xt(this.currentTime), 14, '${rounderCurrTime}', { 'fill':'#e7ecf5', 'text-anchor':'middle', 'font':'10px sans-serif'});
+
+
+		function drawMaker(x: Float) {
+			svg.line(markersGroup, xt(x), svg.element.height(), xt(x), labelHeight / 2.0, { stroke: '#260f00', 'stroke-width' : '2px'});
+		}
+
+		drawMaker(0);
+
 	}
 
 	public function refreshOverlay(?duration: Float) {

+ 21 - 0
hide/comp/SVG.hx

@@ -49,6 +49,27 @@ class SVG extends Component {
 		return make(parent, "path", {d : 'M ${x1} ${y1} Q ${xTurn} ${yTurn}, ${x1 + (x2-x1)/2} ${y1 + (y2-y1)/2}, T ${x2} ${y2}'}, style);
 	}
 
+	public function straightCurve(?parent: Element, x1: Float, y1: Float, x2: Float, y2: Float, advance: Float, round: Float, ?style:Dynamic) {
+
+		var dx = (x2 - advance) - (x1 + advance);
+		var dy = y2 - y1;
+		var len = hxd.Math.max(0.001, hxd.Math.distance(dx, dy));
+
+
+		var diagXOffset = dx/len * advance * round;
+		var diagYOffset = dy/len * advance * round;
+
+		return make(parent, "path",
+			{d : '
+				M ${x1} ${y1}
+				L ${x1+advance * (1.0-round)} ${y1}
+				Q ${x1+advance} ${y1}, ${x1+advance+diagXOffset} ${y1+diagYOffset}
+				L ${x2-advance-diagXOffset} ${y2-diagYOffset}
+				Q ${x2-advance} ${y2}, ${x2-advance * (1.0-round)} ${y2}
+				L ${x2} ${y2}
+				'}, style);
+	}
+
 	public function foreignObject(?parent: Element, x:Float, y:Float, width:Float, height:Float, ?style:Dynamic) {
 		return make(parent, "foreignObject", {x:x, y:y, width:width, height:height}, style);
 	}

+ 21 - 8
hide/comp/Scene.hx

@@ -32,7 +32,6 @@ class Scene extends hide.comp.Component implements h3d.IDrawable {
 		this.config = config;
 		element.addClass("hide-scene-container");
 		canvas = cast new Element("<canvas class='hide-scene' style='width:100%;height:100%'/>").appendTo(element)[0];
-		trace(canvas);
 
 		canvas.addEventListener("mousemove",function(_) canvas.focus());
 		canvas.addEventListener("mouseleave",function(_) canvas.blur());
@@ -349,11 +348,14 @@ class Scene extends hide.comp.Component implements h3d.IDrawable {
 		return loadTexture("", t, onReady);
 	}
 
-	public function loadTexture( modelPath : String, texturePath : String, ?onReady : h3d.mat.Texture -> Void, async=false, ?uncompressed: Bool = false) {
+	public function loadTexture( modelPath : String, texturePath : String, ?onReady : h3d.mat.Texture -> Void,  ?onFail : Void -> Void, async=false, ?uncompressed: Bool = false) {
 		checkCurrent();
 		var path = resolvePath(modelPath, texturePath);
 		if( path == null ) {
-			ide.error("Could not load texture " + { modelPath : modelPath, texturePath : texturePath });
+			if (onFail != null)
+				onFail();
+			else
+				ide.error("Could not load texture " + { modelPath : modelPath, texturePath : texturePath });
 			return null;
 		}
 		var t = texCache.get(path);
@@ -363,14 +365,22 @@ class Scene extends hide.comp.Component implements h3d.IDrawable {
 		}
 		var relPath = StringTools.startsWith(path, ide.resourceDir) ? path.substr(ide.resourceDir.length+1) : path;
 
-		var res = try hxd.res.Loader.currentInstance.load(relPath) catch( e : hxd.res.NotFound ) {
+		function loadUncompressed() {
 			var bytes = sys.io.File.getBytes(path);
-			hxd.res.Any.fromBytes(path, bytes);
+			return hxd.res.Any.fromBytes(path, bytes);
+		}
+
+		var res = try hxd.res.Loader.currentInstance.load(relPath) catch( e ) {
+			if (e is hxd.res.NotFound)
+				ide.quickError('Resource not found, original texture is loaded instead');
+			else if (onFail != null)
+				onFail();
+
+			loadUncompressed();
 		};
 
 		if (uncompressed) {
-			var bytes = sys.io.File.getBytes(path);
-			res = hxd.res.Any.fromBytes(path, bytes);
+			loadUncompressed();
 		}
 
 		if( onReady == null ) onReady = function(_) {};
@@ -381,7 +391,10 @@ class Scene extends hide.comp.Component implements h3d.IDrawable {
 			t.setName( ide.makeRelative(path));
 			texCache.set(path, t);
 		} catch( error : Dynamic ) {
-			throw "Could not load texure " + texturePath + ":\n" + Std.string(error);
+			if (onFail != null)
+				onFail();
+			else
+				throw "Could not load texure " + texturePath + ":\n" + Std.string(error);
 		};
 		return t;
 	}

+ 16 - 9
hide/comp/SceneEditor.hx

@@ -1453,7 +1453,7 @@ class SceneEditor {
 		if (settings == null)
 			return;
 
-		var id = Std.parseInt(settings.camTypeIndex);
+		var id = Std.parseInt(settings.camTypeIndex) ?? 0;
         var newClass = CameraControllerEditor.controllersClasses[id];
         if (Type.getClass(cameraController) != newClass.cl)
             switchCamController(newClass.cl);
@@ -1553,6 +1553,7 @@ class SceneEditor {
 		view.keys.register("sceneeditor.translationMode", gizmo.translationMode);
 		view.keys.register("sceneeditor.rotationMode", gizmo.rotationMode);
 		view.keys.register("sceneeditor.scalingMode", gizmo.scalingMode);
+		view.keys.register("sceneeditor.switchMode", gizmo.switchMode);
 
 		statusText = new h2d.Text(hxd.res.DefaultFont.get(), scene.s2d);
 		statusText = new h2d.Text(hxd.res.DefaultFont.get(), scene.s2d);
@@ -1913,7 +1914,7 @@ class SceneEditor {
 	function createRenderProps(?parent: hrt.prefab.Prefab){
 		if (renderPropsRoot == null) {
 			renderPropsRoot = new hrt.prefab.Reference(parent, parent?.shared ?? new ContextShared());
-			renderPropsRoot.setEditor(this);
+			renderPropsRoot.setEditor(this, this.scene);
 
 			if (parent != null)
 				renderPropsRoot.parent = parent;
@@ -2014,7 +2015,7 @@ class SceneEditor {
 
 		@:privateAccess sceneData.setSharedRec(new ContextShared(root2d,root3d));
 		sceneData.shared.currentPath = view.state.path;
-		sceneData.setEditor(this);
+		sceneData.setEditor(this, this.scene);
 
 		sceneData.make();
 		var bgcol = scene.engine.backgroundColor;
@@ -2828,7 +2829,7 @@ class SceneEditor {
 
 		elt.shared.current3d = elt.parent.findFirstLocal3d();
 		elt.shared.current2d = elt.parent.findFirstLocal2d();
-		elt.setEditor(this);
+		elt.setEditor(this, this.scene);
 		elt.make();
 
 		for( p in elt.flatten() ) {
@@ -3521,7 +3522,13 @@ class SceneEditor {
 			return;
         }
 
-		var elts = selectedPrefabs;
+		// Sort the selection to match the scene order
+		var elts : Array<hrt.prefab.Prefab> = [];
+		for (p in sceneData.flatten()) {
+			if (selectedPrefabs.contains(p))
+				elts.push(p);
+		}
+
 		var parent = elts[0].parent;
 		var parentMat = worldMat(parent);
 		var invParentMat = parentMat.clone();
@@ -3556,13 +3563,11 @@ class SceneEditor {
 		var effectFunc = reparentImpl(elts, group, 0);
 		undo.change(Custom(function(undo) {
 			if(undo) {
-				group.parent = null;
-				//context.shared.contexts.remove(group);
 				effectFunc(true);
+				group.parent = null;
 			}
 			else {
 				group.parent = parent;
-				//context.shared.contexts.set(group, groupCtx);
 				effectFunc(false);
 			}
 			if(undo)
@@ -4042,8 +4047,8 @@ class SceneEditor {
 				};
 			});
 		}
-		return function(undo) {
 
+		return function(undo) {
 			// Remove all the children from their parent before
 			// adding them back in. Makes the index of insert() correct
 			if (!undo) {
@@ -4051,6 +4056,7 @@ class SceneEditor {
 					elt.parent.children.remove(elt);
 				}
 			}
+
 			for(f in effects) {
 				f(undo);
 			}
@@ -4058,6 +4064,7 @@ class SceneEditor {
 			if (!removeInstance(toElt)) {
 				return true;
 			}
+
 			makePrefab(toElt);
 			return false;
 		}

+ 4 - 0
hide/comp/Toolbar.hx

@@ -112,6 +112,10 @@ class Toolbar extends Component {
 		if( (saveToggleState && Ide.inst.currentConfig.get('sceneeditor.${id}', def)) || (!saveToggleState && def) )
 			tog();
 
+		if (saveToggleState) {
+			onToggle(Ide.inst.currentConfig.get('sceneeditor.${id}', def));
+		}
+
 		return {
 			id : id,
 			element : e,

+ 9 - 1
hide/comp/cdb/Cell.hx

@@ -1260,9 +1260,17 @@ class Cell {
 
 		switch( column.type ) {
 		case TId:
+			var isView = SheetView.isView(table.sheet);
+			var lineObj = isView ? SheetView.getOriginalObject(line) : line.obj;
+
 			if (isUniqueID(newValue, true)) {
-				editor.changeID(line.obj, newValue, column, table);
+				editor.changeID(lineObj, newValue, column, table);
 				currentValue = newValue;
+
+				if (isView) {
+					SheetView.reloadSheet(table.sheet);
+					editor.refresh();
+				}
 			}
 			focus();
 		case TString if( column.kind == Script || column.kind == Localizable ):

+ 141 - 17
hide/comp/cdb/Editor.hx

@@ -41,8 +41,14 @@ typedef EditorColumnProps = {
 	var ?categories : Array<String>;
 }
 
+typedef ViewProps = {
+	var originalSheet : String;
+	var sepIndexes : Array<Int>;
+}
+
 typedef EditorSheetProps = {
 	var ?categories : Array<String>;
+	var ?view : ViewProps;
 }
 
 typedef SearchFilter = {
@@ -170,6 +176,10 @@ class Editor extends Component {
 		if( displayMode == null ) displayMode = Table;
 		DataFiles.load();
 		if( currentValue == null ) currentValue = api.copy();
+
+		if (getSheetProps(sheet).view != null)
+			SheetView.reloadSheet(sheet);
+
 		refresh();
 	}
 
@@ -805,6 +815,28 @@ class Editor extends Component {
 
 	public function changeObject( line : Line, column : cdb.Data.Column, value : Dynamic ) {
 		beginChanges();
+
+		// If we are changing value of a view, apply the changes on original sheet instead
+		if(SheetView.isView(line.table.sheet)) {
+			var originalSheet = SheetView.getOriginalSheet(line.table.sheet);
+			var obj = SheetView.getOriginalObject(line);
+
+			var prev = Reflect.field(obj, column.name);
+			if( value == null ) {
+				formulas.setForValue(obj, originalSheet, column, null);
+			} else {
+				Reflect.setField(obj, column.name, value);
+				formulas.removeFromValue(obj, column);
+			}
+
+			originalSheet.realSheet.updateValue(column, line.index, prev);
+			line.evaluate(); // propagate
+			endChanges();
+			SheetView.reloadSheet(line.table.sheet);
+			refresh();
+			return;
+		}
+
 		var prev = Reflect.field(line.obj, column.name);
 		if( value == null ) {
 			formulas.setForValue(line.obj, line.table.sheet, column, null);
@@ -925,6 +957,13 @@ class Editor extends Component {
 	static var runningHooks = false;
 	static var queuedCommand: Void -> Void = null;
 	function save() {
+		// Unload views before saving (we don't want to save their datas since they are loaded
+		// when user select them)
+		for (sheet in base.sheets) {
+			if (SheetView.isView(sheet))
+				SheetView.unloadSheet(sheet);
+		}
+
 		api.save();
 
 		function hookEnd() {
@@ -962,6 +1001,9 @@ class Editor extends Component {
 				}
 			}
 		}
+
+		if (base.sheets.contains(currentSheet) && SheetView.isView(currentSheet))
+			SheetView.loadSheet(currentSheet);
 	}
 
 
@@ -1743,18 +1785,20 @@ class Editor extends Component {
 		#if js
 		var modal = new hide.comp.cdb.ModalColumnForm(this, sheet, col, element);
 		modal.setCallback(function() {
+			var s = SheetView.isView(sheet) ? SheetView.getOriginalSheet(sheet) : sheet;
 			var c = modal.getColumn(col);
 			if (c == null)
 				return;
 			beginChanges(true);
 			var err;
 			if( col != null ) {
+				var orginalCol = s.columns[sheet.columns.indexOf(col)];
 				var newPath = c.name;
 				var back = newPath.split("/");
 				var finalPart = back.pop();
 				var path = finalPart.split(".");
 				c.name = path.pop();
-				err = base.updateColumn(sheet, col, c);
+				err = base.updateColumn(s, orginalCol, c);
 				if (path.length > 0 || back.length > 0) {
 					function handleMoveTable() {
 						var cdbPath = sheet.getPath().split("@");
@@ -1800,8 +1844,9 @@ class Editor extends Component {
 					err = handleMoveTable();
 				}
 			}
-			else
-				err = sheet.addColumn(c, index == null ? null : index + 1);
+			else {
+				err = s.addColumn(c, index == null ? null : index + 1);
+			}
 			endChanges();
 			if (err != null) {
 				modal.error(err);
@@ -1967,8 +2012,11 @@ class Editor extends Component {
 				nextVisibleColumnIndex(table, indexColumn, Left) > -1), click : function () {
 				beginChanges();
 				var nextIndex = nextVisibleColumnIndex(table, indexColumn, Left);
-				sheet.columns.remove(col);
-				sheet.columns.insert(nextIndex, col);
+
+				var s = SheetView.getOriginalSheet(sheet);
+				var colToMove = s.columns[indexColumn];
+				s.columns.remove(colToMove);
+				s.columns.insert(nextIndex, colToMove);
 				if (cursor.x == indexColumn)
 					cursor.set(cursor.table, nextIndex, cursor.y);
 				else if (cursor.x == nextIndex)
@@ -1980,8 +2028,11 @@ class Editor extends Component {
 				nextVisibleColumnIndex(table, indexColumn, Right) < sheet.columns.length), click : function () {
 				beginChanges();
 				var nextIndex = nextVisibleColumnIndex(table, indexColumn, Right);
-				sheet.columns.remove(col);
-				sheet.columns.insert(nextIndex, col);
+
+				var s = SheetView.getOriginalSheet(sheet);
+				var colToMove = s.columns[indexColumn];
+				s.columns.remove(colToMove);
+				s.columns.insert(nextIndex, colToMove);
 				if (cursor.x == indexColumn)
 					cursor.set(cursor.table, nextIndex, cursor.y);
 				else if (cursor.x == nextIndex)
@@ -1998,7 +2049,10 @@ class Editor extends Component {
 						changeObject(cell.line, col, base.getDefault(col,sheet));
 					} else {
 						beginChanges(true);
-						sheet.deleteColumn(col.name);
+						if (SheetView.isView(sheet))
+							SheetView.getOriginalSheet(sheet).deleteColumn(col.name);
+						else
+							sheet.deleteColumn(col.name);
 					}
 					endChanges();
 					refresh();
@@ -2202,26 +2256,58 @@ class Editor extends Component {
 			{
 				label : "Move Up",
 				enabled:  (firstLine.index > 0 || sepIndex >= 0),
-				click : isSelectedLine ? moveLines.bind(selection, -1) : () -> moveLine(line, -1),
+				click : function() {
+					if (SheetView.isView(sheet)) {
+						SheetView.moveLine(this, line, -1);
+						return;
+					}
+
+					if (isSelectedLine)
+						moveLines.bind(selection, -1)
+					else
+						moveLine(line, -1);
+				},
 			},
 			{
 				label : "Move Down",
 				enabled:  (lastLine.index < sheet.lines.length - 1),
-				click : isSelectedLine ? moveLines.bind(selection, 1) : () -> moveLine(line, 1),
+				click : function() {
+					if (SheetView.isView(sheet)) {
+						SheetView.moveLine(this, line, 1);
+						return;
+					}
+
+					if (isSelectedLine)
+						moveLines.bind(selection, 1)
+					else
+						moveLine(line, 1);
+				},
 			},
-			{ label : "Move to Group", enabled : moveSubmenu.length > 0, menu : moveSubmenu },
+			{ label : "Move to Group", enabled : moveSubmenu.length > 0 && !SheetView.isView(sheet), menu : moveSubmenu },
 			{ label : "", isSeparator : true },
 			{ label : "Insert", click : function() {
-				insertLine(line.table,line.index);
+				var isView = SheetView.isView(line.table.sheet);
+				if (isView)
+					SheetView.insertLine(this, line);
+				else
+					insertLine(line.table,line.index);
 				cursor.set(line.table, -1, line.index + 1);
 				focus();
 			}, keys : config.get("key.cdb.insertLine") },
 			{ label : "Duplicate", click : function() {
-				duplicateLine(line.table,line.index);
+				if (SheetView.isView(sheet))
+					SheetView.duplicateLine(this, line);
+				else
+					duplicateLine(line.table,line.index);
 				cursor.set(line.table, -1, line.index + 1);
 				focus();
 			}, keys : config.get("key.duplicate") },
 			{ label : "Delete", click : function() {
+				if (SheetView.isView(sheet)) {
+					SheetView.deleteLine(this, line);
+					return;
+				}
+
 				var id = line.getId();
 				if( id != null && id.length > 0) {
 					var refs = getReferences(id, sheet);
@@ -2231,12 +2317,15 @@ class Editor extends Component {
 							return;
 					}
 				}
+
 				beginChanges();
 				sheet.deleteLine(line.index);
 				endChanges();
 				refreshAll();
 			} },
-			{ label : "Separator", enabled : !sheet.props.hide, checked : sepIndex >= 0, click : function() {
+		];
+		if (!SheetView.isView(sheet)) {
+			menu.push({ label : "Separator", enabled : !sheet.props.hide, checked : sepIndex >= 0, click : function() {
 				beginChanges();
 				if( sepIndex >= 0 ) {
 					sheet.separators.splice(sepIndex, 1);
@@ -2256,8 +2345,8 @@ class Editor extends Component {
 				}
 				endChanges();
 				refresh();
-			} }
-		];
+			}});
+		}
 		if( hasLocText ) {
 			menu.push({ label : "", isSeparator : true });
 			menu.push({
@@ -2344,7 +2433,7 @@ class Editor extends Component {
 		return menu;
 	}
 
-	public function createDBSheet( ?index : Int ) {
+	public function createDBSheet( ?index : Int) {
 		var value = ide.ask("Sheet name");
 		if( value == "" || value == null ) return null;
 		var s = ide.database.createSheet(value, index);
@@ -2357,6 +2446,40 @@ class Editor extends Component {
 		return s;
 	}
 
+	public function createView( originalSheet : cdb.Sheet, viewSheet : cdb.Sheet, ?index : Int) {
+		#if js
+		var modal = new hide.comp.cdb.SheetView.SheetViewModal(this, originalSheet, viewSheet, element);
+		modal.setCallback(function() {
+			beginChanges(true);
+
+			var editForm = viewSheet != null;
+			if (!editForm) {
+				viewSheet = ide.database.createSheet(modal.getSheetName(), index);
+				if (viewSheet == null) {
+					ide.error("Name already exists");
+					return;
+				}
+
+				viewSheet.props.editor = {
+					categories : getSheetProps(originalSheet).categories?.copy(),
+					view : { originalSheet: modal.getOriginalSheet().name, sepIndexes: modal.getCheckedSeparators() }
+				};
+			}
+			else {
+				var props = getSheetProps(viewSheet);
+				props.view.sepIndexes = modal.getCheckedSeparators();
+				SheetView.unloadSheet(viewSheet);
+			}
+
+			endChanges();
+
+			SheetView.loadSheet(viewSheet);
+			modal.closeModal();
+			refresh();
+		});
+		#end
+	}
+
 	public function popupSheet( withMacro = true, ?sheet : cdb.Sheet, ?onChange : Void -> Void ) {
 		if( view != null )
 			return;
@@ -2382,6 +2505,7 @@ class Editor extends Component {
 					endChanges();
 					onChange();
 				}},
+				{ label : '${getSheetProps(sheet).view == null ? "Create View" : "Edit View"}', click : function() { createView(getSheetProps(sheet).view != null ? null : sheet, getSheetProps(sheet).view != null ? sheet : null , index+1); } },
 				{ label : "Categories", menu: categoriesMenu(getSheetProps(sheet).categories, function(cats) {
 					beginChanges();
 					var props = getSheetProps(sheet);

+ 642 - 0
hide/comp/cdb/SheetView.hx

@@ -0,0 +1,642 @@
+package hide.comp.cdb;
+
+typedef LineData = {
+	originalObj : Dynamic,
+	originalIndex : Dynamic,
+	originalId : Dynamic,
+	originalArr : Array<Dynamic>
+}
+
+class SheetView {
+
+	static var changed : Bool;
+	static var skip : Int = 0;
+	static var watching : Map<String, Bool> = new Map();
+
+	#if (editor || cdb_datafiles)
+	static var base(get,never) : cdb.Database;
+	static function get_base() return Ide.inst.database;
+	#else
+	public static var base : cdb.Database;
+	#end
+
+	public static function loadSheet(sheet : cdb.Sheet) @:privateAccess {
+		var v = hide.comp.cdb.Editor.getSheetProps(sheet).view;
+		var originalSheet = base.getSheet(v.originalSheet);
+
+		loadColumns(originalSheet, sheet);
+
+		if (originalSheet.sheet.lines != null) sheet.sheet.lines = [];
+		sheet.sheet.linesData = [];
+
+		// Copy separators that has been picked for the view (and their lines)
+		sheet.sheet.separators = [];
+		for (sIdx in hide.comp.cdb.Editor.getSheetProps(sheet).view.sepIndexes) {
+			var sep = originalSheet.sheet.separators[sIdx];
+			var newSep = Reflect.copy(sep);
+			newSep.index = sheet.lines.length;
+			sheet.sheet.separators.push(newSep);
+
+			// Get the lines that are in this separator
+			for (idx in sep.index...(sIdx == originalSheet.separators.length - 1 ? originalSheet.sheet.lines.length : originalSheet.separators[sIdx + 1].index)) {
+				var newLine = sheet.newLine();
+				var newLineData : LineData = {
+					originalObj: originalSheet.sheet.lines[idx],
+					originalIndex: idx,
+					originalId: getLineId(originalSheet, originalSheet.sheet.lines[idx]),
+					originalArr: null
+				};
+
+				sheet.sheet.linesData.push(newLineData);
+				loadLine(originalSheet.name, newLine, newLineData);
+			}
+		}
+ 	}
+
+	public static function unloadSheet(sheet : cdb.Sheet) @:privateAccess {
+		while(sheet.lines.length > 0)
+			sheet.deleteLine(sheet.lines.length - 1);
+
+		if (sheet.sheet.linesData != null)
+			sheet.sheet.linesData = [];
+
+		if (sheet.sheet.separators != null)
+			sheet.sheet.separators = [];
+
+		var idx = sheet.columns.length - 1;
+		while (idx >= 0) {
+			sheet.deleteColumn(sheet.columns[idx].name);
+			idx--;
+		}
+	}
+
+	public static function reloadSheet(sheet : cdb.Sheet) {
+		var rootSheet = getRootSheet(sheet);
+		if (Editor.getSheetProps(rootSheet).view == null)
+			return;
+
+		unloadSheet(rootSheet);
+		loadSheet(rootSheet);
+	}
+
+	public static function getOriginalSheet(sheet : cdb.Sheet) {
+		var rootSheet = getRootSheet(sheet);
+		if (Editor.getSheetProps(rootSheet).view == null)
+			return sheet;
+
+		return base.getSheet(StringTools.replace(sheet.name, rootSheet.name, Editor.getSheetProps(rootSheet).view.originalSheet));
+	}
+
+	public static function isView(sheet : cdb.Sheet) {
+		return SheetView.getOriginalSheet(sheet) != sheet;
+	}
+
+	/* Lines modifications */
+	public static function insertLine(editor : hide.comp.cdb.Editor, ?line : Line, ?table : Table) : Dynamic {
+		var newLine = null;
+		var table = line != null ? line.table : table;
+		if( table == null || !table.canInsert() )
+			return null;
+
+		editor.beginChanges();
+		var originalSheet = getOriginalSheet(table.sheet);
+		var arr : Array<Dynamic> = SheetView.getOriginalArr(line, table);
+		if (arr != null) {
+			var newLine = {};
+			for( c in @:privateAccess originalSheet.sheet.columns ) {
+				var d = base.getDefault(c, originalSheet);
+
+				if (d != null)
+					Reflect.setField(newLine, c.name, d);
+			}
+
+			arr.insert(line != null ? line.index + 1 : 0, newLine);
+		}
+		else if (line != null) {
+			newLine = originalSheet.newLine(getOriginalIndex(line));
+		}
+		else {
+			newLine = originalSheet.newLine(0);
+		}
+
+		editor.endChanges();
+		editor.refresh();
+		table.refresh();
+		return newLine;
+	}
+
+	public static function deleteLine(editor : hide.comp.cdb.Editor, line : Line) {
+		var originalSheet = SheetView.getOriginalSheet(line.table.sheet);
+		var id = getOriginalId(line);
+		if( id != null && id.length > 0) {
+			var refs = editor.getReferences(id, originalSheet);
+			if( refs.length > 0 ) {
+				var message = [for (r in refs) r.str].join("\n");
+				if( !Ide.inst.confirm('$id is referenced elswhere. Are you sure you want to delete?\n$message') )
+					return;
+			}
+		}
+
+		editor.beginChanges();
+		// If originalArr is not null, it means that we're deleting a line from a sub sheet
+		var arr : Array<Dynamic> = getOriginalArr(line);
+		if (arr != null)
+			arr.remove(arr[line.index]);
+		else
+			originalSheet.deleteLine(getOriginalIndex(line));
+		editor.endChanges();
+		editor.refresh();
+	}
+
+	public static function duplicateLine(editor : hide.comp.cdb.Editor, line : Line) {
+		if( !line.table.canInsert() || line.table.displayMode != Table )
+			return;
+
+		var arr : Array<Dynamic> = getOriginalArr(line);
+		var originalSheet = getOriginalSheet(line.table.sheet);
+		var srcObj = getOriginalObject(line);
+		editor.beginChanges();
+		var obj = arr != null ? {} : originalSheet.newLine(getOriginalIndex(line));
+		for(colId => c in line.table.columns ) {
+			var val = Reflect.field(srcObj, c.name);
+			if( val != null ) {
+				if( c.type != TId ) {
+					// Deep copy
+					Reflect.setField(obj, c.name, haxe.Json.parse(haxe.Json.stringify(val)));
+				} else {
+					// Increment the number at the end of the id if there is one
+
+					var newId = editor.getNewUniqueId(val, line.table, c);
+					if (newId != null) {
+						Reflect.setField(obj, c.name, newId);
+					}
+				}
+			}
+		}
+
+		if (arr != null)
+			arr.insert(line.index + 1, obj);
+
+		editor.endChanges();
+		editor.refresh();
+		line.table.refresh();
+		line.table.getRealSheet().sync();
+	}
+
+	public static function moveLine(editor : hide.comp.cdb.Editor, line : Line, delta : Int, exact : Bool = false) {
+		if( !line.table.canInsert() )
+			return;
+
+		editor.beginChanges();
+		var originalSheet = SheetView.getOriginalSheet(line.table.sheet);
+		var prevIndex = getOriginalIndex(line);
+
+		var index : Null<Int> = null;
+		var currIndex : Null<Int> = getOriginalIndex(line);
+
+		var arr : Array<Dynamic> = getOriginalArr(line);
+		if (arr != null) {
+			var newIndex = currIndex + delta;
+			if (newIndex < 0 || newIndex >= arr.length)
+				throw "Moving lines into another group isn't supported in views yet.";
+		}
+
+		if (!exact) {
+			var distance = (delta >= 0 ? delta : -1 * delta);
+			for( _ in 0...distance ) {
+				if (arr != null) {
+					var tmp = arr[currIndex + delta];
+					arr[currIndex + delta] = arr[currIndex];
+					arr[currIndex] = tmp;
+					currIndex = currIndex + delta;
+
+					if (currIndex < 0 || currIndex >= arr.length)
+						break;
+				}
+				else
+					currIndex = originalSheet.moveLine( currIndex, delta );
+				if( currIndex == null )
+					break;
+				else
+					index = currIndex;
+			}
+		}
+		else {
+			while (index != prevIndex + delta) {
+				if (arr != null) {
+					var tmp = arr[currIndex + delta];
+					arr[currIndex + delta] = arr[currIndex];
+					arr[currIndex] = tmp;
+					currIndex = currIndex + delta;
+
+					if (currIndex < 0 || currIndex >= arr.length)
+						break;
+				}
+				else
+					currIndex = originalSheet.moveLine( currIndex, delta );
+				if( currIndex == null )
+					break;
+				else
+					index = currIndex;
+			}
+		}
+
+		if( index != null ) {
+			if (index != prevIndex) {
+				if ( editor.cursor.y == prevIndex ) editor.cursor.set(editor.cursor.table, editor.cursor.x, index);
+				else if ( editor.cursor.y > prevIndex && editor.cursor.y <= index) editor.cursor.set(editor.cursor.table, editor.cursor.x, editor.cursor.y - 1);
+				else if ( editor.cursor.y < prevIndex && editor.cursor.y >= index) editor.cursor.set(editor.cursor.table, editor.cursor.x, editor.cursor.y + 1);
+			}
+
+			editor.refresh();
+		}
+
+		editor.endChanges();
+		editor.refresh();
+	}
+
+
+	public static function getOriginalObject(?line : Line, ?table : Table) {
+		var sheet = line != null ? line.table.sheet : table.sheet;
+		var path = @:privateAccess sheet.path;
+
+		if (path == null)
+			return @:privateAccess sheet.sheet.linesData[line.index].originalObj;
+
+		var arrRealPath = path.split('@');
+		var originalObj : Dynamic = @:privateAccess SheetView.getRootSheet(sheet).sheet.linesData[getRootLineIndex(line, table)].originalObj;
+
+		for (pIdx => p in arrRealPath) {
+			if (pIdx == 0)
+				continue;
+
+			var field = p.split(':')[0];
+			var index = Std.parseInt(p.split(':')[1]);
+
+			originalObj = Reflect.field(originalObj is Array ? originalObj[index] : originalObj, field);
+		}
+
+		return originalObj is Array ? originalObj[line.index] : originalObj;
+	}
+
+	public static function getOriginalIndex(line : Line) {
+		var sheet = line.table.sheet;
+		var path = @:privateAccess sheet.path;
+
+		if (path == null)
+			return @:privateAccess sheet.sheet.linesData[line.index].originalIndex;
+
+		return line.index;
+	}
+
+	public static function getRootLineIndex(?line : Line, ?table : Table) {
+		var sheet = line != null ? line.table.sheet : table.sheet;
+		var path = @:privateAccess sheet.path;
+
+		if (path != null) {
+			var idx = path.indexOf(':');
+			var nextSub = path.indexOf('@', idx + 1);
+			var nextSubIdx = path.indexOf(':', idx + 1);
+			var next = (nextSub == -1 && nextSubIdx == -1) ? 1000 : (nextSub < nextSubIdx && nextSub != -1) ? nextSub : nextSubIdx;
+
+			return Std.parseInt(path.substr(idx + 1, next));
+		}
+
+		return line.index;
+	}
+
+	public static function getOriginalId(line : Line) {
+		var sheet = line.table.sheet;
+		var path = @:privateAccess sheet.path;
+
+		if (path == null)
+			return @:privateAccess sheet.sheet.linesData[line.index].originalId;
+
+		var splittedPath = (path.split(':'));
+		if (splittedPath.length > 1) {
+			var idx = Std.parseInt(splittedPath.pop());
+			var originalId : Dynamic = @:privateAccess SheetView.getRootSheet(sheet).sheet.linesData[idx].originalId;
+
+			for (elIdx => el in splittedPath.join("").split('@')) {
+				if (elIdx == 0)
+					continue;
+
+				originalId = Reflect.field(originalId, el);
+			}
+
+			return originalId;
+		}
+
+		throw "Not implemented exception";
+	}
+
+	public static function getOriginalArr(?line : Line, ?table : Table) {
+		var sheet = line != null ? line.table.sheet : table.sheet;
+		var path = @:privateAccess sheet.path;
+
+		var arrRealPath = path.split('@');
+		var originalObj : Dynamic = @:privateAccess SheetView.getRootSheet(sheet).sheet.linesData[getRootLineIndex(line, table)].originalObj;
+		var nextOriginalObj : Dynamic = null;
+		for (pIdx => p in arrRealPath) {
+			if (pIdx == 0)
+				continue;
+
+			var field = p.split(':')[0];
+			var index = Std.parseInt(p.split(':')[1]);
+
+			nextOriginalObj = Reflect.field(originalObj is Array ? originalObj[index] : originalObj, field);
+
+			if (nextOriginalObj != null)
+				originalObj = nextOriginalObj
+			else {
+				nextOriginalObj = [];
+				Reflect.setField(originalObj is Array ? originalObj[index] : originalObj, field, nextOriginalObj);
+				originalObj = nextOriginalObj;
+			}
+		}
+
+		return originalObj;
+	}
+
+
+	public function moveLines(editor : hide.comp.cdb.Editor, lines : Array<Line>, delta : Int) {
+		if( lines.length == 0 || !lines[0].table.canInsert() || delta == 0 )
+			return;
+
+		var selDiff: Null<Int> = editor.cursor.select == null ? null : editor.cursor.select.y - editor.cursor.y;
+		editor.beginChanges();
+		lines.sort((a, b) -> { return (a.index - b.index) * delta * -1; });
+		for( l in lines ) {
+			moveLine(editor, l, delta);
+		}
+		if (selDiff != null && hxd.Math.iabs(selDiff) == lines.length - 1)
+			editor.cursor.set(editor.cursor.table, editor.cursor.x, editor.cursor.y, {x: editor.cursor.x, y: editor.cursor.y + selDiff});
+		editor.endChanges();
+	}
+
+	static function getRootSheet(sheet : cdb.Sheet) {
+		var rootSheet = sheet;
+		while (rootSheet.parent != null)
+			rootSheet = rootSheet.parent.sheet;
+
+		return base.getSheet(rootSheet.name.split("@")[0]);
+	}
+
+	static function loadColumns(originalSheet : cdb.Sheet, sheet : cdb.Sheet) {
+		for (c in originalSheet.columns) {
+			var newCol = Reflect.copy(c);
+			sheet.addColumn(newCol);
+
+			if (c.type.match(cdb.Data.ColumnType.TProperties) || c.type.match(cdb.Data.ColumnType.TList)) {
+				loadColumns(originalSheet.getSub(c), sheet.getSub(c));
+			}
+		}
+	}
+
+	static function loadLine(name : String, object : Dynamic, objectData : Dynamic) @:privateAccess {
+		var originalSheet = base.getSheet(name);
+		var addedField = false;
+
+		for (c in originalSheet.sheet.columns) {
+			var hasField = Reflect.hasField(objectData.originalObj, c.name);
+			var v = Reflect.field(objectData.originalObj, c.name);
+
+			var sub = originalSheet.getSub(c);
+			if (sub != null && hasField && c.type.match(cdb.Data.ColumnType.TProperties)) {
+				var vCopy = {};
+				var vData = {
+					originalObj: v,
+					originalIndex: objectData.originalIndex,
+					originalId: objectData.originalId,
+					originalArr: null
+				};
+
+				loadLine(sub.name, vCopy, vData);
+
+				if (vCopy != null) {
+					Reflect.setField(object, c.name, vCopy);
+					addedField = true;
+				}
+
+				continue;
+			}
+
+			if (hasField && sub != null && c.type.match(cdb.Data.ColumnType.TList)) {
+				var vCopy = [];
+
+				for (idx in 0...v.length) {
+					var elCopy = {};
+					var elData = {
+						originalObj: v[idx],
+						originalIndex: idx,
+						originalId: getLineId(sub, v[idx]),
+						originalArr: vCopy
+					};
+
+					loadLine(sub.name, elCopy, elData);
+					vCopy.push(elCopy);
+				}
+
+				if (vCopy != null) {
+					Reflect.setField(object, c.name, vCopy);
+					addedField = true;
+				}
+
+				continue;
+			}
+
+			if (hasField) {
+				Reflect.setField(object, c.name, v);
+				addedField = true;
+			}
+		}
+
+		if (!addedField) {
+			object = null;
+			objectData = null;
+		}
+	}
+
+	static function getLineId(sheet : cdb.Sheet, line : Dynamic) {
+		for( c in sheet.columns ) {
+			if( c.type == TId )
+				return Reflect.field(line, c.name);
+		}
+
+		return null;
+	}
+}
+
+class SheetViewModal extends Modal {
+	var contentModal : Element;
+	var editor : Editor;
+	var originalSheet : cdb.Sheet;
+	var selectedSeparators : Array<Int>;
+
+	public function new( editor : Editor, originalSheet : cdb.Sheet, viewSheet : cdb.Sheet, ?parent,?el) {
+		function findParentSepIdx(sepIdx : Int) : Int {
+			var idx = sepIdx - 1;
+			while (idx > 0) {
+				if (this.originalSheet.separators[idx].level == null || this.originalSheet.separators[idx].level < this.originalSheet.separators[sepIdx].level)
+					return idx;
+
+				idx--;
+			}
+
+			return -1;
+		}
+
+		function findChildrenSepIdx(sepIdx : Int) : Array<Int> {
+			var children = [];
+
+			if (sepIdx < 0 || sepIdx >= this.originalSheet.separators.length)
+				return children;
+
+			for (idx in (sepIdx + 1)...this.originalSheet.separators.length) {
+				var s = this.originalSheet.separators[idx];
+
+				if (findParentSepIdx(idx) == sepIdx)
+					children.push(idx);
+			}
+
+			return children;
+		}
+
+		super(parent,el);
+
+		var editForm = viewSheet != null;
+		var base = editor.base;
+		this.editor = editor;
+		this.selectedSeparators = viewSheet != null ? hide.comp.cdb.Editor.getSheetProps(viewSheet).view.sepIndexes.copy() : [];
+
+		this.originalSheet = originalSheet;
+		if (originalSheet == null) {
+			for (sheet in base.sheets) {
+				if (sheet.name == hide.comp.cdb.Editor.getSheetProps(viewSheet).view.originalSheet) {
+					this.originalSheet = sheet;
+					break;
+				}
+			}
+		}
+
+
+		contentModal = new Element("<div tabindex='0'>").addClass("content-modal").appendTo(content);
+
+		if (editForm)
+			new Element('<h2> Edit view ${viewSheet.name}</h2>').appendTo(contentModal);
+		else
+			new Element("<h2> Create view </h2>").appendTo(contentModal);
+		new Element("<p id='errorModal'></p>").appendTo(contentModal);
+
+		new Element('
+		<div class="sheet-view">
+			<div id="name"><p>Name</p><input id="view-name"/></div>
+			<div>
+				<p>Pick separators to include in the view</p>
+				<div id="separators-picker"></div>
+			</div>
+			<div id="buttons">
+				<input id="create-btn" type="button" value="${editForm ? "Apply" : "Create"}"/><input id="cancel-btn" type="button" value="Cancel"/>
+			</div>
+		</div>').appendTo(contentModal);
+
+		if (viewSheet != null)
+			contentModal.find("#name").css({ display:"none" });
+
+		var separators = contentModal.find("#separators-picker");
+		for (sIdx => s in this.originalSheet.separators) {
+			var sepEl = new Element('
+			<div class="sep level-${s.level}">
+				<input type="checkbox"/>
+				<p>${s.title}</p>
+			</div>');
+
+			var cb = sepEl.find("input");
+			cb.on("change", function(_) {
+				var v = cb.prop("checked");
+
+				function pushSep(sIdx : Int) {
+					if (selectedSeparators.contains(sIdx))
+						return;
+
+					if (this.originalSheet.separators[sIdx].level > 0)
+						pushSep(findParentSepIdx(sIdx));
+
+					selectedSeparators.push(sIdx);
+				}
+
+				function removeSep(sIdx : Int) {
+					if (!selectedSeparators.contains(sIdx))
+						return;
+
+					var childrenSepIdx = findChildrenSepIdx(sIdx);
+					for (childSepIdx in childrenSepIdx)
+						removeSep(childSepIdx);
+
+					selectedSeparators.remove(sIdx);
+				}
+
+				// We do not want an orphan separators, so we want to include parent separators on separator add
+				// and remove children separator on separator remove
+				if (v)
+					pushSep(sIdx)
+				else
+					removeSep(sIdx);
+
+				updateCheckedSeparators();
+			});
+
+			separators.append(sepEl);
+		}
+
+		if (editForm)
+			updateCheckedSeparators();
+
+		element.find("#cancel-btn").click(function(e) { closeModal(); });
+	}
+
+	public function updateCheckedSeparators() {
+		var sepPicker = element.find("#separators-picker");
+		sepPicker.find("input").prop("checked", false);
+		sepPicker.find("input").each(function(idx: Int, el : js.html.Element) {
+			if (!selectedSeparators.contains(idx))
+				return;
+
+			var jEl = new Element(el);
+			jEl.prop("checked", true);
+		});
+	}
+
+	public function getOriginalSheet() {
+		return this.originalSheet;
+	}
+
+	public function getSheetName() {
+		return '${originalSheet.name}(${element.find("#view-name").val()})';
+	}
+
+	public function getCheckedSeparators() {
+		return selectedSeparators;
+	}
+
+	public function setCallback(callback : (Void -> Void)) {
+		element.find("#create-btn").click(function(e) {
+			e.preventDefault();
+			callback();
+		});
+
+		contentModal.find("#edit-btn").click(function(e) {
+			e.preventDefault();
+			callback();
+		});
+	}
+
+	public function closeModal() {
+		content.empty();
+		close();
+	}
+
+	public function error(str : String) {
+		contentModal.find("#errorModal").html(str);
+	}
+}

+ 19 - 5
hide/comp/cdb/Table.hx

@@ -215,7 +215,10 @@ class Table extends Component {
 				if (e.dataTransfer.dropEffect == "none") return false;
 				var pickedLine = getPickedLine(e);
 				if (pickedLine != null) {
-					editor.moveLine(line, pickedLine.index - line.index, true);
+					if(SheetView.isView(sheet))
+						SheetView.moveLine(editor, line, pickedLine.index - line.index, true);
+					else
+						editor.moveLine(line, pickedLine.index - line.index, true);
 					return true;
 				}
 
@@ -307,7 +310,11 @@ class Table extends Component {
 		} else if( sheet.lines.length == 0 && canInsert() ) {
 			var l = J('<tr><td colspan="${columns.length + 1}"><input type="button" value="Insert Line"/></td></tr>');
 			l.find("input").click(function(_) {
-				editor.insertLine(this);
+				var isView = SheetView.isView(sheet);
+				if (isView)
+					SheetView.insertLine(editor, null, this);
+				else
+					editor.insertLine(this);
 				editor.cursor.set(this);
 			});
 			element.append(l);
@@ -765,8 +772,10 @@ class Table extends Component {
 			sel.val("");
 			editor.element.focus();
 			if( v == "$new" ) {
-				editor.newColumn(sheet, null, function(c) {
+				var originalSheet = SheetView.getOriginalSheet(sheet);
+				editor.newColumn(originalSheet, null, function(c) {
 					if( c.opt ) insertProperty(c.name);
+					SheetView.reloadSheet(sheet.realSheet);
 				});
 				return;
 			}
@@ -775,13 +784,18 @@ class Table extends Component {
 	}
 
 	function insertProperty( p : String ) {
+		var isView = SheetView.isView(sheet);
 		var props = sheet.lines[0];
-		for( c in sheet.columns )
+		for( c in SheetView.getOriginalSheet(sheet).columns )
 			if( c.name == p ) {
 				var val = editor.base.getDefault(c, true, sheet);
 				editor.beginChanges();
-				Reflect.setField(props, c.name, val);
+				Reflect.setField(isView ? SheetView.getOriginalObject(lines.length > 0 ? lines[0] : null, this) : props, c.name, val);
 				editor.endChanges();
+				if (SheetView.isView(sheet)) {
+					SheetView.reloadSheet(sheet);
+					editor.refresh();
+				}
 				refresh();
 				for( l in lines )
 					if( l.cells[0].column == c ) {

+ 1 - 5
hide/prefab/ContextShared.hx

@@ -3,17 +3,13 @@ package hide.prefab;
 class ContextShared extends hrt.prefab.ContextShared.ContextSharedBase {
 	#if editor
 	public var editor : hide.comp.SceneEditor;
-	public var scene(get, never) : hide.comp.Scene;
+	public var scene : hide.comp.Scene;
 	public var editorDisplay : Bool;
 
 	public function new(?path : String, ?root2d: h2d.Object = null, ?root3d: h3d.scene.Object = null, isInstance: Bool = true) {
 		super(path, root2d, root3d, isInstance);
 	}
 
-	function get_scene() {
-		return editor.scene;
-	}
-
 	override function onError( e : Dynamic ) {
 		hide.Ide.inst.error(e);
 	}

+ 0 - 782
hide/view/Graph.hx

@@ -1,782 +0,0 @@
-package hide.view;
-
-import haxe.Timer;
-
-import haxe.rtti.Meta;
-import js.jquery.JQuery;
-import h2d.col.Point;
-import h2d.col.IPoint;
-import hide.comp.SVG;
-import hide.view.shadereditor.Box;
-import hrt.shgraph.ShaderNode;
-import hrt.shgraph.ShaderType;
-using Lambda;
-import hrt.shgraph.ShaderType.SType;
-
-enum EdgeState { None; FromInput; FromOutput; }
-
-typedef Edge = { from : Box, outputFrom : Int, to : Box, inputTo : Int, elt : JQuery };
-
-@:access(hide.view.shadereditor.Box)
-class Graph extends FileView {
-
-	var parent : JQuery;
-	var editor : SVG;
-	var editorMatrix : JQuery;
-	var statusBar : JQuery;
-	var statusClose : JQuery;
-
-
-	var listOfBoxes : Array<Box> = [];
-	var listOfEdges : Array<Edge> = [];
-
-	var transformMatrix : Array<Float> = [1, 0, 0, 1, 0, 0];
-	var isPanning : Bool = false;
-	static var MAX_ZOOM = 1.3;
-	static var CENTER_OFFSET_Y = 0.1; // percent of height
-
-	// used for moving when mouse is close to borders
-	static var BORDER_SIZE = 50;
-	static var SPEED_BORDER_MOVE = 0.05;
-	var timerUpdateView : Timer;
-
-	// used for selection
-	var listOfBoxesSelected : Array<Box> = [];
-	var listOfBoxesToMove : Array<Box> = [];
-	var undoSave : Any;
-	var recSelection : JQuery;
-	var startRecSelection : h2d.col.Point;
-	var lastClickDrag : h2d.col.Point;
-	var lastClickPan : h2d.col.Point;
-
-	// used to build edge
-	static var NODE_TRIGGER_NEAR = 2000.0;
-	var isCreatingLink : EdgeState = None;
-	var edgeStyle = {stroke : ""};
-	var startLinkBox : Box;
-	var endLinkBox : Box;
-	var startLinkNodeId : Int;
-	var endLinkNodeId : Int;
-	var currentLink : JQuery; // draft of edge
-
-	// used for deleting
-	var currentEdge : Edge;
-
-	// aaaaaa
-	var domain : hrt.shgraph.ShaderGraph.Domain;
-
-	override function onDisplay() {
-		element.html('
-			<div class="flex vertical" >
-				<div class="flex-elt graph-view" tabindex="0" >
-					<div class="heaps-scene" tabindex="1" >
-					</div>
-					<div id="rightPanel" class="tabs" >
-					</div>
-				</div>
-			</div>');
-		parent = element.find(".heaps-scene");
-		editor = new SVG(parent);
-		editor.element.attr("id", "graph-root");
-		var status = new Element('<div id="status-bar" ><div id="close">-- close --</div><pre></pre></div>');
-		statusBar = status.appendTo(parent).find("pre");
-		statusClose = status.find("#close");
-		statusClose.hide();
-		statusClose.on("click", function(e) {
-			statusBar.html("");
-			statusClose.hide();
-		});
-		statusBar.on("wheel", (e) -> { e.stopPropagation(); });
-
-		editorMatrix = editor.group(editor.element);
-
-		// rectangle Selection
-		parent.on("mousedown", function(e) {
-
-			if (e.button == 0) {
-				startRecSelection = new Point(lX(e.clientX), lY(e.clientY));
-				if (currentEdge != null) {
-					currentEdge.elt.removeClass("selected");
-					currentEdge = null;
-				}
-
-				clearSelectionBoxes();
-				return;
-			}
-			if (e.button == 1) {
-				lastClickPan = new Point(e.clientX, e.clientY);
-				isPanning = true;
-				return;
-			}
-		});
-
-		parent.on("mousemove", function(e : js.jquery.Event) {
-			e.preventDefault();
-			e.cancelBubble=true;
-    		e.returnValue=false;
-			mouseMoveFunction(e.clientX, e.clientY);
-		});
-
-		var document = new Element(js.Browser.document);
-		document.on("mouseup", function(e) {
-			if(timerUpdateView != null)
-				stopUpdateViewPosition();
-			if (e.button == 0) {
-
-				// Stop rectangle selection
-				lastClickDrag = null;
-				startRecSelection = null;
-				if (recSelection != null) {
-					recSelection.remove();
-					recSelection = null;
-					for (b in listOfBoxes)
-						if (b.selected)
-							listOfBoxesSelected.push(b);
-					return;
-				}
-
-				return;
-			}
-
-			// Stop panning
-			if (e.button == 1) {
-				lastClickDrag = null;
-				isPanning = false;
-				return;
-			}
-		});
-
-		// Zoom control
-		parent.on("wheel", function(e) {
-			if (e.originalEvent.deltaY < 0) {
-				zoom(1.1, e.clientX, e.clientY);
-			} else {
-				zoom(0.9, e.clientX, e.clientY);
-			}
-		});
-
-		listOfBoxes = [];
-		listOfEdges = [];
-
-		updateMatrix();
-	}
-
-	function mouseMoveFunction(clientX : Int, clientY : Int) {
-		if (isCreatingLink != None) {
-			startUpdateViewPosition();
-			createLink(clientX, clientY);
-			return;
-		}
-		// Moving edge
-		if (currentEdge != null) {
-			var distOutput = distanceToElement(currentEdge.from.outputs[currentEdge.outputFrom], clientX, clientY);
-			var distInput = distanceToElement(currentEdge.to.inputs[currentEdge.inputTo], clientX, clientY);
-
-			if (distOutput > distInput) {
-				replaceEdge(FromOutput, currentEdge.to.inputs[currentEdge.inputTo], clientX, clientY);
-			} else {
-				replaceEdge(FromInput, currentEdge, clientX, clientY);
-			}
-			currentEdge = null;
-			return;
-		}
-		if (isPanning) {
-			pan(new Point(clientX - lastClickPan.x, clientY - lastClickPan.y));
-			lastClickPan.x = clientX;
-			lastClickPan.y = clientY;
-			return;
-		}
-		// Edit rectangle selection
-		if (startRecSelection != null) {
-			startUpdateViewPosition();
-			var endRecSelection = new h2d.col.Point(lX(clientX), lY(clientY));
-			var xMin = startRecSelection.x;
-			var xMax = endRecSelection.x;
-			var yMin = startRecSelection.y;
-			var yMax = endRecSelection.y;
-
-			if (startRecSelection.x > endRecSelection.x) {
-				xMin = endRecSelection.x;
-				xMax = startRecSelection.x;
-			}
-			if (startRecSelection.y > endRecSelection.y) {
-				yMin = endRecSelection.y;
-				yMax = startRecSelection.y;
-			}
-
-			if (recSelection != null) recSelection.remove();
-			recSelection = editor.rect(editorMatrix, xMin, yMin, xMax - xMin, yMax - yMin).addClass("rect-selection");
-
-			for (box in listOfBoxes) {
-				if (isInside(box, new Point(xMin, yMin), new Point(xMax, yMax))) {
-					box.setSelected(true);
-				} else {
-					box.setSelected(false);
-				}
-			}
-			return;
-		}
-
-		// Move selected boxes
-		if (listOfBoxesSelected.length > 0 && lastClickDrag != null) {
-			startUpdateViewPosition();
-			var dx = (lX(clientX) - lastClickDrag.x);
-			var dy = (lY(clientY) - lastClickDrag.y);
-
-
-			for (b in listOfBoxesToMove) {
-				moveBox(b, b.getX() + dx, b.getY() + dy);
-			}
-			lastClickDrag.x = lX(clientX);
-			lastClickDrag.y = lY(clientY);
-			return;
-		}
-	}
-
-	dynamic function updatePosition(box : Box) { }
-
-	function moveBox(b: Box, x: Float, y: Float) {
-		b.setPosition(x, y);
-		updatePosition(b);
-		// move edges from and to this box
-		for (edge in listOfEdges) {
-			if (edge.from == b || edge.to == b) {
-				edge.elt.remove();
-				edgeStyle.stroke = edge.from.outputs[edge.outputFrom].css("fill");
-				edge.elt = createCurve( edge.from.outputs[edge.outputFrom], edge.to.inputs[edge.inputTo]);
-
-				edge.elt.on("mousedown", function(e) {
-					e.stopPropagation();
-					clearSelectionBoxes();
-					this.currentEdge = edge;
-					currentEdge.elt.addClass("selected");
-				});
-			}
-		}
-	}
-
-	function beginMove(e: js.html.MouseEvent) {
-		lastClickDrag = new Point(lX(e.clientX), lY(e.clientY));
-
-		var boxesToMove : Map<Box, Bool> = [];
-
-		for (b in listOfBoxesSelected) {
-			boxesToMove.set(b, true);
-
-			if (b.comment != null && !e.shiftKey) {
-				var min = inline new Point(b.getX(), b.getY());
-				var max = inline new Point(b.getX() + b.comment.width, b.getY() + b.comment.height);
-
-				for (bb in listOfBoxes) {
-					if (isFullyInside(bb, min, max)) {
-						boxesToMove.set(bb, true);
-					}
-				}
-			}
-		}
-
-		listOfBoxesToMove = [for (k in boxesToMove.keys()) k];
-		undoSave = saveMovedBoxes();
-
-		trace(listOfBoxesToMove);
-	}
-
-	function saveMovedBoxes() {
-		var save : Map<Int, {x: Float, y: Float}> = [];
-		for (b in listOfBoxesToMove) {
-			save.set(b.nodeInstance.id, {x:b.getX(), y: b.getY()});
-		}
-		return save;
-	}
-
-	function endMove() {
-		if (lastClickDrag == null)
-			return;
-
-		lastClickDrag = null;
-
-		if (undoSave != null) {
-			var before : Map<Int, {x: Float, y: Float}> = undoSave;
-			var after : Map<Int, {x: Float, y: Float}> = saveMovedBoxes();
-
-			undo.change(Custom(function(undo) {
-				var toApply = undo ? before : after;
-				for (id => pos in toApply) {
-					var box = listOfBoxes.find((e) -> e.nodeInstance.id == id);
-					moveBox(box, pos.x ,pos.y);
-				}
-			}));
-			undoSave = null;
-		}
-
-		listOfBoxesToMove = [];
-	}
-
-	function addBox(p : Point, nodeClass : Class<ShaderNode>, node : ShaderNode) : Box {
-
-		var className = std.Type.getClassName(nodeClass);
-		className = className.substr(className.lastIndexOf(".") + 1);
-
-		var box = new Box(this, editorMatrix, p.x, p.y, node);
-		var elt = box.getElement();
-		elt.get(0).onmousedown = function(e: js.html.MouseEvent) {
-			if (e.button != 0)
-				return;
-			e.stopPropagation();
-
-			if (!box.selected) {
-				if (!e.ctrlKey) {
-					// when not group selection and click on box not selected
-					clearSelectionBoxes();
-					listOfBoxesSelected = [box];
-				} else
-					listOfBoxesSelected.push(box);
-				box.setSelected(true);
-			}
-			beginMove(e);
-		};
-		elt.mouseup(function(e) {
-			if (e.button != 0)
-				return;
-			endMove();
-		});
-		listOfBoxes.push(box);
-
-		for (inputId => input in box.getInstance().getInputs()) {
-			var defaultValue : String = null;
-			switch (input.def) {
-				case Const(defValue):
-					defaultValue= Reflect.getProperty(box.getInstance().defaults, '${input.name}');
-					if (defaultValue == null) {
-						defaultValue = '$defValue';
-					}
-				default:
-			}
-			var grNode = box.addInput(this, input.name, defaultValue, input.type);
-			if (defaultValue != null) {
-				var fieldEditInput = grNode.find("input");
-				fieldEditInput.on("change", function(ev) {
-					var tmpValue = Std.parseFloat(fieldEditInput.val());
-					if (Math.isNaN(tmpValue) ) {
-						fieldEditInput.addClass("error");
-					} else {
-						// Store the value as a string anyway
-						Reflect.setField(box.getInstance().defaults, '${input.name}', '$tmpValue');
-						fieldEditInput.val(tmpValue);
-						fieldEditInput.removeClass("error");
-					}
-				});
-			}
-			grNode.find(".node").attr("field", inputId);
-			grNode.on("mousedown", function(e : js.jquery.Event) {
-				e.stopPropagation();
-				var node = grNode.find(".node");
-				if (node.attr("hasLink") != null) {
-					replaceEdge(FromOutput, node, e.clientX, e.clientY);
-					return;
-				}
-				isCreatingLink = FromInput;
-				startLinkNodeId = inputId;
-				startLinkBox = box;
-				edgeStyle.stroke = node.css("fill");
-			});
-		}
-		for (outputId => info in box.getInstance().getOutputs()) {
-			var grNode = box.addOutput(this, info.name, info.type);
-			grNode.find(".node").attr("field", outputId);
-			grNode.on("mousedown", function(e) {
-				e.stopPropagation();
-				var node = grNode.find(".node");
-				isCreatingLink = FromOutput;
-				startLinkNodeId = outputId;
-				startLinkBox = box;
-				edgeStyle.stroke = node.css("fill");
-			});
-		}
-
-		box.generateProperties(this, config);
-
-		return box;
-	}
-
-	function removeBox(box : Box, trackChanges = true) {
-		removeEdges(box);
-		box.dispose();
-		listOfBoxes.remove(box);
-	}
-
-	function removeEdges(box : Box) {
-		var length = listOfEdges.length;
-		for (i in 0...length) {
-			var edge = listOfEdges[length-i-1];
-			if (edge.from == box || edge.to == box) {
-				removeEdge(edge); // remove edge from listOfEdges
-			}
-		}
-	}
-
-	function removeEdge(edge : Edge) {
-		edge.elt.remove();
-		edge.to.inputs[edge.inputTo].removeAttr("hasLink");
-		edge.to.inputs[edge.inputTo].parent().removeClass("hasLink");
-		listOfEdges.remove(edge);
-	}
-
-	function replaceEdge(state : EdgeState, ?edge : Edge, ?node : JQuery, x : Int, y : Int) {
-		switch (state) {
-			case FromOutput:
-				for (e in listOfEdges) {
-					if (e.to.inputs[e.inputTo].is(node)) {
-						isCreatingLink = FromOutput;
-						startLinkNodeId = e.outputFrom;
-						startLinkBox = e.from;
-						edgeStyle.stroke = e.from.outputs[e.outputFrom].css("fill");
-						removeEdge(e);
-						createLink(x, y);
-						return;
-					}
-				}
-			case FromInput:
-				for (e in listOfEdges) {
-					if (e.to == edge.to && e.inputTo == edge.inputTo && e.from == edge.from && e.outputFrom == edge.outputFrom) {
-						isCreatingLink = FromInput;
-						startLinkNodeId = e.inputTo;
-						startLinkBox = e.to;
-						edgeStyle.stroke = e.from.outputs[e.outputFrom].css("fill");
-						removeEdge(e);
-						createLink(x, y);
-						return;
-					}
-				}
-			default:
-				return;
-		}
-	}
-
-	function error(str : String, ?idBox : Int) {
-		statusBar.html(str);
-		statusClose.show();
-		statusBar.addClass("error");
-
-		new Element(".box").removeClass("error");
-		if (idBox != null) {
-			var elt = new Element('#${idBox}');
-			elt.addClass("error");
-		}
-	}
-
-	function info(str : String) {
-		statusBar.html(str);
-		statusClose.show();
-		statusBar.removeClass("error");
-		new Element(".box").removeClass("error");
-	}
-
-	function createEdgeInEditorGraph(edge) {
-		listOfEdges.push(edge);
-		edge.to.inputs[edge.inputTo].attr("hasLink", "true");
-		edge.to.inputs[edge.inputTo].parent().addClass("hasLink");
-
-		edge.elt.on("mousedown", function(e) {
-			e.stopPropagation();
-			clearSelectionBoxes();
-			this.currentEdge = edge;
-			currentEdge.elt.addClass("selected");
-		});
-	}
-
-	function createLink(clientX : Int, clientY : Int) {
-
-		var nearestId = -1;
-		var minDistNode = NODE_TRIGGER_NEAR;
-
-		// checking nearest box
-		var nearestBox = null;
-		var minDist = 999999999999999.0;
-		for (i in 0...listOfBoxes.length) {
-			if (listOfBoxes[i].comment != null)
-				continue;
-			var tmpDist = distanceToBox(listOfBoxes[i], clientX, clientY);
-			if (tmpDist < minDist) {
-				minDist = tmpDist;
-				nearestBox = listOfBoxes[i];
-			}
-		}
-		if (nearestBox == null)
-			return;
-
-		// checking nearest node in the nearest box
-		if (isCreatingLink == FromInput) {
-			for (id => o in nearestBox.outputs) {
-				var newMin = distanceToElement(o, clientX, clientY);
-				if (newMin < minDistNode) {
-					nearestId = id;
-					minDistNode = newMin;
-				}
-			}
-		} else {
-			// input has one edge at most
-			for (id => i in nearestBox.inputs) {
-				var newMin = distanceToElement(i, clientX, clientY);
-				if (newMin < minDistNode) {
-					nearestId = id;
-					minDistNode = newMin;
-				}
-			}
-		}
-
-		if (minDistNode < NODE_TRIGGER_NEAR && nearestId >= 0) {
-			endLinkNodeId = nearestId;
-			endLinkBox = nearestBox;
-		} else {
-			endLinkNodeId = -1;
-			endLinkBox = null;
-			minDistNode = null;
-		}
-
-		// create edge
-		if (currentLink != null) currentLink.remove();
-		if (isCreatingLink == FromInput) {
-			currentLink = createCurve(startLinkBox.inputs[startLinkNodeId], endLinkBox?.outputs[endLinkNodeId], minDistNode, clientX, clientY, true);
-		}
-		else {
-			currentLink = createCurve(startLinkBox.outputs[startLinkNodeId], endLinkBox?.inputs[endLinkNodeId], minDistNode, clientX, clientY, true);
-		}
-	}
-
-	function createCurve(start : JQuery, end : JQuery, ?distance : Float, ?x : Float, ?y : Float, ?isDraft : Bool) {
-		var offsetEnd;
-		var offsetStart = start.offset();
-		if (end != null) {
-			offsetEnd = end.offset();
-		} else {
-			offsetEnd = { top : y, left : x };
-		}
-
-		if (isCreatingLink == FromInput) {
-			var tmp = offsetStart;
-			offsetStart = offsetEnd;
-			offsetEnd = tmp;
-		}
-		var startX = lX(offsetStart.left) + Box.NODE_RADIUS;
-		var startY = lY(offsetStart.top) + Box.NODE_RADIUS;
-		var diffDistanceY = offsetEnd.top - offsetStart.top;
-		var signCurveY = ((diffDistanceY > 0) ? -1 : 1);
-		diffDistanceY = Math.abs(diffDistanceY);
-		var valueCurveX = 100;
-		var valueCurveY = 1;
-		var maxDistanceY = 900;
-
-		var curve = editor.curve(null,
-							startX,
-							startY,
-							lX(offsetEnd.left) + Box.NODE_RADIUS,
-							lY(offsetEnd.top) + Box.NODE_RADIUS,
-							startX + valueCurveX * (Math.min(maxDistanceY, diffDistanceY)/maxDistanceY),
-							startY + signCurveY * valueCurveY * (Math.min(maxDistanceY, diffDistanceY)/maxDistanceY),
-							edgeStyle)
-							.addClass("edge");
-		editorMatrix.prepend(curve);
-		if (isDraft)
-			curve.addClass("draft");
-
-		return curve;
-	}
-
-	function clearSelectionBoxes() {
-		for(b in listOfBoxesSelected) b.setSelected(false);
-		listOfBoxesSelected = [];
-		if (this.currentEdge != null) {
-			currentEdge.elt.removeClass("selected");
-		}
-	}
-
-	function startUpdateViewPosition() {
-		if (timerUpdateView != null)
-			return;
-		timerUpdateView = new Timer(0);
-		timerUpdateView.run = function() {
-			var posCursor = new Point(ide.mouseX - parent.offset().left, ide.mouseY - parent.offset().top);
-			var wasUpdated = false;
-			if (posCursor.x < BORDER_SIZE) {
-				pan(new Point((BORDER_SIZE - posCursor.x)*SPEED_BORDER_MOVE, 0));
-				wasUpdated = true;
-			}
-			if (posCursor.y < BORDER_SIZE) {
-				pan(new Point(0, (BORDER_SIZE - posCursor.y)*SPEED_BORDER_MOVE));
-				wasUpdated = true;
-			}
-			var rightBorder = parent.width() - BORDER_SIZE;
-			if (posCursor.x > rightBorder) {
-				pan(new Point((rightBorder - posCursor.x)*SPEED_BORDER_MOVE, 0));
-				wasUpdated = true;
-			}
-			var botBorder = parent.height() - BORDER_SIZE;
-			if (posCursor.y > botBorder) {
-				pan(new Point(0, (botBorder - posCursor.y)*SPEED_BORDER_MOVE));
-				wasUpdated = true;
-			}
-			mouseMoveFunction(ide.mouseX, ide.mouseY);
-		};
-	}
-
-	function stopUpdateViewPosition() {
-		if (timerUpdateView != null) {
-			timerUpdateView.stop();
-			timerUpdateView = null;
-		}
-	}
-
-	function getGraphDims(?boxes) {
-		if( boxes == null )
-			boxes = listOfBoxes;
-		if( boxes.length == 0 ) return null;
-		var xMin = boxes[0].getX();
-		var yMin = boxes[0].getY();
-		var xMax = xMin + boxes[0].getWidth();
-		var yMax = yMin + boxes[0].getHeight();
-		for (i in 1...boxes.length) {
-			var b = boxes[i];
-			xMin = Math.min(xMin, b.getX());
-			yMin = Math.min(yMin, b.getY());
-			xMax = Math.max(xMax, b.getX() + b.getWidth());
-			yMax = Math.max(yMax, b.getY() + b.getHeight());
-		}
-		var center = new IPoint(Std.int(xMin + (xMax - xMin)/2), Std.int(yMin + (yMax - yMin)/2));
-		center.y += Std.int(editor.element.height()*CENTER_OFFSET_Y);
-		return {
-			xMin : xMin,
-			yMin : yMin,
-			xMax : xMax,
-			yMax : yMax,
-			center : center,
-		};
-	}
-
-	function centerView() {
-		if (listOfBoxes.length == 0) return;
-		var dims = getGraphDims();
-		var scale = Math.min(1, Math.min((editor.element.width() - 50) / (dims.xMax - dims.xMin), (editor.element.height() - 50) / (dims.yMax - dims.yMin)));
-
-		transformMatrix[4] = editor.element.width()/2 - dims.center.x;
-		transformMatrix[5] = editor.element.height()/2 - dims.center.y;
-
-		transformMatrix[0] = scale;
-		transformMatrix[3] = scale;
-
-		var x = editor.element.width()/2;
-		var y = editor.element.height()/2;
-
-		transformMatrix[4] = x - (x - transformMatrix[4]) * scale;
-		transformMatrix[5] = y - (y - transformMatrix[5]) * scale;
-
-		updateMatrix();
-	}
-
-	function clampView() {
-		if (listOfBoxes.length == 0) return;
-		var dims = getGraphDims();
-
-		var width = editor.element.width();
-		var height = editor.element.height();
-		var scale = transformMatrix[0];
-
-		if( transformMatrix[4] + dims.xMin * scale > width )
-			transformMatrix[4] = width - dims.xMin * scale;
-		if( transformMatrix[4] + dims.xMax * scale < 0 )
-			transformMatrix[4] = -1 * dims.xMax * scale;
-		if( transformMatrix[5] + dims.yMin * scale > height )
-			transformMatrix[5] = height - dims.yMin * scale;
-		if( transformMatrix[5] + dims.yMax * scale < 0 )
-			transformMatrix[5] = -1 * dims.yMax * scale;
-	}
-
-	function updateMatrix() {
-		editorMatrix.attr({transform: 'matrix(${transformMatrix.join(' ')})'});
-	}
-
-	function zoom(scale : Float, x : Int, y : Int) {
-		if (scale > 1 && transformMatrix[0] > MAX_ZOOM) {
-			return;
-		}
-
-		transformMatrix[0] *= scale;
-		transformMatrix[3] *= scale;
-
-		x -= Std.int(editor.element.offset().left);
-		y -= Std.int(editor.element.offset().top);
-
-		transformMatrix[4] = x - (x - transformMatrix[4]) * scale;
-		transformMatrix[5] = y - (y - transformMatrix[5]) * scale;
-
-		clampView();
-		updateMatrix();
-	}
-
-	function pan(p : Point) {
-		transformMatrix[4] += p.x;
-		transformMatrix[5] += p.y;
-
-		clampView();
-		updateMatrix();
-	}
-
-	function isVisible() : Bool {
-		return editor.element.is(":visible");
-	}
-
-	// Useful method
-	function isInside(b : Box, min : Point, max : Point) {
-		if (max.x < b.getX() || min.x > b.getX() + b.getWidth())
-			return false;
-		if (max.y < b.getY() || min.y > b.getY() + b.getHeight())
-			return false;
-
-		return true;
-	}
-
-	function isFullyInside(b: Box, min : Point, max : Point) {
-		if (min.x > b.getX() || max.x < b.getX() + b.getWidth())
-			return false;
-		if (min.y > b.getY() || max.y < b.getY() + b.getHeight())
-			return false;
-
-		return true;
-	}
-
-	function distanceToBox(b : Box, x : Int, y : Int) {
-		var dx = Math.max(Math.abs(lX(x) - (b.getX() + (b.getWidth() / 2))) - b.getWidth() / 2, 0);
-		var dy = Math.max(Math.abs(lY(y) - (b.getY() + (b.getHeight() / 2))) - b.getHeight() / 2, 0);
-		return dx * dx + dy * dy;
-	}
-	function distanceToElement(element : JQuery, x : Int, y : Int) {
-		if (element == null)
-			return NODE_TRIGGER_NEAR+1;
-		var dx = Math.max(Math.abs(x - (element.offset().left + element.width() / 2)) - element.width() / 2, 0);
-		var dy = Math.max(Math.abs(y - (element.offset().top + element.height() / 2)) - element.height() / 2, 0);
-		return dx * dx + dy * dy;
-	}
-	function gX(x : Float) : Float {
-		return x*transformMatrix[0] + transformMatrix[4];
-	}
-	function gY(y : Float) : Float {
-		return y*transformMatrix[3] + transformMatrix[5];
-	}
-	function gPos(x : Float, y : Float) : Point {
-		return new Point(gX(x), gY(y));
-	}
-	function lX(x : Float) : Float {
-		var screenOffset = editor.element.offset();
-		x -= screenOffset.left;
-		return (x - transformMatrix[4])/transformMatrix[0];
-	}
-	function lY(y : Float) : Float {
-		var screenOffset = editor.element.offset();
-		y -= screenOffset.top;
-		return (y - transformMatrix[5])/transformMatrix[3];
-	}
-	function lPos(x : Float, y : Float) : Point {
-		return new Point(lX(x), lY(y));
-	}
-
-}

+ 1866 - 0
hide/view/GraphEditor.hx

@@ -0,0 +1,1866 @@
+package hide.view;
+
+import haxe.Timer;
+
+import haxe.rtti.Meta;
+import js.jquery.JQuery;
+import h2d.col.Point;
+import h2d.col.IPoint;
+import hide.comp.SVG;
+import hide.view.shadereditor.Box;
+import hrt.shgraph.ShaderNode;
+import hrt.shgraph.ShaderType;
+using Lambda;
+import hrt.shgraph.ShaderType.SType;
+
+import hide.view.GraphInterface.IGraphEditor;
+import hide.view.GraphInterface.IGraphNode;
+import hide.view.GraphInterface.Edge;
+
+enum EdgeState { None; FromInput; FromOutput; }
+
+typedef UndoFn = (isUndo : Bool) -> Void;
+typedef UndoBuffer = Array<UndoFn>;
+typedef SelectionUndoSave = {newSelections: Map<Int, Bool>, buffer: UndoBuffer};
+
+typedef CopySelectionData = {
+	nodes: Array<{id: Int, serData: Dynamic}>,
+	edges: Array<Edge>,
+};
+
+class PreviewShaderAlpha extends hxsl.Shader {
+	static var SRC = {
+		@input var input : {
+			var uv : Vec2;
+		};
+
+		var pixelColor : Vec4;
+
+		function fragment() {
+			var cb = floor(mod(input.uv * 10.0, vec2(2.0)));
+			var check = mod(cb.x + cb.y, 2.0);
+			var color = check >= 1.0 ? vec3(0.22) : vec3(0.44);
+			pixelColor.rgb = mix(color, pixelColor.rgb, pixelColor.a);
+		}
+	}
+}
+
+@:access(hide.view.shadereditor.Box)
+class GraphEditor extends hide.comp.Component {
+	public var editor : hide.view.GraphInterface.IGraphEditor;
+	var heapsScene : JQuery;
+	var editorDisplay : SVG;
+	var editorMatrix : JQuery;
+	public var config : hide.Config;
+
+
+	public var previewsScene : hide.comp.Scene;
+
+	var boxes : Map<Int, Box> = [];
+
+	var transformMatrix : Array<Float> = [1, 0, 0, 1, 0, 0];
+	var isPanning : Bool = false;
+	static var MAX_ZOOM = 2.0;
+	static var CENTER_OFFSET_Y = 0.1; // percent of height
+
+	// used for moving when mouse is close to borders
+	static var BORDER_SIZE = 50;
+	static var SPEED_BORDER_MOVE = 0.05;
+	var timerUpdateView : Timer;
+	// used for selection
+	var boxesSelected : Map<Int, Bool> = [];
+	var boxesToMove : Map<Int, Bool> = [];
+	var undoSave : Any;
+	var recSelection : JQuery;
+	var startRecSelection : h2d.col.Point;
+	var startClickDrag : h2d.col.Point = new h2d.col.Point();
+	var lastClickPan : h2d.col.Point;
+
+	// used to build edge
+	static final NODE_TRIGGER_NEAR = 2000.0;
+
+	var selectedNode : JQuery;
+
+	// used for deleting
+
+	// aaaaaa
+	var domain : hrt.shgraph.ShaderGraph.Domain;
+
+	var addMenu : JQuery;
+
+	var edgeCreationCurve : JQuery = null;
+	var edgeCreationOutput : Null<Int> = null;
+	var edgeCreationInput : Null<Int> = null;
+	var edgeCreationMode : EdgeState = None;
+	var lastCurveX : Float = 0;
+	var lastCurveY : Float = 0;
+	var snapToGrid : Bool = true;
+
+	public var currentUndoBuffer : UndoBuffer = [];
+
+
+
+	var outputsToInputs : hrt.tools.OneToMany = new hrt.tools.OneToMany();
+	// Maps a packIO of an input to it's visual link in the graph
+	var edges : Map<Int, JQuery> = [];
+
+	public function new(config: hide.Config, editor: hide.view.GraphInterface.IGraphEditor, parent: Element = null) {
+		super(parent, new Element('
+		<div class="flex vertical" >
+			<div class="flex-elt graph-view" tabindex="0" >
+				<div class="heaps-scene" tabindex="1" >
+				</div>
+			</div>
+		</div>'));
+		this.config = config;
+		this.editor = editor;
+	}
+
+	public function addUndo(fn: UndoFn) {
+		currentUndoBuffer.push(fn);
+		fn(false);
+	}
+
+	public function commitUndo() {
+		if (currentUndoBuffer.length <= 0) {
+			return;
+		}
+		var buffer = currentUndoBuffer;
+		editor.getUndo().change(Custom(execUndo.bind(buffer)));
+		currentUndoBuffer = [];
+	}
+
+	public function execUndo(buffer: UndoBuffer, isUndo : Bool) {
+		if (isUndo) {
+			for (i in 0...buffer.length) {
+				buffer[buffer.length - i - 1](isUndo);
+			}
+		} else {
+			for (i in 0...buffer.length) {
+				buffer[i](isUndo);
+			}
+		}
+	}
+
+
+
+	public function onDisplay() {
+		heapsScene = element.find(".heaps-scene");
+		editorDisplay = new SVG(heapsScene);
+		editorDisplay.element.attr("id", "graph-root");
+
+
+		editorMatrix = editorDisplay.group(editorDisplay.element);
+
+		var keys = new hide.ui.Keys(element);
+		keys.register("delete", deleteSelection);
+		keys.register("sceneeditor.focus", centerView);
+		keys.register("copy", copySelection);
+		keys.register("paste", paste);
+		keys.register("cut", cutSelection);
+		keys.register("shadergraph.hide", onHide);
+		keys.register("selectAll", selectAll);
+		keys.register("shadergraph.comment", commentFromSelection);
+		keys.register("duplicateInPlace", duplicateSelection);
+		keys.register("duplicate", duplicateSelection);
+		keys.register("graph.openAddMenu", openAddMenu.bind(null,null));
+		keys.register("cancel", cancelAll);
+
+		var miniPreviews = new Element('<div class="mini-preview"></div>');
+		heapsScene.prepend(miniPreviews);
+		previewsScene = new hide.comp.Scene(config, null, miniPreviews);
+		previewsScene.onReady = onMiniPreviewReady;
+		previewsScene.onUpdate = onMiniPreviewUpdate;
+
+		// rectangle Selection
+		var rawheaps = heapsScene.get(0);
+		rawheaps.addEventListener("pointerdown", function(e) {
+
+			if (e.button == 0) {
+				startRecSelection = new Point(lX(e.clientX), lY(e.clientY));
+				// if (currentEdge != null) {
+				// 	currentEdge.elt.removeClass("selected");
+				// 	currentEdge = null;
+				// }
+
+				var save : SelectionUndoSave ={newSelections: new Map<Int, Bool>(), buffer: new UndoBuffer()};
+				undoSave = save;
+
+				closeAddMenu();
+				clearSelectionBoxesUndo(save.buffer);
+				finalizeUserCreateEdge();
+				rawheaps.setPointerCapture(e.pointerId);
+				e.stopPropagation();
+				return;
+			}
+			if (e.button == 1) {
+				lastClickPan = new Point(e.clientX, e.clientY);
+				isPanning = true;
+				return;
+			}
+
+			if (e.button == 2) {
+				if (addMenu?.is(":visible") ?? false) {
+					closeAddMenu();
+					cleanupCreateEdge();
+				}
+				else {
+					openAddMenu();
+				}
+				e.preventDefault();
+				e.stopPropagation();
+			}
+		});
+
+		heapsScene.on("contextmenu", function(e) {
+			e.preventDefault();
+		});
+
+		heapsScene.on("pointermove", function(e : js.jquery.Event) {
+			e.preventDefault();
+			e.cancelBubble=true;
+    		e.returnValue=false;
+			mouseMoveFunction(e);
+		});
+
+		var document = new Element(js.Browser.document);
+		document.on("pointerup", function(e) {
+			if(timerUpdateView != null)
+				stopUpdateViewPosition();
+			if (e.button == 0) {
+				// Stop rectangle selection
+				if (edgeCreationInput != null || edgeCreationOutput != null) {
+					if (edgeCreationInput != null && edgeCreationOutput != null) {
+						finalizeUserCreateEdge();
+						e.stopPropagation();
+						return;
+					}
+					else {
+						openAddMenu();
+						e.stopPropagation();
+						return;
+					}
+				}
+				startClickDrag = null;
+				startRecSelection = null;
+				if (recSelection != null) {
+					recSelection.remove();
+					recSelection = null;
+
+					var save : SelectionUndoSave = undoSave;
+
+					for (id => _ in save.newSelections) {
+						opSelect(id, true, save.buffer);
+					}
+					currentUndoBuffer = save.buffer;
+					commitUndo();
+					undoSave = null;
+					return;
+				}
+
+				return;
+			}
+
+			// Stop panning
+			if (e.button == 1) {
+				isPanning = false;
+				return;
+			}
+		});
+
+		// Zoom control
+		heapsScene.on("wheel", function(e) {
+			if (e.originalEvent.deltaY < 0) {
+				zoom(1.1, e.clientX, e.clientY);
+			} else {
+				zoom(0.9, e.clientX, e.clientY);
+			}
+		});
+
+		boxes = [];
+		outputsToInputs.clear();
+
+		var toolbar = new hide.comp.Toolbar(heapsScene);
+		toolbar.element.on("pointerdown", (e) -> e.stopPropagation());
+		toolbar.element.on("pointerup", (e) -> e.stopPropagation());
+		var toolsDefs = new Array<hide.comp.Toolbar.ToolDef>();
+
+		toolsDefs.push({id: "snapToGrid", title: "Snap to grid", icon: "magnet", type : Toggle(setSnapToGrid), defaultValue: true});
+		toolbar.makeToolbar(toolsDefs,config, keys);
+
+		toolbar.refreshToggles();
+		updateMatrix();
+
+		reloadInternal();
+	}
+
+	function setSnapToGrid(b: Bool) {
+		snapToGrid = b;
+	}
+
+	public function cancelAll() {
+		closeAddMenu();
+		cleanupCreateEdge();
+	}
+
+	var reloadQueued = false;
+	public function reload() {
+		reloadQueued = true;
+	}
+
+	function reloadInternal() {
+		reloadQueued = false;
+		boxesSelected.clear();
+		for (box in boxes) {
+			box.dispose();
+		}
+		boxes.clear();
+		outputsToInputs.clear();
+
+		for (e in edges) {
+			e.remove();
+		}
+		edges.clear();
+
+		var nodes = editor.getNodes();
+		for (node in nodes) {
+			addBox(node);
+		}
+
+		var edges = editor.getEdges();
+		for (edge in edges) {
+			createEdge(edge);
+		}
+	}
+
+	var boxToPreview : Map<Box, h2d.Bitmap>;
+	var miniPreviewInitTimeout = 0;
+	function onMiniPreviewReady() {
+		if (previewsScene.s2d == null) {
+			miniPreviewInitTimeout ++;
+			if (miniPreviewInitTimeout > 10)
+				throw "Couldn't initialize background previews";
+			haxe.Timer.delay(() -> onMiniPreviewReady, 100);
+			return;
+		}
+		var bg = new h2d.Flow(previewsScene.s2d);
+		bg.fillHeight = true;
+		bg.fillWidth = true;
+		bg.backgroundTile = h2d.Tile.fromColor(0x202020);
+		boxToPreview = [];
+
+		var identity : h3d.Matrix = new h3d.Matrix();
+		identity.identity();
+		@:privateAccess previewsScene.s2d.renderer.globals.set("camera.viewProj", identity);
+		@:privateAccess previewsScene.s2d.renderer.globals.set("camera.position", identity.getPosition());
+	}
+
+	/** If this function returns false, the preview update will be skipped**/
+	public dynamic function onPreviewUpdate() : Bool {
+		return true;
+	}
+
+	/** Called for each visible preview in the graph editor **/
+	public dynamic function onNodePreviewUpdate(node: IGraphNode, bitmap: h2d.Bitmap) : Void {
+
+	}
+
+	public dynamic function onSelectionChanged(selectedNodes: Array<IGraphNode>) {
+
+	}
+
+	function onMiniPreviewUpdate(dt: Float) {
+		@:privateAccess
+		/*if (sceneEditor?.scene?.s3d?.renderer?.ctx?.time != null) {
+			sceneEditor.scene.s3d.renderer.ctx.time = previewsScene.s3d.renderer.ctx.time;
+		}*/
+
+		if (reloadQueued) {
+			reloadInternal();
+			return;
+		}
+
+		if (!onPreviewUpdate())
+			return;
+
+		var newBoxToPreview : Map<Box, h2d.Bitmap> = [];
+		for (box in boxes) {
+			if (box.info.preview == null) {
+				continue;
+			}
+			var preview = boxToPreview.get(box);
+			if (preview == null) {
+				var bmp = new h2d.Bitmap(h2d.Tile.fromColor(0xFF00FF,1,1), previewsScene.s2d);
+				bmp.blendMode = None;
+				preview = bmp;
+			} else {
+				boxToPreview.remove(box);
+			}
+			newBoxToPreview.set(box,preview);
+		}
+
+		for (preview in boxToPreview) {
+			preview.remove();
+		}
+		boxToPreview = newBoxToPreview;
+
+		var sceneW = editorDisplay.element.width();
+		var sceneH = editorDisplay.element.height();
+
+		for (box => preview in boxToPreview) {
+			preview.visible = box.info.preview.getVisible();
+			if (!preview.visible)
+				continue;
+
+			preview.x = gX(box.x);
+			preview.y = gY(box.y + box.getHeight());
+			var lw = transformMatrix[0] * box.width;
+			var lh = transformMatrix[3] * box.width;
+			preview.scaleX = lw / preview.tile.width;
+			preview.scaleY = lh / preview.tile.height;
+
+			if (preview.x + lw < 0 || preview.x > sceneW || preview.y + lh < 0 || preview.y > sceneH) {
+				preview.visible = false;
+				continue;
+			}
+
+			onNodePreviewUpdate(box.node, preview);
+		}
+	}
+
+
+	function deleteSelection() {
+		cleanupCreateEdge();
+
+		currentUndoBuffer = [];
+
+		if (boxesSelected.iterator().hasNext()) {
+
+			for (id => _ in boxesSelected) {
+				var box = boxes.get(id);
+				opSelect(id, false, currentUndoBuffer);
+				removeBoxEdges(box, currentUndoBuffer);
+				opBox(box.node, false, currentUndoBuffer);
+			}
+
+			commitUndo();
+		}
+	}
+
+	function opMove(box: Box, x: Float, y: Float, undoBuffer: UndoBuffer) {
+		box.node.getPos(Box.tmpPoint);
+		var prevX = Box.tmpPoint.x;
+		var prevY = Box.tmpPoint.y;
+		if (prevX == x && prevY == y)
+			return;
+		var id = box.node.id;
+		function exec(isUndo: Bool) {
+			var x = !isUndo ? x : prevX;
+			var y = !isUndo ? y : prevY;
+			var box = boxes[id];
+			Box.tmpPoint.set(x,y);
+			box.node.setPos(Box.tmpPoint);
+			moveBox(box, x, y);
+		}
+		exec(false);
+		undoBuffer.push(exec);
+	}
+
+	public function opResize(box: Box, w: Float, h: Float, undoBuffer: UndoBuffer) {
+		box.info.comment.getSize(Box.tmpPoint);
+		var id = box.node.id;
+		var prevW = Box.tmpPoint.x;
+		var prevH = Box.tmpPoint.y;
+		function exec(isUndo : Bool) {
+			var box = boxes.get(id);
+			var vw = !isUndo ? w : prevW;
+			var vh = !isUndo ? h : prevH;
+			Box.tmpPoint.set(vw, vh);
+			box.info.comment.setSize(Box.tmpPoint);
+			box.width = Std.int(vw);
+			box.height = Std.int(vh);
+			box.refreshBox();
+		}
+
+		exec(false);
+		undoBuffer.push(exec);
+	}
+
+	function opSelect(id: Int, doSelect: Bool, undoBuffer: UndoBuffer) {
+		var exec = function(isUndo: Bool) {
+			if (!doSelect) isUndo = !isUndo;
+			if (!isUndo) {
+				boxesSelected.set(id, true);
+				var box = boxes[id];
+				box.setSelected(true);
+			} else {
+				boxesSelected.remove(id);
+				var box = boxes[id];
+				box.setSelected(false);
+			}
+
+			var selectedNodes = [for (k in boxesSelected.keys()) boxes.get(k).node];
+			onSelectionChanged(selectedNodes);
+		}
+		undoBuffer.push(exec);
+		exec(false);
+	}
+
+	static var lastOpenAddMenuPoint = new Point();
+	function openAddMenu(x : Int = 0, y : Int = 0) {
+
+		var boundsWidth = Std.int(element.width());
+		var boundsHeight = Std.int(element.height());
+
+		lastOpenAddMenuPoint.set(lX(ide.mouseX), lY(ide.mouseY));
+
+		var posCursor = new Point(Std.int(ide.mouseX - heapsScene.offset().left) + x, Std.int(ide.mouseY - heapsScene.offset().top) + y);
+		if( posCursor.x < 0 )
+			posCursor.x = 0;
+		if( posCursor.y < 0)
+			posCursor.y = 0;
+
+		if (addMenu != null) {
+			var menuWidth = Std.parseInt(addMenu.css("width")) + 10;
+			var menuHeight = Std.parseInt(addMenu.css("height")) + 10;
+			if( posCursor.x + menuWidth > boundsWidth )
+				posCursor.x = boundsWidth - menuWidth;
+			if( posCursor.y + menuHeight > boundsHeight )
+				posCursor.y = boundsHeight - menuHeight;
+
+			var input = addMenu.find("#search-input");
+			input.val("");
+			addMenu.show();
+			input.focus();
+
+			addMenu.css("left", posCursor.x);
+			addMenu.css("top", posCursor.y);
+
+			for (c in addMenu.find("#results").children().elements()) {
+				c.show();
+			}
+			return;
+		}
+
+		addMenu = new Element('
+		<div id="add-menu">
+			<div class="search-container">
+				<div class="icon" >
+					<i class="ico ico-search"></i>
+				</div>
+				<div class="search-bar" >
+					<input type="text" id="search-input" autocomplete="off" >
+				</div>
+			</div>
+			<div id="results">
+			</div>
+		</div>').appendTo(heapsScene);
+
+		addMenu.on("pointerdown", function(e) {
+			e.stopPropagation();
+		});
+
+		addMenu.on("blur", function(e) {
+			closeAddMenu();
+		});
+
+		var results = addMenu.find("#results");
+		results.on("wheel", function(e) {
+			e.stopPropagation();
+		});
+
+		var nodes = editor.getAddNodesMenu();
+		var prevGroups : Map<String, Element> = [];
+		for (i => node in nodes) {
+			if (prevGroups.get(node.group) == null) {
+				var groupEl = new Element('
+				<div class="group" >
+					<span> ${node.group} </span>
+				</div>').appendTo(results);
+				prevGroups.set(node.group, groupEl);
+			}
+
+			new Element('
+				<div node="$i" >
+					<span> ${node.name} </span> <span> ${node.description} </span>
+				</div>').insertAfter(prevGroups.get(node.group));
+		}
+
+		var menuWidth = Std.parseInt(addMenu.css("width")) + 10;
+		var menuHeight = Std.parseInt(addMenu.css("height")) + 10;
+		if( posCursor.x + menuWidth > boundsWidth )
+			posCursor.x = boundsWidth - menuWidth;
+		if( posCursor.y + menuHeight > boundsHeight )
+			posCursor.y = boundsHeight - menuHeight;
+		addMenu.css("left", posCursor.x);
+		addMenu.css("top", posCursor.y);
+
+		var input = addMenu.find("#search-input");
+		input.focus();
+		var divs = new Element("#results > div");
+		input.on("keydown", function(ev) {
+			if (ev.key == "Escape") {
+				cancelAll();
+				ev.stopPropagation();
+				ev.preventDefault();
+			}
+			if (ev.keyCode == 38 || ev.keyCode == 40) {
+				ev.stopPropagation();
+				ev.preventDefault();
+
+				if (this.selectedNode != null)
+					this.selectedNode.removeClass("selected");
+
+				var selector = "div[node]:not([style*='display: none'])";
+				var elt = this.selectedNode;
+
+				if (ev.keyCode == 38) {
+					do {
+						elt = elt.prev();
+					} while (elt.length > 0 && !elt.is(selector));
+				} else if (ev.keyCode == 40) {
+					do {
+						elt = elt.next();
+					} while (elt.length > 0 && !elt.is(selector));
+				}
+				if (elt.length == 1) {
+					this.selectedNode = elt;
+				}
+				if (this.selectedNode != null)
+					this.selectedNode.addClass("selected");
+
+				var offsetDiff = this.selectedNode.offset().top - results.offset().top;
+				if (offsetDiff > 225) {
+					results.scrollTop((offsetDiff-225)+results.scrollTop());
+				} else if (offsetDiff < 35) {
+					results.scrollTop(results.scrollTop()-(35-offsetDiff));
+				}
+			}
+		});
+
+		function doAdd() {
+			var key = Std.parseInt(this.selectedNode.attr("node"));
+			//var posCursor = new Point(lX(ide.mouseX - 25), lY(ide.mouseY - 10));
+
+			var instance = nodes[key].onConstructNode();
+
+			var createLinkInput = edgeCreationInput;
+			var createLinkOutput = edgeCreationOutput;
+			var fromInput = createLinkInput != null;
+
+
+			if (createLinkInput != null) {
+				createLinkOutput = packIO(instance.id, 0);
+			}
+			else if (createLinkOutput != null) {
+				createLinkInput = packIO(instance.id, 0);
+			}
+
+			var pos = new h2d.col.Point();
+			pos.load(lastOpenAddMenuPoint);
+			if (createLinkInput != null) {
+				pos.set(lastCurveX, lastCurveY);
+			}
+			cleanupCreateEdge();
+
+			instance.setPos(pos);
+			opBox(instance, true, currentUndoBuffer);
+			if (createLinkInput != null && createLinkOutput != null) {
+				var box = boxes[instance.id];
+				var x = (fromInput ? @:privateAccess box.width : 0) - Box.NODE_RADIUS;
+				var y = box.getNodeHeight(0) - Box.NODE_RADIUS;
+				opMove(boxes[instance.id], pos.x - x, pos.y - y, currentUndoBuffer);
+				opEdge(createLinkOutput, createLinkInput, true, currentUndoBuffer);
+			}
+
+			commitUndo();
+			closeAddMenu();
+		}
+
+		input.on("keyup", function(ev) {
+			if (ev.keyCode == 38 || ev.keyCode == 40) {
+				return;
+			}
+
+			if (ev.keyCode == 13) {
+				doAdd();
+			} else {
+				if (this.selectedNode != null)
+					this.selectedNode.removeClass("selected");
+				var value = StringTools.trim(input.val());
+				var children = divs.elements();
+				var isFirst = true;
+				var lastGroup = null;
+				for (elt in children) {
+					if (elt.hasClass("group")) {
+						lastGroup = elt;
+						elt.hide();
+						continue;
+					}
+					if (value.length == 0 || elt.children().first().html().toLowerCase().indexOf(value.toLowerCase()) != -1) {
+						if (isFirst) {
+							this.selectedNode = elt;
+							isFirst = false;
+						}
+						elt.show();
+						if (lastGroup != null)
+							lastGroup.show();
+					} else {
+						elt.hide();
+					}
+				}
+				if (this.selectedNode != null)
+					this.selectedNode.addClass("selected");
+			}
+		});
+		divs.on("pointerover", function(ev) {
+			if (ev.currentTarget.classList.contains("group")) {
+				return;
+			}
+			if (this.selectedNode != null)
+				this.selectedNode.removeClass("selected");
+			this.selectedNode = new Element(ev.currentTarget); // Todo : not make this jquery
+			this.selectedNode.addClass("selected");
+		});
+		divs.on("pointerup", function(ev) {
+			if (ev.currentTarget.classList.contains("group")) {
+				return;
+			}
+
+			doAdd();
+			ev.stopPropagation();
+		});
+	}
+
+	function closeAddMenu() {
+		if (addMenu != null) {
+			addMenu.hide();
+			heapsScene.focus();
+		}
+	}
+
+	function mouseMoveFunction(e: js.jquery.Event) {
+		var clientX = e.clientX;
+		var clientY = e.clientY;
+
+		if (addMenu?.is(":visible"))
+			return;
+		if (edgeCreationInput != null || edgeCreationOutput != null) {
+			startUpdateViewPosition();
+			createLink(clientX, clientY);
+			return;
+		}
+		// Moving edge
+		/*if (currentEdge != null) {
+			var distOutput = distanceToElement(currentEdge.from.outputs[currentEdge.outputFrom], clientX, clientY);
+			var distInput = distanceToElement(currentEdge.to.inputs[currentEdge.inputTo], clientX, clientY);
+
+			if (distOutput > distInput) {
+				replaceEdge(FromOutput, currentEdge.to.inputs[currentEdge.inputTo], clientX, clientY);
+			} else {
+				replaceEdge(FromInput, currentEdge, clientX, clientY);
+			}
+			currentEdge = null;
+			return;
+		}*/
+		if (isPanning) {
+			pan(new Point(clientX - lastClickPan.x, clientY - lastClickPan.y));
+			lastClickPan.x = clientX;
+			lastClickPan.y = clientY;
+			return;
+		}
+		// Edit rectangle selection
+		if (startRecSelection != null) {
+			startUpdateViewPosition();
+			var endRecSelection = new h2d.col.Point(lX(clientX), lY(clientY));
+			var xMin = startRecSelection.x;
+			var xMax = endRecSelection.x;
+			var yMin = startRecSelection.y;
+			var yMax = endRecSelection.y;
+
+			if (startRecSelection.x > endRecSelection.x) {
+				xMin = endRecSelection.x;
+				xMax = startRecSelection.x;
+			}
+			if (startRecSelection.y > endRecSelection.y) {
+				yMin = endRecSelection.y;
+				yMax = startRecSelection.y;
+			}
+
+			if (recSelection != null) recSelection.remove();
+			recSelection = editorDisplay.rect(editorMatrix, xMin, yMin, xMax - xMin, yMax - yMin).addClass("rect-selection");
+
+			var save : SelectionUndoSave = undoSave;
+
+			for (box in boxes) {
+				var shouldSelect = isInside(box, new Point(xMin, yMin), new Point(xMax, yMax));
+				if (shouldSelect) {
+					if (box.info.comment != null) {
+						shouldSelect = isFullyInside(box, new Point(xMin, yMin), new Point(xMax, yMax));
+					}
+				}
+
+				if (shouldSelect) {
+					box.setSelected(true);
+					save.newSelections.set(box.node.id, true);
+				} else {
+					box.setSelected(false);
+					save.newSelections.remove(box.node.id);
+				}
+			}
+			return;
+		}
+
+		// Move selected boxes
+		if (boxesSelected.iterator().hasNext() && startClickDrag != null) {
+			startUpdateViewPosition();
+			var dx = (lX(clientX) - startClickDrag.x);
+			var dy = (lY(clientY) - startClickDrag.y);
+			if (dx == 0 && dy == 0)
+				return;
+
+			var snap = snapToGrid == !e.altKey;
+
+			for (id => _  in boxesToMove) {
+				var b = boxes.get(id);
+				b.node.getPos(Box.tmpPoint);
+
+				// Snap origin of move
+				if (snap) {
+					Box.tmpPoint.x = std.Math.round(Box.tmpPoint.x / Box.NODE_MARGIN) * Box.NODE_MARGIN;
+					Box.tmpPoint.y = std.Math.round(Box.tmpPoint.y / Box.NODE_MARGIN) * Box.NODE_MARGIN;
+				}
+
+				var newX = Box.tmpPoint.x + dx;
+				var newY = Box.tmpPoint.y + dy;
+
+				// Snap movement
+				if (snap) {
+					newX = std.Math.round(newX / Box.NODE_MARGIN) * Box.NODE_MARGIN;
+					newY = std.Math.round(newY / Box.NODE_MARGIN) * Box.NODE_MARGIN;
+				}
+				moveBox(b, newX, newY);
+			}
+			return;
+		}
+	}
+
+	function moveBox(b: Box, x: Float, y: Float) {
+		b.setPosition(x, y);
+
+		var id = b.node.id;
+		// move edges from and to this box
+		for (i => _ in b.info.inputs) {
+			var input = packIO(id, i);
+			var output = outputsToInputs.getLeft(input);
+			if (output != null) {
+				clearEdge(input);
+				var visual = createCurve(output, input);
+				edges.set(input, visual);
+			}
+		}
+
+		for (i => _ in b.info.outputs) {
+			var output = packIO(id, i);
+			for (input in outputsToInputs.iterRights(output)) {
+				clearEdge(input);
+				var visual = createCurve(output, input);
+				edges.set(input, visual);
+			}
+		}
+	}
+
+	public function refreshBox(id: Int) {
+		var node = boxes.get(id).node;
+		removeBox(id);
+		addBox(node);
+		var b = boxes.get(id);
+
+		for (i => _ in b.info.inputs) {
+			var input = packIO(id, i);
+			var output = outputsToInputs.getLeft(input);
+			if (output != null) {
+				clearEdge(input);
+				var visual = createCurve(output, input);
+				edges.set(input, visual);
+			}
+		}
+
+		for (i => _ in b.info.outputs) {
+			var output = packIO(id, i);
+			for (input in outputsToInputs.iterRights(output)) {
+				clearEdge(input);
+				var visual = createCurve(output, input);
+				edges.set(input, visual);
+			}
+		}
+	}
+
+	function beginMove(e: js.html.MouseEvent) {
+		startClickDrag = new Point(lX(e.clientX), lY(e.clientY));
+		boxesToMove.clear();
+
+		for (id => _ in boxesSelected) {
+			var b = boxes.get(id);
+			boxesToMove.set(id, true);
+
+			if (b.info.comment != null && !e.shiftKey) {
+				var bounds = inline b.getBounds();
+				var min = inline new Point(bounds.x, bounds.y);
+				var max = inline new Point(bounds.x + bounds.w, bounds.y + bounds.h);
+
+				for (bb in boxes) {
+					if (isFullyInside(bb, min, max)) {
+						boxesToMove.set(bb.node.id, true);
+					}
+				}
+			}
+		}
+	}
+
+	function saveMovedBoxes() {
+		var save : Map<Int, {x: Float, y: Float}> = [];
+		for (id => _ in boxesToMove) {
+			var b = boxes[id];
+			b.node.getPos(Box.tmpPoint);
+			save.set(b.node.id, {x:Box.tmpPoint.x, y: Box.tmpPoint.y});
+		}
+		return save;
+	}
+
+	function endMove() {
+		startClickDrag = null;
+		for (id => _ in boxesToMove) {
+			for (id => _ in boxesToMove) {
+				var b = boxes[id];
+				opMove(b, b.x, b.y, currentUndoBuffer);
+			}
+		}
+
+		commitUndo();
+		boxesToMove = [];
+	}
+
+	static function edgeFromPack(output: Int, input: Int) : Edge {
+		var output = unpackIO(output);
+		var input = unpackIO(input);
+
+		return {nodeFromId: output.nodeId, outputFromId: output.ioId, nodeToId: input.nodeId, inputToId: input.ioId};
+	}
+
+	function selectAll() {
+		for (id => _ in boxes) {
+			opSelect(id, true, currentUndoBuffer);
+		}
+		commitUndo();
+	}
+
+
+	function commentFromSelection() {
+		if (boxesSelected.empty())
+			return;
+
+		var commentNode = editor.createCommentNode();
+		if (commentNode == null)
+			return;
+		var comment = commentNode.getInfo().comment;
+		if (comment == null)
+			throw "createCommentNode node is not a comment";
+
+		var bounds = inline new h2d.col.Bounds();
+		for (id => _ in boxesSelected) {
+			var box = boxes[id];
+
+			box.node.getPos(Box.tmpPoint);
+			bounds.addPos(Box.tmpPoint.x, Box.tmpPoint.y);
+			var previewHeight = (box.info.preview?.getVisible() ?? false) ? box.width : 0;
+			bounds.addPos(Box.tmpPoint.x + box.width, Box.tmpPoint.y + box.getHeight() + previewHeight);
+		}
+
+		var border = 10;
+		bounds.xMin -= border;
+		bounds.yMin -= border + 34;
+		bounds.xMax += border;
+		bounds.yMax += border;
+
+
+		Box.tmpPoint.set(bounds.xMin, bounds.yMin);
+		commentNode.setPos(Box.tmpPoint);
+		Box.tmpPoint.set(bounds.width, bounds.height);
+		comment.setSize(Box.tmpPoint);
+
+		opBox(commentNode, true, currentUndoBuffer);
+		commitUndo();
+	}
+
+	function onHide() {
+		if (boxesSelected.empty())
+			return;
+
+		var viz = false;
+		for (id => _ in boxesSelected) {
+			if (boxes[id].info.preview?.getVisible() == true ?? false) {
+				viz = true;
+				break;
+			}
+		}
+		for (id => _  in boxesSelected) {
+			var box = boxes.get(id);
+			if (box.info.preview == null)
+				continue;
+			opPreview(box, !viz, currentUndoBuffer);
+		}
+
+		commitUndo();
+	}
+
+	public function opPreview(box: Box, show: Bool, undoBuffer: UndoBuffer) : Void {
+		var prev = box.info.preview.getVisible();
+		if (prev == show)
+			return;
+		function exec(isUndo: Bool) {
+			var v = !isUndo ? show : prev;
+			box.info.preview.setVisible(v);
+		}
+		exec(false);
+		undoBuffer.push(exec);
+	}
+
+	public function opBox(node: IGraphNode, doAdd: Bool, undoBuffer: UndoBuffer) : Void {
+		var data = editor.serializeNode(node);
+
+		var exec = function(isUndo : Bool) : Void {
+			if (!doAdd) isUndo = !isUndo;
+			if (!isUndo) {
+				var node = editor.unserializeNode(data, false);
+				addBox(node);
+				editor.addNode(node);
+			}
+			else {
+				var id = node.id;
+				var box = boxes.get(id);
+
+				removeBox(id);
+				editor.removeNode(id);
+
+				// Sanity check
+				for (i => _ in box.info.inputs) {
+					var inputIO = packIO(box.node.id, i);
+					var outputIO = outputsToInputs.getLeft(inputIO);
+					if (outputIO != null)
+						throw "box has remaining inputs, operation is not atomic";
+				}
+
+				for (i => _ in box.info.outputs) {
+					var outputIO = packIO(box.node.id, i);
+					for (inputIO in outputsToInputs.iterRights(outputIO)) {
+						throw "box has remaining outputs, operation is not atomic";
+					}
+				}
+			}
+		}
+		undoBuffer.push(exec);
+		exec(false);
+	}
+
+	// Only remove the visual of the box
+	function removeBox(id: Int) {
+		var box = boxes.get(id);
+
+		box.dispose();
+		var id = box.node.id;
+		boxes.remove(id);
+	}
+
+	public function opComment(box: Box, newComment: String, undoBuffer: UndoBuffer) : Void {
+		var id = box.node.id;
+		var prev = box.info.comment.getComment();
+		if (newComment == prev)
+			return;
+		function exec(isUndo : Bool) {
+			var box = boxes.get(id);
+			var v = !isUndo ? newComment : prev;
+
+			box.info.comment.setComment(v);
+			box.element.find(".comment-title").get(0).innerText = v;
+		}
+		exec(false);
+		undoBuffer.push(exec);
+	}
+
+
+
+	function opEdge(output: Int, input: Int, doAdd: Bool, undoBuffer: UndoBuffer) : Void {
+		var edge = edgeFromPack(output, input);
+		var previousFrom : Null<Int> = outputsToInputs.getLeft(input);
+		var prevEdge = null;
+		if (previousFrom != null && doAdd) {
+			prevEdge = edgeFromPack(previousFrom, edgeCreationInput);
+		}
+
+		if (editor.canAddEdge(edge)) {
+			var exec = function (isUndo : Bool) : Void {
+				if (!doAdd) isUndo = !isUndo;
+				if (!isUndo) {
+					if (prevEdge != null)
+						removeEdge(prevEdge);
+					createEdge(edge);
+				}
+				else {
+					removeEdge(edge);
+					if (prevEdge != null) {
+						createEdge(prevEdge);
+					}
+				}
+			}
+			undoBuffer.push(exec);
+			exec(false);
+		}
+	}
+
+	function finalizeUserCreateEdge() {
+		if (edgeCreationOutput != null && edgeCreationInput != null) {
+			opEdge(edgeCreationOutput, edgeCreationInput, true, currentUndoBuffer);
+		}
+		commitUndo();
+		cleanupCreateEdge();
+	}
+
+	function cleanupCreateEdge() {
+		edgeCreationOutput = null;
+		edgeCreationInput = null;
+		edgeCreationCurve?.remove();
+		edgeCreationCurve = null;
+		edgeCreationMode = None;
+	}
+
+	static var tmpPoint = new h2d.col.Point();
+	function addBox(node : IGraphNode) : Box {
+		node.editor = this;
+		var box = new Box(this, editorMatrix, node);
+		node.getPos(Box.tmpPoint);
+		box.setPosition(Box.tmpPoint.x, Box.tmpPoint.y);
+
+		var elt = box.getElement();
+		elt.get(0).onpointerdown = function(e: js.html.PointerEvent) {
+			if (e.button != 0)
+				return;
+
+			// if ((cast e.target: js.html.Element).closest("foreignObject") != null && box.info.comment == null)
+			// {
+			// 	e.stopPropagation();
+			// 	return;
+			// }
+			e.stopPropagation();
+
+			if (!box.selected) {
+				if (!e.ctrlKey) {
+					// when not group selection and click on box not selected
+					clearSelectionBoxesUndo(currentUndoBuffer);
+				}
+				opSelect(box.node.id, true, currentUndoBuffer);
+				commitUndo();
+			}
+			elt.get(0).setPointerCapture(e.pointerId);
+			beginMove(e);
+		};
+		elt.get(0).onpointerup = function(e: js.html.PointerEvent) {
+			if (e.button != 0)
+				return;
+			elt.get(0).releasePointerCapture(e.pointerId);
+			endMove();
+		};
+		boxes.set(box.node.id, box);
+
+		for (inputId => input in box.info.inputs) {
+			var defaultValue : String = input.defaultParam?.get();
+			//defaultValue= Reflect.getProperty(box.getInstance().defaults, '${input.name}');
+
+			var grNode = box.addInput(this, input.name, defaultValue, input.color);
+			if (defaultValue != null) {
+				var fieldEditInput = grNode.find("input");
+				fieldEditInput.on("change", function(ev) {
+					var prevValue = Std.parseFloat(input.defaultParam.get()) ?? 0.0;
+					var tmpValue = Std.parseFloat(fieldEditInput.val());
+					if (Math.isNaN(tmpValue) ) {
+						fieldEditInput.addClass("error");
+						fieldEditInput.val(prevValue);
+					} else {
+						var id = box.node.id;
+						function exec(isUndo : Bool) {
+							var box = boxes.get(id);
+							var val = isUndo ? prevValue : tmpValue;
+							// 50 shades of curse
+							var input = box.inputs[inputId].parent().find("input");
+							input.val(val);
+							box.info.inputs[inputId].defaultParam.set(Std.string(val));
+						}
+
+						fieldEditInput.removeClass("error");
+						exec(false);
+						editor.getUndo().change(Custom(exec));
+					}
+				});
+				fieldEditInput.get(0).addEventListener("pointerdown", function(e) {
+					e.stopPropagation();
+					fieldEditInput.get(0).setPointerCapture(e.pointerId);
+				});
+
+				fieldEditInput.get(0).addEventListener("pointermove", function(e) {
+					e.stopPropagation();
+				});
+			}
+			grNode.find(".node").attr("field", inputId);
+			grNode.get(0).addEventListener("pointerdown", function(e) {
+				if (e.button == 0) {
+					e.stopPropagation();
+					cancelAll();
+					heapsScene.get(0).setPointerCapture(e.pointerId);
+					edgeCreationInput = packIO(box.node.id, inputId);
+					edgeCreationMode = FromInput;
+				}
+			});
+		}
+		for (outputId => info in box.info.outputs) {
+			var grNode = box.addOutput(this, info.name, info.color);
+			grNode.find(".node").attr("field", outputId);
+			grNode.get(0).addEventListener("pointerdown", function(e) {
+				if (e.button == 0) {
+					e.stopPropagation();
+					cancelAll();
+					heapsScene.get(0).setPointerCapture(e.pointerId);
+					edgeCreationOutput = packIO(box.node.id, outputId);
+					edgeCreationMode = FromOutput;
+				}
+			});
+		}
+
+		box.generateProperties(this);
+
+		return box;
+	}
+
+	inline static function packIO(id: Int, ioId: Int) {
+		return ioId << 24 | id;
+	}
+
+	inline static function unpackIO(io:Int) {
+		return {
+			nodeId: io & 0xFFFFFF,
+			ioId: io >> 24,
+		};
+	}
+
+	function clearEdge(id: Int) {
+		var e = edges.get(id);
+		if (e == null)
+			return;
+		e.remove();
+		edges.remove(id);
+
+		var io = unpackIO(id);
+	}
+
+	function removeBoxEdges(box : Box, ?undoBuffer : UndoBuffer) {
+		var id = box.getInstance().id;
+		for (i => _ in box.info.inputs) {
+			var inputIO = packIO(id, i);
+			var outputIO = outputsToInputs.getLeft(inputIO);
+			if (outputIO != null) {
+				opEdge(outputIO, inputIO, false, undoBuffer);
+			}
+		}
+
+		for (i => _ in box.info.outputs) {
+			var outputIO = packIO(id, i);
+			for (inputIO in outputsToInputs.iterRights(outputIO)) {
+				opEdge(outputIO, inputIO, false, undoBuffer);
+			}
+		}
+	}
+
+	/** Asserts that editor.canAddEdge(edge) == true **/
+	function createEdge(edge : GraphInterface.Edge){
+		editor.addEdge(edge);
+		var output = packIO(edge.nodeFromId, edge.outputFromId);
+		var input = packIO(edge.nodeToId, edge.inputToId);
+		var prev = outputsToInputs.getLeft(input);
+		if(prev != null)
+			throw "No input should be present";
+		outputsToInputs.insert(output, input);
+
+		var inputElem = boxes.get(edge.nodeToId).inputs[edge.inputToId];
+
+		inputElem.attr("hasLink", "true");
+		inputElem.parent().addClass("hasLink");
+
+		var visual = createCurve(output, input);
+		edges.set(input, visual);
+		return prev;
+	}
+
+	function removeEdge(edge : GraphInterface.Edge) {
+		var id = packIO(edge.nodeToId, edge.inputToId);
+		outputsToInputs.removeRight(id);
+		clearEdge(id);
+
+		var input = boxes[edge.nodeToId].inputs[edge.inputToId];
+		input.removeAttr("hasLink");
+		input.parent().removeClass("hasLink");
+
+		editor.removeEdge(edge.nodeToId, edge.inputToId);
+	}
+
+	/*function replaceEdge(state : EdgeState, ?edge : Edge, ?node : JQuery, x : Int, y : Int) {
+		switch (state) {
+			case FromOutput:
+				for (e in listOfEdges) {
+					if (e.to.inputs[e.inputTo].is(node)) {
+						isCreatingLink = FromOutput;
+						startLinkNodeId = e.outputFrom;
+						startLinkBox = e.from;
+						edgeStyle.stroke = e.from.outputs[e.outputFrom].css("fill");
+						removeEdge(e);
+						createLink(x, y);
+						return;
+					}
+				}
+			case FromInput:
+				for (e in listOfEdges) {
+					if (e.to == edge.to && e.inputTo == edge.inputTo && e.from == edge.from && e.outputFrom == edge.outputFrom) {
+						isCreatingLink = FromInput;
+						startLinkNodeId = e.inputTo;
+						startLinkBox = e.to;
+						edgeStyle.stroke = e.from.outputs[e.outputFrom].css("fill");
+						removeEdge(e);
+						createLink(x, y);
+						return;
+					}
+				}
+			default:
+				return;
+		}
+	}*/
+
+	function error(str : String, ?idBox : Int) {
+		Ide.inst.quickError(str);
+	}
+
+	function info(str : String) {
+		Ide.inst.quickMessage(str);
+	}
+
+	/*function createEdgeInEditorGraph(edge) {
+		listOfEdges.push(edge);
+		edge.to.inputs[edge.inputTo].attr("hasLink", "true");
+		edge.to.inputs[edge.inputTo].parent().addClass("hasLink");
+
+		edge.elt.on("mousedown", function(e) {
+			e.stopPropagation();
+			clearSelectionBoxes();
+			this.currentEdge = edge;
+			currentEdge.elt.addClass("selected");
+		});
+	}*/
+
+	function createLink(clientX : Int, clientY : Int) {
+
+		var nearestId = -1;
+		var minDistNode = NODE_TRIGGER_NEAR;
+
+		// checking nearest box
+		var nearestBox = null;
+		var minDist = 999999999999999.0;
+		for (i => b in boxes) {
+			if (b.info.comment != null)
+				continue;
+			var tmpDist = distanceToBox(b, clientX, clientY);
+			if (tmpDist < minDist) {
+				minDist = tmpDist;
+				nearestBox = b;
+			}
+		}
+		if (nearestBox == null)
+			return;
+
+		// checking nearest node in the nearest box
+		if (edgeCreationMode == FromInput) {
+			for (id => o in nearestBox.outputs) {
+				var newMin = distanceToElement(o, clientX, clientY);
+				if (newMin < minDistNode) {
+					nearestId = id;
+					minDistNode = newMin;
+				}
+			}
+		} else {
+			// input has one edge at most
+			for (id => i in nearestBox.inputs) {
+				var newMin = distanceToElement(i, clientX, clientY);
+				if (newMin < minDistNode) {
+					nearestId = id;
+					minDistNode = newMin;
+				}
+			}
+		}
+
+		var val = null;
+		if (minDistNode < NODE_TRIGGER_NEAR && nearestId >= 0) {
+			val = packIO(nearestBox.node.id, nearestId);
+		}
+
+		if (edgeCreationMode == FromInput) {
+			edgeCreationOutput = val;
+		} else {
+			edgeCreationInput = val;
+		}
+
+		// create edge
+		if (edgeCreationCurve != null) edgeCreationCurve.remove();
+		edgeCreationCurve = createCurve(edgeCreationOutput, edgeCreationInput, minDistNode, clientX, clientY, true);
+	}
+
+	function serializeSelection() : String {
+		var data : CopySelectionData = {
+			nodes:  [],
+			edges: [],
+		};
+		for (nodeId => _ in boxesSelected) {
+			var box = boxes[nodeId];
+			data.nodes.push({id: nodeId, serData: editor.serializeNode(box.node)});
+			for (inputId => _ in box.info.inputs) {
+				var output = outputsToInputs.getLeft(packIO(nodeId, inputId));
+				if (output != null) {
+					var unpack = unpackIO(output);
+					if ( boxesSelected.get(unpack.nodeId) != null) {
+						data.edges.push({nodeFromId: unpack.nodeId, outputFromId: unpack.ioId, nodeToId: nodeId, inputToId: inputId});
+					}
+				}
+			}
+		}
+
+		if (!data.nodes.empty()) {
+			return haxe.Json.stringify(data);
+		}
+		return null;
+	}
+
+	function copySelection() {
+		var str = serializeSelection();
+		if (str != null) {
+			Ide.inst.setClipboard(str);
+		}
+	}
+
+	function cutSelection() {
+		copySelection();
+		deleteSelection();
+	}
+
+	function duplicateSelection() {
+		var str = serializeSelection();
+		createFromString(str, currentUndoBuffer);
+		commitUndo();
+	}
+
+	function createFromString(str: String, undoBuffer: UndoBuffer) {
+		var nodes : Array<IGraphNode> = [];
+		var idRemap : Map<Int, Int> = [];
+		var edges : Array<Edge> = [];
+		try {
+			var data : CopySelectionData = haxe.Json.parse(str);
+			for (nodeInfo in data.nodes) {
+				var node = editor.unserializeNode(nodeInfo.serData, true);
+				nodes.push(node);
+				var newId = node.id;
+				idRemap.set(nodeInfo.id, newId);
+			}
+			for (e in data.edges) {
+				edges.push({nodeFromId: e.nodeFromId, nodeToId: e.nodeToId, outputFromId: e.outputFromId, inputToId: e.inputToId});
+			}
+		}
+		catch (e) {
+			Ide.inst.quickError('Could not paste content of clipboard ($e) "$str"');
+			return;
+		}
+
+		if (nodes.length <= 0)
+			return;
+
+		// center of all the boxes
+		var offset = new h2d.col.Point(0,0);
+		var pt = Box.tmpPoint;
+		for (count => node in nodes) {
+			node.getPos(pt);
+			offset.set(offset.x + (pt.x - offset.x) / (count + 1),offset.y + (pt.y - offset.y) / (count + 1));
+		}
+
+		clearSelectionBoxesUndo(undoBuffer);
+
+		for (node in nodes) {
+			node.getPos(pt);
+			pt -= offset;
+			pt.x += lX(ide.mouseX);
+			pt.y += lY(ide.mouseY);
+			node.setPos(pt);
+			opBox(node, true, undoBuffer);
+			opSelect(node.id, true, undoBuffer);
+		}
+
+		for (edge in edges) {
+			var newFromId = idRemap.get(edge.nodeFromId);
+			var newToId = idRemap.get(edge.nodeToId);
+			opEdge(packIO(newFromId, edge.outputFromId), packIO(newToId, edge.inputToId), true, undoBuffer);
+		}
+	}
+
+	function paste() {
+		var nodes : Array<IGraphNode> = [];
+		var idRemap : Map<Int, Int> = [];
+		var edges : Array<Edge> = [];
+		try {
+			var cb = Ide.inst.getClipboard();
+			var data : CopySelectionData = haxe.Json.parse(cb);
+			for (nodeInfo in data.nodes) {
+				var node = editor.unserializeNode(nodeInfo.serData, true);
+				nodes.push(node);
+				var newId = node.id;
+				idRemap.set(nodeInfo.id, newId);
+			}
+			for (e in data.edges) {
+				edges.push({nodeFromId: e.nodeFromId, nodeToId: e.nodeToId, outputFromId: e.outputFromId, inputToId: e.inputToId});
+			}
+		}
+		catch (e) {
+			Ide.inst.quickError('Could not paste content of clipboard ($e)');
+			return;
+		}
+
+		if (nodes.length <= 0)
+			return;
+
+		// center of all the boxes
+		var offset = new h2d.col.Point(0,0);
+		var pt = Box.tmpPoint;
+		for (count => node in nodes) {
+			node.getPos(pt);
+			offset.set(offset.x + (pt.x - offset.x) / (count + 1),offset.y + (pt.y - offset.y) / (count + 1));
+		}
+
+
+		currentUndoBuffer = [];
+
+		clearSelectionBoxesUndo(currentUndoBuffer);
+
+		for (node in nodes) {
+			node.getPos(pt);
+			pt -= offset;
+			pt.x += lX(ide.mouseX);
+			pt.y += lY(ide.mouseY);
+			node.setPos(pt);
+			opBox(node, true, currentUndoBuffer);
+			opSelect(node.id, true, currentUndoBuffer);
+		}
+
+		for (edge in edges) {
+			var newFromId = idRemap.get(edge.nodeFromId);
+			var newToId = idRemap.get(edge.nodeToId);
+			opEdge(packIO(newFromId, edge.outputFromId), packIO(newToId, edge.inputToId), true, currentUndoBuffer);
+		}
+
+
+		commitUndo();
+	}
+
+	function createCurve(packedOutput: Null<Int>, packedInput: Null<Int>, ?distance : Float, ?x : Float, ?y : Float, ?isDraft : Bool) {
+		var offsetEnd = {top : y ?? 0.0, left : x ?? 0.0};
+		if (packedInput != null) {
+			var input = unpackIO(packedInput);
+			var node = boxes[input.nodeId].inputs[input.ioId];
+			offsetEnd = node.offset();
+		}
+		var offsetStart = {top : y ?? 0.0, left : x ?? 0.0};
+		if (packedOutput != null) {
+			var output = unpackIO(packedOutput);
+			var node = boxes[output.nodeId].outputs[output.ioId];
+			offsetStart = node.offset();
+		}
+
+		if (x != null && y != null) {
+			lastCurveX = lX(x);
+			lastCurveY = lY(y);
+		}
+
+		var startX = lX(offsetStart.left) + Box.NODE_RADIUS;
+		var startY = lY(offsetStart.top) + Box.NODE_RADIUS;
+		var endX = lX(offsetEnd.left) + Box.NODE_RADIUS;
+		var endY = lY(offsetEnd.top) + Box.NODE_RADIUS;
+		var diffDistanceY = offsetEnd.top - offsetStart.top;
+		var signCurveY = ((diffDistanceY > 0) ? -1 : 1);
+		diffDistanceY = Math.abs(diffDistanceY);
+		var valueCurveX = 100;
+		var valueCurveY = 1;
+		var maxDistanceY = 900;
+
+		var curves = editorDisplay.group(null);
+		editorMatrix.prepend(curves);
+
+		var curveViz = editorDisplay.straightCurve(curves,
+			startX,
+			startY,
+			endX,
+			endY,
+			18,
+			0.25,
+			{}).addClass("edge");
+
+		if (!isDraft && packedOutput != null && packedInput != null) {
+			var curveHitbox = editorDisplay.straightCurve(curves,
+				startX,
+				startY,
+				endX,
+				endY,
+				18,
+				0.25,
+				).addClass("edge").addClass("hitbox");
+
+			curveHitbox.on("pointerdown", function(e) {
+
+				if (e.button == 0) {
+					opEdge(packedOutput, packedInput, false, currentUndoBuffer);
+
+					heapsScene.get(0).setPointerCapture(e.pointerId);
+
+					var mx = lX(e.clientX);
+					var my = lY(e.clientY);
+					if (hxd.Math.distance(mx - startX, my - startY, 0) < hxd.Math.distance(mx - endX, my - endY, 0)) {
+						edgeCreationInput = packedInput;
+						edgeCreationMode = FromInput;
+					} else {
+						edgeCreationOutput = packedOutput;
+						edgeCreationMode = FromOutput;
+					}
+
+
+					e.preventDefault();
+					e.stopPropagation();
+				}
+				else if (e.button == 2) {
+					new hide.comp.ContextMenu([
+						{label: "Delete ?", click: function() {
+								var edge = edgeFromPack(packedOutput, packedInput);
+								opEdge(packedOutput, packedInput, false, currentUndoBuffer);
+								commitUndo();
+							}
+						},
+					]);
+					e.preventDefault();
+					e.stopPropagation();
+				}
+
+
+			});
+		}
+
+		if (isDraft)
+			curves.addClass("draft");
+
+		return curves;
+	}
+
+	function clearSelectionBoxesUndo(undoBuffer: UndoBuffer) {
+		for (id => _ in boxesSelected) {
+			opSelect(id, false, undoBuffer);
+		}
+	}
+
+	function startUpdateViewPosition() {
+		if (timerUpdateView != null)
+			return;
+		timerUpdateView = new Timer(0);
+		timerUpdateView.run = function() {
+			var posCursor = new Point(ide.mouseX - heapsScene.offset().left, ide.mouseY - heapsScene.offset().top);
+			var wasUpdated = false;
+			if (posCursor.x < BORDER_SIZE) {
+				pan(new Point((BORDER_SIZE - posCursor.x)*SPEED_BORDER_MOVE, 0));
+				wasUpdated = true;
+			}
+			if (posCursor.y < BORDER_SIZE) {
+				pan(new Point(0, (BORDER_SIZE - posCursor.y)*SPEED_BORDER_MOVE));
+				wasUpdated = true;
+			}
+			var rightBorder = heapsScene.width() - BORDER_SIZE;
+			if (posCursor.x > rightBorder) {
+				pan(new Point((rightBorder - posCursor.x)*SPEED_BORDER_MOVE, 0));
+				wasUpdated = true;
+			}
+			var botBorder = heapsScene.height() - BORDER_SIZE;
+			if (posCursor.y > botBorder) {
+				pan(new Point(0, (botBorder - posCursor.y)*SPEED_BORDER_MOVE));
+				wasUpdated = true;
+			}
+		};
+	}
+
+	function stopUpdateViewPosition() {
+		if (timerUpdateView != null) {
+			timerUpdateView.stop();
+			timerUpdateView = null;
+		}
+	}
+
+	function getGraphDims() {
+		if(!boxes.iterator().hasNext()) return {xMin : -1.0, yMin : -1.0, xMax : 1.0, yMax : 1.0, center : new IPoint(0,0)};
+		var xMin = 1000000.0;
+		var yMin = 1000000.0;
+		var xMax = -1000000.0;
+		var yMax = -1000000.0;
+		for (b in boxes) {
+			b.node.getPos(Box.tmpPoint);
+			var x = Box.tmpPoint.x;
+			var y = Box.tmpPoint.y;
+			xMin = Math.min(xMin, x);
+			yMin = Math.min(yMin, y);
+			xMax = Math.max(xMax, x + b.width);
+			yMax = Math.max(yMax, y + b.getHeight());
+		}
+		var center = new IPoint(Std.int(xMin + (xMax - xMin)/2), Std.int(yMin + (yMax - yMin)/2));
+		center.y += Std.int(editorDisplay.element.height()*CENTER_OFFSET_Y);
+		return {
+			xMin : xMin,
+			yMin : yMin,
+			xMax : xMax,
+			yMax : yMax,
+			center : center,
+		};
+	}
+
+	public function centerView() {
+		if (!boxes.iterator().hasNext()) return;
+		var dims = getGraphDims();
+		var scale = Math.min(1, Math.min((editorDisplay.element.width() - 50) / (dims.xMax - dims.xMin), (editorDisplay.element.height() - 50) / (dims.yMax - dims.yMin)));
+
+		transformMatrix[4] = editorDisplay.element.width()/2 - dims.center.x;
+		transformMatrix[5] = editorDisplay.element.height()/2 - dims.center.y;
+
+		transformMatrix[0] = scale;
+		transformMatrix[3] = scale;
+
+		var x = editorDisplay.element.width()/2;
+		var y = editorDisplay.element.height()/2;
+
+		transformMatrix[4] = x - (x - transformMatrix[4]) * scale;
+		transformMatrix[5] = y - (y - transformMatrix[5]) * scale;
+
+		updateMatrix();
+	}
+
+	function clampView() {
+		if (boxes.iterator().hasNext()) return;
+		var dims = getGraphDims();
+
+		var width = editorDisplay.element.width();
+		var height = editorDisplay.element.height();
+		var scale = transformMatrix[0];
+
+		if( transformMatrix[4] + dims.xMin * scale > width )
+			transformMatrix[4] = width - dims.xMin * scale;
+		if( transformMatrix[4] + dims.xMax * scale < 0 )
+			transformMatrix[4] = -1 * dims.xMax * scale;
+		if( transformMatrix[5] + dims.yMin * scale > height )
+			transformMatrix[5] = height - dims.yMin * scale;
+		if( transformMatrix[5] + dims.yMax * scale < 0 )
+			transformMatrix[5] = -1 * dims.yMax * scale;
+	}
+
+	function updateMatrix() {
+		editorMatrix.attr({transform: 'matrix(${transformMatrix.join(' ')})'});
+	}
+
+	function zoom(scale : Float, x : Int, y : Int) {
+		if (scale > 1 && transformMatrix[0] > MAX_ZOOM) {
+			return;
+		}
+
+		transformMatrix[0] *= scale;
+		transformMatrix[3] *= scale;
+
+		x -= Std.int(editorDisplay.element.offset().left);
+		y -= Std.int(editorDisplay.element.offset().top);
+
+		transformMatrix[4] = x - (x - transformMatrix[4]) * scale;
+		transformMatrix[5] = y - (y - transformMatrix[5]) * scale;
+
+		clampView();
+		updateMatrix();
+	}
+
+	function pan(p : Point) {
+		transformMatrix[4] += p.x;
+		transformMatrix[5] += p.y;
+
+		clampView();
+		updateMatrix();
+	}
+
+	function isVisible() : Bool {
+		return editorDisplay.element.is(":visible");
+	}
+
+	// Useful method
+	function isInside(b : Box, min : Point, max : Point) {
+		var bounds = inline b.getBounds();
+		if (max.x < bounds.x || min.x > bounds.x + bounds.w)
+			return false;
+		if (max.y < bounds.y || min.y > bounds.y + bounds.h)
+			return false;
+
+		return true;
+	}
+
+	function isFullyInside(b: Box, min : Point, max : Point) {
+		var bounds = inline b.getBounds();
+		if (min.x > bounds.x || max.x < bounds.x + bounds.w)
+			return false;
+		if (min.y > bounds.y || max.y < bounds.y + bounds.h)
+			return false;
+
+		return true;
+	}
+
+	function distanceToBox(b : Box, x : Int, y : Int) {
+		var bounds = inline b.getBounds();
+		var dx = Math.max(Math.abs(lX(x) - (bounds.x + (bounds.w / 2))) - bounds.w / 2, 0);
+		var dy = Math.max(Math.abs(lY(y) - (bounds.y + (bounds.h / 2))) - bounds.h / 2, 0);
+		return dx * dx + dy * dy;
+	}
+	function distanceToElement(element : JQuery, x : Int, y : Int) {
+		if (element == null)
+			return NODE_TRIGGER_NEAR+1;
+		var dx = Math.max(Math.abs(x - (element.offset().left + element.width() / 2)) - element.width() / 2, 0);
+		var dy = Math.max(Math.abs(y - (element.offset().top + element.height() / 2)) - element.height() / 2, 0);
+		return dx * dx + dy * dy;
+	}
+	public function gX(x : Float) : Float {
+		return x*transformMatrix[0] + transformMatrix[4];
+	}
+	public function gY(y : Float) : Float {
+		return y*transformMatrix[3] + transformMatrix[5];
+	}
+	public function gPos(x : Float, y : Float) : Point {
+		return new Point(gX(x), gY(y));
+	}
+	public function lX(x : Float) : Float {
+		var screenOffset = editorDisplay.element.offset();
+		x -= screenOffset.left;
+		return (x - transformMatrix[4])/transformMatrix[0];
+	}
+	public function lY(y : Float) : Float {
+		var screenOffset = editorDisplay.element.offset();
+		y -= screenOffset.top;
+		return (y - transformMatrix[5])/transformMatrix[3];
+	}
+	public function lPos(x : Float, y : Float) : Point {
+		return new Point(lX(x), lY(y));
+	}
+
+}

+ 108 - 0
hide/view/GraphInterface.hx

@@ -0,0 +1,108 @@
+package hide.view;
+
+
+typedef GraphNodeInfo = {
+    name: String,
+    ?headerColor: Int,
+    inputs: Array<NodeInput>,
+    outputs: Array<NodeOutput>,
+    ?width: Int,
+
+    /**If set, the node can show a preview pannel**/
+    ?preview: {
+        getVisible : () -> Bool,
+        setVisible : (Bool) -> Void,
+
+        /**If the preview takes over the whole node like in texture editing mode**/
+        fullSize : Bool,
+    },
+
+    ?noHeader: Bool,
+
+    /**If set, the node will be treated as a comment**/
+    ?comment : {
+        getComment : () -> String,
+        setComment : (String) -> Void,
+        getSize : (s: h2d.col.Point) -> Void,
+        setSize : (s: h2d.col.Point) -> Void,
+    }
+};
+
+typedef NodeInput = {
+    /**Display name of the node input **/
+    name: String,
+    ?color: Int,
+
+    /**If set, the input will have a input text box next to it when not connected**/
+    ?defaultParam: {
+        get : () -> String,
+        set : (String) -> Void
+    },
+};
+
+typedef NodeOutput = {
+    name: String,
+    ?color: Int,
+};
+
+typedef AddNodeMenuEntry = {
+    name: String,
+    description: String,
+    group: String,
+
+    /**This function will be called when the user chooses to add a node from the add node menu
+        You should generate a unique ID for the new IGraphNode. Don't add the node to your graph datastructure yet,
+        the Graph editor will call addNode() with this node at the right time.
+    **/
+    onConstructNode: () -> IGraphNode,
+};
+
+
+/**An edge between 2 nodes. the inputs/outpus id are based on the order of the inputs/ouputs returned by IGraphNode.getInfo()**/
+typedef Edge = {
+    nodeFromId : Int,
+    outputFromId : Int,
+    nodeToId : Int,
+    inputToId : Int,
+};
+
+interface IGraphNode {
+     /**
+        Unique identifier for this node. The ID of a given node MUST NERVER change for the entire lifetime of the GraphEditor
+    **/
+    public var id : Int;
+    public var x : Float;
+    public var y : Float;
+    public var editor : GraphEditor;
+
+    public function getInfo() : GraphNodeInfo;
+    public function getPos(p : h2d.col.Point) : Void;
+    public function setPos(p : h2d.col.Point) : Void;
+    public function getPropertiesHTML(width : Float) : Array<hide.Element>;
+}
+
+interface IGraphEditor {
+    public function getNodes() : Iterator<IGraphNode>;
+    public function getEdges() : Iterator<Edge>;
+    public function getAddNodesMenu() : Array<AddNodeMenuEntry>;
+
+    public function addNode(node : IGraphNode) : Void;
+    public function removeNode(id:Int) : Void;
+
+    public function serializeNode(node : IGraphNode) : Dynamic;
+
+    /**If newId is true, then the returned node must have a new unique id. This is used when duplicating nodes**/
+    public function unserializeNode(data: Dynamic, newId: Bool) : IGraphNode;
+
+    /**Create a comment node. Return null if you don't have a comment node in your editor**/
+    public function createCommentNode() : Null<IGraphNode>;
+
+
+    /**Returns false if the edge can't be created because the input/output types don't match**/
+    public function canAddEdge(edge : Edge) : Bool;
+
+    public function addEdge(edge : Edge) : Void;
+    public function removeEdge(nodeToId: Int, inputToId : Int) : Void;
+
+    public function getUndo() : hide.ui.UndoHistory;
+}

+ 33 - 4
hide/view/Image.hx

@@ -119,6 +119,12 @@ class Image extends FileView {
 		}
 
 		var compressionInfo = element.find(".compression-infos");
+		var nativeFormat = new Element('<div class="field">
+		<label>Native format :</label>
+		<label class="native-format">Unknown</label>
+		</div>');
+		compressionInfo.append(nativeFormat);
+
 		addField(compressionInfo, "Format :", "Compression format used to compress texture", "select-format", ["none", "BC1", "BC2", "BC3", "RGBA", "R16F", "RG16F", "RGBA16F", "R32F", "RG32F", "RGBA32F", "R16U", "RG16U", "RGBA16U"] );
 
 		var alphaField = new Element('<div class="field alpha">
@@ -387,8 +393,8 @@ class Image extends FileView {
 			scene.loadTexture(state.path, state.path, function(compressedTexture) {
 				scene.loadTexture(state.path, state.path, function(uncompressedTexture) {
 					onTexturesLoaded(compressedTexture, uncompressedTexture);
-				}, false, true);
-			}, false);
+				}, onError, false, true);
+			}, onError, false);
 		};
 	}
 
@@ -583,6 +589,7 @@ class Image extends FileView {
 		var useAlpha = compressionInfo.find(".use-alpha");
 		var alpha = compressionInfo.find(".alpha-threshold");
 		var maxSize = compressionInfo.find(".max-size");
+		var nativeFormat = compressionInfo.find(".native-format");
 
 		var dirPos = state.path.lastIndexOf("/");
 		var name = dirPos < 0 ? state.path : state.path.substr(dirPos + 1);
@@ -593,7 +600,11 @@ class Image extends FileView {
 		@:privateAccess fs.convert.loadConfig(state.path);
 
 		var localEntry = @:privateAccess new hxd.fs.LocalFileSystem.LocalEntry(fs, name, state.path, Ide.inst.getPath(state.path));
-		fs.convert.run(localEntry);
+
+		try {
+			fs.convert.run(localEntry);
+		}
+		catch (e) onError();
 
 		@:privateAccess var texConvRule = fs.convert.getConvertRule(state.path);
 		var convertRuleEmpty = texConvRule == null || texConvRule.cmd == null || texConvRule.cmd.params == null;
@@ -637,6 +648,8 @@ class Image extends FileView {
 
 		var uncompTWeight = element.find(".uncomp-tex-weight");
 		uncompTWeight.text('Uncompressed texture weight : ${getTextureMemSize(state.path)} mb');
+
+		nativeFormat.text(getTextureNativeFormat(state.path).getName());
 	}
 
 	public function replaceImage(path : String) {
@@ -713,7 +726,10 @@ class Image extends FileView {
 			else
 				comp.params = { alpha:Std.parseInt(alpha.val()), format:format.val().toString(), mips:mips.is(':checked'), size:Std.parseInt(size.val()) };
 
-			comp.convert();
+			try {
+				comp.convert();
+			}
+			catch(e) onError();
 		}
 		else {
 			tmpPath = state.path;
@@ -741,6 +757,15 @@ class Image extends FileView {
 		return @:privateAccess floatToStringPrecision(t.mem.memSize(t) / (1024 * 1024));
 	}
 
+	public function getTextureNativeFormat(path: String) {
+		var p = ide.getPath(path);
+		var bytes = sys.io.File.getBytes(p);
+		var res = hxd.res.Any.fromBytes(p, bytes);
+		var t = res.toTexture();
+
+		return t.format;
+	}
+
 	public function floatToStringPrecision(number:Float, ?precision=2) {
 		number *= Math.pow(10, precision);
 		return Math.round(number) / Math.pow(10, precision);
@@ -770,6 +795,10 @@ class Image extends FileView {
 		updateSliderVisual();
 	}
 
+	public function onError() {
+		Ide.inst.quickError('Can\'t load texture with this compression parameters, original texture is loaded instead!');
+	}
+
 	static var _ = FileTree.registerExtension(Image,hide.Ide.IMG_EXTS.concat(["envd","envs"]),{ icon : "picture-o" });
 
 }

+ 161 - 143
hide/view/shadereditor/Box.hx

@@ -2,9 +2,9 @@ package hide.view.shadereditor;
 
 import hide.comp.SVG;
 import js.jquery.JQuery;
-import hrt.shgraph.ShaderNode;
+import hide.view.GraphInterface;
 
-@:access(hide.view.Graph)
+@:access(hide.view.GraphEditor)
 class Box {
 
 	static final boolColor = "#cc0505";
@@ -17,17 +17,17 @@ class Box {
 	static final samplerColor = "#600aff";
 	static final defaultColor = "#c8c8c8";
 
-	var nodeInstance : ShaderNode;
+	var node : IGraphNode;
+	var info : GraphNodeInfo;
 
 	var x : Float;
 	var y : Float;
-
-	var width : Int = 150;
+	var width : Int;
 	var height : Int;
 	var propsHeight : Int = 0;
 
 	public var HEADER_HEIGHT = 22;
-	@const var NODE_MARGIN = 17;
+	public static final NODE_MARGIN = 18;
 	public static var NODE_RADIUS = 5;
 	@const var NODE_TITLE_PADDING = 10;
 	@const var NODE_INPUT_PADDING = 3;
@@ -37,54 +37,61 @@ class Box {
 	public var outputs : Array<JQuery> = [];
 
 	var hasHeader : Bool = true;
-	var hadToShowInputs : Bool = false;
 	var color : String;
 	var closePreviewBtn : JQuery;
 
 	var element : JQuery;
 	var propertiesGroup : JQuery;
-	public var comment : hrt.shgraph.nodes.Comment = null;
 	static final resizeBorder : Int = 8;
 	static final halfResizeBorder : Int = resizeBorder >> 1;
 
-	public function new(editor : Graph, parent : JQuery, x : Float, y : Float, node : ShaderNode) {
-		this.nodeInstance = node;
+	public function new(editor : GraphEditor, parent : JQuery, node : IGraphNode) {
+		this.node = node;
+		info = node.getInfo();
 
-		var metas = haxe.rtti.Meta.getType(Type.getClass(node));
-		if (metas.width != null) {
-			this.width = metas.width[0];
-		}
 
-		if (Reflect.hasField(metas, "color")) {
-			color = Reflect.field(metas, "color");
-		}
-		var className = node.nameOverride ?? ((metas.name != null) ? metas.name[0] : "Undefined");
+		width = info.width ?? 150;
+		width = Std.int(hxd.Math.ceil(width / NODE_MARGIN) * NODE_MARGIN);
 
-		element = editor.editor.group(parent).addClass("box").addClass("not-selected");
+		//var metas = haxe.rtti.Meta.getType(Type.getClass(node));
+		//if (metas.width != null) {
+		//	this.width = metas.width[0];
+		//}
+
+		//if (Reflect.hasField(metas, "color")) {
+		//	color = Reflect.field(metas, "color");
+		//}
+		//var className = node.nameOverride ?? ((metas.name != null) ? metas.name[0] : "Undefined");
+
+		element = editor.editorDisplay.group(parent).addClass("box").addClass("not-selected");
 		element.attr("id", node.id);
-		setPosition(x, y);
 
-		comment = Std.downcast(node, hrt.shgraph.nodes.Comment);
-		if (comment != null) {
-			this.width = comment.width;
-			this.height = comment.height;
+		if (info.comment != null) {
+			info.comment.getSize(tmpPoint);
+			this.width = Std.int(tmpPoint.x);
+			this.height = Std.int(tmpPoint.y);
 			HEADER_HEIGHT = 34;
 			color = null;
 			this.element.addClass("comment");
 		}
 
-		if (Reflect.hasField(metas, "noheader")) {
+		if (info.noHeader ?? false) {
 			HEADER_HEIGHT = 0;
 			hasHeader = false;
 		}
 
-		// Debug: editor.editor.text(element, 2, -6, 'Node ${node.id}').addClass("node-id-indicator");
+		// Debug: editor.editorDisplay.text(element, 2, -6, 'Node ${node.id}').addClass("node-id-indicator");
 
-		// outline of box
-		editor.editor.rect(element, -1, -1, width+2, getHeight()+2).addClass("outline");
+		if (info.comment == null) {
+			editor.editorDisplay.rect(element, 0,0,width, getHeight()).addClass("background");
+
+		}
+		editor.editorDisplay.rect(element, -1, -1, width+2, getHeight()+2).addClass("outline");
+
+		if (info.comment != null) {
+
+			editor.editorDisplay.rect(element, 0,0,width, HEADER_HEIGHT).addClass("head-box");
 
-		if (comment != null) {
-			var shaderEditor : ShaderEditor = cast editor;
 
 			function makeResizable(elt: js.html.Element, left: Bool, top: Bool, right: Bool, bottom: Bool) {
 				var pressed = false;
@@ -96,7 +103,6 @@ class Box {
 					e.preventDefault();
 					pressed = true;
 					elt.setPointerCapture(e.pointerId);
-					shaderEditor.beforeChange();
 				};
 
 				elt.onpointermove = function(e: js.html.PointerEvent) {
@@ -105,12 +111,12 @@ class Box {
 					e.stopPropagation();
 					e.preventDefault();
 
-					var clientRect = editor.editor.element.get(0).getBoundingClientRect();
+					var clientRect = editor.editorDisplay.element.get(0).getBoundingClientRect();
 
-					var x0 : Int = Std.int(this.x);
-					var y0 : Int = Std.int(this.y);
-					var x1 : Int = x0 + comment.width;
-					var y1 : Int = y0 + comment.height;
+					var x0 : Int = Std.int(x);
+					var y0 : Int = Std.int(y);
+					var x1 : Int = x0 + width;
+					var y1 : Int = y0 + height;
 
 					var mx : Int = Std.int(editor.lX(e.clientX));
 					var my : Int = Std.int(editor.lY(e.clientY));
@@ -132,14 +138,10 @@ class Box {
 						y1 = hxd.Math.imax(my, y0 + minDim);
 					}
 
-					this.x = x0;
-					this.y = y0;
-					comment.width = x1 - x0;
-					comment.height = y1 - y0;
+					setPosition(x0,y0);
 
-					setPosition(this.x,this.y);
-					this.width = comment.width;
-					this.height = comment.height;
+					this.width = x1 - x0;
+					this.height = y1 - y0;
 					refreshBox();
 				}
 
@@ -149,133 +151,140 @@ class Box {
 					pressed = false;
 					e.stopPropagation();
 					e.preventDefault();
-					shaderEditor.afterChange();
-					elt.releasePointerCapture(e.pointerId);
+
+					this.node.getPos(tmpPoint);
+					var current = {x: tmpPoint.x, y:tmpPoint.y, w: this.width, h: this.height};
+					editor.opMove(this, this.x, this.y, editor.currentUndoBuffer);
+					editor.opResize(this, this.width, this.height, editor.currentUndoBuffer);
+					editor.commitUndo();
 				};
 			}
 
-			var elt = editor.editor.rect(element, 0,0,0,0).addClass("resize").get(0);
+			var elt = editor.editorDisplay.rect(element, 0,0,0,0).addClass("resize").get(0);
 			elt.style.cursor = "ns-resize";
 			elt.id = "resizeBot";
 			makeResizable(elt, false,false,false,true);
 
-			var elt = editor.editor.rect(element, 0,0,0,0).addClass("resize").get(0);
+			var elt = editor.editorDisplay.rect(element, 0,0,0,0).addClass("resize").get(0);
 			elt.style.cursor = "ns-resize";
 			elt.id = "resizeTop";
 			makeResizable(elt, false,true,false,false);
 
-			var elt = editor.editor.rect(element, 0,0,0,0).addClass("resize").get(0);
+			var elt = editor.editorDisplay.rect(element, 0,0,0,0).addClass("resize").get(0);
 			elt.style.cursor = "ew-resize";
 			elt.id = "resizeLeft";
 			makeResizable(elt, true,false,false,false);
 
-			var elt = editor.editor.rect(element, 0,0,0,0).addClass("resize").get(0);
+			var elt = editor.editorDisplay.rect(element, 0,0,0,0).addClass("resize").get(0);
 			elt.style.cursor = "ew-resize";
 			elt.id = "resizeRight";
 			makeResizable(elt, false,false,true,false);
 
-			var elt = editor.editor.rect(element, 0,0,0,0).addClass("resize").get(0);
+			var elt = editor.editorDisplay.rect(element, 0,0,0,0).addClass("resize").get(0);
 			elt.style.cursor = "nesw-resize";
 			elt.id = "resizeBotLeft";
 			makeResizable(elt, true,false,false,true);
 
-			var elt = editor.editor.rect(element, 0,0,0,0).addClass("resize").get(0);
+			var elt = editor.editorDisplay.rect(element, 0,0,0,0).addClass("resize").get(0);
 			elt.style.cursor = "nwse-resize";
 			elt.id = "resizeBotRight";
 			makeResizable(elt, false,false,true,true);
 
-			var elt = editor.editor.rect(element, 0,0,0,0).addClass("resize").get(0);
+			var elt = editor.editorDisplay.rect(element, 0,0,0,0).addClass("resize").get(0);
 			elt.style.cursor = "nwse-resize";
 			elt.id = "resizeTopLeft";
 			makeResizable(elt, true,true,false,false);
 
-			var elt = editor.editor.rect(element, 0,0,0,0).addClass("resize").get(0);
+			var elt = editor.editorDisplay.rect(element, 0,0,0,0).addClass("resize").get(0);
 			elt.style.cursor = "nesw-resize";
 			elt.id = "resizeTopRight";
 			makeResizable(elt, false,true,true,false);
+
+			var fo = editor.editorDisplay.foreignObject(element, 7, 2, 0, HEADER_HEIGHT-4);
+			fo.get(0).id = "commentTitle";
+			var commentTitle = new Element("<span contenteditable spellcheck='false'>Comment</span>").addClass("comment-title").appendTo(fo);
+
+			var editable = new hide.comp.ContentEditable(null, commentTitle);
+			editable.value = info.comment.getComment();
+			editable.onChange = function(v: String) {
+				editor.opComment(this, v, editor.currentUndoBuffer);
+				editor.commitUndo();
+			};
+
+			refreshBox();
+			return;
 		}
 
 		// header
 
 		if (hasHeader) {
-			var header = editor.editor.rect(element, 0, 0, this.width, HEADER_HEIGHT).addClass("head-box");
-			if (color != null) header.css("fill", color);
-			if (comment != null) {
-				var fo = editor.editor.foreignObject(element, 7, 2, 0, HEADER_HEIGHT-4);
-				fo.get(0).id = "commentTitle";
-				var commentTitle = new Element("<span contenteditable spellcheck='false'>Comment</span>").addClass("comment-title").appendTo(fo);
-
-				var shaderEditor : ShaderEditor = cast editor;
-
-				var editable = new hide.comp.ContentEditable(null, commentTitle);
-				editable.value = comment.comment;
-				editable.onChange = function(v: String) {
-					shaderEditor.beforeChange();
-					comment.comment = v;
-					shaderEditor.afterChange();
-				};
+			//var header = editor.editorDisplay.rect(element, 0, 0, this.width, HEADER_HEIGHT).addClass("head-box");
+			//if (color != null) header.css("fill", color);
+			if (info.comment != null) {
+
 			}
 			else {
-				editor.editor.text(element, 7, HEADER_HEIGHT-6, className).addClass("title-box");
+				editor.editorDisplay.text(element, 7, HEADER_HEIGHT-6, info.name).addClass("title-box");
 			}
 		}
 
-		if (Reflect.hasField(metas, "alwaysshowinputs")) {
-			hadToShowInputs = true;
-		}
-
-		propertiesGroup = editor.editor.group(element).addClass("properties-group");
+		propertiesGroup = editor.editorDisplay.group(element).addClass("properties-group");
 
 		// nodes div
-		var bg = editor.editor.rect(element, 0, HEADER_HEIGHT, this.width, 0).addClass("nodes");
-		if (!hasHeader && color != null) {
-			bg.css("fill", color);
-		}
 
-		if (node.canHavePreview()) {
-			closePreviewBtn = editor.editor.foreignObject(element, width / 2 - 16, 0, 32,32);
+		editor.editorDisplay.line(element, 0, HEADER_HEIGHT, width, HEADER_HEIGHT).addClass("separator");
+
+		// var bg = editor.editorDisplay.rect(element, 0, HEADER_HEIGHT, this.width, 0).addClass("nodes");
+		// if (!hasHeader && color != null) {
+		// 	bg.css("fill", color);
+		// }
+
+		if (info.preview != null) {
+			closePreviewBtn = editor.editorDisplay.foreignObject(element, width / 2 - 16, 0, 32,32);
 			closePreviewBtn.append(new JQuery('<div class="close-preview"><span class="ico"></span></div>'));
 
 			refreshCloseIcon();
-			closePreviewBtn.on("click", (e) -> {
+			closePreviewBtn.get(0).addEventListener("click", (e) -> {
 				e.stopPropagation();
-				setPreviewVisibility(!node.showPreview);
-			});
+				setPreviewVisibility(!info.preview.getVisible());
+			}, {capture: true});
 		}
 
 		refreshBox();
-		//editor.editor.line(element, width/2, HEADER_HEIGHT, width/2, 0, {display: "none"}).addClass("nodes-separator");
+		//editor.editorDisplay.line(element, width/2, HEADER_HEIGHT, width/2, 0, {display: "none"}).addClass("nodes-separator");
 	}
 
 	public function setPreviewVisibility(visible: Bool) {
-		nodeInstance.showPreview = visible;
-		refreshCloseIcon();
+		if (info.preview != null) {
+			info.preview.setVisible(visible);
+			refreshCloseIcon();
+		}
 	}
 
 	function refreshCloseIcon() {
 		if (closePreviewBtn == null)
 			return;
-		closePreviewBtn.find(".ico").toggleClass("ico-angle-down", !nodeInstance.showPreview);
-		closePreviewBtn.find(".ico").toggleClass("ico-angle-up", nodeInstance.showPreview);
+		var viz = info.preview.getVisible();
+		closePreviewBtn.find(".ico").toggleClass("ico-angle-down", !viz);
+		closePreviewBtn.find(".ico").toggleClass("ico-angle-up", viz);
 	}
 
-	public function addInput(editor : Graph, name : String, valueDefault : String = null, type : hrt.shgraph.ShaderGraph.SgType) {
-		var node = editor.editor.group(element).addClass("input-node-group");
-		var nodeHeight = HEADER_HEIGHT + NODE_MARGIN * (inputs.length+1) + NODE_RADIUS * inputs.length;
-		var style = {fill : ""}
-		style.fill = getTypeColor(type);
+	public function addInput(editor : GraphEditor, name : String, valueDefault : String = null, color : Int) {
+		var node = editor.editorDisplay.group(element).addClass("input-node-group");
+		var nodeHeight = getNodeHeight(inputs.length);
+		var style = {fill : '#${StringTools.hex(color, 6)}'};
 
-		var nodeCircle = editor.editor.circle(node, 0, nodeHeight, NODE_RADIUS, style).addClass("node input-node");
+		var nodeCircle = editor.editorDisplay.circle(node, 0, nodeHeight, NODE_RADIUS, style).addClass("node input-node");
 
 		var nameWidth = 0.0;
-		if (name.length > 0) {
-			var inputName = editor.editor.text(node, NODE_TITLE_PADDING, nodeHeight + 4, name).addClass("title-node");
+		if (name.length > 0 && name != "input") {
+			var inputName = editor.editorDisplay.text(node, NODE_TITLE_PADDING, nodeHeight + 4, name).addClass("title-node");
 			var domName : js.html.svg.GraphicsElement = cast inputName.get()[0];
 			nameWidth = domName.getBBox().width;
 		}
 		if (valueDefault != null) {
 			var widthInput = width / 2 * 0.7;
-			var fObject = editor.editor.foreignObject(
+			var fObject = editor.editorDisplay.foreignObject(
 				node,
 				nameWidth + NODE_TITLE_PADDING + NODE_INPUT_PADDING,
 				nodeHeight - 9,
@@ -314,17 +323,16 @@ class Box {
 		}
 	}
 
-	public function addOutput(editor : Graph, name : String, ?type : hrt.shgraph.ShaderGraph.SgType) {
-		var node = editor.editor.group(element).addClass("output-node-group");
-		var nodeHeight = HEADER_HEIGHT + NODE_MARGIN * (outputs.length+1) + NODE_RADIUS * outputs.length;
-		var style = {fill : ""}
+	public function addOutput(editor : GraphEditor, name : String, color : Int) {
+		var node = editor.editorDisplay.group(element).addClass("output-node-group");
+		var nodeHeight = getNodeHeight(outputs.length);
+		var style = {fill : '#${StringTools.hex(color, 6)}'};
 
-		style.fill = getTypeColor(type);
 
-		var nodeCircle = editor.editor.circle(node, width, nodeHeight, NODE_RADIUS, style).addClass("node output-node");
+		var nodeCircle = editor.editorDisplay.circle(node, width, nodeHeight, NODE_RADIUS, style).addClass("node output-node");
 
 		if (name.length > 0 && name != "output")
-			editor.editor.text(node, width - NODE_TITLE_PADDING - (name.length * 6.75), nodeHeight + 4, name).addClass("title-node");
+			editor.editorDisplay.text(node, width - NODE_TITLE_PADDING, nodeHeight + 4, name).addClass("title-node").attr("text-anchor", "end");
 
 		outputs.push(nodeCircle);
 
@@ -332,16 +340,14 @@ class Box {
 		return node;
 	}
 
-	public function generateProperties(editor : Graph, config:  hide.Config) {
-		var props = nodeInstance.getHTML(this.width, config);
+	public function getNodeHeight(id: Int) {
+		return NODE_MARGIN * (id+2);
+	}
 
-		if (props.length == 0) return;
+	public function generateProperties(editor : GraphEditor) {
+		var props = node.getPropertiesHTML(this.width);
 
-		if (!hadToShowInputs && inputs.length <= 1 && outputs.length <= 1) {
-			element.find(".nodes").remove();
-			element.find(".input-node-group > .title-node").html("");
-			element.find(".output-node-group > .title-node").html("");
-		}
+		if (props.length == 0) return;
 
 		var children = propertiesGroup.children();
 		if (children.length > 0) {
@@ -351,16 +357,20 @@ class Box {
 		}
 
 		// create properties box
-		var bgParam = editor.editor.rect(propertiesGroup, 0, 0, this.width, 0).addClass("properties");
-		if (!hasHeader && color != null) bgParam.css("fill", color);
+		if (!collapseProperties()) {
+			editor.editorDisplay.line(propertiesGroup, 0, 0, this.width, 0).addClass("separator");
+		}
+
+		//var bgParam = editor.editorDisplay.rect(propertiesGroup, 0, 0, this.width, 0).addClass("properties");
+		//if (!hasHeader && color != null) bgParam.css("fill", color);
 		propsHeight = 0;
 
 		for (p in props) {
-			var prop = editor.editor.group(propertiesGroup).addClass("prop-group");
+			var prop = editor.editorDisplay.group(propertiesGroup).addClass("prop-group");
 			prop.attr("transform", 'translate(0, ${propsHeight})');
 
 			var propWidth = (p.width() > 0 ? p.width() : this.width);
-			var fObject = editor.editor.foreignObject(prop, (this.width - propWidth) / 2, 5, propWidth, p.height());
+			var fObject = editor.editorDisplay.foreignObject(prop, (this.width - propWidth) / 2, 5, propWidth, p.height());
 			p.appendTo(fObject);
 			propsHeight += Std.int(p.outerHeight()) + 1;
 		}
@@ -379,13 +389,14 @@ class Box {
 		var nodesHeight = getNodesHeight();
 		var height = getHeight();
 		element.find(".nodes").height(nodesHeight).width(width);
-		element.find(".outline").attr("height", height+2).width(width);
+		element.find(".background").attr("height", height).width(width);
+		element.find(".outline").attr("height", height+2).width(width+2);
 
 		if (hasHeader) {
 			element.find(".head-box").width(width);
 		}
 
-		if (comment != null) {
+		if (info.comment != null) {
 			var hB = halfResizeBorder;
 			var rB = resizeBorder;
 			element.find("#resizeBot").attr("x", hB).attr("y", height - hB).width(width - rB).height(rB);
@@ -400,24 +411,23 @@ class Box {
 		}
 
 		if (inputs.length >= 1 && outputs.length >= 1) {
-			element.find(".nodes-separator").attr("y2", HEADER_HEIGHT + nodesHeight);
+			element.find(".nodes-separator").attr("y2", nodesHeight);
 			element.find(".nodes-separator").show();
-		} else if (!hadToShowInputs) {
-			element.find(".nodes-separator").hide();
 		}
 
 		if (propertiesGroup != null) {
-			propertiesGroup.attr("transform", 'translate(0, ${HEADER_HEIGHT + nodesHeight})');
+			propertiesGroup.attr("transform", 'translate(0, ${collapseProperties() ? getNodeHeight(0) - 16 : nodesHeight})');
 			propertiesGroup.find(".properties").attr("height", propsHeight);
 		}
 
-		closePreviewBtn?.attr("y",HEADER_HEIGHT + nodesHeight + propsHeight - 16);
+		closePreviewBtn?.attr("y", getHeight() - 12);
 	}
 
+	public static var tmpPoint = new h2d.col.Point();
 	public function setPosition(x : Float, y : Float) {
+		element.attr({transform: 'translate(${x} ${y})'});
 		this.x = x;
 		this.y = y;
-		element.attr({transform: 'translate(${x} ${y})'});
 	}
 
 	public function setSelected(b : Bool) {
@@ -435,34 +445,42 @@ class Box {
 			element.find(".title-box").html(str);
 		}
 	}
-	public function getId() {
-		return this.nodeInstance.id;
-	}
+
 	public function getInstance() {
-		return this.nodeInstance;
-	}
-	public function getX() {
-		return this.x;
+		return this.node;
 	}
-	public function getY() {
-		return this.y;
-	}
-	public function getWidth() {
-		return this.width;
+
+	public function collapseProperties() {
+		return info.inputs.length <= 1 && info.outputs.length <= 1;
 	}
+
 	public function getNodesHeight() {
 		var maxNb = Std.int(Math.max(inputs.length, outputs.length));
-		if ((!hadToShowInputs && maxNb <= 1 && propsHeight > 0) || comment != null) {
+		if (info.comment != null) {
 			return 0;
 		}
-		return NODE_MARGIN * (maxNb+1) + NODE_RADIUS * maxNb;
+		return getNodeHeight(maxNb);
 	}
-	public function getHeight() {
-		if (comment != null) {
-			return comment.height;
+	public function getHeight() : Float {
+		if (info.comment != null) {
+			return height;
+		}
+		var nodeHeight = getNodesHeight();
+		if (collapseProperties()) {
+			return hxd.Math.max(nodeHeight, propsHeight);
 		}
-		return HEADER_HEIGHT + getNodesHeight() + propsHeight;
+		return nodeHeight + propsHeight;
 	}
+
+	inline public function getBounds() : {x: Float, y: Float, w: Float, h: Float} {
+		node.getPos(tmpPoint);
+		var x = tmpPoint.x;
+		var y = tmpPoint.y;
+		var w = this.width;
+		var h = getHeight();
+		return {x:x,y:y,w:w,h:h};
+	}
+
 	public function getElement() {
 		return element;
 	}

Datei-Diff unterdrückt, da er zu groß ist
+ 291 - 612
hide/view/shadereditor/ShaderEditor.hx


+ 459 - 0
hide/view/substanceeditor/SubstanceEditor.hx

@@ -0,0 +1,459 @@
+package hide.view.substanceeditor;
+
+import hrt.sbsgraph.SubstanceNode;
+import hide.view.GraphInterface;
+
+class SubstanceEditor extends hide.view.FileView implements GraphInterface.IGraphEditor {
+	public static var DEFAULT_PREVIEW_COLOR = 16716947;
+	public static var DEFAULT_PREVIEW_SIZE = 2048;
+
+	var graphEditor : hide.view.GraphEditor;
+	var substanceGraph : hrt.sbsgraph.SubstanceGraph;
+
+	// Preview
+	var previewScene : hide.comp.Scene;
+	var previewBitmap : h2d.Bitmap;
+	var previewShaderAlpha : GraphEditor.PreviewShaderAlpha;
+	var initializedPreviews : Map<h2d.Bitmap, Bool> = [];
+	var camController : hide.view.l3d.CameraController2D;
+
+	var generationRequested : Bool = true;
+	var previewRefreshRequested : Bool = true;
+
+	var selectedNodes : Array<SubstanceNode>;
+
+	override function onDisplay() {
+		super.onDisplay();
+		element.html("");
+		element.addClass("substance-editor");
+		substanceGraph = cast hide.Ide.inst.loadPrefab(state.path, null,  true);
+		previewShaderAlpha = new GraphEditor.PreviewShaderAlpha();
+
+		if (graphEditor != null)
+			graphEditor.remove();
+
+		graphEditor = new hide.view.GraphEditor(config, this, this.element);
+		graphEditor.onDisplay();
+
+		var rightPanel = new Element(
+			'<div id="right-panel">
+				<div class="group">
+					<div class="header">
+						<div class="icon ico ico-caret-down"></div>
+						<span class="title">Base parameters</span>
+					</div>
+					<div class="content" id="base-parameters"></div>
+				</div>
+				<div class="group">
+					<div class="header">
+						<div class="icon ico ico-caret-down"></div>
+						<span class="title">Specific parameters</span>
+					</div>
+					<div class="content" id="specific-parameters"></div>
+				</div>
+			</div>'
+		);
+
+		var baseParameters = rightPanel.find("#base-parameters");
+		var outputSize = new Element('
+		<h2 class="title">Output size</h2>
+		<div class="fields grid-3">
+			<label>Width</label>
+			<input type="number" id="output-width" title="Width of the output texture (in px)"/>
+			<label>Height</label>
+			<input type="number" id="output-height" title="Height of the output texture (in px)"/>
+		</div>');
+		outputSize.appendTo(baseParameters);
+
+		var outputFormat = new Element('
+		<h2 class="title">Output format</h2>
+		<div class="fields grid-3">
+			<label>Format</label>
+			<select name="formats" id="output-format"/>
+				${ [for (f in Type.allEnums(hxd.PixelFormat)) '<option value="${f.getIndex()}">${f.getName()}</option>'].join("")}
+			</select>
+		</div>');
+		outputFormat.appendTo(baseParameters);
+
+		function addResetButton(fieldEl : Element, fieldName : String) {
+			var resetEl = new Element('<div class="reset icon ico ico-ban" title="Reset value to default (graph global value)"></div>');
+			resetEl.insertAfter(fieldEl);
+
+			resetEl.on("click", function() {
+				Reflect.deleteField(this.selectedNodes[0].overrides, fieldName);
+				Reflect.setField(this.selectedNodes[0], fieldName, Reflect.field(this.substanceGraph, fieldName));
+				resetEl.css({ visibility : "hidden", "pointers-event" : "none"});
+				updateParameters(this.selectedNodes[0]);
+				generate();
+			});
+
+			if (this.selectedNodes == null || this.selectedNodes.length <= 0 || !Reflect.hasField(this.selectedNodes[0].overrides, fieldName)) {
+				resetEl.css({ visibility : "hidden", "pointers-event" : "none"});
+				return;
+			}
+		}
+
+		addResetButton(rightPanel.find("#output-width"), "outputWidth");
+		addResetButton(rightPanel.find("#output-height"), "outputHeight");
+		addResetButton(rightPanel.find("#output-format"), "outputFormat");
+
+		var headers = rightPanel.find(".header");
+		headers.on("click", function(e) {
+			var header = new Element(e.target).closest(".header");
+			var content = header.siblings(".content");
+
+			if (content.css("display") == "none") {
+				content.css({ display: "block" });
+				header.find(".icon").removeClass("ico-caret-right").addClass("ico-caret-down");
+			}
+			else {
+				content.css({ display: "none" });
+				header.find(".icon").removeClass("ico-caret-down").addClass("ico-caret-right");
+			}
+		});
+
+		rightPanel.find("#specific-parameters").prev(".header").css({ display : "none" });
+
+		rightPanel.appendTo(element);
+		updateParameters(null);
+
+		// Preview
+		if (previewScene != null)
+			previewScene.element.remove();
+
+		var texPreview = new Element(
+			'<div id="tex-preview">
+			</div>'
+		);
+
+		previewScene = new hide.comp.Scene(config, null, texPreview);
+		previewScene.onReady = onPreviewSceneReady;
+		previewScene.onUpdate = onPreviewSceneUpdate;
+
+		texPreview.appendTo(graphEditor.element);
+
+		var toolbar = new Element('<div class="hide-toolbar2"></div>').appendTo(texPreview);
+		var group = new Element('<div class="tb-group"></div>').appendTo(toolbar);
+		var menu = new Element('<div class="button2 transparent" title="More options"><div class="ico ico-navicon"></div></div>');
+		menu.appendTo(group);
+		menu.click((e) -> {
+			var menu = new hide.comp.ContextMenu([
+				{ label: "Center preview", click: centerPreviewCamera }
+			]);
+		});
+
+		graphEditor.onPreviewUpdate = onPreviewUpdate;
+		graphEditor.onNodePreviewUpdate = onNodePreviewUpdate;
+		graphEditor.onSelectionChanged = onSelectionChanged;
+	}
+
+	override function getDefaultContent() : haxe.io.Bytes {
+		var p = (new hrt.sbsgraph.SubstanceGraph(null, null)).serialize();
+		return haxe.io.Bytes.ofString(ide.toJSON(p));
+	}
+
+	override function save() {
+		var content = substanceGraph.saveToText();
+		currentSign = ide.makeSignature(content);
+		sys.io.File.saveContent(getPath(), content);
+		super.save();
+	}
+
+	public function onPreviewSceneReady() {
+		if (previewScene.s3d == null || previewScene.s2d == null)
+			throw "Preview scene not ready";
+
+		camController = new hide.view.l3d.CameraController2D(previewScene.s2d);
+
+		var tile = h2d.Tile.fromColor(SubstanceEditor.DEFAULT_PREVIEW_COLOR);
+		previewBitmap = new h2d.Bitmap(tile, previewScene.s2d);
+
+		centerPreviewCamera();
+	}
+
+	public function onPreviewSceneUpdate(dt: Float) {
+		if (!previewRefreshRequested)
+			return;
+
+		previewRefreshRequested = false;
+
+		var outputNodes = substanceGraph.getOutputNodes();
+		if (outputNodes == null || outputNodes.length <= 0)
+			return;
+
+		var outputs = substanceGraph.cachedOutputs.get(outputNodes[0].id);
+		if (outputs == null || outputs.length <= 0 || outputs[0] == null || previewBitmap == null)
+			return;
+
+		try {
+			var tex = Std.downcast(outputs[0], h3d.mat.Texture);
+			var pixels = tex.capturePixels();
+			var outputTexture = new h3d.mat.Texture(tex.width, tex.height, substanceGraph.outputFormat);
+			outputTexture.uploadPixels(pixels);
+			previewBitmap.tile = h2d.Tile.fromTexture(outputTexture);
+
+			centerPreviewCamera();
+		}
+		catch(e) Ide.inst.quickError("Can't create 2D preview");
+	}
+
+	public function onPreviewUpdate() : Bool {
+		checkGeneration();
+
+		@:privateAccess
+		{
+			var engine = graphEditor.previewsScene.engine;
+			var t = engine.getCurrentTarget();
+			graphEditor.previewsScene.s2d.ctx.globals.set("global.pixelSize", new h3d.Vector(2 / (t == null ? engine.width : t.width), 2 / (t == null ? engine.height : t.height)));
+		}
+
+		@:privateAccess
+		if (previewScene.s2d != null) {
+			previewScene.s2d.ctx.time = graphEditor.previewsScene.s2d.ctx.time;
+		}
+
+		return true;
+	}
+
+	public function onNodePreviewUpdate(node: IGraphNode, bitmap: h2d.Bitmap) @:privateAccess {
+		if (initializedPreviews.get(bitmap) == null) {
+			bitmap.addShader(previewShaderAlpha);
+			initializedPreviews.set(bitmap, true);
+		}
+
+		if (substanceGraph.cachedOutputs != null) {
+			var outputs = substanceGraph.cachedOutputs.get(node.id);
+
+			if (outputs == null || outputs.length <= 0)
+				return;
+
+			bitmap.tile = h2d.Tile.fromTexture(outputs[0]);
+		}
+	}
+
+	public function onSelectionChanged(selectedNodes: Array<IGraphNode>) {
+		this.selectedNodes = cast selectedNodes;
+
+		// If there's no selection, show graph editor parameters
+		if (this.selectedNodes.length == 0) {
+			updateParameters(null);
+
+			element.find("#specific-parameters").empty();
+			element.find("#specific-parameters").prev(".header").css({ display : "none" });
+			return;
+		}
+
+		// If there's only one node selected, show its parameters
+		if (this.selectedNodes.length == 1) {
+			updateParameters(this.selectedNodes[0]);
+
+			var el = this.selectedNodes[0].getSpecificParametersHTML();
+			if (el != null) {
+				element.find("#specific-parameters").append(el);
+				element.find("#specific-parameters").prev(".header").css({ display : "flex" });
+			}
+
+			return;
+		}
+	}
+
+	public function getNodes():Iterator<IGraphNode> {
+		return substanceGraph.nodes.iterator();
+	}
+
+	public function getEdges():Iterator<Edge> {
+		var edges : Array<Edge> = [];
+		for (id => node in substanceGraph.nodes) {
+			for (inputId => connection in node.connections) {
+				if (connection != null) {
+					edges.push(
+						{
+							nodeFromId: connection.from.id,
+							outputFromId: connection.outputId,
+							nodeToId: id,
+							inputToId: inputId,
+						});
+				}
+			}
+		}
+		return edges.iterator();
+	}
+
+	public function getAddNodesMenu():Array<AddNodeMenuEntry> {
+		var entries : Array<AddNodeMenuEntry> = [];
+		var id = 0;
+		for (i => node in hrt.sbsgraph.SubstanceNode.registeredNodes) {
+			var metas = haxe.rtti.Meta.getType(node);
+			if (metas.group == null) {
+				continue;
+			}
+
+			var group = metas.group != null ? metas.group[0] : "Other";
+			var name = metas.name != null ? metas.name[0] : "unknown";
+			var description = metas.description != null ? metas.description[0] : "";
+
+			entries.push(
+				{
+					name: name,
+					group: group,
+					description: description,
+					onConstructNode: () -> {
+						@:privateAccess var id = hrt.sbsgraph.SubstanceGraph.CURRENT_NODE_ID++;
+						var inst = std.Type.createInstance(node, []);
+						inst.id = id;
+						return inst;
+					},
+				}
+			);
+		}
+
+		return entries;
+	}
+
+	public function addNode(node: IGraphNode) {
+		substanceGraph.addNode(cast node);
+		generate();
+	}
+
+	public function removeNode(id:Int) {
+		substanceGraph.removeNode(id);
+		generate();
+	}
+
+	public function serializeNode(node: IGraphNode):Dynamic {
+		return (cast node:SubstanceNode).serializeToDynamic();
+	}
+
+	public function unserializeNode(data:Dynamic, newId:Bool): IGraphNode {
+		var node = SubstanceNode.createFromDynamic(data, substanceGraph);
+		if (newId) {
+			@:privateAccess var newId = hrt.sbsgraph.SubstanceGraph.CURRENT_NODE_ID++;
+			node.id = newId;
+		}
+		return node;
+	}
+
+	public function createCommentNode():Null<IGraphNode> {
+		var node = new hrt.sbsgraph.nodes.Comment();
+		node.comment = "Comment";
+		@:privateAccess var newId = hrt.sbsgraph.SubstanceGraph.CURRENT_NODE_ID++;
+		node.id = newId;
+		return node;
+	}
+
+	public function canAddEdge(edge: Edge):Bool {
+		return substanceGraph.canAddEdge({ outputNodeId: edge.nodeFromId, outputId: edge.outputFromId, inputNodeId: edge.nodeToId, inputId: edge.inputToId });
+	}
+
+	public function addEdge(edge: Edge) {
+		var input = substanceGraph.nodes.get(edge.nodeToId);
+		input.connections[edge.inputToId] = {from: substanceGraph.nodes.get(edge.nodeFromId), outputId: edge.outputFromId};
+		generate();
+	}
+
+	public function removeEdge(nodeToId:Int, inputToId:Int) {
+		var input = substanceGraph.nodes.get(nodeToId);
+		input.connections[inputToId] = null;
+		generate();
+	}
+
+	public function getUndo() : hide.ui.UndoHistory {
+		return undo;
+	}
+
+	public function generate() {
+		generationRequested = true;
+	}
+
+	public function refreshPreview() {
+		previewRefreshRequested = true;
+	}
+
+
+	function checkGeneration() {
+		if (!generationRequested)
+			return;
+
+		generationRequested = false;
+		substanceGraph.generate();
+
+		refreshPreview();
+	}
+
+	function centerPreviewCamera() {
+		var tile = previewBitmap.tile;
+
+		var ratio = tile.width > tile.height ? tile.width / tile.height : tile.width / tile.height;
+		previewBitmap.width = tile.width > tile.height ? SubstanceEditor.DEFAULT_PREVIEW_SIZE : SubstanceEditor.DEFAULT_PREVIEW_SIZE * ratio;
+		previewBitmap.height = tile.height > tile.width ? SubstanceEditor.DEFAULT_PREVIEW_SIZE  : SubstanceEditor.DEFAULT_PREVIEW_SIZE * ratio;
+
+		@:privateAccess camController.targetPos.set(previewBitmap.height / 2, previewBitmap.width / 2, (1 / previewBitmap.width) * 300);
+	}
+
+	function updateParameters(node: SubstanceNode) {
+		function updateResetButtonVisibility(el : Element, fieldName : String) {
+			var resetEl = el.next(".reset");
+			if (node == null)
+				resetEl.css({ visibility : "hidden", "pointers-event" : "none" });
+			else {
+				if (Reflect.hasField(node.overrides, fieldName))
+					resetEl.css({ visibility : "visible", "pointers-event" : "auto" });
+				else
+					resetEl.css({ visibility : "hidden", "pointers-event" : "none" });
+			}
+		}
+
+		var outputWidth = element.find("#output-width");
+		outputWidth.val(node == null ? substanceGraph.outputWidth : node.outputWidth);
+		outputWidth.off();
+		outputWidth.on("change", function() {
+			var v = Std.parseInt(outputWidth.val());
+			if (node == null) {
+				substanceGraph.outputWidth = v;
+			}
+			else {
+				node.outputWidth = v;
+				Reflect.setField(node.overrides, "outputWidth", v);
+			}
+			updateResetButtonVisibility(outputWidth, "outputWidth");
+			generate();
+		});
+		updateResetButtonVisibility(outputWidth, "outputWidth");
+
+		var outputHeight = element.find("#output-height");
+		outputHeight.val(node == null ? substanceGraph.outputHeight : node.outputHeight);
+		outputHeight.off();
+		outputHeight.on("change", function() {
+			var v = Std.parseInt(outputHeight.val());
+			if (node == null) {
+				substanceGraph.outputHeight = v;
+			}
+			else {
+				node.outputHeight = v;
+				Reflect.setField(node.overrides, "outputHeight", v);
+			}
+			updateResetButtonVisibility(outputHeight, "outputHeight");
+			generate();
+		});
+		updateResetButtonVisibility(outputHeight, "outputHeight");
+
+		var outputFormat = element.find("#output-format");
+		outputFormat.val(node == null ? substanceGraph.outputFormat.getIndex() : node.outputFormat.getIndex());
+		outputFormat.off();
+		outputFormat.on("change", function() {
+			var v = hxd.PixelFormat.createByIndex(Std.parseInt(outputFormat.val()));
+			if (node == null) {
+				substanceGraph.outputFormat = v;
+			}
+			else {
+				node.outputFormat = v;
+				Reflect.setField(node.overrides, "outputFormat", v);
+			}
+			updateResetButtonVisibility(outputFormat, "outputFormat");
+			generate();
+		});
+		updateResetButtonVisibility(outputFormat, "outputFormat");
+	}
+
+	static var _ = FileTree.registerExtension(SubstanceEditor, ["sbsgraph"], { icon : "scribd", createNew: "Substance Graph" });
+}

+ 3 - 0
hrt/prefab/Light.hx

@@ -78,6 +78,7 @@ class Light extends Object3D {
 	@:s public var firstCascadeSize : Float = 10;
 	@:s public var minPixelRatio : Float = 0.5;
 	@:s public var castingMaxDist : Float = 0.0;
+	@:s public var transitionFraction : Float = 0.15;
 	@:s public var params : Array<CascadeParams> = [];
 	@:s public var debugShader : Bool = false;
 	@:s public var highPrecision : Bool = false;
@@ -217,6 +218,7 @@ class Light extends Object3D {
 						cs.minPixelRatio = minPixelRatio * 0.01;
 						cs.debug = debugDisplay;
 						cs.castingMaxDist = castingMaxDist;
+						cs.transitionFraction = transitionFraction;
 						cs.debugShader = debugShader;
 						params.resize(cascadeNbr);
 						for ( i in 0...params.length )
@@ -637,6 +639,7 @@ class Light extends Object3D {
 					<dt>First cascade size</dt><dd><input type="range" field="firstCascadeSize" min="5" max="100"/></dd>
 					<dt>Range power</dt><dd><input type="range" field="cascadePow" min="0.1" max="10"/></dd>
 					<dt>Casting max dist</dt><dd><input type="range" field="castingMaxDist" min="-1" max="1000"/></dd>
+					<dt>Transition fraction</dt><dd><input type="range" field="transitionFraction" min="0.0" max="0.3"/></dd>
 					<dl>
 						<ul id="params"></ul>
 					</dl>

+ 67 - 30
hrt/prefab/Material.hx

@@ -115,12 +115,61 @@ class Material extends Prefab {
 			mat.mainPass.setPassName(mainPassName);
 	}
 
+	override function makeInstance() {
+		#if editor
+		if (previewSphere != null) {
+			previewSphere.remove();
+			previewSphere = null;
+		}
+
+		var isMatLib = shared.editor != null && shared.parentPrefab == null;
+		if (isMatLib) {
+			var flat = getRoot().flatten(Prefab);
+			for (f in flat) {
+				var cl = Type.getClass(f);
+				if (cl != hrt.prefab.Object3D && cl != hrt.prefab.Prefab && Std.downcast(f, hrt.prefab.Material) == null) {
+					isMatLib = false;
+					break;
+				}
+			}
+		}
+
+
+		if (isMatLib) {
+			var root = shared.root3d;
+
+			var sphere = new h3d.prim.Sphere(1., 64, 48);
+			sphere.addUVs();
+			sphere.addNormals();
+			sphere.addTangents();
+			sphere.colors = sphere.points;
+
+			var m = new h3d.scene.Mesh(sphere);
+			m.name = "previewSphereObjName";
+			@:privateAccess m.material.name = "previewMat";
+			previewSphere = m;
+			root.addChild(previewSphere);
+			var flat = getRoot(false).flatten(Material);
+			@:privateAccess var pos = flat.indexOf(this);
+			previewSphere.x = ( pos - 1) * 5.0;
+		}
+		#end
+
+		updateInstance();
+	}
+
+	#if editor
+	override function findFirstLocal3d() {
+		return previewSphere ?? super.findFirstLocal3d();
+	}
+	#end
+
 	override function updateInstance(?propName ) {
 		var local3d = findFirstLocal3d();
 		if( local3d == null )
 			return;
 
-		var mats = getMaterials();
+		var mats = getMaterials(true);
 
 		if (this.refMatLib != null && this.refMatLib != "") {
 			// We want to save some infos to reapply them after loading datas from the choosen mat
@@ -153,31 +202,6 @@ class Material extends Prefab {
 
 		var props = renderProps();
 
-		#if editor
-		if ( mats == null || mats.length == 0 ) {
-			if (previewSphere != null)
-				previewSphere.remove();
-
-			var parent = findFirstLocal3d();
-
-			var sphere = new h3d.prim.Sphere(1., 64, 48);
-			sphere.addUVs();
-			sphere.addNormals();
-			sphere.addTangents();
-			sphere.colors = sphere.points;
-
-			var m = new h3d.scene.Mesh(sphere);
-			m.name = "previewSphereObjName";
-			@:privateAccess m.material.name = "previewMat";
-			previewSphere = m;
-			parent.addChild(previewSphere);
-
-			var previewCount = findFirstLocal3d().getScene().findAll(o -> o.name == "previewSphereObjName" ? true : null).length;
-			previewSphere.x = ( previewCount - 1) * 5.0;
-
-			mats = [ @:privateAccess m.material ];
-		}
-		#end
 		function loadTextureCb( path : String ) : h3d.mat.Texture {
 			return shared.loadTexture(path, false);
 		}
@@ -185,10 +209,6 @@ class Material extends Prefab {
 			update(m, props, loadTextureCb);
 	}
 
-	override function makeInstance() {
-		updateInstance();
-	}
-
 	function applyOverrides() {
 		if (this.overrides == null || this.overrides.length == 0)
 			return;
@@ -225,6 +245,23 @@ class Material extends Prefab {
 	}
 
 	#if editor
+	override function editorRemoveInstance() : Bool {
+		if (previewSphere != null) {
+			previewSphere.remove();
+			return true;
+		}
+		return false;
+	}
+
+	override function makeInteractive() : hxd.SceneEvents.Interactive {
+		if (previewSphere != null) {
+			var col = new h3d.col.Sphere(0,0,0,1.);
+			var int = new h3d.scene.Interactive(col, previewSphere);
+			return int;
+		}
+		return null;
+	}
+
 	override function edit( ctx : hide.prefab.EditContext ) {
 		super.edit(ctx);
 

+ 7 - 7
hrt/prefab/Prefab.hx

@@ -164,6 +164,7 @@ class Prefab {
 		if( sh.currentPath == null ) sh.currentPath = shared.currentPath;
 		#if editor
 		sh.editor = this.shared.editor;
+		sh.scene = this.shared.scene;
 		#end
 		return this.clone(sh).make(sh);
 	}
@@ -184,6 +185,7 @@ class Prefab {
 				sh = new hrt.prefab.ContextShared(this.shared.currentPath, true);
 				#if editor
 				sh.editor = shared.editor;
+				sh.scene = shared.scene;
 				#end
 			}
 		}
@@ -587,18 +589,16 @@ class Prefab {
 	public function edit(editContext : hide.prefab.EditContext) {
 	}
 
-	public function setEditor(sceneEditor: hide.comp.SceneEditor) {
-		if (sceneEditor == null)
-			throw "No editor for setEditor";
-
+	public function setEditor(sceneEditor: hide.comp.SceneEditor, scene: hide.comp.Scene) {
 		shared.editor = sceneEditor;
+		shared.scene = scene;
 
-		setEditorChildren(sceneEditor);
+		setEditorChildren(sceneEditor, scene);
 	}
 
-	function setEditorChildren(sceneEditor: hide.comp.SceneEditor) {
+	function setEditorChildren(sceneEditor: hide.comp.SceneEditor, scene: hide.comp.Scene) {
 		for (c in children) {
-			c.setEditorChildren(sceneEditor);
+			c.setEditorChildren(sceneEditor, scene);
 		}
 	}
 

+ 4 - 3
hrt/prefab/Reference.hx

@@ -33,11 +33,11 @@ class Reference extends Object3D {
 	}
 
 	#if editor
-	override function setEditorChildren(sceneEditor:hide.comp.SceneEditor) {
-		super.setEditorChildren(sceneEditor);
+	override function setEditorChildren(sceneEditor:hide.comp.SceneEditor, scene: hide.comp.Scene) {
+		super.setEditorChildren(sceneEditor, scene);
 
 		if (refInstance != null) {
-			refInstance.setEditor(sceneEditor);
+			refInstance.setEditor(sceneEditor, scene);
 		}
 	}
 	#end
@@ -92,6 +92,7 @@ class Reference extends Object3D {
 
 		#if editor
 		sh.editor = this.shared.editor;
+		sh.scene = this.shared.scene;
 		#end
 		sh.parentPrefab = this;
 		sh.customMake = this.shared.customMake;

+ 1 - 2
hrt/prefab/Shader.hx

@@ -253,8 +253,7 @@ class Shader extends Prefab {
 		return {
 			icon : "cog",
 			name : name,
-			fileSource : cl == hrt.prefab.DynamicShader ? ["hx"] : null,
-			allowParent : function(p) return p.to(Object2D) != null || p.to(Object3D) != null || p.to(Material) != null
+			fileSource : cl == hrt.prefab.DynamicShader ? ["hx"] : null
 		};
 	}
 

+ 1 - 1
hrt/prefab/fx/Emitter.hx

@@ -571,7 +571,7 @@ class EmitterObject extends h3d.scene.Object {
 			var empty3d = new h3d.scene.Object();
 			var clone = particleTemplate.clone(new hrt.prefab.ContextShared(empty3d));
 			#if editor
-			clone.setEditor(prefab.shared.editor);
+			clone.setEditor(prefab.shared.editor, prefab.shared.scene);
 			#end
 			@:privateAccess clone.makeInstance();
 			var loc3d = Object3D.getLocal3d(clone);

+ 1 - 1
hrt/prefab/l3d/Instance.hx

@@ -29,7 +29,7 @@ class Instance extends Object3D {
 						sh.parentPrefab = this;
 						sh.customMake = this.shared.customMake;
 						#if editor
-						ref.setEditor(shared.editor);
+						ref.setEditor(shared.editor, shared.scene);
 						#end
 
 						instance = ref.make(sh);

+ 2 - 2
hrt/prefab/rfx/ScreenShaderGraph.hx

@@ -121,7 +121,7 @@ class ScreenShaderGraph extends RendererFX {
 					val = new h3d.Vector();
 			case TSampler(_):
 				if( val != null )
-					val = hxd.res.Loader.currentInstance.load(val).toTexture();
+					val = hrt.impl.TextureType.Utils.getTextureFromValue(val);//hxd.res.Loader.currentInstance.load(val).toTexture();
 				else {
 					var childNoise = getOpt(hrt.prefab.l2d.NoiseGenerator, v.name);
 					if(childNoise != null)
@@ -184,7 +184,7 @@ class ScreenShaderGraph extends RendererFX {
 		case TBool:
 			PBool;
 		case TSampler(_):
-			PTexturePath;
+			PTexture;
 		case TVec(n, VFloat):
 			PVec(n);
 		default:

+ 215 - 0
hrt/sbsgraph/Macros.hx

@@ -0,0 +1,215 @@
+package hrt.sbsgraph;
+
+import haxe.macro.Context;
+import haxe.macro.Expr;
+import hxsl.Ast;
+using hxsl.Ast;
+using haxe.macro.Tools;
+
+class Macros {
+	#if macro
+	// static function buildNode() {
+	// 	var fields = Context.getBuildFields();
+	// 	for (f in fields) {
+	// 		if (f.name == "SRC") {
+	// 			switch (f.kind) {
+	// 				case FVar(_, expr) if (expr != null):
+	// 					var pos = expr.pos;
+	// 					if( !Lambda.has(f.access, AStatic) ) f.access.push(AStatic);
+	// 					Context.getLocalClass().get().meta.add(":src", [expr], pos);
+	// 					try {
+	// 						var c = Context.getLocalClass();
+
+	// 						// function map(e: haxe.macro.Expr) {
+	// 						// 	switch(e) {
+	// 						// 		case EMeta("sginput", args, e):
+	// 						// 			trace("sginput");
+	// 						// 	}
+	// 						// }
+
+	// 						// expr.map()
+
+	// 						var varmap : Map<String, hrt.shgraph.SgHxslVar> = [];
+
+	// 						function iter(e: haxe.macro.Expr) : Void {
+	// 							switch(e.expr) {
+	// 								case EMeta(meta, subexpr):
+	// 									switch (meta.name) {
+	// 										case "sginput":
+	// 											var defValue : hrt.shgraph.SgHxslVar.ShaderDefInput = null;
+	// 											if (meta.params != null && meta.params.length > 0) {
+	// 												switch (meta.params[0].expr) {
+	// 													case EConst(v):
+	// 														switch(v) {
+	// 															case CIdent(name):
+	// 																defValue = Var(name);
+	// 															case CString(val):
+	// 																defValue = Var(val);
+	// 															case CFloat(val), CInt(val):
+	// 																defValue = Const(Std.parseFloat(val));
+	// 															default:
+	// 																throw "sginput default param must be an identifier or a integer";
+	// 														}
+	// 													default:
+	// 														trace(meta.params[0].expr);
+	// 														throw "sginput default param must be a constant value";
+	// 												}
+	// 											}
+
+	// 											switch(subexpr.expr) {
+	// 												case EVars(vars):
+	// 													for (v in vars) {
+	// 															var isDynamic = false;
+	// 															switch (v.type) {
+	// 																case TPath(p) if (p.name == "Dynamic"):
+	// 																	isDynamic = true;
+	// 																	p.name = "Vec4"; // Convert dynamic value back to vec4 as a hack
+	// 																default:
+	// 															}
+	// 															varmap.set(v.name, SgInput(isDynamic, defValue));
+	// 														}
+	// 														e.expr = subexpr.expr;
+	// 												default:
+	// 													throw "sginput must be used with variables only";
+	// 											}
+	// 										case "sgoutput":
+	// 											switch(subexpr.expr) {
+	// 												case EVars(vars):
+	// 													for (v in vars) {
+	// 														var isDynamic = false;
+	// 														switch (v.type) {
+	// 															case TPath(p) if (p.name == "Dynamic"):
+	// 																isDynamic = true;
+	// 																p.name = "Vec4"; // Convert dynamic value back to vec4 as a hack
+	// 															default:
+	// 														}
+
+	// 														varmap.set(v.name, SgOutput(isDynamic));
+	// 													}
+	// 													e.expr = subexpr.expr;
+	// 												default:
+	// 													throw "sgoutput must be used with variables only";
+	// 											}
+	// 										case "sgconst":
+	// 											switch(subexpr.expr) {
+	// 												case EVars(vars):
+	// 													for (v in vars) {
+	// 														varmap.set(v.name, SgConst);
+	// 													}
+	// 													e.expr = subexpr.expr;
+	// 												default:
+	// 													throw "sgconst must be used with variables only";
+	// 											}
+	// 										default:
+	// 									}
+	// 								default:
+	// 							}
+	// 						}
+
+	// 						expr.iter(iter);
+	// 						var shader = new hxsl.MacroParser().parseExpr(expr);
+	// 						f.kind = FVar(null, macro @:pos(pos) $v{shader});
+	// 						var name = Std.string(c);
+
+	// 						var check = new hxsl.Checker();
+	// 						check.warning = function(msg,pos) {
+	// 							haxe.macro.Context.warning(msg, pos);
+	// 						};
+
+	// 						check.loadShader = loadShader;
+
+	// 						var shader = check.check(name, shader);
+	// 						//trace(shader);
+	// 						//Printer.check(shader);
+	// 						var serializer = new hxsl.Serializer();
+	// 						var str = Context.defined("display") ? "" : serializer.serialize(shader);
+	// 						f.kind = FVar(null, { expr : EConst(CString(str)), pos : pos } );
+	// 						f.meta.push({
+	// 							name : ":keep",
+	// 							pos : pos,
+	// 						});
+
+	// 						function makeField(name: String, arr: Array<String>) : Field
+	// 						{
+	// 							return {
+	// 								name: name,
+	// 								access: [APublic, AStatic],
+	// 								kind: FVar(macro : Array<String>, macro $v{arr}),
+	// 								pos: f.pos,
+	// 								meta: [{
+	// 										name : ":keep",
+	// 										pos : pos,}
+	// 								],
+	// 							};
+	// 						}
+
+	// 						var finalMap : Map<Int, hrt.shgraph.SgHxslVar> = [];
+
+	// 						for (v in shader.vars) {
+	// 							var info = varmap.get(v.name);
+	// 							if (info != null) {
+	// 								var serId = @:privateAccess serializer.idMap.get(v.id);
+	// 								finalMap.set(serId, info);
+	// 							}
+	// 						}
+
+	// 						fields.push(
+	// 						{
+	// 							name: "_variablesInfos",
+	// 							access: [APublic, AStatic],
+	// 							kind: FVar(macro : Map<Int, hrt.shgraph.SgHxslVar>, macro $v{finalMap}),
+	// 							pos: f.pos,
+	// 							meta: [{
+	// 									name : ":keep",
+	// 									pos : pos,}
+	// 							],
+	// 						}
+	// 						);
+
+
+	// 					} catch( e : hxsl.Ast.Error ) {
+	// 						fields.remove(f);
+	// 						Context.error(e.msg, e.pos);
+	// 					}
+	// 				default:
+	// 			}
+	// 		}
+	// 	}
+	// 	return fields;
+	// }
+
+	// static function loadShader( path : String ) {
+	// 	var m = Context.follow(Context.getType(path));
+	// 	switch( m ) {
+	// 	case TInst(c, _):
+	// 		var c = c.get();
+	// 		for( m in c.meta.get() )
+	// 			if( m.name == ":src" )
+	// 				return new hxsl.MacroParser().parseExpr(m.params[0]);
+	// 	default:
+	// 	}
+	// 	throw path + " is not a shader";
+	// 	return null;
+	// }
+
+	static function autoRegisterNode() {
+		var fields = Context.getBuildFields();
+
+		var thisClass = Context.getLocalClass();
+		var cl = thisClass.get();
+		var clPath = cl.pack.copy();
+		clPath.push(cl.name);
+
+		#if editor
+		fields.push({
+			name: "_",
+			access: [Access.AStatic],
+			kind: FieldType.FVar(macro:Bool, macro SubstanceNode.register($v{cl.name}, ${clPath.toFieldExpr()})),
+			pos: Context.currentPos(),
+		});
+		#end
+
+		return fields;
+	}
+	#end
+}

+ 251 - 0
hrt/sbsgraph/SubstanceGraph.hx

@@ -0,0 +1,251 @@
+package hrt.sbsgraph;
+
+typedef Edge = {
+	?inputNodeId : Int,
+	?nameInput : String, // Fallback if name has changed
+	?inputId : Int,
+	?outputNodeId : Int,
+	?nameOutput : String, // Fallback if name has changed
+	?outputId : Int,
+};
+
+typedef Connection = {
+	from : SubstanceNode,
+	outputId : Int,
+};
+
+class SubstanceGraph extends hrt.prefab.Prefab {
+	public static var CURRENT_NODE_ID = 0;
+
+	public var cachedOutputs : Map<Int, Array<h3d.mat.Texture>> = [];
+	public var nodes : Map<Int, SubstanceNode> = [];
+
+	// Base parameters that can be overrided by nodes
+	@:s public var outputHeight = 256;
+	@:s public var outputWidth = 256;
+	@:s public var outputFormat = hxd.PixelFormat.RGBA;
+
+	override function save() {
+		var json = super.save();
+		Reflect.setField(json, "graph", saveToDynamic());
+		return json;
+	}
+
+	override function load(json : Dynamic) : Void {
+		super.load(json);
+
+		nodes = [];
+		CURRENT_NODE_ID = 0;
+
+		var graphJson = Reflect.getProperty(json, "graph");
+
+		var nodesJson : Array<Dynamic> = Reflect.getProperty(graphJson, "nodes");
+		for (n in nodesJson) {
+			var node = SubstanceNode.createFromDynamic(n, this);
+			this.nodes.set(node.id, node);
+			CURRENT_NODE_ID = hxd.Math.imax(CURRENT_NODE_ID, node.id+1);
+		}
+
+		var edgesJson : Array<Dynamic> = Reflect.getProperty(graphJson, "edges");
+		for (e in edgesJson) {
+			addEdge(e);
+		}
+	}
+
+	override function copy(other: hrt.prefab.Prefab) : Void {
+		throw "Substance graph is not meant to be put in a prefab tree. Use a dynamic shader that references this shadergraph instead";
+	}
+
+	public function saveToDynamic() : Dynamic {
+		var edgesJson : Array<Edge> = [];
+		for (n in nodes) {
+			for (inputId => connection in n.connections) {
+				if (connection == null) continue;
+				var outputId = connection.outputId;
+				edgesJson.push({ outputNodeId: connection.from.id, nameOutput: connection.from.getOutputs()[outputId].name, inputNodeId: n.id, nameInput: n.getInputs()[inputId].name, inputId: inputId, outputId: outputId });
+			}
+		}
+
+		var json = {
+			nodes: [
+				for (n in nodes) n.serializeToDynamic(),
+				],
+				edges: edgesJson
+			};
+
+		return json;
+	}
+
+	public function saveToText() : String {
+		return haxe.Json.stringify(save(), "\t");
+	}
+
+
+	public function addEdge(edge : Edge) {
+		var node = this.nodes.get(edge.inputNodeId);
+		var output = this.nodes.get(edge.outputNodeId);
+
+		var inputs = node.getInputs();
+		var outputs = output.getOutputs();
+
+		var outputId = edge.outputId;
+		var inputId = edge.inputId;
+
+		{
+			// Check if there is an output with that id and if it has the same name
+			// else try to find the id of another output with the same name
+			var output = outputs[outputId];
+			if (output == null || output.name != edge.nameOutput) {
+				for (id => o in outputs) {
+					if (o.name == edge.nameOutput) {
+						outputId = id;
+						break;
+					}
+				}
+			};
+		}
+
+		{
+			var input = inputs[inputId];
+			if (input == null || input.name != edge.nameInput) {
+				for (id => i in inputs) {
+					if (i.name == edge.nameInput) {
+						inputId = id;
+						break;
+					}
+				}
+			}
+		}
+
+		node.connections[inputId] = { from: output, outputId: outputId };
+
+		return true;
+	}
+
+	public function removeEdge(idNode : Int, inputId : Int, update : Bool = true) {
+		var node = this.nodes.get(idNode);
+		if (node.connections[inputId] == null) return;
+
+		node.connections[inputId] = null;
+	}
+
+	public function hasCycle() {
+		var visited : Array<Bool> = [for (n in nodes) false];
+		var recStack : Array<Bool> = [for (n in nodes) false];
+
+		function hasCycleUtil(current : Int, visited : Array<Bool>, recStack: Array<Bool>) {
+			if (recStack[current])
+				return true;
+
+			if (visited[current])
+				return false;
+
+			visited[current] = true;
+			recStack[current] = true;
+
+			var children: Array<Int> = [];
+			for (c in nodes[current].connections) {
+				for (idx => n in nodes) {
+					if (n == c.from)
+						children.push(idx);
+				}
+			}
+
+			for (idx in children) {
+				if (hasCycleUtil(idx, visited, recStack))
+					return true;
+			}
+
+			recStack[current] = false;
+			return false;
+		}
+
+		for (idx => n in nodes)
+			if (hasCycleUtil(idx, visited, recStack))
+				return true;
+
+		return false;
+	}
+
+	public function canAddEdge(edge : Edge) {
+		var node = this.nodes.get(edge.inputNodeId);
+		var output = this.nodes.get(edge.outputNodeId);
+
+		var inputs = node.getInputs();
+		var outputs = output.getOutputs();
+
+		var inputType = inputs[edge.inputId].type;
+		var outputType = outputs[edge.outputId].type;
+
+		// Temp add edge to check for cycle, remove it after
+		var prev = node.connections[edge.inputId];
+		node.connections[edge.inputId] = {from: output, outputId: edge.outputId};
+		var res = hasCycle();
+		node.connections[edge.inputId] = prev;
+
+		if(res)
+			return false;
+
+		return true;
+	}
+
+	public function addNode(sbsNode : SubstanceNode) {
+		this.nodes.set(sbsNode.id, sbsNode);
+	}
+
+	public function removeNode(idNode : Int) {
+		this.nodes.remove(idNode);
+	}
+
+	public function getOutputNodes() : Array<hrt.sbsgraph.nodes.SubstanceOutput> {
+		var outputNodes : Array<hrt.sbsgraph.nodes.SubstanceOutput> = [];
+		for (n in nodes) {
+			if (Std.downcast(n, hrt.sbsgraph.nodes.SubstanceOutput) != null)
+				outputNodes.push(cast n);
+		}
+
+		return outputNodes;
+	}
+
+
+	public function generate() : Map<String, h3d.mat.Texture> {
+		var engine = h3d.Engine.getCurrent();
+		if (engine == null)
+			return null;
+
+		cachedOutputs = [];
+
+		function generateNode(node : SubstanceNode) {
+			for (c in node.connections) {
+				if (c != null && cachedOutputs.get(c.from.id) == null)
+					generateNode(c.from);
+			}
+
+			// Apply parameters before generation
+			node.outputFormat = outputFormat;
+			node.outputHeight = outputHeight;
+			node.outputWidth = outputWidth;
+
+			for (f in Reflect.fields(node.overrides))
+				Reflect.setField(node, f, Reflect.field(node.overrides, f));
+
+			var outputs = node.apply(cachedOutputs);
+			cachedOutputs.set(node.id, outputs);
+		}
+
+		for (n in nodes) {
+			if (cachedOutputs.get(n.id) != null)
+				continue;
+
+			generateNode(n);
+		}
+
+		var outputs : Map<String, h3d.mat.Texture> = [];
+		for (o in getOutputNodes())
+			outputs.set(o.label, cachedOutputs.get(o.id)[0]);
+
+		return outputs;
+	}
+
+	static var _ = hrt.prefab.Prefab.register("sbsgraph", SubstanceGraph, "sbsgraph");
+}

+ 217 - 0
hrt/sbsgraph/SubstanceNode.hx

@@ -0,0 +1,217 @@
+package hrt.sbsgraph;
+
+import Type as HaxeType;
+import hrt.sbsgraph.SubstanceGraph;
+#if editor
+import hide.view.GraphInterface;
+#end
+
+using Lambda;
+
+typedef AliasInfo = { ?nameSearch: String, ?nameOverride : String, ?description : String, ?args : Array<Dynamic>, ?group: String };
+
+@:autoBuild(hrt.sbsgraph.Macros.autoRegisterNode())
+@:keepSub
+@:keep
+class SubstanceNode
+#if editor
+implements hide.view.GraphInterface.IGraphNode
+#end
+{
+	public static var registeredNodes = new Map<String, Class<SubstanceNode>>();
+
+	public var id : Int;
+	public var connections : Array<SubstanceGraph.Connection> = [];
+	public var defaults : Dynamic = {};
+	#if editor
+	public var editor : hide.view.GraphEditor;
+	#end
+	public var x : Float;
+	public var y : Float;
+	public var showPreview : Bool = true;
+	@prop public var nameOverride : String;
+
+	// Base parameters that are overriding graph's parameters
+	public var overrides = {};
+	public var outputHeight = 256;
+	public var outputWidth = 256;
+	public var outputFormat = hxd.PixelFormat.RGBA;
+
+
+	static public function register(key : String, cl : Class<SubstanceNode>) : Bool {
+		registeredNodes.set(key, cl);
+		return true;
+	}
+
+	#if editor
+	public function getInfo() : GraphNodeInfo {
+		var metas = haxe.rtti.Meta.getType(HaxeType.getClass(this));
+		return {
+			name: nameOverride ?? (metas.name != null ? metas.name[0] : "undefined"),
+			inputs: [
+				for (i in getInputs()) {
+					{
+						name: i.name,
+						color: SubstanceNode.getTypeColor(i.type),
+					}
+				}
+			],
+			outputs: [
+				for (o in getOutputs()) {
+					{
+						name: o.name,
+						color: SubstanceNode.getTypeColor(o.type),
+					}
+				}
+			],
+			preview: {
+				getVisible: () -> showPreview,
+				setVisible: (b:Bool) -> showPreview = b,
+				fullSize: false,
+			},
+			width: metas.width != null ? metas.width[0] : null,
+			noHeader: Reflect.hasField(metas, "noheader"),
+		};
+	}
+
+	public function getPos(p : h2d.col.Point) : Void {
+		p.set(x,y);
+	}
+
+	public function setPos(p : h2d.col.Point) : Void {
+		x = p.x;
+		y = p.y;
+	}
+
+	public function getPropertiesHTML(width : Float) : Array<hide.Element> {
+		return [];
+	}
+	#end
+
+	public static function createFromDynamic(data: Dynamic, graph: SubstanceGraph) : SubstanceNode {
+		var type = std.Type.resolveClass(data.type);
+		var inst = std.Type.createInstance(type, []);
+		inst.x = data.x;
+		inst.y = data.y;
+		inst.id = data.id;
+		inst.connections = [];
+		inst.loadProperties(data.properties);
+		return inst;
+	}
+
+	static function getTypeColor(type : Dynamic ) : Int {
+		return switch (type) {
+			case h3d.mat.Texture:
+				0xffaf41;
+			default:
+				0xc8c8c8;
+		}
+	}
+
+	public function serializeToDynamic() : Dynamic {
+		return {
+			x: x,
+			y: y,
+			id: id,
+			type: std.Type.getClassName(std.Type.getClass(this)),
+			properties: saveProperties(),
+		};
+	}
+
+	public function loadProperties(props : Dynamic) {
+		var fields = Reflect.fields(props);
+		showPreview = props.showPreview ?? true;
+		nameOverride = props.nameOverride;
+		overrides = props.overrides != null ? props.overrides : {};
+
+		for (f in fields) {
+			if (f == "defaults") {
+				defaults = Reflect.field(props, f);
+			}
+			else {
+				if (Reflect.hasField(this, f)) {
+					Reflect.setField(this, f, Reflect.field(props, f));
+				}
+			}
+		}
+	}
+
+	public function saveProperties() : Dynamic {
+		var parameters : Dynamic = {};
+
+		var thisClass = std.Type.getClass(this);
+		var fields = std.Type.getInstanceFields(thisClass);
+		var metas = haxe.rtti.Meta.getFields(thisClass);
+		var metaSuperClass = haxe.rtti.Meta.getFields(std.Type.getSuperClass(thisClass));
+
+		for (f in fields) {
+			var m = Reflect.field(metas, f);
+			if (m == null) {
+				m = Reflect.field(metaSuperClass, f);
+				if (m == null)
+					continue;
+			}
+			if (Reflect.hasField(m, "prop")) {
+				var metaData : Array<String> = Reflect.field(m, "prop");
+				if (metaData == null || metaData.length == 0 || metaData[0] != "macro") {
+					Reflect.setField(parameters, f, Reflect.getProperty(this, f));
+				}
+			}
+		}
+
+		if (Reflect.fields(defaults).length > 0) {
+			Reflect.setField(parameters, "defaults", defaults);
+		}
+
+		parameters.nameOverride = nameOverride;
+		parameters.showPreview = showPreview;
+		parameters.overrides = overrides;
+
+		return parameters;
+	}
+
+	public function getInputs() : Array<Dynamic> {
+		return Reflect.field(this, "inputs")??[];
+	}
+
+	public function getOutputs() : Array<Dynamic> {
+		return Reflect.field(this, "outputs")??[];
+	}
+
+	public function apply(vars : Dynamic) : Array<h3d.mat.Texture> {
+		return [];
+	}
+
+	public function getInputData(vars : Dynamic, inputIdx : Int) : Dynamic {
+		if (connections.length <= inputIdx || connections[inputIdx] == null)
+			return getDefaultInputData(inputIdx);
+
+		var outputs = vars.get(connections[inputIdx].from.id);
+		if (outputs == null || outputs.length <= connections[inputIdx].outputId)
+			return getDefaultInputData(inputIdx);
+
+		return vars.get(connections[inputIdx].from.id)[connections[inputIdx].outputId];
+	}
+
+	#if editor
+	public function getSpecificParametersHTML() : hide.Element {
+		return null;
+	}
+	#end
+
+	function getDefaultInputData(inputIdx : Int) {
+		var input = getInputs()[inputIdx];
+
+		switch (input.type) {
+			case h3d.mat.Texture:
+				return new h3d.mat.Texture(1, 1);
+			default:
+				return null;
+		}
+	}
+
+	function createTexture() : h3d.mat.Texture {
+		var tex = new h3d.mat.Texture(outputWidth, outputHeight, null, outputFormat);
+		return tex;
+	}
+}

+ 98 - 0
hrt/sbsgraph/nodes/BnWNoise.hx

@@ -0,0 +1,98 @@
+package hrt.sbsgraph.nodes;
+
+class BnWSpotsNoiseShader extends h3d.shader.ScreenShader {
+    static var SRC = {
+		@param var tex : Sampler2D;
+		@param var seed : Float;
+		@param var scale : Float;
+
+		function random(inVector : Vec2) : Float {
+			return fract(sin(dot(inVector.xy, vec2(12.9898,78.233))) * 43758.5453123);
+		}
+
+		// 2D Noise based on Morgan McGuire @morgan3d
+		// https://www.shadertoy.com/view/4dS3Wd
+		function noise(inVector : Vec2) : Float {
+			var i = floor(inVector);
+			var f = fract(inVector);
+
+			var a = random(i);
+			var b = random(i + vec2(1.0, 0.0));
+			var c = random(i + vec2(0.0, 1.0));
+			var d = random(i + vec2(1.0, 1.0));
+
+			var u = f*f*(3.0-2.0*f);
+
+			return mix(a, b, u.x) +
+					(c - a)* u.y * (1.0 - u.x) +
+					(d - b) * u.x * u.y;
+		}
+
+        function fragment() {
+			pixelColor = vec4(vec3(noise((uvToScreen(calculatedUV) + seed) * scale)) , 1.);
+        }
+
+    }
+}
+
+@name("BnW Spots Noise")
+@description("Black and white spots noise texture")
+@width(120)
+@group("Texture generation")
+class BnWSpotsNoise extends SubstanceNode {
+	var inputs = [];
+	var outputs = [
+		{ name : "output", type: h3d.mat.Texture }
+	];
+
+	@prop var seed : Float = 0;
+	@prop var scale : Float = 1;
+
+	override function apply(vars : Dynamic) : Array<h3d.mat.Texture> {
+		var engine = h3d.Engine.getCurrent();
+		var out = new h3d.mat.Texture(outputWidth, outputHeight, null, outputFormat);
+
+		var shader = new BnWSpotsNoiseShader();
+		shader.tex = out;
+		shader.seed = seed;
+		shader.scale = scale;
+
+		var pass = new h3d.pass.ScreenFx(shader);
+
+		engine.pushTarget(out);
+		pass.render();
+		engine.popTarget();
+
+		return [ out ];
+	}
+
+	#if editor
+	override function getSpecificParametersHTML() {
+		var el = new hide.Element('
+		<div class="fields">
+			<label>Seed</label>
+			<input type="number" id="seed"/>
+			<label>Scale</label>
+			<input type="number" id="scale"/>
+		</div>');
+
+		var seedEl = el.find("#seed");
+		seedEl.val(seed);
+		seedEl.on("change", function(e) {
+			this.seed = Std.parseFloat(seedEl.val());
+			var substanceEditor = Std.downcast(editor.editor, hide.view.substanceeditor.SubstanceEditor);
+			substanceEditor.generate();
+		});
+
+		var scaleEl = el.find("#scale");
+		scaleEl.val(scale);
+		scaleEl.on("change", function(e) {
+			this.scale = Std.parseFloat(scaleEl.val());
+			var substanceEditor = Std.downcast(editor.editor, hide.view.substanceeditor.SubstanceEditor);
+			substanceEditor.generate();
+		});
+
+		return el;
+	}
+	#end
+}

+ 34 - 0
hrt/sbsgraph/nodes/Comment.hx

@@ -0,0 +1,34 @@
+package hrt.sbsgraph.nodes;
+
+#if editor
+import hide.view.GraphInterface;
+#end
+
+@name("Comment")
+@description("A box that allows you to comment your graph")
+@group("Comment")
+class Comment extends SubstanceNode {
+	@prop() public var comment : String = "Comment";
+	@prop() public var width : Float = 200;
+	@prop() public var height : Float = 200;
+
+	public function new () {}
+
+	override function apply(vars : Dynamic) : Array<h3d.mat.Texture> {
+		return null;
+	}
+
+	#if editor
+	override function getInfo() : GraphNodeInfo {
+		var info = super.getInfo();
+		info.comment = {
+			getComment: () -> comment,
+			setComment: (v:String) -> comment = v,
+			getSize: (p: h2d.col.Point) -> p.set(width, height),
+			setSize: (p: h2d.col.Point) -> { width = p.x; height = p.y; },
+		};
+		info.preview = null;
+		return info;
+	}
+	#end
+}

+ 17 - 0
hrt/sbsgraph/nodes/Disc.hx

@@ -0,0 +1,17 @@
+package hrt.sbsgraph.nodes;
+
+@name("Disc")
+@description("Basic disc texture")
+@width(80)
+@group("Texture generation")
+class Disc extends SubstanceNode {
+	var inputs = [];
+	var outputs = [
+		{ name : "output", type: h3d.mat.Texture }
+	];
+
+	override function apply(vars : Dynamic) : Array<h3d.mat.Texture> {
+		var out = h3d.mat.Texture.genDisc(outputWidth, 16777215, 1);
+		return [ out ];
+	}
+}

+ 44 - 0
hrt/sbsgraph/nodes/Multiply.hx

@@ -0,0 +1,44 @@
+package hrt.sbsgraph.nodes;
+
+class MultiplyShader extends h3d.shader.ScreenShader {
+    static var SRC = {
+		@param var tex1 : Sampler2D;
+		@param var tex2 : Sampler2D;
+
+        function fragment() {
+			pixelColor = tex1.get(calculatedUV) * tex2.get(calculatedUV);
+        }
+    }
+}
+
+@name("Multiply")
+@description("The output is the result of the inputs multiplied")
+@width(80)
+@group("Operation")
+class Multiply extends SubstanceNode {
+	var inputs = [
+		{ name : "input1", type: h3d.mat.Texture },
+		{ name : "input2", type: h3d.mat.Texture },
+	];
+
+	var outputs = [
+		{ name : "output", type: h3d.mat.Texture }
+	];
+
+	override function apply(vars : Dynamic) : Array<h3d.mat.Texture> {
+		var engine = h3d.Engine.getCurrent();
+		var out = createTexture();
+
+		var shader = new MultiplyShader();
+		shader.tex1 = cast getInputData(vars, 0);
+		shader.tex2 = cast getInputData(vars , 1);
+
+		var pass = new h3d.pass.ScreenFx(shader);
+
+		engine.pushTarget(out);
+		pass.render();
+		engine.popTarget();
+
+		return [ out ];
+	}
+}

+ 52 - 0
hrt/sbsgraph/nodes/RGBAMerge.hx

@@ -0,0 +1,52 @@
+package hrt.sbsgraph.nodes;
+
+class RGBAMergeShader extends h3d.shader.ScreenShader {
+    static var SRC = {
+		@param var r : Sampler2D;
+		@param var g : Sampler2D;
+		@param var b : Sampler2D;
+		@param var a : Sampler2D;
+
+        function fragment() {
+			pixelColor.r = r.get(calculatedUV).r;
+			pixelColor.g = g.get(calculatedUV).r;
+			pixelColor.b = b.get(calculatedUV).r;
+			pixelColor.a = a.get(calculatedUV).r;
+        }
+    }
+}
+
+@name("RGBA Merge")
+@description("Merge 4 grayscale images entry in a RGBA image")
+@width(100)
+@group("Channel")
+class RGBAMerge extends SubstanceNode {
+	var inputs = [
+		{ name : "R", type: h3d.mat.Texture },
+		{ name : "G", type: h3d.mat.Texture },
+		{ name : "B", type: h3d.mat.Texture },
+		{ name : "A", type: h3d.mat.Texture }
+	];
+	var outputs = [
+		{ name : "output", type: h3d.mat.Texture }
+	];
+
+	override function apply(vars : Dynamic) : Array<h3d.mat.Texture> {
+		var engine = h3d.Engine.getCurrent();
+		var out = createTexture();
+
+		var shader = new RGBAMergeShader();
+		shader.r = cast getInputData(vars, 0);
+		shader.g = cast getInputData(vars , 1);
+		shader.b = cast getInputData(vars , 2);
+		shader.a = cast getInputData(vars , 3);
+
+		var pass = new h3d.pass.ScreenFx(shader);
+
+		engine.pushTarget(out);
+		pass.render();
+		engine.popTarget();
+
+		return [ out ];
+	}
+}

+ 64 - 0
hrt/sbsgraph/nodes/RGBASplit.hx

@@ -0,0 +1,64 @@
+package hrt.sbsgraph.nodes;
+
+class RGBASplitShader extends h3d.shader.ScreenShader {
+    static var SRC = {
+		@param var tex : Sampler2D;
+		@const var channels : Int;
+
+        function fragment() {
+			switch( channels ) {
+				case 0:
+					pixelColor = vec4(tex.get(calculatedUV).rrr, 1.);
+				case 1:
+					pixelColor = vec4(tex.get(calculatedUV).ggg, 1.);
+				case 2:
+					pixelColor = vec4(tex.get(calculatedUV).bbb, 1.);
+				case 3:
+					pixelColor = vec4(tex.get(calculatedUV).aaa, 1.);
+				default:
+					pixelColor = vec4(0,0,0,0);
+			}
+        }
+    }
+}
+
+@name("RGBA Split")
+@description("Separate a RGBA image entry in 4 grayscale images")
+@width(100)
+@group("Channel")
+class RGBASplit extends SubstanceNode {
+	var inputs = [
+		{ name : "RGBA", type: h3d.mat.Texture }
+	];
+
+	var outputs = [
+		{ name : "R", type: h3d.mat.Texture },
+		{ name : "G", type: h3d.mat.Texture },
+		{ name : "B", type: h3d.mat.Texture },
+		{ name : "A", type: h3d.mat.Texture }
+	];
+
+	override function apply(vars : Dynamic) : Array<h3d.mat.Texture> {
+		var engine = h3d.Engine.getCurrent();
+
+		var r = createTexture();
+		var g = createTexture();
+		var b = createTexture();
+		var a = createTexture();
+
+		var texs = [ r, g, b, a];
+
+		var shader = new RGBASplitShader();
+		shader.tex = cast getInputData(vars, 0);
+
+		var pass = new h3d.pass.ScreenFx(shader);
+		for (idx => t in texs) {
+			shader.channels = idx;
+			engine.pushTarget(t);
+			pass.render();
+			engine.popTarget();
+		}
+
+		return texs;
+	}
+}

+ 41 - 0
hrt/sbsgraph/nodes/SubstanceOutput.hx

@@ -0,0 +1,41 @@
+package hrt.sbsgraph.nodes;
+
+@name("Outputs")
+@description("Parameters outputs")
+@width(80)
+@group("Output")
+@color("#A90707")
+class SubstanceOutput extends SubstanceNode {
+	@prop public var label : String = "base-color";
+
+	var inputs = [
+		{ name : "input", type: h3d.mat.Texture }
+	];
+
+	var outputs = [];
+
+	override function apply(vars : Dynamic) : Array<h3d.mat.Texture> {
+		var out = cast getInputData(vars, 0);
+		return [ out ];
+	}
+
+	#if editor
+	override function getSpecificParametersHTML() {
+		var el = new hide.Element('
+		<div class="fields">
+			<label>Label</label>
+			<input id="label"/>
+		</div>');
+
+		var labelEl = el.find("#label");
+		labelEl.val(label);
+		labelEl.on("change", function(e) {
+			this.label = labelEl.val();
+			var substanceEditor = Std.downcast(editor.editor, hide.view.substanceeditor.SubstanceEditor);
+			substanceEditor.generate();
+		});
+
+		return el;
+	}
+	#end
+}

+ 67 - 0
hrt/sbsgraph/nodes/WhiteNoise.hx

@@ -0,0 +1,67 @@
+package hrt.sbsgraph.nodes;
+
+class WhiteNoiseShader extends h3d.shader.ScreenShader {
+    static var SRC = {
+		@param var tex : Sampler2D;
+		@param var seed : Float;
+
+		function random(inVector : Vec2) : Float {
+			return fract(sin(dot(inVector.xy, vec2(12.9898,78.233))) * 43758.5453123);
+		}
+
+        function fragment() {
+			pixelColor = vec4(vec3(random(uvToScreen(calculatedUV) + seed)) , 1.);
+        }
+
+    }
+}
+
+@name("White Noise")
+@description("White noise texture")
+@width(100)
+@group("Texture generation")
+class WhiteNoise extends SubstanceNode {
+	var inputs = [];
+	var outputs = [
+		{ name : "output", type: h3d.mat.Texture }
+	];
+
+	@prop var seed : Float = 0;
+
+	override function apply(vars : Dynamic) : Array<h3d.mat.Texture> {
+		var engine = h3d.Engine.getCurrent();
+		var out = new h3d.mat.Texture(outputWidth, outputHeight, null, outputFormat);
+
+		var shader = new WhiteNoiseShader();
+		shader.tex = out;
+		shader.seed = seed;
+
+		var pass = new h3d.pass.ScreenFx(shader);
+
+		engine.pushTarget(out);
+		pass.render();
+		engine.popTarget();
+
+		return [ out ];
+	}
+
+	#if editor
+	override function getSpecificParametersHTML() {
+		var el = new hide.Element('
+		<div class="fields">
+			<label>Seed</label>
+			<input type="number" id="seed"/>
+		</div>');
+
+		var seedEl = el.find("#seed");
+		seedEl.val(seed);
+		seedEl.on("change", function(e) {
+			this.seed = Std.parseFloat(seedEl.val());
+			var substanceEditor = Std.downcast(editor.editor, hide.view.substanceeditor.SubstanceEditor);
+			substanceEditor.generate();
+		});
+
+		return el;
+	}
+	#end
+}

+ 12 - 8
hrt/shgraph/NodeGenContext.hx

@@ -128,7 +128,12 @@ class NodeGenContext {
 					def = {v: v, defValue: defValue, __init__: null};
 					if (parent != null) {
 						var p = Variables.Globals[parent];
-						v.parent = globalVars.getOrPut(p.name, {v : {id : hxsl.Tools.allocVarId(), name: p.name, type: TStruct([]), kind: kind}, defValue: null, __init__: null}).v;
+						switch (p.varkind) {
+							case KVar(kind, _, _):
+								v.parent = globalVars.getOrPut(p.name, {v : {id : hxsl.Tools.allocVarId(), name: p.name, type: TStruct([]), kind: kind}, defValue: null, __init__: null}).v;
+							default:
+								throw "Parent var must be a KVar";
+						}
 						switch(v.parent.type) {
 							case TStruct(arr):
 								arr.push(v);
@@ -193,16 +198,15 @@ class NodeGenContext {
 		if (delta > 0) {
 			var args = [];
 			if (sourceSize == 1) {
-				for (_ in 0...targetSize) {
+				for (i in 0...targetSize) {
 					args.push(sourceExpr);
 				}
 			}
 			else {
-
 				args.push(sourceExpr);
 				for (i in 0...delta) {
 					// Set alpha to 1.0 by default on upcasts casts
-					var value = i == delta - 1 ? 1.0 : 0.0;
+					var value = ((sourceSize + i) == 3) ? 1.0 : 0.0;
 					args.push({e : TConst(CFloat(value)), p: sourceExpr.p, t: TFloat});
 				}
 			}
@@ -267,9 +271,9 @@ class NodeGenContext {
 	/**
 		API used by ShaderGraphGenContext
 	**/
-	function initForNode(node: ShaderGraph.Node, nodeInputExprs: Array<TExpr>) {
-		nodeInputInfo = node.instance.getInputs();
-		nodeOutputInfo = node.instance.getOutputs();
+	function initForNode(node: ShaderNode, nodeInputExprs: Array<TExpr>) {
+		nodeInputInfo = node.getInputs();
+		nodeOutputInfo = node.getOutputs();
 		this.node = node;
 		this.nodeInputExprs = nodeInputExprs;
 
@@ -297,7 +301,7 @@ class NodeGenContext {
 		}
 	}
 
-	var node : ShaderGraph.Node = null;
+	var node : ShaderNode = null;
 
 	var currentPreviewId: Int = -1;
 	var expressions: Array<TExpr> = [];

+ 10 - 9
hrt/shgraph/Random.hx

@@ -1,7 +1,7 @@
 package hrt.shgraph;
 
 @name("Random")
-@description("Generate a random value between min and max using the given seed")
+@description("[CURRENTLY BROKEN] Generate a random value between min and max using the given seed")
 @group("Channel")
 class Random extends ShaderNodeHxsl {
 
@@ -11,16 +11,17 @@ class Random extends ShaderNodeHxsl {
 		@sginput(1.0) var max : Float;
 		@sgoutput var output : Float;
 
-		function pcg(v : Int) : Int
-		{
-			var state : Int = v * 0x2C9277B5 + 0xAC564B05;
-			var word = ((state >>> ((state >>> 28) + 4)) ^ state) * 0x108EF2D9;
-			return (word >>> 22) ^ word;
-		}
+		// shadernodeHsxl support for other functions is currently broken
+		// function pcg(v : Int) : Int
+		// {
+		// 	var state : Int = v * 0x2C9277B5 + 0xAC564B05;
+		// 	var word = ((state >>> ((state >>> 28) + 4)) ^ state) * 0x108EF2D9;
+		// 	return (word >>> 22) ^ word;
+		// }
 
 		function fragment() {
-			var rand = pcg(pcg(int(seed.x)) + int(seed.y));
-			output = float(rand & 0xFFFF)/float(0xFFFF) * (max-min) + min;
+			//var rand = pcg(pcg(int(seed.x)) + int(seed.y));
+			output = 0.0;
 		}
 	}
 }

+ 3 - 2
hrt/shgraph/ShaderGlobalInput.hx

@@ -16,8 +16,8 @@ class ShaderGlobalInput extends ShaderNode {
 			{display: "Pixel Size", g: PixelSize},
 		];
 
-	public function new(idx: Int) {
-		variableIdx = idx;
+	public function new(idx: Null<Int>) {
+		variableIdx = idx ?? variableIdx;
 	}
 
 	var outputs : Array<ShaderNode.OutputInfo>;
@@ -68,6 +68,7 @@ class ShaderGlobalInput extends ShaderNode {
 			var value = input.val();
 			outputs = null;
 			this.variableIdx = value;
+			requestRecompile();
 		});
 
 		elements.push(element);

+ 69 - 73
hrt/shgraph/ShaderGraph.hx

@@ -135,29 +135,17 @@ typedef ShaderNodeDef = {
 	?functions: Array<TFunction>,
 };
 
-typedef Node = {
-	x : Float,
-	y : Float,
-	id : Int,
-	type : String,
-	?properties : Dynamic,
-	?instance : ShaderNode,
-	?outputs: Array<Node>,
-	?indegree : Int,
-	?generateId : Int, // Id used to index the node in the generate function
-};
-
 typedef Edge = {
 	?outputNodeId : Int,
-	nameOutput : String, // Fallback if name has changed
+	?nameOutput : String, // Fallback if name has changed
 	?outputId : Int,
 	?inputNodeId : Int,
-	nameInput : String, // Fallback if name has changed
+	?nameInput : String, // Fallback if name has changed
 	?inputId : Int,
 };
 
 typedef Connection = {
-	from : Node,
+	from : ShaderNode,
 	outputId : Int,
 };
 
@@ -176,14 +164,6 @@ enum Domain {
 	Fragment;
 }
 
-
-typedef GenNodeInfo = {
-	outputToInputMap: Map<String, Array<{node: Node, inputName: String}>>,
-	inputTypes: Array<Type>,
-	?outputs: Map<String, TVar>,
-	?def: ShaderGraph.ShaderNodeDef,
-}
-
 @:structInit @:publicFields
 class
 ExternVarDef {
@@ -206,7 +186,7 @@ class ShaderGraphGenContext {
 	var nodes : Array<{
 		var outputs: Array<Array<{to: Int, input: Int}>>;
 		var inputs : Array<TExpr>;
-		var node : Node;
+		var node : ShaderNode;
 	}>;
 
 	var inputNodes : Array<Int> = [];
@@ -230,7 +210,7 @@ class ShaderGraphGenContext {
 			var node = nodes[nodeId];
 			genContext.initForNode(node.node, node.inputs);
 
-			node.node.instance.generate(genContext);
+			node.node.generate(genContext);
 
 			for (outputId => expr in genContext.outputs) {
 				if (expr == null) throw "null expr for output " + outputId;
@@ -283,7 +263,7 @@ class ShaderGraphGenContext {
 
 		for (id => node in nodes) {
 			if (node == null) continue;
-			var inst = node.node.instance;
+			var inst = node.node;
 			var empty = true;
 			var inputs = inst.getInputs();
 
@@ -293,7 +273,7 @@ class ShaderGraphGenContext {
 				if (connection == null)
 					continue;
 				empty = false;
-				var nodeOutputs = connection.from.instance.getOutputs();
+				var nodeOutputs = connection.from.getOutputs();
 				var outputs = nodes[connection.from.id].outputs;
 				if (outputs == null) {
 					outputs = [];
@@ -427,7 +407,7 @@ class ShaderGraph extends hrt.prefab.Prefab {
 		var inits : Array<{variable: TVar, value: Dynamic}>= [];
 
 		var shaderData : ShaderData = {
-			name: "",
+			name: this.shared.currentPath,
 			vars: [],
 			funs: [],
 		};
@@ -618,10 +598,8 @@ class ShaderGraph extends hrt.prefab.Prefab {
 	public function setParameterDefaultValue(id : Int, newDefaultValue : Dynamic) : Bool {
 		var p = parametersAvailable.get(id);
 		if (p != null) {
-			if (newDefaultValue != null) {
-				p.defaultValue = newDefaultValue;
-				return true;
-			}
+			p.defaultValue = newDefaultValue;
+			return true;
 		}
 		return false;
 	}
@@ -650,7 +628,7 @@ class Graph {
 	var cachedGen : ShaderNodeDef = null;
 	var allParamDefaultValue = [];
 	var current_node_id = 0;
-	var nodes : Map<Int, Node> = [];
+	var nodes : Map<Int, ShaderNode> = [];
 
 	public var parent : ShaderGraph = null;
 
@@ -667,24 +645,13 @@ class Graph {
 		generate(Reflect.getProperty(json, "nodes"), Reflect.getProperty(json, "edges"));
 	}
 
-	public function generate(nodes : Array<Node>, edges : Array<Edge>) {
+	public function generate(nodes : Array<Dynamic>, edges : Array<Edge>) {
+		current_node_id = 0;
 		for (n in nodes) {
-			n.outputs = [];
-			var cl = std.Type.resolveClass(n.type);
-			if( cl == null ) throw "Missing shader node "+n.type;
-			n.instance = std.Type.createInstance(cl, []);
-			n.instance.setId(n.id);
-			n.instance.loadProperties(n.properties);
-			this.nodes.set(n.id, n);
-
-			var shaderParam = Std.downcast(n.instance, ShaderParam);
-			if (shaderParam != null) {
-				var paramShader = getParameter(shaderParam.parameterId);
-				shaderParam.variable = paramShader.variable;
-			}
+			var node = ShaderNode.createFromDynamic(n, parent);
+			this.nodes.set(node.id, node);
+			current_node_id = hxd.Math.imax(current_node_id, node.id+1);
 		}
-		if (nodes[nodes.length-1] != null)
-			this.current_node_id = nodes[nodes.length-1].id+1;
 
 		// Migration patch
 		for (e in edges) {
@@ -699,13 +666,53 @@ class Graph {
 		}
 	}
 
-	public function addEdge(edge : Edge) {
+	public function canAddEdge(edge : Edge) {
 		var node = this.nodes.get(edge.inputNodeId);
 		var output = this.nodes.get(edge.outputNodeId);
 
-		var inputs = node.instance.getInputs();
-		var outputs = output.instance.getOutputs();
+		var inputs = node.getInputs();
+		var outputs = output.getOutputs();
+
+		var inputType = inputs[edge.inputId].type;
+		var outputType = outputs[edge.outputId].type;
+
+		if (!areTypesCompatible(inputType, outputType)) {
+			return false;
+		}
+
+		function hasCycle(node: ShaderNode, ?visited: Map<ShaderNode, Bool>) : Bool {
+			var visited = visited?.copy() ?? [];
+			if (visited.get(node) != null) {
+				return true;
+			}
+			visited.set(node, true);
+			for (id => conn in node.connections) {
+				if (conn != null) {
+					if (hasCycle(conn.from, visited))
+						return true;
+				}
+			}
+			return false;
+		}
+
+		var prev = node.connections[edge.inputId];
+		node.connections[edge.inputId] = {from: output, outputId: edge.outputId};
+
+		var res = hasCycle(node);
+		node.connections[edge.inputId] = prev;
+
+		if (res)
+			return false;
+
+		return true;
+	}
+
+	public function addEdge(edge : Edge) {
+		var node = this.nodes.get(edge.inputNodeId);
+		var output = this.nodes.get(edge.outputNodeId);
 
+		var inputs = node.getInputs();
+		var outputs = output.getOutputs();
 
 		var outputId = edge.outputId;
 		var inputId = edge.inputId;
@@ -736,7 +743,7 @@ class Graph {
 			}
 		}
 
-		node.instance.connections[inputId] = {from: output, outputId: outputId};
+		node.connections[inputId] = {from: output, outputId: outputId};
 
 		#if editor
 		if (hasCycle()){
@@ -759,6 +766,10 @@ class Graph {
 		return true;
 	}
 
+	public function addNode(shNode : ShaderNode) {
+		this.nodes.set(shNode.id, shNode);
+	}
+
 	public function areTypesCompatible(input: SgType, output: SgType) : Bool {
 		return switch (input) {
 			case SgFloat(_):
@@ -777,10 +788,9 @@ class Graph {
 
 	public function removeEdge(idNode, inputId, update = true) {
 		var node = this.nodes.get(idNode);
-		if (node.instance.connections[inputId] == null) return;
-		this.nodes.get(node.instance.connections[inputId].from.id).outputs.remove(node);
+		if (node.connections[inputId] == null) return;
 
-		node.instance.connections[inputId] = null;
+		node.connections[inputId] = null;
 	}
 
 	public function setPosition(idNode : Int, x : Float, y : Float) {
@@ -801,20 +811,6 @@ class Graph {
 		return parent.getParameter(id);
 	}
 
-
-	public function addNode(x : Float, y : Float, nameClass : Class<ShaderNode>, args: Array<Dynamic>) {
-		var node : Node = { x : x, y : y, id : current_node_id, type: std.Type.getClassName(nameClass) };
-
-		node.instance = std.Type.createInstance(nameClass, args);
-		node.instance.setId(current_node_id);
-		node.outputs = [];
-
-		this.nodes.set(node.id, node);
-		current_node_id++;
-
-		return node.instance;
-	}
-
 	public function hasCycle() : Bool {
 		var ctx = new ShaderGraphGenContext(this, false);
 		@:privateAccess ctx.initNodes();
@@ -829,15 +825,15 @@ class Graph {
 	public function saveToDynamic() : Dynamic {
 		var edgesJson : Array<Edge> = [];
 		for (n in nodes) {
-			for (inputId => connection in n.instance.connections) {
+			for (inputId => connection in n.connections) {
 				if (connection == null) continue;
 				var outputId = connection.outputId;
-				edgesJson.push({ outputNodeId: connection.from.id, nameOutput: connection.from.instance.getOutputs()[outputId].name, inputNodeId: n.id, nameInput: n.instance.getInputs()[inputId].name, inputId: inputId, outputId: outputId });
+				edgesJson.push({ outputNodeId: connection.from.id, nameOutput: connection.from.getOutputs()[outputId].name, inputNodeId: n.id, nameInput: n.getInputs()[inputId].name, inputId: inputId, outputId: outputId });
 			}
 		}
 		var json = {
 			nodes: [
-				for (n in nodes) { x : Std.int(n.x), y : Std.int(n.y), id: n.id, type: n.type, properties : n.instance.saveProperties() }
+				for (n in nodes) n.serializeToDynamic(),
 			],
 			edges: edgesJson
 		};

+ 1 - 0
hrt/shgraph/ShaderInput.hx

@@ -109,6 +109,7 @@ class ShaderInput extends ShaderNode {
 		input.on("change", function(e) {
 			variable = input.val();
 			outputs = null;
+			requestRecompile();
 		});
 
 		elements.push(element);

+ 126 - 5
hrt/shgraph/ShaderNode.hx

@@ -11,6 +11,10 @@ import hrt.shgraph.AstTools.*;
 import hrt.shgraph.ShaderGraph;
 import hrt.shgraph.SgHxslVar.ShaderDefInput;
 
+#if editor
+import hide.view.GraphInterface; 
+#end
+
 
 class AlphaPreview extends hxsl.Shader {
 	static var SRC = {
@@ -39,13 +43,134 @@ typedef AliasInfo = {?nameSearch: String, ?nameOverride : String, ?description :
 @:autoBuild(hrt.shgraph.Macros.autoRegisterNode())
 @:keepSub
 @:keep
-class ShaderNode {
+class ShaderNode 
+#if editor
+implements hide.view.GraphInterface.IGraphNode 
+#end
+{
 
 	public var id : Int;
+	public var x : Float;
+	public var y : Float;
 	public var showPreview : Bool = true;
 	@prop public var nameOverride : String;
 
 
+	#if editor
+	// IGraphNode Interface
+	public function getInfo() : GraphNodeInfo {
+		var metas = haxe.rtti.Meta.getType(HaxeType.getClass(this));
+		return {
+			name: nameOverride ?? (metas.name != null ? metas.name[0] : "undefined"),
+			inputs: [
+				for (i in getInputs()) {
+					var defaultParam = null;
+					switch (i.def) {
+						case Const(intialValue):
+							defaultParam = {
+								get: () -> Std.string(Reflect.getProperty(defaults, i.name) ?? intialValue),
+								set: setDefaultParam.bind(i.name),
+							};
+						default:
+					}
+					{
+						name: i.name,
+						color: getTypeColor(i.type),
+						defaultParam: defaultParam,
+					}
+				}
+			],
+			outputs: [
+				for (o in getOutputs()) {
+					{
+						name: o.name,
+						color: getTypeColor(o.type),
+					}
+				}
+			],
+			preview: {
+				getVisible: () -> showPreview,
+				setVisible: (b:Bool) -> showPreview = b,
+				fullSize: false,
+			},
+			width: metas.width != null ? metas.width[0] : null,
+			noHeader: Reflect.hasField(metas, "noheader"),
+		};
+	}
+
+	static function getTypeColor(type: SgType) : Int {
+		return switch (type) {
+			case SgFloat(1):
+				0x00ff73;
+			case SgFloat(2):
+				0x5eff00;
+			case SgFloat(3):
+				0xeeff00;
+			case SgFloat(4):
+				0xfc6703;
+			case SgInt:
+				0x00ffea;
+			case SgSampler:
+				0x600aff;
+			default:
+				0xc8c8c8;
+		}
+	}
+
+	public function getId() : Int {
+		return id;
+	}
+
+	public function getPos(p : h2d.col.Point) : Void {
+		p.set(x,y);
+	}
+
+	public function setPos(p : h2d.col.Point) : Void {
+		x = p.x;
+		y = p.y;
+	}
+
+	public function getPropertiesHTML(width : Float) : Array<hide.Element> {
+		return [];
+	}
+
+	public var editor : hide.view.GraphEditor;
+
+	public function setDefaultParam(name: String, value: String) {
+		Reflect.setField(defaults, name, Std.parseFloat(value));
+		requestRecompile();
+	}
+
+	public function requestRecompile() {
+		Std.downcast(editor.editor, hide.view.shadereditor.ShaderEditor)?.requestRecompile();
+	}
+	#end
+
+	public function serializeToDynamic() : Dynamic {
+		return {
+			x: x,
+			y: y,
+			id: id,
+			type: std.Type.getClassName(std.Type.getClass(this)),
+			properties: saveProperties(),
+		};
+	}
+
+	public static function createFromDynamic(data: Dynamic, graph: ShaderGraph) : ShaderNode {
+		var type = std.Type.resolveClass(data.type);
+		var inst = std.Type.createInstance(type, []);
+		var shaderParam = Std.downcast(inst, ShaderParam);
+		if (shaderParam != null) {
+			shaderParam.shaderGraph = graph;
+		}
+		inst.x = data.x;
+		inst.y = data.y;
+		inst.id = data.id;
+		inst.connections = [];
+		inst.loadProperties(data.properties);
+		return inst;
+	}
+
 	public var defaults : Dynamic = {};
 
 	/**
@@ -159,10 +284,6 @@ class ShaderNode {
 		return props;
 	}
 
-	function getPropertiesHTML(width : Float) : Array<hide.Element> {
-		return [];
-	}
-
 	static public var registeredNodes = new Map<String, Class<ShaderNode>>();
 
 	static public function register(key : String, cl : Class<ShaderNode>) : Bool {

+ 1 - 0
hrt/shgraph/ShaderOutput.hx

@@ -94,6 +94,7 @@ class ShaderOutput extends ShaderNode {
 		input.on("change", function(e) {
 			variable = input.val();
 			inputs = null;
+			requestRecompile();
 		});
 
 		elements.push(element);

+ 13 - 59
hrt/shgraph/ShaderParam.hx

@@ -2,16 +2,21 @@ package hrt.shgraph;
 
 using hxsl.Ast;
 
-@noheader()
+@name("Parameter")
 @width(120)
 @color("#d6d6d6")
 class ShaderParam extends ShaderNode {
-
-
 	@prop() public var parameterId : Int;
 	@prop() public var perInstance : Bool;
 
+	public var shaderGraph : ShaderGraph;
+
+	public function new() {
+		
+	}
+
 	override function getOutputs() : Array<ShaderNode.OutputInfo> {
+		var variable = getVariable();
 		var t = switch(variable.type) {
 			case TFloat:
 				SgFloat(1);
@@ -22,10 +27,11 @@ class ShaderParam extends ShaderNode {
 			default:
 				throw "Unhandled var type " + variable.type;
 		}
-		return [{name: "output", type: t}];
+		return [{name: variable.name, type: t}];
 	}
 
 	override function generate(ctx: NodeGenContext) {
+		var variable = getVariable();
 		var v = ctx.getGlobalParam(variable.name, variable.type);
 
 		ctx.setOutput(0, v);
@@ -39,8 +45,9 @@ class ShaderParam extends ShaderNode {
 		}
 	}
 
-	public var variable : TVar;
-
+	function getVariable() : TVar {
+		return shaderGraph.getParameter(parameterId).variable;
+	}
 
 	override public function loadProperties(props : Dynamic) {
 		parameterId = Reflect.field(props, "parameterId");
@@ -56,57 +63,4 @@ class ShaderParam extends ShaderNode {
 		return parameters;
 	}
 
-	#if editor
-	private var parameterName : String;
-	private var eltName : hide.Element;
-
-	private var parameterDisplay : String;
-	private var displayDiv : hide.Element;
-	public function setName(s : String) {
-		parameterName = s;
-		if (eltName != null)
-			eltName.html(s);
-	}
-	public function setDisplayValue(value : String) {
-		parameterDisplay = value;
-		switch (this.variable.type) {
-			case TFloat:
-				if (displayDiv != null)
-					displayDiv.html(value);
-			case TSampler(_):
-				if (displayDiv != null)
-					displayDiv.css("background-image", 'url(${value})');
-			case TVec(4, VFloat):
-				if (displayDiv != null)
-					displayDiv.css("background-color", value);
-			default:
-		}
-	}
-	override public function getPropertiesHTML(width : Float) : Array<hide.Element> {
-		var elements = super.getPropertiesHTML(width);
-		var height = 25;
-		switch (this.variable.type) {
-			case TFloat:
-				displayDiv = new hide.Element('<div class="float-preview" ></div>');
-				height += 20;
-			case TSampler(_):
-				displayDiv = null;
-			case TVec(4, VFloat):
-				displayDiv = null;
-			default:
-				displayDiv = null;
-		}
-		var element = new hide.Element('<div style="width: 110px; height: ${height}px"></div>');
-		if (displayDiv != null) {
-			setDisplayValue(parameterDisplay);
-			displayDiv.appendTo(element);
-		}
-		eltName = new hide.Element('<div class="paramVisible" >${parameterName}</div>').appendTo(element);
-
-		elements.push(element);
-
-		return elements;
-	}
-	#end
-
 }

+ 2 - 2
hrt/shgraph/Variables.hx

@@ -56,8 +56,8 @@ class Variables {
 
 		g[CalculatedUV] 		= {type: TVec(2, VFloat), 	name: "calculatedUV", varkind: KVar(Var)};
 
-		g[Time] 				= {type: TFloat, 	name: "time", 			varkind: KVar(Local, Global)};
-		g[PixelSize]			= {type: TVec(2, VFloat), 	name: "pixelSize", 		varkind: KVar(Local, Global)};
+		g[Time] 				= {type: TFloat, 	name: "time", 			varkind: KVar(Global, Global)};
+		g[PixelSize]			= {type: TVec(2, VFloat), 	name: "pixelSize", 		varkind: KVar(Global, Global)};
 		g[Global] 				= {type: TVoid, 	name: "global", 		varkind: KVar(Global)};
 
 		g[Input]			= {type: TVoid, name: "input", varkind: KVar(Input)};

+ 0 - 1
hrt/shgraph/nodes/Add.hx

@@ -16,5 +16,4 @@ class Add extends Operation {
 			output = a + b;
 		}
 	}
-
 }

+ 20 - 0
hrt/shgraph/nodes/AlphaOver.hx

@@ -0,0 +1,20 @@
+package hrt.shgraph.nodes;
+
+@name("Alpha Over")
+@description("Output is A if A.")
+@width(100)
+@group("Operation")
+class AlphaOver extends Operation {
+
+	static var SRC = {
+		@sginput(0.0) var a : Vec4;
+		@sginput(0.0) var b : Vec4;
+        @sginput(1.0) var opacity : Float;
+		@sgoutput var output : Vec4;
+		function fragment() {
+            var alpha = opacity * a.a;
+			output.rgb = a.rgb * alpha + b.rgb * (1.0 - alpha);
+            output.a = clamp(alpha + b.a,0.0, 1.0);
+		}
+	}
+}

+ 1 - 1
hrt/shgraph/nodes/CombineAlpha.hx

@@ -11,7 +11,7 @@ using hxsl.Ast;
 class CombineAlpha extends ShaderNodeHxsl {
 
 	static var SRC = {
-		@sginput var rgb : Vec3;
+		@sginput(0.0) var rgb : Vec3;
 		@sginput(1.0) var a : Float;
 		@sgoutput var output : Vec4;
 

+ 24 - 3
hrt/shgraph/nodes/Comment.hx

@@ -2,17 +2,38 @@ package hrt.shgraph.nodes;
 
 using hxsl.Ast;
 
+#if editor
+import hide.view.GraphInterface;
+#end
+
 @name("Comment")
 @description("A box that allows you to comment your graph")
 @group("Comment")
 class Comment extends ShaderNode {
-	@prop() public var comment : String = "";
-	@prop() public var width : Int = 200;
-	@prop() public var height : Int = 200;
+	@prop() public var comment : String = "Comment";
+	@prop() public var width : Float = 200;
+	@prop() public var height : Float = 200;
 
 	override function generate(ctx: NodeGenContext) {}
 
 	override function canHavePreview():Bool {
 		return false;
 	}
+
+	public function new() {
+	}
+
+	#if editor
+	override function getInfo() : GraphNodeInfo {
+		var info = super.getInfo();
+		info.comment = {
+			getComment: () -> comment,
+			setComment: (v:String) -> comment = v,
+			getSize: (p: h2d.col.Point) -> p.set(width, height),
+			setSize: (p: h2d.col.Point) -> {width = p.x; height = p.y;},
+		};
+		info.preview = null;
+		return info;
+	}
+	#end
 }

+ 3 - 4
hrt/shgraph/nodes/Dissolve.hx

@@ -9,18 +9,17 @@ using hxsl.Ast;
 class Dissolve extends ShaderNodeHxsl {
 
 	static var SRC = {
-		@sginput var rgba : Vec4;
 		@sginput var dissolveMap : Vec4;
 		@sginput(0.5) var progress : Float;
 		@sginput(0.5) var saturation : Float;
 		@sginput(1.0) var width : Float;
-		@sgoutput var output : Vec4;
+		@sgoutput var alpha : Float;
+
 
 		function fragment() {
 			var edge = mix(1.0 + width, -width, progress);
 			var ramp = saturate((1.0 + saturation) * (width - abs(edge - dissolveMap.r)) / width);
-			output.rgb = rgba.rgb;
-			output.a = rgba.a * ramp * dissolveMap.a;
+			alpha = ramp * dissolveMap.a;
 		}
 	};
 }

+ 1 - 0
hrt/shgraph/nodes/Vec2.hx

@@ -8,6 +8,7 @@ using hxsl.Ast;
 @name("Vec2")
 @description("Create a vector of size 2 from 2 floats")
 @group("Channel")
+@width(80)
 class Vec2 extends ShaderNodeHxsl {
 
 	static var SRC = {

+ 1 - 0
hrt/shgraph/nodes/Vec3.hx

@@ -8,6 +8,7 @@ using hxsl.Ast;
 @name("Vec3")
 @description("Create a vector of size 3 from 3 floats")
 @group("Channel")
+@width(80)
 class Vec3 extends ShaderNodeHxsl {
 
 	static var SRC = {

+ 1 - 0
hrt/shgraph/nodes/Vec4.hx

@@ -8,6 +8,7 @@ using hxsl.Ast;
 @name("Vec4")
 @description("Create a vector of size 4 from 4 floats")
 @group("Channel")
+@width(80)
 class Vec4 extends ShaderNodeHxsl {
 
 	static var SRC = {

+ 11 - 0
hrt/tools/Gizmo.hx

@@ -255,6 +255,17 @@ class Gizmo extends h3d.scene.Object {
 		onChangeMode(editMode);
 	}
 
+	public function switchMode() {
+		switch (editMode) {
+			case Translation:
+				rotationMode();
+			case Rotation:
+				scalingMode();
+			case Scaling:
+				translationMode();
+		}
+	}
+
 	public function startMove(mode: TransformMode, ?duplicating=false) {
 		if (mode == Scale || (axisScale && (mode == MoveX || mode == MoveY || mode == MoveZ)))
 			mouseLock = true;

+ 89 - 0
hrt/tools/OneToMany.hx

@@ -0,0 +1,89 @@
+package hrt.tools;
+
+typedef Left = Int;
+typedef Right = Int;
+
+/**
+    Represent a collection of One -> Many relationship, i.e Multiple `Right` elements can point to an unique `Left` element.
+    For example, a Parent -> Children relationship can be modeled with this, or a Output -> Input in a shadergraph.
+**/
+class OneToMany {
+    var left : Map<Left, Map<Right, Bool>>;
+    var right : Map<Right, Left>;
+
+    public function new() {
+        left = new Map<Left, Map<Right, Bool>>();
+        right = new Map<Right, Left>();
+    }
+
+    /**
+        Add a relation in the collection.
+        If the right element already has a relation, it will be removed and replaced by this one.
+    **/
+    public function insert(l: Left, r: Right) {
+        removeRight(r);
+        var rights : Map<Right, Bool> = left.get(l);
+        if (rights == null) {
+            rights = [];
+            left.set(l, rights);
+        }
+        rights.set(r, true);
+        right.set(r, l);
+    }
+
+    public function removeRight(r: Right) : Null<Left> {
+        var prevL = right.get(r);
+        right.remove(r);
+        if (prevL != null) {
+            left.get(prevL).remove(r);
+        }
+        return prevL;
+    }
+
+    public function removeLeft(l: Left) {
+        var prevRs = left.get(l);
+        left.remove(l);
+        if (prevRs != null) {
+            for(r => _ in prevRs) {
+                right.remove(r);
+            }
+        }
+    }
+
+    public function iterAll() : KeyValueIterator<Int, Iterator<Right>> {
+        var iter = left.keyValueIterator();
+        return {
+            next: () -> {
+                var n = iter.next();
+                return {key: n.key, value: n.value.keys()};
+            },
+            hasNext: () -> {
+                return iter.hasNext();
+            }
+        };
+    }
+
+    public function iterRights(l: Left) : Iterator<Right> {
+        var rights = left.get(l);
+        if (rights != null) {
+            return rights.keys();
+        }
+        return {
+            next: () -> {
+                return -1;
+            },
+            hasNext: () -> {
+                return false;
+            }
+        };
+    }
+
+    public function getLeft(r: Right) {
+        return right.get(r);
+    }
+
+    public function clear() {
+        right.clear();
+        left.clear();
+    }
+}

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.