Canvas.js 25 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057
  1. import {
  2. isNumber,
  3. assign,
  4. forEach,
  5. every,
  6. debounce,
  7. bind,
  8. reduce
  9. } from 'min-dash';
  10. import {
  11. add as collectionAdd,
  12. remove as collectionRemove
  13. } from '../util/Collections';
  14. import {
  15. getType
  16. } from '../util/Elements';
  17. import {
  18. append as svgAppend,
  19. attr as svgAttr,
  20. classes as svgClasses,
  21. create as svgCreate,
  22. transform as svgTransform
  23. } from 'tiny-svg';
  24. import { createMatrix as createMatrix } from 'tiny-svg';
  25. function round(number, resolution) {
  26. return Math.round(number * resolution) / resolution;
  27. }
  28. function ensurePx(number) {
  29. return isNumber(number) ? number + 'px' : number;
  30. }
  31. /**
  32. * Creates a HTML container element for a SVG element with
  33. * the given configuration
  34. *
  35. * @param {Object} options
  36. * @return {HTMLElement} the container element
  37. */
  38. function createContainer(options) {
  39. options = assign({}, { width: '100%', height: '100%' }, options);
  40. var container = options.container || document.body;
  41. // create a <div> around the svg element with the respective size
  42. // this way we can always get the correct container size
  43. // (this is impossible for <svg> elements at the moment)
  44. var parent = document.createElement('div');
  45. parent.setAttribute('class', 'djs-container');
  46. assign(parent.style, {
  47. position: 'relative',
  48. overflow: 'hidden',
  49. width: ensurePx(options.width),
  50. height: ensurePx(options.height)
  51. });
  52. container.appendChild(parent);
  53. return parent;
  54. }
  55. function createGroup(parent, cls, childIndex) {
  56. var group = svgCreate('g');
  57. svgClasses(group).add(cls);
  58. var index = childIndex !== undefined ? childIndex : parent.childNodes.length - 1;
  59. // must ensure second argument is node or _null_
  60. // cf. https://developer.mozilla.org/en-US/docs/Web/API/Node/insertBefore
  61. parent.insertBefore(group, parent.childNodes[index] || null);
  62. return group;
  63. }
  64. var BASE_LAYER = 'base';
  65. var REQUIRED_MODEL_ATTRS = {
  66. shape: [ 'x', 'y', 'width', 'height' ],
  67. connection: [ 'waypoints' ]
  68. };
  69. /**
  70. * The main drawing canvas.
  71. *
  72. * @class
  73. * @constructor
  74. *
  75. * @emits Canvas#canvas.init
  76. *
  77. * @param {Object} config
  78. * @param {EventBus} eventBus
  79. * @param {GraphicsFactory} graphicsFactory
  80. * @param {ElementRegistry} elementRegistry
  81. */
  82. export default function Canvas(config, eventBus, graphicsFactory, elementRegistry) {
  83. this._eventBus = eventBus;
  84. this._elementRegistry = elementRegistry;
  85. this._graphicsFactory = graphicsFactory;
  86. this._init(config || {});
  87. }
  88. Canvas.$inject = [
  89. 'config.canvas',
  90. 'eventBus',
  91. 'graphicsFactory',
  92. 'elementRegistry'
  93. ];
  94. Canvas.prototype._init = function(config) {
  95. var eventBus = this._eventBus;
  96. // Creates a <svg> element that is wrapped into a <div>.
  97. // This way we are always able to correctly figure out the size of the svg element
  98. // by querying the parent node.
  99. //
  100. // (It is not possible to get the size of a svg element cross browser @ 2014-04-01)
  101. //
  102. // <div class="djs-container" style="width: {desired-width}, height: {desired-height}">
  103. // <svg width="100%" height="100%">
  104. // ...
  105. // </svg>
  106. // </div>
  107. // html container
  108. var container = this._container = createContainer(config);
  109. var svg = this._svg = svgCreate('svg');
  110. svgAttr(svg, { width: '100%', height: '100%' });
  111. svgAppend(container, svg);
  112. var viewport = this._viewport = createGroup(svg, 'viewport');
  113. this._layers = {};
  114. // debounce canvas.viewbox.changed events
  115. // for smoother diagram interaction
  116. if (config.deferUpdate !== false) {
  117. this._viewboxChanged = debounce(bind(this._viewboxChanged, this), 300);
  118. }
  119. eventBus.on('diagram.init', function() {
  120. /**
  121. * An event indicating that the canvas is ready to be drawn on.
  122. *
  123. * @memberOf Canvas
  124. *
  125. * @event canvas.init
  126. *
  127. * @type {Object}
  128. * @property {SVGElement} svg the created svg element
  129. * @property {SVGElement} viewport the direct parent of diagram elements and shapes
  130. */
  131. eventBus.fire('canvas.init', {
  132. svg: svg,
  133. viewport: viewport
  134. });
  135. }, this);
  136. // reset viewbox on shape changes to
  137. // recompute the viewbox
  138. eventBus.on([
  139. 'shape.added',
  140. 'connection.added',
  141. 'shape.removed',
  142. 'connection.removed',
  143. 'elements.changed'
  144. ], function() {
  145. delete this._cachedViewbox;
  146. }, this);
  147. eventBus.on('diagram.destroy', 500, this._destroy, this);
  148. eventBus.on('diagram.clear', 500, this._clear, this);
  149. };
  150. Canvas.prototype._destroy = function(emit) {
  151. this._eventBus.fire('canvas.destroy', {
  152. svg: this._svg,
  153. viewport: this._viewport
  154. });
  155. var parent = this._container.parentNode;
  156. if (parent) {
  157. parent.removeChild(this._container);
  158. }
  159. delete this._svg;
  160. delete this._container;
  161. delete this._layers;
  162. delete this._rootElement;
  163. delete this._viewport;
  164. };
  165. Canvas.prototype._clear = function() {
  166. var self = this;
  167. var allElements = this._elementRegistry.getAll();
  168. // remove all elements
  169. allElements.forEach(function(element) {
  170. var type = getType(element);
  171. if (type === 'root') {
  172. self.setRootElement(null, true);
  173. } else {
  174. self._removeElement(element, type);
  175. }
  176. });
  177. // force recomputation of view box
  178. delete this._cachedViewbox;
  179. };
  180. /**
  181. * Returns the default layer on which
  182. * all elements are drawn.
  183. *
  184. * @returns {SVGElement}
  185. */
  186. Canvas.prototype.getDefaultLayer = function() {
  187. return this.getLayer(BASE_LAYER, 0);
  188. };
  189. /**
  190. * Returns a layer that is used to draw elements
  191. * or annotations on it.
  192. *
  193. * Non-existing layers retrieved through this method
  194. * will be created. During creation, the optional index
  195. * may be used to create layers below or above existing layers.
  196. * A layer with a certain index is always created above all
  197. * existing layers with the same index.
  198. *
  199. * @param {String} name
  200. * @param {Number} index
  201. *
  202. * @returns {SVGElement}
  203. */
  204. Canvas.prototype.getLayer = function(name, index) {
  205. if (!name) {
  206. throw new Error('must specify a name');
  207. }
  208. var layer = this._layers[name];
  209. if (!layer) {
  210. layer = this._layers[name] = this._createLayer(name, index);
  211. }
  212. // throw an error if layer creation / retrival is
  213. // requested on different index
  214. if (typeof index !== 'undefined' && layer.index !== index) {
  215. throw new Error('layer <' + name + '> already created at index <' + index + '>');
  216. }
  217. return layer.group;
  218. };
  219. /**
  220. * Creates a given layer and returns it.
  221. *
  222. * @param {String} name
  223. * @param {Number} [index=0]
  224. *
  225. * @return {Object} layer descriptor with { index, group: SVGGroup }
  226. */
  227. Canvas.prototype._createLayer = function(name, index) {
  228. if (!index) {
  229. index = 0;
  230. }
  231. var childIndex = reduce(this._layers, function(childIndex, layer) {
  232. if (index >= layer.index) {
  233. childIndex++;
  234. }
  235. return childIndex;
  236. }, 0);
  237. return {
  238. group: createGroup(this._viewport, 'layer-' + name, childIndex),
  239. index: index
  240. };
  241. };
  242. /**
  243. * Returns the html element that encloses the
  244. * drawing canvas.
  245. *
  246. * @return {DOMNode}
  247. */
  248. Canvas.prototype.getContainer = function() {
  249. return this._container;
  250. };
  251. // markers //////////////////////
  252. Canvas.prototype._updateMarker = function(element, marker, add) {
  253. var container;
  254. if (!element.id) {
  255. element = this._elementRegistry.get(element);
  256. }
  257. // we need to access all
  258. container = this._elementRegistry._elements[element.id];
  259. if (!container) {
  260. return;
  261. }
  262. forEach([ container.gfx, container.secondaryGfx ], function(gfx) {
  263. if (gfx) {
  264. // invoke either addClass or removeClass based on mode
  265. if (add) {
  266. svgClasses(gfx).add(marker);
  267. } else {
  268. svgClasses(gfx).remove(marker);
  269. }
  270. }
  271. });
  272. /**
  273. * An event indicating that a marker has been updated for an element
  274. *
  275. * @event element.marker.update
  276. * @type {Object}
  277. * @property {djs.model.Element} element the shape
  278. * @property {Object} gfx the graphical representation of the shape
  279. * @property {String} marker
  280. * @property {Boolean} add true if the marker was added, false if it got removed
  281. */
  282. this._eventBus.fire('element.marker.update', { element: element, gfx: container.gfx, marker: marker, add: !!add });
  283. };
  284. /**
  285. * Adds a marker to an element (basically a css class).
  286. *
  287. * Fires the element.marker.update event, making it possible to
  288. * integrate extension into the marker life-cycle, too.
  289. *
  290. * @example
  291. * canvas.addMarker('foo', 'some-marker');
  292. *
  293. * var fooGfx = canvas.getGraphics('foo');
  294. *
  295. * fooGfx; // <g class="... some-marker"> ... </g>
  296. *
  297. * @param {String|djs.model.Base} element
  298. * @param {String} marker
  299. */
  300. Canvas.prototype.addMarker = function(element, marker) {
  301. this._updateMarker(element, marker, true);
  302. };
  303. /**
  304. * Remove a marker from an element.
  305. *
  306. * Fires the element.marker.update event, making it possible to
  307. * integrate extension into the marker life-cycle, too.
  308. *
  309. * @param {String|djs.model.Base} element
  310. * @param {String} marker
  311. */
  312. Canvas.prototype.removeMarker = function(element, marker) {
  313. this._updateMarker(element, marker, false);
  314. };
  315. /**
  316. * Check the existence of a marker on element.
  317. *
  318. * @param {String|djs.model.Base} element
  319. * @param {String} marker
  320. */
  321. Canvas.prototype.hasMarker = function(element, marker) {
  322. if (!element.id) {
  323. element = this._elementRegistry.get(element);
  324. }
  325. var gfx = this.getGraphics(element);
  326. return svgClasses(gfx).has(marker);
  327. };
  328. /**
  329. * Toggles a marker on an element.
  330. *
  331. * Fires the element.marker.update event, making it possible to
  332. * integrate extension into the marker life-cycle, too.
  333. *
  334. * @param {String|djs.model.Base} element
  335. * @param {String} marker
  336. */
  337. Canvas.prototype.toggleMarker = function(element, marker) {
  338. if (this.hasMarker(element, marker)) {
  339. this.removeMarker(element, marker);
  340. } else {
  341. this.addMarker(element, marker);
  342. }
  343. };
  344. Canvas.prototype.getRootElement = function() {
  345. if (!this._rootElement) {
  346. this.setRootElement({ id: '__implicitroot', children: [] });
  347. }
  348. return this._rootElement;
  349. };
  350. // root element handling //////////////////////
  351. /**
  352. * Sets a given element as the new root element for the canvas
  353. * and returns the new root element.
  354. *
  355. * @param {Object|djs.model.Root} element
  356. * @param {Boolean} [override] whether to override the current root element, if any
  357. *
  358. * @return {Object|djs.model.Root} new root element
  359. */
  360. Canvas.prototype.setRootElement = function(element, override) {
  361. if (element) {
  362. this._ensureValid('root', element);
  363. }
  364. var currentRoot = this._rootElement,
  365. elementRegistry = this._elementRegistry,
  366. eventBus = this._eventBus;
  367. if (currentRoot) {
  368. if (!override) {
  369. throw new Error('rootElement already set, need to specify override');
  370. }
  371. // simulate element remove event sequence
  372. eventBus.fire('root.remove', { element: currentRoot });
  373. eventBus.fire('root.removed', { element: currentRoot });
  374. elementRegistry.remove(currentRoot);
  375. }
  376. if (element) {
  377. var gfx = this.getDefaultLayer();
  378. // resemble element add event sequence
  379. eventBus.fire('root.add', { element: element });
  380. elementRegistry.add(element, gfx, this._svg);
  381. eventBus.fire('root.added', { element: element, gfx: gfx });
  382. }
  383. this._rootElement = element;
  384. return element;
  385. };
  386. // add functionality //////////////////////
  387. Canvas.prototype._ensureValid = function(type, element) {
  388. if (!element.id) {
  389. throw new Error('element must have an id');
  390. }
  391. if (this._elementRegistry.get(element.id)) {
  392. throw new Error('element with id ' + element.id + ' already exists');
  393. }
  394. var requiredAttrs = REQUIRED_MODEL_ATTRS[type];
  395. var valid = every(requiredAttrs, function(attr) {
  396. return typeof element[attr] !== 'undefined';
  397. });
  398. if (!valid) {
  399. throw new Error(
  400. 'must supply { ' + requiredAttrs.join(', ') + ' } with ' + type);
  401. }
  402. };
  403. Canvas.prototype._setParent = function(element, parent, parentIndex) {
  404. collectionAdd(parent.children, element, parentIndex);
  405. element.parent = parent;
  406. };
  407. /**
  408. * Adds an element to the canvas.
  409. *
  410. * This wires the parent <-> child relationship between the element and
  411. * a explicitly specified parent or an implicit root element.
  412. *
  413. * During add it emits the events
  414. *
  415. * * <{type}.add> (element, parent)
  416. * * <{type}.added> (element, gfx)
  417. *
  418. * Extensions may hook into these events to perform their magic.
  419. *
  420. * @param {String} type
  421. * @param {Object|djs.model.Base} element
  422. * @param {Object|djs.model.Base} [parent]
  423. * @param {Number} [parentIndex]
  424. *
  425. * @return {Object|djs.model.Base} the added element
  426. */
  427. Canvas.prototype._addElement = function(type, element, parent, parentIndex) {
  428. parent = parent || this.getRootElement();
  429. var eventBus = this._eventBus,
  430. graphicsFactory = this._graphicsFactory;
  431. this._ensureValid(type, element);
  432. eventBus.fire(type + '.add', { element: element, parent: parent });
  433. this._setParent(element, parent, parentIndex);
  434. // create graphics
  435. var gfx = graphicsFactory.create(type, element, parentIndex);
  436. this._elementRegistry.add(element, gfx);
  437. // update its visual
  438. graphicsFactory.update(type, element, gfx);
  439. eventBus.fire(type + '.added', { element: element, gfx: gfx });
  440. return element;
  441. };
  442. /**
  443. * Adds a shape to the canvas
  444. *
  445. * @param {Object|djs.model.Shape} shape to add to the diagram
  446. * @param {djs.model.Base} [parent]
  447. * @param {Number} [parentIndex]
  448. *
  449. * @return {djs.model.Shape} the added shape
  450. */
  451. Canvas.prototype.addShape = function(shape, parent, parentIndex) {
  452. return this._addElement('shape', shape, parent, parentIndex);
  453. };
  454. /**
  455. * Adds a connection to the canvas
  456. *
  457. * @param {Object|djs.model.Connection} connection to add to the diagram
  458. * @param {djs.model.Base} [parent]
  459. * @param {Number} [parentIndex]
  460. *
  461. * @return {djs.model.Connection} the added connection
  462. */
  463. Canvas.prototype.addConnection = function(connection, parent, parentIndex) {
  464. return this._addElement('connection', connection, parent, parentIndex);
  465. };
  466. /**
  467. * Internal remove element
  468. */
  469. Canvas.prototype._removeElement = function(element, type) {
  470. var elementRegistry = this._elementRegistry,
  471. graphicsFactory = this._graphicsFactory,
  472. eventBus = this._eventBus;
  473. element = elementRegistry.get(element.id || element);
  474. if (!element) {
  475. // element was removed already
  476. return;
  477. }
  478. eventBus.fire(type + '.remove', { element: element });
  479. graphicsFactory.remove(element);
  480. // unset parent <-> child relationship
  481. collectionRemove(element.parent && element.parent.children, element);
  482. element.parent = null;
  483. eventBus.fire(type + '.removed', { element: element });
  484. elementRegistry.remove(element);
  485. return element;
  486. };
  487. /**
  488. * Removes a shape from the canvas
  489. *
  490. * @param {String|djs.model.Shape} shape or shape id to be removed
  491. *
  492. * @return {djs.model.Shape} the removed shape
  493. */
  494. Canvas.prototype.removeShape = function(shape) {
  495. /**
  496. * An event indicating that a shape is about to be removed from the canvas.
  497. *
  498. * @memberOf Canvas
  499. *
  500. * @event shape.remove
  501. * @type {Object}
  502. * @property {djs.model.Shape} element the shape descriptor
  503. * @property {Object} gfx the graphical representation of the shape
  504. */
  505. /**
  506. * An event indicating that a shape has been removed from the canvas.
  507. *
  508. * @memberOf Canvas
  509. *
  510. * @event shape.removed
  511. * @type {Object}
  512. * @property {djs.model.Shape} element the shape descriptor
  513. * @property {Object} gfx the graphical representation of the shape
  514. */
  515. return this._removeElement(shape, 'shape');
  516. };
  517. /**
  518. * Removes a connection from the canvas
  519. *
  520. * @param {String|djs.model.Connection} connection or connection id to be removed
  521. *
  522. * @return {djs.model.Connection} the removed connection
  523. */
  524. Canvas.prototype.removeConnection = function(connection) {
  525. /**
  526. * An event indicating that a connection is about to be removed from the canvas.
  527. *
  528. * @memberOf Canvas
  529. *
  530. * @event connection.remove
  531. * @type {Object}
  532. * @property {djs.model.Connection} element the connection descriptor
  533. * @property {Object} gfx the graphical representation of the connection
  534. */
  535. /**
  536. * An event indicating that a connection has been removed from the canvas.
  537. *
  538. * @memberOf Canvas
  539. *
  540. * @event connection.removed
  541. * @type {Object}
  542. * @property {djs.model.Connection} element the connection descriptor
  543. * @property {Object} gfx the graphical representation of the connection
  544. */
  545. return this._removeElement(connection, 'connection');
  546. };
  547. /**
  548. * Return the graphical object underlaying a certain diagram element
  549. *
  550. * @param {String|djs.model.Base} element descriptor of the element
  551. * @param {Boolean} [secondary=false] whether to return the secondary connected element
  552. *
  553. * @return {SVGElement}
  554. */
  555. Canvas.prototype.getGraphics = function(element, secondary) {
  556. return this._elementRegistry.getGraphics(element, secondary);
  557. };
  558. /**
  559. * Perform a viewbox update via a given change function.
  560. *
  561. * @param {Function} changeFn
  562. */
  563. Canvas.prototype._changeViewbox = function(changeFn) {
  564. // notify others of the upcoming viewbox change
  565. this._eventBus.fire('canvas.viewbox.changing');
  566. // perform actual change
  567. changeFn.apply(this);
  568. // reset the cached viewbox so that
  569. // a new get operation on viewbox or zoom
  570. // triggers a viewbox re-computation
  571. this._cachedViewbox = null;
  572. // notify others of the change; this step
  573. // may or may not be debounced
  574. this._viewboxChanged();
  575. };
  576. Canvas.prototype._viewboxChanged = function() {
  577. this._eventBus.fire('canvas.viewbox.changed', { viewbox: this.viewbox() });
  578. };
  579. /**
  580. * Gets or sets the view box of the canvas, i.e. the
  581. * area that is currently displayed.
  582. *
  583. * The getter may return a cached viewbox (if it is currently
  584. * changing). To force a recomputation, pass `false` as the first argument.
  585. *
  586. * @example
  587. *
  588. * canvas.viewbox({ x: 100, y: 100, width: 500, height: 500 })
  589. *
  590. * // sets the visible area of the diagram to (100|100) -> (600|100)
  591. * // and and scales it according to the diagram width
  592. *
  593. * var viewbox = canvas.viewbox(); // pass `false` to force recomputing the box.
  594. *
  595. * console.log(viewbox);
  596. * // {
  597. * // inner: Dimensions,
  598. * // outer: Dimensions,
  599. * // scale,
  600. * // x, y,
  601. * // width, height
  602. * // }
  603. *
  604. * // if the current diagram is zoomed and scrolled, you may reset it to the
  605. * // default zoom via this method, too:
  606. *
  607. * var zoomedAndScrolledViewbox = canvas.viewbox();
  608. *
  609. * canvas.viewbox({
  610. * x: 0,
  611. * y: 0,
  612. * width: zoomedAndScrolledViewbox.outer.width,
  613. * height: zoomedAndScrolledViewbox.outer.height
  614. * });
  615. *
  616. * @param {Object} [box] the new view box to set
  617. * @param {Number} box.x the top left X coordinate of the canvas visible in view box
  618. * @param {Number} box.y the top left Y coordinate of the canvas visible in view box
  619. * @param {Number} box.width the visible width
  620. * @param {Number} box.height
  621. *
  622. * @return {Object} the current view box
  623. */
  624. Canvas.prototype.viewbox = function(box) {
  625. if (box === undefined && this._cachedViewbox) {
  626. return this._cachedViewbox;
  627. }
  628. var viewport = this._viewport,
  629. innerBox,
  630. outerBox = this.getSize(),
  631. matrix,
  632. transform,
  633. scale,
  634. x, y;
  635. if (!box) {
  636. // compute the inner box based on the
  637. // diagrams default layer. This allows us to exclude
  638. // external components, such as overlays
  639. innerBox = this.getDefaultLayer().getBBox();
  640. transform = svgTransform(viewport);
  641. matrix = transform ? transform.matrix : createMatrix();
  642. scale = round(matrix.a, 1000);
  643. x = round(-matrix.e || 0, 1000);
  644. y = round(-matrix.f || 0, 1000);
  645. box = this._cachedViewbox = {
  646. x: x ? x / scale : 0,
  647. y: y ? y / scale : 0,
  648. width: outerBox.width / scale,
  649. height: outerBox.height / scale,
  650. scale: scale,
  651. inner: {
  652. width: innerBox.width,
  653. height: innerBox.height,
  654. x: innerBox.x,
  655. y: innerBox.y
  656. },
  657. outer: outerBox
  658. };
  659. return box;
  660. } else {
  661. this._changeViewbox(function() {
  662. scale = Math.min(outerBox.width / box.width, outerBox.height / box.height);
  663. var matrix = this._svg.createSVGMatrix()
  664. .scale(scale)
  665. .translate(-box.x, -box.y);
  666. svgTransform(viewport, matrix);
  667. });
  668. }
  669. return box;
  670. };
  671. /**
  672. * Gets or sets the scroll of the canvas.
  673. *
  674. * @param {Object} [delta] the new scroll to apply.
  675. *
  676. * @param {Number} [delta.dx]
  677. * @param {Number} [delta.dy]
  678. */
  679. Canvas.prototype.scroll = function(delta) {
  680. var node = this._viewport;
  681. var matrix = node.getCTM();
  682. if (delta) {
  683. this._changeViewbox(function() {
  684. delta = assign({ dx: 0, dy: 0 }, delta || {});
  685. matrix = this._svg.createSVGMatrix().translate(delta.dx, delta.dy).multiply(matrix);
  686. setCTM(node, matrix);
  687. });
  688. }
  689. return { x: matrix.e, y: matrix.f };
  690. };
  691. /**
  692. * Gets or sets the current zoom of the canvas, optionally zooming
  693. * to the specified position.
  694. *
  695. * The getter may return a cached zoom level. Call it with `false` as
  696. * the first argument to force recomputation of the current level.
  697. *
  698. * @param {String|Number} [newScale] the new zoom level, either a number, i.e. 0.9,
  699. * or `fit-viewport` to adjust the size to fit the current viewport
  700. * @param {String|Point} [center] the reference point { x: .., y: ..} to zoom to, 'auto' to zoom into mid or null
  701. *
  702. * @return {Number} the current scale
  703. */
  704. Canvas.prototype.zoom = function(newScale, center) {
  705. if (!newScale) {
  706. return this.viewbox(newScale).scale;
  707. }
  708. if (newScale === 'fit-viewport') {
  709. return this._fitViewport(center);
  710. }
  711. var outer,
  712. matrix;
  713. this._changeViewbox(function() {
  714. if (typeof center !== 'object') {
  715. outer = this.viewbox().outer;
  716. center = {
  717. x: outer.width / 2,
  718. y: outer.height / 2
  719. };
  720. }
  721. matrix = this._setZoom(newScale, center);
  722. });
  723. return round(matrix.a, 1000);
  724. };
  725. function setCTM(node, m) {
  726. var mstr = 'matrix(' + m.a + ',' + m.b + ',' + m.c + ',' + m.d + ',' + m.e + ',' + m.f + ')';
  727. node.setAttribute('transform', mstr);
  728. }
  729. Canvas.prototype._fitViewport = function(center) {
  730. var vbox = this.viewbox(),
  731. outer = vbox.outer,
  732. inner = vbox.inner,
  733. newScale,
  734. newViewbox;
  735. // display the complete diagram without zooming in.
  736. // instead of relying on internal zoom, we perform a
  737. // hard reset on the canvas viewbox to realize this
  738. //
  739. // if diagram does not need to be zoomed in, we focus it around
  740. // the diagram origin instead
  741. if (inner.x >= 0 &&
  742. inner.y >= 0 &&
  743. inner.x + inner.width <= outer.width &&
  744. inner.y + inner.height <= outer.height &&
  745. !center) {
  746. newViewbox = {
  747. x: 0,
  748. y: 0,
  749. width: Math.max(inner.width + inner.x, outer.width),
  750. height: Math.max(inner.height + inner.y, outer.height)
  751. };
  752. } else {
  753. newScale = Math.min(1, outer.width / inner.width, outer.height / inner.height);
  754. newViewbox = {
  755. x: inner.x + (center ? inner.width / 2 - outer.width / newScale / 2 : 0),
  756. y: inner.y + (center ? inner.height / 2 - outer.height / newScale / 2 : 0),
  757. width: outer.width / newScale,
  758. height: outer.height / newScale
  759. };
  760. }
  761. this.viewbox(newViewbox);
  762. return this.viewbox(false).scale;
  763. };
  764. Canvas.prototype._setZoom = function(scale, center) {
  765. var svg = this._svg,
  766. viewport = this._viewport;
  767. var matrix = svg.createSVGMatrix();
  768. var point = svg.createSVGPoint();
  769. var centerPoint,
  770. originalPoint,
  771. currentMatrix,
  772. scaleMatrix,
  773. newMatrix;
  774. currentMatrix = viewport.getCTM();
  775. var currentScale = currentMatrix.a;
  776. if (center) {
  777. centerPoint = assign(point, center);
  778. // revert applied viewport transformations
  779. originalPoint = centerPoint.matrixTransform(currentMatrix.inverse());
  780. // create scale matrix
  781. scaleMatrix = matrix
  782. .translate(originalPoint.x, originalPoint.y)
  783. .scale(1 / currentScale * scale)
  784. .translate(-originalPoint.x, -originalPoint.y);
  785. newMatrix = currentMatrix.multiply(scaleMatrix);
  786. } else {
  787. newMatrix = matrix.scale(scale);
  788. }
  789. setCTM(this._viewport, newMatrix);
  790. return newMatrix;
  791. };
  792. /**
  793. * Returns the size of the canvas
  794. *
  795. * @return {Dimensions}
  796. */
  797. Canvas.prototype.getSize = function() {
  798. return {
  799. width: this._container.clientWidth,
  800. height: this._container.clientHeight
  801. };
  802. };
  803. /**
  804. * Return the absolute bounding box for the given element
  805. *
  806. * The absolute bounding box may be used to display overlays in the
  807. * callers (browser) coordinate system rather than the zoomed in/out
  808. * canvas coordinates.
  809. *
  810. * @param {ElementDescriptor} element
  811. * @return {Bounds} the absolute bounding box
  812. */
  813. Canvas.prototype.getAbsoluteBBox = function(element) {
  814. var vbox = this.viewbox();
  815. var bbox;
  816. // connection
  817. // use svg bbox
  818. if (element.waypoints) {
  819. var gfx = this.getGraphics(element);
  820. bbox = gfx.getBBox();
  821. }
  822. // shapes
  823. // use data
  824. else {
  825. bbox = element;
  826. }
  827. var x = bbox.x * vbox.scale - vbox.x * vbox.scale;
  828. var y = bbox.y * vbox.scale - vbox.y * vbox.scale;
  829. var width = bbox.width * vbox.scale;
  830. var height = bbox.height * vbox.scale;
  831. return {
  832. x: x,
  833. y: y,
  834. width: width,
  835. height: height
  836. };
  837. };
  838. /**
  839. * Fires an event in order other modules can react to the
  840. * canvas resizing
  841. */
  842. Canvas.prototype.resized = function() {
  843. // force recomputation of view box
  844. delete this._cachedViewbox;
  845. this._eventBus.fire('canvas.resized');
  846. };