Browse Source

Merge branch 'main' of github.com:le5le-com/visualization-design

Alsmile 2 năm trước cách đây
mục cha
commit
8bdb0bce25

+ 1 - 0
package.json

@@ -14,6 +14,7 @@
     "fast-xml-parser": "^4.0.1",
     "file-saver": "^2.0.5",
     "jszip": "^3.10.0",
+    "localforage": "^1.10.0",
     "monaco-editor": "^0.37.1",
     "tdesign-vue-next": "^1.3.0",
     "vue": "^3.2.37",

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 196 - 214
pnpm-lock.yaml


+ 1543 - 0
src/assets/canvas2svg.js

@@ -0,0 +1,1543 @@
+/*!!
+ *  Canvas 2 Svg v1.0.19
+ *  A low level canvas to SVG converter. Uses a mock canvas context to build an SVG document.
+ *
+ *  Licensed under the MIT license:
+ *  http://www.opensource.org/licenses/mit-license.php
+ *
+ *  Author:
+ *  Kerry Liu
+ *
+ *  Copyright (c) 2014 Gliffy Inc.
+ */
+
+(function() {
+  'use strict';
+
+  var STYLES, ctx, CanvasGradient, CanvasPattern, namedEntities;
+
+  //helper function to format a string
+  function format(str, args) {
+    var keys = Object.keys(args),
+      i;
+    for (i = 0; i < keys.length; i++) {
+      str = str.replace(
+        new RegExp('\\{' + keys[i] + '\\}', 'gi'),
+        args[keys[i]]
+      );
+    }
+    return str;
+  }
+
+  //helper function that generates a random string
+  function randomString(holder) {
+    var chars, randomstring, i;
+    if (!holder) {
+      throw new Error(
+        'cannot create a random attribute name for an undefined object'
+      );
+    }
+    chars = 'ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz';
+    randomstring = '';
+    do {
+      randomstring = '';
+      for (i = 0; i < 12; i++) {
+        randomstring += chars[Math.floor(Math.random() * chars.length)];
+      }
+    } while (holder[randomstring]);
+    return randomstring;
+  }
+
+  //helper function to map named to numbered entities
+  function createNamedToNumberedLookup(items, radix) {
+    var i,
+      entity,
+      lookup = {},
+      base10,
+      base16;
+    items = items.split(',');
+    radix = radix || 10;
+    // Map from named to numbered entities.
+    for (i = 0; i < items.length; i += 2) {
+      entity = '&' + items[i + 1] + ';';
+      base10 = parseInt(items[i], radix);
+      lookup[entity] = '&#' + base10 + ';';
+    }
+    //FF and IE need to create a regex from hex values ie &nbsp; == \xa0
+    lookup['\\xa0'] = '&#160;';
+    return lookup;
+  }
+
+  //helper function to map canvas-textAlign to svg-textAnchor
+  function getTextAnchor(textAlign) {
+    //TODO: support rtl languages
+    var mapping = {
+      left: 'start',
+      right: 'end',
+      center: 'middle',
+      start: 'start',
+      end: 'end'
+    };
+    return mapping[textAlign] || mapping.start;
+  }
+
+  //helper function to map canvas-textBaseline to svg-dominantBaseline
+  function getDominantBaseline(textBaseline) {
+    //INFO: not supported in all browsers
+    var mapping = {
+      alphabetic: 'alphabetic',
+      hanging: 'hanging',
+      top: 'text-before-edge',
+      bottom: 'text-after-edge',
+      middle: 'central'
+    };
+    return mapping[textBaseline] || mapping.alphabetic;
+  }
+
+  // Unpack entities lookup where the numbers are in radix 32 to reduce the size
+  // entity mapping courtesy of tinymce
+  namedEntities = createNamedToNumberedLookup(
+    '50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' +
+      '5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' +
+      '5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,' +
+      '5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,' +
+      '68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,' +
+      '6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,' +
+      '6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,' +
+      '75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,' +
+      '7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,' +
+      '7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,' +
+      'sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,' +
+      'st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,' +
+      't9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,' +
+      'tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,' +
+      'u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,' +
+      '81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,' +
+      '8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,' +
+      '8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,' +
+      '8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,' +
+      '8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,' +
+      'nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,' +
+      'rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,' +
+      'Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,' +
+      '80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,' +
+      '811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro',
+    32
+  );
+
+  //Some basic mappings for attributes and default values.
+  STYLES = {
+    strokeStyle: {
+      svgAttr: 'stroke', //corresponding svg attribute
+      canvas: '#000000', //canvas default
+      svg: 'none', //svg default
+      apply: 'stroke' //apply on stroke() or fill()
+    },
+    fillStyle: {
+      svgAttr: 'fill',
+      canvas: '#000000',
+      svg: null, //svg default is black, but we need to special case this to handle canvas stroke without fill
+      apply: 'fill'
+    },
+    lineCap: {
+      svgAttr: 'stroke-linecap',
+      canvas: 'butt',
+      svg: 'butt',
+      apply: 'stroke'
+    },
+    lineJoin: {
+      svgAttr: 'stroke-linejoin',
+      canvas: 'miter',
+      svg: 'miter',
+      apply: 'stroke'
+    },
+    miterLimit: {
+      svgAttr: 'stroke-miterlimit',
+      canvas: 10,
+      svg: 4,
+      apply: 'stroke'
+    },
+    lineWidth: {
+      svgAttr: 'stroke-width',
+      canvas: 1,
+      svg: 1,
+      apply: 'stroke'
+    },
+    globalAlpha: {
+      svgAttr: 'opacity',
+      canvas: 1,
+      svg: 1,
+      apply: 'fill stroke'
+    },
+    font: {
+      //font converts to multiple svg attributes, there is custom logic for this
+      canvas: '12px Arial'
+    },
+    shadowColor: {
+      canvas: '#000000'
+    },
+    shadowOffsetX: {
+      canvas: 0
+    },
+    shadowOffsetY: {
+      canvas: 0
+    },
+    shadowBlur: {
+      canvas: 0
+    },
+    textAlign: {
+      canvas: 'start'
+    },
+    textBaseline: {
+      canvas: 'alphabetic'
+    },
+    lineDash: {
+      svgAttr: 'stroke-dasharray',
+      canvas: [],
+      svg: null,
+      apply: 'stroke'
+    }
+  };
+
+  /**
+   *
+   * @param gradientNode - reference to the gradient
+   * @constructor
+   */
+  CanvasGradient = function(gradientNode, ctx) {
+    this.__root = gradientNode;
+    this.__ctx = ctx;
+  };
+
+  /**
+   * Adds a color stop to the gradient root
+   */
+  CanvasGradient.prototype.addColorStop = function(offset, color) {
+    var stop = this.__ctx.__createElement('stop'),
+      regex,
+      matches;
+    stop.setAttribute('offset', offset);
+    if (color.indexOf('rgba') !== -1) {
+      //separate alpha value, since webkit can't handle it
+      regex = /rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d?\.?\d*)\s*\)/gi;
+      matches = regex.exec(color);
+      stop.setAttribute(
+        'stop-color',
+        format('rgb({r},{g},{b})', {
+          r: matches[1],
+          g: matches[2],
+          b: matches[3]
+        })
+      );
+      stop.setAttribute('stop-opacity', matches[4]);
+    } else {
+      stop.setAttribute('stop-color', color);
+    }
+    this.__root.appendChild(stop);
+  };
+
+  CanvasPattern = function(pattern, ctx) {
+    this.__root = pattern;
+    this.__ctx = ctx;
+  };
+
+  /**
+   * The mock canvas context
+   * @param o - options include:
+   * ctx - existing Context2D to wrap around
+   * width - width of your canvas (defaults to 500)
+   * height - height of your canvas (defaults to 500)
+   * enableMirroring - enables canvas mirroring (get image data) (defaults to false)
+   * document - the document object (defaults to the current document)
+   */
+  ctx = function(o) {
+    var defaultOptions = { width: 500, height: 500, enableMirroring: false },
+      options;
+
+    //keep support for this way of calling C2S: new C2S(width,height)
+    if (arguments.length > 1) {
+      options = defaultOptions;
+      options.width = arguments[0];
+      options.height = arguments[1];
+    } else if (!o) {
+      options = defaultOptions;
+    } else {
+      options = o;
+    }
+
+    if (!(this instanceof ctx)) {
+      //did someone call this without new?
+      return new ctx(options);
+    }
+
+    //setup options
+    this.width = options.width || defaultOptions.width;
+    this.height = options.height || defaultOptions.height;
+    this.enableMirroring =
+      options.enableMirroring !== undefined
+        ? options.enableMirroring
+        : defaultOptions.enableMirroring;
+
+    this.canvas = this; ///point back to this instance!
+    this.__document = options.document || document;
+
+    // allow passing in an existing context to wrap around
+    // if a context is passed in, we know a canvas already exist
+    if (options.ctx) {
+      this.__ctx = options.ctx;
+    } else {
+      this.__canvas = this.__document.createElement('canvas');
+      this.__ctx = this.__canvas.getContext('2d');
+    }
+
+    this.__setDefaultStyles();
+    this.__stack = [this.__getStyleState()];
+    this.__groupStack = [];
+
+    //the root svg element
+    this.__root = this.__document.createElementNS(
+      'http://www.w3.org/2000/svg',
+      'svg'
+    );
+    this.__root.setAttribute('version', 1.1);
+    this.__root.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
+    this.__root.setAttributeNS(
+      'http://www.w3.org/2000/xmlns/',
+      'xmlns:xlink',
+      'http://www.w3.org/1999/xlink'
+    );
+    this.__root.setAttribute('width', this.width);
+    this.__root.setAttribute('height', this.height);
+
+    //make sure we don't generate the same ids in defs
+    this.__ids = {};
+
+    //defs tag
+    this.__defs = this.__document.createElementNS(
+      'http://www.w3.org/2000/svg',
+      'defs'
+    );
+    this.__root.appendChild(this.__defs);
+
+    //also add a group child. the svg element can't use the transform attribute
+    this.__currentElement = this.__document.createElementNS(
+      'http://www.w3.org/2000/svg',
+      'g'
+    );
+    this.__root.appendChild(this.__currentElement);
+  };
+
+  /**
+   * Creates the specified svg element
+   * @private
+   */
+  ctx.prototype.__createElement = function(elementName, properties, resetFill) {
+    if (typeof properties === 'undefined') {
+      properties = {};
+    }
+
+    var element = this.__document.createElementNS(
+        'http://www.w3.org/2000/svg',
+        elementName
+      ),
+      keys = Object.keys(properties),
+      i,
+      key;
+    if (resetFill) {
+      //if fill or stroke is not specified, the svg element should not display. By default SVG's fill is black.
+      element.setAttribute('fill', 'none');
+      element.setAttribute('stroke', 'none');
+    }
+    for (i = 0; i < keys.length; i++) {
+      key = keys[i];
+      element.setAttribute(key, properties[key]);
+    }
+    return element;
+  };
+
+  /**
+   * Applies default canvas styles to the context
+   * @private
+   */
+  ctx.prototype.__setDefaultStyles = function() {
+    //default 2d canvas context properties see:http://www.w3.org/TR/2dcontext/
+    var keys = Object.keys(STYLES),
+      i,
+      key;
+    for (i = 0; i < keys.length; i++) {
+      key = keys[i];
+      this[key] = STYLES[key].canvas;
+    }
+  };
+
+  ctx.prototype.setAttrs = function(pen) {
+    if (!pen) {
+      return;
+    }
+
+    var currentElement = this.__currentElement;
+    currentElement.setAttribute('id', pen.id);
+    currentElement.setAttribute('name', pen.name);
+    pen.text && currentElement.setAttribute('text', pen.text);
+    pen.lineName && currentElement.setAttribute('line-name', pen.lineName);
+    // 连线起终点
+    if (pen.type == 1) {
+      const from = pen.anchors[0];
+      from.connectTo && currentElement.setAttribute('from-id', from.connectTo);
+      const to = pen.anchors[pen.anchors.length - 1];
+      to.connectTo && currentElement.setAttribute('to-id', to.connectTo);
+    }
+
+    // 业务数据
+    if (!Array.isArray(pen.form)) {
+      return;
+    }
+    pen.form.forEach(({key}) => {
+      // TODO: pen[key] 若是一个对象,待考虑
+      currentElement.setAttribute('data-' + key, pen[key]);
+    });
+  };
+
+  /**
+   * Applies styles on restore
+   * @param styleState
+   * @private
+   */
+  ctx.prototype.__applyStyleState = function(styleState) {
+    var keys = Object.keys(styleState),
+      i,
+      key;
+    for (i = 0; i < keys.length; i++) {
+      key = keys[i];
+      this[key] = styleState[key];
+    }
+  };
+
+  /**
+   * Gets the current style state
+   * @return {Object}
+   * @private
+   */
+  ctx.prototype.__getStyleState = function() {
+    var i,
+      styleState = {},
+      keys = Object.keys(STYLES),
+      key;
+    for (i = 0; i < keys.length; i++) {
+      key = keys[i];
+      styleState[key] = this[key];
+    }
+    return styleState;
+  };
+
+  /**
+   * Apples the current styles to the current SVG element. On "ctx.fill" or "ctx.stroke"
+   * @param type
+   * @private
+   */
+  ctx.prototype.__applyStyleToCurrentElement = function(type) {
+    var currentElement = this.__currentElement;
+    var currentStyleGroup = this.__currentElementsToStyle;
+    if (currentStyleGroup) {
+      currentElement.setAttribute(type, '');
+      currentElement = currentStyleGroup.element;
+      currentStyleGroup.children.forEach(function(node) {
+        node.setAttribute(type, '');
+      });
+    }
+
+    var keys = Object.keys(STYLES),
+      i,
+      style,
+      value,
+      id,
+      regex,
+      matches;
+    for (i = 0; i < keys.length; i++) {
+      style = STYLES[keys[i]];
+      value = this[keys[i]];
+      if (style.apply) {
+        //is this a gradient or pattern?
+        if (value instanceof CanvasPattern) {
+          //pattern
+          if (value.__ctx) {
+            //copy over defs
+            while (value.__ctx.__defs.childNodes.length) {
+              id = value.__ctx.__defs.childNodes[0].getAttribute('id');
+              this.__ids[id] = id;
+              this.__defs.appendChild(value.__ctx.__defs.childNodes[0]);
+            }
+          }
+          currentElement.setAttribute(
+            style.apply,
+            format('url(#{id})', { id: value.__root.getAttribute('id') })
+          );
+        } else if (value instanceof CanvasGradient) {
+          //gradient
+          currentElement.setAttribute(
+            style.apply,
+            format('url(#{id})', { id: value.__root.getAttribute('id') })
+          );
+        } else if (style.apply.indexOf(type) !== -1 && style.svg !== value) {
+          if (
+            (style.svgAttr === 'stroke' || style.svgAttr === 'fill') &&
+            value &&
+            value.indexOf('rgba') !== -1
+          ) {
+            //separate alpha value, since illustrator can't handle it
+            regex = /rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d?\.?\d*)\s*\)/gi;
+            matches = regex.exec(value);
+            currentElement.setAttribute(
+              style.svgAttr,
+              format('rgb({r},{g},{b})', {
+                r: matches[1],
+                g: matches[2],
+                b: matches[3]
+              })
+            );
+            //should take globalAlpha here
+            var opacity = matches[4];
+            var globalAlpha = this.globalAlpha;
+            if (globalAlpha != null) {
+              opacity *= globalAlpha;
+            }
+            currentElement.setAttribute(style.svgAttr + '-opacity', opacity);
+          } else {
+            var attr = style.svgAttr;
+            if (keys[i] === 'globalAlpha') {
+              attr = type + '-' + style.svgAttr;
+              if (currentElement.getAttribute(attr)) {
+                //fill-opacity or stroke-opacity has already been set by stroke or fill.
+                continue;
+              }
+            }
+            //otherwise only update attribute if right type, and not svg default
+            currentElement.setAttribute(attr, value);
+          }
+        }
+      }
+    }
+  };
+
+  /**
+   * Will return the closest group or svg node. May return the current element.
+   * @private
+   */
+  ctx.prototype.__closestGroupOrSvg = function(node) {
+    node = node || this.__currentElement;
+    if (node.nodeName === 'g' || node.nodeName === 'svg') {
+      return node;
+    } else {
+      return this.__closestGroupOrSvg(node.parentNode);
+    }
+  };
+
+  /**
+   * Returns the serialized value of the svg so far
+   * @param fixNamedEntities - Standalone SVG doesn't support named entities, which document.createTextNode encodes.
+   *                           If true, we attempt to find all named entities and encode it as a numeric entity.
+   * @return serialized svg
+   */
+  ctx.prototype.getSerializedSvg = function(fixNamedEntities) {
+    var serialized = new XMLSerializer().serializeToString(this.__root),
+      keys,
+      i,
+      key,
+      value,
+      regexp,
+      xmlns;
+
+    //IE search for a duplicate xmnls because they didn't implement setAttributeNS correctly
+    xmlns = /xmlns="http:\/\/www\.w3\.org\/2000\/svg".+xmlns="http:\/\/www\.w3\.org\/2000\/svg/gi;
+    if (xmlns.test(serialized)) {
+      serialized = serialized.replace(
+        'xmlns="http://www.w3.org/2000/svg',
+        'xmlns:xlink="http://www.w3.org/1999/xlink'
+      );
+    }
+
+    if (fixNamedEntities) {
+      keys = Object.keys(namedEntities);
+      //loop over each named entity and replace with the proper equivalent.
+      for (i = 0; i < keys.length; i++) {
+        key = keys[i];
+        value = namedEntities[key];
+        regexp = new RegExp(key, 'gi');
+        if (regexp.test(serialized)) {
+          serialized = serialized.replace(regexp, value);
+        }
+      }
+    }
+
+    return serialized;
+  };
+
+  /**
+   * Returns the root svg
+   * @return
+   */
+  ctx.prototype.getSvg = function() {
+    return this.__root;
+  };
+  /**
+   * Will generate a group tag.
+   */
+  ctx.prototype.save = function() {
+    var group = this.__createElement('g');
+    var parent = this.__closestGroupOrSvg();
+    this.__groupStack.push(parent);
+    parent.appendChild(group);
+    this.__currentElement = group;
+    this.__stack.push(this.__getStyleState());
+  };
+  /**
+   * Sets current element to parent, or just root if already root
+   */
+  ctx.prototype.restore = function() {
+    this.__currentElement = this.__groupStack.pop();
+    this.__currentElementsToStyle = null;
+    //Clearing canvas will make the poped group invalid, currentElement is set to the root group node.
+    if (!this.__currentElement) {
+      this.__currentElement = this.__root.childNodes[1];
+    }
+    var state = this.__stack.pop();
+    this.__applyStyleState(state);
+  };
+
+  /**
+   * Helper method to add transform
+   * @private
+   */
+  ctx.prototype.__addTransform = function(t) {
+    //if the current element has siblings, add another group
+    var parent = this.__closestGroupOrSvg();
+    if (parent.childNodes.length > 0) {
+      if (this.__currentElement.nodeName === 'path') {
+        if (!this.__currentElementsToStyle)
+          this.__currentElementsToStyle = { element: parent, children: [] };
+        this.__currentElementsToStyle.children.push(this.__currentElement);
+        this.__applyCurrentDefaultPath();
+      }
+
+      var group = this.__createElement('g');
+      parent.appendChild(group);
+      this.__currentElement = group;
+    }
+
+    var transform = this.__currentElement.getAttribute('transform');
+    if (transform) {
+      transform += ' ';
+    } else {
+      transform = '';
+    }
+    transform += t;
+    this.__currentElement.setAttribute('transform', transform);
+  };
+
+  /**
+   *  scales the current element
+   */
+  ctx.prototype.scale = function(x, y) {
+    if (y === undefined) {
+      y = x;
+    }
+    this.__addTransform(format('scale({x},{y})', { x: x, y: y }));
+  };
+
+  /**
+   * rotates the current element
+   */
+  ctx.prototype.rotate = function(angle) {
+    var degrees = (angle * 180) / Math.PI;
+    this.__addTransform(
+      format('rotate({angle},{cx},{cy})', { angle: degrees, cx: 0, cy: 0 })
+    );
+  };
+
+  /**
+   * translates the current element
+   */
+  ctx.prototype.translate = function(x, y) {
+    this.__addTransform(format('translate({x},{y})', { x: x, y: y }));
+  };
+
+  /**
+   * applies a transform to the current element
+   */
+  ctx.prototype.transform = function(a, b, c, d, e, f) {
+    this.__addTransform(
+      format('matrix({a},{b},{c},{d},{e},{f})', {
+        a: a,
+        b: b,
+        c: c,
+        d: d,
+        e: e,
+        f: f
+      })
+    );
+  };
+
+  /**
+   * Create a new Path Element
+   */
+  ctx.prototype.beginPath = function() {
+    var path, parent;
+
+    // Note that there is only one current default path, it is not part of the drawing state.
+    // See also: https://html.spec.whatwg.org/multipage/scripting.html#current-default-path
+    this.__currentDefaultPath = '';
+    this.__currentPosition = {};
+
+    path = this.__createElement('path', {}, true);
+    parent = this.__closestGroupOrSvg();
+    parent.appendChild(path);
+    this.__currentElement = path;
+  };
+
+  /**
+   * Helper function to apply currentDefaultPath to current path element
+   * @private
+   */
+  ctx.prototype.__applyCurrentDefaultPath = function() {
+    var currentElement = this.__currentElement;
+    if (currentElement.nodeName === 'path') {
+      currentElement.setAttribute('d', this.__currentDefaultPath);
+    } else {
+      console.error(
+        'Attempted to apply path command to node',
+        currentElement.nodeName
+      );
+    }
+  };
+
+  ctx.prototype.svgPath = function(text) {
+    this.__addPathCommand(text);
+  };
+
+  /**
+   * Helper function to add path command
+   * @private
+   */
+  ctx.prototype.__addPathCommand = function(command) {
+    this.__currentDefaultPath += ' ';
+    this.__currentDefaultPath += command;
+  };
+
+  /**
+   * Adds the move command to the current path element,
+   * if the currentPathElement is not empty create a new path element
+   */
+  ctx.prototype.moveTo = function(x, y) {
+    if (this.__currentElement.nodeName !== 'path') {
+      this.beginPath();
+    }
+
+    // creates a new subpath with the given point
+    this.__currentPosition = { x: x, y: y };
+    this.__addPathCommand(format('M {x} {y}', { x: x, y: y }));
+  };
+
+  /**
+   * Closes the current path
+   */
+  ctx.prototype.closePath = function() {
+    if (this.__currentDefaultPath) {
+      this.__addPathCommand('Z');
+    }
+  };
+
+  /**
+   * Adds a line to command
+   */
+  ctx.prototype.lineTo = function(x, y) {
+    this.__currentPosition = { x: x, y: y };
+    if (this.__currentDefaultPath.indexOf('M') > -1) {
+      this.__addPathCommand(format('L {x} {y}', { x: x, y: y }));
+    } else {
+      this.__addPathCommand(format('M {x} {y}', { x: x, y: y }));
+    }
+  };
+
+  /**
+   * Add a bezier command
+   */
+  ctx.prototype.bezierCurveTo = function(cp1x, cp1y, cp2x, cp2y, x, y) {
+    this.__currentPosition = { x: x, y: y };
+    this.__addPathCommand(
+      format('C {cp1x} {cp1y} {cp2x} {cp2y} {x} {y}', {
+        cp1x: cp1x,
+        cp1y: cp1y,
+        cp2x: cp2x,
+        cp2y: cp2y,
+        x: x,
+        y: y
+      })
+    );
+  };
+
+  /**
+   * Adds a quadratic curve to command
+   */
+  ctx.prototype.quadraticCurveTo = function(cpx, cpy, x, y) {
+    this.__currentPosition = { x: x, y: y };
+    this.__addPathCommand(
+      format('Q {cpx} {cpy} {x} {y}', { cpx: cpx, cpy: cpy, x: x, y: y })
+    );
+  };
+
+  /**
+   * Return a new normalized vector of given vector
+   */
+  var normalize = function(vector) {
+    var len = Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1]);
+    return [vector[0] / len, vector[1] / len];
+  };
+
+  /**
+   * Adds the arcTo to the current path
+   *
+   * @see http://www.w3.org/TR/2015/WD-2dcontext-20150514/#dom-context-2d-arcto
+   */
+  ctx.prototype.arcTo = function(x1, y1, x2, y2, radius) {
+    // Let the point (x0, y0) be the last point in the subpath.
+    var x0 = this.__currentPosition && this.__currentPosition.x;
+    var y0 = this.__currentPosition && this.__currentPosition.y;
+
+    // First ensure there is a subpath for (x1, y1).
+    if (typeof x0 == 'undefined' || typeof y0 == 'undefined') {
+      return;
+    }
+
+    // Negative values for radius must cause the implementation to throw an IndexSizeError exception.
+    if (radius < 0) {
+      throw new Error(
+        'IndexSizeError: The radius provided (' + radius + ') is negative.'
+      );
+    }
+
+    // If the point (x0, y0) is equal to the point (x1, y1),
+    // or if the point (x1, y1) is equal to the point (x2, y2),
+    // or if the radius radius is zero,
+    // then the method must add the point (x1, y1) to the subpath,
+    // and connect that point to the previous point (x0, y0) by a straight line.
+    if ((x0 === x1 && y0 === y1) || (x1 === x2 && y1 === y2) || radius === 0) {
+      this.lineTo(x1, y1);
+      return;
+    }
+
+    // Otherwise, if the points (x0, y0), (x1, y1), and (x2, y2) all lie on a single straight line,
+    // then the method must add the point (x1, y1) to the subpath,
+    // and connect that point to the previous point (x0, y0) by a straight line.
+    var unit_vec_p1_p0 = normalize([x0 - x1, y0 - y1]);
+    var unit_vec_p1_p2 = normalize([x2 - x1, y2 - y1]);
+    if (
+      unit_vec_p1_p0[0] * unit_vec_p1_p2[1] ===
+      unit_vec_p1_p0[1] * unit_vec_p1_p2[0]
+    ) {
+      this.lineTo(x1, y1);
+      return;
+    }
+
+    // Otherwise, let The Arc be the shortest arc given by circumference of the circle that has radius radius,
+    // and that has one point tangent to the half-infinite line that crosses the point (x0, y0) and ends at the point (x1, y1),
+    // and that has a different point tangent to the half-infinite line that ends at the point (x1, y1), and crosses the point (x2, y2).
+    // The points at which this circle touches these two lines are called the start and end tangent points respectively.
+
+    // note that both vectors are unit vectors, so the length is 1
+    var cos =
+      unit_vec_p1_p0[0] * unit_vec_p1_p2[0] +
+      unit_vec_p1_p0[1] * unit_vec_p1_p2[1];
+    var theta = Math.acos(Math.abs(cos));
+
+    // Calculate origin
+    var unit_vec_p1_origin = normalize([
+      unit_vec_p1_p0[0] + unit_vec_p1_p2[0],
+      unit_vec_p1_p0[1] + unit_vec_p1_p2[1]
+    ]);
+    var len_p1_origin = radius / Math.sin(theta / 2);
+    var x = x1 + len_p1_origin * unit_vec_p1_origin[0];
+    var y = y1 + len_p1_origin * unit_vec_p1_origin[1];
+
+    // Calculate start angle and end angle
+    // rotate 90deg clockwise (note that y axis points to its down)
+    var unit_vec_origin_start_tangent = [-unit_vec_p1_p0[1], unit_vec_p1_p0[0]];
+    // rotate 90deg counter clockwise (note that y axis points to its down)
+    var unit_vec_origin_end_tangent = [unit_vec_p1_p2[1], -unit_vec_p1_p2[0]];
+    var getAngle = function(vector) {
+      // get angle (clockwise) between vector and (1, 0)
+      var x = vector[0];
+      var y = vector[1];
+      if (y >= 0) {
+        // note that y axis points to its down
+        return Math.acos(x);
+      } else {
+        return -Math.acos(x);
+      }
+    };
+    var startAngle = getAngle(unit_vec_origin_start_tangent);
+    var endAngle = getAngle(unit_vec_origin_end_tangent);
+
+    // Connect the point (x0, y0) to the start tangent point by a straight line
+    this.lineTo(
+      x + unit_vec_origin_start_tangent[0] * radius,
+      y + unit_vec_origin_start_tangent[1] * radius
+    );
+
+    // Connect the start tangent point to the end tangent point by arc
+    // and adding the end tangent point to the subpath.
+    this.arc(x, y, radius, startAngle, endAngle);
+  };
+
+  /**
+   * Sets the stroke property on the current element
+   */
+  ctx.prototype.stroke = function() {
+    if (this.__currentElement.nodeName === 'path') {
+      this.__currentElement.setAttribute('paint-order', 'fill stroke markers');
+    }
+    this.__applyCurrentDefaultPath();
+    this.__applyStyleToCurrentElement('stroke');
+  };
+
+  /**
+   * Sets fill properties on the current element
+   */
+  ctx.prototype.fill = function() {
+    if (this.__currentElement.nodeName === 'path') {
+      this.__currentElement.setAttribute('paint-order', 'stroke fill markers');
+    }
+    this.__applyCurrentDefaultPath();
+    this.__applyStyleToCurrentElement('fill');
+  };
+
+  /**
+   *  Adds a rectangle to the path.
+   */
+  ctx.prototype.rect = function(x, y, width, height) {
+    if (this.__currentElement.nodeName !== 'path') {
+      this.beginPath();
+    }
+    this.moveTo(x, y);
+    this.lineTo(x + width, y);
+    this.lineTo(x + width, y + height);
+    this.lineTo(x, y + height);
+    this.lineTo(x, y);
+    this.closePath();
+  };
+
+  /**
+   * adds a rectangle element
+   */
+  ctx.prototype.fillRect = function(x, y, width, height) {
+    var rect, parent;
+    rect = this.__createElement(
+      'rect',
+      {
+        x: x,
+        y: y,
+        width: width,
+        height: height
+      },
+      true
+    );
+    parent = this.__closestGroupOrSvg();
+    parent.appendChild(rect);
+    this.__currentElement = rect;
+    this.__applyStyleToCurrentElement('fill');
+  };
+
+  /**
+   * Draws a rectangle with no fill
+   * @param x
+   * @param y
+   * @param width
+   * @param height
+   */
+  ctx.prototype.strokeRect = function(x, y, width, height) {
+    var rect, parent;
+    rect = this.__createElement(
+      'rect',
+      {
+        x: x,
+        y: y,
+        width: width,
+        height: height
+      },
+      true
+    );
+    parent = this.__closestGroupOrSvg();
+    parent.appendChild(rect);
+    this.__currentElement = rect;
+    this.__applyStyleToCurrentElement('stroke');
+  };
+
+  /**
+   * Clear entire canvas:
+   * 1. save current transforms
+   * 2. remove all the childNodes of the root g element
+   */
+  ctx.prototype.__clearCanvas = function() {
+    var current = this.__closestGroupOrSvg(),
+      transform = current.getAttribute('transform');
+    var rootGroup = this.__root.childNodes[1];
+    var childNodes = rootGroup.childNodes;
+    for (var i = childNodes.length - 1; i >= 0; i--) {
+      if (childNodes[i]) {
+        rootGroup.removeChild(childNodes[i]);
+      }
+    }
+    this.__currentElement = rootGroup;
+    //reset __groupStack as all the child group nodes are all removed.
+    this.__groupStack = [];
+    if (transform) {
+      this.__addTransform(transform);
+    }
+  };
+
+  /**
+   * "Clears" a canvas by just drawing a white rectangle in the current group.
+   */
+  ctx.prototype.clearRect = function(x, y, width, height) {
+    //clear entire canvas
+    if (x === 0 && y === 0 && width === this.width && height === this.height) {
+      this.__clearCanvas();
+      return;
+    }
+    var rect,
+      parent = this.__closestGroupOrSvg();
+    rect = this.__createElement(
+      'rect',
+      {
+        x: x,
+        y: y,
+        width: width,
+        height: height,
+        fill: '#FFFFFF'
+      },
+      true
+    );
+    parent.appendChild(rect);
+  };
+
+  /**
+   * Adds a linear gradient to a defs tag.
+   * Returns a canvas gradient object that has a reference to it's parent def
+   */
+  ctx.prototype.createLinearGradient = function(x1, y1, x2, y2) {
+    var grad = this.__createElement(
+      'linearGradient',
+      {
+        id: randomString(this.__ids),
+        x1: x1 + 'px',
+        x2: x2 + 'px',
+        y1: y1 + 'px',
+        y2: y2 + 'px',
+        gradientUnits: 'userSpaceOnUse'
+      },
+      false
+    );
+    this.__defs.appendChild(grad);
+    return new CanvasGradient(grad, this);
+  };
+
+  /**
+   * Adds a radial gradient to a defs tag.
+   * Returns a canvas gradient object that has a reference to it's parent def
+   */
+  ctx.prototype.createRadialGradient = function(x0, y0, r0, x1, y1, r1) {
+    var grad = this.__createElement(
+      'radialGradient',
+      {
+        id: randomString(this.__ids),
+        cx: x1 + 'px',
+        cy: y1 + 'px',
+        r: r1 + 'px',
+        fx: x0 + 'px',
+        fy: y0 + 'px',
+        gradientUnits: 'userSpaceOnUse'
+      },
+      false
+    );
+    this.__defs.appendChild(grad);
+    return new CanvasGradient(grad, this);
+  };
+
+  /**
+   * Parses the font string and returns svg mapping
+   * @private
+   */
+  ctx.prototype.__parseFont = function() {
+    var fontPart = this.font.split(' ') || [];
+    if (fontPart[3] && fontPart[3].indexOf('/') > -1) {
+      fontPart[3] = fontPart[3].split('/')[0];
+    }
+    var family = '';
+    for (var i = 4; i < fontPart.length; ++i) {
+      family += fontPart[i] + ' ';
+    }
+
+    if (fontPart.length === 2) {
+      family = fontPart[1];
+      fontPart = ['normal', 'normal', 'normal', fontPart[0]];
+    }
+
+    var data = {
+      style: fontPart[0] || 'normal',
+      size: fontPart[3] || '12px',
+      family: family || 'Arial',
+      weight: fontPart[2] || 'normal',
+      decoration: fontPart[1] || 'normal',
+      href: null
+    };
+
+    //canvas doesn't support underline natively, but we can pass this attribute
+    if (this.__fontUnderline === 'underline') {
+      data.decoration = 'underline';
+    }
+
+    //canvas also doesn't support linking, but we can pass this as well
+    if (this.__fontHref) {
+      data.href = this.__fontHref;
+    }
+
+    return data;
+  };
+
+  /**
+   * Helper to link text fragments
+   * @param font
+   * @param element
+   * @return {*}
+   * @private
+   */
+  ctx.prototype.__wrapTextLink = function(font, element) {
+    if (font.href) {
+      var a = this.__createElement('a');
+      a.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', font.href);
+      a.appendChild(element);
+      return a;
+    }
+    return element;
+  };
+
+  /**
+   * Fills or strokes text
+   * @param text
+   * @param x
+   * @param y
+   * @param action - stroke or fill
+   * @private
+   */
+  ctx.prototype.__applyText = function(text, x, y, action) {
+    var font = this.__parseFont(),
+      parent = this.__closestGroupOrSvg(),
+      textElement = this.__createElement(
+        'text',
+        {
+          'font-family': font.family,
+          'font-size': font.size,
+          'font-style': font.style,
+          'font-weight': font.weight,
+          'text-decoration': font.decoration,
+          x: x,
+          y: y,
+          'text-anchor': getTextAnchor(this.textAlign),
+          'dominant-baseline': getDominantBaseline(this.textBaseline)
+        },
+        true
+      );
+
+    if (font.family === 'topology') {
+      text = '--le5le--' + (+text.charCodeAt()).toString(16) + ';';
+    }
+    textElement.appendChild(this.__document.createTextNode(text));
+    this.__currentElement = textElement;
+    this.__applyStyleToCurrentElement(action);
+    parent.appendChild(this.__wrapTextLink(font, textElement));
+  };
+
+  /**
+   * Creates a text element
+   * @param text
+   * @param x
+   * @param y
+   */
+  ctx.prototype.fillText = function(text, x, y) {
+    this.__applyText(text, x, y, 'fill');
+  };
+
+  /**
+   * Strokes text
+   * @param text
+   * @param x
+   * @param y
+   */
+  ctx.prototype.strokeText = function(text, x, y) {
+    this.__applyText(text, x, y, 'stroke');
+  };
+
+  /**
+   * No need to implement this for svg.
+   * @param text
+   * @return {TextMetrics}
+   */
+  ctx.prototype.measureText = function(text) {
+    this.__ctx.font = this.font;
+    return this.__ctx.measureText(text);
+  };
+
+  /**
+   *  Arc command!
+   */
+  ctx.prototype.arc = function(
+    x,
+    y,
+    radius,
+    startAngle,
+    endAngle,
+    counterClockwise
+  ) {
+    // in canvas no circle is drawn if no angle is provided.
+    if (startAngle === endAngle) {
+      return;
+    }
+
+    if (this.__currentElement.nodeName !== 'path') {
+      this.beginPath();
+    }
+
+    startAngle = startAngle % (2 * Math.PI);
+    endAngle = endAngle % (2 * Math.PI);
+    if (startAngle === endAngle) {
+      //circle time! subtract some of the angle so svg is happy (svg elliptical arc can't draw a full circle)
+      endAngle =
+        (endAngle + 2 * Math.PI - 0.001 * (counterClockwise ? -1 : 1)) %
+        (2 * Math.PI);
+    }
+    var endX = x + radius * Math.cos(endAngle),
+      endY = y + radius * Math.sin(endAngle),
+      startX = x + radius * Math.cos(startAngle),
+      startY = y + radius * Math.sin(startAngle),
+      sweepFlag = counterClockwise ? 0 : 1,
+      largeArcFlag = 0,
+      diff = endAngle - startAngle;
+
+    // https://github.com/gliffy/canvas2svg/issues/4
+    if (diff < 0) {
+      diff += 2 * Math.PI;
+    }
+
+    if (counterClockwise) {
+      largeArcFlag = diff > Math.PI ? 0 : 1;
+    } else {
+      largeArcFlag = diff > Math.PI ? 1 : 0;
+    }
+
+    this.lineTo(startX, startY);
+    this.__addPathCommand(
+      format(
+        'A {rx} {ry} {xAxisRotation} {largeArcFlag} {sweepFlag} {endX} {endY}',
+        {
+          rx: radius,
+          ry: radius,
+          xAxisRotation: 0,
+          largeArcFlag: largeArcFlag,
+          sweepFlag: sweepFlag,
+          endX: endX,
+          endY: endY
+        }
+      )
+    );
+
+    this.__currentPosition = { x: endX, y: endY };
+  };
+
+  /**
+   * Generates a ClipPath from the clip command.
+   */
+  ctx.prototype.clip = function() {
+    var group = this.__closestGroupOrSvg(),
+      clipPath = this.__createElement('clipPath'),
+      id = randomString(this.__ids),
+      newGroup = this.__createElement('g');
+
+    this.__applyCurrentDefaultPath();
+    group.removeChild(this.__currentElement);
+    clipPath.setAttribute('id', id);
+    clipPath.appendChild(this.__currentElement);
+
+    this.__defs.appendChild(clipPath);
+
+    //set the clip path to this group
+    group.setAttribute('clip-path', format('url(#{id})', { id: id }));
+
+    //clip paths can be scaled and transformed, we need to add another wrapper group to avoid later transformations
+    // to this path
+    group.appendChild(newGroup);
+
+    this.__currentElement = newGroup;
+  };
+
+  /**
+   * Draws a canvas, image or mock context to this canvas.
+   * Note that all svg dom manipulation uses node.childNodes rather than node.children for IE support.
+   * http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-drawimage
+   */
+  ctx.prototype.drawImage = function() {
+    //convert arguments to a real array
+    var args = Array.prototype.slice.call(arguments),
+      image = args[0],
+      dx,
+      dy,
+      dw,
+      dh,
+      sx = 0,
+      sy = 0,
+      sw,
+      sh,
+      parent,
+      svg,
+      defs,
+      group,
+      currentElement,
+      svgImage,
+      canvas,
+      context,
+      id;
+
+    if (args.length === 3) {
+      dx = args[1];
+      dy = args[2];
+      sw = image.width;
+      sh = image.height;
+      dw = sw;
+      dh = sh;
+    } else if (args.length === 5) {
+      dx = args[1];
+      dy = args[2];
+      dw = args[3];
+      dh = args[4];
+      sw = image.width;
+      sh = image.height;
+    } else if (args.length === 9) {
+      sx = args[1];
+      sy = args[2];
+      sw = args[3];
+      sh = args[4];
+      dx = args[5];
+      dy = args[6];
+      dw = args[7];
+      dh = args[8];
+    } else {
+      throw new Error(
+        'Invalid number of arguments passed to drawImage: ' + arguments.length
+      );
+    }
+
+    parent = this.__closestGroupOrSvg();
+    currentElement = this.__currentElement;
+    var translateDirective = 'translate(' + dx + ', ' + dy + ')';
+    if (image instanceof ctx) {
+      //canvas2svg mock canvas context. In the future we may want to clone nodes instead.
+      //also I'm currently ignoring dw, dh, sw, sh, sx, sy for a mock context.
+      svg = image.getSvg().cloneNode(true);
+      if (svg.childNodes && svg.childNodes.length > 1) {
+        defs = svg.childNodes[0];
+        while (defs.childNodes.length) {
+          id = defs.childNodes[0].getAttribute('id');
+          this.__ids[id] = id;
+          this.__defs.appendChild(defs.childNodes[0]);
+        }
+        group = svg.childNodes[1];
+        if (group) {
+          //save original transform
+          var originTransform = group.getAttribute('transform');
+          var transformDirective;
+          if (originTransform) {
+            transformDirective = originTransform + ' ' + translateDirective;
+          } else {
+            transformDirective = translateDirective;
+          }
+          group.setAttribute('transform', transformDirective);
+          parent.appendChild(group);
+        }
+      }
+    } else if (image.nodeName === 'CANVAS' || image.nodeName === 'IMG') {
+      //canvas or image
+      svgImage = this.__createElement('image');
+      svgImage.setAttribute('width', dw);
+      svgImage.setAttribute('height', dh);
+      svgImage.setAttribute('preserveAspectRatio', 'none');
+
+      if (sx || sy || sw !== image.width || sh !== image.height) {
+        //crop the image using a temporary canvas
+        canvas = this.__document.createElement('canvas');
+        canvas.width = dw;
+        canvas.height = dh;
+        context = canvas.getContext('2d');
+        context.drawImage(image, sx, sy, sw, sh, 0, 0, dw, dh);
+        image = canvas;
+      }
+      svgImage.setAttribute('transform', translateDirective);
+      var imgSrc = image.getAttribute('src');
+      if (imgSrc[0] === '/') {
+        imgSrc = location.protocol + '//' + location.host + imgSrc;
+      }
+      svgImage.setAttributeNS(
+        'http://www.w3.org/1999/xlink',
+        'xlink:href',
+        image.nodeName === 'CANVAS' ? image.toDataURL() : imgSrc
+      );
+      parent.appendChild(svgImage);
+    }
+  };
+
+  /**
+   * Generates a pattern tag
+   */
+  ctx.prototype.createPattern = function(image, repetition) {
+    var pattern = this.__document.createElementNS(
+        'http://www.w3.org/2000/svg',
+        'pattern'
+      ),
+      id = randomString(this.__ids),
+      img;
+    pattern.setAttribute('id', id);
+    pattern.setAttribute('width', image.width);
+    pattern.setAttribute('height', image.height);
+    if (image.nodeName === 'CANVAS' || image.nodeName === 'IMG') {
+      img = this.__document.createElementNS(
+        'http://www.w3.org/2000/svg',
+        'image'
+      );
+      img.setAttribute('width', image.width);
+      img.setAttribute('height', image.height);
+      img.setAttributeNS(
+        'http://www.w3.org/1999/xlink',
+        'xlink:href',
+        image.nodeName === 'CANVAS'
+          ? image.toDataURL()
+          : image.getAttribute('src')
+      );
+      pattern.appendChild(img);
+      this.__defs.appendChild(pattern);
+    } else if (image instanceof ctx) {
+      pattern.appendChild(image.__root.childNodes[1]);
+      this.__defs.appendChild(pattern);
+    }
+    return new CanvasPattern(pattern, this);
+  };
+
+  ctx.prototype.setLineDash = function(dashArray) {
+    if (dashArray && dashArray.length > 0) {
+      this.lineDash = dashArray.join(',');
+    } else {
+      this.lineDash = null;
+    }
+  };
+
+  /*
+   * Ellipse command
+   * @param x
+   * @param y
+   * @param radiusX
+   * @param radiusY
+   * @param startAngle
+   * @param endAngle
+   * @counterClockwise
+   */
+  ctx.prototype.ellipse = function(
+    x,
+    y,
+    radiusX,
+    radiusY,
+    rotation,
+    startAngle,
+    endAngle,
+    counterClockwise
+  ) {
+    //ellipse is the same svg command as arc, but with a radiusX and radiusY instead of just radius
+    if (startAngle === endAngle) {
+      return;
+    }
+
+    if (this.__currentElement.nodeName !== 'path') {
+      this.beginPath();
+    }
+
+    startAngle = startAngle % (2 * Math.PI);
+    endAngle = endAngle % (2 * Math.PI);
+    if (startAngle === endAngle) {
+      endAngle =
+        (endAngle + 2 * Math.PI - 0.001 * (counterClockwise ? -1 : 1)) %
+        (2 * Math.PI);
+    }
+    var endX =
+        x +
+        Math.cos(-rotation) * radiusX * Math.cos(endAngle) +
+        Math.sin(-rotation) * radiusY * Math.sin(endAngle),
+      endY =
+        y -
+        Math.sin(-rotation) * radiusX * Math.cos(endAngle) +
+        Math.cos(-rotation) * radiusY * Math.sin(endAngle),
+      startX =
+        x +
+        Math.cos(-rotation) * radiusX * Math.cos(startAngle) +
+        Math.sin(-rotation) * radiusY * Math.sin(startAngle),
+      startY =
+        y -
+        Math.sin(-rotation) * radiusX * Math.cos(startAngle) +
+        Math.cos(-rotation) * radiusY * Math.sin(startAngle),
+      sweepFlag = counterClockwise ? 0 : 1,
+      largeArcFlag = 0,
+      diff = endAngle - startAngle;
+
+    if (diff < 0) {
+      diff += 2 * Math.PI;
+    }
+
+    if (counterClockwise) {
+      largeArcFlag = diff > Math.PI ? 0 : 1;
+    } else {
+      largeArcFlag = diff > Math.PI ? 1 : 0;
+    }
+
+    this.lineTo(startX, startY);
+    this.__addPathCommand(
+      format(
+        'A {rx} {ry} {xAxisRotation} {largeArcFlag} {sweepFlag} {endX} {endY}',
+        {
+          rx: radiusX,
+          ry: radiusY,
+          xAxisRotation: rotation * (180 / Math.PI),
+          largeArcFlag: largeArcFlag,
+          sweepFlag: sweepFlag,
+          endX: endX,
+          endY: endY
+        }
+      )
+    );
+
+    this.__currentPosition = { x: endX, y: endY };
+  };
+
+  /**
+   * Not yet implemented
+   */
+  ctx.prototype.drawFocusRing = function() {};
+  ctx.prototype.createImageData = function() {};
+  ctx.prototype.getImageData = function() {};
+  ctx.prototype.putImageData = function() {};
+  ctx.prototype.globalCompositeOperation = function() {};
+  ctx.prototype.setTransform = function() {};
+
+  //add options for alternative namespace
+  if (typeof window === 'object') {
+    window.C2S = ctx;
+  }
+
+  // CommonJS/Browserify
+  if (typeof module === 'object' && typeof module.exports === 'object') {
+    module.exports = ctx;
+  }
+})();

