graphEditor.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. import {
  2. DiagramEngine,
  3. DiagramModel,
  4. DiagramWidget,
  5. MoveCanvasAction,
  6. LinkModel
  7. } from "storm-react-diagrams";
  8. import * as React from "react";
  9. import { GlobalState } from './globalState';
  10. import { GenericNodeFactory } from './components/diagram/generic/genericNodeFactory';
  11. import { GenericNodeModel } from './components/diagram/generic/genericNodeModel';
  12. import { NodeMaterialBlock } from 'babylonjs/Materials/Node/nodeMaterialBlock';
  13. import { NodeMaterialConnectionPoint } from 'babylonjs/Materials/Node/nodeMaterialBlockConnectionPoint';
  14. import { NodeListComponent } from './components/nodeList/nodeListComponent';
  15. import { PropertyTabComponent } from './components/propertyTab/propertyTabComponent';
  16. import { Portal } from './portal';
  17. import { TextureNodeFactory } from './components/diagram/texture/textureNodeFactory';
  18. import { DefaultNodeModel } from './components/diagram/defaultNodeModel';
  19. import { TextureNodeModel } from './components/diagram/texture/textureNodeModel';
  20. import { DefaultPortModel } from './components/diagram/defaultPortModel';
  21. import { InputNodeFactory } from './components/diagram/input/inputNodeFactory';
  22. import { InputNodeModel } from './components/diagram/input/inputNodeModel';
  23. import { TextureBlock } from 'babylonjs/Materials/Node/Blocks/Fragment/textureBlock';
  24. import { Vector2, Vector3, Vector4, Matrix, Color3, Color4 } from 'babylonjs/Maths/math';
  25. import { LogComponent } from './components/log/logComponent';
  26. import { LightBlock } from 'babylonjs/Materials/Node/Blocks/Dual/lightBlock';
  27. import { LightNodeModel } from './components/diagram/light/lightNodeModel';
  28. import { LightNodeFactory } from './components/diagram/light/lightNodeFactory';
  29. require("storm-react-diagrams/dist/style.min.css");
  30. require("./main.scss");
  31. require("./components/diagram/diagram.scss");
  32. /*
  33. Graph Editor Overview
  34. Storm React setup:
  35. GenericNodeModel - Represents the nodes in the graph and can be any node type (eg. texture, vector2, etc)
  36. GenericNodeWidget - Renders the node model in the graph
  37. GenericPortModel - Represents the input/output of a node (contained within each GenericNodeModel)
  38. Generating/modifying the graph:
  39. Generating node graph - the createNodeFromObject method is used to recursively create the graph
  40. Modifications to the graph - The listener in the constructor of GraphEditor listens for port changes and updates the node material based on changes
  41. Saving the graph/generating code - Not yet done
  42. */
  43. interface IGraphEditorProps {
  44. globalState: GlobalState;
  45. }
  46. export class NodeCreationOptions {
  47. column: number;
  48. nodeMaterialBlock?: NodeMaterialBlock;
  49. type?: string;
  50. connection?: NodeMaterialConnectionPoint;
  51. }
  52. export class GraphEditor extends React.Component<IGraphEditorProps> {
  53. private _engine: DiagramEngine;
  54. private _model: DiagramModel;
  55. private _nodes = new Array<DefaultNodeModel>();
  56. /** @hidden */
  57. public _toAdd: LinkModel[] | null = [];
  58. /**
  59. * Current row/column position used when adding new nodes
  60. */
  61. private _rowPos = new Array<number>();
  62. /**
  63. * Creates a node and recursivly creates its parent nodes from it's input
  64. * @param nodeMaterialBlock
  65. */
  66. public createNodeFromObject(options: NodeCreationOptions) {
  67. // Update rows/columns
  68. if (this._rowPos[options.column] == undefined) {
  69. this._rowPos[options.column] = 0;
  70. } else {
  71. this._rowPos[options.column]++;
  72. }
  73. // Create new node in the graph
  74. var newNode: DefaultNodeModel;
  75. var filterInputs = [];
  76. if (options.nodeMaterialBlock) {
  77. if (options.nodeMaterialBlock instanceof TextureBlock) {
  78. newNode = new TextureNodeModel();
  79. filterInputs.push("uv");
  80. } else if (options.nodeMaterialBlock instanceof LightBlock) {
  81. newNode = new LightNodeModel();
  82. filterInputs.push("worldPosition");
  83. filterInputs.push("worldNormal");
  84. filterInputs.push("cameraPosition");
  85. } else {
  86. newNode = new GenericNodeModel();
  87. }
  88. if (options.nodeMaterialBlock.isFinalMerger) {
  89. this.props.globalState.nodeMaterial!.addOutputNode(options.nodeMaterialBlock);
  90. }
  91. } else {
  92. newNode = new InputNodeModel();
  93. (newNode as InputNodeModel).connection = options.connection;
  94. }
  95. this._nodes.push(newNode)
  96. newNode.setPosition(1600 - (300 * options.column), 210 * this._rowPos[options.column])
  97. this._model.addAll(newNode);
  98. if (options.nodeMaterialBlock) {
  99. newNode.prepare(options, this._nodes, this._model, this, filterInputs);
  100. }
  101. return newNode;
  102. }
  103. componentDidMount() {
  104. if (this.props.globalState.hostDocument) {
  105. var widget = (this.refs["test"] as DiagramWidget);
  106. widget.setState({ document: this.props.globalState.hostDocument })
  107. this.props.globalState.hostDocument!.addEventListener("keyup", widget.onKeyUpPointer as any, false);
  108. }
  109. }
  110. componentWillUnmount() {
  111. if (this.props.globalState.hostDocument) {
  112. var widget = (this.refs["test"] as DiagramWidget);
  113. this.props.globalState.hostDocument!.removeEventListener("keyup", widget.onKeyUpPointer as any, false);
  114. }
  115. }
  116. constructor(props: IGraphEditorProps) {
  117. super(props);
  118. // setup the diagram engine
  119. this._engine = new DiagramEngine();
  120. this._engine.installDefaultFactories()
  121. this._engine.registerNodeFactory(new GenericNodeFactory(this.props.globalState));
  122. this._engine.registerNodeFactory(new TextureNodeFactory(this.props.globalState));
  123. this._engine.registerNodeFactory(new LightNodeFactory(this.props.globalState));
  124. this._engine.registerNodeFactory(new InputNodeFactory(this.props.globalState));
  125. this.props.globalState.onRebuildRequiredObservable.add(() => {
  126. if (this.props.globalState.nodeMaterial) {
  127. this.buildMaterial();
  128. }
  129. this.forceUpdate();
  130. });
  131. this.props.globalState.onResetRequiredObservable.add(() => {
  132. this._rowPos = [];
  133. this.build();
  134. if (this.props.globalState.nodeMaterial) {
  135. this.buildMaterial();
  136. }
  137. });
  138. this.props.globalState.onUpdateRequiredObservable.add(() => {
  139. this.forceUpdate();
  140. });
  141. this.props.globalState.onZoomToFitRequiredObservable.add(() => {
  142. this._engine.zoomToFit();
  143. });
  144. this.build();
  145. }
  146. buildMaterial() {
  147. if (!this.props.globalState.nodeMaterial) {
  148. return;
  149. }
  150. try {
  151. this.props.globalState.nodeMaterial.build(true);
  152. this.props.globalState.onLogRequiredObservable.notifyObservers("Node material build successful");
  153. }
  154. catch (err) {
  155. this.props.globalState.onLogRequiredObservable.notifyObservers(err);
  156. }
  157. }
  158. build() {
  159. // setup the diagram model
  160. this._model = new DiagramModel();
  161. // Listen to events
  162. this._model.addListener({
  163. nodesUpdated: (e) => {
  164. if (!e.isCreated) {
  165. // Block is deleted
  166. let targetBlock = (e.node as GenericNodeModel).block;
  167. if (targetBlock && targetBlock.isFinalMerger) {
  168. this.props.globalState.nodeMaterial!.removeOutputNode(targetBlock);
  169. }
  170. }
  171. },
  172. linksUpdated: (e) => {
  173. if (!e.isCreated) {
  174. // Link is deleted
  175. this.props.globalState.onSelectionChangedObservable.notifyObservers(null);
  176. var link = DefaultPortModel.SortInputOutput(e.link.sourcePort as DefaultPortModel, e.link.targetPort as DefaultPortModel);
  177. if (link) {
  178. if (link.input.connection) {
  179. if (link.output.connection) {
  180. // Disconnect standard nodes
  181. link.output.connection.disconnectFrom(link.input.connection)
  182. link.input.syncWithNodeMaterialConnectionPoint(link.input.connection)
  183. link.output.syncWithNodeMaterialConnectionPoint(link.output.connection)
  184. } else if (link.input.connection.value) {
  185. link.input.connection.value = null;
  186. }
  187. }
  188. }
  189. }
  190. e.link.addListener({
  191. sourcePortChanged: () => {
  192. console.log("port change")
  193. },
  194. targetPortChanged: () => {
  195. // Link is created with a target port
  196. var link = DefaultPortModel.SortInputOutput(e.link.sourcePort as DefaultPortModel, e.link.targetPort as DefaultPortModel);
  197. if (link) {
  198. if (link.output.connection && link.input.connection) {
  199. link.output.connection.connectTo(link.input.connection)
  200. } else if (link.input.connection) {
  201. if (!link.output.connection) { // Input Node
  202. let name = link.output.name;
  203. link.output.syncWithNodeMaterialConnectionPoint(link.input.connection);
  204. link.output.name = name;
  205. (link.output.getNode() as InputNodeModel).connection = link.output.connection!;
  206. link.input.connection.value = link.output.defaultValue;
  207. }
  208. }
  209. if (this.props.globalState.nodeMaterial) {
  210. this.buildMaterial();
  211. }
  212. }
  213. }
  214. })
  215. }
  216. });
  217. // Load graph of nodes from the material
  218. if (this.props.globalState.nodeMaterial) {
  219. var material: any = this.props.globalState.nodeMaterial;
  220. material._vertexOutputNodes.forEach((n: any) => {
  221. this.createNodeFromObject({ column: 0, nodeMaterialBlock: n });
  222. })
  223. material._fragmentOutputNodes.forEach((n: any) => {
  224. this.createNodeFromObject({ column: 0, nodeMaterialBlock: n });
  225. })
  226. }
  227. // load model into engine
  228. setTimeout(() => {
  229. if (this._toAdd) {
  230. this._model.addAll(...this._toAdd);
  231. }
  232. this._toAdd = null;
  233. this._engine.setDiagramModel(this._model);
  234. this.forceUpdate();
  235. }, 550);
  236. }
  237. addNodeFromClass(ObjectClass: typeof NodeMaterialBlock) {
  238. var block = new ObjectClass(ObjectClass.prototype.getClassName())
  239. var localNode = this.createNodeFromObject({ column: 0, nodeMaterialBlock: block })
  240. var widget = (this.refs["test"] as DiagramWidget);
  241. this.forceUpdate();
  242. // This is needed to fix link offsets when created, (eg. create a fog block)
  243. // Todo figure out how to correct this without this
  244. setTimeout(() => {
  245. widget.startFiringAction(new MoveCanvasAction(1, 0, this._model));
  246. }, 500);
  247. return localNode;
  248. }
  249. addValueNode(type: string, column = 0, connection?: NodeMaterialConnectionPoint) {
  250. var localNode = this.createNodeFromObject({ column: column, type: type, connection: connection })
  251. var outPort = new DefaultPortModel(type, "output");
  252. localNode.addPort(outPort);
  253. if (!connection) {
  254. switch (type) {
  255. case "Vector2":
  256. outPort.defaultValue = Vector2.Zero();
  257. break;
  258. case "Vector3":
  259. outPort.defaultValue = Vector3.Zero();
  260. break;
  261. case "Vector4":
  262. outPort.defaultValue = Vector4.Zero();
  263. break;
  264. case "Matrix":
  265. outPort.defaultValue = Matrix.Identity();
  266. break;
  267. case "Color3":
  268. outPort.defaultValue = Color3.White();
  269. break;
  270. case "Color4":
  271. outPort.defaultValue = new Color4(1, 1, 1, 1);
  272. break;
  273. }
  274. }
  275. return localNode;
  276. }
  277. render() {
  278. return (
  279. <Portal globalState={this.props.globalState}>
  280. <div id="node-editor-graph-root">
  281. {/* Node creation menu */}
  282. <NodeListComponent globalState={this.props.globalState} onAddValueNode={b => this.addValueNode(b)} onAddNodeFromClass={b => this.addNodeFromClass(b)} />
  283. {/* The node graph diagram */}
  284. <DiagramWidget deleteKeys={[46]} ref={"test"} inverseZoom={true} className="diagram-container" diagramEngine={this._engine} maxNumberPointsPerLink={0} />
  285. {/* Property tab */}
  286. <PropertyTabComponent globalState={this.props.globalState} />
  287. <LogComponent globalState={this.props.globalState} />
  288. </div>
  289. </Portal>
  290. );
  291. }
  292. }