/*! JointJS v0.9.7 - JavaScript diagramming library 2016-04-20
|
|
|
This Source Code Form is subject to the terms of the Mozilla Public
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
*/
|
(function(root, factory) {
|
|
if (typeof define === 'function' && define.amd) {
|
|
// AMD. Register as an anonymous module.
|
define(['g'], function(g) {
|
return factory(g);
|
});
|
|
} else if (typeof exports === 'object') {
|
|
// Node. Does not work with strict CommonJS, but
|
// only CommonJS-like environments that support module.exports,
|
// like Node.
|
var g = require('./geometry');
|
|
module.exports = factory(g);
|
|
} else {
|
|
// Browser globals.
|
var g = root.g;
|
|
root.Vectorizer = root.V = factory(g);
|
}
|
|
}(this, function(g) {
|
|
// Vectorizer.
|
// -----------
|
|
// A tiny library for making your life easier when dealing with SVG.
|
// The only Vectorizer dependency is the Geometry library.
|
|
// Copyright © 2012 - 2015 client IO (http://client.io)
|
|
var V;
|
var Vectorizer;
|
|
V = Vectorizer = (function() {
|
|
'use strict';
|
|
var hasSvg = typeof window === 'object' &&
|
!!(
|
window.SVGAngle ||
|
document.implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#BasicStructure', '1.1')
|
);
|
|
// SVG support is required.
|
if (!hasSvg) {
|
|
// Return a function that throws an error when it is used.
|
return function() {
|
throw new Error('SVG is required to use Vectorizer.');
|
};
|
}
|
|
// XML namespaces.
|
var ns = {
|
xmlns: 'http://www.w3.org/2000/svg',
|
xlink: 'http://www.w3.org/1999/xlink'
|
};
|
|
var SVGversion = '1.1';
|
|
var V = function(el, attrs, children) {
|
|
// This allows using V() without the new keyword.
|
if (!(this instanceof V)) {
|
return V.apply(Object.create(V.prototype), arguments);
|
}
|
|
if (!el) return;
|
|
if (V.isV(el)) {
|
el = el.node;
|
}
|
|
attrs = attrs || {};
|
|
if (V.isString(el)) {
|
|
if (el.toLowerCase() === 'svg') {
|
|
// Create a new SVG canvas.
|
el = V.createSvgDocument();
|
|
} else if (el[0] === '<') {
|
|
// Create element from an SVG string.
|
// Allows constructs of type: `document.appendChild(V('<rect></rect>').node)`.
|
|
var svgDoc = V.createSvgDocument(el);
|
|
// Note that `V()` might also return an array should the SVG string passed as
|
// the first argument contain more than one root element.
|
if (svgDoc.childNodes.length > 1) {
|
|
// Map child nodes to `V`s.
|
var arrayOfVels = [];
|
var i, len;
|
|
for (i = 0, len = svgDoc.childNodes.length; i < len; i++) {
|
|
var childNode = svgDoc.childNodes[i];
|
arrayOfVels.push(new V(document.importNode(childNode, true)));
|
}
|
|
return arrayOfVels;
|
}
|
|
el = document.importNode(svgDoc.firstChild, true);
|
|
} else {
|
|
el = document.createElementNS(ns.xmlns, el);
|
}
|
}
|
|
this.node = el;
|
|
if (!this.node.id) {
|
this.node.id = V.uniqueId();
|
}
|
|
this.setAttributes(attrs);
|
|
if (children) {
|
this.append(children);
|
}
|
|
return this;
|
};
|
|
/**
|
* @param {SVGGElement} toElem
|
* @returns {SVGMatrix}
|
*/
|
V.prototype.getTransformToElement = function(toElem) {
|
|
return toElem.getScreenCTM().inverse().multiply(this.node.getScreenCTM());
|
};
|
|
/**
|
* @param {SVGMatrix} matrix
|
* @returns {V, SVGMatrix} Setter / Getter
|
*/
|
V.prototype.transform = function(matrix) {
|
|
if (V.isUndefined(matrix)) {
|
return (this.node.parentNode)
|
? this.getTransformToElement(this.node.parentNode)
|
: this.node.getScreenCTM();
|
}
|
|
var svgTransform = V.createSVGTransform(matrix);
|
this.node.transform.baseVal.appendItem(svgTransform);
|
return this;
|
};
|
|
V.prototype.translate = function(tx, ty, opt) {
|
|
opt = opt || {};
|
ty = ty || 0;
|
|
var transformAttr = this.attr('transform') || '';
|
var transform = V.parseTransformString(transformAttr);
|
|
// Is it a getter?
|
if (V.isUndefined(tx)) {
|
return transform.translate;
|
}
|
|
transformAttr = transformAttr.replace(/translate\([^\)]*\)/g, '').trim();
|
|
var newTx = opt.absolute ? tx : transform.translate.tx + tx;
|
var newTy = opt.absolute ? ty : transform.translate.ty + ty;
|
var newTranslate = 'translate(' + newTx + ',' + newTy + ')';
|
|
// Note that `translate()` is always the first transformation. This is
|
// usually the desired case.
|
this.attr('transform', (newTranslate + ' ' + transformAttr).trim());
|
return this;
|
};
|
|
V.prototype.rotate = function(angle, cx, cy, opt) {
|
|
opt = opt || {};
|
|
var transformAttr = this.attr('transform') || '';
|
var transform = V.parseTransformString(transformAttr);
|
|
// Is it a getter?
|
if (V.isUndefined(angle)) {
|
return transform.rotate;
|
}
|
|
transformAttr = transformAttr.replace(/rotate\([^\)]*\)/g, '').trim();
|
|
angle %= 360;
|
|
var newAngle = opt.absolute ? angle : transform.rotate.angle + angle;
|
var newOrigin = (cx !== undefined && cy !== undefined) ? ',' + cx + ',' + cy : '';
|
var newRotate = 'rotate(' + newAngle + newOrigin + ')';
|
|
this.attr('transform', (transformAttr + ' ' + newRotate).trim());
|
return this;
|
};
|
|
// Note that `scale` as the only transformation does not combine with previous values.
|
V.prototype.scale = function(sx, sy) {
|
|
sy = V.isUndefined(sy) ? sx : sy;
|
|
var transformAttr = this.attr('transform') || '';
|
var transform = V.parseTransformString(transformAttr);
|
|
// Is it a getter?
|
if (V.isUndefined(sx)) {
|
return transform.scale;
|
}
|
|
transformAttr = transformAttr.replace(/scale\([^\)]*\)/g, '').trim();
|
|
var newScale = 'scale(' + sx + ',' + sy + ')';
|
|
this.attr('transform', (transformAttr + ' ' + newScale).trim());
|
return this;
|
};
|
|
// Get SVGRect that contains coordinates and dimension of the real bounding box,
|
// i.e. after transformations are applied.
|
// If `target` is specified, bounding box will be computed relatively to `target` element.
|
V.prototype.bbox = function(withoutTransformations, target) {
|
|
// If the element is not in the live DOM, it does not have a bounding box defined and
|
// so fall back to 'zero' dimension element.
|
if (!this.node.ownerSVGElement) return { x: 0, y: 0, width: 0, height: 0 };
|
|
var box;
|
try {
|
|
box = this.node.getBBox();
|
// We are creating a new object as the standard says that you can't
|
// modify the attributes of a bbox.
|
box = { x: box.x, y: box.y, width: box.width, height: box.height };
|
|
} catch (e) {
|
|
// Fallback for IE.
|
box = {
|
x: this.node.clientLeft,
|
y: this.node.clientTop,
|
width: this.node.clientWidth,
|
height: this.node.clientHeight
|
};
|
}
|
|
if (withoutTransformations) {
|
|
return box;
|
}
|
|
var matrix = this.getTransformToElement(target || this.node.ownerSVGElement);
|
|
return V.transformRect(box, matrix);
|
};
|
|
V.prototype.text = function(content, opt) {
|
|
// Replace all spaces with the Unicode No-break space (http://www.fileformat.info/info/unicode/char/a0/index.htm).
|
// IE would otherwise collapse all spaces into one.
|
content = V.sanitizeText(content);
|
opt = opt || {};
|
var lines = content.split('\n');
|
var i = 0;
|
var tspan;
|
|
// `alignment-baseline` does not work in Firefox.
|
// Setting `dominant-baseline` on the `<text>` element doesn't work in IE9.
|
// In order to have the 0,0 coordinate of the `<text>` element (or the first `<tspan>`)
|
// in the top left corner we translate the `<text>` element by `0.8em`.
|
// See `http://www.w3.org/Graphics/SVG/WG/wiki/How_to_determine_dominant_baseline`.
|
// See also `http://apike.ca/prog_svg_text_style.html`.
|
var y = this.attr('y');
|
if (!y) {
|
this.attr('y', '0.8em');
|
}
|
|
// An empty text gets rendered into the DOM in webkit-based browsers.
|
// In order to unify this behaviour across all browsers
|
// we rather hide the text element when it's empty.
|
this.attr('display', content ? null : 'none');
|
|
// Preserve spaces. In other words, we do not want consecutive spaces to get collapsed to one.
|
this.node.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
|
|
// Easy way to erase all `<tspan>` children;
|
this.node.textContent = '';
|
|
var textNode = this.node;
|
|
if (opt.textPath) {
|
|
// Wrap the text in the SVG <textPath> element that points
|
// to a path defined by `opt.textPath` inside the internal `<defs>` element.
|
var defs = this.find('defs');
|
if (defs.length === 0) {
|
defs = V('defs');
|
this.append(defs);
|
}
|
|
// If `opt.textPath` is a plain string, consider it to be directly the
|
// SVG path data for the text to go along (this is a shortcut).
|
// Otherwise if it is an object and contains the `d` property, then this is our path.
|
var d = Object(opt.textPath) === opt.textPath ? opt.textPath.d : opt.textPath;
|
if (d) {
|
var path = V('path', { d: d });
|
defs.append(path);
|
}
|
|
var textPath = V('textPath');
|
// Set attributes on the `<textPath>`. The most important one
|
// is the `xlink:href` that points to our newly created `<path/>` element in `<defs/>`.
|
// Note that we also allow the following construct:
|
// `t.text('my text', { textPath: { 'xlink:href': '#my-other-path' } })`.
|
// In other words, one can completely skip the auto-creation of the path
|
// and use any other arbitrary path that is in the document.
|
if (!opt.textPath['xlink:href'] && path) {
|
textPath.attr('xlink:href', '#' + path.node.id);
|
}
|
|
if (Object(opt.textPath) === opt.textPath) {
|
textPath.attr(opt.textPath);
|
}
|
this.append(textPath);
|
// Now all the `<tspan>`s will be inside the `<textPath>`.
|
textNode = textPath.node;
|
}
|
|
var offset = 0;
|
|
for (var i = 0; i < lines.length; i++) {
|
|
var line = lines[i];
|
// Shift all the <tspan> but first by one line (`1em`)
|
var lineHeight = opt.lineHeight || '1em';
|
if (opt.lineHeight === 'auto') {
|
lineHeight = '1.5em';
|
}
|
var vLine = V('tspan', { dy: (i == 0 ? '0em' : lineHeight), x: this.attr('x') || 0 });
|
vLine.addClass('v-line');
|
|
if (line) {
|
|
if (opt.annotations) {
|
|
// Get the line height based on the biggest font size in the annotations for this line.
|
var maxFontSize = 0;
|
|
// Find the *compacted* annotations for this line.
|
var lineAnnotations = V.annotateString(lines[i], V.isArray(opt.annotations) ? opt.annotations : [opt.annotations], { offset: -offset, includeAnnotationIndices: opt.includeAnnotationIndices });
|
for (var j = 0; j < lineAnnotations.length; j++) {
|
|
var annotation = lineAnnotations[j];
|
if (V.isObject(annotation)) {
|
|
var fontSize = parseInt(annotation.attrs['font-size'], 10);
|
if (fontSize && fontSize > maxFontSize) {
|
maxFontSize = fontSize;
|
}
|
|
tspan = V('tspan', annotation.attrs);
|
if (opt.includeAnnotationIndices) {
|
// If `opt.includeAnnotationIndices` is `true`,
|
// set the list of indices of all the applied annotations
|
// in the `annotations` attribute. This list is a comma
|
// separated list of indices.
|
tspan.attr('annotations', annotation.annotations);
|
}
|
if (annotation.attrs['class']) {
|
tspan.addClass(annotation.attrs['class']);
|
}
|
tspan.node.textContent = annotation.t;
|
|
} else {
|
|
tspan = document.createTextNode(annotation || ' ');
|
|
}
|
vLine.append(tspan);
|
}
|
|
if (opt.lineHeight === 'auto' && maxFontSize && i !== 0) {
|
|
vLine.attr('dy', (maxFontSize * 1.2) + 'px');
|
}
|
|
} else {
|
|
vLine.node.textContent = line;
|
}
|
|
} else {
|
|
// Make sure the textContent is never empty. If it is, add a dummy
|
// character and make it invisible, making the following lines correctly
|
// relatively positioned. `dy=1em` won't work with empty lines otherwise.
|
vLine.addClass('v-empty-line');
|
vLine.node.style.opacity = 0;
|
vLine.node.textContent = '-';
|
}
|
|
V(textNode).append(vLine);
|
|
offset += line.length + 1; // + 1 = newline character.
|
}
|
|
return this;
|
};
|
|
V.prototype.attr = function(name, value) {
|
|
if (V.isUndefined(name)) {
|
|
// Return all attributes.
|
var attributes = this.node.attributes;
|
var attrs = {};
|
|
for (var i = 0; i < attributes.length; i++) {
|
attrs[attributes[i].nodeName] = attributes[i].nodeValue;
|
}
|
|
return attrs;
|
}
|
|
if (V.isString(name) && V.isUndefined(value)) {
|
return this.node.getAttribute(name);
|
}
|
|
if (typeof name === 'object') {
|
|
for (var attrName in name) {
|
if (name.hasOwnProperty(attrName)) {
|
V.setAttribute(this.node, attrName, name[attrName]);
|
}
|
}
|
|
} else {
|
|
V.setAttribute(this.node, name, value);
|
}
|
|
return this;
|
};
|
|
V.prototype.remove = function() {
|
|
if (this.node.parentNode) {
|
this.node.parentNode.removeChild(this.node);
|
}
|
|
return this;
|
};
|
|
V.prototype.empty = function() {
|
|
while (this.node.firstChild) {
|
this.node.removeChild(this.node.firstChild);
|
}
|
|
return this;
|
};
|
|
V.prototype.setAttributes = function(attrs) {
|
|
var key;
|
|
for (key in attrs) {
|
V.setAttribute(this.node, key, attrs[key]);
|
}
|
|
return this;
|
};
|
|
V.prototype.append = function(els) {
|
|
if (!V.isArray(els)) {
|
els = [els];
|
}
|
|
var i, len, el;
|
|
for (i = 0, len = els.length; i < len; i++) {
|
el = els[i];
|
this.node.appendChild(V.isV(el) ? el.node : (el.nodeName && el || el[0]));
|
}
|
|
return this;
|
};
|
|
V.prototype.prepend = function(el) {
|
|
this.node.insertBefore(V.isV(el) ? el.node : el, this.node.firstChild);
|
|
return this;
|
};
|
|
V.prototype.svg = function() {
|
|
return this.node instanceof window.SVGSVGElement ? this : V(this.node.ownerSVGElement);
|
};
|
|
V.prototype.defs = function() {
|
|
var defs = this.svg().node.getElementsByTagName('defs');
|
|
return (defs && defs.length) ? V(defs[0]) : undefined;
|
};
|
|
V.prototype.clone = function() {
|
|
var clone = V(this.node.cloneNode(true/* deep */));
|
// Note that clone inherits also ID. Therefore, we need to change it here.
|
clone.node.id = V.uniqueId();
|
return clone;
|
};
|
|
V.prototype.findOne = function(selector) {
|
|
var found = this.node.querySelector(selector);
|
return found ? V(found) : undefined;
|
};
|
|
V.prototype.find = function(selector) {
|
|
var vels = [];
|
var nodes = this.node.querySelectorAll(selector);
|
|
if (nodes) {
|
|
// Map DOM elements to `V`s.
|
for (var i = 0; i < nodes.length; i++) {
|
vels.push(V(nodes[i]));
|
}
|
}
|
|
return vels;
|
};
|
|
// Find an index of an element inside its container.
|
V.prototype.index = function() {
|
|
var index = 0;
|
var node = this.node.previousSibling;
|
|
while (node) {
|
// nodeType 1 for ELEMENT_NODE
|
if (node.nodeType === 1) index++;
|
node = node.previousSibling;
|
}
|
|
return index;
|
};
|
|
V.prototype.findParentByClass = function(className, terminator) {
|
|
var ownerSVGElement = this.node.ownerSVGElement;
|
var node = this.node.parentNode;
|
|
while (node && node !== terminator && node !== ownerSVGElement) {
|
|
var vel = V(node);
|
if (vel.hasClass(className)) {
|
return vel;
|
}
|
|
node = node.parentNode;
|
}
|
|
return null;
|
};
|
|
// Convert global point into the coordinate space of this element.
|
V.prototype.toLocalPoint = function(x, y) {
|
|
var svg = this.svg().node;
|
|
var p = svg.createSVGPoint();
|
p.x = x;
|
p.y = y;
|
|
try {
|
|
var globalPoint = p.matrixTransform(svg.getScreenCTM().inverse());
|
var globalToLocalMatrix = this.getTransformToElement(svg).inverse();
|
|
} catch (e) {
|
// IE9 throws an exception in odd cases. (`Unexpected call to method or property access`)
|
// We have to make do with the original coordianates.
|
return p;
|
}
|
|
return globalPoint.matrixTransform(globalToLocalMatrix);
|
};
|
|
V.prototype.translateCenterToPoint = function(p) {
|
|
var bbox = this.bbox();
|
var center = g.rect(bbox).center();
|
|
this.translate(p.x - center.x, p.y - center.y);
|
};
|
|
// Efficiently auto-orient an element. This basically implements the orient=auto attribute
|
// of markers. The easiest way of understanding on what this does is to imagine the element is an
|
// arrowhead. Calling this method on the arrowhead makes it point to the `position` point while
|
// being auto-oriented (properly rotated) towards the `reference` point.
|
// `target` is the element relative to which the transformations are applied. Usually a viewport.
|
V.prototype.translateAndAutoOrient = function(position, reference, target) {
|
|
// Clean-up previously set transformations except the scale. If we didn't clean up the
|
// previous transformations then they'd add up with the old ones. Scale is an exception as
|
// it doesn't add up, consider: `this.scale(2).scale(2).scale(2)`. The result is that the
|
// element is scaled by the factor 2, not 8.
|
|
var s = this.scale();
|
this.attr('transform', '');
|
this.scale(s.sx, s.sy);
|
|
var svg = this.svg().node;
|
var bbox = this.bbox(false, target);
|
|
// 1. Translate to origin.
|
var translateToOrigin = svg.createSVGTransform();
|
translateToOrigin.setTranslate(-bbox.x - bbox.width / 2, -bbox.y - bbox.height / 2);
|
|
// 2. Rotate around origin.
|
var rotateAroundOrigin = svg.createSVGTransform();
|
var angle = g.point(position).changeInAngle(position.x - reference.x, position.y - reference.y, reference);
|
rotateAroundOrigin.setRotate(angle, 0, 0);
|
|
// 3. Translate to the `position` + the offset (half my width) towards the `reference` point.
|
var translateFinal = svg.createSVGTransform();
|
var finalPosition = g.point(position).move(reference, bbox.width / 2);
|
translateFinal.setTranslate(position.x + (position.x - finalPosition.x), position.y + (position.y - finalPosition.y));
|
|
// 4. Apply transformations.
|
var ctm = this.getTransformToElement(target);
|
var transform = svg.createSVGTransform();
|
transform.setMatrix(
|
translateFinal.matrix.multiply(
|
rotateAroundOrigin.matrix.multiply(
|
translateToOrigin.matrix.multiply(
|
ctm)))
|
);
|
|
// Instead of directly setting the `matrix()` transform on the element, first, decompose
|
// the matrix into separate transforms. This allows us to use normal Vectorizer methods
|
// as they don't work on matrices. An example of this is to retrieve a scale of an element.
|
// this.node.transform.baseVal.initialize(transform);
|
|
var decomposition = V.decomposeMatrix(transform.matrix);
|
|
this.translate(decomposition.translateX, decomposition.translateY);
|
this.rotate(decomposition.rotation);
|
// Note that scale has been already applied, hence the following line stays commented. (it's here just for reference).
|
//this.scale(decomposition.scaleX, decomposition.scaleY);
|
|
return this;
|
};
|
|
V.prototype.animateAlongPath = function(attrs, path) {
|
|
var animateMotion = V('animateMotion', attrs);
|
var mpath = V('mpath', { 'xlink:href': '#' + V(path).node.id });
|
|
animateMotion.append(mpath);
|
|
this.append(animateMotion);
|
try {
|
animateMotion.node.beginElement();
|
} catch (e) {
|
// Fallback for IE 9.
|
// Run the animation programatically if FakeSmile (`http://leunen.me/fakesmile/`) present
|
if (document.documentElement.getAttribute('smiling') === 'fake') {
|
|
// Register the animation. (See `https://answers.launchpad.net/smil/+question/203333`)
|
var animation = animateMotion.node;
|
animation.animators = [];
|
|
var animationID = animation.getAttribute('id');
|
if (animationID) id2anim[animationID] = animation;
|
|
var targets = getTargets(animation);
|
for (var i = 0, len = targets.length; i < len; i++) {
|
var target = targets[i];
|
var animator = new Animator(animation, target, i);
|
animators.push(animator);
|
animation.animators[i] = animator;
|
animator.register();
|
}
|
}
|
}
|
};
|
|
V.prototype.hasClass = function(className) {
|
|
return new RegExp('(\\s|^)' + className + '(\\s|$)').test(this.node.getAttribute('class'));
|
};
|
|
V.prototype.addClass = function(className) {
|
|
if (!this.hasClass(className)) {
|
var prevClasses = this.node.getAttribute('class') || '';
|
this.node.setAttribute('class', (prevClasses + ' ' + className).trim());
|
}
|
|
return this;
|
};
|
|
V.prototype.removeClass = function(className) {
|
|
if (this.hasClass(className)) {
|
var newClasses = this.node.getAttribute('class').replace(new RegExp('(\\s|^)' + className + '(\\s|$)', 'g'), '$2');
|
this.node.setAttribute('class', newClasses);
|
}
|
|
return this;
|
};
|
|
V.prototype.toggleClass = function(className, toAdd) {
|
|
var toRemove = V.isUndefined(toAdd) ? this.hasClass(className) : !toAdd;
|
|
if (toRemove) {
|
this.removeClass(className);
|
} else {
|
this.addClass(className);
|
}
|
|
return this;
|
};
|
|
// Interpolate path by discrete points. The precision of the sampling
|
// is controlled by `interval`. In other words, `sample()` will generate
|
// a point on the path starting at the beginning of the path going to the end
|
// every `interval` pixels.
|
// The sampler can be very useful for e.g. finding intersection between two
|
// paths (finding the two closest points from two samples).
|
V.prototype.sample = function(interval) {
|
|
interval = interval || 1;
|
var node = this.node;
|
var length = node.getTotalLength();
|
var samples = [];
|
var distance = 0;
|
var sample;
|
while (distance < length) {
|
sample = node.getPointAtLength(distance);
|
samples.push({ x: sample.x, y: sample.y, distance: distance });
|
distance += interval;
|
}
|
return samples;
|
};
|
|
V.prototype.convertToPath = function() {
|
|
var path = V('path');
|
path.attr(this.attr());
|
var d = this.convertToPathData();
|
if (d) {
|
path.attr('d', d);
|
}
|
return path;
|
};
|
|
V.prototype.convertToPathData = function() {
|
|
var tagName = this.node.tagName.toUpperCase();
|
|
switch (tagName) {
|
case 'PATH':
|
return this.attr('d');
|
case 'LINE':
|
return V.convertLineToPathData(this.node);
|
case 'POLYGON':
|
return V.convertPolygonToPathData(this.node);
|
case 'POLYLINE':
|
return V.convertPolylineToPathData(this.node);
|
case 'ELLIPSE':
|
return V.convertEllipseToPathData(this.node);
|
case 'CIRCLE':
|
return V.convertCircleToPathData(this.node);
|
case 'RECT':
|
return V.convertRectToPathData(this.node);
|
}
|
|
throw new Error(tagName + ' cannot be converted to PATH.');
|
};
|
|
// Find the intersection of a line starting in the center
|
// of the SVG `node` ending in the point `ref`.
|
// `target` is an SVG element to which `node`s transformations are relative to.
|
// In JointJS, `target` is the `paper.viewport` SVG group element.
|
// Note that `ref` point must be in the coordinate system of the `target` for this function to work properly.
|
// Returns a point in the `target` coordinte system (the same system as `ref` is in) if
|
// an intersection is found. Returns `undefined` otherwise.
|
V.prototype.findIntersection = function(ref, target) {
|
|
var svg = this.svg().node;
|
target = target || svg;
|
var bbox = g.rect(this.bbox(false, target));
|
var center = bbox.center();
|
|
if (!bbox.intersectionWithLineFromCenterToPoint(ref)) return undefined;
|
|
var spot;
|
var tagName = this.node.localName.toUpperCase();
|
|
// Little speed up optimalization for `<rect>` element. We do not do conversion
|
// to path element and sampling but directly calculate the intersection through
|
// a transformed geometrical rectangle.
|
if (tagName === 'RECT') {
|
|
var gRect = g.rect(
|
parseFloat(this.attr('x') || 0),
|
parseFloat(this.attr('y') || 0),
|
parseFloat(this.attr('width')),
|
parseFloat(this.attr('height'))
|
);
|
// Get the rect transformation matrix with regards to the SVG document.
|
var rectMatrix = this.getTransformToElement(target);
|
// Decompose the matrix to find the rotation angle.
|
var rectMatrixComponents = V.decomposeMatrix(rectMatrix);
|
// Now we want to rotate the rectangle back so that we
|
// can use `intersectionWithLineFromCenterToPoint()` passing the angle as the second argument.
|
var resetRotation = svg.createSVGTransform();
|
resetRotation.setRotate(-rectMatrixComponents.rotation, center.x, center.y);
|
var rect = V.transformRect(gRect, resetRotation.matrix.multiply(rectMatrix));
|
spot = g.rect(rect).intersectionWithLineFromCenterToPoint(ref, rectMatrixComponents.rotation);
|
|
} else if (tagName === 'PATH' || tagName === 'POLYGON' || tagName === 'POLYLINE' || tagName === 'CIRCLE' || tagName === 'ELLIPSE') {
|
|
var pathNode = (tagName === 'PATH') ? this : this.convertToPath();
|
var samples = pathNode.sample();
|
var minDistance = Infinity;
|
var closestSamples = [];
|
|
var i, sample, gp, centerDistance, refDistance, distance;
|
|
for (i = 0; i < samples.length; i++) {
|
|
sample = samples[i];
|
// Convert the sample point in the local coordinate system to the global coordinate system.
|
gp = V.createSVGPoint(sample.x, sample.y);
|
gp = gp.matrixTransform(this.getTransformToElement(target));
|
sample = g.point(gp);
|
centerDistance = sample.distance(center);
|
// Penalize a higher distance to the reference point by 10%.
|
// This gives better results. This is due to
|
// inaccuracies introduced by rounding errors and getPointAtLength() returns.
|
refDistance = sample.distance(ref) * 1.1;
|
distance = centerDistance + refDistance;
|
|
if (distance < minDistance) {
|
minDistance = distance;
|
closestSamples = [{ sample: sample, refDistance: refDistance }];
|
} else if (distance < minDistance + 1) {
|
closestSamples.push({ sample: sample, refDistance: refDistance });
|
}
|
}
|
|
closestSamples.sort(function(a, b) {
|
return a.refDistance - b.refDistance;
|
});
|
|
if (closestSamples[0]) {
|
spot = closestSamples[0].sample;
|
}
|
}
|
|
return spot;
|
};
|
|
// Create an SVG document element.
|
// If `content` is passed, it will be used as the SVG content of the `<svg>` root element.
|
V.createSvgDocument = function(content) {
|
|
var svg = '<svg xmlns="' + ns.xmlns + '" xmlns:xlink="' + ns.xlink + '" version="' + SVGversion + '">' + (content || '') + '</svg>';
|
var xml = V.parseXML(svg, { async: false });
|
return xml.documentElement;
|
};
|
|
V.idCounter = 0;
|
|
// A function returning a unique identifier for this client session with every call.
|
V.uniqueId = function() {
|
|
var id = ++V.idCounter + '';
|
return 'v-' + id;
|
};
|
|
// Replace all spaces with the Unicode No-break space (http://www.fileformat.info/info/unicode/char/a0/index.htm).
|
// IE would otherwise collapse all spaces into one. This is used in the text() method but it is
|
// also exposed so that the programmer can use it in case he needs to. This is useful e.g. in tests
|
// when you want to compare the actual DOM text content without having to add the unicode character in
|
// the place of all spaces.
|
V.sanitizeText = function(text) {
|
|
return (text || '').replace(/ /g, '\u00A0');
|
};
|
|
V.isUndefined = function(value) {
|
|
return typeof value === 'undefined';
|
};
|
|
V.isString = function(value) {
|
|
return typeof value === 'string';
|
};
|
|
V.isObject = function(value) {
|
|
return value && (typeof value === 'object');
|
};
|
|
V.isArray = Array.isArray;
|
|
V.parseXML = function(data, opt) {
|
|
opt = opt || {};
|
|
var xml;
|
|
try {
|
var parser = new DOMParser();
|
|
if (!V.isUndefined(opt.async)) {
|
parser.async = opt.async;
|
}
|
|
xml = parser.parseFromString(data, 'text/xml');
|
} catch (error) {
|
xml = undefined;
|
}
|
|
if (!xml || xml.getElementsByTagName('parsererror').length) {
|
throw new Error('Invalid XML: ' + data);
|
}
|
|
return xml;
|
};
|
|
V.setAttribute = function(el, name, value) {
|
|
if (name.indexOf(':') > -1) {
|
// Attribute names can be namespaced. E.g. `image` elements
|
// have a `xlink:href` attribute to set the source of the image.
|
var combinedKey = name.split(':');
|
el.setAttributeNS(ns[combinedKey[0]], combinedKey[1], value);
|
|
} else if (name === 'id') {
|
el.id = value;
|
} else {
|
el.setAttribute(name, value);
|
}
|
};
|
|
V.parseTransformString = function(transform) {
|
|
var translate, rotate, scale;
|
|
if (transform) {
|
|
var separator = /[ ,]+/;
|
|
var translateMatch = transform.match(/translate\((.*)\)/);
|
if (translateMatch) {
|
translate = translateMatch[1].split(separator);
|
}
|
var rotateMatch = transform.match(/rotate\((.*)\)/);
|
if (rotateMatch) {
|
rotate = rotateMatch[1].split(separator);
|
}
|
var scaleMatch = transform.match(/scale\((.*)\)/);
|
if (scaleMatch) {
|
scale = scaleMatch[1].split(separator);
|
}
|
}
|
|
var sx = (scale && scale[0]) ? parseFloat(scale[0]) : 1;
|
|
return {
|
translate: {
|
tx: (translate && translate[0]) ? parseInt(translate[0], 10) : 0,
|
ty: (translate && translate[1]) ? parseInt(translate[1], 10) : 0
|
},
|
rotate: {
|
angle: (rotate && rotate[0]) ? parseInt(rotate[0], 10) : 0,
|
cx: (rotate && rotate[1]) ? parseInt(rotate[1], 10) : undefined,
|
cy: (rotate && rotate[2]) ? parseInt(rotate[2], 10) : undefined
|
},
|
scale: {
|
sx: sx,
|
sy: (scale && scale[1]) ? parseFloat(scale[1]) : sx
|
}
|
};
|
};
|
|
V.deltaTransformPoint = function(matrix, point) {
|
|
var dx = point.x * matrix.a + point.y * matrix.c + 0;
|
var dy = point.x * matrix.b + point.y * matrix.d + 0;
|
return { x: dx, y: dy };
|
};
|
|
V.decomposeMatrix = function(matrix) {
|
|
// @see https://gist.github.com/2052247
|
|
// calculate delta transform point
|
var px = V.deltaTransformPoint(matrix, { x: 0, y: 1 });
|
var py = V.deltaTransformPoint(matrix, { x: 1, y: 0 });
|
|
// calculate skew
|
var skewX = ((180 / Math.PI) * Math.atan2(px.y, px.x) - 90);
|
var skewY = ((180 / Math.PI) * Math.atan2(py.y, py.x));
|
|
return {
|
|
translateX: matrix.e,
|
translateY: matrix.f,
|
scaleX: Math.sqrt(matrix.a * matrix.a + matrix.b * matrix.b),
|
scaleY: Math.sqrt(matrix.c * matrix.c + matrix.d * matrix.d),
|
skewX: skewX,
|
skewY: skewY,
|
rotation: skewX // rotation is the same as skew x
|
};
|
};
|
|
V.isV = function(object) {
|
|
return object instanceof V;
|
};
|
|
// For backwards compatibility:
|
V.isVElement = V.isV;
|
|
var svgDocument = V('svg').node;
|
|
V.createSVGMatrix = function(matrix) {
|
|
var svgMatrix = svgDocument.createSVGMatrix();
|
for (var component in matrix) {
|
svgMatrix[component] = matrix[component];
|
}
|
|
return svgMatrix;
|
};
|
|
V.createSVGTransform = function(matrix) {
|
|
if (!V.isUndefined(matrix)) {
|
|
if (!(matrix instanceof SVGMatrix)) {
|
matrix = V.createSVGMatrix(matrix);
|
}
|
|
return svgDocument.createSVGTransformFromMatrix(matrix);
|
}
|
|
return svgDocument.createSVGTransform();
|
};
|
|
V.createSVGPoint = function(x, y) {
|
|
var p = svgDocument.createSVGPoint();
|
p.x = x;
|
p.y = y;
|
return p;
|
};
|
|
V.transformRect = function(r, matrix) {
|
|
var p = svgDocument.createSVGPoint();
|
|
p.x = r.x;
|
p.y = r.y;
|
var corner1 = p.matrixTransform(matrix);
|
|
p.x = r.x + r.width;
|
p.y = r.y;
|
var corner2 = p.matrixTransform(matrix);
|
|
p.x = r.x + r.width;
|
p.y = r.y + r.height;
|
var corner3 = p.matrixTransform(matrix);
|
|
p.x = r.x;
|
p.y = r.y + r.height;
|
var corner4 = p.matrixTransform(matrix);
|
|
var minX = Math.min(corner1.x, corner2.x, corner3.x, corner4.x);
|
var maxX = Math.max(corner1.x, corner2.x, corner3.x, corner4.x);
|
var minY = Math.min(corner1.y, corner2.y, corner3.y, corner4.y);
|
var maxY = Math.max(corner1.y, corner2.y, corner3.y, corner4.y);
|
|
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
};
|
|
V.transformPoint = function(p, matrix) {
|
|
return V.createSVGPoint(p.x, p.y).matrixTransform(matrix);
|
};
|
|
// Convert a style represented as string (e.g. `'fill="blue"; stroke="red"'`) to
|
// an object (`{ fill: 'blue', stroke: 'red' }`).
|
V.styleToObject = function(styleString) {
|
var ret = {};
|
var styles = styleString.split(';');
|
for (var i = 0; i < styles.length; i++) {
|
var style = styles[i];
|
var pair = style.split('=');
|
ret[pair[0].trim()] = pair[1].trim();
|
}
|
return ret;
|
};
|
|
// Inspired by d3.js https://github.com/mbostock/d3/blob/master/src/svg/arc.js
|
V.createSlicePathData = function(innerRadius, outerRadius, startAngle, endAngle) {
|
|
var svgArcMax = 2 * Math.PI - 1e-6;
|
var r0 = innerRadius;
|
var r1 = outerRadius;
|
var a0 = startAngle;
|
var a1 = endAngle;
|
var da = (a1 < a0 && (da = a0, a0 = a1, a1 = da), a1 - a0);
|
var df = da < Math.PI ? '0' : '1';
|
var c0 = Math.cos(a0);
|
var s0 = Math.sin(a0);
|
var c1 = Math.cos(a1);
|
var s1 = Math.sin(a1);
|
|
return (da >= svgArcMax)
|
? (r0
|
? 'M0,' + r1
|
+ 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + (-r1)
|
+ 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + r1
|
+ 'M0,' + r0
|
+ 'A' + r0 + ',' + r0 + ' 0 1,0 0,' + (-r0)
|
+ 'A' + r0 + ',' + r0 + ' 0 1,0 0,' + r0
|
+ 'Z'
|
: 'M0,' + r1
|
+ 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + (-r1)
|
+ 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + r1
|
+ 'Z')
|
: (r0
|
? 'M' + r1 * c0 + ',' + r1 * s0
|
+ 'A' + r1 + ',' + r1 + ' 0 ' + df + ',1 ' + r1 * c1 + ',' + r1 * s1
|
+ 'L' + r0 * c1 + ',' + r0 * s1
|
+ 'A' + r0 + ',' + r0 + ' 0 ' + df + ',0 ' + r0 * c0 + ',' + r0 * s0
|
+ 'Z'
|
: 'M' + r1 * c0 + ',' + r1 * s0
|
+ 'A' + r1 + ',' + r1 + ' 0 ' + df + ',1 ' + r1 * c1 + ',' + r1 * s1
|
+ 'L0,0'
|
+ 'Z');
|
};
|
|
// Merge attributes from object `b` with attributes in object `a`.
|
// Note that this modifies the object `a`.
|
// Also important to note that attributes are merged but CSS classes are concatenated.
|
V.mergeAttrs = function(a, b) {
|
|
for (var attr in b) {
|
|
if (attr === 'class') {
|
// Concatenate classes.
|
a[attr] = a[attr] ? a[attr] + ' ' + b[attr] : b[attr];
|
} else if (attr === 'style') {
|
// `style` attribute can be an object.
|
if (V.isObject(a[attr]) && V.isObject(b[attr])) {
|
// `style` stored in `a` is an object.
|
a[attr] = V.mergeAttrs(a[attr], b[attr]);
|
} else if (V.isObject(a[attr])) {
|
// `style` in `a` is an object but it's a string in `b`.
|
// Convert the style represented as a string to an object in `b`.
|
a[attr] = V.mergeAttrs(a[attr], V.styleToObject(b[attr]));
|
} else if (V.isObject(b[attr])) {
|
// `style` in `a` is a string, in `b` it's an object.
|
a[attr] = V.mergeAttrs(V.styleToObject(a[attr]), b[attr]);
|
} else {
|
// Both styles are strings.
|
a[attr] = V.mergeAttrs(V.styleToObject(a[attr]), V.styleToObject(b[attr]));
|
}
|
} else {
|
a[attr] = b[attr];
|
}
|
}
|
|
return a;
|
};
|
|
V.annotateString = function(t, annotations, opt) {
|
|
annotations = annotations || [];
|
opt = opt || {};
|
|
var offset = opt.offset || 0;
|
var compacted = [];
|
var batch;
|
var ret = [];
|
var item;
|
var prev;
|
|
for (var i = 0; i < t.length; i++) {
|
|
item = ret[i] = t[i];
|
|
for (var j = 0; j < annotations.length; j++) {
|
|
var annotation = annotations[j];
|
var start = annotation.start + offset;
|
var end = annotation.end + offset;
|
|
if (i >= start && i < end) {
|
// Annotation applies.
|
if (V.isObject(item)) {
|
// There is more than one annotation to be applied => Merge attributes.
|
item.attrs = V.mergeAttrs(V.mergeAttrs({}, item.attrs), annotation.attrs);
|
} else {
|
item = ret[i] = { t: t[i], attrs: annotation.attrs };
|
}
|
if (opt.includeAnnotationIndices) {
|
(item.annotations || (item.annotations = [])).push(j);
|
}
|
}
|
}
|
|
prev = ret[i - 1];
|
|
if (!prev) {
|
|
batch = item;
|
|
} else if (V.isObject(item) && V.isObject(prev)) {
|
// Both previous item and the current one are annotations. If the attributes
|
// didn't change, merge the text.
|
if (JSON.stringify(item.attrs) === JSON.stringify(prev.attrs)) {
|
batch.t += item.t;
|
} else {
|
compacted.push(batch);
|
batch = item;
|
}
|
|
} else if (V.isObject(item)) {
|
// Previous item was a string, current item is an annotation.
|
compacted.push(batch);
|
batch = item;
|
|
} else if (V.isObject(prev)) {
|
// Previous item was an annotation, current item is a string.
|
compacted.push(batch);
|
batch = item;
|
|
} else {
|
// Both previous and current item are strings.
|
batch = (batch || '') + item;
|
}
|
}
|
|
if (batch) {
|
compacted.push(batch);
|
}
|
|
return compacted;
|
};
|
|
V.findAnnotationsAtIndex = function(annotations, index) {
|
|
var found = [];
|
|
if (annotations) {
|
|
annotations.forEach(function(annotation) {
|
|
if (annotation.start < index && index <= annotation.end) {
|
found.push(annotation);
|
}
|
});
|
}
|
|
return found;
|
};
|
|
V.findAnnotationsBetweenIndexes = function(annotations, start, end) {
|
|
var found = [];
|
|
if (annotations) {
|
|
annotations.forEach(function(annotation) {
|
|
if ((start >= annotation.start && start < annotation.end) || (end > annotation.start && end <= annotation.end) || (annotation.start >= start && annotation.end < end)) {
|
found.push(annotation);
|
}
|
});
|
}
|
|
return found;
|
};
|
|
// Shift all the text annotations after character `index` by `offset` positions.
|
V.shiftAnnotations = function(annotations, index, offset) {
|
|
if (annotations) {
|
|
annotations.forEach(function(annotation) {
|
|
if (annotation.start < index && annotation.end >= index) {
|
annotation.end += offset;
|
} else if (annotation.start >= index) {
|
annotation.start += offset;
|
annotation.end += offset;
|
}
|
});
|
}
|
|
return annotations;
|
};
|
|
V.convertLineToPathData = function(line) {
|
|
line = V(line);
|
var d = [
|
'M', line.attr('x1'), line.attr('y1'),
|
'L', line.attr('x2'), line.attr('y2')
|
].join(' ');
|
return d;
|
};
|
|
V.convertPolygonToPathData = function(polygon) {
|
|
polygon = V(polygon);
|
|
var points = V.getPointsFromSvgNode(polygon.node);
|
|
if (!(points.length > 0)) return null;
|
|
return V.svgPointsToPath(points);
|
};
|
|
V.convertPolylineToPathData = function(polyline) {
|
|
var points = V.getPointsFromSvgNode(polyline.node);
|
|
if (!(points.length > 0)) return null;
|
|
return V.svgPointsToPath(points);
|
};
|
|
V.svgPointsToPath = function(points) {
|
|
var i;
|
|
for (i = 0; i < points.length; i++) {
|
points[i] = points[i].x + ' ' + points[i].y;
|
}
|
|
return 'M ' + points.join(' L') + ' Z';
|
};
|
|
V.getPointsFromSvgNode = function(node) {
|
|
var points = [];
|
var i;
|
|
for (i = 0; i < node.points.numberOfItems; i++) {
|
points.push(node.points.getItem(i));
|
}
|
|
return points;
|
};
|
|
V.KAPPA = 0.5522847498307935;
|
|
V.convertCircleToPathData = function(circle) {
|
|
circle = V(circle);
|
var cx = parseFloat(circle.attr('cx')) || 0;
|
var cy = parseFloat(circle.attr('cy')) || 0;
|
var r = parseFloat(circle.attr('r'));
|
var cd = r * V.KAPPA; // Control distance.
|
|
var d = [
|
'M', cx, cy - r, // Move to the first point.
|
'C', cx + cd, cy - r, cx + r, cy - cd, cx + r, cy, // I. Quadrant.
|
'C', cx + r, cy + cd, cx + cd, cy + r, cx, cy + r, // II. Quadrant.
|
'C', cx - cd, cy + r, cx - r, cy + cd, cx - r, cy, // III. Quadrant.
|
'C', cx - r, cy - cd, cx - cd, cy - r, cx, cy - r, // IV. Quadrant.
|
'Z'
|
].join(' ');
|
return d;
|
};
|
|
V.convertEllipseToPathData = function(ellipse) {
|
|
ellipse = V(ellipse);
|
var cx = parseFloat(ellipse.attr('cx')) || 0;
|
var cy = parseFloat(ellipse.attr('cy')) || 0;
|
var rx = parseFloat(ellipse.attr('rx'));
|
var ry = parseFloat(ellipse.attr('ry')) || rx;
|
var cdx = rx * V.KAPPA; // Control distance x.
|
var cdy = ry * V.KAPPA; // Control distance y.
|
|
var d = [
|
'M', cx, cy - ry, // Move to the first point.
|
'C', cx + cdx, cy - ry, cx + rx, cy - cdy, cx + rx, cy, // I. Quadrant.
|
'C', cx + rx, cy + cdy, cx + cdx, cy + ry, cx, cy + ry, // II. Quadrant.
|
'C', cx - cdx, cy + ry, cx - rx, cy + cdy, cx - rx, cy, // III. Quadrant.
|
'C', cx - rx, cy - cdy, cx - cdx, cy - ry, cx, cy - ry, // IV. Quadrant.
|
'Z'
|
].join(' ');
|
return d;
|
};
|
|
V.convertRectToPathData = function(rect) {
|
|
rect = V(rect);
|
var x = parseFloat(rect.attr('x')) || 0;
|
var y = parseFloat(rect.attr('y')) || 0;
|
var width = parseFloat(rect.attr('width')) || 0;
|
var height = parseFloat(rect.attr('height')) || 0;
|
var rx = parseFloat(rect.attr('rx')) || 0;
|
var ry = parseFloat(rect.attr('ry')) || 0;
|
var bbox = g.rect(x, y, width, height);
|
|
var d;
|
|
if (!rx && !ry) {
|
|
d = [
|
'M', bbox.origin().x, bbox.origin().y,
|
'H', bbox.corner().x,
|
'V', bbox.corner().y,
|
'H', bbox.origin().x,
|
'V', bbox.origin().y,
|
'Z'
|
].join(' ');
|
|
} else {
|
|
var r = x + width;
|
var b = y + height;
|
d = [
|
'M', x + rx, y,
|
'L', r - rx, y,
|
'Q', r, y, r, y + ry,
|
'L', r, y + height - ry,
|
'Q', r, b, r - rx, b,
|
'L', x + rx, b,
|
'Q', x, b, x, b - rx,
|
'L', x, y + ry,
|
'Q', x, y, x + rx, y,
|
'Z'
|
].join(' ');
|
}
|
return d;
|
};
|
|
// Convert a rectangle to SVG path commands. `r` is an object of the form:
|
// `{ x: [number], y: [number], width: [number], height: [number], top-ry: [number], top-ry: [number], bottom-rx: [number], bottom-ry: [number] }`,
|
// where `x, y, width, height` are the usual rectangle attributes and [top-/bottom-]rx/ry allows for
|
// specifying radius of the rectangle for all its sides (as opposed to the built-in SVG rectangle
|
// that has only `rx` and `ry` attributes).
|
V.rectToPath = function(r) {
|
|
var topRx = r.rx || r['top-rx'] || 0;
|
var bottomRx = r.rx || r['bottom-rx'] || 0;
|
var topRy = r.ry || r['top-ry'] || 0;
|
var bottomRy = r.ry || r['bottom-ry'] || 0;
|
|
return [
|
'M', r.x, r.y + topRy,
|
'v', r.height - topRy - bottomRy,
|
'a', bottomRx, bottomRy, 0, 0, 0, bottomRx, bottomRy,
|
'h', r.width - 2 * bottomRx,
|
'a', bottomRx, bottomRy, 0, 0, 0, bottomRx, -bottomRy,
|
'v', -(r.height - bottomRy - topRy),
|
'a', topRx, topRy, 0, 0, 0, -topRx, -topRy,
|
'h', -(r.width - 2 * topRx),
|
'a', topRx, topRy, 0, 0, 0, -topRx, topRy
|
].join(' ');
|
};
|
|
return V;
|
|
})();
|
|
|
return V;
|
|
}));
|