+ 2 - 1
src/global.d.ts

@@ -1,7 +1,8 @@
-import { Meta2d } from '@meta2d/core';
+import { Meta2d } from "@meta2d/core";
 
 declare global {
   var meta2d: Meta2d;
+  var C2S: any;
 }
 
 declare interface Window {

+ 53 - 0
src/services/api.ts

@@ -1,2 +1,55 @@
 //所有的接口请求
 import axios from "axios";
+export const cdn = import.meta.env.VITE_ROUTER_BASE
+  ? ""
+  : "https://assets.le5lecdn.com";
+
+export const upCdn = import.meta.env.VITE_ROUTER_BASE
+  ? ""
+  : "https://drive.le5lecdn.com";
+
+export async function delImage(image: string) {
+  if (image.startsWith(upCdn)) {
+    await axios.delete("/file" + image.replace(upCdn, ""));
+  } else {
+    await axios.delete(`${image}`);
+  }
+  return true;
+}
+
+export async function getFolders(query: any) {
+  const folder: any = await axios.post("/data/folders/get", {
+    query,
+  });
+  if (folder.error) {
+    return;
+  } else {
+    return folder;
+  }
+}
+
+export async function updateFolders(data: any) {
+  const folder: any = await axios.post("/data/folders/update", data);
+  if (folder.error) {
+    return;
+  } else {
+    return folder;
+  }
+}
+
+export async function addCollection(collection: string, data: any) {
+  return await axios.post(`/data/${collection}/add`, data); // 新增
+}
+
+export async function updateCollection(collection: string, data: any) {
+  return await axios.post(`/data/${collection}/update`, data); // 新增
+}
+
+// export async function addCollection(collection: string, data: any) {
+//   return await axios.post(`/data/${collection}/add`, data); // 新增
+// }
+export async function getLe5le2d(id: string) {
+  return await axios.post("/data/le5le2d/get", {
+    id,
+  });
+}

+ 28 - 0
src/services/file.ts

@@ -69,3 +69,31 @@ export async function readFile(file: Blob) {
     reader.readAsText(file);
   });
 }
