瀏覽代碼

[shgraph] Variable editing

Clément Espeute 7 月之前
父節點
當前提交
24e6a494c7
共有 7 個文件被更改,包括 613 次插入160 次删除
  1. 134 72
      bin/style.css
  2. 270 74
      bin/style.less
  3. 14 10
      hide/comp/FancyArray.hx
  4. 129 1
      hide/view/shadereditor/ShaderEditor.hx
  5. 50 3
      hrt/shgraph/ShaderGraph.hx
  6. 8 0
      hrt/shgraph/nodes/VarRead.hx
  7. 8 0
      hrt/shgraph/nodes/VarWrite.hx

+ 134 - 72
bin/style.css

@@ -4352,9 +4352,102 @@ graph-editor-root properties-container .anim-list {
   flex-grow: 0;
   flex-shrink: 0;
 }
+: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-smallfancy-button {
+  --size: 20px;
+}
+.fancy-normal fancy-button,
+.fancy-normalfancy-button {
+  --size: 28px;
+}
+.fancy-big fancy-button,
+.fancy-bigfancy-button {
+  --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;
+  justify-content: space-evenly;
+  user-select: none;
+  gap: 0.5em;
+  font-size: calc(var(--size) * 0.5);
+  margin-left: -1px;
+  --radius: 3px;
+}
+fancy-button:has(.label) {
+  padding-left: 1em;
+  padding-right: 1em;
+}
+fancy-button .label {
+  font-weight: 200;
+  text-rendering: optimizeLegibility;
+}
+fancy-button:hover {
+  background-color: #555;
+}
+fancy-button:active {
+  translate: 0px 1px;
+}
+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;
   align-items: stretch;
