瀏覽代碼

perfect_header_file

ananzhusen 2 年之前
父節點
當前提交
077bc8d2b7
共有 5 個文件被更改,包括 2195 次插入275 次删除
  1. 1543 0
      src/assets/canvas2svg.js
  2. 2 1
      src/global.d.ts
  3. 9 3
      src/services/api.ts
  4. 639 46
      src/views/components/Header.vue
  5. 2 225
      src/views/components/View.vue

+ 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 {
 declare global {
   var meta2d: Meta2d;
   var meta2d: Meta2d;
+  var C2S: any;
 }
 }
 
 
 declare interface Window {
 declare interface Window {

+ 9 - 3
src/services/api.ts

@@ -1,10 +1,16 @@
 //所有的接口请求
 //所有的接口请求
 import axios from "axios";
 import axios from "axios";
-export const cdnUrl = "https://drive.le5lecdn.com";
+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) {
 export async function delImage(image: string) {
-  if (image.startsWith(cdnUrl)) {
-    await axios.delete("/file" + image.replace(cdnUrl, ""));
+  if (image.startsWith(upCdn)) {
+    await axios.delete("/file" + image.replace(upCdn, ""));
   } else {
   } else {
     await axios.delete(`${image}`);
     await axios.delete(`${image}`);
   }
   }

+ 639 - 46
src/views/components/Header.vue

@@ -23,16 +23,16 @@
           <a>导入文件</a>
           <a>导入文件</a>
         </t-dropdown-item>
         </t-dropdown-item>
         <t-dropdown-item>
         <t-dropdown-item>
-          <a>保存</a>
+          <a @click="save()">保存</a>
         </t-dropdown-item>
         </t-dropdown-item>
         <t-dropdown-item>
         <t-dropdown-item>
-          <a>另保存</a>
+          <a @click="save(SaveType.SaveAs)">另保存</a>
         </t-dropdown-item>
         </t-dropdown-item>
         <t-dropdown-item divider="true">
         <t-dropdown-item divider="true">
-          <a>下载JSON文件</a>
+          <a @click="downloadJson">下载JSON文件</a>
         </t-dropdown-item>
         </t-dropdown-item>
         <t-dropdown-item>
         <t-dropdown-item>
-          <a>
+          <a @click="downloadZip">
             <div class="flex">
             <div class="flex">
               导出为ZIP文件 <span class="flex-grow"></span>
               导出为ZIP文件 <span class="flex-grow"></span>
               <span><label>VIP</label></span>
               <span><label>VIP</label></span>
@@ -40,7 +40,7 @@
           </a>
           </a>
         </t-dropdown-item>
         </t-dropdown-item>
         <t-dropdown-item>
         <t-dropdown-item>
-          <a>
+          <a @click="downloadHtml">
             <div class="flex">
             <div class="flex">
               导出为HTML <span class="flex-grow"></span>
               导出为HTML <span class="flex-grow"></span>
               <span><label>VIP</label></span>
               <span><label>VIP</label></span>
@@ -48,7 +48,7 @@
           </a>
           </a>
         </t-dropdown-item>
         </t-dropdown-item>
         <t-dropdown-item>
         <t-dropdown-item>
-          <a>
+          <a @click="downloadVue3">
             <div class="flex">
             <div class="flex">
               导出为Vue3组件 <span class="flex-grow"></span>
               导出为Vue3组件 <span class="flex-grow"></span>
               <span><label>VIP</label></span>
               <span><label>VIP</label></span>
@@ -56,7 +56,7 @@
           </a>
           </a>
         </t-dropdown-item>
         </t-dropdown-item>
         <t-dropdown-item>
         <t-dropdown-item>
-          <a>
+          <a @click="downloadVue2">
             <div class="flex">
             <div class="flex">
               导出为Vue2组件 <span class="flex-grow"></span>
               导出为Vue2组件 <span class="flex-grow"></span>
               <span><label>VIP</label></span>
               <span><label>VIP</label></span>
@@ -64,7 +64,7 @@
           </a>
           </a>
         </t-dropdown-item>
         </t-dropdown-item>
         <t-dropdown-item divider="true">
         <t-dropdown-item divider="true">
-          <a>
+          <a @click="downloadReact">
             <div class="flex">
             <div class="flex">
               导出为React组件 <span class="flex-grow"></span>
               导出为React组件 <span class="flex-grow"></span>
               <span><label>VIP</label></span>
               <span><label>VIP</label></span>
@@ -72,10 +72,10 @@
           </a>
           </a>
         </t-dropdown-item>
         </t-dropdown-item>
         <t-dropdown-item>
         <t-dropdown-item>
-          <a>下载为PNG</a>
+          <a @click="downloadPng">下载为PNG</a>
         </t-dropdown-item>
         </t-dropdown-item>
         <t-dropdown-item>
         <t-dropdown-item>
-          <a>下载为SVG</a>
+          <a @click="downloadSvg">下载为SVG</a>
         </t-dropdown-item>
         </t-dropdown-item>
       </t-dropdown-menu>
       </t-dropdown-menu>
     </t-dropdown>
     </t-dropdown>
@@ -272,31 +272,46 @@
 </template>
 </template>
 
 
 <script lang="ts" setup>
 <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 } from "vue";
+import { useRouter, useRoute } from "vue-router";
+import { useUser } from "@/services/user";
+import { NotifyPlugin, MessagePlugin } from "tdesign-vue-next";
 import {
 import {
   showNotification,
   showNotification,
   Meta2dBackData,
   Meta2dBackData,
   dealwithFormatbeforeOpen,
   dealwithFormatbeforeOpen,
   gotoAccount,
   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";
 
 
 const router = useRouter();
 const router = useRouter();
+const route = useRoute();
+
 const market = import.meta.env.VITE_MARKET;
 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 { user, message, getUser, getMessage, signout } = useUser();
 
 
 const isNew = ref(false);
 const isNew = ref(false);
 const data = reactive({
 const data = reactive({
-  name: '空白文件',
+  name: "空白文件",
 });
 });
 
 
 function login() {
 function login() {
@@ -314,7 +329,7 @@ function login() {
   //     }
   //     }
 }
 }
 
 
-const title = '系统可能不会保存您所做的更改,是否继续?';
+const title = "系统可能不会保存您所做的更改,是否继续?";
 const newFile = async () => {
 const newFile = async () => {
   if (isNew.value) {
   if (isNew.value) {
     if (await showNotification(title)) {
     if (await showNotification(title)) {
@@ -390,30 +405,30 @@ async function newfile(noRouter: boolean = false) {
   // 打开文件操作不跳转
   // 打开文件操作不跳转
   !noRouter &&
   !noRouter &&
     router.replace({
     router.replace({
-      path: '/',
-      query: { r: Date.now() + '' },
+      path: "/",
+      query: { r: Date.now() + "" },
     });
     });
 }
 }
 
 
 function load(newT: boolean = false) {
 function load(newT: boolean = false) {
-  const input = document.createElement('input');
-  input.type = 'file';
+  const input = document.createElement("input");
+  input.type = "file";
   input.onchange = (event) => {
   input.onchange = (event) => {
     const elem = event.target as HTMLInputElement;
     const elem = event.target as HTMLInputElement;
     if (elem.files && elem.files[0]) {
     if (elem.files && elem.files[0]) {
       newT && newfile(true);
       newT && newfile(true);
       // 路由跳转 可能在 openFile 后执行
       // 路由跳转 可能在 openFile 后执行
-      if (elem.files[0].name.endsWith('.json')) {
+      if (elem.files[0].name.endsWith(".json")) {
         openJson(elem.files[0], newT);
         openJson(elem.files[0], newT);
-      } else if (elem.files[0].name.endsWith('.svg')) {
+      } else if (elem.files[0].name.endsWith(".svg")) {
         MessagePlugin.info(
         MessagePlugin.info(
-          '可二次编辑但转换存在损失,若作为图片使用,请使用右侧属性面板的上传图片功能'
+          "可二次编辑但转换存在损失,若作为图片使用,请使用右侧属性面板的上传图片功能"
         );
         );
         openSvg(elem.files[0]);
         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);
         openZip(elem.files[0], newT);
       } else {
       } else {
-        MessagePlugin.info('打开文件只支持 json,svg,zip 格式');
+        MessagePlugin.info("打开文件只支持 json,svg,zip 格式");
       }
       }
     }
     }
   };
   };
@@ -425,7 +440,7 @@ const openJson = async (file: File, isNew: boolean = false) => {
   try {
   try {
     let data: Meta2dBackData = JSON.parse(text);
     let data: Meta2dBackData = JSON.parse(text);
     if (!data.name) {
     if (!data.name) {
-      data.name = file.name.replace('.json', '');
+      data.name = file.name.replace(".json", "");
     }
     }
     if (!data.version || compareVersion(data.version, baseVer) === -1) {
     if (!data.version || compareVersion(data.version, baseVer) === -1) {
       // 如果版本号不存在或者版本号 version < 1.0.0
       // 如果版本号不存在或者版本号 version < 1.0.0
@@ -453,12 +468,12 @@ const openSvg = async (file: File) => {
   const text = await readFile(file);
   const text = await readFile(file);
   const pens: Pen[] = parseSvg(text);
   const pens: Pen[] = parseSvg(text);
   meta2d.canvas.addCaches = pens;
   meta2d.canvas.addCaches = pens;
-  MessagePlugin.info('svg转换成功,请点击画布决定放置位置');
+  MessagePlugin.info("svg转换成功,请点击画布决定放置位置");
 };
 };
 
 
 const openZip = async (file: File, isNew: boolean = false) => {
 const openZip = async (file: File, isNew: boolean = false) => {
   if (!(user && user.username)) {
   if (!(user && user.username)) {
-    MessagePlugin.warning('请先登录,否则无法保存!');
+    MessagePlugin.warning(noLoginTip);
     return;
     return;
   }
   }
 
 
@@ -468,16 +483,16 @@ const openZip = async (file: File, isNew: boolean = false) => {
     return;
     return;
   }
   }
 
 
-  const { default: JSZip } = await import('jszip');
+  const { default: JSZip } = await import("jszip");
   const zip = new JSZip();
   const zip = new JSZip();
   await zip.loadAsync(file);
   await zip.loadAsync(file);
 
 
-  let dataStr = '';
+  let dataStr = "";
   for (const key in zip.files) {
   for (const key in zip.files) {
     if (zip.files[key].dir) {
     if (zip.files[key].dir) {
       continue;
       continue;
     }
     }
-    if (key.endsWith('.json')) {
+    if (key.endsWith(".json")) {
       // 认为只有一个 json 文件
       // 认为只有一个 json 文件
       // dataStr = await zip.file(key).async('string');
       // dataStr = await zip.file(key).async('string');
       break;
       break;
@@ -499,15 +514,15 @@ const openZip = async (file: File, isNew: boolean = false) => {
     let _keyLower = key.toLowerCase();
     let _keyLower = key.toLowerCase();
     // if (!key.endsWith('.json') && (_png !== -1 || _img !== -1 || _image !== -1 || _file !== -1)) {
     // if (!key.endsWith('.json') && (_png !== -1 || _img !== -1 || _image !== -1 || _file !== -1)) {
     if (
     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) {
       if (extPos > 0) {
         ext = filename.substr(extPos);
         ext = filename.substr(extPos);
       }
       }
@@ -558,7 +573,7 @@ const openZip = async (file: File, isNew: boolean = false) => {
     let data: Meta2dBackData = JSON.parse(dataStr);
     let data: Meta2dBackData = JSON.parse(dataStr);
     if (data) {
     if (data) {
       if (!data.name) {
       if (!data.name) {
-        data.name = file.name.replace('.zip', '');
+        data.name = file.name.replace(".zip", "");
       }
       }
       if (!data.version || compareVersion(data.version, baseVer) === -1) {
       if (!data.version || compareVersion(data.version, baseVer) === -1) {
         // 如果版本号不存在或者版本号 version < 1.0.0
         // 如果版本号不存在或者版本号 version < 1.0.0
@@ -596,6 +611,584 @@ async function loadFile(newT: boolean = false) {
 async function openFile() {
 async function openFile() {
   loadFile(true);
   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);
+};
 </script>
 </script>
 <style lang="postcss" scoped>
 <style lang="postcss" scoped>
 .app-header {
 .app-header {

+ 2 - 225
src/views/components/View.vue

@@ -5,10 +5,10 @@
         <a><t-icon name="add" @click="newFile" /></a>
         <a><t-icon name="add" @click="newFile" /></a>
       </t-tooltip>
       </t-tooltip>
       <t-tooltip content="保存" placement="bottom">
       <t-tooltip content="保存" placement="bottom">
-        <a> <t-icon name="save" @click="save" /></a>
+        <a> <t-icon name="save" @click="save(SaveType.Save)" /></a>
       </t-tooltip>
       </t-tooltip>
       <t-tooltip content="保存为模板" placement="bottom">
       <t-tooltip content="保存为模板" placement="bottom">
-        <a><t-icon name="layers" @click="saveAsComponents" /></a>
+        <a><t-icon name="layers" @click="save(SaveType.Save,true)" /></a>
       </t-tooltip>
       </t-tooltip>
       <t-tooltip content="格式化" placement="bottom">
       <t-tooltip content="格式化" placement="bottom">
         <a>
         <a>
@@ -103,20 +103,8 @@
 import { Meta2d, Options } from "@meta2d/core";
 import { Meta2d, Options } from "@meta2d/core";
 import { onMounted, onUnmounted ,watch} from "vue";
 import { onMounted, onUnmounted ,watch} from "vue";
 import { registerBasicDiagram } from "@/services/register";
 import { registerBasicDiagram } from "@/services/register";
-import { Meta2dBackData, checkData } from "@/services/utils";
 import { useRouter, useRoute } from "vue-router";
 import { useRouter, useRoute } from "vue-router";
 import { useUser } from "@/services/user";
 import { useUser } from "@/services/user";
-import { MessagePlugin } from "tdesign-vue-next";
-import localforage from "localforage";
-import { dataURLtoBlob, upload } from "@/services/file";
-import {
-  delImage,
-  getFolders,
-  addCollection,
-  updateCollection,
-  updateFolders,
-} from "@/services/api";
-import { baseVer } from "@/services/upgrade";
 import { getLe5le2d } from "@/services/api";
 import { getLe5le2d } from "@/services/api";
 
 
 const router = useRouter();
 const router = useRouter();
@@ -160,217 +148,6 @@ onUnmounted(() => {
     meta2d.destroy();
     meta2d.destroy();
   }
   }
 });
 });
-
-enum SaveType {
-  Save,
-  SaveAs,
-}
-
-//本地保存图纸数据 key
-const localMeta2dDataName = "meta2dData";
-
-const save = async (type: SaveType = SaveType.Save) => {
-  meta2d.stopAnimate();
-  const data: Meta2dBackData = meta2d.data();
-  if (!(user && user.username)) {
-    MessagePlugin.warning("请先登录,否则无法保存!");
-    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 (data.component) {
-    // 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 saveAsComponents = () => {
-  // meta2d.store.data.component = true;
-  save();
-}
 </script>
 </script>
 <style lang="postcss" scoped>
 <style lang="postcss" scoped>
 .meta2d {
 .meta2d {