+
+export function dataURLtoBlob(base64: string) {
+  let arr: any = base64.split(","),
+    mime = arr[0].match(/:(.*?);/)[1],
+    bstr = atob(arr[1]),
+    n = bstr.length,
+    u8arr = new Uint8Array(n);
+  while (n--) {
+    u8arr[n] = bstr.charCodeAt(n);
+  }
+  return new Blob([u8arr], { type: mime });
+}
+
+/**
+ * 图片转 Blob
+ * @param img 图片
+ */
+export function saveToBlob(img: HTMLImageElement): Blob {
+  const canvas: HTMLCanvasElement = document.createElement("canvas");
+  canvas.setAttribute("origin-clean", "false");
+  canvas.width = img.width;
+  canvas.height = img.height;
+
+  const context: any = canvas.getContext("2d");
+  context.filter = window.getComputedStyle(img).filter;
+  context.drawImage(img, 0, 0, canvas.width, canvas.height);
+  return dataURLtoBlob(canvas.toDataURL());
+}

+ 31 - 0
src/services/theme.ts

@@ -0,0 +1,31 @@
+export const themes: any = {
+  dark: {
+    "--color-primary": "#4583ff",
+    "--color-primary-hover": "#1677ff",
+    "--color-primary-disabled": "#bae7ff",
+    "--color-background": "#1e2430",
+    "--color-background-active": "#161f2c",
+    "--color-background-hover": "#181f29",
+    "--color-background-input": "#303746",
+    "--color-background-editor": "#0f151f",
+    "--color-background-popup": "#303746",
+  },
+  light: {
+    "--color-primary": "#1890ff",
+    "--color-primary-hover": "#1791ff",
+    "--color-primary-disabled": "#bae7ff",
+    "--color-background": "#fff",
+    "--color-background-active": "#161f2c",
+    "--color-background-hover": "#181f29",
+    "--color-background-input": "#303746",
+    "--color-background-editor": "#0f151f",
+    "--color-background-popup": "#303746",
+  },
+};
+
+export function switchTheme(themeName: string) {
+  let theme = themes[themeName];
+  for (let key in theme) {
+    document.documentElement.style.setProperty(key, theme[key]);
+  }
+}

