workbench.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. import * as React from "react";
  2. import { GlobalState } from '../globalState';
  3. import { GUINode } from './guiNode';
  4. import * as dagre from 'dagre';
  5. import { Nullable } from 'babylonjs/types';
  6. import { DataStorage } from 'babylonjs/Misc/dataStorage';
  7. import { IEditorData} from '../nodeLocationInfo';
  8. import 'babylonjs-gui/2D/';
  9. require("./graphCanvas.scss");
  10. export interface IWorkbenchComponentProps {
  11. globalState: GlobalState
  12. }
  13. export type FramePortData = {
  14. }
  15. export const isFramePortData = (variableToCheck: any): variableToCheck is FramePortData => {
  16. if (variableToCheck) {
  17. return (variableToCheck as FramePortData) !== undefined;
  18. }
  19. else return false;
  20. }
  21. export class WorkbenchComponent extends React.Component<IWorkbenchComponentProps> {
  22. private readonly MinZoom = 0.1;
  23. private readonly MaxZoom = 4;
  24. private _hostCanvas: HTMLDivElement;
  25. private _graphCanvas: HTMLDivElement;
  26. private _selectionContainer: HTMLDivElement;
  27. private _frameContainer: HTMLDivElement;
  28. private _svgCanvas: HTMLElement;
  29. private _rootContainer: HTMLDivElement;
  30. private _guiNodes: GUINode[] = [];
  31. private _mouseStartPointX: Nullable<number> = null;
  32. private _mouseStartPointY: Nullable<number> = null
  33. private _selectionStartX = 0;
  34. private _selectionStartY = 0;
  35. private _x = 0;
  36. private _y = 0;
  37. private _zoom = 1;
  38. private _selectedGuiNodes: GUINode[] = [];
  39. private _gridSize = 20;
  40. private _selectionBox: Nullable<HTMLDivElement> = null;
  41. private _frameCandidate: Nullable<HTMLDivElement> = null;
  42. private _altKeyIsPressed = false;
  43. private _ctrlKeyIsPressed = false;
  44. private _oldY = -1;
  45. public _frameIsMoving = false;
  46. public _isLoading = false;
  47. public isOverGUINode = false;
  48. public get gridSize() {
  49. return this._gridSize;
  50. }
  51. public set gridSize(value: number) {
  52. this._gridSize = value;
  53. this.updateTransform();
  54. }
  55. public get globalState(){
  56. return this.props.globalState;
  57. }
  58. public get nodes() {
  59. return this._guiNodes;
  60. }
  61. public get zoom() {
  62. return this._zoom;
  63. }
  64. public set zoom(value: number) {
  65. if (this._zoom === value) {
  66. return;
  67. }
  68. this._zoom = value;
  69. this.updateTransform();
  70. }
  71. public get x() {
  72. return this._x;
  73. }
  74. public set x(value: number) {
  75. this._x = value;
  76. this.updateTransform();
  77. }
  78. public get y() {
  79. return this._y;
  80. }
  81. public set y(value: number) {
  82. this._y = value;
  83. this.updateTransform();
  84. }
  85. public get selectedGuiNodes() {
  86. return this._selectedGuiNodes;
  87. }
  88. public get canvasContainer() {
  89. return this._graphCanvas;
  90. }
  91. public get hostCanvas() {
  92. return this._hostCanvas;
  93. }
  94. public get svgCanvas() {
  95. return this._svgCanvas;
  96. }
  97. public get selectionContainer() {
  98. return this._selectionContainer;
  99. }
  100. public get frameContainer() {
  101. return this._frameContainer;
  102. }
  103. constructor(props: IWorkbenchComponentProps) {
  104. super(props);
  105. props.globalState.onSelectionChangedObservable.add(selection => {
  106. if (!selection) {
  107. this.selectedGuiNodes.forEach(element => {
  108. element.isSelected = false;
  109. });
  110. this._selectedGuiNodes = [];
  111. }
  112. else {
  113. if (selection instanceof GUINode){
  114. if (this._ctrlKeyIsPressed) {
  115. if (this._selectedGuiNodes.indexOf(selection) === -1) {
  116. this._selectedGuiNodes.push(selection);
  117. }
  118. }
  119. else {
  120. this._selectedGuiNodes = [selection];
  121. }
  122. }
  123. else {
  124. this._selectedGuiNodes = [];
  125. }
  126. }
  127. });
  128. props.globalState.onGridSizeChanged.add(() => {
  129. this.gridSize = DataStorage.ReadNumber("GridSize", 20);
  130. });
  131. this.props.globalState.hostDocument!.addEventListener("keyup", () => this.onKeyUp(), false);
  132. this.props.globalState.hostDocument!.addEventListener("keydown", evt => {
  133. this._altKeyIsPressed = evt.altKey;
  134. this._ctrlKeyIsPressed = evt.ctrlKey;
  135. }, false);
  136. this.props.globalState.hostDocument!.defaultView!.addEventListener("blur", () => {
  137. this._altKeyIsPressed = false;
  138. this._ctrlKeyIsPressed = false;
  139. }, false);
  140. // Store additional data to serialization object
  141. this.props.globalState.storeEditorData = (editorData, graphFrame) => {
  142. editorData.frames = [];
  143. if (graphFrame) {
  144. } else {
  145. editorData.x = this.x;
  146. editorData.y = this.y;
  147. editorData.zoom = this.zoom;
  148. }
  149. }
  150. this.props.globalState.workbench = this;
  151. }
  152. public getGridPosition(position: number, useCeil = false) {
  153. let gridSize = this.gridSize;
  154. if (gridSize === 0) {
  155. return position;
  156. }
  157. if (useCeil) {
  158. return gridSize * Math.ceil(position / gridSize);
  159. }
  160. return gridSize * Math.floor(position / gridSize);
  161. }
  162. public getGridPositionCeil(position: number) {
  163. let gridSize = this.gridSize;
  164. if (gridSize === 0) {
  165. return position;
  166. }
  167. return gridSize * Math.ceil(position / gridSize);
  168. }
  169. updateTransform() {
  170. this._rootContainer.style.transform = `translate(${this._x}px, ${this._y}px) scale(${this._zoom})`;
  171. if (DataStorage.ReadBoolean("ShowGrid", true)) {
  172. this._hostCanvas.style.backgroundSize = `${this._gridSize * this._zoom}px ${this._gridSize * this._zoom}px`;
  173. this._hostCanvas.style.backgroundPosition = `${this._x}px ${this._y}px`;
  174. } else {
  175. this._hostCanvas.style.backgroundSize = `0`;
  176. }
  177. }
  178. onKeyUp() {
  179. this._altKeyIsPressed = false;
  180. this._ctrlKeyIsPressed = false;
  181. this._oldY = -1;
  182. }
  183. findNodeFromGuiElement(guiElement: BABYLON.GUI.Control) {
  184. return this._guiNodes.filter(n => n.guiNode === guiElement)[0];
  185. }
  186. reset() {
  187. for (var node of this._guiNodes) {
  188. node.dispose();
  189. }
  190. this._guiNodes = [];
  191. this._graphCanvas.innerHTML = "";
  192. this._svgCanvas.innerHTML = "";
  193. }
  194. appendBlock(guiElement: BABYLON.GUI.Control) {
  195. var newGuiNode = new GUINode(this.props.globalState, guiElement);
  196. newGuiNode.appendVisual(this._graphCanvas, this);
  197. this._guiNodes.push(newGuiNode);
  198. this.globalState.guiTexture.addControl(guiElement);
  199. return newGuiNode;
  200. }
  201. distributeGraph() {
  202. this.x = 0;
  203. this.y = 0;
  204. this.zoom = 1;
  205. let graph = new dagre.graphlib.Graph();
  206. graph.setGraph({});
  207. graph.setDefaultEdgeLabel(() => ({}));
  208. graph.graph().rankdir = "LR";
  209. // Build dagre graph
  210. this._guiNodes.forEach(node => {
  211. graph.setNode(node.id.toString(), {
  212. id: node.id,
  213. type: "node",
  214. width: node.width,
  215. height: node.height
  216. });
  217. });
  218. // Distribute
  219. dagre.layout(graph);
  220. // Update graph
  221. let dagreNodes = graph.nodes().map(node => graph.node(node));
  222. dagreNodes.forEach((dagreNode: any) => {
  223. if (!dagreNode) {
  224. return;
  225. }
  226. if (dagreNode.type === "node") {
  227. for (var node of this._guiNodes) {
  228. if (node.id === dagreNode.id) {
  229. node.x = dagreNode.x - dagreNode.width / 2;
  230. node.y = dagreNode.y - dagreNode.height / 2;
  231. node.cleanAccumulation();
  232. return;
  233. }
  234. }
  235. return;
  236. }
  237. });
  238. }
  239. componentDidMount() {
  240. this._hostCanvas = this.props.globalState.hostDocument.getElementById("graph-canvas") as HTMLDivElement;
  241. this._rootContainer = this.props.globalState.hostDocument.getElementById("graph-container") as HTMLDivElement;
  242. this._graphCanvas = this.props.globalState.hostDocument.getElementById("graph-canvas-container") as HTMLDivElement;
  243. this._svgCanvas = this.props.globalState.hostDocument.getElementById("graph-svg-container") as HTMLElement;
  244. this._selectionContainer = this.props.globalState.hostDocument.getElementById("selection-container") as HTMLDivElement;
  245. this._frameContainer = this.props.globalState.hostDocument.getElementById("frame-container") as HTMLDivElement;
  246. this.gridSize = DataStorage.ReadNumber("GridSize", 20);
  247. this.updateTransform();
  248. }
  249. onMove(evt: React.PointerEvent) {
  250. // Selection box
  251. if (this._selectionBox) {
  252. const rootRect = this.canvasContainer.getBoundingClientRect();
  253. const localX = evt.pageX - rootRect.left;
  254. const localY = evt.pageY - rootRect.top;
  255. if (localX > this._selectionStartX) {
  256. this._selectionBox.style.left = `${this._selectionStartX / this.zoom}px`;
  257. this._selectionBox.style.width = `${(localX - this._selectionStartX) / this.zoom}px`;
  258. } else {
  259. this._selectionBox.style.left = `${localX / this.zoom}px`;
  260. this._selectionBox.style.width = `${(this._selectionStartX - localX) / this.zoom}px`;
  261. }
  262. if (localY > this._selectionStartY) {
  263. this._selectionBox.style.top = `${this._selectionStartY / this.zoom}px`;
  264. this._selectionBox.style.height = `${(localY - this._selectionStartY) / this.zoom}px`;
  265. } else {
  266. this._selectionBox.style.top = `${localY / this.zoom}px`;
  267. this._selectionBox.style.height = `${(this._selectionStartY - localY) / this.zoom}px`;
  268. }
  269. this.props.globalState.onSelectionBoxMoved.notifyObservers(this._selectionBox.getBoundingClientRect());
  270. return;
  271. }
  272. // Zoom with mouse + alt
  273. if (this._altKeyIsPressed && evt.buttons === 1) {
  274. if (this._oldY < 0) {
  275. this._oldY = evt.pageY;
  276. }
  277. let zoomDelta = (evt.pageY - this._oldY) / 10;
  278. if (Math.abs(zoomDelta) > 5) {
  279. const oldZoom = this.zoom;
  280. this.zoom = Math.max(Math.min(this.MaxZoom, this.zoom + zoomDelta / 100), this.MinZoom);
  281. const boundingRect = evt.currentTarget.getBoundingClientRect();
  282. const clientWidth = boundingRect.width;
  283. const widthDiff = clientWidth * this.zoom - clientWidth * oldZoom;
  284. const clientX = evt.clientX - boundingRect.left;
  285. const xFactor = (clientX - this.x) / oldZoom / clientWidth;
  286. this.x = this.x - widthDiff * xFactor;
  287. this._oldY = evt.pageY;
  288. }
  289. return;
  290. }
  291. // Move canvas and/or guiNodes
  292. if (this._mouseStartPointX != null && this._mouseStartPointY != null) {
  293. var x = this._mouseStartPointX;
  294. var y = this._mouseStartPointY;
  295. let selected = false;
  296. this.selectedGuiNodes.forEach(element => {
  297. selected = element._onMove(new BABYLON.Vector2(evt.clientX, evt.clientY),
  298. new BABYLON.Vector2( x, y)) || selected;
  299. });
  300. if(!selected) {
  301. this._rootContainer.style.cursor = "move";
  302. this.x += evt.clientX - this._mouseStartPointX;
  303. this.y += evt.clientY - this._mouseStartPointY;
  304. }
  305. this._mouseStartPointX = evt.clientX;
  306. this._mouseStartPointY = evt.clientY;
  307. }
  308. }
  309. onDown(evt: React.PointerEvent<HTMLElement>) {
  310. this._rootContainer.setPointerCapture(evt.pointerId);
  311. // Selection?
  312. /*if (evt.currentTarget === this._hostCanvas && evt.ctrlKey) {
  313. this._selectionBox = this.props.globalState.hostDocument.createElement("div");
  314. this._selectionBox.classList.add("selection-box");
  315. this._selectionContainer.appendChild(this._selectionBox);
  316. const rootRect = this.canvasContainer.getBoundingClientRect();
  317. this._selectionStartX = (evt.pageX - rootRect.left);
  318. this._selectionStartY = (evt.pageY - rootRect.top);
  319. this._selectionBox.style.left = `${this._selectionStartX / this.zoom}px`;
  320. this._selectionBox.style.top = `${this._selectionStartY / this.zoom}px`;
  321. this._selectionBox.style.width = "0px";
  322. this._selectionBox.style.height = "0px";
  323. return;
  324. }*/
  325. console.log('workbench click');
  326. if(!this.isOverGUINode) {
  327. console.log('unclicked');
  328. this.props.globalState.onSelectionChangedObservable.notifyObservers(null);
  329. }
  330. this._mouseStartPointX = evt.clientX;
  331. this._mouseStartPointY = evt.clientY;
  332. }
  333. onUp(evt: React.PointerEvent) {
  334. this._mouseStartPointX = null;
  335. this._mouseStartPointY = null;
  336. this._rootContainer.releasePointerCapture(evt.pointerId);
  337. this._oldY = -1;
  338. if (this._selectionBox) {
  339. this._selectionBox.parentElement!.removeChild(this._selectionBox);
  340. this._selectionBox = null;
  341. }
  342. if (this._frameCandidate) {
  343. this._frameCandidate.parentElement!.removeChild(this._frameCandidate);
  344. this._frameCandidate = null;
  345. }
  346. }
  347. onWheel(evt: React.WheelEvent) {
  348. let delta = evt.deltaY < 0 ? 0.1 : -0.1;
  349. let oldZoom = this.zoom;
  350. this.zoom = Math.min(Math.max(this.MinZoom, this.zoom + delta * this.zoom), this.MaxZoom);
  351. const boundingRect = evt.currentTarget.getBoundingClientRect();
  352. const clientWidth = boundingRect.width;
  353. const clientHeight = boundingRect.height;
  354. const widthDiff = clientWidth * this.zoom - clientWidth * oldZoom;
  355. const heightDiff = clientHeight * this.zoom - clientHeight * oldZoom;
  356. const clientX = evt.clientX - boundingRect.left;
  357. const clientY = evt.clientY - boundingRect.top;
  358. const xFactor = (clientX - this.x) / oldZoom / clientWidth;
  359. const yFactor = (clientY - this.y) / oldZoom / clientHeight;
  360. this.x = this.x - widthDiff * xFactor;
  361. this.y = this.y - heightDiff * yFactor;
  362. evt.stopPropagation();
  363. }
  364. zoomToFit() {
  365. // Get negative offset
  366. let minX = 0;
  367. let minY = 0;
  368. this._guiNodes.forEach(node => {
  369. if (node.x < minX) {
  370. minX = node.x;
  371. }
  372. if (node.y < minY) {
  373. minY = node.y;
  374. }
  375. });
  376. // Restore to 0
  377. this._guiNodes.forEach(node => {
  378. node.x += -minX;
  379. node.y += -minY;
  380. node.cleanAccumulation();
  381. });
  382. // Get correct zoom
  383. const xFactor = this._rootContainer.clientWidth / this._rootContainer.scrollWidth;
  384. const yFactor = this._rootContainer.clientHeight / this._rootContainer.scrollHeight;
  385. const zoomFactor = xFactor < yFactor ? xFactor : yFactor;
  386. this.zoom = zoomFactor;
  387. this.x = 0;
  388. this.y = 0;
  389. }
  390. processEditorData(editorData: IEditorData) {
  391. this.x = editorData.x || 0;
  392. this.y = editorData.y || 0;
  393. this.zoom = editorData.zoom || 1;
  394. }
  395. public createGUICanvas()
  396. {
  397. // Get the canvas element from the DOM.
  398. const canvas = document.getElementById("graph-canvas") as HTMLCanvasElement;
  399. // Associate a Babylon Engine to it.
  400. const engine = new BABYLON.Engine(canvas);
  401. // Create our first scene.
  402. var scene = new BABYLON.Scene(engine);
  403. scene.clearColor = new BABYLON.Color4(0.2, 0.2, 0.3, 0.1);
  404. // This creates and positions a free camera (non-mesh)
  405. var camera = new BABYLON.FreeCamera("camera1", new BABYLON.Vector3(0, 5, -10), scene);
  406. // This targets the camera to scene origin
  407. camera.setTarget(BABYLON.Vector3.Zero());
  408. // This attaches the camera to the canvas
  409. //camera.attachControl(true);
  410. // GUI
  411. this.globalState.guiTexture = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("UI");
  412. scene.getEngine().onCanvasPointerOutObservable.clear();
  413. // Watch for browser/canvas resize events
  414. window.addEventListener("resize", function () {
  415. engine.resize();
  416. });
  417. engine.runRenderLoop(() => {this.updateGUIs(); scene.render()});
  418. }
  419. updateGUIs()
  420. {
  421. this._guiNodes.forEach(element => {
  422. element.updateVisual();
  423. });
  424. }
  425. render() {
  426. return <canvas id="graph-canvas"
  427. onWheel={evt => this.onWheel(evt)}
  428. onPointerMove={evt => this.onMove(evt)}
  429. onPointerDown={evt => this.onDown(evt)}
  430. onPointerUp={evt => this.onUp(evt)}
  431. >
  432. <div id="graph-container">
  433. <div id="graph-canvas-container">
  434. </div>
  435. <div id="frame-container">
  436. </div>
  437. <svg id="graph-svg-container">
  438. </svg>
  439. <div id="selection-container">
  440. </div>
  441. </div>
  442. </canvas>
  443. }
  444. }