@@ -4364,93 +4457,60 @@ fancy-array {
 fancy-array * {
   box-sizing: border-box;
 }
+fancy-array fancy-array {
+  padding-right: -1px;
+}
 fancy-array fancy-items {
   display: flex;
   flex-direction: column;
-  align-items: stretch;
-  background-color: var(--bg-3);
-  gap: 1px;
+  gap: 2px;
+  justify-content: stretch;
 }
 fancy-array fancy-items fancy-item {
   display: flex;
   flex-direction: column;
-  justify-items: stretch;
-  border-radius: var(--basic-border-radius);
   position: relative;
-  background-color: var(--bg-3);
 }
-fancy-array fancy-items fancy-item header {
+fancy-array fancy-items fancy-item fancy-item-header {
   display: flex;
-  align-items: center;
-  background-color: var(--bg-2);
-  padding: calc(var(--basic-padding) * 2);
-  gap: 3px;
+  color: var(--fancy-quiet-text-color);
+  border-radius: 3px;
+  background-color: #444;
 }
-fancy-array fancy-items fancy-item header .fill {
+fancy-array fancy-items fancy-item fancy-item-header > input[type="text"] {
+  min-width: 0;
+  width: 0;
   flex-grow: 1;
-}
-fancy-array fancy-items fancy-item header input {
-  border-radius: var(--basic-border-radius);
-  background-color: var(--bg-3);
+  background: none;
+  outline: none;
   border: none;
+  font-size: 14px;
 }
-fancy-array fancy-items fancy-item header input:focus {
-  outline: var(--basic-border);
-}
-fancy-array fancy-items fancy-item header .reorder {
-  padding: var(--basic-padding);
-}
-fancy-array fancy-items fancy-item header .reorder,
-fancy-array fancy-items fancy-item header .toggle-open {
-  color: #777;
-}
-fancy-array fancy-items fancy-item header .reorder:hover,
-fancy-array fancy-items fancy-item header .toggle-open:hover {
-  color: #AAA;
-}
-fancy-array fancy-items fancy-item content {
-  border-top: var(--basic-border);
-  padding: var(--basic-padding);
-}
-fancy-array fancy-items fancy-item content > 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;
-}
-fancy-array fancy-items fancy-item content > ul li {
-  grid-column: 1 / -1;
-  display: grid;
-  grid-template-columns: subgrid;
-  align-items: center;
-}
-fancy-array fancy-items fancy-item content > ul li dd {
-  margin: 0;
-  text-align: right;
-}
-fancy-array fancy-items fancy-item content > ul li > .hide-range {
-  display: flex;
-}
-fancy-array fancy-items fancy-item content > ul li > .hide-range input[type=range] {
-  flex-grow: 1;
+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 content > ul li > .hide-range input[type=text] {
-  width: 0;
-  flex-basis: 44px;
+fancy-array fancy-items fancy-item fancy-item-header > input[type="text"]:focus {
+  outline: 1px solid var(--fancy-border-color-focus);
+  border-radius: 3px;
 }
-fancy-array fancy-items fancy-item.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;
+  /* border-left: 1px solid var(--border-color); */
+  margin-bottom: 0.5em;
 }
-fancy-array fancy-items fancy-item.folded .ico-chevron-down {
-  transform: rotate(-90deg);
+fancy-array fancy-items fancy-item .toggle-open * {
+  transition: transform 0.2s;
 }
-fancy-array fancy-items fancy-item .ico-chevron-down {
-  transition: transform 0.25s;
-  transform: rotate(0deg);
+fancy-array fancy-items fancy-item.open > fancy-item-header .toggle-open * {
+  transform: rotate(90deg);
+}
+fancy-array fancy-items fancy-item.open > fancy-item-content {
+  display: block;
 }
 fancy-array fancy-items fancy-item.hovertop:before,
 fancy-array fancy-items fancy-item.hoverbot:after {
@@ -4462,13 +4522,15 @@ fancy-array fancy-items fancy-item.hoverbot:after {
   content: "";
 }
 fancy-array fancy-items fancy-item:before {
-  border-top: 10px solid rgba(114, 180, 255, 0.75);
-  top: 0px;
+  background: linear-gradient(0deg, rgba(114, 180, 255, 0) 0%, rgba(114, 180, 255, 0.75) 100%);
+  top: -2px;
+  height: 10px;
   pointer-events: none;
 }
 fancy-array fancy-items fancy-item:after {
-  border-bottom: 10px solid rgba(114, 180, 255, 0.75);
-  bottom: 0px;
+  background: linear-gradient(180deg, rgba(114, 180, 255, 0) 0%, rgba(114, 180, 255, 0.75) 100%);
+  height: 10px;
+  bottom: -2px;
   pointer-events: none;
 }
 center-content {

+ 270 - 74
bin/style.less

@@ -5164,7 +5164,227 @@ graph-editor-root {
 	}
 }
 
+
+: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 {
+		--size: 20px;
+	}
+}
+
+.fancy-normal {
+	fancy-button, &fancy-button {
+		--size: 28px;
+	}
+}
+
+.fancy-big {
+	fancy-button, &fancy-button {
+		--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;
+	justify-content: space-evenly;
+
+	user-select: none;
+
+	&:has(.label) {
+		padding-left: 1.0em;
+		padding-right: 1.0em;
+	}
+
+	gap: 0.5em;
+
+	font-size: calc(var(--size) * 0.5);
+
+	.label {
+		font-weight: 200;
+		text-rendering: optimizeLegibility;
+	}
+
+	&: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;
+	// border-left: 1px solid #444;
+	padding: 0;
+	margin: 0 0.5em;
+	height: 16px;
+}
+
+// fancy-array {
+// 	* {
+// 		box-sizing: border-box;
+// 	}
+
+// 	overflow-y: auto;
+// 	overflow-x: clip;
+
+// 	&::-webkit-scrollbar {
+// 		width: 3px;
+
+// 		background: rgba(127,127,127,0.5);
+// 	}
+
+// 	&::-webkit-scrollbar-thumb {
+// 		background: #AAA;
+// 		border-radius: 5px;
+// 	}
+
+// 	border: 1px solid #333;
+// 	border-radius: 5px;
+
+
+// 	display: flex;
+// 	flex-direction: column;
+
+// 	border-radius: 5px;
+// 	padding: 2em;
+
+// 	gap: 2px;
+
+// 	width: 100%;
+
+// 	justify-content: stretch;
+
+// 	fancy-list-item {
+// 		display: flex;
+// 		flex-direction: column;
+
+// 		fancy-list-header {
+// 			display: flex;
+
+// 			color: var(--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;
+
+// 				color: var(--main-text-color);
+// 				font-size: 14px;
+
+// 				&:hover {
+// 					outline: 1px solid var(--border-color);
+// 					border-radius: 3px;
+// 				}
+// 			}
+// 		}
+
+// 		fancy-list-content {
+// 			display: none;
+// 			margin-top: 2px;
+
+// 			padding:  0.2em 1em;
+// 			padding-right: 0;
+// 			margin-right: calc(-0.2em - 1px);
+
+
+// 			/* border-left: 1px solid var(--border-color); */
+
+// 			margin-bottom: 0.5em;
+// 		}
+
+// 		.open-button .ico {
+// 			transition: transform 0.2s;
+// 		}
+
+// 		&.open {
+// 			> fancy-list-header .open-button .ico {
+// 				transform: rotate(90deg);
+// 			}
+
+// 			> fancy-list-content{
+// 				display: block;
+// 			}
+// 		}
+// 	}
+// }
+
 fancy-array {
+	box-sizing: border-box;
 
 	* {
 		box-sizing: border-box;
@@ -5172,6 +5392,7 @@ fancy-array {
 
 	padding: var(--basic-padding);
 	border-radius: var(--basic-border-radius);
+	border: var(--basic-border);
 	display: flex;
 	flex-direction: column;
 	align-items: stretch;
@@ -5180,109 +5401,82 @@ fancy-array {
 
 	overflow-y: auto;
 
+	fancy-array {
+		padding-right: -1px;
+	}
+
 	fancy-items {
 		display: flex;
 		flex-direction: column;
-		align-items: stretch;
-		background-color: var(--bg-3);
 
-		gap: 1px;
+		gap: 2px;
+
+		justify-content: stretch;
 
 		fancy-item {
 			display: flex;
 			flex-direction: column;
-			justify-items: stretch;
-
-			border-radius: var(--basic-border-radius);
 			position: relative;
-			background-color: var(--bg-3);
 
-			header {
+			fancy-item-header {
 				display: flex;
-				align-items: center;
-				background-color: var(--bg-2);
-				padding: calc(var(--basic-padding) * 2);
 
-				gap: 3px;
+				color: var(--fancy-quiet-text-color);
+				border-radius: 3px;
+
+				background-color: #444;
+
+				>input[type="text"] {
+					min-width: 0;
+					width: 0;
 
-				.fill {
 					flex-grow: 1;
-				}
 
-				input {
-					border-radius: var(--basic-border-radius);
-					background-color: var(--bg-3);
+					background: none;
+					outline: none;
 					border: none;
 
-					&:focus {
-						outline: var(--basic-border);
-					}
-				}
-
-				.reorder {
-					padding: var(--basic-padding);
-				}
+					font-size: 14px;
 
-				.reorder, .toggle-open {
-					color: #777;
 
 					&:hover {
-						color: #AAA;
+						outline: 1px solid var(--fancy-border-color);
+						border-radius: 3px;
+					}
+
+					&:focus {
+						outline: 1px solid var(--fancy-border-color-focus);
+						border-radius: 3px;
 					}
 				}
 			}
 
-			content {
-				border-top: var(--basic-border);
-				padding: var(--basic-padding);
+			fancy-item-content {
+				display: none;
+				margin-top: 2px;
 
-				> ul {
-					padding: var(--basic-padding);
-					display: flex;
-					flex-direction: column;
-					list-style: none;
+				padding:  0.2em 1em;
+				padding-right: 0px;
+				padding-right: 0;
 
-					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;
-						}
+				/* border-left: 1px solid var(--border-color); */
 
-						>.hide-range {
-							display: flex;
-							input[type=range] {
-								flex-grow: 1;
-							}
-							input[type=text] {
-								width: 0;
-								flex-basis: 44px;
-							}
-						}
-					}
-				}
+				margin-bottom: 0.5em;
 			}
 
-			&.folded content {
-				display: none;
+			.toggle-open * {
+				transition: transform 0.2s;
 			}
 
-			&.folded .ico-chevron-down {
-				transform: rotate(-90deg);
-			}
+			&.open {
+				> fancy-item-header .toggle-open *{
+					transform: rotate(90deg);
+				}
 
-			.ico-chevron-down {
-				transition: transform 0.25s;
-				transform: rotate(0deg);
+				> fancy-item-content{
+					display: block;
+				}
 			}
 
 
@@ -5296,14 +5490,16 @@ fancy-array {
 			}
 
 			&:before {
-				border-top: 10px solid rgba(114, 180, 255, 0.75);
-				top: 0px;
+				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 {
-				border-bottom: 10px solid rgba(114, 180, 255, 0.75);
-				bottom: 0px;
+				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;
 			}
 		}

+ 14 - 10
hide/comp/FancyArray.hx

@@ -43,18 +43,22 @@ class FancyArray<T> extends hide.comp.Component {
 
 		for (i => item in items) {
 			var paramElement = new Element('<fancy-item>
-				<header>
-					<div class="reorder ico ico-reorder" draggable="true"></div>
-					<div class="ico ico-chevron-down toggle-open"></div>
+				<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>
-					<button-2 class="menu no-border"><div class="ico ico-ellipsis-v"/></button-2>
-				</header>
+					<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("folded", !open);
+			paramElement.toggleClass("open", open);
 
 			var name = paramElement.find("input");
 
@@ -137,13 +141,13 @@ class FancyArray<T> extends hide.comp.Component {
 			if (getItemContent != null) {
 				var contentElement = getItemContent(item);
 				if (contentElement != null) {
-					var content = new Element("<content></content>").appendTo(paramElement);
+					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("folded", !state.open);
+						paramElement.toggleClass("open", state.open);
 					});
 				} else {
 					toggleOpen.remove();
@@ -153,7 +157,7 @@ class FancyArray<T> extends hide.comp.Component {
 			}
 
 			if (removeItem != null) {
-				paramElement.find("header").get(0).addEventListener("contextmenu", function (e : js.html.MouseEvent) {
+				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)}
@@ -167,7 +171,7 @@ class FancyArray<T> extends hide.comp.Component {
 						{label: "Delete", click: () -> removeItem(i)}
 					]);
 				});
-			}			paramElement.find("header").get(0).addEventListener("contextmenu", function (e : js.html.MouseEvent) {
+			}			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)}

+ 129 - 1
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;
@@ -230,9 +234,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="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 +261,20 @@ class ShaderEditor extends hide.view.FileView implements GraphInterface.IGraphEd
 			</div>'
 		);
 
+		variableList = new hide.comp.FancyArray(null, rightPannel.find(".variables"), "variables", "variables");
+
+		variableList.getItems = () -> currentGraph.variables;
+		variableList.getItemName = (v: ShaderGraphVariable) -> v.name;
+		variableList.reorderItem = moveVariable;
+		variableList.removeItem = removeVariable;
+		variableList.setItemName = renameVariable;
+
+		variableList.refresh();
+
+		rightPannel.find(".add-variable").on("click", (e) -> {
+			createVariable(SgFloat(2));
+		});
+
 		rightPannel.find("#centerView").click((e) -> graphEditor.centerView());
 
 		domainSelection = rightPannel.find("#domainSelection");
@@ -410,6 +438,106 @@ class ShaderEditor extends hide.view.FileView implements GraphInterface.IGraphEd
 		}
 	}
 
+	function createVariable(type: SgType) {
+		var name = "New Variable";
+		var i = 0;
+		var index = 0;
+		while(i < currentGraph.variables.length) {
+			if (currentGraph.variables[i].name == name) {
+				i = 0;
+				index ++;
+				name = 'New Variable ($index)';
+			} else {
+				i++;
+			}
+		}
+
+		var variable : ShaderGraphVariable = {
+			name: name,
+			type: type,
+			defValue: [0,0],
+		}
+		var graph = currentGraph;
+
+		function exec(isUndo: Bool) {
+			if (!isUndo) {
+				graph.variables.push(variable);
+			}
+			else {
+				graph.variables.remove(variable);
+			}
+			variableList.refresh();
+		}
+		exec(false);
+		undo.change(Custom(exec));
+	}
+
+	function renameVariable(variable: ShaderGraphVariable, newName: String) {
+		var graph = currentGraph;
+		var oldName = variable.name;
+		function exec(isUndo: Bool) {
+			variable.name = !isUndo ? newName : oldName;
+			variableList.refresh();
+
+			var index = graph.variables.indexOf(variable);
+			for (node in currentGraph.nodes) {
+				if (Std.downcast(node, hrt.shgraph.nodes.VarRead)?.varId == index ||
+					Std.downcast(node, hrt.shgraph.nodes.VarWrite)?.varId == index) {
+					graphEditor.refreshBox(node.id);
+				}
+			}
+		}
+
+		exec(false);
+		undo.change(Custom(exec));
+	}
+
+	function moveVariable(oldIndex: Int, newIndex: Int) {
+		var graph = currentGraph;
+		function exec(isUndo: Bool) {
+			if (!isUndo) {
+				var rem = graph.variables.splice(oldIndex, 1);
+				graph.variables.insert(newIndex, rem[0]);
+			}
+			else {
+				var rem = graph.variables.splice(newIndex, 1);
+				graph.variables.insert(oldIndex, rem[0]);
+			}
+			variableList.refresh();
+		}
+		exec(false);
+		undo.change(Custom(exec));
+	}
+
+	function removeVariable(index: Int) {
+		var usedInGraph = false;
+		for (node in currentGraph.nodes) {
+			if (Std.downcast(node, hrt.shgraph.nodes.VarRead)?.varId == index ||
+				Std.downcast(node, hrt.shgraph.nodes.VarWrite)?.varId == index) {
+				usedInGraph = true;
+				break;
+			}
+		}
+		if (usedInGraph) {
+			hide.Ide.inst.quickError("Variable is used in graph");
+			return;
+		}
+
+		var graph = currentGraph;
+		var variable = graph.variables[index];
+		function exec(isUndo: Bool) {
+			if (!isUndo) {
+				graph.variables.splice(index, 1);
+			}
+			else {
+				graph.variables.insert(index, variable);
+			}
+			variableList.refresh();
+		}
+		exec(false);
+		undo.change(Custom(exec));
+	}
+
 	function createParameter(type : HxslType) {
 		@:privateAccess var paramShaderID : Int = shaderGraph.current_param_id++;
 		@:privateAccess

+ 50 - 3
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:
@@ -121,8 +143,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,6 +193,13 @@ ExternVarDef {
 	var paramIndex: Null<Int> = null;
 }
 
+@:structInit @:publicFields
+class ShaderGraphVariable {
+	var name: String;
+	var type: SgType;
+	var defValue: Dynamic;
+}
+
 @:access(hrt.shgraph.Graph)
 class ShaderGraphGenContext {
 	var graph : Graph;
@@ -690,6 +717,8 @@ class Graph {
 	var current_node_id = 0;
 	var nodes : Map<Int, ShaderNode> = [];
 
+	var variables : Array<ShaderGraphVariable> = [];
+
 	public var parent : ShaderGraph = null;
 
 	public var domain : Domain = Fragment;
@@ -703,6 +732,14 @@ class Graph {
 	public function load(json : Dynamic) {
 		nodes = [];
 		generate(Reflect.getProperty(json, "nodes"), Reflect.getProperty(json, "edges"));
+
+		for (variable in json.variables ?? []) {
+			variables.push({
+				name: variable.name,
+				type: unserializeSgType(variable.type),
+				defValue: variable.defValue,
+			});
+		}
 	}
 
 	public function generate(nodes : Array<Dynamic>, edges : Array<Edge>) {
@@ -910,11 +947,21 @@ 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 variablesJson = [];
+		for (variable in variables) {
+			variablesJson.push({
+				name: variable.name,
+				type: serializeSgType(variable.type),
+				defValue: variable.defValue,
+			});
+		}
+
 		var json = {
 			nodes: [
 				for (n in nodes) n.serializeToDynamic(),
 			],
-			edges: edgesJson
+			edges: edgesJson,
+			variables: variablesJson,
 		};
 
 		return json;

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

@@ -23,4 +23,12 @@ class VarRead extends ShaderNode {
 		ctx.addPreview(out);
 		#end
 	}
+
+	#if editor
+	override function getInfo():hide.view.GraphInterface.GraphNodeInfo {
+		var info = super.getInfo();
+		info.name = "Read: " + @:privateAccess (cast editor.editor: hide.view.shadereditor.ShaderEditor).currentGraph.variables[varId].name;
+		return info;
+	}
+	#end
 }

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

@@ -20,4 +20,12 @@ class VarWrite extends ShaderNode {
 		ctx.addExpr(AstTools.makeVarDecl(ctx.getLocalTVar("_sg_var_write", TVec(4, VFloat)), input));
 		ctx.addPreview(input);
 	}
+
+	#if editor
+	override function getInfo():hide.view.GraphInterface.GraphNodeInfo {
+		var info = super.getInfo();
+		info.name = "Write: " + @:privateAccess (cast editor.editor: hide.view.shadereditor.ShaderEditor).currentGraph.variables[varId].name;
+		return info;
+	}
+	#end
 }