+ 41 - 0
src/services/utils.ts

@@ -29,6 +29,7 @@ export interface Meta2dBackData extends Meta2dData {
   username?: string;
   editorId?: string;
   editorName?: string;
+  teams?: { id?: string; name?: string }[];
 }
 
 const notification = ref<any>(null);
@@ -127,3 +128,43 @@ export function strictAssign(
   Object.assign(undefinedSource, target);
   Object.assign(source, undefinedSource);
 }
+
+export function checkData(data: Meta2dData) {
+  const pens: Pen[] = data.pens || [];
+  for (let i = 0; i < pens.length; i++) {
+    const pen: any = pens[i];
+    pen.events?.forEach((event: any) => {
+      delete event.setProps;
+    });
+
+    //处理画笔是脏数据的情况
+    if (
+      !(
+        pen.x > -Infinity &&
+        pen.x < Infinity &&
+        pen.y > -Infinity &&
+        pen.y < Infinity &&
+        pen.width > -Infinity &&
+        pen.width < Infinity &&
+        pen.height > -Infinity &&
+        pen.height < Infinity
+      )
+    ) {
+      pens.splice(i, 1);
+      --i;
+    } else if (
+      pen.x == null ||
+      pen.y == null ||
+      pen.width == null ||
+      pen.height == null
+    ) {
+      pens.splice(i, 1);
+      --i;
+    }
+  }
+
+  if (Array.isArray(data.mqttOptions)) {
+    // mqttOptions 是数组则认为是脏数据,删掉
+    data.mqttOptions = {};
+  }
+}

