workbench.tsx 18 KB

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