graphFrame.ts 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805
  1. import { GraphNode } from './graphNode';
  2. import { GraphCanvasComponent } from './graphCanvas';
  3. import { Nullable } from 'babylonjs/types';
  4. import { Observer, Observable } from 'babylonjs/Misc/observable';
  5. import { NodeLink } from './nodeLink';
  6. import { IFrameData } from '../nodeLocationInfo';
  7. import { Color3 } from 'babylonjs/Maths/math.color';
  8. import { NodePort } from './nodePort';
  9. import { SerializationTools } from '../serializationTools';
  10. import { StringTools } from '../stringTools';
  11. enum ResizingDirection {
  12. Right,
  13. Left,
  14. Top,
  15. Bottom
  16. }
  17. export class GraphFrame {
  18. private readonly CollapsedWidth = 200;
  19. private static _FrameCounter = 0;
  20. private _name: string;
  21. private _color: Color3;
  22. private _x = 0;
  23. private _y = 0;
  24. private _gridAlignedX = 0;
  25. private _gridAlignedY = 0;
  26. private _width: number;
  27. private _height: number;
  28. public element: HTMLDivElement;
  29. private _borderElement: HTMLDivElement;
  30. private _headerElement: HTMLDivElement;
  31. private _headerTextElement: HTMLDivElement;
  32. private _headerCollapseElement: HTMLDivElement;
  33. private _headerCloseElement: HTMLDivElement;
  34. private _commentsElement: HTMLDivElement;
  35. private _portContainer: HTMLDivElement;
  36. private _outputPortContainer: HTMLDivElement;
  37. private _inputPortContainer: HTMLDivElement;
  38. private _nodes: GraphNode[] = [];
  39. private _ownerCanvas: GraphCanvasComponent;
  40. private _mouseStartPointX: Nullable<number> = null;
  41. private _mouseStartPointY: Nullable<number> = null;
  42. private _onSelectionChangedObserver: Nullable<Observer<Nullable<GraphNode | NodeLink | GraphFrame>>>;
  43. private _isCollapsed = false;
  44. private _ports: NodePort[] = [];
  45. private _controlledPorts: NodePort[] = [];
  46. private _id: number;
  47. private _comments: string;
  48. private _frameIsResizing: boolean;
  49. private _resizingDirection: Nullable<ResizingDirection>;
  50. private _minFrameHeight = 40;
  51. private _minFrameWidth = 220;
  52. private mouseXLimit: Nullable<number>;
  53. public onExpandStateChanged = new Observable<GraphFrame>();
  54. private readonly CloseSVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><g id="Layer_2" data-name="Layer 2"><path d="M16,15l5.85,5.84-1,1L15,15.93,9.15,21.78l-1-1L14,15,8.19,9.12l1-1L15,14l5.84-5.84,1,1Z"/></g></svg>`;
  55. private readonly ExpandSVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><g id="Layer_2" data-name="Layer 2"><path d="M22.31,7.69V22.31H7.69V7.69ZM21.19,8.81H8.81V21.19H21.19Zm-6.75,6.75H11.06V14.44h3.38V11.06h1.12v3.38h3.38v1.12H15.56v3.38H14.44Z"/></g></svg>`;
  56. private readonly CollapseSVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><g id="Layer_2" data-name="Layer 2"><path d="M22.31,7.69V22.31H7.69V7.69ZM21.19,8.81H8.81V21.19H21.19Zm-2.25,6.75H11.06V14.44h7.88Z"/></g></svg>`;
  57. public get id() {
  58. return this._id;
  59. }
  60. public get isCollapsed() {
  61. return this._isCollapsed;
  62. }
  63. private _createInputPort(port: NodePort, node: GraphNode) {
  64. let localPort = NodePort.CreatePortElement(port.connectionPoint, node, this._inputPortContainer, null, this._ownerCanvas.globalState)
  65. this._ports.push(localPort);
  66. port.delegatedPort = localPort;
  67. this._controlledPorts.push(port);
  68. }
  69. public set isCollapsed(value: boolean) {
  70. if (this._isCollapsed === value) {
  71. return;
  72. }
  73. this._isCollapsed = value;
  74. this._ownerCanvas._frameIsMoving = true;
  75. // Need to delegate the outside ports to the frame
  76. if (value) {
  77. this.element.classList.add("collapsed");
  78. this._moveFrame((this.width - this.CollapsedWidth) / 2, 0);
  79. for (var node of this._nodes) {
  80. node.isVisible = false;
  81. for (var port of node.outputPorts) { // Output
  82. if (port.connectionPoint.hasEndpoints) {
  83. let portAdded = false;
  84. for (var link of node.links) {
  85. if (link.portA === port && this.nodes.indexOf(link.nodeB!) === -1) {
  86. let localPort: NodePort;
  87. if (!portAdded) {
  88. portAdded = true;
  89. localPort = NodePort.CreatePortElement(port.connectionPoint, link.nodeB!, this._outputPortContainer, null, this._ownerCanvas.globalState);
  90. this._ports.push(localPort);
  91. } else {
  92. localPort = this._ports.filter(p => p.connectionPoint === port.connectionPoint)[0];
  93. }
  94. port.delegatedPort = localPort;
  95. this._controlledPorts.push(port);
  96. link.isVisible = true;
  97. }
  98. }
  99. } else {
  100. let localPort = NodePort.CreatePortElement(port.connectionPoint, node, this._outputPortContainer, null, this._ownerCanvas.globalState)
  101. this._ports.push(localPort);
  102. port.delegatedPort = localPort;
  103. this._controlledPorts.push(port);
  104. }
  105. }
  106. for (var port of node.inputPorts) { // Input
  107. if (port.connectionPoint.isConnected) {
  108. for (var link of node.links) {
  109. if (link.portB === port && this.nodes.indexOf(link.nodeA) === -1) {
  110. this._createInputPort(port, node);
  111. link.isVisible = true;
  112. }
  113. }
  114. } else {
  115. this._createInputPort(port, node);
  116. }
  117. }
  118. }
  119. } else {
  120. this.element.classList.remove("collapsed");
  121. this._outputPortContainer.innerHTML = "";
  122. this._inputPortContainer.innerHTML = "";
  123. this._ports.forEach(p => {
  124. p.dispose();
  125. });
  126. this._controlledPorts.forEach(port => {
  127. port.delegatedPort = null;
  128. port.refresh();
  129. })
  130. this._ports = [];
  131. this._controlledPorts = [];
  132. for (var node of this._nodes) {
  133. node.isVisible = true;
  134. }
  135. this._moveFrame(-(this.width - this.CollapsedWidth) / 2, 0);
  136. }
  137. this.cleanAccumulation();
  138. this._ownerCanvas._frameIsMoving = false;
  139. // UI
  140. if (this._isCollapsed) {
  141. this._headerCollapseElement.innerHTML = this.ExpandSVG;
  142. this._headerCollapseElement.title = "Expand";
  143. } else {
  144. this._headerCollapseElement.innerHTML = this.CollapseSVG;
  145. this._headerCollapseElement.title = "Collapse";
  146. }
  147. this.onExpandStateChanged.notifyObservers(this);
  148. }
  149. public get nodes() {
  150. return this._nodes;
  151. }
  152. public get name() {
  153. return this._name;
  154. }
  155. public set name(value: string) {
  156. this._name = value;
  157. this._headerTextElement.innerHTML = value;
  158. }
  159. public get color() {
  160. return this._color;
  161. }
  162. public set color(value: Color3) {
  163. this._color = value;
  164. this._headerElement.style.background = `rgba(${value.r * 255}, ${value.g * 255}, ${value.b * 255}, 1)`;
  165. this._headerElement.style.borderColor = `rgba(${value.r * 255}, ${value.g * 255}, ${value.b * 255}, 1)`;
  166. this.element.style.background = `rgba(${value.r * 255}, ${value.g * 255}, ${value.b * 255}, 0.7)`;
  167. }
  168. public get x() {
  169. return this._x;
  170. }
  171. public set x(value: number) {
  172. if (this._x === value) {
  173. return;
  174. }
  175. this._x = value;
  176. this._gridAlignedX = this._ownerCanvas.getGridPosition(value);
  177. this.element.style.left = `${this._gridAlignedX}px`;
  178. }
  179. public get y() {
  180. return this._y;
  181. }
  182. public set y(value: number) {
  183. if (this._y === value) {
  184. return;
  185. }
  186. this._y = value;
  187. this._gridAlignedY = this._ownerCanvas.getGridPosition(value);
  188. this.element.style.top = `${this._gridAlignedY}px`;
  189. }
  190. public get width() {
  191. return this._width;
  192. }
  193. public set width(value: number) {
  194. if (this._width === value) {
  195. return;
  196. }
  197. let viableWidth = value > this._minFrameWidth ? value : this._minFrameWidth;
  198. this._width = viableWidth;
  199. var gridAlignedRight = this._ownerCanvas.getGridPositionCeil(viableWidth + this._gridAlignedX);
  200. this.element.style.width = `${gridAlignedRight - this._gridAlignedX}px`;
  201. }
  202. public get height() {
  203. return this._height;
  204. }
  205. public set height(value: number) {
  206. if (this._height === value) {
  207. return;
  208. }
  209. this._height = value;
  210. var gridAlignedBottom = this._ownerCanvas.getGridPositionCeil(value + this._gridAlignedY);
  211. this.element.style.height = `${gridAlignedBottom - this._gridAlignedY}px`;
  212. }
  213. public get comments(): string {
  214. return this._comments;
  215. }
  216. public set comments(comments: string) {
  217. if (comments && comments.length > 0) {
  218. this.element.style.gridTemplateRows = "40px 40px calc(100% - 80px)";
  219. this._borderElement.style.gridRow = "1 / span 3";
  220. this._portContainer.style.gridRow = "3";
  221. this._commentsElement.style.display = "grid";
  222. this._commentsElement.style.gridRow = "2";
  223. this._commentsElement.style.gridColumn = "1";
  224. this._commentsElement.style.paddingLeft = "10px";
  225. this._commentsElement.style.fontStyle = "italic";
  226. this._commentsElement.innerText = comments;
  227. }
  228. this._comments = comments;
  229. }
  230. public constructor(candidate: Nullable<HTMLDivElement>, canvas: GraphCanvasComponent, doNotCaptureNodes = false) {
  231. this._id = GraphFrame._FrameCounter++;
  232. this._ownerCanvas = canvas;
  233. const root = canvas.frameContainer;
  234. this.element = root.ownerDocument!.createElement("div");
  235. this.element.classList.add("frame-box");
  236. root.appendChild(this.element);
  237. this._headerElement = root.ownerDocument!.createElement("div");
  238. this._headerElement.classList.add("frame-box-header");
  239. this._headerElement.addEventListener("dblclick", () => {
  240. this.isCollapsed = !this.isCollapsed;
  241. });
  242. this.element.appendChild(this._headerElement);
  243. this._borderElement = root.ownerDocument!.createElement("div");
  244. this._borderElement.classList.add("frame-box-border");
  245. this.element.appendChild(this._borderElement);
  246. // add resizing side handles
  247. const rightHandle: HTMLDivElement = root.ownerDocument!.createElement("div");
  248. rightHandle.className = "handle right-handle";
  249. this.element.appendChild(rightHandle);
  250. rightHandle.addEventListener("pointerdown", this._onRightHandlePointerDown);
  251. const leftHandle: HTMLDivElement = root.ownerDocument!.createElement("div");
  252. leftHandle.className = "handle left-handle";
  253. this.element.appendChild(leftHandle);
  254. leftHandle.addEventListener("pointerdown", this._onLeftHandlePointerDown);
  255. const bottomHandle: HTMLDivElement = root.ownerDocument!.createElement("div");
  256. bottomHandle.className = "handle bottom-handle";
  257. this.element.appendChild(bottomHandle);
  258. bottomHandle.addEventListener("pointerdown", this._onBottomHandlePointerDown);
  259. const topHandle: HTMLDivElement = root.ownerDocument!.createElement("div");
  260. topHandle.className = "handle top-handle";
  261. this.element.appendChild(topHandle);
  262. topHandle.addEventListener("pointerdown", this._onTopHandlePointerDown);
  263. this._headerTextElement = root.ownerDocument!.createElement("div");
  264. this._headerTextElement.classList.add("frame-box-header-title");
  265. this._headerElement.appendChild(this._headerTextElement);
  266. this._headerCollapseElement = root.ownerDocument!.createElement("div");
  267. this._headerCollapseElement.classList.add("frame-box-header-collapse");
  268. this._headerCollapseElement.classList.add("frame-box-header-button");
  269. this._headerCollapseElement.title = "Collapse";
  270. this._headerCollapseElement.ondragstart= () => false;
  271. this._headerCollapseElement.addEventListener("pointerdown", (evt) => {
  272. this._headerCollapseElement.classList.add("down");
  273. evt.stopPropagation();
  274. });
  275. this._headerCollapseElement.addEventListener("pointerup", (evt) => {
  276. evt.stopPropagation();
  277. this._headerCollapseElement.classList.remove("down");
  278. this.isCollapsed = !this.isCollapsed;
  279. });
  280. this._headerCollapseElement.innerHTML = this.CollapseSVG;
  281. this._headerElement.appendChild(this._headerCollapseElement);
  282. this._headerCloseElement = root.ownerDocument!.createElement("div");
  283. this._headerCloseElement.classList.add("frame-box-header-close");
  284. this._headerCloseElement.classList.add("frame-box-header-button");
  285. this._headerCloseElement.title = "Close";
  286. this._headerCloseElement.ondragstart= () => false;
  287. this._headerCloseElement.addEventListener("pointerdown", (evt) => {
  288. evt.stopPropagation();
  289. });
  290. this._headerCloseElement.addEventListener("pointerup", (evt) => {
  291. evt.stopPropagation();
  292. this.dispose();
  293. });
  294. this._headerCloseElement.innerHTML = this.CloseSVG;
  295. this._headerElement.appendChild(this._headerCloseElement);
  296. this._portContainer = root.ownerDocument!.createElement("div");
  297. this._portContainer.classList.add("port-container");
  298. this.element.appendChild(this._portContainer);
  299. this._outputPortContainer = root.ownerDocument!.createElement("div");
  300. this._outputPortContainer.classList.add("outputsContainer");
  301. this._portContainer.appendChild(this._outputPortContainer);
  302. this._inputPortContainer = root.ownerDocument!.createElement("div");
  303. this._inputPortContainer.classList.add("inputsContainer");
  304. this._portContainer.appendChild(this._inputPortContainer);
  305. this.name = "Frame";
  306. this.color = Color3.FromInts(72, 72, 72);
  307. if (candidate) {
  308. this.x = parseFloat(candidate.style.left!.replace("px", ""));
  309. this.y = parseFloat(candidate.style.top!.replace("px", ""));
  310. this.width = parseFloat(candidate.style.width!.replace("px", ""));
  311. this.height = parseFloat(candidate.style.height!.replace("px", ""));
  312. this.cleanAccumulation();
  313. }
  314. this._headerTextElement.addEventListener("pointerdown", evt => this._onDown(evt));
  315. this._headerTextElement.addEventListener("pointerup", evt => this._onUp(evt));
  316. this._headerTextElement.addEventListener("pointermove", evt => this._onMove(evt));
  317. this._onSelectionChangedObserver = canvas.globalState.onSelectionChangedObservable.add(node => {
  318. if (node === this) {
  319. this.element.classList.add("selected");
  320. } else {
  321. this.element.classList.remove("selected");
  322. }
  323. });
  324. this._commentsElement = document.createElement('div');
  325. this._commentsElement.className = 'frame-comments';
  326. this._commentsElement.style.color = 'white';
  327. this._commentsElement.style.fontSize = '16px';
  328. this.element.appendChild(this._commentsElement);
  329. // Get nodes
  330. if (!doNotCaptureNodes) {
  331. this.refresh();
  332. }
  333. }
  334. public refresh() {
  335. this._nodes = [];
  336. this._ownerCanvas.globalState.onFrameCreated.notifyObservers(this);
  337. }
  338. public addNode(node: GraphNode) {
  339. let index = this.nodes.indexOf(node);
  340. if (index === -1) {
  341. this.nodes.push(node);
  342. }
  343. }
  344. public removeNode(node: GraphNode) {
  345. let index = this.nodes.indexOf(node);
  346. if (index > -1) {
  347. this.nodes.splice(index, 1);
  348. }
  349. }
  350. public syncNode(node: GraphNode) {
  351. if (this.isCollapsed) {
  352. return;
  353. }
  354. if (node.isOverlappingFrame(this)) {
  355. this.addNode(node);
  356. } else {
  357. this.removeNode(node);
  358. }
  359. }
  360. public cleanAccumulation() {
  361. for (var selectedNode of this._nodes) {
  362. selectedNode.cleanAccumulation();
  363. }
  364. this.x = this._ownerCanvas.getGridPosition(this.x);
  365. this.y = this._ownerCanvas.getGridPosition(this.y);
  366. }
  367. private _onDown(evt: PointerEvent) {
  368. evt.stopPropagation();
  369. this._mouseStartPointX = evt.clientX;
  370. this._mouseStartPointY = evt.clientY;
  371. this._headerTextElement.setPointerCapture(evt.pointerId);
  372. this._ownerCanvas.globalState.onSelectionChangedObservable.notifyObservers(this);
  373. this._ownerCanvas._frameIsMoving = true;
  374. this.move(this._ownerCanvas.getGridPosition(this.x), this._ownerCanvas.getGridPosition(this.y))
  375. }
  376. public move(newX: number, newY: number, align = true) {
  377. let oldX = this.x;
  378. let oldY = this.y;
  379. this.x = newX;
  380. this.y = newY;
  381. for (var selectedNode of this._nodes) {
  382. selectedNode.x += this.x - oldX;
  383. selectedNode.y += this.y - oldY;
  384. if (align) {
  385. selectedNode.cleanAccumulation(true);
  386. }
  387. }
  388. }
  389. private _onUp(evt: PointerEvent) {
  390. evt.stopPropagation();
  391. this.cleanAccumulation();
  392. this._mouseStartPointX = null;
  393. this._mouseStartPointY = null;
  394. this._headerTextElement.releasePointerCapture(evt.pointerId);
  395. this._ownerCanvas._frameIsMoving = false;
  396. }
  397. private _moveFrame(offsetX: number, offsetY: number) {
  398. this.x += offsetX;
  399. this.y += offsetY;
  400. for (var selectedNode of this._nodes) {
  401. selectedNode.x += offsetX;
  402. selectedNode.y += offsetY;
  403. }
  404. }
  405. private _onMove(evt: PointerEvent) {
  406. if (this._mouseStartPointX === null || this._mouseStartPointY === null || evt.ctrlKey || this._frameIsResizing) {
  407. return;
  408. }
  409. let newX = (evt.clientX - this._mouseStartPointX) / this._ownerCanvas.zoom;
  410. let newY = (evt.clientY - this._mouseStartPointY) / this._ownerCanvas.zoom;
  411. this._moveFrame(newX, newY);
  412. this._mouseStartPointX = evt.clientX;
  413. this._mouseStartPointY = evt.clientY;
  414. evt.stopPropagation();
  415. }
  416. private initResizing = (evt: PointerEvent) => {
  417. evt.stopPropagation();
  418. this._mouseStartPointX = evt.clientX;
  419. this._mouseStartPointY = evt.clientY;
  420. this._frameIsResizing = true;
  421. }
  422. private cleanUpResizing = (evt: PointerEvent) => {
  423. evt.stopPropagation();
  424. this._frameIsResizing = false;
  425. this._resizingDirection = null;
  426. this._mouseStartPointX = null;
  427. this._mouseStartPointY = null;
  428. this.mouseXLimit = null;
  429. this.refresh();
  430. }
  431. private _onRightHandlePointerDown = (evt: PointerEvent) => {
  432. // tslint:disable-next-line: no-this-assignment
  433. const _this = this;
  434. this.initResizing(evt)
  435. _this._resizingDirection = ResizingDirection.Right;
  436. _this.mouseXLimit = evt.clientX - (_this.width - _this._minFrameWidth);
  437. _this._ownerCanvas.hostCanvas.addEventListener("pointerup", _this._onRightHandlePointerUp);
  438. _this._ownerCanvas.hostCanvas.addEventListener("pointermove", _this._onRightHandlePointerMove);
  439. }
  440. private _onRightHandlePointerMove = (evt: PointerEvent) => {
  441. const slack = (this.element.offsetWidth - this._minFrameWidth) * this._ownerCanvas.zoom;
  442. const xLimit = (this._mouseStartPointX as number) - slack;
  443. this._moveRightHandle(evt, xLimit);
  444. }
  445. private _moveRightHandle = (evt: PointerEvent, xLimit: number) => {
  446. // tslint:disable-next-line: no-this-assignment
  447. const _this = this;
  448. if (_this.mouseXLimit) {
  449. if (_this._resizingDirection !== ResizingDirection.Right || _this._mouseStartPointX === null || _this._mouseStartPointY === null || evt.clientX < xLimit) {
  450. return;
  451. }
  452. if (_this._resizingDirection === ResizingDirection.Right ) {
  453. evt.stopPropagation();
  454. const distanceMouseMoved = (evt.clientX - _this._mouseStartPointX) / _this._ownerCanvas.zoom;
  455. _this._expandRight(distanceMouseMoved, evt.clientX);
  456. _this._mouseStartPointX = evt.clientX;
  457. }
  458. }
  459. }
  460. private _expandRight(widthModification: number, x: number) {
  461. const frameElementWidth = parseFloat(this.element.style.width.replace("px", ""));
  462. if ((frameElementWidth + widthModification) > 20) {
  463. this._mouseStartPointX = x;
  464. this.element.style.width = `${frameElementWidth + widthModification}px`;
  465. }
  466. }
  467. private _onRightHandlePointerUp = (evt: PointerEvent) => {
  468. // tslint:disable-next-line: no-this-assignment
  469. const _this = this;
  470. if (_this._resizingDirection === ResizingDirection.Right) {
  471. _this.width = parseFloat(_this.element.style.width.replace("px", ""));
  472. _this._ownerCanvas.hostCanvas.removeEventListener("pointerup", _this._onRightHandlePointerUp);
  473. _this._ownerCanvas.hostCanvas.removeEventListener("pointermove", _this._onRightHandlePointerMove);
  474. _this.cleanUpResizing(evt);
  475. }
  476. }
  477. private _onBottomHandlePointerDown = (evt: PointerEvent) => {
  478. // tslint:disable-next-line: no-this-assignment
  479. const _this = this;
  480. this.initResizing(evt);
  481. _this._resizingDirection = ResizingDirection.Bottom;
  482. _this._ownerCanvas.hostCanvas.addEventListener("pointermove", _this._onBottomHandlePointerMove);
  483. _this._ownerCanvas.hostCanvas.addEventListener("pointerup", _this._onBottomHandlePointerUp);
  484. }
  485. private _onBottomHandlePointerMove = (evt: PointerEvent) => {
  486. const slack = (this.element.offsetHeight - this._minFrameHeight) * this._ownerCanvas.zoom;
  487. const yLimit = (this._mouseStartPointY as number) - slack;
  488. this._moveBottomHandle(evt, yLimit);
  489. }
  490. private _moveBottomHandle = (evt: PointerEvent, yLimit:number) => {
  491. // tslint:disable-next-line: no-this-assignment
  492. const _this = this;
  493. if (_this._resizingDirection !== ResizingDirection.Bottom || _this._mouseStartPointX === null || _this._mouseStartPointY === null || evt.clientY < yLimit) {
  494. return;
  495. }
  496. if (_this._resizingDirection === ResizingDirection.Bottom) {
  497. evt.stopPropagation();
  498. const distanceMouseMoved = (evt.clientY - _this._mouseStartPointY) / _this._ownerCanvas.zoom;
  499. _this._expandBottom(distanceMouseMoved);
  500. _this._mouseStartPointY = evt.clientY;
  501. }
  502. }
  503. private _expandBottom(heightModification: number) {
  504. const frameElementHeight = parseFloat(this.element.style.height.replace("px", ""));
  505. this.element.style.height = `${frameElementHeight + heightModification}px`;
  506. }
  507. private _onBottomHandlePointerUp = (evt: PointerEvent) => {
  508. // tslint:disable-next-line: no-this-assignment
  509. const _this = this;
  510. if (_this._resizingDirection === ResizingDirection.Bottom) {
  511. _this.height = parseFloat(_this.element.style.height.replace("px", ""));
  512. _this._ownerCanvas.hostCanvas.removeEventListener("pointermove", _this._onBottomHandlePointerMove);
  513. _this._ownerCanvas.hostCanvas.removeEventListener("pointerup", _this._onBottomHandlePointerUp);
  514. _this.cleanUpResizing(evt);
  515. }
  516. }
  517. private _onLeftHandlePointerDown = (evt: PointerEvent) => {
  518. // tslint:disable-next-line: no-this-assignment
  519. const _this = this;
  520. this.initResizing(evt);
  521. _this._resizingDirection = ResizingDirection.Left;
  522. _this.mouseXLimit = evt.clientX + _this.width - _this._minFrameWidth;
  523. _this._ownerCanvas.hostCanvas.addEventListener("pointerup", _this._onLeftHandlePointerUp);
  524. _this._ownerCanvas.hostCanvas.addEventListener("pointermove", _this._onLeftHandlePointerMove);
  525. }
  526. private _onLeftHandlePointerMove = (evt: PointerEvent) => {
  527. const slack = (this.element.offsetWidth - this._minFrameWidth) * this._ownerCanvas.zoom;
  528. const xLimit = (this._mouseStartPointX as number) + slack;
  529. this._moveLeftHandle(evt, xLimit);
  530. }
  531. private _moveLeftHandle = (evt: PointerEvent, xLimit: number) => {
  532. // tslint:disable-next-line: no-this-assignment
  533. const _this = this;
  534. if (_this.mouseXLimit) {
  535. if (_this._resizingDirection !== ResizingDirection.Left || _this._mouseStartPointX === null || _this._mouseStartPointY === null || evt.clientX > xLimit) {
  536. return;
  537. }
  538. if (_this._resizingDirection === ResizingDirection.Left) {
  539. evt.stopPropagation();
  540. const distanceMouseMoved = (evt.clientX - _this._mouseStartPointX) / _this._ownerCanvas.zoom;
  541. _this._expandLeft(distanceMouseMoved);
  542. _this._mouseStartPointX = evt.clientX;
  543. }
  544. }
  545. }
  546. private _expandLeft(widthModification: number) {
  547. const frameElementWidth = parseFloat(this.element.style.width.replace("px", ""));
  548. const frameElementLeft = parseFloat(this.element.style.left.replace("px", ""));
  549. this.element.style.width = `${frameElementWidth - widthModification}px`;
  550. this.element.style.left = `${frameElementLeft + widthModification}px`;
  551. }
  552. private _onLeftHandlePointerUp = (evt: PointerEvent) => {
  553. // tslint:disable-next-line: no-this-assignment
  554. const _this = this;
  555. if (_this._resizingDirection === ResizingDirection.Left) {
  556. _this.x = parseFloat(_this.element.style.left!.replace("px", ""));
  557. _this.width = parseFloat(_this.element.style.width.replace("px", ""));
  558. _this._ownerCanvas.hostCanvas.removeEventListener("pointerup", _this._onLeftHandlePointerUp);
  559. _this._ownerCanvas.hostCanvas.removeEventListener("pointermove", _this._onLeftHandlePointerMove);
  560. _this.cleanUpResizing(evt);
  561. }
  562. }
  563. private _onTopHandlePointerDown = (evt: PointerEvent) => {
  564. // tslint:disable-next-line: no-this-assignment
  565. const _this = this;
  566. this.initResizing(evt)
  567. _this._resizingDirection = ResizingDirection.Top;
  568. _this._ownerCanvas.hostCanvas.addEventListener("pointerup", _this._onTopHandlePointerUp);
  569. _this._ownerCanvas.hostCanvas.addEventListener("pointermove", _this._onTopHandlePointerMove);
  570. }
  571. private _onTopHandlePointerMove = (evt: PointerEvent) => {
  572. const slack = (this.element.offsetHeight - this._minFrameHeight) * this._ownerCanvas.zoom;
  573. const yLimit = (this._mouseStartPointY as number) + slack;
  574. this._moveTopHandle(evt, yLimit);
  575. }
  576. private _moveTopHandle = (evt: PointerEvent, yLimit: number) => {
  577. // tslint:disable-next-line: no-this-assignment
  578. const _this = this;
  579. if (_this._resizingDirection !== ResizingDirection.Top || _this._mouseStartPointX === null || _this._mouseStartPointY === null || evt.clientY > yLimit) {
  580. return;
  581. }
  582. if (_this._resizingDirection === ResizingDirection.Top) {
  583. evt.stopPropagation();
  584. const distanceMouseMoved = (evt.clientY - _this._mouseStartPointY) / _this._ownerCanvas.zoom;
  585. _this._expandTop(distanceMouseMoved);
  586. _this._mouseStartPointY = evt.clientY;
  587. }
  588. }
  589. private _expandTop(heightModification: number) {
  590. const frameElementHeight = parseFloat(this.element.style.height.replace("px", ""));
  591. const frameElementTop = parseFloat(this.element.style.top.replace("px", ""));
  592. this.element.style.height = `${frameElementHeight - heightModification}px`;
  593. this.element.style.top = `${frameElementTop + heightModification}px`;
  594. }
  595. private _onTopHandlePointerUp = (evt: PointerEvent) => {
  596. // tslint:disable-next-line: no-this-assignment
  597. const _this = this;
  598. if (_this._resizingDirection === ResizingDirection.Top) {
  599. _this.y = parseFloat(_this.element.style.top!.replace("px", ""));
  600. _this.height = parseFloat(_this.element.style.height.replace("px", ""));
  601. _this._ownerCanvas.hostCanvas.removeEventListener("pointerup", _this._onTopHandlePointerUp);
  602. _this._ownerCanvas.hostCanvas.removeEventListener("pointermove", _this._onTopHandlePointerMove);
  603. _this.cleanUpResizing(evt);
  604. }
  605. }
  606. public dispose() {
  607. this.isCollapsed = false;
  608. if (this._onSelectionChangedObserver) {
  609. this._ownerCanvas.globalState.onSelectionChangedObservable.remove(this._onSelectionChangedObserver);
  610. }
  611. this.element.parentElement!.removeChild(this.element);
  612. this._ownerCanvas.frames.splice(this._ownerCanvas.frames.indexOf(this), 1);
  613. this.onExpandStateChanged.clear();
  614. }
  615. public serialize(): IFrameData {
  616. return {
  617. x: this._x,
  618. y: this._y,
  619. width: this._width,
  620. height: this._height,
  621. color: this._color.asArray(),
  622. name: this.name,
  623. isCollapsed: this.isCollapsed,
  624. blocks: this.nodes.map(n => n.block.uniqueId),
  625. comments: this._comments
  626. }
  627. }
  628. public export() {
  629. const state = this._ownerCanvas.globalState;
  630. const json = SerializationTools.Serialize(state.nodeMaterial, state, this.nodes.map(n => n.block));
  631. StringTools.DownloadAsFile(state.hostDocument, json, this._name + ".json");
  632. }
  633. public static Parse(serializationData: IFrameData, canvas: GraphCanvasComponent, map?: {[key: number]: number}) {
  634. let newFrame = new GraphFrame(null, canvas, true);
  635. const isCollapsed = !!serializationData.isCollapsed;
  636. newFrame.x = serializationData.x;
  637. newFrame.y = serializationData.y;
  638. newFrame.width = serializationData.width;
  639. newFrame.height = serializationData.height;
  640. newFrame.name = serializationData.name;
  641. newFrame.color = Color3.FromArray(serializationData.color);
  642. newFrame.comments = serializationData.comments;
  643. if (serializationData.blocks && map) {
  644. for (var blockId of serializationData.blocks) {
  645. let actualId = map[blockId];
  646. let node = canvas.nodes.filter(n => n.block.uniqueId === actualId);
  647. if (node.length) {
  648. newFrame.nodes.push(node[0]);
  649. }
  650. }
  651. } else {
  652. newFrame.refresh();
  653. }
  654. newFrame.isCollapsed = isCollapsed;
  655. if (isCollapsed) {
  656. canvas._frameIsMoving = true;
  657. newFrame._moveFrame(-(newFrame.width - newFrame.CollapsedWidth) / 2, 0);
  658. let diff = serializationData.x - newFrame.x;
  659. newFrame._moveFrame(diff, 0);
  660. newFrame.cleanAccumulation();
  661. for (var selectedNode of newFrame.nodes) {
  662. selectedNode.refresh();
  663. }
  664. canvas._frameIsMoving = false;
  665. }
  666. return newFrame;
  667. }
  668. }