+ 1 - 1
src/views/components/FileProps.vue

@@ -264,7 +264,7 @@ const changeValue = (e: any, key: string) => {
 };
 
 onMounted(() => {
-  initMeta2dCanvas();
+  // initMeta2dCanvas();
   openData();
   meta2d.on('opened', openData);
 });

+ 798 - 86
src/views/components/Header.vue

@@ -22,16 +22,16 @@
           <a>导入文件</a>
         </t-dropdown-item>
         <t-dropdown-item>
-          <a>保存</a>
+          <a @click="save()">保存</a>
         </t-dropdown-item>
         <t-dropdown-item>
-          <a>另保存</a>
+          <a @click="save(SaveType.SaveAs)">另保存</a>
         </t-dropdown-item>
         <t-dropdown-item divider="true">
-          <a>下载JSON文件</a>
+          <a @click="downloadJson">下载JSON文件</a>
         </t-dropdown-item>
         <t-dropdown-item>
-          <a>
+          <a @click="downloadZip">
             <div class="flex">
               导出为ZIP文件 <span class="flex-grow"></span>
               <span><label>VIP</label></span>
@@ -39,7 +39,7 @@
           </a>
         </t-dropdown-item>
         <t-dropdown-item>
-          <a>
+          <a @click="downloadHtml">
             <div class="flex">
               导出为HTML <span class="flex-grow"></span>
               <span><label>VIP</label></span>
@@ -47,7 +47,7 @@
           </a>
         </t-dropdown-item>
         <t-dropdown-item>
-          <a>
+          <a @click="downloadVue3">
             <div class="flex">
               导出为Vue3组件 <span class="flex-grow"></span>
               <span><label>VIP</label></span>
@@ -55,7 +55,7 @@
           </a>
         </t-dropdown-item>
         <t-dropdown-item>
-          <a>
+          <a @click="downloadVue2">
             <div class="flex">
               导出为Vue2组件 <span class="flex-grow"></span>
               <span><label>VIP</label></span>
@@ -63,7 +63,7 @@
           </a>
         </t-dropdown-item>
         <t-dropdown-item divider="true">
-          <a>
+          <a @click="downloadReact">
             <div class="flex">
               导出为React组件 <span class="flex-grow"></span>
               <span><label>VIP</label></span>
@@ -71,10 +71,10 @@
           </a>
         </t-dropdown-item>
         <t-dropdown-item>
-          <a>下载为PNG</a>
+          <a @click="downloadPng">下载为PNG</a>
         </t-dropdown-item>
         <t-dropdown-item>
-          <a>下载为SVG</a>
+          <a @click="downloadSvg">下载为SVG</a>
         </t-dropdown-item>
       </t-dropdown-menu>
     </t-dropdown>
@@ -87,59 +87,59 @@
       <a> 编辑 </a>
       <t-dropdown-menu>
         <t-dropdown-item>
-          <a>
+          <a @click="onUndo">
             <div class="flex">
               撤销 <span class="flex-grow"></span> Ctrl + Z
             </div>
           </a>
         </t-dropdown-item>
         <t-dropdown-item divider="true">
-          <a>
+          <a @click="onRedo">
             <div class="flex">
               恢复 <span class="flex-grow"></span> Ctrl + Y
             </div>
           </a>
         </t-dropdown-item>
         <t-dropdown-item>
-          <a>
+          <a @click="onCut">
             <div class="flex">
               剪切 <span class="flex-grow"></span> Ctrl + X
             </div>
           </a>
         </t-dropdown-item>
         <t-dropdown-item>
-          <a>
+          <a @click="onCopy">
             <div class="flex">
               复制 <span class="flex-grow"></span> Ctrl + C
             </div>
           </a>
         </t-dropdown-item>
         <t-dropdown-item divider="true">
-          <a>
+          <a @click="onPaste">
             <div class="flex">
               粘贴 <span class="flex-grow"></span> Ctrl + V
             </div>
           </a>
         </t-dropdown-item>
         <t-dropdown-item>
-          <a>
+          <a @click="onToggleAnchor">
             <div class="flex">
               添加/删除锚点 <span class="flex-grow"></span> A
             </div>
           </a>
         </t-dropdown-item>
         <t-dropdown-item>
