UNPKG

@antv/x6

Version:

JavaScript diagramming library that uses SVG and HTML for rendering.

418 lines 15.5 kB
var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; import { Point, Line, Rectangle, Polyline, Ellipse, Path } from '../../geometry'; import { attr } from './attr'; import { sample, toPath, getPointsFromSvgElement } from './path'; import { ensureId, isSVGGraphicsElement, createSvgElement, isHTMLElement, } from './elem'; import { getComputedStyle } from './style'; import { createSVGPoint, createSVGMatrix, decomposeMatrix, transformRectangle, transformStringToMatrix, } from './matrix'; /** * Returns the bounding box of the element after transformations are * applied. If `withoutTransformations` is `true`, transformations of * the element will not be considered when computing the bounding box. * If `target` is specified, bounding box will be computed relatively * to the `target` element. */ export function bbox(elem, withoutTransformations, target) { let box; const ownerSVGElement = elem.ownerSVGElement; // 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 (!ownerSVGElement) { return new Rectangle(0, 0, 0, 0); } try { box = elem.getBBox(); } catch (e) { // Fallback for IE. box = { x: elem.clientLeft, y: elem.clientTop, width: elem.clientWidth, height: elem.clientHeight, }; } if (withoutTransformations) { return Rectangle.create(box); } const matrix = getTransformToElement(elem, target || ownerSVGElement); return transformRectangle(box, matrix); } /** * Returns the bounding box of the element after transformations are * applied. Unlike `bbox()`, this function fixes a browser implementation * bug to return the correct bounding box if this elemenent is a group of * svg elements (if `options.recursive` is specified). */ export function getBBox(elem, options = {}) { let outputBBox; const ownerSVGElement = elem.ownerSVGElement; // 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 the element is not an SVGGraphicsElement, we could not measure the // bounding box either if (!ownerSVGElement || !isSVGGraphicsElement(elem)) { if (isHTMLElement(elem)) { // If the element is a HTMLElement, return the position relative to the body const { left, top, width, height } = getBoundingOffsetRect(elem); return new Rectangle(left, top, width, height); } return new Rectangle(0, 0, 0, 0); } let target = options.target; const recursive = options.recursive; if (!recursive) { try { outputBBox = elem.getBBox(); } catch (e) { outputBBox = { x: elem.clientLeft, y: elem.clientTop, width: elem.clientWidth, height: elem.clientHeight, }; } if (!target) { return Rectangle.create(outputBBox); } // transform like target const matrix = getTransformToElement(elem, target); return transformRectangle(outputBBox, matrix); } // recursive { const children = elem.childNodes; const n = children.length; if (n === 0) { return getBBox(elem, { target }); } if (!target) { target = elem; // eslint-disable-line } for (let i = 0; i < n; i += 1) { const child = children[i]; let childBBox; if (child.childNodes.length === 0) { childBBox = getBBox(child, { target }); } else { // if child is a group element, enter it with a recursive call childBBox = getBBox(child, { target, recursive: true }); } if (!outputBBox) { outputBBox = childBBox; } else { outputBBox = outputBBox.union(childBBox); } } return outputBBox; } } // BBox is calculated by the attribute on the node export function getBBoxByElementAttr(elem) { let node = elem; let tagName = node ? node.tagName.toLowerCase() : ''; // find shape node while (tagName === 'g') { node = node.firstElementChild; tagName = node ? node.tagName.toLowerCase() : ''; } const attr = (name) => { const s = node.getAttribute(name); const v = s ? parseFloat(s) : 0; return Number.isNaN(v) ? 0 : v; }; let r; let bbox; switch (tagName) { case 'rect': bbox = new Rectangle(attr('x'), attr('y'), attr('width'), attr('height')); break; case 'circle': r = attr('r'); bbox = new Rectangle(attr('cx') - r, attr('cy') - r, 2 * r, 2 * r); break; default: break; } return bbox; } // Matrix is calculated by the transform attribute on the node export function getMatrixByElementAttr(elem, target) { let matrix = createSVGMatrix(); if (isSVGGraphicsElement(target) && isSVGGraphicsElement(elem)) { let node = elem; const matrixList = []; while (node && node !== target) { const transform = node.getAttribute('transform') || null; const nodeMatrix = transformStringToMatrix(transform); matrixList.push(nodeMatrix); node = node.parentNode; } matrixList.reverse().forEach((m) => { matrix = matrix.multiply(m); }); } return matrix; } /** * Returns an DOMMatrix that specifies the transformation necessary * to convert `elem` coordinate system into `target` coordinate system. */ export function getTransformToElement(elem, target) { if (isSVGGraphicsElement(target) && isSVGGraphicsElement(elem)) { const targetCTM = target.getScreenCTM(); const nodeCTM = elem.getScreenCTM(); if (targetCTM && nodeCTM) { return targetCTM.inverse().multiply(nodeCTM); } } // Could not get actual transformation matrix return createSVGMatrix(); } /** * Converts a global point with coordinates `x` and `y` into the * coordinate space of the element. */ export function toLocalPoint(elem, x, y) { const svg = elem instanceof SVGSVGElement ? elem : elem.ownerSVGElement; const p = svg.createSVGPoint(); p.x = x; p.y = y; try { const ctm = svg.getScreenCTM(); const globalPoint = p.matrixTransform(ctm.inverse()); const globalToLocalMatrix = getTransformToElement(elem, svg).inverse(); return globalPoint.matrixTransform(globalToLocalMatrix); } catch (e) { return p; } } /** * Convert the SVGElement to an equivalent geometric shape. The element's * transformations are not taken into account. * * SVGRectElement => Rectangle * * SVGLineElement => Line * * SVGCircleElement => Ellipse * * SVGEllipseElement => Ellipse * * SVGPolygonElement => Polyline * * SVGPolylineElement => Polyline * * SVGPathElement => Path * * others => Rectangle */ export function toGeometryShape(elem) { const attr = (name) => { const s = elem.getAttribute(name); const v = s ? parseFloat(s) : 0; return Number.isNaN(v) ? 0 : v; }; switch (elem instanceof SVGElement && elem.nodeName.toLowerCase()) { case 'rect': return new Rectangle(attr('x'), attr('y'), attr('width'), attr('height')); case 'circle': return new Ellipse(attr('cx'), attr('cy'), attr('r'), attr('r')); case 'ellipse': return new Ellipse(attr('cx'), attr('cy'), attr('rx'), attr('ry')); case 'polyline': { const points = getPointsFromSvgElement(elem); return new Polyline(points); } case 'polygon': { const points = getPointsFromSvgElement(elem); if (points.length > 1) { points.push(points[0]); } return new Polyline(points); } case 'path': { let d = elem.getAttribute('d'); if (!Path.isValid(d)) { d = Path.normalize(d); } return Path.parse(d); } case 'line': { return new Line(attr('x1'), attr('y1'), attr('x2'), attr('y2')); } default: break; } // Anything else is a rectangle return getBBox(elem); } export function getIntersection(elem, ref, target) { const svg = elem instanceof SVGSVGElement ? elem : elem.ownerSVGElement; target = target || svg; // eslint-disable-line const bbox = getBBox(target); const center = bbox.getCenter(); if (!bbox.intersectsWithLineFromCenterToPoint(ref)) { return null; } let spot = null; const tagName = elem.tagName.toLowerCase(); // Little speed up optimization for `<rect>` element. We do not do convert // to path element and sampling but directly calculate the intersection // through a transformed geometrical rectangle. if (tagName === 'rect') { const gRect = new Rectangle(parseFloat(elem.getAttribute('x') || '0'), parseFloat(elem.getAttribute('y') || '0'), parseFloat(elem.getAttribute('width') || '0'), parseFloat(elem.getAttribute('height') || '0')); // Get the rect transformation matrix with regards to the SVG document. const rectMatrix = getTransformToElement(elem, target); const rectMatrixComponents = decomposeMatrix(rectMatrix); // Rotate the rectangle back so that we can use // `intersectsWithLineFromCenterToPoint()`. const reseted = svg.createSVGTransform(); reseted.setRotate(-rectMatrixComponents.rotation, center.x, center.y); const rect = transformRectangle(gRect, reseted.matrix.multiply(rectMatrix)); spot = Rectangle.create(rect).intersectsWithLineFromCenterToPoint(ref, rectMatrixComponents.rotation); } else if (tagName === 'path' || tagName === 'polygon' || tagName === 'polyline' || tagName === 'circle' || tagName === 'ellipse') { const pathNode = tagName === 'path' ? elem : toPath(elem); const samples = sample(pathNode); let minDistance = Infinity; let closestSamples = []; for (let i = 0, ii = samples.length; i < ii; i += 1) { const sample = samples[i]; // Convert the sample point in the local coordinate system // to the global coordinate system. let gp = createSVGPoint(sample.x, sample.y); gp = gp.matrixTransform(getTransformToElement(elem, target)); const ggp = Point.create(gp); const centerDistance = ggp.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. const refDistance = ggp.distance(ref) * 1.1; const distance = centerDistance + refDistance; if (distance < minDistance) { minDistance = distance; closestSamples = [{ sample, refDistance }]; } else if (distance < minDistance + 1) { closestSamples.push({ sample, refDistance }); } } closestSamples.sort((a, b) => a.refDistance - b.refDistance); if (closestSamples[0]) { spot = Point.create(closestSamples[0].sample); } } return spot; } export function animate(elem, options) { return createAnimation(elem, options, 'animate'); } export function animateTransform(elem, options) { return createAnimation(elem, options, 'animateTransform'); } function createAnimation(elem, options, type) { // @see // https://www.w3.org/TR/SVG11/animate.html#AnimateElement // https://developer.mozilla.org/en-US/docs/Web/API/SVGAnimateElement // https://developer.mozilla.org/en-US/docs/Web/API/SVGAnimateTransformElement const animate = createSvgElement(type); elem.appendChild(animate); try { return setupAnimation(animate, options); } catch (error) { // pass } return () => { }; } function setupAnimation(animate, options) { const { start, complete, repeat } = options, attrs = __rest(options, ["start", "complete", "repeat"]); attr(animate, attrs); start && animate.addEventListener('beginEvent', start); complete && animate.addEventListener('endEvent', complete); repeat && animate.addEventListener('repeatEvent', repeat); const ani = animate; ani.beginElement(); return () => ani.endElement(); } /** * Animate the element along the path SVG element (or Vector object). * `attrs` contain Animation Timing attributes describing the animation. */ export function animateAlongPath(elem, options, path) { const id = ensureId(path); // https://developer.mozilla.org/en-US/docs/Web/API/SVGAnimationElement const animate = createSvgElement('animateMotion'); const mpath = createSvgElement('mpath'); attr(mpath, { 'xlink:href': `#${id}` }); animate.appendChild(mpath); elem.appendChild(animate); try { return setupAnimation(animate, options); } catch (e) { // Fallback for IE 9. if (document.documentElement.getAttribute('smiling') === 'fake') { // Register the animation. (See `https://answers.launchpad.net/smil/+question/203333`) const ani = animate; ani.animators = []; const win = window; const animationID = ani.getAttribute('id'); if (animationID) { win.id2anim[animationID] = ani; } const targets = win.getTargets(ani); for (let i = 0, ii = targets.length; i < ii; i += 1) { const target = targets[i]; const animator = new win.Animator(ani, target, i); win.animators.push(animator); ani.animators[i] = animator; animator.register(); } } } return () => { }; } export function getBoundingOffsetRect(elem) { let left = 0; let top = 0; let width = 0; let height = 0; if (elem) { let current = elem; while (current) { left += current.offsetLeft; top += current.offsetTop; current = current.offsetParent; if (current) { left += parseInt(getComputedStyle(current, 'borderLeft'), 10); top += parseInt(getComputedStyle(current, 'borderTop'), 10); } } width = elem.offsetWidth; height = elem.offsetHeight; } return { left, top, width, height }; } //# sourceMappingURL=geom.js.map