graphCanvas.tsx 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967
  1. import * as React from "react";
  2. import { GlobalState } from '../globalState';
  3. import { NodeMaterialBlock } from 'babylonjs/Materials/Node/nodeMaterialBlock';
  4. import { NodeMaterialBlockConnectionPointTypes } from 'babylonjs/Materials/Node/Enums/nodeMaterialBlockConnectionPointTypes';
  5. import { GraphNode } from './graphNode';
  6. import * as dagre from 'dagre';
  7. import { Nullable } from 'babylonjs/types';
  8. import { NodeLink } from './nodeLink';
  9. import { NodePort } from './nodePort';
  10. import { NodeMaterialConnectionPoint, NodeMaterialConnectionPointDirection, NodeMaterialConnectionPointCompatibilityStates } from 'babylonjs/Materials/Node/nodeMaterialBlockConnectionPoint';
  11. import { Vector2 } from 'babylonjs/Maths/math.vector';
  12. import { FragmentOutputBlock } from 'babylonjs/Materials/Node/Blocks/Fragment/fragmentOutputBlock';
  13. import { InputBlock } from 'babylonjs/Materials/Node/Blocks/Input/inputBlock';
  14. import { DataStorage } from 'babylonjs/Misc/dataStorage';
  15. import { GraphFrame } from './graphFrame';
  16. import { IEditorData, IFrameData } from '../nodeLocationInfo';
  17. import { FrameNodePort } from './frameNodePort';
  18. require("./graphCanvas.scss");
  19. export interface IGraphCanvasComponentProps {
  20. globalState: GlobalState,
  21. onEmitNewBlock: (block: NodeMaterialBlock) => GraphNode
  22. }
  23. export type FramePortData = {
  24. frame: GraphFrame,
  25. port: FrameNodePort
  26. }
  27. export const isFramePortData = (variableToCheck: any): variableToCheck is FramePortData => {
  28. if (variableToCheck) {
  29. return (variableToCheck as FramePortData).port !== undefined;
  30. }
  31. else return false;
  32. }
  33. export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentProps> {
  34. private readonly MinZoom = 0.1;
  35. private readonly MaxZoom = 4;
  36. private _hostCanvas: HTMLDivElement;
  37. private _graphCanvas: HTMLDivElement;
  38. private _selectionContainer: HTMLDivElement;
  39. private _frameContainer: HTMLDivElement;
  40. private _svgCanvas: HTMLElement;
  41. private _rootContainer: HTMLDivElement;
  42. private _nodes: GraphNode[] = [];
  43. private _links: NodeLink[] = [];
  44. private _mouseStartPointX: Nullable<number> = null;
  45. private _mouseStartPointY: Nullable<number> = null
  46. private _dropPointX = 0;
  47. private _dropPointY = 0;
  48. private _selectionStartX = 0;
  49. private _selectionStartY = 0;
  50. private _candidateLinkedHasMoved = false;
  51. private _x = 0;
  52. private _y = 0;
  53. private _zoom = 1;
  54. private _selectedNodes: GraphNode[] = [];
  55. private _selectedLink: Nullable<NodeLink> = null;
  56. private _selectedPort: Nullable<NodePort> = null;
  57. private _candidateLink: Nullable<NodeLink> = null;
  58. private _candidatePort: Nullable<NodePort | FrameNodePort> = null;
  59. private _gridSize = 20;
  60. private _selectionBox: Nullable<HTMLDivElement> = null;
  61. private _selectedFrame: Nullable<GraphFrame> = null;
  62. private _frameCandidate: Nullable<HTMLDivElement> = null;
  63. private _frames: GraphFrame[] = [];
  64. private _altKeyIsPressed = false;
  65. private _ctrlKeyIsPressed = false;
  66. private _oldY = -1;
  67. public _frameIsMoving = false;
  68. public _isLoading = false;
  69. public get gridSize() {
  70. return this._gridSize;
  71. }
  72. public set gridSize(value: number) {
  73. this._gridSize = value;
  74. this.updateTransform();
  75. }
  76. public get globalState(){
  77. return this.props.globalState;
  78. }
  79. public get nodes() {
  80. return this._nodes;
  81. }
  82. public get links() {
  83. return this._links;
  84. }
  85. public get frames() {
  86. return this._frames;
  87. }
  88. public get zoom() {
  89. return this._zoom;
  90. }
  91. public set zoom(value: number) {
  92. if (this._zoom === value) {
  93. return;
  94. }
  95. this._zoom = value;
  96. this.updateTransform();
  97. }
  98. public get x() {
  99. return this._x;
  100. }
  101. public set x(value: number) {
  102. this._x = value;
  103. this.updateTransform();
  104. }
  105. public get y() {
  106. return this._y;
  107. }
  108. public set y(value: number) {
  109. this._y = value;
  110. this.updateTransform();
  111. }
  112. public get selectedNodes() {
  113. return this._selectedNodes;
  114. }
  115. public get selectedLink() {
  116. return this._selectedLink;
  117. }
  118. public get selectedFrame() {
  119. return this._selectedFrame;
  120. }
  121. public get selectedPort() {
  122. return this._selectedPort;
  123. }
  124. public get canvasContainer() {
  125. return this._graphCanvas;
  126. }
  127. public get hostCanvas() {
  128. return this._hostCanvas;
  129. }
  130. public get svgCanvas() {
  131. return this._svgCanvas;
  132. }
  133. public get selectionContainer() {
  134. return this._selectionContainer;
  135. }
  136. public get frameContainer() {
  137. return this._frameContainer;
  138. }
  139. constructor(props: IGraphCanvasComponentProps) {
  140. super(props);
  141. props.globalState.onSelectionChangedObservable.add(selection => {
  142. if (!selection) {
  143. this._selectedNodes = [];
  144. this._selectedLink = null;
  145. this._selectedFrame = null;
  146. this._selectedPort = null;
  147. } else {
  148. if (selection instanceof NodeLink) {
  149. this._selectedNodes = [];
  150. this._selectedFrame = null;
  151. this._selectedLink = selection;
  152. this._selectedPort = null;
  153. } else if (selection instanceof GraphFrame) {
  154. this._selectedNodes = [];
  155. this._selectedFrame = selection;
  156. this._selectedLink = null;
  157. this._selectedPort = null;
  158. } else if (selection instanceof GraphNode){
  159. if (this._ctrlKeyIsPressed) {
  160. if (this._selectedNodes.indexOf(selection) === -1) {
  161. this._selectedNodes.push(selection);
  162. }
  163. } else {
  164. this._selectedNodes = [selection];
  165. }
  166. } else if(selection instanceof NodePort){
  167. this._selectedNodes = [];
  168. this._selectedFrame = null;
  169. this._selectedLink = null;
  170. this._selectedPort = selection;
  171. } else {
  172. this._selectedNodes = [];
  173. this._selectedFrame = null;
  174. this._selectedLink = null;
  175. this._selectedPort = selection.port;
  176. }
  177. }
  178. });
  179. props.globalState.onCandidatePortSelectedObservable.add(port => {
  180. this._candidatePort = port;
  181. });
  182. props.globalState.onGridSizeChanged.add(() => {
  183. this.gridSize = DataStorage.ReadNumber("GridSize", 20);
  184. });
  185. this.props.globalState.hostDocument!.addEventListener("keyup", () => this.onKeyUp(), false);
  186. this.props.globalState.hostDocument!.addEventListener("keydown", evt => {
  187. this._altKeyIsPressed = evt.altKey;
  188. this._ctrlKeyIsPressed = evt.ctrlKey;
  189. }, false);
  190. this.props.globalState.hostDocument!.defaultView!.addEventListener("blur", () => {
  191. this._altKeyIsPressed = false;
  192. this._ctrlKeyIsPressed = false;
  193. }, false);
  194. // Store additional data to serialization object
  195. this.props.globalState.storeEditorData = (editorData, graphFrame) => {
  196. editorData.frames = [];
  197. if (graphFrame) {
  198. editorData.frames.push(graphFrame!.serialize());
  199. } else {
  200. editorData.x = this.x;
  201. editorData.y = this.y;
  202. editorData.zoom = this.zoom;
  203. for (var frame of this._frames) {
  204. editorData.frames.push(frame.serialize());
  205. }
  206. }
  207. }
  208. }
  209. public getGridPosition(position: number, useCeil = false) {
  210. let gridSize = this.gridSize;
  211. if (gridSize === 0) {
  212. return position;
  213. }
  214. if (useCeil) {
  215. return gridSize * Math.ceil(position / gridSize);
  216. }
  217. return gridSize * Math.floor(position / gridSize);
  218. }
  219. public getGridPositionCeil(position: number) {
  220. let gridSize = this.gridSize;
  221. if (gridSize === 0) {
  222. return position;
  223. }
  224. return gridSize * Math.ceil(position / gridSize);
  225. }
  226. updateTransform() {
  227. this._rootContainer.style.transform = `translate(${this._x}px, ${this._y}px) scale(${this._zoom})`;
  228. if (DataStorage.ReadBoolean("ShowGrid", true)) {
  229. this._hostCanvas.style.backgroundSize = `${this._gridSize * this._zoom}px ${this._gridSize * this._zoom}px`;
  230. this._hostCanvas.style.backgroundPosition = `${this._x}px ${this._y}px`;
  231. } else {
  232. this._hostCanvas.style.backgroundSize = `0`;
  233. }
  234. }
  235. onKeyUp() {
  236. this._altKeyIsPressed = false;
  237. this._ctrlKeyIsPressed = false;
  238. this._oldY = -1;
  239. }
  240. findNodeFromBlock(block: NodeMaterialBlock) {
  241. return this.nodes.filter(n => n.block === block)[0];
  242. }
  243. reset() {
  244. for (var node of this._nodes) {
  245. node.dispose();
  246. }
  247. const frames = this._frames.splice(0);
  248. for (var frame of frames) {
  249. frame.dispose();
  250. }
  251. this._nodes = [];
  252. this._frames = [];
  253. this._links = [];
  254. this._graphCanvas.innerHTML = "";
  255. this._svgCanvas.innerHTML = "";
  256. }
  257. connectPorts(pointA: NodeMaterialConnectionPoint, pointB: NodeMaterialConnectionPoint) {
  258. var blockA = pointA.ownerBlock;
  259. var blockB = pointB.ownerBlock;
  260. var nodeA = this.findNodeFromBlock(blockA);
  261. var nodeB = this.findNodeFromBlock(blockB);
  262. if (!nodeA || !nodeB) {
  263. return;
  264. }
  265. var portA = nodeA.getPortForConnectionPoint(pointA);
  266. var portB = nodeB.getPortForConnectionPoint(pointB);
  267. if (!portA || !portB) {
  268. return;
  269. }
  270. for (var currentLink of this._links) {
  271. if (currentLink.portA === portA && currentLink.portB === portB) {
  272. return;
  273. }
  274. if (currentLink.portA === portB && currentLink.portB === portA) {
  275. return;
  276. }
  277. }
  278. const link = new NodeLink(this, portA, nodeA, portB, nodeB);
  279. this._links.push(link);
  280. nodeA.links.push(link);
  281. nodeB.links.push(link);
  282. }
  283. removeLink(link: NodeLink) {
  284. let index = this._links.indexOf(link);
  285. if (index > -1) {
  286. this._links.splice(index, 1);
  287. }
  288. link.dispose();
  289. }
  290. appendBlock(block: NodeMaterialBlock) {
  291. let newNode = new GraphNode(block, this.props.globalState);
  292. newNode.appendVisual(this._graphCanvas, this);
  293. this._nodes.push(newNode);
  294. return newNode;
  295. }
  296. distributeGraph() {
  297. this.x = 0;
  298. this.y = 0;
  299. this.zoom = 1;
  300. let graph = new dagre.graphlib.Graph();
  301. graph.setGraph({});
  302. graph.setDefaultEdgeLabel(() => ({}));
  303. graph.graph().rankdir = "LR";
  304. // Build dagre graph
  305. this._nodes.forEach(node => {
  306. if (this._frames.some(f => f.nodes.indexOf(node) !== -1)) {
  307. return;
  308. }
  309. graph.setNode(node.id.toString(), {
  310. id: node.id,
  311. type: "node",
  312. width: node.width,
  313. height: node.height
  314. });
  315. });
  316. this._frames.forEach(frame => {
  317. graph.setNode(frame.id.toString(), {
  318. id: frame.id,
  319. type: "frame",
  320. width: frame.element.clientWidth,
  321. height: frame.element.clientHeight
  322. });
  323. })
  324. this._nodes.forEach(node => {
  325. node.block.outputs.forEach(output => {
  326. if (!output.hasEndpoints) {
  327. return;
  328. }
  329. output.endpoints.forEach(endpoint => {
  330. let sourceFrames = this._frames.filter(f => f.nodes.indexOf(node) !== -1);
  331. let targetFrames = this._frames.filter(f => f.nodes.some(n => n.block === endpoint.ownerBlock));
  332. let sourceId = sourceFrames.length > 0 ? sourceFrames[0].id : node.id;
  333. let targetId = targetFrames.length > 0 ? targetFrames[0].id : endpoint.ownerBlock.uniqueId;
  334. graph.setEdge(sourceId.toString(), targetId.toString());
  335. });
  336. });
  337. });
  338. // Distribute
  339. dagre.layout(graph);
  340. // Update graph
  341. let dagreNodes = graph.nodes().map(node => graph.node(node));
  342. dagreNodes.forEach((dagreNode: any) => {
  343. if (!dagreNode) {
  344. return;
  345. }
  346. if (dagreNode.type === "node") {
  347. for (var node of this._nodes) {
  348. if (node.id === dagreNode.id) {
  349. node.x = dagreNode.x - dagreNode.width / 2;
  350. node.y = dagreNode.y - dagreNode.height / 2;
  351. node.cleanAccumulation();
  352. return;
  353. }
  354. }
  355. return;
  356. }
  357. for (var frame of this._frames) {
  358. if (frame.id === dagreNode.id) {
  359. this._frameIsMoving = true;
  360. frame.move(dagreNode.x - dagreNode.width / 2, dagreNode.y - dagreNode.height / 2, false);
  361. frame.cleanAccumulation();
  362. this._frameIsMoving = false;
  363. return;
  364. }
  365. }
  366. });
  367. }
  368. componentDidMount() {
  369. this._hostCanvas = this.props.globalState.hostDocument.getElementById("graph-canvas") as HTMLDivElement;
  370. this._rootContainer = this.props.globalState.hostDocument.getElementById("graph-container") as HTMLDivElement;
  371. this._graphCanvas = this.props.globalState.hostDocument.getElementById("graph-canvas-container") as HTMLDivElement;
  372. this._svgCanvas = this.props.globalState.hostDocument.getElementById("graph-svg-container") as HTMLElement;
  373. this._selectionContainer = this.props.globalState.hostDocument.getElementById("selection-container") as HTMLDivElement;
  374. this._frameContainer = this.props.globalState.hostDocument.getElementById("frame-container") as HTMLDivElement;
  375. this.gridSize = DataStorage.ReadNumber("GridSize", 20);
  376. this.updateTransform();
  377. }
  378. onMove(evt: React.PointerEvent) {
  379. // Selection box
  380. if (this._selectionBox) {
  381. const rootRect = this.canvasContainer.getBoundingClientRect();
  382. const localX = evt.pageX - rootRect.left;
  383. const localY = evt.pageY - rootRect.top;
  384. if (localX > this._selectionStartX) {
  385. this._selectionBox.style.left = `${this._selectionStartX / this.zoom}px`;
  386. this._selectionBox.style.width = `${(localX - this._selectionStartX) / this.zoom}px`;
  387. } else {
  388. this._selectionBox.style.left = `${localX / this.zoom}px`;
  389. this._selectionBox.style.width = `${(this._selectionStartX - localX) / this.zoom}px`;
  390. }
  391. if (localY > this._selectionStartY) {
  392. this._selectionBox.style.top = `${this._selectionStartY / this.zoom}px`;
  393. this._selectionBox.style.height = `${(localY - this._selectionStartY) / this.zoom}px`;
  394. } else {
  395. this._selectionBox.style.top = `${localY / this.zoom}px`;
  396. this._selectionBox.style.height = `${(this._selectionStartY - localY) / this.zoom}px`;
  397. }
  398. this.props.globalState.onSelectionBoxMoved.notifyObservers(this._selectionBox.getBoundingClientRect());
  399. return;
  400. }
  401. // Candidate frame box
  402. if (this._frameCandidate) {
  403. const rootRect = this.canvasContainer.getBoundingClientRect();
  404. const localX = evt.pageX - rootRect.left;
  405. const localY = evt.pageY - rootRect.top;
  406. if (localX > this._selectionStartX) {
  407. this._frameCandidate.style.left = `${this._selectionStartX / this.zoom}px`;
  408. this._frameCandidate.style.width = `${(localX - this._selectionStartX) / this.zoom}px`;
  409. } else {
  410. this._frameCandidate.style.left = `${localX / this.zoom}px`;
  411. this._frameCandidate.style.width = `${(this._selectionStartX - localX) / this.zoom}px`;
  412. }
  413. if (localY > this._selectionStartY) {
  414. this._frameCandidate.style.top = `${this._selectionStartY / this.zoom}px`;
  415. this._frameCandidate.style.height = `${(localY - this._selectionStartY) / this.zoom}px`;
  416. } else {
  417. this._frameCandidate.style.top = `${localY / this.zoom}px`;
  418. this._frameCandidate.style.height = `${(this._selectionStartY - localY) / this.zoom}px`;
  419. }
  420. return;
  421. }
  422. // Candidate link
  423. if (this._candidateLink) {
  424. const rootRect = this.canvasContainer.getBoundingClientRect();
  425. this._candidatePort = null;
  426. this.props.globalState.onCandidateLinkMoved.notifyObservers(new Vector2(evt.pageX, evt.pageY));
  427. this._dropPointX = (evt.pageX - rootRect.left) / this.zoom;
  428. this._dropPointY = (evt.pageY - rootRect.top) / this.zoom;
  429. this._candidateLink.update(this._dropPointX, this._dropPointY, true);
  430. this._candidateLinkedHasMoved = true;
  431. return;
  432. }
  433. // Zoom with mouse + alt
  434. if (this._altKeyIsPressed && evt.buttons === 1) {
  435. if (this._oldY < 0) {
  436. this._oldY = evt.pageY;
  437. }
  438. let zoomDelta = (evt.pageY - this._oldY) / 10;
  439. if (Math.abs(zoomDelta) > 5) {
  440. const oldZoom = this.zoom;
  441. this.zoom = Math.max(Math.min(this.MaxZoom, this.zoom + zoomDelta / 100), this.MinZoom);
  442. const boundingRect = evt.currentTarget.getBoundingClientRect();
  443. const clientWidth = boundingRect.width;
  444. const widthDiff = clientWidth * this.zoom - clientWidth * oldZoom;
  445. const clientX = evt.clientX - boundingRect.left;
  446. const xFactor = (clientX - this.x) / oldZoom / clientWidth;
  447. this.x = this.x - widthDiff * xFactor;
  448. this._oldY = evt.pageY;
  449. }
  450. return;
  451. }
  452. // Move canvas
  453. this._rootContainer.style.cursor = "move";
  454. if (this._mouseStartPointX === null || this._mouseStartPointY === null) {
  455. return;
  456. }
  457. this.x += evt.clientX - this._mouseStartPointX;
  458. this.y += evt.clientY - this._mouseStartPointY;
  459. this._mouseStartPointX = evt.clientX;
  460. this._mouseStartPointY = evt.clientY;
  461. }
  462. onDown(evt: React.PointerEvent<HTMLElement>) {
  463. this._rootContainer.setPointerCapture(evt.pointerId);
  464. // Selection?
  465. if (evt.currentTarget === this._hostCanvas && evt.ctrlKey) {
  466. this._selectionBox = this.props.globalState.hostDocument.createElement("div");
  467. this._selectionBox.classList.add("selection-box");
  468. this._selectionContainer.appendChild(this._selectionBox);
  469. const rootRect = this.canvasContainer.getBoundingClientRect();
  470. this._selectionStartX = (evt.pageX - rootRect.left);
  471. this._selectionStartY = (evt.pageY - rootRect.top);
  472. this._selectionBox.style.left = `${this._selectionStartX / this.zoom}px`;
  473. this._selectionBox.style.top = `${this._selectionStartY / this.zoom}px`;
  474. this._selectionBox.style.width = "0px";
  475. this._selectionBox.style.height = "0px";
  476. return;
  477. }
  478. // Frame?
  479. if (evt.currentTarget === this._hostCanvas && evt.shiftKey) {
  480. this._frameCandidate = this.props.globalState.hostDocument.createElement("div");
  481. this._frameCandidate.classList.add("frame-box");
  482. this._frameContainer.appendChild(this._frameCandidate);
  483. const rootRect = this.canvasContainer.getBoundingClientRect();
  484. this._selectionStartX = (evt.pageX - rootRect.left);
  485. this._selectionStartY = (evt.pageY - rootRect.top);
  486. this._frameCandidate.style.left = `${this._selectionStartX / this.zoom}px`;
  487. this._frameCandidate.style.top = `${this._selectionStartY / this.zoom}px`;
  488. this._frameCandidate.style.width = "0px";
  489. this._frameCandidate.style.height = "0px";
  490. return;
  491. }
  492. // Port dragging
  493. if (evt.nativeEvent.srcElement && (evt.nativeEvent.srcElement as HTMLElement).nodeName === "IMG") {
  494. if (!this._candidateLink) {
  495. let portElement = ((evt.nativeEvent.srcElement as HTMLElement).parentElement as any).port as NodePort;
  496. this._candidateLink = new NodeLink(this, portElement, portElement.node);
  497. this._candidateLinkedHasMoved = false;
  498. }
  499. return;
  500. }
  501. this.props.globalState.onSelectionChangedObservable.notifyObservers(null);
  502. this._mouseStartPointX = evt.clientX;
  503. this._mouseStartPointY = evt.clientY;
  504. }
  505. onUp(evt: React.PointerEvent) {
  506. this._mouseStartPointX = null;
  507. this._mouseStartPointY = null;
  508. this._rootContainer.releasePointerCapture(evt.pointerId);
  509. this._oldY = -1;
  510. if (this._candidateLink) {
  511. if (this._candidateLinkedHasMoved) {
  512. this.processCandidatePort();
  513. this.props.globalState.onCandidateLinkMoved.notifyObservers(null);
  514. } else { // is a click event on NodePort
  515. if(this._candidateLink.portA instanceof FrameNodePort) { //only on Frame Node Ports
  516. const port = this._candidateLink.portA;
  517. const frame = this.frames.find((frame: GraphFrame) => frame.id === port.parentFrameId);
  518. if (frame) {
  519. const data: FramePortData = {
  520. frame,
  521. port
  522. }
  523. this.props.globalState.onSelectionChangedObservable.notifyObservers(data);
  524. }
  525. } else if(this._candidateLink.portA instanceof NodePort){
  526. this.props.globalState.onSelectionChangedObservable.notifyObservers(this._candidateLink.portA );
  527. }
  528. }
  529. this._candidateLink.dispose();
  530. this._candidateLink = null;
  531. this._candidatePort = null;
  532. }
  533. if (this._selectionBox) {
  534. this._selectionBox.parentElement!.removeChild(this._selectionBox);
  535. this._selectionBox = null;
  536. }
  537. if (this._frameCandidate) {
  538. let newFrame = new GraphFrame(this._frameCandidate, this);
  539. this._frames.push(newFrame);
  540. this._frameCandidate.parentElement!.removeChild(this._frameCandidate);
  541. this._frameCandidate = null;
  542. this.props.globalState.onSelectionChangedObservable.notifyObservers(newFrame);
  543. }
  544. }
  545. onWheel(evt: React.WheelEvent) {
  546. let delta = evt.deltaY < 0 ? 0.1 : -0.1;
  547. let oldZoom = this.zoom;
  548. this.zoom = Math.min(Math.max(this.MinZoom, this.zoom + delta * this.zoom), this.MaxZoom);
  549. const boundingRect = evt.currentTarget.getBoundingClientRect();
  550. const clientWidth = boundingRect.width;
  551. const clientHeight = boundingRect.height;
  552. const widthDiff = clientWidth * this.zoom - clientWidth * oldZoom;
  553. const heightDiff = clientHeight * this.zoom - clientHeight * oldZoom;
  554. const clientX = evt.clientX - boundingRect.left;
  555. const clientY = evt.clientY - boundingRect.top;
  556. const xFactor = (clientX - this.x) / oldZoom / clientWidth;
  557. const yFactor = (clientY - this.y) / oldZoom / clientHeight;
  558. this.x = this.x - widthDiff * xFactor;
  559. this.y = this.y - heightDiff * yFactor;
  560. evt.stopPropagation();
  561. }
  562. zoomToFit() {
  563. // Get negative offset
  564. let minX = 0;
  565. let minY = 0;
  566. this._nodes.forEach(node => {
  567. if (this._frames.some(f => f.nodes.indexOf(node) !== -1)) {
  568. return;
  569. }
  570. if (node.x < minX) {
  571. minX = node.x;
  572. }
  573. if (node.y < minY) {
  574. minY = node.y;
  575. }
  576. });
  577. this._frames.forEach(frame => {
  578. if (frame.x < minX) {
  579. minX = frame.x;
  580. }
  581. if (frame.y < minY) {
  582. minY = frame.y;
  583. }
  584. });
  585. // Restore to 0
  586. this._frames.forEach(frame => {
  587. frame.x += -minX;
  588. frame.y += -minY;
  589. frame.cleanAccumulation();
  590. });
  591. this._nodes.forEach(node => {
  592. node.x += -minX;
  593. node.y += -minY;
  594. node.cleanAccumulation();
  595. });
  596. // Get correct zoom
  597. const xFactor = this._rootContainer.clientWidth / this._rootContainer.scrollWidth;
  598. const yFactor = this._rootContainer.clientHeight / this._rootContainer.scrollHeight;
  599. const zoomFactor = xFactor < yFactor ? xFactor : yFactor;
  600. this.zoom = zoomFactor;
  601. this.x = 0;
  602. this.y = 0;
  603. }
  604. processCandidatePort() {
  605. let pointB = this._candidateLink!.portA.connectionPoint;
  606. let nodeB = this._candidateLink!.portA.node;
  607. let pointA: NodeMaterialConnectionPoint;
  608. let nodeA: GraphNode;
  609. if (this._candidatePort) {
  610. pointA = this._candidatePort.connectionPoint;
  611. nodeA = this._candidatePort.node;
  612. } else {
  613. if (pointB.direction === NodeMaterialConnectionPointDirection.Output) {
  614. return;
  615. }
  616. // No destination so let's spin a new input block
  617. let pointName = "output", emittedBlock;
  618. let customInputBlock = this._candidateLink!.portA.connectionPoint.createCustomInputBlock();
  619. if (!customInputBlock) {
  620. emittedBlock = new InputBlock(NodeMaterialBlockConnectionPointTypes[this._candidateLink!.portA.connectionPoint.type], undefined, this._candidateLink!.portA.connectionPoint.type);
  621. } else {
  622. [emittedBlock, pointName] = customInputBlock;
  623. }
  624. this.props.globalState.nodeMaterial.attachedBlocks.push(emittedBlock);
  625. pointA = (emittedBlock as any)[pointName];
  626. if (!emittedBlock.isInput) {
  627. emittedBlock.autoConfigure(this.props.globalState.nodeMaterial);
  628. nodeA = this.props.onEmitNewBlock(emittedBlock);
  629. } else {
  630. nodeA = this.appendBlock(emittedBlock);
  631. }
  632. nodeA.x = this._dropPointX - 200;
  633. nodeA.y = this._dropPointY - 50;
  634. let x = nodeA.x - 250;
  635. let y = nodeA.y;
  636. emittedBlock.inputs.forEach((connection) => {
  637. if (connection.connectedPoint) {
  638. var existingNodes = this.nodes.filter((n) => { return n.block === (connection as any).connectedPoint.ownerBlock });
  639. let connectedNode = existingNodes[0];
  640. if (connectedNode.x === 0 && connectedNode.y === 0) {
  641. connectedNode.x = x;
  642. connectedNode.y = y;
  643. connectedNode.cleanAccumulation();
  644. y += 80;
  645. }
  646. }
  647. });
  648. }
  649. if (pointA.direction === NodeMaterialConnectionPointDirection.Input) {
  650. let temp = pointB;
  651. pointB = pointA;
  652. pointA = temp;
  653. let tempNode = nodeA;
  654. nodeA = nodeB;
  655. nodeB = tempNode;
  656. }
  657. if (pointB.connectedPoint === pointA) {
  658. return;
  659. }
  660. if (pointB === pointA) {
  661. return;
  662. }
  663. if (pointB.direction === pointA.direction) {
  664. return;
  665. }
  666. if (pointB.ownerBlock === pointA.ownerBlock) {
  667. return;
  668. }
  669. // Check compatibility
  670. let isFragmentOutput = pointB.ownerBlock.getClassName() === "FragmentOutputBlock";
  671. let compatibilityState = pointA.checkCompatibilityState(pointB);
  672. if ((pointA.needDualDirectionValidation || pointB.needDualDirectionValidation) && compatibilityState === NodeMaterialConnectionPointCompatibilityStates.Compatible && !(pointA instanceof InputBlock)) {
  673. compatibilityState = pointB.checkCompatibilityState(pointA);
  674. }
  675. if (compatibilityState === NodeMaterialConnectionPointCompatibilityStates.Compatible) {
  676. if (isFragmentOutput) {
  677. let fragmentBlock = pointB.ownerBlock as FragmentOutputBlock;
  678. if (pointB.name === "rgb" && fragmentBlock.rgba.isConnected) {
  679. nodeB.getLinksForConnectionPoint(fragmentBlock.rgba)[0].dispose();
  680. } else if (pointB.name === "rgba" && fragmentBlock.rgb.isConnected) {
  681. nodeB.getLinksForConnectionPoint(fragmentBlock.rgb)[0].dispose();
  682. }
  683. }
  684. } else {
  685. let message = "";
  686. switch (compatibilityState) {
  687. case NodeMaterialConnectionPointCompatibilityStates.TypeIncompatible:
  688. message = "Cannot connect two different connection types";
  689. break;
  690. case NodeMaterialConnectionPointCompatibilityStates.TargetIncompatible:
  691. message = "Source block can only work in fragment shader whereas destination block is currently aimed for the vertex shader";
  692. break;
  693. }
  694. this.props.globalState.onErrorMessageDialogRequiredObservable.notifyObservers(message);
  695. return;
  696. }
  697. let linksToNotifyForDispose: Nullable<NodeLink[]> = null;
  698. if (pointB.isConnected) {
  699. let links = nodeB.getLinksForConnectionPoint(pointB);
  700. linksToNotifyForDispose = links.slice();
  701. links.forEach(link => {
  702. link.dispose(false);
  703. });
  704. }
  705. if (pointB.ownerBlock.inputsAreExclusive) { // Disconnect all inputs if block has exclusive inputs
  706. pointB.ownerBlock.inputs.forEach(i => {
  707. let links = nodeB.getLinksForConnectionPoint(i);
  708. if (!linksToNotifyForDispose) {
  709. linksToNotifyForDispose = links.slice();
  710. } else {
  711. linksToNotifyForDispose.push(...links.slice());
  712. }
  713. links.forEach(link => {
  714. link.dispose(false);
  715. });
  716. })
  717. }
  718. pointA.connectTo(pointB);
  719. this.connectPorts(pointA, pointB);
  720. if (pointB.innerType === NodeMaterialBlockConnectionPointTypes.AutoDetect) {
  721. // need to potentially propagate the type of pointA to other ports of blocks connected to owner of pointB
  722. const refreshNode = (node: GraphNode) => {
  723. node.refresh();
  724. const links = node.links;
  725. // refresh first the nodes so that the right types are assigned to the auto-detect ports
  726. links.forEach((link) => {
  727. const nodeA = link.nodeA, nodeB = link.nodeB;
  728. if (!visitedNodes.has(nodeA)) {
  729. visitedNodes.add(nodeA);
  730. refreshNode(nodeA);
  731. }
  732. if (nodeB && !visitedNodes.has(nodeB)) {
  733. visitedNodes.add(nodeB);
  734. refreshNode(nodeB);
  735. }
  736. });
  737. // then refresh the links to display the right color between ports
  738. links.forEach((link) => {
  739. if (!visitedLinks.has(link)) {
  740. visitedLinks.add(link);
  741. link.update();
  742. }
  743. });
  744. };
  745. const visitedNodes = new Set<GraphNode>([nodeA]);
  746. const visitedLinks = new Set<NodeLink>([nodeB.links[nodeB.links.length - 1]]);
  747. refreshNode(nodeB);
  748. } else {
  749. nodeB.refresh();
  750. }
  751. linksToNotifyForDispose?.forEach((link) => {
  752. link.onDisposedObservable.notifyObservers(link);
  753. link.onDisposedObservable.clear();
  754. });
  755. this.props.globalState.onRebuildRequiredObservable.notifyObservers();
  756. }
  757. processEditorData(editorData: IEditorData) {
  758. const frames = this._frames.splice(0);
  759. for (var frame of frames) {
  760. frame.dispose();
  761. }
  762. this._frames = [];
  763. this.x = editorData.x || 0;
  764. this.y = editorData.y || 0;
  765. this.zoom = editorData.zoom || 1;
  766. // Frames
  767. if (editorData.frames) {
  768. for (var frameData of editorData.frames) {
  769. var frame = GraphFrame.Parse(frameData, this, editorData.map);
  770. this._frames.push(frame);
  771. }
  772. }
  773. }
  774. addFrame(frameData: IFrameData) {
  775. const frame = GraphFrame.Parse(frameData, this, this.props.globalState.nodeMaterial.editorData.map);
  776. this._frames.push(frame);
  777. this.globalState.onSelectionChangedObservable.notifyObservers(frame);
  778. }
  779. render() {
  780. return (
  781. <div id="graph-canvas"
  782. onWheel={evt => this.onWheel(evt)}
  783. onPointerMove={evt => this.onMove(evt)}
  784. onPointerDown={evt => this.onDown(evt)}
  785. onPointerUp={evt => this.onUp(evt)}
  786. >
  787. <div id="graph-container">
  788. <div id="graph-canvas-container">
  789. </div>
  790. <div id="frame-container">
  791. </div>
  792. <svg id="graph-svg-container">
  793. </svg>
  794. <div id="selection-container">
  795. </div>
  796. </div>
  797. </div>
  798. );
  799. }
  800. }