-          <a>
+          <a @click="onAddAnchorHand">
             <div class="flex">添加手柄 <span class="flex-grow"></span> H</div>
           </a>
         </t-dropdown-item>
         <t-dropdown-item>
-          <a>
+          <a @click="onRemoveAnchorHand">
             <div class="flex">删除手柄 <span class="flex-grow"></span> D</div>
           </a>
         </t-dropdown-item>
         <t-dropdown-item>
-          <a>
+          <a @click="onToggleAnchorHand">
             <div class="flex">
               切换手柄 <span class="flex-grow"></span> Shift
             </div>
@@ -156,42 +156,44 @@
       <a> 工具 </a>
       <t-dropdown-menu>
         <t-dropdown-item>
-          <a>窗口大小</a>
+          <a @click="onScaleWindow">窗口大小</a>
         </t-dropdown-item>
         <t-dropdown-item>
-          <a>放大</a>
+          <a @click="onScaleUp">放大</a>
         </t-dropdown-item>
         <t-dropdown-item>
-          <a>缩小</a>
+          <a @click="onScaleDown">缩小</a>
         </t-dropdown-item>
         <t-dropdown-item divider="true">
-          <a>100%视图</a>
+          <a @click="onScaleView">100%视图</a>
         </t-dropdown-item>
         <t-dropdown-item>
-          <a>鹰眼地图</a>
+          <a @click="showMap">鹰眼地图</a>
         </t-dropdown-item>
         <t-dropdown-item divider="true">
-          <a>放大镜</a>
+          <a @click="showMagnifier">放大镜</a>
         </t-dropdown-item>
         <t-dropdown-item>
-          <a>
+          <a @click="onAutoAnchor">
             <div class="flex middle">
-              自动锚点 <span class="flex-grow"></span> <t-icon name="check" />
+              自动锚点 <span class="flex-grow"></span>
+              <t-icon v-show="autoAnchor" name="check" />
             </div>
           </a>
         </t-dropdown-item>
         <t-dropdown-item divider="true">
-          <a>
+          <a @click="onDisableAnchor">
             <div class="flex middle">
-              禁用锚点 <span class="flex-grow"></span> <t-icon name="check" />
+              禁用锚点 <span class="flex-grow"></span>
+              <t-icon v-show="disableAnchor" name="check" />
             </div>
           </a>
         </t-dropdown-item>
         <t-dropdown-item>
-          <a>明亮主题</a>
+          <a @click="switchTheme('light')">明亮主题</a>
         </t-dropdown-item>
         <t-dropdown-item>
-          <a>暗黑主题</a>
+          <a @click="switchTheme('dark')">暗黑主题</a>
         </t-dropdown-item>
       </t-dropdown-menu>
     </t-dropdown>
@@ -203,27 +205,12 @@
     >
       <a> 帮助 </a>
       <t-dropdown-menu>
-        <t-dropdown-item>
-          <a>产品介绍</a>
-        </t-dropdown-item>
-        <t-dropdown-item>
-          <a>快速上手</a>
-        </t-dropdown-item>
-        <t-dropdown-item>
-          <a>使用手册</a>
-        </t-dropdown-item>
-        <t-dropdown-item divider="true">
-          <a>快捷键</a>
-        </t-dropdown-item>
-        <t-dropdown-item divider="true">
-          <a>企业服务与支持</a>
-        </t-dropdown-item>
-        <t-dropdown-item>
-          <a>关于我们</a>
+        <t-dropdown-item v-for="item in helpList" :divider="item.divider">
+          <a :href="item.url" :target="item.target">{{ item.name }}</a>
         </t-dropdown-item>
       </t-dropdown-menu>
     </t-dropdown>
-    <input v-model="data.name" />
+    <input v-model="data.name" @input="inputMeta2dName" />
 
     <div style="width: 290px; flex-shrink: 0"></div>
     <t-dropdown
@@ -271,31 +258,63 @@
 </template>
 
 <script lang="ts" setup>
-import { reactive, ref } from 'vue';
-import { useRouter } from 'vue-router';
-import { useUser } from '@/services/user';
-import { NotifyPlugin, MessagePlugin } from 'tdesign-vue-next';
+import { reactive, ref, onMounted, onUnmounted, nextTick } from "vue";
+import { useRouter, useRoute } from "vue-router";
+import { useUser } from "@/services/user";
+import { NotifyPlugin, MessagePlugin } from "tdesign-vue-next";
 import {
   showNotification,
   Meta2dBackData,
   dealwithFormatbeforeOpen,
   gotoAccount,
-} from '@/services/utils';
-import { readFile, upload } from '@/services/file';
-import { compareVersion, baseVer, upgrade } from '@/services/upgrade';
-import { parseSvg } from '@meta2d/svg';
-import { Pen } from '@meta2d/core';
+  checkData,
+} from "@/services/utils";
+import { readFile, upload, dataURLtoBlob } from "@/services/file";
+import { compareVersion, baseVer, upgrade } from "@/services/upgrade";
+import { parseSvg } from "@meta2d/svg";
+import { Pen, Rect, getGlobalColor, isShowChild } from "@meta2d/core";
+import localforage from "localforage";
+import {
+  delImage,
+  getFolders,
+  addCollection,
+  updateCollection,
+  updateFolders,
+  cdn,
+  upCdn,
+} from "@/services/api";
+import JSZip from "jszip";
+import axios from "axios";
+import { switchTheme } from "@/services/theme";
 
 const router = useRouter();
+const route = useRoute();
+
 const market = import.meta.env.VITE_MARKET;
 
