transform.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. import { Dom, NumberExt } from '@antv/x6-common';
  2. import { Point, Rectangle } from '@antv/x6-geometry';
  3. import { Base } from './base';
  4. import { Util } from '../util';
  5. export class TransformManager extends Base {
  6. get container() {
  7. return this.graph.view.container;
  8. }
  9. get viewport() {
  10. return this.graph.view.viewport;
  11. }
  12. get stage() {
  13. return this.graph.view.stage;
  14. }
  15. init() {
  16. this.resize();
  17. }
  18. /**
  19. * Returns the current transformation matrix of the graph.
  20. */
  21. getMatrix() {
  22. const transform = this.viewport.getAttribute('transform');
  23. if (transform !== this.viewportTransformString) {
  24. // `getCTM`: top-left relative to the SVG element
  25. // `getScreenCTM`: top-left relative to the document
  26. this.viewportMatrix = this.viewport.getCTM();
  27. this.viewportTransformString = transform;
  28. }
  29. // Clone the cached current transformation matrix.
  30. // If no matrix previously stored the identity matrix is returned.
  31. return Dom.createSVGMatrix(this.viewportMatrix);
  32. }
  33. /**
  34. * Sets new transformation with the given `matrix`
  35. */
  36. setMatrix(matrix) {
  37. const ctm = Dom.createSVGMatrix(matrix);
  38. const transform = Dom.matrixToTransformString(ctm);
  39. this.viewport.setAttribute('transform', transform);
  40. this.viewportMatrix = ctm;
  41. this.viewportTransformString = transform;
  42. }
  43. resize(width, height) {
  44. let w = width === undefined ? this.options.width : width;
  45. let h = height === undefined ? this.options.height : height;
  46. this.options.width = w;
  47. this.options.height = h;
  48. if (typeof w === 'number') {
  49. w = Math.round(w);
  50. }
  51. if (typeof h === 'number') {
  52. h = Math.round(h);
  53. }
  54. this.container.style.width = w == null ? '' : `${w}px`;
  55. this.container.style.height = h == null ? '' : `${h}px`;
  56. const size = this.getComputedSize();
  57. this.graph.trigger('resize', Object.assign({}, size));
  58. return this;
  59. }
  60. getComputedSize() {
  61. let w = this.options.width;
  62. let h = this.options.height;
  63. if (!NumberExt.isNumber(w)) {
  64. w = this.container.clientWidth;
  65. }
  66. if (!NumberExt.isNumber(h)) {
  67. h = this.container.clientHeight;
  68. }
  69. return { width: w, height: h };
  70. }
  71. getScale() {
  72. return Dom.matrixToScale(this.getMatrix());
  73. }
  74. scale(sx, sy = sx, ox = 0, oy = 0) {
  75. sx = this.clampScale(sx); // eslint-disable-line
  76. sy = this.clampScale(sy); // eslint-disable-line
  77. if (ox || oy) {
  78. const ts = this.getTranslation();
  79. const tx = ts.tx - ox * (sx - 1);
  80. const ty = ts.ty - oy * (sy - 1);
  81. if (tx !== ts.tx || ty !== ts.ty) {
  82. this.translate(tx, ty);
  83. }
  84. }
  85. const matrix = this.getMatrix();
  86. matrix.a = sx;
  87. matrix.d = sy;
  88. this.setMatrix(matrix);
  89. this.graph.trigger('scale', { sx, sy, ox, oy });
  90. return this;
  91. }
  92. clampScale(scale) {
  93. const range = this.graph.options.scaling;
  94. return NumberExt.clamp(scale, range.min || 0.01, range.max || 16);
  95. }
  96. getZoom() {
  97. return this.getScale().sx;
  98. }
  99. zoom(factor, options) {
  100. options = options || {}; // eslint-disable-line
  101. let sx = factor;
  102. let sy = factor;
  103. const scale = this.getScale();
  104. const clientSize = this.getComputedSize();
  105. let cx = clientSize.width / 2;
  106. let cy = clientSize.height / 2;
  107. if (!options.absolute) {
  108. sx += scale.sx;
  109. sy += scale.sy;
  110. }
  111. if (options.scaleGrid) {
  112. sx = Math.round(sx / options.scaleGrid) * options.scaleGrid;
  113. sy = Math.round(sy / options.scaleGrid) * options.scaleGrid;
  114. }
  115. if (options.maxScale) {
  116. sx = Math.min(options.maxScale, sx);
  117. sy = Math.min(options.maxScale, sy);
  118. }
  119. if (options.minScale) {
  120. sx = Math.max(options.minScale, sx);
  121. sy = Math.max(options.minScale, sy);
  122. }
  123. if (options.center) {
  124. cx = options.center.x;
  125. cy = options.center.y;
  126. }
  127. sx = this.clampScale(sx);
  128. sy = this.clampScale(sy);
  129. if (cx || cy) {
  130. const ts = this.getTranslation();
  131. const tx = cx - (cx - ts.tx) * (sx / scale.sx);
  132. const ty = cy - (cy - ts.ty) * (sy / scale.sy);
  133. if (tx !== ts.tx || ty !== ts.ty) {
  134. this.translate(tx, ty);
  135. }
  136. }
  137. this.scale(sx, sy);
  138. return this;
  139. }
  140. getRotation() {
  141. return Dom.matrixToRotation(this.getMatrix());
  142. }
  143. rotate(angle, cx, cy) {
  144. if (cx == null || cy == null) {
  145. const bbox = Util.getBBox(this.stage);
  146. cx = bbox.width / 2; // eslint-disable-line
  147. cy = bbox.height / 2; // eslint-disable-line
  148. }
  149. const ctm = this.getMatrix()
  150. .translate(cx, cy)
  151. .rotate(angle)
  152. .translate(-cx, -cy);
  153. this.setMatrix(ctm);
  154. return this;
  155. }
  156. getTranslation() {
  157. return Dom.matrixToTranslation(this.getMatrix());
  158. }
  159. translate(tx, ty) {
  160. const matrix = this.getMatrix();
  161. matrix.e = tx || 0;
  162. matrix.f = ty || 0;
  163. this.setMatrix(matrix);
  164. const ts = this.getTranslation();
  165. this.options.x = ts.tx;
  166. this.options.y = ts.ty;
  167. this.graph.trigger('translate', Object.assign({}, ts));
  168. return this;
  169. }
  170. setOrigin(ox, oy) {
  171. return this.translate(ox || 0, oy || 0);
  172. }
  173. fitToContent(gridWidth, gridHeight, padding, options) {
  174. if (typeof gridWidth === 'object') {
  175. const opts = gridWidth;
  176. gridWidth = opts.gridWidth || 1; // eslint-disable-line
  177. gridHeight = opts.gridHeight || 1; // eslint-disable-line
  178. padding = opts.padding || 0; // eslint-disable-line
  179. options = opts; // eslint-disable-line
  180. }
  181. else {
  182. gridWidth = gridWidth || 1; // eslint-disable-line
  183. gridHeight = gridHeight || 1; // eslint-disable-line
  184. padding = padding || 0; // eslint-disable-line
  185. if (options == null) {
  186. options = {}; // eslint-disable-line
  187. }
  188. }
  189. const paddings = NumberExt.normalizeSides(padding);
  190. const border = options.border || 0;
  191. const contentArea = options.contentArea
  192. ? Rectangle.create(options.contentArea)
  193. : this.getContentArea(options);
  194. if (border > 0) {
  195. contentArea.inflate(border);
  196. }
  197. const scale = this.getScale();
  198. const translate = this.getTranslation();
  199. const sx = scale.sx;
  200. const sy = scale.sy;
  201. contentArea.x *= sx;
  202. contentArea.y *= sy;
  203. contentArea.width *= sx;
  204. contentArea.height *= sy;
  205. let width = Math.max(Math.ceil((contentArea.width + contentArea.x) / gridWidth), 1) *
  206. gridWidth;
  207. let height = Math.max(Math.ceil((contentArea.height + contentArea.y) / gridHeight), 1) * gridHeight;
  208. let tx = 0;
  209. let ty = 0;
  210. if ((options.allowNewOrigin === 'negative' && contentArea.x < 0) ||
  211. (options.allowNewOrigin === 'positive' && contentArea.x >= 0) ||
  212. options.allowNewOrigin === 'any') {
  213. tx = Math.ceil(-contentArea.x / gridWidth) * gridWidth;
  214. tx += paddings.left;
  215. width += tx;
  216. }
  217. if ((options.allowNewOrigin === 'negative' && contentArea.y < 0) ||
  218. (options.allowNewOrigin === 'positive' && contentArea.y >= 0) ||
  219. options.allowNewOrigin === 'any') {
  220. ty = Math.ceil(-contentArea.y / gridHeight) * gridHeight;
  221. ty += paddings.top;
  222. height += ty;
  223. }
  224. width += paddings.right;
  225. height += paddings.bottom;
  226. // Make sure the resulting width and height are greater than minimum.
  227. width = Math.max(width, options.minWidth || 0);
  228. height = Math.max(height, options.minHeight || 0);
  229. // Make sure the resulting width and height are lesser than maximum.
  230. width = Math.min(width, options.maxWidth || Number.MAX_SAFE_INTEGER);
  231. height = Math.min(height, options.maxHeight || Number.MAX_SAFE_INTEGER);
  232. const size = this.getComputedSize();
  233. const sizeChanged = width !== size.width || height !== size.height;
  234. const originChanged = tx !== translate.tx || ty !== translate.ty;
  235. // Change the dimensions only if there is a size discrepency or an origin change
  236. if (originChanged) {
  237. this.translate(tx, ty);
  238. }
  239. if (sizeChanged) {
  240. this.resize(width, height);
  241. }
  242. return new Rectangle(-tx / sx, -ty / sy, width / sx, height / sy);
  243. }
  244. scaleContentToFit(options = {}) {
  245. this.scaleContentToFitImpl(options);
  246. }
  247. scaleContentToFitImpl(options = {}, translate = true) {
  248. let contentBBox;
  249. let contentLocalOrigin;
  250. if (options.contentArea) {
  251. const contentArea = options.contentArea;
  252. contentBBox = this.graph.localToGraph(contentArea);
  253. contentLocalOrigin = Point.create(contentArea);
  254. }
  255. else {
  256. contentBBox = this.getContentBBox(options);
  257. contentLocalOrigin = this.graph.graphToLocal(contentBBox);
  258. }
  259. if (!contentBBox.width || !contentBBox.height) {
  260. return;
  261. }
  262. const padding = NumberExt.normalizeSides(options.padding);
  263. const minScale = options.minScale || 0;
  264. const maxScale = options.maxScale || Number.MAX_SAFE_INTEGER;
  265. const minScaleX = options.minScaleX || minScale;
  266. const maxScaleX = options.maxScaleX || maxScale;
  267. const minScaleY = options.minScaleY || minScale;
  268. const maxScaleY = options.maxScaleY || maxScale;
  269. let fittingBox;
  270. if (options.viewportArea) {
  271. fittingBox = options.viewportArea;
  272. }
  273. else {
  274. const computedSize = this.getComputedSize();
  275. const currentTranslate = this.getTranslation();
  276. fittingBox = {
  277. x: currentTranslate.tx,
  278. y: currentTranslate.ty,
  279. width: computedSize.width,
  280. height: computedSize.height,
  281. };
  282. }
  283. fittingBox = Rectangle.create(fittingBox).moveAndExpand({
  284. x: padding.left,
  285. y: padding.top,
  286. width: -padding.left - padding.right,
  287. height: -padding.top - padding.bottom,
  288. });
  289. const currentScale = this.getScale();
  290. let newSX = (fittingBox.width / contentBBox.width) * currentScale.sx;
  291. let newSY = (fittingBox.height / contentBBox.height) * currentScale.sy;
  292. if (options.preserveAspectRatio !== false) {
  293. newSX = newSY = Math.min(newSX, newSY);
  294. }
  295. // snap scale to a grid
  296. const gridSize = options.scaleGrid;
  297. if (gridSize) {
  298. newSX = gridSize * Math.floor(newSX / gridSize);
  299. newSY = gridSize * Math.floor(newSY / gridSize);
  300. }
  301. // scale min/max boundaries
  302. newSX = NumberExt.clamp(newSX, minScaleX, maxScaleX);
  303. newSY = NumberExt.clamp(newSY, minScaleY, maxScaleY);
  304. this.scale(newSX, newSY);
  305. if (translate) {
  306. const origin = this.options;
  307. const newOX = fittingBox.x - contentLocalOrigin.x * newSX - origin.x;
  308. const newOY = fittingBox.y - contentLocalOrigin.y * newSY - origin.y;
  309. this.translate(newOX, newOY);
  310. }
  311. }
  312. getContentArea(options = {}) {
  313. // use geometry calc default
  314. if (options.useCellGeometry !== false) {
  315. return this.model.getAllCellsBBox() || new Rectangle();
  316. }
  317. return Util.getBBox(this.stage);
  318. }
  319. getContentBBox(options = {}) {
  320. return this.graph.localToGraph(this.getContentArea(options));
  321. }
  322. getGraphArea() {
  323. const rect = Rectangle.fromSize(this.getComputedSize());
  324. return this.graph.graphToLocal(rect);
  325. }
  326. zoomToRect(rect, options = {}) {
  327. const area = Rectangle.create(rect);
  328. const graph = this.graph;
  329. options.contentArea = area;
  330. if (options.viewportArea == null) {
  331. options.viewportArea = {
  332. x: graph.options.x,
  333. y: graph.options.y,
  334. width: this.options.width,
  335. height: this.options.height,
  336. };
  337. }
  338. this.scaleContentToFitImpl(options, false);
  339. const center = area.getCenter();
  340. this.centerPoint(center.x, center.y);
  341. return this;
  342. }
  343. zoomToFit(options = {}) {
  344. return this.zoomToRect(this.getContentArea(options), options);
  345. }
  346. centerPoint(x, y) {
  347. const clientSize = this.getComputedSize();
  348. const scale = this.getScale();
  349. const ts = this.getTranslation();
  350. const cx = clientSize.width / 2;
  351. const cy = clientSize.height / 2;
  352. x = typeof x === 'number' ? x : cx; // eslint-disable-line
  353. y = typeof y === 'number' ? y : cy; // eslint-disable-line
  354. x = cx - x * scale.sx; // eslint-disable-line
  355. y = cy - y * scale.sy; // eslint-disable-line
  356. if (ts.tx !== x || ts.ty !== y) {
  357. this.translate(x, y);
  358. }
  359. }
  360. centerContent(options) {
  361. const rect = this.graph.getContentArea(options);
  362. const center = rect.getCenter();
  363. this.centerPoint(center.x, center.y);
  364. }
  365. centerCell(cell) {
  366. return this.positionCell(cell, 'center');
  367. }
  368. positionPoint(point, x, y) {
  369. const clientSize = this.getComputedSize();
  370. // eslint-disable-next-line
  371. x = NumberExt.normalizePercentage(x, Math.max(0, clientSize.width));
  372. if (x < 0) {
  373. x = clientSize.width + x; // eslint-disable-line
  374. }
  375. // eslint-disable-next-line
  376. y = NumberExt.normalizePercentage(y, Math.max(0, clientSize.height));
  377. if (y < 0) {
  378. y = clientSize.height + y; // eslint-disable-line
  379. }
  380. const ts = this.getTranslation();
  381. const scale = this.getScale();
  382. const dx = x - point.x * scale.sx;
  383. const dy = y - point.y * scale.sy;
  384. if (ts.tx !== dx || ts.ty !== dy) {
  385. this.translate(dx, dy);
  386. }
  387. }
  388. positionRect(rect, pos) {
  389. const bbox = Rectangle.create(rect);
  390. switch (pos) {
  391. case 'center':
  392. return this.positionPoint(bbox.getCenter(), '50%', '50%');
  393. case 'top':
  394. return this.positionPoint(bbox.getTopCenter(), '50%', 0);
  395. case 'top-right':
  396. return this.positionPoint(bbox.getTopRight(), '100%', 0);
  397. case 'right':
  398. return this.positionPoint(bbox.getRightMiddle(), '100%', '50%');
  399. case 'bottom-right':
  400. return this.positionPoint(bbox.getBottomRight(), '100%', '100%');
  401. case 'bottom':
  402. return this.positionPoint(bbox.getBottomCenter(), '50%', '100%');
  403. case 'bottom-left':
  404. return this.positionPoint(bbox.getBottomLeft(), 0, '100%');
  405. case 'left':
  406. return this.positionPoint(bbox.getLeftMiddle(), 0, '50%');
  407. case 'top-left':
  408. return this.positionPoint(bbox.getTopLeft(), 0, 0);
  409. default:
  410. return this;
  411. }
  412. }
  413. positionCell(cell, pos) {
  414. const bbox = cell.getBBox();
  415. return this.positionRect(bbox, pos);
  416. }
  417. positionContent(pos, options) {
  418. const rect = this.graph.getContentArea(options);
  419. return this.positionRect(rect, pos);
  420. }
  421. }
  422. //# sourceMappingURL=transform.js.map