graphCanvas.tsx 31 KB

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