-const baseUrl = import.meta.env.BASE_URL || '/';
+const baseUrl = import.meta.env.BASE_URL || "/";
 
 const { user, message, getUser, getMessage, signout } = useUser();
 
 const isNew = ref(false);
 const data = reactive({
-  name: '空白文件',
+  name: "空白文件",
+});
+
+const inputMeta2dName = (value) => {
+  meta2d.store.data.name = data.name;
+};
+
+const initMeta2dName = () => {
+  data.name = meta2d.store.data.name;
+};
+
+nextTick(() => {
+  meta2d.on("opened", initMeta2dName);
+});
+
+onUnmounted(() => {
+  meta2d.off("opened", initMeta2dName);
 });
 
 function login() {
@@ -313,7 +332,7 @@ function login() {
   //     }
 }
 
-const title = '系统可能不会保存您所做的更改,是否继续?';
+const title = "系统可能不会保存您所做的更改,是否继续?";
 const newFile = async () => {
   if (isNew.value) {
     if (await showNotification(title)) {
@@ -353,7 +372,7 @@ const drawingPencil = () => {
     } else {
       meta2d.stopPencil();
     }
-    pencil.value = meta2d.canvas.pencil;
+    pencil.value = meta2d.canvas.pencil || false;
   } catch (e: any) {
     MessagePlugin.warning(e.message);
   }
@@ -389,30 +408,30 @@ async function newfile(noRouter: boolean = false) {
   // 打开文件操作不跳转
   !noRouter &&
     router.replace({
-      path: '/',
-      query: { r: Date.now() + '' },
+      path: "/",
+      query: { r: Date.now() + "" },
     });
 }
 
 function load(newT: boolean = false) {
-  const input = document.createElement('input');
-  input.type = 'file';
+  const input = document.createElement("input");
+  input.type = "file";
   input.onchange = (event) => {
     const elem = event.target as HTMLInputElement;
     if (elem.files && elem.files[0]) {
       newT && newfile(true);
       // 路由跳转 可能在 openFile 后执行
-      if (elem.files[0].name.endsWith('.json')) {
+      if (elem.files[0].name.endsWith(".json")) {
         openJson(elem.files[0], newT);
-      } else if (elem.files[0].name.endsWith('.svg')) {
+      } else if (elem.files[0].name.endsWith(".svg")) {
         MessagePlugin.info(
-          '可二次编辑但转换存在损失,若作为图片使用,请使用右侧属性面板的上传图片功能'
+          "可二次编辑但转换存在损失,若作为图片使用,请使用右侧属性面板的上传图片功能"
         );
         openSvg(elem.files[0]);
-      } else if (elem.files[0].name.endsWith('.zip')) {
+      } else if (elem.files[0].name.endsWith(".zip")) {
         openZip(elem.files[0], newT);
       } else {
-        MessagePlugin.info('打开文件只支持 json,svg,zip 格式');
+        MessagePlugin.info("打开文件只支持 json,svg,zip 格式");
       }
     }
   };
@@ -424,7 +443,7 @@ const openJson = async (file: File, isNew: boolean = false) => {
   try {
     let data: Meta2dBackData = JSON.parse(text);
     if (!data.name) {
-      data.name = file.name.replace('.json', '');
+      data.name = file.name.replace(".json", "");
     }
     if (!data.version || compareVersion(data.version, baseVer) === -1) {
       // 如果版本号不存在或者版本号 version < 1.0.0
@@ -452,12 +471,12 @@ const openSvg = async (file: File) => {
   const text = await readFile(file);
   const pens: Pen[] = parseSvg(text);
   meta2d.canvas.addCaches = pens;
-  MessagePlugin.info('svg转换成功,请点击画布决定放置位置');
+  MessagePlugin.info("svg转换成功,请点击画布决定放置位置");
 };
 
 const openZip = async (file: File, isNew: boolean = false) => {
   if (!(user && user.username)) {
-    MessagePlugin.warning('请先登录,否则无法保存!');
+    MessagePlugin.warning(noLoginTip);
     return;
   }
 
@@ -467,16 +486,16 @@ const openZip = async (file: File, isNew: boolean = false) => {
     return;
   }
 
-  const { default: JSZip } = await import('jszip');
+  const { default: JSZip } = await import("jszip");
   const zip = new JSZip();
   await zip.loadAsync(file);
 
-  let dataStr = '';
+  let dataStr = "";
   for (const key in zip.files) {
     if (zip.files[key].dir) {
       continue;
     }
-    if (key.endsWith('.json')) {
+    if (key.endsWith(".json")) {
       // 认为只有一个 json 文件
       // dataStr = await zip.file(key).async('string');
       break;
@@ -498,15 +517,15 @@ const openZip = async (file: File, isNew: boolean = false) => {
     let _keyLower = key.toLowerCase();
     // if (!key.endsWith('.json') && (_png !== -1 || _img !== -1 || _image !== -1 || _file !== -1)) {
     if (
-      _keyLower.endsWith('.png') ||
-      _keyLower.endsWith('.svg') ||
-      _keyLower.endsWith('.gif') ||
-      _keyLower.endsWith('.jpg') ||
-      _keyLower.endsWith('.jpeg')
+      _keyLower.endsWith(".png") ||
+      _keyLower.endsWith(".svg") ||
+      _keyLower.endsWith(".gif") ||
+      _keyLower.endsWith(".jpg") ||
+      _keyLower.endsWith(".jpeg")
     ) {
-      let filename = key.substr(key.lastIndexOf('/') + 1);
-      const extPos = filename.lastIndexOf('.');
-      let ext = '';
+      let filename = key.substr(key.lastIndexOf("/") + 1);
+      const extPos = filename.lastIndexOf(".");
+      let ext = "";
       if (extPos > 0) {
         ext = filename.substr(extPos);
       }
@@ -557,7 +576,7 @@ const openZip = async (file: File, isNew: boolean = false) => {
     let data: Meta2dBackData = JSON.parse(dataStr);
     if (data) {
       if (!data.name) {
-        data.name = file.name.replace('.zip', '');
+        data.name = file.name.replace(".zip", "");
       }
       if (!data.version || compareVersion(data.version, baseVer) === -1) {
         // 如果版本号不存在或者版本号 version < 1.0.0
@@ -595,6 +614,699 @@ async function loadFile(newT: boolean = false) {
 async function openFile() {
   loadFile(true);
 }
+
+enum SaveType {
+  Save,
+  SaveAs,
+}
+
+//本地保存图纸数据 key
+const localMeta2dDataName = "meta2dData";
+const save = async (type: SaveType = SaveType.Save, component?: boolean) => {
+  meta2d.stopAnimate();
+  const data: Meta2dBackData = meta2d.data();
+  if (!(user && user.username)) {
+    MessagePlugin.warning(noLoginTip);
+    localforage.setItem(localMeta2dDataName, JSON.stringify(data));
+    return;
+  }
+  checkData(data);
+  if (!data._id && route.query.id) {
+    data._id = route.query.id as string;
+  }
+
+  if (
+    (globalThis as any).beforeSaveMeta2d &&
+    !(await (globalThis as any).beforeSaveMeta2d(data))
+  ) {
+    return;
+  }
+  if (type === SaveType.SaveAs) {
+    //另存为去掉teams信息
+    delete data.teams;
+  }
+  //如果不是自己创建的团队图纸,就不去修改缩略图(没有权限去删除缩略图)
+  if (!((data as any).teams && data.owner?.id !== user.id)) {
+    let blob: Blob;
+    try {
+      blob = dataURLtoBlob(meta2d.toPng(10) + "");
+    } catch (e) {
+      MessagePlugin.error(
+        "无法下载,宽度不合法,画布可能没有画笔/画布大小超出浏览器最大限制"
+      );
+      return;
+    }
+    if (data._id && type === SaveType.Save) {
+      if (data.image && !(await delImage(data.image))) {
+        return;
+      }
+    }
+
+    const file = await upload(blob, true);
+    if (!file) {
+      return;
+    }
+
+    // 缩略图
+    data.image = file.url;
+    (meta2d.store.data as Meta2dBackData).image = data.image;
+  }
+
+  if (component) {
+    data.component = true;
+    // pens 存储原数据用于二次编辑 ; componentDatas 组合后的数据,用于复用
+    data.componentDatas = meta2d.toComponent(
+      undefined,
+      (meta2d.store.data as Meta2dBackData).showChild,
+      false //自定义组合节点生成默认锚点
+    );
+  } else {
+    data.component = false; // 必要值
+  }
+  let collection = data.component ? "le5le2d-components" : "le5le2d";
+  let ret: any;
+  if (!data.name) {
+    // 文件名称
+    data.name = `meta2d.${new Date().toLocaleString()}`;
+    (meta2d.store.data as Meta2dBackData).name = data.name;
+  }
+  !data.version && (data.version = baseVer);
+
+  let list = undefined;
+  let folder: any = undefined;
+  let folderId = undefined;
+  if (
+    !data.component &&
+    data.folder &&
+    !(data.teams && data.owner?.id !== user.id)
+  ) {
+    //自己的图纸才允许去请求
+    folder = getFolders({
+      type: collection,
+      name: data.folder,
+    });
+    if (folder) {
+      list = folder.list; //团队图纸文件夹
+      folderId = folder._id;
+    }
+  }
+  if (!list) {
+    list = [];
+  }
+
+  if (type === SaveType.SaveAs) {
+    // 另存为一定走 新增 ,由于后端 未控制 userId 等属性,清空一下
+    const delAttrs = [
+      "userId",
+      "id",
+      "shared",
+      "star",
+      "view",
+      "username",
+      "editorName",
+      "editorId",
+      "createdAt",
+      "updatedAt",
+      "recommend",
+    ];
+    for (const k of delAttrs) {
+      delete (data as any)[k];
+    }
+    ret = addCollection(collection, data); // 新增
+    if (!data.component) {
+      list.push({
+        id: ret._id,
+        image: data.image,
+        name: data.name,
+        component: data.component,
+      });
+    }
+  } else {
+    if (data._id && data.teams && data.owner?.id !== user.id) {
+      // 团队图纸 不允许修改文件夹信息
+      delete data.folder;
+      ret = updateCollection(collection, data);
+    } else if (data._id) {
+      ret = updateCollection(collection, data);
+      if (!data.component) {
+        list.forEach((i: any) => {
+          if (i.id === data._id) {
+            i.image = data.image;
+          }
+        });
+      }
+      //TODO 处理老接口图纸情况
+      let one = list.find((item: any) => item.id === data._id);
+      if (!data.component && !one) {
+        list.push({
+          id: ret._id,
+          image: data.image,
+          name: data.name,
+          component: data.component,
+        });
+      }
+    } else {
+      ret = addCollection(collection, data); // 新增
+      if (!data.component) {
+        list.push({
+          id: ret._id,
+          image: data.image,
+          name: data.name,
+          component: data.component,
+        });
+      }
+    }
+  }
+
+  if (ret.error) {
+    return null;
+  } else {
+    if (!data.component && folderId) {
+      const updateRet: any = updateFolders({
+        _id: folderId,
+        list,
+      });
+      if (updateRet.error) {
+        return null;
+      }
+    }
+    // showModelSaveAsPop.value = false;
+  }
+  //  保存图纸之后的钩子函数
+  (window as any).afterSaveMeta2d &&
+    (await (window as any).afterSaveMeta2d(ret));
+  if (
+    !data._id ||
+    data.owner?.id !== user.id ||
+    route.query.version ||
+    type === SaveType.SaveAs // 另存为肯定走新增,也会产生新的 id
+  ) {
+    data._id = ret._id;
+    (meta2d.store.data as Meta2dBackData)._id = data._id;
+    router.replace({
+      path: "/",
+      query: {
+        id: data._id,
+        r: Date.now() + "",
+        component: data.component + "",
+      },
+    });
+  }
+
+  MessagePlugin.success("保存成功!");
+  // 保存成功,重新请求文件夹
+  meta2d.emit("t-save-success", true);
+  // 已保存,不再是新的,无需提示保存
+  // isNew.value = false;
+  localforage.removeItem(localMeta2dDataName);
+};
+
+const downloadJson = () => {
+  const data: Meta2dBackData = meta2d.data();
+  if (data._id) delete data._id;
+  checkData(data);
+  import("file-saver").then(({ saveAs }) => {
+    saveAs(
+      new Blob(
+        [JSON.stringify(data).replaceAll(cdn, "").replaceAll(upCdn, "")],
+        {
+          type: "text/plain;charset=utf-8",
+        }
+      ),
+      `${data.name || "le5le.meta2d"}.json`
+    );
+  });
+};
+
+const noLoginTip = "请先登录,否则无法保存!";
+const downloadZip = async () => {
+  if (!(user && user.username)) {
+    MessagePlugin.warning(noLoginTip);
+    return;
+  }
+
+  if (!user.vipExpired) {
+    gotoAccount();
+    return;
+  }
+
+  MessagePlugin.info("正在下载打包中,可能需要几分钟,请耐心等待...");
+  const [{ default: JSZip }, { saveAs }] = await Promise.all([
+    import("jszip"),
+    import("file-saver"),
+  ]);
+
+  const zip: any = new JSZip();
+  const data: Meta2dBackData = meta2d.data();
+  let _fileName =
+    (data.name && data.name.replace(/\//g, "_").replace(/:/g, "_")) ||
+    "le5le.meta2d";
+  const _zip = zip.folder(`${_fileName}`);
+  if (data._id) delete data._id;
+  checkData(data);
+  _zip.file(
+    `${_fileName}.json`,
+    JSON.stringify(data).replaceAll(cdn, "").replaceAll(upCdn, "")
+  );
+  await zipImages(_zip, meta2d.store.data.pens);
+
+  const blob = await zip.generateAsync({ type: "blob" });
+  saveAs(blob, `${_fileName}.zip`);
+};
+
+const downloadHtml = async () => {
+  if (!(user && user.username)) {
+    MessagePlugin.warning(noLoginTip);
+    return;
+  }
+
+  if (!user.vipExpired) {
+    gotoAccount();
+    return;
+  }
+
+  MessagePlugin.info("正在下载打包中,可能需要几分钟,请耐心等待...");
+
+  const data: Meta2dBackData = meta2d.data();
+  if (data._id) delete data._id;
+  checkData(data);
+  const [{ default: JSZip }, { saveAs }] = await Promise.all([
+    import("jszip"),
+    import("file-saver"),
+  ]);
+  const zip = new JSZip();
+  let _fileName =
+    (data.name && data.name.replace(/\//g, "_").replace(/:/g, "_")) ||
+    "le5le.meta2d";
+
+  //处理cdn图片地址
+  const _zip: any = zip.folder(`${_fileName}`);
+  _zip.file(
+    "data.json",
+    JSON.stringify(data).replaceAll(cdn, "").replaceAll(upCdn, "")
+  );
+  await Promise.all([zipImages(_zip, meta2d.store.data.pens), zipFiles(_zip)]);
+  const blob = await zip.generateAsync({ type: "blob" });
+  saveAs(blob, `${_fileName}.zip`);
+};
+
+enum Frame {
+  vue2,
+  vue3,
+  react,
+}
+
+const downloadVue3 = async () => {
+  downloadAsFrame(Frame.vue3);
+};
+
+const downloadVue2 = async () => {
+  downloadAsFrame(Frame.vue2);
+};
+
+const downloadReact = async () => {
+  downloadAsFrame(Frame.react);
+};
+
+async function downloadAsFrame(type: Frame) {
+  if (!(user && user.username)) {
+    MessagePlugin.warning(noLoginTip);
+    return;
+  }
+
+  if (!user.vipExpired) {
+    gotoAccount();
+    return;
+  }
+
+  MessagePlugin.info("正在下载打包中,可能需要几分钟,请耐心等待...");
+
+  const data: Meta2dBackData = meta2d.data();
+  if (data._id) delete data._id;
+  checkData(data);
+  const [{ default: JSZip }, { saveAs }] = await Promise.all([
+    import("jszip"),
+    import("file-saver"),
+  ]);
+  const zip = new JSZip();
+  let _fileName =
+    (data.name && data.name.replace(/\//g, "_").replace(/:/g, "_")) ||
+    "le5le.meta2d";
+  const _zip: any = zip.folder(`${_fileName}`);
+  _zip.file(
+    "data.json",
+    JSON.stringify(data).replaceAll(cdn, "").replaceAll(upCdn, "")
+  );
+  await Promise.all([
+    zipImages(_zip, meta2d.store.data.pens),
+    type === Frame.vue3
+      ? zipVue3Files(_zip)
+      : type === Frame.vue2
+      ? zipVue2Files(_zip)
+      : zipReactFiles(_zip),
+  ]);
+  const blob = await zip.generateAsync({ type: "blob" });
+  saveAs(blob, `${_fileName}.zip`);
+}
+
+async function zipVue3Files(zip: JSZip) {
+  const files = [
+    "/view/js/marked.min.js",
+    "/view/js/lcjs.iife.js",
+    "/view/vue3/Meta2d.vue",
+    "/view/index.html",
+    "/view/js/meta2d.js",
+    "/view/使用说明.md",
+  ] as const;
+  // 文件同时加载
+  await Promise.all(files.map((filePath) => zipFile(zip, filePath)));
+}
+
+async function zipVue2Files(zip: JSZip) {
+  const files = [
+    "/view/js/marked.min.js",
+    "/view/js/lcjs.iife.js",
+    "/view/vue2/Meta2d.vue",
+    "/view/index.html",
+    "/view/js/meta2d.js",
+    "/view/使用说明.md",
+  ] as const;
+  // 文件同时加载
+  await Promise.all(files.map((filePath) => zipFile(zip, filePath)));
+}
+
+async function zipReactFiles(zip: JSZip) {
+  const files = [
+    "/view/js/marked.min.js",
+    "/view/js/lcjs.iife.js",
+    "/view/react/Meta2d.jsx",
+    "/view/react/Meta2d.css",
+    "/view/index.html",
+    "/view/js/meta2d.js",
+    "/view/使用说明.md",
+  ] as const;
+  // 文件同时加载
+  await Promise.all(files.map((filePath) => zipFile(zip, filePath)));
+}
+
+async function zipFiles(zip: JSZip) {
+  const files = [
+    "/view/js/marked.min.js",
+    "/view/js/lcjs.iife.js",
+    "/view/js/index.js",
+    "/view/js/meta2d.js",
+    "/view/index.html",
+    "/view/index.css",
+    "/view/favicon.ico",
+    "/view/使用说明.pdf",
+  ] as const;
+  // 文件同时加载
+  await Promise.all(files.map((filePath) => zipFile(zip, filePath)));
+}
+
+async function zipFile(zip: JSZip, filePath: string) {
+  const res: Blob = await axios.get(cdn + "/2d" + filePath, {
+    responseType: "blob",
+  });
+  zip.file(filePath.replace("/view", ""), res, { createFolders: true });
+}
+
+/**
+ * 图片放到 zip 里
+ * @param pens 可以是非具有 calculative 的 pen
+ */
+async function zipImages(zip: JSZip, pens: Pen[]) {
+  if (!pens) {
+    return;
+  }
+
+  // 不止 image 上有图片, strokeImage ,backgroundImage 也有图片
+  const imageKeys = [
+    {
+      string: "image",
+    },
+    { string: "strokeImage" },
+    { string: "backgroundImage" },
+  ] as const;
+  const images: string[] = [];
+  for (const pen of pens) {
+    for (const i of imageKeys) {
+      const image = pen[i.string];
+      if (image) {
+        // HTMLImageElement 无法精确控制图片格式
+        if (
+          image.startsWith("/") ||
+          image.startsWith(cdn) ||
+          image.startsWith(upCdn)
+        ) {
+          // 只考虑相对路径下的 image ,绝对路径图片无需下载
+          if (!images.includes(image)) {
+            images.push(image);
+          }
+        }
+      }
+    }
+    // 无需递归遍历子节点,现在所有的节点都在外层
+  }
+  await Promise.all(images.map((image) => zipImage(zip, image)));
+}
+
+async function zipImage(zip: JSZip, image: string) {
+  const res: Blob = await axios.get(image, {
+    responseType: "blob",
+    params: {
+      isZip: true,
+    },
+  });
+  zip.file(cdn ? image.replace(cdn, "").replace(upCdn, "") : image, res, {
+    createFolders: true,
+  });
+}
+
+const downloadImageTips =
+  "无法下载,宽度不合法,画布可能没有画笔/画布大小超出浏览器最大限制";
+
+const downloadPng = () => {
+  const name = (meta2d.store.data as Meta2dBackData).name;
+  try {
+    meta2d.downloadPng(name ? name + ".png" : undefined);
+  } catch (e) {
+    MessagePlugin.warning(downloadImageTips);
+  }
+};
+
+async function getIconDefs(url: string) {
+  let res: any = await axios.get(url);
+  let str = res.match(/@font-face([\s\S]*?)\}/)[1];
+  str = `@font-face ${str} }`;
+  return str;
+}
+
+const downloadSvg = async () => {
+  await import("@/assets/canvas2svg");
+  if (!C2S) {
+    MessagePlugin.error("请先加载乐吾乐官网下的canvas2svg.js");
+    return;
+  }
+
+  const rect: any = meta2d.getRect();
+  if (!isFinite(rect.width)) {
+    MessagePlugin.error(downloadImageTips);
+    return;
+  }
+  rect.x -= 10;
+  rect.y -= 10;
+  const ctx = new C2S(rect.width + 20, rect.height + 20);
+  ctx.textBaseline = "middle";
+  ctx.strokeStyle = getGlobalColor(meta2d.store);
+  for (const pen of meta2d.store.data.pens) {
+    // 不使用 calculative.inView 的原因是,如果 pen 在 view 之外,那么它的 calculative.inView 为 false,但是它的绘制还是需要的
+    if (!isShowChild(pen, meta2d.store) || pen.visible == false) {
+      continue;
+    }
+    meta2d.renderPenRaw(ctx, pen, rect);
+  }
+
+  let mySerializedSVG = ctx.getSerializedSvg();
+  let icon_pens = meta2d.store.data.pens.filter(
+    (item) => item.iconFamily && item.icon
+  );
+  if (icon_pens && icon_pens.length > 0) {
+    let iconList = [
+      "/icon/国家电网/iconfont.css",
+      "/icon/电气工程/iconfont.css",
+      "/icon/通用图标/iconfont.css",
+    ];
+    let defsList: any = await Promise.all(
+      iconList.map((item) => getIconDefs(item))
+    );
+    mySerializedSVG = mySerializedSVG.replace(
+      "<defs/>",
+      `<defs>
+    <style type="text/css">
+${defsList.join("\n")}
+</style>
+{{bk}}
+  </defs>
+{{bkRect}}`
+    );
+  }
+  /*  mySerializedSVG = mySerializedSVG.replace(
+        '<defs/>',
+        `<defs>
+    <style type="text/css">
+  @font-face {
+    font-family: 'ticon';
+    src: url('icon/通用图标/iconfont.ttf') format('truetype');
+  }
+</style>
+{{bk}}
+  </defs>
+{{bkRect}}`
+      );
+*/
+  if (meta2d.store.data.background) {
+    mySerializedSVG = mySerializedSVG.replace("{{bk}}", "");
+    mySerializedSVG = mySerializedSVG.replace(
+      "{{bkRect}}",
+      `<rect x="0" y="0" width="100%" height="100%" fill="${meta2d.store.data.background}"></rect>`
+    );
+  } else {
+    mySerializedSVG = mySerializedSVG.replace("{{bk}}", "");
+    mySerializedSVG = mySerializedSVG.replace("{{bkRect}}", "");
+  }
+
+  mySerializedSVG = mySerializedSVG.replace(/--le5le--/g, "&#x");
+
+  const urlObject: any = (window as any).URL || window;
+  const export_blob = new Blob([mySerializedSVG]);
+  const url = urlObject.createObjectURL(export_blob);
+
+  const a = document.createElement("a");
+  a.setAttribute(
+    "download",
+    `${(meta2d.store.data as Meta2dBackData).name || "le5le.meta2d"}.svg`
+  );
+  a.setAttribute("href", url);
+  const evt = document.createEvent("MouseEvents");
+  evt.initEvent("click", true, true);
+  a.dispatchEvent(evt);
+};
+
+const onUndo = () => {
+  meta2d.undo();
+};
+
+const onRedo = () => {
+  meta2d.redo();
+};
+
+const onCut = () => {
+  meta2d.cut();
+};
+
+const onCopy = () => {
+  meta2d.copy();
+};
+
+const onPaste = () => {
+  meta2d.paste();
+};
+
+const onToggleAnchor = () => {
+  //取消连线状态
+  meta2d.canvas.drawingLineName && drawPen();
+  meta2d.toggleAnchorMode();
+};
+
+const onAddAnchorHand = () => {
+  meta2d.addAnchorHand();
+};
+
+const onRemoveAnchorHand = () => {
+  meta2d.removeAnchorHand();
+};
+
+const onToggleAnchorHand = () => {
+  meta2d.toggleAnchorHand();
+};
+
+const onScaleWindow = () => {
+  meta2d.fitView();
+};
+
+const onScaleUp = () => {
+  const _scale = meta2d.store.data.scale + 0.1;
+  meta2d.scale(_scale);
+};
+
+const onScaleDown = () => {
+  const _scale = meta2d.store.data.scale - 0.1;
+  meta2d.scale(_scale);
+};
+
+const onScaleView = () => {
+  meta2d.scale(1);
+  meta2d.centerView();
+};
+
+const autoAnchor = ref(true);
+const onAutoAnchor = () => {
+  meta2d.store.options.autoAnchor = !meta2d.store.options.autoAnchor;
+  autoAnchor.value = meta2d.store.options.autoAnchor;
+};
+
+const onDisableAnchor = () => {
+  meta2d.store.options.disableAnchor = !meta2d.store.options.disableAnchor;
+  changeDisableAnchor();
+};
+
+const disableAnchor = ref(true);
+const changeDisableAnchor = () => {
+  const { disableAnchor: disableAnchorOption, autoAnchor: autoAnchorOption } =
+    meta2d.store.options;
+  disableAnchor.value = disableAnchorOption || false;
+  if (disableAnchorOption && autoAnchorOption) {
+    // 禁用瞄点开了,需要关闭自动瞄点
+    onAutoAnchor();
+  }
+};
+
+
+const helpList = [
+  {
+    name: "产品介绍",
+    url: "https://doc.le5le.com/document/118756411",
+    target: "_blank",
+  },
+  {
+    name: "快速上手",
+    url: "https://doc.le5le.com/document/119363000",
+    target: "_blank",
+  },
+  {
+    name: "使用手册",
+    url: "https://doc.le5le.com/document/118764244",
+    target: "_blank",
+  },
+  {
+    name: "快捷键",
+    url: "https://doc.le5le.com/document/119620214",
+    target: "_blank",
+    divider: true,
+  },
+  {
+    name: "企业服务与支持",
+    url: "https://doc.le5le.com/document/119296274",
+    target: "_blank",
+    divider: true,
+  },
+  {
+    name: "关于我们",
+    url: "https://le5le.com/about.html",
+    target: "_blank",
+  },
+];
 </script>
 <style lang="postcss" scoped>
 .app-header {

+ 18 - 14
src/views/components/View.vue

@@ -2,13 +2,13 @@
   <div class="meta2d">
     <div class="tools">
       <t-tooltip content="新建" placement="bottom">
-        <a><t-icon name="add" /></a>
+        <a><t-icon name="add" @click="newFile" /></a>
       </t-tooltip>
       <t-tooltip content="保存" placement="bottom">
-        <a> <t-icon name="save" /></a>
+        <a> <t-icon name="save" @click="save(SaveType.Save)" /></a>
       </t-tooltip>
       <t-tooltip content="保存为模板" placement="bottom">
-        <a><t-icon name="layers" /></a>
+        <a><t-icon name="layers" @click="save(SaveType.Save,true)" /></a>
       </t-tooltip>
       <t-tooltip content="格式化" placement="bottom">
         <a>
@@ -100,19 +100,25 @@
 </template>
 
 <script lang="ts" setup>
-import { Meta2d, Options } from '@meta2d/core';
-import { onMounted, onUnmounted, watch } from 'vue';
-import { useRoute, useRouter } from 'vue-router';
+import { Meta2d, Options } from "@meta2d/core";
+import { onMounted, onUnmounted ,watch} from "vue";
+import { registerBasicDiagram } from "@/services/register";
+import { useRouter, useRoute } from "vue-router";
+import { useUser } from "@/services/user";
+import { getLe5le2d } from "@/services/api";
 
-import { registerBasicDiagram } from '@/services/register';
-import axios from 'axios';
-
-const route = useRoute();
 const router = useRouter();
+const route = useRoute();
+const { user, message, getUser, getMessage, signout } = useUser();
 
 const meta2dOptions: Options = {
-  cdn: 'https://assets.le5lecdn.com',
+  cdn: "https://assets.le5lecdn.com",
   rule: true,
+  background: '#1e2430',
+  x: 32,
+  y: 32,
+  width: 1920,
+  height: 1080,
 };
 onMounted(() => {
   meta2d = new Meta2d('meta2d', meta2dOptions);
@@ -129,9 +135,7 @@ const watcher = watch(
 
 const open = async () => {
   if (route.query.id) {
-    const ret: any = await axios.post('/data/le5le2d/get', {
-      id: route.query.id,
-    });
+    const ret: any = getLe5le2d(route.query.id+'');
     ret && meta2d.open(ret);
   } else {
     meta2d.open();

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác