Przeglądaj źródła

[Shadergraph] Added shader variables (#265)

* [shgraph] Add VarWrite/Read nodes

* [shgraph] Refacto findLocalVarWrite

* fancyarray

* [animgraph] Refactored Param list into FancyArray

* Styling

* [shgraph] Variable editing

* [shgraph] Local variables editor support

* [shgraph] proper var codegen

* [shgraph] Correct tpying of shader local vars, each node now has a ref to the graph

* [shgraph] Variables v1

* [shgraph] Variables are now shader graph whide, global variable support

* [shgraph] Review fixes
Valden 7 miesięcy temu
rodzic
commit
2a23b2922b

+ 159 - 68
bin/style.css

@@ -4225,10 +4225,10 @@ hide-popover hide-content {
   --bg-0: #000;
   --bg-1: #111;
   --bg-2: #222;
+  --bg-3: #333;
 }
 button-2 {
   min-height: 16px;
-  min-width: 24px;
   display: flex;
   justify-content: center;
   align-items: center;
@@ -4253,6 +4253,14 @@ button-2 value {
 button-2:active {
   box-shadow: inset var(--sublte-shadow);
 }
+button-2.no-border {
+  background-color: red;
+  border: none;
+  color: #777;
+}
+button-2.no-border:hover {
+  color: #AAA;
+}
 /** GenericGraphEditor**/
 graph-editor-root {
   width: 100%;
@@ -4340,86 +4348,171 @@ graph-editor-root properties-container graph-parameters {
 graph-editor-root properties-container graph-parameters h1 {
   font-size: 1.2em;
 }
-graph-editor-root properties-container graph-parameters > ul {
-  padding: var(--basic-padding);
-  display: flex;
-  flex-direction: column;
-  list-style: none;
-  align-items: stretch;
-  overflow-y: auto;
-  gap: 2px;
-}
-graph-editor-root properties-container graph-parameters > ul graph-parameter {
-  display: flex;
-  flex-direction: column;
-  justify-items: stretch;
-  border: var(--basic-border);
-  border-radius: var(--basic-border-radius);
-  position: relative;
+graph-editor-root properties-container .anim-list {
+  flex-grow: 0;
+  flex-shrink: 0;
 }
-graph-editor-root properties-container graph-parameters > ul graph-parameter header {
+:root {
+  --fancy-border-color: #777;
+  --fancy-border-color-focus: rgba(114, 180, 255, 0.75);
+  --fancy-main-text-color: #FFF;
+  --fancy-quiet-text-color: #999;
+}
+.fancy-small fancy-button,
+fancy-button.fancy-small {
+  --size: 20px;
+}
+.fancy-normal fancy-button,
+fancy-button.fancy-normal {
+  --size: 28px;
+}
+.fancy-big fancy-button,
+fancy-button.fancy-big {
+  --size: 36px;
+}
+fancy-button {
+  --size: 28px;
+  border: 1px solid var(--fancy-border-color);
+  aspect-ratio: 1 / 1;
+  text-align: center;
+  height: var(--size);
   display: flex;
+  flex-direction: row;
   align-items: center;
-  background-color: #202020;
-  padding: var(--basic-padding);
-  gap: 3px;
+  justify-content: space-evenly;
+  user-select: none;
+  gap: 0.5em;
+  font-size: calc(var(--size) * 0.5);
+  margin-left: -1px;
+  --radius: 3px;
 }
-graph-editor-root properties-container graph-parameters > ul graph-parameter header .fill {
-  flex-grow: 1;
+fancy-button:has(.label) {
+  padding-left: 1em;
+  padding-right: 1em;
 }
-graph-editor-root properties-container graph-parameters > ul graph-parameter header input {
-  border-radius: var(--basic-border-radius);
-  border: var(--basic-border);
+fancy-button .label {
+  font-weight: 200;
+  text-rendering: optimizeLegibility;
 }
-graph-editor-root properties-container graph-parameters > ul graph-parameter header .reorder {
-  padding: var(--basic-padding);
+fancy-button:hover {
+  background-color: #555;
 }
-graph-editor-root properties-container graph-parameters > ul graph-parameter content {
-  border-top: var(--basic-border);
-  padding: var(--basic-padding);
+fancy-button:active {
+  translate: 0px 1px;
 }
-graph-editor-root properties-container graph-parameters > ul graph-parameter content > ul {
+fancy-button.quiet {
+  border: none;
+  padding: 1px;
+}
+fancy-button.quieter {
+  border: none;
+  padding: 1px;
+  color: var(--fancy-quiet-text-color);
+}
+fancy-button.quieter:hover {
+  background: none;
+  color: var(--fancy-main-text-color);
+}
+fancy-button.compact {
+  aspect-ratio: unset;
+}
+fancy-button:first-child,
+:not(fancy-button) + fancy-button {
+  margin-left: 0px;
+  border-start-start-radius: var(--radius);
+  border-end-start-radius: var(--radius);
+}
+fancy-button:last-child,
+fancy-button:has( + :not(fancy-button)) {
+  border-start-end-radius: var(--radius);
+  border-end-end-radius: var(--radius);
+}
+fancy-button.selected {
+  color: #000;
+  background-color: #DDD;
+}
+fancy-button.selected:hover {
+  background-color: #FFF;
+}
+fancy-separator {
+  border: none;
+  height: 0;
+  min-width: 0;
+  max-width: 0;
+  padding: 0;
+  margin: 0 0.5em;
+  height: 16px;
+}
+fancy-array {
+  box-sizing: border-box;
   padding: var(--basic-padding);
+  border-radius: var(--basic-border-radius);
+  border: var(--basic-border);
   display: flex;
   flex-direction: column;
-  list-style: none;
-  display: grid;
-  column-gap: 10px;
-  row-gap: 1px;
-  grid-template-columns: 2fr 5fr;
+  align-items: stretch;
+  background-color: var(--bg-2);
+  overflow-y: auto;
 }
-graph-editor-root properties-container graph-parameters > ul graph-parameter content > ul li {
-  grid-column: 1 / -1;
-  display: grid;
-  grid-template-columns: subgrid;
-  align-items: center;
+fancy-array * {
+  box-sizing: border-box;
 }
-graph-editor-root properties-container graph-parameters > ul graph-parameter content > ul li dd {
-  margin: 0;
-  text-align: right;
+fancy-array fancy-array {
+  padding-right: -1px;
 }
-graph-editor-root properties-container graph-parameters > ul graph-parameter content > ul li > .hide-range {
+fancy-array fancy-items {
   display: flex;
+  flex-direction: column;
+  gap: 2px;
+  justify-content: stretch;
 }
-graph-editor-root properties-container graph-parameters > ul graph-parameter content > ul li > .hide-range input[type=range] {
-  flex-grow: 1;
+fancy-array fancy-items fancy-item {
+  display: flex;
+  flex-direction: column;
+  position: relative;
 }
-graph-editor-root properties-container graph-parameters > ul graph-parameter content > ul li > .hide-range input[type=text] {
+fancy-array fancy-items fancy-item fancy-item-header {
+  display: flex;
+  color: var(--fancy-quiet-text-color);
+  border-radius: 3px;
+  background-color: #444;
+}
+fancy-array fancy-items fancy-item fancy-item-header > input[type="text"] {
+  min-width: 0;
   width: 0;
-  flex-basis: 44px;
+  flex-grow: 1;
+  background: none;
+  outline: none;
+  border: none;
+  font-size: 14px;
+}
+fancy-array fancy-items fancy-item fancy-item-header > input[type="text"]:hover {
+  outline: 1px solid var(--fancy-border-color);
+  border-radius: 3px;
+}
+fancy-array fancy-items fancy-item fancy-item-header > input[type="text"]:focus {
+  outline: 1px solid var(--fancy-border-color-focus);
+  border-radius: 3px;
 }
-graph-editor-root properties-container graph-parameters > ul graph-parameter.folded content {
+fancy-array fancy-items fancy-item fancy-item-content {
   display: none;
+  margin-top: 2px;
+  padding: 0.2em 1em;
+  padding-right: 0px;
+  padding-right: 0;
+  margin-bottom: 0.5em;
+}
+fancy-array fancy-items fancy-item .toggle-open * {
+  transition: transform 0.2s;
 }
-graph-editor-root properties-container graph-parameters > ul graph-parameter.folded .ico-chevron-down {
-  transform: rotate(-90deg);
+fancy-array fancy-items fancy-item.open > fancy-item-header .toggle-open * {
+  transform: rotate(90deg);
 }
-graph-editor-root properties-container graph-parameters > ul graph-parameter .ico-chevron-down {
-  transition: transform 0.25s;
-  transform: rotate(0deg);
+fancy-array fancy-items fancy-item.open > fancy-item-content {
+  display: block;
 }
-graph-editor-root properties-container graph-parameters > ul graph-parameter.hovertop:before,
-graph-editor-root properties-container graph-parameters > ul graph-parameter.hoverbot:after {
+fancy-array fancy-items fancy-item.hovertop:before,
+fancy-array fancy-items fancy-item.hoverbot:after {
   display: block;
   position: absolute;
   z-index: 100;
@@ -4427,20 +4520,18 @@ graph-editor-root properties-container graph-parameters > ul graph-parameter.hov
   width: 100%;
   content: "";
 }
-graph-editor-root properties-container graph-parameters > ul graph-parameter:before {
-  border-top: 10px solid rgba(114, 180, 255, 0.75);
-  top: 0px;
+fancy-array fancy-items fancy-item:before {
+  background: linear-gradient(0deg, rgba(114, 180, 255, 0) 0%, rgba(114, 180, 255, 0.75) 100%);
+  top: -2px;
+  height: 10px;
   pointer-events: none;
 }
-graph-editor-root properties-container graph-parameters > ul graph-parameter:after {
-  border-bottom: 10px solid rgba(114, 180, 255, 0.75);
-  bottom: 0px;
+fancy-array fancy-items fancy-item:after {
+  background: linear-gradient(180deg, rgba(114, 180, 255, 0) 0%, rgba(114, 180, 255, 0.75) 100%);
+  height: 10px;
+  bottom: -2px;
   pointer-events: none;
 }
-graph-editor-root properties-container .anim-list {
-  flex-grow: 0;
-  flex-shrink: 0;
-}
 center-content {
   width: 100%;
   height: 100%;

+ 234 - 100
bin/style.less

@@ -5002,11 +5002,11 @@ hide-popover {
 	--bg-0: #000;
 	--bg-1: #111;
 	--bg-2: #222;
+	--bg-3: #333;
 }
 
 button-2 {
 	min-height: 16px;
-	min-width: 24px;
 	display: flex;
 	justify-content: center;
 	align-items: center;
@@ -5034,6 +5034,17 @@ button-2 {
 	&:active {
 		box-shadow: inset var(--sublte-shadow);
 	}
+
+	&.no-border {
+		background-color: red;
+		border: none;
+
+		color: #777;
+
+		&:hover {
+			color: #AAA;
+		}
+	}
 }
 
 /** GenericGraphEditor**/
@@ -5144,133 +5155,256 @@ graph-editor-root {
 			h1 {
 				font-size: 1.2em;
 			}
+		}
 
-			> ul {
-				padding: var(--basic-padding);
-				display: flex;
-				flex-direction: column;
-				list-style: none;
-				align-items: stretch;
+		.anim-list {
+			flex-grow: 0;
+			flex-shrink: 0;
+		}
+	}
+}
 
-				overflow-y: auto;
-				gap: 2px;
-				graph-parameter {
-					display: flex;
-					flex-direction: column;
-					justify-items: stretch;
 
-					border: var(--basic-border);
-					border-radius: var(--basic-border-radius);
-					position: relative;
+:root {
+	--fancy-border-color: #777;
+	--fancy-border-color-focus: rgba(114, 180, 255, 0.75);
+	--fancy-main-text-color: #FFF;
+	--fancy-quiet-text-color: #999;
+}
 
-					header {
-						display: flex;
-						align-items: center;
-						background-color: #202020;
-						padding: var(--basic-padding);
+.fancy-small {
+	fancy-button, fancy-button& {
+		--size: 20px;
+	}
+}
 
-						gap: 3px;
+.fancy-normal {
+	fancy-button, fancy-button& {
+		--size: 28px;
+	}
+}
 
-						.fill {
-							flex-grow: 1;
-						}
+.fancy-big {
+	fancy-button, fancy-button& {
+		--size: 36px;
+	}
+}
 
-						input {
-							border-radius: var(--basic-border-radius);
-							border: var(--basic-border);
-						}
+fancy-button {
+	--size: 28px;
+	border: 1px solid var(--fancy-border-color);
 
-						.reorder {
-							padding: var(--basic-padding);
-						}
-					}
+	aspect-ratio: 1 / 1;
 
-					content {
-						border-top: var(--basic-border);
-						padding: var(--basic-padding);
+	text-align: center;
 
-						> ul {
-							padding: var(--basic-padding);
-							display: flex;
-							flex-direction: column;
-							list-style: none;
-
-							display: grid;
-							column-gap: 10px;
-							row-gap: 1px;
-							grid-template-columns: 2fr 5fr;
-
-							li {
-								grid-column: 1 / -1;
-								display: grid;
-								grid-template-columns: subgrid;
-								align-items: center;
-
-								dd {
-									margin: 0;
-									text-align: right;
-								}
+	height: var(--size);
 
-								>.hide-range {
-									display: flex;
-									input[type=range] {
-										flex-grow: 1;
-									}
-									input[type=text] {
-										width: 0;
-										flex-basis: 44px;
-									}
-								}
-							}
-						}
-					}
+	display: flex;
+	flex-direction: row;
+	align-items: center;
+	justify-content: space-evenly;
 
-					&.folded content {
-						display: none;
-					}
+	user-select: none;
 
-					&.folded .ico-chevron-down {
-						transform: rotate(-90deg);
-					}
+	&:has(.label) {
+		padding-left: 1.0em;
+		padding-right: 1.0em;
+	}
 
-					.ico-chevron-down {
-						transition: transform 0.25s;
-						transform: rotate(0deg);
-					}
+	gap: 0.5em;
 
+	font-size: calc(var(--size) * 0.5);
 
-					&.hovertop:before, &.hoverbot:after {
-						display: block;
-						position: absolute;
-						z-index: 100;
-						margin: 0 auto;
-						width: 100%;
-						content: "";
-					}
+	.label {
+		font-weight: 200;
+		text-rendering: optimizeLegibility;
+	}
 
-					&:before {
-						border-top: 10px solid rgba(114, 180, 255, 0.75);
-						top: 0px;
-						pointer-events: none;
+	&:hover {
+		background-color: #555;
+	}
+
+	&:active {
+		translate: 0px 1px;
+	}
+
+	&.quiet {
+		border: none;
+		padding: 1px;
+	}
+
+	&.quieter {
+		border: none;
+		padding: 1px;
+
+		color: var(--fancy-quiet-text-color);
+
+		&:hover {
+			background: none;
+			color: var(--fancy-main-text-color);
+
+		}
+	}
+
+	&.compact {
+		aspect-ratio: unset;
+	}
+
+	margin-left: -1px;
+
+	--radius: 3px;
+	&:first-child, :not(&) + & {
+		margin-left: 0px;
+		border-start-start-radius: var(--radius);
+		border-end-start-radius: var(--radius);
+	}
+
+	&:last-child, &:has(+ :not(&)) {
+		border-start-end-radius: var(--radius);
+		border-end-end-radius: var(--radius);
+	}
+
+	&.selected {
+		color: #000;
+		background-color: #DDD;
+
+		&:hover {
+			background-color: #FFF;
+		}
+	}
+}
+
+fancy-separator {
+	border: none;
+	height: 0;
+	min-width: 0;
+	max-width: 0;
+	padding: 0;
+	margin: 0 0.5em;
+	height: 16px;
+}
+
+fancy-array {
+	box-sizing: border-box;
+
+	* {
+		box-sizing: border-box;
+	}
+
+	padding: var(--basic-padding);
+	border-radius: var(--basic-border-radius);
+	border: var(--basic-border);
+	display: flex;
+	flex-direction: column;
+	align-items: stretch;
+
+	background-color: var(--bg-2);
+
+	overflow-y: auto;
+
+	fancy-array {
+		padding-right: -1px;
+	}
+
+	fancy-items {
+		display: flex;
+		flex-direction: column;
+
+		gap: 2px;
+
+		justify-content: stretch;
+
+		fancy-item {
+			display: flex;
+			flex-direction: column;
+			position: relative;
+
+			fancy-item-header {
+				display: flex;
+
+				color: var(--fancy-quiet-text-color);
+				border-radius: 3px;
+
+				background-color: #444;
+
+				>input[type="text"] {
+					min-width: 0;
+					width: 0;
+
+					flex-grow: 1;
+
+					background: none;
+					outline: none;
+					border: none;
+
+					font-size: 14px;
+
+
+					&:hover {
+						outline: 1px solid var(--fancy-border-color);
+						border-radius: 3px;
 					}
 
-					&:after {
-						border-bottom: 10px solid rgba(114, 180, 255, 0.75);
-						bottom: 0px;
-						pointer-events: none;
+					&:focus {
+						outline: 1px solid var(--fancy-border-color-focus);
+						border-radius: 3px;
 					}
 				}
 			}
 
-		}
+			fancy-item-content {
+				display: none;
+				margin-top: 2px;
 
-		.anim-list {
-			flex-grow: 0;
-			flex-shrink: 0;
+				padding:  0.2em 1em;
+				padding-right: 0px;
+				padding-right: 0;
+
+				margin-bottom: 0.5em;
+			}
+
+			.toggle-open * {
+				transition: transform 0.2s;
+			}
+
+			&.open {
+				> fancy-item-header .toggle-open *{
+					transform: rotate(90deg);
+				}
+
+				> fancy-item-content{
+					display: block;
+				}
+			}
+
+			&.hovertop:before, &.hoverbot:after {
+				display: block;
+				position: absolute;
+				z-index: 100;
+				margin: 0 auto;
+				width: 100%;
+				content: "";
+			}
+
+			&:before {
+				background: linear-gradient(0deg, rgba(114, 180, 255, 0.0) 0%, rgba(114, 180, 255, 0.75) 100%);
+				top: -2px;
+				height: 10px;
+				pointer-events: none;
+			}
+
+			&:after {
+				background: linear-gradient(180deg, rgba(114, 180, 255, 0.0) 0%, rgba(114, 180, 255, 0.75) 100%);
+				height: 10px;
+				bottom: -2px;
+				pointer-events: none;
+			}
 		}
 	}
 }
 
+
 center-content {
 	width: 100%;
 	height: 100%;

+ 221 - 0
hide/comp/FancyArray.hx

@@ -0,0 +1,221 @@
+package hide.comp;
+
+typedef FancyItemState = {
+	?open: Bool,
+}
+
+class FancyArray<T> extends hide.comp.Component {
+	var itemState : Array<FancyItemState>;
+	var name : String;
+	var fancyItems: Element;
+
+	public function new(parent: Element = null, e: Element = null, name: String, displayKey: String) {
+		if (e == null)
+			e = new Element("<fancy-array></fancy-array>");
+		super(parent, e);
+
+		fancyItems = new Element("<fancy-items></fancy-items>").appendTo(element);
+
+		this.name = name;
+		saveDisplayKey = displayKey + "/" + name;
+
+		try {
+			itemState = cast haxe.Json.parse(getDisplayState("state")) ?? [];
+		} catch(_) {
+			itemState = [];
+		}
+	}
+
+	var dragKeyName : String;
+	public function getDragKeyName() {
+		if (dragKeyName == null)
+			dragKeyName = '$name:index'.toLowerCase();
+		return dragKeyName;
+	}
+
+	function saveState() {
+		saveDisplayState("state", haxe.Json.stringify(itemState));
+	}
+
+	public function refresh() : Void {
+		fancyItems.empty();
+		var items = getItems();
+
+		for (i => item in items) {
+			var paramElement = new Element('<fancy-item>
+				<fancy-item-header class="fancy-small">
+					<fancy-button class="quieter reorder" draggable="true">
+						<div class="ico ico-reorder"></div>
+					</fancy-button>
+					<fancy-button class="quieter toggle-open">
+						<div class="ico ico-chevron-right"></div>
+					</fancy-button>
+					<input type="text" value="${getItemName(item)}" class="fill"></input>
+					<fancy-button class="menu quieter"><div class="ico ico-ellipsis-v"></div></fancy-button>
+				</fancy-item-header>
+			</fancy-item>').appendTo(fancyItems);
+
+			itemState[i] ??= {};
+			var state = itemState[i];
+			var open : Bool = state.open ?? false;
+			paramElement.toggleClass("open", open);
+
+			var name = paramElement.find("input");
+
+			if (setItemName != null) {
+				name.on("change", (e) -> {
+					setItemName(item, name.val());
+				});
+
+				name.on("keydown", (e) -> {
+					if (e.keyCode == 13) {
+						name.blur();
+						e.stopPropagation();
+					}
+				});
+			}
+
+			name.on("contextmenu", (e) -> {
+                e.stopPropagation();
+            });
+
+			var reorder = paramElement.find(".reorder");
+			if (reorderItem != null) {
+
+				inline function isAfter(e) {
+					return e.clientY > (paramElement.offset().top + paramElement.outerHeight() / 2.0);
+				}
+
+				reorder.get(0).ondragstart = (e: js.html.DragEvent) -> {
+					e.dataTransfer.setDragImage(paramElement.get(0), Std.int(paramElement.width()), 0);
+
+					e.dataTransfer.setData(getDragKeyName(), '${i}');
+					e.dataTransfer.dropEffect = "move";
+				}
+
+				paramElement.get(0).addEventListener("dragover", function(e : js.html.DragEvent) {
+					if (!e.dataTransfer.types.contains(getDragKeyName()))
+						return;
+					var after = isAfter(e);
+					paramElement.toggleClass("hovertop", !after);
+					paramElement.toggleClass("hoverbot", after);
+					e.preventDefault();
+				});
+
+				paramElement.get(0).addEventListener("dragleave", function(e : js.html.DragEvent) {
+					if (!e.dataTransfer.types.contains(getDragKeyName()))
+						return;
+					paramElement.toggleClass("hovertop", false);
+					paramElement.toggleClass("hoverbot", false);
+				});
+
+				paramElement.get(0).addEventListener("dragenter", function(e : js.html.DragEvent) {
+					if (!e.dataTransfer.types.contains(getDragKeyName()))
+						return;
+					e.preventDefault();
+				});
+
+				paramElement.get(0).addEventListener("drop", function(e : js.html.DragEvent) {
+					var toMoveIndex = Std.parseInt(e.dataTransfer.getData(getDragKeyName()));
+					paramElement.toggleClass("hovertop", false);
+					paramElement.toggleClass("hoverbot", false);
+					if (i == null)
+						return;
+					var after = isAfter(e);
+
+					var newIndex = i;
+
+					if (!after) newIndex -= 1;
+					if (toMoveIndex == newIndex)
+						return;
+					if (newIndex < i) {
+						newIndex += 1;
+					}
+					reorderItem(toMoveIndex, newIndex);
+				});
+			} else {
+				reorder.remove();
+			}
+
+			var toggleOpen = paramElement.find(".toggle-open");
+			if (getItemContent != null) {
+				var contentElement = getItemContent(item);
+				if (contentElement != null) {
+					var content = new Element("<fancy-item-content></fancy-item-content>").appendTo(paramElement);
+					contentElement.appendTo(content);
+
+					toggleOpen.on("click", (e) -> {
+						state.open = !state.open;
+						saveState();
+						paramElement.toggleClass("open", state.open);
+					});
+				} else {
+					toggleOpen.remove();
+				}
+			} else {
+				toggleOpen.remove();
+			}
+
+			if (removeItem != null) {
+				paramElement.find("fancy-item-header").get(0).addEventListener("contextmenu", function (e : js.html.MouseEvent) {
+					e.preventDefault();
+					hide.comp.ContextMenu.createFromEvent(e, [
+						{label: "Delete", click: () -> removeItem(i)}
+					]);
+				});
+
+				var menu = paramElement.find(".menu");
+				menu.on("click", (e) -> {
+					e.preventDefault();
+					hide.comp.ContextMenu.createDropdown(menu.get(0), [
+						{label: "Delete", click: () -> removeItem(i)}
+					]);
+				});
+			}			paramElement.find("fancy-item-header").get(0).addEventListener("contextmenu", function (e : js.html.MouseEvent) {
+				e.preventDefault();
+				hide.comp.ContextMenu.createFromEvent(e, [
+					{label: "Delete", click: () -> removeItem(i)}
+				]);
+			});
+
+			var menu = paramElement.find(".menu");
+			menu.on("click", (e) -> {
+				e.preventDefault();
+				hide.comp.ContextMenu.createDropdown(menu.get(0), [
+					{label: "Delete", click: () -> removeItem(i)}
+				]);
+			});
+		}
+	}
+
+	public var reorderItem : (oldIndex: Int, newIndex: Int) -> Void = null;
+
+	/**
+		If left null, no item cannot be added to the list
+	**/
+	public var insertItem : (index: Int) -> Void = null;
+
+	/**
+		If left null, the items cannot be removed from the list
+	**/
+	public var removeItem : (index: Int) -> Void = null;
+
+	/**
+		If left null, the item name is read only
+	**/
+	public var setItemName: (item: T, name: String) -> Void = null;
+
+	/**
+		If left null, or if the function returns null, the item cannot be open
+	**/
+	public var getItemContent: (item: T) -> Element;
+
+	public dynamic function getItems() : Array<T> {
+		return [];
+	}
+
+	public dynamic function getItemName(item: T) : String {
+		return null;
+	}
+
+}

+ 1 - 1
hide/comp/PropsEditor.hx

@@ -69,7 +69,7 @@ class PropsEditor extends Component {
 			var e = new Element('<input type="range" field="${p.name}" step="1">').appendTo(parent);
 			if( min != null ) e.attr("min", "" + min);
 			if(p.def != null) e.attr("value", "" + p.def);
-			e.attr("max", "" + (max == null ? 100 : max));
+			if(max != null) e.attr("max", "" + max);
 		case PFloat(min, max):
 			var e = new Element('<input type="range" field="${p.name}">').appendTo(parent);
 			if(p.def != null) e.attr("value", "" + p.def);

+ 51 - 139
hide/view/animgraph/AnimGraphEditor.hx

@@ -17,7 +17,7 @@ class AnimGraphEditor extends GenericGraphEditor {
     var animGraph : hrt.animgraph.AnimGraph;
     public var previewPrefab : hrt.prefab.Prefab;
 
-    var parametersList : hide.Element;
+    var parametersList : hide.comp.FancyArray<hrt.animgraph.AnimGraph.Parameter>;
     var previewAnimation : AnimGraphInstance = null;
 
     var previewNode : hrt.animgraph.nodes.AnimNode = null;
@@ -50,7 +50,52 @@ class AnimGraphEditor extends GenericGraphEditor {
         addParameterBtn.click((e) -> {
             addParameter();
         });
-        parametersList = new Element("<ul></ul>").appendTo(parameters);
+
+        parametersList = new hide.comp.FancyArray<hrt.animgraph.AnimGraph.Parameter>(parameters, "Parameters", saveDisplayKey);
+        parametersList.getItems = () -> animGraph.parameters;
+        parametersList.getItemName = (param) -> param.name;
+        parametersList.setItemName = (param, name) -> {
+            var prev = param.name;
+            param.name = name;
+            undo.change(Field(param, "name", prev), () ->  {
+                var toRefresh = animGraph.nodes.filter((n) -> Std.downcast(n, hrt.animgraph.nodes.FloatParameter)?.parameter == param);
+                for (node in toRefresh) {
+                    graphEditor.refreshBox(node.id);
+                }
+                parametersList.refresh();
+            });
+            var toRefresh = animGraph.nodes.filter((n) -> Std.downcast(n, hrt.animgraph.nodes.FloatParameter)?.parameter == param);
+            for (node in toRefresh) {
+                graphEditor.refreshBox(node.id);
+            }
+        }
+        parametersList.reorderItem = (oldIndex: Int, newIndex: Int) -> {
+            execMoveParameterTo(oldIndex, newIndex);
+        }
+        parametersList.removeItem = (index: Int) -> {
+            execRemoveParam(index);
+        }
+        parametersList.getItemContent = (param: hrt.animgraph.AnimGraph.Parameter) -> {
+            if (previewAnimation != null) {
+                var props = new Element("<ul>");
+                var slider = new Element('<li><dd>Preview</dd><input type="range" min="-1.0" max="1.0" step="0.01" value="${param.runtimeValue}"></input></li>').appendTo(props).find("input");
+                var range = new hide.comp.Range(null,slider);
+
+                range.setOnChangeUndo(undo, () -> param.runtimeValue, (v:Float) -> {
+                    param.runtimeValue = v;
+                    var runtimeParam = previewAnimation.parameterMap.get(param.name);
+                    if (runtimeParam != null) {
+                        runtimeParam.runtimeValue = param.runtimeValue;
+                    }
+                });
+
+                var def = new Element('<li><dd>Default</dd><input type="range" min="-1.0" max="1.0" step="0.01" value="${param.defaultValue}"></input></li>').appendTo(props).find("input");
+                var range = new hide.comp.Range(null,def);
+                range.setOnChangeUndo(undo, () -> param.defaultValue, (v:Float) -> param.defaultValue = v);
+                return props;
+            }
+            return null;
+        }
 
         refreshPamamList();
 
@@ -65,7 +110,7 @@ class AnimGraphEditor extends GenericGraphEditor {
         new AnimList(propertiesContainer, null, getAnims(scenePreview, {animDirectory: animGraph.animFolder, assetPath: state.path}));
 
         graphEditor.element.get(0).addEventListener("dragover", (e: js.html.DragEvent) -> {
-            if (e.dataTransfer.types.contains("index"))
+            if (e.dataTransfer.types.contains(parametersList.getDragKeyName()))
                 e.preventDefault(); // prevent default to allow drop
 
             if (e.dataTransfer.types.contains(AnimList.dragEventKey))
@@ -78,7 +123,7 @@ class AnimGraphEditor extends GenericGraphEditor {
             // Handle drag from Parameters list
 
 
-            var paramIndex = Std.parseInt(e.dataTransfer.getData("index"));
+            var paramIndex = Std.parseInt(e.dataTransfer.getData(parametersList.getDragKeyName()));
             if (paramIndex != null) {
                 e.preventDefault();
                 var inst = new hrt.animgraph.nodes.FloatParameter();
@@ -321,133 +366,7 @@ class AnimGraphEditor extends GenericGraphEditor {
     }
 
     function refreshPamamList() {
-        parametersList.html("");
-        for (paramIndex => param in animGraph.parameters) {
-            var paramElement = new Element('<graph-parameter>
-                <header>
-                    <div class="reorder ico ico-reorder" draggable="true"></div>
-                    <div class="ico ico-chevron-down toggle-open"></div>
-                    <input type="text" value="${param.name}" class="fill"></input>
-                    <button-2 class="menu"><div class="ico ico-ellipsis-v"/></button-2>
-                </header>
-            </graph-parameter>').appendTo(parametersList);
-
-            var open : Bool = getDisplayState('param.${paramIndex}') ?? false;
-            paramElement.toggleClass("folded", open);
-
-            var name = paramElement.find("input");
-            name.on("change", (e) -> {
-                var prev = param.name;
-                var curr = name.val();
-
-                function exec(isUndo: Bool) {
-                    if (!isUndo) {
-                        param.name = curr;
-                    } else {
-                        param.name = prev;
-                    }
-                    name.val(param.name);
-                    var toRefresh = animGraph.nodes.filter((n) -> Std.downcast(n, hrt.animgraph.nodes.FloatParameter)?.parameter == param);
-                    for (node in toRefresh) {
-                        graphEditor.refreshBox(node.id);
-                    }
-                }
-
-                exec(false);
-                undo.change(Custom(exec));
-            });
-
-            name.on("contextmenu", (e) -> {
-                e.stopPropagation();
-            });
-
-            var toggleOpen = paramElement.find(".toggle-open");
-            toggleOpen.on("click", (e) -> {
-                open = !open;
-                saveDisplayState('param.${paramIndex}', open);
-                paramElement.toggleClass("folded", open);
-            });
-
-            var reorder = paramElement.find(".reorder");
-            reorder.get(0).ondragstart = (e: js.html.DragEvent) -> {
-                e.dataTransfer.setDragImage(paramElement.get(0), Std.int(paramElement.width()), 0);
-
-                e.dataTransfer.setData("index", '${paramIndex}');
-                e.dataTransfer.dropEffect = "move";
-            }
-
-            inline function isAfter(e) {
-                return e.clientY > (paramElement.offset().top + paramElement.outerHeight() / 2.0);
-            }
-
-            paramElement.get(0).addEventListener("dragover", function(e : js.html.DragEvent) {
-                if (!e.dataTransfer.types.contains("index"))
-                    return;
-                var after = isAfter(e);
-                paramElement.toggleClass("hovertop", !after);
-                paramElement.toggleClass("hoverbot", after);
-                e.preventDefault();
-            });
-
-            paramElement.get(0).addEventListener("dragleave", function(e : js.html.DragEvent) {
-                if (!e.dataTransfer.types.contains("index"))
-                    return;
-                paramElement.toggleClass("hovertop", false);
-                paramElement.toggleClass("hoverbot", false);
-            });
-
-            paramElement.get(0).addEventListener("dragenter", function(e : js.html.DragEvent) {
-                if (!e.dataTransfer.types.contains("index"))
-                    return;
-                e.preventDefault();
-            });
-
-            paramElement.get(0).addEventListener("drop", function(e : js.html.DragEvent) {
-                var toMoveIndex = Std.parseInt(e.dataTransfer.getData("index"));
-                paramElement.toggleClass("hovertop", false);
-                paramElement.toggleClass("hoverbot", false);
-                if (paramIndex == null)
-                    return;
-                var after = isAfter(e);
-                execMoveParameterTo(toMoveIndex, paramIndex, after);
-            });
-
-
-            var content = new Element("<content></content>").appendTo(paramElement);
-            var props = new Element("<ul>").appendTo(content);
-            if (previewAnimation != null) {
-                var slider = new Element('<li><dd>Preview</dd><input type="range" min="-1.0" max="1.0" step="0.01" value="${param.runtimeValue}"></input></li>').appendTo(props).find("input");
-                var range = new hide.comp.Range(null,slider);
-
-                range.setOnChangeUndo(undo, () -> param.runtimeValue, (v:Float) -> {
-                    param.runtimeValue = v;
-                    var runtimeParam = previewAnimation.parameterMap.get(param.name);
-                    if (runtimeParam != null) {
-                        runtimeParam.runtimeValue = param.runtimeValue;
-                    }
-                });
-
-                var def = new Element('<li><dd>Default</dd><input type="range" min="-1.0" max="1.0" step="0.01" value="${param.defaultValue}"></input></li>').appendTo(props).find("input");
-                var range = new hide.comp.Range(null,def);
-                range.setOnChangeUndo(undo, () -> param.defaultValue, (v:Float) -> param.defaultValue = v);
-            }
-
-            paramElement.find("header").get(0).addEventListener("contextmenu", function (e : js.html.MouseEvent) {
-                e.preventDefault();
-                hide.comp.ContextMenu.createFromEvent(e, [
-                    {label: "Delete", click: () -> execRemoveParam(paramIndex)}
-                ]);
-            });
-
-            var menu = paramElement.find(".menu");
-            menu.on("click", (e) -> {
-                e.preventDefault();
-                hide.comp.ContextMenu.createDropdown(menu.get(0), [
-                    {label: "Delete", click: () -> execRemoveParam(paramIndex)}
-                ]);
-            });
-        }
-
+        parametersList.refresh();
         scenePreview.onObjectLoaded = () -> {
             setPreview(cast animGraph.nodes.find((f) -> Std.downcast(f, hrt.animgraph.nodes.Output) != null));
         }
@@ -469,14 +388,7 @@ class AnimGraphEditor extends GenericGraphEditor {
         undo.change(Custom(exec));
     }
 
-    function execMoveParameterTo(oldIndex: Int, newIndex: Int, after: Bool) {
-        if (!after) newIndex -= 1;
-		if (oldIndex == newIndex)
-			return;
-        if (newIndex < oldIndex) {
-            newIndex += 1;
-        }
-
+    function execMoveParameterTo(oldIndex: Int, newIndex: Int) {
 		function exec(isUndo: Bool) {
             if (!isUndo) {
                 var param = animGraph.parameters.splice(oldIndex, 1)[0];

+ 311 - 19
hide/view/shadereditor/ShaderEditor.hx

@@ -138,6 +138,9 @@ class PreviewSettings {
 	public var height : Int = 300;
 	public function new() {};
 }
+
+@:access(hrt.shgraph.ShaderGraph)
+@:access(hrt.shgraph.Graph)
 class ShaderEditor extends hide.view.FileView implements GraphInterface.IGraphEditor {
 	var graphEditor : hide.view.GraphEditor;
 	var shaderGraph : hrt.shgraph.ShaderGraph;
@@ -164,6 +167,7 @@ class ShaderEditor extends hide.view.FileView implements GraphInterface.IGraphEd
 	var meshPreviewRenderPropsRoot : h3d.scene.Object;
 
 	var parametersList : JQuery;
+	var variableList : hide.comp.FancyArray<ShaderGraphVariable>;
 
 	var previewElem : Element;
 	var draggedParamId : Int;
@@ -203,25 +207,56 @@ class ShaderEditor extends hide.view.FileView implements GraphInterface.IGraphEd
 			graphEditor.centerView();
 		}, 50);
 
-		graphEditor.element.on("drop" ,function(e) {
-			var posCursor = new Point(graphEditor.lX(ide.mouseX - 25), graphEditor.lY(ide.mouseY - 10));
+		graphEditor.element.get(0).ondrop = (e:js.html.DragEvent) -> {
+			var posCursor = new Point(graphEditor.lX(e.clientX - 25), graphEditor.lY(e.clientY-25));
+
+			function addNode(inst: ShaderNode) : Void {
+				@:privateAccess var id = currentGraph.current_node_id++;
+				inst.id = id;
+				inst.setPos(posCursor);
+				inst.graph = currentGraph;
+
+				graphEditor.opBox(inst, true, graphEditor.currentUndoBuffer);
+				graphEditor.commitUndo();
+			}
+			if (e.dataTransfer.types.contains(variableList.getDragKeyName())) {
+				var index = Std.parseInt(e.dataTransfer.getData(variableList.getDragKeyName()));
+				var hasAnyWrite = false;
+				shaderGraph.mapShaderVar((v) -> {
+					if (v.varId == index && Std.downcast(v, hrt.shgraph.nodes.VarWrite) != null) {
+						hasAnyWrite = true;
+						return false;
+					}
+					return true;
+				});
+
+				if (hasAnyWrite) {
+					var read = new hrt.shgraph.nodes.VarRead();
+					read.varId = index;
+					addNode(read);
+				} else {
+					hide.comp.ContextMenu.createFromPoint(e.clientX, e.clientY, [{
+						label: "Write", click: () -> {
+							var write = new hrt.shgraph.nodes.VarWrite();
+							write.varId = index;
+							addNode(write);
+						}
+					},
+					{
+						label: "Read", click: () -> {
+							var read = new hrt.shgraph.nodes.VarRead();
+							read.varId = index;
+							addNode(read);
+						}
+					}]);
+				}
+				return;
+			}
+
 			var inst = new ShaderParam();
-			@:privateAccess var id = currentGraph.current_node_id++;
-			inst.id = id;
 			inst.parameterId = draggedParamId;
-			inst.shaderGraph = shaderGraph;
-			inst.setPos(posCursor);
-
-			graphEditor.opBox(inst, true, graphEditor.currentUndoBuffer);
-			graphEditor.commitUndo();
-			// var node = Std.downcast(currentGraph.addNode(posCursor.x, posCursor.y, ShaderParam, []), ShaderParam);
-			// node.parameterId = draggedParamId;
-			// var paramShader = shaderGraph.getParameter(draggedParamId);
-			// node.variable = paramShader.variable;
-			// node.setName(paramShader.name);
-			//setDisplayValue(node, paramShader.type, paramShader.defaultValue);
-			//addBox(posCursor, ShaderParam, node);
-		});
+			addNode(inst);
+		};
 
 		var rightPannel = new Element(
 			'<div id="rightPanel">
@@ -230,9 +265,19 @@ class ShaderEditor extends hide.view.FileView implements GraphInterface.IGraphEd
 
 					<div id="parametersList" class="hide-scene-tree hide-list">
 					</div>
+
+					<input id="createParameter" type="button" value="Add parameter" />
 				</div>
+
+
+				<div class="hide-block flexible" >
+					<fancy-array class="variables" style="flex-grow: 1">
+
+					</fancy-array>
+					<fancy-button class="fancy-small add-variable">Add Variable</fancy-button>
+				</div>
+
 				<div class="options-block hide-block">
-					<input id="createParameter" type="button" value="Add parameter" />
 					<div>
 						Shader :
 						<select id="domainSelection"></select>
@@ -247,6 +292,47 @@ class ShaderEditor extends hide.view.FileView implements GraphInterface.IGraphEd
 			</div>'
 		);
 
+		variableList = new hide.comp.FancyArray(null, rightPannel.find(".variables"), "variables", "variables");
+
+		variableList.getItems = () -> shaderGraph.variables;
+		variableList.getItemName = (v: ShaderGraphVariable) -> v.name;
+		variableList.reorderItem = moveVariable;
+		variableList.removeItem = removeVariable;
+		variableList.setItemName = renameVariable;
+		variableList.getItemContent = getVariableContent;
+
+		variableList.refresh();
+
+		var addVariable = rightPannel.find(".add-variable");
+		addVariable.on("click", (e) -> {
+			hide.comp.ContextMenu.createDropdown(addVariable.get(0), [
+				{
+					label: "Int",
+					click: () -> createVariable(SgInt),
+				},
+				{
+					label: "Float",
+					click: () -> createVariable(SgFloat(1)),
+				},
+				{
+					label: "Vec 2",
+					click: () -> createVariable(SgFloat(2)),
+				},
+				{
+					label: "Vec 3",
+					click: () -> createVariable(SgFloat(3)),
+				},
+				{
+					label: "Vec 4",
+					click: () -> createVariable(SgFloat(4)),
+				},
+				{
+					label: "Color",
+					click: () -> createVariable(SgFloat(4), true),
+				},
+			]);
+		});
+
 		rightPannel.find("#centerView").click((e) -> graphEditor.centerView());
 
 		domainSelection = rightPannel.find("#domainSelection");
@@ -402,6 +488,7 @@ class ShaderEditor extends hide.view.FileView implements GraphInterface.IGraphEd
 			domainSelection.val(haxe.EnumTools.EnumValueTools.getName(curr));
 			graphEditor.reload();
 			graphEditor.centerView();
+			requestRecompile();
 		}
 
 		exec(false);
@@ -410,6 +497,211 @@ class ShaderEditor extends hide.view.FileView implements GraphInterface.IGraphEd
 		}
 	}
 
+
+	function createVariable(type: SgType, isColor: Bool = false) {
+		var name = "New Variable";
+		var i = 0;
+		var index = 0;
+		while(i < shaderGraph.variables.length) {
+			if (shaderGraph.variables[i].name == name) {
+				i = 0;
+				index ++;
+				name = 'New Variable ($index)';
+			} else {
+				i++;
+			}
+		}
+
+		var variable : ShaderGraphVariable = {
+			name: name,
+			type: type,
+			defValue: hrt.shgraph.ShaderGraph.getSgTypeDefVal(type),
+			isColor: isColor,
+		}
+
+		function exec(isUndo: Bool) {
+			if (!isUndo) {
+				shaderGraph.variables.push(variable);
+			}
+			else {
+				shaderGraph.variables.remove(variable);
+			}
+			variableList.refresh();
+			requestRecompile();
+		}
+		exec(false);
+		undo.change(Custom(exec));
+	}
+
+	var validNameCheck = ~/^[_a-zA-Z][_a-zA-Z0-9]*$/;
+
+	function renameVariable(variable: ShaderGraphVariable, newName: String) {
+		if (!validNameCheck.match(newName))
+		{
+			variableList.refresh();
+			ide.quickError('"$newName" is not a valid variable name (must start with _ or a letter, and only contains letters, numbers and underscores)');
+			return;
+		}
+
+		var oldName = variable.name;
+		function exec(isUndo: Bool) {
+			variable.name = !isUndo ? newName : oldName;
+			variableList.refresh();
+
+			var index = shaderGraph.variables.indexOf(variable);
+			currentGraph.mapShaderVar((variable: hrt.shgraph.nodes.ShaderVar) -> {
+				if (variable.varId == index) {
+					graphEditor.refreshBox(variable.id);
+				}
+				return true;
+			});
+
+			for (node in currentGraph.nodes) {
+
+			}
+		}
+
+		exec(false);
+		undo.change(Custom(exec));
+	}
+
+	function getVariableContent(variable: ShaderGraphVariable) {
+		var e = new Element('<div><div>Def value</div></div>');
+
+		switch(variable.type) {
+			case SgFloat(n):
+				if (n >= 3 && variable.isColor) {
+					hide.comp.PropsEditor.makePropEl({name: "defValue", t: PColor}, e);
+				} else {
+					hide.comp.PropsEditor.makePropEl({name: "defValue", t: PVec(n)}, e);
+				}
+
+				if (n >= 3) {
+					var colorCheckbox = new Element('<div>Is Color</div>').appendTo(e);
+					hide.comp.PropsEditor.makePropEl({name: "isColor", t: PBool}, colorCheckbox);
+				}
+			case SgInt:
+				hide.comp.PropsEditor.makePropEl({name: "defValue", t: PInt()}, e);
+			default:
+				throw "Unsupported variable type";
+		}
+
+		var globalCheckbox = new Element('<div title="If the variable is set to global, it will use the exact same name in the generated shader code, allowing it to be shared between multiple shaders in the shaderlist">Is Global <input type="checkbox"/></div>').appendTo(e);
+		var cb = globalCheckbox.find("input");
+		cb.prop("checked", variable.isGlobal);
+		cb.on("change", (e) -> {
+			var old = variable.isGlobal;
+			var val = cb.prop("checked");
+			variable.isGlobal = val;
+			for (graph in shaderGraph.graphs) {
+				if (graph.hasCycle()) {
+					variable.isGlobal = old;
+					ide.quickError('Cannot change isGlobal because variable write and reads are dependant on each other, and isGlobal change the order of the read and writes and it would create a cycle', 10.0);
+					variableList.refresh();
+					return;
+				}
+			}
+
+			undo.change(Field(variable, "isGlobal", old), () -> {
+				requestRecompile();
+				variableList.refresh();
+			});
+			requestRecompile();
+		});
+
+		var editRoot = new Element();
+		var edit = new hide.comp.PropsEditor(undo, editRoot);
+		edit.add(e, variable, (name: String) -> {
+			if (name == "isColor") {
+				variableList.refresh();
+			}
+			else if(StringTools.contains(name, "defValue")) {
+				requestRecompile();
+			}
+		});
+		return e;
+	}
+
+	function moveVariable(oldIndex: Int, newIndex: Int) {
+		var graph = currentGraph;
+		var remap: Array<Int> = [];
+		function exec(isUndo: Bool) {
+			if (!isUndo) {
+				var oldOrder = shaderGraph.variables.copy();
+				var rem = shaderGraph.variables.splice(oldIndex, 1);
+				shaderGraph.variables.insert(newIndex, rem[0]);
+
+				for (oldIndex => v in oldOrder) {
+					remap[oldIndex] = shaderGraph.variables.indexOf(v);
+				}
+
+				shaderGraph.mapShaderVar((v) -> {
+					v.varId = remap[v.varId];
+					return true;
+				});
+			}
+			else {
+				var rem = shaderGraph.variables.splice(newIndex, 1);
+				shaderGraph.variables.insert(oldIndex, rem[0]);
+
+				shaderGraph.mapShaderVar((v) -> {
+					v.varId = remap.indexOf(v.varId);
+					return true;
+				});
+			}
+			variableList.refresh();
+			requestRecompile();
+		}
+		exec(false);
+		undo.change(Custom(exec));
+	}
+
+	function removeVariable(index: Int) {
+		var usedInGraph = false;
+		shaderGraph.mapShaderVar((v) -> {
+			if (v.varId == index) {
+				usedInGraph = true;
+				return false;
+			}
+			return true;
+		});
+
+		if (usedInGraph) {
+			hide.Ide.inst.quickError("Can't remove, variable is used in this Shader Graph");
+			return;
+		}
+
+		var variable = shaderGraph.variables[index];
+		function exec(isUndo: Bool) {
+			if (!isUndo) {
+				shaderGraph.variables.splice(index, 1);
+
+				// fix id of variables above ours
+				shaderGraph.mapShaderVar((v) -> {
+					if (v.varId > index) {
+						v.varId --;
+					}
+					return true;
+				});
+			}
+			else {
+				shaderGraph.variables.insert(index, variable);
+
+				// fix id of variables above ours
+				shaderGraph.mapShaderVar((v) -> {
+					if (v.varId > index-1) {
+						v.varId ++;
+					}
+					return true;
+				});
+			}
+			variableList.refresh();
+			requestRecompile();
+		}
+		exec(false);
+		undo.change(Custom(exec));
+	}
+
 	function createParameter(type : HxslType) {
 		@:privateAccess var paramShaderID : Int = shaderGraph.current_param_id++;
 		@:privateAccess
@@ -1538,7 +1830,7 @@ class ShaderEditor extends hide.view.FileView implements GraphInterface.IGraphEd
 	}
 
 	public function unserializeNode(data : Dynamic, newId : Bool) : IGraphNode {
-		var node = ShaderNode.createFromDynamic(data, shaderGraph);
+		var node = ShaderNode.createFromDynamic(data, currentGraph);
 		if (newId) {
 			@:privateAccess var newId = currentGraph.current_node_id++;
 			node.setId(newId);

+ 13 - 0
hrt/shgraph/AstTools.hx

@@ -23,6 +23,19 @@ class AstTools {
 		);
 	}
 
+	public static function makeDynamic(type: Type, value: Dynamic) : TExpr {
+		switch(type) {
+			case TInt:
+				return makeInt(value is Int ? cast value : 0);
+			case TFloat:
+				return makeFloat(value is Float ? cast value : 0);
+			case TVec(size, VFloat):
+				return makeVec((value is Array && value.length == size) ? cast value : [for (_ in 0...size) 0.0]);
+			default:
+				throw "unsupported type " + type;
+		}
+	}
+
 	public inline static function makeAssign(to: TExpr, from: TExpr) : TExpr {
 		return makeExpr(TBinop(OpAssign, to, from), to.t);
 	}

+ 21 - 3
hrt/shgraph/NodeGenContext.hx

@@ -9,8 +9,8 @@ import hrt.shgraph.ShaderGraph;
 import hrt.shgraph.ShaderNode;
 
 class NodeGenContextSubGraph extends NodeGenContext {
-	public function new(parentCtx : NodeGenContext) {
-		super(parentCtx?.domain ?? Fragment);
+	public function new(graph: ShaderGraph.Graph, parentCtx : NodeGenContext) {
+		super(graph, parentCtx?.domain ?? Fragment);
 		this.parentCtx = parentCtx;
 	}
 
@@ -77,9 +77,11 @@ class NodeGenContext {
 	// Pour les rares nodes qui ont besoin de differencier entre vertex et fragment
 	public var domain : ShaderGraph.Domain;
 	public var previewDomain: ShaderGraph.Domain = null;
+	public var graph: ShaderGraph.Graph = null;
 
-	public function new(domain: ShaderGraph.Domain) {
+	public function new(graph: ShaderGraph.Graph, domain: ShaderGraph.Domain) {
 		this.domain = domain;
+		this.graph = graph;
 	}
 
 	// For general input/output of the shader graph. Allocate a new global var if name is not found,
@@ -121,6 +123,21 @@ class NodeGenContext {
 		expressions.push(makeAssign(v, expr));
 	}
 
+	public function getShaderVariable(id: Int, init: TExpr = null) : TVar {
+		var graphVar = graph.parent.variables[id];
+		var type = ShaderGraph.sgTypeToType(graphVar.type);
+		var variable = MapUtils.getOrPut(shaderVariables, id, {
+			var varId = hxsl.Ast.Tools.allocVarId();
+			var name = if (graphVar.isGlobal) graphVar.name else '_local_${graphVar.name}_$varId';
+			{variable: {id: varId, name: name, type: type, kind: Local}, isInit: false}
+		});
+		if (init != null && !variable.isInit) {
+			variable.isInit = true;
+			addExpr(AstTools.makeAssign(AstTools.makeVar(variable.variable), init));
+		}
+		return variable.variable;
+	}
+
 	function getOrAllocateFromTVar(tvar: TVar) : TVar {
 		var fullName = AstTools.getFullName(tvar);
 
@@ -365,4 +382,5 @@ class NodeGenContext {
 
 	var nodeInputInfo : Array<InputInfo>;
 	var globalVars: Map<String, ShaderGraph.ExternVarDef> = [];
+	var shaderVariables: Map<Int, {variable: TVar, isInit: Bool}> = [];
 }

+ 172 - 10
hrt/shgraph/ShaderGraph.hx

@@ -61,6 +61,28 @@ enum SgType {
 	SgGeneric(id: Int, constraint: (newType: Type, previousType: Type) -> Null<Type>);
 }
 
+function serializeSgType(t: SgType) : String {
+	switch(t) {
+		case SgFloat(dimmension):
+			return 'SgFloat,$dimmension';
+		case SgSampler, SgInt, SgBool:
+			return EnumValueTools.getName(t);
+		case SgGeneric(_,_):
+			throw "Can't serialize generic variables";
+	}
+}
+
+function unserializeSgType(s: String) : SgType {
+	var split = s.split(",");
+	var name = split.shift();
+	switch(name) {
+		case "SgFloat":
+			return SgFloat(Std.parseInt(split[0]));
+		default:
+			return std.Type.createEnum(SgType, name);
+	}
+}
+
 function typeToSgType(t: Type) : SgType {
 	return switch(t) {
 		case TFloat:
@@ -95,6 +117,21 @@ function sgTypeToType(t: SgType) : Type {
 	}
 }
 
+function getSgTypeDefVal(t: SgType) : Dynamic {
+	return switch(t) {
+		case SgBool:
+			return false;
+		case SgFloat(1):
+			return 0.0;
+		case SgFloat(n):
+			return [for (i in 0...n) 0.0];
+		case SgInt:
+			return 0;
+		default:
+			throw "Can't have default value for type " + t;
+	}
+}
+
 function ConstraintFloat(newType: Type, previousType: Type) : Null<Type> {
 	function getN(type:Type) {
 		return switch(type) {
@@ -121,8 +158,6 @@ function ConstraintFloat(newType: Type, previousType: Type) : Null<Type> {
 	}
 }
 
-
-
 typedef ShaderNodeDefInVar = {v: TVar, internal: Bool, ?defVal: ShaderDefInput, isDynamic: Bool};
 typedef ShaderNodeDefOutVar = {v: TVar, internal: Bool, isDynamic: Bool};
 typedef ShaderNodeDef = {
@@ -173,9 +208,19 @@ ExternVarDef {
 	var paramIndex: Null<Int> = null;
 }
 
+@:structInit @:publicFields
+class ShaderGraphVariable {
+	var name: String;
+	var type: SgType;
+	var defValue: Dynamic;
+	var isColor: Bool = false;
+	var isGlobal: Bool = false;
+}
+
 @:access(hrt.shgraph.Graph)
 class ShaderGraphGenContext {
 	var graph : Graph;
+
 	var includePreviews : Bool;
 
 	public function new(graph: Graph, includePreviews: Bool = false) {
@@ -202,7 +247,7 @@ class ShaderGraphGenContext {
 		initNodes();
 		var sortedNodes = sortGraph();
 
-		genContext = genContext ?? new NodeGenContext(graph.domain);
+		genContext = genContext ?? new NodeGenContext(graph, graph.domain);
 		var expressions : Array<TExpr> = [];
 		genContext.expressions = expressions;
 
@@ -267,14 +312,43 @@ class ShaderGraphGenContext {
 			var empty = true;
 			var inputs = inst.getInputs();
 
+			var asShaderVar = Std.downcast(node.node, hrt.shgraph.nodes.ShaderVar);
+			if (asShaderVar != null) {
+				var variable = graph.parent.variables[asShaderVar.varId];
+
+				if (variable.isGlobal) {
+					// Global Write depend on their Read counterpart (because the write must happen after all the reads)
+					var write = Std.downcast(asShaderVar, hrt.shgraph.nodes.VarWrite);
+					if (write != null) {
+						for (node in nodes) {
+							var asRead = Std.downcast(node?.node, hrt.shgraph.nodes.VarRead);
+							if (asRead == null || asRead.varId != write.varId)
+								continue;
+							nodeTopology[asRead.id].to.push(id);
+							nodeTopology[id].incoming ++;
+							empty = false;
+						}
+					}
+
+				} else {
+					// Local Reads depend on their Write counterpart (because all the reads must happen after the write)
+					var write = graph.findLocalVarWrite(Std.downcast(node.node, hrt.shgraph.nodes.VarRead));
+					if (write != null) {
+						nodeTopology[write.id].to.push(id);
+						nodeTopology[id].incoming ++;
+						empty = false;
+					}
+				}
+			}
+
 
-			// Todo : store ID of input in connections instead of relying on the "name" at runtime
 			for (inputId => connection in inst.connections) {
 				if (connection == null)
 					continue;
 				empty = false;
 				var nodeOutputs = connection.from.getOutputs();
 				var outputs = nodes[connection.from.id].outputs;
+
 				if (outputs == null) {
 					outputs = [];
 					nodes[connection.from.id].outputs = [];
@@ -294,8 +368,6 @@ class ShaderGraphGenContext {
 				nodeTopology[id].incoming ++;
 				totalEdges++;
 			}
-			for (inputId => input in inputs) {
-			}
 			if (empty) {
 				nodeToExplore.push(id);
 			}
@@ -323,10 +395,11 @@ class ShaderGraphGenContext {
 		return sortedNodes;
 	}
 }
-
+@:privateAccess(hrt.shgraph.Graph)
 class ShaderGraph extends hrt.prefab.Prefab {
 
 	var graphs : Array<Graph> = [];
+	public var variables : Array<ShaderGraphVariable> = [];
 
 	var cachedDef : hrt.prefab.Cache.ShaderDef = null;
 
@@ -338,6 +411,16 @@ class ShaderGraph extends hrt.prefab.Prefab {
 		parametersAvailable = [];
 		parametersKeys = [];
 
+		for (variable in json.variables ?? []) {
+			variables.push({
+				name: variable.name,
+				type: unserializeSgType(variable.type),
+				defValue: variable.defValue,
+				isColor: variable.isColor,
+				isGlobal: variable.isGlobal,
+			});
+		}
+
 		loadParameters(json.parameters ?? []);
 		for (domain in haxe.EnumTools.getConstructors(Domain)) {
 			var graph = new Graph(this, haxe.EnumTools.createByName(Domain, domain));
@@ -361,6 +444,18 @@ class ShaderGraph extends hrt.prefab.Prefab {
 			for (p in parametersAvailable) { id : p.id, name : p.name, type : [p.type.getName(), p.type.getParameters().toString()], defaultValue : p.defaultValue, index : p.index, internal : p.internal }
 		];
 
+		json.variables = [
+			for (variable in variables) {
+				{
+					name: variable.name,
+					type: serializeSgType(variable.type),
+					defValue: variable.defValue,
+					isColor: variable.isColor,
+					isGlobal: variable.isGlobal,
+				}
+			}
+		];
+
 		for (graph in graphs) {
 			var serName = EnumValueTools.getName(graph.domain);
 			Reflect.setField(json, serName, graph.saveToDynamic());
@@ -418,13 +513,14 @@ class ShaderGraph extends hrt.prefab.Prefab {
 		};
 
 
-		var nodeGen = new NodeGenContext(Vertex);
+		var nodeGen = new NodeGenContext(null, Vertex);
 		nodeGen.previewDomain = previewDomain;
 
 		for (i => graph in graphs) {
 			if (previewDomain != null && previewDomain != graph.domain)
 				continue;
 			nodeGen.domain = graph.domain;
+			nodeGen.graph = graph;
 			var ctx = new ShaderGraphGenContext(graph);
 			var gen = ctx.generate(nodeGen);
 
@@ -519,6 +615,12 @@ class ShaderGraph extends hrt.prefab.Prefab {
 			}
 		}
 
+		for (id => variable in nodeGen.shaderVariables) {
+			var initExpr = AstTools.makeAssign(AstTools.makeVar(variable.variable), AstTools.makeDynamic(variable.variable.type, this.variables[id].defValue));
+			__init__exprs.push(initExpr);
+			shaderData.vars.push(variable.variable);
+		}
+
 		if (__init__exprs.length != 0) {
 			var funcVar : TVar = {
 				name : "__init__",
@@ -672,6 +774,16 @@ class ShaderGraph extends hrt.prefab.Prefab {
 		}
 	}
 
+	/**
+		Iterate on all the shaderVars in the graph, breaking if the cb return false
+	**/
+	public function mapShaderVar(cb: (v: hrt.shgraph.nodes.ShaderVar) -> Bool) {
+		for (graph in graphs) {
+			if (!graph.mapShaderVar(cb))
+				return;
+		}
+	}
+
 	public function getGraph(domain: Domain) {
 		return graphs[domain.getIndex()];
 	}
@@ -702,7 +814,7 @@ class Graph {
 	public function generate(nodes : Array<Dynamic>, edges : Array<Edge>) {
 		current_node_id = 0;
 		for (n in nodes) {
-			var node = ShaderNode.createFromDynamic(n, parent);
+			var node = ShaderNode.createFromDynamic(n, this);
 			this.nodes.set(node.id, node);
 			current_node_id = hxd.Math.imax(current_node_id, node.id+1);
 		}
@@ -751,6 +863,34 @@ class Graph {
 						return true;
 				}
 			}
+
+			var asShaderVar = Std.downcast(node, hrt.shgraph.nodes.ShaderVar);
+			if (asShaderVar != null) {
+				var variable = parent.variables[asShaderVar.varId];
+
+				if (variable.isGlobal) {
+					// Global Write depend on their Read counterpart (because the write must happen after all the reads)
+					var write = Std.downcast(asShaderVar, hrt.shgraph.nodes.VarWrite);
+					if (write != null) {
+						for (node in nodes) {
+							var asRead = Std.downcast(node, hrt.shgraph.nodes.VarRead);
+							if (asRead == null || asRead.varId != write.varId)
+								continue;
+							if (hasCycle(asRead, visited))
+								return true;
+						}
+					}
+
+				} else {
+					// Local Reads depend on their Write counterpart (because all the reads must happen after the write)
+					var write = findLocalVarWrite(Std.downcast(node, hrt.shgraph.nodes.VarRead));
+					if (write != null) {
+						if (hasCycle(write, visited))
+							return true;
+					}
+				}
+			}
+
 			return false;
 		}
 
@@ -766,6 +906,12 @@ class Graph {
 		return true;
 	}
 
+	public function findLocalVarWrite(read: hrt.shgraph.nodes.VarRead) : Null<hrt.shgraph.nodes.VarWrite> {
+		if (read == null)
+			return null;
+		return cast nodes.find((f) -> Std.downcast(f, hrt.shgraph.nodes.VarWrite)?.varId == read.varId);
+	}
+
 	public function addEdge(edge : Edge, checkCycles: Bool = true) {
 		var node = this.nodes.get(edge.inputNodeId);
 		var output = this.nodes.get(edge.outputNodeId);
@@ -890,14 +1036,30 @@ class Graph {
 				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
+			edges: edgesJson,
 		};
 
 		return json;
 	}
 
+	/**
+		Iterate on all the shaderVars in the graph, breaking if the cb return false
+	**/
+	public function mapShaderVar(cb: (v: hrt.shgraph.nodes.ShaderVar) -> Bool) {
+		for (node in nodes) {
+			var asVar = Std.downcast(node, hrt.shgraph.nodes.ShaderVar);
+			if (asVar != null) {
+				if (cb(asVar) == false) {
+					return false;
+				}
+			}
+		}
+		return true;
+	}
+
 }

+ 4 - 5
hrt/shgraph/ShaderNode.hx

@@ -53,6 +53,8 @@ implements hide.view.GraphInterface.IGraphNode
 	public var x : Float;
 	public var y : Float;
 	public var showPreview : Bool = true;
+	public var graph : ShaderGraph.Graph;
+
 	@prop public var nameOverride : String;
 
 
@@ -156,16 +158,13 @@ implements hide.view.GraphInterface.IGraphNode
 		};
 	}
 
-	public static function createFromDynamic(data: Dynamic, graph: ShaderGraph) : ShaderNode {
+	public static function createFromDynamic(data: Dynamic, graph: ShaderGraph.Graph) : 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.graph = graph;
 		inst.connections = [];
 		inst.loadProperties(data.properties);
 		return inst;

+ 1 - 3
hrt/shgraph/ShaderParam.hx

@@ -9,8 +9,6 @@ class ShaderParam extends ShaderNode {
 	@prop() public var parameterId : Int;
 	@prop() public var perInstance : Bool;
 
-	public var shaderGraph : ShaderGraph;
-
 	public function new() {
 
 	}
@@ -46,7 +44,7 @@ class ShaderParam extends ShaderNode {
 	}
 
 	function getVariable() : TVar {
-		return shaderGraph.getParameter(parameterId).variable;
+		return graph.parent.getParameter(parameterId).variable;
 	}
 
 	override public function loadProperties(props : Dynamic) {

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

@@ -2,5 +2,4 @@ package hrt.shgraph.nodes;
 
 abstract class ShaderVar extends ShaderNode {
 	@prop() public var varId : Int = 0;
-	var graph: Graph;
 }

+ 3 - 3
hrt/shgraph/nodes/SubGraph.hx

@@ -25,7 +25,7 @@ class SubGraph extends ShaderNode {
 		var graph = shader.getGraph(ctx.domain);
 
 		var genCtx = new ShaderGraphGenContext(graph, false);
-		genCtx.generate(new NodeGenContext.NodeGenContextSubGraph(ctx));
+		genCtx.generate(new NodeGenContext.NodeGenContextSubGraph(graph, ctx));
 	}
 
 	override public function getInputs() : Array<ShaderNode.InputInfo> {
@@ -37,7 +37,7 @@ class SubGraph extends ShaderNode {
 		var graph = shader.getGraph(hrt.shgraph.ShaderGraph.Domain.Fragment);
 
 		var genCtx = new ShaderGraphGenContext(graph, false);
-		var nodeGenCtx = new NodeGenContext.NodeGenContextSubGraph(null);
+		var nodeGenCtx = new NodeGenContext.NodeGenContextSubGraph(graph, null);
 		genCtx.generate(nodeGenCtx);
 		var inputs: Array<ShaderNode.InputInfo> = [];
 
@@ -59,7 +59,7 @@ class SubGraph extends ShaderNode {
 		var graph = shader.getGraph(hrt.shgraph.ShaderGraph.Domain.Fragment);
 
 		var genCtx = new ShaderGraphGenContext(graph, false);
-		var nodeGenCtx = new NodeGenContext.NodeGenContextSubGraph(null);
+		var nodeGenCtx = new NodeGenContext.NodeGenContextSubGraph(graph, null);
 		genCtx.generate(nodeGenCtx);
 		var outputs: Array<ShaderNode.InputInfo> = [];
 

+ 32 - 0
hrt/shgraph/nodes/VarRead.hx

@@ -0,0 +1,32 @@
+package hrt.shgraph.nodes;
+
+
+@name("Var Read")
+@description("Read a value from a local variable")
+@width(80)
+@group("Variables")
+class VarRead extends ShaderVar {
+
+	public function new() {
+	}
+
+	var outputs: Array<ShaderNode.OutputInfo>;
+	override public function getOutputs() : Array<ShaderNode.OutputInfo> {
+		if (outputs == null) {
+			// cache the output array to avoid multiple allocations
+			outputs = [{name:"error", type: SgBool}];
+		}
+		// reassign name and type in case they have changed since the last getOutput
+		outputs[0].name = graph.parent.variables[varId]?.name ?? "error";
+		outputs[0].type = graph.parent.variables[varId]?.type ?? SgBool;
+		return outputs;
+	}
+
+	override function generate(ctx:NodeGenContext) {
+		var out = AstTools.makeVar(ctx.getShaderVariable(varId));
+		ctx.setOutput(0, out);
+		#if editor
+		ctx.addPreview(out);
+		#end
+	}
+}

+ 28 - 0
hrt/shgraph/nodes/VarWrite.hx

@@ -0,0 +1,28 @@
+package hrt.shgraph.nodes;
+
+@name("Var Write")
+@description("Write a value to a local variable")
+@width(80)
+@group("Variables")
+class VarWrite extends ShaderVar {
+
+	public function new() {
+	}
+
+	var inputs: Array<ShaderNode.InputInfo>;
+	override public function getInputs() : Array<ShaderNode.InputInfo> {
+		if (inputs == null) {
+			inputs = [{name:"error", type: SgBool}];
+		}
+		// reassign name and type in case they have changed since the last getInput
+		inputs[0].name = graph.parent.variables[varId]?.name ?? "error";
+		inputs[0].type = graph.parent.variables[varId]?.type ?? SgBool;
+		return inputs;
+	}
+
+	override function generate(ctx:NodeGenContext) {
+		var input = ctx.getInput(0);
+		var tVar = ctx.getShaderVariable(varId, input);
+		ctx.addPreview(AstTools.makeVar(tVar));
+	}
+}