瀏覽代碼

add zorro

guoziyun 1 年之前
父節點
當前提交
a084056f67
共有 100 個文件被更改,包括 34366 次插入37 次删除
  1. 32 1
      app.js
  2. 9 0
      bin/artsite
  3. 6 0
      libs/utils/index.js
  4. 2 11
      libs/utils/lang.js
  5. 250 0
      libs/utils/pager.js
  6. 51 0
      libs/utils/session.js
  7. 82 0
      libs/utils/validators.js
  8. 5 5
      models/schema-art.js
  9. 89 0
      package-lock.json
  10. 5 0
      package.json
  11. 20 20
      routes/index.js
  12. 190 0
      routes/napi/web/art.js
  13. 104 0
      routes/napi/web/auth.js
  14. 18 0
      routes/napi/web/menu.js
  15. 149 0
      routes/napi/web/role.js
  16. 206 0
      routes/napi/web/user.js
  17. 16 0
      zorro/.browserslistrc
  18. 42 0
      zorro/.gitignore
  19. 30 0
      zorro/README.md
  20. 160 0
      zorro/angular.json
  21. 10954 0
      zorro/package-lock.json
  22. 49 0
      zorro/package.json
  23. 32 0
      zorro/proxy.config.json
  24. 321 0
      zorro/src/app/admin-layout.component.ts
  25. 38 0
      zorro/src/app/app-routing.module.ts
  26. 15 0
      zorro/src/app/app.component.ts
  27. 107 0
      zorro/src/app/app.module.ts
  28. 30 0
      zorro/src/app/auth/auth.guard.ts
  29. 14 0
      zorro/src/app/auth/auth.module.ts
  30. 93 0
      zorro/src/app/auth/auth.service.ts
  31. 82 0
      zorro/src/app/auth/can.directive.ts
  32. 9 0
      zorro/src/app/auth/interfaces.ts
  33. 21 0
      zorro/src/app/icons-provider.module.ts
  34. 369 0
      zorro/src/app/lib/filler/check-edit/check-edit-layer.ts
  35. 1030 0
      zorro/src/app/lib/filler/check-edit/myfloodfill.ts
  36. 219 0
      zorro/src/app/lib/filler/color/color.ts
  37. 341 0
      zorro/src/app/lib/filler/color/dE00.ts
  38. 112 0
      zorro/src/app/lib/filler/common/animator.ts
  39. 184 0
      zorro/src/app/lib/filler/common/auto-color-map.ts
  40. 13 0
      zorro/src/app/lib/filler/common/centers.worker.ts
  41. 64 0
      zorro/src/app/lib/filler/common/color-merge.ts
  42. 144 0
      zorro/src/app/lib/filler/common/color-order.ts
  43. 1537 0
      zorro/src/app/lib/filler/common/coloredmap.ts
  44. 104 0
      zorro/src/app/lib/filler/common/easing.ts
  45. 2377 0
      zorro/src/app/lib/filler/common/etrace.ts
  46. 2439 0
      zorro/src/app/lib/filler/common/etrace2.ts
  47. 65 0
      zorro/src/app/lib/filler/common/fillarea.ts
  48. 37 0
      zorro/src/app/lib/filler/common/filltask.ts
  49. 832 0
      zorro/src/app/lib/filler/common/floodfill.ts
  50. 1025 0
      zorro/src/app/lib/filler/common/floodfill2.ts
  51. 48 0
      zorro/src/app/lib/filler/common/interfaces.ts
  52. 192 0
      zorro/src/app/lib/filler/common/kmeans.ts
  53. 204 0
      zorro/src/app/lib/filler/common/polygon.ts
  54. 1435 0
      zorro/src/app/lib/filler/common/potrace.ts
  55. 103 0
      zorro/src/app/lib/filler/common/random-color.ts
  56. 45 0
      zorro/src/app/lib/filler/common/repeater.ts
  57. 351 0
      zorro/src/app/lib/filler/common/svg-generator.ts
  58. 274 0
      zorro/src/app/lib/filler/common/svg-generator2.ts
  59. 285 0
      zorro/src/app/lib/filler/common/svg-generator3.ts
  60. 512 0
      zorro/src/app/lib/filler/common/utils.ts
  61. 292 0
      zorro/src/app/lib/filler/common/work-center-finder.ts
  62. 33 0
      zorro/src/app/lib/filler/core/canvas-layer.ts
  63. 26 0
      zorro/src/app/lib/filler/core/color-picker-tool.ts
  64. 16 0
      zorro/src/app/lib/filler/core/editor-config.ts
  65. 800 0
      zorro/src/app/lib/filler/core/editor.ts
  66. 52 0
      zorro/src/app/lib/filler/core/eventemitter.ts
  67. 43 0
      zorro/src/app/lib/filler/core/image-layer.ts
  68. 19 0
      zorro/src/app/lib/filler/core/index.d.ts
  69. 20 0
      zorro/src/app/lib/filler/core/interface.ts
  70. 79 0
      zorro/src/app/lib/filler/core/layer.ts
  71. 277 0
      zorro/src/app/lib/filler/core/matrix.ts
  72. 28 0
      zorro/src/app/lib/filler/core/pantool.ts
  73. 152 0
      zorro/src/app/lib/filler/core/rect.ts
  74. 67 0
      zorro/src/app/lib/filler/core/tool.ts
  75. 109 0
      zorro/src/app/lib/filler/core/utils.ts
  76. 33 0
      zorro/src/app/lib/filler/editor.directive.ts
  77. 17 0
      zorro/src/app/lib/filler/filler.module.ts
  78. 24 0
      zorro/src/app/lib/filler/map-edit/map-brush-tool.ts
  79. 849 0
      zorro/src/app/lib/filler/map-edit/map-edit-layer.ts
  80. 14 0
      zorro/src/app/lib/filler/map-edit/map-edit-tool.ts
  81. 33 0
      zorro/src/app/lib/filler/map-edit/map-merge-tool.ts
  82. 28 0
      zorro/src/app/lib/filler/map-edit/map-pencil-tool.ts
  83. 18 0
      zorro/src/app/lib/filler/map-edit/map-recover-tool.ts
  84. 19 0
      zorro/src/app/lib/filler/map-edit/map-split-area-tool.ts
  85. 392 0
      zorro/src/app/lib/filler/mark-edit/mark-edit-layer.ts
  86. 155 0
      zorro/src/app/lib/filler/mark-edit/mark-tool.ts
  87. 89 0
      zorro/src/app/lib/filler/number-edit/number-bucket-tool.ts
  88. 1672 0
      zorro/src/app/lib/filler/number-edit/number-edit-layer.ts
  89. 16 0
      zorro/src/app/lib/filler/number-edit/number-edit-tool.ts
  90. 77 0
      zorro/src/app/lib/filler/number-edit/number-erase-tool.ts
  91. 48 0
      zorro/src/app/lib/filler/number-edit/number-play-tool.ts
  92. 38 0
      zorro/src/app/lib/filler/number-edit/number-select-tool.ts
  93. 5 0
      zorro/src/app/lib/filler/order-edit/README.md
  94. 786 0
      zorro/src/app/lib/filler/order-edit/order-edit-layer.ts
  95. 43 0
      zorro/src/app/lib/filler/order-edit/order-play-tool.ts
  96. 37 0
      zorro/src/app/lib/filler/order-edit/order-select-tool.ts
  97. 82 0
      zorro/src/app/lib/filler/tool-config.ts
  98. 35 0
      zorro/src/app/lib/pager/cache.service.ts
  99. 113 0
      zorro/src/app/lib/pager/op-button.directive.ts
  100. 158 0
      zorro/src/app/lib/pager/pager-filter.component.ts

+ 32 - 1
app.js

@@ -1,9 +1,14 @@
 const express = require('express');
 const path = require('path');
 const app = express();
-const { getLocale } = require('./libs/utils');
 const config = require('./config/app')
 
+const session = require('express-session');
+const RedisStore = require('connect-redis')(session);
+
+const bodyParser = require('body-parser');
+const authChecker = require('./libs/auth/checker');
+
 // 设置视图引擎为EJS
 app.set('view engine', 'ejs');
 
@@ -15,6 +20,31 @@ app.use(express.static(config.STATIC_DIR));
 app.use(express.static(path.join(__dirname, 'dist')));
 
 
+
+app.use(session({
+  store: new RedisStore({
+    prefix: 'artsite_sess:'
+  }),
+  cookie: config.cookie,
+  saveUninitialized: false,
+  secret: 'MhxzKhl123.',
+  resave: false,
+  name: config.sessionName || sid,
+}));
+
+
+app.use(bodyParser.json());
+app.use(bodyParser.urlencoded({
+  extended: false
+}));
+
+
+
+app.use('/napi/web/auth', require('./routes/napi/web/auth'));
+app.use('/napi/web/menu', authChecker.checkLogin, require('./routes/napi/web/menu'));
+app.use('/napi/web/art', authChecker.checkLogin, require('./routes/napi/web/art'));
+
+
 app.use('/thumbs/v1', require('./routes/res/thumbs'));
 
 app.use('/', require('./routes/index'));
@@ -42,6 +72,7 @@ app.use(function (err, req, res, next) {
   res.render('404', { title: '404 Error', description: 'PAGE NOT FOUND' });
 });
 
+
 // 启动服务器,监听3000端口
 const PORT = process.env.PORT || 3000;
 app.listen(PORT, () => {

+ 9 - 0
bin/artsite

@@ -0,0 +1,9 @@
+#!/usr/bin/env node
+
+/**
+ * Module dependencies.
+ */
+
+var app = require('../app');
+
+console.log("app start...");

+ 6 - 0
libs/utils/index.js

@@ -0,0 +1,6 @@
+module.exports = {
+  validators: require('./validators'),
+  pager: require('./pager'),
+  session: require('./session'),
+  lang: require('./lang'),
+};

+ 2 - 11
libs/utils.js → libs/utils/lang.js

@@ -1,4 +1,4 @@
-const languages = require('../config/language');
+const languages = require('../../config/language');
 const createError = require('http-errors');
 const ObjectId = require('mongoose').Types.ObjectId;
 
@@ -23,16 +23,7 @@ function ensureLanguage(lang) {
   return 'en';
 }
 
-/**
- * 验证ID
- */
-function validateId(id, errMsg) {
-  errMsg = errMsg || 'ID错误!';
-  if (!ObjectId.isValid(id)) {
-    throw createError(400, errMsg);
-  }
-}
 
 module.exports = {
-  getLocale, ensureLanguage, validateId
+  getLocale, ensureLanguage,
 }

+ 250 - 0
libs/utils/pager.js

@@ -0,0 +1,250 @@
+const datefns = require('date-fns');
+const ObjectId = require('mongoose').Types.ObjectId;
+
+/**
+ * Get table header of the model
+ * @param {Model} model 
+ */
+function getHeaders(model, full) {
+  let headers = Object.keys(model.schema.paths).filter((key) => {
+    //TODO 
+    return key.substr(0, 1) != '_';
+  }).map((key) => {
+    let options = model.schema.path(key).options || {};
+    return {
+      data: key,
+      title: options.desc || key,
+      orderable: !(options.orderable === false),
+      searchable: options.searchable || false,
+      listingOrder: options.listingOrder || 1000,
+      listing: options.listing,
+      unit: options.unit,
+      type: getType(options.type),
+    }
+  })
+
+  if (!full) {
+    headers = headers.filter(function (obj) {
+      if (obj.listing === false) {
+        return false;
+      } else return true;
+    });
+  }
+  headers = headers.sort((a, b) => {
+    return a.listingOrder - b.listingOrder;
+  });
+  //console.table(headers);
+  return headers;
+}
+
+function getType(typeFunc) {
+  let type;
+  switch (typeFunc) {
+    case Number:
+      type = "number";
+      break;
+    case String:
+      type = 'string';
+      break;
+    case Date:
+      type = 'date';
+      break;
+    case Boolean:
+      type = 'boolean';
+      break;
+    default:
+      type = "other";
+  }
+  return type;
+}
+
+function getHeadersBuilder(model) {
+  return (req, res, next) => {
+    (async () => {
+      let headers = await getHeaders(model);
+      res.json(headers);
+      //res.json({ page, length, draw, recordsFiltered, recordsTotal, data: data.length });
+    })().catch(next);
+  }
+}
+
+
+
+/**
+ * 
+ * @param  {Model} model
+ * @returns 
+ */
+function getListBuilder(model, populates, isOwnerFunc) {
+
+  /**
+   * 
+   * @param {import("express").Request} req 
+   * @param {import("express").Response} res 
+   * @param {import("express").NextFunction} next 
+   */
+  function f(req, res, next) {
+    (async () => {
+
+      let { page, length, draw, search, filters } = req.query;
+
+      //let paths = model.schema.paths;
+      let schema = model.schema;
+
+      let baseQuery = req.query.base || {};
+      let query = Object.assign({}, baseQuery);
+
+
+      //filters
+      try {
+        filters = JSON.parse(filters);
+        //console.log('filters: ', filters);
+        Object.keys(filters)
+          .filter(data => schema.paths[data]) //filter not in schema
+          .filter(data => {
+            let value = filters[data];
+            if (value === undefined) return false; //undefined
+            if (Array.isArray(value) && value.length <= 0) return false;
+            return true;
+          }).forEach(data => {
+            //console.log('data', data);
+            let options = schema.path(data).options;
+            //console.log('options', options);
+            let value = filters[data];
+            if (Array.isArray(value) && options.type == Date) {
+              query[data] = {};
+              if (value[0]) {
+                query[data].$gte = datefns.startOfDay(new Date(value[0]));
+              }
+              if (value[1]) {
+                query[data].$lte = datefns.endOfDay(new Date(value[1]));
+              }
+            } else if (Array.isArray(value)) {
+              if (query[data]) {
+                query[data].$in = value;
+              } else {
+                query[data] = { $in: value };
+              }
+
+            } else {
+              query[data] = value;
+            }
+          })
+      } catch (e) {
+        console.warn(e);
+      }
+
+
+      //search
+      if (search) { search = search.trim(); }
+      if (search) {
+        search = escapeRegExp(search);
+        let match = { $regex: new RegExp(search, 'i') };
+        let searchable = getHeaders(model).filter(h => h.searchable);
+        if (searchable.length > 0) {
+          query.$or = searchable.map(header => {
+            let field = {};
+            field[header.data] = match;
+            return field;
+          });
+          if (ObjectId.isValid(search)) {
+            query.$or.push({ _id: search }); // add id search
+          }
+        }
+      }
+
+
+      //console.log('search: ', search);
+      //console.log('query : ', JSON.stringify(query));
+
+      //pagination
+      page = parseInt(page) || 1;
+      if (page <= 0) page = 1;
+      length = parseInt(length) || 20;
+      if (length <= 0) length = 20;
+      else if (length > 500) length = 500;
+
+      draw = parseInt(draw) || 0;
+      let start = (page - 1) * length
+      let recordsTotal = await model.countDocuments(baseQuery);
+      let recordsFiltered = await model.countDocuments(query);
+
+      // console.log('baseQuery', baseQuery);
+      // console.log('query', query);
+
+      /** @type {Query} */
+      let theQuery = model.find(query)
+        .skip(start)
+        .limit(length);
+
+      //order
+      let { orderBy, order } = req.query;
+      if (orderBy) {
+        if (order != 'asc' && order != 'desc') order = 'desc';
+        let sort = {};
+        sort[orderBy] = order;
+        theQuery.sort(sort)
+      }
+
+      //populate
+      if (populates) {
+        populates.forEach(p => theQuery.populate(p))
+      }
+
+      let data = await theQuery.exec();
+
+      data = data.map(item => item.toObject());
+
+      if (isOwnerFunc) {
+        data.forEach(item => {
+          item.isOwner = isOwnerFunc(item, req);
+        })
+      }
+
+      res.json({ page, length, draw, recordsFiltered, recordsTotal, data, filters, query });
+      //res.json({ page, length, draw, recordsFiltered, recordsTotal, data: data.length });
+    })().catch(next);
+  }
+
+
+  return f;
+
+}
+
+
+function escapeRegExp(str) {
+  var len;
+  var s;
+  var i;
+
+  var RE_CHARS = /[-\/\\^$*+?.()|[\]{}]/g; // eslint-disable-line no-useless-escape
+
+
+  // Check if the string starts with a forward slash...
+  if (str[0] === '/') {
+    // Find the last forward slash...
+    len = str.length;
+    for (i = len - 1; i >= 0; i--) {
+      if (str[i] === '/') {
+        break;
+      }
+    }
+  }
+  // If we searched the string to no avail or if the first letter is not `/`, assume that the string is not of the form `/[...]/[guimy]`:
+  if (i === void 0 || i <= 0) {
+    return str.replace(RE_CHARS, '\\$&');
+  }
+  // We need to de-construct the string...
+  s = str.substring(1, i);
+
+  // Only escape the characters between the `/`:
+  s = s.replace(RE_CHARS, '\\$&');
+
+  // Reassemble:
+  str = str[0] + s + str.substring(i);
+
+  return str;
+}
+
+
+module.exports = { getHeaders, getListBuilder, getHeadersBuilder, escapeRegExp }

+ 51 - 0
libs/utils/session.js

@@ -0,0 +1,51 @@
+
+
+
+/**
+ * 
+ * @param {import("express").Request} req 
+ */
+async function all(req) {
+  /** @type {sessionStore} */
+  let store = req.sessionStore;
+  return new Promise((done, reject) => {
+    store.all((err, sessions) => {
+      if (err) reject(err)
+      else done(sessions);
+    })
+  })
+}
+
+/**
+ * 
+ * @param {import("express").Request} req 
+ * @param {*} id session id
+ * @returns 
+ */
+async function destroy(req, id) {
+  /** @type {sessionStore} */
+  let store = req.sessionStore;
+  return new Promise((done, reject) => {
+    store.destroy(id, (err, sessions) => {
+      if (err) reject(err)
+      else done();
+    })
+  })
+}
+
+/**
+ * 根据用户id清空session
+ * @param {import("express").Request} req    
+ * @param {*} uid  user id
+ */
+async function destroyUser(req, uid) {
+  let sessions = await all(req);
+  let userSessions = sessions.filter(ses => ses.user && ses.user._id == uid);
+  for (var i = 0; i < userSessions.length; i++) {
+    await destroy(req, userSessions[i].id);
+  }
+}
+
+
+
+module.exports = { all, destroy, destroyUser }

+ 82 - 0
libs/utils/validators.js

@@ -0,0 +1,82 @@
+const ObjectId = require('mongoose').Types.ObjectId;
+const createError = require('http-errors');
+const moment = require('moment');
+
+/**
+ * 验证ID
+ */
+function validateId(id, errMsg) {
+  errMsg = errMsg || 'ID错误!';
+  if (!ObjectId.isValid(id)) {
+    throw createError(400, errMsg);
+  }
+}
+
+const phoneRegex = /^1[\d]{10}$/;
+function validatePhone(phone, errMsg) {
+  errMsg = errMsg || '手机号格式错误'
+  if (!phoneRegex.test(phone)) {
+    throw createError(400, errMsg)
+  }
+}
+
+const usernameRegex = /^[a-zA-Z0-9_\.]{5,}$/;
+function validateUsername(username, errMsg) {
+  errMsg = errMsg || '用户名格式错误'
+  if (!usernameRegex.test(username)) {
+    throw createError(400, errMsg)
+  }
+}
+
+const passwordRegex = /^.{8,}$/
+function validatePassword(password, errMsg) {
+  errMsg = errMsg || '密码格式错误'
+  if (!passwordRegex.test(password)) {
+    throw createError(400, errMsg)
+  }
+}
+
+function validateName(name, errMsg) {
+  if (!name || name.length < 2) {
+    throw createError(400, errMsg || '名字最少两位')
+  }
+}
+
+const idNoRegex = /^[\d]{17}[\dx]$/i
+function validateIdNo(no, errMsg) {
+  if (!idNoRegex.test(no))
+    throw createError(400, '身份证格式错误');
+}
+
+let vinRegex = /^[A-HJ-NPR-Z0-9]{17}$/;
+function validateVIN(vin) {
+  if (!vinRegex.test(vin))
+    throw createError(400, 'vin错误')
+}
+
+let DATE_FORMAT = ['YYYYMMDD', 'YYYY-MM-DD', 'YYYY/MM/DD'];
+function validateDate(date) {
+  let mmt = moment(date, DATE_FORMAT, true);
+  if (!mmt.isValid()) throw createError(400, '日期格式错误')
+  let startOfDay = mmt.startOf('day').toDate();
+  let endOfDay = mmt.endOf('day').toDate();
+  return [startOfDay, endOfDay];
+}
+
+
+function isValidDate(d) {
+  return d instanceof Date && !isNaN(d);
+}
+
+
+module.exports = {
+  validateId,
+  validatePhone,
+  validateName,
+  validateIdNo,
+  validateVIN,
+  validateDate,
+  validatePassword,
+  validateUsername,
+  isValidDate,
+}

+ 5 - 5
models/schema-art.js

@@ -123,12 +123,12 @@ let artSchema = new Schema({
 
 function artTransform(doc, ret) {
   //make thumb for art.
-  if (doc.hasSpecial) {
-    doc.thumb = `${config.resHost}/thumbs/v2/special_outline/480/${doc._id}.png`
-  } else {
-    doc.thumb = `${config.resHost}/thumbs/v2/page/480/${doc.pageId}.png`
+  ret.thumb = `${config.resHost}/thumbs/v2/page/320/${doc.pageId}.png?t=${doc.lastMod.getTime()}`;
+  if (doc.work && doc.hasSpecial) {
+    ret.thumb = `${config.resHost}/thumbs/v2/special/320/${doc._id}.png?t=${doc.lastMod.getTime()}`;
+  } else if (doc.work) {
+    ret.thumb = `${config.resHost}/thumbs/v2/work/320/${doc._id}.png?t=${doc.lastMod.getTime()}`
   }
-
 }
 
 artSchema.index({

+ 89 - 0
package-lock.json

@@ -9,11 +9,16 @@
       "version": "1.0.0",
       "license": "ISC",
       "dependencies": {
+        "accesscontrol": "^2.2.1",
+        "bcryptjs": "^2.4.3",
         "bluebird": "^3.7.2",
+        "body-parser": "^1.20.3",
         "connect-redis": "^3.3.3",
         "date-fns": "^4.1.0",
         "ejs": "^3.1.10",
         "express": "^4.21.2",
+        "express-session": "^1.18.1",
+        "moment": "^2.30.1",
         "mongoose": "^8.9.5",
         "node-fetch": "^2.7.0",
         "sharp": "^0.33.5"
@@ -403,6 +408,14 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/accesscontrol": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/accesscontrol/-/accesscontrol-2.2.1.tgz",
+      "integrity": "sha512-52EvFk/J9EF+w4mYQoKnOTkEMj01R1U5n2fc1dai6x1xkgOks3DGkx01qQL2cKFxGmE4Tn1krAU3jJA9L1NMkg==",
+      "dependencies": {
+        "notation": "^1.3.6"
+      }
+    },
     "node_modules/ansi-styles": {
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -432,6 +445,11 @@
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
       "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
     },
+    "node_modules/bcryptjs": {
+      "version": "2.4.3",
+      "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
+      "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="
+    },
     "node_modules/bluebird": {
       "version": "3.7.2",
       "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@@ -806,6 +824,37 @@
         "url": "https://opencollective.com/express"
       }
     },
+    "node_modules/express-session": {
+      "version": "1.18.1",
+      "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz",
+      "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==",
+      "dependencies": {
+        "cookie": "0.7.2",
+        "cookie-signature": "1.0.7",
+        "debug": "2.6.9",
+        "depd": "~2.0.0",
+        "on-headers": "~1.0.2",
+        "parseurl": "~1.3.3",
+        "safe-buffer": "5.2.1",
+        "uid-safe": "~2.1.5"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/express-session/node_modules/cookie": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+      "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/express-session/node_modules/cookie-signature": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+      "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="
+    },
     "node_modules/filelist": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
@@ -1097,6 +1146,14 @@
         "node": "*"
       }
     },
+    "node_modules/moment": {
+      "version": "2.30.1",
+      "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
+      "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/mongodb": {
       "version": "6.12.0",
       "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.12.0.tgz",
@@ -1268,6 +1325,11 @@
         "webidl-conversions": "^3.0.0"
       }
     },
+    "node_modules/notation": {
+      "version": "1.3.6",
+      "resolved": "https://registry.npmjs.org/notation/-/notation-1.3.6.tgz",
+      "integrity": "sha512-DIuJmrP/Gg1DcXKaApsqcjsJD6jEccqKSfmU3BUx/f1GHsMiTJh70cERwYc64tOmTRTARCeMwkqNNzjh3AHhiw=="
+    },
     "node_modules/object-inspect": {
       "version": "1.13.3",
       "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
@@ -1290,6 +1352,14 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/on-headers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+      "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/parseurl": {
       "version": "1.3.3",
       "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -1337,6 +1407,14 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/random-bytes": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
+      "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/range-parser": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -1658,6 +1736,17 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/uid-safe": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
+      "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
+      "dependencies": {
+        "random-bytes": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/unpipe": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",

+ 5 - 0
package.json

@@ -10,11 +10,16 @@
   "license": "ISC",
   "description": "",
   "dependencies": {
+    "accesscontrol": "^2.2.1",
+    "bcryptjs": "^2.4.3",
     "bluebird": "^3.7.2",
+    "body-parser": "^1.20.3",
     "connect-redis": "^3.3.3",
     "date-fns": "^4.1.0",
     "ejs": "^3.1.10",
     "express": "^4.21.2",
+    "express-session": "^1.18.1",
+    "moment": "^2.30.1",
     "mongoose": "^8.9.5",
     "node-fetch": "^2.7.0",
     "sharp": "^0.33.5"

+ 20 - 20
routes/index.js

@@ -8,7 +8,7 @@ const tags = require('../config/tag');
 const languages = require('../config/language');
 const translate = require('../config/translate');
 const meta = require('../config/meta');
-const { getLocale, ensureLanguage, validateId } = require('../libs/utils');
+const utils = require('../libs/utils');
 const { format } = require('date-fns');
 const { getListBuilder } = require('../libs/pager');
 
@@ -19,15 +19,15 @@ const artSelect = 'name title desc width height date publishTime tags lastMod my
 
 
 router.get('/', (req, res, next) => {
-  let locale = getLocale(req.acceptsLanguages());
-  let lang = ensureLanguage(locale);
+  let locale = utils.lang.getLocale(req.acceptsLanguages());
+  let lang = utils.lang.ensureLanguage(locale);
   return res.redirect(`/${lang}`);
 });
 
 
 router.get('/:lang/', function (req, res, next) {
   (async function () {
-    let lang = ensureLanguage(req.params.lang);
+    let lang = utils.lang.ensureLanguage(req.params.lang);
 
     let baseSort = { publishTime: 'desc' };
 
@@ -105,7 +105,7 @@ router.get('/:lang/', function (req, res, next) {
 
 router.get('/:lang/category/:tag?', function (req, res, next) {
   (async function () {
-    let lang = ensureLanguage(req.params.lang);
+    let lang = utils.lang.ensureLanguage(req.params.lang);
     let tag = req.params.tag;
     if (!tag) tag = 'latest';
 
@@ -145,7 +145,7 @@ router.get('/:lang/category/:tag?', function (req, res, next) {
 
 router.get('/:lang/tag/:tag?', function (req, res, next) {
   (async function () {
-    let lang = ensureLanguage(req.params.lang);
+    let lang = utils.lang.ensureLanguage(req.params.lang);
     let tag = req.params.tag;
     if (!tag) tag = 'latest';
 
@@ -188,7 +188,7 @@ router.get('/:lang/tag/:tag?', function (req, res, next) {
 
 router.get('/:lang/search', function (req, res, next) {
   (async function () {
-    let lang = ensureLanguage(req.params.lang);
+    let lang = utils.lang.ensureLanguage(req.params.lang);
     let search = req.query.search;
 
 
@@ -229,7 +229,7 @@ router.get('/:lang/search', function (req, res, next) {
 
 router.get('/:lang/special', function (req, res, next) {
   (async function () {
-    let lang = ensureLanguage(req.params.lang);
+    let lang = utils.lang.ensureLanguage(req.params.lang);
 
     let query = {
       title: meta.specialTitle[lang],
@@ -269,7 +269,7 @@ router.get('/:lang/special', function (req, res, next) {
 router.get('/:lang/albums', function (req, res, next) {
   (async function () {
 
-    let lang = ensureLanguage(req.params.lang);
+    let lang = utils.lang.ensureLanguage(req.params.lang);
 
     // 专辑
     let albums = await models.ArtAlbum
@@ -311,9 +311,9 @@ router.get('/:lang/albums', function (req, res, next) {
 router.get('/:lang/album/:id', function (req, res, next) {
   (async function () {
 
-    let lang = ensureLanguage(req.params.lang);
+    let lang = utils.lang.ensureLanguage(req.params.lang);
     let id = req.params.id;
-    validateId(id);
+    utils.validators.validateId(id);
 
     // 专辑
     let doc = await models.ArtAlbum
@@ -356,7 +356,7 @@ router.get('/:lang/album/:id', function (req, res, next) {
 router.get('/:lang/designers', function (req, res, next) {
   (async function () {
 
-    let lang = ensureLanguage(req.params.lang);
+    let lang = utils.lang.ensureLanguage(req.params.lang);
 
 
     let docs = await models.Art.aggregate([
@@ -417,9 +417,9 @@ router.get('/:lang/designers', function (req, res, next) {
 router.get('/:lang/designer/:id', function (req, res, next) {
   (async function () {
 
-    let lang = ensureLanguage(req.params.lang);
+    let lang = utils.lang.ensureLanguage(req.params.lang);
     let id = req.params.id;
-    validateId(id);
+    utils.validators.validateId(id);
 
     let user = await models.User.findById(id).select('name username');
     if (!user) throw createError(404, 'User Not Found!');
@@ -480,12 +480,12 @@ function getRealId(str) {
 
 router.get('/:lang/coloring-page/:str', function (req, res, next) {
   (async function () {
-    let lang = ensureLanguage(req.params.lang);
+    let lang = utils.lang.ensureLanguage(req.params.lang);
     let str = req.params.str;  // 拟人化的id,形如 beautiful-house-daldkaghlda3232, 最后一个-后面的才是真正的id
     let id = getRealId(str);
 
 
-    validateId(id);
+    utils.validators.validateId(id);
 
     let doc = await models.Art
       .findById(id)
@@ -541,10 +541,10 @@ router.get('/:lang/coloring-page/:str', function (req, res, next) {
 
 router.get('/:lang/detail/:id', function (req, res, next) {
   (async function () {
-    let lang = ensureLanguage(req.params.lang);
+    let lang = utils.lang.ensureLanguage(req.params.lang);
     let id = req.params.id;
 
-    validateId(id);
+    utils.validators.validateId(id);
 
     let doc = await models.Art
       .findById(id)
@@ -599,7 +599,7 @@ router.get('/play/:id', function (req, res, next) {
       description: meta.playDescription.en,
     };
 
-    validateId(id);
+    utils.validators.validateId(id);
     let doc = await models.Art.findById(id);
     if (!doc) throw createError(404, 'Art Not Found!');
 
@@ -612,7 +612,7 @@ router.get('/play/:id', function (req, res, next) {
 
 router.get('/:lang/info', function (req, res, next) {
   (async function () {
-    let lang = ensureLanguage(req.params.lang);
+    let lang = utils.lang.ensureLanguage(req.params.lang);
     let data = {
       title: meta.infoTitle[lang],
       description: meta.infoDescription[lang],

+ 190 - 0
routes/napi/web/art.js

@@ -0,0 +1,190 @@
+var express = require('express');
+var createError = require('http-errors')
+var router = express.Router();
+
+const models = require('../../../models');
+const utils = require('../../../libs/utils');
+
+const auth = require('../../../libs/auth');
+
+
+let artUserPopulates = Object.keys(models.Art.schema.paths)
+  .filter(path => 'User' == models.Art.schema.path(path).options.ref)
+  .map(path => ({ path, select: 'username name' }));
+console.log('artUserPopulates:', artUserPopulates);
+
+
+/**表头 */
+router.get('/pager/headers', auth.need('read:own', 'art'), utils.pager.getHeadersBuilder(models.Art));
+
+/**所有作品列表  */
+router.get('/pager/all', auth.need('read:any', 'art'), (req, res, next) => {
+  (async () => {
+    req.query.base = { drop: false };
+    next();
+  })().catch(next)
+}, utils.pager.getListBuilder(models.Art, artUserPopulates));
+
+/**回收站列表 */
+router.get('/pager/removed', auth.need('read:own', 'art'),
+  (req, res, next) => {
+    (async () => {
+      req.query.base = { drop: true }
+      let ac = await auth.getAc(req);
+      if (!ac.can('user').readAny('art').granted) {
+        req.query.base.user = req.session.user._id;
+      }
+      next();
+    })().catch(next)
+  }, utils.pager.getListBuilder(models.Art, artUserPopulates));
+
+
+/**我的作品列表 */
+router.get('/pager/my', auth.need('read:own', 'art'), (req, res, next) => {
+  (async () => {
+    req.query.base = {
+      user: req.session.user._id,
+      drop: false,
+    }
+    next();
+  })().catch(next)
+}, utils.pager.getListBuilder(models.Art, artUserPopulates));
+
+
+/**获取某个用户的作品列表 */
+router.get('/pager/user/:username', auth.need('read:any', 'art'), (req, res, next) => {
+  (async () => {
+    let { username } = req.params;
+    let user = await models.User.findOne({ username });
+    if (!user) throw createError(404, '用户不存在');
+    req.query.base = {
+      user: user._id,
+      drop: false,
+    }
+    next();
+  })().catch(next)
+}, utils.pager.getListBuilder(models.Art, artUserPopulates));
+
+
+/**待发布列表*/
+router.get('/pager/pending', auth.need('read:any', 'art-publish'), (req, res, next) => {
+  (async () => {
+    req.query.base = {
+      drop: false,
+      status: 7000,
+      $or: [{ publishSchedule: { $exists: false } }, { publishSchedule: null }]
+    }
+    next();
+  })().catch(next)
+}, utils.pager.getListBuilder(models.Art, artUserPopulates));
+
+
+
+
+
+/**
+ *
+ * 获取作品基础信息, 排除任何二进制数据
+ *
+ */
+router.get('/:id', auth.need('read:own', 'art'), function (req, res, next) {
+  (async function () {
+
+    let user = req.session.user._id;
+    let id = req.params.id;
+    utils.validators.validateId(id);
+    /**@type {Query} */
+    let query = models.Art.findOne({ $or: [{ _id: id }, { pageId: id }] });
+    query.populate({ path: 'bins', select: '-data' });
+    artUserPopulates.forEach(p => {
+      query.populate(p);
+    })
+    query.populate({ path: 'publishSchedule', select: 'done' });
+
+    let doc = await query.exec();
+    if (!doc) throw createError(404, '作品不存在!')
+
+    let ac = await auth.getAc(req);
+    if (!ac.can('user').readAny('art').granted && doc.user._id != req.session.user._id) {
+      throw createError(403, '权限不足');
+    }
+
+    let out = doc.toObject();
+
+    if (doc.work) { out.zip = utils.thumb.zipPath(doc._id); }
+
+    if (out.upstream) {
+      let { upstreamBase } = req.app.get('app-config');
+      out.upstreamUrl = `${upstreamBase}/app/pages/detail/${doc._id}`;
+    }
+
+    res.json(out);
+  })().catch(next)
+});
+
+
+/**
+ * 更新作品tags,name
+ * patch-meta
+ */
+router.patch('/meta', auth.need('update:own', 'art'), (req, res, next) => {
+  (async function () {
+    let user = req.session.user._id;
+    let { tags, name, desc, epgs, pageId, } = req.body;
+    if (!tags || !name || !epgs || !Array.isArray(tags)) {
+      throw createError(400, '参数错误');
+    }
+    tags = tags.map(t => t.trim()).filter(t => t);
+    utils.validators.validateId(pageId, 'ID错误');
+
+    let doc = await models.Art.findById(pageId);
+    if (!doc) throw createError(404, '作品不存在!');
+
+    let ac = await auth.getAc(req);
+    let isOwner = doc.user == user;
+    if (!ac.can('user').updateAny('art').granted && !isOwner) {
+      throw createError(403, '权限不足');
+    }
+
+    doc.tags = tags;
+    doc.epgs = epgs;
+    doc.name = name;
+    doc.desc = desc;
+    await doc.save();
+    res.json({
+      msg: 'ok'
+    })
+  })().catch(next)
+});
+
+
+
+
+
+
+////////other-apis//////
+
+
+router.get('/agg/tags', function (req, res, next) {
+  (async function () {
+    let tags = await models.Art.aggregate([
+      { $project: { tags: 1 } },
+      { $unwind: '$tags' },
+      {
+        $group: {
+          _id: '$tags',
+          count: { $sum: 1 }
+        }
+      },
+      { $sort: { count: -1 } },
+    ]);
+    tags = tags.map(t => ({ value: t._id, label: t._id }));
+    res.json(tags);
+  })().catch(next)
+});
+
+
+
+
+
+module.exports = router;

+ 104 - 0
routes/napi/web/auth.js

@@ -0,0 +1,104 @@
+var express = require('express');
+var createError = require('http-errors')
+const bcrypt = require('bcryptjs');
+
+const models = require('../../../models');
+const utils = require('../../../libs/utils');
+const authChecker = require('../../../libs/auth/checker');
+const auth = require('../../../libs/auth');
+
+const router = express.Router();
+
+router.get('/guard', authChecker.checkLogin, function (req, res, next) {
+  (async function () {
+    res.json({
+      msg: 'ok'
+    })
+  })().catch(next)
+});
+
+router.get('/profile', authChecker.checkLogin, function (req, res, next) {
+  (async function () {
+    res.json(req.session.user);
+  })().catch(next)
+});
+
+router.get('/sign-out', authChecker.checkLogin, function (req, res, next) {
+  (async function () {
+    //delete req.session.user;
+    req.session.destroy();
+    res.status(200)
+      .clearCookie('test_sid', { path: '/' })
+      .json({
+        msg: 'ok'
+      })
+  })().catch(next)
+});
+
+router.post('/sign-in', function (req, res, next) {
+  (async function () {
+    let { username, password } = req.body;
+    if (!username || !password) throw createError(401, 'Invalid params.');
+    username = username.trim();
+    password = password.trim();
+
+    let doc = await models.User.findOne({ username });
+    if (!doc) throw createError(401, '用户名或者密码错误');
+    let match = await bcrypt.compare(password, doc.password);
+    if (!match) throw createError(401, '用户名或者密码错误');
+    if (doc.disabled) throw createError(401, '账户已停用,请联系管理员');
+    doc.dateLastSignin = new Date();
+    doc.ipLastSignin = req.ip;
+    await doc.save();
+    req.session.user = doc.toObject();
+    res.json({
+      msg: 'ok'
+    })
+  })().catch(next)
+});
+
+
+router.patch('/password', authChecker.checkLogin, function (req, res, next) {
+  (async function () {
+    let userId = req.session.user._id;
+    let { password } = req.body;
+    if (!password) throw createError(400, 'Invalid params.');
+    let doc = await models.User.findById(userId);
+    if (!doc) throw createError('系统错误');
+    let salt = await bcrypt.genSalt(10);
+    doc.password = await bcrypt.hash(password, salt);
+    await doc.save();
+    res.json({
+      msg: 'ok'
+    })
+  })().catch(next)
+});
+
+
+router.get('/grants', authChecker.checkLogin, function (req, res, next) {
+  (async function () {
+    let doc = await models.User.findById(req.session.user._id).populate('roles');
+    let grants = [];
+    if (['chengen', 'guoziyun'].includes(doc.username)) {
+      grants = auth.base.fullGrants();
+    } else {
+      doc.roles.forEach(role => {
+        grants = grants.concat(role.grants);
+      })
+    }
+    res.json(grants);
+  })().catch(next)
+});
+
+
+
+router.post('/template', function (req, res, next) {
+  (async function () {
+    throw createError(404, 'YOUR SHOULD NOT SEE THIS');
+  })().catch(next)
+});
+
+
+
+
+module.exports = router;

+ 18 - 0
routes/napi/web/menu.js

@@ -0,0 +1,18 @@
+const express = require('express');
+const router = express.Router();
+const authChecker = require('../../../libs/auth/checker');
+const auth = require('../../../libs/auth');
+
+
+
+router.get('/', authChecker.checkLogin, function (req, res, next) {
+  (async function () {
+    let ac = await auth.getAc(req);
+    let menu = new auth.Menu(require('../../../libs/auth/menu-config'), ac);
+    let menuList = menu.getByRole('user');
+    res.json(menuList);
+  })().catch(next)
+});
+
+
+module.exports = router;

+ 149 - 0
routes/napi/web/role.js

@@ -0,0 +1,149 @@
+const createError = require('http-errors');
+const express = require('express');
+const router = express.Router();
+const models = require('../../../models');
+const utils = require('../../../libs/utils');
+const auth = require('../../../libs/auth');
+
+const isOwner = (item, req) => item.user && item.user._id == req.session.user._id;
+
+router.get('/pager/headers', auth.need('read:own', 'role'), utils.pager.getHeadersBuilder(models.Role));
+router.get('/pager', auth.need('read:own', 'role'), (req, res, next) => {
+  (async () => {
+    let ac = await auth.getAc(req);
+    if (!ac.can('user').readAny('role').granted) {
+      req.query.base = { user: req.session.user._id };
+    }
+    next();
+  })().catch(next)
+}, utils.pager.getListBuilder(models.Role, [{ path: 'user', select: 'username' }], isOwner));
+
+
+router.get('/options', auth.need('read:own', 'user'), function (req, res, next) {
+  (async function () {
+    let ac = await auth.getAc(req);
+    if (ac.can('user').createAny('user').granted) { //all roles
+      let roles = await models.Role.find().select('name').lean()
+      res.json(roles.map(r => ({ label: r.name, value: r._id })));
+    } else {
+      //用户创建的角色
+      let roles = await models.Role.find({ user: req.session.user._id }).select('name').lean()
+      //用户拥有的角色
+      let userDoc = await models.User.findById(req.session.user._id).populate({ path: 'roles', select: 'name' }).lean();
+      roles = roles.concat(userDoc.roles);
+      console.log('roles:', roles);
+      //去除重复
+      let result = roles.filter((role, index) => {
+        let myIndex = roles.findIndex(r => r._id.equals(role._id));
+        console.log(index, myIndex);
+        return myIndex === index;
+      }).map(r => ({ label: r.name, value: r._id }))
+
+      res.json(result);
+    }
+  })().catch(next);
+});
+
+
+// 直接取角色对应的用户列表
+router.get('/options2', auth.need('read:own', 'user'), function (req, res, next) {
+  (async function () {
+    let ac = await auth.getAc(req);
+    if (ac.can('user').createAny('user').granted) { //all roles
+      let roles = await models.Role.find().select('name').lean()
+      for (let ro of roles) {
+        let users = await models.UserRole.find({ 'role': ro._id }).lean() || [];
+        users = users.map(e => e.user);
+        ro.users = users;
+      }
+      res.json(roles.map(r => ({ label: r.name, value: r.users })));
+    }
+  })().catch(next);
+});
+
+
+
+
+router.post('/', auth.need('create:own', 'role'), function (req, res, next) {
+  (async function () {
+    //await utils.async.delay(500);
+    let data = req.body;
+    let { name, grants } = data;
+    if (!name || !grants)
+      throw createError(400, '参数错误');
+    let user = req.session.user._id;
+    let role = models.Role({ name, grants, user })
+    role = await role.save();
+    res.json({ msg: 'ok' });
+  })().catch(next);
+});
+
+
+router.patch('/:id', auth.need('update:own', 'role'), function (req, res, next) {
+  (async function () {
+    //await utils.async.delay(500);
+    let { id } = req.params;
+    utils.validators.validateId(id);
+    let role = await models.Role.findById(id);
+    if (!role) throw createError(404, '角色不存在');
+    let ac = await auth.getAc(req);
+    if (!ac.can('user').updateAny('role').granted && role.user != req.session.user._id) {
+      throw createError(403, '没有权限');
+    }
+    let { name, grants } = req.body;
+    if (!name || !grants)
+      throw createError(400, '参数错误');
+    role.set({ name, grants });
+    role = await role.save();
+    res.json({ msg: 'ok' });
+  })().catch(next);
+});
+
+
+router.delete('/:id', auth.need('delete:own', 'role'), function (req, res, next) {
+  (async function () {
+    //await utils.async.delay(500);
+    let { id } = req.params;
+    utils.validators.validateId(id);
+    let role = await models.Role.findById(id);
+    if (!role) throw createError(404, '角色不存在');
+    let ac = await auth.getAc(req);
+    if (!ac.can('user').deleteAny('role').granted && role.user != req.session.user._id) {
+      throw createError(403, '没有权限');
+    }
+    if (!role.user)
+      throw createError(400, '预定义角色不能删除')
+    await role.delete();
+    res.json(role)
+  })().catch(next);
+});
+
+
+router.get('/:id', auth.need('read:own', 'role'), function (req, res, next) {
+  (async function () {
+    //await utils.async.delay(500);
+    let { id } = req.params;
+    utils.validators.validateId(id);
+    let role = await models.Role.findById(id).lean();
+    let ac = await auth.getAc(req);
+    if (!ac.can('user').deleteAny('role').granted && role.user != req.session.user._id) {
+      throw createError(403, '没有权限');
+    }
+    if (!role) throw createError(404, '角色不存在');
+    res.json(role)
+  })().catch(next);
+});
+
+
+router.get('/auth/config', auth.need('read:own', 'role'), function (req, res, next) {
+  (async function () {
+    let { resources, actions, possessions } = auth.base;
+    res.json({ resources, actions, possessions });
+  })().catch(next);
+});
+
+
+
+
+
+module.exports = router;

+ 206 - 0
routes/napi/web/user.js

@@ -0,0 +1,206 @@
+var express = require('express');
+var createError = require('http-errors')
+const bcrypt = require('bcryptjs');
+
+const models = require('../../../models');
+const utils = require('../../../libs/utils');
+const auth = require('../../../libs/auth');
+
+const router = express.Router();
+
+
+
+router.get('/pager/headers', auth.need('read:own', 'user'), utils.pager.getHeadersBuilder(models.User));
+
+router.get('/pager', auth.need('read:own', 'user'), (req, res, next) => {
+  (async () => {
+    if (!req.ac) throw createError(403, 'NO AC');
+    /**@type {AccessControl} */
+    let ac = req.ac;
+    let readAny = ac.can('user').readAny('user').granted;
+    if (readAny) next()
+    else {
+      req.query.base = { createBy: req.session.user._id };
+      next();
+    }
+  })().catch(next);
+}, utils.pager.getListBuilder(models.User, [
+  { path: 'createBy', select: 'username' },
+  { path: 'roles', select: 'name' },
+], (item, req) => { return item.createBy && (item.createBy._id == req.session.user._id) }
+));
+
+
+/**
+ * user-create
+ */
+router.post('/', auth.need('create:own', 'user'), function (req, res, next) {
+  (async function () {
+    let { username, password, name, phone, email, roles } = req.body;
+    if (!username || !password || !phone || !email || !name) throw createError(400, '缺少参数');
+    utils.validators.validatePhone(phone);
+    utils.validators.validateUsername(username);
+    utils.validators.validatePassword(password);
+    let doc = await models.User.findOne({ $or: [{ username }, { phone }, { email }] }).select('username phone')
+      .lean()
+      .exec();
+    if (doc) throw createError(400, '用户名或手机号或邮箱已被占用');
+    let salt = await bcrypt.genSalt(10);
+    password = await bcrypt.hash(password, salt);
+    let createBy = req.session.user._id;
+    let ipLastSignin = req.ip;
+    let user = new models.User({ username, password, name, phone, email, roles, createBy, ipLastSignin });
+    await user.save();
+    res.json({
+      msg: 'ok',
+    })
+  })().catch(next)
+});
+
+
+router.get('/select/options', function (req, res, next) {
+  (async function () {
+    let docs = await models.User.find().select('username').lean();
+    docs = docs.map(u => ({ label: u.username, value: u._id }));
+    res.json(docs);
+  })().catch(next)
+});
+
+
+
+router.get('/:id', auth.need('read:own', 'user'), function (req, res, next) {
+  (async function () {
+    utils.validators.validateId(req.params.id);
+    let doc = await models.User.findById(req.params.id).select('-password').lean();
+    if (!doc) throw createError(404, 'Not found!');
+    if (!auth.can.readAny(req, 'user') && doc.createBy != req.session.user._id) throw createError(403, '权限不足');
+    res.json(doc);
+  })().catch(next)
+});
+
+
+
+router.patch('/:id', auth.need('update:own', 'user'), function (req, res, next) {
+  (async function () {
+    utils.validators.validateId(req.params.id);
+    let doc = await models.User.findById(req.params.id);
+    if (!doc) throw createError(404, 'Not found!');
+    let updates = {};
+    let editableFields = ['name', 'email', 'roles', 'epgs', 'phone', 'disabled'];
+    Object.keys(req.body).filter(key => editableFields.includes(key)).forEach(key => {
+      updates[key] = req.body[key];
+    })
+    doc.set(updates);
+    console.log('user update:', doc.getChanges());
+    await doc.save();
+
+    if (doc.disabled === true) {
+      await utils.session.destroyUser(req, doc._id); //清空用户session
+    }
+    //await utils.async.delay(1000);
+    res.json({ msg: 'ok' });
+  })().catch(next)
+});
+
+
+/** patch-password */
+router.patch('/:id/password', auth.need('update:own', 'user'), function (req, res, next) {
+  (async function () {
+    utils.validators.validateId(req.params.id);
+    let doc = await models.User.findById(req.params.id);
+    if (!doc) throw createError(404, 'Not found!');
+    let password = randomPassword();
+    let salt = await bcrypt.genSalt(10);
+    doc.password = await bcrypt.hash(password, salt);
+    await utils.session.destroyUser(req, doc._id);
+    await doc.save();
+    res.json({
+      name: doc.name,
+      username: doc.username,
+      password,
+    });
+  })().catch(next)
+});
+
+
+
+/**
+ * 检查username占用
+ */
+router.post('/check/username', auth.need('create:own', 'user'), function (req, res, next) {
+  (async function () {
+    let { id, username } = req.body;
+    if (!username) throw createError(400, 'Invalid request');
+    let doc = await models.User.findOne({ username }).select('username').lean().exec();
+    if (doc && doc._id != id) throw createError(400, '已占用');
+    res.json({ msg: 'ok' })
+  })().catch(next)
+});
+
+/**
+ * 检查手机号占用
+ */
+router.post('/check/phone', auth.need('create:own', 'user'), function (req, res, next) {
+  (async function () {
+    let { id, phone } = req.body;
+    if (!phone) throw createError(400, 'Invalid request');
+    let doc = await models.User.findOne({ phone }).select('phone').lean().exec();
+    if (doc && doc._id != id) throw createError(400, '已占用');
+    res.json({ msg: 'ok' })
+  })().catch(next)
+});
+
+
+/**
+ * 检查邮箱占用
+ */
+router.post('/check/email', auth.need('create:own', 'user'), function (req, res, next) {
+  (async function () {
+    let { id, email } = req.body;
+    if (!email) throw createError(400, 'Invalid request');
+    let doc = await models.User.findOne({ email }).select('email').lean().exec();
+    if (doc && doc._id != id) throw createError(400, '已占用');
+    res.json({ msg: 'ok' })
+  })().catch(next)
+});
+
+router.get('/session/list', auth.need('read:any', 'user'), function (req, res, next) {
+  (async function () {
+    let sessions = await utils.session.all(req);
+    let data = sessions.map(s => {
+      s.user.sesid = s.id;
+      return s.user;
+    });
+    res.json({ data });
+  })().catch(next)
+});
+
+
+router.delete('/session/:id', auth.need('delete:any', 'user'), function (req, res, next) {
+  (async function () {
+    /**@type {any[]} */
+    let sessions = await utils.session.all(req);
+    let ses = sessions.find(s => s.id == req.params.id);
+    if (!ses) throw createError(404, 'session不存在');
+    //await utils.async.delay(1000);
+    if (ses.id == req.session.id) throw createError(400, '不能删除你自己的session');
+    await utils.session.destroy(req, req.params.id);
+    res.json({ msg: 'ok' });
+  })().catch(next)
+});
+
+
+
+
+function randomPassword() {
+  let wishlist = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~!@-#$';
+  let pass = Array(20).fill(0).map(() => {
+    return wishlist.charAt(Math.floor(Math.random() * wishlist.length))
+  }).join('');
+  console.log('pass', pass);
+  return pass;
+}
+
+
+
+module.exports = router;

+ 16 - 0
zorro/.browserslistrc

@@ -0,0 +1,16 @@
+# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
+# For additional information regarding the format and rule options, please see:
+# https://github.com/browserslist/browserslist#queries
+
+# For the full list of supported browsers by the Angular framework, please see:
+# https://angular.io/guide/browser-support
+
+# You can see what browsers were selected by your queries by running:
+#   npx browserslist
+
+last 1 Chrome version
+last 1 Firefox version
+last 2 Edge major versions
+last 2 Safari major versions
+last 2 iOS major versions
+Firefox ESR

+ 42 - 0
zorro/.gitignore

@@ -0,0 +1,42 @@
+# See http://help.github.com/ignore-files/ for more about ignoring files.
+
+# Compiled output
+/dist
+/tmp
+/out-tsc
+/bazel-out
+
+# Node
+/node_modules
+npm-debug.log
+yarn-error.log
+
+# IDEs and editors
+.idea/
+.project
+.classpath
+.c9/
+*.launch
+.settings/
+*.sublime-workspace
+
+# Visual Studio Code
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+.history/*
+
+# Miscellaneous
+/.angular/cache
+.sass-cache/
+/connect.lock
+/coverage
+/libpeerconnection.log
+testem.log
+/typings
+
+# System files
+.DS_Store
+Thumbs.db

+ 30 - 0
zorro/README.md

@@ -0,0 +1,30 @@
+# PcoloringZorro
+
+This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 13.3.0.
+
+## Development server
+
+Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
+
+## Code scaffolding
+
+Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
+
+## Build
+
+Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
+
+## Running unit tests
+
+Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
+
+## Running end-to-end tests
+
+Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
+
+## Further help
+
+To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
+
+## Generate i18n
+ng extract-i18n --output-path src/locale

+ 160 - 0
zorro/angular.json

@@ -0,0 +1,160 @@
+{
+  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+  "version": 1,
+  "cli": {
+    "cache": {
+      "enabled": false
+    },
+    "analytics": false
+  },
+  "newProjectRoot": "projects",
+  "projects": {
+    "pcoloring-zorro": {
+      "projectType": "application",
+      "schematics": {
+        "@schematics/angular:component": {
+          "inlineTemplate": true,
+          "inlineStyle": true,
+          "style": "less",
+          "skipTests": true
+        },
+        "@schematics/angular:class": {
+          "skipTests": true
+        },
+        "@schematics/angular:directive": {
+          "skipTests": true
+        },
+        "@schematics/angular:guard": {
+          "skipTests": true
+        },
+        "@schematics/angular:interceptor": {
+          "skipTests": true
+        },
+        "@schematics/angular:pipe": {
+          "skipTests": true
+        },
+        "@schematics/angular:resolver": {
+          "skipTests": true
+        },
+        "@schematics/angular:service": {
+          "skipTests": true
+        },
+        "@schematics/angular:application": {
+          "strict": true
+        }
+      },
+      "root": "",
+      "sourceRoot": "src",
+      "prefix": "app",
+      "i18n": {
+        "sourceLocale": "zh",
+        "locales": {
+          "en": {
+            "translation": "src/locale/messages.en.xlf"
+          }
+        }
+      },
+      "architect": {
+        "build": {
+          "builder": "@angular-devkit/build-angular:browser",
+          "options": {
+            "localize": true,
+            "baseHref": "/app/",
+            "outputPath": "../public/app/",
+            "index": "src/index.html",
+            "main": "src/main.ts",
+            "polyfills": "src/polyfills.ts",
+            "tsConfig": "tsconfig.app.json",
+            "inlineStyleLanguage": "less",
+            "assets": [
+              "src/favicon.ico",
+              "src/assets",
+              {
+                "glob": "**/*",
+                "input": "./node_modules/@ant-design/icons-angular/src/inline-svg/",
+                "output": "/assets/"
+              }
+
+            ],
+            "styles": [
+              "src/assets/bootstrap/bootstrap.scss",
+              "src/styles.less",
+              "node_modules/pace-js/themes/blue/pace-theme-minimal.css"
+            ],
+            "scripts": [
+              "node_modules/pace-js/pace.min.js",
+              "node_modules/@popperjs/core/dist/umd/popper.min.js",
+              "node_modules/bootstrap/dist/js/bootstrap.min.js",
+              "src/assets/js/pngquant.min.js"
+            ],
+            "webWorkerTsConfig": "tsconfig.worker.json"
+          },
+          "configurations": {
+            "production": {
+              "budgets": [
+                {
+                  "type": "initial",
+                  "maximumWarning": "500kb",
+                  "maximumError": "3mb"
+                },
+                {
+                  "type": "anyComponentStyle",
+                  "maximumWarning": "2kb",
+                  "maximumError": "4kb"
+                }
+              ],
+              "fileReplacements": [
+                {
+                  "replace": "src/environments/environment.ts",
+                  "with": "src/environments/environment.prod.ts"
+                }
+              ],
+              "outputHashing": "all"
+            },
+            "development": {
+              "buildOptimizer": false,
+              "optimization": false,
+              "vendorChunk": true,
+              "extractLicenses": false,
+              "sourceMap": true,
+              "namedChunks": true,
+              "localize": ["zh"]
+            },
+            "development-en": {
+              "buildOptimizer": false,
+              "optimization": false,
+              "vendorChunk": true,
+              "extractLicenses": false,
+              "sourceMap": true,
+              "namedChunks": true,
+              "localize": ["en"]
+            }
+          },
+          "defaultConfiguration": "production"
+        },
+        "serve": {
+          "builder": "@angular-devkit/build-angular:dev-server",
+          "configurations": {
+            "production": {
+              "browserTarget": "pcoloring-zorro:build:production"
+            },
+            "development": {
+              "browserTarget": "pcoloring-zorro:build:development"
+            },
+            "development-en": {
+              "browserTarget": "pcoloring-zorro:build:development-en"
+            }
+          },
+          "defaultConfiguration": "development"
+        },
+        "extract-i18n": {
+          "builder": "@angular-devkit/build-angular:extract-i18n",
+          "options": {
+            "browserTarget": "pcoloring-zorro:build"
+          }
+        }
+      }
+    }
+  },
+  "defaultProject": "pcoloring-zorro"
+}

+ 10954 - 0
zorro/package-lock.json

@@ -0,0 +1,10954 @@
+{
+  "name": "pcoloring-zorro",
+  "version": "0.1.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "pcoloring-zorro",
+      "version": "0.1.0",
+      "dependencies": {
+        "@angular/animations": "~13.3.0",
+        "@angular/cdk": "~13.3.0",
+        "@angular/cli": "^13.3.11",
+        "@angular/common": "~13.3.0",
+        "@angular/compiler": "~13.3.0",
+        "@angular/core": "~13.3.0",
+        "@angular/forms": "~13.3.0",
+        "@angular/localize": "~13.3.0",
+        "@angular/platform-browser": "~13.3.0",
+        "@angular/platform-browser-dynamic": "~13.3.0",
+        "@angular/router": "~13.3.0",
+        "@ng-bootstrap/ng-bootstrap": "^12.0.1",
+        "@ngneat/edit-in-place": "^1.6.1",
+        "@popperjs/core": "^2.11.4",
+        "accesscontrol": "^2.2.1",
+        "bootstrap": "^5.1.3",
+        "browser-image-compression": "^2.0.2",
+        "date-fns": "^2.28.0",
+        "gl-matrix": "^3.4.3",
+        "ng-zorro-antd": "^13.1.1",
+        "ngx-color-picker": "^12.0.1",
+        "pace-js": "^1.2.4",
+        "rxjs": "~7.5.0",
+        "sortablejs": "^1.15.0",
+        "tslib": "^2.3.0",
+        "zone.js": "~0.11.4"
+      },
+      "devDependencies": {
+        "@angular-devkit/build-angular": "~13.3.3",
+        "@angular/compiler-cli": "~13.3.0",
+        "@types/gl-matrix": "^3.2.0",
+        "@types/node": "^12.11.1",
+        "@types/offscreencanvas": "^2019.7.3",
+        "@types/sortablejs": "^1.10.7",
+        "typescript": "~4.6.2"
+      }
+    },
+    "node_modules/@ampproject/remapping": {
+      "version": "2.2.0",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.1.0",
+        "@jridgewell/trace-mapping": "^0.3.9"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@angular-devkit/architect": {
+      "version": "0.1303.11",
+      "license": "MIT",
+      "dependencies": {
+        "@angular-devkit/core": "13.3.11",
+        "rxjs": "6.6.7"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.15.0 || >=16.10.0",
+        "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+        "yarn": ">= 1.13.0"
+      }
+    },
+    "node_modules/@angular-devkit/architect/node_modules/rxjs": {
+      "version": "6.6.7",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "tslib": "^1.9.0"
+      },
+      "engines": {
+        "npm": ">=2.0.0"
+      }
+    },
+    "node_modules/@angular-devkit/architect/node_modules/tslib": {
+      "version": "1.14.1",
+      "license": "0BSD"
+    },
+    "node_modules/@angular-devkit/build-angular": {
+      "version": "13.3.11",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-13.3.11.tgz",
+      "integrity": "sha512-H4tpdmRu+6HSjsL+swV/8qj8v0YSDq6lpb31EYajlBB6fDj+YJQvHgaWvexSWl6eIqgDKXcujhNUjNi1enjwHw==",
+      "dev": true,
+      "dependencies": {
+        "@ampproject/remapping": "2.2.0",
+        "@angular-devkit/architect": "0.1303.11",
+        "@angular-devkit/build-webpack": "0.1303.11",
+        "@angular-devkit/core": "13.3.11",
+        "@babel/core": "7.16.12",
+        "@babel/generator": "7.16.8",
+        "@babel/helper-annotate-as-pure": "7.16.7",
+        "@babel/plugin-proposal-async-generator-functions": "7.16.8",
+        "@babel/plugin-transform-async-to-generator": "7.16.8",
+        "@babel/plugin-transform-runtime": "7.16.10",
+        "@babel/preset-env": "7.16.11",
+        "@babel/runtime": "7.16.7",
+        "@babel/template": "7.16.7",
+        "@discoveryjs/json-ext": "0.5.6",
+        "@ngtools/webpack": "13.3.11",
+        "ansi-colors": "4.1.1",
+        "babel-loader": "8.2.5",
+        "babel-plugin-istanbul": "6.1.1",
+        "browserslist": "^4.9.1",
+        "cacache": "15.3.0",
+        "circular-dependency-plugin": "5.2.2",
+        "copy-webpack-plugin": "10.2.1",
+        "core-js": "3.20.3",
+        "critters": "0.0.16",
+        "css-loader": "6.5.1",
+        "esbuild-wasm": "0.14.22",
+        "glob": "7.2.0",
+        "https-proxy-agent": "5.0.0",
+        "inquirer": "8.2.0",
+        "jsonc-parser": "3.0.0",
+        "karma-source-map-support": "1.4.0",
+        "less": "4.1.2",
+        "less-loader": "10.2.0",
+        "license-webpack-plugin": "4.0.2",
+        "loader-utils": "3.2.1",
+        "mini-css-extract-plugin": "2.5.3",
+        "minimatch": "3.0.5",
+        "open": "8.4.0",
+        "ora": "5.4.1",
+        "parse5-html-rewriting-stream": "6.0.1",
+        "piscina": "3.2.0",
+        "postcss": "8.4.5",
+        "postcss-import": "14.0.2",
+        "postcss-loader": "6.2.1",
+        "postcss-preset-env": "7.2.3",
+        "regenerator-runtime": "0.13.9",
+        "resolve-url-loader": "5.0.0",
+        "rxjs": "6.6.7",
+        "sass": "1.49.9",
+        "sass-loader": "12.4.0",
+        "semver": "7.3.5",
+        "source-map-loader": "3.0.1",
+        "source-map-support": "0.5.21",
+        "stylus": "0.56.0",
+        "stylus-loader": "6.2.0",
+        "terser": "5.14.2",
+        "text-table": "0.2.0",
+        "tree-kill": "1.2.2",
+        "tslib": "2.3.1",
+        "webpack": "5.76.1",
+        "webpack-dev-middleware": "5.3.0",
+        "webpack-dev-server": "4.7.3",
+        "webpack-merge": "5.8.0",
+        "webpack-subresource-integrity": "5.1.0"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.15.0 || >=16.10.0",
+        "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+        "yarn": ">= 1.13.0"
+      },
+      "optionalDependencies": {
+        "esbuild": "0.14.22"
+      },
+      "peerDependencies": {
+        "@angular/compiler-cli": "^13.0.0 || ^13.3.0-rc.0",
+        "@angular/localize": "^13.0.0 || ^13.3.0-rc.0",
+        "@angular/service-worker": "^13.0.0 || ^13.3.0-rc.0",
+        "karma": "^6.3.0",
+        "ng-packagr": "^13.0.0",
+        "protractor": "^7.0.0",
+        "tailwindcss": "^2.0.0 || ^3.0.0",
+        "typescript": ">=4.4.3 <4.7"
+      },
+      "peerDependenciesMeta": {
+        "@angular/localize": {
+          "optional": true
+        },
+        "@angular/service-worker": {
+          "optional": true
+        },
+        "karma": {
+          "optional": true
+        },
+        "ng-packagr": {
+          "optional": true
+        },
+        "protractor": {
+          "optional": true
+        },
+        "tailwindcss": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@angular-devkit/build-angular/node_modules/@babel/core": {
+      "version": "7.16.12",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.16.12.tgz",
+      "integrity": "sha512-dK5PtG1uiN2ikk++5OzSYsitZKny4wOCD0nrO4TqnW4BVBTQ2NGS3NgilvT/TEyxTST7LNyWV/T4tXDoD3fOgg==",
+      "dev": true,
+      "dependencies": {
+        "@babel/code-frame": "^7.16.7",
+        "@babel/generator": "^7.16.8",
+        "@babel/helper-compilation-targets": "^7.16.7",
+        "@babel/helper-module-transforms": "^7.16.7",
+        "@babel/helpers": "^7.16.7",
+        "@babel/parser": "^7.16.12",
+        "@babel/template": "^7.16.7",
+        "@babel/traverse": "^7.16.10",
+        "@babel/types": "^7.16.8",
+        "convert-source-map": "^1.7.0",
+        "debug": "^4.1.0",
+        "gensync": "^1.0.0-beta.2",
+        "json5": "^2.1.2",
+        "semver": "^6.3.0",
+        "source-map": "^0.5.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/babel"
+      }
+    },
+    "node_modules/@angular-devkit/build-angular/node_modules/@babel/core/node_modules/semver": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/@angular-devkit/build-angular/node_modules/@babel/generator": {
+      "version": "7.16.8",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.16.8.tgz",
+      "integrity": "sha512-1ojZwE9+lOXzcWdWmO6TbUzDfqLD39CmEhN8+2cX9XkDo5yW1OpgfejfliysR2AWLpMamTiOiAp/mtroaymhpw==",
+      "dev": true,
+      "dependencies": {
+        "@babel/types": "^7.16.8",
+        "jsesc": "^2.5.1",
+        "source-map": "^0.5.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@angular-devkit/build-angular/node_modules/@babel/template": {
+      "version": "7.16.7",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz",
+      "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==",
+      "dev": true,
+      "dependencies": {
+        "@babel/code-frame": "^7.16.7",
+        "@babel/parser": "^7.16.7",
+        "@babel/types": "^7.16.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@angular-devkit/build-angular/node_modules/https-proxy-agent": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
+      "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
+      "dev": true,
+      "dependencies": {
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/@angular-devkit/build-angular/node_modules/postcss": {
+      "version": "8.4.5",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.5.tgz",
+      "integrity": "sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==",
+      "dev": true,
+      "dependencies": {
+        "nanoid": "^3.1.30",
+        "picocolors": "^1.0.0",
+        "source-map-js": "^1.0.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/postcss/"
+      }
+    },
+    "node_modules/@angular-devkit/build-angular/node_modules/rxjs": {
+      "version": "6.6.7",
+      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
+      "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
+      "dev": true,
+      "dependencies": {
+        "tslib": "^1.9.0"
+      },
+      "engines": {
+        "npm": ">=2.0.0"
+      }
+    },
+    "node_modules/@angular-devkit/build-angular/node_modules/rxjs/node_modules/tslib": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+      "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+      "dev": true
+    },
+    "node_modules/@angular-devkit/build-angular/node_modules/source-map": {
+      "version": "0.5.7",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+      "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/@angular-devkit/build-angular/node_modules/tslib": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+      "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
+      "dev": true
+    },
+    "node_modules/@angular-devkit/build-webpack": {
+      "version": "0.1303.11",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1303.11.tgz",
+      "integrity": "sha512-599pWAQLq7i/fmEZLb7PaNU6nmPC3EZbJk1nU/UBcpx7FWs9e0o2XQE2PCAs0buqtQxVjSgY6kMO8ex5dUmgUQ==",
+      "dev": true,
+      "dependencies": {
+        "@angular-devkit/architect": "0.1303.11",
+        "rxjs": "6.6.7"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.15.0 || >=16.10.0",
+        "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+        "yarn": ">= 1.13.0"
+      },
+      "peerDependencies": {
+        "webpack": "^5.30.0",
+        "webpack-dev-server": "^4.0.0"
+      }
+    },
+    "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": {
+      "version": "6.6.7",
+      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
+      "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
+      "dev": true,
+      "dependencies": {
+        "tslib": "^1.9.0"
+      },
+      "engines": {
+        "npm": ">=2.0.0"
+      }
+    },
+    "node_modules/@angular-devkit/build-webpack/node_modules/tslib": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+      "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+      "dev": true
+    },
+    "node_modules/@angular-devkit/core": {
+      "version": "13.3.11",
+      "license": "MIT",
+      "dependencies": {
+        "ajv": "8.9.0",
+        "ajv-formats": "2.1.1",
+        "fast-json-stable-stringify": "2.1.0",
+        "magic-string": "0.25.7",
+        "rxjs": "6.6.7",
+        "source-map": "0.7.3"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.15.0 || >=16.10.0",
+        "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+        "yarn": ">= 1.13.0"
+      },
+      "peerDependencies": {
+        "chokidar": "^3.5.2"
+      },
+      "peerDependenciesMeta": {
+        "chokidar": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@angular-devkit/core/node_modules/rxjs": {
+      "version": "6.6.7",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "tslib": "^1.9.0"
+      },
+      "engines": {
+        "npm": ">=2.0.0"
+      }
+    },
+    "node_modules/@angular-devkit/core/node_modules/tslib": {
+      "version": "1.14.1",
+      "license": "0BSD"
+    },
+    "node_modules/@angular-devkit/schematics": {
+      "version": "13.3.11",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-13.3.11.tgz",
+      "integrity": "sha512-ben+EGXpCrClnIVAAnEQmhQdKmnnqFhMp5BqMxgOslSYBAmCutLA6rBu5vsc8kZcGian1wt+lueF7G1Uk5cGBg==",
+      "dependencies": {
+        "@angular-devkit/core": "13.3.11",
+        "jsonc-parser": "3.0.0",
+        "magic-string": "0.25.7",
+        "ora": "5.4.1",
+        "rxjs": "6.6.7"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.15.0 || >=16.10.0",
+        "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+        "yarn": ">= 1.13.0"
+      }
+    },
+    "node_modules/@angular-devkit/schematics/node_modules/rxjs": {
+      "version": "6.6.7",
+      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
+      "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
+      "dependencies": {
+        "tslib": "^1.9.0"
+      },
+      "engines": {
+        "npm": ">=2.0.0"
+      }
+    },
+    "node_modules/@angular-devkit/schematics/node_modules/tslib": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+      "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+    },
+    "node_modules/@angular/animations": {
+      "version": "13.3.12",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.3.0"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.15.0 || >=16.10.0"
+      },
+      "peerDependencies": {
+        "@angular/core": "13.3.12"
+      }
+    },
+    "node_modules/@angular/cdk": {
+      "version": "13.3.9",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.3.0"
+      },
+      "optionalDependencies": {
+        "parse5": "^5.0.0"
+      },
+      "peerDependencies": {
+        "@angular/common": "^13.0.0 || ^14.0.0-0",
+        "@angular/core": "^13.0.0 || ^14.0.0-0",
+        "rxjs": "^6.5.3 || ^7.4.0"
+      }
+    },
+    "node_modules/@angular/cli": {
+      "version": "13.3.11",
+      "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-13.3.11.tgz",
+      "integrity": "sha512-LTuQ1wC/VJiHqHx8nYJCx0EJv1Ek7R6VvP/5vmr/+M8oVvJ2zSh/aIbcPg6BTL0YEfMI6nX41mUjPBUfF0q2OA==",
+      "hasInstallScript": true,
+      "dependencies": {
+        "@angular-devkit/architect": "0.1303.11",
+        "@angular-devkit/core": "13.3.11",
+        "@angular-devkit/schematics": "13.3.11",
+        "@schematics/angular": "13.3.11",
+        "@yarnpkg/lockfile": "1.1.0",
+        "ansi-colors": "4.1.1",
+        "debug": "4.3.3",
+        "ini": "2.0.0",
+        "inquirer": "8.2.0",
+        "jsonc-parser": "3.0.0",
+        "npm-package-arg": "8.1.5",
+        "npm-pick-manifest": "6.1.1",
+        "open": "8.4.0",
+        "ora": "5.4.1",
+        "pacote": "12.0.3",
+        "resolve": "1.22.0",
+        "semver": "7.3.5",
+        "symbol-observable": "4.0.0",
+        "uuid": "8.3.2"
+      },
+      "bin": {
+        "ng": "bin/ng.js"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.15.0 || >=16.10.0",
+        "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+        "yarn": ">= 1.13.0"
+      }
+    },
+    "node_modules/@angular/common": {
+      "version": "13.3.12",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.3.0"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.15.0 || >=16.10.0"
+      },
+      "peerDependencies": {
+        "@angular/core": "13.3.12",
+        "rxjs": "^6.5.3 || ^7.4.0"
+      }
+    },
+    "node_modules/@angular/compiler": {
+      "version": "13.3.12",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.3.0"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.15.0 || >=16.10.0"
+      }
+    },
+    "node_modules/@angular/compiler-cli": {
+      "version": "13.3.12",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/core": "^7.17.2",
+        "chokidar": "^3.0.0",
+        "convert-source-map": "^1.5.1",
+        "dependency-graph": "^0.11.0",
+        "magic-string": "^0.26.0",
+        "reflect-metadata": "^0.1.2",
+        "semver": "^7.0.0",
+        "sourcemap-codec": "^1.4.8",
+        "tslib": "^2.3.0",
+        "yargs": "^17.2.1"
+      },
+      "bin": {
+        "ng-xi18n": "bundles/src/bin/ng_xi18n.js",
+        "ngc": "bundles/src/bin/ngc.js",
+        "ngcc": "bundles/ngcc/main-ngcc.js"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.15.0 || >=16.10.0"
+      },
+      "peerDependencies": {
+        "@angular/compiler": "13.3.12",
+        "typescript": ">=4.4.2 <4.7"
+      }
+    },
+    "node_modules/@angular/compiler-cli/node_modules/@babel/core": {
+      "version": "7.21.4",
+      "license": "MIT",
+      "dependencies": {
+        "@ampproject/remapping": "^2.2.0",
+        "@babel/code-frame": "^7.21.4",
+        "@babel/generator": "^7.21.4",
+        "@babel/helper-compilation-targets": "^7.21.4",
+        "@babel/helper-module-transforms": "^7.21.2",
+        "@babel/helpers": "^7.21.0",
+        "@babel/parser": "^7.21.4",
+        "@babel/template": "^7.20.7",
+        "@babel/traverse": "^7.21.4",
+        "@babel/types": "^7.21.4",
+        "convert-source-map": "^1.7.0",
+        "debug": "^4.1.0",
+        "gensync": "^1.0.0-beta.2",
+        "json5": "^2.2.2",
+        "semver": "^6.3.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/babel"
+      }
+    },
+    "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": {
+      "version": "6.3.0",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/@angular/compiler-cli/node_modules/@babel/generator": {
+      "version": "7.21.4",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.21.4",
+        "@jridgewell/gen-mapping": "^0.3.2",
+        "@jridgewell/trace-mapping": "^0.3.17",
+        "jsesc": "^2.5.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@angular/compiler-cli/node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.3",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/set-array": "^1.0.1",
+        "@jridgewell/sourcemap-codec": "^1.4.10",
+        "@jridgewell/trace-mapping": "^0.3.9"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@angular/compiler-cli/node_modules/magic-string": {
+      "version": "0.26.7",
+      "license": "MIT",
+      "dependencies": {
+        "sourcemap-codec": "^1.4.8"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@angular/core": {
+      "version": "13.3.12",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.3.0"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.15.0 || >=16.10.0"
+      },
+      "peerDependencies": {
+        "rxjs": "^6.5.3 || ^7.4.0",
+        "zone.js": "~0.11.4"
+      }
+    },
+    "node_modules/@angular/forms": {
+      "version": "13.3.12",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.3.0"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.15.0 || >=16.10.0"
+      },
+      "peerDependencies": {
+        "@angular/common": "13.3.12",
+        "@angular/core": "13.3.12",
+        "@angular/platform-browser": "13.3.12",
+        "rxjs": "^6.5.3 || ^7.4.0"
+      }
+    },
+    "node_modules/@angular/localize": {
+      "version": "13.3.12",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/core": "7.17.2",
+        "glob": "7.2.0",
+        "yargs": "^17.2.1"
+      },
+      "bin": {
+        "localize-extract": "tools/bundles/src/extract/cli.js",
+        "localize-migrate": "tools/bundles/src/migrate/cli.js",
+        "localize-translate": "tools/bundles/src/translate/cli.js"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.15.0 || >=16.10.0"
+      },
+      "peerDependencies": {
+        "@angular/compiler": "13.3.12",
+        "@angular/compiler-cli": "13.3.12"
+      }
+    },
+    "node_modules/@angular/localize/node_modules/@babel/core": {
+      "version": "7.17.2",
+      "license": "MIT",
+      "dependencies": {
+        "@ampproject/remapping": "^2.0.0",
+        "@babel/code-frame": "^7.16.7",
+        "@babel/generator": "^7.17.0",
+        "@babel/helper-compilation-targets": "^7.16.7",
+        "@babel/helper-module-transforms": "^7.16.7",
+        "@babel/helpers": "^7.17.2",
+        "@babel/parser": "^7.17.0",
+        "@babel/template": "^7.16.7",
+        "@babel/traverse": "^7.17.0",
+        "@babel/types": "^7.17.0",
+        "convert-source-map": "^1.7.0",
+        "debug": "^4.1.0",
+        "gensync": "^1.0.0-beta.2",
+        "json5": "^2.1.2",
+        "semver": "^6.3.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/babel"
+      }
+    },
+    "node_modules/@angular/localize/node_modules/@babel/generator": {
+      "version": "7.21.4",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.21.4",
+        "@jridgewell/gen-mapping": "^0.3.2",
+        "@jridgewell/trace-mapping": "^0.3.17",
+        "jsesc": "^2.5.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@angular/localize/node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.3",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/set-array": "^1.0.1",
+        "@jridgewell/sourcemap-codec": "^1.4.10",
+        "@jridgewell/trace-mapping": "^0.3.9"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@angular/localize/node_modules/semver": {
+      "version": "6.3.0",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/@angular/platform-browser": {
+      "version": "13.3.12",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.3.0"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.15.0 || >=16.10.0"
+      },
+      "peerDependencies": {
+        "@angular/animations": "13.3.12",
+        "@angular/common": "13.3.12",
+        "@angular/core": "13.3.12"
+      },
+      "peerDependenciesMeta": {
+        "@angular/animations": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@angular/platform-browser-dynamic": {
+      "version": "13.3.12",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.3.0"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.15.0 || >=16.10.0"
+      },
+      "peerDependencies": {
+        "@angular/common": "13.3.12",
+        "@angular/compiler": "13.3.12",
+        "@angular/core": "13.3.12",
+        "@angular/platform-browser": "13.3.12"
+      }
+    },
+    "node_modules/@angular/router": {
+      "version": "13.3.12",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.3.0"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.15.0 || >=16.10.0"
+      },
+      "peerDependencies": {
+        "@angular/common": "13.3.12",
+        "@angular/core": "13.3.12",
+        "@angular/platform-browser": "13.3.12",
+        "rxjs": "^6.5.3 || ^7.4.0"
+      }
+    },
+    "node_modules/@ant-design/colors": {
+      "version": "5.1.1",
+      "license": "MIT",
+      "dependencies": {
+        "@ctrl/tinycolor": "^3.3.1"
+      }
+    },
+    "node_modules/@ant-design/icons-angular": {
+      "version": "13.1.0",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/colors": "^5.0.0",
+        "tslib": "^2.0.0"
+      },
+      "peerDependencies": {
+        "@angular/common": "^13.0.1",
+        "@angular/core": "^13.0.0",
+        "@angular/platform-browser": "^13.0.1",
+        "rxjs": "^6.4.0 || ^7.4.0"
+      }
+    },
+    "node_modules/@assemblyscript/loader": {
+      "version": "0.10.1",
+      "dev": true,
+      "license": "Apache-2.0"
+    },
+    "node_modules/@babel/code-frame": {
+      "version": "7.21.4",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/highlight": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/compat-data": {
+      "version": "7.21.4",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/core": {
+      "version": "7.20.12",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz",
+      "integrity": "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==",
+      "dependencies": {
+        "@ampproject/remapping": "^2.1.0",
+        "@babel/code-frame": "^7.18.6",
+        "@babel/generator": "^7.20.7",
+        "@babel/helper-compilation-targets": "^7.20.7",
+        "@babel/helper-module-transforms": "^7.20.11",
+        "@babel/helpers": "^7.20.7",
+        "@babel/parser": "^7.20.7",
+        "@babel/template": "^7.20.7",
+        "@babel/traverse": "^7.20.12",
+        "@babel/types": "^7.20.7",
+        "convert-source-map": "^1.7.0",
+        "debug": "^4.1.0",
+        "gensync": "^1.0.0-beta.2",
+        "json5": "^2.2.2",
+        "semver": "^6.3.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/babel"
+      }
+    },
+    "node_modules/@babel/core/node_modules/semver": {
+      "version": "6.3.0",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/@babel/generator": {
+      "version": "7.20.14",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.14.tgz",
+      "integrity": "sha512-AEmuXHdcD3A52HHXxaTmYlb8q/xMEhoRP67B3T4Oq7lbmSoqroMZzjnGj3+i1io3pdnF8iBYVu4Ilj+c4hBxYg==",
+      "dependencies": {
+        "@babel/types": "^7.20.7",
+        "@jridgewell/gen-mapping": "^0.3.2",
+        "jsesc": "^2.5.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.3",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
+      "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
+      "dependencies": {
+        "@jridgewell/set-array": "^1.0.1",
+        "@jridgewell/sourcemap-codec": "^1.4.10",
+        "@jridgewell/trace-mapping": "^0.3.9"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/helper-annotate-as-pure": {
+      "version": "7.16.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz",
+      "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==",
+      "dev": true,
+      "dependencies": {
+        "@babel/types": "^7.16.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": {
+      "version": "7.18.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz",
+      "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-explode-assignable-expression": "^7.18.6",
+        "@babel/types": "^7.18.9"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-compilation-targets": {
+      "version": "7.21.4",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/compat-data": "^7.21.4",
+        "@babel/helper-validator-option": "^7.21.0",
+        "browserslist": "^4.21.3",
+        "lru-cache": "^5.1.1",
+        "semver": "^6.3.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+      "version": "6.3.0",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/@babel/helper-create-class-features-plugin": {
+      "version": "7.21.4",
+      "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.4.tgz",
+      "integrity": "sha512-46QrX2CQlaFRF4TkwfTt6nJD7IHq8539cCL7SDpqWSDeJKY1xylKKY5F/33mJhLZ3mFvKv2gGrVS6NkyF6qs+Q==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-annotate-as-pure": "^7.18.6",
+        "@babel/helper-environment-visitor": "^7.18.9",
+        "@babel/helper-function-name": "^7.21.0",
+        "@babel/helper-member-expression-to-functions": "^7.21.0",
+        "@babel/helper-optimise-call-expression": "^7.18.6",
+        "@babel/helper-replace-supers": "^7.20.7",
+        "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0",
+        "@babel/helper-split-export-declaration": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-annotate-as-pure": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz",
+      "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/types": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-create-regexp-features-plugin": {
+      "version": "7.21.4",
+      "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.4.tgz",
+      "integrity": "sha512-M00OuhU+0GyZ5iBBN9czjugzWrEq2vDpf/zCYHxxf93ul/Q5rv+a5h+/+0WnI1AebHNVtl5bFV0qsJoH23DbfA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-annotate-as-pure": "^7.18.6",
+        "regexpu-core": "^5.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/helper-annotate-as-pure": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz",
+      "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/types": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-define-polyfill-provider": {
+      "version": "0.3.3",
+      "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz",
+      "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-compilation-targets": "^7.17.7",
+        "@babel/helper-plugin-utils": "^7.16.7",
+        "debug": "^4.1.1",
+        "lodash.debounce": "^4.0.8",
+        "resolve": "^1.14.2",
+        "semver": "^6.1.2"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.4.0-0"
+      }
+    },
+    "node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/@babel/helper-environment-visitor": {
+      "version": "7.18.9",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-explode-assignable-expression": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz",
+      "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==",
+      "dev": true,
+      "dependencies": {
+        "@babel/types": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-function-name": {
+      "version": "7.21.0",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/template": "^7.20.7",
+        "@babel/types": "^7.21.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-hoist-variables": {
+      "version": "7.18.6",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-member-expression-to-functions": {
+      "version": "7.21.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz",
+      "integrity": "sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q==",
+      "dev": true,
+      "dependencies": {
+        "@babel/types": "^7.21.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-imports": {
+      "version": "7.21.4",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.21.4"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-transforms": {
+      "version": "7.21.2",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-environment-visitor": "^7.18.9",
+        "@babel/helper-module-imports": "^7.18.6",
+        "@babel/helper-simple-access": "^7.20.2",
+        "@babel/helper-split-export-declaration": "^7.18.6",
+        "@babel/helper-validator-identifier": "^7.19.1",
+        "@babel/template": "^7.20.7",
+        "@babel/traverse": "^7.21.2",
+        "@babel/types": "^7.21.2"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-optimise-call-expression": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz",
+      "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/types": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-plugin-utils": {
+      "version": "7.20.2",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-remap-async-to-generator": {
+      "version": "7.18.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz",
+      "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-annotate-as-pure": "^7.18.6",
+        "@babel/helper-environment-visitor": "^7.18.9",
+        "@babel/helper-wrap-function": "^7.18.9",
+        "@babel/types": "^7.18.9"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-annotate-as-pure": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz",
+      "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/types": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-replace-supers": {
+      "version": "7.20.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz",
+      "integrity": "sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-environment-visitor": "^7.18.9",
+        "@babel/helper-member-expression-to-functions": "^7.20.7",
+        "@babel/helper-optimise-call-expression": "^7.18.6",
+        "@babel/template": "^7.20.7",
+        "@babel/traverse": "^7.20.7",
+        "@babel/types": "^7.20.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-simple-access": {
+      "version": "7.20.2",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.20.2"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
+      "version": "7.20.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz",
+      "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==",
+      "dev": true,
+      "dependencies": {
+        "@babel/types": "^7.20.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-split-export-declaration": {
+      "version": "7.18.6",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.19.4",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.19.1",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-option": {
+      "version": "7.21.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-wrap-function": {
+      "version": "7.20.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz",
+      "integrity": "sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-function-name": "^7.19.0",
+        "@babel/template": "^7.18.10",
+        "@babel/traverse": "^7.20.5",
+        "@babel/types": "^7.20.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helpers": {
+      "version": "7.21.0",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/template": "^7.20.7",
+        "@babel/traverse": "^7.21.0",
+        "@babel/types": "^7.21.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/highlight": {
+      "version": "7.18.6",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.18.6",
+        "chalk": "^2.0.0",
+        "js-tokens": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.21.4",
+      "license": "MIT",
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz",
+      "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
+      "version": "7.20.7",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz",
+      "integrity": "sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.20.2",
+        "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0",
+        "@babel/plugin-proposal-optional-chaining": "^7.20.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.13.0"
+      }
+    },
+    "node_modules/@babel/plugin-proposal-async-generator-functions": {
+      "version": "7.16.8",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.8.tgz",
+      "integrity": "sha512-71YHIvMuiuqWJQkebWJtdhQTfd4Q4mF76q2IX37uZPkG9+olBxsX+rH1vkhFto4UeJZ9dPY2s+mDvhDm1u2BGQ==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.16.7",
+        "@babel/helper-remap-async-to-generator": "^7.16.8",
+        "@babel/plugin-syntax-async-generators": "^7.8.4"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-proposal-class-properties": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz",
+      "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-create-class-features-plugin": "^7.18.6",
+        "@babel/helper-plugin-utils": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-proposal-class-static-block": {
+      "version": "7.21.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz",
+      "integrity": "sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-create-class-features-plugin": "^7.21.0",
+        "@babel/helper-plugin-utils": "^7.20.2",
+        "@babel/plugin-syntax-class-static-block": "^7.14.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.12.0"
+      }
+    },
+    "node_modules/@babel/plugin-proposal-dynamic-import": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz",
+      "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.18.6",
+        "@babel/plugin-syntax-dynamic-import": "^7.8.3"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-proposal-export-namespace-from": {
+      "version": "7.18.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz",
+      "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.18.9",
+        "@babel/plugin-syntax-export-namespace-from": "^7.8.3"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-proposal-json-strings": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz",
+      "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.18.6",
+        "@babel/plugin-syntax-json-strings": "^7.8.3"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-proposal-logical-assignment-operators": {
+      "version": "7.20.7",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz",
+      "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.20.2",
+        "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz",
+      "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.18.6",
+        "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-proposal-numeric-separator": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz",
+      "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.18.6",
+        "@babel/plugin-syntax-numeric-separator": "^7.10.4"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-proposal-object-rest-spread": {
+      "version": "7.20.7",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz",
+      "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==",
+      "dev": true,
+      "dependencies": {
+        "@babel/compat-data": "^7.20.5",
+        "@babel/helper-compilation-targets": "^7.20.7",
+        "@babel/helper-plugin-utils": "^7.20.2",
+        "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
+        "@babel/plugin-transform-parameters": "^7.20.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-proposal-optional-catch-binding": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz",
+      "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.18.6",
+        "@babel/plugin-syntax-optional-catch-binding": "^7.8.3"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-proposal-optional-chaining": {
+      "version": "7.21.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz",
+      "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.20.2",
+        "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0",
+        "@babel/plugin-syntax-optional-chaining": "^7.8.3"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-proposal-private-methods": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz",
+      "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-create-class-features-plugin": "^7.18.6",
+        "@babel/helper-plugin-utils": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-proposal-private-property-in-object": {
+      "version": "7.21.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0.tgz",
+      "integrity": "sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-annotate-as-pure": "^7.18.6",
+        "@babel/helper-create-class-features-plugin": "^7.21.0",
+        "@babel/helper-plugin-utils": "^7.20.2",
+        "@babel/plugin-syntax-private-property-in-object": "^7.14.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-proposal-private-property-in-object/node_modules/@babel/helper-annotate-as-pure": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz",
+      "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/types": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/plugin-proposal-unicode-property-regex": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz",
+      "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-create-regexp-features-plugin": "^7.18.6",
+        "@babel/helper-plugin-utils": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=4"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-async-generators": {
+      "version": "7.8.4",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz",
+      "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.8.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-class-properties": {
+      "version": "7.12.13",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz",
+      "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.12.13"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-class-static-block": {
+      "version": "7.14.5",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz",
+      "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.14.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-dynamic-import": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz",
+      "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.8.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-export-namespace-from": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz",
+      "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.8.3"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-json-strings": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz",
+      "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.8.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-logical-assignment-operators": {
+      "version": "7.10.4",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz",
+      "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.10.4"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz",
+      "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.8.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-numeric-separator": {
+      "version": "7.10.4",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz",
+      "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.10.4"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-object-rest-spread": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz",
+      "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.8.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-optional-catch-binding": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz",
+      "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.8.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-optional-chaining": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz",
+      "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.8.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-private-property-in-object": {
+      "version": "7.14.5",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz",
+      "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.14.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-top-level-await": {
+      "version": "7.14.5",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz",
+      "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.14.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-arrow-functions": {
+      "version": "7.20.7",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz",
+      "integrity": "sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.20.2"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-async-to-generator": {
+      "version": "7.16.8",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.16.8.tgz",
+      "integrity": "sha512-MtmUmTJQHCnyJVrScNzNlofQJ3dLFuobYn3mwOTKHnSCMtbNsqvF71GQmJfFjdrXSsAA7iysFmYWw4bXZ20hOg==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-module-imports": "^7.16.7",
+        "@babel/helper-plugin-utils": "^7.16.7",
+        "@babel/helper-remap-async-to-generator": "^7.16.8"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-block-scoped-functions": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz",
+      "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-block-scoping": {
+      "version": "7.21.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.21.0.tgz",
+      "integrity": "sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.20.2"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-classes": {
+      "version": "7.21.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.21.0.tgz",
+      "integrity": "sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-annotate-as-pure": "^7.18.6",
+        "@babel/helper-compilation-targets": "^7.20.7",
+        "@babel/helper-environment-visitor": "^7.18.9",
+        "@babel/helper-function-name": "^7.21.0",
+        "@babel/helper-optimise-call-expression": "^7.18.6",
+        "@babel/helper-plugin-utils": "^7.20.2",
+        "@babel/helper-replace-supers": "^7.20.7",
+        "@babel/helper-split-export-declaration": "^7.18.6",
+        "globals": "^11.1.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-annotate-as-pure": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz",
+      "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/types": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-computed-properties": {
+      "version": "7.20.7",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz",
+      "integrity": "sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.20.2",
+        "@babel/template": "^7.20.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-destructuring": {
+      "version": "7.21.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.21.3.tgz",
+      "integrity": "sha512-bp6hwMFzuiE4HqYEyoGJ/V2LeIWn+hLVKc4pnj++E5XQptwhtcGmSayM029d/j2X1bPKGTlsyPwAubuU22KhMA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.20.2"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-dotall-regex": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz",
+      "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-create-regexp-features-plugin": "^7.18.6",
+        "@babel/helper-plugin-utils": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-duplicate-keys": {
+      "version": "7.18.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz",
+      "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.18.9"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-exponentiation-operator": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz",
+      "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6",
+        "@babel/helper-plugin-utils": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-for-of": {
+      "version": "7.21.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.0.tgz",
+      "integrity": "sha512-LlUYlydgDkKpIY7mcBWvyPPmMcOphEyYA27Ef4xpbh1IiDNLr0kZsos2nf92vz3IccvJI25QUwp86Eo5s6HmBQ==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.20.2"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-function-name": {
+      "version": "7.18.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz",
+      "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-compilation-targets": "^7.18.9",
+        "@babel/helper-function-name": "^7.18.9",
+        "@babel/helper-plugin-utils": "^7.18.9"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-literals": {
+      "version": "7.18.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz",
+      "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.18.9"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-member-expression-literals": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz",
+      "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-modules-amd": {
+      "version": "7.20.11",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz",
+      "integrity": "sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-module-transforms": "^7.20.11",
+        "@babel/helper-plugin-utils": "^7.20.2"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-modules-commonjs": {
+      "version": "7.21.2",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.2.tgz",
+      "integrity": "sha512-Cln+Yy04Gxua7iPdj6nOV96smLGjpElir5YwzF0LBPKoPlLDNJePNlrGGaybAJkd0zKRnOVXOgizSqPYMNYkzA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-module-transforms": "^7.21.2",
+        "@babel/helper-plugin-utils": "^7.20.2",
+        "@babel/helper-simple-access": "^7.20.2"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-modules-systemjs": {
+      "version": "7.20.11",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz",
+      "integrity": "sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-hoist-variables": "^7.18.6",
+        "@babel/helper-module-transforms": "^7.20.11",
+        "@babel/helper-plugin-utils": "^7.20.2",
+        "@babel/helper-validator-identifier": "^7.19.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-modules-umd": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz",
+      "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-module-transforms": "^7.18.6",
+        "@babel/helper-plugin-utils": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-named-capturing-groups-regex": {
+      "version": "7.20.5",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz",
+      "integrity": "sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-create-regexp-features-plugin": "^7.20.5",
+        "@babel/helper-plugin-utils": "^7.20.2"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-new-target": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz",
+      "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-object-super": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz",
+      "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.18.6",
+        "@babel/helper-replace-supers": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-parameters": {
+      "version": "7.21.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.21.3.tgz",
+      "integrity": "sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.20.2"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-property-literals": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz",
+      "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-regenerator": {
+      "version": "7.20.5",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz",
+      "integrity": "sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.20.2",
+        "regenerator-transform": "^0.15.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-reserved-words": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz",
+      "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-runtime": {
+      "version": "7.16.10",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.16.10.tgz",
+      "integrity": "sha512-9nwTiqETv2G7xI4RvXHNfpGdr8pAA+Q/YtN3yLK7OoK7n9OibVm/xymJ838a9A6E/IciOLPj82lZk0fW6O4O7w==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-module-imports": "^7.16.7",
+        "@babel/helper-plugin-utils": "^7.16.7",
+        "babel-plugin-polyfill-corejs2": "^0.3.0",
+        "babel-plugin-polyfill-corejs3": "^0.5.0",
+        "babel-plugin-polyfill-regenerator": "^0.3.0",
+        "semver": "^6.3.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-runtime/node_modules/semver": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/@babel/plugin-transform-shorthand-properties": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz",
+      "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-spread": {
+      "version": "7.20.7",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz",
+      "integrity": "sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.20.2",
+        "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-sticky-regex": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz",
+      "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-template-literals": {
+      "version": "7.18.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz",
+      "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.18.9"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-typeof-symbol": {
+      "version": "7.18.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz",
+      "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.18.9"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-unicode-escapes": {
+      "version": "7.18.10",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz",
+      "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.18.9"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-unicode-regex": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz",
+      "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-create-regexp-features-plugin": "^7.18.6",
+        "@babel/helper-plugin-utils": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/preset-env": {
+      "version": "7.16.11",
+      "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.16.11.tgz",
+      "integrity": "sha512-qcmWG8R7ZW6WBRPZK//y+E3Cli151B20W1Rv7ln27vuPaXU/8TKms6jFdiJtF7UDTxcrb7mZd88tAeK9LjdT8g==",
+      "dev": true,
+      "dependencies": {
+        "@babel/compat-data": "^7.16.8",
+        "@babel/helper-compilation-targets": "^7.16.7",
+        "@babel/helper-plugin-utils": "^7.16.7",
+        "@babel/helper-validator-option": "^7.16.7",
+        "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.16.7",
+        "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.16.7",
+        "@babel/plugin-proposal-async-generator-functions": "^7.16.8",
+        "@babel/plugin-proposal-class-properties": "^7.16.7",
+        "@babel/plugin-proposal-class-static-block": "^7.16.7",
+        "@babel/plugin-proposal-dynamic-import": "^7.16.7",
+        "@babel/plugin-proposal-export-namespace-from": "^7.16.7",
+        "@babel/plugin-proposal-json-strings": "^7.16.7",
+        "@babel/plugin-proposal-logical-assignment-operators": "^7.16.7",
+        "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.7",
+        "@babel/plugin-proposal-numeric-separator": "^7.16.7",
+        "@babel/plugin-proposal-object-rest-spread": "^7.16.7",
+        "@babel/plugin-proposal-optional-catch-binding": "^7.16.7",
+        "@babel/plugin-proposal-optional-chaining": "^7.16.7",
+        "@babel/plugin-proposal-private-methods": "^7.16.11",
+        "@babel/plugin-proposal-private-property-in-object": "^7.16.7",
+        "@babel/plugin-proposal-unicode-property-regex": "^7.16.7",
+        "@babel/plugin-syntax-async-generators": "^7.8.4",
+        "@babel/plugin-syntax-class-properties": "^7.12.13",
+        "@babel/plugin-syntax-class-static-block": "^7.14.5",
+        "@babel/plugin-syntax-dynamic-import": "^7.8.3",
+        "@babel/plugin-syntax-export-namespace-from": "^7.8.3",
+        "@babel/plugin-syntax-json-strings": "^7.8.3",
+        "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4",
+        "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
+        "@babel/plugin-syntax-numeric-separator": "^7.10.4",
+        "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
+        "@babel/plugin-syntax-optional-catch-binding": "^7.8.3",
+        "@babel/plugin-syntax-optional-chaining": "^7.8.3",
+        "@babel/plugin-syntax-private-property-in-object": "^7.14.5",
+        "@babel/plugin-syntax-top-level-await": "^7.14.5",
+        "@babel/plugin-transform-arrow-functions": "^7.16.7",
+        "@babel/plugin-transform-async-to-generator": "^7.16.8",
+        "@babel/plugin-transform-block-scoped-functions": "^7.16.7",
+        "@babel/plugin-transform-block-scoping": "^7.16.7",
+        "@babel/plugin-transform-classes": "^7.16.7",
+        "@babel/plugin-transform-computed-properties": "^7.16.7",
+        "@babel/plugin-transform-destructuring": "^7.16.7",
+        "@babel/plugin-transform-dotall-regex": "^7.16.7",
+        "@babel/plugin-transform-duplicate-keys": "^7.16.7",
+        "@babel/plugin-transform-exponentiation-operator": "^7.16.7",
+        "@babel/plugin-transform-for-of": "^7.16.7",
+        "@babel/plugin-transform-function-name": "^7.16.7",
+        "@babel/plugin-transform-literals": "^7.16.7",
+        "@babel/plugin-transform-member-expression-literals": "^7.16.7",
+        "@babel/plugin-transform-modules-amd": "^7.16.7",
+        "@babel/plugin-transform-modules-commonjs": "^7.16.8",
+        "@babel/plugin-transform-modules-systemjs": "^7.16.7",
+        "@babel/plugin-transform-modules-umd": "^7.16.7",
+        "@babel/plugin-transform-named-capturing-groups-regex": "^7.16.8",
+        "@babel/plugin-transform-new-target": "^7.16.7",
+        "@babel/plugin-transform-object-super": "^7.16.7",
+        "@babel/plugin-transform-parameters": "^7.16.7",
+        "@babel/plugin-transform-property-literals": "^7.16.7",
+        "@babel/plugin-transform-regenerator": "^7.16.7",
+        "@babel/plugin-transform-reserved-words": "^7.16.7",
+        "@babel/plugin-transform-shorthand-properties": "^7.16.7",
+        "@babel/plugin-transform-spread": "^7.16.7",
+        "@babel/plugin-transform-sticky-regex": "^7.16.7",
+        "@babel/plugin-transform-template-literals": "^7.16.7",
+        "@babel/plugin-transform-typeof-symbol": "^7.16.7",
+        "@babel/plugin-transform-unicode-escapes": "^7.16.7",
+        "@babel/plugin-transform-unicode-regex": "^7.16.7",
+        "@babel/preset-modules": "^0.1.5",
+        "@babel/types": "^7.16.8",
+        "babel-plugin-polyfill-corejs2": "^0.3.0",
+        "babel-plugin-polyfill-corejs3": "^0.5.0",
+        "babel-plugin-polyfill-regenerator": "^0.3.0",
+        "core-js-compat": "^3.20.2",
+        "semver": "^6.3.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/preset-env/node_modules/semver": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/@babel/preset-modules": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz",
+      "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.0.0",
+        "@babel/plugin-proposal-unicode-property-regex": "^7.4.4",
+        "@babel/plugin-transform-dotall-regex": "^7.4.4",
+        "@babel/types": "^7.4.4",
+        "esutils": "^2.0.2"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/regjsgen": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz",
+      "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==",
+      "dev": true
+    },
+    "node_modules/@babel/runtime": {
+      "version": "7.16.7",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz",
+      "integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==",
+      "dev": true,
+      "dependencies": {
+        "regenerator-runtime": "^0.13.4"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/template": {
+      "version": "7.20.7",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz",
+      "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==",
+      "dependencies": {
+        "@babel/code-frame": "^7.18.6",
+        "@babel/parser": "^7.20.7",
+        "@babel/types": "^7.20.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/traverse": {
+      "version": "7.21.4",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.21.4",
+        "@babel/generator": "^7.21.4",
+        "@babel/helper-environment-visitor": "^7.18.9",
+        "@babel/helper-function-name": "^7.21.0",
+        "@babel/helper-hoist-variables": "^7.18.6",
+        "@babel/helper-split-export-declaration": "^7.18.6",
+        "@babel/parser": "^7.21.4",
+        "@babel/types": "^7.21.4",
+        "debug": "^4.1.0",
+        "globals": "^11.1.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/traverse/node_modules/@babel/generator": {
+      "version": "7.21.4",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.21.4",
+        "@jridgewell/gen-mapping": "^0.3.2",
+        "@jridgewell/trace-mapping": "^0.3.17",
+        "jsesc": "^2.5.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/traverse/node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.3",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/set-array": "^1.0.1",
+        "@jridgewell/sourcemap-codec": "^1.4.10",
+        "@jridgewell/trace-mapping": "^0.3.9"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.21.4",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.19.4",
+        "@babel/helper-validator-identifier": "^7.19.1",
+        "to-fast-properties": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@csstools/postcss-progressive-custom-properties": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz",
+      "integrity": "sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==",
+      "dev": true,
+      "dependencies": {
+        "postcss-value-parser": "^4.2.0"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >=16"
+      },
+      "peerDependencies": {
+        "postcss": "^8.3"
+      }
+    },
+    "node_modules/@csstools/selector-specificity": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz",
+      "integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==",
+      "dev": true,
+      "engines": {
+        "node": "^14 || ^16 || >=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/csstools"
+      },
+      "peerDependencies": {
+        "postcss-selector-parser": "^6.0.10"
+      }
+    },
+    "node_modules/@ctrl/tinycolor": {
+      "version": "3.6.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@discoveryjs/json-ext": {
+      "version": "0.5.6",
+      "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz",
+      "integrity": "sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA==",
+      "dev": true,
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/@gar/promisify": {
+      "version": "1.1.3",
+      "license": "MIT"
+    },
+    "node_modules/@istanbuljs/load-nyc-config": {
+      "version": "1.1.0",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "camelcase": "^5.3.1",
+        "find-up": "^4.1.0",
+        "get-package-type": "^0.1.0",
+        "js-yaml": "^3.13.1",
+        "resolve-from": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/@istanbuljs/schema": {
+      "version": "0.1.3",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.1.1",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/set-array": "^1.0.0",
+        "@jridgewell/sourcemap-codec": "^1.4.10"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/set-array": {
+      "version": "1.1.2",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/source-map": {
+      "version": "0.3.3",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.0",
+        "@jridgewell/trace-mapping": "^0.3.9"
+      }
+    },
+    "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.3",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/set-array": "^1.0.1",
+        "@jridgewell/sourcemap-codec": "^1.4.10",
+        "@jridgewell/trace-mapping": "^0.3.9"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.4.15",
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.18",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "3.1.0",
+        "@jridgewell/sourcemap-codec": "1.4.14"
+      }
+    },
+    "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.4.14",
+      "license": "MIT"
+    },
+    "node_modules/@ng-bootstrap/ng-bootstrap": {
+      "version": "12.1.2",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.3.0"
+      },
+      "peerDependencies": {
+        "@angular/common": "^13.0.0",
+        "@angular/core": "^13.0.0",
+        "@angular/forms": "^13.0.0",
+        "@angular/localize": "^13.0.0",
+        "@popperjs/core": "^2.10.2",
+        "rxjs": "^6.5.3 || ^7.4.0"
+      }
+    },
+    "node_modules/@ngneat/edit-in-place": {
+      "version": "1.6.1",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.0.0"
+      },
+      "peerDependencies": {
+        "@angular/common": ">=8.0.0",
+        "@angular/core": ">=8.0.0"
+      }
+    },
+    "node_modules/@ngtools/webpack": {
+      "version": "13.3.11",
+      "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-13.3.11.tgz",
+      "integrity": "sha512-gB33hTbc/RJmHyIgSUYj8ErPazhYYm7yfapOnvwHdYhCjrj1TKkR1ierOlhJtpfBYUQg6FChdl2YpyIQNPjWMA==",
+      "dev": true,
+      "engines": {
+        "node": "^12.20.0 || ^14.15.0 || >=16.10.0",
+        "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+        "yarn": ">= 1.13.0"
+      },
+      "peerDependencies": {
+        "@angular/compiler-cli": "^13.0.0",
+        "typescript": ">=4.4.3 <4.7",
+        "webpack": "^5.30.0"
+      }
+    },
+    "node_modules/@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dev": true,
+      "dependencies": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "dev": true,
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dev": true,
+      "dependencies": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@npmcli/fs": {
+      "version": "1.1.1",
+      "license": "ISC",
+      "dependencies": {
+        "@gar/promisify": "^1.0.1",
+        "semver": "^7.3.5"
+      }
+    },
+    "node_modules/@npmcli/git": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-2.1.0.tgz",
+      "integrity": "sha512-/hBFX/QG1b+N7PZBFs0bi+evgRZcK9nWBxQKZkGoXUT5hJSwl5c4d7y8/hm+NQZRPhQ67RzFaj5UM9YeyKoryw==",
+      "dependencies": {
+        "@npmcli/promise-spawn": "^1.3.2",
+        "lru-cache": "^6.0.0",
+        "mkdirp": "^1.0.4",
+        "npm-pick-manifest": "^6.1.1",
+        "promise-inflight": "^1.0.1",
+        "promise-retry": "^2.0.1",
+        "semver": "^7.3.5",
+        "which": "^2.0.2"
+      }
+    },
+    "node_modules/@npmcli/git/node_modules/lru-cache": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@npmcli/git/node_modules/yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+    },
+    "node_modules/@npmcli/installed-package-contents": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-1.0.7.tgz",
+      "integrity": "sha512-9rufe0wnJusCQoLpV9ZPKIVP55itrM5BxOXs10DmdbRfgWtHy1LDyskbwRnBghuB0PrF7pNPOqREVtpz4HqzKw==",
+      "dependencies": {
+        "npm-bundled": "^1.1.1",
+        "npm-normalize-package-bin": "^1.0.1"
+      },
+      "bin": {
+        "installed-package-contents": "index.js"
+      },
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@npmcli/move-file": {
+      "version": "1.1.2",
+      "license": "MIT",
+      "dependencies": {
+        "mkdirp": "^1.0.4",
+        "rimraf": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@npmcli/node-gyp": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-1.0.3.tgz",
+      "integrity": "sha512-fnkhw+fmX65kiLqk6E3BFLXNC26rUhK90zVwe2yncPliVT/Qos3xjhTLE59Df8KnPlcwIERXKVlU1bXoUQ+liA=="
+    },
+    "node_modules/@npmcli/promise-spawn": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-1.3.2.tgz",
+      "integrity": "sha512-QyAGYo/Fbj4MXeGdJcFzZ+FkDkomfRBrPM+9QYJSg+PxgAUL+LU3FneQk37rKR2/zjqkCV1BLHccX98wRXG3Sg==",
+      "dependencies": {
+        "infer-owner": "^1.0.4"
+      }
+    },
+    "node_modules/@npmcli/run-script": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-2.0.0.tgz",
+      "integrity": "sha512-fSan/Pu11xS/TdaTpTB0MRn9guwGU8dye+x56mEVgBEd/QsybBbYcAL0phPXi8SGWFEChkQd6M9qL4y6VOpFig==",
+      "dependencies": {
+        "@npmcli/node-gyp": "^1.0.2",
+        "@npmcli/promise-spawn": "^1.3.2",
+        "node-gyp": "^8.2.0",
+        "read-package-json-fast": "^2.0.1"
+      }
+    },
+    "node_modules/@popperjs/core": {
+      "version": "2.11.7",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/popperjs"
+      }
+    },
+    "node_modules/@schematics/angular": {
+      "version": "13.3.11",
+      "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-13.3.11.tgz",
+      "integrity": "sha512-imKBnKYEse0SBVELZO/753nkpt3eEgpjrYkB+AFWF9YfO/4RGnYXDHoH8CFkzxPH9QQCgNrmsVFNiYGS+P/S1A==",
+      "dependencies": {
+        "@angular-devkit/core": "13.3.11",
+        "@angular-devkit/schematics": "13.3.11",
+        "jsonc-parser": "3.0.0"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.15.0 || >=16.10.0",
+        "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+        "yarn": ">= 1.13.0"
+      }
+    },
+    "node_modules/@tootallnate/once": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
+      "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/@types/body-parser": {
+      "version": "1.19.2",
+      "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
+      "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==",
+      "dev": true,
+      "dependencies": {
+        "@types/connect": "*",
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/bonjour": {
+      "version": "3.5.10",
+      "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz",
+      "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==",
+      "dev": true,
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/connect": {
+      "version": "3.4.35",
+      "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
+      "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
+      "dev": true,
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/connect-history-api-fallback": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz",
+      "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==",
+      "dev": true,
+      "dependencies": {
+        "@types/express-serve-static-core": "*",
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/eslint": {
+      "version": "8.37.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "*",
+        "@types/json-schema": "*"
+      }
+    },
+    "node_modules/@types/eslint-scope": {
+      "version": "3.7.4",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/eslint": "*",
+        "@types/estree": "*"
+      }
+    },
+    "node_modules/@types/estree": {
+      "version": "0.0.51",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/express": {
+      "version": "4.17.17",
+      "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz",
+      "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==",
+      "dev": true,
+      "dependencies": {
+        "@types/body-parser": "*",
+        "@types/express-serve-static-core": "^4.17.33",
+        "@types/qs": "*",
+        "@types/serve-static": "*"
+      }
+    },
+    "node_modules/@types/express-serve-static-core": {
+      "version": "4.17.33",
+      "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz",
+      "integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==",
+      "dev": true,
+      "dependencies": {
+        "@types/node": "*",
+        "@types/qs": "*",
+        "@types/range-parser": "*"
+      }
+    },
+    "node_modules/@types/gl-matrix": {
+      "version": "3.2.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "gl-matrix": "*"
+      }
+    },
+    "node_modules/@types/http-proxy": {
+      "version": "1.17.10",
+      "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.10.tgz",
+      "integrity": "sha512-Qs5aULi+zV1bwKAg5z1PWnDXWmsn+LxIvUGv6E2+OOMYhclZMO+OXd9pYVf2gLykf2I7IV2u7oTHwChPNsvJ7g==",
+      "dev": true,
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/json-schema": {
+      "version": "7.0.11",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/mime": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz",
+      "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==",
+      "dev": true
+    },
+    "node_modules/@types/node": {
+      "version": "12.20.55",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/offscreencanvas": {
+      "version": "2019.7.3",
+      "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
+      "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==",
+      "dev": true
+    },
+    "node_modules/@types/parse-json": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
+      "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
+      "dev": true
+    },
+    "node_modules/@types/qs": {
+      "version": "6.9.7",
+      "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
+      "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
+      "dev": true
+    },
+    "node_modules/@types/range-parser": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
+      "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
+      "dev": true
+    },
+    "node_modules/@types/retry": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
+      "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
+      "dev": true
+    },
+    "node_modules/@types/serve-index": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz",
+      "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==",
+      "dev": true,
+      "dependencies": {
+        "@types/express": "*"
+      }
+    },
+    "node_modules/@types/serve-static": {
+      "version": "1.15.1",
+      "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz",
+      "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==",
+      "dev": true,
+      "dependencies": {
+        "@types/mime": "*",
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/sockjs": {
+      "version": "0.3.33",
+      "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz",
+      "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==",
+      "dev": true,
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/sortablejs": {
+      "version": "1.15.1",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/ws": {
+      "version": "8.5.4",
+      "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz",
+      "integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==",
+      "dev": true,
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@webassemblyjs/ast": {
+      "version": "1.11.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@webassemblyjs/helper-numbers": "1.11.1",
+        "@webassemblyjs/helper-wasm-bytecode": "1.11.1"
+      }
+    },
+    "node_modules/@webassemblyjs/floating-point-hex-parser": {
+      "version": "1.11.1",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@webassemblyjs/helper-api-error": {
+      "version": "1.11.1",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@webassemblyjs/helper-buffer": {
+      "version": "1.11.1",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@webassemblyjs/helper-numbers": {
+      "version": "1.11.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@webassemblyjs/floating-point-hex-parser": "1.11.1",
+        "@webassemblyjs/helper-api-error": "1.11.1",
+        "@xtuc/long": "4.2.2"
+      }
+    },
+    "node_modules/@webassemblyjs/helper-wasm-bytecode": {
+      "version": "1.11.1",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@webassemblyjs/helper-wasm-section": {
+      "version": "1.11.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@webassemblyjs/ast": "1.11.1",
+        "@webassemblyjs/helper-buffer": "1.11.1",
+        "@webassemblyjs/helper-wasm-bytecode": "1.11.1",
+        "@webassemblyjs/wasm-gen": "1.11.1"
+      }
+    },
+    "node_modules/@webassemblyjs/ieee754": {
+      "version": "1.11.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@xtuc/ieee754": "^1.2.0"
+      }
+    },
+    "node_modules/@webassemblyjs/leb128": {
+      "version": "1.11.1",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@xtuc/long": "4.2.2"
+      }
+    },
+    "node_modules/@webassemblyjs/utf8": {
+      "version": "1.11.1",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@webassemblyjs/wasm-edit": {
+      "version": "1.11.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@webassemblyjs/ast": "1.11.1",
+        "@webassemblyjs/helper-buffer": "1.11.1",
+        "@webassemblyjs/helper-wasm-bytecode": "1.11.1",
+        "@webassemblyjs/helper-wasm-section": "1.11.1",
+        "@webassemblyjs/wasm-gen": "1.11.1",
+        "@webassemblyjs/wasm-opt": "1.11.1",
+        "@webassemblyjs/wasm-parser": "1.11.1",
+        "@webassemblyjs/wast-printer": "1.11.1"
+      }
+    },
+    "node_modules/@webassemblyjs/wasm-gen": {
+      "version": "1.11.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@webassemblyjs/ast": "1.11.1",
+        "@webassemblyjs/helper-wasm-bytecode": "1.11.1",
+        "@webassemblyjs/ieee754": "1.11.1",
+        "@webassemblyjs/leb128": "1.11.1",
+        "@webassemblyjs/utf8": "1.11.1"
+      }
+    },
+    "node_modules/@webassemblyjs/wasm-opt": {
+      "version": "1.11.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@webassemblyjs/ast": "1.11.1",
+        "@webassemblyjs/helper-buffer": "1.11.1",
+        "@webassemblyjs/wasm-gen": "1.11.1",
+        "@webassemblyjs/wasm-parser": "1.11.1"
+      }
+    },
+    "node_modules/@webassemblyjs/wasm-parser": {
+      "version": "1.11.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@webassemblyjs/ast": "1.11.1",
+        "@webassemblyjs/helper-api-error": "1.11.1",
+        "@webassemblyjs/helper-wasm-bytecode": "1.11.1",
+        "@webassemblyjs/ieee754": "1.11.1",
+        "@webassemblyjs/leb128": "1.11.1",
+        "@webassemblyjs/utf8": "1.11.1"
+      }
+    },
+    "node_modules/@webassemblyjs/wast-printer": {
+      "version": "1.11.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@webassemblyjs/ast": "1.11.1",
+        "@xtuc/long": "4.2.2"
+      }
+    },
+    "node_modules/@xtuc/ieee754": {
+      "version": "1.2.0",
+      "dev": true,
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@xtuc/long": {
+      "version": "4.2.2",
+      "dev": true,
+      "license": "Apache-2.0"
+    },
+    "node_modules/@yarnpkg/lockfile": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
+      "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ=="
+    },
+    "node_modules/abab": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
+      "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
+      "dev": true
+    },
+    "node_modules/abbrev": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+      "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
+    },
+    "node_modules/accepts": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+      "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+      "dev": true,
+      "dependencies": {
+        "mime-types": "~2.1.34",
+        "negotiator": "0.6.3"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/accesscontrol": {
+      "version": "2.2.1",
+      "license": "MIT",
+      "dependencies": {
+        "notation": "^1.3.6"
+      }
+    },
+    "node_modules/acorn": {
+      "version": "8.8.2",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/acorn-import-assertions": {
+      "version": "1.8.0",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "acorn": "^8"
+      }
+    },
+    "node_modules/adjust-sourcemap-loader": {
+      "version": "4.0.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "loader-utils": "^2.0.0",
+        "regex-parser": "^2.2.11"
+      },
+      "engines": {
+        "node": ">=8.9"
+      }
+    },
+    "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": {
+      "version": "2.0.4",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "big.js": "^5.2.2",
+        "emojis-list": "^3.0.0",
+        "json5": "^2.1.2"
+      },
+      "engines": {
+        "node": ">=8.9.0"
+      }
+    },
+    "node_modules/agent-base": {
+      "version": "6.0.2",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/agentkeepalive": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz",
+      "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==",
+      "dependencies": {
+        "humanize-ms": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 8.0.0"
+      }
+    },
+    "node_modules/aggregate-error": {
+      "version": "3.1.0",
+      "license": "MIT",
+      "dependencies": {
+        "clean-stack": "^2.0.0",
+        "indent-string": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ajv": {
+      "version": "8.9.0",
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "json-schema-traverse": "^1.0.0",
+        "require-from-string": "^2.0.2",
+        "uri-js": "^4.2.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/ajv-formats": {
+      "version": "2.1.1",
+      "license": "MIT",
+      "dependencies": {
+        "ajv": "^8.0.0"
+      },
+      "peerDependencies": {
+        "ajv": "^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "ajv": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/ajv-keywords": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
+      "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
+      "dev": true,
+      "dependencies": {
+        "fast-deep-equal": "^3.1.3"
+      },
+      "peerDependencies": {
+        "ajv": "^8.8.2"
+      }
+    },
+    "node_modules/ansi-colors": {
+      "version": "4.1.1",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/ansi-escapes": {
+      "version": "4.3.2",
+      "license": "MIT",
+      "dependencies": {
+        "type-fest": "^0.21.3"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/ansi-html-community": {
+      "version": "0.0.8",
+      "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz",
+      "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==",
+      "dev": true,
+      "engines": [
+        "node >= 0.8.0"
+      ],
+      "bin": {
+        "ansi-html": "bin/ansi-html"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "3.2.1",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^1.9.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/anymatch": {
+      "version": "3.1.3",
+      "license": "ISC",
+      "dependencies": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/aproba": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
+      "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ=="
+    },
+    "node_modules/are-we-there-yet": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz",
+      "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==",
+      "deprecated": "This package is no longer supported.",
+      "dependencies": {
+        "delegates": "^1.0.0",
+        "readable-stream": "^3.6.0"
+      },
+      "engines": {
+        "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+      }
+    },
+    "node_modules/argparse": {
+      "version": "1.0.10",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "sprintf-js": "~1.0.2"
+      }
+    },
+    "node_modules/array-flatten": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz",
+      "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==",
+      "dev": true
+    },
+    "node_modules/array-union": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz",
+      "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==",
+      "dev": true,
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/async": {
+      "version": "2.6.4",
+      "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
+      "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
+      "dev": true,
+      "dependencies": {
+        "lodash": "^4.17.14"
+      }
+    },
+    "node_modules/atob": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
+      "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
+      "dev": true,
+      "bin": {
+        "atob": "bin/atob.js"
+      },
+      "engines": {
+        "node": ">= 4.5.0"
+      }
+    },
+    "node_modules/autoprefixer": {
+      "version": "10.4.14",
+      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz",
+      "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+        }
+      ],
+      "dependencies": {
+        "browserslist": "^4.21.5",
+        "caniuse-lite": "^1.0.30001464",
+        "fraction.js": "^4.2.0",
+        "normalize-range": "^0.1.2",
+        "picocolors": "^1.0.0",
+        "postcss-value-parser": "^4.2.0"
+      },
+      "bin": {
+        "autoprefixer": "bin/autoprefixer"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      },
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
+    "node_modules/babel-loader": {
+      "version": "8.2.5",
+      "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.5.tgz",
+      "integrity": "sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ==",
+      "dev": true,
+      "dependencies": {
+        "find-cache-dir": "^3.3.1",
+        "loader-utils": "^2.0.0",
+        "make-dir": "^3.1.0",
+        "schema-utils": "^2.6.5"
+      },
+      "engines": {
+        "node": ">= 8.9"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0",
+        "webpack": ">=2"
+      }
+    },
+    "node_modules/babel-loader/node_modules/loader-utils": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
+      "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
+      "dev": true,
+      "dependencies": {
+        "big.js": "^5.2.2",
+        "emojis-list": "^3.0.0",
+        "json5": "^2.1.2"
+      },
+      "engines": {
+        "node": ">=8.9.0"
+      }
+    },
+    "node_modules/babel-plugin-istanbul": {
+      "version": "6.1.1",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.0.0",
+        "@istanbuljs/load-nyc-config": "^1.0.0",
+        "@istanbuljs/schema": "^0.1.2",
+        "istanbul-lib-instrument": "^5.0.4",
+        "test-exclude": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/babel-plugin-polyfill-corejs2": {
+      "version": "0.3.3",
+      "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz",
+      "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==",
+      "dev": true,
+      "dependencies": {
+        "@babel/compat-data": "^7.17.7",
+        "@babel/helper-define-polyfill-provider": "^0.3.3",
+        "semver": "^6.1.1"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/babel-plugin-polyfill-corejs3": {
+      "version": "0.5.3",
+      "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.3.tgz",
+      "integrity": "sha512-zKsXDh0XjnrUEW0mxIHLfjBfnXSMr5Q/goMe/fxpQnLm07mcOZiIZHBNWCMx60HmdvjxfXcalac0tfFg0wqxyw==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-define-polyfill-provider": "^0.3.2",
+        "core-js-compat": "^3.21.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/babel-plugin-polyfill-regenerator": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz",
+      "integrity": "sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-define-polyfill-provider": "^0.3.1"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "license": "MIT"
+    },
+    "node_modules/base64-js": {
+      "version": "1.5.1",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/batch": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
+      "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==",
+      "dev": true
+    },
+    "node_modules/big.js": {
+      "version": "5.2.2",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/binary-extensions": {
+      "version": "2.2.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/bl": {
+      "version": "4.1.0",
+      "license": "MIT",
+      "dependencies": {
+        "buffer": "^5.5.0",
+        "inherits": "^2.0.4",
+        "readable-stream": "^3.4.0"
+      }
+    },
+    "node_modules/body-parser": {
+      "version": "1.20.1",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
+      "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
+      "dev": true,
+      "dependencies": {
+        "bytes": "3.1.2",
+        "content-type": "~1.0.4",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "destroy": "1.2.0",
+        "http-errors": "2.0.0",
+        "iconv-lite": "0.4.24",
+        "on-finished": "2.4.1",
+        "qs": "6.11.0",
+        "raw-body": "2.5.1",
+        "type-is": "~1.6.18",
+        "unpipe": "1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8",
+        "npm": "1.2.8000 || >= 1.4.16"
+      }
+    },
+    "node_modules/body-parser/node_modules/bytes": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+      "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/body-parser/node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "dev": true,
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/body-parser/node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "dev": true
+    },
+    "node_modules/bonjour": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz",
+      "integrity": "sha512-RaVTblr+OnEli0r/ud8InrU7D+G0y6aJhlxaLa6Pwty4+xoxboF1BsUI45tujvRpbj9dQVoglChqonGAsjEBYg==",
+      "dev": true,
+      "dependencies": {
+        "array-flatten": "^2.1.0",
+        "deep-equal": "^1.0.1",
+        "dns-equal": "^1.0.0",
+        "dns-txt": "^2.0.2",
+        "multicast-dns": "^6.0.1",
+        "multicast-dns-service-types": "^1.1.0"
+      }
+    },
+    "node_modules/boolbase": {
+      "version": "1.0.0",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/bootstrap": {
+      "version": "5.2.3",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/twbs"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/bootstrap"
+        }
+      ],
+      "license": "MIT",
+      "peerDependencies": {
+        "@popperjs/core": "^2.11.6"
+      }
+    },
+    "node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/braces": {
+      "version": "3.0.2",
+      "license": "MIT",
+      "dependencies": {
+        "fill-range": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/browser-image-compression": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/browser-image-compression/-/browser-image-compression-2.0.2.tgz",
+      "integrity": "sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==",
+      "dependencies": {
+        "uzip": "0.20201231.0"
+      }
+    },
+    "node_modules/browserslist": {
+      "version": "4.21.5",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "caniuse-lite": "^1.0.30001449",
+        "electron-to-chromium": "^1.4.284",
+        "node-releases": "^2.0.8",
+        "update-browserslist-db": "^1.0.10"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/buffer": {
+      "version": "5.7.1",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "base64-js": "^1.3.1",
+        "ieee754": "^1.1.13"
+      }
+    },
+    "node_modules/buffer-from": {
+      "version": "1.1.2",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/buffer-indexof": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz",
+      "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==",
+      "dev": true
+    },
+    "node_modules/builtins": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz",
+      "integrity": "sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ=="
+    },
+    "node_modules/bytes": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+      "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/cacache": {
+      "version": "15.3.0",
+      "license": "ISC",
+      "dependencies": {
+        "@npmcli/fs": "^1.0.0",
+        "@npmcli/move-file": "^1.0.1",
+        "chownr": "^2.0.0",
+        "fs-minipass": "^2.0.0",
+        "glob": "^7.1.4",
+        "infer-owner": "^1.0.4",
+        "lru-cache": "^6.0.0",
+        "minipass": "^3.1.1",
+        "minipass-collect": "^1.0.2",
+        "minipass-flush": "^1.0.5",
+        "minipass-pipeline": "^1.2.2",
+        "mkdirp": "^1.0.3",
+        "p-map": "^4.0.0",
+        "promise-inflight": "^1.0.1",
+        "rimraf": "^3.0.2",
+        "ssri": "^8.0.1",
+        "tar": "^6.0.2",
+        "unique-filename": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/cacache/node_modules/lru-cache": {
+      "version": "6.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/cacache/node_modules/yallist": {
+      "version": "4.0.0",
+      "license": "ISC"
+    },
+    "node_modules/call-bind": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+      "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+      "dev": true,
+      "dependencies": {
+        "function-bind": "^1.1.1",
+        "get-intrinsic": "^1.0.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/callsites": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/camelcase": {
+      "version": "5.3.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001478",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "CC-BY-4.0"
+    },
+    "node_modules/chalk": {
+      "version": "2.4.2",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^3.2.1",
+        "escape-string-regexp": "^1.0.5",
+        "supports-color": "^5.3.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/chardet": {
+      "version": "0.7.0",
+      "license": "MIT"
+    },
+    "node_modules/chokidar": {
+      "version": "3.5.3",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://paulmillr.com/funding/"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "anymatch": "~3.1.2",
+        "braces": "~3.0.2",
+        "glob-parent": "~5.1.2",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.6.0"
+      },
+      "engines": {
+        "node": ">= 8.10.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/chownr": {
+      "version": "2.0.0",
+      "license": "ISC",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/chrome-trace-event": {
+      "version": "1.0.3",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0"
+      }
+    },
+    "node_modules/circular-dependency-plugin": {
+      "version": "5.2.2",
+      "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.2.2.tgz",
+      "integrity": "sha512-g38K9Cm5WRwlaH6g03B9OEz/0qRizI+2I7n+Gz+L5DxXJAPAiWQvwlYNm1V1jkdpUv95bOe/ASm2vfi/G560jQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=6.0.0"
+      },
+      "peerDependencies": {
+        "webpack": ">=4.0.1"
+      }
+    },
+    "node_modules/clean-stack": {
+      "version": "2.2.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/cli-cursor": {
+      "version": "3.1.0",
+      "license": "MIT",
+      "dependencies": {
+        "restore-cursor": "^3.1.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/cli-spinners": {
+      "version": "2.8.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/cli-width": {
+      "version": "3.0.0",
+      "license": "ISC",
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/cliui": {
+      "version": "8.0.1",
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.1",
+        "wrap-ansi": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/clone": {
+      "version": "1.0.4",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/clone-deep": {
+      "version": "4.0.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-plain-object": "^2.0.4",
+        "kind-of": "^6.0.2",
+        "shallow-clone": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "1.9.3",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "1.1.3"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.3",
+      "license": "MIT"
+    },
+    "node_modules/color-support": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
+      "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
+      "bin": {
+        "color-support": "bin.js"
+      }
+    },
+    "node_modules/colorette": {
+      "version": "2.0.19",
+      "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz",
+      "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==",
+      "dev": true
+    },
+    "node_modules/commander": {
+      "version": "2.20.3",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/commondir": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
+      "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
+      "dev": true
+    },
+    "node_modules/compressible": {
+      "version": "2.0.18",
+      "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
+      "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
+      "dev": true,
+      "dependencies": {
+        "mime-db": ">= 1.43.0 < 2"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/compression": {
+      "version": "1.7.4",
+      "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz",
+      "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==",
+      "dev": true,
+      "dependencies": {
+        "accepts": "~1.3.5",
+        "bytes": "3.0.0",
+        "compressible": "~2.0.16",
+        "debug": "2.6.9",
+        "on-headers": "~1.0.2",
+        "safe-buffer": "5.1.2",
+        "vary": "~1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/compression/node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "dev": true,
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/compression/node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "dev": true
+    },
+    "node_modules/compression/node_modules/safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+      "dev": true
+    },
+    "node_modules/concat-map": {
+      "version": "0.0.1",
+      "license": "MIT"
+    },
+    "node_modules/connect-history-api-fallback": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz",
+      "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/console-control-strings": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+      "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="
+    },
+    "node_modules/content-disposition": {
+      "version": "0.5.4",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+      "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+      "dev": true,
+      "dependencies": {
+        "safe-buffer": "5.2.1"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/content-type": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+      "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/convert-source-map": {
+      "version": "1.9.0",
+      "license": "MIT"
+    },
+    "node_modules/cookie": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
+      "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie-signature": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+      "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+      "dev": true
+    },
+    "node_modules/copy-anything": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz",
+      "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==",
+      "dev": true,
+      "dependencies": {
+        "is-what": "^3.14.1"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "node_modules/copy-webpack-plugin": {
+      "version": "10.2.1",
+      "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-10.2.1.tgz",
+      "integrity": "sha512-nr81NhCAIpAWXGCK5thrKmfCQ6GDY0L5RN0U+BnIn/7Us55+UCex5ANNsNKmIVtDRnk0Ecf+/kzp9SUVrrBMLg==",
+      "dev": true,
+      "dependencies": {
+        "fast-glob": "^3.2.7",
+        "glob-parent": "^6.0.1",
+        "globby": "^12.0.2",
+        "normalize-path": "^3.0.0",
+        "schema-utils": "^4.0.0",
+        "serialize-javascript": "^6.0.0"
+      },
+      "engines": {
+        "node": ">= 12.20.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "webpack": "^5.1.0"
+      }
+    },
+    "node_modules/copy-webpack-plugin/node_modules/glob-parent": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+      "dev": true,
+      "dependencies": {
+        "is-glob": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/copy-webpack-plugin/node_modules/schema-utils": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz",
+      "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==",
+      "dev": true,
+      "dependencies": {
+        "@types/json-schema": "^7.0.9",
+        "ajv": "^8.8.0",
+        "ajv-formats": "^2.1.1",
+        "ajv-keywords": "^5.0.0"
+      },
+      "engines": {
+        "node": ">= 12.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      }
+    },
+    "node_modules/core-js": {
+      "version": "3.20.3",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.20.3.tgz",
+      "integrity": "sha512-vVl8j8ph6tRS3B8qir40H7yw7voy17xL0piAjlbBUsH7WIfzoedL/ZOr1OV9FyZQLWXsayOJyV4tnRyXR85/ag==",
+      "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.",
+      "dev": true,
+      "hasInstallScript": true,
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/core-js"
+      }
+    },
+    "node_modules/core-js-compat": {
+      "version": "3.30.0",
+      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.30.0.tgz",
+      "integrity": "sha512-P5A2h/9mRYZFIAP+5Ab8ns6083IyVpSclU74UNvbGVQ8VM7n3n3/g2yF3AkKQ9NXz2O+ioxLbEWKnDtgsFamhg==",
+      "dev": true,
+      "dependencies": {
+        "browserslist": "^4.21.5"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/core-js"
+      }
+    },
+    "node_modules/core-util-is": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+      "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+      "dev": true
+    },
+    "node_modules/cosmiconfig": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
+      "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
+      "dev": true,
+      "dependencies": {
+        "@types/parse-json": "^4.0.0",
+        "import-fresh": "^3.2.1",
+        "parse-json": "^5.0.0",
+        "path-type": "^4.0.0",
+        "yaml": "^1.10.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/critters": {
+      "version": "0.0.16",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "chalk": "^4.1.0",
+        "css-select": "^4.2.0",
+        "parse5": "^6.0.1",
+        "parse5-htmlparser2-tree-adapter": "^6.0.1",
+        "postcss": "^8.3.7",
+        "pretty-bytes": "^5.3.0"
+      }
+    },
+    "node_modules/critters/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/critters/node_modules/chalk": {
+      "version": "4.1.2",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/critters/node_modules/color-convert": {
+      "version": "2.0.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/critters/node_modules/color-name": {
+      "version": "1.1.4",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/critters/node_modules/has-flag": {
+      "version": "4.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/critters/node_modules/parse5": {
+      "version": "6.0.1",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/critters/node_modules/supports-color": {
+      "version": "7.2.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+      "dev": true,
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/css": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz",
+      "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==",
+      "dev": true,
+      "dependencies": {
+        "inherits": "^2.0.4",
+        "source-map": "^0.6.1",
+        "source-map-resolve": "^0.6.0"
+      }
+    },
+    "node_modules/css-blank-pseudo": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz",
+      "integrity": "sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==",
+      "dev": true,
+      "dependencies": {
+        "postcss-selector-parser": "^6.0.9"
+      },
+      "bin": {
+        "css-blank-pseudo": "dist/cli.cjs"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >=16"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4"
+      }
+    },
+    "node_modules/css-has-pseudo": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz",
+      "integrity": "sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==",
+      "dev": true,
+      "dependencies": {
+        "postcss-selector-parser": "^6.0.9"
+      },
+      "bin": {
+        "css-has-pseudo": "dist/cli.cjs"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >=16"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4"
+      }
+    },
+    "node_modules/css-loader": {
+      "version": "6.5.1",
+      "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.5.1.tgz",
+      "integrity": "sha512-gEy2w9AnJNnD9Kuo4XAP9VflW/ujKoS9c/syO+uWMlm5igc7LysKzPXaDoR2vroROkSwsTS2tGr1yGGEbZOYZQ==",
+      "dev": true,
+      "dependencies": {
+        "icss-utils": "^5.1.0",
+        "postcss": "^8.2.15",
+        "postcss-modules-extract-imports": "^3.0.0",
+        "postcss-modules-local-by-default": "^4.0.0",
+        "postcss-modules-scope": "^3.0.0",
+        "postcss-modules-values": "^4.0.0",
+        "postcss-value-parser": "^4.1.0",
+        "semver": "^7.3.5"
+      },
+      "engines": {
+        "node": ">= 12.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "webpack": "^5.0.0"
+      }
+    },
+    "node_modules/css-prefers-color-scheme": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz",
+      "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==",
+      "dev": true,
+      "bin": {
+        "css-prefers-color-scheme": "dist/cli.cjs"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >=16"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4"
+      }
+    },
+    "node_modules/css-select": {
+      "version": "4.3.0",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "boolbase": "^1.0.0",
+        "css-what": "^6.0.1",
+        "domhandler": "^4.3.1",
+        "domutils": "^2.8.0",
+        "nth-check": "^2.0.1"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/fb55"
+      }
+    },
+    "node_modules/css-what": {
+      "version": "6.1.0",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">= 6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/fb55"
+      }
+    },
+    "node_modules/css/node_modules/source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/cssdb": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-5.1.0.tgz",
+      "integrity": "sha512-/vqjXhv1x9eGkE/zO6o8ZOI7dgdZbLVLUGyVRbPgk6YipXbW87YzUCcO+Jrmi5bwJlAH6oD+MNeZyRgXea1GZw==",
+      "dev": true
+    },
+    "node_modules/cssesc": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+      "dev": true,
+      "bin": {
+        "cssesc": "bin/cssesc"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/date-fns": {
+      "version": "2.29.3",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.11"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/date-fns"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.3.3",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "2.1.2"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/decode-uri-component": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
+      "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/deep-equal": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz",
+      "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==",
+      "dev": true,
+      "dependencies": {
+        "is-arguments": "^1.0.4",
+        "is-date-object": "^1.0.1",
+        "is-regex": "^1.0.4",
+        "object-is": "^1.0.1",
+        "object-keys": "^1.1.1",
+        "regexp.prototype.flags": "^1.2.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/default-gateway": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz",
+      "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==",
+      "dev": true,
+      "dependencies": {
+        "execa": "^5.0.0"
+      },
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/defaults": {
+      "version": "1.0.4",
+      "license": "MIT",
+      "dependencies": {
+        "clone": "^1.0.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/define-lazy-prop": {
+      "version": "2.0.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/define-properties": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz",
+      "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==",
+      "dev": true,
+      "dependencies": {
+        "has-property-descriptors": "^1.0.0",
+        "object-keys": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/del": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz",
+      "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==",
+      "dev": true,
+      "dependencies": {
+        "globby": "^11.0.1",
+        "graceful-fs": "^4.2.4",
+        "is-glob": "^4.0.1",
+        "is-path-cwd": "^2.2.0",
+        "is-path-inside": "^3.0.2",
+        "p-map": "^4.0.0",
+        "rimraf": "^3.0.2",
+        "slash": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/del/node_modules/array-union": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+      "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/del/node_modules/globby": {
+      "version": "11.1.0",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+      "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+      "dev": true,
+      "dependencies": {
+        "array-union": "^2.1.0",
+        "dir-glob": "^3.0.1",
+        "fast-glob": "^3.2.9",
+        "ignore": "^5.2.0",
+        "merge2": "^1.4.1",
+        "slash": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/del/node_modules/slash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+      "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/delegates": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+      "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="
+    },
+    "node_modules/depd": {
+      "version": "2.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/dependency-graph": {
+      "version": "0.11.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6.0"
+      }
+    },
+    "node_modules/destroy": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+      "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.8",
+        "npm": "1.2.8000 || >= 1.4.16"
+      }
+    },
+    "node_modules/detect-node": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
+      "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
+      "dev": true
+    },
+    "node_modules/dir-glob": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+      "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+      "dev": true,
+      "dependencies": {
+        "path-type": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/dns-equal": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz",
+      "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==",
+      "dev": true
+    },
+    "node_modules/dns-packet": {
+      "version": "1.3.4",
+      "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz",
+      "integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==",
+      "dev": true,
+      "dependencies": {
+        "ip": "^1.1.0",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/dns-packet/node_modules/ip": {
+      "version": "1.1.8",
+      "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz",
+      "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==",
+      "dev": true
+    },
+    "node_modules/dns-txt": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz",
+      "integrity": "sha512-Ix5PrWjphuSoUXV/Zv5gaFHjnaJtb02F2+Si3Ht9dyJ87+Z/lMmy+dpNHtTGraNK958ndXq2i+GLkWsWHcKaBQ==",
+      "dev": true,
+      "dependencies": {
+        "buffer-indexof": "^1.0.0"
+      }
+    },
+    "node_modules/dom-serializer": {
+      "version": "1.4.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "domelementtype": "^2.0.1",
+        "domhandler": "^4.2.0",
+        "entities": "^2.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+      }
+    },
+    "node_modules/domelementtype": {
+      "version": "2.3.0",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fb55"
+        }
+      ],
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/domhandler": {
+      "version": "4.3.1",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "domelementtype": "^2.2.0"
+      },
+      "engines": {
+        "node": ">= 4"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/domhandler?sponsor=1"
+      }
+    },
+    "node_modules/domutils": {
+      "version": "2.8.0",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "dom-serializer": "^1.0.1",
+        "domelementtype": "^2.2.0",
+        "domhandler": "^4.2.0"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/domutils?sponsor=1"
+      }
+    },
+    "node_modules/ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+      "dev": true
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.4.361",
+      "license": "ISC"
+    },
+    "node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "license": "MIT"
+    },
+    "node_modules/emojis-list": {
+      "version": "3.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/encodeurl": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+      "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/encoding": {
+      "version": "0.1.13",
+      "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
+      "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
+      "optional": true,
+      "dependencies": {
+        "iconv-lite": "^0.6.2"
+      }
+    },
+    "node_modules/encoding/node_modules/iconv-lite": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+      "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+      "optional": true,
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/enhanced-resolve": {
+      "version": "5.12.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "^4.2.4",
+        "tapable": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/entities": {
+      "version": "2.2.0",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/env-paths": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+      "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/err-code": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
+      "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="
+    },
+    "node_modules/errno": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
+      "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==",
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "prr": "~1.0.1"
+      },
+      "bin": {
+        "errno": "cli.js"
+      }
+    },
+    "node_modules/error-ex": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+      "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+      "dev": true,
+      "dependencies": {
+        "is-arrayish": "^0.2.1"
+      }
+    },
+    "node_modules/es-module-lexer": {
+      "version": "0.9.3",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/esbuild": {
+      "version": "0.14.22",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.22.tgz",
+      "integrity": "sha512-CjFCFGgYtbFOPrwZNJf7wsuzesx8kqwAffOlbYcFDLFuUtP8xloK1GH+Ai13Qr0RZQf9tE7LMTHJ2iVGJ1SKZA==",
+      "dev": true,
+      "hasInstallScript": true,
+      "optional": true,
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "optionalDependencies": {
+        "esbuild-android-arm64": "0.14.22",
+        "esbuild-darwin-64": "0.14.22",
+        "esbuild-darwin-arm64": "0.14.22",
+        "esbuild-freebsd-64": "0.14.22",
+        "esbuild-freebsd-arm64": "0.14.22",
+        "esbuild-linux-32": "0.14.22",
+        "esbuild-linux-64": "0.14.22",
+        "esbuild-linux-arm": "0.14.22",
+        "esbuild-linux-arm64": "0.14.22",
+        "esbuild-linux-mips64le": "0.14.22",
+        "esbuild-linux-ppc64le": "0.14.22",
+        "esbuild-linux-riscv64": "0.14.22",
+        "esbuild-linux-s390x": "0.14.22",
+        "esbuild-netbsd-64": "0.14.22",
+        "esbuild-openbsd-64": "0.14.22",
+        "esbuild-sunos-64": "0.14.22",
+        "esbuild-windows-32": "0.14.22",
+        "esbuild-windows-64": "0.14.22",
+        "esbuild-windows-arm64": "0.14.22"
+      }
+    },
+    "node_modules/esbuild-android-arm64": {
+      "version": "0.14.22",
+      "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.22.tgz",
+      "integrity": "sha512-k1Uu4uC4UOFgrnTj2zuj75EswFSEBK+H6lT70/DdS4mTAOfs2ECv2I9ZYvr3w0WL0T4YItzJdK7fPNxcPw6YmQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-darwin-64": {
+      "version": "0.14.22",
+      "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.22.tgz",
+      "integrity": "sha512-d8Ceuo6Vw6HM3fW218FB6jTY6O3r2WNcTAU0SGsBkXZ3k8SDoRLd3Nrc//EqzdgYnzDNMNtrWegK2Qsss4THhw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-darwin-arm64": {
+      "version": "0.14.22",
+      "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.22.tgz",
+      "integrity": "sha512-YAt9Tj3SkIUkswuzHxkaNlT9+sg0xvzDvE75LlBo4DI++ogSgSmKNR6B4eUhU5EUUepVXcXdRIdqMq9ppeRqfw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-freebsd-64": {
+      "version": "0.14.22",
+      "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.22.tgz",
+      "integrity": "sha512-ek1HUv7fkXMy87Qm2G4IRohN+Qux4IcnrDBPZGXNN33KAL0pEJJzdTv0hB/42+DCYWylSrSKxk3KUXfqXOoH4A==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-freebsd-arm64": {
+      "version": "0.14.22",
+      "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.22.tgz",
+      "integrity": "sha512-zPh9SzjRvr9FwsouNYTqgqFlsMIW07O8mNXulGeQx6O5ApgGUBZBgtzSlBQXkHi18WjrosYfsvp5nzOKiWzkjQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-32": {
+      "version": "0.14.22",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.22.tgz",
+      "integrity": "sha512-SnpveoE4nzjb9t2hqCIzzTWBM0RzcCINDMBB67H6OXIuDa4KqFqaIgmTchNA9pJKOVLVIKd5FYxNiJStli21qg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-64": {
+      "version": "0.14.22",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.22.tgz",
+      "integrity": "sha512-Zcl9Wg7gKhOWWNqAjygyqzB+fJa19glgl2JG7GtuxHyL1uEnWlpSMytTLMqtfbmRykIHdab797IOZeKwk5g0zg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-arm": {
+      "version": "0.14.22",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.22.tgz",
+      "integrity": "sha512-soPDdbpt/C0XvOOK45p4EFt8HbH5g+0uHs5nUKjHVExfgR7du734kEkXR/mE5zmjrlymk5AA79I0VIvj90WZ4g==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-arm64": {
+      "version": "0.14.22",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.22.tgz",
+      "integrity": "sha512-8q/FRBJtV5IHnQChO3LHh/Jf7KLrxJ/RCTGdBvlVZhBde+dk3/qS9fFsUy+rs3dEi49aAsyVitTwlKw1SUFm+A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-mips64le": {
+      "version": "0.14.22",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.22.tgz",
+      "integrity": "sha512-SiNDfuRXhGh1JQLLA9JPprBgPVFOsGuQ0yDfSPTNxztmVJd8W2mX++c4FfLpAwxuJe183mLuKf7qKCHQs5ZnBQ==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-ppc64le": {
+      "version": "0.14.22",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.22.tgz",
+      "integrity": "sha512-6t/GI9I+3o1EFm2AyN9+TsjdgWCpg2nwniEhjm2qJWtJyJ5VzTXGUU3alCO3evopu8G0hN2Bu1Jhz2YmZD0kng==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-riscv64": {
+      "version": "0.14.22",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.22.tgz",
+      "integrity": "sha512-AyJHipZKe88sc+tp5layovquw5cvz45QXw5SaDgAq2M911wLHiCvDtf/07oDx8eweCyzYzG5Y39Ih568amMTCQ==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-s390x": {
+      "version": "0.14.22",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.22.tgz",
+      "integrity": "sha512-Sz1NjZewTIXSblQDZWEFZYjOK6p8tV6hrshYdXZ0NHTjWE+lwxpOpWeElUGtEmiPcMT71FiuA9ODplqzzSxkzw==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-netbsd-64": {
+      "version": "0.14.22",
+      "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.22.tgz",
+      "integrity": "sha512-TBbCtx+k32xydImsHxvFgsOCuFqCTGIxhzRNbgSL1Z2CKhzxwT92kQMhxort9N/fZM2CkRCPPs5wzQSamtzEHA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-openbsd-64": {
+      "version": "0.14.22",
+      "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.22.tgz",
+      "integrity": "sha512-vK912As725haT313ANZZZN+0EysEEQXWC/+YE4rQvOQzLuxAQc2tjbzlAFREx3C8+uMuZj/q7E5gyVB7TzpcTA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-sunos-64": {
+      "version": "0.14.22",
+      "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.22.tgz",
+      "integrity": "sha512-/mbJdXTW7MTcsPhtfDsDyPEOju9EOABvCjeUU2OJ7fWpX/Em/H3WYDa86tzLUbcVg++BScQDzqV/7RYw5XNY0g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-wasm": {
+      "version": "0.14.22",
+      "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.14.22.tgz",
+      "integrity": "sha512-FOSAM29GN1fWusw0oLMv6JYhoheDIh5+atC72TkJKfIUMID6yISlicoQSd9gsNSFsNBvABvtE2jR4JB1j4FkFw==",
+      "dev": true,
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-windows-32": {
+      "version": "0.14.22",
+      "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.22.tgz",
+      "integrity": "sha512-1vRIkuvPTjeSVK3diVrnMLSbkuE36jxA+8zGLUOrT4bb7E/JZvDRhvtbWXWaveUc/7LbhaNFhHNvfPuSw2QOQg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-windows-64": {
+      "version": "0.14.22",
+      "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.22.tgz",
+      "integrity": "sha512-AxjIDcOmx17vr31C5hp20HIwz1MymtMjKqX4qL6whPj0dT9lwxPexmLj6G1CpR3vFhui6m75EnBEe4QL82SYqw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-windows-arm64": {
+      "version": "0.14.22",
+      "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.22.tgz",
+      "integrity": "sha512-5wvQ+39tHmRhNpu2Fx04l7QfeK3mQ9tKzDqqGR8n/4WUxsFxnVLfDRBGirIfk4AfWlxk60kqirlODPoT5LqMUg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.1.1",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+      "dev": true
+    },
+    "node_modules/escape-string-regexp": {
+      "version": "1.0.5",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/eslint-scope": {
+      "version": "5.1.1",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "esrecurse": "^4.3.0",
+        "estraverse": "^4.1.1"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/esprima": {
+      "version": "4.0.1",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "bin": {
+        "esparse": "bin/esparse.js",
+        "esvalidate": "bin/esvalidate.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/esrecurse": {
+      "version": "4.3.0",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/esrecurse/node_modules/estraverse": {
+      "version": "5.3.0",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/estraverse": {
+      "version": "4.3.0",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/esutils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/eventemitter-asyncresource": {
+      "version": "1.0.0",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/eventemitter3": {
+      "version": "4.0.7",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+      "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+      "dev": true
+    },
+    "node_modules/events": {
+      "version": "3.3.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.x"
+      }
+    },
+    "node_modules/execa": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+      "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+      "dev": true,
+      "dependencies": {
+        "cross-spawn": "^7.0.3",
+        "get-stream": "^6.0.0",
+        "human-signals": "^2.1.0",
+        "is-stream": "^2.0.0",
+        "merge-stream": "^2.0.0",
+        "npm-run-path": "^4.0.1",
+        "onetime": "^5.1.2",
+        "signal-exit": "^3.0.3",
+        "strip-final-newline": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sindresorhus/execa?sponsor=1"
+      }
+    },
+    "node_modules/express": {
+      "version": "4.18.2",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
+      "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
+      "dev": true,
+      "dependencies": {
+        "accepts": "~1.3.8",
+        "array-flatten": "1.1.1",
+        "body-parser": "1.20.1",
+        "content-disposition": "0.5.4",
+        "content-type": "~1.0.4",
+        "cookie": "0.5.0",
+        "cookie-signature": "1.0.6",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "finalhandler": "1.2.0",
+        "fresh": "0.5.2",
+        "http-errors": "2.0.0",
+        "merge-descriptors": "1.0.1",
+        "methods": "~1.1.2",
+        "on-finished": "2.4.1",
+        "parseurl": "~1.3.3",
+        "path-to-regexp": "0.1.7",
+        "proxy-addr": "~2.0.7",
+        "qs": "6.11.0",
+        "range-parser": "~1.2.1",
+        "safe-buffer": "5.2.1",
+        "send": "0.18.0",
+        "serve-static": "1.15.0",
+        "setprototypeof": "1.2.0",
+        "statuses": "2.0.1",
+        "type-is": "~1.6.18",
+        "utils-merge": "1.0.1",
+        "vary": "~1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.10.0"
+      }
+    },
+    "node_modules/express/node_modules/array-flatten": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+      "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+      "dev": true
+    },
+    "node_modules/express/node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "dev": true,
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/express/node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "dev": true
+    },
+    "node_modules/external-editor": {
+      "version": "3.1.0",
+      "license": "MIT",
+      "dependencies": {
+        "chardet": "^0.7.0",
+        "iconv-lite": "^0.4.24",
+        "tmp": "^0.0.33"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "license": "MIT"
+    },
+    "node_modules/fast-glob": {
+      "version": "3.2.12",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
+      "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==",
+      "dev": true,
+      "dependencies": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=8.6.0"
+      }
+    },
+    "node_modules/fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "license": "MIT"
+    },
+    "node_modules/fastq": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
+      "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
+      "dev": true,
+      "dependencies": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "node_modules/faye-websocket": {
+      "version": "0.11.4",
+      "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz",
+      "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==",
+      "dev": true,
+      "dependencies": {
+        "websocket-driver": ">=0.5.1"
+      },
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/figures": {
+      "version": "3.2.0",
+      "license": "MIT",
+      "dependencies": {
+        "escape-string-regexp": "^1.0.5"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/fill-range": {
+      "version": "7.0.1",
+      "license": "MIT",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/finalhandler": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
+      "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
+      "dev": true,
+      "dependencies": {
+        "debug": "2.6.9",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "on-finished": "2.4.1",
+        "parseurl": "~1.3.3",
+        "statuses": "2.0.1",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/finalhandler/node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "dev": true,
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/finalhandler/node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "dev": true
+    },
+    "node_modules/find-cache-dir": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
+      "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==",
+      "dev": true,
+      "dependencies": {
+        "commondir": "^1.0.1",
+        "make-dir": "^3.0.2",
+        "pkg-dir": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/avajs/find-cache-dir?sponsor=1"
+      }
+    },
+    "node_modules/find-up": {
+      "version": "4.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "locate-path": "^5.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.15.2",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
+      "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fraction.js": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
+      "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==",
+      "dev": true,
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "type": "patreon",
+        "url": "https://www.patreon.com/infusion"
+      }
+    },
+    "node_modules/fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fs-minipass": {
+      "version": "2.1.0",
+      "license": "ISC",
+      "dependencies": {
+        "minipass": "^3.0.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/fs-monkey": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz",
+      "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==",
+      "dev": true
+    },
+    "node_modules/fs.realpath": {
+      "version": "1.0.0",
+      "license": "ISC"
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.2",
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.1",
+      "license": "MIT"
+    },
+    "node_modules/functions-have-names": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+      "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+      "dev": true,
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/gauge": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz",
+      "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==",
+      "deprecated": "This package is no longer supported.",
+      "dependencies": {
+        "aproba": "^1.0.3 || ^2.0.0",
+        "color-support": "^1.1.3",
+        "console-control-strings": "^1.1.0",
+        "has-unicode": "^2.0.1",
+        "signal-exit": "^3.0.7",
+        "string-width": "^4.2.3",
+        "strip-ansi": "^6.0.1",
+        "wide-align": "^1.1.5"
+      },
+      "engines": {
+        "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+      }
+    },
+    "node_modules/gensync": {
+      "version": "1.0.0-beta.2",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/get-caller-file": {
+      "version": "2.0.5",
+      "license": "ISC",
+      "engines": {
+        "node": "6.* || 8.* || >= 10.*"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz",
+      "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==",
+      "dev": true,
+      "dependencies": {
+        "function-bind": "^1.1.1",
+        "has": "^1.0.3",
+        "has-symbols": "^1.0.3"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-package-type": {
+      "version": "0.1.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/get-stream": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+      "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/gl-matrix": {
+      "version": "3.4.3",
+      "license": "MIT"
+    },
+    "node_modules/glob": {
+      "version": "7.2.0",
+      "license": "ISC",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.0.4",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/glob-parent": {
+      "version": "5.1.2",
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/glob-to-regexp": {
+      "version": "0.4.1",
+      "dev": true,
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/globals": {
+      "version": "11.12.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/globby": {
+      "version": "12.2.0",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-12.2.0.tgz",
+      "integrity": "sha512-wiSuFQLZ+urS9x2gGPl1H5drc5twabmm4m2gTR27XDFyjUHJUNsS8o/2aKyIF6IoBaR630atdher0XJ5g6OMmA==",
+      "dev": true,
+      "dependencies": {
+        "array-union": "^3.0.1",
+        "dir-glob": "^3.0.1",
+        "fast-glob": "^3.2.7",
+        "ignore": "^5.1.9",
+        "merge2": "^1.4.1",
+        "slash": "^4.0.0"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/graceful-fs": {
+      "version": "4.2.11",
+      "license": "ISC"
+    },
+    "node_modules/handle-thing": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
+      "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==",
+      "dev": true
+    },
+    "node_modules/has": {
+      "version": "1.0.3",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
+    "node_modules/has-flag": {
+      "version": "3.0.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/has-property-descriptors": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
+      "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
+      "dev": true,
+      "dependencies": {
+        "get-intrinsic": "^1.1.1"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+      "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
+      "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+      "dev": true,
+      "dependencies": {
+        "has-symbols": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-unicode": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+      "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="
+    },
+    "node_modules/hdr-histogram-js": {
+      "version": "2.0.3",
+      "dev": true,
+      "license": "BSD",
+      "dependencies": {
+        "@assemblyscript/loader": "^0.10.1",
+        "base64-js": "^1.2.0",
+        "pako": "^1.0.3"
+      }
+    },
+    "node_modules/hdr-histogram-percentiles-obj": {
+      "version": "3.0.0",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/hosted-git-info": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
+      "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==",
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/hosted-git-info/node_modules/lru-cache": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/hosted-git-info/node_modules/yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+    },
+    "node_modules/hpack.js": {
+      "version": "2.1.6",
+      "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
+      "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==",
+      "dev": true,
+      "dependencies": {
+        "inherits": "^2.0.1",
+        "obuf": "^1.0.0",
+        "readable-stream": "^2.0.1",
+        "wbuf": "^1.1.0"
+      }
+    },
+    "node_modules/hpack.js/node_modules/readable-stream": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+      "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+      "dev": true,
+      "dependencies": {
+        "core-util-is": "~1.0.0",
+        "inherits": "~2.0.3",
+        "isarray": "~1.0.0",
+        "process-nextick-args": "~2.0.0",
+        "safe-buffer": "~5.1.1",
+        "string_decoder": "~1.1.1",
+        "util-deprecate": "~1.0.1"
+      }
+    },
+    "node_modules/hpack.js/node_modules/safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+      "dev": true
+    },
+    "node_modules/hpack.js/node_modules/string_decoder": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+      "dev": true,
+      "dependencies": {
+        "safe-buffer": "~5.1.0"
+      }
+    },
+    "node_modules/html-entities": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz",
+      "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==",
+      "dev": true
+    },
+    "node_modules/http-cache-semantics": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
+      "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
+    },
+    "node_modules/http-deceiver": {
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
+      "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==",
+      "dev": true
+    },
+    "node_modules/http-errors": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+      "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+      "dev": true,
+      "dependencies": {
+        "depd": "2.0.0",
+        "inherits": "2.0.4",
+        "setprototypeof": "1.2.0",
+        "statuses": "2.0.1",
+        "toidentifier": "1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/http-parser-js": {
+      "version": "0.5.8",
+      "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz",
+      "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==",
+      "dev": true
+    },
+    "node_modules/http-proxy": {
+      "version": "1.18.1",
+      "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
+      "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
+      "dev": true,
+      "dependencies": {
+        "eventemitter3": "^4.0.0",
+        "follow-redirects": "^1.0.0",
+        "requires-port": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/http-proxy-agent": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz",
+      "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==",
+      "dependencies": {
+        "@tootallnate/once": "1",
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/http-proxy-middleware": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz",
+      "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==",
+      "dev": true,
+      "dependencies": {
+        "@types/http-proxy": "^1.17.8",
+        "http-proxy": "^1.18.1",
+        "is-glob": "^4.0.1",
+        "is-plain-obj": "^3.0.0",
+        "micromatch": "^4.0.2"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "@types/express": "^4.17.13"
+      },
+      "peerDependenciesMeta": {
+        "@types/express": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+      "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+      "dependencies": {
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/human-signals": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+      "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+      "dev": true,
+      "engines": {
+        "node": ">=10.17.0"
+      }
+    },
+    "node_modules/humanize-ms": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
+      "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
+      "dependencies": {
+        "ms": "^2.0.0"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.4.24",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/icss-utils": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
+      "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
+      "dev": true,
+      "engines": {
+        "node": "^10 || ^12 || >= 14"
+      },
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
+    "node_modules/ieee754": {
+      "version": "1.2.1",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/ignore": {
+      "version": "5.2.4",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
+      "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
+      "dev": true,
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/ignore-walk": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-4.0.1.tgz",
+      "integrity": "sha512-rzDQLaW4jQbh2YrOFlJdCtX8qgJTehFRYiUB2r1osqTeDzV/3+Jh8fz1oAPzUThf3iku8Ds4IDqawI5d8mUiQw==",
+      "dependencies": {
+        "minimatch": "^3.0.4"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/image-size": {
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
+      "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
+      "dev": true,
+      "optional": true,
+      "bin": {
+        "image-size": "bin/image-size.js"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/immutable": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz",
+      "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==",
+      "dev": true
+    },
+    "node_modules/import-fresh": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+      "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+      "dev": true,
+      "dependencies": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/import-fresh/node_modules/resolve-from": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/imurmurhash": {
+      "version": "0.1.4",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.19"
+      }
+    },
+    "node_modules/indent-string": {
+      "version": "4.0.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/infer-owner": {
+      "version": "1.0.4",
+      "license": "ISC"
+    },
+    "node_modules/inflight": {
+      "version": "1.0.6",
+      "license": "ISC",
+      "dependencies": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "license": "ISC"
+    },
+    "node_modules/ini": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
+      "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/inquirer": {
+      "version": "8.2.0",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-escapes": "^4.2.1",
+        "chalk": "^4.1.1",
+        "cli-cursor": "^3.1.0",
+        "cli-width": "^3.0.0",
+        "external-editor": "^3.0.3",
+        "figures": "^3.0.0",
+        "lodash": "^4.17.21",
+        "mute-stream": "0.0.8",
+        "ora": "^5.4.1",
+        "run-async": "^2.4.0",
+        "rxjs": "^7.2.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0",
+        "through": "^2.3.6"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/inquirer/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/inquirer/node_modules/chalk": {
+      "version": "4.1.2",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/inquirer/node_modules/color-convert": {
+      "version": "2.0.1",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/inquirer/node_modules/color-name": {
+      "version": "1.1.4",
+      "license": "MIT"
+    },
+    "node_modules/inquirer/node_modules/has-flag": {
+      "version": "4.0.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/inquirer/node_modules/supports-color": {
+      "version": "7.2.0",
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ip-address": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
+      "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
+      "dependencies": {
+        "jsbn": "1.1.0",
+        "sprintf-js": "^1.1.3"
+      },
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/ip-address/node_modules/sprintf-js": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
+      "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="
+    },
+    "node_modules/ipaddr.js": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz",
+      "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==",
+      "dev": true,
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/is-arguments": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
+      "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "has-tostringtag": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-arrayish": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+      "dev": true
+    },
+    "node_modules/is-binary-path": {
+      "version": "2.1.0",
+      "license": "MIT",
+      "dependencies": {
+        "binary-extensions": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-core-module": {
+      "version": "2.12.0",
+      "license": "MIT",
+      "dependencies": {
+        "has": "^1.0.3"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-date-object": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
+      "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+      "dev": true,
+      "dependencies": {
+        "has-tostringtag": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-docker": {
+      "version": "2.2.1",
+      "license": "MIT",
+      "bin": {
+        "is-docker": "cli.js"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "license": "MIT",
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-interactive": {
+      "version": "1.0.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-lambda": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz",
+      "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ=="
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/is-path-cwd": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
+      "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/is-path-inside": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+      "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-plain-obj": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz",
+      "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-plain-object": {
+      "version": "2.0.4",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "isobject": "^3.0.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-regex": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
+      "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "has-tostringtag": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-stream": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+      "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-unicode-supported": {
+      "version": "0.1.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-what": {
+      "version": "3.14.1",
+      "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz",
+      "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==",
+      "dev": true
+    },
+    "node_modules/is-wsl": {
+      "version": "2.2.0",
+      "license": "MIT",
+      "dependencies": {
+        "is-docker": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+      "dev": true
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "license": "ISC"
+    },
+    "node_modules/isobject": {
+      "version": "3.0.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/istanbul-lib-coverage": {
+      "version": "3.2.0",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/istanbul-lib-instrument": {
+      "version": "5.2.1",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@babel/core": "^7.12.3",
+        "@babel/parser": "^7.14.7",
+        "@istanbuljs/schema": "^0.1.2",
+        "istanbul-lib-coverage": "^3.2.0",
+        "semver": "^6.3.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/istanbul-lib-instrument/node_modules/semver": {
+      "version": "6.3.0",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/jest-worker": {
+      "version": "27.5.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*",
+        "merge-stream": "^2.0.0",
+        "supports-color": "^8.0.0"
+      },
+      "engines": {
+        "node": ">= 10.13.0"
+      }
+    },
+    "node_modules/jest-worker/node_modules/has-flag": {
+      "version": "4.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/jest-worker/node_modules/supports-color": {
+      "version": "8.1.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/supports-color?sponsor=1"
+      }
+    },
+    "node_modules/js-tokens": {
+      "version": "4.0.0",
+      "license": "MIT"
+    },
+    "node_modules/js-yaml": {
+      "version": "3.14.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "argparse": "^1.0.7",
+        "esprima": "^4.0.0"
+      },
+      "bin": {
+        "js-yaml": "bin/js-yaml.js"
+      }
+    },
+    "node_modules/jsbn": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
+      "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
+    },
+    "node_modules/jsesc": {
+      "version": "2.5.2",
+      "license": "MIT",
+      "bin": {
+        "jsesc": "bin/jsesc"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/json-parse-even-better-errors": {
+      "version": "2.3.1",
+      "license": "MIT"
+    },
+    "node_modules/json-schema-traverse": {
+      "version": "1.0.0",
+      "license": "MIT"
+    },
+    "node_modules/json5": {
+      "version": "2.2.3",
+      "license": "MIT",
+      "bin": {
+        "json5": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/jsonc-parser": {
+      "version": "3.0.0",
+      "license": "MIT"
+    },
+    "node_modules/jsonparse": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
+      "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==",
+      "engines": [
+        "node >= 0.2.0"
+      ]
+    },
+    "node_modules/karma-source-map-support": {
+      "version": "1.4.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "source-map-support": "^0.5.5"
+      }
+    },
+    "node_modules/kind-of": {
+      "version": "6.0.3",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/klona": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz",
+      "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==",
+      "dev": true,
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/less": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/less/-/less-4.1.2.tgz",
+      "integrity": "sha512-EoQp/Et7OSOVu0aJknJOtlXZsnr8XE8KwuzTHOLeVSEx8pVWUICc8Q0VYRHgzyjX78nMEyC/oztWFbgyhtNfDA==",
+      "dev": true,
+      "dependencies": {
+        "copy-anything": "^2.0.1",
+        "parse-node-version": "^1.0.1",
+        "tslib": "^2.3.0"
+      },
+      "bin": {
+        "lessc": "bin/lessc"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "optionalDependencies": {
+        "errno": "^0.1.1",
+        "graceful-fs": "^4.1.2",
+        "image-size": "~0.5.0",
+        "make-dir": "^2.1.0",
+        "mime": "^1.4.1",
+        "needle": "^2.5.2",
+        "source-map": "~0.6.0"
+      }
+    },
+    "node_modules/less-loader": {
+      "version": "10.2.0",
+      "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-10.2.0.tgz",
+      "integrity": "sha512-AV5KHWvCezW27GT90WATaDnfXBv99llDbtaj4bshq6DvAihMdNjaPDcUMa6EXKLRF+P2opFenJp89BXg91XLYg==",
+      "dev": true,
+      "dependencies": {
+        "klona": "^2.0.4"
+      },
+      "engines": {
+        "node": ">= 12.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "less": "^3.5.0 || ^4.0.0",
+        "webpack": "^5.0.0"
+      }
+    },
+    "node_modules/less/node_modules/make-dir": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
+      "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "pify": "^4.0.1",
+        "semver": "^5.6.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/less/node_modules/pify": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+      "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+      "dev": true,
+      "optional": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/less/node_modules/semver": {
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+      "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+      "dev": true,
+      "optional": true,
+      "bin": {
+        "semver": "bin/semver"
+      }
+    },
+    "node_modules/less/node_modules/source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "dev": true,
+      "optional": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/license-webpack-plugin": {
+      "version": "4.0.2",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "webpack-sources": "^3.0.0"
+      },
+      "peerDependenciesMeta": {
+        "webpack": {
+          "optional": true
+        },
+        "webpack-sources": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/lines-and-columns": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+      "dev": true
+    },
+    "node_modules/loader-runner": {
+      "version": "4.3.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.11.5"
+      }
+    },
+    "node_modules/loader-utils": {
+      "version": "3.2.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 12.13.0"
+      }
+    },
+    "node_modules/locate-path": {
+      "version": "5.0.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-locate": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/lodash": {
+      "version": "4.17.21",
+      "license": "MIT"
+    },
+    "node_modules/lodash.debounce": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+      "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
+      "dev": true
+    },
+    "node_modules/log-symbols": {
+      "version": "4.1.0",
+      "license": "MIT",
+      "dependencies": {
+        "chalk": "^4.1.0",
+        "is-unicode-supported": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/log-symbols/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/log-symbols/node_modules/chalk": {
+      "version": "4.1.2",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/log-symbols/node_modules/color-convert": {
+      "version": "2.0.1",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/log-symbols/node_modules/color-name": {
+      "version": "1.1.4",
+      "license": "MIT"
+    },
+    "node_modules/log-symbols/node_modules/has-flag": {
+      "version": "4.0.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/log-symbols/node_modules/supports-color": {
+      "version": "7.2.0",
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/lru-cache": {
+      "version": "5.1.1",
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^3.0.2"
+      }
+    },
+    "node_modules/magic-string": {
+      "version": "0.25.7",
+      "license": "MIT",
+      "dependencies": {
+        "sourcemap-codec": "^1.4.4"
+      }
+    },
+    "node_modules/make-dir": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+      "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+      "dev": true,
+      "dependencies": {
+        "semver": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/make-dir/node_modules/semver": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/make-fetch-happen": {
+      "version": "9.1.0",
+      "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz",
+      "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==",
+      "dependencies": {
+        "agentkeepalive": "^4.1.3",
+        "cacache": "^15.2.0",
+        "http-cache-semantics": "^4.1.0",
+        "http-proxy-agent": "^4.0.1",
+        "https-proxy-agent": "^5.0.0",
+        "is-lambda": "^1.0.1",
+        "lru-cache": "^6.0.0",
+        "minipass": "^3.1.3",
+        "minipass-collect": "^1.0.2",
+        "minipass-fetch": "^1.3.2",
+        "minipass-flush": "^1.0.5",
+        "minipass-pipeline": "^1.2.4",
+        "negotiator": "^0.6.2",
+        "promise-retry": "^2.0.1",
+        "socks-proxy-agent": "^6.0.0",
+        "ssri": "^8.0.0"
+      },
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/make-fetch-happen/node_modules/lru-cache": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/make-fetch-happen/node_modules/yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+    },
+    "node_modules/media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/memfs": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.0.tgz",
+      "integrity": "sha512-yK6o8xVJlQerz57kvPROwTMgx5WtGwC2ZxDtOUsnGl49rHjYkfQoPNZPCKH73VdLE1BwBu/+Fx/NL8NYMUw2aA==",
+      "dev": true,
+      "dependencies": {
+        "fs-monkey": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 4.0.0"
+      }
+    },
+    "node_modules/merge-descriptors": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+      "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==",
+      "dev": true
+    },
+    "node_modules/merge-stream": {
+      "version": "2.0.0",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/merge2": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/micromatch": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
+      "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+      "dev": true,
+      "dependencies": {
+        "braces": "^3.0.2",
+        "picomatch": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+      "dev": true,
+      "bin": {
+        "mime": "cli.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mimic-fn": {
+      "version": "2.1.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/mini-css-extract-plugin": {
+      "version": "2.5.3",
+      "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.5.3.tgz",
+      "integrity": "sha512-YseMB8cs8U/KCaAGQoqYmfUuhhGW0a9p9XvWXrxVOkE3/IiISTLw4ALNt7JR5B2eYauFM+PQGSbXMDmVbR7Tfw==",
+      "dev": true,
+      "dependencies": {
+        "schema-utils": "^4.0.0"
+      },
+      "engines": {
+        "node": ">= 12.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "webpack": "^5.0.0"
+      }
+    },
+    "node_modules/mini-css-extract-plugin/node_modules/schema-utils": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz",
+      "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==",
+      "dev": true,
+      "dependencies": {
+        "@types/json-schema": "^7.0.9",
+        "ajv": "^8.8.0",
+        "ajv-formats": "^2.1.1",
+        "ajv-keywords": "^5.0.0"
+      },
+      "engines": {
+        "node": ">= 12.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      }
+    },
+    "node_modules/minimalistic-assert": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+      "dev": true
+    },
+    "node_modules/minimatch": {
+      "version": "3.0.5",
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/minimist": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+      "dev": true,
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/minipass": {
+      "version": "3.3.6",
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/minipass-collect": {
+      "version": "1.0.2",
+      "license": "ISC",
+      "dependencies": {
+        "minipass": "^3.0.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/minipass-fetch": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz",
+      "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==",
+      "dependencies": {
+        "minipass": "^3.1.0",
+        "minipass-sized": "^1.0.3",
+        "minizlib": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "optionalDependencies": {
+        "encoding": "^0.1.12"
+      }
+    },
+    "node_modules/minipass-flush": {
+      "version": "1.0.5",
+      "license": "ISC",
+      "dependencies": {
+        "minipass": "^3.0.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/minipass-json-stream": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz",
+      "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==",
+      "dependencies": {
+        "jsonparse": "^1.3.1",
+        "minipass": "^3.0.0"
+      }
+    },
+    "node_modules/minipass-pipeline": {
+      "version": "1.2.4",
+      "license": "ISC",
+      "dependencies": {
+        "minipass": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/minipass-sized": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz",
+      "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==",
+      "dependencies": {
+        "minipass": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/minipass/node_modules/yallist": {
+      "version": "4.0.0",
+      "license": "ISC"
+    },
+    "node_modules/minizlib": {
+      "version": "2.1.2",
+      "license": "MIT",
+      "dependencies": {
+        "minipass": "^3.0.0",
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/minizlib/node_modules/yallist": {
+      "version": "4.0.0",
+      "license": "ISC"
+    },
+    "node_modules/mkdirp": {
+      "version": "1.0.4",
+      "license": "MIT",
+      "bin": {
+        "mkdirp": "bin/cmd.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.2",
+      "license": "MIT"
+    },
+    "node_modules/multicast-dns": {
+      "version": "6.2.3",
+      "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz",
+      "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==",
+      "dev": true,
+      "dependencies": {
+        "dns-packet": "^1.3.1",
+        "thunky": "^1.0.2"
+      },
+      "bin": {
+        "multicast-dns": "cli.js"
+      }
+    },
+    "node_modules/multicast-dns-service-types": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz",
+      "integrity": "sha512-cnAsSVxIDsYt0v7HmC0hWZFwwXSh+E6PgCrREDuN/EsjgLwA5XRmlMHhSiDPrt6HxY1gTivEa/Zh7GtODoLevQ==",
+      "dev": true
+    },
+    "node_modules/mute-stream": {
+      "version": "0.0.8",
+      "license": "ISC"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.6",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/needle": {
+      "version": "2.9.1",
+      "resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz",
+      "integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==",
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "debug": "^3.2.6",
+        "iconv-lite": "^0.4.4",
+        "sax": "^1.2.4"
+      },
+      "bin": {
+        "needle": "bin/needle"
+      },
+      "engines": {
+        "node": ">= 4.4.x"
+      }
+    },
+    "node_modules/needle/node_modules/debug": {
+      "version": "3.2.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+      "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "ms": "^2.1.1"
+      }
+    },
+    "node_modules/negotiator": {
+      "version": "0.6.3",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/neo-async": {
+      "version": "2.6.2",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/ng-zorro-antd": {
+      "version": "13.4.0",
+      "license": "MIT",
+      "dependencies": {
+        "@angular/cdk": "^13.0.1",
+        "@ant-design/icons-angular": "^13.0.1",
+        "date-fns": "^2.16.1",
+        "tslib": "^2.3.0"
+      },
+      "peerDependencies": {
+        "@angular/animations": "^13.0.1",
+        "@angular/common": "^13.0.1",
+        "@angular/core": "^13.0.1",
+        "@angular/forms": "^13.0.1",
+        "@angular/platform-browser": "^13.0.1",
+        "@angular/router": "^13.0.1"
+      }
+    },
+    "node_modules/ngx-color-picker": {
+      "version": "12.0.1",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.3.0"
+      },
+      "peerDependencies": {
+        "@angular/common": ">=9.0.0",
+        "@angular/core": ">=9.0.0",
+        "@angular/forms": ">=9.0.0"
+      }
+    },
+    "node_modules/nice-napi": {
+      "version": "1.0.2",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "!win32"
+      ],
+      "dependencies": {
+        "node-addon-api": "^3.0.0",
+        "node-gyp-build": "^4.2.2"
+      }
+    },
+    "node_modules/node-addon-api": {
+      "version": "3.2.1",
+      "dev": true,
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/node-forge": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
+      "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
+      "dev": true,
+      "engines": {
+        "node": ">= 6.13.0"
+      }
+    },
+    "node_modules/node-gyp": {
+      "version": "8.4.1",
+      "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz",
+      "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==",
+      "dependencies": {
+        "env-paths": "^2.2.0",
+        "glob": "^7.1.4",
+        "graceful-fs": "^4.2.6",
+        "make-fetch-happen": "^9.1.0",
+        "nopt": "^5.0.0",
+        "npmlog": "^6.0.0",
+        "rimraf": "^3.0.2",
+        "semver": "^7.3.5",
+        "tar": "^6.1.2",
+        "which": "^2.0.2"
+      },
+      "bin": {
+        "node-gyp": "bin/node-gyp.js"
+      },
+      "engines": {
+        "node": ">= 10.12.0"
+      }
+    },
+    "node_modules/node-gyp-build": {
+      "version": "4.6.0",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "bin": {
+        "node-gyp-build": "bin.js",
+        "node-gyp-build-optional": "optional.js",
+        "node-gyp-build-test": "build-test.js"
+      }
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.10",
+      "license": "MIT"
+    },
+    "node_modules/nopt": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
+      "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
+      "dependencies": {
+        "abbrev": "1"
+      },
+      "bin": {
+        "nopt": "bin/nopt.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/normalize-path": {
+      "version": "3.0.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/normalize-range": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+      "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/notation": {
+      "version": "1.3.6",
+      "license": "MIT"
+    },
+    "node_modules/npm-bundled": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz",
+      "integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==",
+      "dependencies": {
+        "npm-normalize-package-bin": "^1.0.1"
+      }
+    },
+    "node_modules/npm-install-checks": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-4.0.0.tgz",
+      "integrity": "sha512-09OmyDkNLYwqKPOnbI8exiOZU2GVVmQp7tgez2BPi5OZC8M82elDAps7sxC4l//uSUtotWqoEIDwjRvWH4qz8w==",
+      "dependencies": {
+        "semver": "^7.1.1"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/npm-normalize-package-bin": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz",
+      "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA=="
+    },
+    "node_modules/npm-package-arg": {
+      "version": "8.1.5",
+      "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.1.5.tgz",
+      "integrity": "sha512-LhgZrg0n0VgvzVdSm1oiZworPbTxYHUJCgtsJW8mGvlDpxTM1vSJc3m5QZeUkhAHIzbz3VCHd/R4osi1L1Tg/Q==",
+      "dependencies": {
+        "hosted-git-info": "^4.0.1",
+        "semver": "^7.3.4",
+        "validate-npm-package-name": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/npm-packlist": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-3.0.0.tgz",
+      "integrity": "sha512-L/cbzmutAwII5glUcf2DBRNY/d0TFd4e/FnaZigJV6JD85RHZXJFGwCndjMWiiViiWSsWt3tiOLpI3ByTnIdFQ==",
+      "dependencies": {
+        "glob": "^7.1.6",
+        "ignore-walk": "^4.0.1",
+        "npm-bundled": "^1.1.1",
+        "npm-normalize-package-bin": "^1.0.1"
+      },
+      "bin": {
+        "npm-packlist": "bin/index.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/npm-pick-manifest": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-6.1.1.tgz",
+      "integrity": "sha512-dBsdBtORT84S8V8UTad1WlUyKIY9iMsAmqxHbLdeEeBNMLQDlDWWra3wYUx9EBEIiG/YwAy0XyNHDd2goAsfuA==",
+      "dependencies": {
+        "npm-install-checks": "^4.0.0",
+        "npm-normalize-package-bin": "^1.0.1",
+        "npm-package-arg": "^8.1.2",
+        "semver": "^7.3.4"
+      }
+    },
+    "node_modules/npm-registry-fetch": {
+      "version": "12.0.2",
+      "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-12.0.2.tgz",
+      "integrity": "sha512-Df5QT3RaJnXYuOwtXBXS9BWs+tHH2olvkCLh6jcR/b/u3DvPMlp3J0TvvYwplPKxHMOwfg287PYih9QqaVFoKA==",
+      "dependencies": {
+        "make-fetch-happen": "^10.0.1",
+        "minipass": "^3.1.6",
+        "minipass-fetch": "^1.4.1",
+        "minipass-json-stream": "^1.0.1",
+        "minizlib": "^2.1.2",
+        "npm-package-arg": "^8.1.5"
+      },
+      "engines": {
+        "node": "^12.13.0 || ^14.15.0 || >=16"
+      }
+    },
+    "node_modules/npm-registry-fetch/node_modules/@npmcli/fs": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz",
+      "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==",
+      "dependencies": {
+        "@gar/promisify": "^1.1.3",
+        "semver": "^7.3.5"
+      },
+      "engines": {
+        "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+      }
+    },
+    "node_modules/npm-registry-fetch/node_modules/@npmcli/move-file": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz",
+      "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==",
+      "deprecated": "This functionality has been moved to @npmcli/fs",
+      "dependencies": {
+        "mkdirp": "^1.0.4",
+        "rimraf": "^3.0.2"
+      },
+      "engines": {
+        "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+      }
+    },
+    "node_modules/npm-registry-fetch/node_modules/@tootallnate/once": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
+      "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/npm-registry-fetch/node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/npm-registry-fetch/node_modules/cacache": {
+      "version": "16.1.3",
+      "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz",
+      "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==",
+      "dependencies": {
+        "@npmcli/fs": "^2.1.0",
+        "@npmcli/move-file": "^2.0.0",
+        "chownr": "^2.0.0",
+        "fs-minipass": "^2.1.0",
+        "glob": "^8.0.1",
+        "infer-owner": "^1.0.4",
+        "lru-cache": "^7.7.1",
+        "minipass": "^3.1.6",
+        "minipass-collect": "^1.0.2",
+        "minipass-flush": "^1.0.5",
+        "minipass-pipeline": "^1.2.4",
+        "mkdirp": "^1.0.4",
+        "p-map": "^4.0.0",
+        "promise-inflight": "^1.0.1",
+        "rimraf": "^3.0.2",
+        "ssri": "^9.0.0",
+        "tar": "^6.1.11",
+        "unique-filename": "^2.0.0"
+      },
+      "engines": {
+        "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+      }
+    },
+    "node_modules/npm-registry-fetch/node_modules/glob": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
+      "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
+      "deprecated": "Glob versions prior to v9 are no longer supported",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^5.0.1",
+        "once": "^1.3.0"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/npm-registry-fetch/node_modules/http-proxy-agent": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+      "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+      "dependencies": {
+        "@tootallnate/once": "2",
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/npm-registry-fetch/node_modules/lru-cache": {
+      "version": "7.18.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+      "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/npm-registry-fetch/node_modules/make-fetch-happen": {
+      "version": "10.2.1",
+      "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz",
+      "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==",
+      "dependencies": {
+        "agentkeepalive": "^4.2.1",
+        "cacache": "^16.1.0",
+        "http-cache-semantics": "^4.1.0",
+        "http-proxy-agent": "^5.0.0",
+        "https-proxy-agent": "^5.0.0",
+        "is-lambda": "^1.0.1",
+        "lru-cache": "^7.7.1",
+        "minipass": "^3.1.6",
+        "minipass-collect": "^1.0.2",
+        "minipass-fetch": "^2.0.3",
+        "minipass-flush": "^1.0.5",
+        "minipass-pipeline": "^1.2.4",
+        "negotiator": "^0.6.3",
+        "promise-retry": "^2.0.1",
+        "socks-proxy-agent": "^7.0.0",
+        "ssri": "^9.0.0"
+      },
+      "engines": {
+        "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+      }
+    },
+    "node_modules/npm-registry-fetch/node_modules/make-fetch-happen/node_modules/minipass-fetch": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz",
+      "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==",
+      "dependencies": {
+        "minipass": "^3.1.6",
+        "minipass-sized": "^1.0.3",
+        "minizlib": "^2.1.2"
+      },
+      "engines": {
+        "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+      },
+      "optionalDependencies": {
+        "encoding": "^0.1.13"
+      }
+    },
+    "node_modules/npm-registry-fetch/node_modules/minimatch": {
+      "version": "5.1.6",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+      "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/npm-registry-fetch/node_modules/socks-proxy-agent": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz",
+      "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==",
+      "dependencies": {
+        "agent-base": "^6.0.2",
+        "debug": "^4.3.3",
+        "socks": "^2.6.2"
+      },
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/npm-registry-fetch/node_modules/ssri": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz",
+      "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==",
+      "dependencies": {
+        "minipass": "^3.1.1"
+      },
+      "engines": {
+        "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+      }
+    },
+    "node_modules/npm-registry-fetch/node_modules/unique-filename": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz",
+      "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==",
+      "dependencies": {
+        "unique-slug": "^3.0.0"
+      },
+      "engines": {
+        "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+      }
+    },
+    "node_modules/npm-registry-fetch/node_modules/unique-slug": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz",
+      "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==",
+      "dependencies": {
+        "imurmurhash": "^0.1.4"
+      },
+      "engines": {
+        "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+      }
+    },
+    "node_modules/npm-run-path": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+      "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+      "dev": true,
+      "dependencies": {
+        "path-key": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/npmlog": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz",
+      "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==",
+      "deprecated": "This package is no longer supported.",
+      "dependencies": {
+        "are-we-there-yet": "^3.0.0",
+        "console-control-strings": "^1.1.0",
+        "gauge": "^4.0.3",
+        "set-blocking": "^2.0.0"
+      },
+      "engines": {
+        "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+      }
+    },
+    "node_modules/nth-check": {
+      "version": "2.1.1",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "boolbase": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/nth-check?sponsor=1"
+      }
+    },
+    "node_modules/object-inspect": {
+      "version": "1.12.3",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
+      "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
+      "dev": true,
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/object-is": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz",
+      "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "define-properties": "^1.1.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/object-keys": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/obuf": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
+      "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
+      "dev": true
+    },
+    "node_modules/on-finished": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+      "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+      "dev": true,
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/on-headers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+      "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "license": "ISC",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/onetime": {
+      "version": "5.1.2",
+      "license": "MIT",
+      "dependencies": {
+        "mimic-fn": "^2.1.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/open": {
+      "version": "8.4.0",
+      "license": "MIT",
+      "dependencies": {
+        "define-lazy-prop": "^2.0.0",
+        "is-docker": "^2.1.1",
+        "is-wsl": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/ora": {
+      "version": "5.4.1",
+      "license": "MIT",
+      "dependencies": {
+        "bl": "^4.1.0",
+        "chalk": "^4.1.0",
+        "cli-cursor": "^3.1.0",
+        "cli-spinners": "^2.5.0",
+        "is-interactive": "^1.0.0",
+        "is-unicode-supported": "^0.1.0",
+        "log-symbols": "^4.1.0",
+        "strip-ansi": "^6.0.0",
+        "wcwidth": "^1.0.1"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/ora/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/ora/node_modules/chalk": {
+      "version": "4.1.2",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/ora/node_modules/color-convert": {
+      "version": "2.0.1",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/ora/node_modules/color-name": {
+      "version": "1.1.4",
+      "license": "MIT"
+    },
+    "node_modules/ora/node_modules/has-flag": {
+      "version": "4.0.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ora/node_modules/supports-color": {
+      "version": "7.2.0",
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/os-tmpdir": {
+      "version": "1.0.2",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/p-limit": {
+      "version": "2.3.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-try": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-locate": {
+      "version": "4.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-limit": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/p-map": {
+      "version": "4.0.0",
+      "license": "MIT",
+      "dependencies": {
+        "aggregate-error": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-retry": {
+      "version": "4.6.2",
+      "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
+      "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
+      "dev": true,
+      "dependencies": {
+        "@types/retry": "0.12.0",
+        "retry": "^0.13.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/p-retry/node_modules/retry": {
+      "version": "0.13.1",
+      "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
+      "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/p-try": {
+      "version": "2.2.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/pace-js": {
+      "version": "1.2.4",
+      "license": "MIT"
+    },
+    "node_modules/pacote": {
+      "version": "12.0.3",
+      "resolved": "https://registry.npmjs.org/pacote/-/pacote-12.0.3.tgz",
+      "integrity": "sha512-CdYEl03JDrRO3x18uHjBYA9TyoW8gy+ThVcypcDkxPtKlw76e4ejhYB6i9lJ+/cebbjpqPW/CijjqxwDTts8Ow==",
+      "dependencies": {
+        "@npmcli/git": "^2.1.0",
+        "@npmcli/installed-package-contents": "^1.0.6",
+        "@npmcli/promise-spawn": "^1.2.0",
+        "@npmcli/run-script": "^2.0.0",
+        "cacache": "^15.0.5",
+        "chownr": "^2.0.0",
+        "fs-minipass": "^2.1.0",
+        "infer-owner": "^1.0.4",
+        "minipass": "^3.1.3",
+        "mkdirp": "^1.0.3",
+        "npm-package-arg": "^8.0.1",
+        "npm-packlist": "^3.0.0",
+        "npm-pick-manifest": "^6.0.0",
+        "npm-registry-fetch": "^12.0.0",
+        "promise-retry": "^2.0.1",
+        "read-package-json-fast": "^2.0.1",
+        "rimraf": "^3.0.2",
+        "ssri": "^8.0.1",
+        "tar": "^6.1.0"
+      },
+      "bin": {
+        "pacote": "lib/bin.js"
+      },
+      "engines": {
+        "node": "^12.13.0 || ^14.15.0 || >=16"
+      }
+    },
+    "node_modules/pako": {
+      "version": "1.0.11",
+      "dev": true,
+      "license": "(MIT AND Zlib)"
+    },
+    "node_modules/parent-module": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+      "dev": true,
+      "dependencies": {
+        "callsites": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/parse-json": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+      "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+      "dev": true,
+      "dependencies": {
+        "@babel/code-frame": "^7.0.0",
+        "error-ex": "^1.3.1",
+        "json-parse-even-better-errors": "^2.3.0",
+        "lines-and-columns": "^1.1.6"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/parse-node-version": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz",
+      "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/parse5": {
+      "version": "5.1.1",
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/parse5-html-rewriting-stream": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-6.0.1.tgz",
+      "integrity": "sha512-vwLQzynJVEfUlURxgnf51yAJDQTtVpNyGD8tKi2Za7m+akukNHxCcUQMAa/mUGLhCeicFdpy7Tlvj8ZNKadprg==",
+      "dev": true,
+      "dependencies": {
+        "parse5": "^6.0.1",
+        "parse5-sax-parser": "^6.0.1"
+      }
+    },
+    "node_modules/parse5-html-rewriting-stream/node_modules/parse5": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
+      "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
+      "dev": true
+    },
+    "node_modules/parse5-htmlparser2-tree-adapter": {
+      "version": "6.0.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "parse5": "^6.0.1"
+      }
+    },
+    "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": {
+      "version": "6.0.1",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/parse5-sax-parser": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-6.0.1.tgz",
+      "integrity": "sha512-kXX+5S81lgESA0LsDuGjAlBybImAChYRMT+/uKCEXFBFOeEhS52qUCydGhU3qLRD8D9DVjaUo821WK7DM4iCeg==",
+      "dev": true,
+      "dependencies": {
+        "parse5": "^6.0.1"
+      }
+    },
+    "node_modules/parse5-sax-parser/node_modules/parse5": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
+      "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
+      "dev": true
+    },
+    "node_modules/parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/path-exists": {
+      "version": "4.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-is-absolute": {
+      "version": "1.0.1",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-parse": {
+      "version": "1.0.7",
+      "license": "MIT"
+    },
+    "node_modules/path-to-regexp": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+      "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==",
+      "dev": true
+    },
+    "node_modules/path-type": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+      "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/picocolors": {
+      "version": "1.0.0",
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "2.3.1",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pify": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+      "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/piscina": {
+      "version": "3.2.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "eventemitter-asyncresource": "^1.0.0",
+        "hdr-histogram-js": "^2.0.1",
+        "hdr-histogram-percentiles-obj": "^3.0.0"
+      },
+      "optionalDependencies": {
+        "nice-napi": "^1.0.2"
+      }
+    },
+    "node_modules/pkg-dir": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+      "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+      "dev": true,
+      "dependencies": {
+        "find-up": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/portfinder": {
+      "version": "1.0.32",
+      "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz",
+      "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==",
+      "dev": true,
+      "dependencies": {
+        "async": "^2.6.4",
+        "debug": "^3.2.7",
+        "mkdirp": "^0.5.6"
+      },
+      "engines": {
+        "node": ">= 0.12.0"
+      }
+    },
+    "node_modules/portfinder/node_modules/debug": {
+      "version": "3.2.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+      "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+      "dev": true,
+      "dependencies": {
+        "ms": "^2.1.1"
+      }
+    },
+    "node_modules/portfinder/node_modules/mkdirp": {
+      "version": "0.5.6",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+      "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+      "dev": true,
+      "dependencies": {
+        "minimist": "^1.2.6"
+      },
+      "bin": {
+        "mkdirp": "bin/cmd.js"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.4.21",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz",
+      "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        }
+      ],
+      "dependencies": {
+        "nanoid": "^3.3.4",
+        "picocolors": "^1.0.0",
+        "source-map-js": "^1.0.2"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/postcss-attribute-case-insensitive": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz",
+      "integrity": "sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ==",
+      "dev": true,
+      "dependencies": {
+        "postcss-selector-parser": "^6.0.10"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >=16"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/csstools"
+      },
+      "peerDependencies": {
+        "postcss": "^8.2"
+      }
+    },
+    "node_modules/postcss-color-functional-notation": {
+      "version": "4.2.4",
+      "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz",
+      "integrity": "sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg==",
+      "dev": true,
+      "dependencies": {
+        "postcss-value-parser": "^4.2.0"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >=16"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/csstools"
+      },
+      "peerDependencies": {
+        "postcss": "^8.2"
+      }
+    },
+    "node_modules/postcss-color-hex-alpha": {
+      "version": "8.0.4",
+      "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz",
+      "integrity": "sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ==",
+      "dev": true,
+      "dependencies": {
+        "postcss-value-parser": "^4.2.0"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >=16"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/csstools"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4"
+      }
+    },
+    "node_modules/postcss-color-rebeccapurple": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.1.1.tgz",
+      "integrity": "sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg==",
+      "dev": true,
+      "dependencies": {
+        "postcss-value-parser": "^4.2.0"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >=16"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/csstools"
+      },
+      "peerDependencies": {
+        "postcss": "^8.2"
+      }
+    },
+    "node_modules/postcss-custom-media": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz",
+      "integrity": "sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg==",
+      "dev": true,
+      "dependencies": {
+        "postcss-value-parser": "^4.2.0"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >=16"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/csstools"
+      },
+      "peerDependencies": {
+        "postcss": "^8.3"
+      }
+    },
+    "node_modules/postcss-custom-properties": {
+      "version": "12.1.11",
+      "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.11.tgz",
+      "integrity": "sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ==",
+      "dev": true,
+      "dependencies": {
+        "postcss-value-parser": "^4.2.0"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >=16"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/csstools"
+      },
+      "peerDependencies": {
+        "postcss": "^8.2"
+      }
+    },
+    "node_modules/postcss-custom-selectors": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz",
+      "integrity": "sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg==",
+      "dev": true,
+      "dependencies": {
+        "postcss-selector-parser": "^6.0.4"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >=16"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/csstools"
+      },
+      "peerDependencies": {
+        "postcss": "^8.3"
+      }
+    },
+    "node_modules/postcss-dir-pseudo-class": {
+      "version": "6.0.5",
+      "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.5.tgz",
+      "integrity": "sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA==",
+      "dev": true,
+      "dependencies": {
+        "postcss-selector-parser": "^6.0.10"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >=16"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/csstools"
+      },
+      "peerDependencies": {
+        "postcss": "^8.2"
+      }
+    },
+    "node_modules/postcss-double-position-gradients": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.2.tgz",
+      "integrity": "sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ==",
+      "dev": true,
+      "dependencies": {
+        "@csstools/postcss-progressive-custom-properties": "^1.1.0",
+        "postcss-value-parser": "^4.2.0"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >=16"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/csstools"
+      },
+      "peerDependencies": {
+        "postcss": "^8.2"
+      }
+    },
+    "node_modules/postcss-env-function": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-4.0.6.tgz",
+      "integrity": "sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==",
+      "dev": true,
+      "dependencies": {
+        "postcss-value-parser": "^4.2.0"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >=16"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4"
+      }
+    },
+    "node_modules/postcss-focus-visible": {
+      "version": "6.0.4",
+      "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz",
+      "integrity": "sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==",
+      "dev": true,
+      "dependencies": {
+        "postcss-selector-parser": "^6.0.9"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >=16"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4"
+      }
+    },
+    "node_modules/postcss-focus-within": {
+      "version": "5.0.4",
+      "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz",
+      "integrity": "sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==",
+      "dev": true,
+      "dependencies": {
+        "postcss-selector-parser": "^6.0.9"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >=16"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4"
+      }
+    },
+    "node_modules/postcss-font-variant": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz",
+      "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==",
+      "dev": true,
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
+    "node_modules/postcss-gap-properties": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz",
+      "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==",
+      "dev": true,
+      "engines": {
+        "node": "^12 || ^14 || >=16"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/csstools"
+      },
+      "peerDependencies": {
+        "postcss": "^8.2"
+      }
+    },
+    "node_modules/postcss-image-set-function": {
+      "version": "4.0.7",
+      "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-4.0.7.tgz",
+      "integrity": "sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw==",
+      "dev": true,
+      "dependencies": {
+        "postcss-value-parser": "^4.2.0"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >=16"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/csstools"
+      },
+      "peerDependencies": {
+        "postcss": "^8.2"
+      }
+    },
+    "node_modules/postcss-import": {
+      "version": "14.0.2",
+      "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.0.2.tgz",
+      "integrity": "sha512-BJ2pVK4KhUyMcqjuKs9RijV5tatNzNa73e/32aBVE/ejYPe37iH+6vAu9WvqUkB5OAYgLHzbSvzHnorybJCm9g==",
+      "dev": true,
+      "dependencies": {
+        "postcss-value-parser": "^4.0.0",
+        "read-cache": "^1.0.0",
+        "resolve": "^1.1.7"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "postcss": "^8.0.0"
+      }
+    },
+    "node_modules/postcss-initial": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz",
+      "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==",
+      "dev": true,
+      "peerDependencies": {
+        "postcss": "^8.0.0"
+      }
+    },
+    "node_modules/postcss-lab-function": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz",
+      "integrity": "sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w==",
+      "dev": true,
+      "dependencies": {
+        "@csstools/postcss-progressive-custom-properties": "^1.1.0",
+        "postcss-value-parser": "^4.2.0"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >=16"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/csstools"
+      },
+      "peerDependencies": {
+        "postcss": "^8.2"
+      }
+    },
+    "node_modules/postcss-loader": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz",
+      "integrity": "sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==",
+      "dev": true,
+      "dependencies": {
+        "cosmiconfig": "^7.0.0",
+        "klona": "^2.0.5",
+        "semver": "^7.3.5"
+      },
+      "engines": {
+        "node": ">= 12.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "postcss": "^7.0.0 || ^8.0.1",
+        "webpack": "^5.0.0"
+      }
+    },
+    "node_modules/postcss-logical": {
+      "version": "5.0.4",
+      "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz",
+      "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==",
+      "dev": true,
+      "engines": {
+        "node": "^12 || ^14 || >=16"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4"
+      }
+    },
+    "node_modules/postcss-media-minmax": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz",
+      "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
+    "node_modules/postcss-modules-extract-imports": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
+      "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==",
+      "dev": true,
+      "engines": {
+        "node": "^10 || ^12 || >= 14"
+      },
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
+    "node_modules/postcss-modules-local-by-default": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz",
+      "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==",
+      "dev": true,
+      "dependencies": {
+        "icss-utils": "^5.0.0",
+        "postcss-selector-parser": "^6.0.2",
+        "postcss-value-parser": "^4.1.0"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >= 14"
+      },
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
+    "node_modules/postcss-modules-scope": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz",
+      "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==",
+      "dev": true,
+      "dependencies": {
+        "postcss-selector-parser": "^6.0.4"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >= 14"
+      },
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
+    "node_modules/postcss-modules-values": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz",
+      "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==",
+      "dev": true,
+      "dependencies": {
+        "icss-utils": "^5.0.0"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >= 14"
+      },
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
+    "node_modules/postcss-nesting": {
+      "version": "10.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.2.0.tgz",
+      "integrity": "sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA==",
+      "dev": true,
+      "dependencies": {
+        "@csstools/selector-specificity": "^2.0.0",
+        "postcss-selector-parser": "^6.0.10"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >=16"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/csstools"
+      },
+      "peerDependencies": {
+        "postcss": "^8.2"
+      }
+    },
+    "node_modules/postcss-overflow-shorthand": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz",
+      "integrity": "sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A==",
+      "dev": true,
+      "dependencies": {
+        "postcss-value-parser": "^4.2.0"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >=16"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/csstools"
+      },
+      "peerDependencies": {
+        "postcss": "^8.2"
+      }
+    },
+    "node_modules/postcss-page-break": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz",
+      "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==",
+      "dev": true,
+      "peerDependencies": {
+        "postcss": "^8"
+      }
+    },
+    "node_modules/postcss-place": {
+      "version": "7.0.5",
+      "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.5.tgz",
+      "integrity": "sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g==",
+      "dev": true,
+      "dependencies": {
+        "postcss-value-parser": "^4.2.0"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >=16"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/csstools"
+      },
+      "peerDependencies": {
+        "postcss": "^8.2"
+      }
+    },
+    "node_modules/postcss-preset-env": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.2.3.tgz",
+      "integrity": "sha512-Ok0DhLfwrcNGrBn8sNdy1uZqWRk/9FId0GiQ39W4ILop5GHtjJs8bu1MY9isPwHInpVEPWjb4CEcEaSbBLpfwA==",
+      "dev": true,
+      "dependencies": {
+        "autoprefixer": "^10.4.2",
+        "browserslist": "^4.19.1",
+        "caniuse-lite": "^1.0.30001299",
+        "css-blank-pseudo": "^3.0.2",
+        "css-has-pseudo": "^3.0.3",
+        "css-prefers-color-scheme": "^6.0.2",
+        "cssdb": "^5.0.0",
+        "postcss-attribute-case-insensitive": "^5.0.0",
+        "postcss-color-functional-notation": "^4.2.1",
+        "postcss-color-hex-alpha": "^8.0.2",
+        "postcss-color-rebeccapurple": "^7.0.2",
+        "postcss-custom-media": "^8.0.0",
+        "postcss-custom-properties": "^12.1.2",
+        "postcss-custom-selectors": "^6.0.0",
+        "postcss-dir-pseudo-class": "^6.0.3",
+        "postcss-double-position-gradients": "^3.0.4",
+        "postcss-env-function": "^4.0.4",
+        "postcss-focus-visible": "^6.0.3",
+        "postcss-focus-within": "^5.0.3",
+        "postcss-font-variant": "^5.0.0",
+        "postcss-gap-properties": "^3.0.2",
+        "postcss-image-set-function": "^4.0.4",
+        "postcss-initial": "^4.0.1",
+        "postcss-lab-function": "^4.0.3",
+        "postcss-logical": "^5.0.3",
+        "postcss-media-minmax": "^5.0.0",
+        "postcss-nesting": "^10.1.2",
+        "postcss-overflow-shorthand": "^3.0.2",
+        "postcss-page-break": "^3.0.4",
+        "postcss-place": "^7.0.3",
+        "postcss-pseudo-class-any-link": "^7.0.2",
+        "postcss-replace-overflow-wrap": "^4.0.0",
+        "postcss-selector-not": "^5.0.0"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >=16"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4"
+      }
+    },
+    "node_modules/postcss-pseudo-class-any-link": {
+      "version": "7.1.6",
+      "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.6.tgz",
+      "integrity": "sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w==",
+      "dev": true,
+      "dependencies": {
+        "postcss-selector-parser": "^6.0.10"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >=16"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/csstools"
+      },
+      "peerDependencies": {
+        "postcss": "^8.2"
+      }
+    },
+    "node_modules/postcss-replace-overflow-wrap": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz",
+      "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==",
+      "dev": true,
+      "peerDependencies": {
+        "postcss": "^8.0.3"
+      }
+    },
+    "node_modules/postcss-selector-not": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-5.0.0.tgz",
+      "integrity": "sha512-/2K3A4TCP9orP4TNS7u3tGdRFVKqz/E6pX3aGnriPG0jU78of8wsUcqE4QAhWEU0d+WnMSF93Ah3F//vUtK+iQ==",
+      "dev": true,
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      },
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
+    "node_modules/postcss-selector-parser": {
+      "version": "6.0.11",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz",
+      "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==",
+      "dev": true,
+      "dependencies": {
+        "cssesc": "^3.0.0",
+        "util-deprecate": "^1.0.2"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/postcss-value-parser": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+      "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+      "dev": true
+    },
+    "node_modules/pretty-bytes": {
+      "version": "5.6.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/process-nextick-args": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+      "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+      "dev": true
+    },
+    "node_modules/promise-inflight": {
+      "version": "1.0.1",
+      "license": "ISC"
+    },
+    "node_modules/promise-retry": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
+      "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
+      "dependencies": {
+        "err-code": "^2.0.2",
+        "retry": "^0.12.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/proxy-addr": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+      "dev": true,
+      "dependencies": {
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/proxy-addr/node_modules/ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/prr": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
+      "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==",
+      "dev": true,
+      "optional": true
+    },
+    "node_modules/punycode": {
+      "version": "2.3.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/qs": {
+      "version": "6.11.0",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
+      "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
+      "dev": true,
+      "dependencies": {
+        "side-channel": "^1.0.4"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ]
+    },
+    "node_modules/randombytes": {
+      "version": "2.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "^5.1.0"
+      }
+    },
+    "node_modules/range-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/raw-body": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
+      "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
+      "dev": true,
+      "dependencies": {
+        "bytes": "3.1.2",
+        "http-errors": "2.0.0",
+        "iconv-lite": "0.4.24",
+        "unpipe": "1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/raw-body/node_modules/bytes": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+      "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/read-cache": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+      "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+      "dev": true,
+      "dependencies": {
+        "pify": "^2.3.0"
+      }
+    },
+    "node_modules/read-package-json-fast": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-2.0.3.tgz",
+      "integrity": "sha512-W/BKtbL+dUjTuRL2vziuYhp76s5HZ9qQhd/dKfWIZveD0O40453QNyZhC0e63lqZrAQ4jiOapVoeJ7JrszenQQ==",
+      "dependencies": {
+        "json-parse-even-better-errors": "^2.3.0",
+        "npm-normalize-package-bin": "^1.0.1"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/readable-stream": {
+      "version": "3.6.2",
+      "license": "MIT",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/readdirp": {
+      "version": "3.6.0",
+      "license": "MIT",
+      "dependencies": {
+        "picomatch": "^2.2.1"
+      },
+      "engines": {
+        "node": ">=8.10.0"
+      }
+    },
+    "node_modules/reflect-metadata": {
+      "version": "0.1.13",
+      "license": "Apache-2.0"
+    },
+    "node_modules/regenerate": {
+      "version": "1.4.2",
+      "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
+      "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==",
+      "dev": true
+    },
+    "node_modules/regenerate-unicode-properties": {
+      "version": "10.1.0",
+      "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz",
+      "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==",
+      "dev": true,
+      "dependencies": {
+        "regenerate": "^1.4.2"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/regenerator-runtime": {
+      "version": "0.13.9",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
+      "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==",
+      "dev": true
+    },
+    "node_modules/regenerator-transform": {
+      "version": "0.15.1",
+      "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz",
+      "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==",
+      "dev": true,
+      "dependencies": {
+        "@babel/runtime": "^7.8.4"
+      }
+    },
+    "node_modules/regex-parser": {
+      "version": "2.2.11",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/regexp.prototype.flags": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz",
+      "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "define-properties": "^1.1.3",
+        "functions-have-names": "^1.2.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/regexpu-core": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz",
+      "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==",
+      "dev": true,
+      "dependencies": {
+        "@babel/regjsgen": "^0.8.0",
+        "regenerate": "^1.4.2",
+        "regenerate-unicode-properties": "^10.1.0",
+        "regjsparser": "^0.9.1",
+        "unicode-match-property-ecmascript": "^2.0.0",
+        "unicode-match-property-value-ecmascript": "^2.1.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/regjsparser": {
+      "version": "0.9.1",
+      "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz",
+      "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==",
+      "dev": true,
+      "dependencies": {
+        "jsesc": "~0.5.0"
+      },
+      "bin": {
+        "regjsparser": "bin/parser"
+      }
+    },
+    "node_modules/regjsparser/node_modules/jsesc": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
+      "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==",
+      "dev": true,
+      "bin": {
+        "jsesc": "bin/jsesc"
+      }
+    },
+    "node_modules/require-directory": {
+      "version": "2.1.1",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/require-from-string": {
+      "version": "2.0.2",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/requires-port": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+      "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
+      "dev": true
+    },
+    "node_modules/resolve": {
+      "version": "1.22.0",
+      "license": "MIT",
+      "dependencies": {
+        "is-core-module": "^2.8.1",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      },
+      "bin": {
+        "resolve": "bin/resolve"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/resolve-from": {
+      "version": "5.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/resolve-url-loader": {
+      "version": "5.0.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "adjust-sourcemap-loader": "^4.0.0",
+        "convert-source-map": "^1.7.0",
+        "loader-utils": "^2.0.0",
+        "postcss": "^8.2.14",
+        "source-map": "0.6.1"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/resolve-url-loader/node_modules/loader-utils": {
+      "version": "2.0.4",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "big.js": "^5.2.2",
+        "emojis-list": "^3.0.0",
+        "json5": "^2.1.2"
+      },
+      "engines": {
+        "node": ">=8.9.0"
+      }
+    },
+    "node_modules/resolve-url-loader/node_modules/source-map": {
+      "version": "0.6.1",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/restore-cursor": {
+      "version": "3.1.0",
+      "license": "MIT",
+      "dependencies": {
+        "onetime": "^5.1.0",
+        "signal-exit": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/retry": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+      "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/reusify": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+      "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+      "dev": true,
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/rimraf": {
+      "version": "3.0.2",
+      "license": "ISC",
+      "dependencies": {
+        "glob": "^7.1.3"
+      },
+      "bin": {
+        "rimraf": "bin.js"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/run-async": {
+      "version": "2.4.1",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "node_modules/rxjs": {
+      "version": "7.5.7",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "tslib": "^2.1.0"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "license": "MIT"
+    },
+    "node_modules/sass": {
+      "version": "1.49.9",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.49.9.tgz",
+      "integrity": "sha512-YlYWkkHP9fbwaFRZQRXgDi3mXZShslVmmo+FVK3kHLUELHHEYrCmL1x6IUjC7wLS6VuJSAFXRQS/DxdsC4xL1A==",
+      "dev": true,
+      "dependencies": {
+        "chokidar": ">=3.0.0 <4.0.0",
+        "immutable": "^4.0.0",
+        "source-map-js": ">=0.6.2 <2.0.0"
+      },
+      "bin": {
+        "sass": "sass.js"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
+    "node_modules/sass-loader": {
+      "version": "12.4.0",
+      "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.4.0.tgz",
+      "integrity": "sha512-7xN+8khDIzym1oL9XyS6zP6Ges+Bo2B2xbPrjdMHEYyV3AQYhd/wXeru++3ODHF0zMjYmVadblSKrPrjEkL8mg==",
+      "dev": true,
+      "dependencies": {
+        "klona": "^2.0.4",
+        "neo-async": "^2.6.2"
+      },
+      "engines": {
+        "node": ">= 12.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "fibers": ">= 3.1.0",
+        "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0",
+        "sass": "^1.3.0",
+        "webpack": "^5.0.0"
+      },
+      "peerDependenciesMeta": {
+        "fibers": {
+          "optional": true
+        },
+        "node-sass": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/sax": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+      "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
+      "dev": true
+    },
+    "node_modules/schema-utils": {
+      "version": "2.7.1",
+      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz",
+      "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==",
+      "dev": true,
+      "dependencies": {
+        "@types/json-schema": "^7.0.5",
+        "ajv": "^6.12.4",
+        "ajv-keywords": "^3.5.2"
+      },
+      "engines": {
+        "node": ">= 8.9.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      }
+    },
+    "node_modules/schema-utils/node_modules/ajv": {
+      "version": "6.12.6",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+      "dev": true,
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/schema-utils/node_modules/ajv-keywords": {
+      "version": "3.5.2",
+      "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
+      "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
+      "dev": true,
+      "peerDependencies": {
+        "ajv": "^6.9.1"
+      }
+    },
+    "node_modules/schema-utils/node_modules/json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+      "dev": true
+    },
+    "node_modules/select-hose": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
+      "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==",
+      "dev": true
+    },
+    "node_modules/selfsigned": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz",
+      "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==",
+      "dev": true,
+      "dependencies": {
+        "node-forge": "^1"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/semver": {
+      "version": "7.3.5",
+      "license": "ISC",
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/semver/node_modules/lru-cache": {
+      "version": "6.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/semver/node_modules/yallist": {
+      "version": "4.0.0",
+      "license": "ISC"
+    },
+    "node_modules/send": {
+      "version": "0.18.0",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
+      "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+      "dev": true,
+      "dependencies": {
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "destroy": "1.2.0",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "fresh": "0.5.2",
+        "http-errors": "2.0.0",
+        "mime": "1.6.0",
+        "ms": "2.1.3",
+        "on-finished": "2.4.1",
+        "range-parser": "~1.2.1",
+        "statuses": "2.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/send/node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "dev": true,
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/send/node_modules/debug/node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "dev": true
+    },
+    "node_modules/send/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "dev": true
+    },
+    "node_modules/serialize-javascript": {
+      "version": "6.0.1",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "randombytes": "^2.1.0"
+      }
+    },
+    "node_modules/serve-index": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz",
+      "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==",
+      "dev": true,
+      "dependencies": {
+        "accepts": "~1.3.4",
+        "batch": "0.6.1",
+        "debug": "2.6.9",
+        "escape-html": "~1.0.3",
+        "http-errors": "~1.6.2",
+        "mime-types": "~2.1.17",
+        "parseurl": "~1.3.2"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/serve-index/node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "dev": true,
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/serve-index/node_modules/depd": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+      "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/serve-index/node_modules/http-errors": {
+      "version": "1.6.3",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
+      "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==",
+      "dev": true,
+      "dependencies": {
+        "depd": "~1.1.2",
+        "inherits": "2.0.3",
+        "setprototypeof": "1.1.0",
+        "statuses": ">= 1.4.0 < 2"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/serve-index/node_modules/inherits": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+      "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==",
+      "dev": true
+    },
+    "node_modules/serve-index/node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "dev": true
+    },
+    "node_modules/serve-index/node_modules/setprototypeof": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
+      "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==",
+      "dev": true
+    },
+    "node_modules/serve-index/node_modules/statuses": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+      "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/serve-static": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
+      "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
+      "dev": true,
+      "dependencies": {
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "parseurl": "~1.3.3",
+        "send": "0.18.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/set-blocking": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+      "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
+    },
+    "node_modules/setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+      "dev": true
+    },
+    "node_modules/shallow-clone": {
+      "version": "3.0.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "kind-of": "^6.0.2"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dev": true,
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/side-channel": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+      "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.0",
+        "get-intrinsic": "^1.0.2",
+        "object-inspect": "^1.9.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/signal-exit": {
+      "version": "3.0.7",
+      "license": "ISC"
+    },
+    "node_modules/slash": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz",
+      "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==",
+      "dev": true,
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/smart-buffer": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
+      "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
+      "engines": {
+        "node": ">= 6.0.0",
+        "npm": ">= 3.0.0"
+      }
+    },
+    "node_modules/sockjs": {
+      "version": "0.3.24",
+      "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz",
+      "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==",
+      "dev": true,
+      "dependencies": {
+        "faye-websocket": "^0.11.3",
+        "uuid": "^8.3.2",
+        "websocket-driver": "^0.7.4"
+      }
+    },
+    "node_modules/socks": {
+      "version": "2.8.3",
+      "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",
+      "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==",
+      "dependencies": {
+        "ip-address": "^9.0.5",
+        "smart-buffer": "^4.2.0"
+      },
+      "engines": {
+        "node": ">= 10.0.0",
+        "npm": ">= 3.0.0"
+      }
+    },
+    "node_modules/socks-proxy-agent": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz",
+      "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==",
+      "dependencies": {
+        "agent-base": "^6.0.2",
+        "debug": "^4.3.3",
+        "socks": "^2.6.2"
+      },
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/sortablejs": {
+      "version": "1.15.0",
+      "license": "MIT"
+    },
+    "node_modules/source-map": {
+      "version": "0.7.3",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.0.2",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/source-map-loader": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.1.tgz",
+      "integrity": "sha512-Vp1UsfyPvgujKQzi4pyDiTOnE3E4H+yHvkVRN3c/9PJmQS4CQJExvcDvaX/D+RV+xQben9HJ56jMJS3CgUeWyA==",
+      "dev": true,
+      "dependencies": {
+        "abab": "^2.0.5",
+        "iconv-lite": "^0.6.3",
+        "source-map-js": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 12.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "webpack": "^5.0.0"
+      }
+    },
+    "node_modules/source-map-loader/node_modules/iconv-lite": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+      "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+      "dev": true,
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/source-map-resolve": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz",
+      "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==",
+      "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated",
+      "dev": true,
+      "dependencies": {
+        "atob": "^2.1.2",
+        "decode-uri-component": "^0.2.0"
+      }
+    },
+    "node_modules/source-map-support": {
+      "version": "0.5.21",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "buffer-from": "^1.0.0",
+        "source-map": "^0.6.0"
+      }
+    },
+    "node_modules/source-map-support/node_modules/source-map": {
+      "version": "0.6.1",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/sourcemap-codec": {
+      "version": "1.4.8",
+      "license": "MIT"
+    },
+    "node_modules/spdy": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz",
+      "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==",
+      "dev": true,
+      "dependencies": {
+        "debug": "^4.1.0",
+        "handle-thing": "^2.0.0",
+        "http-deceiver": "^1.2.7",
+        "select-hose": "^2.0.0",
+        "spdy-transport": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/spdy-transport": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz",
+      "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==",
+      "dev": true,
+      "dependencies": {
+        "debug": "^4.1.0",
+        "detect-node": "^2.0.4",
+        "hpack.js": "^2.1.6",
+        "obuf": "^1.1.2",
+        "readable-stream": "^3.0.6",
+        "wbuf": "^1.7.3"
+      }
+    },
+    "node_modules/sprintf-js": {
+      "version": "1.0.3",
+      "dev": true,
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/ssri": {
+      "version": "8.0.1",
+      "license": "ISC",
+      "dependencies": {
+        "minipass": "^3.1.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/statuses": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+      "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/string_decoder": {
+      "version": "1.3.0",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "~5.2.0"
+      }
+    },
+    "node_modules/string-width": {
+      "version": "4.2.3",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-final-newline": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+      "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/stylus": {
+      "version": "0.56.0",
+      "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.56.0.tgz",
+      "integrity": "sha512-Ev3fOb4bUElwWu4F9P9WjnnaSpc8XB9OFHSFZSKMFL1CE1oM+oFXWEgAqPmmZIyhBihuqIQlFsVTypiiS9RxeA==",
+      "dev": true,
+      "dependencies": {
+        "css": "^3.0.0",
+        "debug": "^4.3.2",
+        "glob": "^7.1.6",
+        "safer-buffer": "^2.1.2",
+        "sax": "~1.2.4",
+        "source-map": "^0.7.3"
+      },
+      "bin": {
+        "stylus": "bin/stylus"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/stylus-loader": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/stylus-loader/-/stylus-loader-6.2.0.tgz",
+      "integrity": "sha512-5dsDc7qVQGRoc6pvCL20eYgRUxepZ9FpeK28XhdXaIPP6kXr6nI1zAAKFQgP5OBkOfKaURp4WUpJzspg1f01Gg==",
+      "dev": true,
+      "dependencies": {
+        "fast-glob": "^3.2.7",
+        "klona": "^2.0.4",
+        "normalize-path": "^3.0.0"
+      },
+      "engines": {
+        "node": ">= 12.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "stylus": ">=0.52.4",
+        "webpack": "^5.0.0"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "5.5.0",
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/symbol-observable": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
+      "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==",
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/tapable": {
+      "version": "2.2.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/tar": {
+      "version": "6.1.13",
+      "license": "ISC",
+      "dependencies": {
+        "chownr": "^2.0.0",
+        "fs-minipass": "^2.0.0",
+        "minipass": "^4.0.0",
+        "minizlib": "^2.1.1",
+        "mkdirp": "^1.0.3",
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/tar/node_modules/minipass": {
+      "version": "4.2.8",
+      "license": "ISC",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/tar/node_modules/yallist": {
+      "version": "4.0.0",
+      "license": "ISC"
+    },
+    "node_modules/terser": {
+      "version": "5.14.2",
+      "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
+      "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
+      "dev": true,
+      "dependencies": {
+        "@jridgewell/source-map": "^0.3.2",
+        "acorn": "^8.5.0",
+        "commander": "^2.20.0",
+        "source-map-support": "~0.5.20"
+      },
+      "bin": {
+        "terser": "bin/terser"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/terser-webpack-plugin": {
+      "version": "5.3.7",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/trace-mapping": "^0.3.17",
+        "jest-worker": "^27.4.5",
+        "schema-utils": "^3.1.1",
+        "serialize-javascript": "^6.0.1",
+        "terser": "^5.16.5"
+      },
+      "engines": {
+        "node": ">= 10.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "webpack": "^5.1.0"
+      },
+      "peerDependenciesMeta": {
+        "@swc/core": {
+          "optional": true
+        },
+        "esbuild": {
+          "optional": true
+        },
+        "uglify-js": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/terser-webpack-plugin/node_modules/ajv": {
+      "version": "6.12.6",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": {
+      "version": "3.5.2",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "ajv": "^6.9.1"
+      }
+    },
+    "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": {
+      "version": "0.4.1",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/terser-webpack-plugin/node_modules/schema-utils": {
+      "version": "3.1.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/json-schema": "^7.0.8",
+        "ajv": "^6.12.5",
+        "ajv-keywords": "^3.5.2"
+      },
+      "engines": {
+        "node": ">= 10.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      }
+    },
+    "node_modules/terser-webpack-plugin/node_modules/terser": {
+      "version": "5.16.9",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "@jridgewell/source-map": "^0.3.2",
+        "acorn": "^8.5.0",
+        "commander": "^2.20.0",
+        "source-map-support": "~0.5.20"
+      },
+      "bin": {
+        "terser": "bin/terser"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/test-exclude": {
+      "version": "6.0.0",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "@istanbuljs/schema": "^0.1.2",
+        "glob": "^7.1.4",
+        "minimatch": "^3.0.4"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/text-table": {
+      "version": "0.2.0",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/through": {
+      "version": "2.3.8",
+      "license": "MIT"
+    },
+    "node_modules/thunky": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
+      "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
+      "dev": true
+    },
+    "node_modules/tmp": {
+      "version": "0.0.33",
+      "license": "MIT",
+      "dependencies": {
+        "os-tmpdir": "~1.0.2"
+      },
+      "engines": {
+        "node": ">=0.6.0"
+      }
+    },
+    "node_modules/to-fast-properties": {
+      "version": "2.0.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "license": "MIT",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/toidentifier": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
+    "node_modules/tree-kill": {
+      "version": "1.2.2",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "tree-kill": "cli.js"
+      }
+    },
+    "node_modules/tslib": {
+      "version": "2.5.0",
+      "license": "0BSD"
+    },
+    "node_modules/type-fest": {
+      "version": "0.21.3",
+      "license": "(MIT OR CC0-1.0)",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/type-is": {
+      "version": "1.6.18",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+      "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+      "dev": true,
+      "dependencies": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.24"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/typed-assert": {
+      "version": "1.0.9",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/typescript": {
+      "version": "4.6.4",
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=4.2.0"
+      }
+    },
+    "node_modules/unicode-canonical-property-names-ecmascript": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",
+      "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/unicode-match-property-ecmascript": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz",
+      "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==",
+      "dev": true,
+      "dependencies": {
+        "unicode-canonical-property-names-ecmascript": "^2.0.0",
+        "unicode-property-aliases-ecmascript": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/unicode-match-property-value-ecmascript": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz",
+      "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==",
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/unicode-property-aliases-ecmascript": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz",
+      "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==",
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/unique-filename": {
+      "version": "1.1.1",
+      "license": "ISC",
+      "dependencies": {
+        "unique-slug": "^2.0.0"
+      }
+    },
+    "node_modules/unique-slug": {
+      "version": "2.0.2",
+      "license": "ISC",
+      "dependencies": {
+        "imurmurhash": "^0.1.4"
+      }
+    },
+    "node_modules/unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/update-browserslist-db": {
+      "version": "1.0.10",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "escalade": "^3.1.1",
+        "picocolors": "^1.0.0"
+      },
+      "bin": {
+        "browserslist-lint": "cli.js"
+      },
+      "peerDependencies": {
+        "browserslist": ">= 4.21.0"
+      }
+    },
+    "node_modules/uri-js": {
+      "version": "4.4.1",
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "license": "MIT"
+    },
+    "node_modules/utils-merge": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
+    "node_modules/uuid": {
+      "version": "8.3.2",
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
+    "node_modules/uzip": {
+      "version": "0.20201231.0",
+      "resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz",
+      "integrity": "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng=="
+    },
+    "node_modules/validate-npm-package-name": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz",
+      "integrity": "sha512-M6w37eVCMMouJ9V/sdPGnC5H4uDr73/+xdq0FBLO3TFFX1+7wiUY6Es328NN+y43tmY+doUdN9g9J21vqB7iLw==",
+      "dependencies": {
+        "builtins": "^1.0.3"
+      }
+    },
+    "node_modules/vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/watchpack": {
+      "version": "2.4.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "glob-to-regexp": "^0.4.1",
+        "graceful-fs": "^4.1.2"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/wbuf": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz",
+      "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==",
+      "dev": true,
+      "dependencies": {
+        "minimalistic-assert": "^1.0.0"
+      }
+    },
+    "node_modules/wcwidth": {
+      "version": "1.0.1",
+      "license": "MIT",
+      "dependencies": {
+        "defaults": "^1.0.3"
+      }
+    },
+    "node_modules/webpack": {
+      "version": "5.76.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/eslint-scope": "^3.7.3",
+        "@types/estree": "^0.0.51",
+        "@webassemblyjs/ast": "1.11.1",
+        "@webassemblyjs/wasm-edit": "1.11.1",
+        "@webassemblyjs/wasm-parser": "1.11.1",
+        "acorn": "^8.7.1",
+        "acorn-import-assertions": "^1.7.6",
+        "browserslist": "^4.14.5",
+        "chrome-trace-event": "^1.0.2",
+        "enhanced-resolve": "^5.10.0",
+        "es-module-lexer": "^0.9.0",
+        "eslint-scope": "5.1.1",
+        "events": "^3.2.0",
+        "glob-to-regexp": "^0.4.1",
+        "graceful-fs": "^4.2.9",
+        "json-parse-even-better-errors": "^2.3.1",
+        "loader-runner": "^4.2.0",
+        "mime-types": "^2.1.27",
+        "neo-async": "^2.6.2",
+        "schema-utils": "^3.1.0",
+        "tapable": "^2.1.1",
+        "terser-webpack-plugin": "^5.1.3",
+        "watchpack": "^2.4.0",
+        "webpack-sources": "^3.2.3"
+      },
+      "bin": {
+        "webpack": "bin/webpack.js"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependenciesMeta": {
+        "webpack-cli": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/webpack-dev-middleware": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.0.tgz",
+      "integrity": "sha512-MouJz+rXAm9B1OTOYaJnn6rtD/lWZPy2ufQCH3BPs8Rloh/Du6Jze4p7AeLYHkVi0giJnYLaSGDC7S+GM9arhg==",
+      "dev": true,
+      "dependencies": {
+        "colorette": "^2.0.10",
+        "memfs": "^3.2.2",
+        "mime-types": "^2.1.31",
+        "range-parser": "^1.2.1",
+        "schema-utils": "^4.0.0"
+      },
+      "engines": {
+        "node": ">= 12.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "webpack": "^4.0.0 || ^5.0.0"
+      }
+    },
+    "node_modules/webpack-dev-middleware/node_modules/schema-utils": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz",
+      "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==",
+      "dev": true,
+      "dependencies": {
+        "@types/json-schema": "^7.0.9",
+        "ajv": "^8.8.0",
+        "ajv-formats": "^2.1.1",
+        "ajv-keywords": "^5.0.0"
+      },
+      "engines": {
+        "node": ">= 12.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      }
+    },
+    "node_modules/webpack-dev-server": {
+      "version": "4.7.3",
+      "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.7.3.tgz",
+      "integrity": "sha512-mlxq2AsIw2ag016nixkzUkdyOE8ST2GTy34uKSABp1c4nhjZvH90D5ZRR+UOLSsG4Z3TFahAi72a3ymRtfRm+Q==",
+      "dev": true,
+      "dependencies": {
+        "@types/bonjour": "^3.5.9",
+        "@types/connect-history-api-fallback": "^1.3.5",
+        "@types/serve-index": "^1.9.1",
+        "@types/sockjs": "^0.3.33",
+        "@types/ws": "^8.2.2",
+        "ansi-html-community": "^0.0.8",
+        "bonjour": "^3.5.0",
+        "chokidar": "^3.5.2",
+        "colorette": "^2.0.10",
+        "compression": "^1.7.4",
+        "connect-history-api-fallback": "^1.6.0",
+        "default-gateway": "^6.0.3",
+        "del": "^6.0.0",
+        "express": "^4.17.1",
+        "graceful-fs": "^4.2.6",
+        "html-entities": "^2.3.2",
+        "http-proxy-middleware": "^2.0.0",
+        "ipaddr.js": "^2.0.1",
+        "open": "^8.0.9",
+        "p-retry": "^4.5.0",
+        "portfinder": "^1.0.28",
+        "schema-utils": "^4.0.0",
+        "selfsigned": "^2.0.0",
+        "serve-index": "^1.9.1",
+        "sockjs": "^0.3.21",
+        "spdy": "^4.0.2",
+        "strip-ansi": "^7.0.0",
+        "webpack-dev-middleware": "^5.3.0",
+        "ws": "^8.1.0"
+      },
+      "bin": {
+        "webpack-dev-server": "bin/webpack-dev-server.js"
+      },
+      "engines": {
+        "node": ">= 12.13.0"
+      },
+      "peerDependencies": {
+        "webpack": "^4.37.0 || ^5.0.0"
+      },
+      "peerDependenciesMeta": {
+        "webpack-cli": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/webpack-dev-server/node_modules/ansi-regex": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
+      "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+      "dev": true,
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/webpack-dev-server/node_modules/schema-utils": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz",
+      "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==",
+      "dev": true,
+      "dependencies": {
+        "@types/json-schema": "^7.0.9",
+        "ajv": "^8.8.0",
+        "ajv-formats": "^2.1.1",
+        "ajv-keywords": "^5.0.0"
+      },
+      "engines": {
+        "node": ">= 12.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      }
+    },
+    "node_modules/webpack-dev-server/node_modules/strip-ansi": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz",
+      "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==",
+      "dev": true,
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/webpack-merge": {
+      "version": "5.8.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "clone-deep": "^4.0.1",
+        "wildcard": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/webpack-sources": {
+      "version": "3.2.3",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/webpack-subresource-integrity": {
+      "version": "5.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "typed-assert": "^1.0.8"
+      },
+      "engines": {
+        "node": ">= 12"
+      },
+      "peerDependencies": {
+        "html-webpack-plugin": ">= 5.0.0-beta.1 < 6",
+        "webpack": "^5.12.0"
+      },
+      "peerDependenciesMeta": {
+        "html-webpack-plugin": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/webpack/node_modules/ajv": {
+      "version": "6.12.6",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/webpack/node_modules/ajv-keywords": {
+      "version": "3.5.2",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "ajv": "^6.9.1"
+      }
+    },
+    "node_modules/webpack/node_modules/json-schema-traverse": {
+      "version": "0.4.1",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/webpack/node_modules/schema-utils": {
+      "version": "3.1.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/json-schema": "^7.0.8",
+        "ajv": "^6.12.5",
+        "ajv-keywords": "^3.5.2"
+      },
+      "engines": {
+        "node": ">= 10.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      }
+    },
+    "node_modules/websocket-driver": {
+      "version": "0.7.4",
+      "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
+      "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==",
+      "dev": true,
+      "dependencies": {
+        "http-parser-js": ">=0.5.1",
+        "safe-buffer": ">=5.1.0",
+        "websocket-extensions": ">=0.1.1"
+      },
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/websocket-extensions": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
+      "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/wide-align": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
+      "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
+      "dependencies": {
+        "string-width": "^1.0.2 || 2 || 3 || 4"
+      }
+    },
+    "node_modules/wildcard": {
+      "version": "2.0.0",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/wrap-ansi": {
+      "version": "7.0.0",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/color-convert": {
+      "version": "2.0.1",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/color-name": {
+      "version": "1.1.4",
+      "license": "MIT"
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "license": "ISC"
+    },
+    "node_modules/ws": {
+      "version": "8.13.0",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
+      "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
+      "dev": true,
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/y18n": {
+      "version": "5.0.8",
+      "license": "ISC",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/yallist": {
+      "version": "3.1.1",
+      "license": "ISC"
+    },
+    "node_modules/yaml": {
+      "version": "1.10.2",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+      "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/yargs": {
+      "version": "17.7.1",
+      "license": "MIT",
+      "dependencies": {
+        "cliui": "^8.0.1",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "require-directory": "^2.1.1",
+        "string-width": "^4.2.3",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^21.1.1"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/yargs-parser": {
+      "version": "21.1.1",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/zone.js": {
+      "version": "0.11.8",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.3.0"
+      }
+    }
+  }
+}

+ 49 - 0
zorro/package.json

@@ -0,0 +1,49 @@
+{
+  "name": "pcoloring-zorro",
+  "version": "0.1.0",
+  "scripts": {
+    "ng": "ng",
+    "start": "ng serve --port 4308 --host 0.0.0.0 --proxy-config proxy.config.json --open true --disable-host-check",
+    "start:en": "ng serve --configuration development-en --port 4308 --host 0.0.0.0 --proxy-config proxy.config.json --open true --disable-host-check",
+    "build": "ng build --localize --prod",
+    "watch": "ng build --localize --watch --configuration development"
+  },
+  "private": true,
+  "dependencies": {
+    "@angular/animations": "~13.3.0",
+    "@angular/cdk": "~13.3.0",
+    "@angular/cli": "^13.3.11",
+    "@angular/common": "~13.3.0",
+    "@angular/compiler": "~13.3.0",
+    "@angular/core": "~13.3.0",
+    "@angular/forms": "~13.3.0",
+    "@angular/localize": "~13.3.0",
+    "@angular/platform-browser": "~13.3.0",
+    "@angular/platform-browser-dynamic": "~13.3.0",
+    "@angular/router": "~13.3.0",
+    "@ng-bootstrap/ng-bootstrap": "^12.0.1",
+    "@ngneat/edit-in-place": "^1.6.1",
+    "@popperjs/core": "^2.11.4",
+    "accesscontrol": "^2.2.1",
+    "bootstrap": "^5.1.3",
+    "browser-image-compression": "^2.0.2",
+    "date-fns": "^2.28.0",
+    "gl-matrix": "^3.4.3",
+    "ng-zorro-antd": "^13.1.1",
+    "ngx-color-picker": "^12.0.1",
+    "pace-js": "^1.2.4",
+    "rxjs": "~7.5.0",
+    "sortablejs": "^1.15.0",
+    "tslib": "^2.3.0",
+    "zone.js": "~0.11.4"
+  },
+  "devDependencies": {
+    "@angular-devkit/build-angular": "~13.3.3",
+    "@angular/compiler-cli": "~13.3.0",
+    "@types/gl-matrix": "^3.2.0",
+    "@types/node": "^12.11.1",
+    "@types/offscreencanvas": "^2019.7.3",
+    "@types/sortablejs": "^1.10.7",
+    "typescript": "~4.6.2"
+  }
+}

+ 32 - 0
zorro/proxy.config.json

@@ -0,0 +1,32 @@
+{
+  "/api/*": {
+    "target": "http://localhost:3000",
+    "secure": false,
+    "logLevel": "debug"
+  },
+  "/napi/*": {
+    "target": "http://localhost:3000",
+    "secure": false,
+    "logLevel": "debug"
+  },
+  "/static/*": {
+    "target": "http://localhost:3000",
+    "secure": false,
+    "logLevel": "debug"
+  },
+  "/thumbs/*": {
+    "target": "http://localhost:3000",
+    "secure": false,
+    "logLevel": "debug"
+  },
+  "/zips/*": {
+    "target": "http://localhost:3000",
+    "secure": false,
+    "logLevel": "debug"
+  },
+  "/res/*": {
+    "target": "http://localhost:3000",
+    "secure": false,
+    "logLevel": "debug"
+  }
+}

+ 321 - 0
zorro/src/app/admin-layout.component.ts

@@ -0,0 +1,321 @@
+import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
+import { ActivatedRoute, NavigationEnd, Router, RouterStateSnapshot } from '@angular/router';
+import { NzContentComponent } from 'ng-zorro-antd/layout';
+import { NzMessageService } from 'ng-zorro-antd/message';
+import { delay, filter, firstValueFrom, interval, repeatWhen, Subscription, switchMap, takeWhile, timer } from 'rxjs';
+import { AuthService } from './auth/auth.service';
+import { MsgService } from './pages/msg/msg.service';
+
+@Component({
+  selector: 'admin-layout',
+  template: `
+
+
+<nz-layout class="app-layout">
+  <nz-sider class="menu-sidebar"
+            [nzCollapsible]="true"
+            nzTheme="dark"
+            [nzCollapsedWidth]="64"
+            nzWidth="216px"
+            [nzCollapsed]="isCollapsed"
+            [nzTrigger]="null">
+    <div class="menu-sidebar-inner">
+      <div class="sidebar-logo">
+        <a href="https://ng.ant.design/" target="_blank">
+          <img src="https://ng.ant.design/assets/img/logo.svg" alt="logo">
+          <h1 i18n>创作平台</h1>
+        </a>
+      </div>
+      <admin-menu class="menu" [isCollapsed]="isCollapsed"></admin-menu>
+    </div>
+
+  </nz-sider>
+  <nz-layout >
+
+    <nz-header class="header">
+      <div class="app-header d-flex flex-row ">
+        <span class="header-trigger" (click)="isCollapsed = !isCollapsed">
+            <i class="trigger" nz-icon [nzType]="isCollapsed ? 'menu-unfold' : 'menu-fold'" ></i>
+        </span>
+
+        <ul nz-menu nzMode="horizontal">
+          <li nz-submenu  nzIcon="user">
+            <span title class="d-inline-block">
+              {{profile?.username}}
+              <i nz-icon nzType="down" style="font-size:12px;"  ></i>
+            </span>
+            <ul>
+                <li nz-menu-item (click)="logout()" >
+                  <i nz-icon nzType="logout"  ></i> 
+                  <span class="d-inline-block pr-1" i18n>退出</span>
+                </li>
+            </ul>
+          </li>
+          <!-- <a (click)="open()">
+            <nz-badge nzSize="small" [nzCount]="unreadMsgs?.length" [nzOffset]="[10, -3]" i18n-nzTitle nzTitle="系统消息">
+              <i nz-icon nzType="bell" style="font-size:12px;"></i>
+            </nz-badge>
+          </a> -->
+          <!-- <nz-drawer [nzClosable]="false" [nzVisible]="visible" nzPlacement="right" (nzOnClose)="close()" >
+            <ng-container *nzDrawerContent>
+              <ng-container *ngFor="let msg of unreadMsgs; let i=index">
+                <div>
+                <span style="color:#cd5c5c; font-size:12px" i18n>(未读)</span>
+                  {{msg.from.name || msg.from.username}} {{msg.time | dateDist }}<ng-container i18n>前</ng-container> <strong>{{msg.content}}</strong>:
+                  <div style="display: flex;">
+                      <div style="display: inline-block; margin-right: 10px">
+                        <a (click)="onDetail(msg.art._id); setRead([msg._id])">
+                          <img [src]="msg.art.thumb" width="100px" loading="lazy" />
+                        </a>
+                      </div>
+                      <div style="display: inline-block; font-size: 13px; color: #5f9ea0">
+                        <ng-container i18n>作品</ng-container>: {{msg.art.name}} <br />
+                        <ng-container i18n>作者</ng-container>: {{msg.art.user.name || msg.art.user.username}} <br />
+                        <ng-container i18n>区块数</ng-container>: {{msg.art.areaCount}} <br />
+                        <ng-container i18n>类型</ng-container>: {msg.art.hasSpecial ? 'true' : 'false', select,  true {彩绘}  false {普通}} <br />
+                        <ng-container i18n>分类</ng-container>: {{msg.art.tags.join(', &nbsp;')}} 
+                      </div>
+                    </div>
+                </div>
+              </ng-container>
+              <ng-container *ngFor="let msg of readedMsgs; let i=index">
+                <div>
+                  <span style="color:#5f9ea0; font-size:12px" i18n>(已读)</span>
+                  {{msg.from.name || msg.from.username}} {{msg.time | dateDist }}<ng-container i18n>前</ng-container> <strong>{{msg.content}}</strong>:
+                    <div style="display: flex;">
+                      <div style="margin-right: 10px">
+                        <a (click)="onDetail(msg.art._id)">
+                          <img [src]="msg.art.thumb" width="100px" loading="lazy" />
+                        </a>
+                      </div>
+                      <div style="font-size: 13px; color: #5f9ea0">
+                        <ng-container i18n>作品</ng-container>: {{msg.art.name}} <br />
+                        <ng-container i18n>作者</ng-container>: {{msg.art.user.name || msg.art.user.username}} <br />
+                        <ng-container i18n>区块数</ng-container>: {{msg.art.areaCount}} <br />
+                        <ng-container i18n>类型</ng-container>: {msg.art.hasSpecial ? 'true' : 'false', select,  true {彩绘}  false {普通}} <br />
+                        <ng-container i18n>分类</ng-container>: {{msg.art.tags.join(', &nbsp;')}} 
+                      </div>
+                    </div>
+                </div>
+              </ng-container>
+            </ng-container>
+          </nz-drawer>  -->
+        </ul>
+      </div>
+    </nz-header>
+    <nz-content >
+      <div class="inner-content" #content >
+        <router-outlet></router-outlet>
+      </div>
+    </nz-content>
+  </nz-layout>
+</nz-layout>
+
+  `,
+  styles: [`
+
+:host {
+  display: flex;
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.app-layout {
+  align-items:stretch;
+}
+
+.menu-sidebar {
+  position: relative;
+  z-index: 1001;
+  min-height: 100vh;
+  box-shadow: 2px 0 6px rgba(0,21,41,.35);
+}
+
+.menu-sidebar-inner{
+  position:sticky;
+  height:100vh;
+  top:0;
+  left:0;
+  display:flex;
+  flex-direction:column;
+  background: #001529;
+}
+
+.menu {
+  flex:1;
+}
+
+.sidebar-logo {
+  height: 64px;
+  padding-left: 14px;
+  overflow: hidden;
+  line-height: 64px;
+}
+
+.sidebar-logo img {
+  display: inline-block;
+  height: 32px;
+  width: 32px;
+  vertical-align: middle;
+}
+
+.sidebar-logo h1 {
+  display: inline-block;
+  margin: 0 0 0 20px;
+  color: #fff;
+  font-weight: 600;
+  font-size: 14px;
+  font-family: Avenir,Helvetica Neue,Arial,Helvetica,sans-serif;
+  vertical-align: middle;
+}
+
+nz-header {
+  padding: 0;
+  width: 100%;
+  z-index: 1000;
+}
+
+.app-header {
+  position: relative;
+  justify-content: space-between;
+  height: 64px;
+  padding: 0;
+  background: #fff;
+  box-shadow: 0 1px 4px rgba(0,21,41,.08);
+}
+
+.header-trigger {
+  height: 64px;
+  padding: 0px 24px;
+  font-size: 20px;
+  cursor: pointer;
+  transition: all .3s,padding 0s;
+}
+
+.trigger:hover {
+  color: #1890ff;
+}
+
+nz-content {
+  margin: 0px;
+  background-color:white;
+}
+
+.inner-content {
+  padding: 10px;
+  background: #fff;
+  height: 100%;
+}
+
+nz-badge {
+  margin-right: 40px;
+}
+
+    `]
+})
+export class AdminLayout implements OnInit, OnDestroy {
+
+
+  @ViewChild('content') nzContent: ElementRef;
+
+  isCollapsed = true;
+  profile: any;
+  currentUrl: string;
+  // msgInterval: Subscription;  // 轮询查阅消息定时器
+  // unreadMsgs: any[] = []; // 未读消息列表
+  // readedMsgs: any[] = []; // 已读消息列表
+  constructor(
+    private auth: AuthService,
+    private message: NzMessageService,
+    private router: Router,
+    private msg: MsgService,
+  ) {
+    firstValueFrom(this.auth.profile()).then(res => {
+      this.profile = res;
+      console.log('profile:', this.profile);
+    }).catch(err => {
+      this.message.warning(err.error?.message || err.message);
+    })
+
+    //获取当前的url, 用于logout return
+    this.router.events
+      .pipe(filter((event: any) => event instanceof NavigationEnd))
+      .subscribe((event: any) => {
+        this.currentUrl = event.url;
+      });
+
+    this.router.events.subscribe(e => {
+      /*
+      if (this.nzContent?.nativeElement) {
+        let el = this.nzContent?.nativeElement as HTMLElement;
+        console.log('navigation:', el.scrollTop, e);
+      }else{
+        console.log('navigation:', e);
+      }
+      */
+    })
+
+  }
+
+
+  ngOnInit() {
+    // 启动轮询
+    // this.msgInterval = this.msg.msgPolling.subscribe((res: any) => {
+    //   this.unreadMsgs = res.unread;
+    //   this.readedMsgs = res.readed;
+    // })
+  }
+
+  ngOnDestroy(): void {
+    // this.msgInterval.unsubscribe();
+  }
+
+  logout() {
+    firstValueFrom(this.auth.signOut()).then(res => {
+      this.router.navigate(['/auth/login'], {
+        queryParams: {
+          return: this.currentUrl,
+        }
+      });
+    }).catch(err => {
+      this.message.warning(err.error?.message || err.message);
+    })
+  }
+
+  onDetail(artId: string) {
+    this.router.navigate(['/pages/detail', artId]); 
+  }
+
+  // setAllRead() {
+  //   // 没处理好, 应注意ids可能过大
+  //   if (this.unreadMsgs.length > 0) {
+  //     let ids = this.unreadMsgs.map(m => m._id);
+  //     firstValueFrom(this.msg.setRead(ids)).then(res => {
+  //       this.unreadMsgs = [];
+  //     }).catch(err => {
+  //       this.message.warning(err.error?.message || err.message);
+  //     })
+  //   }
+  // }
+
+  // setRead(ids: string[]) {
+  //   firstValueFrom(this.msg.setRead(ids)).then(res => {
+  //     this.unreadMsgs = [];
+  //   }).catch(err => {
+  //     this.message.warning(err.error?.message || err.message);
+  //   })
+  // }
+
+  ///////////////////////// right Drawer //////////////////////
+  // visible = false;
+  // open(): void {
+  //   this.visible = true;
+  // }
+
+  // close(): void {
+  //   this.visible = false;
+  //   this.setAllRead();
+  // }
+
+}

+ 38 - 0
zorro/src/app/app-routing.module.ts

@@ -0,0 +1,38 @@
+import { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+import { AdminLayout } from './admin-layout.component';
+import { AuthGuard } from './auth/auth.guard';
+
+const routes: Routes = [
+  { path: '', pathMatch: 'full', redirectTo: '/pages/my' },
+  { path: 'auth', loadChildren: () => import('./pages/auth/auth.module').then(m => m.AuthModule) },
+  { path: 'art', loadChildren: () => import('./pages/art/art.module').then(m => m.ArtModule) },
+  { path: 'webgl', loadChildren: () => import('./pages/webgl/webgl.module').then(m => m.WebglModule) },
+
+  {
+    canActivate: [AuthGuard],
+    path: '',
+    component: AdminLayout,
+    children: [
+      { path: 'pages', loadChildren: () => import('./pages/page/page.module').then(m => m.PageModule) },
+      { path: 'score', loadChildren: () => import('./pages/score/score.module').then(m => m.ScoreModule) },
+      { path: 'sys', loadChildren: () => import('./pages/sys/sys.module').then(m => m.SysModule) },
+      { path: 'system', loadChildren: () => import('./pages/sys/sys.module').then(m => m.SysModule) },
+      { path: 'content', loadChildren: () => import('./pages/content/content.module').then(m => m.ContentModule) },
+      { path: 'epg', loadChildren: () => import('./pages/epg/epg.module').then(m => m.EpgModule) },
+      { path: 'coloring', loadChildren: () => import('./projs/coloring/coloring.module').then(m => m.ColoringModule) },
+    ],
+  }
+
+];
+
+@NgModule({
+  imports: [RouterModule.forRoot(routes,
+    {
+      //scrollPositionRestoration: 'enabled',
+      //anchorScrolling: "enabled",
+    }
+  )],
+  exports: [RouterModule]
+})
+export class AppRoutingModule { }

+ 15 - 0
zorro/src/app/app.component.ts

@@ -0,0 +1,15 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+  selector: 'app-root',
+  template: '<router-outlet></router-outlet>',
+  styles: []
+})
+export class AppComponent implements OnInit {
+
+  constructor() { }
+
+  ngOnInit(): void {
+  }
+
+}

+ 107 - 0
zorro/src/app/app.module.ts

@@ -0,0 +1,107 @@
+import { NgModule } from '@angular/core';
+import { BrowserModule } from '@angular/platform-browser';
+
+import { AppRoutingModule } from './app-routing.module';
+import { AdminLayout } from './admin-layout.component';
+import { LOCALE_ID } from '@angular/core';
+import { NZ_I18N, zh_CN, en_US } from 'ng-zorro-antd/i18n';
+import { registerLocaleData, ViewportScroller } from '@angular/common';
+import zh from '@angular/common/locales/zh';
+import en from '@angular/common/locales/en';
+import { FormsModule } from '@angular/forms';
+import { HttpClientModule } from '@angular/common/http';
+import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations';
+import { IconsProviderModule } from './icons-provider.module';
+import { NzLayoutModule } from 'ng-zorro-antd/layout';
+import { NzMenuModule } from 'ng-zorro-antd/menu';
+import { NzFormModule } from 'ng-zorro-antd/form';
+
+import { AppComponent } from './app.component';
+import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
+import { MenuComponent } from './menu.component';
+import { NzDropDownModule } from 'ng-zorro-antd/dropdown';
+import { NzAvatarModule } from 'ng-zorro-antd/avatar';
+import { NzButtonModule } from 'ng-zorro-antd/button';
+import { NzMessageModule } from 'ng-zorro-antd/message';
+import { Router, RouteReuseStrategy, Scroll } from '@angular/router';
+import { filter } from 'rxjs';
+import { MyLayoutComponent } from './my-layout.component';
+import { NzNoAnimationModule } from 'ng-zorro-antd/core/no-animation';
+import { NzBadgeModule } from 'ng-zorro-antd/badge';
+import { NzDrawerModule } from 'ng-zorro-antd/drawer';
+import { UtilsModule } from 'src/app/lib/utils/utils.module';
+
+
+
+registerLocaleData(zh);
+registerLocaleData(en);
+
+@NgModule({
+  declarations: [
+    AdminLayout,
+    AppComponent,
+    MenuComponent,
+    MyLayoutComponent,
+  ],
+  imports: [
+    BrowserModule,
+    AppRoutingModule,
+    FormsModule,
+    HttpClientModule,
+    BrowserAnimationsModule,
+    //NoopAnimationsModule,
+    IconsProviderModule,
+    NzLayoutModule,
+    NzMenuModule,
+    NzFormModule,
+    NgbModule,
+    NzDropDownModule,
+    NzAvatarModule,
+    NzButtonModule,
+    NzMessageModule,
+    NzNoAnimationModule,
+    NzBadgeModule,
+    NzDrawerModule,
+    UtilsModule,
+  ],
+
+  // providers: [
+  //   { provide: NZ_I18N, useValue: zh_CN },
+  //   //{ provide: RouteReuseStrategy, useClass: CacheRouteReuseStrategy }
+  // ],
+  /** 根据 LOCALE_ID 自动切换 ng-zorro-antd 语言 **/
+  providers: [
+    {
+      provide: NZ_I18N,
+      useFactory: (localId: string) => {
+        switch (localId) {
+          case 'en':
+            return en_US;
+          /** 与 angular.json i18n/locales 配置一致 **/
+          case 'zh':
+            return zh_CN;
+          default:
+            return zh_CN;
+        }
+      },
+      deps: [LOCALE_ID]
+    }
+  ],
+
+  bootstrap: [AppComponent]
+})
+export class AppModule {
+  constructor(router: Router, scroller: ViewportScroller) {
+    router.events.pipe(
+      filter((e: any): e is Scroll => e instanceof Scroll)
+    ).subscribe(e => {
+      if (e.position) {
+        console.log(`scrollTo:${e.position}`);
+        setTimeout(() => {
+          scroller.scrollToPosition(e.position);
+        }, 100);
+      }
+    })
+
+  }
+}

+ 30 - 0
zorro/src/app/auth/auth.guard.ts

@@ -0,0 +1,30 @@
+import { Injectable } from '@angular/core';
+import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
+import { firstValueFrom, Observable } from 'rxjs';
+import { AuthService } from './auth.service';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class AuthGuard implements CanActivate {
+  constructor(private auth: AuthService, private router: Router) { }
+
+  canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
+    return firstValueFrom(this.auth.guard())
+      .then(res => Promise.resolve(true))
+      .catch(err => {
+        if (err.status == 401)
+          return Promise.resolve(false)
+        else return Promise.resolve(true);
+      }).then(auth => {
+        if (!auth)
+          this.router.navigate(['/auth/login'], {
+            queryParams: {
+              return: state.url
+            }
+          });
+        return auth;
+      });
+  }
+}
+

+ 14 - 0
zorro/src/app/auth/auth.module.ts

@@ -0,0 +1,14 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { CanDirective } from './can.directive';
+
+
+
+@NgModule({
+  declarations: [CanDirective],
+  imports: [
+    CommonModule
+  ],
+  exports : [CanDirective]
+})
+export class AuthModule { }

+ 93 - 0
zorro/src/app/auth/auth.service.ts

@@ -0,0 +1,93 @@
+import { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { AccessControl } from 'accesscontrol';
+import { firstValueFrom, Subject } from 'rxjs';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class AuthService {
+
+
+  private _ac: AccessControl = new AccessControl([
+    { role: 'user', resource: 'dummy', action: 'read:own', attributes: '*' },
+  ]);
+
+  public acChange = new Subject<any>();
+
+  set ac(ac: AccessControl) {
+    this._ac = ac;
+    this.acChange.next(1);
+  }
+
+  get ac() {
+    return this._ac;
+  }
+
+  $ac: Promise<AccessControl>;
+
+
+  constructor(private http: HttpClient) {
+    this.$ac = firstValueFrom(this.getGrants())
+      .then(resp => {
+        let grants = grantsTransform(resp as any[]);
+        console.log('grants', grants);
+        return new AccessControl(grants);
+      })
+      .catch(err => {
+        console.error('AccessControl initialze failed:', err);
+        return this._ac;
+      }).then(ac => {
+        console.log('ac 初始化成功');
+        return ac;
+      })
+  }
+
+
+
+  signOut() {
+    return this.http.get('/napi/web/auth/sign-out');
+  }
+
+  signIn(data) {
+    return this.http.post('/napi/web/auth/sign-in', data);
+  }
+
+  guard() {
+    return this.http.get('/napi/web/auth/guard');
+  }
+
+  profile() {
+    return this.http.get('/napi/web/auth/profile');
+  }
+
+  getMenus() {
+    return this.http.get('/napi/web/menu');
+  }
+
+  //TODO
+  checkUsername(username) {
+    return this.http.post('/napi/web/auth/check-username', { username });
+  }
+
+  checkPhone(phone) {
+    return this.http.post('/napi/web/auth/check-phone', { phone });
+  }
+
+  getGrants() {
+    return this.http.get(`/napi/web/auth/grants`);
+  }
+
+}
+
+function grantsTransform(resp: any[]) {
+  return resp.map(item => {
+    let { resource, action, possession, attributes } = item;
+    return {
+      role: 'user',
+      resource, attributes,
+      action: `${action}:${possession}`,
+    }
+  })
+}
+

+ 82 - 0
zorro/src/app/auth/can.directive.ts

@@ -0,0 +1,82 @@
+import { Directive, ElementRef, Input, OnDestroy, TemplateRef, ViewContainerRef } from '@angular/core';
+import { AccessControl, Permission, Query } from 'accesscontrol';
+import { Subscription } from 'rxjs';
+import { AuthService } from './auth.service';
+import { ActionType, PossessionType, QueryType } from './interfaces';
+
+@Directive({
+  selector: '[can]'
+})
+export class CanDirective implements OnDestroy {
+  query: QueryType;
+
+
+  // @Input() canOwn: boolean;
+
+  //readAny:art
+  @Input('can') set authCan(authQuery: QueryType) {
+    this.query = authQuery;
+    this.update();
+  }
+
+  constructor(
+    private sys: AuthService,
+    private templateRef: TemplateRef<any>,
+    private viewContainer: ViewContainerRef,
+  ) {
+
+  }
+
+  private async isAuthed() {
+    let ac = await this.sys.$ac;
+
+
+    if (ac == null) {
+      console.warn('[authCan]', `No ac`);
+      return false;
+    }
+    if (!this.query) {
+      console.warn('[authCan]', `No auth query`);
+      return false;
+    }
+
+    let [action, possession, resource, owned] = this.query;
+    //console.log('authQuery', action, resource, owned);
+    //即便查询是 own, 但是用户用用any权限, 可以直接跳过下面的检查
+    if(possession == 'own') {
+      let isAnyAuthed = this.checkAuth(ac, action, 'any', resource);
+      if (isAnyAuthed) return true;
+    }
+
+    let isAuthed = this.checkAuth(ac, action, possession, resource);
+    if (possession == 'any') return isAuthed;
+    if (action == 'create') return isAuthed;
+    return isAuthed && owned;
+  }
+
+  private checkAuth(ac: AccessControl, action: ActionType, possession: PossessionType, resource: string): boolean {
+    try {
+      let role = 'user';
+      let grant = ac.permission({ role, action, possession, resource })
+      //console.log('granted:', grant.granted, this.query);
+      return grant.granted;
+    } catch (err) {
+      console.warn('[authCan]', err);
+      return false;
+    }
+  }
+
+
+  private async update() {
+    let isAuthed = await this.isAuthed();
+    if (isAuthed) {
+      this.viewContainer.createEmbeddedView(this.templateRef);
+    } else {
+      this.viewContainer.clear();
+    }
+  }
+
+  ngOnDestroy() {
+  }
+
+}

+ 9 - 0
zorro/src/app/auth/interfaces.ts

@@ -0,0 +1,9 @@
+
+
+
+export type ActionType = 'create' | 'read' | 'update' | 'delete';
+export type PossessionType = 'own' | 'any';
+
+//action, resource, owned
+//['read', 'user', false]
+export type QueryType = [ActionType, PossessionType, string, boolean?];

+ 21 - 0
zorro/src/app/icons-provider.module.ts

@@ -0,0 +1,21 @@
+import { NgModule } from '@angular/core';
+import { NZ_ICONS, NzIconModule } from 'ng-zorro-antd/icon';
+
+import {
+  MenuFoldOutline,
+  MenuUnfoldOutline,
+  FormOutline,
+  DashboardOutline
+} from '@ant-design/icons-angular/icons';
+
+const icons = [MenuFoldOutline, MenuUnfoldOutline, DashboardOutline, FormOutline];
+
+@NgModule({
+  imports: [NzIconModule],
+  exports: [NzIconModule],
+  providers: [
+    { provide: NZ_ICONS, useValue: icons }
+  ]
+})
+export class IconsProviderModule {
+}

+ 369 - 0
zorro/src/app/lib/filler/check-edit/check-edit-layer.ts

@@ -0,0 +1,369 @@
+import FillArea from "../common/fillarea";
+import { Centers } from "../common/interfaces";
+import Repeater from "../common/repeater";
+import Utils from "../common/utils";
+import { Point } from "../core/interface";
+import Layer from "../core/layer";
+import Rect from "../core/rect";
+import MyFloodFill from './myfloodfill';
+
+const TAG = 'CheckEditLayer';
+
+export default class CheckEditLayer extends Layer {
+  raw: HTMLImageElement;
+  page: HTMLImageElement;
+  pagePixels: Uint32Array;
+  map: HTMLImageElement;
+  mapPixels: Uint32Array;
+  centers: Centers;
+  areaMap: {};
+  R: number = 1; //小区块半径阈值
+  N: number = 10; //最多允许的小区块个数
+
+  smallAreas: FillArea[] = []; //小区块列表
+  noCenterAreas: FillArea[] = []; //未计算出中心点的area
+
+  noCloseLines: Rect[] = [];  //未闭合线条 
+
+  colorCanvas: HTMLCanvasElement; // 将小区块上色的图层
+  markCanvas: HTMLCanvasElement; // 将小区快圈起来的图层
+
+  showMark: boolean = true;  // 显示标记
+  lineMark: boolean = true; // 显示闭合线条提示
+  showPage: boolean = true; // 显示线稿
+  showMap: boolean = false; // 显示map
+
+  sortedAreaList: FillArea[] = [];
+
+  floodImgs: HTMLImageElement[] = [];
+  _showFloodIdx: number = 100;
+  set showFloodIdx(idx: number) {
+    this._showFloodIdx = idx;
+    this.invalidate();
+  }
+  get showFloodIdx() {
+    return this._showFloodIdx;
+  }
+
+  result: number = 0; // 0: checking... 1-check success, 2-check failed
+
+  override get defaultToolKey(): string { return 'pan'; }
+
+  constructor(raw: HTMLImageElement, page: HTMLImageElement, map: HTMLImageElement, centers: Centers, R: number, N: number) {
+    super(page.width, page.height);
+
+    this.raw = raw;
+    this.page = page;
+    this.map = map;
+    this.centers = centers;
+    this.R = R || this.R;
+    this.N = N || this.N;
+    this.result = 0;
+
+    this.colorCanvas = Utils.createCanvas(this.width, this.height);
+    this.markCanvas = Utils.createCanvas(this.width, this.height);
+
+    if (this.centers) {
+      this.init();
+    }
+
+  }
+
+  init() {
+
+    this.pagePixels = new Uint32Array(Utils.getImageData(this.page).data.buffer);
+
+    this.mapPixels = new Uint32Array(Utils.getImageData(this.map).data.buffer);
+    this.createAreaMap();
+    this.sortedAreaList = Object.keys(this.areaMap).map(key => this.areaMap[key])
+    .sort((a, b) => {
+      return a.count - b.count;
+    })
+
+    let keys = Object.keys(this.centers);
+    let smalls = keys.filter(a => this.centers[a].radius <= this.R);
+    let smallAreas = smalls.map(a => this.areaMap[a]);
+    smallAreas = smallAreas.filter(a => a);
+
+    this.noCenterAreas = Object.keys(this.areaMap).filter(a => !this.centers[a]).map(a => this.areaMap[a]);
+    this.smallAreas = this.noCenterAreas.concat(smallAreas);
+    this.smallAreas.sort((a, b) => { return a.count - b.count });
+
+    if (this.smallAreas.length > this.N) {
+      this.result = 2;
+    } else {
+      this.result = 1;
+    }
+
+    this.noCloseLines = MyFloodFill.lineCloseCheck(this.page);
+
+    console.log("centers count: " + Object.keys(this.centers).length
+      + ", area count: " + Object.keys(this.areaMap).length 
+      + ", no center count: " + this.noCenterAreas.length
+      + ", small area: " + this.smallAreas.length);
+
+    this.initHint();
+
+    this.invalidate();
+
+    // // 生成floodfill各个过程的结果
+    // setTimeout(() => {
+    //   this.makeFloodFill();
+    // }, 1000);
+  }
+
+  initHint() {
+    // init colorCanvas
+    let ctx = this.colorCanvas.getContext('2d');
+    ctx.clearRect(0, 0, this.width, this.height);
+    let imgData = ctx.getImageData(0, 0, this.colorCanvas.width, this.colorCanvas.height);
+    let pixels = new Uint32Array(imgData.data.buffer);
+    this.smallAreas.forEach((area: FillArea) => {
+      let ai, aw, i, x, y;
+      let areaPixelCount = area.width() * area.height();
+      aw = area.width();
+      for (ai = 0; ai < areaPixelCount; ai++) {
+        x = area.left + ai % aw;
+        y = area.top + Math.floor(ai / aw);
+        i = y * this.width + x;
+        if (this.mapPixels[i] == area.color) {
+          pixels[i] = area.color;
+        }
+      }
+    })
+    ctx.putImageData(imgData, 0, 0);
+
+    // init markCanvas
+    ctx = this.markCanvas.getContext('2d');
+    ctx.clearRect(0, 0, this.width, this.height);
+    ctx.strokeStyle = 'orange';
+    ctx.lineWidth = 2;
+    
+    this.smallAreas.forEach(a => {
+      let rect = new Rect(a.left, a.top, a.right, a.bottom);
+      ctx.strokeRect(rect.left, rect.top, rect.width(), rect.height());      
+    })
+
+    ctx.strokeStyle = 'red';
+    this.noCenterAreas.forEach(a => {
+      let rect = new Rect(a.left, a.top, a.right, a.bottom);
+      ctx.strokeRect(rect.left, rect.top, rect.width(), rect.height());      
+    })
+
+    if (this.lineMark) {
+      ctx.lineWidth = 2;
+      ctx.strokeStyle = 'blue';
+      this.noCloseLines.forEach(a => {
+        ctx.strokeRect(a.left, a.top, a.width(), a.height());
+      })
+    }
+
+  }
+
+
+  update(page?: HTMLImageElement, map?: HTMLImageElement, raw?: HTMLImageElement, centers?: Centers) {
+    this.page = page || this.page;
+    this.map = map || this.map;
+    this.raw = raw || this.raw;
+
+    let centerChange = false;
+    if (centers && this.centers != centers) {
+      this.centers = centers;
+      centerChange = true;
+    }
+    if (centerChange) {  // 中心点更新完了才重新初始化
+      this.init();
+    } else {
+      this.result = 0;
+    }
+  }
+
+  /**
+   * Draw this layer.
+   * @Override Layer#draw(ctx)
+   */
+  override draw(ctx) {
+    ctx.imageSmoothingEnabled = this.smoothing;
+
+    ctx.fillStyle = "white";
+    ctx.fillRect(0, 0, this.width, this.height);
+
+    // for test
+    if (this.showFloodIdx < this.floodImgs.length) {
+      ctx.drawImage(this.floodImgs[this.showFloodIdx], 0, 0, this.page.width, this.page.height, 0, 0, this.width, this.height);
+      if (this.showPage) {
+        if (this.raw) {
+          ctx.drawImage(this.raw, 0, 0, this.page.width, this.page.height, 0, 0, this.width, this.height);
+        } else {
+          ctx.drawImage(this.page, 0, 0, this.page.width, this.page.height, 0, 0, this.width, this.height);
+        }
+      }
+      if (this.showMark) {
+        ctx.drawImage(this.markCanvas, 0, 0);
+      }
+      return;
+    }
+
+    if (this.showMap) {
+      ctx.drawImage(this.map, 0, 0, this.page.width, this.page.height, 0, 0, this.width, this.height);
+    }
+
+    if (this.showMark) {
+      ctx.drawImage(this.colorCanvas, 0, 0);
+    }
+
+    if (this.showPage) {
+      if (this.raw) {
+        ctx.drawImage(this.raw, 0, 0, this.page.width, this.page.height, 0, 0, this.width, this.height);
+      } else {
+        ctx.drawImage(this.page, 0, 0, this.page.width, this.page.height, 0, 0, this.width, this.height);
+      }
+    }
+
+    if (this.showMark) {
+      ctx.drawImage(this.markCanvas, 0, 0);
+    }
+
+    if (this.blinkCanvas) {
+      ctx.drawImage(this.blinkCanvas, 0, 0);
+    }
+
+  }
+
+  
+  /**
+   * Create area map from map pixels.
+   */
+   createAreaMap() {
+    let width = this.width;
+    let height = this.height;
+    let floodArr = this.mapPixels;
+    let areaMap = {};
+    let i, x, y, color;
+    for (i = 0; i < floodArr.length; i++) {
+      color = floodArr[i];
+      if (color == 0) continue;
+      x = i % width;
+      y = Math.floor(i / width);
+      if (areaMap[color]) {
+        areaMap[color].addPoint(x, y);
+      } else {
+        areaMap[color] = new FillArea(color, x, y);
+      }
+    }
+    this.areaMap = areaMap;
+  }
+
+  toggleMark() {
+    this.showMark = !this.showMark;
+    this.invalidate();
+  }
+
+  toggleLineMark() {
+    this.lineMark = !this.lineMark;
+    this.initHint();
+    this.invalidate();
+  }
+
+  togglePage() {
+    this.showPage = !this.showPage;
+    this.invalidate();
+  }
+
+  toggleMap() {
+    this.showMap = !this.showMap;
+    this.invalidate();
+  }
+
+  toggleParse() {
+    if (this.showFloodIdx == 100) {
+      this.showPage = false;
+      this.showFloodIdx = 0;
+    } else {
+      this.showFloodIdx = 100;
+      this.showPage = true;
+    }
+  }
+
+  getWorkBlob(): Promise<Blob> {
+    let canvas = Utils.createCanvas(this.width, this.height);
+    let ctx = canvas.getContext('2d');
+    ctx.drawImage(this.colorCanvas, 0, 0);
+    if (this.raw) {
+      ctx.drawImage(this.raw, 0, 0, this.page.width, this.page.height, 0, 0, this.width, this.height);
+    } else {
+      ctx.drawImage(this.page, 0, 0, this.page.width, this.page.height, 0, 0, this.width, this.height);
+    }
+    ctx.drawImage(this.markCanvas, 0, 0);
+    return new Promise((done, reject) => { canvas.toBlob(b => { done(b) }) });
+  }
+  
+
+  blinkCanvas: HTMLCanvasElement;
+  repeater: Repeater;
+  blinkByAreas(areas: FillArea[]) {
+    //Stop the previous running repeater.
+    if (this.repeater) this.repeater.cancel();
+
+    //没有考虑scale
+    let _blinkCanvas = Utils.createCanvas(this.width, this.height);
+    let ctx = _blinkCanvas.getContext('2d');
+    ctx.clearRect(0, 0, this.width, this.height);
+
+    let imgData = ctx.getImageData(0, 0, _blinkCanvas.width, _blinkCanvas.height);
+    let pixels = new Uint32Array(imgData.data.buffer);
+    //console.log(TAG, 'pixels=', pixels);
+
+    areas.forEach((area: FillArea) => {
+      let ai, aw, i, x, y;
+      let areaPixelCount = area.width() * area.height();
+      aw = area.width();
+      for (ai = 0; ai < areaPixelCount; ai++) {
+        x = area.left + ai % aw;
+        y = area.top + Math.floor(ai / aw);
+        i = y * this.width + x;
+        if (this.mapPixels[i] == area.color) {
+          pixels[i] = 0xffffffff;
+        }
+      }
+    })
+    ctx.putImageData(imgData, 0, 0);
+
+    let rect = new Rect(
+      Math.min.call(null, ...areas.map(a => a.left)),
+      Math.min.call(null, ...areas.map(a => a.top)),
+      Math.max.call(null, ...areas.map(a => a.right)),
+      Math.max.call(null, ...areas.map(a => a.bottom))
+    );
+
+    ctx.strokeStyle = '#ffffff';
+    ctx.lineWidth = 2;
+    ctx.strokeRect(rect.left, rect.top, rect.width(), rect.height());
+
+    this.blinkCanvas = _blinkCanvas;
+    this.invalidate();
+
+    this.repeater = new Repeater(() => {
+      this.blinkCanvas = this.blinkCanvas ? null : _blinkCanvas;
+      this.invalidate();
+    }, () => {
+      this.blinkCanvas = null;
+      this.repeater = null;
+      this.invalidate();
+    }, 5, 300).start();
+
+    return rect;
+  }
+
+
+  /**
+   * 生成floodfill各个阶段处理的结果图(调试用)
+   */
+  async makeFloodFill() {
+    for (let i = 0; i <= 7; i++) {
+      let img = await MyFloodFill.createMapImage(this.raw, i);
+      this.floodImgs.push(img);
+    }
+  }
+
+}

+ 1030 - 0
zorro/src/app/lib/filler/check-edit/myfloodfill.ts

@@ -0,0 +1,1030 @@
+import Utils from '../common/utils';
+import FillArea from '../common/fillarea';
+import Rect from '../core/rect';
+
+var _colors = {};
+
+export default class MyFloodFill {
+
+  private inputImage: HTMLImageElement;
+  private canvas: HTMLCanvasElement;
+  private ctx: CanvasRenderingContext2D;
+  private inputImageData: ImageData;
+  private floodFilled: ImageData;
+
+  constructor(image: HTMLImageElement, scale: number = 1, step: number = 0) {
+    this.inputImage = image;
+    this.canvas = createCanvas(image.width, image.height);
+    this.ctx = this.canvas.getContext('2d');
+    Utils.setImageSmoothing(this.ctx, false);
+
+    let _canvas = createCanvas(image.width * scale, image.height * scale);
+    let _ctx = _canvas.getContext('2d');
+    Utils.setImageSmoothing(_ctx, false);
+    _ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, _canvas.width, _canvas.height);
+    this.inputImageData = _ctx.getImageData(0, 0, _canvas.width, _canvas.height);
+    this.sumColor(this.inputImageData);
+    this.floodFilled = createFloodFillImage(this.inputImageData, scale, step);
+    // img data to image.
+    _ctx.putImageData(this.floodFilled, 0, 0);
+
+    this.ctx.drawImage(_canvas, 0, 0, _canvas.width, _canvas.height, 0, 0, this.canvas.width, this.canvas.height);
+  }
+
+  sumColor(imgData: ImageData) {
+    let hash = {};
+
+    /*
+    let _pixels = new Uint8Array(imgData.data.buffer);
+    for (var i = 0; i < _pixels.length / 4; i++) {
+      if (_pixels[i * 4 + 3] < 128) {
+        _pixels[i * 4] = 0;
+        _pixels[i * 4 + 1] = 0;
+        _pixels[i * 4 + 2] = 0;
+        _pixels[i * 4 + 3] = 0;
+      }
+    }
+    */
+
+    let pixels = new Uint32Array(imgData.data.buffer);
+    for (var i = 0; i < pixels.length; i++) {
+      if (hash[pixels[i]]) hash[pixels[i]]++;
+      else hash[pixels[i]] = 1;
+    }
+
+    let res = Object.keys(hash).map((colorStr: string) => {
+      let color = parseInt(colorStr);
+      let count = hash[colorStr];
+      return {
+        //color: color.toString(16),
+        color: Utils.getColorFromInteger(color),
+        count
+      };
+    }).sort((a, b) => {
+      return b.count - a.count
+    })
+    console.log('colorSum', res);
+  }
+
+  getAreaCount(): number {
+    let hash: any = {};
+    let pixels = new Uint32Array(this.floodFilled.data.buffer);
+    for (var i = 0; i < pixels.length; i++) {
+      hash[pixels[i]] = 1;
+    }
+    return Object.keys(hash).length;
+  }
+
+  /**
+   * 输出图片
+   * @returns 
+   */
+  toImage(): Promise<HTMLImageElement> {
+    return new Promise((done, reject) => {
+      var img = new Image();
+      img.onload = function () { done(img); };
+      img.onerror = reject;
+      img.src = this.canvas.toDataURL('image/png');
+    });
+  }
+
+  /**
+   * 输出文件
+   * @returns 
+   */
+  toBlob(): Promise<Blob> {
+    return new Promise((done, reject) => {
+      this.canvas.toBlob((blob: Blob) => done(blob), 'image/png');
+    });
+  }
+
+
+
+
+
+  /**
+   * 创建map, 输出文件
+   * @param image 
+   * @returns 
+   */
+  static createMapBlob(image: HTMLImageElement): Promise<Blob> {
+    return new MyFloodFill(image).toBlob();
+  }
+
+
+  /**
+   * 创建MAP, 输出图片
+   * @param image 线稿图片
+   * @returns 
+   */
+  static createMapImage(image: HTMLImageElement, step: number = 0): Promise<HTMLImageElement> {
+    return new MyFloodFill(image, 1, step).toImage();
+  }
+
+
+  /**
+   * 线条闭合检测
+   * @param image 
+   */
+  static lineCloseCheck(image: HTMLImageElement): Rect[] {
+    let _canvas = createCanvas(image.width, image.height);
+    let _ctx = _canvas.getContext('2d');
+    Utils.setImageSmoothing(_ctx, false);
+    _ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, _canvas.width, _canvas.height);
+    let original = _ctx.getImageData(0, 0, _canvas.width, _canvas.height);
+    let floodFilled = copyImage(_ctx, original);
+
+    // 将线条打薄,即透明度<128的置为空白
+    setLowAlphaToEmpty(original, floodFilled);
+
+    // 开始检测断点。 算法:寻找只有一个邻居的线条点
+    let result: {x: number, y: number}[] = [];
+    let floodArr = new Uint32Array(floodFilled.data.buffer);
+    let width = original.width;
+    let height = original.height;
+    let neighbours;
+    let count;
+    let x, y, index, idx, p, px, py, i, j, expand;
+    for (x = 2; x < width - 2; x++) {  // 略过2px的边缘
+      for (y = 2; y < height - 2; y++) {
+        index = y * width + x;
+        if (floodArr[index] != 0) {
+          neighbours = getNearestNeighbours(1);
+          count = 0;
+          for (j = 0; j < neighbours.length; j++) {
+            p = neighbours[j];
+            px = x + p.x;
+            py = y + p.y;
+            idx = py * width + px;
+            if (px > 0 && px < width && py > 0 && py < height && floodArr[idx] != 0) {
+              count++;
+            }
+          }
+          if (count <= 1) {  // 只有一个邻居,则判别为未闭合
+            result.push({x, y});
+          }
+        }
+      }
+    }
+
+    console.log("疑似断点数:" + result.length);
+
+    // 对结果进行进一步的筛选。算法:将找到的断点扩大为5*5的小矩形,判断有没有跟它相交的矩形,如果有则是有效结果,没有则剔除之
+    let smallRects: Rect[] = [];
+    let middleResult: {x: number, y: number}[] = [];
+    expand = 2;
+    for (i = 0; i < result.length; i++) {
+      let p = result[i];
+      let left = Math.max(0, p.x - expand);
+      let right = Math.min(width - 1, p.x + expand);
+      let top = Math.max(0, p.y - expand);
+      let bottom = Math.min(height - 1, p.y + expand);
+      let rect = new Rect(left, top, right, bottom);
+      smallRects.push(rect);
+    }
+    for (i = 0; i < smallRects.length; i++) {
+      let rect = smallRects[i];
+      for (j = i+1; j < smallRects.length; j++) {
+        if (checkIntersect(rect, smallRects[j])) {
+          break;
+        }
+      }
+      if (j < smallRects.length) { // 有相交,说明是有效的断点
+        middleResult.push(result[i]);
+      }
+    }
+    console.log("初筛后断点数:" + middleResult.length);
+
+    // 对外呈现的结果:对result结果进行进一步的整理,扩大为矩形框,同时去掉相交的矩形
+    let finalResult: Rect[] = [];
+    expand = 10;
+    for (i = 0; i < middleResult.length; i++) {
+      let p = middleResult[i];
+      let left = Math.max(0, p.x - expand);
+      let right = Math.min(width - 1, p.x + expand);
+      let top = Math.max(0, p.y - expand);
+      let bottom = Math.min(height - 1, p.y + expand);
+      let rect = new Rect(left, top, right, bottom);
+      for (j = 0; j < finalResult.length; j++) {
+        if (checkIntersect(finalResult[j], rect)) {
+          break;
+        }
+      }
+      if (j >= finalResult.length) { // 没有相交
+        finalResult.push(rect);
+      }
+    }
+
+    console.log("最终未闭合线条检测结果:" + finalResult.length);
+    return finalResult;
+  }
+
+
+}
+
+// 判断某点是否边缘点
+function isEdge(x, y, w, h): boolean {
+  if (x < 2 || x >= w - 2 || y < 2 || y >= h - 2 ) return true;
+  else return false;
+}
+
+/**
+ * Create a fill map for image
+ * @param imgData image data.
+ */
+function createFloodFillImage(imgData, scale: number = 1, step: number = 0) {
+  var ctx = createContext(imgData.width, imgData.height);
+  var width = imgData.width;
+  var height = imgData.height;
+
+  var original = imgData;
+  var floodFilled = copyImage(ctx, original);
+
+
+  console.time('createFloodFillImage');
+
+  if (step >= 1) {
+
+    // guoziyun: 改动的地方:在floodfill之前先对alpha线条点做处理,将其置为0。
+    // 目的是为了避免线条像素化后两个线条之间如果间隙过窄,会形成隔断。本应属于同一个区块的地方变成两个区块
+    // 实测发现这样会把线条打薄,导致很多区块失去线条约束都连成了一片了,不可取
+    setLowAlphaToEmpty(original, floodFilled);
+  }
+
+  if (step >= 2) {
+    // flood fill, 先对page图做一次floodfill
+    console.time('floodFillAll');
+    floodFillAll(floodFilled);
+    console.timeEnd('floodFillAll');
+  }
+
+  if (step >= 3) {
+    // remove original pixels in dest,将线条点置为透明(值非0的点)
+    setHightAlphaToEmpty(original, floodFilled);
+    // var srcArr = new Uint32Array(original.data.buffer);
+    // var destArr = new Uint32Array(floodFilled.data.buffer);
+    // for (var i = 0; i < srcArr.length; i++) {
+    //   if (srcArr[i] != 0) {
+    //     destArr[i] = 0;
+    //   }
+    // }
+  }
+
+  if (step >= 4) {
+    // 在这里先merge一轮小区块
+    console.time('mergeAreas');
+    mergeAreas(floodFilled);
+    console.timeEnd('mergeAreas');
+  }
+
+
+  if (step >= 5) {
+    // fill alpha gap 将原apha线条点(0 < alpha位 < 255)的点涂成与临近的区块颜色
+    console.time('fillAlphaGap');
+    fillAlphaGap(original, floodFilled, 50 * scale);
+    console.timeEnd('fillAlphaGap');
+  }
+
+  if (step >= 6) {
+    // 合并小区块
+    console.time('mergeAreas');
+    mergeAreas(floodFilled);
+    console.timeEnd('mergeAreas');
+  }
+  
+  if (step >= 7 ) {
+    // fill empty gap 最后将之前置为0的纯黑线条点涂为临近的区块颜色
+    console.time('fillEmptyGap');
+    fillEmptyGap(floodFilled, 10 * scale);
+    console.timeEnd('fillEmptyGap');
+  }
+  
+  console.timeEnd('createFloodFillImage');
+  return floodFilled;
+}
+
+/**
+ * 将线条像素化后alpha < 128 的过度点置为空
+ * @param srcImage source ImageData  ojbect.
+ * @param floodFilledImage flood filled ImageData object.
+ */
+function  setLowAlphaToEmpty(srcImage, floodFilledImage) {
+  var srcBuf = srcImage.data; // to get the alpha directly
+  var floodArr = new Uint32Array(floodFilledImage.data.buffer);
+  var width = srcImage.width;
+  var height = srcImage.height;
+  var j, x, y, index;
+
+  // set all alpha points to empty
+  for (x = 0; x < width; x++) {
+    for (y = 0; y < height; y++) {
+      if (isEdge(x, y, width, height)) continue;  // 如果是边缘点不做此处理,主要是为了解决边缘虚化区块容易无法很好分隔的问题 
+      index = y * width + x;
+      j = index * 4 + 3; // alpha byte index
+      if (srcBuf[j] < 128) { // 颜色比较浅的才认为是空白,比较深的还当作线条
+        floodArr[index] = 0;
+      }
+    }
+  }
+}
+
+/**
+ * 将线条像素化后alpha >= 128 的点置为空
+ * @param srcImage source ImageData  ojbect.
+ * @param floodFilledImage flood filled ImageData object.
+ */
+ function setHightAlphaToEmpty(srcImage, floodFilledImage) {
+  var srcBuf = srcImage.data; // to get the alpha directly
+  var floodArr = new Uint32Array(floodFilledImage.data.buffer);
+  var width = srcImage.width;
+  var height = srcImage.height;
+  var j, x, y, index;
+
+  // set all alpha points to empty
+  for (x = 0; x < width; x++) {
+    for (y = 0; y < height; y++) {
+      index = y * width + x;
+      j = index * 4 + 3; // alpha byte index
+      if (srcBuf[j] >= 128 || (srcBuf[j] > 0 && isEdge(x, y, width, height)) ) {  //别忘了边缘点
+        floodArr[index] = 0;
+      }
+    }
+  }
+}
+
+/**
+ * Flood Fill image with random Color
+ */
+function floodFillAll(imgData) {
+  var width = imgData.width;
+  var height = imgData.height;
+  var pixels = new Uint32Array(imgData.data.buffer);
+  var x, y;
+  // var oldColor = new Uint8Array([0, 0, 0, 0]);
+  var oldColor = 0;
+  var newColor;
+  var fillFunc = floodFill;
+  // if(Math.random() < 0.5) fillFunc = floodFillEdge;
+  var maxStack = 0;
+  var stackSize;
+
+  // console.log('fillFunc: ' + fillFunc.name);
+  for (var i = 0; i < pixels.length; i++) {
+    if (pixels[i] == 0) {
+      x = i % width;
+      y = Math.floor(i / width);
+      newColor = randomColor();
+      // newColor = randomUniqueColor();
+      stackSize = fillFunc(imgData, x, y, oldColor, newColor);
+      if (stackSize > maxStack)
+        maxStack = stackSize;
+      // floodFill(imgData, x, y, oldColor, newColor);
+      // floodFillEdge(imgData, x, y, oldColor, newColor);
+    }
+  }
+  console.log('floodFillAll@maxStackSize', maxStack * 100 / (width * height));
+}
+
+/**
+ *
+ * Flood fill x,y.
+ * @param imgData ctx.getImageData()
+ * @param x
+ * @param y
+ * @param oldColor Uint8Array [r, g, b, a]
+ * @param oldColor Uint8Array [r, g, b, a]
+ * @param oldColor int
+ * @param newColor int
+ *
+ */
+function floodFill(imgData, x, y, oldColor, newColor) {
+  var width = imgData.width;
+  var height = imgData.height;
+  var stack = new Array();
+  var index = y * width + x;
+  var pixels = new Uint32Array(imgData.data.buffer);
+  // var oc = new Uint32Array(oldColor.buffer);
+  var oc = oldColor;
+  // var nc = new Uint32Array(newColor.buffer);
+  var nc = newColor;
+  // console.log('start flood fill: ', x, y, width, height, oldColor, newColor);
+  var maxStack = 0;
+
+  if (pixels[index] == oc) {
+    stack.push(index);
+  }
+
+  var p;
+  while (stack.length > 0) {
+    if (stack.length > maxStack)
+      maxStack = stack.length;
+    p = stack.pop();
+    x = p % width;
+
+    // if p is colored, continue
+    if (pixels[p] == nc)
+      continue;
+
+    pixels[p] = nc;
+    // left
+    if (x > 0 && pixels[p - 1] == oc) {
+      stack.push(p - 1);
+    }
+    // top
+    if (p > (width - 1) && pixels[p - width] == oc) {
+      stack.push(p - width);
+    }
+    // right
+    if (x < (width - 1) && pixels[p + 1] == oc) {
+      stack.push(p + 1);
+    }
+    // bottom
+    if (p < ((height - 1) * width) && pixels[p + width] == oc) {
+      stack.push(p + width);
+    }
+
+    // // left top
+    // if (x > 0 && p > (width -1) && pixels[p - width -1] == oc) {
+    //   stack.push(p - width -1);
+    // }
+    // // right top
+    // if (x < (width -1) && p > (width -1) && pixels[p - width + 1] == oc) {
+    //   stack.push(p - width + 1);
+    // }
+    // // left bottom
+    // if (x > 0 && p < ((height - 1) * width) && pixels[p + width -1] == oc) {
+    //   stack.push(p + width - 1);
+    // }
+    // // rigth bottom
+    // if (x < (width - 1) && p < ((height - 1) * width) && pixels[p + width + 1] == oc) {
+    //   stack.push(p + width + 1);
+    // }
+  }
+  // console.log('floodFill@maxStack', maxStack / (width*height));
+  return maxStack;
+}
+
+/**
+ *
+ * Flood fill x,y.
+ * @param imgData ctx.getImageData()
+ * @param x
+ * @param y
+ * @param oldColor Uint8Array [r, g, b, a]
+ * @param newColor Uint8Array [r, g, b, a]
+ *
+ */
+function floodFillEdge(imgData, x, y, oldColor, newColor) {
+  var width = imgData.width;
+  var height = imgData.height;
+  var stack = new Array();
+  var index = y * width + x;
+  var pixels = new Uint32Array(imgData.data.buffer);
+  var oc = new Uint32Array(oldColor.buffer);
+  var nc = new Uint32Array(newColor.buffer);
+  // console.log('start flood fill: ', x, y, width, height, oldColor, newColor);
+
+  // edges
+  var estack = new Array();
+
+  if (pixels[index] == oc[0]) {
+    stack.push(index);
+  }
+
+  var p;
+  while (stack.length > 0) {
+    p = stack.pop();
+    x = p % width;
+
+    // if p is colored, continue
+    if (pixels[p] == nc[0])
+      continue;
+
+    pixels[p] = nc[0];
+
+    // left
+    if (x > 0 && pixels[p - 1] == oc[0]) {
+      stack.push(p - 1);
+    } else if (x > 0 && pixels[p - 1] != oc[0]) {
+      estack.push(p - 1);
+    }
+
+    // top
+    if (p > (width - 1) && pixels[p - width] == oc[0]) {
+      stack.push(p - width);
+    } else if (p > (width - 1) && pixels[p - width] != oc[0]) {
+      estack.push(p - width);
+    }
+
+    // right
+    if (x < (width - 1) && pixels[p + 1] == oc[0]) {
+      stack.push(p + 1);
+    } else if (x < (width - 1) && pixels[p + 1] != oc[0]) {
+      estack.push(p + 1);
+    }
+
+    // bottom
+    if (p < ((height - 1) * width) && pixels[p + width] == oc[0]) {
+      stack.push(p + width);
+    } else if (p < ((height - 1) * width) && pixels[p + width] != oc[0]) {
+      estack.push(p + width);
+    }
+  }
+
+  var r, g, b, a, t;
+  var curPixel = new Uint8Array(4);
+  var curPixel32 = new Uint32Array(curPixel.buffer);
+
+  // fill edges.
+  while (estack.length > 0) {
+    p = estack.pop();
+    // if p is alread colored.
+    if (pixels[p] == nc[0])
+      continue;
+    // if p is oldColor, enter a new fill area, continue
+    if (pixels[p] == oc[0])
+      continue;
+    // calculate p's distance witha oldColor
+    curPixel32[0] = pixels[p];
+    t = Math.sqrt(Math.pow(oldColor[0] - curPixel[0], 2) +
+      Math.pow(oldColor[1] - curPixel[1], 2) +
+      Math.pow(oldColor[2] - curPixel[2], 2) +
+      Math.pow(oldColor[3] - curPixel[3], 2));
+
+    if (t > 128)
+      continue;
+    pixels[p] = nc[0];
+
+    // left
+    if (x > 0) {
+      estack.push(p - 1);
+    }
+
+    // top
+    if (p > (width - 1)) {
+      estack.push(p - width);
+    }
+
+    // right
+    if (x < (width - 1)) {
+      estack.push(p + 1);
+    }
+
+    if (p < ((height - 1) * width)) {
+      estack.push(p + width);
+    }
+  }
+}
+
+/**
+ *  Build neighbourhood for areas.
+ *  @param floodArr
+ *  @param areaMap
+ *  @param area which need to build it's neighbours
+ */
+/*
+function buildNeighbour(floodArr, areaMap, area) { var areaMap = {}; }
+*/
+
+function createFillAreaMap(floodImg) {
+  var width = floodImg.width;
+  var height = floodImg.height;
+  var floodArr = new Uint32Array(floodImg.data.buffer);
+  var areaMap = {};
+  for (var i = 0; i < floodArr.length; i++) {
+    var color = floodArr[i];
+    var x = i % width;
+    var y = Math.floor(i / width);
+    if (areaMap[color]) {
+      areaMap[color].addPoint(x, y);
+    } else {
+      areaMap[color] = new FillArea(color, x, y);
+    }
+  }
+  return areaMap;
+}
+
+function mergeAreas(floodFilledImage, threshold?) {
+  const kk = (floodFilledImage.width / 1000) * 20;
+  threshold = threshold || kk;
+  var areaMap = createFillAreaMap(floodFilledImage);
+  var keys = Object.keys(areaMap);
+  var floodArr = new Uint32Array(floodFilledImage.data.buffer);
+  var width = floodFilledImage.width;
+  var height = floodFilledImage.height;
+  var areas = keys.map(function (key) { return areaMap[key]; });
+  var smallAreas = areas.filter(function (a) { return a.count < threshold; });
+
+  console.log('mergeAreas@areaSize', areas.length, smallAreas.length);
+  var i, j, x, y, index, p, px, py;
+  var neighbours = getNearestNeighbours(1);
+  smallAreas.forEach(function (area) {
+    // area.neighbours = [];
+    let neighbourSet = new Set();
+    for (x = area.left; x <= area.right; x++) {
+      for (y = area.top; y <= area.bottom; y++) {
+        index = y * width + x;
+        if (floodArr[index] == area.color) {
+          // check pixe's neighbour
+          for (j = 0; j < neighbours.length; j++) {
+            p = neighbours[j];
+            px = x + p.x;
+            py = y + p.y;
+            index = py * width + px;
+            if (px > 0 && px < width && py > 0 && py < height &&
+              floodArr[index] != 0 && floodArr[index] != area.color) {
+              // area.neighbours.push(areaMap[floodArr[index]]);
+              neighbourSet.add(areaMap[floodArr[index]]);
+            }
+          }
+        }
+      }
+    }
+    area.neighbours = Array.from(neighbourSet);
+  });
+
+  // remove the area has no neighbours.
+  smallAreas =
+    smallAreas.filter(function (area) { return area.neighbours.length > 0; });
+  // console.log('mergeAreas@areaSize', areas.length, smallAreas.length);
+
+  // for each samll area.
+  // get it's direct neighbour area list
+  // select the biggest one, and merge with it.
+  // if no neighbours mark it can't merge.
+
+  /**
+   * merge another area
+   */
+  function mergeToMe(me, other) {
+    // if i am a small area.
+    // merge other's neighbours
+    if (me.neighbours) {
+      me.neighbours =
+        me.neighbours.concat(other.neighbours)
+          .filter(function (a) { return a != me && a != other; });
+    }
+
+    // set other merged
+    other.merged = true;
+
+    for (x = other.left; x <= other.right; x++) {
+      for (y = other.top; y <= other.bottom; y++) {
+        index = y * width + x;
+        if (floodArr[index] == other.color) {
+          floodArr[index] = me.color;
+          me.addPoint(x, y);
+        }
+      }
+    }
+  }
+
+  var loop = 0;
+  do {
+    loop++;
+
+    smallAreas.forEach(function (area) {
+      if (area.merged)
+        return;
+      var neighbour;
+      for (var i = 0; i < area.neighbours.length; i++) {
+        // merge other small areas.
+        neighbour = area.neighbours[i];
+        // if neighbour is merged or neighbour is a large one
+        if (neighbour.merged || !neighbour.neighbours) {
+          continue;
+        }
+        mergeToMe(area, neighbour);
+      }
+      if (area.neighbours.length > 0) {
+        // merge me to the first large neighbour
+        mergeToMe(area.neighbours[0], area);
+      }
+    });
+
+    smallAreas = smallAreas.filter(function (
+      area) { return !area.merged && area.neighbours.length > 0; });
+
+    console.log('mergeAreas@loop:' + loop, smallAreas.length);
+
+    if (smallAreas.length == 0)
+      break;
+
+  } while (loop < 4);
+}
+
+/**
+ * Fill blank pixels with color.
+ */
+function fillEmptyGap(floodFilledImage, maxDistance) {
+  var floodArr = new Uint32Array(floodFilledImage.data.buffer);
+  var width = floodFilledImage.width;
+  var height = floodFilledImage.height;
+  var emptyPoints = []; // x,y,x,y,x,y......
+  var unFilledPoints = [];
+  var taskArr = []; // index, color, index, color
+  var i, j, x, y, index, color, p, px, py;
+
+  var neighbours = getNearestNeighbours(1);
+
+  // get all empty points
+  for (x = 0; x < width; x++) {
+    for (y = 0; y < height; y++) {
+      index = y * width + x;
+      if (floodArr[index] == 0) {
+        emptyPoints.push(x, y);
+      }
+    }
+  }
+
+  console.log('FillEmptyGap#emptyPoints@length', emptyPoints.length / 2);
+  var loop = 0;
+
+  do {
+    loop++;
+
+    // loop all alpha points and try to fill.
+    var found;
+    for (i = 0; i < emptyPoints.length; i += 2) {
+      x = emptyPoints[i];
+      y = emptyPoints[i + 1];
+      found = false;
+      for (j = 0; j < neighbours.length; j++) {
+        p = neighbours[j];
+        px = x + p.x;
+        py = y + p.y;
+        index = py * width + px;
+        if (px > 0 && px < width && py > 0 && py < height &&
+          floodArr[index] != 0) {
+          taskArr.push(y * width + x, floodArr[index]);
+          found = true;
+          break;
+        }
+      }
+      if (!found)
+        unFilledPoints.push(x, y);
+    }
+
+    console.log('fillEmptyGap@loop:' + loop, emptyPoints.length,
+      taskArr.length / 2, unFilledPoints.length / 2);
+
+    // no more alpha pixels can find it's neighbours.
+    if (taskArr.length == 0)
+      break;
+
+    // do the task
+    for (i = 0; i < taskArr.length; i += 2) {
+      index = taskArr[i];
+      color = taskArr[i + 1];
+      floodArr[index] = color;
+    }
+
+    taskArr = [];
+    emptyPoints = unFilledPoints;
+    unFilledPoints = [];
+
+  } while (loop <= maxDistance);
+}
+
+/**
+ * Fill pixels in srcImage with alpha<255
+ * with the nearest filled color
+ * @param srcImage source ImageData  ojbect.
+ * @param floodFilledImage flood filled ImageData object.
+ * @param maxDistance
+ */
+function fillAlphaGap(srcImage, floodFilledImage, maxDistance) {
+  maxDistance = maxDistance || 2;
+  var srcArr = new Uint32Array(srcImage.data.buffer);
+  var srcBuf = srcImage.data; // to get the alpha directly
+  var floodArr = new Uint32Array(floodFilledImage.data.buffer);
+  var width = srcImage.width;
+  var height = srcImage.height;
+  var alphaPoints = []; // x,y,x,y,x,y......
+  var unFilledPoints = [];
+  var taskArr = []; // index, color, index, color
+  var i, j, x, y, index, color, p, px, py;
+
+  var neighbours = getNearestNeighbours(1);
+
+  // get all alpha points
+  for (x = 0; x < width; x++) {
+    for (y = 0; y < height; y++) {
+      index = y * width + x;
+      j = index * 4 + 3; // alpha byte index
+      if (floodArr[index] == 0 && srcBuf[j] < 255 
+        && (srcBuf[j] >= 128 || (srcBuf[j] > 0 && isEdge(x, y, width, height)))) {  // < 128 的前面已经处理过了
+        alphaPoints.push(x, y);
+      }
+    }
+  }
+
+  // console.log('neighbours:', neighbours);
+  // console.log('alphaPoints@length', alphaPoints.length / 2);
+  var loop = 0;
+
+  do {
+    loop++;
+
+    // loop all alpha points and try to fill.
+    var found;
+    for (i = 0; i < alphaPoints.length; i += 2) {
+      x = alphaPoints[i];
+      y = alphaPoints[i + 1];
+      found = false;
+      for (j = 0; j < neighbours.length; j++) {
+        p = neighbours[j];
+        px = x + p.x;
+        py = y + p.y;
+        index = py * width + px;
+        if (px > 0 && px < width && py > 0 && py < height &&
+          floodArr[index] != 0) {
+          taskArr.push(y * width + x, floodArr[index]);
+          found = true;
+          break;
+        }
+      }
+      if (!found)
+        unFilledPoints.push(x, y);
+    }
+
+    console.log('fillAlphaGap@loop:' + loop, alphaPoints.length,
+      taskArr.length / 2, unFilledPoints.length / 2);
+
+    // no more alpha pixels can find it's neighbours.
+    if (taskArr.length == 0)
+      break;
+
+    // do the task
+    for (i = 0; i < taskArr.length; i += 2) {
+      index = taskArr[i];
+      color = taskArr[i + 1];
+      floodArr[index] = color;
+    }
+
+    taskArr = [];
+    alphaPoints = unFilledPoints;
+    unFilledPoints = [];
+
+  } while (loop <= maxDistance);
+}
+
+/**
+ * Get the nearest N neighbours
+ * and sort by distance.
+ * @param n integer >=1
+ */
+function getNearestNeighbours(n) {
+  var distArr = [];
+  for (var i = (-1) * n; i <= n; i++) {
+    for (var j = (-1) * n; j <= n; j++) {
+      if (i == 0 && j == 0) {
+        // exclued self
+        continue;
+      }
+      var distance = Math.pow(i, 2) + Math.pow(j, 2);
+      distArr.push({ x: i, y: j, dist: distance });
+    }
+  }
+
+  distArr.sort(function (a, b) { return a.dist - b.dist; });
+
+  return distArr;
+}
+
+function fillGapOld(ctx, floodFilled) {
+  // create gap bitmap
+  var gapImg = ctx.createImageData(floodFilled);
+  var width = floodFilled.width;
+  var height = floodFilled.height;
+  var maxDistance = 2;
+  var floodArr = new Uint32Array(floodFilled.data.buffer);
+  var gapArr = new Uint32Array(gapImg.data.buffer);
+
+  // Build a nearest points array based on (0, 0)
+  // and sort it by it's distance to (0,0)
+  var distArr = [];
+  for (var i = (-1) * maxDistance; i <= maxDistance; i++) {
+    for (var j = (-1) * maxDistance; j <= maxDistance; j++) {
+      if (i != 0 && j != 0) {
+        var distance = Math.pow(i, 2) + Math.pow(j, 2);
+        distArr.push({ x: i, y: j, dist: distance });
+      }
+    }
+  }
+
+  distArr.sort(function (a, b) { return a.dist - b.dist; });
+
+  // for every gap pixels in flood image, find the nearest colored pixel
+  // and set as it's color.
+  var i: number, j: number, x: number, y: number, px: number, py: number;
+  for (i = 0; i < floodArr.length; i++) {
+    if (floodArr[i] == 0) {
+      x = i % width;
+      y = Math.floor(i / width);
+      /*
+      var arr = distArr.map(function(obj){
+        return {
+          x : obj.x + x,
+          y : obj.y + y
+        }
+      });
+      */
+      for (j = 0; j < distArr.length; j++) {
+        var p = distArr[j];
+        px = x + p.x;
+        py = y + p.y;
+
+        // if(p.x > 0 && p.x < width && p.y > 0 && p.y < height &&
+        // floodArr[p.y*width+p.x] !=0 ) {
+        if (px > 0 && px < width && py > 0 && py < height &&
+          floodArr[py * width + px] != 0) {
+          gapArr[y * width + x] = floodArr[py * width + px];
+          break;
+        }
+      }
+      // this is the gap
+      // find nearest filled pixel
+    }
+  }
+
+  // merge the gap bitmap with the flood bitmap
+  for (var i = 0; i < floodArr.length; i++) {
+    if (floodArr[i] == 0) {
+      floodArr[i] = gapArr[i];
+    }
+  }
+}
+
+/**
+ *  Copy image data from src
+ *  @param ctx  canvas context
+ *  @param src  source image data.
+ */
+function copyImage(ctx, src) {
+  var dest = ctx.createImageData(src);
+  var srcArr = new Uint32Array(src.data.buffer);
+  var destArr = new Uint32Array(dest.data.buffer);
+  for (var i = 0; i < srcArr.length; i++) {
+    destArr[i] = srcArr[i];
+  }
+  return dest;
+}
+
+/**
+ * Create canvas context by specified dimension.
+ */
+function createContext(width, height) {
+  return createCanvas(width, height).getContext('2d');
+}
+
+/**
+ * Create canvas
+ * @param width
+ * @param height
+ */
+function createCanvas(width, height) {
+  var canvas = document.createElement('canvas');
+  canvas.width = width;
+  canvas.height = height;
+  return canvas;
+}
+
+function randomUniqueColor() {
+  do {
+    var color = randomColor();
+    if (!_colors[color]) {
+      _colors[color] = color;
+      return color;
+    }
+  } while (1)
+  return randomColor();
+}
+
+function randomColor() {
+  var color = new Uint8Array([0, 0, 0, 255]);
+  for (var i = 0; i < 3; i++) {
+    color[i] = Math.floor(Math.random() * 256);
+  }
+  var u32 = new Uint32Array(color.buffer);
+  return u32[0];
+}
+
+function emptyColor() {
+  var color = new Uint8Array([255, 255, 255, 255]);
+  var a = new Uint32Array(color.buffer);
+  return a[0];
+}
+
+
+// 判断两个矩形是否相交
+function checkIntersect(rectA: Rect, rectB: Rect): boolean {
+  let nonIntersect = 
+    (rectB.right < rectA.left) || 
+    (rectB.left > rectA.right) ||
+    (rectB.bottom < rectA.top) ||
+    (rectB.top > rectA.bottom);
+  
+  let intersect = !nonIntersect;
+  return intersect;
+}

+ 219 - 0
zorro/src/app/lib/filler/color/color.ts

@@ -0,0 +1,219 @@
+import dE00 from "./dE00";
+
+export const D65 = [
+  0.4124564, 0.3575761, 0.1804375,
+  0.2126729, 0.7151522, 0.0721750,
+  0.0193339, 0.1191920, 0.9503041];
+
+export const rD65 = [
+  3.2404542, -1.5371385, -0.4985314,
+  -0.9692660, 1.8760108, 0.0415560,
+  0.0556434, -0.2040259, 1.0572252
+]
+
+
+// 0.008856  Initial CIE standard
+// 216/24389 Intent of CIE standard, updated to standard in 2004
+export const cieE = 216 / 24389;
+// 903.3     Initial CIE standard
+// 24389/27  Intent of CIE standard, updated to standard in 2004
+export const cieK = 24389 / 27;
+
+
+export interface referenceWhite {
+  x: number
+  y: number
+  z: number
+}
+export const D65_ReferenceWhite = { x: 0.95047, y: 1, z: 1.08883 };
+
+/**
+ * CIELab color space.
+ */
+export class Lab {
+  constructor(public L: number = 0, public a: number = 0, public b: number = 0) { }
+  toString() { return `Lab(${this.L}, ${this.a}, ${this.b})`; }
+
+  static fromsRGB(srgb: sRGB): Lab {
+    return srgb.toXYZ().toLab();
+  }
+
+  static fromRGBA(rgba: number): Lab {
+    return sRGB.fromRGBA(rgba).toXYZ().toLab();
+  }
+
+
+  deltaE(other: Lab): number {
+    //return this.deltaE76(other);
+    //return this.deltaE94(other);
+    return this.deltaE00(other);
+  }
+
+  /**
+   * 计算两个颜色值的距离
+   * 参考: https://en.wikipedia.org/wiki/Color_difference#CIE76 
+   * @param other 
+   * @returns 
+   */
+  deltaE76(other: Lab): number {
+    return Math.sqrt(
+      Math.pow(this.L - other.L, 2) +
+      Math.pow(this.a - other.a, 2) +
+      Math.pow(this.b - other.b, 2)
+    )
+  }
+
+  /**
+   * 计算两个颜色的距离
+   * 参考: https://en.wikipedia.org/wiki/Color_difference#CIE94
+   * @param other 
+   * @returns 
+   */
+  deltaE94(other: Lab): number {
+    let self = this;
+    const KL = 1;
+    const K1 = 0.045;
+    const K2 = 0.015;
+    const wLightness = 1;
+    const wChroma = 1;
+
+
+    let deltaL = self.L - other.L;
+    let c1 = Math.sqrt(self.a * self.a + self.b * self.b);
+    let c2 = Math.sqrt(other.a * other.a + other.b * other.b);
+    let deltaCab = c1 - c2;
+
+    let deltaA = self.a - other.a;
+    let deltaB = self.b - other.b;
+    let deltaHab = Math.sqrt(deltaA * deltaA + deltaB * deltaB - deltaCab * deltaCab) || 0;
+
+    let Sl = 1;
+    let Sc = 1 + K1 * c1;
+    let Sh = 1 + K2 * c1;
+
+    return Math.sqrt(
+      Math.pow(deltaL / (KL * Sl), 2) +
+      Math.pow(deltaCab / (wChroma * Sc), 2) +
+      Math.pow(deltaHab / Sh, 2)
+    );
+
+  }
+
+  deltaE00(other: Lab) {
+    let self = this;
+    let x1 = { L: self.L, A: self.a, B: self.b };
+    let x2 = { L: other.L, A: other.a, B: other.b };
+    return new dE00(x1, x2, { hue: 1 }).getDeltaE();
+  }
+
+}
+
+/**
+ * XYZ color space
+ */
+export class XYZ {
+  constructor(public x: number = 0, public y: number = 0, public z: number = 0) { }
+  toString() { return `XYZ(${this.x}, ${this.y}, ${this.z})`; }
+
+  tosRGB(): sRGB {
+    let srgb = new sRGB();
+    let lsrgb = new sRGB(); //linear sRGB
+    let xyz = this;
+    lsrgb.r = rD65[0] * xyz.x + rD65[1] * xyz.y + rD65[2] * xyz.z;
+    lsrgb.g = rD65[3] * xyz.x + rD65[4] * xyz.y + rD65[5] * xyz.z;
+    lsrgb.b = rD65[6] * xyz.x + rD65[7] * xyz.y + rD65[8] * xyz.z;
+    srgb.r = lsrgb.r <= 0.0031308 ? 12.92 * lsrgb.r : 1.055 * Math.pow(lsrgb.r, 1 / 2.4) - 0.055;
+    srgb.g = lsrgb.g <= 0.0031308 ? 12.92 * lsrgb.g : 1.055 * Math.pow(lsrgb.g, 1 / 2.4) - 0.055;
+    srgb.b = lsrgb.b <= 0.0031308 ? 12.92 * lsrgb.b : 1.055 * Math.pow(lsrgb.b, 1 / 2.4) - 0.055;
+    return srgb;
+  }
+
+  toLab(): Lab {
+    let xyz = this;
+    let xr = xyz.x / D65_ReferenceWhite.x;
+    let yr = xyz.y / D65_ReferenceWhite.y;
+    let zr = xyz.z / D65_ReferenceWhite.z;
+    let fx = xr > cieE ? Math.pow(xr, 1 / 3) : (cieK * xr + 16) / 116;
+    let fy = yr > cieE ? Math.pow(yr, 1 / 3) : (cieK * yr + 16) / 116;
+    let fz = zr > cieE ? Math.pow(zr, 1 / 3) : (cieK * zr + 16) / 116;
+
+    let L = 116 * fy - 16;
+    let a = 500 * (fx - fy);
+    let b = 200 * (fy - fz);
+    L = Math.max(L, 0) // specular white can be over 100
+
+    return new Lab(L, a, b);
+  }
+}
+
+/**
+ * Standard RGB color
+ */
+export class sRGB {
+  constructor(public r: number = 0, public g: number = 0, public b: number = 0) { }
+  toString() { return `sRGB(${this.r}, ${this.g}, ${this.b})` }
+
+  /**
+   * Convert to XYZ color space.
+   * @returns 
+   */
+  toXYZ(): XYZ {
+    let srgb = this;
+    let lsrgb: sRGB = new sRGB(); //linear sRGB
+    let xyz: XYZ = new XYZ();
+    lsrgb.r = srgb.r <= 0.0405 ? srgb.r / 12.92 : Math.pow((srgb.r + 0.055) / 1.055, 2.4);
+    lsrgb.g = srgb.g <= 0.0405 ? srgb.g / 12.92 : Math.pow((srgb.g + 0.055) / 1.055, 2.4);
+    lsrgb.b = srgb.b <= 0.0405 ? srgb.b / 12.92 : Math.pow((srgb.b + 0.055) / 1.055, 2.4);
+    xyz.x = lsrgb.r * D65[0] + lsrgb.g * D65[1] + lsrgb.b * D65[2];
+    xyz.y = lsrgb.r * D65[3] + lsrgb.g * D65[4] + lsrgb.b * D65[5];
+    xyz.z = lsrgb.r * D65[6] + lsrgb.g * D65[7] + lsrgb.b * D65[8];
+    return xyz;
+  }
+
+  /**
+   * Convert sRGBA to color hex.
+   * @returns 
+   */
+  toHex(): string {
+    let r = this.partToHex(this.r);
+    let g = this.partToHex(this.g);
+    let b = this.partToHex(this.b);
+    return [r, g, b].join('');
+  }
+
+  /**
+   * Convert single rgb part to HEX.
+   * @param part 
+   * @returns 
+   */
+  partToHex(part: number) {
+    let i: number = Math.round(part * 255);
+    i = i > 255 ? 255 : i;
+    i = i < 0 ? 0 : i;
+    return i.toString(16).padStart(2, '0');
+  }
+
+  /**
+   * RGBA integer to sRGB
+   * @param rgba 
+   * @returns 
+   */
+  static fromRGBA(rgba: number) {
+    let uint32 = new Uint32Array(1);
+    uint32[0] = rgba;
+    let uint8 = new Uint8Array(uint32.buffer);
+    return new sRGB(uint8[0] / 255, uint8[1] / 255, uint8[2] / 255);
+  }
+
+  static white() : sRGB {
+    return new sRGB(1, 1, 1);
+  }
+
+  static black() : sRGB {
+    return new sRGB(0, 0, 0);
+  }
+
+}
+
+
+

+ 341 - 0
zorro/src/app/lib/filler/color/dE00.ts

@@ -0,0 +1,341 @@
+
+/**
+ * @class dE00
+ * @classdesc
+ * The CIE2000 color difference algorithm.
+ * http://en.wikipedia.org/wiki/Color_difference#CIEDE2000
+ * @constructs dE00
+ * @memberOf DeltaE
+ * @property {object} x1 The LAB color configuration object.
+ * @property {number} x1.L The lightness value, on scale of 0-100.
+ * @property {number} x1.A The chroma value, on scale of -128 to 128.
+ * @property {number} x1.B The hue value, on scale of -128 to 128.
+ * @property {object} x2 The LAB color configuration object.
+ * @property {number} x2.L The lightness value, on scale of 0-100.
+ * @property {number} x2.A The chroma value, on scale of -128 to 128.
+ * @property {number} x2.B The hue value, on scale of -128 to 128.
+ * @property {object} weights The weights configuration object.
+ * @property {number} weights.lightness A weight factor to apply to lightness.
+ * @property {number} weights.chroma A weight factor to apply to chroma.
+ * @property {number} weights.hue A weight factor to apply to hue.
+ * @example
+ * var deltaE = new dE00(
+ *     {L:50, A:50, B:50},
+ *     {L:100, A:50, B:50},
+ * );
+ * console.log(deltaE.getDeltaE());
+ */
+export default class dE00 {
+    x1: any;
+    x2: any;
+    weights: any;
+    ksubL: any;
+    ksubC: any;
+    ksubH: any;
+    deltaLPrime: number;
+    LBar: number;
+    C1: number;
+    C2: number;
+    CBar: number;
+    aPrime1: any;
+    aPrime2: any;
+    CPrime1: number;
+    CPrime2: number;
+    CBarPrime: number;
+    deltaCPrime: number;
+    SsubL: number;
+    SsubC: number;
+    hPrime1: number;
+    hPrime2: number;
+    deltahPrime: number;
+    deltaHPrime: number;
+    HBarPrime: number;
+    T: number;
+    SsubH: number;
+    RsubT: number;
+    
+    constructor(x1, x2, weights) {
+        var sqrt = Math.sqrt;
+        var pow = Math.pow;
+
+        this.x1 = x1;
+        this.x2 = x2;
+
+        this.weights = weights || {};
+        this.ksubL = this.weights.lightness || 1;
+        this.ksubC = this.weights.chroma || 1;
+        this.ksubH = this.weights.hue || 1;
+
+        // Delta L Prime
+        this.deltaLPrime = x2.L - x1.L;
+
+        // L Bar
+        this.LBar = (x1.L + x2.L) / 2;
+
+        // C1 & C2
+        this.C1 = sqrt(pow(x1.A, 2) + pow(x1.B, 2));
+        this.C2 = sqrt(pow(x2.A, 2) + pow(x2.B, 2));
+
+        // C Bar
+        this.CBar = (this.C1 + this.C2) / 2;
+
+        // A Prime 1
+        this.aPrime1 = x1.A +
+            (x1.A / 2) *
+            (1 - sqrt(
+                pow(this.CBar, 7) /
+                (pow(this.CBar, 7) + pow(25, 7))
+            ));
+
+        // A Prime 2
+        this.aPrime2 = x2.A +
+            (x2.A / 2) *
+            (1 - sqrt(
+                pow(this.CBar, 7) /
+                (pow(this.CBar, 7) + pow(25, 7))
+            ));
+
+        // C Prime 1
+        this.CPrime1 = sqrt(
+            pow(this.aPrime1, 2) +
+            pow(x1.B, 2)
+        );
+
+        // C Prime 2
+        this.CPrime2 = sqrt(
+            pow(this.aPrime2, 2) +
+            pow(x2.B, 2)
+        );
+
+        // C Bar Prime
+        this.CBarPrime = (this.CPrime1 + this.CPrime2) / 2;
+
+        // Delta C Prime
+        this.deltaCPrime = this.CPrime2 - this.CPrime1;
+
+        // S sub L
+        this.SsubL = 1 + (
+            (0.015 * pow(this.LBar - 50, 2)) /
+            sqrt(20 + pow(this.LBar - 50, 2))
+        );
+
+        // S sub C
+        this.SsubC = 1 + 0.045 * this.CBarPrime;
+
+        /**
+         * Properties set in getDeltaE method, for access to convenience functions
+         */
+        // h Prime 1
+        this.hPrime1 = 0;
+
+        // h Prime 2
+        this.hPrime2 = 0;
+
+        // Delta h Prime
+        this.deltahPrime = 0;
+
+        // Delta H Prime
+        this.deltaHPrime = 0;
+
+        // H Bar Prime
+        this.HBarPrime = 0;
+
+        // T
+        this.T = 0;
+
+        // S sub H
+        this.SsubH = 0;
+
+        // R sub T
+        this.RsubT = 0;
+    }
+
+    /**
+     * Returns the deltaE value.
+     * @method
+     * @returns {number}
+     */
+    getDeltaE() {
+        var sqrt = Math.sqrt;
+        var sin = Math.sin;
+        var pow = Math.pow;
+
+        // h Prime 1
+        this.hPrime1 = this.gethPrime1();
+
+        // h Prime 2
+        this.hPrime2 = this.gethPrime2();
+
+        // Delta h Prime
+        this.deltahPrime = this.getDeltahPrime();
+
+        // Delta H Prime
+        this.deltaHPrime = 2 * sqrt(this.CPrime1 * this.CPrime2) * sin(this.degreesToRadians(this.deltahPrime) / 2);
+
+        // H Bar Prime
+        this.HBarPrime = this.getHBarPrime();
+
+        // T
+        this.T = this.getT();
+
+        // S sub H
+        this.SsubH = 1 + 0.015 * this.CBarPrime * this.T;
+
+        // R sub T
+        this.RsubT = this.getRsubT();
+
+        // Put it all together!
+        var lightness = this.deltaLPrime / (this.ksubL * this.SsubL);
+        var chroma = this.deltaCPrime / (this.ksubC * this.SsubC);
+        var hue = this.deltaHPrime / (this.ksubH * this.SsubH);
+
+        return sqrt(
+            pow(lightness, 2) +
+            pow(chroma, 2) +
+            pow(hue, 2) +
+            this.RsubT * chroma * hue
+        );
+    }
+
+    /**
+     * Returns the RT variable calculation.
+     * @method
+     * @returns {number}
+     */
+    getRsubT() {
+        var sin = Math.sin;
+        var sqrt = Math.sqrt;
+        var pow = Math.pow;
+        var exp = Math.exp;
+
+        return -2 *
+            sqrt(
+                pow(this.CBarPrime, 7) /
+                (pow(this.CBarPrime, 7) + pow(25, 7))
+            ) *
+            sin(this.degreesToRadians(
+                60 *
+                exp(
+                    -(
+                        pow(
+                            (this.HBarPrime - 275) / 25, 2
+                        )
+                    )
+                )
+            ));
+    }
+
+    /**
+     * Returns the T variable calculation.
+     * @method
+     * @returns {number}
+     */
+    getT() {
+        var cos = Math.cos;
+
+        return 1 -
+            0.17 * cos(this.degreesToRadians(this.HBarPrime - 30)) +
+            0.24 * cos(this.degreesToRadians(2 * this.HBarPrime)) +
+            0.32 * cos(this.degreesToRadians(3 * this.HBarPrime + 6)) -
+            0.20 * cos(this.degreesToRadians(4 * this.HBarPrime - 63));
+    }
+
+    /**
+     * Returns the H Bar Prime variable calculation.
+     * @method
+     * @returns {number}
+     */
+    getHBarPrime() {
+        var abs = Math.abs;
+
+        if (abs(this.hPrime1 - this.hPrime2) > 180) {
+            return (this.hPrime1 + this.hPrime2 + 360) / 2
+        }
+
+        return (this.hPrime1 + this.hPrime2) / 2
+    }
+
+    /**
+     * Returns the Delta h Prime variable calculation.
+     * @method
+     * @returns {number}
+     */
+    getDeltahPrime() {
+        var abs = Math.abs;
+
+        // When either C′1 or C′2 is zero, then Δh′ is irrelevant and may be set to
+        // zero.
+        if (0 === this.C1 || 0 === this.C2) {
+            return 0;
+        }
+
+        if (abs(this.hPrime1 - this.hPrime2) <= 180) {
+            return this.hPrime2 - this.hPrime1;
+        }
+
+        if (this.hPrime2 <= this.hPrime1) {
+            return this.hPrime2 - this.hPrime1 + 360;
+        } else {
+            return this.hPrime2 - this.hPrime1 - 360;
+        }
+    }
+
+    /**
+     * Returns the h Prime 1 variable calculation.
+     * @method
+     * @returns {number}
+     */
+    gethPrime1() {
+        return this._gethPrimeFn(this.x1.B, this.aPrime1);
+    }
+
+    /**
+     * Returns the h Prime 2 variable calculation.
+     * @method
+     * @returns {number}
+     */
+    gethPrime2() {
+        return this._gethPrimeFn(this.x2.B, this.aPrime2);
+    }
+
+    /**
+     * A helper function to calculate the h Prime 1 and h Prime 2 values.
+     * @method
+     * @private
+     * @returns {number}
+     */
+    _gethPrimeFn(x, y) {
+        var hueAngle;
+
+        if (x === 0 && y === 0) {
+            return 0;
+        }
+
+        hueAngle = this.radiansToDegrees(Math.atan2(x, y));
+
+        if (hueAngle >= 0) {
+            return hueAngle;
+        } else {
+            return hueAngle + 360;
+        }
+    }
+
+    /**
+     * Gives the radian equivalent of a specified degree angle.
+     * @method
+     * @returns {number}
+     */
+    radiansToDegrees(radians) {
+        return radians * (180 / Math.PI);
+    }
+
+    /**
+     * Gives the degree equivalent of a specified radian.
+     * @method
+     * @returns {number}
+     */
+    degreesToRadians(degrees) {
+        return degrees * (Math.PI / 180);
+    }
+
+}

+ 112 - 0
zorro/src/app/lib/filler/common/animator.ts

@@ -0,0 +1,112 @@
+import EventEmitter from '../core/eventemitter';
+
+/**
+ * Animator.
+ */
+export default class Animator extends EventEmitter {
+  from: any;
+  to: any;
+  value: any;
+  duration: number;
+  interval: any;
+  easing: Function;
+
+  constructor(from, to) {
+    super();
+    this.from = from || 0;
+    this.to = to || 1;
+    this.duration = 1000;
+    this.interval = null;
+    this.value = this.from;
+    this.easing = null;
+  }
+
+  setEasing(easing : Function) {
+    this.easing = easing;
+    return this;
+  }
+
+  set(from, to) {
+    if (this.interval)
+      this.cancel();
+    this.from = from;
+    this.to = to;
+    this.value = this.from;
+    return this;
+  }
+
+  /**
+   * Start animator
+   */
+  start() {
+    let self = this;
+    if (self.interval)
+      self.cancel();
+    let startTs = new Date().getTime();
+    let value, spend;
+    self.interval = setInterval(function() {
+      spend = new Date().getTime() - startTs;
+      if (spend > self.duration) {
+        self.value = self.to;
+        self.cancel();
+      } else {
+        self.updateValues(spend / self.duration);
+      }
+      self.trigger('animationUpdate', self.value);
+    }, 1000 / 60); // 60 frames per second.
+    return self;
+  }
+
+  updateValues(progress) {
+    // progress = Easing.easeOutBounce(progress);
+    // progress = Easing.easeInOutQuad(progress);
+    // progress = Easing.easeInOutQuadBack(progress);
+    // progress = Easing.easeInOutQuadBack2(progress);
+    // progress = Easing.overshoot(progress);
+    // progress = Easing.easeInQuad(progress);
+    // progress = Easing.easeInCubic(progress);
+    // progress = Easing.easeInQuart(progress);
+
+    if (this.easing) {
+      progress = this.easing.call(null, progress);
+    }
+
+    let self = this;
+    //console.log('self.from:', self.from, 'self.to:', self.to);
+
+    if (self.from.length) {
+      self.value = [];
+      for (var i = 0; i < self.from.length; i++) {
+        self.value[i] = self.from[i] + (self.to[i] - self.from[i]) * progress;
+      }
+    } else {
+      self.value = self.from + (self.to - self.from) * progress;
+    }
+  }
+
+  /**
+   * Cancel the animator
+   */
+  cancel() {
+    if (this.interval) {
+      clearInterval(this.interval);
+      this.interval = null;
+    }
+    return self;
+  }
+
+  /**
+   * Set animation duration
+   */
+  setDuration(duration) {
+    this.duration = duration;
+    return this;
+  }
+
+  getValue() { return this.value; }
+
+  /**
+   * Check animator is running or not
+   */
+  isRunning() { return this.interval != null; }
+}

+ 184 - 0
zorro/src/app/lib/filler/common/auto-color-map.ts

@@ -0,0 +1,184 @@
+import Utils from "./utils";
+import FillArea from "./fillarea";
+import { AreaMap, ColorInfo, ColorMap, ColorSumItem } from "./interfaces";
+import { mergeByColorMap } from "./color-merge";
+
+const TAG = 'AutoColorMap';
+
+export default class AutoColorMap {
+  page: HTMLImageElement;
+  map: HTMLImageElement;
+  colored: HTMLImageElement;
+
+  areaMap: AreaMap;
+  colorMap: ColorMap;
+  colorSum: ColorSumItem[];
+  mapData: ImageData;
+  mapPixels: Uint32Array;
+  pageData: ImageData;
+  pagePixels: Uint32Array;
+  coloredData: ImageData;
+  coloredPixels: Uint32Array;
+
+  constructor(page: HTMLImageElement, map: HTMLImageElement, colored: HTMLImageElement) {
+    this.page = page;
+    this.map = map;
+    this.colored = colored;
+
+    this.mapData = Utils.getImageData(this.map);
+    this.mapPixels = new Uint32Array(this.mapData.data.buffer);
+    this.pageData = Utils.getImageData(this.page);
+    this.pagePixels = new Uint32Array(this.pageData.data.buffer);
+    this.coloredData = Utils.getImageData(this.colored);
+    this.coloredPixels = new Uint32Array(this.coloredData.data.buffer);
+
+    //makesure colored no alpha
+    let pixels = new Uint8Array(this.coloredPixels.buffer);
+    for (var i = 0; i < this.coloredPixels.length; i++) {
+      pixels[i * 4 + 3] = 0xff;
+    }
+
+    //cut the map with page.
+    for (var i = 0; i < this.mapPixels.length; i++) {
+      if (this.pagePixels[i] != 0) this.mapPixels[i] = 0;
+    }
+
+    this.createAreaMap();
+    console.log(TAG, 'areaMap=', this.areaMap);
+
+    this.process();
+    this.colorSum = this.getColorSum();
+    
+    // //颜色数超过100 自动合并颜色
+    // if(this.colorSum.length >= 100) {
+    //   this.colorMap = mergeByColorMap(this.colorMap);
+    //   this.colorSum = this.getColorSum();
+    // }
+  }
+
+  process() {
+    let areas: FillArea[] = Object.values(this.areaMap);
+    this.colorMap = {};
+    for (var i = 0; i < areas.length; i++) {
+      this.colorMap[areas[i].color] = this.getColorFromColored(areas[i]);
+    }
+  }
+
+  /**
+   * create work blob from the color map.
+   * @returns 
+   */
+  public getWorkBlob() : Promise<Blob> {
+    let canvas: HTMLCanvasElement = Utils.createCanvas(this.width, this.height);
+    let ctx: CanvasRenderingContext2D = canvas.getContext('2d');
+    let imgData: ImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+    let pixels = new Uint32Array(imgData.data.buffer);
+    for (var i = 0; i < pixels.length; i++) {
+      if (this.colorMap[this.mapPixels[i]]) {
+        pixels[i] = this.colorMap[this.mapPixels[i]].color;
+      }
+    }
+    ctx.putImageData(imgData, 0, 0);
+    ctx.drawImage(this.page, 0, 0);
+    return new Promise((done, reject) => {
+      canvas.toBlob((blob: Blob) => { done(blob); }, 'image/png');
+    })
+  }
+
+
+  /**
+   * 统计这个区域的所有像素的颜色,选出最具代表的颜色
+   * @param area 
+   * @returns 
+   */
+  getColorFromColored(area: FillArea): ColorInfo {
+    let ai, aw, i, x, y;
+    let sum = {};
+    aw = area.width();
+    let count = area.width() * area.height();
+    for (ai = 0; ai < count; ai++) {
+      x = area.left + ai % aw;
+      y = area.top + Math.floor(ai / aw);
+      i = y * this.width + x;
+      if (this.mapPixels[i] == area.color) {
+        let color = this.coloredPixels[i];
+        if (!sum[color]) sum[color] = 1;
+        else sum[color] = sum[color] + 1;
+      }
+    }
+    let maxColor: string;
+    let maxCount = 0;
+    Object.keys(sum).forEach((color: string) => {
+      if (sum[color] > maxCount) {
+        maxCount = sum[color];
+        maxColor = color;
+      }
+    });
+    //console.log('sum:', sum);
+    let colorInt = parseInt(maxColor);
+    let colorCss = Utils.getColorFromInteger(colorInt);
+
+    return {
+      color: colorInt,
+      cssColor: colorCss,
+    };
+  }
+
+
+  get width() { return this.page.width }
+  get height() { return this.page.height }
+
+  /**
+   * Create area map from map pixels.
+   */
+  createAreaMap() {
+    let width = this.width;
+    let floodArr = this.mapPixels;
+    let areaMap = {};
+    let i, x, y, color;
+    for (i = 0; i < floodArr.length; i++) {
+      color = floodArr[i];
+      if (color == 0) continue;
+      x = i % width;
+      y = Math.floor(i / width);
+      if (areaMap[color]) {
+        areaMap[color].addPoint(x, y);
+      } else {
+        areaMap[color] = new FillArea(color, x, y);
+      }
+    }
+    this.areaMap = areaMap;
+  }
+
+  public static getColorMap(page: HTMLImageElement, map: HTMLImageElement, colored: HTMLImageElement): ColorMap {
+    let acm = new AutoColorMap(page, map, colored);
+    return acm.colorMap;
+  }
+
+
+  /**
+   * 获取
+   * @returns 
+   */
+  public getColorSum(): ColorSumItem[] {
+    let sumHash = {};
+    Object.keys(this.colorMap).forEach(_areaIndex => {
+      let areaIndex = parseInt(_areaIndex);
+      let { color, cssColor } = this.colorMap[areaIndex];
+      if (sumHash[color]) {
+        sumHash[color].total++;
+        sumHash[color].areas.push(areaIndex);
+      } else {
+        sumHash[color] = {
+          total: 1,
+          cssColor,
+          color,
+          areas: [areaIndex]
+        }
+      }
+    })
+    return Object.keys(sumHash).map(key => {
+      return sumHash[key];
+    })
+  }
+}

+ 13 - 0
zorro/src/app/lib/filler/common/centers.worker.ts

@@ -0,0 +1,13 @@
+import WorkCenterFinder from "./work-center-finder";
+
+/// <reference lib="webworker" />
+
+addEventListener('message', ({ data }) => {
+  let {pagePixels, mapPixels, width, height} = data;
+  console.log("worker recive: width=" + width + " height=" + height);
+  console.time("calculate centers");
+  const centers = new WorkCenterFinder(pagePixels, mapPixels, width, height).calculate();
+  console.timeEnd("calculate centers");
+  postMessage(centers);
+
+});

+ 64 - 0
zorro/src/app/lib/filler/common/color-merge.ts

@@ -0,0 +1,64 @@
+
+import { Lab, sRGB } from "../color/color";
+import { ColorInfo, ColorMap, ColorSumItem } from "./interfaces";
+import { Neigbours, simpleMerge } from "./kmeans";
+
+export function colorDistance(a: ColorSumItem, b: ColorSumItem): number {
+  return Math.abs(a.lab.deltaE(b.lab));
+}
+
+
+/**
+ * Convert colorMap to color list.
+ * @param colorMap 
+ * @returns 
+ */
+export function colorMapToColorSum(colorMap: ColorMap): ColorSumItem[] {
+  let sumHash = {};
+  Object.keys(colorMap).forEach(_areaIndex => {
+    let areaIndex = parseInt(_areaIndex);
+    let { color, cssColor } = colorMap[areaIndex];
+    if (sumHash[color]) {
+      sumHash[color].total++;
+      sumHash[color].areas.push(areaIndex);
+    } else {
+      sumHash[color] = {
+        cssColor,
+        color,
+        total: 1,
+        lab: Lab.fromRGBA(color),
+        areas: [areaIndex],
+      }
+    }
+  })
+
+  return Object.values(sumHash);
+}
+
+/**
+ * 根据最小颜色距离合并colorMap
+ * @param input  输入colorMap
+ * @param minDist  最小颜色距离
+ * @returns  颜色合并后的 colorMap
+ */
+export function mergeByColorMap(input: ColorMap, minDist: number = 4): ColorMap {
+  let dataset: ColorSumItem[] = colorMapToColorSum(input);
+  let result: Neigbours<ColorSumItem>[] = simpleMerge(dataset, minDist, colorDistance);
+
+  let colorMap = {};
+  result.forEach(nei => {
+    let color = nei.self.color;
+    let cssColor = nei.self.cssColor;
+    let colorInfo: ColorInfo = { color, cssColor };
+    nei.neigbours.forEach(item => {
+      item.areas.forEach(area => {
+        colorMap[area] = colorInfo;
+      })
+    })
+  })
+
+  return colorMap;
+}
+
+
+

+ 144 - 0
zorro/src/app/lib/filler/common/color-order.ts

@@ -0,0 +1,144 @@
+import { Centers, ColorMap } from "./interfaces";
+
+/**
+  * 检查colorOrder是否可用, 即:colorOrder里是否已涵盖colorMap出现的所有颜色
+  * @param colorMap 区块填色表
+  * @param colorOrder 颜色排序列表
+  */
+export function validateOrder(colorMap: ColorMap, colorOrder: number[]): boolean {
+  if (!colorMap || !colorOrder || colorOrder.length <= 0) {
+    return false;
+  }
+
+  // colorMap里出现的颜色列表
+  let allColors = Array.from(new Set(Object.values(colorMap).map(value => {
+    return value.color;
+  })));
+
+  // 判断order是否包含全部colorMap中的颜色
+  if (colorOrder.length < allColors.length) {
+    return false;
+  }
+  for (let i = 0; i < allColors.length; i++) {
+    if (!colorOrder.includes(allColors[i])) {
+      return false;
+    }
+  }
+  return true;
+}
+
+
+/**
+ * 获得可用的colorOrder
+ * 算法: 
+ *    1. 如果有可用的手动顺序colorOrder, 则返回colorOrder
+ *    2. 如果colorOrder不可用, 则判断自动顺序orderAuto是否可用, 可用即返回orderAuto
+ *    3. 如果以上都不可用, 那么根据中心点centers和colorMap生成新的order并返回(注:该新order实际上是自动顺序orderAuto)
+ *    4. 如果中心点没有, 只能返回null
+ * 
+ * @param colorMap 区块填色表
+ * @param centers 中心点
+ * @param colorOrder 手动顺序
+ * @param orderAuto 自动顺序        
+ */
+export function getColorOrder(colorMap: ColorMap, centers: Centers, colorOrder: number[], orderAuto: number[]): number[] {
+  if (validateOrder(colorMap, colorOrder)) {
+    return colorOrder;
+  }
+  if (validateOrder(colorMap, orderAuto)) {
+    return orderAuto;
+  }
+  return generateOrderAuto(colorMap, centers);
+}
+
+
+/**
+ * 获得可用的colorOrder
+ * 算法: 跟以上getColorOrder的区别是: 如果有colorOrder, 哪怕不完整, 也优先采用colorOrder排序, 剩余部分自动排序
+ * 
+ * @param colorMap 区块填色表
+ * @param centers 中心点
+ * @param colorOrder 手动顺序
+ * @param orderAuto 自动顺序        
+ */
+export function getColorOrder2(colorMap: ColorMap, centers: Centers, colorOrder: number[], orderAuto: number[]): number[] {
+  if (validateOrder(colorMap, colorOrder)) { // 手动排序完全可用, 直接返回
+    return colorOrder;
+  }
+  if (!validateOrder(colorMap, orderAuto)) { // 检查下自动排序是否OK, 如果不OK则重新生成
+    orderAuto = generateOrderAuto(colorMap, centers);
+  }
+  if (!colorOrder || colorOrder.length <= 0) { // colorOrder完全没有, 直接返回orderAuto
+    return orderAuto;
+  }
+
+  // 接下来结合colorOrder和orderAuto 重新排序
+
+  // 所有颜色
+  let allColors = Array.from(new Set(Object.values(colorMap).map(value => {
+    return value.color;
+  })));
+
+  // 首先根据colorOrder先做一轮排序
+  allColors.sort((a, b) => {
+    let aidx = colorOrder.indexOf(a);
+    let bidx = colorOrder.indexOf(b);
+    aidx = aidx < 0 ? 9999 : aidx;
+    bidx = bidx < 0 ? 9999 : bidx;
+    return aidx - bidx;
+  });
+  // 接着对剩下未排序的部分根据orderAuto进行排序
+  allColors.sort((a, b) => {
+    if (colorOrder.includes(a) || colorOrder.includes(b)) {
+      return 0;  // 出现在colorOrder里的元素已经排过了,相对位置不变
+    }
+    let aidx = orderAuto.indexOf(a);
+    let bidx = orderAuto.indexOf(b);
+    aidx = aidx < 0 ? 9999 : aidx;
+    bidx = bidx < 0 ? 9999 : bidx;
+    return aidx - bidx;
+  });
+
+  return allColors;
+
+}
+
+/**
+ * 生成自动顺序
+ * @param colorMap 
+ * @param centers 
+ * @returns 
+ */
+export function generateOrderAuto(colorMap: ColorMap, centers: Centers): number[] {
+  // 没有现成的order, 需要现场生成, 取决于有没有centers
+  if (!centers || Object.keys(centers).length <= 0) {
+    return null;
+  }
+
+  let colorHash = {};
+  Object.keys(colorMap).forEach(areaIndex => {
+    let { color } = colorMap[areaIndex];
+    let point = centers[areaIndex];
+    if (!point) { //标记!!! 发现colorMap里有的area在centers中心点中不存在, 导致出错. 需要认真评估这个问题的原因以及所带来的影响
+      console.error("[generateOrderAuto] areaIndex: " + areaIndex + " in colorMap cannot be found in centers!");
+      return;
+    }
+    if (!colorHash[color]) {
+      colorHash[color] = {
+        color: color,
+        maxRadius: point.radius,
+      }
+    } else {
+      if (point.radius > colorHash[color].maxRadius) {
+        colorHash[color].maxRadius = point.radius;
+      }
+    }
+  })
+  let order = Object.values(colorHash)  // 生成默认order
+    .sort((a: any, b: any) => {
+      return b.maxRadius - a.maxRadius;
+    })
+    .map((item: any) => { return item.color });
+
+  return order;
+}

+ 1537 - 0
zorro/src/app/lib/filler/common/coloredmap.ts

@@ -0,0 +1,1537 @@
+import { Lab } from "../color/color";
+import FillArea from "./fillarea";
+import { AreaMap } from "./interfaces";
+import Utils from "./utils";
+
+interface ColorInfo {
+  color: number;
+  lab: Lab;
+}
+
+/**
+ * 根据上色图生成map图
+ */
+export default class ColoredMap {
+  page: HTMLImageElement;
+  map: HTMLImageElement;
+  colored: HTMLImageElement;
+
+  mapData: ImageData;
+  mapPixels: Uint32Array;
+  pageData: ImageData;
+  pagePixels: Uint32Array;
+  coloredData: ImageData;
+  coloredPixels: Uint32Array;
+
+  newMapData: ImageData;
+  newMapPixels: Uint32Array;
+
+  /**
+   * 
+   * @param page 线稿图
+   * @param map 原map图
+   * @param colored 上色图
+   */
+  constructor(page: HTMLImageElement, map: HTMLImageElement, colored: HTMLImageElement) {
+    this.page = page;
+    this.map = map;
+    this.colored = colored;
+
+    this.mapData = Utils.getImageData(this.map);
+    this.mapPixels = new Uint32Array(this.mapData.data.buffer);
+    this.pageData = Utils.getImageData(this.page);
+    this.pagePixels = new Uint32Array(this.pageData.data.buffer);
+    this.coloredData = Utils.getImageData(this.colored);
+    this.coloredPixels = new Uint32Array(this.coloredData.data.buffer);
+
+    this.newMapData = Utils.getImageData(this.map);
+    this.newMapPixels = new Uint32Array(this.newMapData.data.buffer);
+
+    //makesure colored no alpha
+    let pixels = new Uint8Array(this.coloredPixels.buffer);
+    let count = 0;
+    for (let i = 0; i < this.coloredPixels.length; i++) {
+      if (pixels[i * 4 + 3] != 0xff) {
+        console.log(this.coloredPixels[i].toString(16) + " " + pixels[i * 4 + 3]);
+        count++;
+      }
+      pixels[i * 4 + 3] = 0xff;
+    }
+    console.log("alpha count: " + count);
+
+    this.process();
+  }
+
+  get width() { return this.page.width }
+  get height() { return this.page.height }
+
+  /**
+   * 根据上色图解析重新生成map
+   * 用于线稿图线条不完整(未囊括所有区块,如过渡部分的阴影并没有画线条),将根据上色图重新生成map,补充遗留区块
+   * 算法:
+   * 1. 拷贝出一张新的map图, 先将线条的部分置为0(线条部分暂不参与子区块的floodfill运算)
+   * 2. 针对map的每一个区块,遍历找到新的颜色点,对该颜色点做floodfill操作(注:跟该点色值相同的周边颜色,即是floodfill的条件,但实测发现周边色值十分复杂,存在大量微小变化,因此这里实际是取一个色值范围)
+   * 3. 接着对做完floodfill的新的map图做mergearea操作, 合并较小区块 
+   * 4. 对子区块(原属同一个大区块里的)根据颜色值做merge操作(意思是两个子区块如果代表颜色十分接近甚至相同,应该合并。实测发现很有必要)
+   */
+  process() {
+    let floodColorDist = 8;  // 重要!允许flood的最大色差范围。调小了区块连不成片,在后续merge操作可能就被吃掉了;调大了有可能肉眼可见两个色差不大的区块直接flood就连在一起了
+    let areaMap = this.createAreaMap(this.newMapData);
+    let areas: FillArea[] = Object.values(areaMap);
+    console.log("area count: " + areas.length);
+    let ai, len, aw, i, x, y;
+    let floodCount, totalCount = 0;
+
+    console.time("process");
+
+    //cut the new map with page.
+    for (let i = 0; i < this.newMapPixels.length; i++) {
+      if (this.pagePixels[i] != 0) this.newMapPixels[i] = 0;
+    }
+
+    // 统计各区块的颜色, 先做一轮净化
+    console.time("purify colored");
+    areas.forEach(area => {
+      let colorList = this.selectAreaColors(area);
+      this.purifyColoredArea(area, colorList);
+    })
+    console.timeEnd("purify colored");
+
+    console.time("area floodfill");
+    areas.forEach(area => {
+      console.log(area);
+      floodCount = 0;
+      aw = area.width();
+      len = area.width() * area.height();
+      for (ai = 0; ai < len; ai++) {
+        x = area.left + ai % aw;
+        y = area.top + Math.floor(ai / aw);
+        i = y * this.width + x;
+        if (this.newMapPixels[i] == area.color && this.newMapPixels[i] != 0) {
+          // find a new diffrent color, need to floodfill
+          let srcColor = this.coloredPixels[i];
+          let dstColor = this.randomColor();
+          this.floodFill(area, i, srcColor, dstColor);
+          floodCount++;
+          totalCount++;
+        }
+      }
+      console.log("area: " + area.color + " " + area.count + " floodfill done! floodCount=" + floodCount);
+    })
+    console.log("map floodfill finish. areas count: " + areas.length + " total flood count: " + totalCount);
+    console.timeEnd("area floodfill");
+
+    console.time("mergeAreas");
+    this.mergeAreas(this.newMapData);
+    console.timeEnd("mergeAreas");
+
+    // merge一轮过后,消灭10个点以内的小区块,这些区块一般都是没有邻居的孤立小点
+    console.time("removeSmallArea");
+    this.removeSmallArea(this.newMapData, 10);
+    console.timeEnd("removeSmallArea");
+
+    // fill alpha gap
+    console.time('fillAlphaGap');
+    this.fillAlphaGap(this.pageData, this.newMapData, 50);
+    console.timeEnd('fillAlphaGap');
+
+    console.time("mergeAreas");
+    this.mergeAreas(this.newMapData);
+    console.timeEnd("mergeAreas");
+
+    console.time("fillEmptyGap");
+    this.fillEmptyGap(this.newMapData, 10);
+    console.timeEnd("fillEmptyGap");
+
+    // // do merge again after fillEmptyGap
+    // console.time("mergeAreas again");
+    // this.mergeAreas(this.newMapData);
+    // console.timeEnd("mergeAreas again");
+
+
+    console.timeEnd("process");
+
+  }
+
+  getAreaCount(): number {
+    let hash: any = {};
+    for (let i = 0; i < this.newMapPixels.length; i++) {
+      hash[this.newMapPixels[i]] = 1;
+    }
+    return Object.keys(hash).length;
+  }
+
+  /**
+* 输出图片
+* @returns 
+*/
+  toImage(): Promise<HTMLImageElement> {
+    let canvas: HTMLCanvasElement = Utils.createCanvas(this.width, this.height);
+    let ctx: CanvasRenderingContext2D = canvas.getContext('2d');
+    ctx.putImageData(this.newMapData, 0, 0);
+    return new Promise((done, reject) => {
+      let img = new Image();
+      img.onload = function () { done(img); };
+      img.onerror = reject;
+      img.src = canvas.toDataURL('image/png');
+    });
+  }
+
+  /**
+   * 输出文件
+   * @returns 
+   */
+  toBlob(): Promise<Blob> {
+    let canvas: HTMLCanvasElement = Utils.createCanvas(this.width, this.height);
+    let ctx: CanvasRenderingContext2D = canvas.getContext('2d');
+    ctx.putImageData(this.newMapData, 0, 0);
+    return new Promise((done, reject) => {
+      canvas.toBlob((blob: Blob) => done(blob), 'image/png');
+    });
+  }
+
+
+  /**
+   * 创建map, 输出文件
+   * @param page 线稿图
+   * @param map 区块图
+   * @param colored 上色图
+   * @returns 
+   */
+  static createMapBlob(page: HTMLImageElement, map: HTMLImageElement, colored: HTMLImageElement): Promise<Blob> {
+    return new ColoredMap(page, map, colored).toBlob();
+  }
+
+  /**
+   * 创建MAP, 输出图片
+   * @param page 线稿图
+   * @param map 区块图
+   * @param colored 上色图
+   * @returns 
+   */
+  static createMapImage(page: HTMLImageElement, map: HTMLImageElement, colored: HTMLImageElement): Promise<HTMLImageElement> {
+    return new ColoredMap(page, map, colored).toImage();
+  }
+
+  /**
+   * 选取区块代表性颜色
+   * @param area 
+   * @returns 
+   */
+  selectAreaColors(area: FillArea) {
+    let minCount = 40;
+    let percent = 0.1;
+    let minColorDist = 4;
+    let pixels = this.newMapPixels;
+    let width = this.width;
+    let ai, aw, i, j, x, y;
+    let sum = {};
+    aw = area.width();
+    let count = area.width() * area.height();
+    for (ai = 0; ai < count; ai++) {
+      x = area.left + ai % aw;
+      y = area.top + Math.floor(ai / aw);
+      i = y * width + x;
+      if (pixels[i] == area.color) {
+        let color = this.coloredPixels[i];
+        if (!sum[color]) sum[color] = 1;
+        else sum[color] = sum[color] + 1;
+      }
+    }
+    let colors = Object.keys(sum).sort((a, b) => { return sum[b] - sum[a] });
+    let colorList;
+    let firstColor = colors[0];
+    // 初筛一轮
+    colors = colors.filter((c, i) => {
+      if (i == 0) return true; // always return the first one
+      return (sum[c] > minCount || (sum[c] / area.count) > percent || (sum[c] / sum[firstColor] > 0.2))
+    })
+    // 再筛一轮,去掉比较接近的颜色
+    colorList = colors.map(c => {
+      let cInt = parseInt(c);
+      return { color: cInt, lab: Lab.fromRGBA(cInt) };
+    })
+    for (i = 0; i < colorList.length; i++) {
+      for (j = i + 1; j < colorList.length; j++) {
+        if (colorList[i].lab.deltaE(colorList[j].lab) < minColorDist) {
+          colorList.splice(j, 1);
+          j--;
+        }
+      }
+    }
+
+    console.log("area: " + area.color, + " " + area.count + " total colors:" + Object.keys(sum).length + " select colors:" + colorList.length);
+    return colorList;
+  }
+
+  /**
+   * 对colored图指定区块范围按给定颜色进行净化,其他杂色都汇聚到主流颜色去
+   * @param area 
+   * @param colors 
+   */
+  purifyColoredArea(area: FillArea, colorList: ColorInfo[]) {
+    let pixels = this.newMapPixels;
+    let coloredPixels = this.coloredPixels;
+    let width = this.width;
+    let ai, aw, i, x, y;
+    aw = area.width();
+    let count = area.width() * area.height();
+    for (ai = 0; ai < count; ai++) {
+      x = area.left + ai % aw;
+      y = area.top + Math.floor(ai / aw);
+      i = y * width + x;
+      if (pixels[i] == area.color) {
+        let color = coloredPixels[i];
+        coloredPixels[i] = this.getCloserColor(color, colorList);
+      }
+    }
+  }
+
+  /**
+   * 从colorList列表中获取与color最为接近的颜色
+   * @param color 
+   * @param colorList 
+   * @returns 
+   */
+  getCloserColor(color: number, colorList: ColorInfo[]) {
+    if (colorList.length == 1) return colorList[0].color;
+    for (let i = 0; i < colorList.length; i++) {
+      if (color == colorList[i].color) {
+        return color;
+      }
+    }
+
+    let close = colorList[0].color;
+    let dist, minDist = 99999;
+
+    let colorLab = Lab.fromRGBA(color);
+    colorList.forEach(c => {
+      dist = colorLab.deltaE(c.lab);
+      if (dist < minDist) {
+        close = c.color;
+        minDist = dist;
+      }
+    })
+
+    return close;
+  }
+
+  /**
+   * 对指定区块的指定color做floodFill操作
+   * @param area 区块area
+   * @param index 起始下标
+   * @param srcColor 对应在colored上色图中的颜色 
+   * @param dstColor 涂到新的mapPixels中的颜色
+   * @param maxColorDist
+   * @returns 
+   */
+  floodFill(area: FillArea, index: number, srcColor: number, dstColor: number) {
+    let width = this.width;
+    let height = this.height;
+    let mapPixels = this.newMapPixels;
+    let coloredPixels = this.coloredPixels;
+
+    let stack = new Array();
+    let maxStack = 0;
+
+    if (coloredPixels[index] == srcColor) {
+      stack.push(index);
+    }
+
+    let x, p;
+    while (stack.length > 0) {
+      if (stack.length > maxStack)
+        maxStack = stack.length;
+      p = stack.pop();
+      x = p % width;
+
+      // if p is colored, continue
+      if (mapPixels[p] == dstColor)
+        continue;
+
+      mapPixels[p] = dstColor;
+      // left
+      if (x > 0 && mapPixels[p - 1] == area.color
+        && coloredPixels[p - 1] == srcColor) {
+        stack.push(p - 1);
+      }
+      // top
+      if (p > (width - 1) && mapPixels[p - width] == area.color
+        && coloredPixels[p - width] == srcColor) {
+        stack.push(p - width);
+      }
+      // right
+      if (x < (width - 1) && mapPixels[p + 1] == area.color
+        && coloredPixels[p + 1] == srcColor) {
+        stack.push(p + 1);
+      }
+      // bottom
+      if (p < ((height - 1) * width) && mapPixels[p + width] == area.color
+        && coloredPixels[p + width] == srcColor) {
+        stack.push(p + width);
+      }
+
+      // left top
+      if (x > 0 && p > (width - 1) && mapPixels[p - width - 1] == area.color
+        && coloredPixels[p - width - 1] == srcColor) {
+        stack.push(p - width - 1);
+      }
+      // right top
+      if (x < (width - 1) && p > (width - 1) && mapPixels[p - width + 1] == area.color
+        && coloredPixels[p - width + 1] == srcColor) {
+        stack.push(p - width + 1);
+      }
+      // left bottom
+      if (x > 0 && p < ((height - 1) * width) && mapPixels[p + width - 1] == area.color
+        && coloredPixels[p + width - 1] == srcColor) {
+        stack.push(p + width - 1);
+      }
+      // rigth bottom
+      if (x < (width - 1) && p < ((height - 1) * width) && mapPixels[p + width + 1] == area.color
+        && coloredPixels[p + width + 1] == srcColor) {
+        stack.push(p + width + 1);
+      }
+    }
+    // console.log('floodFill@maxStack', maxStack / (width*height));
+
+    return maxStack;
+  }
+
+
+  /**
+   * 对指定区块的指定color做floodFill操作, 返回被floodfill的点个数
+   * @param area 区块area
+   * @param index 起始下标
+   * @param srcColor 对应在colored上色图中的颜色 
+   * @param dstColor 涂到新的mapPixels中的颜色
+   * @param maxColorDist
+   * @returns 
+   */
+  areaFloodFill(area: FillArea, index: number, srcColor: number, dstColor: number, maxColorDist?: number) {
+    let width = this.width;
+    let height = this.height;
+    let mapPixels = this.newMapPixels;
+    let coloredPixels = this.coloredPixels;
+    maxColorDist = maxColorDist || 8;  // 此数值至关重要
+
+    let stack = new Array();
+    let maxStack = 0;
+
+    if (coloredPixels[index] == srcColor) {
+      stack.push(index);
+    }
+
+    let x, p;
+    while (stack.length > 0) {
+      if (stack.length > maxStack)
+        maxStack = stack.length;
+      p = stack.pop();
+      x = p % width;
+
+      // if p is colored, continue
+      if (mapPixels[p] == dstColor)
+        continue;
+
+      mapPixels[p] = dstColor;
+      // left
+      if (x > 0 && mapPixels[p - 1] == area.color
+        && this.colorDistance(coloredPixels[p - 1], srcColor) < maxColorDist) {
+        stack.push(p - 1);
+      }
+      // top
+      if (p > (width - 1) && mapPixels[p - width] == area.color
+        && this.colorDistance(coloredPixels[p - width], srcColor) < maxColorDist) {
+        stack.push(p - width);
+      }
+      // right
+      if (x < (width - 1) && mapPixels[p + 1] == area.color
+        && this.colorDistance(coloredPixels[p + 1], srcColor) < maxColorDist) {
+        stack.push(p + 1);
+      }
+      // bottom
+      if (p < ((height - 1) * width) && mapPixels[p + width] == area.color
+        && this.colorDistance(coloredPixels[p + width], srcColor) < maxColorDist) {
+        stack.push(p + width);
+      }
+
+      // left top
+      if (x > 0 && p > (width - 1) && mapPixels[p - width - 1] == area.color
+        && this.colorDistance(coloredPixels[p - width - 1], srcColor) < maxColorDist) {
+        stack.push(p - width - 1);
+      }
+      // right top
+      if (x < (width - 1) && p > (width - 1) && mapPixels[p - width + 1] == area.color
+        && this.colorDistance(coloredPixels[p - width + 1], srcColor) < maxColorDist) {
+        stack.push(p - width + 1);
+      }
+      // left bottom
+      if (x > 0 && p < ((height - 1) * width) && mapPixels[p + width - 1] == area.color
+        && this.colorDistance(coloredPixels[p + width - 1], srcColor) < maxColorDist) {
+        stack.push(p + width - 1);
+      }
+      // rigth bottom
+      if (x < (width - 1) && p < ((height - 1) * width) && mapPixels[p + width + 1] == area.color
+        && this.colorDistance(coloredPixels[p + width + 1], srcColor) < maxColorDist) {
+        stack.push(p + width + 1);
+      }
+    }
+    // console.log('floodFill@maxStack', maxStack / (width*height));
+
+    return maxStack;
+  }
+
+
+
+
+  /**
+   * 合并小区快
+   * @param imgdata 
+   * @param threshold 
+   */
+  mergeAreas(imgdata: ImageData, threshold?: number) {
+    const kk = (this.width / 1000) * 20;
+    threshold = threshold || kk;
+    let areaMap = this.createAreaMap(imgdata);
+    let keys = Object.keys(areaMap);
+    let floodArr = new Uint32Array(imgdata.data.buffer);
+    let width = imgdata.width;
+    let height = imgdata.height;
+    let areas = keys.map(function (key) { return areaMap[key]; });
+    let smallAreas = areas.filter(function (a) { return a.count < threshold; });
+
+    console.log('mergeAreas@areaSize', areas.length, smallAreas.length);
+    let i, j, x, y, index, p, px, py;
+    let neighbours = this.getNearestNeighbours(1);
+    smallAreas.forEach(function (area) {
+      // area.neighbours = [];
+      let neighbourSet = new Set();
+      for (x = area.left; x <= area.right; x++) {
+        for (y = area.top; y <= area.bottom; y++) {
+          index = y * width + x;
+          if (floodArr[index] == area.color) {
+            // check pixe's neighbour
+            for (j = 0; j < neighbours.length; j++) {
+              p = neighbours[j];
+              px = x + p.x;
+              py = y + p.y;
+              index = py * width + px;
+              if (px > 0 && px < width && py > 0 && py < height &&
+                floodArr[index] != 0 && floodArr[index] != area.color) {
+                // area.neighbours.push(areaMap[floodArr[index]]);
+                neighbourSet.add(areaMap[floodArr[index]]);
+              }
+            }
+          }
+        }
+      }
+      area.neighbours = Array.from(neighbourSet);
+    });
+
+    // remove the area has no neighbours.
+    smallAreas =
+      smallAreas.filter(function (area) { return area.neighbours.length > 0; });
+    // console.log('mergeAreas@areaSize', areas.length, smallAreas.length);
+
+    // for each samll area.
+    // get it's direct neighbour area list
+    // select the biggest one, and merge with it.
+    // if no neighbours mark it can't merge.
+
+    /**
+     * merge another area
+     */
+    function mergeToMe(me, other) {
+      // if i am a small area.
+      // merge other's neighbours
+      if (me.neighbours) {
+        me.neighbours =
+          me.neighbours.concat(other.neighbours)
+            .filter(function (a) { return a != me && a != other; });
+      }
+
+      // set other merged
+      other.merged = true;
+
+      for (x = other.left; x <= other.right; x++) {
+        for (y = other.top; y <= other.bottom; y++) {
+          index = y * width + x;
+          if (floodArr[index] == other.color) {
+            floodArr[index] = me.color;
+            me.addPoint(x, y);
+          }
+        }
+      }
+    }
+
+    let loop = 0;
+    do {
+      loop++;
+
+      smallAreas.forEach(function (area) {
+        if (area.merged)
+          return;
+        let neighbour;
+        for (let i = 0; i < area.neighbours.length; i++) {
+          // merge other small areas.
+          neighbour = area.neighbours[i];
+          // if neighbour is merged or neighbour is a large one
+          if (neighbour.merged || !neighbour.neighbours) {
+            continue;
+          }
+          mergeToMe(area, neighbour);
+        }
+        if (area.neighbours.length > 0) {
+          // merge me to the first large neighbour
+          mergeToMe(area.neighbours[0], area);
+        }
+      });
+
+      smallAreas = smallAreas.filter(function (
+        area) { return !area.merged && area.neighbours.length > 0; });
+
+      console.log('mergeAreas@loop:' + loop, smallAreas.length);
+
+      if (smallAreas.length == 0)
+        break;
+
+    } while (loop < 4);
+  }
+
+  /**
+   * fill small area with alpha
+   * @param imgdata 
+   * @param threshold 
+   */
+  removeSmallArea(imgdata: ImageData, threshold?: number) {
+    threshold = threshold || 8;
+    let width = imgdata.width;
+    let height = imgdata.height;
+    let floodArr = new Uint32Array(imgdata.data.buffer);
+    let areaMap = this.createAreaMap(imgdata);
+    let keys = Object.keys(areaMap);
+    let areas = keys.map(function (key) { return areaMap[key]; });
+    let smallAreas = areas.filter(function (a) { return a.count < threshold; });
+    let i, j, x, y, index, p, px, py;
+    let neighbours = this.getNearestNeighbours(1);
+
+    console.log("total areas count: " + areas.length + " smallAreas count:" + smallAreas.length);
+
+    smallAreas.forEach(function (area) {
+      let neighbourSet = new Set();
+      for (x = area.left; x <= area.right; x++) {
+        for (y = area.top; y <= area.bottom; y++) {
+          index = y * width + x;
+          if (floodArr[index] == area.color) {
+            floodArr[index] = 0;  // 直接置为0,等同线条处理, 也可以考虑结合以下注释掉的代码,只remove没有neighbour的孤立区块
+            // check pixe's neighbour
+            // for (j = 0; j < neighbours.length; j++) {
+            //   p = neighbours[j];
+            //   px = x + p.x;
+            //   py = y + p.y;
+            //   index = py * width + px;
+            //   if (px > 0 && px < width && py > 0 && py < height &&
+            //     floodArr[index] != 0 && floodArr[index] != area.color) {
+            //     neighbourSet.add(areaMap[floodArr[index]]);
+            //   }
+            // }
+          }
+        }
+      }
+      // if (neighbourSet.size <= 0) { // 没有邻居,孤立小区块
+      //   for (x = area.left; x <= area.right; x++) {
+      //     for (y = area.top; y <= area.bottom; y++) {
+      //       index = y * width + x;
+      //       if (floodArr[index] == area.color) {
+      //         floodArr[index] = 0;  // 直接置为0,等同线条处理, 也可以考虑结合以下注释掉的代码,只remove没有neighbour的孤立区块
+      //       }
+      //     }
+      //   }
+      // }
+    });
+
+  }
+
+  /**
+   * 按照中心点半径合并区块
+   * @param imgdata 
+   * @param threshold 
+   */
+  mergeAreasByRudias(imgdata: ImageData, threshold?: number) {
+    threshold = threshold || 4;
+    let areaMap = this.createAreaMap(imgdata);
+    let keys = Object.keys(areaMap);
+    let floodArr = new Uint32Array(imgdata.data.buffer);
+    let width = imgdata.width;
+    let height = imgdata.height;
+    let areas = keys.map(function (key) { return areaMap[key]; });
+    // 计算每个区块的中心点半径
+    areas.forEach(area => {
+      area.radius = this.findAreaCenter(area).radius;
+    })
+    let smallAreas = areas.filter(function (a) { return a.radius < threshold; });
+
+    console.log('mergeAreasByRudias@areaSize', areas.length, smallAreas.length);
+
+    let i, j, x, y, index, p, px, py;
+    let neighbours = this.getNearestNeighbours(1);
+    smallAreas.forEach(function (area) {
+      // area.neighbours = [];
+      let neighbourSet = new Set();
+      for (x = area.left; x <= area.right; x++) {
+        for (y = area.top; y <= area.bottom; y++) {
+          index = y * width + x;
+          if (floodArr[index] == area.color) {
+            // check pixe's neighbour
+            for (j = 0; j < neighbours.length; j++) {
+              p = neighbours[j];
+              px = x + p.x;
+              py = y + p.y;
+              index = py * width + px;
+              if (px > 0 && px < width && py > 0 && py < height &&
+                areaMap[floodArr[index]] &&
+                floodArr[index] != 0 && floodArr[index] != area.color) {
+                // area.neighbours.push(subAreaMap[floodArr[index]]);
+                neighbourSet.add(areaMap[floodArr[index]]);
+              }
+            }
+          }
+        }
+      }
+      area.neighbours = Array.from(neighbourSet);
+    });
+
+
+    // remove the area has no neighbours.
+    smallAreas = smallAreas.filter(function (area) { return area.neighbours.length > 0; });
+
+    /**
+     * merge another area
+     */
+    function mergeToMe(me, other) {
+      // if i am a small area.
+      // merge other's neighbours
+      if (me.neighbours) {
+        me.neighbours =
+          me.neighbours.concat(other.neighbours)
+            .filter(function (a) { return a != me && a != other; });
+      }
+
+      // set other merged
+      other.merged = true;
+
+      for (x = other.left; x <= other.right; x++) {
+        for (y = other.top; y <= other.bottom; y++) {
+          index = y * width + x;
+          if (floodArr[index] == other.color) {
+            floodArr[index] = me.color;
+            me.addPoint(x, y);
+          }
+        }
+      }
+    }
+
+    let loop = 0;
+    do {
+      loop++;
+
+      smallAreas.forEach(function (area) {
+        if (area.merged)
+          return;
+        let neighbour;
+        for (let i = 0; i < area.neighbours.length; i++) {
+          // merge other small areas.
+          neighbour = area.neighbours[i];
+          // if neighbour is merged or neighbour is a large one
+          if (neighbour.merged || !neighbour.neighbours) {
+            continue;
+          }
+          mergeToMe(area, neighbour);
+        }
+        // area.neighbours = area.neighbours.sort((a, b) => {return a.count - b.count});
+        if (area.neighbours.length > 0) {
+          // merge me to the first large neighbour
+          mergeToMe(area.neighbours[0], area);
+        }
+      });
+
+      smallAreas = smallAreas.filter(function (
+        area) { return !area.merged && area.neighbours.length > 0; });
+
+      console.log('mergeAreasByRudias@loop:' + loop, smallAreas.length);
+
+      if (smallAreas.length == 0)
+        break;
+
+    } while (loop < 4);
+
+  }
+
+  /**
+     * 合并原区块中代表颜色是一致的相邻区块
+     * @param imgdata 
+     */
+  mergeAreasByColor(imgdata: ImageData, maxColorDist?: number) {
+    maxColorDist = maxColorDist || 4;
+    let originMapPixels = new Uint32Array(Utils.getImageData(this.map).data.buffer);
+    let areaMap = this.createAreaMap(imgdata);
+    let keys = Object.keys(areaMap);
+    let floodArr = new Uint32Array(imgdata.data.buffer);
+    let width = imgdata.width;
+    let height = imgdata.height;
+    let areas = keys.map(function (key) { return areaMap[key]; });
+
+    let neighbours = this.getNearestNeighbours(1);
+    let j, x, y, index, p, px, py;
+
+    areas.forEach(area => {
+      area.mainColor = this.getColorFromColored(area);
+    })
+
+    areas.forEach(area => {
+      let neighbourSet = new Set();
+      for (x = area.left; x <= area.right; x++) {
+        for (y = area.top; y <= area.bottom; y++) {
+          index = y * width + x;
+          if (floodArr[index] == area.color) {
+            let originColor = originMapPixels[index];
+            // check pixels neighbour
+            for (j = 0; j < neighbours.length; j++) {
+              p = neighbours[j];
+              px = x + p.x;
+              py = y + p.y;
+              index = py * width + px;
+              if (px > 0 && px < width && py > 0 && py < height &&
+                floodArr[index] != 0 && floodArr[index] != area.color
+                // && this.pagePixels[index] == 0 // 同属原来的大区块
+                && originColor == originMapPixels[index]) {
+                // area.neighbours.push(areaMap[floodArr[index]]);
+                neighbourSet.add(areaMap[floodArr[index]]);
+              }
+            }
+          }
+        }
+      }
+      area.neighbours = Array.from(neighbourSet).filter((a: any) => { return this.colorDistance(a.mainColor, area.mainColor) < maxColorDist });
+    })
+
+    let smallAreas = areas.filter(function (a) { return a.neighbours.length > 0; });
+
+
+    console.log('mergeAreasByColor@areaSize', areas.length, smallAreas.length);
+
+    /**
+     * merge another area
+     */
+    function mergeToMe(me, other) {
+      // if i am a small area.
+      // merge other's neighbours
+      if (me.neighbours) {
+        me.neighbours =
+          me.neighbours.concat(other.neighbours)
+            .filter(function (a) { return a != me && a != other; });
+      }
+
+      // set other merged
+      other.merged = true;
+
+      for (x = other.left; x <= other.right; x++) {
+        for (y = other.top; y <= other.bottom; y++) {
+          index = y * width + x;
+          if (floodArr[index] == other.color) {
+            floodArr[index] = me.color;
+            me.addPoint(x, y);
+          }
+        }
+      }
+    }
+
+    let loop = 0;
+    do {
+      loop++;
+
+      smallAreas.forEach(function (area) {
+        if (area.merged)
+          return;
+        let neighbour;
+        for (let i = 0; i < area.neighbours.length; i++) {
+          // merge other small areas.
+          neighbour = area.neighbours[i];
+          // if neighbour is merged or neighbour is a large one
+          if (neighbour.merged || !neighbour.neighbours) {
+            continue;
+          }
+          mergeToMe(area, neighbour);
+        }
+        if (area.neighbours.length > 0) {
+          // merge me to the first large neighbour
+          mergeToMe(area.neighbours[0], area);
+        }
+      });
+
+      smallAreas = smallAreas.filter(function (
+        area) { return !area.merged && area.neighbours.length > 0; });
+
+      console.log('mergeAreasByColor@loop:' + loop, smallAreas.length);
+
+      if (smallAreas.length == 0)
+        break;
+
+    } while (loop < 4);
+  }
+
+
+  /**
+   * Fill pixels in srcImage with alpha<255
+   * with the nearest filled color
+   * @param srcImage source ImageData  ojbect.
+   * @param floodFilledImage flood filled ImageData object.
+   * @param maxDistance
+   */
+  fillAlphaGap(srcImage, floodFilledImage, maxDistance) {
+    maxDistance = maxDistance || 2;
+    var srcArr = new Uint32Array(srcImage.data.buffer);
+    var srcBuf = srcImage.data; // to get the alpha directly
+    var floodArr = new Uint32Array(floodFilledImage.data.buffer);
+    var width = srcImage.width;
+    var height = srcImage.height;
+    var alphaPoints = []; // x,y,x,y,x,y......
+    var unFilledPoints = [];
+    var taskArr = []; // index, color, index, color
+    var i, j, x, y, index, color, p, px, py;
+
+    var neighbours = this.getNearestNeighbours(1);
+
+    // get all alpha points
+    for (x = 0; x < width; x++) {
+      for (y = 0; y < height; y++) {
+        index = y * width + x;
+        j = index * 4 + 3; // alpha byte index
+        if (floodArr[index] == 0 && srcBuf[j] < 255 ) {
+          alphaPoints.push(x, y);
+        }
+      }
+    }
+
+    // console.log('neighbours:', neighbours);
+    // console.log('alphaPoints@length', alphaPoints.length / 2);
+    var loop = 0;
+
+    do {
+      loop++;
+
+      // loop all alpha points and try to fill.
+      var found;
+      for (i = 0; i < alphaPoints.length; i += 2) {
+        x = alphaPoints[i];
+        y = alphaPoints[i + 1];
+        found = false;
+        for (j = 0; j < neighbours.length; j++) {
+          p = neighbours[j];
+          px = x + p.x;
+          py = y + p.y;
+          index = py * width + px;
+          if (px > 0 && px < width && py > 0 && py < height &&
+            floodArr[index] != 0) {
+            taskArr.push(y * width + x, floodArr[index]);
+            found = true;
+            break;
+          }
+        }
+        if (!found)
+          unFilledPoints.push(x, y);
+      }
+
+      console.log('fillAlphaGap@loop:' + loop, alphaPoints.length,
+        taskArr.length / 2, unFilledPoints.length / 2);
+
+      // no more alpha pixels can find it's neighbours.
+      if (taskArr.length == 0)
+        break;
+
+      // do the task
+      for (i = 0; i < taskArr.length; i += 2) {
+        index = taskArr[i];
+        color = taskArr[i + 1];
+        floodArr[index] = color;
+      }
+
+      taskArr = [];
+      alphaPoints = unFilledPoints;
+      unFilledPoints = [];
+
+    } while (loop <= maxDistance);
+  }
+
+
+  /**
+  * Fill blank pixels with color.
+  */
+  fillEmptyGap(floodFilledImage, maxDistance) {
+    let floodArr = new Uint32Array(floodFilledImage.data.buffer);
+    let width = floodFilledImage.width;
+    let height = floodFilledImage.height;
+    let emptyPoints = []; // x,y,x,y,x,y......
+    let unFilledPoints = [];
+    let taskArr = []; // index, color, index, color
+    let i, j, x, y, index, color, p, px, py;
+
+    let neighbours = this.getNearestNeighbours(1);
+
+    // get all empty points
+    for (x = 0; x < width; x++) {
+      for (y = 0; y < height; y++) {
+        index = y * width + x;
+        if (floodArr[index] == 0) {
+          emptyPoints.push(x, y);
+        }
+      }
+    }
+
+    console.log('FillEmptyGap#emptyPoints@length', emptyPoints.length / 2);
+    let loop = 0;
+
+    do {
+      loop++;
+
+      // loop all alpha points and try to fill.
+      let found;
+      for (i = 0; i < emptyPoints.length; i += 2) {
+        x = emptyPoints[i];
+        y = emptyPoints[i + 1];
+        found = false;
+        for (j = 0; j < neighbours.length; j++) {
+          p = neighbours[j];
+          px = x + p.x;
+          py = y + p.y;
+          index = py * width + px;
+          if (px > 0 && px < width && py > 0 && py < height &&
+            floodArr[index] != 0) {
+            taskArr.push(y * width + x, floodArr[index]);
+            found = true;
+            break;
+          }
+        }
+        if (!found)
+          unFilledPoints.push(x, y);
+      }
+
+      console.log('fillEmptyGap@loop:' + loop, emptyPoints.length,
+        taskArr.length / 2, unFilledPoints.length / 2);
+
+      // no more alpha pixels can find it's neighbours.
+      if (taskArr.length == 0)
+        break;
+
+      // do the task
+      for (i = 0; i < taskArr.length; i += 2) {
+        index = taskArr[i];
+        color = taskArr[i + 1];
+        floodArr[index] = color;
+      }
+
+      taskArr = [];
+      emptyPoints = unFilledPoints;
+      unFilledPoints = [];
+
+    } while (loop <= maxDistance);
+  }
+
+
+  /**
+  * Create area map from map pixels.
+  */
+  createAreaMap(imgdata: ImageData): AreaMap {
+    let width = imgdata.width;
+    let floodArr = new Uint32Array(imgdata.data.buffer);
+    let areaMap: AreaMap = {};
+    let i, x, y, color;
+    for (i = 0; i < floodArr.length; i++) {
+      color = floodArr[i];
+      // if (color == 0) continue;
+      x = i % width;
+      y = Math.floor(i / width);
+      if (areaMap[color]) {
+        areaMap[color].addPoint(x, y);
+      } else {
+        areaMap[color] = new FillArea(color, x, y);
+      }
+    }
+    return areaMap;
+  }
+
+  /**
+  * Create subarea map from area.
+  */
+  createSubAreaMap(area: FillArea): AreaMap {
+    let orignalMapPixels = new Uint32Array(Utils.getImageData(this.map).data.buffer);
+    let width = this.width;
+    let floodArr = this.newMapPixels;
+    let areaMap: AreaMap = {};
+    let i, x, y, color;
+    for (i = 0; i < floodArr.length; i++) {
+      if (orignalMapPixels[i] != area.color) continue;
+      color = floodArr[i];
+      x = i % width;
+      y = Math.floor(i / width);
+      if (areaMap[color]) {
+        areaMap[color].addPoint(x, y);
+      } else {
+        areaMap[color] = new FillArea(color, x, y);
+      }
+    }
+    return areaMap;
+  }
+
+  /**
+   * Finder center of area.
+   * @param {FillArea} area 
+   */
+  findAreaCenter(area) {
+
+    console.log('area', area);
+
+    let ai, ax, ay, aw, ah, i, x, y;
+    aw = area.width();
+    ah = area.height();
+
+    let areaPixels = new Uint32Array(aw * ah);
+
+    for (ai = 0; ai < areaPixels.length; ai++) {
+      ax = ai % aw;
+      ay = (ai - ax) / aw;
+      x = area.left + ax;
+      y = area.top + ay;
+      i = y * this.width + x;
+      if (this.newMapPixels[i] == area.color) {
+        areaPixels[ai] = this.newMapPixels[i];
+      } else {
+        areaPixels[ai] = 0;
+      }
+    }
+
+
+    //find contours
+    let flag, pixel;
+    let contours = [];
+    for (ax = 0; ax < aw; ax++) {
+      flag = 0;
+      for (ay = 0; ay < ah; ay++) {
+        ai = ay * aw + ax;
+        pixel = areaPixels[ai];
+        if (!flag && pixel) {
+          flag = 1;
+          contours.push(ai);
+        } else if (flag && !pixel) {
+          flag = 0;
+          contours.push(ai - aw);
+        } else if (flag && pixel) continue;
+        else if (!flag && !pixel) continue;
+      }
+      if (flag) {
+        //last point
+        contours.push(ai);
+      }
+    }
+
+    for (ay = 0; ay < ah; ay++) {
+      flag = 0;
+      for (ax = 0; ax < aw; ax++) {
+        ai = ay * aw + ax;
+        pixel = areaPixels[ai];
+        if (!flag && pixel) {
+          flag = 1;
+          contours.push(ai);
+        } else if (flag && !pixel) {
+          flag = 0;
+          contours.push(ai - 1);
+        } else if (flag && pixel) continue;
+        else if (!flag && !pixel) continue;
+      }
+      if (flag) {
+        //last point
+        contours.push(ai);
+      }
+    }
+
+
+
+    let contoursHash = {}
+    let contoursNew = [];
+    for (i = 0; i < contours.length; i++) {
+      ai = contours[i];
+      if (contoursHash[ai]) continue;
+      else {
+        contoursNew.push(ai);
+        contoursHash[ai] = true;
+      }
+    }
+    console.log('contours size:', contours.length, contoursNew.length);
+    contoursHash = null;
+    contours = contoursNew;
+
+    let contoursX = [];
+    let contoursY = [];
+    for (i = 0; i < contours.length; i++) {
+      ai = contours[i];
+      x = ai % aw;
+      y = (ai - x) / aw;
+      contoursX[i] = x;
+      contoursY[i] = y;
+    }
+
+
+    //console.log('contoursXY', contoursX, contoursY);
+
+    let minDist, maxDist, maxAi, dist, x1, y1, x2, y2, dx, dy, radius;
+    maxDist = 0;
+    for (ai = 0; ai < areaPixels.length; ai++) {
+      pixel = areaPixels[ai];
+      minDist = Number.MAX_VALUE;
+      x1 = ai % aw;
+      y1 = (ai - x1) / aw;
+      if (pixel) {
+        for (i = 0; i < contours.length; i++) {
+          dx = x1 - contoursX[i];
+          dy = y1 - contoursY[i];
+          dist = dx * dx + dy * dy;
+          if (dist < minDist) minDist = dist;
+          //性能优化
+          if (minDist < maxDist) break;
+        }
+        if (minDist > maxDist) {
+          maxDist = minDist;
+          maxAi = ai;
+        }
+      }
+
+    }
+
+    x = maxAi % aw;
+    y = (maxAi - x) / aw;
+    radius = Math.sqrt(maxDist)
+    console.log('maxAi: ', maxAi, x, y, maxDist, radius);
+
+
+    let gx = x + area.left;
+    let gy = y + area.top;
+
+    return {
+      x: gx,
+      y: gy,
+      radius
+    }
+
+  }
+
+  /**
+   * 统计这个区域的所有像素的颜色,选出最具代表的颜色
+   * @param area 
+   * @returns 
+   */
+  getColorFromColored(area: FillArea): number {
+    let ai, aw, i, x, y;
+    let sum = {};
+    aw = area.width();
+    let count = area.width() * area.height();
+    for (ai = 0; ai < count; ai++) {
+      x = area.left + ai % aw;
+      y = area.top + Math.floor(ai / aw);
+      i = y * this.width + x;
+      if (this.newMapPixels[i] == area.color && this.pagePixels[i] == 0) { // 排除线条点
+        let color = this.coloredPixels[i];
+        if (!sum[color]) sum[color] = 1;
+        else sum[color] = sum[color] + 1;
+      }
+    }
+    let maxColor: string;
+    let maxCount = 0;
+    Object.keys(sum).forEach((color: string) => {
+      if (sum[color] > maxCount) {
+        maxCount = sum[color];
+        maxColor = color;
+      }
+    });
+    //console.log('sum:', sum);
+    let colorInt = parseInt(maxColor);
+
+    return colorInt;
+  }
+
+
+  /**
+ * Get the nearest N neighbours
+ * and sort by distance.
+ * @param n integer >=1
+ */
+  getNearestNeighbours(n) {
+    let distArr = [];
+    for (let i = (-1) * n; i <= n; i++) {
+      for (let j = (-1) * n; j <= n; j++) {
+        if (i == 0 && j == 0) {
+          // exclued self
+          continue;
+        }
+        let distance = Math.pow(i, 2) + Math.pow(j, 2);
+        distArr.push({ x: i, y: j, dist: distance });
+      }
+    }
+
+    distArr.sort(function (a, b) { return a.dist - b.dist; });
+
+    return distArr;
+  }
+
+
+
+  randomColor(): number {
+    let color = new Uint8Array([0, 0, 0, 255]);
+    for (let i = 0; i < 3; i++) {
+      color[i] = Math.floor(Math.random() * 256);
+    }
+    let u32 = new Uint32Array(color.buffer);
+    return u32[0];
+  }
+
+  colorDistance(color1: number, color2: number) {
+    let lab1 = Lab.fromRGBA(color1);
+    let lab2 = Lab.fromRGBA(color2);
+    let distance = Math.abs(lab1.deltaE(lab2));
+    return distance;
+  }
+
+
+  /**
+ * 按照大小(像素点个数)合并子区块(已废弃)
+ * @param area 
+ * @param threshold 
+ */
+  mergeSubAreasBySize(area, threshold?) {
+    let width = this.width;
+    let height = this.height;
+    const kk = (this.width / 1000) * 20;
+    threshold = threshold || kk;
+    let floodArr = this.newMapPixels;
+    let subAreaMap = this.createSubAreaMap(area);
+    let keys = Object.keys(subAreaMap);
+    let subAreas = Object.values(subAreaMap);
+    let smallSubAreas = subAreas.filter(a => { return a.count < threshold; });
+    console.log('mergeSubAreasBySize@areaSize', subAreas.length, smallSubAreas.length);
+
+    let i, j, x, y, index, p, px, py;
+    let neighbours = this.getNearestNeighbours(1);
+    smallSubAreas.forEach(function (area) {
+      // area.neighbours = [];
+      let neighbourSet = new Set();
+      for (x = area.left; x <= area.right; x++) {
+        for (y = area.top; y <= area.bottom; y++) {
+          index = y * width + x;
+          if (floodArr[index] == area.color) {
+            // check pixe's neighbour
+            for (j = 0; j < neighbours.length; j++) {
+              p = neighbours[j];
+              px = x + p.x;
+              py = y + p.y;
+              index = py * width + px;
+              if (px > 0 && px < width && py > 0 && py < height &&
+                subAreaMap[floodArr[index]] &&
+                floodArr[index] != 0 && floodArr[index] != area.color) {
+                // area.neighbours.push(subAreaMap[floodArr[index]]);
+                neighbourSet.add(subAreaMap[floodArr[index]]);
+              }
+            }
+          }
+        }
+      }
+      area.neighbours = Array.from(neighbourSet);
+    });
+
+    // remove the area has no neighbours.
+    smallSubAreas = smallSubAreas.filter(function (area) { return area.neighbours.length > 0; });
+
+    /**
+     * merge another area
+     */
+    function mergeToMe(me, other) {
+      // if i am a small area.
+      // merge other's neighbours
+      if (me.neighbours) {
+        me.neighbours =
+          me.neighbours.concat(other.neighbours)
+            .filter(function (a) { return a != me && a != other; });
+      }
+
+      // set other merged
+      other.merged = true;
+
+      for (x = other.left; x <= other.right; x++) {
+        for (y = other.top; y <= other.bottom; y++) {
+          index = y * width + x;
+          if (floodArr[index] == other.color) {
+            floodArr[index] = me.color;
+            me.addPoint(x, y);
+          }
+        }
+      }
+    }
+
+    let loop = 0;
+    do {
+      loop++;
+
+      smallSubAreas.forEach(function (area) {
+        if (area.merged)
+          return;
+        let neighbour;
+        for (let i = 0; i < area.neighbours.length; i++) {
+          // merge other small areas.
+          neighbour = area.neighbours[i];
+          // if neighbour is merged or neighbour is a large one
+          if (neighbour.merged || !neighbour.neighbours) {
+            continue;
+          }
+          mergeToMe(area, neighbour);
+        }
+        // area.neighbours = area.neighbours.sort((a, b) => {return a.count - b.count});
+        if (area.neighbours.length > 0) {
+          // merge me to the first large neighbour
+          mergeToMe(area.neighbours[0], area);
+        }
+      });
+
+      smallSubAreas = smallSubAreas.filter(function (
+        area) { return !area.merged && area.neighbours.length > 0; });
+
+      console.log('mergeSubAreasBySize@loop:' + loop, smallSubAreas.length);
+
+      if (smallSubAreas.length == 0)
+        break;
+
+    } while (loop < 4);
+
+  }
+
+  /**
+   * 按照中心点半径合并子区块(已废弃)
+   * @param area 
+   */
+  mergeSubAreasByRadius(area, threshold?) {
+    let width = this.width;
+    let height = this.height;
+    threshold = threshold || 4;
+    let floodArr = this.newMapPixels;
+    let subAreaMap = this.createSubAreaMap(area);
+    let subAreas = Object.values(subAreaMap);
+    // 计算每个区块的中心点半径
+    subAreas.forEach(area => {
+      area.radius = this.findAreaCenter(area).radius;
+    })
+    let smallSubAreas = subAreas.filter(a => { return a.radius < threshold });
+    console.log('mergeSubAreasByRadius@areaSize', subAreas.length, smallSubAreas.length);
+
+    let i, j, x, y, index, p, px, py;
+    let neighbours = this.getNearestNeighbours(1);
+    smallSubAreas.forEach(function (area) {
+      // area.neighbours = [];
+      let neighbourSet = new Set();
+      for (x = area.left; x <= area.right; x++) {
+        for (y = area.top; y <= area.bottom; y++) {
+          index = y * width + x;
+          if (floodArr[index] == area.color) {
+            // check pixe's neighbour
+            for (j = 0; j < neighbours.length; j++) {
+              p = neighbours[j];
+              px = x + p.x;
+              py = y + p.y;
+              index = py * width + px;
+              if (px > 0 && px < width && py > 0 && py < height &&
+                subAreaMap[floodArr[index]] &&
+                floodArr[index] != 0 && floodArr[index] != area.color) {
+                // area.neighbours.push(subAreaMap[floodArr[index]]);
+                neighbourSet.add(subAreaMap[floodArr[index]]);
+              }
+            }
+          }
+        }
+      }
+      area.neighbours = Array.from(neighbourSet);
+    });
+
+    // remove the area has no neighbours.
+    smallSubAreas = smallSubAreas.filter(function (area) { return area.neighbours.length > 0; });
+
+    /**
+     * merge another area
+     */
+    function mergeToMe(me, other) {
+      // if i am a small area.
+      // merge other's neighbours
+      if (me.neighbours) {
+        me.neighbours =
+          me.neighbours.concat(other.neighbours)
+            .filter(function (a) { return a != me && a != other; });
+      }
+
+      // set other merged
+      other.merged = true;
+
+      for (x = other.left; x <= other.right; x++) {
+        for (y = other.top; y <= other.bottom; y++) {
+          index = y * width + x;
+          if (floodArr[index] == other.color) {
+            floodArr[index] = me.color;
+            me.addPoint(x, y);
+          }
+        }
+      }
+    }
+
+    let loop = 0;
+    do {
+      loop++;
+
+      smallSubAreas.forEach(function (area) {
+        if (area.merged)
+          return;
+        let neighbour;
+        for (let i = 0; i < area.neighbours.length; i++) {
+          // merge other small areas.
+          neighbour = area.neighbours[i];
+          // if neighbour is merged or neighbour is a large one
+          if (neighbour.merged || !neighbour.neighbours) {
+            continue;
+          }
+          mergeToMe(area, neighbour);
+        }
+        // area.neighbours = area.neighbours.sort((a, b) => {return a.count - b.count});
+        if (area.neighbours.length > 0) {
+          // merge me to the first large neighbour
+          mergeToMe(area.neighbours[0], area);
+        }
+      });
+
+      smallSubAreas = smallSubAreas.filter(function (
+        area) { return !area.merged && area.neighbours.length > 0; });
+
+      console.log('mergeSubAreasByRudias@loop:' + loop, smallSubAreas.length);
+
+      if (smallSubAreas.length == 0)
+        break;
+
+    } while (loop < 4);
+
+  }
+
+  /**
+   * just for test
+   * @returns 
+   */
+  createColored2(): Promise<Blob> {
+    let canvas: HTMLCanvasElement = Utils.createCanvas(this.width, this.height);
+    let ctx: CanvasRenderingContext2D = canvas.getContext('2d');
+    ctx.putImageData(this.coloredData, 0, 0);
+    return new Promise((done, reject) => {
+      canvas.toBlob((blob: Blob) => done(blob), 'image/png');
+    });
+  }
+
+}

+ 104 - 0
zorro/src/app/lib/filler/common/easing.ts

@@ -0,0 +1,104 @@
+/*
+ * Easing Functions - inspired from http://gizma.com/easing/
+ * only considering the t value for the range [0, 1] => [0, 1]
+ */
+export default class Easing {
+  // no easing, no acceleration
+  static linear(t) {
+    return t
+  }
+
+  // accelerating from zero velocity
+  static easeInQuad(t) {
+    return t * t
+  }
+
+  // decelerating to zero velocity
+  static easeOutQuad(t) {
+    return t * (2 - t)
+  }
+
+  // acceleration until halfway, then deceleration
+  static easeInOutQuad(t) {
+    return t < .5 ? 2 * t * t : -1 + (4 - 2 * t) * t
+  }
+
+  static easeInOutQuadBack(t) {
+    return t < .5 ? 2 * t * t : (-1 + (4 - 2 * t) * t) * (1 + 1 / 2 - t / 2);
+  }
+
+  static easeInOutQuadBack2(t) {
+    let k = 0.15;
+    return t < .5 ? 2 * t * t :
+      (-1 + (4 - 2 * t) * t) * (-(k * 16 * (t - 0.75) * (t - 0.75) + 1 - k) + 2);
+  }
+
+  /**
+   * Overshoot.
+   */
+  static overshoot(t) {
+    let mTension = 1;
+    t -= 1;
+    return t * t * ((mTension + 1) * t + mTension) + 1;
+  }
+
+
+  // accelerating from zero velocity
+  static easeInCubic(t) {
+    return t * t * t
+  }
+
+  // decelerating to zero velocity
+  static easeOutCubic(t) {
+    return (--t) * t * t + 1
+  }
+
+  // acceleration until halfway, then deceleration
+  static easeInOutCubic(t) {
+    return t < .5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
+  }
+
+  // accelerating from zero velocity
+  static easeInQuart(t) {
+    return t * t * t * t
+  }
+
+  // decelerating to zero velocity
+  static easeOutQuart(t) {
+    return 1 - (--t) * t * t * t
+  }
+
+  // acceleration until halfway, then deceleration
+  static easeInOutQuart(t) {
+    return t < .5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t
+  }
+
+
+
+  // accelerating from zero velocity
+  static easeInQuint(t) {
+    return t * t * t * t * t
+  }
+
+  // decelerating to zero velocity
+  static easeOutQuint(t) {
+    return 1 + (--t) * t * t * t * t
+  }
+
+  // acceleration until halfway, then deceleration
+  static easeInOutQuint(t) {
+    return t < .5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t
+  }
+
+  static easeOutBounce(t) {
+    if (t < 1 / 2.75) {
+      return (7.5625 * t * t);
+    } else if (t < 2 / 2.75) {
+      return (7.5625 * (t -= 1.5 / 2.75) * t + 0.75);
+    } else if (t < 2.5 / 2.75) {
+      return (7.5625 * (t -= 2.25 / 2.75) * t + 0.9375);
+    } else {
+      return (7.5625 * (t -= 2.625 / 2.75) * t + 0.984375);
+    }
+  }
+}

+ 2377 - 0
zorro/src/app/lib/filler/common/etrace.ts

@@ -0,0 +1,2377 @@
+import Polygon from "./polygon";
+
+interface Debug {
+  bulgeCount: number;  // 异常凸起点个数
+  createSegmentFailed: number; // 创建segment出错数
+  spCount: number; // 特殊端点个数(找不到邻居的segment端点)
+  spCountAfterOrderAdjust: number; // 经过单调纠偏后剩余的特殊端点数
+  spCountAfterInsertAdjust: number; // 经过插值纠偏后剩余的特殊端点数
+  totalIncompletePaths: number; // 非闭合的路径个数
+  doesntMatterIncompletePaths: number; // 无所谓的非闭合路径个数
+  realIncompletePaths: number;
+  removeIncompletePaths: number; // 删除掉的孤立非闭合路径个数
+  remvoeInvalidPaths: number; // 非法path的个数
+  clipAdjustCount: number; // clip切割点调整个数
+  selfIntersectCount: number; // 自相交多边形数量
+}
+
+interface Point {
+  x: number;
+  y: number;
+  color?: number;
+  a?: Point;
+  b?: Point;
+  neighbors?: Array<Point>;
+  traced?: boolean;
+  id?: string;
+  adjusted?: boolean;
+  bulge?: boolean;
+  x0?: number;
+  y0?: number;
+}
+
+interface Segment {
+  endA : Point;
+  endB : Point; 
+  pt : Array<Point>;
+  areaA : number;
+  areaB :  number; 
+  curve : Curve;
+  po: number[];
+  m: number;
+  invalid?: boolean;
+}
+
+interface Path {
+  segments: Array<Segment>;  // 一个path由多个segment构成
+  complete?: boolean; // 是否完整
+  s?: Point;  // path的起点
+  e?: Point;  // path的终点
+  curve?: Curve;   // bezier curve 信息, 由各个子segment的curve信息合成
+  line?: Array<Point>; // 存放直接连线的顶点,即多边形顶点,基本同polygon,只不过可能经过reverse了
+  polygon?: Polygon; // path对应的拟合多边形,有各个segment合并而成
+  sign?: number;  // 1: 正曲线(逆时针);-1: 负曲线(顺时针);按一般惯例,大圆包小圆的场景,大圆为正(逆时针),小圆为负(顺时针)
+}
+
+interface PathMap {
+  [key: number]: Array<Path> // key即区块id
+}
+
+// 两个区块相交点集
+interface IntersectMap {
+  [key: string]: PointSet  // key的组成: color1 + '-' color2
+}
+
+
+// 一系列点的集合
+export class PointSet {
+  w: number;
+  h: number;
+  size: number;
+  data: Set<number>;
+
+  constructor(w: number, h: number, data: Set<number>) {
+    this.w = w;
+    this.h = h;
+    this.size = w * h;
+    this.data = new Set(data);
+  }
+
+  at(x: number, y: number) {
+    return (x >= 0 && x < this.w && y >= 0 && y < this.h) &&
+      this.data.has(this.w * y + x);
+  };
+
+  index(i: number) {
+    let y = Math.floor(i / this.w);
+    let x = i - y * this.w;
+    return { x, y };
+  };
+
+  remove(x: number, y: number) {
+    this.data.delete(this.w * y + x);
+  }
+
+  copy() {
+    let contour = new PointSet(this.w, this.h, this.data);
+    return contour;
+  };
+
+  neighbors(x: number, y: number): Point[] {
+    let neibs = [];
+
+    if (this.at(x - 1, y)) { // left
+      neibs.push({ x: x - 1, y: y});
+    } 
+    if (this.at(x + 1, y)) { // right
+      neibs.push({ x: x + 1, y: y }); 
+    } 
+    if (this.at(x, y - 1)) { // up
+      neibs.push({ x: x, y: y - 1 });
+    } 
+    if (this.at( x, y + 1)) { // down
+      neibs.push( { x: x, y: y + 1 });
+    } 
+    if (this.at(x - 1, y - 1)) { // left up
+      neibs.push({ x: x - 1, y: y - 1 });
+    } 
+    if (this.at(x + 1, y - 1)) { // right up
+      neibs.push({ x: x + 1, y: y - 1 });
+    } 
+    if (this.at(x - 1, y + 1)) { // left down
+      neibs.push({ x: x - 1, y: y + 1 });
+    } 
+    if (this.at(x + 1, y + 1)) { // right down
+      neibs.push({ x: x + 1, y: y + 1 });
+    }
+    return neibs;
+  }
+
+}
+
+class Curve {
+  n: number;
+  // tag是CORNER或CURVE类型标志,分解出来的每一段拟合曲线,有CURVE和CORNER两种,
+  // CURVE表示贝塞尔曲线(a, u, w, b),相应的c将存放u, w, b 3个点
+  // CORNER表示是直线折角(a, v, b),相应的c将存放v, b 2个点
+  tag: any[];
+  // c是控制点,tag 和 c 配合,长度是tag的3倍,因为一个tag对应3个点,
+  // 如果是CURVE, 那么c[i][0]和c[i][1]分别对应 u 和 w 控制点,c[i][2]对应b
+  // 如果CORNER,那么c[i][0]没用,c[i][1]表示控制点即两条线段交点 v,c[i][2]表示b点
+  //(注:起始点 a 不需要存储,每一段的起始点 a 起始就是上一段的结束点 b, 第一个起始点 a 是末尾那段的结束点 b
+  // 但这里有个问题, 这是针对闭合路径来说的,如果是开放路径, 这里是有问题的,也是要改造调整的地方); 
+  c: any[]; 
+  vertex: any[];  // 调整后的顶点,小数点坐标点
+
+  s: Point;  // 起点
+  e: Point;  // 终点
+
+  constructor(n) {
+    this.n = n;
+    this.tag = new Array(n);
+    this.c = new Array(n * 3);
+    this.vertex = new Array(n);
+  }
+}
+
+export interface EtraceOption {
+  alphamax?: number;
+}
+
+export class ETrace {
+  width: number;
+  height: number;
+  mapPixels: Uint32Array;
+  intersectMap: IntersectMap = {};
+  segments: Segment[] = [];
+  pathMap: PathMap = {};
+
+  config: EtraceOption = {
+    alphamax: 1,
+  };
+
+  // for debug
+  hContour: Set<number> = new Set();
+  vContour: Set<number> = new Set();
+  poSet: Set<number> = new Set();
+
+  debugInfo: Debug;
+
+
+  constructor(mapPixels: Uint32Array, width: number, height: number) {
+    this.mapPixels = mapPixels;
+    this.width = width;
+    this.height = height;
+
+    this.debugInfo = { 
+      bulgeCount: 0, 
+      createSegmentFailed: 0,
+      spCount: 0, 
+      spCountAfterOrderAdjust: 0, 
+      spCountAfterInsertAdjust: 0, 
+      totalIncompletePaths: 0, 
+      realIncompletePaths: 0,
+      doesntMatterIncompletePaths: 0,
+      removeIncompletePaths: 0, 
+      remvoeInvalidPaths: 0,
+      clipAdjustCount: 0,
+      selfIntersectCount: 0,
+    };
+
+    this.pureMap();
+    this.contourScan();
+    this.makeSegments();
+    this.mergeSegmentEndpoint();
+    this.segmentsToPaths();
+    this.curvedPaths();
+
+    console.log(this.debugInfo);
+  }
+
+  // 对map图进行一轮净化 ,消灭封闭area内的孤点
+  private pureMap() {
+    let x, y, index, count, color;
+    let neibColors, pureNeibColors, changeColor;
+    let w = this.width;
+    let h = this.height;
+    let mapPixels = this.mapPixels;
+    let shift = [
+      { x: 0, y: -1 },
+      { x: 1, y: 0 },
+      { x: 0, y: 1 },
+      { x: -1, y: 0 },
+      { x: 1, y: -1 },
+      { x: 1, y: 1 },
+      { x: -1, y: 1 },
+      { x: -1, y: -1 },
+    ];
+
+    for (x = 0; x < w; x++) {
+      for (y = 0; y < h; y++) {
+        index = y * w + x;
+        color = mapPixels[index];
+        // 周边邻居点
+        neibColors = shift.map(s => mapPixels[(y + s.y) * w + (x + s.x)]).filter(c => c);
+        pureNeibColors = neibColors.filter((item, idx, arr) => arr.indexOf(item, 0) == idx);  // 去重
+
+        if (!pureNeibColors. find(c => c == color)) { // 没有邻居跟它的颜色一致
+          if (pureNeibColors.length > 1) {
+            pureNeibColors = pureNeibColors.map(c => {
+              count = 0;
+              for (let nc of neibColors) {
+                if (nc == c) {
+                  count++;
+                }
+              }
+              return { c, count };
+            });
+            pureNeibColors.sort((a, b) => b.count - a.count);
+            changeColor = pureNeibColors[0].c;
+          } else {
+            changeColor = pureNeibColors[0];
+          }
+          
+          mapPixels[index] = changeColor;
+        }
+      }
+    }
+  }
+
+  // 对map进行全边界扫描, 得到相交线集合
+  private contourScan() {
+    let mapPixels = this.mapPixels;
+    let intersectMap = this.intersectMap;
+    let chooseMap = {};
+    let w = this.width;
+    let h = this.height;
+    let x, y, color1, color2, index, index1, index2;
+
+    let hContour = this.hContour;
+    let vContour = this.vContour;
+
+    // 两个区块交界处,选择其一作为边缘点
+    function chooseEdgePoint(color1, color2, index1, index2) {
+      let index: number;
+      let color: number;
+      let key1 = color2 + '-' + color1;
+      let key2 = color1 + '-' + color2;
+      let value1 = chooseMap[key1];
+      let value2 = chooseMap[key2];
+      
+      if (value1) {
+        color = value1;
+      } else if (value2) {
+        color = value2;
+      } else {
+        color = color1;
+        chooseMap[key1] = color;
+      }
+
+      if (color == color1) index = index1;
+      else if (color == color2) index = index2;  
+      
+
+      return index;
+    }
+
+    function isBorder(x, y) {
+      return (x == 0 || y == 0 || x == w - 1  || y == h - 1);
+    }
+
+    function addBorder(index, color) {
+      let key = color + '-';
+      if (!intersectMap[key]) {
+        intersectMap[key] = new PointSet(w, h, new Set<number>());
+      }
+      intersectMap[key].data.add(index);
+    }
+
+    function addIntersect(index, color1, color2) {
+      let key = color2 + '-' + color1;
+      if (!intersectMap[key]) {
+        key = color1 + '-' + color2;
+        if (!intersectMap[key]) {
+          intersectMap[key] = new PointSet(w, h, new Set<number>());
+        }
+      }
+      intersectMap[key].data.add(index);
+    }
+
+    for (x = 0; x < w; x++) {
+      index1 = x;
+      color1 = mapPixels[index1];
+      for (y = 0; y < h; y++) {
+        index2 = y * w + x;
+        color2 = mapPixels[index2];
+        if (color1 != color2) { 
+          index = chooseEdgePoint(color1, color2, index1, index2);
+          addIntersect(index, color1, color2);
+          color1 = color2;
+
+          hContour.add(index);
+        } 
+        if (isBorder(x, y)) {
+          addBorder(index2, color2);
+        }
+        index1 = index2;
+      }
+    }
+
+    for (y = 0; y < h; y++) {
+      index1 = y * w;
+      color1 = mapPixels[index1];
+      for (x = 0; x < w; x++) {
+        index2 = y * w + x;
+        color2 = mapPixels[index2];
+        if (color1 != color2) {
+          index = chooseEdgePoint(color1, color2, index1, index2);
+          addIntersect(index, color1, color2);
+          color1 = color2;  
+
+          vContour.add(index);
+        } 
+        if (isBorder(x, y)) {
+          addBorder(index2, color2);
+        }
+        index1 = index2;
+      }
+    }
+  }
+
+
+  // 相交线进一步处理生成segments
+  private makeSegments() {
+
+    let self = this;
+    let intersectMap = this.intersectMap;
+    let w = this.width, h = this.height;
+    let keys = Object.keys(intersectMap);
+    for (let key of keys) {
+      let areas = key.split('-').map(e => +e); // 将key分解成两个area id
+      let pixels = Array.from(intersectMap[key].data).map(e => {
+        let y = Math.floor(e / w);
+        let x = e - y * w;
+        return { x, y };
+      });
+      
+      let segs = pixelSort(pixels, areas[0], areas[1]);
+      segs.forEach(s => this.segments.push(s));
+    }
+
+    function newPoint(p: Point) {
+      return { x: p.x, y: p.y };
+    }
+
+
+    function pixelSort(pixels: Array<Point>, areaA: number, areaB: number): Segment[] {
+
+      // if (areaA == 4282964191 && areaB == 4289335318) {
+      //   console.log('pause');
+      //   for (let p of pixels) {
+      //     console.log(`{x: ${p.x}, y: ${p.y}},`);
+      //   }
+      // }
+      let segment;
+      let segments = [];
+      let pHash = {};
+      pixels.forEach(p => {
+        p.id = `${p.x}.${p.y}`
+        pHash[p.id] = p;
+      })
+  
+      let shift = [
+        { x: 0, y: -1 },
+        { x: 1, y: 0 },
+        { x: 0, y: 1 },
+        { x: -1, y: 0 },
+        { x: 1, y: -1 },
+        { x: 1, y: 1 },
+        { x: -1, y: 1 },
+        { x: -1, y: -1 },
+      ];
+  
+      pixels.forEach(p => {
+        p.neighbors = shift.map(s => `${p.x + s.x}.${p.y + s.y}`)
+          .map(id => pHash[id])
+          .filter(p => p);
+      })
+
+      // 邻居降序排列
+      pixels.forEach(p => {
+        p.neighbors.sort((a, b) => a.neighbors.length - b.neighbors.length)
+      })
+  
+      for (let i = 0; i < pixels.length; i++) {
+        let p = pixels[i];
+
+        if (p.neighbors.length == 0) { // 没有邻居的孤点,直接处理成segment
+          // console.log(`found single point(${p.x}, ${p.y}) in pixels. areaA=${areaA}, areaB=${areaB}`);
+          let seg = { endA: newPoint(p), endB: newPoint(p), pt: [newPoint(p)], areaA, areaB };
+          segments.push(seg);
+          pixels.splice(i, 1);
+          i--;
+          continue;
+        }
+
+        if (p.neighbors.length == 1) { // 只有一个邻居,可能是起止点或异常凸起点,如果是异常凸起点,需要干掉
+          let neib = p.neighbors[0];
+          let idx, left = false, right = false, up = false, down = false;
+          for (let j = 0; j < neib.neighbors.length; j++) {
+            let nb = neib.neighbors[j];
+            if (nb == p) {
+              idx = j;
+              continue;
+            }
+            if (nb.x < neib.x) left = true;
+            if (nb.x > neib.x) right = true;
+            if (nb.y > neib.y) down = true;
+            if (nb.y < neib.y) up = true;
+          }
+          if ((left && right) || (up && down)) {
+            console.warn(`found a bulge: (${p.x}, ${p.y}) in pixels. areaA=${areaA}, areaB=${areaB}`);
+            self.debugInfo.bulgeCount++;
+            p.bulge = true;
+            neib.neighbors.splice(idx, 1);
+            pixels.splice(i, 1);
+            i--;
+          }
+        }
+      }
+
+      // trace,让点集变成有序
+      do {
+        let index = pixels.findIndex(p => !p.traced);
+        if (index < 0) break;
+        let p = pixels[index];
+        trace(p);
+        // create segment
+        segment = createSegment(pixels, areaA, areaB);
+        if (segment) {
+          segments.push(segment);
+        }
+        pixels = pixels.filter(p => !p.traced);
+      } while (true);
+
+      return segments;
+  
+    }
+
+    function trace(p) {
+      let n;
+      let startP = p;
+      p.traced = true;
+      console.log('tracing');
+  
+      if (!p.a) {
+        do {
+          n = p.neighbors.find(pp => !pp.traced);
+          if (n) {
+            p.a = n;
+            n.b = p;
+            p = n;
+            p.traced = true;
+          } else {
+            break;
+          }
+        } while (1)
+      }
+  
+      p = startP;
+
+      if (!p.b) {
+        do {
+          n = p.neighbors.find(pp => !pp.traced);
+          if (n) {
+            p.b = n;
+            n.a = p;
+            p = n;
+            p.traced = true;
+          } else {
+            break;
+          }
+        } while(1)
+  
+      }
+    }
+
+    function createSegment(pixels: Point[], areaA, areaB) {
+      let segment;
+      let endA = pixels.find(e => e.a && !e.b);
+      let endB = pixels.find(e => !e.a && e.b);
+      let pt = [];
+
+
+      if (!endA || !endB) {
+        console.warn("create segment failed! pixel length = " + pixels.length);
+        self.debugInfo.createSegmentFailed++;
+        return null;
+      }
+
+      let p = endA;
+
+      do {
+        pt.push({ x: p.x, y: p.y });
+        p = p.a;
+        if (!p) break;
+      } while (true)
+
+      // check
+      if (endA.x != pt[0].x || endA.y != pt[0].y 
+        || endB.x != pt[pt.length - 1].x || endB.y != pt[pt.length - 1].y) {
+          console.error("something wrong!");
+      }
+
+      segment = { 
+        endA: newPoint(pt[0]), 
+        endB: newPoint(pt[pt.length - 1]),
+        pt, areaA, areaB 
+      };
+
+      return segment;
+
+    }
+  
+  }
+
+  // segment顶点调整,即距离相近的起始点,统一成同一个顶点
+  private mergeSegmentEndpoint() {
+
+    function sign(i) {
+      return i > 0 ? 1 : i < 0 ? -1 : 0;
+    }
+
+    function equal(pt1: Point, pt2: Point) {
+      return pt1.x == pt2.x && pt1.y == pt2.y;
+    }
+    
+    function ddist(p, q) {
+      return Math.sqrt((p.x - q.x) * (p.x - q.x) + (p.y - q.y) * (p.y - q.y));
+    }
+
+    function isSameSign(s1, s2) {
+      if (s1 == 0 || s2 == 0) return true;
+      if (s1 == s2) return true;
+      return false;
+    }
+
+    function isNeighbor(pt1, pt2) {
+      if (Math.abs(pt1.x - pt2.x) <= 1 && Math.abs(pt1.y - pt2.y) <= 1) return true;
+      return false;
+    }
+
+    function exchange(list, idx1, idx2) {
+      let tmp = list[idx1];
+      list[idx1] = list[idx2];
+      list[idx2] = tmp;
+    }
+
+    /** 
+     * 对segment进行纠偏
+     * 所有的segment的端点应该能要能够找到与其衔接的其他segment端点,称为邻居segment,如果没有,那就是trace异常,需要纠偏
+     * 可能导致的情况有: 
+     * 1)segment端点前或后3个点非单调变化,这种调整下顺序或许就可以顺利找到邻居segment
+     * 2)循环打结的场景,这种情况其实有两个segment其端点非常接近,但并非紧挨着距离是1的邻居,导致不能成功配对。可以通过增加插值点来就解决
+     */
+    function adjustSpecialEndpoint(endpoints) {
+      
+      // 筛选出没有找到邻居的endpoint
+      let specialEndpoints = endpoints.filter(p => {
+        if (endpoints.find(pp => pp != p && isNeighbor(pp, p))) return false;
+        return true;
+      });
+
+      self.debugInfo.spCount = specialEndpoints.length;
+
+      console.warn(`需要纠偏的特殊点数:${specialEndpoints.length}`, specialEndpoints);
+
+      if (specialEndpoints.length == 0) {
+        console.log("无需纠偏");
+        return;
+      }
+
+      console.log('开始进行单调纠偏');
+      
+      // 对这些没有邻居的endpoint对应的segment进行单调变化纠偏
+      for (let sp of specialEndpoints) {
+        let seg = sp.seg;
+        if (seg.pt.length <= 2) continue;
+
+        if (equal(sp, seg.endA)) {  // 看头部
+          let dirx1 = sign(seg.pt[1].x - seg.pt[0].x);
+          let dirx2 = sign(seg.pt[2].x - seg.pt[1].x);
+          let diry1 = sign(seg.pt[1].y - seg.pt[0].y);
+          let diry2 = sign(seg.pt[2].y - seg.pt[1].y);
+          if (!isSameSign(dirx1, dirx2) || !isSameSign(diry1, diry2)) {
+            exchange(seg.pt, 0, 1);
+            seg.endA = { x: seg.pt[0].x, y: seg.pt[0].y };
+            sp.x = seg.endA.x;
+            sp.y = seg.endA.y;
+          }
+        } else if (equal(sp, seg.endB)) { // 看尾巴
+          let n = seg.pt.length;
+          let dirx1 = sign(seg.pt[n-2].x - seg.pt[n-3].x);
+          let dirx2 = sign(seg.pt[n-1].x - seg.pt[n-2].x);
+          let diry1 = sign(seg.pt[n-2].y - seg.pt[n-3].y);
+          let diry2 = sign(seg.pt[n-1].y - seg.pt[n-2].y);
+          if (!isSameSign(dirx1, dirx2) || !isSameSign(diry1, diry2)) {
+            exchange(seg.pt, n - 1, n - 2);
+            seg.endB = { x: seg.pt[n-1].x, y: seg.pt[n-1].y };
+            sp.x = seg.endB.x;
+            sp.y = seg.endB.y;
+          }
+        }
+      }
+
+      specialEndpoints = endpoints.filter(p => {
+        if (endpoints.find(pp => pp != p && isNeighbor(pp, p))) return false;
+        return true;
+      });
+
+      self.debugInfo.spCountAfterOrderAdjust = specialEndpoints.length;
+
+      if (specialEndpoints.length > 0) {
+        console.warn(`经过单调纠偏后,仍有 ${specialEndpoints.length} 个特殊点`, specialEndpoints);
+      } else {
+        console.log('单调纠偏后已不存在特殊点!');
+        return;
+      }
+
+      // 对异常偏离值是2的两个endpoint进行纠偏
+      let spA, spB, cp;
+      do {
+        spA = specialEndpoints.find(sp => !sp.done);
+        if (!spA) break;
+
+        spA.done = true;
+
+        // 找到靠近spA距离是2的对端spB
+        spB = specialEndpoints.find(sp => sp != spA && !sp.done && Math.abs(sp.x - spA.x) <= 2 && Math.abs(sp.y - spA.y) <= 2);
+        if (!spB) continue;
+
+        // 找到一个桥接点能够连接spA和spB的
+        cp = spA.seg.pt.find(p => isNeighbor(p, spA) && isNeighbor(p, spB));
+        if (cp) { // 在spA的segment中找到,则将cp添加到spB的segment中
+          if (equal(spB.seg.endA, spB)) {
+            spB.seg.pt.unshift({ x: cp.x, y: cp.y });
+            spB.seg.endA = { x: cp.x, y: cp.y };
+          } else if (equal(spB.seg.endB, spB)) {
+            spB.seg.pt.push({ x: cp.x, y: cp.y });
+            spB.seg.endB = { x: cp.x, y: cp.y };
+          }
+          spB.x = cp.x;
+          spB.y = cp.y;
+          spB.done = true;
+          continue;
+        }
+        cp = spB.seg.pt.find(p => isNeighbor(p, spA) && isNeighbor(p, spB));
+        if (cp) { // 在spB的segment中找到,则将cp添加到spA的segment中
+          if (equal(spA.seg.endA, spA)) {
+            spA.seg.pt.unshift({ x: cp.x, y: cp.y });
+            spA.seg.endA = { x: cp.x, y: cp.y };
+          } else if (equal(spA.seg.endB, spA)) {
+            spA.seg.pt.push({ x: cp.x, y: cp.y });
+            spA.seg.endB = { x: cp.x, y: cp.y };
+          }
+          spB.x = cp.x;
+          spB.y = cp.y;
+          spB.done = true;
+          continue;
+        }
+
+      } while(true)
+
+
+      specialEndpoints = endpoints.filter(p => {
+        if (endpoints.find(pp => pp != p && isNeighbor(pp, p))) return false;
+        return true;
+      });
+
+      self.debugInfo.spCountAfterInsertAdjust = specialEndpoints.length;
+
+      if (specialEndpoints.length > 0) {
+        console.warn(`经过插值纠偏后,仍有 ${specialEndpoints.length} 个特殊点`, specialEndpoints);
+      } else {
+        console.log('插值纠偏后已不存在特殊点!');
+        return;
+      }
+
+    }
+
+    // 递归寻邻近的endpoint,形成一个完整的group
+    function findNeighborEndpoint(group, endpoints) {
+      let groupEndpoints = group.endpoints;
+      let ninGruopEndpoints = endpoints.filter(s => !s.ingroup);
+      let foundEndpoint = null;
+
+      for (let point of ninGruopEndpoints) {
+        if (groupEndpoints.find(p => isNeighbor(p, point))) {
+          foundEndpoint = point;
+          break;
+        }
+      }
+      if (foundEndpoint) {
+        group.endpoints.push(foundEndpoint);
+        foundEndpoint.ingroup = true;
+        findNeighborEndpoint(group, endpoints);
+      }
+
+    }
+
+    // 在一个endpoint group中选出组长
+    function electGroupLeader(group) {
+
+      let i, j, pureEps, filterEps, dist;
+
+      pureEps = group.endpoints.filter((item, index, arr) => 
+        arr.findIndex(e => e.x == item.x && e.y == item.y) == index);
+
+      for (i = 0; i < pureEps.length; i++) {
+        dist = 0;
+        for (j = 0; j < pureEps.length; j++) {
+          if (i == j)  continue;
+          dist += ddist(pureEps[i], pureEps[j]);
+        }
+        pureEps[i].dist = dist;
+      }
+      pureEps.sort((a, b) => a.dist - b.dist);
+      filterEps = pureEps.slice(0, 2);
+
+      if(pureEps.length >= 2 && pureEps[0].dist == pureEps[1].dist) { // 头两个dist都相等,还要再决策下,谁出现的次数多就取谁
+        filterEps.forEach(p => {
+          let count = 0;
+          for (i = 0; i < group.endpoints.length; i++) {
+            if (equal(p, group.endpoints[i])) {
+              count++;
+            }
+          }
+          p.count = count;
+        })
+        filterEps.sort((a, b) => b.count - a.count);
+      }
+
+      group.leader = filterEps[0];
+
+    }
+
+    // 将相邻的endpoint合并成同一个
+    function mergeEndpointIngroup(group) {
+
+      let endpoint, seg, leader, old, neib;
+
+      leader = group.leader;
+
+      for (let i = 0; i < group.endpoints.length; i++) {
+        endpoint = group.endpoints[i];
+        seg = endpoint.seg;
+
+        if (seg.pt.length <= 2) {  // 只有1个和2个点的segment已经没有存在的必要
+          seg.invalid = true;
+          continue;
+        }
+
+        if (equal(endpoint, leader)) {
+          continue;
+        }
+
+        // if (equal(leader, seg.endA) || equal(leader, seg.endB)) {
+        //   continue;
+        // }
+
+        if (equal(seg.endA, endpoint)) {
+          old = seg.endA;
+          if (isNeighbor(seg.endA, leader)) {
+            seg.pt.unshift({ x: leader.x, y: leader.y });
+            seg.endA = { x: leader.x, y: leader.y, adjusted: true, ox: old.x, oy: old.y };
+          } else { // endpoint距离leader太远, 需要中间插值过渡过去
+            do {
+              neib = getNeighborToLeader(seg.endA);
+              if (!neib) break;
+
+              seg.pt.unshift({ x: neib.x, y: neib.y });
+              seg.endA = { x: neib.x, y: neib.y, adjusted: true, ox: old.x, oy: old.y };
+
+              if (neib == leader) break;
+
+            } while (1)
+          }
+
+        } else if (equal(seg.endB, endpoint)) {
+          old = seg.endB;
+          if(isNeighbor(seg.endB, leader)) {
+            seg.pt.push({ x: leader.x, y: leader.y });
+            seg.endB = { x: leader.x, y: leader.y, adjusted: true, ox: old.x, oy: old.y };
+          } else { // endpoint距离leader太远, 需要中间插值过渡过去
+            do {
+              neib = getNeighborToLeader(seg.endB);
+              if (!neib) break;
+        
+              seg.pt.push({ x: neib.x, y: neib.y });
+              seg.endB = { x: neib.x, y: neib.y, adjusted: true, ox: old.x, oy: old.y };
+
+              if (neib == leader) break;
+
+            } while (1)
+          }
+        }
+
+      }
+
+      function getNeighborToLeader(p: Point) {
+        let fitneib;
+        let neibs = [];
+        for (let dp of group.endpoints) {
+          if (!equal(p, dp) && isNeighbor(p, dp)) {
+            neibs.push(dp);
+          }
+        }
+
+        let dist, minP2L = 999;
+        for (let neib of neibs) {
+          dist = ddist(neib, leader);
+          if ( dist < minP2L) {
+            minP2L = dist;
+            fitneib = neib; 
+          }
+        }
+
+        return fitneib;
+
+      }
+
+    }
+
+    let self = this;
+    let segments = this.segments;
+    let endpoints = [];
+    let ninGroupPoint;
+    let group;
+    let count;
+
+    // 先剔除无效孤点
+    count = 0;
+    let singleSegs = segments.filter(s => s.pt.length == 1);
+    for (let ss of singleSegs) {
+      let findseg = segments.find(s => s != ss && (equal(s.endA, ss.endA) || equal(s.endB, ss.endA)));
+      if (findseg) {
+        let idx = segments.findIndex(s => s == ss);
+        segments.splice(idx, 1);
+        count++;
+      }
+    }
+    console.log(`delete single point segments. totoal: ${singleSegs.length}, delete: ${count}`);
+
+    
+    // 整理出所有的endpoint 
+    segments.forEach(s => {
+      endpoints.push({ x: s.endA.x, y: s.endA.y, seg: s });
+      endpoints.push({ x: s.endB.x, y: s.endB.y, seg: s });
+    })
+
+    // 先对所有的endpoint做一轮检查,所有的endpoint至少有一个邻居, 如果一个endpoint找不到邻居,那么有可能需要调整对应segment的pt和端点信息
+    // 这么做也是对trace算法的一个纠偏, 某些segment点集trace的结果首尾3个点可能不符合要求,x, y坐标变化没有做到单调增/减。 实测有发现这种情况
+    adjustSpecialEndpoint(endpoints);
+
+
+    // 然后对他们进行分组(相邻的endpoint分为一组)
+    do {
+
+      ninGroupPoint = endpoints.find(s => !s.ingroup);
+      
+      if (!ninGroupPoint) break;
+
+      group = { endpoints: [ninGroupPoint] };
+
+      ninGroupPoint.ingroup = true;
+      
+      findNeighborEndpoint(group, endpoints);
+
+      electGroupLeader(group); 
+
+      mergeEndpointIngroup(group);
+
+    } while (true)
+
+    // 调整完顶点后删除无效segment
+    let count1 = this.segments.length;
+    this.segments = segments.filter(s => !s.invalid);
+    let count2 = this.segments.length;
+    console.log(`顶点合并后删除 ${count1 - count2} 个无效segment`);
+  }
+
+
+  // segment组合成path
+  private segmentsToPaths() {
+
+    function equal(pt1: Point, pt2: Point) {
+      return pt1.x == pt2.x && pt1.y == pt2.y;
+    }
+
+    // 判断两个点是否靠近
+    function near(pt1: Point, pt2: Point) {
+      let threshold = 5;
+      let dx = Math.abs(pt1.x - pt2.x);
+      let dy = Math.abs(pt1.y - pt2.y);
+
+      // let dd = Math.sqrt(dx * dx + dy * dy);
+      // if (dd < threshold) return true;
+      // return false;
+
+      if (dx <= threshold && dy <= threshold) return true;
+      return false;
+    }
+
+    // 从一个segment出发按序找到一个完整的path
+    function findPath(startSeg, areaSegs, areaId) {
+      
+      let path: Path = { segments: [], complete: false };
+      let flag = 'B';  // 指示下一个是找endA还是endB
+      let seg = startSeg;
+      let nextseg;
+
+      seg.inpath = true;
+      path.segments.push(seg);
+      path.s = { x: seg.endA.x, y: seg.endA.y };
+
+      if (equal(startSeg.endA, startSeg.endB)) {  // 自封闭环,是独立path,不用再继续寻找了,直接返回
+        path.complete = true;
+        path.e = { x: seg.endB.x, y: seg.endB.y };
+        return path;
+      }
+
+      do {
+
+        if (flag == 'B') {
+          // 找下一个与当前seg的endB端点衔接上的segment(注:排除自闭合的segment)
+          nextseg = areaSegs.find(s => !s.inpath && !equal(s.endA, s.endB) 
+            && (equal(s.endA, seg.endB) || equal(s.endB, seg.endB)));
+          if (!nextseg) break;
+
+          if (equal(seg.endB, nextseg.endA)) flag = 'B';
+          else if (equal(seg.endB, nextseg.endB)) flag = 'A';
+          seg = nextseg;
+        } else if (flag == 'A') {
+          // 找下一个与当前seg的endA端点衔接上的segment(注:排除自闭合的segment)
+          nextseg = areaSegs.find(s => !s.inpath && !equal(s.endA, s.endB) 
+            && (equal(s.endA, seg.endA) || equal(s.endB, seg.endA)));
+          if (!nextseg) break;
+
+          if (equal(seg.endA, nextseg.endA)) flag = 'B';
+          else if (equal(seg.endA, nextseg.endB)) flag = 'A';
+          seg = nextseg;
+        }
+
+        seg.inpath = true;
+        path.segments.push(seg);
+
+        if (equal(seg.endA, startSeg.endA) || equal(seg.endB, startSeg.endA)) {
+          path.complete = true
+          break;
+        }
+
+      } while (true)
+
+
+      let endPoint;
+      if (flag == 'A') {
+        endPoint = path.segments[path.segments.length - 1].endA;
+      } else if (flag == 'B') {
+        endPoint = path.segments[path.segments.length - 1].endB;
+      }
+      path.e = { x: endPoint.x, y: endPoint.y };
+
+
+      if (path.complete) return path;
+
+      // 如果顺序走下来发现不能拼接成一个完整的path,那么尝试逆向寻找,让path尽可能完整
+      // console.warn(`${areaId} path incomplete! try reverse find`, path);
+
+      seg = startSeg;
+      flag = 'A';
+
+      do {
+
+        if (flag == 'B') {
+          // 找下一个与当前seg的endB端点衔接上的segment(注:排除自闭合的segment)
+          nextseg = areaSegs.find(s => !s.inpath && !equal(s.endA, s.endB) 
+            && (equal(s.endA, seg.endB) || equal(s.endB, seg.endB)));
+          if (!nextseg) break;
+
+          if (equal(seg.endB, nextseg.endA)) flag = 'B';
+          else if (equal(seg.endB, nextseg.endB)) flag = 'A'
+          seg = nextseg;
+        } else if (flag == 'A') {
+          // 找下一个与当前seg的endA端点衔接上的segment(注:排除自闭合的segment)
+          nextseg = areaSegs.find(s => !s.inpath && !equal(s.endA, s.endB) 
+            && (equal(s.endA, seg.endA) || equal(s.endB, seg.endA)));
+          if (!nextseg) break;
+
+          if (equal(seg.endA, nextseg.endA)) flag = 'B';
+          else if (equal(seg.endA, nextseg.endB)) flag = 'A'
+          seg = nextseg;
+        }
+
+        seg.inpath = true;
+        path.segments.unshift(seg);
+
+        if (equal(seg.endA, endPoint) || equal(seg.endB, endPoint)) {
+          path.complete = true
+          break;
+        }
+
+      } while (true)
+
+      let startPoint;
+      if (flag == 'A') {
+        startPoint = path.segments[0].endA;
+      } else if (flag == 'B') {
+        startPoint = path.segments[0].endB;
+      }
+      path.s = { x: startPoint.x, y: startPoint.y };
+
+      if (!path.complete) {
+        console.warn(`${areaId} found a incomplete path`, path);
+        self.debugInfo.totalIncompletePaths++;
+      }
+
+      return path;
+
+    }
+
+    // 强制合并不完整的path
+    function tryMergePaths(area, pathlist) {
+      let mergeflag;
+      let incompletCount = 0;
+      for (let path of pathlist) {
+        if (!path.complete) {
+          incompletCount++;
+        }
+      }
+      if (incompletCount == 1) {
+        console.log(`${area} incomplete path probably not a problem`, pathlist);
+        self.debugInfo.doesntMatterIncompletePaths++;
+      }
+      if (incompletCount < 2) return;  // nothing to merge
+
+      // start merge
+      do {
+        let path, path1 = null, path2 = null;
+        for (let i = 0; i < pathlist.length; i++) {
+          path = pathlist[i];
+          if (!path.complete && !near(path.s, path.e)) {
+            path1 = path; // find path1
+            for (let j = i + 1; j < pathlist.length; j++) {
+              path = pathlist[j];
+              if (!path.complete && !near(path.s, path.e)) {
+                if (canMerge(path1, path)) { // find path2
+                  path2 = path;
+                  break;
+                }
+              }
+            }
+          }
+          if (path1 && path2) {
+            break;
+          }
+        }
+
+        if (path1 && path2) {
+          let newpath = merge(path1, path2);
+          pathlist.splice(pathlist.findIndex(p => p == path1), 1);
+          pathlist.splice(pathlist.findIndex(p => p == path2), 1);
+          pathlist.unshift(newpath);
+          console.log("merge:", path1, path2, newpath, pathlist);
+        } else {
+          break;
+        }
+
+      } while(true)
+
+      for (let path of pathlist) {
+        if (!path.complete && equal(path.s, path.e)) {
+          path.complete = true;
+        }
+      }
+      for (let path of pathlist) {
+        if (!path.complete) {
+          self.debugInfo.realIncompletePaths++;
+        }
+      }
+
+      function canMerge(path1, path2): boolean {
+        if (near(path1.e, path2.s)) {
+          mergeflag = '1e2s';
+          return true;
+          let seg1 = path1.segments[path1.segments.length - 1];
+          let seg2 = path2.segments[0];
+          if ((seg1.areaA == seg2.areaA && seg1.areaB == seg2.areaB)
+            || (seg1.areaA == seg2.areaB && seg1.areaB == seg2.areaA)) {
+            mergeflag = '1e2s';
+            return true;
+          }
+        }
+        if (near(path1.e, path2.e)) {
+          mergeflag = '1e2e';
+          return true;
+          let seg1 = path1.segments[path1.segments.length - 1];
+          let seg2 = path2.segments[path2.segments.length - 1];
+          if ((seg1.areaA == seg2.areaA && seg1.areaB == seg2.areaB)
+            || (seg1.areaA == seg2.areaB && seg1.areaB == seg2.areaA)) {
+            mergeflag = '1e2e';
+            return true;
+          }
+        }
+        if (near(path1.s, path2.s)) {
+          mergeflag = '1s2s';
+          return true;
+          let seg1 = path1.segments[0];
+          let seg2 = path2.segments[0];
+          if ((seg1.areaA == seg2.areaA && seg1.areaB == seg2.areaB)
+            || (seg1.areaA == seg2.areaB && seg1.areaB == seg2.areaA)) {
+            mergeflag = '1s2s';
+            return true;
+          }
+        }
+        if (near(path1.s, path2.e)) {
+          mergeflag = '1s2e';
+          return true;
+          let seg1 = path1.segments[0];
+          let seg2 = path2.segments[path2.segments.length - 1];
+          if ((seg1.areaA == seg2.areaA && seg1.areaB == seg2.areaB)
+            || (seg1.areaA == seg2.areaB && seg1.areaB == seg2.areaA)) {
+            mergeflag = '1s2e';
+            return true;
+          }
+        }
+        return false;
+      }
+
+      function merge(path1, path2): Path {
+        let newpath: Path = { segments: [], complete: false };
+
+        if (mergeflag == '1e2s') {
+          for (let i = 0; i < path1.segments.length; i++) {
+            newpath.segments.push(path1.segments[i]);
+          }
+          for (let i = 0; i < path2.segments.length; i++) {
+            newpath.segments.push(path2.segments[i]);
+          }
+          newpath.s = path1.s;
+          newpath.e = path2.e;
+        } else if (mergeflag == '1e2e') {
+          for (let i = 0; i < path1.segments.length; i++) {
+            newpath.segments.push(path1.segments[i]);
+          }
+          for (let i = path2.segments.length - 1; i >= 0; i--) {
+            newpath.segments.push(path2.segments[i]);
+          }
+          newpath.s = path1.s;
+          newpath.e = path2.s;
+        } else if (mergeflag == '1s2s') {
+          for (let i = path2.segments.length - 1; i >= 0; i--) {
+            newpath.segments.push(path2.segments[i]);
+          }
+          for (let i = 0; i < path1.segments.length; i++) {
+            newpath.segments.push(path1.segments[i]);
+          }
+          newpath.s = path2.e;
+          newpath.e = path1.e;
+        } else if (mergeflag == '1s2e') {
+          for (let i = 0; i < path2.segments.length; i++) {
+            newpath.segments.push(path2.segments[i]);
+          }
+          for (let i = 0; i < path1.segments.length; i++) {
+            newpath.segments.push(path1.segments[i]);
+          }
+          newpath.s = path2.s;
+          newpath.e = path1.e;
+        }
+        return newpath;
+      }
+    }
+
+
+    // pathlist中的path是有顺序的,如大圆套小圆的场景,大圆path必须排在前面先画,然后moveTo,再画小圆
+    function sortPathlist(pathlist) {
+
+      if (pathlist.length <= 1) return;
+
+      // 先把每个path的矩形框出来
+      for (let path of pathlist) {
+        let p = path.segments[0].pt[0];
+        let l = p.x, r = p.x, t = p.y, b = p.y;
+        for (let seg of path.segments) {
+          seg.pt.forEach(p => {
+            if (p.x < l) l = p.x;
+            if (p.x > r) r = p.x;
+            if (p.y < t) t = p.y;
+            if (p.y > b) b = p.y;
+          })
+        }
+        path.area = ( r - l ) * ( b - t ); // 矩形框面积
+      }
+
+      // 按矩形框大小排序, 从大到小降序
+      pathlist.sort((a, b) => b.area - a.area);
+
+      pathlist.forEach(p => delete p.area);
+
+    }
+
+    function removeIncompletePaths(pathMap: PathMap) {
+
+      console.log("删除孤立非闭合路径");
+
+      let keys = Object.keys(pathMap);
+      let datas = [];
+      for (let key of keys) {
+        let pathlist = pathMap[key];
+        for (let i = 0; i < pathlist.length; i++) {
+          let path = pathlist[i];
+          if (!path.complete && path.segments.length == 1 && path.segments[0].pt.length < 30) {
+          // if (!path.complete && path.segments.length == 1) {
+            datas.push({pathlist, path});
+          }
+        }
+      }
+
+      console.log(`incomplete paths count: ${datas.length}`);
+
+      let data1, data2;
+      let count = 0;
+      for (let i = 0; i < datas.length; i++) {
+        data1 = datas[i];
+        if (data1.done) continue;
+
+        for (let j = i + 1; j < datas.length; j++) {
+          data2 = datas[j];
+          if(data1.path.segments[0] == data2.path.segments[0]) {
+            data1.pathlist.splice(data1.pathlist.findIndex(p => p == data1.path), 1);
+            data2.pathlist.splice(data2.pathlist.findIndex(p => p == data2.path), 1);
+            data1.done = true;
+            data2.done = true;
+            count += 2;
+            console.log(`remove path `, data1.path);
+            console.log(`remove path `, data2.path);
+          }
+        }
+        
+      }
+      
+      console.log(`删除孤立非闭合路径:${count}`);
+      self.debugInfo.removeIncompletePaths = count;
+    }
+
+
+    let self = this;
+
+    let areaSegHash = {};
+
+    this.segments.forEach(seg => {
+      if (seg.areaA) {
+        if (!areaSegHash[seg.areaA]) {
+          areaSegHash[seg.areaA] = new Set();
+        }
+        areaSegHash[seg.areaA].add(seg);
+      };
+      if (seg.areaB) {
+        if (!areaSegHash[seg.areaB]) {
+          areaSegHash[seg.areaB] = new Set();
+        }
+        areaSegHash[seg.areaB].add(seg);
+      }
+    })
+
+    let areas = Object.keys(areaSegHash);
+    // 将同一个area的segment组合成path,注:可能组成多个path
+    for (let area of areas) {
+      let pathlist = [];
+      let areaSegs = Array.from(areaSegHash[area]) as [any];  // 属于该area的所有segment
+      let startSeg, path;
+
+      do {
+        
+        startSeg = areaSegs.find(s => !s.inpath);
+        
+        if (!startSeg) break;
+
+        path = findPath(startSeg, areaSegs, area);
+
+        pathlist.push(path);
+
+      } while (true)
+
+      areaSegs.forEach(seg => delete seg.inpath);
+
+      sortPathlist(pathlist);
+
+      tryMergePaths(area, pathlist);
+
+      sortPathlist(pathlist);  // do sort again after merge paths
+
+      this.pathMap[area] = pathlist;
+
+    }
+
+    // delete alone incomplete path
+    // removeIncompletePaths(this.pathMap);
+
+  }
+
+
+
+  //////////////////////////////////////////////////////////////////////////////////////
+  /////////////////////  以下是将segment曲线化的核心算法,参考potrace ////////////////////
+
+  // path曲线化, 具体来说: 先将各个segment分别解析成多边形得到顶点, 然后path将各个segment的顶点汇聚起来,然后曲线化
+  private curvedPaths(mode = 1) {
+
+    function equal(pt1: Point, pt2: Point) {
+      return pt1.x == pt2.x && pt1.y == pt2.y;
+    }
+
+    function ddist(p, q) {
+      return Math.sqrt((p.x - q.x) * (p.x - q.x) + (p.y - q.y) * (p.y - q.y));
+    }
+
+    function mod(a, n) {
+      return a >= n ? a % n : a >= 0 ? a : n - 1 - (-1 - a) % n;
+    }
+
+    function sign(i) {
+      return i > 0 ? 1 : i < 0 ? -1 : 0;
+    }
+
+    function xprod(p1, p2) {
+      return p1.x * p2.y - p1.y * p2.x;
+    }
+
+    function cyclic(a, b, c) {
+      if (a <= c) {
+        return (a <= b && b < c);
+      } else {
+        return (a <= b || b < c);
+      }
+    }
+
+    function Sum(x, y, xy, x2, y2) {
+      this.x = x;
+      this.y = y;
+      this.xy = xy;
+      this.x2 = x2;
+      this.y2 = y2;
+    }
+
+    function quadform(Q, w) {
+      let v = new Array(3), i, j, sum;
+
+      v[0] = w.x;
+      v[1] = w.y;
+      v[2] = 1;
+      sum = 0.0;
+
+      for (i = 0; i < 3; i++) {
+        for (j = 0; j < 3; j++) {
+          sum += v[i] * Q.at(i, j) * v[j];
+        }
+      }
+      return sum;
+    }
+
+    function Quad() {
+      this.data = [0, 0, 0, 0, 0, 0, 0, 0, 0];
+    }
+
+    Quad.prototype.at = function (x, y) {
+      return this.data[x * 3 + y];
+    };
+
+    function interval(lambda, a, b) {
+      let res = { x: 0, y: 0 };
+
+      res.x = a.x + lambda * (b.x - a.x);
+      res.y = a.y + lambda * (b.y - a.y);
+      return res;
+    }
+
+    
+    function dorth_infty(p0, p2) {
+      var r = { x: 0, y: 0 };
+
+      r.y = sign(p2.x - p0.x);
+      r.x = -sign(p2.y - p0.y);
+
+      return r;
+    }
+
+    function ddenom(p0, p2) {
+      let r = dorth_infty(p0, p2);
+
+      return r.y * (p2.x - p0.x) - r.x * (p2.y - p0.y);
+    }
+
+    function dpara(p0, p1, p2) {
+      let x1, y1, x2, y2;
+
+      x1 = p1.x - p0.x;
+      y1 = p1.y - p0.y;
+      x2 = p2.x - p0.x;
+      y2 = p2.y - p0.y;
+
+      return x1 * y2 - x2 * y1;
+    }
+
+    // 判断一个segment是否闭合
+    function isClosed(seg) {
+      if (seg.pt.length <= 2) return false;
+
+      let pt = seg.pt;
+      let n = seg.pt.length;
+
+      if (Math.abs(pt[0].x - pt[n-1].x) <= 1 && Math.abs(pt[0].y - pt[n-1].y) <= 1) return true;
+      return false;
+    }
+
+    function calcSums(seg) {
+      let i, x, y;
+      seg.sums = [];
+      let s = seg.sums;
+      s.push(new Sum(0, 0, 0, 0, 0));
+      for (i = 0; i < seg.pt.length; i++) {
+        x = seg.pt[i].x - seg.endA.x;
+        y = seg.pt[i].y - seg.endA.y;
+        s.push(new Sum(s[i].x + x, s[i].y + y, s[i].xy + x * y,
+          s[i].x2 + x * x, s[i].y2 + y * y));
+      }
+    }
+
+    function calcLon(seg) {
+
+      let n = seg.pt.length, pt = seg.pt, dir,
+        pivk = new Array(n),
+        nc = new Array(n),
+        ct = new Array(4);
+      seg.lon = new Array(n);
+
+      let isclosed = isClosed(seg);
+
+      let constraint = [ { x: 0, y: 0 }, {x: 0, y: 0} ],
+        cur = { x: 0, y: 0 },
+        off = { x: 0, y: 0 },
+        dk = { x: 0, y: 0 },
+        foundk;
+
+      let i, j, k1, a, b, c, d, k = 0;
+      for (i = n - 1; i >= 0; i--) {
+        if (pt[i].x != pt[k].x && pt[i].y != pt[k].y) {
+          k = i + 1;
+        }
+        if (k >= n) {
+          k = 0;  // add by guoziyun. to fit unclosed path
+        }
+        nc[i] = k;
+      }
+
+      for (i = n - 1; i >= 0; i--) {
+        ct[0] = ct[1] = ct[2] = ct[3] = 0;
+
+        if (isclosed || i < n - 1)  {  // do this if path is closed or not the last point
+          dir = (3 + 3 * (pt[mod(i + 1, n)].x - pt[i].x) +
+          (pt[mod(i + 1, n)].y - pt[i].y)) / 2;
+          ct[dir]++;
+        }
+
+        constraint[0].x = 0;
+        constraint[0].y = 0;
+        constraint[1].x = 0;
+        constraint[1].y = 0;
+
+        k = nc[i];
+        k1 = i;
+        while (1) {
+          foundk = 0;
+          dir = (3 + 3 * sign(pt[k].x - pt[k1].x) +
+            sign(pt[k].y - pt[k1].y)) / 2;
+          // modify by guoziyun
+          if (dir == 0 || dir == 1 || dir == 2 || dir == 3) {
+            ct[dir]++;
+          } 
+
+          if (ct[0] && ct[1] && ct[2] && ct[3]) {
+            pivk[i] = k1;
+            foundk = 1;
+            break;
+          }
+
+          cur.x = pt[k].x - pt[i].x;
+          cur.y = pt[k].y - pt[i].y;
+
+          if (xprod(constraint[0], cur) < 0 || xprod(constraint[1], cur) > 0) {
+            break;
+          }
+
+          if (Math.abs(cur.x) <= 1 && Math.abs(cur.y) <= 1) {
+
+          } else {
+            off.x = cur.x + ((cur.y >= 0 && (cur.y > 0 || cur.x < 0)) ? 1 : -1);
+            off.y = cur.y + ((cur.x <= 0 && (cur.x < 0 || cur.y < 0)) ? 1 : -1);
+            if (xprod(constraint[0], off) >= 0) {
+              constraint[0].x = off.x;
+              constraint[0].y = off.y;
+            }
+            off.x = cur.x + ((cur.y <= 0 && (cur.y < 0 || cur.x < 0)) ? 1 : -1);
+            off.y = cur.y + ((cur.x >= 0 && (cur.x > 0 || cur.y < 0)) ? 1 : -1);
+            if (xprod(constraint[1], off) <= 0) {
+              constraint[1].x = off.x;
+              constraint[1].y = off.y;
+            }
+          }
+          k1 = k;
+          k = nc[k1];
+          if (!cyclic(k, i, k1)) {
+            break;
+          }
+        }
+        if (foundk === 0) {
+          dk.x = sign(pt[k].x - pt[k1].x);
+          dk.y = sign(pt[k].y - pt[k1].y);
+          cur.x = pt[k1].x - pt[i].x;
+          cur.y = pt[k1].y - pt[i].y;
+
+          a = xprod(constraint[0], cur);
+          b = xprod(constraint[0], dk);
+          c = xprod(constraint[1], cur);
+          d = xprod(constraint[1], dk);
+
+          j = 10000000;
+          if (b < 0) {
+            j = Math.floor(a / -b);
+          }
+          if (d > 0) {
+            j = Math.min(j, Math.floor(-c / d));
+          }
+          pivk[i] = mod(k1 + j, n);
+        }
+      }
+
+      j = pivk[n - 1];
+      seg.lon[n - 1] = j;
+      for (i = n - 2; i >= 0; i--) {
+        if (cyclic(i + 1, pivk[i], j)) {
+          j = pivk[i];
+        }
+        seg.lon[i] = j;
+      }
+
+      for (i = n - 1; cyclic(mod(i + 1, n), j, seg.lon[i]); i--) {
+        seg.lon[i] = j;
+      }
+    }
+
+
+    function bestPolygon(seg) {
+
+      function penalty3(seg, i, j) {
+
+        let n = seg.pt.length, pt = seg.pt, sums = seg.sums;
+        let x, y, xy, x2, y2,
+          k, a, b, c, s,
+          px, py, ex, ey,
+          r = 0;
+        if (j >= n) {
+          j -= n;
+          r = 1;
+        }
+
+        if (r === 0) {
+          x = sums[j + 1].x - sums[i].x;
+          y = sums[j + 1].y - sums[i].y;
+          x2 = sums[j + 1].x2 - sums[i].x2;
+          xy = sums[j + 1].xy - sums[i].xy;
+          y2 = sums[j + 1].y2 - sums[i].y2;
+          k = j + 1 - i;
+        } else {
+          x = sums[j + 1].x - sums[i].x + sums[n].x;
+          y = sums[j + 1].y - sums[i].y + sums[n].y;
+          x2 = sums[j + 1].x2 - sums[i].x2 + sums[n].x2;
+          xy = sums[j + 1].xy - sums[i].xy + sums[n].xy;
+          y2 = sums[j + 1].y2 - sums[i].y2 + sums[n].y2;
+          k = j + 1 - i + n;
+        }
+
+        px = (pt[i].x + pt[j].x) / 2.0 - pt[0].x;
+        py = (pt[i].y + pt[j].y) / 2.0 - pt[0].y;
+        ey = (pt[j].x - pt[i].x);
+        ex = -(pt[j].y - pt[i].y);
+
+        a = ((x2 - 2 * x * px) / k + px * px);
+        b = ((xy - x * py - y * px) / k + px * py);
+        c = ((y2 - 2 * y * py) / k + py * py);
+
+        s = ex * ex * a + 2 * ex * ey * b + ey * ey * c;
+
+        return Math.sqrt(s);
+      }
+
+      let i, j, m, k,
+        n = seg.pt.length,
+        pen = new Array(n + 1),
+        prev = new Array(n + 1),
+        clip0 = new Array(n),
+        clip1 = new Array(n + 1),
+        seg0 = new Array(n + 1),
+        seg1 = new Array(n + 1),
+        thispen, best, c;
+
+      for (i = 0; i < n; i++) {
+        c = mod(seg.lon[mod(i - 1, n)] - 1, n);
+        if (c == i) {
+          c = mod(i + 1, n);
+        }
+        if (c < i) {
+          clip0[i] = n;
+        } else {
+          clip0[i] = c;
+        }
+      }
+
+      j = 1;
+      for (i = 0; i < n; i++) {
+        while (j <= clip0[i]) {
+          clip1[j] = i;
+          j++;
+        }
+      }
+
+      i = 0;
+      for (j = 0; i < n; j++) {
+        seg0[j] = i;
+        i = clip0[i];
+      }
+      seg0[j] = n;
+      m = j;
+
+      i = n;
+      for (j = m; j > 0; j--) {
+        seg1[j] = i;
+        i = clip1[i];
+      }
+      seg1[0] = 0;
+
+      pen[0] = 0;
+      for (j = 1; j <= m; j++) {
+        for (i = seg1[j]; i <= seg0[j]; i++) {
+          best = -1;
+          for (k = seg0[j - 1]; k >= clip1[i]; k--) {
+            thispen = penalty3(seg, k, i) + pen[k];
+            if (best < 0 || thispen < best) {
+              prev[i] = k;
+              best = thispen;
+            }
+          }
+          pen[i] = best;
+        }
+      }
+      seg.m = m;
+      seg.po = new Array(m);
+
+      for (i = n, j = m - 1; i > 0; j--) {
+        i = prev[i];
+        seg.po[j] = i;
+      }
+
+    }
+    
+    
+
+    function adjustVertices(seg) {
+
+      function pointslope(seg, i, j, ctr, dir) {
+
+        let n = seg.pt.length, sums = seg.sums,
+          x, y, x2, xy, y2,
+          k, a, b, c, lambda2, l, r = 0;
+
+        while (j >= n) {
+          j -= n;
+          r += 1;
+        }
+        while (i >= n) {
+          i -= n;
+          r -= 1;
+        }
+        while (j < 0) {
+          j += n;
+          r -= 1;
+        }
+        while (i < 0) {
+          i += n;
+          r += 1;
+        }
+
+        x = sums[j + 1].x - sums[i].x + r * sums[n].x;
+        y = sums[j + 1].y - sums[i].y + r * sums[n].y;
+        x2 = sums[j + 1].x2 - sums[i].x2 + r * sums[n].x2;
+        xy = sums[j + 1].xy - sums[i].xy + r * sums[n].xy;
+        y2 = sums[j + 1].y2 - sums[i].y2 + r * sums[n].y2;
+        k = j + 1 - i + r * n;
+
+        ctr.x = x / k;
+        ctr.y = y / k;
+
+        a = (x2 - x * x / k) / k;
+        b = (xy - x * y / k) / k;
+        c = (y2 - y * y / k) / k;
+
+        lambda2 = (a + c + Math.sqrt((a - c) * (a - c) + 4 * b * b)) / 2;
+
+        a -= lambda2;
+        c -= lambda2;
+
+        if (Math.abs(a) >= Math.abs(c)) {
+          l = Math.sqrt(a * a + b * b);
+          if (l !== 0) {
+            dir.x = -b / l;
+            dir.y = a / l;
+          }
+        } else {
+          l = Math.sqrt(c * c + b * b);
+          if (l !== 0) {
+            dir.x = -c / l;
+            dir.y = b / l;
+          }
+        }
+        if (l === 0) {
+          dir.x = dir.y = 0;
+        }
+      }
+
+      let m = seg.m, po = seg.po, n = seg.pt.length, pt = seg.pt,
+        x0 = seg.endA.x, y0 = seg.endA.y,
+        ctr = new Array(m), dir = new Array(m),
+        q = new Array(m),
+        v = new Array(3), d, i, j, k, l,
+        s = { x: 0, y: 0 };
+      
+      seg.curve = new Curve(m);
+
+      for (i = 0; i < m; i++) {
+        j = po[mod(i + 1, m)];
+        j = mod(j - po[i], n) + po[i];
+        ctr[i] = { x: 0, y: 0 };
+        dir[i] = { x: 0, y: 0 };
+        pointslope(seg, po[i], j, ctr[i], dir[i]);
+      }
+
+      for (i = 0; i < m; i++) {
+        q[i] = new Quad();
+        d = dir[i].x * dir[i].x + dir[i].y * dir[i].y;
+        if (d === 0.0) {
+          for (j = 0; j < 3; j++) {
+            for (k = 0; k < 3; k++) {
+              q[i].data[j * 3 + k] = 0;
+            }
+          }
+        } else {
+          v[0] = dir[i].y;
+          v[1] = -dir[i].x;
+          v[2] = - v[1] * ctr[i].y - v[0] * ctr[i].x;
+          for (l = 0; l < 3; l++) {
+            for (k = 0; k < 3; k++) {
+              q[i].data[l * 3 + k] = v[l] * v[k] / d;
+            }
+          }
+        }
+      }
+
+      var Q, w, dx, dy, det, min, cand, xmin, ymin, z;
+      for (i = 0; i < m; i++) {
+        Q = new Quad();
+        w = { x: 0, y : 0 };
+
+        s.x = pt[po[i]].x - x0;
+        s.y = pt[po[i]].y - y0;
+
+        j = mod(i - 1, m);
+
+        for (l = 0; l < 3; l++) {
+          for (k = 0; k < 3; k++) {
+            Q.data[l * 3 + k] = q[j].at(l, k) + q[i].at(l, k);
+          }
+        }
+
+        while (1) {
+
+          det = Q.at(0, 0) * Q.at(1, 1) - Q.at(0, 1) * Q.at(1, 0);
+          if (det !== 0.0) {
+            w.x = (-Q.at(0, 2) * Q.at(1, 1) + Q.at(1, 2) * Q.at(0, 1)) / det;
+            w.y = (Q.at(0, 2) * Q.at(1, 0) - Q.at(1, 2) * Q.at(0, 0)) / det;
+            break;
+          }
+
+          if (Q.at(0, 0) > Q.at(1, 1)) {
+            v[0] = -Q.at(0, 1);
+            v[1] = Q.at(0, 0);
+          } else if (Q.at(1, 1)) {
+            v[0] = -Q.at(1, 1);
+            v[1] = Q.at(1, 0);
+          } else {
+            v[0] = 1;
+            v[1] = 0;
+          }
+          d = v[0] * v[0] + v[1] * v[1];
+          v[2] = - v[1] * s.y - v[0] * s.x;
+          for (l = 0; l < 3; l++) {
+            for (k = 0; k < 3; k++) {
+              Q.data[l * 3 + k] += v[l] * v[k] / d;
+            }
+          }
+        }
+        dx = Math.abs(w.x - s.x);
+        dy = Math.abs(w.y - s.y);
+        if (dx <= 0.5 && dy <= 0.5) {
+          seg.curve.vertex[i] = { x: w.x + x0, y: w.y + y0 };
+          continue;
+        }
+
+        min = quadform(Q, s);
+        xmin = s.x;
+        ymin = s.y;
+
+        if (Q.at(0, 0) !== 0.0) {
+          for (z = 0; z < 2; z++) {
+            w.y = s.y - 0.5 + z;
+            w.x = - (Q.at(0, 1) * w.y + Q.at(0, 2)) / Q.at(0, 0);
+            dx = Math.abs(w.x - s.x);
+            cand = quadform(Q, w);
+            if (dx <= 0.5 && cand < min) {
+              min = cand;
+              xmin = w.x;
+              ymin = w.y;
+            }
+          }
+        }
+
+        if (Q.at(1, 1) !== 0.0) {
+          for (z = 0; z < 2; z++) {
+            w.x = s.x - 0.5 + z;
+            w.y = - (Q.at(1, 0) * w.x + Q.at(1, 2)) / Q.at(1, 1);
+            dy = Math.abs(w.y - s.y);
+            cand = quadform(Q, w);
+            if (dy <= 0.5 && cand < min) {
+              min = cand;
+              xmin = w.x;
+              ymin = w.y;
+            }
+          }
+        }
+
+        for (l = 0; l < 2; l++) {
+          for (k = 0; k < 2; k++) {
+            w.x = s.x - 0.5 + l;
+            w.y = s.y - 0.5 + k;
+            cand = quadform(Q, w);
+            if (cand < min) {
+              min = cand;
+              xmin = w.x;
+              ymin = w.y;
+            }
+          }
+        }
+        seg.curve.vertex[i] = { x: xmin + x0, y: ymin + y0 };
+      }
+    }
+
+    function smooth(path, alphamax = 1) {
+      let m = path.curve.vertex.length;
+      let curve = new Curve(m); // curve 信息重新洗过
+      curve.vertex = path.curve.vertex;
+      path.curve = curve;
+
+      let i, j, k, dd, denom, alpha,
+        p2, p3, p4;
+
+      for (i = 0; i < m; i++) {
+        j = mod(i + 1, m);
+        k = mod(i + 2, m);
+        p4 = interval(1 / 2.0, curve.vertex[k], curve.vertex[j]);
+
+        denom = ddenom(curve.vertex[i], curve.vertex[k]);
+        if (denom !== 0.0) {
+          dd = dpara(curve.vertex[i], curve.vertex[j], curve.vertex[k]) / denom;
+          dd = Math.abs(dd);
+          alpha = dd > 1 ? (1 - 1.0 / dd) : 0;
+          alpha = alpha / 0.75;
+        } else {
+          alpha = 4 / 3.0;
+        }
+
+        if (alpha >= alphamax) {
+          curve.tag[j] = "CORNER";
+          curve.c[3 * j + 1] = curve.vertex[j];
+          curve.c[3 * j + 2] = p4;
+        } else {
+          if (alpha < 0.55) {
+            alpha = 0.55;
+          } else if (alpha > 1) {
+            alpha = 1;
+          }
+          p2 = interval(0.5 + 0.5 * alpha, curve.vertex[i], curve.vertex[j]);
+          p3 = interval(0.5 + 0.5 * alpha, curve.vertex[k], curve.vertex[j]);
+          curve.tag[j] = "CURVE";
+          curve.c[3 * j + 0] = p2;
+          curve.c[3 * j + 1] = p3;
+          curve.c[3 * j + 2] = p4;
+        }
+      }
+
+    }
+
+
+    function reversePath(path) {
+      // reverse smooth curve
+      let n = path.curve.n, tag0 = path.curve.tag, c0 = path.curve.c;
+      let tag = new Array(n), c = new Array(n * 3);
+      let i, j;
+
+      for (i = 0, j = n - 1; i < n; i++, j--) {
+        tag[i] = tag0[j];
+        if (tag[i] == 'CORNER') {
+          c[i * 3 + 1] = c0[j * 3 + 1];
+          c[i * 3 + 2] = c0[j * 3 - 1 < 0 ? n * 3 - 1 : j * 3 - 1];
+        } else if (tag[i] == 'CURVE') {
+          c[i * 3 + 0] = c0[j * 3 + 1];
+          c[i * 3 + 1] = c0[j * 3 + 0];
+          c[i * 3 + 2] = c0[j * 3 - 1 < 0 ? n * 3 - 1 : j * 3 - 1];
+        }
+      }
+
+      path.curve.tag = tag;
+      path.curve.c = c;
+
+      // reverse line curve
+      path.line.reverse();
+
+    }
+
+    // 对开放segment的曲线化
+    function smoothSegment(seg, reverse = false, alphamax = 1) {
+
+      let i, j, k, dd, denom, alpha,
+      s, e,
+      p0, pn, p2, p3, p4,
+      m, curve, vertex, tag, c;
+
+      m = seg.curve.vertex.length, curve = seg.curve;
+
+      if (!reverse) {
+        s = { x: seg.endA.x, y: seg.endA.y };
+        e = { x: seg.endB.x, y: seg.endB.y };
+        vertex = curve.vertex.map(e => e);
+      } else {
+        s = { x: seg.endB.x, y: seg.endB.y };
+        e = { x: seg.endA.x, y: seg.endA.y };
+        vertex = curve.vertex.map(e => e).reverse();
+      }
+      p0 = interval(1 / 2.0, vertex[0], s);
+      pn = interval(1 / 2.0, e, vertex[m - 1]);
+    
+      tag = new Array(m + 2);
+      c = new Array( (m + 2) * 3);
+
+      vertex.unshift(s);
+      vertex.push(e);
+
+      for (i = 0; i < m; i++) {
+        j = i + 1;
+        k = i + 2;
+        p4 = interval(1 / 2.0, vertex[k], vertex[j]);
+
+        denom = ddenom(vertex[i], vertex[k]);
+        if (denom !== 0.0) {
+          dd = dpara(vertex[i], vertex[j], vertex[k]) / denom;
+          dd = Math.abs(dd);
+          alpha = dd > 1 ? (1 - 1.0 / dd) : 0;
+          alpha = alpha / 0.75;
+        } else {
+          alpha = 4 / 3.0;
+        }
+
+        if (alpha >= alphamax) {
+          tag[j] = "CORNER";
+          c[3 * j + 1] = vertex[j];
+          c[3 * j + 2] = p4;
+        } else {
+          if (alpha < 0.55) {
+            alpha = 0.55;
+          } else if (alpha > 1) {
+            alpha = 1;
+          }
+          p2 = interval(0.5 + 0.5 * alpha, vertex[i], vertex[j]);
+          p3 = interval(0.5 + 0.5 * alpha, vertex[k], vertex[j]);
+          tag[j] = "CURVE";
+          c[3 * j + 0] = p2;
+          c[3 * j + 1] = p3;
+          c[3 * j + 2] = p4;
+        }
+      }
+
+
+      tag[0] = "CORNER";
+      c[1] = vertex[0];
+      c[2] = p0;
+
+      tag[m + 1] = "CORNER";
+      c[ (m + 1) * 3 + 1] = pn;
+      c[ (m + 1) * 3 + 2] = vertex[vertex.length - 1];
+      
+      curve.n = tag.length;
+      curve.tag = tag;
+      curve.c = c;
+    }
+
+    // 构建path的拟合多边形,由path下属的segments的po合并而成
+    function buildPathPolygon(path) {
+
+      // 连线前每个segment把两个端点加上
+      function fillEndpoint(segments) {
+        for (let seg of segments) {
+          if(!equal(seg.pt[seg.po[0]], seg.endA)) {
+            seg.po.unshift(0);
+          }
+          if (!equal(seg.pt[seg.po[seg.po.length - 1]], seg.endB)) {
+            seg.po.push(seg.pt.length - 1);
+          }
+        }
+      }
+
+      fillEndpoint(path.segments);
+
+      let seg0 = path.segments[0];
+      let po = seg0.po.map(idx => { return {x: seg0.pt[idx].x, y: seg0.pt[idx].y} });
+      let next = seg0.endB;
+
+      if (!equal(path.s, seg0.endA) && equal(path.s, seg0.endB)) {
+        po.reverse();
+        next = seg0.endA;
+      }
+
+      for (let i = 1; i < path.segments.length; i++) {
+        let seg = path.segments[i];
+        let ppo = seg.po.map(idx => { return {x: seg.pt[idx].x, y: seg.pt[idx].y} });
+        if (equal(next, seg.endA)) {  // 正序
+          if (equal(ppo[0], po[po.length - 1])) {
+            po.pop();
+          }
+          po = po.concat(ppo);
+          next = seg.endB;
+        } else if (equal(next, seg.endB)){ // 反序,需要reverse
+          ppo.reverse();
+          if (equal(ppo[0], po[po.length - 1])) {
+            po.pop();
+          }
+          po = po.concat(ppo);
+          next = seg.endA;
+        } else {  // 以上都不是,说明是断连强行merge拼接起来的,需要进一步判断
+          let da = ddist(next, seg.endA);
+          let db = ddist(next, seg.endB);
+          if (da <= db) {
+            if (equal(ppo[0], po[po.length - 1])) {
+              po.pop();
+            }
+            po = po.concat(ppo);
+            next = seg.endB;
+      } else {
+            ppo.reverse();
+            if (equal(ppo[0], po[po.length - 1])) {
+              po.pop();
+        }
+            po = po.concat(ppo);
+            next = seg.endA;
+          }
+        }
+      }
+
+      if (equal(po[0], po[po.length - 1])) {
+        po.pop()
+      }
+
+      path.polygon = new Polygon(po);
+      path.sign = path.polygon.dir();
+      // if (path.polygon.isIntersect()) {
+      //   self.debugInfo.selfIntersectCount++;
+      // }
+      for (let p of po) {
+        let idx = Math.round(p.y) * self.width + Math.round(p.x);
+        self.poSet.add(idx);
+      }
+
+    }
+
+    function isPathValid(path: Path) {
+      if (!path.polygon.isValid() || path.sign == 0) {
+        return false;
+      } else {
+        return true;
+      }
+      }
+
+    // 连线方式拟合path
+    function linePath(path) {
+      path.line = path.polygon.po.slice();
+    }
+
+    // bezier曲线化path
+    function curvePath(path, alphamax) {
+
+      function mergeSegmentCurves(path) {
+        let segments = path.segments;
+        let n = 0;
+        for (let seg of segments) {
+          n += seg.curve.n;
+        }
+        path.curve = new Curve(n);
+  
+        let index = 0;
+  
+        // 合并tag信息
+        for (let seg of segments) {
+          for (let i = 0; i < seg.curve.n; i++) {
+            path.curve.tag[i + index] = seg.curve.tag[i];
+          }
+          index += seg.curve.n;
+        }
+  
+        // 合并c信息
+        index = 0;
+        for (let seg of segments) {
+          for (let i = 0; i < 3 * seg.curve.n; i++) {
+            path.curve.c[i + index] = seg.curve.c[i];
+          }
+          index += seg.curve.n * 3;
+        }
+  
+      }
+
+
+      // 先对path下的所有segment分别进行曲线化
+      if (path.segments.length == 1) {
+        if (path.complete) {
+          smooth(path.segments[0], alphamax);
+        } else {
+          smoothSegment(path.segments[0], false, alphamax);
+        }
+      } else {
+        let startend = path.s;
+        for (let seg of path.segments) {
+          if (equal(startend, seg.endA)) {  // 正序
+            smoothSegment(seg, false, alphamax);
+            startend = seg.endB;
+          } else if (equal(startend, seg.endB)){ // 反序,需要reverse
+            smoothSegment(seg, true, alphamax);
+            startend = seg.endA;
+          } else {  // 以上都不是,说明是断连强行merge拼接起来的,需要进一步判断
+            let da = ddist(startend, seg.endA);
+            let db = ddist(startend, seg.endB);
+            if (da <= db) {
+              smoothSegment(seg, false, alphamax);
+              startend = seg.endB;
+            } else {
+              smoothSegment(seg, true, alphamax);
+              startend = seg.endA;
+            }
+          }
+        }
+      }
+      // 然后merge一个统一结果到path
+      mergeSegmentCurves(path);
+
+    }
+
+    // 调整方向
+    function doReverse(pathlist: Array<Path>) {
+      if (!pathlist || pathlist.length <= 0) {
+        console.warn("pathlist is empty!");
+        return;
+      }
+
+      let path0 = pathlist[0];
+      if (path0.sign == -1) { // 第一个强制为逆时针
+        reversePath(path0);
+        path0.sign = 1;
+        // console.log('reverse path', path0, pathlist);
+      }
+
+      let n = pathlist.length;
+      let i, j;
+
+      for (i = 1; i < n; i++) {
+        for (j = i - 1; j >= 0; j--) {
+          if (pathlist[j].polygon.contains(pathlist[i].polygon)) { // find a parent path contain this path
+            break;
+          }
+        }
+        if (j >= 0) { // 找到,包含它的parent,如果sign相等,则reverse
+          if (pathlist[i].sign == pathlist[j].sign) {
+            reversePath(pathlist[i]);
+            pathlist[i].sign = pathlist[i].sign == 1 ? -1 : 1;
+            // console.log('reverse path', pathlist[i], pathlist);
+          }
+        } else { // 没找到包含它的parent,说明是独立path,强制为逆时针
+          if (pathlist[i].sign == -1) {
+            reversePath(pathlist[i]);
+            pathlist[i].sign = 1;
+            // console.log('reverse path', pathlist[i], pathlist);
+          }
+        }
+      }
+    }
+
+
+    let self = this;
+
+    for (let seg of this.segments) {
+      calcSums(seg);
+      calcLon(seg);
+      bestPolygon(seg);  // 得到po,即多边形的顶点,依然是整形坐标的点,即原pt里的点的下标
+      adjustVertices(seg);  // 对顶点进行微调,调整后的顶点放在curve.vertex数据结构中,变成小数
+    }
+
+    let keys = Object.keys(this.pathMap);
+    for (let key of keys) {
+      let pathlist = this.pathMap[key];
+      for (let i = 0; i < pathlist.length; i++) {
+        let path = pathlist[i];
+        buildPathPolygon(path);
+        if (!isPathValid(path)) {
+          console.warn(`found a invalid path, po.length=${path.polygon.po.length} sign=${path.sign}, remove!`, path);
+          pathlist.splice(i, 1);
+          i--;
+          this.debugInfo.remvoeInvalidPaths++;
+          continue;
+        }
+        linePath(path);
+        curvePath(path, this.config.alphamax);
+      }
+      doReverse(pathlist);
+    }
+  }
+  
+  
+  getSVG(area: number, opt_type: string = null, pathOnly : boolean = false, color: string = 'black', mode: string = 'curve') {
+
+    function path(curve) {
+
+      function bezier(i) {
+        let b = 'C' + parseFloat((curve.c[i * 3 + 0].x).toFixed(3)) + ' ' +
+          parseFloat((curve.c[i * 3 + 0].y).toFixed(3)) + ',';
+        b += parseFloat((curve.c[i * 3 + 1].x).toFixed(3)) + ' ' +
+          parseFloat((curve.c[i * 3 + 1].y).toFixed(3)) + ',';
+        b += parseFloat((curve.c[i * 3 + 2].x).toFixed(3)) + ' ' +
+          parseFloat((curve.c[i * 3 + 2].y).toFixed(3)) + '';
+        return b;
+      }
+
+      function segment(i) {
+        let s = 'L' + parseFloat((curve.c[i * 3 + 1].x).toFixed(3)) + ' ' +
+          parseFloat((curve.c[i * 3 + 1].y).toFixed(3)) + ' ';
+        s += parseFloat((curve.c[i * 3 + 2].x).toFixed(3)) + ' ' +
+          parseFloat((curve.c[i * 3 + 2].y).toFixed(3)) + '';
+        return s;
+      }
+
+      let n = curve.n, i;
+      let p = 'M' + (curve.c[(n - 1) * 3 + 2].x).toFixed(3) + ' ' + (curve.c[(n - 1) * 3 + 2].y).toFixed(3) + '';
+      for (i = 0; i < n; i++) {
+        if (curve.tag[i] === "CURVE") {
+          p += bezier(i);
+        } else if (curve.tag[i] === "CORNER") {
+          p += segment(i);
+        }
+      }
+      return p;
+
+    }
+
+    function line(po) {
+      let p = 'M' + parseFloat(po[0].x.toFixed(3)) + ' ' + parseFloat(po[0].y.toFixed(3)) + 'L';
+      for (let i = 1; i < po.length; i++) {
+        p += parseFloat(po[i].x.toFixed(3)) + ' ' + parseFloat(po[i].y.toFixed(3)) + ' ';
+      }
+      p += 'Z'
+      return p;
+    }
+
+    
+    let w = this.width, h = this.height;
+    let pathlist = this.pathMap[area];
+    let len = pathlist.length;
+    let c, i, strokec, fillc, fillrule;
+
+    let pathStr = '';
+    if (opt_type === "curve") {
+      strokec = color ||  "black";
+      fillc = "none";
+      fillrule = '';
+    } else {
+      strokec = "none";
+      // strokec = color || "none";
+      fillc = color || "black";
+      fillrule = ' fill-rule="evenodd"';
+    }
+
+    pathStr += '<path stroke="' + strokec + '" fill="' + fillc + '"' + fillrule;
+
+    pathStr += ' d="';
+
+    if (mode == 'line') {
+      for (let p of pathlist) {
+        pathStr += line(p.line);
+      }
+    } else {
+      for (let p of pathlist) {
+        c = p.curve;
+        if (!c || !c.n ) {
+          console.warn("invalid curve ", pathlist[i]);
+          continue;
+        }
+        pathStr += path(c);
+      }
+    }
+    
+
+    pathStr += '"/>';
+
+
+    let svg = `<svg id="svg" version="1.1" width="${w}" height="${h}" xmlns="http://www.w3.org/2000/svg">${pathStr}</svg>`;
+
+    return pathOnly ? pathStr : svg;
+
+  }
+
+
+  getDebugInfo() {
+    return this.debugInfo;
+  }
+
+
+}

+ 2439 - 0
zorro/src/app/lib/filler/common/etrace2.ts

@@ -0,0 +1,2439 @@
+import Polygon from "./polygon";
+
+interface Debug {
+  bulgeCount: number;  // 异常凸起点个数
+  createSegmentFailed: number; // 创建segment出错数
+  spCount: number; // 特殊端点个数(找不到邻居的segment端点)
+  spCountAfterOrderAdjust: number; // 经过单调纠偏后剩余的特殊端点数
+  spCountAfterInsertAdjust: number; // 经过插值纠偏后剩余的特殊端点数
+  totalIncompletePaths: number; // 非闭合的路径个数
+  doesntMatterIncompletePaths: number; // 无所谓的非闭合路径个数
+  realIncompletePaths: number;
+  removeIncompletePaths: number; // 删除掉的孤立非闭合路径个数
+  remvoeInvalidPaths: number; // 非法path的个数
+  clipAdjustCount: number; // clip切割点调整个数
+  selfIntersectCount: number; // 自相交多边形数量
+}
+
+interface Point {
+  x: number;
+  y: number;
+  color?: number;
+  a?: Point;
+  b?: Point;
+  neighbors?: Array<Point>;
+  traced?: boolean;
+  id?: string;
+  adjusted?: boolean;
+  bulge?: boolean;
+  x0?: number;
+  y0?: number;
+}
+
+interface Segment {
+  endA : Point;
+  endB : Point; 
+  pt : Array<Point>;
+  areaA : number;
+  areaB :  number; 
+  curve : Curve;
+  po: number[];
+  m: number;
+  invalid?: boolean;
+}
+
+interface Path {
+  segments: Array<Segment>;  // 一个path由多个segment构成
+  complete?: boolean; // 是否完整
+  s?: Point;  // path的起点
+  e?: Point;  // path的终点
+  curve?: Curve;   // bezier curve 信息, 由各个子segment的curve信息合成
+  line?: Array<Point>; // 存放直接连线的顶点,即多边形顶点,基本同polygon,只不过可能经过reverse了
+  polygon?: Polygon; // path对应的拟合多边形,有各个segment合并而成
+  sign?: number;  // 1: 正曲线(逆时针);-1: 负曲线(顺时针);按一般惯例,大圆包小圆的场景,大圆为正(逆时针),小圆为负(顺时针)
+}
+
+interface PathMap {
+  [key: number]: Array<Path> // key即区块id
+}
+
+// 两个区块相交点集
+interface IntersectMap {
+  [key: string]: Set<string>  // key的组成: color1 + '-' + color2,  集合中的元素其实是点Point,用x-y字符串表示
+}
+
+class Curve {
+  n: number;
+  // tag是CORNER或CURVE类型标志,分解出来的每一段拟合曲线,有CURVE和CORNER两种,
+  // CURVE表示贝塞尔曲线(a, u, w, b),相应的c将存放u, w, b 3个点
+  // CORNER表示是直线折角(a, v, b),相应的c将存放v, b 2个点
+  tag: any[];
+  // c是控制点,tag 和 c 配合,长度是tag的3倍,因为一个tag对应3个点,
+  // 如果是CURVE, 那么c[i][0]和c[i][1]分别对应 u 和 w 控制点,c[i][2]对应b
+  // 如果CORNER,那么c[i][0]没用,c[i][1]表示控制点即两条线段交点 v,c[i][2]表示b点
+  //(注:起始点 a 不需要存储,每一段的起始点 a 起始就是上一段的结束点 b, 第一个起始点 a 是末尾那段的结束点 b
+  // 但这里有个问题, 这是针对闭合路径来说的,如果是开放路径, 这里是有问题的,也是要改造调整的地方); 
+  c: any[]; 
+  vertex: any[];  // 调整后的顶点,小数点坐标点
+
+  s: Point;  // 起点
+  e: Point;  // 终点
+
+  constructor(n) {
+    this.n = n;
+    this.tag = new Array(n);
+    this.c = new Array(n * 3);
+    this.vertex = new Array(n);
+  }
+}
+
+export interface EtraceOption {
+  alphamax?: number;
+}
+
+export class ETrace {
+  width: number;
+  height: number;
+  mapPixels: Uint32Array;
+  intersectMap: IntersectMap = {};
+  segments: Segment[] = [];
+  pathMap: PathMap = {};
+
+  config: EtraceOption = {
+    alphamax: 1,
+  };
+
+  // for debug
+  hContour: Set<number> = new Set();
+  vContour: Set<number> = new Set();
+  poSet: Set<number> = new Set();
+
+  debugInfo: Debug;
+
+
+  constructor(mapPixels: Uint32Array, width: number, height: number) {
+    // this.mapPixels = mapPixels;
+    // this.width = width;
+    // this.height = height;
+
+    let result = this.expandMap(mapPixels, width, height);
+    this.mapPixels = result.newMapPixels;
+    this.width = result.newWidth;
+    this.height = result.newHeight;
+
+    this.debugInfo = { 
+      bulgeCount: 0, 
+      createSegmentFailed: 0,
+      spCount: 0, 
+      spCountAfterOrderAdjust: 0, 
+      spCountAfterInsertAdjust: 0, 
+      totalIncompletePaths: 0, 
+      realIncompletePaths: 0,
+      doesntMatterIncompletePaths: 0,
+      removeIncompletePaths: 0, 
+      remvoeInvalidPaths: 0,
+      clipAdjustCount: 0,
+      selfIntersectCount: 0,
+    };
+
+    this.pureMap();
+    this.contourScan();
+    this.makeSegments();
+    this.mergeSegmentEndpoint();
+    this.segmentsToPaths();
+    this.curvedPaths();
+
+    console.log(this.debugInfo);
+  }
+
+  /**
+   * map图扩展一个像素,相当于右下各加一条边,解决右下两边未填充完全容易出现缝隙的问题
+   * @param mapPixels 原map图
+   * @param width 原map图宽度
+   * @param height 原map图高度
+   * @returns 
+   */
+  private expandMap(mapPixels: Uint32Array, width: number, height: number) {
+    // 向右下两边扩展一个像素
+    let newWidth = width + 1;
+    let newHeight = height + 1;
+    let newMapPixels = new Uint32Array(newWidth * newHeight);
+    let x, y, index, index0;
+
+    // 填充原map图已有的点
+    for (x = 0; x < width; x++) {
+      for (y = 0; y < height; y++) {
+        index = y * newWidth + x;
+        index0 = y * width + x;
+        newMapPixels[index] = mapPixels[index0];
+      }
+    }
+
+    // 最右侧的一条新边(其新值等于左侧邻点)
+    x = width;
+    for (y = 0; y < height; y++) {
+      index = y * newWidth + x;
+      index0 = y * width + (x - 1);
+      newMapPixels[index] = mapPixels[index0];
+    }
+
+    // 最下侧的一条新边(其新值等于上侧邻点)
+    y = height;
+    for (x = 0; x < width; x++) {
+      index = x * newHeight + y;
+      index0 = x * height + (y - 1);
+      newMapPixels[index] = mapPixels[index0];
+    }
+
+    // 右下最远端的那个新的点(其新值等于左上侧邻点)
+    index = height * newWidth + width;
+    index0 = (height - 1) * width + (width - 1);
+    newMapPixels[index] = mapPixels[index0];
+
+    return { newMapPixels, newWidth, newHeight };
+  }
+
+  // 对map图进行一轮净化 ,消灭封闭area内的孤点
+  private pureMap() {
+    let x, y, index, count, color;
+    let neibColors, pureNeibColors, changeColor;
+    let w = this.width;
+    let h = this.height;
+    let mapPixels = this.mapPixels;
+    let shift = [
+      { x: 0, y: -1 },
+      { x: 1, y: 0 }, 
+      { x: 0, y: 1 },
+      { x: -1, y: 0 },
+      { x: 1, y: -1 },
+      { x: 1, y: 1 },
+      { x: -1, y: 1 },
+      { x: -1, y: -1 },
+    ];
+
+    for (x = 0; x < w; x++) {
+      for (y = 0; y < h; y++) {
+        index = y * w + x;
+        color = mapPixels[index];
+        // 周边邻居点
+        neibColors = shift.map(s => mapPixels[(y + s.y) * w + (x + s.x)]).filter(c => c);
+        pureNeibColors = neibColors.filter((item, idx, arr) => arr.indexOf(item, 0) == idx);  // 去重
+
+        if (!pureNeibColors. find(c => c == color)) { // 没有邻居跟它的颜色一致
+          if (pureNeibColors.length > 1) {
+            pureNeibColors = pureNeibColors.map(c => {
+              count = 0;
+              for (let nc of neibColors) {
+                if (nc == c) {
+                  count++;
+                }
+              }
+              return { c, count };
+            });
+            pureNeibColors.sort((a, b) => b.count - a.count);
+            changeColor = pureNeibColors[0].c;
+          } else {
+            changeColor = pureNeibColors[0];
+          }
+          
+          mapPixels[index] = changeColor;
+        }
+      }
+    }
+  }
+
+  // 对map进行全边界扫描, 得到相交线集合
+  private contourScan() {
+    let mapPixels = this.mapPixels;
+    let intersectMap = this.intersectMap;
+    let chooseMap = {};
+    let w = this.width;
+    let h = this.height;
+    let x, y, color1, color2, index, index1, index2;
+
+    let hContour = this.hContour;
+    let vContour = this.vContour;
+
+    function indexToPoint(index) {
+      let y = Math.floor(index / w);
+      let x = index - y * w;
+      return { x, y };
+    }
+
+    function interval(i1, i2) {
+      let a = indexToPoint(i1);
+      let b = indexToPoint(i2);
+      let lambda = 1 / 2.0;
+      let res = { x: 0, y: 0 };
+
+      res.x = a.x + lambda * (b.x - a.x);
+      res.y = a.y + lambda * (b.y - a.y);
+      return res;
+    }
+
+    // 两个区块交界处,选择其一作为边缘点
+    function chooseEdgePoint(color1, color2, index1, index2) {
+      let index: number;
+      let color: number;
+      let key1 = color2 + '-' + color1;
+      let key2 = color1 + '-' + color2;
+      let value1 = chooseMap[key1];
+      let value2 = chooseMap[key2];
+      
+      if (value1) {
+        color = value1;
+      } else if (value2) {
+        color = value2;
+      } else {
+        color = color1;
+        chooseMap[key1] = color;
+      }
+
+      if (color == color1) index = index1;
+      else if (color == color2) index = index2;  
+      
+
+      return index;
+    }
+
+    function isBorder(x, y) {
+      return (x == 0 || y == 0 || x == w - 1  || y == h - 1);
+    }
+
+    function addBorder(point: Point, color: number) {
+      let key = color + '-';
+      if (!intersectMap[key]) {
+        intersectMap[key] = new Set();
+      }
+      intersectMap[key].add(`${point.x}-${point.y}`);
+    }
+
+    function addIntersect(point: Point, color1: number, color2: number) {
+      let key = color2 + '-' + color1;
+      if (!intersectMap[key]) {
+        key = color1 + '-' + color2;
+        if (!intersectMap[key]) {
+          intersectMap[key] = new Set();
+        }
+      }
+      intersectMap[key].add(`${point.x}-${point.y}`);
+    }
+
+    for (x = 0; x < w; x++) {
+      index1 = x;
+      color1 = mapPixels[index1];
+      for (y = 0; y < h; y++) {
+        index2 = y * w + x;
+        color2 = mapPixels[index2];
+        if (color1 != color2) { 
+          index = chooseEdgePoint(color1, color2, index1, index2);
+          addIntersect(interval(index1, index2), color1, color2);
+          color1 = color2;
+
+          hContour.add(index);
+        } 
+        if (isBorder(x, y)) {
+          addBorder({x, y}, color2);
+        }
+        index1 = index2;
+      }
+    }
+
+    for (y = 0; y < h; y++) {
+      index1 = y * w;
+      color1 = mapPixels[index1];
+      for (x = 0; x < w; x++) {
+        index2 = y * w + x;
+        color2 = mapPixels[index2];
+        if (color1 != color2) {
+          index = chooseEdgePoint(color1, color2, index1, index2);
+          addIntersect(interval(index1, index2), color1, color2);
+          color1 = color2;  
+
+          vContour.add(index);
+        } 
+        if (isBorder(x, y)) {
+          addBorder({x, y}, color2);
+        }
+        index1 = index2;
+      }
+    }
+  }
+
+
+  // 相交线进一步处理生成segments
+  private makeSegments() {
+
+    let self = this;
+    let intersectMap = this.intersectMap;
+    let keys = Object.keys(intersectMap);
+    for (let key of keys) {
+      let areas = key.split('-').map(e => +e); // 将key分解成两个area id
+      let pixels = Array.from(intersectMap[key]).map(e => { 
+        let p = e.split('-'); 
+        return { x: +p[0], y: +p[1] };
+      });
+      
+      let segs = pixelSort(pixels, areas[0], areas[1]);
+      segs.forEach(s => this.segments.push(s));
+    }
+
+    function newPoint(p: Point) {
+      return { x: p.x, y: p.y };
+    }
+
+
+    function pixelSort(pixels: Array<Point>, areaA: number, areaB: number): Segment[] {
+
+      // if (areaA == 4289262896 && areaB == 4292380759) {
+      //   console.log('pause');
+      //   for (let p of pixels) {
+      //     console.log(`{x: ${p.x}, y: ${p.y}},`);
+      //   }
+      // }
+      let segment;
+      let segments = [];
+      let pHash = {};
+      pixels.forEach(p => {
+        p.id = `${p.x}-${p.y}`
+        pHash[p.id] = p;
+      })
+  
+      let shift = [
+        { x: 0, y: -1 },
+        { x: 1, y: 0 },
+        { x: 0, y: 1 },
+        { x: -1, y: 0 },
+        // { x: 1, y: -1 },
+        // { x: 1, y: 1 },
+        // { x: -1, y: 1 },
+        // { x: -1, y: -1 },
+        { x: -0.5, y: -0.5 },
+        { x: 0.5, y: -0.5 },
+        { x: 0.5, y: 0.5 },
+        { x: -0.5, y: 0.5 },
+      ];
+  
+      pixels.forEach(p => {
+        p.neighbors = shift.map(s => `${p.x + s.x}-${p.y + s.y}`)
+          .map(id => pHash[id])
+          .filter(p => p);
+      })
+
+      // 邻居降序排列
+      pixels.forEach(p => {
+        p.neighbors.sort((a, b) => a.neighbors.length - b.neighbors.length)
+      })
+  
+      for (let i = 0; i < pixels.length; i++) {
+        let p = pixels[i];
+
+        if (p.neighbors.length == 0) { // 没有邻居的孤点,直接处理成segment
+          // console.log(`found single point(${p.x}, ${p.y}) in pixels. areaA=${areaA}, areaB=${areaB}`);
+          let seg = { endA: newPoint(p), endB: newPoint(p), pt: [newPoint(p)], areaA, areaB };
+          segments.push(seg);
+          pixels.splice(i, 1);
+          i--;
+          continue;
+        }
+
+        if (p.neighbors.length == 1) { // 只有一个邻居,可能是起止点或异常凸起点,如果是异常凸起点,需要干掉
+          let neib = p.neighbors[0];
+          let idx, left = false, right = false, up = false, down = false;
+          for (let j = 0; j < neib.neighbors.length; j++) {
+            let nb = neib.neighbors[j];
+            if (nb == p) {
+              idx = j;
+              continue;
+            }
+            if (nb.x < neib.x) left = true;
+            if (nb.x > neib.x) right = true;
+            if (nb.y > neib.y) down = true;
+            if (nb.y < neib.y) up = true;
+          }
+          if ((left && right) || (up && down)) {
+            console.warn(`found a bulge: (${p.x}, ${p.y}) in pixels. areaA=${areaA}, areaB=${areaB}`);
+            self.debugInfo.bulgeCount++;
+            p.bulge = true;
+            neib.neighbors.splice(idx, 1);
+            pixels.splice(i, 1);
+            i--;
+          }
+        }
+      }
+
+      // trace,让点集变成有序
+      do {
+        let index = pixels.findIndex(p => !p.traced);
+        if (index < 0) break;
+        let p = pixels[index];
+        trace(p);
+        // create segment
+        segment = createSegment(pixels, areaA, areaB);
+        if (segment) {
+          segments.push(segment);
+        }
+        pixels = pixels.filter(p => !p.traced);
+      } while (true);
+
+      return segments;
+  
+    }
+
+    function trace(p) {
+      let n;
+      let startP = p;
+      p.traced = true;
+      console.log('tracing');
+  
+      if (!p.a) {
+        do {
+          n = p.neighbors.find(pp => !pp.traced);
+          if (n) {
+            p.a = n;
+            n.b = p;
+            p = n;
+            p.traced = true;
+          } else {
+            break;
+          }
+        } while (1)
+      }
+  
+      p = startP;
+
+      if (!p.b) {
+        do {
+          n = p.neighbors.find(pp => !pp.traced);
+          if (n) {
+            p.b = n;
+            n.a = p;
+            p = n;
+            p.traced = true;
+          } else {
+            break;
+          }
+        } while(1)
+  
+      }
+    }
+
+    function createSegment(pixels: Point[], areaA, areaB) {
+      let segment;
+      let endA = pixels.find(e => e.a && !e.b);
+      let endB = pixels.find(e => !e.a && e.b);
+      let pt = [];
+
+
+      if (!endA || !endB) {
+        console.warn("create segment failed! pixel length = " + pixels.length);
+        self.debugInfo.createSegmentFailed++;
+        return null;
+      }
+
+      let p = endA;
+
+      do {
+        pt.push({ x: p.x, y: p.y });
+        p = p.a;
+        if (!p) break;
+      } while (true)
+
+      // check
+      if (endA.x != pt[0].x || endA.y != pt[0].y 
+        || endB.x != pt[pt.length - 1].x || endB.y != pt[pt.length - 1].y) {
+          console.error("something wrong!");
+      }
+
+      segment = { 
+        endA: newPoint(pt[0]), 
+        endB: newPoint(pt[pt.length - 1]),
+        pt, areaA, areaB 
+      };
+
+      return segment;
+
+    }
+  
+  }
+
+  // segment顶点调整,即距离相近的起始点,统一成同一个顶点
+  private mergeSegmentEndpoint() {
+
+    function sign(i) {
+      return i > 0 ? 1 : i < 0 ? -1 : 0;
+    }
+
+    function equal(pt1: Point, pt2: Point) {
+      return pt1.x == pt2.x && pt1.y == pt2.y;
+    }
+    
+    function include(pts: Point[], pt) {
+      return pts.findIndex(e => e.x == pt.x && e.y == pt.y) >= 0 ? true : false;
+    }
+
+    function ddist(p, q) {
+      return Math.sqrt((p.x - q.x) * (p.x - q.x) + (p.y - q.y) * (p.y - q.y));
+    }
+
+    function isSameSign(s1, s2) {
+      if (s1 == 0 || s2 == 0) return true;
+      if (s1 == s2) return true;
+      return false;
+    }
+
+    function isNeighbor(pt1, pt2) {
+      let distx = Math.abs(pt1.x - pt2.x);
+      let disty = Math.abs(pt1.y - pt2.y);
+
+      if (distx <= 1 && disty <= 1) return true;
+      return false;
+
+      // if (distx <= 1 && disty <= 1 && (distx + disty) <= 1) return true;
+      // return false;
+
+      // if (Math.abs(pt1.x - pt2.x) <= 1 && Math.abs(pt1.y - pt2.y) <= 1) return true;
+      // return false;
+    }
+
+    function exchange(list, idx1, idx2) {
+      let tmp = list[idx1];
+      list[idx1] = list[idx2];
+      list[idx2] = tmp;
+    }
+
+    /** 
+     * 对segment进行纠偏
+     * 所有的segment的端点应该能要能够找到与其衔接的其他segment端点,称为邻居segment,如果没有,那就是trace异常,需要纠偏
+     * 可能导致的情况有: 
+     * 1)segment端点前或后3个点非单调变化,这种调整下顺序或许就可以顺利找到邻居segment
+     * 2)循环打结的场景,这种情况其实有两个segment其端点非常接近,但并非紧挨着距离是1的邻居,导致不能成功配对。可以通过增加插值点来就解决
+     */
+    function adjustSpecialEndpoint(endpoints) {
+      
+      // 筛选出没有找到邻居的endpoint
+      let specialEndpoints = endpoints.filter(p => {
+        if (endpoints.find(pp => pp != p && isNeighbor(pp, p))) return false;
+        return true;
+      });
+
+      self.debugInfo.spCount = specialEndpoints.length;
+
+      console.warn(`需要纠偏的特殊点数:${specialEndpoints.length}`, specialEndpoints);
+
+      if (specialEndpoints.length == 0) {
+        console.log("无需纠偏");
+        return;
+      }
+
+      console.log('开始进行单调纠偏');
+      
+      // 对这些没有邻居的endpoint对应的segment进行单调变化纠偏
+      for (let sp of specialEndpoints) {
+        let seg = sp.seg;
+        if (seg.pt.length <= 2) continue;
+
+        if (equal(sp, seg.endA)) {  // 看头部
+          let dirx1 = sign(seg.pt[1].x - seg.pt[0].x);
+          let dirx2 = sign(seg.pt[2].x - seg.pt[1].x);
+          let diry1 = sign(seg.pt[1].y - seg.pt[0].y);
+          let diry2 = sign(seg.pt[2].y - seg.pt[1].y);
+          if (!isSameSign(dirx1, dirx2) || !isSameSign(diry1, diry2)) {
+            exchange(seg.pt, 0, 1);
+            seg.endA = { x: seg.pt[0].x, y: seg.pt[0].y };
+            sp.x = seg.endA.x;
+            sp.y = seg.endA.y;
+          }
+        } else if (equal(sp, seg.endB)) { // 看尾巴
+          let n = seg.pt.length;
+          let dirx1 = sign(seg.pt[n-2].x - seg.pt[n-3].x);
+          let dirx2 = sign(seg.pt[n-1].x - seg.pt[n-2].x);
+          let diry1 = sign(seg.pt[n-2].y - seg.pt[n-3].y);
+          let diry2 = sign(seg.pt[n-1].y - seg.pt[n-2].y);
+          if (!isSameSign(dirx1, dirx2) || !isSameSign(diry1, diry2)) {
+            exchange(seg.pt, n - 1, n - 2);
+            seg.endB = { x: seg.pt[n-1].x, y: seg.pt[n-1].y };
+            sp.x = seg.endB.x;
+            sp.y = seg.endB.y;
+          }
+        }
+      }
+
+      specialEndpoints = endpoints.filter(p => {
+        if (endpoints.find(pp => pp != p && isNeighbor(pp, p))) return false;
+        return true;
+      });
+
+      self.debugInfo.spCountAfterOrderAdjust = specialEndpoints.length;
+
+      if (specialEndpoints.length > 0) {
+        console.warn(`经过单调纠偏后,仍有 ${specialEndpoints.length} 个特殊点`, specialEndpoints);
+      } else {
+        console.log('单调纠偏后已不存在特殊点!');
+        return;
+      }
+
+      // 对异常偏离值是2的两个endpoint进行纠偏
+      let spA, spB, cp;
+      do {
+        spA = specialEndpoints.find(sp => !sp.done);
+        if (!spA) break;
+
+        spA.done = true;
+
+        // 找到靠近spA距离是2的对端spB
+        spB = specialEndpoints.find(sp => sp != spA && !sp.done && Math.abs(sp.x - spA.x) <= 2 && Math.abs(sp.y - spA.y) <= 2);
+        if (!spB) continue;
+
+        // 找到一个桥接点能够连接spA和spB的
+        cp = spA.seg.pt.find(p => isNeighbor(p, spA) && isNeighbor(p, spB));
+        if (cp) { // 在spA的segment中找到,则将cp添加到spB的segment中
+          if (equal(spB.seg.endA, spB)) {
+            spB.seg.pt.unshift({ x: cp.x, y: cp.y });
+            spB.seg.endA = { x: cp.x, y: cp.y };
+          } else if (equal(spB.seg.endB, spB)) {
+            spB.seg.pt.push({ x: cp.x, y: cp.y });
+            spB.seg.endB = { x: cp.x, y: cp.y };
+          }
+          spB.x = cp.x;
+          spB.y = cp.y;
+          spB.done = true;
+          continue;
+        }
+        cp = spB.seg.pt.find(p => isNeighbor(p, spA) && isNeighbor(p, spB));
+        if (cp) { // 在spB的segment中找到,则将cp添加到spA的segment中
+          if (equal(spA.seg.endA, spA)) {
+            spA.seg.pt.unshift({ x: cp.x, y: cp.y });
+            spA.seg.endA = { x: cp.x, y: cp.y };
+          } else if (equal(spA.seg.endB, spA)) {
+            spA.seg.pt.push({ x: cp.x, y: cp.y });
+            spA.seg.endB = { x: cp.x, y: cp.y };
+          }
+          spB.x = cp.x;
+          spB.y = cp.y;
+          spB.done = true;
+          continue;
+        }
+
+      } while(true)
+
+
+      specialEndpoints = endpoints.filter(p => {
+        if (endpoints.find(pp => pp != p && isNeighbor(pp, p))) return false;
+        return true;
+      });
+
+      self.debugInfo.spCountAfterInsertAdjust = specialEndpoints.length;
+
+      if (specialEndpoints.length > 0) {
+        console.warn(`经过插值纠偏后,仍有 ${specialEndpoints.length} 个特殊点`, specialEndpoints);
+      } else {
+        console.log('插值纠偏后已不存在特殊点!');
+        return;
+      }
+
+    }
+
+    // 递归寻邻近的endpoint,形成一个完整的group
+    function findNeighborEndpoint(group, endpoints) {
+      let groupEndpoints = group.endpoints;
+      let ninGruopEndpoints = endpoints.filter(s => !s.ingroup);
+      let foundEndpoint = null;
+
+      for (let point of ninGruopEndpoints) {
+        if (groupEndpoints.find(p => isNeighbor(p, point))) {
+          foundEndpoint = point;
+          break;
+        }
+      }
+      if (foundEndpoint) {
+        group.endpoints.push(foundEndpoint);
+        foundEndpoint.ingroup = true;
+        findNeighborEndpoint(group, endpoints);
+      }
+
+    }
+
+    // 在一个endpoint group中选出组长
+    function electGroupLeader(group) {
+
+      let i, j, pureEps, filterEps, dist;
+
+      pureEps = group.endpoints.filter((item, index, arr) => 
+        arr.findIndex(e => e.x == item.x && e.y == item.y) == index);
+
+      for (i = 0; i < pureEps.length; i++) {
+        dist = 0;
+        for (j = 0; j < pureEps.length; j++) {
+          if (i == j)  continue;
+          dist += ddist(pureEps[i], pureEps[j]);
+        }
+        pureEps[i].dist = dist;
+      }
+      pureEps.sort((a, b) => a.dist - b.dist);
+      filterEps = pureEps.slice(0, 2);
+
+      if(pureEps.length >= 2 && pureEps[0].dist == pureEps[1].dist) { // 头两个dist都相等,还要再决策下,谁出现的次数多就取谁
+        filterEps.forEach(p => {
+          let count = 0;
+          for (i = 0; i < group.endpoints.length; i++) {
+            if (equal(p, group.endpoints[i])) {
+              count++;
+            }
+          }
+          p.count = count;
+        })
+        filterEps.sort((a, b) => b.count - a.count);
+      }
+
+      group.leader = filterEps[0];
+
+    }
+
+    // 将相邻的endpoint合并成同一个
+    function mergeEndpointIngroup(group) {
+
+      let endpoint, seg, leader, old, neib;
+
+      leader = group.leader;
+
+      for (let i = 0; i < group.endpoints.length; i++) {
+        endpoint = group.endpoints[i];
+        seg = endpoint.seg;
+
+        if (seg.pt.length <= 2) {  // 只有1个和2个点的segment已经没有存在的必要
+          seg.invalid = true;
+          continue;
+        }
+
+        if (equal(endpoint, leader)) {
+          continue;
+        }
+
+        // if (equal(leader, seg.endA) || equal(leader, seg.endB)) {
+        //   continue;
+        // }
+
+        if (equal(seg.endA, endpoint)) {
+          old = seg.endA;
+          if (isNeighbor(seg.endA, leader)) {
+            seg.pt.unshift({ x: leader.x, y: leader.y });
+            seg.endA = { x: leader.x, y: leader.y, adjusted: true, ox: old.x, oy: old.y };
+          } else { // endpoint距离leader太远, 需要中间插值过渡过去
+            let exclude: Point[] = [seg.endA];
+            do {
+              neib = getNeighborToLeader(seg.endA, exclude);
+              if (!neib) break;
+
+              seg.pt.unshift({ x: neib.x, y: neib.y });
+              seg.endA = { x: neib.x, y: neib.y, adjusted: true, ox: old.x, oy: old.y };
+              exclude.push(seg.pt[0]);
+              if (neib == leader) break;
+
+            } while (1)
+          }
+
+        } else if (equal(seg.endB, endpoint)) {
+          old = seg.endB;
+          if(isNeighbor(seg.endB, leader)) {
+            seg.pt.push({ x: leader.x, y: leader.y });
+            seg.endB = { x: leader.x, y: leader.y, adjusted: true, ox: old.x, oy: old.y };
+          } else { // endpoint距离leader太远, 需要中间插值过渡过去
+            let exclude: Point[] = [seg.endB];
+            do {
+              neib = getNeighborToLeader(seg.endB, exclude);
+              if (!neib) break;
+        
+              seg.pt.push({ x: neib.x, y: neib.y });
+              seg.endB = { x: neib.x, y: neib.y, adjusted: true, ox: old.x, oy: old.y };
+              exclude.push(seg.pt[seg.pt.length - 1]);
+              if (neib == leader) break;
+
+            } while (1)
+          }
+        }
+
+      }
+
+      function getNeighborToLeader(p: Point, exclude: Point[]) {
+        let fitneib;
+        let neibs = [];
+        for (let dp of group.endpoints) {
+          if (isNeighbor(p, dp) && !equal(p, dp) && !include(exclude, dp)) {
+            neibs.push(dp);
+          }
+        }
+
+        let dist, minP2L = 999;
+        for (let neib of neibs) {
+          dist = ddist(neib, leader);
+          if ( dist < minP2L) {
+            minP2L = dist;
+            fitneib = neib; 
+          }
+        }
+
+        return fitneib;
+
+      }
+
+    }
+
+    let self = this;
+    let segments = this.segments;
+    let endpoints = [];
+    let ninGroupPoint;
+    let group;
+    let count;
+
+    // 先剔除无效孤点
+    count = 0;
+    let singleSegs = segments.filter(s => s.pt.length == 1);
+    for (let ss of singleSegs) {
+      let findseg = segments.find(s => s != ss && (equal(s.endA, ss.endA) || equal(s.endB, ss.endA)));
+      if (findseg) {
+        let idx = segments.findIndex(s => s == ss);
+        segments.splice(idx, 1);
+        count++;
+      }
+    }
+    console.log(`delete single point segments. totoal: ${singleSegs.length}, delete: ${count}`);
+
+    
+    // 整理出所有的endpoint 
+    segments.forEach(s => {
+      endpoints.push({ x: s.endA.x, y: s.endA.y, seg: s });
+      endpoints.push({ x: s.endB.x, y: s.endB.y, seg: s });
+    })
+
+    // 先对所有的endpoint做一轮检查,所有的endpoint至少有一个邻居, 如果一个endpoint找不到邻居,那么有可能需要调整对应segment的pt和端点信息
+    // 这么做也是对trace算法的一个纠偏, 某些segment点集trace的结果首尾3个点可能不符合要求,x, y坐标变化没有做到单调增/减。 实测有发现这种情况
+    adjustSpecialEndpoint(endpoints);
+
+
+    // 然后对他们进行分组(相邻的endpoint分为一组)
+    do {
+
+      ninGroupPoint = endpoints.find(s => !s.ingroup);
+      
+      if (!ninGroupPoint) break;
+
+      group = { endpoints: [ninGroupPoint] };
+
+      ninGroupPoint.ingroup = true;
+      
+      findNeighborEndpoint(group, endpoints);
+
+      electGroupLeader(group); 
+
+      mergeEndpointIngroup(group);
+
+    } while (true)
+
+    // 调整完顶点后删除无效segment
+    let count1 = this.segments.length;
+    this.segments = segments.filter(s => !s.invalid);
+    let count2 = this.segments.length;
+    console.log(`顶点合并后删除 ${count1 - count2} 个无效segment`);
+  }
+
+
+  // segment组合成path
+  private segmentsToPaths() {
+
+    function equal(pt1: Point, pt2: Point) {
+      return pt1.x == pt2.x && pt1.y == pt2.y;
+    }
+
+    // 判断两个点是否靠近
+    function near(pt1: Point, pt2: Point) {
+      let threshold = 5;
+      let dx = Math.abs(pt1.x - pt2.x);
+      let dy = Math.abs(pt1.y - pt2.y);
+
+      // let dd = Math.sqrt(dx * dx + dy * dy);
+      // if (dd < threshold) return true;
+      // return false;
+
+      if (dx <= threshold && dy <= threshold) return true;
+      return false;
+    }
+
+    // 从一个segment出发按序找到一个完整的path
+    function findPath(startSeg, areaSegs, areaId) {
+      
+      // 可能能找到多个next segment,这里做一个挑选
+      function findNextSegment(seg, endpoint, path) {
+        let validsegs = [];
+
+        for (let s of areaSegs) {
+          if (!s.inpath && !equal(s.endA, s.endB) // 排除自闭和segment
+            && (equal(s.endA, endpoint) || equal(s.endB, endpoint))) { // find one
+              s.score = s.pt.length;
+              if ((s.areaA == seg.areaA && s.areaB == seg.areaB) 
+                || (s.areaA == seg.areaB && s.areaB == seg.areaA)) {
+                s.score *= 1000;
+              }
+              if (equal(s.endA, endpoint)) {
+                if (path.segments.find(ss => equal(ss.endA, s.endB) || equal(ss.endB, s.endB))) {
+                  s.score *= 0;
+                }
+              } else if (equal(s.endB, endpoint)) {
+                if (path.segments.find(ss => equal(ss.endA, s.endA) || equal(ss.endB, s.endA))) {
+                  s.score *= 0;
+                }
+              }
+              validsegs.push(s);
+          }
+        }
+
+        validsegs.sort((a, b) => b.score - a.score);
+        
+        return validsegs[0];
+      }
+
+      let path: Path = { segments: [], complete: false };
+      let flag = 'B';  // 指示下一个是找endA还是endB
+      let seg = startSeg;
+      let nextseg;
+
+      seg.inpath = true;
+      path.segments.push(seg);
+      path.s = { x: seg.endA.x, y: seg.endA.y };
+
+      if (equal(startSeg.endA, startSeg.endB)) {  // 自封闭环,是独立path,不用再继续寻找了,直接返回
+        path.complete = true;
+        path.e = { x: seg.endB.x, y: seg.endB.y };
+        return path;
+      }
+
+      do {
+
+        if (flag == 'B') {
+          // 找下一个与当前seg的endB端点衔接上的segment(注:排除自闭合的segment)
+          // nextseg = areaSegs.find(s => !s.inpath && !equal(s.endA, s.endB) 
+          //   && (equal(s.endA, seg.endB) || equal(s.endB, seg.endB)));
+          nextseg = findNextSegment(seg, seg.endB, path);
+          if (!nextseg) break;
+
+          if (equal(seg.endB, nextseg.endA)) flag = 'B';
+          else if (equal(seg.endB, nextseg.endB)) flag = 'A';
+          seg = nextseg;
+        } else if (flag == 'A') {
+          // 找下一个与当前seg的endA端点衔接上的segment(注:排除自闭合的segment)
+          // nextseg = areaSegs.find(s => !s.inpath && !equal(s.endA, s.endB) 
+          //   && (equal(s.endA, seg.endA) || equal(s.endB, seg.endA)));
+          nextseg = findNextSegment(seg, seg.endA, path);
+          if (!nextseg) break;
+
+          if (equal(seg.endA, nextseg.endA)) flag = 'B';
+          else if (equal(seg.endA, nextseg.endB)) flag = 'A';
+          seg = nextseg;
+        }
+
+        seg.inpath = true;
+        path.segments.push(seg);
+
+        if (equal(seg.endA, startSeg.endA) || equal(seg.endB, startSeg.endA)) {
+          path.complete = true
+          break;
+        }
+
+      } while (true)
+
+      
+      let endPoint;
+      if (flag == 'A') {
+        endPoint = path.segments[path.segments.length - 1].endA;
+      } else if (flag == 'B') {
+        endPoint = path.segments[path.segments.length - 1].endB;
+      }
+      path.e = { x: endPoint.x, y: endPoint.y };
+
+
+      if (path.complete) return path;
+
+      // 如果顺序走下来发现不能拼接成一个完整的path,那么尝试逆向寻找,让path尽可能完整
+      // console.warn(`${areaId} path incomplete! try reverse find`, path);
+      
+      seg = startSeg;
+      flag = 'A';
+
+      do {
+
+        if (flag == 'B') {
+          // 找下一个与当前seg的endB端点衔接上的segment(注:排除自闭合的segment)
+          nextseg = areaSegs.find(s => !s.inpath && !equal(s.endA, s.endB) 
+            && (equal(s.endA, seg.endB) || equal(s.endB, seg.endB)));
+          if (!nextseg) break;
+
+          if (equal(seg.endB, nextseg.endA)) flag = 'B';
+          else if (equal(seg.endB, nextseg.endB)) flag = 'A'
+          seg = nextseg;
+        } else if (flag == 'A') {
+          // 找下一个与当前seg的endA端点衔接上的segment(注:排除自闭合的segment)
+          nextseg = areaSegs.find(s => !s.inpath && !equal(s.endA, s.endB) 
+            && (equal(s.endA, seg.endA) || equal(s.endB, seg.endA)));
+          if (!nextseg) break;
+
+          if (equal(seg.endA, nextseg.endA)) flag = 'B';
+          else if (equal(seg.endA, nextseg.endB)) flag = 'A'
+          seg = nextseg;
+        }
+
+        seg.inpath = true;
+        path.segments.unshift(seg);
+
+        if (equal(seg.endA, endPoint) || equal(seg.endB, endPoint)) {
+          path.complete = true
+          break;
+        }
+
+      } while (true)
+
+      let startPoint;
+      if (flag == 'A') {
+        startPoint = path.segments[0].endA;
+      } else if (flag == 'B') {
+        startPoint = path.segments[0].endB;
+      }
+      path.s = { x: startPoint.x, y: startPoint.y };
+
+      if (!path.complete) {
+        console.warn(`${areaId} found a incomplete path`, path);
+        self.debugInfo.totalIncompletePaths++;
+      }
+
+      return path;
+
+    }
+
+    // 强制合并不完整的path
+    function tryMergePaths(area, pathlist) {
+      let mergeflag;
+      let incompletCount = 0;
+      for (let path of pathlist) {
+        if (!path.complete) {
+          incompletCount++;
+        }
+      }
+      if (incompletCount == 1) {
+        console.log(`${area} incomplete path probably not a problem`, pathlist);
+        self.debugInfo.doesntMatterIncompletePaths++;
+      }
+      if (incompletCount < 2) return;  // nothing to merge
+
+      // start merge
+      do {
+        let path, path1 = null, path2 = null;
+        for (let i = 0; i < pathlist.length; i++) {
+          path = pathlist[i];
+          if (!path.complete && !near(path.s, path.e)) {
+            path1 = path; // find path1
+            for (let j = i + 1; j < pathlist.length; j++) {
+              path = pathlist[j];
+              if (!path.complete && !near(path.s, path.e)) {
+                if (canMerge(path1, path)) { // find path2
+                  path2 = path;
+                  break;
+                }
+              }
+            }
+          }
+          if (path1 && path2) {
+            break;
+          }
+        }
+
+        if (path1 && path2) {
+          let newpath = merge(path1, path2);
+          pathlist.splice(pathlist.findIndex(p => p == path1), 1);
+          pathlist.splice(pathlist.findIndex(p => p == path2), 1);
+          pathlist.unshift(newpath);
+          console.log("merge:", path1, path2, newpath, pathlist);
+        } else {
+          break;
+        }
+
+      } while(true)
+
+      for (let path of pathlist) {
+        if (!path.complete && equal(path.s, path.e)) {
+          path.complete = true;
+        }
+      }
+      for (let path of pathlist) {
+        if (!path.complete) {
+          self.debugInfo.realIncompletePaths++;
+        }
+      }
+
+      function canMerge(path1, path2): boolean {
+        if (near(path1.e, path2.s)) {
+          mergeflag = '1e2s';
+          return true;
+          let seg1 = path1.segments[path1.segments.length - 1];
+          let seg2 = path2.segments[0];
+          if ((seg1.areaA == seg2.areaA && seg1.areaB == seg2.areaB)
+            || (seg1.areaA == seg2.areaB && seg1.areaB == seg2.areaA)) {
+            mergeflag = '1e2s';
+            return true;
+          }
+        }
+        if (near(path1.e, path2.e)) {
+          mergeflag = '1e2e';
+          return true;
+          let seg1 = path1.segments[path1.segments.length - 1];
+          let seg2 = path2.segments[path2.segments.length - 1];
+          if ((seg1.areaA == seg2.areaA && seg1.areaB == seg2.areaB)
+            || (seg1.areaA == seg2.areaB && seg1.areaB == seg2.areaA)) {
+            mergeflag = '1e2e';
+            return true;
+          }
+        }
+        if (near(path1.s, path2.s)) {
+          mergeflag = '1s2s';
+          return true;
+          let seg1 = path1.segments[0];
+          let seg2 = path2.segments[0];
+          if ((seg1.areaA == seg2.areaA && seg1.areaB == seg2.areaB)
+            || (seg1.areaA == seg2.areaB && seg1.areaB == seg2.areaA)) {
+            mergeflag = '1s2s';
+            return true;
+          }
+        }
+        if (near(path1.s, path2.e)) {
+          mergeflag = '1s2e';
+          return true;
+          let seg1 = path1.segments[0];
+          let seg2 = path2.segments[path2.segments.length - 1];
+          if ((seg1.areaA == seg2.areaA && seg1.areaB == seg2.areaB)
+            || (seg1.areaA == seg2.areaB && seg1.areaB == seg2.areaA)) {
+            mergeflag = '1s2e';
+            return true;
+          }
+        }
+        return false;
+      }
+
+      function merge(path1, path2): Path {
+        let newpath: Path = { segments: [], complete: false };
+
+        if (mergeflag == '1e2s') {
+          for (let i = 0; i < path1.segments.length; i++) {
+            newpath.segments.push(path1.segments[i]);
+          }
+          for (let i = 0; i < path2.segments.length; i++) {
+            newpath.segments.push(path2.segments[i]);
+          }
+          newpath.s = path1.s;
+          newpath.e = path2.e;
+        } else if (mergeflag == '1e2e') {
+          for (let i = 0; i < path1.segments.length; i++) {
+            newpath.segments.push(path1.segments[i]);
+          }
+          for (let i = path2.segments.length - 1; i >= 0; i--) {
+            newpath.segments.push(path2.segments[i]);
+          }
+          newpath.s = path1.s;
+          newpath.e = path2.s;
+        } else if (mergeflag == '1s2s') {
+          for (let i = path2.segments.length - 1; i >= 0; i--) {
+            newpath.segments.push(path2.segments[i]);
+          }
+          for (let i = 0; i < path1.segments.length; i++) {
+            newpath.segments.push(path1.segments[i]);
+          }
+          newpath.s = path2.e;
+          newpath.e = path1.e;
+        } else if (mergeflag == '1s2e') {
+          for (let i = 0; i < path2.segments.length; i++) {
+            newpath.segments.push(path2.segments[i]);
+          }
+          for (let i = 0; i < path1.segments.length; i++) {
+            newpath.segments.push(path1.segments[i]);
+          }
+          newpath.s = path2.s;
+          newpath.e = path1.e;
+        }
+        return newpath;
+      }
+    }
+
+
+    // pathlist中的path是有顺序的,如大圆套小圆的场景,大圆path必须排在前面先画,然后moveTo,再画小圆
+    function sortPathlist(pathlist) {
+
+      if (pathlist.length <= 1) return;
+
+      // 先把每个path的矩形框出来
+      for (let path of pathlist) {
+        let p = path.segments[0].pt[0];
+        let l = p.x, r = p.x, t = p.y, b = p.y;
+        for (let seg of path.segments) {
+          seg.pt.forEach(p => {
+            if (p.x < l) l = p.x;
+            if (p.x > r) r = p.x;
+            if (p.y < t) t = p.y;
+            if (p.y > b) b = p.y;
+          })
+        }
+        path.area = ( r - l ) * ( b - t ); // 矩形框面积
+      }
+
+      // 按矩形框大小排序, 从大到小降序
+      pathlist.sort((a, b) => b.area - a.area);
+
+      pathlist.forEach(p => delete p.area);
+
+    }
+
+    function removeIncompletePaths(pathMap: PathMap) {
+
+      console.log("删除孤立非闭合路径");
+
+      let keys = Object.keys(pathMap);
+      let datas = [];
+      for (let key of keys) {
+        let pathlist = pathMap[key];
+        for (let i = 0; i < pathlist.length; i++) {
+          let path = pathlist[i];
+          if (!path.complete && path.segments.length == 1 && path.segments[0].pt.length < 30) {
+          // if (!path.complete && path.segments.length == 1) {
+            datas.push({pathlist, path});
+          }
+        }
+      }
+
+      console.log(`incomplete paths count: ${datas.length}`);
+
+      let data1, data2;
+      let count = 0;
+      for (let i = 0; i < datas.length; i++) {
+        data1 = datas[i];
+        if (data1.done) continue;
+
+        for (let j = i + 1; j < datas.length; j++) {
+          data2 = datas[j];
+          if(data1.path.segments[0] == data2.path.segments[0]) {
+            data1.pathlist.splice(data1.pathlist.findIndex(p => p == data1.path), 1);
+            data2.pathlist.splice(data2.pathlist.findIndex(p => p == data2.path), 1);
+            data1.done = true;
+            data2.done = true;
+            count += 2;
+            console.log(`remove path `, data1.path);
+            console.log(`remove path `, data2.path);
+          }
+        }
+        
+      }
+      
+      console.log(`删除孤立非闭合路径:${count}`);
+      self.debugInfo.removeIncompletePaths = count;
+    }
+
+
+    let self = this;
+
+    let areaSegHash = {};
+
+    this.segments.forEach(seg => {
+      if (seg.areaA) {
+        if (!areaSegHash[seg.areaA]) {
+          areaSegHash[seg.areaA] = new Set();
+        }
+        areaSegHash[seg.areaA].add(seg);
+      };
+      if (seg.areaB) {
+        if (!areaSegHash[seg.areaB]) {
+          areaSegHash[seg.areaB] = new Set();
+        }
+        areaSegHash[seg.areaB].add(seg);
+      }
+    })
+
+    let areas = Object.keys(areaSegHash);
+    // 将同一个area的segment组合成path,注:可能组成多个path
+    for (let area of areas) {
+      let pathlist = [];
+      let areaSegs = Array.from(areaSegHash[area]) as [any];  // 属于该area的所有segment
+      let startSeg, path;
+
+      if (area == '4287193771') {
+        console.log("pause");
+      }
+
+      do {
+        
+        startSeg = areaSegs.find(s => !s.inpath);
+        
+        if (!startSeg) break;
+
+        path = findPath(startSeg, areaSegs, area);
+
+        pathlist.push(path);
+
+      } while (true)
+
+      areaSegs.forEach(seg => delete seg.inpath);
+
+      sortPathlist(pathlist);
+
+      tryMergePaths(area, pathlist);
+
+      sortPathlist(pathlist);  // do sort again after merge paths
+
+      this.pathMap[area] = pathlist;
+
+    }
+
+    // delete alone incomplete path
+    // removeIncompletePaths(this.pathMap);
+
+  }
+
+
+
+  //////////////////////////////////////////////////////////////////////////////////////
+  /////////////////////  以下是将segment曲线化的核心算法,参考potrace ////////////////////
+
+  // path曲线化, 具体来说: 先将各个segment分别解析成多边形得到顶点, 然后path将各个segment的顶点汇聚起来,然后曲线化
+  private curvedPaths(mode = 1) {
+
+    function equal(pt1: Point, pt2: Point) {
+      return pt1.x == pt2.x && pt1.y == pt2.y;
+    }
+
+    function ddist(p, q) {
+      return Math.sqrt((p.x - q.x) * (p.x - q.x) + (p.y - q.y) * (p.y - q.y));
+    }
+
+    function mod(a, n) {
+      return a >= n ? a % n : a >= 0 ? a : n - 1 - (-1 - a) % n;
+    }
+
+    function sign(i) {
+      return i > 0 ? 1 : i < 0 ? -1 : 0;
+    }
+
+    function xprod(p1, p2) {
+      return p1.x * p2.y - p1.y * p2.x;
+    }
+
+    function cyclic(a, b, c) {
+      if (a <= c) {
+        return (a <= b && b < c);
+      } else {
+        return (a <= b || b < c);
+      }
+    }
+
+    function Sum(x, y, xy, x2, y2) {
+      this.x = x;
+      this.y = y;
+      this.xy = xy;
+      this.x2 = x2;
+      this.y2 = y2;
+    }
+
+    function quadform(Q, w) {
+      let v = new Array(3), i, j, sum;
+
+      v[0] = w.x;
+      v[1] = w.y;
+      v[2] = 1;
+      sum = 0.0;
+
+      for (i = 0; i < 3; i++) {
+        for (j = 0; j < 3; j++) {
+          sum += v[i] * Q.at(i, j) * v[j];
+        }
+      }
+      return sum;
+    }
+
+    function Quad() {
+      this.data = [0, 0, 0, 0, 0, 0, 0, 0, 0];
+    }
+
+    Quad.prototype.at = function (x, y) {
+      return this.data[x * 3 + y];
+    };
+
+    function interval(lambda, a, b) {
+      let res = { x: 0, y: 0 };
+
+      res.x = a.x + lambda * (b.x - a.x);
+      res.y = a.y + lambda * (b.y - a.y);
+      return res;
+    }
+
+    
+    function dorth_infty(p0, p2) {
+      var r = { x: 0, y: 0 };
+
+      r.y = sign(p2.x - p0.x);
+      r.x = -sign(p2.y - p0.y);
+
+      return r;
+    }
+
+    function ddenom(p0, p2) {
+      let r = dorth_infty(p0, p2);
+
+      return r.y * (p2.x - p0.x) - r.x * (p2.y - p0.y);
+    }
+
+    function dpara(p0, p1, p2) {
+      let x1, y1, x2, y2;
+
+      x1 = p1.x - p0.x;
+      y1 = p1.y - p0.y;
+      x2 = p2.x - p0.x;
+      y2 = p2.y - p0.y;
+
+      return x1 * y2 - x2 * y1;
+    }
+
+    // 判断一个segment是否闭合
+    function isClosed(seg) {
+      if (seg.pt.length <= 2) return false;
+
+      let pt = seg.pt;
+      let n = seg.pt.length;
+
+      if (Math.abs(pt[0].x - pt[n-1].x) <= 1 && Math.abs(pt[0].y - pt[n-1].y) <= 1) return true;
+      return false;
+    }
+
+    function calcSums(seg) {
+      let i, x, y;
+      seg.sums = [];
+      let s = seg.sums;
+      s.push(new Sum(0, 0, 0, 0, 0));
+      for (i = 0; i < seg.pt.length; i++) {
+        x = seg.pt[i].x - seg.endA.x;
+        y = seg.pt[i].y - seg.endA.y;
+        s.push(new Sum(s[i].x + x, s[i].y + y, s[i].xy + x * y,
+          s[i].x2 + x * x, s[i].y2 + y * y));
+      }
+    }
+
+    function calcLon(seg) {
+
+      let n = seg.pt.length, pt = seg.pt, dir,
+        pivk = new Array(n),
+        nc = new Array(n),
+        ct = new Array(4);
+      seg.lon = new Array(n);
+
+      let isclosed = isClosed(seg);
+
+      let constraint = [ { x: 0, y: 0 }, {x: 0, y: 0} ],
+        cur = { x: 0, y: 0 },
+        off = { x: 0, y: 0 },
+        dk = { x: 0, y: 0 },
+        foundk;
+
+      let i, j, k1, a, b, c, d, k = 0;
+      for (i = n - 1; i >= 0; i--) {
+        if (pt[i].x != pt[k].x && pt[i].y != pt[k].y) {
+          k = i + 1;
+        }
+        if (k >= n) {
+          k = 0;  // add by guoziyun. to fit unclosed path
+        }
+        nc[i] = k;
+      }
+
+      for (i = n - 1; i >= 0; i--) {
+        ct[0] = ct[1] = ct[2] = ct[3] = 0;
+
+        if (isclosed || i < n - 1)  {  // do this if path is closed or not the last point
+          dir = (3 + 3 * (pt[mod(i + 1, n)].x - pt[i].x) +
+          (pt[mod(i + 1, n)].y - pt[i].y)) / 2;
+          ct[dir]++;
+        }
+
+        constraint[0].x = 0;
+        constraint[0].y = 0;
+        constraint[1].x = 0;
+        constraint[1].y = 0;
+
+        k = nc[i];
+        k1 = i;
+        while (1) {
+          foundk = 0;
+          dir = (3 + 3 * sign(pt[k].x - pt[k1].x) +
+            sign(pt[k].y - pt[k1].y)) / 2;
+          // modify by guoziyun
+          if (dir == 0 || dir == 1 || dir == 2 || dir == 3) {
+            ct[dir]++;
+          } 
+
+          if (ct[0] && ct[1] && ct[2] && ct[3]) {
+            pivk[i] = k1;
+            foundk = 1;
+            break;
+          }
+
+          cur.x = pt[k].x - pt[i].x;
+          cur.y = pt[k].y - pt[i].y;
+
+          if (xprod(constraint[0], cur) < 0 || xprod(constraint[1], cur) > 0) {
+            break;
+          }
+
+          if (Math.abs(cur.x) <= 1 && Math.abs(cur.y) <= 1) {
+
+          } else {
+            off.x = cur.x + ((cur.y >= 0 && (cur.y > 0 || cur.x < 0)) ? 1 : -1);
+            off.y = cur.y + ((cur.x <= 0 && (cur.x < 0 || cur.y < 0)) ? 1 : -1);
+            if (xprod(constraint[0], off) >= 0) {
+              constraint[0].x = off.x;
+              constraint[0].y = off.y;
+            }
+            off.x = cur.x + ((cur.y <= 0 && (cur.y < 0 || cur.x < 0)) ? 1 : -1);
+            off.y = cur.y + ((cur.x >= 0 && (cur.x > 0 || cur.y < 0)) ? 1 : -1);
+            if (xprod(constraint[1], off) <= 0) {
+              constraint[1].x = off.x;
+              constraint[1].y = off.y;
+            }
+          }
+          k1 = k;
+          k = nc[k1];
+          if (!cyclic(k, i, k1)) {
+            break;
+          }
+        }
+        if (foundk === 0) {
+          dk.x = sign(pt[k].x - pt[k1].x);
+          dk.y = sign(pt[k].y - pt[k1].y);
+          cur.x = pt[k1].x - pt[i].x;
+          cur.y = pt[k1].y - pt[i].y;
+
+          a = xprod(constraint[0], cur);
+          b = xprod(constraint[0], dk);
+          c = xprod(constraint[1], cur);
+          d = xprod(constraint[1], dk);
+
+          j = 10000000;
+          if (b < 0) {
+            j = Math.floor(a / -b);
+          }
+          if (d > 0) {
+            j = Math.min(j, Math.floor(-c / d));
+          }
+          pivk[i] = mod(k1 + j, n);
+        }
+      }
+
+      j = pivk[n - 1];
+      seg.lon[n - 1] = j;
+      for (i = n - 2; i >= 0; i--) {
+        if (cyclic(i + 1, pivk[i], j)) {
+          j = pivk[i];
+        }
+        seg.lon[i] = j;
+      }
+
+      for (i = n - 1; cyclic(mod(i + 1, n), j, seg.lon[i]); i--) {
+        seg.lon[i] = j;
+      }
+    }
+
+
+    function bestPolygon(seg) {
+
+      function penalty3(seg, i, j) {
+
+        let n = seg.pt.length, pt = seg.pt, sums = seg.sums;
+        let x, y, xy, x2, y2,
+          k, a, b, c, s,
+          px, py, ex, ey,
+          r = 0;
+        if (j >= n) {
+          j -= n;
+          r = 1;
+        }
+
+        if (r === 0) {
+          x = sums[j + 1].x - sums[i].x;
+          y = sums[j + 1].y - sums[i].y;
+          x2 = sums[j + 1].x2 - sums[i].x2;
+          xy = sums[j + 1].xy - sums[i].xy;
+          y2 = sums[j + 1].y2 - sums[i].y2;
+          k = j + 1 - i;
+        } else {
+          x = sums[j + 1].x - sums[i].x + sums[n].x;
+          y = sums[j + 1].y - sums[i].y + sums[n].y;
+          x2 = sums[j + 1].x2 - sums[i].x2 + sums[n].x2;
+          xy = sums[j + 1].xy - sums[i].xy + sums[n].xy;
+          y2 = sums[j + 1].y2 - sums[i].y2 + sums[n].y2;
+          k = j + 1 - i + n;
+        }
+
+        px = (pt[i].x + pt[j].x) / 2.0 - pt[0].x;
+        py = (pt[i].y + pt[j].y) / 2.0 - pt[0].y;
+        ey = (pt[j].x - pt[i].x);
+        ex = -(pt[j].y - pt[i].y);
+
+        a = ((x2 - 2 * x * px) / k + px * px);
+        b = ((xy - x * py - y * px) / k + px * py);
+        c = ((y2 - 2 * y * py) / k + py * py);
+
+        s = ex * ex * a + 2 * ex * ey * b + ey * ey * c;
+
+        return Math.sqrt(s);
+      }
+
+      let i, j, m, k,
+        n = seg.pt.length,
+        pen = new Array(n + 1),
+        prev = new Array(n + 1),
+        clip0 = new Array(n),
+        clip1 = new Array(n + 1),
+        seg0 = new Array(n + 1),
+        seg1 = new Array(n + 1),
+        thispen, best, c;
+
+      for (i = 0; i < n; i++) {
+        c = mod(seg.lon[mod(i - 1, n)] - 1, n);
+        if (c == i) {
+          c = mod(i + 1, n);
+        }
+        if (c < i) {
+          clip0[i] = n;
+        } else {
+          clip0[i] = c;
+        }
+      }
+
+      // 矫正clip0
+      if (clip0[0] == n - 1 && clip0[1] != n - 1) {
+        console.warn("clip0 有异常,需矫正", clip0, seg);
+        clip0[0] = 1;
+        self.debugInfo.clipAdjustCount++;
+      }
+
+      j = 1;
+      for (i = 0; i < n; i++) {
+        while (j <= clip0[i]) {
+          clip1[j] = i;
+          j++;
+        }
+      }
+
+      i = 0;
+      for (j = 0; i < n; j++) {
+        seg0[j] = i;
+        i = clip0[i];
+      }
+      seg0[j] = n;
+      m = j;
+
+      i = n;
+      for (j = m; j > 0; j--) {
+        seg1[j] = i;
+        i = clip1[i];
+      }
+      seg1[0] = 0;
+
+      pen[0] = 0;
+      for (j = 1; j <= m; j++) {
+        for (i = seg1[j]; i <= seg0[j]; i++) {
+          best = -1;
+          for (k = seg0[j - 1]; k >= clip1[i]; k--) {
+            thispen = penalty3(seg, k, i) + pen[k];
+            if (best < 0 || thispen < best) {
+              prev[i] = k;
+              best = thispen;
+            }
+          }
+          pen[i] = best;
+        }
+      }
+      seg.m = m;
+      seg.po = new Array(m);
+
+      for (i = n, j = m - 1; i > 0; j--) {
+        i = prev[i];
+        seg.po[j] = i;
+      }
+
+    }
+    
+    
+
+    function adjustVertices(seg) {
+
+      function pointslope(seg, i, j, ctr, dir) {
+
+        let n = seg.pt.length, sums = seg.sums,
+          x, y, x2, xy, y2,
+          k, a, b, c, lambda2, l, r = 0;
+
+        while (j >= n) {
+          j -= n;
+          r += 1;
+        }
+        while (i >= n) {
+          i -= n;
+          r -= 1;
+        }
+        while (j < 0) {
+          j += n;
+          r -= 1;
+        }
+        while (i < 0) {
+          i += n;
+          r += 1;
+        }
+
+        x = sums[j + 1].x - sums[i].x + r * sums[n].x;
+        y = sums[j + 1].y - sums[i].y + r * sums[n].y;
+        x2 = sums[j + 1].x2 - sums[i].x2 + r * sums[n].x2;
+        xy = sums[j + 1].xy - sums[i].xy + r * sums[n].xy;
+        y2 = sums[j + 1].y2 - sums[i].y2 + r * sums[n].y2;
+        k = j + 1 - i + r * n;
+
+        ctr.x = x / k;
+        ctr.y = y / k;
+
+        a = (x2 - x * x / k) / k;
+        b = (xy - x * y / k) / k;
+        c = (y2 - y * y / k) / k;
+
+        lambda2 = (a + c + Math.sqrt((a - c) * (a - c) + 4 * b * b)) / 2;
+
+        a -= lambda2;
+        c -= lambda2;
+
+        if (Math.abs(a) >= Math.abs(c)) {
+          l = Math.sqrt(a * a + b * b);
+          if (l !== 0) {
+            dir.x = -b / l;
+            dir.y = a / l;
+          }
+        } else {
+          l = Math.sqrt(c * c + b * b);
+          if (l !== 0) {
+            dir.x = -c / l;
+            dir.y = b / l;
+          }
+        }
+        if (l === 0) {
+          dir.x = dir.y = 0;
+        }
+      }
+
+      let m = seg.m, po = seg.po, n = seg.pt.length, pt = seg.pt,
+        x0 = seg.endA.x, y0 = seg.endA.y,
+        ctr = new Array(m), dir = new Array(m),
+        q = new Array(m),
+        v = new Array(3), d, i, j, k, l,
+        s = { x: 0, y: 0 };
+      
+      seg.curve = new Curve(m);
+
+      for (i = 0; i < m; i++) {
+        j = po[mod(i + 1, m)];
+        j = mod(j - po[i], n) + po[i];
+        ctr[i] = { x: 0, y: 0 };
+        dir[i] = { x: 0, y: 0 };
+        pointslope(seg, po[i], j, ctr[i], dir[i]);
+      }
+
+      for (i = 0; i < m; i++) {
+        q[i] = new Quad();
+        d = dir[i].x * dir[i].x + dir[i].y * dir[i].y;
+        if (d === 0.0) {
+          for (j = 0; j < 3; j++) {
+            for (k = 0; k < 3; k++) {
+              q[i].data[j * 3 + k] = 0;
+            }
+          }
+        } else {
+          v[0] = dir[i].y;
+          v[1] = -dir[i].x;
+          v[2] = - v[1] * ctr[i].y - v[0] * ctr[i].x;
+          for (l = 0; l < 3; l++) {
+            for (k = 0; k < 3; k++) {
+              q[i].data[l * 3 + k] = v[l] * v[k] / d;
+            }
+          }
+        }
+      }
+
+      var Q, w, dx, dy, det, min, cand, xmin, ymin, z;
+      for (i = 0; i < m; i++) {
+        Q = new Quad();
+        w = { x: 0, y : 0 };
+
+        s.x = pt[po[i]].x - x0;
+        s.y = pt[po[i]].y - y0;
+
+        j = mod(i - 1, m);
+
+        for (l = 0; l < 3; l++) {
+          for (k = 0; k < 3; k++) {
+            Q.data[l * 3 + k] = q[j].at(l, k) + q[i].at(l, k);
+          }
+        }
+
+        while (1) {
+
+          det = Q.at(0, 0) * Q.at(1, 1) - Q.at(0, 1) * Q.at(1, 0);
+          if (det !== 0.0) {
+            w.x = (-Q.at(0, 2) * Q.at(1, 1) + Q.at(1, 2) * Q.at(0, 1)) / det;
+            w.y = (Q.at(0, 2) * Q.at(1, 0) - Q.at(1, 2) * Q.at(0, 0)) / det;
+            break;
+          }
+
+          if (Q.at(0, 0) > Q.at(1, 1)) {
+            v[0] = -Q.at(0, 1);
+            v[1] = Q.at(0, 0);
+          } else if (Q.at(1, 1)) {
+            v[0] = -Q.at(1, 1);
+            v[1] = Q.at(1, 0);
+          } else {
+            v[0] = 1;
+            v[1] = 0;
+          }
+          d = v[0] * v[0] + v[1] * v[1];
+          v[2] = - v[1] * s.y - v[0] * s.x;
+          for (l = 0; l < 3; l++) {
+            for (k = 0; k < 3; k++) {
+              Q.data[l * 3 + k] += v[l] * v[k] / d;
+            }
+          }
+        }
+        dx = Math.abs(w.x - s.x);
+        dy = Math.abs(w.y - s.y);
+        if (dx <= 0.5 && dy <= 0.5) {
+          seg.curve.vertex[i] = { x: w.x + x0, y: w.y + y0 };
+          continue;
+        }
+
+        min = quadform(Q, s);
+        xmin = s.x;
+        ymin = s.y;
+
+        if (Q.at(0, 0) !== 0.0) {
+          for (z = 0; z < 2; z++) {
+            w.y = s.y - 0.5 + z;
+            w.x = - (Q.at(0, 1) * w.y + Q.at(0, 2)) / Q.at(0, 0);
+            dx = Math.abs(w.x - s.x);
+            cand = quadform(Q, w);
+            if (dx <= 0.5 && cand < min) {
+              min = cand;
+              xmin = w.x;
+              ymin = w.y;
+            }
+          }
+        }
+
+        if (Q.at(1, 1) !== 0.0) {
+          for (z = 0; z < 2; z++) {
+            w.x = s.x - 0.5 + z;
+            w.y = - (Q.at(1, 0) * w.x + Q.at(1, 2)) / Q.at(1, 1);
+            dy = Math.abs(w.y - s.y);
+            cand = quadform(Q, w);
+            if (dy <= 0.5 && cand < min) {
+              min = cand;
+              xmin = w.x;
+              ymin = w.y;
+            }
+          }
+        }
+
+        for (l = 0; l < 2; l++) {
+          for (k = 0; k < 2; k++) {
+            w.x = s.x - 0.5 + l;
+            w.y = s.y - 0.5 + k;
+            cand = quadform(Q, w);
+            if (cand < min) {
+              min = cand;
+              xmin = w.x;
+              ymin = w.y;
+            }
+          }
+        }
+        seg.curve.vertex[i] = { x: xmin + x0, y: ymin + y0 };
+      }
+    }
+
+    function smooth(path, alphamax = 1) {
+      let m = path.curve.vertex.length;
+      let curve = new Curve(m); // curve 信息重新洗过
+      curve.vertex = path.curve.vertex;
+      path.curve = curve;
+
+      let i, j, k, dd, denom, alpha,
+        p2, p3, p4;
+
+      for (i = 0; i < m; i++) {
+        j = mod(i + 1, m);
+        k = mod(i + 2, m);
+        p4 = interval(1 / 2.0, curve.vertex[k], curve.vertex[j]);
+
+        denom = ddenom(curve.vertex[i], curve.vertex[k]);
+        if (denom !== 0.0) {
+          dd = dpara(curve.vertex[i], curve.vertex[j], curve.vertex[k]) / denom;
+          dd = Math.abs(dd);
+          alpha = dd > 1 ? (1 - 1.0 / dd) : 0;
+          alpha = alpha / 0.75;
+        } else {
+          alpha = 4 / 3.0;
+        }
+
+        if (alpha >= alphamax) {
+          curve.tag[j] = "CORNER";
+          curve.c[3 * j + 1] = curve.vertex[j];
+          curve.c[3 * j + 2] = p4;
+        } else {
+          if (alpha < 0.55) {
+            alpha = 0.55;
+          } else if (alpha > 1) {
+            alpha = 1;
+          }
+          p2 = interval(0.5 + 0.5 * alpha, curve.vertex[i], curve.vertex[j]);
+          p3 = interval(0.5 + 0.5 * alpha, curve.vertex[k], curve.vertex[j]);
+          curve.tag[j] = "CURVE";
+          curve.c[3 * j + 0] = p2;
+          curve.c[3 * j + 1] = p3;
+          curve.c[3 * j + 2] = p4;
+        }
+      }
+
+    }
+
+
+    function reversePath(path) {
+      // reverse smooth curve
+      let n = path.curve.n, tag0 = path.curve.tag, c0 = path.curve.c;
+      let tag = new Array(n), c = new Array(n * 3);
+      let i, j;
+
+      for (i = 0, j = n - 1; i < n; i++, j--) {
+        tag[i] = tag0[j];
+        if (tag[i] == 'CORNER') {
+          c[i * 3 + 1] = c0[j * 3 + 1];
+          c[i * 3 + 2] = c0[j * 3 - 1 < 0 ? n * 3 - 1 : j * 3 - 1];
+        } else if (tag[i] == 'CURVE') {
+          c[i * 3 + 0] = c0[j * 3 + 1];
+          c[i * 3 + 1] = c0[j * 3 + 0];
+          c[i * 3 + 2] = c0[j * 3 - 1 < 0 ? n * 3 - 1 : j * 3 - 1];
+        }
+      }
+
+      path.curve.tag = tag;
+      path.curve.c = c;
+
+      // reverse line curve
+      path.line.reverse();
+
+    }
+
+    // 对开放segment的曲线化
+    function smoothSegment(seg, reverse = false, alphamax = 1) {
+
+      let i, j, k, dd, denom, alpha,
+      s, e,
+      p0, pn, p2, p3, p4,
+      m, curve, vertex, tag, c;
+
+      m = seg.curve.vertex.length, curve = seg.curve;
+
+      if (!reverse) {
+        s = { x: seg.endA.x, y: seg.endA.y };
+        e = { x: seg.endB.x, y: seg.endB.y };
+        vertex = curve.vertex.map(e => e);
+      } else {
+        s = { x: seg.endB.x, y: seg.endB.y };
+        e = { x: seg.endA.x, y: seg.endA.y };
+        vertex = curve.vertex.map(e => e).reverse();
+      }
+      p0 = interval(1 / 2.0, vertex[0], s);
+      pn = interval(1 / 2.0, e, vertex[m - 1]);
+    
+      tag = new Array(m + 2);
+      c = new Array( (m + 2) * 3);
+
+      vertex.unshift(s);
+      vertex.push(e);
+
+      for (i = 0; i < m; i++) {
+        j = i + 1;
+        k = i + 2;
+        p4 = interval(1 / 2.0, vertex[k], vertex[j]);
+
+        denom = ddenom(vertex[i], vertex[k]);
+        if (denom !== 0.0) {
+          dd = dpara(vertex[i], vertex[j], vertex[k]) / denom;
+          dd = Math.abs(dd);
+          alpha = dd > 1 ? (1 - 1.0 / dd) : 0;
+          alpha = alpha / 0.75;
+        } else {
+          alpha = 4 / 3.0;
+        }
+
+        if (alpha >= alphamax) {
+          tag[j] = "CORNER";
+          c[3 * j + 1] = vertex[j];
+          c[3 * j + 2] = p4;
+        } else {
+          if (alpha < 0.55) {
+            alpha = 0.55;
+          } else if (alpha > 1) {
+            alpha = 1;
+          }
+          p2 = interval(0.5 + 0.5 * alpha, vertex[i], vertex[j]);
+          p3 = interval(0.5 + 0.5 * alpha, vertex[k], vertex[j]);
+          tag[j] = "CURVE";
+          c[3 * j + 0] = p2;
+          c[3 * j + 1] = p3;
+          c[3 * j + 2] = p4;
+        }
+      }
+
+
+      tag[0] = "CORNER";
+      c[1] = vertex[0];
+      c[2] = p0;
+
+      tag[m + 1] = "CORNER";
+      c[ (m + 1) * 3 + 1] = pn;
+      c[ (m + 1) * 3 + 2] = vertex[vertex.length - 1];
+      
+      curve.n = tag.length;
+      curve.tag = tag;
+      curve.c = c;
+    }
+
+    // 构建path的拟合多边形,由path下属的segments的po合并而成
+    function buildPathPolygon(path) {
+
+      // 连线前每个segment把两个端点加上
+      function fillEndpoint(segments) {
+        for (let seg of segments) {
+          if(!equal(seg.pt[seg.po[0]], seg.endA)) {
+            seg.po.unshift(0);
+          }
+          if (!equal(seg.pt[seg.po[seg.po.length - 1]], seg.endB)) {
+            seg.po.push(seg.pt.length - 1);
+          }
+        }
+      }
+
+      fillEndpoint(path.segments);
+
+      let seg0 = path.segments[0];
+      let po = seg0.po.map(idx => { return {x: seg0.pt[idx].x, y: seg0.pt[idx].y} });
+      let next = seg0.endB;
+
+      if (!equal(path.s, seg0.endA) && equal(path.s, seg0.endB)) {
+        po.reverse();
+        next = seg0.endA;
+      }
+
+      for (let i = 1; i < path.segments.length; i++) {
+        let seg = path.segments[i];
+        let ppo = seg.po.map(idx => { return {x: seg.pt[idx].x, y: seg.pt[idx].y} });
+        if (equal(next, seg.endA)) {  // 正序
+          if (equal(ppo[0], po[po.length - 1])) {
+            po.pop();
+          }
+          po = po.concat(ppo);
+          next = seg.endB;
+        } else if (equal(next, seg.endB)){ // 反序,需要reverse
+          ppo.reverse();
+          if (equal(ppo[0], po[po.length - 1])) {
+            po.pop();
+          }
+          po = po.concat(ppo);
+          next = seg.endA;
+        } else {  // 以上都不是,说明是断连强行merge拼接起来的,需要进一步判断
+          let da = ddist(next, seg.endA);
+          let db = ddist(next, seg.endB);
+          if (da <= db) {
+            if (equal(ppo[0], po[po.length - 1])) {
+              po.pop();
+            }
+            po = po.concat(ppo);
+            next = seg.endB;
+          } else {
+            ppo.reverse();
+            if (equal(ppo[0], po[po.length - 1])) {
+              po.pop();
+            }
+            po = po.concat(ppo);
+            next = seg.endA;
+          }
+        }
+      }
+
+      if (equal(po[0], po[po.length - 1])) {
+        po.pop()
+      }
+
+      path.polygon = new Polygon(po);
+      path.sign = path.polygon.dir();
+      // if (path.polygon.isIntersect()) {
+      //   self.debugInfo.selfIntersectCount++;
+      // }
+      for (let p of po) {
+        let idx = Math.round(p.y) * self.width + Math.round(p.x);
+        self.poSet.add(idx);
+      }
+
+    }
+
+    function isPathValid(path: Path) {
+      if (!path.polygon.isValid() || path.sign == 0) {
+        return false;
+      } else {
+        return true;
+      }
+    }
+
+    // 连线方式拟合path
+    function linePath(path) {
+      path.line = path.polygon.po.slice();
+    }
+
+    // bezier曲线化path
+    function curvePath(path, alphamax) {
+
+      function mergeSegmentCurves(path) {
+        let segments = path.segments;
+        let n = 0;
+        for (let seg of segments) {
+          n += seg.curve.n;
+        }
+        path.curve = new Curve(n);
+  
+        let index = 0;
+  
+        // 合并tag信息
+        for (let seg of segments) {
+          for (let i = 0; i < seg.curve.n; i++) {
+            path.curve.tag[i + index] = seg.curve.tag[i];
+          }
+          index += seg.curve.n;
+        }
+  
+        // 合并c信息
+        index = 0;
+        for (let seg of segments) {
+          for (let i = 0; i < 3 * seg.curve.n; i++) {
+            path.curve.c[i + index] = seg.curve.c[i];
+          }
+          index += seg.curve.n * 3;
+        }
+  
+      }
+
+
+      // 先对path下的所有segment分别进行曲线化
+      if (path.segments.length == 1) {
+        if (path.complete) {
+          smooth(path.segments[0], alphamax);
+        } else {
+          smoothSegment(path.segments[0], false, alphamax);
+        }
+      } else {
+        let startend = path.s;
+        for (let seg of path.segments) {
+          if (equal(startend, seg.endA)) {  // 正序
+            smoothSegment(seg, false, alphamax);
+            startend = seg.endB;
+          } else if (equal(startend, seg.endB)){ // 反序,需要reverse
+            smoothSegment(seg, true, alphamax);
+            startend = seg.endA;
+          } else {  // 以上都不是,说明是断连强行merge拼接起来的,需要进一步判断
+            let da = ddist(startend, seg.endA);
+            let db = ddist(startend, seg.endB);
+            if (da <= db) {
+              smoothSegment(seg, false, alphamax);
+              startend = seg.endB;
+            } else {
+              smoothSegment(seg, true, alphamax);
+              startend = seg.endA;
+            }
+          }
+        }
+      }
+      // 然后merge一个统一结果到path
+      mergeSegmentCurves(path);
+
+    }
+
+    // 调整方向
+    function doReverse(pathlist: Array<Path>) {
+      if (!pathlist || pathlist.length <= 0) {
+        console.warn("pathlist is empty!");
+        return;
+      }
+
+      let path0 = pathlist[0];
+      if (path0.sign == -1) { // 第一个强制为逆时针
+        reversePath(path0);
+        path0.sign = 1;
+        // console.log('reverse path', path0, pathlist);
+      }
+
+      let n = pathlist.length;
+      let i, j;
+
+      for (i = 1; i < n; i++) {
+        for (j = i - 1; j >= 0; j--) {
+          if (pathlist[j].polygon.contains(pathlist[i].polygon)) { // find a parent path contain this path
+            break;
+          }
+        }
+        if (j >= 0) { // 找到,包含它的parent,如果sign相等,则reverse
+          if (pathlist[i].sign == pathlist[j].sign) {
+            reversePath(pathlist[i]);
+            pathlist[i].sign = pathlist[i].sign == 1 ? -1 : 1;
+            // console.log('reverse path', pathlist[i], pathlist);
+          }
+        } else { // 没找到包含它的parent,说明是独立path,强制为逆时针
+          if (pathlist[i].sign == -1) {
+            reversePath(pathlist[i]);
+            pathlist[i].sign = 1;
+            // console.log('reverse path', pathlist[i], pathlist);
+          }
+        }
+      }
+    }
+
+
+    let self = this;
+
+    for (let seg of this.segments) {
+      calcSums(seg);
+      calcLon(seg);
+      bestPolygon(seg);  // 得到po,即多边形的顶点,依然是整形坐标的点,即原pt里的点的下标
+      adjustVertices(seg);  // 对顶点进行微调,调整后的顶点放在curve.vertex数据结构中,变成小数
+    }
+
+    let keys = Object.keys(this.pathMap);
+    for (let key of keys) {
+      let pathlist = this.pathMap[key];
+      for (let i = 0; i < pathlist.length; i++) {
+        let path = pathlist[i];
+        buildPathPolygon(path);
+        if (!isPathValid(path)) {
+          console.warn(`found a invalid path, po.length=${path.polygon.po.length} sign=${path.sign}, remove!`, path);
+          pathlist.splice(i, 1);
+          i--;
+          this.debugInfo.remvoeInvalidPaths++;
+          continue;
+        }
+        linePath(path);
+        curvePath(path, this.config.alphamax);
+      }
+      doReverse(pathlist);
+    }
+  }
+  
+  
+  getSVG(area: number, opt_type: string = null, pathOnly : boolean = false, color: string = 'black', mode: string = 'curve') {
+
+    function path(curve) {
+
+      function bezier(i) {
+        let b = 'C' + parseFloat((curve.c[i * 3 + 0].x).toFixed(3)) + ' ' +
+          parseFloat((curve.c[i * 3 + 0].y).toFixed(3)) + ',';
+        b += parseFloat((curve.c[i * 3 + 1].x).toFixed(3)) + ' ' +
+          parseFloat((curve.c[i * 3 + 1].y).toFixed(3)) + ',';
+        b += parseFloat((curve.c[i * 3 + 2].x).toFixed(3)) + ' ' +
+          parseFloat((curve.c[i * 3 + 2].y).toFixed(3)) + '';
+        return b;
+      }
+
+      function segment(i) {
+        let s = 'L' + parseFloat((curve.c[i * 3 + 1].x).toFixed(3)) + ' ' +
+          parseFloat((curve.c[i * 3 + 1].y).toFixed(3)) + ' ';
+        s += parseFloat((curve.c[i * 3 + 2].x).toFixed(3)) + ' ' +
+          parseFloat((curve.c[i * 3 + 2].y).toFixed(3)) + '';
+        return s;
+      }
+
+      let n = curve.n, i;
+      let p = 'M' + (curve.c[(n - 1) * 3 + 2].x).toFixed(3) + ' ' + (curve.c[(n - 1) * 3 + 2].y).toFixed(3) + '';
+      for (i = 0; i < n; i++) {
+        if (curve.tag[i] === "CURVE") {
+          p += bezier(i);
+        } else if (curve.tag[i] === "CORNER") {
+          p += segment(i);
+        }
+      }
+      return p;
+
+    }
+
+    function line(po) {
+      let p = 'M' + parseFloat(po[0].x.toFixed(3)) + ' ' + parseFloat(po[0].y.toFixed(3)) + 'L';
+      for (let i = 1; i < po.length; i++) {
+        p += parseFloat(po[i].x.toFixed(3)) + ' ' + parseFloat(po[i].y.toFixed(3)) + ' ';
+      }
+      p += 'Z'
+      return p;
+    }
+
+    
+    let w = this.width, h = this.height;
+    let pathlist = this.pathMap[area];
+    let len = pathlist.length;
+    let c, i, strokec, fillc, fillrule;
+
+    let pathStr = '';
+    if (opt_type === "curve") {
+      strokec = color ||  "black";
+      fillc = "none";
+      fillrule = '';
+    } else {
+      strokec = "none";
+      // strokec = color || "none";
+      fillc = color || "black";
+      fillrule = ' fill-rule="evenodd"';
+    }
+
+    pathStr += '<path stroke="' + strokec + '" fill="' + fillc + '"' + fillrule;
+
+    pathStr += ' d="';
+
+    if (mode == 'line') {
+      for (let p of pathlist) {
+        pathStr += line(p.line);
+      }
+    } else {
+      for (let p of pathlist) {
+        c = p.curve;
+        if (!c || !c.n ) {
+          console.warn("invalid curve ", pathlist[i]);
+          continue;
+        }
+        pathStr += path(c);
+      }
+    }
+    
+
+    pathStr += '"/>';
+
+
+    let svg = `<svg id="svg" version="1.1" width="${w}" height="${h}" xmlns="http://www.w3.org/2000/svg">${pathStr}</svg>`;
+
+    return pathOnly ? pathStr : svg;
+
+  }
+
+
+  getDebugInfo() {
+    return this.debugInfo;
+  }
+
+
+}

+ 65 - 0
zorro/src/app/lib/filler/common/fillarea.ts

@@ -0,0 +1,65 @@
+export default class FillArea {
+  color: number;
+  left: number;
+  top: number;
+  right: number;
+  bottom: number;
+  count: number = 1;
+  isColored: boolean = false; //todo delete
+  lastColor: number = 0; //最终颜色
+  lastCssColor: string = null;
+  cssColor: any;
+
+  /**
+   * Fill area of the specified color.
+   */
+  constructor(color, x, y) {
+    this.color = color;
+    this.left = this.right = x;
+    this.top = this.bottom = y;
+    this.lastColor = 0;  //最终的颜色
+  }
+
+  get colorRgba(): string { return FillArea.getRgbaColor(this.color) }
+
+  static getRgbaColor(colorInt) {
+    let a = new Uint32Array(1);
+    a[0] = colorInt;
+    let b = new Uint8Array(a.buffer);
+    return `rgba(${b[0]},${b[1]},${b[2]},${b[3] / 255})`;
+  }
+
+  setColored(color, cssColor) {
+    this.lastColor = color;
+    this.cssColor = cssColor;
+    this.isColored = true;
+  }
+
+  /**
+   * Add point to the fill area
+   */
+  addPoint(x, y) {
+    if (x < this.left) this.left = x;
+    if (x > this.right) this.right = x;
+    if (y < this.top) this.top = y;
+    if (y > this.bottom) this.bottom = y;
+    this.count++;
+  }
+
+  /**
+   * Width of the fill area
+   */
+  width() {
+    return (this.right - this.left + 1);
+  }
+
+
+  /**
+   * Height of the fill area.
+   */
+  height() {
+    return (this.bottom - this.top + 1);
+  }
+
+
+}

+ 37 - 0
zorro/src/app/lib/filler/common/filltask.ts

@@ -0,0 +1,37 @@
+export class FillTask {
+
+  type: any;
+  x : number; 
+  y : number;
+  color : any;
+  textureImage: any;
+  area: any;
+  areaIndex: any;
+  brush: any;
+  size: any;
+
+  constructor(type : FILL_TYPE, x ? : number, y ?:  number) {
+    this.type = type;
+  }
+
+
+  static undoTask() {
+    return new FillTask(FILL_TYPE.UNDO);
+  }
+
+  static redoTask() {
+    return new FillTask(FILL_TYPE.REDO);
+  }
+
+}
+
+export enum FILL_TYPE {
+  SOLID = 1,
+  LINEAR_GRADIENT = 2,
+  RADIAL_GRADIENT = 3,
+  DRAW = 4,
+  BRUSH = 4,
+  ERASE = 100,
+  UNDO = 1000,
+  REDO = 1001,
+}

+ 832 - 0
zorro/src/app/lib/filler/common/floodfill.ts

@@ -0,0 +1,832 @@
+import Utils from './utils';
+import FillArea from './fillarea';
+
+var _colors = {};
+
+export default class FloodFill {
+
+  private inputImage: HTMLImageElement;
+  private canvas: HTMLCanvasElement;
+  private ctx: CanvasRenderingContext2D;
+  private inputImageData: ImageData;
+  private floodFilled: ImageData;
+
+  constructor(image: HTMLImageElement, scale: number = 1) {
+    this.inputImage = image;
+    this.canvas = createCanvas(image.width, image.height);
+    this.ctx = this.canvas.getContext('2d');
+    Utils.setImageSmoothing(this.ctx, false);
+
+    let _canvas = createCanvas(image.width * scale, image.height * scale);
+    let _ctx = _canvas.getContext('2d');
+    Utils.setImageSmoothing(_ctx, false);
+    _ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, _canvas.width, _canvas.height);
+    this.inputImageData = _ctx.getImageData(0, 0, _canvas.width, _canvas.height);
+    this.sumColor(this.inputImageData);
+    this.floodFilled = createFloodFillImage(this.inputImageData, scale);
+    // img data to image.
+    _ctx.putImageData(this.floodFilled, 0, 0);
+
+    this.ctx.drawImage(_canvas, 0, 0, _canvas.width, _canvas.height, 0, 0, this.canvas.width, this.canvas.height);
+  }
+
+  sumColor(imgData: ImageData) {
+    let hash = {};
+
+    /*
+    let _pixels = new Uint8Array(imgData.data.buffer);
+    for (var i = 0; i < _pixels.length / 4; i++) {
+      if (_pixels[i * 4 + 3] < 128) {
+        _pixels[i * 4] = 0;
+        _pixels[i * 4 + 1] = 0;
+        _pixels[i * 4 + 2] = 0;
+        _pixels[i * 4 + 3] = 0;
+      }
+    }
+    */
+
+    let pixels = new Uint32Array(imgData.data.buffer);
+    for (var i = 0; i < pixels.length; i++) {
+      if (hash[pixels[i]]) hash[pixels[i]]++;
+      else hash[pixels[i]] = 1;
+    }
+
+    let res = Object.keys(hash).map((colorStr: string) => {
+      let color = parseInt(colorStr);
+      let count = hash[colorStr];
+      return {
+        //color: color.toString(16),
+        color: Utils.getColorFromInteger(color),
+        count
+      };
+    }).sort((a, b) => {
+      return b.count - a.count
+    })
+    console.log('colorSum', res);
+  }
+
+  getAreaCount(): number {
+    let hash: any = {};
+    let pixels = new Uint32Array(this.floodFilled.data.buffer);
+    for (var i = 0; i < pixels.length; i++) {
+      hash[pixels[i]] = 1;
+    }
+    return Object.keys(hash).length;
+  }
+
+  /**
+   * 输出图片
+   * @returns 
+   */
+  toImage(): Promise<HTMLImageElement> {
+    return new Promise((done, reject) => {
+      var img = new Image();
+      img.onload = function () { done(img); };
+      img.onerror = reject;
+      img.src = this.canvas.toDataURL('image/png');
+    });
+  }
+
+  /**
+   * 输出文件
+   * @returns 
+   */
+  toBlob(): Promise<Blob> {
+    return new Promise((done, reject) => {
+      this.canvas.toBlob((blob: Blob) => done(blob), 'image/png');
+    });
+  }
+
+
+
+
+
+  /**
+   * 创建map, 输出文件
+   * @param image 
+   * @returns 
+   */
+  static createMapBlob(image: HTMLImageElement): Promise<Blob> {
+    return new FloodFill(image).toBlob();
+  }
+
+
+  /**
+   * 创建MAP, 输出图片
+   * @param image 线稿图片
+   * @returns 
+   */
+  static createMapImage(image: HTMLImageElement): Promise<HTMLImageElement> {
+    return new FloodFill(image).toImage();
+  }
+
+
+}
+
+/**
+ * Create a fill map for image
+ * @param imgData image data.
+ */
+function createFloodFillImage(imgData, scale: number = 1) {
+  var ctx = createContext(imgData.width, imgData.height);
+  var width = imgData.width;
+  var height = imgData.height;
+
+  var original = imgData;
+  var floodFilled = copyImage(ctx, original);
+  console.time('createFloodFillImage');
+
+  // flood fill
+  console.time('floodFillAll');
+  floodFillAll(floodFilled);
+  console.timeEnd('floodFillAll');
+
+  // remove original pixels in dest
+  var srcArr = new Uint32Array(original.data.buffer);
+  var destArr = new Uint32Array(floodFilled.data.buffer);
+  for (var i = 0; i < srcArr.length; i++) {
+    if (srcArr[i] != 0) {
+      destArr[i] = 0;
+    }
+  }
+
+  // fill alpha gap
+  console.time('fillAlphaGap');
+  fillAlphaGap(original, floodFilled, 50 * scale);
+  console.timeEnd('fillAlphaGap');
+
+  console.time('mergeAreas');
+  mergeAreas(floodFilled);
+  console.timeEnd('mergeAreas');
+
+  // fill alpha gap
+  console.time('fillEmptyGap');
+  fillEmptyGap(floodFilled, 10 * scale);
+  console.timeEnd('fillEmptyGap');
+
+  console.timeEnd('createFloodFillImage');
+  return floodFilled;
+}
+
+/**
+ * Flood Fill image with random Color
+ */
+function floodFillAll(imgData) {
+  var width = imgData.width;
+  var height = imgData.height;
+  var pixels = new Uint32Array(imgData.data.buffer);
+  var x, y;
+  // var oldColor = new Uint8Array([0, 0, 0, 0]);
+  var oldColor = 0;
+  var newColor;
+  var fillFunc = floodFill;
+  // if(Math.random() < 0.5) fillFunc = floodFillEdge;
+  var maxStack = 0;
+  var stackSize;
+
+  // console.log('fillFunc: ' + fillFunc.name);
+  for (var i = 0; i < pixels.length; i++) {
+    if (pixels[i] == 0) {
+      x = i % width;
+      y = Math.floor(i / width);
+      newColor = randomColor();
+      // newColor = randomUniqueColor();
+      stackSize = fillFunc(imgData, x, y, oldColor, newColor);
+      if (stackSize > maxStack)
+        maxStack = stackSize;
+      // floodFill(imgData, x, y, oldColor, newColor);
+      // floodFillEdge(imgData, x, y, oldColor, newColor);
+    }
+  }
+  console.log('floodFillAll@maxStackSize', maxStack * 100 / (width * height));
+}
+
+/**
+ *
+ * Flood fill x,y.
+ * @param imgData ctx.getImageData()
+ * @param x
+ * @param y
+ * @param oldColor Uint8Array [r, g, b, a]
+ * @param oldColor Uint8Array [r, g, b, a]
+ * @param oldColor int
+ * @param newColor int
+ *
+ */
+function floodFill(imgData, x, y, oldColor, newColor) {
+  var width = imgData.width;
+  var height = imgData.height;
+  var stack = new Array();
+  var index = y * width + x;
+  var pixels = new Uint32Array(imgData.data.buffer);
+  // var oc = new Uint32Array(oldColor.buffer);
+  var oc = oldColor;
+  // var nc = new Uint32Array(newColor.buffer);
+  var nc = newColor;
+  // console.log('start flood fill: ', x, y, width, height, oldColor, newColor);
+  var maxStack = 0;
+
+  if (pixels[index] == oc) {
+    stack.push(index);
+  }
+
+  var p;
+  while (stack.length > 0) {
+    if (stack.length > maxStack)
+      maxStack = stack.length;
+    p = stack.pop();
+    x = p % width;
+
+    // if p is colored, continue
+    if (pixels[p] == nc)
+      continue;
+
+    pixels[p] = nc;
+    // left
+    if (x > 0 && pixels[p - 1] == oc) {
+      stack.push(p - 1);
+    }
+    // top
+    if (p > (width - 1) && pixels[p - width] == oc) {
+      stack.push(p - width);
+    }
+    // right
+    if (x < (width - 1) && pixels[p + 1] == oc) {
+      stack.push(p + 1);
+    }
+    // bottom
+    if (p < ((height - 1) * width) && pixels[p + width] == oc) {
+      stack.push(p + width);
+    }
+
+    // left top
+    if (x > 0 && p > (width -1) && pixels[p - width -1] == oc) {
+      stack.push(p - width -1);
+    }
+    // right top
+    if (x < (width -1) && p > (width -1) && pixels[p - width + 1] == oc) {
+      stack.push(p - width + 1);
+    }
+    // left bottom
+    if (x > 0 && p < ((height - 1) * width) && pixels[p + width -1] == oc) {
+      stack.push(p + width - 1);
+    }
+    // rigth bottom
+    if (x < (width - 1) && p < ((height - 1) * width) && pixels[p + width + 1] == oc) {
+      stack.push(p + width + 1);
+    }
+  }
+  // console.log('floodFill@maxStack', maxStack / (width*height));
+  return maxStack;
+}
+
+/**
+ *
+ * Flood fill x,y.
+ * @param imgData ctx.getImageData()
+ * @param x
+ * @param y
+ * @param oldColor Uint8Array [r, g, b, a]
+ * @param newColor Uint8Array [r, g, b, a]
+ *
+ */
+function floodFillEdge(imgData, x, y, oldColor, newColor) {
+  var width = imgData.width;
+  var height = imgData.height;
+  var stack = new Array();
+  var index = y * width + x;
+  var pixels = new Uint32Array(imgData.data.buffer);
+  var oc = new Uint32Array(oldColor.buffer);
+  var nc = new Uint32Array(newColor.buffer);
+  // console.log('start flood fill: ', x, y, width, height, oldColor, newColor);
+
+  // edges
+  var estack = new Array();
+
+  if (pixels[index] == oc[0]) {
+    stack.push(index);
+  }
+
+  var p;
+  while (stack.length > 0) {
+    p = stack.pop();
+    x = p % width;
+
+    // if p is colored, continue
+    if (pixels[p] == nc[0])
+      continue;
+
+    pixels[p] = nc[0];
+
+    // left
+    if (x > 0 && pixels[p - 1] == oc[0]) {
+      stack.push(p - 1);
+    } else if (x > 0 && pixels[p - 1] != oc[0]) {
+      estack.push(p - 1);
+    }
+
+    // top
+    if (p > (width - 1) && pixels[p - width] == oc[0]) {
+      stack.push(p - width);
+    } else if (p > (width - 1) && pixels[p - width] != oc[0]) {
+      estack.push(p - width);
+    }
+
+    // right
+    if (x < (width - 1) && pixels[p + 1] == oc[0]) {
+      stack.push(p + 1);
+    } else if (x < (width - 1) && pixels[p + 1] != oc[0]) {
+      estack.push(p + 1);
+    }
+
+    // bottom
+    if (p < ((height - 1) * width) && pixels[p + width] == oc[0]) {
+      stack.push(p + width);
+    } else if (p < ((height - 1) * width) && pixels[p + width] != oc[0]) {
+      estack.push(p + width);
+    }
+  }
+
+  var r, g, b, a, t;
+  var curPixel = new Uint8Array(4);
+  var curPixel32 = new Uint32Array(curPixel.buffer);
+
+  // fill edges.
+  while (estack.length > 0) {
+    p = estack.pop();
+    // if p is alread colored.
+    if (pixels[p] == nc[0])
+      continue;
+    // if p is oldColor, enter a new fill area, continue
+    if (pixels[p] == oc[0])
+      continue;
+    // calculate p's distance witha oldColor
+    curPixel32[0] = pixels[p];
+    t = Math.sqrt(Math.pow(oldColor[0] - curPixel[0], 2) +
+      Math.pow(oldColor[1] - curPixel[1], 2) +
+      Math.pow(oldColor[2] - curPixel[2], 2) +
+      Math.pow(oldColor[3] - curPixel[3], 2));
+
+    if (t > 128)
+      continue;
+    pixels[p] = nc[0];
+
+    // left
+    if (x > 0) {
+      estack.push(p - 1);
+    }
+
+    // top
+    if (p > (width - 1)) {
+      estack.push(p - width);
+    }
+
+    // right
+    if (x < (width - 1)) {
+      estack.push(p + 1);
+    }
+
+    if (p < ((height - 1) * width)) {
+      estack.push(p + width);
+    }
+  }
+}
+
+/**
+ *  Build neighbourhood for areas.
+ *  @param floodArr
+ *  @param areaMap
+ *  @param area which need to build it's neighbours
+ */
+/*
+function buildNeighbour(floodArr, areaMap, area) { var areaMap = {}; }
+*/
+
+function createFillAreaMap(floodImg) {
+  var width = floodImg.width;
+  var height = floodImg.height;
+  var floodArr = new Uint32Array(floodImg.data.buffer);
+  var areaMap = {};
+  for (var i = 0; i < floodArr.length; i++) {
+    var color = floodArr[i];
+    var x = i % width;
+    var y = Math.floor(i / width);
+    if (areaMap[color]) {
+      areaMap[color].addPoint(x, y);
+    } else {
+      areaMap[color] = new FillArea(color, x, y);
+    }
+  }
+  return areaMap;
+}
+
+function mergeAreas(floodFilledImage, threshold?) {
+  const kk = (floodFilledImage.width / 1000) * 20;
+  threshold = threshold || kk;
+  var areaMap = createFillAreaMap(floodFilledImage);
+  var keys = Object.keys(areaMap);
+  var floodArr = new Uint32Array(floodFilledImage.data.buffer);
+  var width = floodFilledImage.width;
+  var height = floodFilledImage.height;
+  var areas = keys.map(function (key) { return areaMap[key]; });
+  var smallAreas = areas.filter(function (a) { return a.count < threshold; });
+
+  console.log('mergeAreas@areaSize', areas.length, smallAreas.length);
+  var i, j, x, y, index, p, px, py;
+  var neighbours = getNearestNeighbours(1);
+  smallAreas.forEach(function (area) {
+    // area.neighbours = [];
+    let neighbourSet = new Set();
+    for (x = area.left; x <= area.right; x++) {
+      for (y = area.top; y <= area.bottom; y++) {
+        index = y * width + x;
+        if (floodArr[index] == area.color) {
+          // check pixe's neighbour
+          for (j = 0; j < neighbours.length; j++) {
+            p = neighbours[j];
+            px = x + p.x;
+            py = y + p.y;
+            index = py * width + px;
+            if (px > 0 && px < width && py > 0 && py < height &&
+              floodArr[index] != 0 && floodArr[index] != area.color) {
+              // area.neighbours.push(areaMap[floodArr[index]]);
+              neighbourSet.add(areaMap[floodArr[index]]);
+            }
+          }
+        }
+      }
+    }
+    area.neighbours = Array.from(neighbourSet);
+  });
+
+  // remove the area has no neighbours.
+  smallAreas =
+    smallAreas.filter(function (area) { return area.neighbours.length > 0; });
+  // console.log('mergeAreas@areaSize', areas.length, smallAreas.length);
+
+  // for each samll area.
+  // get it's direct neighbour area list
+  // select the biggest one, and merge with it.
+  // if no neighbours mark it can't merge.
+
+  /**
+   * merge another area
+   */
+  function mergeToMe(me, other) {
+    // if i am a small area.
+    // merge other's neighbours
+    if (me.neighbours) {
+      me.neighbours =
+        me.neighbours.concat(other.neighbours)
+          .filter(function (a) { return a != me && a != other; });
+    }
+
+    // set other merged
+    other.merged = true;
+
+    for (x = other.left; x <= other.right; x++) {
+      for (y = other.top; y <= other.bottom; y++) {
+        index = y * width + x;
+        if (floodArr[index] == other.color) {
+          floodArr[index] = me.color;
+          me.addPoint(x, y);
+        }
+      }
+    }
+  }
+
+  var loop = 0;
+  do {
+    loop++;
+
+    smallAreas.forEach(function (area) {
+      if (area.merged)
+        return;
+      var neighbour;
+      for (var i = 0; i < area.neighbours.length; i++) {
+        // merge other small areas.
+        neighbour = area.neighbours[i];
+        // if neighbour is merged or neighbour is a large one
+        if (neighbour.merged || !neighbour.neighbours) {
+          continue;
+        }
+        mergeToMe(area, neighbour);
+      }
+      if (area.neighbours.length > 0) {
+        // merge me to the first large neighbour
+        mergeToMe(area.neighbours[0], area);
+      }
+    });
+
+    smallAreas = smallAreas.filter(function (
+      area) { return !area.merged && area.neighbours.length > 0; });
+
+    console.log('mergeAreas@loop:' + loop, smallAreas.length);
+
+    if (smallAreas.length == 0)
+      break;
+
+  } while (loop < 4);
+}
+
+/**
+ * Fill blank pixels with color.
+ */
+function fillEmptyGap(floodFilledImage, maxDistance) {
+  var floodArr = new Uint32Array(floodFilledImage.data.buffer);
+  var width = floodFilledImage.width;
+  var height = floodFilledImage.height;
+  var emptyPoints = []; // x,y,x,y,x,y......
+  var unFilledPoints = [];
+  var taskArr = []; // index, color, index, color
+  var i, j, x, y, index, color, p, px, py;
+
+  var neighbours = getNearestNeighbours(1);
+
+  // get all empty points
+  for (x = 0; x < width; x++) {
+    for (y = 0; y < height; y++) {
+      index = y * width + x;
+      if (floodArr[index] == 0) {
+        emptyPoints.push(x, y);
+      }
+    }
+  }
+
+  console.log('FillEmptyGap#emptyPoints@length', emptyPoints.length / 2);
+  var loop = 0;
+
+  do {
+    loop++;
+
+    // loop all alpha points and try to fill.
+    var found;
+    for (i = 0; i < emptyPoints.length; i += 2) {
+      x = emptyPoints[i];
+      y = emptyPoints[i + 1];
+      found = false;
+      for (j = 0; j < neighbours.length; j++) {
+        p = neighbours[j];
+        px = x + p.x;
+        py = y + p.y;
+        index = py * width + px;
+        if (px > 0 && px < width && py > 0 && py < height &&
+          floodArr[index] != 0) {
+          taskArr.push(y * width + x, floodArr[index]);
+          found = true;
+          break;
+        }
+      }
+      if (!found)
+        unFilledPoints.push(x, y);
+    }
+
+    console.log('fillEmptyGap@loop:' + loop, emptyPoints.length,
+      taskArr.length / 2, unFilledPoints.length / 2);
+
+    // no more alpha pixels can find it's neighbours.
+    if (taskArr.length == 0)
+      break;
+
+    // do the task
+    for (i = 0; i < taskArr.length; i += 2) {
+      index = taskArr[i];
+      color = taskArr[i + 1];
+      floodArr[index] = color;
+    }
+
+    taskArr = [];
+    emptyPoints = unFilledPoints;
+    unFilledPoints = [];
+
+  } while (loop <= maxDistance);
+}
+
+/**
+ * Fill pixels in srcImage with alpha<255
+ * with the nearest filled color
+ * @param srcImage source ImageData  ojbect.
+ * @param floodFilledImage flood filled ImageData object.
+ * @param maxDistance
+ */
+function fillAlphaGap(srcImage, floodFilledImage, maxDistance) {
+  maxDistance = maxDistance || 2;
+  var srcArr = new Uint32Array(srcImage.data.buffer);
+  var srcBuf = srcImage.data; // to get the alpha directly
+  var floodArr = new Uint32Array(floodFilledImage.data.buffer);
+  var width = srcImage.width;
+  var height = srcImage.height;
+  var alphaPoints = []; // x,y,x,y,x,y......
+  var unFilledPoints = [];
+  var taskArr = []; // index, color, index, color
+  var i, j, x, y, index, color, p, px, py;
+
+  var neighbours = getNearestNeighbours(1);
+
+  // get all alpha points
+  for (x = 0; x < width; x++) {
+    for (y = 0; y < height; y++) {
+      index = y * width + x;
+      j = index * 4 + 3; // alpha byte index
+      if (floodArr[index] == 0 && srcBuf[j] < 255) {
+        alphaPoints.push(x, y);
+      }
+    }
+  }
+
+  // console.log('neighbours:', neighbours);
+  // console.log('alphaPoints@length', alphaPoints.length / 2);
+  var loop = 0;
+
+  do {
+    loop++;
+
+    // loop all alpha points and try to fill.
+    var found;
+    for (i = 0; i < alphaPoints.length; i += 2) {
+      x = alphaPoints[i];
+      y = alphaPoints[i + 1];
+      found = false;
+      for (j = 0; j < neighbours.length; j++) {
+        p = neighbours[j];
+        px = x + p.x;
+        py = y + p.y;
+        index = py * width + px;
+        if (px > 0 && px < width && py > 0 && py < height &&
+          floodArr[index] != 0) {
+          taskArr.push(y * width + x, floodArr[index]);
+          found = true;
+          break;
+        }
+      }
+      if (!found)
+        unFilledPoints.push(x, y);
+    }
+
+    console.log('fillAlphaGap@loop:' + loop, alphaPoints.length,
+      taskArr.length / 2, unFilledPoints.length / 2);
+
+    // no more alpha pixels can find it's neighbours.
+    if (taskArr.length == 0)
+      break;
+
+    // do the task
+    for (i = 0; i < taskArr.length; i += 2) {
+      index = taskArr[i];
+      color = taskArr[i + 1];
+      floodArr[index] = color;
+    }
+
+    taskArr = [];
+    alphaPoints = unFilledPoints;
+    unFilledPoints = [];
+
+  } while (loop <= maxDistance);
+}
+
+/**
+ * Get the nearest N neighbours
+ * and sort by distance.
+ * @param n integer >=1
+ */
+function getNearestNeighbours(n) {
+  var distArr = [];
+  for (var i = (-1) * n; i <= n; i++) {
+    for (var j = (-1) * n; j <= n; j++) {
+      if (i == 0 && j == 0) {
+        // exclued self
+        continue;
+      }
+      var distance = Math.pow(i, 2) + Math.pow(j, 2);
+      distArr.push({ x: i, y: j, dist: distance });
+    }
+  }
+
+  distArr.sort(function (a, b) { return a.dist - b.dist; });
+
+  return distArr;
+}
+
+function fillGapOld(ctx, floodFilled) {
+  // create gap bitmap
+  var gapImg = ctx.createImageData(floodFilled);
+  var width = floodFilled.width;
+  var height = floodFilled.height;
+  var maxDistance = 2;
+  var floodArr = new Uint32Array(floodFilled.data.buffer);
+  var gapArr = new Uint32Array(gapImg.data.buffer);
+
+  // Build a nearest points array based on (0, 0)
+  // and sort it by it's distance to (0,0)
+  var distArr = [];
+  for (var i = (-1) * maxDistance; i <= maxDistance; i++) {
+    for (var j = (-1) * maxDistance; j <= maxDistance; j++) {
+      if (i != 0 && j != 0) {
+        var distance = Math.pow(i, 2) + Math.pow(j, 2);
+        distArr.push({ x: i, y: j, dist: distance });
+      }
+    }
+  }
+
+  distArr.sort(function (a, b) { return a.dist - b.dist; });
+
+  // for every gap pixels in flood image, find the nearest colored pixel
+  // and set as it's color.
+  var i: number, j: number, x: number, y: number, px: number, py: number;
+  for (i = 0; i < floodArr.length; i++) {
+    if (floodArr[i] == 0) {
+      x = i % width;
+      y = Math.floor(i / width);
+      /*
+      var arr = distArr.map(function(obj){
+        return {
+          x : obj.x + x,
+          y : obj.y + y
+        }
+      });
+      */
+      for (j = 0; j < distArr.length; j++) {
+        var p = distArr[j];
+        px = x + p.x;
+        py = y + p.y;
+
+        // if(p.x > 0 && p.x < width && p.y > 0 && p.y < height &&
+        // floodArr[p.y*width+p.x] !=0 ) {
+        if (px > 0 && px < width && py > 0 && py < height &&
+          floodArr[py * width + px] != 0) {
+          gapArr[y * width + x] = floodArr[py * width + px];
+          break;
+        }
+      }
+      // this is the gap
+      // find nearest filled pixel
+    }
+  }
+
+  // merge the gap bitmap with the flood bitmap
+  for (var i = 0; i < floodArr.length; i++) {
+    if (floodArr[i] == 0) {
+      floodArr[i] = gapArr[i];
+    }
+  }
+}
+
+/**
+ *  Copy image data from src
+ *  @param ctx  canvas context
+ *  @param src  source image data.
+ */
+function copyImage(ctx, src) {
+  var dest = ctx.createImageData(src);
+  var srcArr = new Uint32Array(src.data.buffer);
+  var destArr = new Uint32Array(dest.data.buffer);
+  for (var i = 0; i < srcArr.length; i++) {
+    destArr[i] = srcArr[i];
+  }
+  return dest;
+}
+
+/**
+ * Create canvas context by specified dimension.
+ */
+function createContext(width, height) {
+  return createCanvas(width, height).getContext('2d');
+}
+
+/**
+ * Create canvas
+ * @param width
+ * @param height
+ */
+function createCanvas(width, height) {
+  var canvas = document.createElement('canvas');
+  canvas.width = width;
+  canvas.height = height;
+  return canvas;
+}
+
+function randomUniqueColor() {
+  do {
+    var color = randomColor();
+    if (!_colors[color]) {
+      _colors[color] = color;
+      return color;
+    }
+  } while (1)
+  return randomColor();
+}
+
+function randomColor() {
+  var color = new Uint8Array([0, 0, 0, 255]);
+  for (var i = 0; i < 3; i++) {
+    color[i] = Math.floor(Math.random() * 256);
+  }
+  var u32 = new Uint32Array(color.buffer);
+  return u32[0];
+}
+
+function emptyColor() {
+  var color = new Uint8Array([255, 255, 255, 255]);
+  var a = new Uint32Array(color.buffer);
+  return a[0];
+}

+ 1025 - 0
zorro/src/app/lib/filler/common/floodfill2.ts

@@ -0,0 +1,1025 @@
+import Utils from './utils';
+import FillArea from './fillarea';
+import RandomColor from './random-color';
+
+var _colors = {};
+var colors: RandomColor;
+
+// 增加以下几个字段,是为了解决bug, 如果是已经有了map图,需要区块分割, 需要参考下以前map图,避免颜色不够用
+var mapPixels: Uint32Array = null;  // 原有map图pixels,首次创建的orignalMap为空
+var useColors: Set<number> = null;  // 记录已使用的颜色
+
+export default class FloodFill2 {
+
+  private inputImage: HTMLImageElement;
+  private canvas: HTMLCanvasElement;
+  private ctx: CanvasRenderingContext2D;
+  private inputImageData: ImageData;
+  private floodFilled: ImageData;
+
+
+  constructor(image: HTMLImageElement, map: HTMLImageElement = null, scale: number = 1, extcolors: number[] = []) {
+    colors = new RandomColor(3000, extcolors);
+    useColors = new Set();
+    mapPixels = null;
+    if (map) {
+      let mapData = Utils.getImageData(map);
+      mapPixels = new Uint32Array(mapData.data.buffer);
+    }
+    this.inputImage = image;
+    this.canvas = createCanvas(image.width, image.height);
+    this.ctx = this.canvas.getContext('2d');
+    Utils.setImageSmoothing(this.ctx, false);
+
+    let _canvas = createCanvas(image.width * scale, image.height * scale);
+    let _ctx = _canvas.getContext('2d');
+    Utils.setImageSmoothing(_ctx, false);
+    _ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, _canvas.width, _canvas.height);
+    this.inputImageData = _ctx.getImageData(0, 0, _canvas.width, _canvas.height);
+    this.sumColor(this.inputImageData);
+    this.floodFilled = createFloodFillImage(this.inputImageData, scale);
+    // img data to image.
+    _ctx.putImageData(this.floodFilled, 0, 0);
+
+    this.ctx.drawImage(_canvas, 0, 0, _canvas.width, _canvas.height, 0, 0, this.canvas.width, this.canvas.height);
+  }
+
+  sumColor(imgData: ImageData) {
+    let hash = {};
+
+    /*
+    let _pixels = new Uint8Array(imgData.data.buffer);
+    for (var i = 0; i < _pixels.length / 4; i++) {
+      if (_pixels[i * 4 + 3] < 128) {
+        _pixels[i * 4] = 0;
+        _pixels[i * 4 + 1] = 0;
+        _pixels[i * 4 + 2] = 0;
+        _pixels[i * 4 + 3] = 0;
+      }
+    }
+    */
+
+    let pixels = new Uint32Array(imgData.data.buffer);
+    for (var i = 0; i < pixels.length; i++) {
+      if (hash[pixels[i]]) hash[pixels[i]]++;
+      else hash[pixels[i]] = 1;
+    }
+
+    let res = Object.keys(hash).map((colorStr: string) => {
+      let color = parseInt(colorStr);
+      let count = hash[colorStr];
+      return {
+        //color: color.toString(16),
+        color: Utils.getColorFromInteger(color),
+        count
+      };
+    }).sort((a, b) => {
+      return b.count - a.count
+    })
+    console.log('colorSum', res);
+  }
+
+  getAreaCount(): number {
+    let hash: any = {};
+    let pixels = new Uint32Array(this.floodFilled.data.buffer);
+    for (var i = 0; i < pixels.length; i++) {
+      hash[pixels[i]] = 1;
+    }
+    return Object.keys(hash).length;
+  }
+
+  /**
+   * 输出图片
+   * @returns 
+   */
+  toImage(): Promise<HTMLImageElement> {
+    return new Promise((done, reject) => {
+      var img = new Image();
+      img.onload = function () { done(img); };
+      img.onerror = reject;
+      img.src = this.canvas.toDataURL('image/png');
+    });
+  }
+
+  /**
+   * 输出文件
+   * @returns 
+   */
+  toBlob(): Promise<Blob> {
+    return new Promise((done, reject) => {
+      this.canvas.toBlob((blob: Blob) => done(blob), 'image/png');
+    });
+  }
+
+
+
+
+
+  /**
+   * 创建map, 输出文件
+   * @param image 
+   * @returns 
+   */
+  static createMapBlob(image: HTMLImageElement, map: HTMLImageElement, scale: number = 1, extcolors: number[] = []): Promise<Blob> {
+    return new FloodFill2(image, map, scale, extcolors).toBlob();
+  }
+
+
+  /**
+   * 创建MAP, 输出图片
+   * @param image 线稿图片
+   * @returns 
+   */
+  static createMapImage(image: HTMLImageElement, map: HTMLImageElement, scale: number = 1, extcolors: number[] = []): Promise<HTMLImageElement> {
+    return new FloodFill2(image, map, scale, extcolors).toImage();
+  }
+
+
+}
+
+// 判断某点是否边缘点
+function isEdge(x, y, w, h): boolean {
+  if (x < 2 || x >= w - 2 || y < 2 || y >= h - 2) return true;
+  else return false;
+}
+
+/**
+ * Create a fill map for image
+ * @param imgData image data.
+ */
+function createFloodFillImage(imgData, scale: number = 1) {
+  var ctx = createContext(imgData.width, imgData.height);
+  var width = imgData.width;
+  var height = imgData.height;
+
+  var original = imgData;
+  var floodFilled = copyImage(ctx, original);
+  console.time('createFloodFillImage');
+
+  // 在floodfill之前先把alpha<128的边缘线条点处理成空白,这样做可以有效消除小区块
+  setLowAlphaToEmpty(original, floodFilled);
+
+  // flood fill
+  console.time('floodFillAll');
+  floodFillAll(floodFilled);
+  console.timeEnd('floodFillAll');
+
+  // 剩余的高透明度(alpha>=128)线条点(alpha<128的前面已经处理过了)置为空白,准备进行进一步的fill操作
+  setHightAlphaToEmpty(original, floodFilled);
+
+  // 在这里先merge一轮小区块
+  console.time('mergeAreas');
+  mergeAreas(floodFilled);
+  console.timeEnd('mergeAreas');
+
+  // fill alpha gap
+  console.time('fillAlphaGap');
+  fillAlphaGap(original, floodFilled, 50 * scale);
+  console.timeEnd('fillAlphaGap');
+
+  console.time('mergeAreas');
+  mergeAreas(floodFilled);
+  console.timeEnd('mergeAreas');
+
+  // fill alpha gap
+  console.time('fillEmptyGap');
+  fillEmptyGap(floodFilled, 10 * scale);
+  console.timeEnd('fillEmptyGap');
+
+  console.timeEnd('createFloodFillImage');
+  return floodFilled;
+}
+
+/**
+ * 将线条像素化后alpha < 128 的过度点置为空
+ * @param srcImage source ImageData  ojbect.
+ * @param floodFilledImage flood filled ImageData object.
+ */
+function setLowAlphaToEmpty(srcImage, floodFilledImage) {
+  var srcBuf = srcImage.data; // to get the alpha directly
+  var floodArr = new Uint32Array(floodFilledImage.data.buffer);
+  var width = srcImage.width;
+  var height = srcImage.height;
+  var j, x, y, index;
+
+  // set all alpha points to empty
+  for (x = 0; x < width; x++) {
+    for (y = 0; y < height; y++) {
+      if (isEdge(x, y, width, height)) continue;  // 如果是边缘点不做此处理,主要是为了解决边缘虚化区块容易无法很好分隔的问题 
+      index = y * width + x;
+      j = index * 4 + 3; // alpha byte index
+      if (srcBuf[j] < 128) { // 颜色比较浅的才认为是空白,比较深的还当作线条
+        floodArr[index] = 0;
+      }
+    }
+  }
+}
+
+/**
+ * 将线条像素化后alpha >= 128 的点置为空
+ * @param srcImage source ImageData  ojbect.
+ * @param floodFilledImage flood filled ImageData object.
+ */
+function setHightAlphaToEmpty(srcImage, floodFilledImage) {
+  var srcBuf = srcImage.data; // to get the alpha directly
+  var floodArr = new Uint32Array(floodFilledImage.data.buffer);
+  var width = srcImage.width;
+  var height = srcImage.height;
+  var j, x, y, index;
+
+  // set all alpha points to empty
+  for (x = 0; x < width; x++) {
+    for (y = 0; y < height; y++) {
+      index = y * width + x;
+      j = index * 4 + 3; // alpha byte index
+      if (srcBuf[j] >= 128 || (srcBuf[j] > 0 && isEdge(x, y, width, height))) {  //别忘了边缘点
+        floodArr[index] = 0;
+      }
+    }
+  }
+}
+
+/**
+ * Flood Fill image with random Color
+ */
+function floodFillAll(imgData) {
+  var width = imgData.width;
+  var height = imgData.height;
+  var pixels = new Uint32Array(imgData.data.buffer);
+  var x, y;
+  // var oldColor = new Uint8Array([0, 0, 0, 0]);
+  var oldColor = 0;
+  var newColor;
+  // var fillFunc = floodFill;
+  var fillFunc = floodFillUseSet;
+  // if(Math.random() < 0.5) fillFunc = floodFillEdge;
+  var maxStack = 0;
+  var stackSize;
+
+  // console.log('fillFunc: ' + fillFunc.name);
+  for (var i = 0; i < pixels.length; i++) {
+    if (pixels[i] == 0) {
+      x = i % width;
+      y = Math.floor(i / width);
+      // newColor = randomColor();
+      // newColor = randomUniqueColor();
+
+      // 如果有参考map图,颜色选择优先与原map图保持一致
+      newColor = null;
+      if (mapPixels) {
+        newColor = mapPixels[i];
+        // 如果此颜色已经用过了,不能再用, 需要重新生成
+        if (useColors.has(newColor)) {
+          newColor = null;
+        }
+      }
+      // 没有参考map图或者颜色已经使用过的情况
+      if (!newColor) {
+        newColor = randomDistanceColor();
+      }
+      // 将此color加入集合
+      if (newColor) {
+        useColors.add(newColor);
+      } else {
+        console.warn("no new color, should not happen!")
+      }
+
+
+      stackSize = fillFunc(imgData, x, y, oldColor, newColor);
+      if (stackSize > maxStack)
+        maxStack = stackSize;
+      // floodFill(imgData, x, y, oldColor, newColor);
+      // floodFillEdge(imgData, x, y, oldColor, newColor);
+      console.log('floodFillAll@i', i);
+      if (i == 6646622) {
+        console.log("pause");
+      }
+    }
+  }
+  console.log('floodFillAll@maxStackSize', maxStack * 100 / (width * height));
+}
+
+/**
+ *
+ * Flood fill x,y.
+ * @param imgData ctx.getImageData()
+ * @param x
+ * @param y
+ * @param oldColor Uint8Array [r, g, b, a]
+ * @param oldColor Uint8Array [r, g, b, a]
+ * @param oldColor int
+ * @param newColor int
+ *
+ */
+function floodFill(imgData, x, y, oldColor, newColor) {
+  var width = imgData.width;
+  var height = imgData.height;
+  var stack = new Array();
+  var index = y * width + x;
+  var pixels = new Uint32Array(imgData.data.buffer);
+  // var oc = new Uint32Array(oldColor.buffer);
+  var oc = oldColor;
+  // var nc = new Uint32Array(newColor.buffer);
+  var nc = newColor;
+  // console.log('start flood fill: ', x, y, width, height, oldColor, newColor);
+  var maxStack = 0;
+
+  if (pixels[index] == oc) {
+    stack.push(index);
+  }
+
+  var p;
+  while (stack.length > 0) {
+    if (stack.length > maxStack)
+      maxStack = stack.length;
+    p = stack.pop();
+    x = p % width;
+
+    // if p is colored, continue
+    if (pixels[p] == nc)
+      continue;
+
+    pixels[p] = nc;
+    // left
+    if (x > 0 && pixels[p - 1] == oc) {
+      try {
+        stack.push(p - 1);
+      } catch (e) {
+        console.log(e);
+      }
+    }
+    // top
+    if (p > (width - 1) && pixels[p - width] == oc) {
+      try {
+        stack.push(p - width);
+      } catch (e) {
+        console.log(e);
+      }
+    }
+    // right
+    if (x < (width - 1) && pixels[p + 1] == oc) {
+      try {
+        stack.push(p + 1);
+      } catch (e) {
+        console.log(e);
+      }
+    }
+    // bottom
+    if (p < ((height - 1) * width) && pixels[p + width] == oc) {
+      try {
+        stack.push(p + width);
+      } catch (e) {
+        console.log(stack.length);
+        console.log(e);
+      }
+    }
+
+    // 注释掉,仍然采用4个点,以避免线条较薄的情况两个相邻区块搅和在一起
+    // // left top
+    // if (x > 0 && p > (width -1) && pixels[p - width -1] == oc) {
+    //   stack.push(p - width -1);
+    // }
+    // // right top
+    // if (x < (width -1) && p > (width -1) && pixels[p - width + 1] == oc) {
+    //   stack.push(p - width + 1);
+    // }
+    // // left bottom
+    // if (x > 0 && p < ((height - 1) * width) && pixels[p + width -1] == oc) {
+    //   stack.push(p + width - 1);
+    // }
+    // // rigth bottom
+    // if (x < (width - 1) && p < ((height - 1) * width) && pixels[p + width + 1] == oc) {
+    //   stack.push(p + width + 1);
+    // }
+  }
+  // console.log('floodFill@maxStack', maxStack / (width*height));
+  return maxStack;
+}
+
+
+/**
+ * 原来的floodFille函数stack使用数组,数组里有大量重复元素,导致越界,改造成使用set集合
+ * Flood fill x,y.
+ * @param imgData ctx.getImageData()
+ * @param x
+ * @param y
+ * @param oldColor Uint8Array [r, g, b, a]
+ * @param oldColor Uint8Array [r, g, b, a]
+ * @param oldColor int
+ * @param newColor int
+ *
+ */
+function floodFillUseSet(imgData, x, y, oldColor, newColor) {
+  var width = imgData.width;
+  var height = imgData.height;
+  var stack = new Set();
+  var index = y * width + x;
+  var pixels = new Uint32Array(imgData.data.buffer);
+  // var oc = new Uint32Array(oldColor.buffer);
+  var oc = oldColor;
+  // var nc = new Uint32Array(newColor.buffer);
+  var nc = newColor;
+  // console.log('start flood fill: ', x, y, width, height, oldColor, newColor);
+  var maxStack = 0;
+
+  if (pixels[index] == oc) {
+    stack.add(index);
+  }
+
+  var p;
+  while (stack.size > 0) {
+    if (stack.size > maxStack)
+      maxStack = stack.size;
+    // p = stack.pop();
+    for (const v of stack.values()) {
+      p = v;
+      break;
+    }
+    stack.delete(p);
+
+    x = p % width;
+
+    // if p is colored, continue
+    if (pixels[p] == nc)
+      continue;
+
+    pixels[p] = nc;
+    // left
+    if (x > 0 && pixels[p - 1] == oc) {
+      stack.add(p - 1);
+    }
+    // top
+    if (p > (width - 1) && pixels[p - width] == oc) {
+      stack.add(p - width);
+    }
+    // right
+    if (x < (width - 1) && pixels[p + 1] == oc) {
+      stack.add(p + 1);
+    }
+    // bottom
+    if (p < ((height - 1) * width) && pixels[p + width] == oc) {
+      stack.add(p + width);
+    }
+  }
+  console.log('floodFill@maxStack', maxStack, maxStack / (width * height));
+  return maxStack;
+}
+
+
+
+/**
+ *
+ * Flood fill x,y.
+ * @param imgData ctx.getImageData()
+ * @param x
+ * @param y
+ * @param oldColor Uint8Array [r, g, b, a]
+ * @param newColor Uint8Array [r, g, b, a]
+ *
+ */
+function floodFillEdge(imgData, x, y, oldColor, newColor) {
+  var width = imgData.width;
+  var height = imgData.height;
+  var stack = new Array();
+  var index = y * width + x;
+  var pixels = new Uint32Array(imgData.data.buffer);
+  var oc = new Uint32Array(oldColor.buffer);
+  var nc = new Uint32Array(newColor.buffer);
+  // console.log('start flood fill: ', x, y, width, height, oldColor, newColor);
+
+  // edges
+  var estack = new Array();
+
+  if (pixels[index] == oc[0]) {
+    stack.push(index);
+  }
+
+  var p;
+  while (stack.length > 0) {
+    p = stack.pop();
+    x = p % width;
+
+    // if p is colored, continue
+    if (pixels[p] == nc[0])
+      continue;
+
+    pixels[p] = nc[0];
+
+    // left
+    if (x > 0 && pixels[p - 1] == oc[0]) {
+      stack.push(p - 1);
+    } else if (x > 0 && pixels[p - 1] != oc[0]) {
+      estack.push(p - 1);
+    }
+
+    // top
+    if (p > (width - 1) && pixels[p - width] == oc[0]) {
+      stack.push(p - width);
+    } else if (p > (width - 1) && pixels[p - width] != oc[0]) {
+      estack.push(p - width);
+    }
+
+    // right
+    if (x < (width - 1) && pixels[p + 1] == oc[0]) {
+      stack.push(p + 1);
+    } else if (x < (width - 1) && pixels[p + 1] != oc[0]) {
+      estack.push(p + 1);
+    }
+
+    // bottom
+    if (p < ((height - 1) * width) && pixels[p + width] == oc[0]) {
+      stack.push(p + width);
+    } else if (p < ((height - 1) * width) && pixels[p + width] != oc[0]) {
+      estack.push(p + width);
+    }
+  }
+
+  var r, g, b, a, t;
+  var curPixel = new Uint8Array(4);
+  var curPixel32 = new Uint32Array(curPixel.buffer);
+
+  // fill edges.
+  while (estack.length > 0) {
+    p = estack.pop();
+    // if p is alread colored.
+    if (pixels[p] == nc[0])
+      continue;
+    // if p is oldColor, enter a new fill area, continue
+    if (pixels[p] == oc[0])
+      continue;
+    // calculate p's distance witha oldColor
+    curPixel32[0] = pixels[p];
+    t = Math.sqrt(Math.pow(oldColor[0] - curPixel[0], 2) +
+      Math.pow(oldColor[1] - curPixel[1], 2) +
+      Math.pow(oldColor[2] - curPixel[2], 2) +
+      Math.pow(oldColor[3] - curPixel[3], 2));
+
+    if (t > 128)
+      continue;
+    pixels[p] = nc[0];
+
+    // left
+    if (x > 0) {
+      estack.push(p - 1);
+    }
+
+    // top
+    if (p > (width - 1)) {
+      estack.push(p - width);
+    }
+
+    // right
+    if (x < (width - 1)) {
+      estack.push(p + 1);
+    }
+
+    if (p < ((height - 1) * width)) {
+      estack.push(p + width);
+    }
+  }
+}
+
+/**
+ *  Build neighbourhood for areas.
+ *  @param floodArr
+ *  @param areaMap
+ *  @param area which need to build it's neighbours
+ */
+/*
+function buildNeighbour(floodArr, areaMap, area) { var areaMap = {}; }
+*/
+
+function createFillAreaMap(floodImg) {
+  var width = floodImg.width;
+  var height = floodImg.height;
+  var floodArr = new Uint32Array(floodImg.data.buffer);
+  var areaMap = {};
+  for (var i = 0; i < floodArr.length; i++) {
+    var color = floodArr[i];
+    var x = i % width;
+    var y = Math.floor(i / width);
+    if (areaMap[color]) {
+      areaMap[color].addPoint(x, y);
+    } else {
+      areaMap[color] = new FillArea(color, x, y);
+    }
+  }
+  return areaMap;
+}
+
+function mergeAreas(floodFilledImage, threshold?) {
+  const kk = (floodFilledImage.width / 1000) * 20;
+  threshold = threshold || kk;
+  var areaMap = createFillAreaMap(floodFilledImage);
+  var keys = Object.keys(areaMap);
+  var floodArr = new Uint32Array(floodFilledImage.data.buffer);
+  var width = floodFilledImage.width;
+  var height = floodFilledImage.height;
+  var areas = keys.map(function (key) { return areaMap[key]; });
+  var smallAreas = areas.filter(function (a) { return a.count < threshold; });
+
+  console.log('mergeAreas@areaSize', areas.length, smallAreas.length);
+  var i, j, x, y, index, p, px, py;
+  var neighbours = getNearestNeighbours(1);
+  smallAreas.forEach(function (area) {
+    // area.neighbours = [];
+    let neighbourSet = new Set();
+    for (x = area.left; x <= area.right; x++) {
+      for (y = area.top; y <= area.bottom; y++) {
+        index = y * width + x;
+        if (floodArr[index] == area.color) {
+          // check pixe's neighbour
+          for (j = 0; j < neighbours.length; j++) {
+            p = neighbours[j];
+            px = x + p.x;
+            py = y + p.y;
+            index = py * width + px;
+            if (px > 0 && px < width && py > 0 && py < height &&
+              floodArr[index] != 0 && floodArr[index] != area.color) {
+              // area.neighbours.push(areaMap[floodArr[index]]);
+              neighbourSet.add(areaMap[floodArr[index]]);
+            }
+          }
+        }
+      }
+    }
+    area.neighbours = Array.from(neighbourSet);
+  });
+
+  // remove the area has no neighbours.
+  smallAreas =
+    smallAreas.filter(function (area) { return area.neighbours.length > 0; });
+  // console.log('mergeAreas@areaSize', areas.length, smallAreas.length);
+
+  // for each samll area.
+  // get it's direct neighbour area list
+  // select the biggest one, and merge with it.
+  // if no neighbours mark it can't merge.
+
+  /**
+   * merge another area
+   */
+  function mergeToMe(me, other) {
+    // if i am a small area.
+    // merge other's neighbours
+    if (me.neighbours) {
+      me.neighbours =
+        me.neighbours.concat(other.neighbours)
+          .filter(function (a) { return a != me && a != other; });
+    }
+
+    // set other merged
+    other.merged = true;
+
+    for (x = other.left; x <= other.right; x++) {
+      for (y = other.top; y <= other.bottom; y++) {
+        index = y * width + x;
+        if (floodArr[index] == other.color) {
+          floodArr[index] = me.color;
+          me.addPoint(x, y);
+        }
+      }
+    }
+  }
+
+  var loop = 0;
+  do {
+    loop++;
+
+    smallAreas.forEach(function (area) {
+      if (area.merged)
+        return;
+      var neighbour;
+      for (var i = 0; i < area.neighbours.length; i++) {
+        // merge other small areas.
+        neighbour = area.neighbours[i];
+        // if neighbour is merged or neighbour is a large one
+        if (neighbour.merged || !neighbour.neighbours) {
+          continue;
+        }
+        mergeToMe(area, neighbour);
+      }
+      if (area.neighbours.length > 0) {
+        // merge me to the first large neighbour
+        mergeToMe(area.neighbours[0], area);
+      }
+    });
+
+    smallAreas = smallAreas.filter(function (
+      area) { return !area.merged && area.neighbours.length > 0; });
+
+    console.log('mergeAreas@loop:' + loop, smallAreas.length);
+
+    if (smallAreas.length == 0)
+      break;
+
+  } while (loop < 4);
+}
+
+/**
+ * Fill blank pixels with color.
+ */
+function fillEmptyGap(floodFilledImage, maxDistance) {
+  var floodArr = new Uint32Array(floodFilledImage.data.buffer);
+  var width = floodFilledImage.width;
+  var height = floodFilledImage.height;
+  var emptyPoints = []; // x,y,x,y,x,y......
+  var unFilledPoints = [];
+  var taskArr = []; // index, color, index, color
+  var i, j, x, y, index, color, p, px, py;
+
+  var neighbours = getNearestNeighbours(1);
+
+  // get all empty points
+  for (x = 0; x < width; x++) {
+    for (y = 0; y < height; y++) {
+      index = y * width + x;
+      if (floodArr[index] == 0) {
+        emptyPoints.push(x, y);
+      }
+    }
+  }
+
+  console.log('FillEmptyGap#emptyPoints@length', emptyPoints.length / 2);
+  var loop = 0;
+
+  do {
+    loop++;
+
+    // loop all alpha points and try to fill.
+    var found;
+    for (i = 0; i < emptyPoints.length; i += 2) {
+      x = emptyPoints[i];
+      y = emptyPoints[i + 1];
+      found = false;
+      for (j = 0; j < neighbours.length; j++) {
+        p = neighbours[j];
+        px = x + p.x;
+        py = y + p.y;
+        index = py * width + px;
+        if (px > 0 && px < width && py > 0 && py < height &&
+          floodArr[index] != 0) {
+          taskArr.push(y * width + x, floodArr[index]);
+          found = true;
+          break;
+        }
+      }
+      if (!found)
+        unFilledPoints.push(x, y);
+    }
+
+    console.log('fillEmptyGap@loop:' + loop, emptyPoints.length,
+      taskArr.length / 2, unFilledPoints.length / 2);
+
+    // no more alpha pixels can find it's neighbours.
+    if (taskArr.length == 0)
+      break;
+
+    // do the task
+    for (i = 0; i < taskArr.length; i += 2) {
+      index = taskArr[i];
+      color = taskArr[i + 1];
+      floodArr[index] = color;
+    }
+
+    taskArr = [];
+    emptyPoints = unFilledPoints;
+    unFilledPoints = [];
+
+  } while (loop <= maxDistance);
+}
+
+/**
+ * Fill pixels in srcImage with alpha<255
+ * with the nearest filled color
+ * @param srcImage source ImageData  ojbect.
+ * @param floodFilledImage flood filled ImageData object.
+ * @param maxDistance
+ */
+function fillAlphaGap(srcImage, floodFilledImage, maxDistance) {
+  maxDistance = maxDistance || 2;
+  var srcArr = new Uint32Array(srcImage.data.buffer);
+  var srcBuf = srcImage.data; // to get the alpha directly
+  var floodArr = new Uint32Array(floodFilledImage.data.buffer);
+  var width = srcImage.width;
+  var height = srcImage.height;
+  var alphaPoints = []; // x,y,x,y,x,y......
+  var unFilledPoints = [];
+  var taskArr = []; // index, color, index, color
+  var i, j, x, y, index, color, p, px, py;
+
+  var neighbours = getNearestNeighbours(1);
+
+  // get all alpha points
+  for (x = 0; x < width; x++) {
+    for (y = 0; y < height; y++) {
+      index = y * width + x;
+      j = index * 4 + 3; // alpha byte index
+      if (floodArr[index] == 0 && srcBuf[j] < 255
+        && (srcBuf[j] >= 128 || (srcBuf[j] > 0 && isEdge(x, y, width, height)))) {  // < 128 的前面已经处理过了(别忘了边缘点)
+        alphaPoints.push(x, y);
+      }
+    }
+  }
+
+  // console.log('neighbours:', neighbours);
+  // console.log('alphaPoints@length', alphaPoints.length / 2);
+  var loop = 0;
+
+  do {
+    loop++;
+
+    // loop all alpha points and try to fill.
+    var found;
+    for (i = 0; i < alphaPoints.length; i += 2) {
+      x = alphaPoints[i];
+      y = alphaPoints[i + 1];
+      found = false;
+      for (j = 0; j < neighbours.length; j++) {
+        p = neighbours[j];
+        px = x + p.x;
+        py = y + p.y;
+        index = py * width + px;
+        if (px > 0 && px < width && py > 0 && py < height &&
+          floodArr[index] != 0) {
+          taskArr.push(y * width + x, floodArr[index]);
+          found = true;
+          break;
+        }
+      }
+      if (!found)
+        unFilledPoints.push(x, y);
+    }
+
+    console.log('fillAlphaGap@loop:' + loop, alphaPoints.length,
+      taskArr.length / 2, unFilledPoints.length / 2);
+
+    // no more alpha pixels can find it's neighbours.
+    if (taskArr.length == 0)
+      break;
+
+    // do the task
+    for (i = 0; i < taskArr.length; i += 2) {
+      index = taskArr[i];
+      color = taskArr[i + 1];
+      floodArr[index] = color;
+    }
+
+    taskArr = [];
+    alphaPoints = unFilledPoints;
+    unFilledPoints = [];
+
+  } while (loop <= maxDistance);
+}
+
+/**
+ * Get the nearest N neighbours
+ * and sort by distance.
+ * @param n integer >=1
+ */
+function getNearestNeighbours(n) {
+  var distArr = [];
+  for (var i = (-1) * n; i <= n; i++) {
+    for (var j = (-1) * n; j <= n; j++) {
+      if (i == 0 && j == 0) {
+        // exclued self
+        continue;
+      }
+      var distance = Math.pow(i, 2) + Math.pow(j, 2);
+      distArr.push({ x: i, y: j, dist: distance });
+    }
+  }
+
+  distArr.sort(function (a, b) { return a.dist - b.dist; });
+
+  return distArr;
+}
+
+function fillGapOld(ctx, floodFilled) {
+  // create gap bitmap
+  var gapImg = ctx.createImageData(floodFilled);
+  var width = floodFilled.width;
+  var height = floodFilled.height;
+  var maxDistance = 2;
+  var floodArr = new Uint32Array(floodFilled.data.buffer);
+  var gapArr = new Uint32Array(gapImg.data.buffer);
+
+  // Build a nearest points array based on (0, 0)
+  // and sort it by it's distance to (0,0)
+  var distArr = [];
+  for (var i = (-1) * maxDistance; i <= maxDistance; i++) {
+    for (var j = (-1) * maxDistance; j <= maxDistance; j++) {
+      if (i != 0 && j != 0) {
+        var distance = Math.pow(i, 2) + Math.pow(j, 2);
+        distArr.push({ x: i, y: j, dist: distance });
+      }
+    }
+  }
+
+  distArr.sort(function (a, b) { return a.dist - b.dist; });
+
+  // for every gap pixels in flood image, find the nearest colored pixel
+  // and set as it's color.
+  var i: number, j: number, x: number, y: number, px: number, py: number;
+  for (i = 0; i < floodArr.length; i++) {
+    if (floodArr[i] == 0) {
+      x = i % width;
+      y = Math.floor(i / width);
+      /*
+      var arr = distArr.map(function(obj){
+        return {
+          x : obj.x + x,
+          y : obj.y + y
+        }
+      });
+      */
+      for (j = 0; j < distArr.length; j++) {
+        var p = distArr[j];
+        px = x + p.x;
+        py = y + p.y;
+
+        // if(p.x > 0 && p.x < width && p.y > 0 && p.y < height &&
+        // floodArr[p.y*width+p.x] !=0 ) {
+        if (px > 0 && px < width && py > 0 && py < height &&
+          floodArr[py * width + px] != 0) {
+          gapArr[y * width + x] = floodArr[py * width + px];
+          break;
+        }
+      }
+      // this is the gap
+      // find nearest filled pixel
+    }
+  }
+
+  // merge the gap bitmap with the flood bitmap
+  for (var i = 0; i < floodArr.length; i++) {
+    if (floodArr[i] == 0) {
+      floodArr[i] = gapArr[i];
+    }
+  }
+}
+
+/**
+ *  Copy image data from src
+ *  @param ctx  canvas context
+ *  @param src  source image data.
+ */
+function copyImage(ctx, src) {
+  var dest = ctx.createImageData(src);
+  var srcArr = new Uint32Array(src.data.buffer);
+  var destArr = new Uint32Array(dest.data.buffer);
+  for (var i = 0; i < srcArr.length; i++) {
+    destArr[i] = srcArr[i];
+  }
+  return dest;
+}
+
+/**
+ * Create canvas context by specified dimension.
+ */
+function createContext(width, height) {
+  return createCanvas(width, height).getContext('2d');
+}
+
+/**
+ * Create canvas
+ * @param width
+ * @param height
+ */
+function createCanvas(width, height) {
+  var canvas = document.createElement('canvas');
+  canvas.width = width;
+  canvas.height = height;
+  return canvas;
+}
+
+function randomUniqueColor() {
+  let color: number;
+  do {
+    color = randomColor();
+    if (!_colors[color]) {
+      _colors[color] = color;
+      return color;
+    }
+  } while (1)
+  return color;
+}
+
+function randomDistanceColor() {
+  return colors.get();
+}
+
+function randomColor() {
+  var color = new Uint8Array([0, 0, 0, 255]);
+  for (var i = 0; i < 3; i++) {
+    color[i] = Math.floor(Math.random() * 256);
+  }
+  var u32 = new Uint32Array(color.buffer);
+  return u32[0];
+}
+
+function emptyColor() {
+  var color = new Uint8Array([255, 255, 255, 255]);
+  var a = new Uint32Array(color.buffer);
+  return a[0];
+}

+ 48 - 0
zorro/src/app/lib/filler/common/interfaces.ts

@@ -0,0 +1,48 @@
+import { Lab } from "../color/color";
+import FillArea from "./fillarea";
+
+
+
+
+/**
+ * 颜色信息
+ */
+export interface ColorInfo {
+  color : number, 
+  cssColor : string, 
+}
+
+export interface ColorSumItem extends ColorInfo  {
+  //color : number, 
+  //cssColor : string, 
+  total  : number, //色块数
+  areas  : number[], //area indexes
+  lab? : Lab,
+  distanceFromWhite? : number,
+}
+
+/**
+ * 上色信息
+ */
+export interface ColorMap{
+  [key : number] : ColorInfo,
+}
+
+
+export interface Centers{
+  [key : number] : {x : number, y : number , radius : number},
+}
+
+
+export interface AreaMap {
+  [key : number] : FillArea,
+}
+
+
+// 标注数据结构
+export interface Mark {
+  x: number,  // x坐标
+  y: number,  // y坐标
+  radius: number, // 半径
+  note: string, // 批注
+}

+ 192 - 0
zorro/src/app/lib/filler/common/kmeans.ts

@@ -0,0 +1,192 @@
+
+export interface Label<Type> {
+  centroid: Type;
+  data: Type[];
+}
+
+export type DistanceFunc<Type> = (a: Type, b: Type) => number;
+
+export interface shuffleItem<Type> {
+  item: Type,
+  random: number,
+}
+
+
+/**
+ * 从数据集中随机挑选指定数量的数据
+ * @param dataset 
+ * @param size 
+ * @returns 
+ */
+export function randomPickup<Type>(dataset: Type[], size: number): Type[] {
+  if (dataset.length <= size) return dataset;
+  let shuffle: Type[] = dataset.map(item => {
+    return { item: item, random: Math.random() } as shuffleItem<Type>;
+  })
+    .sort((a: shuffleItem<Type>, b: shuffleItem<Type>) => { return a.random - b.random; })
+    .map((si: shuffleItem<Type>) => si.item).slice(0, size);
+  return shuffle;
+}
+
+
+/**
+ * 挑选数据集中心点
+ * @param dataset 
+ * @param distanceFunc 
+ * @returns 
+ */
+export function pickupCentroid<Type>(dataset: Type[], distanceFunc: DistanceFunc<Type>): Type {
+  let minDist = Number.MAX_SAFE_INTEGER;
+  let minDistIndex = 0;
+  for (var i = 0; i < dataset.length; i++) {
+    let dist = dataset.reduce((r: number, c: Type) => {
+      return r + distanceFunc(c, dataset[i]);
+    }, 0);
+    if (dist < minDist) {
+      minDist = dist;
+      minDistIndex = i;
+    }
+  }
+  return dataset[minDistIndex];
+}
+
+/**
+ * 验证 k-means结果
+ * @param labels 
+ * @param distanceFunc 
+ * @param minDist 
+ * @returns 
+ */
+export function validateKmeans<Type>(labels: Label<Type>[], distanceFunc: DistanceFunc<Type>, minDist): boolean {
+  for (var i = 0; i < labels.length; i++) {
+    let label = labels[i];
+    for (var j = 0; j < label.data.length; j++) {
+      if (distanceFunc(label.centroid, label.data[j]) >= minDist) return false;
+    }
+  }
+  return true;
+}
+
+
+/**
+ * 
+ * @param dataset  
+ * @param k 
+ * @param distanceFunc 
+ * @returns 
+ */
+export function kmeans<Type>(dataset: Type[], k: number, distanceFunc: DistanceFunc<Type>): Label<Type>[] {
+  let centroids: Type[] = randomPickup<Type>(dataset, k);
+
+  let loop = 0;
+  let labels: Label<Type>[];
+
+  do {
+    labels = centroids.map((item: Type) => {
+      return { centroid: item, data: [] } as Label<Type>;
+    })
+
+    //按中心点进行归类
+    for (var i = 0; i < dataset.length; i++) {
+      let item: Type = dataset[i];
+      let nearestLabel: Label<Type> = null;
+      let neareastDist: number = 0;
+      let dist: number;
+      for (var j = 0; j < labels.length; j++) {
+        dist = distanceFunc(item, labels[j].centroid);
+        if (nearestLabel == null || dist < neareastDist) {
+          nearestLabel = labels[j];
+          neareastDist = dist;
+        }
+      }
+      nearestLabel.data.push(item);
+    }
+
+    //validate Result
+    if (validateKmeans(labels, distanceFunc, 4) || loop > 100) break;
+
+    //重新计算中心点
+    centroids = labels.map(label => pickupCentroid(label.data, distanceFunc))
+
+    loop++;
+  } while (true);
+  console.log('loop:', loop);
+
+  return labels;
+}
+
+
+
+
+
+
+
+
+export interface Split<Type> {
+  mergable: Type[];
+  standalone: Type[];
+}
+
+export function splitByMinDistance<Type>(dataset: Type[], minDist: number = 2.3, distanceFunc: DistanceFunc<Type>): Split<Type> {
+  let standalone: Type[] = [];
+  let mergable: Type[] = [];
+  for (var i = 0; i < dataset.length; i++) {
+    let nereasets = dataset.filter(item => distanceFunc(item, dataset[i]) < minDist);
+    if (nereasets.length <= 1) standalone.push(dataset[i]);
+    else mergable.push(dataset[i]);
+  }
+  return { mergable, standalone };
+}
+
+
+
+
+
+export interface Neigbours<Type> {
+  self: Type;
+  neigbours: Type[];
+}
+
+/**
+ * Generic merge by distance.
+ * @param dataset 
+ * @param minDist 
+ * @param distanceFunc 
+ * @returns 
+ */
+export function simpleMerge<Type>(dataset: Type[], minDist: number = 2.3, distanceFunc: DistanceFunc<Type>): Neigbours<Type>[] {
+  let data = [...dataset];
+  let result = [];
+
+  let list: Neigbours<Type>[] = data.map((item: Type) => ({ self: item, neigbours: [] }));
+  //找到每个颜色最近的颜色
+  list.forEach(nei => {
+    nei.neigbours = data.filter(item => distanceFunc(item, nei.self) < minDist);
+  })
+
+
+  do {
+    list = list.sort((a, b) => a.neigbours.length - b.neigbours.length);
+    //console.log(list.length);
+    let best = list.pop();
+    result.push(best);
+    list = list.filter(nei => best.neigbours.indexOf(nei.self) < 0)
+    if (best.neigbours.length > 1) {
+      list.forEach(nei => {
+        nei.neigbours = nei.neigbours.filter(item => best.neigbours.indexOf(item) < 0);
+      })
+    }
+  } while (list.length > 0);
+
+
+  return result;
+}
+
+
+
+
+
+
+
+
+

+ 204 - 0
zorro/src/app/lib/filler/common/polygon.ts

@@ -0,0 +1,204 @@
+/**
+ * Polygon
+ */
+
+interface Point {
+  x: number;
+  y: number;
+}
+
+interface Line {
+  p1: Point;
+  p2: Point;
+}
+
+interface Edge {
+  p1: Point;
+  p2: Point;
+  a: number;
+  b: number;
+  c: number;
+}
+
+export default class Polygon {
+  po: Array<Point>;
+  edges: Array<Edge>;
+  n: number;
+
+  constructor(po) {
+    if (po) {
+      this.po = po.slice();
+      this.n = po.length;
+      this.buildEdges();
+    }
+  }
+
+  private buildEdges() {
+    if (!this.isValid()) return;
+    let n = this.n, po = this.po;
+    let edges = new Array<Edge>(n);
+
+    for (let i = 1; i <= n; i++) {
+      edges[i - 1] = {
+        p1: po[i - 1],
+        p2: po[i % n],
+        a: po[i % n].y - po[i - 1].y,
+        b: -(po[i % n].x - po[i - 1].x),
+        c: po[i % n].x * po[i - 1].y - po[i - 1].x * po[i % n].y
+      }
+    }
+    this.edges = edges;
+
+  }
+
+  /**
+   * check if polygon is valid
+   */
+  isValid(): boolean {
+    if (!this.po || this.po.length < 3) return false;
+    else return true;
+  }
+
+  /**
+   * check a polygon is ordered clockwise or counterclockwise
+   * Return the clockwise status of a curve, clockwise or counterclockwise
+   *  0: return 0 for incomputables eg: colinear points;  1: counterclockwise;  -1: clockwise; 
+   * 
+   * It is assumed that
+   * - the polygon is closed
+   * - the last point is not repeated
+   * - the polygon is simple (does not intersect itself or have holes)
+   */
+  dir(): number {
+    let sum = 0;
+    let po = this.po;
+
+    if (!po || po.length < 3) return 0;
+
+    po.push(po[0]);
+    for (let i = 1; i < po.length; i++) {
+      sum += (po[i].x - po[i - 1].x) * (po[i].y + po[i - 1].y);
+    }
+    po.pop();
+
+    if (sum > 0) return 1;
+    else if (sum < 0) return -1;
+    else return 0;
+  }
+
+  /** 
+   * https://www.jianshu.com/p/ba03c600a557
+   * check if a pixel is in a polygon
+   * @param p : pixel to check
+   * @return true if contain and false if not
+   */
+  inside(p: Point): boolean {
+    //  判断涉嫌是否与边相交
+    function isRayIntersectEdge(p: Point, edge: Edge): boolean {
+      if (edge.p1.y == edge.p2.y) return false;  // 排除与射线平行、重合,线段首尾端点重合的情况
+      if (edge.p1.y > p.y && edge.p2.y > p.y) return false; // 线段在射线上边
+      if (edge.p1.y < p.y && edge.p2.y < p.y) return false; // 线段在射线下边
+      if (edge.p1.y == p.y && edge.p2.y > p.y) return false; // 交点为下端点
+      if (edge.p2.y == p.y && edge.p1.y > p.y) return false; // 交点为下端点
+      if (edge.p1.x < p.x && edge.p2.x < p.x) return false; // 线段在射线左边
+
+      let xseg = edge.p2.x - (edge.p2.x - edge.p1.x) * (edge.p2.y - p.y) / (edge.p2.y - edge.p1.y);
+      if (xseg < p.x) return false; // 交点在射线起点的左侧
+      return true;
+    }
+
+    let n = this.n;
+    let edges = this.edges;
+    let count = 0;
+    for (let edge of edges) {
+      if (p.x == edge.p1.x && p.y == edge.p1.y) return true; // 直接跟多边形顶点重合,直接判定为inside
+      if (edge.p1.y == edge.p2.y && p.y == edge.p1.y
+        && p.x >= Math.min(edge.p1.x, edge.p2.x)
+        && p.x <= Math.max(edge.p1.x, edge.p2.x)) return true; // 边与射线平行,且该点直接是在边上,也直接判定为inside
+      if (isRayIntersectEdge(p, edge)) {
+        count++;
+      }
+    }
+
+    if (count % 2 == 0) { // outside
+      return false;
+    } else { // inside
+      return true;
+    }
+  }
+
+
+  /**
+   * check is contains another polygon
+   * @param poly 
+   */
+  contains(poly: Polygon): boolean {
+
+    let po = poly.po;
+
+    for (let p of po) {
+      if (!this.inside(p)) {
+          return false;
+      }
+    }
+    return true;
+  }
+
+  /**
+   * check if the polygon self intersect
+   * @returns 
+   */
+  isIntersect(): boolean {
+    if (!this.isValid()) return false;
+
+    let n = this.n;
+    let edges = this.edges;
+    let interPos: Point;
+    if (n <= 3) return false;
+    for (let i = 0; i < n; i++) {
+      interPos = crossPos(i, (i + 2) % n);
+      if (!interPos) { // 两条边平行
+        continue;
+      }
+      if (isInLine(interPos, edges[i]) && isInLine(interPos, edges[(i + 2) % n])) {
+        return true;
+      }
+    }
+
+    return false;
+
+    // 求两条边的交点(通过叉积求二维直线交点)
+    function crossPos(index1, index2): Point {
+      let pos = null;
+
+      let A1 = edges[index1].a;
+      let B1 = edges[index1].b;
+      let A2 = edges[index2].a;
+      let B2 = edges[index2].b;
+      let C1 = edges[index1].c;
+      let C2 = edges[index2].c;
+
+      let m = A1 * B2 - A2 * B1;
+      if (m != 0) {
+        pos = { x: (C2 * B1 - C1 * B2) / m, y: (C1 * A2 - C2 * A1) / m };
+      }
+
+      return pos;
+    }
+
+    // 判断点是否在线段内
+    function isInLine(pos: Point, edge: Edge): boolean {
+      let maxX = Math.max(edge.p1.x, edge.p2.x);
+      let minX = Math.min(edge.p1.x, edge.p2.x);
+      let maxY = Math.max(edge.p1.y, edge.p2.y);
+      let minY = Math.min(edge.p1.y, edge.p2.y);
+
+      if (pos.x <= maxX && pos.x >= minX && pos.y <= maxY && pos.y >= minY) {
+        return true;
+      }
+      return false;
+    }
+
+  }
+
+}

+ 1435 - 0
zorro/src/app/lib/filler/common/potrace.ts

@@ -0,0 +1,1435 @@
+/* Copyright (C) 2001-2013 Peter Selinger.
+ *
+ * A Typescript port of Potrace (http://potrace.sourceforge.net).
+ * 
+ * Licensed under the GPL
+ * 
+ * Usage
+ *   loadImageFromFile(file) : load image from File API
+ *   loadImageFromUrl(url): load image from URL
+ *     because of the same-origin policy, can not load image from another domain.
+ *     input color/grayscale image is simply converted to binary image. no pre-
+ *     process is performed.
+ * 
+ *   setParameter({para1: value, ...}) : set parameters
+ *     parameters:
+ *        turnpolicy ("black" / "white" / "left" / "right" / "minority" / "majority")
+ *          how to resolve ambiguities in path decomposition. (default: "minority")       
+ *        turdsize
+ *          suppress speckles of up to this size (default: 2)
+ *        optcurve (true / false)
+ *          turn on/off curve optimization (default: true)
+ *        alphamax
+ *          corner threshold parameter (default: 1)
+ *        opttolerance 
+ *          curve optimization tolerance (default: 0.2)
+ *       
+ *   process(callback) : wait for the image be loaded, then run potrace algorithm,
+ *                       then call callback function.
+ * 
+ *   getSVG(size, opt_type) : return a string of generated SVG image.
+ *                                    result_image_size = original_image_size * size
+ *                                    optional parameter opt_type can be "curve"
+ */
+
+
+
+
+
+
+
+class Point {
+  x: number;
+  y: number;
+  constructor(x: number = 0, y: number = 0) {
+    this.x = x;
+    this.y = y;
+  }
+
+  copy() {
+    return new Point(this.x, this.y);
+  };
+}
+
+
+
+export class Bitmap {
+  w: number;
+  h: number;
+  size: number;
+  arraybuffer: ArrayBuffer;
+  data: Int8Array;
+
+  constructor(w: number, h: number) {
+    this.w = w;
+    this.h = h;
+    this.size = w * h;
+    this.arraybuffer = new ArrayBuffer(this.size);
+    this.data = new Int8Array(this.arraybuffer);
+  }
+
+  at(x: number, y: number) {
+    return (x >= 0 && x < this.w && y >= 0 && y < this.h) &&
+      this.data[this.w * y + x] === 1;
+  };
+
+  index(i: number) {
+    var point = new Point();
+    point.y = Math.floor(i / this.w);
+    point.x = i - point.y * this.w;
+    return point;
+  };
+
+  flip(x: number, y: number) {
+    if (this.at(x, y)) {
+      this.data[this.w * y + x] = 0;
+    } else {
+      this.data[this.w * y + x] = 1;
+    }
+  };
+
+  copy() {
+    var bm = new Bitmap(this.w, this.h), i;
+    for (i = 0; i < this.size; i++) {
+      bm.data[i] = this.data[i];
+    }
+    return bm;
+  };
+}
+
+class Path {
+  area: number;
+  len: number;
+  curve: {};
+  pt: any[];
+  minX: number;
+  minY: number;
+  maxX: number;
+  maxY: number;
+  sign: string;
+
+  constructor() {
+    this.area = 0;
+    this.len = 0;
+    this.curve = {};
+    this.pt = [];
+    this.minX = 100000;
+    this.minY = 100000;
+    this.maxX = -1;
+    this.maxY = -1;
+  }
+}
+
+class Curve {
+  n: any;
+  tag: any[];
+  c: any[];
+  alphacurve: number;
+  vertex: any[];
+  alpha: any[];
+  beta: any[];
+  alpha0: any[];
+
+  constructor(n) {
+    this.n = n;
+    this.tag = new Array(n);
+    this.c = new Array(n * 3);
+    this.alphacurve = 0;
+    this.vertex = new Array(n);
+    this.alpha = new Array(n);
+    this.alpha0 = new Array(n);
+    this.beta = new Array(n);
+  }
+}
+
+export enum TurnPolicy {
+  black = 'black',
+  white = 'white',
+  left = 'left',
+  right = 'right',
+  minority = 'mitory',
+  majority = 'majority',
+}
+
+export interface PotraceOption {
+  turnpolicy?: "black" | "white" | "left" | "right" | "minority" | "majority";
+  turdsize?: number;
+  optcurve?: boolean;
+  alphamax?: number;
+  opttolerance?: number;
+  threshold?: number;
+}
+
+
+export class Potrace {
+  bm: Bitmap = null;
+  pathlist: Path[] = [];
+  callback: Function;
+  info: PotraceOption = {
+    //isReady: false,
+    turnpolicy: "minority",
+    turdsize: 2,
+    optcurve: true,
+    alphamax: 1,
+    opttolerance: 0.2,
+    threshold : 128,
+  };
+
+
+  /**
+   * 
+   * @param image HTMLImageElement or HTMLCanvasElement or Bitmap
+   * @param option 
+   */
+  constructor(image: Bitmap | HTMLImageElement | HTMLCanvasElement, option?: PotraceOption) {
+
+    if (option) {
+      Object.keys(option).forEach(key => {
+        this.info[key] = option[key];
+      })
+    }
+    let canvas: HTMLCanvasElement;
+    let ctx: CanvasRenderingContext2D;
+    let bitmap: Bitmap;
+
+    if (image instanceof Bitmap) {
+      bitmap = image as Bitmap;
+    } else if (image instanceof HTMLCanvasElement) {
+      canvas = image as HTMLCanvasElement;
+      ctx = canvas.getContext('2d');
+      bitmap = new Bitmap(canvas.width, canvas.height);
+      let imgdataobj = ctx.getImageData(0, 0, bitmap.w, bitmap.h);
+      let l = imgdataobj.data.length, i, j, color;
+      
+      for (i = 0, j = 0; i < l; i += 4, j++) {
+        color = 0.2126 * imgdataobj.data[i] + 0.7153 * imgdataobj.data[i + 1] +
+          0.0721 * imgdataobj.data[i + 2];
+        bitmap.data[j] = (color < this.info.threshold ? 1 : 0);
+      }
+    } else if (image instanceof HTMLImageElement) {
+      canvas = document.createElement('canvas');
+      let img = image as HTMLImageElement;
+      //draw image on canvas
+      canvas.width = img.width;
+      canvas.height = img.height;
+      ctx = canvas.getContext('2d');
+      //add white background 
+      ctx.fillStyle = "#ffffff";
+      ctx.fillRect(0, 0, canvas.width, canvas.height);
+      ctx.drawImage(img, 0, 0);
+
+      bitmap = new Bitmap(canvas.width, canvas.height);
+      let imgdataobj = ctx.getImageData(0, 0, bitmap.w, bitmap.h);
+      let l = imgdataobj.data.length, i, j, color;
+
+      for (i = 0, j = 0; i < l; i += 4, j++) {
+        color = Math.round(0.2126 * imgdataobj.data[i] + 0.7153 * imgdataobj.data[i + 1] + 0.0721 * imgdataobj.data[i + 2]);
+        bitmap.data[j] = (color < this.info.threshold ? 1 : 0);
+      }
+    } else {
+      console.log("image type not support!");
+      return;
+    }
+
+    this.bm = bitmap;
+    this.bmToPathlist();
+    this.processPath();
+  }
+
+  /**
+   * Get potrace instance from blob
+   * @param file Blob
+   * @returns 
+   */
+  static fromFile(file: Blob, option?: PotraceOption): Promise<Potrace> {
+    return new Promise((done, reject) => {
+      let reader = new FileReader();
+      let image = new Image();
+      image.onerror = reject;
+      image.onload = () => done(new Potrace(image, option));
+      reader.addEventListener('load', () => {
+        image.src = reader.result as string;
+      }, false);
+      reader.readAsDataURL(file);
+    });
+  }
+
+  /**
+   * 从远程图像获取Potrace对象
+   * @param url 
+   * @returns 
+   */
+  static fromUrl(url: string, option?: PotraceOption): Promise<Potrace> {
+    return new Promise((done, reject) => {
+      let image = new Image();
+      image.onload = () => done(new Potrace(image, option));
+      image.onerror = reject;
+      image.src = url;
+    });
+  }
+
+  setParameter(obj: any) {
+    var key: string;
+    for (key in obj) {
+      if (obj.hasOwnProperty(key)) {
+        this.info[key] = obj[key];
+      }
+    }
+  }
+
+
+
+
+  /*
+  clear() {
+    this.bm = null;
+    this.pathlist = [];
+    this.callback = null;
+    this.info.isReady = false;
+  }
+  */
+
+  /**
+   * 获取blob对象,方便上传
+   * @param size 
+   * @param opt_type 
+   * @returns 
+   */
+  getBlob(size: number, opt_type?: string): Blob {
+    const blob = new Blob([this.getSVG(size, opt_type)], { type: 'image/svg+xml' });
+    return blob;
+  }
+
+  /**
+   * 获取生成的svg
+   * @param size 
+   * @param opt_type 
+   * @returns 
+   */
+  getSVG(size: number, opt_type: string = null, pathOnly : boolean = false, color: string = 'black'): string {
+    let self = this;
+
+    function path(curve) {
+
+      function bezier(i) {
+        var b = 'C ' + (curve.c[i * 3 + 0].x * size).toFixed(3) + ' ' +
+          (curve.c[i * 3 + 0].y * size).toFixed(3) + ',';
+        b += (curve.c[i * 3 + 1].x * size).toFixed(3) + ' ' +
+          (curve.c[i * 3 + 1].y * size).toFixed(3) + ',';
+        b += (curve.c[i * 3 + 2].x * size).toFixed(3) + ' ' +
+          (curve.c[i * 3 + 2].y * size).toFixed(3) + ' ';
+        return b;
+      }
+
+      function segment(i) {
+        var s = 'L ' + (curve.c[i * 3 + 1].x * size).toFixed(3) + ' ' +
+          (curve.c[i * 3 + 1].y * size).toFixed(3) + ' ';
+        s += (curve.c[i * 3 + 2].x * size).toFixed(3) + ' ' +
+          (curve.c[i * 3 + 2].y * size).toFixed(3) + ' ';
+        return s;
+      }
+
+      var n = curve.n, i;
+      var p = 'M' + (curve.c[(n - 1) * 3 + 2].x * size).toFixed(3) +
+        ' ' + (curve.c[(n - 1) * 3 + 2].y * size).toFixed(3) + ' ';
+      for (i = 0; i < n; i++) {
+        if (curve.tag[i] === "CURVE") {
+          p += bezier(i);
+        } else if (curve.tag[i] === "CORNER") {
+          p += segment(i);
+        }
+      }
+      //p += 
+      return p;
+    }
+
+    var w = self.bm.w * size, h = self.bm.h * size,
+      len = self.pathlist.length, c, i, strokec, fillc, fillrule;
+
+
+    let pathStr = '';
+    if (opt_type === "curve") {
+      strokec = color ||  "black";
+      fillc = "none";
+      fillrule = '';
+    } else {
+      strokec = "none";
+      fillc = color || "black";
+      fillrule = ' fill-rule="evenodd"';
+    }
+    pathStr += '<path stroke="' + strokec + '" fill="' + fillc + '"' + fillrule;
+
+    pathStr += ' d="';
+    for (i = 0; i < len; i++) {
+      c = self.pathlist[i].curve;
+      pathStr += path(c);
+    }
+
+    pathStr += '"/>';
+
+
+    var svg = `<svg id="svg" version="1.1" width="${w}" height="${h}" xmlns="http://www.w3.org/2000/svg">${pathStr}</svg>`;
+
+    return pathOnly ? pathStr : svg;
+  }
+
+
+
+  bmToPathlist() {
+    let self = this;
+
+    var bm1 = this.bm.copy(),
+      currentPoint = new Point(0, 0),
+      path;
+
+    function findNext(point) {
+      var i = bm1.w * point.y + point.x;
+      while (i < bm1.size && bm1.data[i] !== 1) {
+        i++;
+      }
+      return i < bm1.size && bm1.index(i);
+    }
+
+    function majority(x, y) {
+      var i, a, ct;
+      for (i = 2; i < 5; i++) {
+        ct = 0;
+        for (a = -i + 1; a <= i - 1; a++) {
+          ct += bm1.at(x + a, y + i - 1) ? 1 : -1;
+          ct += bm1.at(x + i - 1, y + a - 1) ? 1 : -1;
+          ct += bm1.at(x + a - 1, y - i) ? 1 : -1;
+          ct += bm1.at(x - i, y + a) ? 1 : -1;
+        }
+        if (ct > 0) {
+          return 1;
+        } else if (ct < 0) {
+          return 0;
+        }
+      }
+      return 0;
+    }
+
+    function findPath(point) {
+      var path = new Path(),
+        x = point.x, y = point.y,
+        dirx = 0, diry = 1, tmp;
+
+      path.sign = self.bm.at(point.x, point.y) ? "+" : "-";
+
+      while (1) {
+        path.pt.push(new Point(x, y));
+        if (x > path.maxX)
+          path.maxX = x;
+        if (x < path.minX)
+          path.minX = x;
+        if (y > path.maxY)
+          path.maxY = y;
+        if (y < path.minY)
+          path.minY = y;
+        path.len++;
+
+        x += dirx;
+        y += diry;
+        path.area -= x * diry;
+
+        if (x === point.x && y === point.y)
+          break;
+
+        var l = bm1.at(x + (dirx + diry - 1) / 2, y + (diry - dirx - 1) / 2);
+        var r = bm1.at(x + (dirx - diry - 1) / 2, y + (diry + dirx - 1) / 2);
+
+        if (r && !l) {
+          if (self.info.turnpolicy === "right" ||
+            (self.info.turnpolicy === "black" && path.sign === '+') ||
+            (self.info.turnpolicy === "white" && path.sign === '-') ||
+            (self.info.turnpolicy === "majority" && majority(x, y)) ||
+            (self.info.turnpolicy === "minority" && !majority(x, y))) {
+            tmp = dirx;
+            dirx = -diry;
+            diry = tmp;
+          } else {
+            tmp = dirx;
+            dirx = diry;
+            diry = -tmp;
+          }
+        } else if (r) {
+          tmp = dirx;
+          dirx = -diry;
+          diry = tmp;
+        } else if (!l) {
+          tmp = dirx;
+          dirx = diry;
+          diry = -tmp;
+        }
+      }
+      return path;
+    }
+
+    function xorPath(path) {
+      var y1 = path.pt[0].y,
+        len = path.len,
+        x, y, maxX, minY, i, j;
+      for (i = 1; i < len; i++) {
+        x = path.pt[i].x;
+        y = path.pt[i].y;
+
+        if (y !== y1) {
+          minY = y1 < y ? y1 : y;
+          maxX = path.maxX;
+          for (j = x; j < maxX; j++) {
+            bm1.flip(j, minY);
+          }
+          y1 = y;
+        }
+      }
+
+    }
+
+    while (currentPoint = findNext(currentPoint)) {
+
+      path = findPath(currentPoint);
+
+      xorPath(path);
+
+      if (path.area > self.info.turdsize) {
+        self.pathlist.push(path);
+      }
+    }
+
+  }
+
+
+  processPath() {
+    let self = this;
+
+    function Quad() {
+      this.data = [0, 0, 0, 0, 0, 0, 0, 0, 0];
+    }
+
+    Quad.prototype.at = function (x, y) {
+      return this.data[x * 3 + y];
+    };
+
+    function Sum(x, y, xy, x2, y2) {
+      this.x = x;
+      this.y = y;
+      this.xy = xy;
+      this.x2 = x2;
+      this.y2 = y2;
+    }
+
+    function mod(a, n) {
+      return a >= n ? a % n : a >= 0 ? a : n - 1 - (-1 - a) % n;
+    }
+
+    function xprod(p1, p2) {
+      return p1.x * p2.y - p1.y * p2.x;
+    }
+
+    function cyclic(a, b, c) {
+      if (a <= c) {
+        return (a <= b && b < c);
+      } else {
+        return (a <= b || b < c);
+      }
+    }
+
+    function sign(i) {
+      return i > 0 ? 1 : i < 0 ? -1 : 0;
+    }
+
+    function quadform(Q, w) {
+      var v = new Array(3), i, j, sum;
+
+      v[0] = w.x;
+      v[1] = w.y;
+      v[2] = 1;
+      sum = 0.0;
+
+      for (i = 0; i < 3; i++) {
+        for (j = 0; j < 3; j++) {
+          sum += v[i] * Q.at(i, j) * v[j];
+        }
+      }
+      return sum;
+    }
+
+    function interval(lambda, a, b) {
+      var res = new Point();
+
+      res.x = a.x + lambda * (b.x - a.x);
+      res.y = a.y + lambda * (b.y - a.y);
+      return res;
+    }
+
+    function dorth_infty(p0, p2) {
+      var r = new Point();
+
+      r.y = sign(p2.x - p0.x);
+      r.x = -sign(p2.y - p0.y);
+
+      return r;
+    }
+
+    function ddenom(p0, p2) {
+      var r = dorth_infty(p0, p2);
+
+      return r.y * (p2.x - p0.x) - r.x * (p2.y - p0.y);
+    }
+
+    function dpara(p0, p1, p2) {
+      var x1, y1, x2, y2;
+
+      x1 = p1.x - p0.x;
+      y1 = p1.y - p0.y;
+      x2 = p2.x - p0.x;
+      y2 = p2.y - p0.y;
+
+      return x1 * y2 - x2 * y1;
+    }
+
+    function cprod(p0, p1, p2, p3) {
+      var x1, y1, x2, y2;
+
+      x1 = p1.x - p0.x;
+      y1 = p1.y - p0.y;
+      x2 = p3.x - p2.x;
+      y2 = p3.y - p2.y;
+
+      return x1 * y2 - x2 * y1;
+    }
+
+    function iprod(p0, p1, p2) {
+      var x1, y1, x2, y2;
+
+      x1 = p1.x - p0.x;
+      y1 = p1.y - p0.y;
+      x2 = p2.x - p0.x;
+      y2 = p2.y - p0.y;
+
+      return x1 * x2 + y1 * y2;
+    }
+
+    function iprod1(p0, p1, p2, p3) {
+      var x1, y1, x2, y2;
+
+      x1 = p1.x - p0.x;
+      y1 = p1.y - p0.y;
+      x2 = p3.x - p2.x;
+      y2 = p3.y - p2.y;
+
+      return x1 * x2 + y1 * y2;
+    }
+
+    function ddist(p, q) {
+      return Math.sqrt((p.x - q.x) * (p.x - q.x) + (p.y - q.y) * (p.y - q.y));
+    }
+
+    function bezier(t, p0, p1, p2, p3) {
+      var s = 1 - t, res = new Point();
+
+      res.x = s * s * s * p0.x + 3 * (s * s * t) * p1.x + 3 * (t * t * s) * p2.x + t * t * t * p3.x;
+      res.y = s * s * s * p0.y + 3 * (s * s * t) * p1.y + 3 * (t * t * s) * p2.y + t * t * t * p3.y;
+
+      return res;
+    }
+
+    function tangent(p0, p1, p2, p3, q0, q1) {
+      var A, B, C, a, b, c, d, s, r1, r2;
+
+      A = cprod(p0, p1, q0, q1);
+      B = cprod(p1, p2, q0, q1);
+      C = cprod(p2, p3, q0, q1);
+
+      a = A - 2 * B + C;
+      b = -2 * A + 2 * B;
+      c = A;
+
+      d = b * b - 4 * a * c;
+
+      if (a === 0 || d < 0) {
+        return -1.0;
+      }
+
+      s = Math.sqrt(d);
+
+      r1 = (-b + s) / (2 * a);
+      r2 = (-b - s) / (2 * a);
+
+      if (r1 >= 0 && r1 <= 1) {
+        return r1;
+      } else if (r2 >= 0 && r2 <= 1) {
+        return r2;
+      } else {
+        return -1.0;
+      }
+    }
+
+    function calcSums(path) {
+      var i, x, y;
+      path.x0 = path.pt[0].x;
+      path.y0 = path.pt[0].y;
+
+      path.sums = [];
+      var s = path.sums;
+      s.push(new Sum(0, 0, 0, 0, 0));
+      for (i = 0; i < path.len; i++) {
+        x = path.pt[i].x - path.x0;
+        y = path.pt[i].y - path.y0;
+        s.push(new Sum(s[i].x + x, s[i].y + y, s[i].xy + x * y,
+          s[i].x2 + x * x, s[i].y2 + y * y));
+      }
+    }
+
+    function calcLon(path) {
+
+      var n = path.len, pt = path.pt, dir,
+        pivk = new Array(n),
+        nc = new Array(n),
+        ct = new Array(4);
+      path.lon = new Array(n);
+
+      var constraint = [new Point(), new Point()],
+        cur = new Point(),
+        off = new Point(),
+        dk = new Point(),
+        foundk;
+
+      var i, j, k1, a, b, c, d, k = 0;
+      for (i = n - 1; i >= 0; i--) {
+        if (pt[i].x != pt[k].x && pt[i].y != pt[k].y) {
+          k = i + 1;
+        }
+        nc[i] = k;
+      }
+
+      for (i = n - 1; i >= 0; i--) {
+        ct[0] = ct[1] = ct[2] = ct[3] = 0;
+        dir = (3 + 3 * (pt[mod(i + 1, n)].x - pt[i].x) +
+          (pt[mod(i + 1, n)].y - pt[i].y)) / 2;
+        ct[dir]++;
+
+        constraint[0].x = 0;
+        constraint[0].y = 0;
+        constraint[1].x = 0;
+        constraint[1].y = 0;
+
+        k = nc[i];
+        k1 = i;
+        while (1) {
+          foundk = 0;
+          dir = (3 + 3 * sign(pt[k].x - pt[k1].x) +
+            sign(pt[k].y - pt[k1].y)) / 2;
+          ct[dir]++;
+
+          if (ct[0] && ct[1] && ct[2] && ct[3]) {
+            pivk[i] = k1;
+            foundk = 1;
+            break;
+          }
+
+          cur.x = pt[k].x - pt[i].x;
+          cur.y = pt[k].y - pt[i].y;
+
+          if (xprod(constraint[0], cur) < 0 || xprod(constraint[1], cur) > 0) {
+            break;
+          }
+
+          if (Math.abs(cur.x) <= 1 && Math.abs(cur.y) <= 1) {
+
+          } else {
+            off.x = cur.x + ((cur.y >= 0 && (cur.y > 0 || cur.x < 0)) ? 1 : -1);
+            off.y = cur.y + ((cur.x <= 0 && (cur.x < 0 || cur.y < 0)) ? 1 : -1);
+            if (xprod(constraint[0], off) >= 0) {
+              constraint[0].x = off.x;
+              constraint[0].y = off.y;
+            }
+            off.x = cur.x + ((cur.y <= 0 && (cur.y < 0 || cur.x < 0)) ? 1 : -1);
+            off.y = cur.y + ((cur.x >= 0 && (cur.x > 0 || cur.y < 0)) ? 1 : -1);
+            if (xprod(constraint[1], off) <= 0) {
+              constraint[1].x = off.x;
+              constraint[1].y = off.y;
+            }
+          }
+          k1 = k;
+          k = nc[k1];
+          if (!cyclic(k, i, k1)) {
+            break;
+          }
+        }
+        if (foundk === 0) {
+          dk.x = sign(pt[k].x - pt[k1].x);
+          dk.y = sign(pt[k].y - pt[k1].y);
+          cur.x = pt[k1].x - pt[i].x;
+          cur.y = pt[k1].y - pt[i].y;
+
+          a = xprod(constraint[0], cur);
+          b = xprod(constraint[0], dk);
+          c = xprod(constraint[1], cur);
+          d = xprod(constraint[1], dk);
+
+          j = 10000000;
+          if (b < 0) {
+            j = Math.floor(a / -b);
+          }
+          if (d > 0) {
+            j = Math.min(j, Math.floor(-c / d));
+          }
+          pivk[i] = mod(k1 + j, n);
+        }
+      }
+
+      j = pivk[n - 1];
+      path.lon[n - 1] = j;
+      for (i = n - 2; i >= 0; i--) {
+        if (cyclic(i + 1, pivk[i], j)) {
+          j = pivk[i];
+        }
+        path.lon[i] = j;
+      }
+
+      for (i = n - 1; cyclic(mod(i + 1, n), j, path.lon[i]); i--) {
+        path.lon[i] = j;
+      }
+    }
+
+    function bestPolygon(path) {
+
+      function penalty3(path, i, j) {
+
+        var n = path.len, pt = path.pt, sums = path.sums;
+        var x, y, xy, x2, y2,
+          k, a, b, c, s,
+          px, py, ex, ey,
+          r = 0;
+        if (j >= n) {
+          j -= n;
+          r = 1;
+        }
+
+        if (r === 0) {
+          x = sums[j + 1].x - sums[i].x;
+          y = sums[j + 1].y - sums[i].y;
+          x2 = sums[j + 1].x2 - sums[i].x2;
+          xy = sums[j + 1].xy - sums[i].xy;
+          y2 = sums[j + 1].y2 - sums[i].y2;
+          k = j + 1 - i;
+        } else {
+          x = sums[j + 1].x - sums[i].x + sums[n].x;
+          y = sums[j + 1].y - sums[i].y + sums[n].y;
+          x2 = sums[j + 1].x2 - sums[i].x2 + sums[n].x2;
+          xy = sums[j + 1].xy - sums[i].xy + sums[n].xy;
+          y2 = sums[j + 1].y2 - sums[i].y2 + sums[n].y2;
+          k = j + 1 - i + n;
+        }
+
+        px = (pt[i].x + pt[j].x) / 2.0 - pt[0].x;
+        py = (pt[i].y + pt[j].y) / 2.0 - pt[0].y;
+        ey = (pt[j].x - pt[i].x);
+        ex = -(pt[j].y - pt[i].y);
+
+        a = ((x2 - 2 * x * px) / k + px * px);
+        b = ((xy - x * py - y * px) / k + px * py);
+        c = ((y2 - 2 * y * py) / k + py * py);
+
+        s = ex * ex * a + 2 * ex * ey * b + ey * ey * c;
+
+        return Math.sqrt(s);
+      }
+
+      var i, j, m, k,
+        n = path.len,
+        pen = new Array(n + 1),
+        prev = new Array(n + 1),
+        clip0 = new Array(n),
+        clip1 = new Array(n + 1),
+        seg0 = new Array(n + 1),
+        seg1 = new Array(n + 1),
+        thispen, best, c;
+
+      for (i = 0; i < n; i++) {
+        c = mod(path.lon[mod(i - 1, n)] - 1, n);
+        if (c == i) {
+          c = mod(i + 1, n);
+        }
+        if (c < i) {
+          clip0[i] = n;
+        } else {
+          clip0[i] = c;
+        }
+      }
+
+      j = 1;
+      for (i = 0; i < n; i++) {
+        while (j <= clip0[i]) {
+          clip1[j] = i;
+          j++;
+        }
+      }
+
+      i = 0;
+      for (j = 0; i < n; j++) {
+        seg0[j] = i;
+        i = clip0[i];
+      }
+      seg0[j] = n;
+      m = j;
+
+      i = n;
+      for (j = m; j > 0; j--) {
+        seg1[j] = i;
+        i = clip1[i];
+      }
+      seg1[0] = 0;
+
+      pen[0] = 0;
+      for (j = 1; j <= m; j++) {
+        for (i = seg1[j]; i <= seg0[j]; i++) {
+          best = -1;
+          for (k = seg0[j - 1]; k >= clip1[i]; k--) {
+            thispen = penalty3(path, k, i) + pen[k];
+            if (best < 0 || thispen < best) {
+              prev[i] = k;
+              best = thispen;
+            }
+          }
+          pen[i] = best;
+        }
+      }
+      path.m = m;
+      path.po = new Array(m);
+
+      for (i = n, j = m - 1; i > 0; j--) {
+        i = prev[i];
+        path.po[j] = i;
+      }
+    }
+
+    function adjustVertices(path) {
+
+      function pointslope(path, i, j, ctr, dir) {
+
+        var n = path.len, sums = path.sums,
+          x, y, x2, xy, y2,
+          k, a, b, c, lambda2, l, r = 0;
+
+        while (j >= n) {
+          j -= n;
+          r += 1;
+        }
+        while (i >= n) {
+          i -= n;
+          r -= 1;
+        }
+        while (j < 0) {
+          j += n;
+          r -= 1;
+        }
+        while (i < 0) {
+          i += n;
+          r += 1;
+        }
+
+        x = sums[j + 1].x - sums[i].x + r * sums[n].x;
+        y = sums[j + 1].y - sums[i].y + r * sums[n].y;
+        x2 = sums[j + 1].x2 - sums[i].x2 + r * sums[n].x2;
+        xy = sums[j + 1].xy - sums[i].xy + r * sums[n].xy;
+        y2 = sums[j + 1].y2 - sums[i].y2 + r * sums[n].y2;
+        k = j + 1 - i + r * n;
+
+        ctr.x = x / k;
+        ctr.y = y / k;
+
+        a = (x2 - x * x / k) / k;
+        b = (xy - x * y / k) / k;
+        c = (y2 - y * y / k) / k;
+
+        lambda2 = (a + c + Math.sqrt((a - c) * (a - c) + 4 * b * b)) / 2;
+
+        a -= lambda2;
+        c -= lambda2;
+
+        if (Math.abs(a) >= Math.abs(c)) {
+          l = Math.sqrt(a * a + b * b);
+          if (l !== 0) {
+            dir.x = -b / l;
+            dir.y = a / l;
+          }
+        } else {
+          l = Math.sqrt(c * c + b * b);
+          if (l !== 0) {
+            dir.x = -c / l;
+            dir.y = b / l;
+          }
+        }
+        if (l === 0) {
+          dir.x = dir.y = 0;
+        }
+      }
+
+      var m = path.m, po = path.po, n = path.len, pt = path.pt,
+        x0 = path.x0, y0 = path.y0,
+        ctr = new Array(m), dir = new Array(m),
+        q = new Array(m),
+        v = new Array(3), d, i, j, k, l,
+        s = new Point();
+
+      path.curve = new Curve(m);
+
+      for (i = 0; i < m; i++) {
+        j = po[mod(i + 1, m)];
+        j = mod(j - po[i], n) + po[i];
+        ctr[i] = new Point();
+        dir[i] = new Point();
+        pointslope(path, po[i], j, ctr[i], dir[i]);
+      }
+
+      for (i = 0; i < m; i++) {
+        q[i] = new Quad();
+        d = dir[i].x * dir[i].x + dir[i].y * dir[i].y;
+        if (d === 0.0) {
+          for (j = 0; j < 3; j++) {
+            for (k = 0; k < 3; k++) {
+              q[i].data[j * 3 + k] = 0;
+            }
+          }
+        } else {
+          v[0] = dir[i].y;
+          v[1] = -dir[i].x;
+          v[2] = - v[1] * ctr[i].y - v[0] * ctr[i].x;
+          for (l = 0; l < 3; l++) {
+            for (k = 0; k < 3; k++) {
+              q[i].data[l * 3 + k] = v[l] * v[k] / d;
+            }
+          }
+        }
+      }
+
+      var Q, w, dx, dy, det, min, cand, xmin, ymin, z;
+      for (i = 0; i < m; i++) {
+        Q = new Quad();
+        w = new Point();
+
+        s.x = pt[po[i]].x - x0;
+        s.y = pt[po[i]].y - y0;
+
+        j = mod(i - 1, m);
+
+        for (l = 0; l < 3; l++) {
+          for (k = 0; k < 3; k++) {
+            Q.data[l * 3 + k] = q[j].at(l, k) + q[i].at(l, k);
+          }
+        }
+
+        while (1) {
+
+          det = Q.at(0, 0) * Q.at(1, 1) - Q.at(0, 1) * Q.at(1, 0);
+          if (det !== 0.0) {
+            w.x = (-Q.at(0, 2) * Q.at(1, 1) + Q.at(1, 2) * Q.at(0, 1)) / det;
+            w.y = (Q.at(0, 2) * Q.at(1, 0) - Q.at(1, 2) * Q.at(0, 0)) / det;
+            break;
+          }
+
+          if (Q.at(0, 0) > Q.at(1, 1)) {
+            v[0] = -Q.at(0, 1);
+            v[1] = Q.at(0, 0);
+          } else if (Q.at(1, 1)) {
+            v[0] = -Q.at(1, 1);
+            v[1] = Q.at(1, 0);
+          } else {
+            v[0] = 1;
+            v[1] = 0;
+          }
+          d = v[0] * v[0] + v[1] * v[1];
+          v[2] = - v[1] * s.y - v[0] * s.x;
+          for (l = 0; l < 3; l++) {
+            for (k = 0; k < 3; k++) {
+              Q.data[l * 3 + k] += v[l] * v[k] / d;
+            }
+          }
+        }
+        dx = Math.abs(w.x - s.x);
+        dy = Math.abs(w.y - s.y);
+        if (dx <= 0.5 && dy <= 0.5) {
+          path.curve.vertex[i] = new Point(w.x + x0, w.y + y0);
+          continue;
+        }
+
+        min = quadform(Q, s);
+        xmin = s.x;
+        ymin = s.y;
+
+        if (Q.at(0, 0) !== 0.0) {
+          for (z = 0; z < 2; z++) {
+            w.y = s.y - 0.5 + z;
+            w.x = - (Q.at(0, 1) * w.y + Q.at(0, 2)) / Q.at(0, 0);
+            dx = Math.abs(w.x - s.x);
+            cand = quadform(Q, w);
+            if (dx <= 0.5 && cand < min) {
+              min = cand;
+              xmin = w.x;
+              ymin = w.y;
+            }
+          }
+        }
+
+        if (Q.at(1, 1) !== 0.0) {
+          for (z = 0; z < 2; z++) {
+            w.x = s.x - 0.5 + z;
+            w.y = - (Q.at(1, 0) * w.x + Q.at(1, 2)) / Q.at(1, 1);
+            dy = Math.abs(w.y - s.y);
+            cand = quadform(Q, w);
+            if (dy <= 0.5 && cand < min) {
+              min = cand;
+              xmin = w.x;
+              ymin = w.y;
+            }
+          }
+        }
+
+        for (l = 0; l < 2; l++) {
+          for (k = 0; k < 2; k++) {
+            w.x = s.x - 0.5 + l;
+            w.y = s.y - 0.5 + k;
+            cand = quadform(Q, w);
+            if (cand < min) {
+              min = cand;
+              xmin = w.x;
+              ymin = w.y;
+            }
+          }
+        }
+
+        path.curve.vertex[i] = new Point(xmin + x0, ymin + y0);
+      }
+    }
+
+    function reverse(path) {
+      var curve = path.curve, m = curve.n, v = curve.vertex, i, j, tmp;
+
+      for (i = 0, j = m - 1; i < j; i++, j--) {
+        tmp = v[i];
+        v[i] = v[j];
+        v[j] = tmp;
+      }
+    }
+
+    function smooth(path) {
+      var m = path.curve.n, curve = path.curve;
+
+      var i, j, k, dd, denom, alpha,
+        p2, p3, p4;
+
+      for (i = 0; i < m; i++) {
+        j = mod(i + 1, m);
+        k = mod(i + 2, m);
+        p4 = interval(1 / 2.0, curve.vertex[k], curve.vertex[j]);
+
+        denom = ddenom(curve.vertex[i], curve.vertex[k]);
+        if (denom !== 0.0) {
+          dd = dpara(curve.vertex[i], curve.vertex[j], curve.vertex[k]) / denom;
+          dd = Math.abs(dd);
+          alpha = dd > 1 ? (1 - 1.0 / dd) : 0;
+          alpha = alpha / 0.75;
+        } else {
+          alpha = 4 / 3.0;
+        }
+        //console.log(curve.alpha0, j)
+        curve.alpha0[j] = alpha;
+
+        if (alpha >= self.info.alphamax) {
+          curve.tag[j] = "CORNER";
+          curve.c[3 * j + 1] = curve.vertex[j];
+          curve.c[3 * j + 2] = p4;
+        } else {
+          if (alpha < 0.55) {
+            alpha = 0.55;
+          } else if (alpha > 1) {
+            alpha = 1;
+          }
+          p2 = interval(0.5 + 0.5 * alpha, curve.vertex[i], curve.vertex[j]);
+          p3 = interval(0.5 + 0.5 * alpha, curve.vertex[k], curve.vertex[j]);
+          curve.tag[j] = "CURVE";
+          curve.c[3 * j + 0] = p2;
+          curve.c[3 * j + 1] = p3;
+          curve.c[3 * j + 2] = p4;
+        }
+        curve.alpha[j] = alpha;
+        curve.beta[j] = 0.5;
+      }
+      curve.alphacurve = 1;
+    }
+
+    function optiCurve(path) {
+      function Opti() {
+        this.pen = 0;
+        this.c = [new Point(), new Point()];
+        this.t = 0;
+        this.s = 0;
+        this.alpha = 0;
+      }
+
+      function opti_penalty(path, i, j, res, opttolerance, convc, areac) {
+        var m = path.curve.n, curve = path.curve, vertex = curve.vertex,
+          k, k1, k2, conv, i1,
+          area, alpha, d, d1, d2,
+          p0, p1, p2, p3, pt,
+          A, R, A1, A2, A3, A4,
+          s, t;
+
+        if (i == j) {
+          return 1;
+        }
+
+        k = i;
+        i1 = mod(i + 1, m);
+        k1 = mod(k + 1, m);
+        conv = convc[k1];
+        if (conv === 0) {
+          return 1;
+        }
+        d = ddist(vertex[i], vertex[i1]);
+        for (k = k1; k != j; k = k1) {
+          k1 = mod(k + 1, m);
+          k2 = mod(k + 2, m);
+          if (convc[k1] != conv) {
+            return 1;
+          }
+          if (sign(cprod(vertex[i], vertex[i1], vertex[k1], vertex[k2])) !=
+            conv) {
+            return 1;
+          }
+          if (iprod1(vertex[i], vertex[i1], vertex[k1], vertex[k2]) <
+            d * ddist(vertex[k1], vertex[k2]) * -0.999847695156) {
+            return 1;
+          }
+        }
+
+        p0 = curve.c[mod(i, m) * 3 + 2].copy();
+        p1 = vertex[mod(i + 1, m)].copy();
+        p2 = vertex[mod(j, m)].copy();
+        p3 = curve.c[mod(j, m) * 3 + 2].copy();
+
+        area = areac[j] - areac[i];
+        area -= dpara(vertex[0], curve.c[i * 3 + 2], curve.c[j * 3 + 2]) / 2;
+        if (i >= j) {
+          area += areac[m];
+        }
+
+        A1 = dpara(p0, p1, p2);
+        A2 = dpara(p0, p1, p3);
+        A3 = dpara(p0, p2, p3);
+
+        A4 = A1 + A3 - A2;
+
+        if (A2 == A1) {
+          return 1;
+        }
+
+        t = A3 / (A3 - A4);
+        s = A2 / (A2 - A1);
+        A = A2 * t / 2.0;
+
+        if (A === 0.0) {
+          return 1;
+        }
+
+        R = area / A;
+        alpha = 2 - Math.sqrt(4 - R / 0.3);
+
+        res.c[0] = interval(t * alpha, p0, p1);
+        res.c[1] = interval(s * alpha, p3, p2);
+        res.alpha = alpha;
+        res.t = t;
+        res.s = s;
+
+        p1 = res.c[0].copy();
+        p2 = res.c[1].copy();
+
+        res.pen = 0;
+
+        for (k = mod(i + 1, m); k != j; k = k1) {
+          k1 = mod(k + 1, m);
+          t = tangent(p0, p1, p2, p3, vertex[k], vertex[k1]);
+          if (t < -0.5) {
+            return 1;
+          }
+          pt = bezier(t, p0, p1, p2, p3);
+          d = ddist(vertex[k], vertex[k1]);
+          if (d === 0.0) {
+            return 1;
+          }
+          d1 = dpara(vertex[k], vertex[k1], pt) / d;
+          if (Math.abs(d1) > opttolerance) {
+            return 1;
+          }
+          if (iprod(vertex[k], vertex[k1], pt) < 0 ||
+            iprod(vertex[k1], vertex[k], pt) < 0) {
+            return 1;
+          }
+          res.pen += d1 * d1;
+        }
+
+        for (k = i; k != j; k = k1) {
+          k1 = mod(k + 1, m);
+          t = tangent(p0, p1, p2, p3, curve.c[k * 3 + 2], curve.c[k1 * 3 + 2]);
+          if (t < -0.5) {
+            return 1;
+          }
+          pt = bezier(t, p0, p1, p2, p3);
+          d = ddist(curve.c[k * 3 + 2], curve.c[k1 * 3 + 2]);
+          if (d === 0.0) {
+            return 1;
+          }
+          d1 = dpara(curve.c[k * 3 + 2], curve.c[k1 * 3 + 2], pt) / d;
+          d2 = dpara(curve.c[k * 3 + 2], curve.c[k1 * 3 + 2], vertex[k1]) / d;
+          d2 *= 0.75 * curve.alpha[k1];
+          if (d2 < 0) {
+            d1 = -d1;
+            d2 = -d2;
+          }
+          if (d1 < d2 - opttolerance) {
+            return 1;
+          }
+          if (d1 < d2) {
+            res.pen += (d1 - d2) * (d1 - d2);
+          }
+        }
+
+        return 0;
+      }
+
+      var curve = path.curve, m = curve.n, vert = curve.vertex,
+        pt = new Array(m + 1),
+        pen = new Array(m + 1),
+        len = new Array(m + 1),
+        opt = new Array(m + 1),
+        om, i, j, r,
+        o = new Opti(), p0,
+        i1, area, alpha, ocurve,
+        s, t;
+
+      var convc = new Array(m), areac = new Array(m + 1);
+
+      for (i = 0; i < m; i++) {
+        if (curve.tag[i] == "CURVE") {
+          convc[i] = sign(dpara(vert[mod(i - 1, m)], vert[i], vert[mod(i + 1, m)]));
+        } else {
+          convc[i] = 0;
+        }
+      }
+
+      area = 0.0;
+      areac[0] = 0.0;
+      p0 = curve.vertex[0];
+      for (i = 0; i < m; i++) {
+        i1 = mod(i + 1, m);
+        if (curve.tag[i1] == "CURVE") {
+          alpha = curve.alpha[i1];
+          area += 0.3 * alpha * (4 - alpha) *
+            dpara(curve.c[i * 3 + 2], vert[i1], curve.c[i1 * 3 + 2]) / 2;
+          area += dpara(p0, curve.c[i * 3 + 2], curve.c[i1 * 3 + 2]) / 2;
+        }
+        areac[i + 1] = area;
+      }
+
+      pt[0] = -1;
+      pen[0] = 0;
+      len[0] = 0;
+
+
+      for (j = 1; j <= m; j++) {
+        pt[j] = j - 1;
+        pen[j] = pen[j - 1];
+        len[j] = len[j - 1] + 1;
+
+        for (i = j - 2; i >= 0; i--) {
+          r = opti_penalty(path, i, mod(j, m), o, self.info.opttolerance, convc,
+            areac);
+          if (r) {
+            break;
+          }
+          if (len[j] > len[i] + 1 ||
+            (len[j] == len[i] + 1 && pen[j] > pen[i] + o.pen)) {
+            pt[j] = i;
+            pen[j] = pen[i] + o.pen;
+            len[j] = len[i] + 1;
+            opt[j] = o;
+            o = new Opti();
+          }
+        }
+      }
+      om = len[m];
+      ocurve = new Curve(om);
+      s = new Array(om);
+      t = new Array(om);
+
+      j = m;
+      for (i = om - 1; i >= 0; i--) {
+        if (pt[j] == j - 1) {
+          ocurve.tag[i] = curve.tag[mod(j, m)];
+          ocurve.c[i * 3 + 0] = curve.c[mod(j, m) * 3 + 0];
+          ocurve.c[i * 3 + 1] = curve.c[mod(j, m) * 3 + 1];
+          ocurve.c[i * 3 + 2] = curve.c[mod(j, m) * 3 + 2];
+          ocurve.vertex[i] = curve.vertex[mod(j, m)];
+          ocurve.alpha[i] = curve.alpha[mod(j, m)];
+          ocurve.alpha0[i] = curve.alpha0[mod(j, m)];
+          ocurve.beta[i] = curve.beta[mod(j, m)];
+          s[i] = t[i] = 1.0;
+        } else {
+          ocurve.tag[i] = "CURVE";
+          ocurve.c[i * 3 + 0] = opt[j].c[0];
+          ocurve.c[i * 3 + 1] = opt[j].c[1];
+          ocurve.c[i * 3 + 2] = curve.c[mod(j, m) * 3 + 2];
+          ocurve.vertex[i] = interval(opt[j].s, curve.c[mod(j, m) * 3 + 2],
+            vert[mod(j, m)]);
+          ocurve.alpha[i] = opt[j].alpha;
+          ocurve.alpha0[i] = opt[j].alpha;
+          s[i] = opt[j].s;
+          t[i] = opt[j].t;
+        }
+        j = pt[j];
+      }
+
+      for (i = 0; i < om; i++) {
+        i1 = mod(i + 1, om);
+        ocurve.beta[i] = s[i] / (s[i] + t[i1]);
+      }
+      ocurve.alphacurve = 1;
+      path.curve = ocurve;
+    }
+
+    for (var i = 0; i < self.pathlist.length; i++) {
+      var path = self.pathlist[i];
+      calcSums(path);
+      calcLon(path);
+      bestPolygon(path);
+      adjustVertices(path);
+
+      if (path.sign === "-") {
+        reverse(path);
+      }
+
+      smooth(path);
+
+      if (self.info.optcurve) {
+        optiCurve(path);
+      }
+    }
+
+  }
+
+
+  /*
+  return {
+    loadImageFromFile: loadImageFromFile,
+    loadImageFromUrl: loadImageFromUrl,
+    setParameter: setParameter,
+    process: process,
+    getSVG: getSVG,
+    img: imgElement
+  };
+  */
+
+}
+

+ 103 - 0
zorro/src/app/lib/filler/common/random-color.ts

@@ -0,0 +1,103 @@
+
+export default class RandomColor {
+
+  private countPerChannel: number;
+  private numberArray: Array<number>;
+
+  constructor(count: number, extcolors: number[] = []) {
+    this.countPerChannel = Math.ceil(Math.cbrt(count));
+    let colors = Math.pow(this.countPerChannel, 3);
+    let colorsArray = new Uint32Array(colors);
+    let bytesArray = new Uint8Array(colorsArray.buffer);
+
+    let stepPerChannel = this.countPerChannel - 1;
+
+    var index = 0;
+    for (var r = 0; r <= stepPerChannel; r++) {
+      var rr = Math.round((r * 255) / stepPerChannel);
+      for (var g = 0; g <= stepPerChannel; g++) {
+        var gg = Math.round((g * 255) / stepPerChannel);
+        for (var b = 0; b <= stepPerChannel; b++) {
+          var bb = Math.round((b * 255) / stepPerChannel);
+          bytesArray[index * 4 + 0] = rr;
+          bytesArray[index * 4 + 1] = gg;
+          bytesArray[index * 4 + 2] = bb;
+          bytesArray[index * 4 + 3] = 255;
+          index++;
+        }
+      }
+    }
+
+    this.numberArray = new Array(colors);
+    for (var i = 0; i < colorsArray.length; i++) {
+      this.numberArray[i] = colorsArray[i];
+    }
+
+    for (let extcolor of extcolors) {
+      let index = this.numberArray.findIndex(e => e == extcolor);
+      if (index >= 0) {
+        this.numberArray.splice(index, 1);
+      }
+    }
+
+    console.log(this.numberArray.length, this.numberArray);
+  }
+
+  get() {
+    var index = Math.floor(Math.random() * this.numberArray.length);
+    var color = this.numberArray.splice(index, 1);
+    return color[0];
+  }
+
+  checkDistance() {
+    const size = this.numberArray.length;
+    var dist;
+    var minDist = 1000;
+    for (var x = 0; x < size; x++) {
+      for (var y = x + 1; y < size; y++) {
+        dist = this.distance(this.numberArray[x], this.numberArray[y]);
+        if (dist < minDist) {
+          minDist = dist;
+        }
+      }
+    }
+    console.log(`minDist=${minDist}`);
+  }
+
+
+  distance(c1: number, c2: number, debug: boolean = false): number {
+    let a1 = (c1 >> 24) & 0xff;
+    let b1 = (c1 >> 16) & 0xff;
+    let g1 = (c1 >> 8) & 0xff;
+    let r1 = c1 & 0xff;
+
+    let a2 = (c2 >> 24) & 0xff;
+    let b2 = (c2 >> 16) & 0xff;
+    let g2 = (c2 >> 8) & 0xff;
+    let r2 = c2 & 0xff;
+
+    let rr = r1 - r2;
+    let gg = g1 - g2;
+    let bb = b1 - b2;
+
+    let dist = Math.sqrt(rr * rr + gg * gg + bb * bb) / 255;
+
+    if (debug) {
+      console.log(`${c1}, ${c2}, dist:${dist}`);
+      console.log(r1, g1, b1, a1);
+      console.log(r2, g2, b2, a2);
+    }
+
+    return dist;
+  }
+}
+
+// //至少3000个颜色
+// let rand = new RandomColor(9000);
+// rand.checkDistance();
+
+// var c1 = rand.get();
+// var c2 = rand.get();
+// rand.distance(c1, c2, true)
+
+

+ 45 - 0
zorro/src/app/lib/filler/common/repeater.ts

@@ -0,0 +1,45 @@
+
+
+/**
+ * 重复指定次数,然后结束。
+ */
+export default class Repeater {
+  private onRepeat: Function;
+  private onEnd: Function;
+  private count: number;
+  private duration: number;
+  private interval = null;
+  constructor(onRepeat: Function, onEnd: Function, count: number = 6, duration: number = 500) {
+    this.onRepeat = onRepeat;
+    this.onEnd = onEnd;
+    this.count = count;
+    this.duration = duration;
+  }
+
+  start() {
+    if (this.interval) { this.cancel(); }
+
+    let _count = 0;
+    this.interval = setInterval(() => {
+      if (_count < this.count) {
+        _count++;
+        this.onRepeat(_count);
+      } else {
+        this.cancel();
+      }
+    }, this.duration)
+    return this;
+  }
+
+  cancel() {
+    if (this.interval) {
+      clearInterval(this.interval);
+      this.interval = null;
+      this.onEnd();
+    }
+    return this;
+  }
+
+
+}
+

+ 351 - 0
zorro/src/app/lib/filler/common/svg-generator.ts

@@ -0,0 +1,351 @@
+import { validateOrder } from "./color-order";
+import FillArea from "./fillarea";
+import { AreaMap, Centers, ColorMap } from "./interfaces";
+import { Bitmap, Potrace } from "./potrace";
+import Utils from "./utils";
+
+
+export default class SVGGenerator {
+  mapData: ImageData;
+  mapPixels: Uint32Array;
+  pageData: ImageData;
+  pagePixels: Uint32Array;
+  orgData: ImageData;
+  orgPixels: Uint32Array;
+
+  areaMap: AreaMap;
+
+  areaCanvas: HTMLCanvasElement;
+  areaCtx: CanvasRenderingContext2D;
+  colorBlack: number;
+
+  public progress : number;
+  public done : boolean = false;
+
+  svg : string;
+  paths : string[] = [];
+
+  constructor(private map: HTMLImageElement, private page: HTMLImageElement, private org: HTMLImageElement,
+    private colorMap: ColorMap, private centers: Centers, private colorOrder: any) {
+    
+    this.pageData = Utils.getImageData(this.page);
+    this.pagePixels = new Uint32Array(this.pageData.data.buffer);
+    this.mapData = Utils.getImageData(this.map);
+    this.mapPixels = new Uint32Array(this.mapData.data.buffer);
+    this.areaMap = SVGGenerator.createAreaMap(this.mapPixels, this.map.width);
+
+    this.areaCanvas = Utils.createCanvas(this.map.width, this.map.height);
+    this.areaCtx = this.areaCanvas.getContext('2d');
+
+    this.colorBlack = Utils.getColorInteger("#000000");
+
+  }
+
+  async process(flag: number = 0) : Promise<Blob> {
+
+    let output = [];
+    output.push(`<svg id="svg" version="1.1" width="${this.map.width}" height="${this.map.height}" viewBox="0,0,${this.map.width},${this.map.height}" xmlns="http://www.w3.org/2000/svg">`);
+
+    output.push(`\t<g id="areas">`);
+    
+
+    let colorMap = this.colorMap
+    let colorOrder = this.colorOrder;
+    let colorMapKeys = Object.keys(colorMap);
+    // 判断colorOrder是否已经涵盖了colorMap里出现的颜色, 如果涵盖则采用order, 否则不采用
+    if (validateOrder(colorMap, colorOrder)) {  // 根据colorOrder 给colorMap做个排序, 这个顺序将与生成的svg息息相关
+      colorMapKeys.sort((a, b) => {
+        let aidx = colorOrder.indexOf(colorMap[a].color);
+        let bidx = colorOrder.indexOf(colorMap[b].color);
+        return aidx - bidx;
+      });
+    }
+
+    let color: string;
+    for (let i = 0; i < colorMapKeys.length; i++) {
+      let area = this.areaMap[colorMapKeys[i]];
+      if (!area || !this.centers || !this.centers[area.color]) continue;
+      color = Utils.colorToHex(this.colorMap[area.color].cssColor);
+      let bitmap = this.getExpandAreaBitmap(area, 1);
+      let path = this.getAreaPath2(area, color, bitmap);
+      this.paths.push(path);
+      output.push(`\t\t${path}`);
+      console.log(i / colorMapKeys.length, path);
+      this.progress = i / colorMapKeys.length;
+      await Utils.delay(1);
+    }
+    
+    if (flag  == 1) { // 对于未涂色的区块,也生成相应的area path, fill='none'
+      let unColoredAreas = Object.keys(this.areaMap).filter(a => { return !this.colorMap[a]});
+      for (let i = 0; i < unColoredAreas.length; i++) {
+        let area = this.areaMap[unColoredAreas[i]];
+        if (!this.centers || !this.centers[area.color]) continue;
+        let path = this.getAreaPath2(area);
+        this.paths.push(path);
+        output.push(`\t\t${path}`);
+      }
+    }
+
+    output.push(`\t</g>`)
+
+
+    output.push(`\t<g id="outline">`)
+
+    let p =   new Potrace(this.org || this.page);
+    let path = p.getSVG(1, null, true, "#000000");
+    output.push(`\t\t${path}`);
+    output.push(`\t</g>`)
+
+
+    output.push(`</svg>`);
+
+    let svg = output.join('\n');
+    this.svg = svg;
+
+    this.done = true;
+
+    console.log('svg:', svg);
+    return new Blob([svg], {type : "image/svg+xml"});
+  }
+
+  private async getAreaPath(area: FillArea): Promise<string> {
+    let svg = '';
+    let width = this.map.width;
+    let height = this.map.height;
+
+
+    this.areaCtx.clearRect(0, 0, width, height);
+    let imageData = this.areaCtx.getImageData(0, 0, width, height);
+    let pixels = new Uint32Array(imageData.data.buffer);
+
+    let ai, aw, i, x, y;
+    aw = area.width();
+    let total = area.width() * area.height();
+    for (ai = 0; ai < total; ai++) {
+      x = area.left + ai % aw;
+      y = area.top + Math.floor(ai / aw);
+      i = y * width + x;
+      if (this.mapPixels[i] == area.color) {
+        pixels[i] = this.colorBlack;
+      }
+    }
+    this.areaCtx.putImageData(imageData, 0, 0);
+    let blob = await this.canvasToBlob(this.areaCanvas);
+    let p = await Potrace.fromFile(blob);
+    let color = Utils.colorToHex(this.colorMap[area.color].cssColor);
+    svg = p.getSVG(1, null, true, color);
+
+    //center
+    let center =  this.centers[area.color];
+    let id = `${area.color}`;
+    let c = `${center.x},${center.y},${center.radius.toFixed(5)}`;
+    svg = svg.replace('path', `path id="${id}" c="${c}"`);
+
+    return svg;
+  }
+
+
+  private getAreaPath2(area: FillArea, color: string = 'none', bitmap?: Bitmap): string {
+    let svg = '';
+    let width = this.map.width;
+    let height = this.map.height;
+
+    if (!bitmap) {
+      bitmap = new Bitmap(width, height);
+      // 生成potrace所需的bitmap
+      let ai, aw, i, x, y;
+      aw = area.width();
+      let total = area.width() * area.height();
+      for (ai = 0; ai < total; ai++) {
+        x = area.left + ai % aw;
+        y = area.top + Math.floor(ai / aw);
+        i = y * width + x;
+        if (this.mapPixels[i] == area.color) {
+          bitmap.data[i] = 1;
+        }
+      }
+    }
+
+    let p = new Potrace(bitmap);
+    svg = p.getSVG(1, null, true, color);
+
+    //center
+    let center =  this.centers[area.color];
+    let id = `${area.color}`;
+    let c = `${center.x},${center.y},${center.radius.toFixed(5)}`;
+    svg = svg.replace('path', `path id="${id}" c="${c}"`);
+
+    return svg;
+  }
+
+  /**
+   * 扩大area, 返回相应的bitmap
+   * @param area 原area
+   * @param borderWidth 扩大的宽度
+   * @returns 直接返回bitmap
+   */
+  private getExpandAreaBitmap(area: FillArea, borderWidth: number): Bitmap {
+    let ai, ax, ay, aw, ah, i, j, x, y, p, px, py, index, idx, width, height;
+    aw = area.width();
+    ah = area.height();
+    width = this.map.width;
+    height = this.map.height;
+    let bitmap = new Bitmap(width, height);
+    
+    // 直接生成potrace所需的bitmap
+    let total = area.width() * area.height();
+    for (ai = 0; ai < total; ai++) {
+      x = area.left + ai % aw;
+      y = area.top + Math.floor(ai / aw);
+      i = y * width + x;
+      if (this.mapPixels[i] == area.color) {
+        bitmap.data[i] = 1;
+      }
+    }
+
+    let areaPixels = new Uint32Array(aw * ah);
+
+    for (ai = 0; ai < areaPixels.length; ai++) {
+      ax = ai % aw;
+      ay = (ai - ax) / aw;
+      x = area.left + ax;
+      y = area.top + ay;
+      i = y * width + x;
+      if (this.mapPixels[i] == area.color) {
+        areaPixels[ai] = this.mapPixels[i];
+      } else {
+        areaPixels[ai] = 0;
+      }
+    }
+
+    //find contours
+    let flag, pixel;
+    let contours = [];
+    for (ax = 0; ax < aw; ax++) {
+      flag = 0;
+      for (ay = 0; ay < ah; ay++) {
+        ai = ay * aw + ax;
+        pixel = areaPixels[ai];
+        if (!flag && pixel) {
+          flag = 1;
+          contours.push(ai);
+        } else if (flag && !pixel) {
+          flag = 0;
+          contours.push(ai - aw);
+        } else if (flag && pixel) continue;
+        else if (!flag && !pixel) continue;
+      }
+      if (flag) {
+        //last point
+        contours.push(ai);
+      }
+    }
+
+    for (ay = 0; ay < ah; ay++) {
+      flag = 0;
+      for (ax = 0; ax < aw; ax++) {
+        ai = ay * aw + ax;
+        pixel = areaPixels[ai];
+        if (!flag && pixel) {
+          flag = 1;
+          contours.push(ai);
+        } else if (flag && !pixel) {
+          flag = 0;
+          contours.push(ai - 1);
+        } else if (flag && pixel) continue;
+        else if (!flag && !pixel) continue;
+      }
+      if (flag) {
+        //last point
+        contours.push(ai);
+      }
+    }
+
+    let neighbours = this.getNearestNeighbours(borderWidth);
+    let neighbourSet: Set<number> = new Set();
+    for(i = 0; i < contours.length; i++) {
+      ai = contours[i];
+      ax = ai % aw;
+      ay = (ai - ax) / aw;
+      x = area.left + ax;
+      y = area.top + ay;
+      index = y * width + x;
+      idx = index * 4 + 3;
+      if (this.pagePixels[index] 
+        && this.pageData.data[idx] == 255) continue; // 如果该点实打实的线条,则不渗透
+        
+      for (j = 0; j < neighbours.length; j++) {
+        p = neighbours[j];
+        px = x + p.x;
+        py = y + p.y;
+        index = py * width + px;
+        idx = index * 4 + 3;
+        if (px > 0 && px < width && py > 0 && py < height
+          && this.mapPixels[index] != area.color) {
+              neighbourSet.add(index);
+        }
+      }
+    }
+
+    // expand bitmap
+    for (let value of neighbourSet) {
+      bitmap.data[value] = 1;
+    }
+
+    return bitmap;
+  }
+
+    /**
+ * Get the nearest N neighbours
+ * and sort by distance.
+ * @param n integer >=1
+ */
+private getNearestNeighbours(n) {
+  var distArr = [];
+  for (var i = (-1) * n; i <= n; i++) {
+    for (var j = (-1) * n; j <= n; j++) {
+      if (i == 0 && j == 0) {
+        // exclued self
+        continue;
+      }
+      var distance = Math.pow(i, 2) + Math.pow(j, 2);
+      distArr.push({ x: i, y: j, dist: distance });
+    }
+  }
+
+  distArr.sort(function (a, b) { return a.dist - b.dist; });
+
+  return distArr;
+}
+
+  private canvasToBlob(canvas: HTMLCanvasElement): Promise<Blob> {
+    return new Promise((done, reject) => {
+      canvas.toBlob((blob) => {
+        done(blob);
+      }, 'image/png');
+    })
+  }
+
+
+  /**
+   * Create area map from map pixels.
+   */
+  public static createAreaMap(pixels: Uint32Array, width: number): AreaMap {
+    let areaMap = {};
+    let i: number, x: number, y: number, color: number;
+    for (i = 0; i < pixels.length; i++) {
+      color = pixels[i];
+      if (color == 0) continue;
+      x = i % width;
+      y = Math.floor(i / width);
+      if (areaMap[color]) {
+        areaMap[color].addPoint(x, y);
+      } else {
+        areaMap[color] = new FillArea(color, x, y);
+      }
+    }
+    return areaMap;
+  }
+
+
+}

+ 274 - 0
zorro/src/app/lib/filler/common/svg-generator2.ts

@@ -0,0 +1,274 @@
+import { validateOrder } from "./color-order";
+import { ETrace } from "./etrace";
+import FillArea from "./fillarea";
+import { AreaMap, Centers, ColorMap } from "./interfaces";
+import { Potrace } from "./potrace";
+import Utils from "./utils";
+
+
+export default class SVGGenerator {
+  mapData: ImageData;
+  mapPixels: Uint32Array;
+  pageData: ImageData;
+  pagePixels: Uint32Array;
+  orgData: ImageData;
+  orgPixels: Uint32Array;
+
+  areaMap: AreaMap;
+
+  areaCanvas: HTMLCanvasElement;
+  areaCtx: CanvasRenderingContext2D;
+  colorBlack: number;
+  colorRed: number;
+
+  public progress : number;
+  public done : boolean = false;
+
+  etrace: ETrace;
+
+  blobs: {} = {};
+
+
+  canvas: HTMLCanvasElement;
+  ctx: CanvasRenderingContext2D;
+
+  constructor(private map: HTMLImageElement, private page: HTMLImageElement, private org: HTMLImageElement,
+    private colorMap: ColorMap, private centers: Centers, private colorOrder: any) {
+    
+    this.pageData = Utils.getImageData(this.page);
+    this.pagePixels = new Uint32Array(this.pageData.data.buffer);
+    this.mapData = Utils.getImageData(this.map);
+    this.mapPixels = new Uint32Array(this.mapData.data.buffer);
+    this.areaMap = SVGGenerator.createAreaMap(this.mapPixels, this.map.width);
+
+    this.areaCanvas = Utils.createCanvas(this.map.width, this.map.height);
+    this.areaCtx = this.areaCanvas.getContext('2d');
+
+    this.colorBlack = Utils.getColorInteger("#000000");
+    this.colorRed = Utils.getColorInteger("#FF0000");
+
+    this.canvas = Utils.createCanvas(this.map.width, this.map.height);
+    this.ctx = this.canvas.getContext('2d');
+    this.ctx.clearRect(0, 0, this.map.width, this.map.height);
+
+  }
+
+  async process(flag: number = 0) : Promise<{}> {
+
+    let color: string;
+    let p, path, area, svg;
+    let output = [];
+    let output2 = [];
+    let output3 = [];
+    let output4 = [];
+
+    output.push(`<svg id="svg" version="1.1" width="${this.map.width}" height="${this.map.height}" viewBox="0,0,${this.map.width},${this.map.height}" xmlns="http://www.w3.org/2000/svg">`);
+    output.push(`\t<g id="areas">`);
+
+    output2.push(`<svg id="svg" version="1.1" width="${this.map.width}" height="${this.map.height}" viewBox="0,0,${this.map.width},${this.map.height}" xmlns="http://www.w3.org/2000/svg">`);
+    output2.push(`\t<g id="areas">`);
+    
+    output3.push(`<svg id="svg" version="1.1" width="${this.map.width}" height="${this.map.height}" viewBox="0,0,${this.map.width},${this.map.height}" xmlns="http://www.w3.org/2000/svg">`);
+    output3.push(`\t<g id="areas">`);
+
+    output4.push(`<svg id="svg" version="1.1" width="${this.map.width}" height="${this.map.height}" viewBox="0,0,${this.map.width},${this.map.height}" xmlns="http://www.w3.org/2000/svg">`);
+    output4.push(`\t<g id="areas">`);
+
+    let colorMap = this.colorMap
+    let colorOrder = this.colorOrder;
+    let colorMapKeys = Object.keys(colorMap);
+    // 判断colorOrder是否已经涵盖了colorMap里出现的颜色, 如果涵盖则采用order, 否则不采用
+    if (validateOrder(colorMap, colorOrder)) {  // 根据colorOrder 给colorMap做个排序, 这个顺序将与生成的svg息息相关
+      colorMapKeys.sort((a, b) => {
+        let aidx = colorOrder.indexOf(colorMap[a].color);
+        let bidx = colorOrder.indexOf(colorMap[b].color);
+        return aidx - bidx;
+      });
+    }
+
+    this.etrace = new ETrace(this.mapPixels, this.map.width, this.map.height);
+    
+
+    for (let i = 0; i < colorMapKeys.length; i++) {
+      area = this.areaMap[colorMapKeys[i]];
+      if (!area || !this.centers || !this.centers[area.color]) continue;
+      color = Utils.colorToHex(this.colorMap[area.color].cssColor);
+      path = this.getAreaPath(area, color, 'curve');
+
+      if (!path) continue; 
+      
+      output.push(`\t\t${path}`);
+      output2.push(`\t\t${path}`);
+
+      path = this.getAreaPath(area, color, 'line');
+      output3.push(`\t\t${path}`);
+      output4.push(`\t\t${path}`);
+
+      
+      // console.log(i / colorMapKeys.length, path);
+
+      this.progress = i / colorMapKeys.length;
+      await Utils.delay(1);
+    }
+    
+    // if (flag  == 1) { // 对于未涂色的区块,也生成相应的area path, fill='none'
+    //   let unColoredAreas = Object.keys(this.areaMap).filter(a => { return !this.colorMap[a]});
+    //   for (let i = 0; i < unColoredAreas.length; i++) {
+    //     let area = this.areaMap[unColoredAreas[i]];
+    //     if (!this.centers || !this.centers[area.color]) continue;
+    //     let path = this.getAreaPath2(area, 'none', 'curve');
+    //     output.push(`\t\t${path}`);
+    //   }
+    // }
+
+    output.push(`\t</g>`);
+    output2.push(`\t</g>`);
+    output3.push(`\t</g>`);
+    output4.push(`\t</g>`);
+
+
+    p =   new Potrace(this.org || this.page);
+    path = p.getSVG(1, null, true, "#000000");
+
+    output.push(`\t<g id="outline">`);
+    output.push(`\t\t${path}`);
+    output.push(`\t</g>`);
+
+    output3.push(`\t<g id="outline">`)
+    output3.push(`\t\t${path}`);
+    output3.push(`\t</g>`);
+
+
+    output.push(`</svg>`);
+    output2.push(`</svg>`);
+    output3.push(`</svg>`);
+    output4.push(`</svg>`);
+
+
+    svg = output.join('\n');
+    this.blobs['svg1'] = new Blob([svg], {type : "image/svg+xml"});
+    svg = output2.join('\n');
+    this.blobs['svg2'] = new Blob([svg], {type : "image/svg+xml"});
+    svg = output3.join('\n');
+    this.blobs['svg3'] = new Blob([svg], {type : "image/svg+xml"});
+    svg = output4.join('\n');
+    this.blobs['svg4'] = new Blob([svg], {type : "image/svg+xml"});
+
+    this.blobs['contour'] = await this.getContourBlob();
+    // this.blobs['hcontour'] = await this.getHContourBlob();
+    // this.blobs['vcontour'] = await this.getVContourBlob();
+    
+
+    this.done = true;
+
+    // return new Blob([svg], {type : "image/svg+xml"});
+    return this.blobs;
+  }
+
+  private getAreaPath(area: FillArea, color: string = 'none', mode: string = 'curve'): string {
+    let svg = '';
+
+    svg = this.etrace.getSVG(area.color, null, true, color, mode);
+
+    if (svg.includes(`d=""`)) {
+      return null;
+    }
+
+    //center
+    let center =  this.centers[area.color];
+    let id = `${area.color}`;
+    let c = `${center.x},${center.y},${center.radius.toFixed(5)}`;
+    svg = svg.replace('path', `path id="${id}" c="${c}"`);
+
+    return svg;
+  }
+
+
+  private canvasToBlob(canvas: HTMLCanvasElement): Promise<Blob> {
+    return new Promise((done, reject) => {
+      canvas.toBlob((blob) => {
+        done(blob);
+      }, 'image/png');
+    })
+  }
+
+  /**
+   * Create area map from map pixels.
+   */
+  public static createAreaMap(pixels: Uint32Array, width: number): AreaMap {
+    let areaMap = {};
+    let i: number, x: number, y: number, color: number;
+    for (i = 0; i < pixels.length; i++) {
+      color = pixels[i];
+      if (color == 0) continue;
+      x = i % width;
+      y = Math.floor(i / width);
+      if (areaMap[color]) {
+        areaMap[color].addPoint(x, y);
+      } else {
+        areaMap[color] = new FillArea(color, x, y);
+      }
+    }
+    return areaMap;
+  }
+
+
+  private randomColor() {
+    var color = new Uint8Array([0, 0, 0, 255]);
+    for (var i = 0; i < 3; i++) {
+      color[i] = Math.floor(Math.random() * 256);
+    }
+    var u32 = new Uint32Array(color.buffer);
+    return u32[0];
+  }
+
+
+
+  async getContourBlob() {
+    let intersectMap = this.etrace.intersectMap;
+    this.ctx.clearRect(0, 0, this.map.width, this.map.height);
+    let imageData = this.ctx.getImageData(0, 0, this.map.width, this.map.height);
+    let pixels = new Uint32Array(imageData.data.buffer);
+    let keys = Object.keys(intersectMap);
+    for (let key of keys) {
+      let contours = intersectMap[key].data;
+      let color = this.randomColor();
+      for (let idx of contours) {
+        pixels[idx] = color;
+      }
+    }
+    this.ctx.putImageData(imageData, 0, 0);
+    let blob = await this.canvasToBlob(this.canvas);
+    return blob;
+  }
+
+
+  async getHContourBlob() {
+    this.ctx.clearRect(0, 0, this.map.width, this.map.height);
+    let imageData = this.ctx.getImageData(0, 0, this.map.width, this.map.height);
+    let pixels = new Uint32Array(imageData.data.buffer);
+
+    for (let index of this.etrace.hContour) {
+      pixels[index] = this.colorBlack;
+    }
+
+    this.ctx.putImageData(imageData, 0, 0);
+    let blob = await this.canvasToBlob(this.canvas);
+    return blob;
+  }
+
+  async getVContourBlob() {
+    this.ctx.clearRect(0, 0, this.map.width, this.map.height);
+    let imageData = this.ctx.getImageData(0, 0, this.map.width, this.map.height);
+    let pixels = new Uint32Array(imageData.data.buffer);
+
+    for (let index of this.etrace.vContour) {
+      pixels[index] = this.colorBlack;
+    }
+
+    this.ctx.putImageData(imageData, 0, 0);
+    let blob = await this.canvasToBlob(this.canvas);
+    return blob;
+  }
+
+}

+ 285 - 0
zorro/src/app/lib/filler/common/svg-generator3.ts

@@ -0,0 +1,285 @@
+import { validateOrder } from "./color-order";
+import { ETrace } from "./etrace2";
+import FillArea from "./fillarea";
+import { AreaMap, Centers, ColorMap } from "./interfaces";
+import { Potrace } from "./potrace";
+import Utils from "./utils";
+
+
+export default class SVGGenerator {
+  mapData: ImageData;
+  mapPixels: Uint32Array;
+  pageData: ImageData;
+  pagePixels: Uint32Array;
+  orgData: ImageData;
+  orgPixels: Uint32Array;
+
+  areaMap: AreaMap;
+
+  areaCanvas: HTMLCanvasElement;
+  areaCtx: CanvasRenderingContext2D;
+  colorBlack: number;
+  colorRed: number;
+
+  public progress : number;
+  public done : boolean = false;
+
+  etrace: ETrace;
+
+  blobs: {} = {};
+
+
+  canvas: HTMLCanvasElement;
+  ctx: CanvasRenderingContext2D;
+
+  constructor(private map: HTMLImageElement, private page: HTMLImageElement, private org: HTMLImageElement,
+    private colorMap: ColorMap, private centers: Centers, private colorOrder: any) {
+    
+    this.pageData = Utils.getImageData(this.page);
+    this.pagePixels = new Uint32Array(this.pageData.data.buffer);
+    this.mapData = Utils.getImageData(this.map);
+    this.mapPixels = new Uint32Array(this.mapData.data.buffer);
+    this.areaMap = SVGGenerator.createAreaMap(this.mapPixels, this.map.width);
+
+    this.areaCanvas = Utils.createCanvas(this.map.width, this.map.height);
+    this.areaCtx = this.areaCanvas.getContext('2d');
+
+    this.colorBlack = Utils.getColorInteger("#000000");
+    this.colorRed = Utils.getColorInteger("#FF0000");
+
+    this.canvas = Utils.createCanvas(this.map.width, this.map.height);
+    this.ctx = this.canvas.getContext('2d');
+    this.ctx.clearRect(0, 0, this.map.width, this.map.height);
+
+  }
+
+  async process(flag: number = 0) : Promise<{}> {
+
+    let color: string;
+    let p, path, area, svg;
+    let output = [];
+    let output2 = [];
+    let output3 = [];
+    let output4 = [];
+
+    output.push(`<svg id="svg" version="1.1" width="${this.map.width}" height="${this.map.height}" viewBox="0,0,${this.map.width},${this.map.height}" xmlns="http://www.w3.org/2000/svg">`);
+    output.push(`\t<g id="areas">`);
+
+    output2.push(`<svg id="svg" version="1.1" width="${this.map.width}" height="${this.map.height}" viewBox="0,0,${this.map.width},${this.map.height}" xmlns="http://www.w3.org/2000/svg">`);
+    output2.push(`\t<g id="areas">`);
+    
+    output3.push(`<svg id="svg" version="1.1" width="${this.map.width}" height="${this.map.height}" viewBox="0,0,${this.map.width},${this.map.height}" xmlns="http://www.w3.org/2000/svg">`);
+    output3.push(`\t<g id="areas">`);
+
+    output4.push(`<svg id="svg" version="1.1" width="${this.map.width}" height="${this.map.height}" viewBox="0,0,${this.map.width},${this.map.height}" xmlns="http://www.w3.org/2000/svg">`);
+    output4.push(`\t<g id="areas">`);
+
+    let colorMap = this.colorMap
+    let colorOrder = this.colorOrder;
+    let colorMapKeys = Object.keys(colorMap);
+    // 判断colorOrder是否已经涵盖了colorMap里出现的颜色, 如果涵盖则采用order, 否则不采用
+    if (validateOrder(colorMap, colorOrder)) {  // 根据colorOrder 给colorMap做个排序, 这个顺序将与生成的svg息息相关
+      colorMapKeys.sort((a, b) => {
+        let aidx = colorOrder.indexOf(colorMap[a].color);
+        let bidx = colorOrder.indexOf(colorMap[b].color);
+        return aidx - bidx;
+      });
+    }
+
+    this.etrace = new ETrace(this.mapPixels, this.map.width, this.map.height);
+    
+
+    for (let i = 0; i < colorMapKeys.length; i++) {
+      area = this.areaMap[colorMapKeys[i]];
+      if (!area || !this.centers || !this.centers[area.color]) continue;
+      color = Utils.colorToHex(this.colorMap[area.color].cssColor);
+      path = this.getAreaPath(area, color, 'curve');
+
+      if (!path) continue; 
+      
+      output.push(`\t\t${path}`);
+      output2.push(`\t\t${path}`);
+
+      path = this.getAreaPath(area, color, 'line');
+      output3.push(`\t\t${path}`);
+      output4.push(`\t\t${path}`);
+
+      
+      // console.log(i / colorMapKeys.length, path);
+
+      this.progress = i / colorMapKeys.length;
+      await Utils.delay(1);
+    }
+    
+    // if (flag  == 1) { // 对于未涂色的区块,也生成相应的area path, fill='none'
+    //   let unColoredAreas = Object.keys(this.areaMap).filter(a => { return !this.colorMap[a]});
+    //   for (let i = 0; i < unColoredAreas.length; i++) {
+    //     let area = this.areaMap[unColoredAreas[i]];
+    //     if (!this.centers || !this.centers[area.color]) continue;
+    //     let path = this.getAreaPath2(area, 'none', 'curve');
+    //     output.push(`\t\t${path}`);
+    //   }
+    // }
+
+    output.push(`\t</g>`);
+    output2.push(`\t</g>`);
+    output3.push(`\t</g>`);
+    output4.push(`\t</g>`);
+
+
+    p =   new Potrace(this.org || this.page);
+    path = p.getSVG(1, null, true, "#000000");
+
+    output.push(`\t<g id="outline">`);
+    output.push(`\t\t${path}`);
+    output.push(`\t</g>`);
+
+    output3.push(`\t<g id="outline">`)
+    output3.push(`\t\t${path}`);
+    output3.push(`\t</g>`);
+
+
+    output.push(`</svg>`);
+    output2.push(`</svg>`);
+    output3.push(`</svg>`);
+    output4.push(`</svg>`);
+
+
+    svg = output.join('\n');
+    this.blobs['svg1'] = new Blob([svg], {type : "image/svg+xml"});
+    svg = output2.join('\n');
+    this.blobs['svg2'] = new Blob([svg], {type : "image/svg+xml"});
+    svg = output3.join('\n');
+    this.blobs['svg3'] = new Blob([svg], {type : "image/svg+xml"});
+    svg = output4.join('\n');
+    this.blobs['svg4'] = new Blob([svg], {type : "image/svg+xml"});
+
+    this.blobs['contour'] = await this.getContourBlob();
+    // this.blobs['hcontour'] = await this.getHContourBlob();
+    // this.blobs['vcontour'] = await this.getVContourBlob();
+    
+
+    this.done = true;
+
+    // return new Blob([svg], {type : "image/svg+xml"});
+    return this.blobs;
+  }
+
+  private getAreaPath(area: FillArea, color: string = 'none', mode: string = 'curve'): string {
+    let svg = '';
+
+    svg = this.etrace.getSVG(area.color, null, true, color, mode);
+
+    if (svg.includes(`d=""`)) {
+      return null;
+    }
+
+    //center
+    let center =  this.centers[area.color];
+    let id = `${area.color}`;
+    let c = `${center.x},${center.y},${center.radius.toFixed(5)}`;
+    svg = svg.replace('path', `path id="${id}" c="${c}"`);
+
+    return svg;
+  }
+
+
+  private canvasToBlob(canvas: HTMLCanvasElement): Promise<Blob> {
+    return new Promise((done, reject) => {
+      canvas.toBlob((blob) => {
+        done(blob);
+      }, 'image/png');
+    })
+  }
+
+  /**
+   * Create area map from map pixels.
+   */
+  public static createAreaMap(pixels: Uint32Array, width: number): AreaMap {
+    let areaMap = {};
+    let i: number, x: number, y: number, color: number;
+    for (i = 0; i < pixels.length; i++) {
+      color = pixels[i];
+      if (color == 0) continue;
+      x = i % width;
+      y = Math.floor(i / width);
+      if (areaMap[color]) {
+        areaMap[color].addPoint(x, y);
+      } else {
+        areaMap[color] = new FillArea(color, x, y);
+      }
+    }
+    return areaMap;
+  }
+
+
+  private randomColor() {
+    var color = new Uint8Array([0, 0, 0, 255]);
+    for (var i = 0; i < 3; i++) {
+      color[i] = Math.floor(Math.random() * 256);
+    }
+    var u32 = new Uint32Array(color.buffer);
+    return u32[0];
+  }
+
+
+
+  async getContourBlob() {
+    let w = this.map.width + 1;  // 注意下,因etrace会对map扩展+1, 所以这里+1
+    let h = this.map.height + 1;
+    let intersectMap = this.etrace.intersectMap;
+    this.ctx.clearRect(0, 0, w, h);
+    let imageData = this.ctx.getImageData(0, 0, w, h);
+    let pixels = new Uint32Array(imageData.data.buffer);
+    let keys = Object.keys(intersectMap);
+    for (let key of keys) {
+      let contours = intersectMap[key];
+      let color = this.randomColor();
+      for (let p of contours) {
+        let sp = p.split('-'); 
+        // let x = Math.floor(+sp[0]), y = Math.floor(+sp[1]);
+        let x = Math.round(+sp[0]), y = Math.round(+sp[1]);
+        let idx = y * w + x;
+        pixels[idx] = color;
+      }
+    }
+
+    let poSet = this.etrace.poSet;
+    for (let idx of poSet) {
+      pixels[idx] = this.colorBlack;
+    }
+    this.ctx.putImageData(imageData, 0, 0);
+    let blob = await this.canvasToBlob(this.canvas);
+    return blob;
+  }
+
+
+  async getHContourBlob() {
+    this.ctx.clearRect(0, 0, this.map.width, this.map.height);
+    let imageData = this.ctx.getImageData(0, 0, this.map.width, this.map.height);
+    let pixels = new Uint32Array(imageData.data.buffer);
+
+    for (let index of this.etrace.hContour) {
+      pixels[index] = this.colorBlack;
+    }
+
+    this.ctx.putImageData(imageData, 0, 0);
+    let blob = await this.canvasToBlob(this.canvas);
+    return blob;
+  }
+
+  async getVContourBlob() {
+    this.ctx.clearRect(0, 0, this.map.width, this.map.height);
+    let imageData = this.ctx.getImageData(0, 0, this.map.width, this.map.height);
+    let pixels = new Uint32Array(imageData.data.buffer);
+
+    for (let index of this.etrace.vContour) {
+      pixels[index] = this.colorBlack;
+    }
+
+    this.ctx.putImageData(imageData, 0, 0);
+    let blob = await this.canvasToBlob(this.canvas);
+    return blob;
+  }
+
+}

+ 512 - 0
zorro/src/app/lib/filler/common/utils.ts

@@ -0,0 +1,512 @@
+
+/**
+ * Temp canvas for color parse.
+ */
+var tempCanvas = document.createElement('canvas');
+tempCanvas.width = 1;
+tempCanvas.height = 1;
+var tempCtx = tempCanvas.getContext('2d');
+
+export default class Utils {
+
+  static setImageSmoothing(_ctx: CanvasRenderingContext2D, smoothing: boolean) {
+    let ctx = _ctx as any;
+    ctx.msImageSmoothingEnabled = smoothing;
+    ctx.mozImageSmoothingEnabled = smoothing;
+    ctx.webkitImageSmoothingEnabled = smoothing;
+    ctx.msImageSmoothingEnabled = smoothing;
+    ctx.imageSmoothingEnabled = smoothing;
+  }
+
+  static isFunction(functionToCheck) {
+    return functionToCheck &&
+      {}.toString.call(functionToCheck) === '[object Function]';
+  }
+
+  /**
+   * Check if image loaded.
+   */
+  static isLoaded(img) {
+    if (!img)
+      return false;
+
+    // During the onload event, IE correctly identifies any images that
+    // weren’t downloaded as not complete. Others should too. Gecko-based
+    // browsers act like NS4 in that they report this incorrectly.
+    if (!img.complete) {
+      return false;
+    }
+
+    // However, they do have two very useful properties: naturalWidth and
+    // naturalHeight. These give the true size of the image. If it failed
+    // to load, either of these should be zero.
+    if (img.naturalWidth === 0) {
+      return false;
+    }
+
+    // No other way of checking: assume it’s ok.
+    return true;
+  }
+
+  /**
+   * Promise load image.
+   */
+  static loadImage(url) {
+    return new Promise(function (done, reject) {
+      var image = new Image();
+      image.onload = function () { done(image); };
+      image.onerror = reject;
+      image.src = url;
+    });
+  }
+
+  /**
+   * Promise load array of images
+   */
+  static loadImages(urls) {
+    var ps = urls.map(function (url) { return Utils.loadImage(url); });
+    return Promise.all(ps);
+  }
+
+  static createCanvas(width, height) {
+    var canvas = document.createElement('canvas');
+    canvas.width = width;
+    canvas.height = height;
+    return canvas;
+  }
+
+  static getImageData(image: HTMLImageElement): ImageData {
+    let canvas = Utils.createCanvas(image.width, image.height);
+    let ctx = canvas.getContext('2d');
+    Utils.setImageSmoothing(ctx, false);
+    ctx.drawImage(image, 0, 0);
+    return ctx.getImageData(0, 0, image.width, image.height);
+  }
+
+
+  static convertToPngBlob(srcImg: HTMLImageElement): Promise<Blob> {
+    let canvas = Utils.createCanvas(srcImg.width, srcImg.height);
+    let ctx = canvas.getContext('2d');
+    Utils.setImageSmoothing(ctx, false);
+    ctx.drawImage(srcImg, 0, 0);
+    return new Promise((done, reject) => {
+      canvas.toBlob((blob: Blob) => done(blob));
+    });
+  }
+
+  static convertToJpegBlob(srcImg: HTMLImageElement, quality: number = 0.8): Promise<Blob> {
+    let canvas = Utils.createCanvas(srcImg.width, srcImg.height);
+    let ctx = canvas.getContext('2d');
+    Utils.setImageSmoothing(ctx, false);
+    ctx.drawImage(srcImg, 0, 0);
+    return new Promise((done, reject) => {
+      canvas.toBlob((blob: Blob) => done(blob), 'image/jpeg', quality);
+    });
+  }
+
+  /**
+   * parse color, return rgba obj.
+   */
+  static getRgba(color) {
+    tempCtx.fillStyle = color;
+    tempCtx.fillRect(0, 0, 1, 1);
+    let imgData = tempCtx.getImageData(0, 0, 1, 1);
+    return {
+      red: imgData.data[0], green: imgData.data[1], blue: imgData.data[2],
+      alpha: imgData.data[3],
+    }
+  }
+
+  static getColorInteger(color) {
+    tempCtx.fillStyle = color;
+    tempCtx.fillRect(0, 0, 1, 1);
+    let imgData = tempCtx.getImageData(0, 0, 1, 1);
+    let arr = new Uint32Array(imgData.data.buffer);
+    return arr[0];
+  }
+
+  static getColorFromInteger(intColor) {
+    //console.log('getColorFromInteger: ', intColor);
+    let a = new Uint32Array(1)
+    a[0] = intColor;
+    let b = new Uint8Array(a.buffer);
+    return `rgba(${b[0]},${b[1]},${b[2]},${b[3] / 255})`;
+  }
+
+
+  static toHsl(color) {
+    tempCtx.fillStyle = color;
+    tempCtx.fillRect(0, 0, 1, 1);
+    let imgData = tempCtx.getImageData(0, 0, 1, 1);
+  }
+
+  /**
+   * 将任意颜色转换成hex, 忽略透明度
+   * @param colorStr 
+   * @returns 
+   */
+  static colorToHex(colorStr): string {
+    let arr: Uint32Array = new Uint32Array(1);
+    arr[0] = this.getColorInteger(colorStr);
+    let buf: Uint8Array = new Uint8Array(arr.buffer);
+    let r = buf[0].toString(16).padStart(2, '0').toUpperCase();
+    let g = buf[1].toString(16).padStart(2, '0').toUpperCase();
+    let b = buf[2].toString(16).padStart(2, '0').toUpperCase();
+    return `#${r}${g}${b}`;
+  }
+
+  static makeRgba(rgba) {
+    return 'rgba(' + rgba.red + ',' + rgba.green + ',' + rgba.blue + ',' +
+      rgba.alpha / 255 + ')';
+  }
+
+  static transparentColor(color) { return Utils.alphaColor(color, 0); }
+
+  static alphaColor(color, alpha) {
+    let rgba = Utils.getRgba(color);
+    rgba.alpha = alpha * 255;
+    return Utils.makeRgba(rgba);
+  }
+
+  static randomColor() {
+    return Utils.makeRgba({
+      red: Math.round(Math.random() * 255),
+      green: Math.round(Math.random() * 255),
+      blue: Math.round(Math.random() * 255),
+      alpha: 255,
+    });
+  }
+
+  // 计算两个颜色的距离
+  static colorDistance(c1: number, c2: number): number {
+    let a1 = (c1 >> 24) & 0xff;
+    let b1 = (c1 >> 16) & 0xff;
+    let g1 = (c1 >> 8) & 0xff;
+    let r1 = c1 & 0xff;
+
+    let a2 = (c2 >> 24) & 0xff;
+    let b2 = (c2 >> 16) & 0xff;
+    let g2 = (c2 >> 8) & 0xff;
+    let r2 = c2 & 0xff;
+
+    let rr = r1 - r2;
+    let gg = g1 - g2;
+    let bb = b1 - b2;
+
+    let dist = Math.sqrt(rr * rr + gg * gg + bb * bb) / 255;
+
+    if (dist < 0.01) {
+      console.log(`${c1}, ${c2}, dist:${dist}`);
+      console.log(r1, g1, b1, a1);
+      console.log(r2, g2, b2, a2);
+    }
+
+    return dist;
+  }
+
+  /**
+   * Draw rounded corners rect.
+   */
+  static roundedCornersRect(ctx, left, top, width, height, radius) {
+    // console.log('Utils@roundedCornersRect() ', left, top, width, height,
+    // radius);
+    let right = left + width;
+    let bottom = top + height;
+    ctx.beginPath();
+    ctx.moveTo(left + radius, top);
+    ctx.lineTo(right - radius, top);
+    ctx.arcTo(right, top, right, top + radius, radius);
+    ctx.lineTo(right, bottom - radius);
+    ctx.arcTo(right, bottom, right - radius, bottom, radius)
+    ctx.lineTo(left + radius, bottom);
+    ctx.arcTo(left, bottom, left, bottom - radius, radius);
+    ctx.lineTo(left, top + radius);
+    ctx.arcTo(left, top, left + radius, top, radius);
+    ctx.closePath();
+  }
+
+  /**
+   * Get distance of p1 and p2.
+   */
+  static distance(p1, p2) {
+    let dx = p1.x - p2.x;
+    let dy = p1.y - p2.y;
+    let dist = Math.sqrt(dx * dx + dy * dy);
+    return dist;
+  }
+
+  /*
+   * Get the angle of p1 and p2.
+   */
+  static angle(p1, p2) {
+    let dx = p2.x - p1.x;
+    let dy = p2.y - p1.y;
+    return Math.atan2(dy, dx);
+  }
+
+  /**
+   * Fill points between p1 and p2, by specified gap.
+   */
+  static fillPoints(p1, p2, gap) {
+    let dist = Utils.distance(p1, p2);
+    let angle = Utils.angle(p1, p2);
+    let i;
+    let pts = [], p;
+    for (i = gap; i < dist; i += gap) {
+      p = {};
+      p.x = p1.x + i * Math.cos(angle);
+      p.y = p1.y + i * Math.sin(angle);
+      pts.push(p);
+    }
+    return pts;
+  }
+
+  /**
+   * Create scaled image.
+   */
+  static createScaledImage(image, scale): Promise<HTMLImageElement> {
+    let width = Math.round(image.width * scale);
+    let height = Math.round(image.height * scale);
+    let canvas = Utils.createCanvas(width, height);
+    let ctx = canvas.getContext('2d');
+    Utils.setImageSmoothing(ctx, false);
+    ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height);
+
+    return new Promise(function (done, reject) {
+      let img = new Image();
+      img.onload = function () { done(img); };
+      img.onerror = reject;
+      img.src = canvas.toDataURL();
+    });
+  }
+
+  /**
+   * load file as array buffer.
+   */
+  static loadFileAsArrayBuffer(file: Blob) {
+    return new Promise(function (done, reject) {
+      let reader = new FileReader();
+      reader.onload = function (e: any) { done(e.target.result); };
+      reader.onerror = reject;
+      reader.readAsArrayBuffer(file);
+    });
+  }
+
+  /**
+   * 将blob对象转成图片对象
+   * @param blob 
+   * @returns 
+   */
+  static blobToImage(blob: Blob): Promise<HTMLImageElement> {
+    return new Promise((done, reject) => {
+      let reader = new FileReader();
+      let image = new Image();
+      image.onerror = reject;
+      image.onload = () => done(image);
+      reader.addEventListener('load', () => {
+        image.src = reader.result as string;
+      }, false);
+      reader.readAsDataURL(blob);
+    });
+  }
+
+
+  /**
+   * Convert image to png blob.
+   * @param inputImg 
+   * @returns 
+   */
+  static toPngBlob(inputImg: HTMLImageElement): Promise<Blob> {
+    let canvas = Utils.createCanvas(inputImg.width, inputImg.height);
+    let ctx = canvas.getContext('2d');
+    ctx.drawImage(inputImg, 0, 0);
+    return new Promise((done, reject) => {
+      return canvas.toBlob(blob => done(blob), 'image/png');
+    });
+  }
+
+  /**
+   * convert image to dataURL
+   * @param inputImg 
+   * @param type inputImg type, default: image/png
+   */
+  static imageToDataURL(inputImg: HTMLImageElement, type?: string): string {
+    let canvas = Utils.createCanvas(inputImg.width, inputImg.height);
+    let ctx = canvas.getContext('2d');
+    ctx.drawImage(inputImg, 0, 0);
+    return canvas.toDataURL();
+  }
+
+
+  /**
+   *
+   * Count differenct colors of image
+   */
+  static countColors(image) {
+    let imageData = Utils.getImageData(image);
+    let pixels = new Uint32Array(imageData.data.buffer);
+    let p, hash = {};
+    for (let i = 0; i < pixels.length; i++) {
+      p = pixels[i];
+      if (hash[p])
+        hash[p]++;
+      else
+        hash[p] = 1;
+    }
+    return Object.keys(hash).length;
+  }
+
+
+
+  static loadImageFromUrl(url): Promise<HTMLImageElement> {
+    return new Promise((done, reject) => {
+      let image = new Image();
+      image.onload = () => done(image);
+      image.onerror = reject;
+      image.src = url;
+    });
+  }
+
+  /**
+   * Return image from a base64 string.
+   * @param buffer 
+   */
+  static base64ToImage(base64str, mime): Promise<HTMLImageElement> {
+    mime = mime || 'image/png';
+    //if (!base64str) return ;
+    let dataURI = `data:${mime};base64,${base64str}`;
+    //console.log('dataURI: ', dataURI);
+    return new Promise((done, reject) => {
+      let image = new Image();
+      image.onload = () => { done(image) }
+      image.onerror = function (error) {
+        let e = new Error(`base64ToImage failed with dataURI:${dataURI}`);
+        reject(e);
+      };
+      image.src = dataURI;
+    })
+  }
+
+
+
+  /**
+   * 
+   * @returns 当前是否全屏
+   */
+  public static isFullscreen() {
+    let _doc: any = document;
+    return _doc && (_doc.fullscreenElement || _doc.mozFullScreenElement ||
+      _doc.webkitFullscreenElement || _doc.msFullscreenElement);
+
+  }
+
+  /**
+   * 切换全屏
+   * @param elem 
+   */
+  public static async toggleFullscreen(elem?) {
+    try {
+      elem = elem || document.documentElement;
+      let _elem: any = elem;
+      let _doc: any = document;
+
+      if (!_doc.fullscreenElement && !_doc.mozFullScreenElement &&
+        !_doc.webkitFullscreenElement && !_doc.msFullscreenElement) {
+        if (_elem.requestFullscreen) {
+          await _elem.requestFullscreen();
+        } else if (_elem.msRequestFullscreen) {
+          await _elem.msRequestFullscreen();
+        } else if (_elem.mozRequestFullScreen) {
+          await _elem.mozRequestFullScreen();
+        } else if (_elem.webkitRequestFullscreen) {
+          let _element: any = Element;
+          await _elem.webkitRequestFullscreen(_element.ALLOW_KEYBOARD_INPUT);
+        }
+      } else {
+        if (_doc.exitFullscreen) {
+          await _doc.exitFullscreen();
+        } else if (_doc.msExitFullscreen) {
+          await _doc.msExitFullscreen();
+        } else if (_doc.mozCancelFullScreen) {
+          await _doc.mozCancelFullScreen();
+        } else if (_doc.webkitExitFullscreen) {
+          await _doc.webkitExitFullscreen();
+        }
+      }
+    } catch (e) {
+      console.warn(e);
+    }
+  }
+
+
+  /*
+static betterColor(hexColor) {
+  let hsl = Utils.hexToHsl(hexColor);
+  let out = hsluv.hsluvToHex(hsl);
+  return out;
+}
+
+
+static hexToHsl(H) {
+  let ex = /^#([\da-f]{3}){1,2}$/i;
+  if (ex.test(H)) {
+    // convert hex to RGB first
+    let r = 0,
+      g = 0,
+      b = 0;
+    if (H.length == 4) {
+      r = "0x" + H[1] + H[1];
+      g = "0x" + H[2] + H[2];
+      b = "0x" + H[3] + H[3];
+    } else if (H.length == 7) {
+      r = "0x" + H[1] + H[2];
+      g = "0x" + H[3] + H[4];
+      b = "0x" + H[5] + H[6];
+    }
+    // then to HSL
+    r /= 255;
+    g /= 255;
+    b /= 255;
+    let cmin = Math.min(r, g, b),
+      cmax = Math.max(r, g, b),
+      delta = cmax - cmin,
+      h = 0,
+      s = 0,
+      l = 0;
+
+    if (delta == 0)
+      h = 0;
+    else if (cmax == r)
+      h = ((g - b) / delta) % 6;
+    else if (cmax == g)
+      h = (b - r) / delta + 2;
+    else
+      h = (r - g) / delta + 4;
+
+    h = Math.round(h * 60);
+
+    if (h < 0)
+      h += 360;
+
+    l = (cmax + cmin) / 2;
+    s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
+    s = +(s * 100).toFixed(1);
+    l = +(l * 100).toFixed(1);
+
+    //return "hsl(" + h + "," + s + "%," + l + "%)";
+    return [Math.round(h), Math.round(s), Math.round(l)];
+
+  } else {
+    return "Invalid input color";
+  }
+}
+*/
+
+  public static delay(ms): Promise<any> {
+    return new Promise((done) => {
+      setTimeout(done, ms);
+    });
+  }
+
+
+}

+ 292 - 0
zorro/src/app/lib/filler/common/work-center-finder.ts

@@ -0,0 +1,292 @@
+import FillArea from "./fillarea";
+import { AreaMap, Centers } from "./interfaces";
+
+/**
+ * 找到填色页所有区域的最大内切圆中心点以及半径
+ */
+ export default class WorkCenterFinder {
+  pagePixels: Uint32Array;
+  mapPixels: Uint32Array;
+  areaMap: AreaMap;
+  width: number;
+  height: number;
+
+  /**
+   * webworker 不支持操作dom, 改写一下
+   * @param pagePixels
+   * @param mapPixels
+   * @param width
+   * @param height
+   */
+  constructor(pagePixels: Uint32Array, mapPixels: Uint32Array, width: number, height: number) {
+    this.pagePixels = pagePixels;
+    this.mapPixels = mapPixels;
+    this.areaMap = {};
+    this.width = width;
+    this.height = height;
+  }
+
+  calculate() {
+    //裁剪
+    for (let i = 0; i < this.pagePixels.length; i++) {
+      if (this.pagePixels[i]) this.mapPixels[i] = 0;
+    }
+    this.createAreaMap();
+    let centers = this.findAll();
+    return centers;
+  }
+
+  findAll() {
+    console.time('findCenters');
+    let areas = Object.keys(this.areaMap).map(key => {
+      return this.areaMap[key];
+    })
+
+    let centers: Centers = {};
+    areas.forEach(area => {
+      //skip large area.
+      //if (area.left == 0) return null;
+      let center = this.findAreaCenter(area);
+      if (isNaN(center.x)) {
+        console.log('xxxxxxxxxxxxxxxxxxxx', center, area)
+      } else {
+        centers[area.color] = center;
+      }
+    })
+
+    console.timeEnd('findCenters');
+    return centers;
+  }
+
+  /**
+   * Finder center of area.
+   * @param {FillArea} area 
+   */
+  findAreaCenter(area) {
+
+    console.log('area', area);
+
+    let ai, ax, ay, aw, ah, i, x, y, gx, gy, radius;
+    aw = area.width();
+    ah = area.height();
+
+    let areaPixels = new Uint32Array(aw * ah);
+
+    for (ai = 0; ai < areaPixels.length; ai++) {
+      ax = ai % aw;
+      ay = (ai - ax) / aw;
+      x = area.left + ax;
+      y = area.top + ay;
+      i = y * this.width + x;
+      if (this.mapPixels[i] == area.color) {
+        areaPixels[ai] = this.mapPixels[i];
+      } else {
+        areaPixels[ai] = 0;
+      }
+    }
+
+
+    //find contours
+    let flag, pixel;
+    let contours = [];
+    for (ax = 0; ax < aw; ax++) {
+      flag = 0;
+      for (ay = 0; ay < ah; ay++) {
+        ai = ay * aw + ax;
+        pixel = areaPixels[ai];
+        if (!flag && pixel) {
+          flag = 1;
+          contours.push(ai);
+        } else if (flag && !pixel) {
+          flag = 0;
+          contours.push(ai - aw);
+        } else if (flag && pixel) continue;
+        else if (!flag && !pixel) continue;
+      }
+      if (flag) {
+        //last point
+        contours.push(ai);
+      }
+    }
+
+    for (ay = 0; ay < ah; ay++) {
+      flag = 0;
+      for (ax = 0; ax < aw; ax++) {
+        ai = ay * aw + ax;
+        pixel = areaPixels[ai];
+        if (!flag && pixel) {
+          flag = 1;
+          contours.push(ai);
+        } else if (flag && !pixel) {
+          flag = 0;
+          contours.push(ai - 1);
+        } else if (flag && pixel) continue;
+        else if (!flag && !pixel) continue;
+      }
+      if (flag) {
+        //last point
+        contours.push(ai);
+      }
+    }
+
+
+
+    let contoursHash = {}
+    let contoursNew = [];
+    for (i = 0; i < contours.length; i++) {
+      ai = contours[i];
+      if (contoursHash[ai]) continue;
+      else {
+        contoursNew.push(ai);
+        contoursHash[ai] = true;
+      }
+    }
+    console.log('contours size:', contours.length, contoursNew.length);
+    contoursHash = null;
+    contours = contoursNew;
+
+    let contoursX = [];
+    let contoursY = [];
+    for (i = 0; i < contours.length; i++) {
+      ai = contours[i];
+      x = ai % aw;
+      y = (ai - x) / aw;
+      contoursX[i] = x;
+      contoursY[i] = y;
+    }
+
+
+    //console.log('contoursXY', contoursX, contoursY);
+
+    let minRadius, maxRadius, maxAi, dist, x1, y1, dx, dy, maxX, maxY;
+    maxRadius = 0;
+    for (ai = 0; ai < areaPixels.length; ai++) {
+      pixel = areaPixels[ai];
+      minRadius = Number.MAX_VALUE;
+      x1 = ai % aw;
+      y1 = (ai - x1) / aw;
+      if (pixel) {
+        for (i = 0; i < contours.length; i++) {
+          dx = x1 - contoursX[i];
+          dy = y1 - contoursY[i];
+          dist = dx * dx + dy * dy;
+          if (dist < minRadius) minRadius = dist;
+          //性能优化
+          if (minRadius < maxRadius) break;
+        }
+        if (minRadius > maxRadius) {
+          maxRadius = minRadius;
+          maxAi = ai;
+        }
+      }
+    }
+
+    // 得到最大内切圆的位置和半径
+    maxX = maxAi % aw;
+    maxY = (maxAi - maxX) / aw;
+    let x0 = aw / 2;   // 几何中心点坐标x
+    let y0 = ah / 2;   // 几何中心点坐标y
+    let maxDist = (maxX - x0) * (maxX - x0) + (maxY - y0) * (maxY - y0);  // 最大内切圆心到几何中心点的距离
+
+    console.log('1------ maxAi: ', maxAi, maxX, maxY, maxRadius);
+
+    if (maxDist == 0) { // 说明找到的最大内切圆就正在区域矩形中心,可以不用往下进行了
+      console.log("find the center exactly, no need to adjust");
+
+      radius = Math.sqrt(maxRadius);
+      gx = maxX + area.left;
+      gy = maxY + area.top;
+  
+      return {
+        x: gx,
+        y: gy,
+        radius
+      }
+    }
+
+    if (maxRadius <= 0 || isNaN(maxX) || isNaN(maxY)) { // 异常情况,不要继续下去了,直接返回
+      console.log("find center something wrong!!!");
+      
+      radius = Math.sqrt(maxRadius);
+      gx = maxX + area.left;
+      gy = maxY + area.top;
+  
+      return {
+        x: gx,
+        y: gy,
+        radius
+      }
+    }
+
+    // 重新根据score = ( radius / max_radius ) * 0.8 +  (1 - dist / max_dist) * 0.2 再次做矫正
+    let finalX, finalY, finalAi, finalRadius, maxScore, score;
+    maxScore = 0;
+    for (ai = 0; ai < areaPixels.length; ai++) {
+      pixel = areaPixels[ai];
+      minRadius = Number.MAX_VALUE;
+      x1 = ai % aw;
+      y1 = (ai - x1) / aw;
+      if (pixel) {
+        for (i = 0; i < contours.length; i++) {
+          dx = x1 - contoursX[i];
+          dy = y1 - contoursY[i];
+          dist = dx * dx + dy * dy;
+          if (dist < minRadius) minRadius = dist;
+          // //性能优化
+          // if (minRadius < maxRadius) break;
+        }
+        dist = (x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0); // 该点到几何中心点的距离
+        score = (minRadius / maxRadius) * 0.8 + (1 - dist /  maxDist) * 0.2;
+        if (score > maxScore) {
+          maxScore = score;
+          finalRadius = minRadius;
+          finalAi = ai;
+        }
+      }
+    }
+
+    finalX = finalAi % aw;
+    finalY = (finalAi - finalX) / aw;
+    radius = Math.sqrt(finalRadius);
+    console.log('2------ finalAi: ', finalAi, finalX, finalY, finalRadius);
+
+    gx = finalX + area.left;
+    gy = finalY + area.top;
+
+    return {
+      x: gx,
+      y: gy,
+      radius
+    }
+
+  }
+
+  /**
+   * Create area map
+   */
+  createAreaMap() {
+    let width = this.width;
+    let height = this.height;
+    let pixels = this.mapPixels;
+    let areaMap = this.areaMap;
+    let i, x, y, color;
+    for (i = 0; i < pixels.length; i++) {
+      color = pixels[i];
+      if (!color) continue;
+      x = i % width;
+      y = (i - x) / width;
+      if (areaMap[color]) {
+        areaMap[color].addPoint(x, y);
+      } else {
+        areaMap[color] = new FillArea(color, x, y);
+      }
+    }
+    this.areaMap = areaMap;
+  }
+
+}
+
+
+
+
+

+ 33 - 0
zorro/src/app/lib/filler/core/canvas-layer.ts

@@ -0,0 +1,33 @@
+import Utils from "../common/utils";
+import Layer from "./layer";
+import PanTool from "./pantool";
+
+
+
+
+/**
+ * Simple image layer 
+ */
+export default class CanvasLayer extends Layer {
+
+  public canvas: HTMLCanvasElement;
+
+
+  constructor(width: number, height: number) {
+    super(width, height);
+    this.addTool(new PanTool());
+    this.canvas = Utils.createCanvas(width, height);
+  }
+
+  override get defaultToolKey(): string { return new PanTool().key }
+
+
+  override draw(ctx: CanvasRenderingContext2D): void {
+
+    Utils.setImageSmoothing(ctx, false);
+    ctx.drawImage(this.canvas, 0, 0);
+
+  }
+
+
+}

+ 26 - 0
zorro/src/app/lib/filler/core/color-picker-tool.ts

@@ -0,0 +1,26 @@
+import Tool from './tool';
+
+export default class ColorPickerTool extends Tool {
+
+
+
+  constructor() {
+    super('color-picker', 'url("/static/cursor/cursor-color-dropper.png") 0 24, default');
+  }
+
+  /**
+   * Always in progress
+   * Prevent control drag while pan.
+   */
+  override inProgress(evt ? : Event) {
+    return true;
+  }
+
+
+  override onclick(evt : MouseEvent) {
+    console.log('ColorPickerTool#onclick()', evt._contentPoint, this.editor);
+    if (this.editor && this.editor.pickColor) {
+      this.editor.pickColor(evt._canvasPoint, evt._contentPoint);
+    }
+  }
+}

+ 16 - 0
zorro/src/app/lib/filler/core/editor-config.ts

@@ -0,0 +1,16 @@
+
+
+
+export default interface EditorConfig {
+  borderStyle?: string;
+  border?: number;
+  shadowColor?: string;
+  shadow?: any;
+  bgColor?: string;
+  padding?: number;
+  height?: number;
+  width?: number;
+  scale?: number;
+  minScale? : number;
+  maxScale? : number;
+}

+ 800 - 0
zorro/src/app/lib/filler/core/editor.ts

@@ -0,0 +1,800 @@
+
+import Matrix from './matrix';
+import Rect from './rect';
+import PanTool from './pantool';
+import ColorPickerTool from './color-picker-tool';
+import EventEmitter from './eventemitter';
+import Utils from './utils';
+import Tool from './tool';
+import Layer from './layer';
+import { Padding, Point } from './interface';
+import EditorConfig from './editor-config';
+import Animator from '../common/animator';
+import Easing from '../common/easing';
+
+type MouseEventHandler = (evt: MouseEvent) => void;
+type EventHandler = (evt: EventHandler) => void;
+type KeyboardEventHandler = (evt: KeyboardEvent) => void;
+type WheelEventHandler = (evt: WheelEvent) => void;
+type ResizeEventHandler = () => void;
+
+const TAG = 'Editor';
+
+export default class Editor extends EventEmitter {
+
+  canvas: HTMLCanvasElement;
+  ctx: CanvasRenderingContext2D | null;
+
+
+  private _matrix: Matrix = Matrix.identical;
+
+
+  backgroundLayer: Layer | null;
+  bgColor: string;
+
+
+  private invalidateHandler: Function;
+
+  private lastPoint: Point | null = null;
+  private isDragging: boolean = false;
+
+  wheelDy: number = 0;
+  STEP: number = 0;
+
+  private tools: Tool[] = [];
+  private currentTool: Tool | null;
+
+
+  private needDraw: boolean = false;
+  private theMarkRect: Rect | null = null;
+
+  public readonly ready: Promise<void>;
+
+  //用于计算bestFitMatrix
+  padding: Padding = { left: 250, top: 80, right: 266, bottom: 80 };
+  //bestFitStyle
+  bestFitStyle: 'canavsCenter' | 'viewportCenter' = 'canavsCenter';
+
+
+  /**
+   * Event handlers.
+   * Keep them for removing, prevent memory leaking.
+   */
+  private resizeHandler: ResizeEventHandler;
+  private clickHander: MouseEventHandler;
+  private dblclickHandler: MouseEventHandler;
+  private mousemoveHandler: MouseEventHandler;
+  private mousedownHandler: MouseEventHandler;
+  private mouseupHandler: MouseEventHandler;
+  private keydownHandler: KeyboardEventHandler;
+  private wheelHandler: WheelEventHandler;
+
+  opts: EditorConfig = {
+    minScale : 0.05,
+    maxScale : 5,
+  }
+
+
+  constructor(canvas: HTMLCanvasElement, _opts?: EditorConfig, _padding?: any) {
+    super();
+    if (_opts) {
+      Object.assign(this.opts, _opts);
+    }
+    if (_padding) {
+      Object.assign(this.padding, _padding);
+    }
+    this.canvas = canvas;
+    var self = this;
+    this.ctx = canvas.getContext('2d');
+
+    this.matrix = Matrix.identical;
+    this.currentTool = null;
+    this.backgroundLayer = null;
+    this.bgColor = "#efefef";
+
+    //this.invalidateFunc = function () { self.invalidate(); }
+    this.invalidateHandler = this.invalidate.bind(this);
+
+    this.resizeHandler = this.onresize.bind(this);
+    window.addEventListener('resize', this.resizeHandler);
+
+    this.clickHander = this.onclick.bind(this);
+    canvas.addEventListener('click', this.clickHander);
+
+    this.dblclickHandler = this.ondblclick.bind(this);
+    canvas.addEventListener('dblclick', this.dblclickHandler);
+
+    this.mousemoveHandler = this.onmousemove.bind(this);
+    document.addEventListener('mousemove', this.mousemoveHandler);
+
+    this.mousedownHandler = this.onmousedown.bind(this);
+    canvas.addEventListener('mousedown', this.mousedownHandler);
+
+    this.mouseupHandler = this.onmouseup.bind(this);
+    canvas.addEventListener('mouseup', this.mouseupHandler);
+
+    this.keydownHandler = this.onkeydown.bind(this);
+    document.addEventListener('keydown', this.keydownHandler);
+
+    //鼠标滚轮
+    this.wheelHandler = this.onwheel.bind(this);
+    canvas.addEventListener('wheel', this.wheelHandler);
+
+
+    //init tools
+    this.addTool(new PanTool());
+    //this.addTool(new ColorPickerTool());
+    this.setToolByKey(new PanTool().key);
+
+    //Editor是否初始化完成
+    this.ready = new Promise((done) => {
+      //启动定时器,检查页面是否渲染完成, canvas获得实际尺寸
+      let checkInterval = setInterval(() => {
+        console.log(this.canvas.offsetHeight, this.canvas.offsetWidth);
+        if (this.canvas.offsetHeight != 0 && this.canvas.offsetWidth != 0) {
+          clearInterval(checkInterval);
+          this.updateCanvas();
+          done();
+        }
+      }, 10);
+    })
+  }
+
+
+  /**
+   * 获取canvas的宽高
+   */
+  get width(): number { return this.canvas.width }
+  get height(): number { return this.canvas.height }
+
+  /**
+   * 获取缩放参数
+   * 默认使用 window.devicePixelRatio, 建议使用默认值
+   */
+  get scaleX(): number { return this.opts.scale || window.devicePixelRatio }
+  get scaleY(): number { return this.opts.scale || window.devicePixelRatio }
+
+  /**
+   * 获取要展示的内容的宽高
+   */
+  get contentWidth(): number { return this.backgroundLayer && this.backgroundLayer.getWidth() || 200 }
+  get contentHeight(): number { return this.backgroundLayer && this.backgroundLayer.getHeight() || 200 }
+
+
+  /**
+   * 根据当前 devicePixelRatio 以及 canvas的实际尺寸重新设置 canvas的宽高
+   * devicePixelRatio, 参考: https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio 
+   * 这里要注意canvas宽高和canvas的实际尺寸是两回事。 canvas的实际尺寸是由其css属性来决定的,
+   * 其大小可能由于屏幕缩放,窗口调整而发生变化。 而canvas的宽高并不会自动变化。 所以我们需要
+   * 随时检测canvas的实际尺寸,并根据实际尺寸来调整宽高, 以保证其宽高的一致性, 否则可能会变形。 
+   */
+  updateCanvas() {
+    let _width = this.canvas.offsetWidth;
+    let _height = this.canvas.offsetHeight;
+    this.canvas.width = Math.floor(_width * this.scaleX);
+    this.canvas.height = Math.floor(_height * this.scaleY);
+    this.invalidate();
+    this.debugCanvas();
+  }
+
+  set matrix(matrix: Matrix) {
+    this._matrix = matrix;
+    this.invalidate();
+  }
+
+  get matrix() {
+    return this._matrix;
+  }
+
+
+  debugCanvas() {
+    let canvas = this.canvas;
+    console.log('canvas dimention:', canvas.width, canvas.height);
+    console.log('canvas dimention2:', canvas.offsetWidth, canvas.offsetHeight);
+  }
+
+  /**
+   * 计算能将内容最佳展示出来的matrix
+   * TODO : 目前是简单计算了一下, 还需优化。 
+   */
+  get bestFitMatrix(): Matrix {
+    let scaleX = (this.width - this.padding.left * this.scaleX - this.padding.right * this.scaleX) / this.contentWidth;
+    let scaleY = (this.height - this.padding.top * this.scaleY - this.padding.bottom * this.scaleY) / this.contentHeight;
+    let scale = Math.min(scaleX, scaleY);
+    let translateX = (this.width - this.contentWidth * scale) / 2;
+    let translateY = (this.height - this.contentHeight * scale) / 2;
+    return new Matrix([scale, 0, 0, scale, translateX, translateY]);
+  }
+
+
+  onresize() {
+    console.log('onresize()..');
+    this.updateCanvas();
+  }
+
+  /**
+   * 鼠标事件预处理
+   * 转换为相对于canvas的坐标
+   * @param evt 
+   */
+  preprocessMouseEvent(evt: MouseEvent) {
+    let self = this;
+    let t = evt.target as HTMLElement;
+    //console.log(TAG, 'target=', evt.type, evt.button, evt.target == this.canvas, t.tagName);
+    //let x = evt.pageX - this.canvas.offsetLeft;
+    //let y = evt.pageY - this.canvas.offsetTop;
+
+    //console.log(`(${this.canvas.offsetLeft}, ${this.canvas.offsetTop})`)
+    //console.log(`(${evt.offsetX}, ${evt.offsetY}) (${evt.pageX}, ${evt.pageY}) (${x},${y})`);
+
+    //fill canvas xy
+    if (evt.target == this.canvas) {
+      evt._canvasPoint = {
+        x: evt.offsetX * self.scaleX,
+        y: evt.offsetY * self.scaleY,
+      }
+    } else {
+      //'mousemove' event是挂在document下的,需要单独处理,先简单处理一下
+      evt._canvasPoint = {
+        x: (evt.pageX - this.canvas.offsetLeft) * self.scaleX,
+        y: (evt.pageY - this.canvas.offsetTop) * self.scaleY,
+      }
+    }
+
+    //content xy
+    evt._contentPoint = this.matrix.invMapPoint(evt._canvasPoint.x, evt._canvasPoint.y);
+  }
+
+  /**
+   * Internal events.
+   */
+  onDragStart(evt: MouseEvent) {
+    console.log('onDragStart()');
+    this.currentTool && this.currentTool.onDragStart(evt);
+  }
+
+  onDragMove(evt: MouseEvent) {
+    this.currentTool && this.currentTool.onDragMove(evt);
+  }
+
+  onDragEnd(evt: MouseEvent) {
+    var self = this;
+    console.log('onDragEnd()');
+    self.currentTool && self.currentTool.onDragEnd(evt);
+  }
+
+  /**
+   * 'mousedown' listener
+   */
+  onmousedown(evt: MouseEvent) {
+    this.preprocessMouseEvent(evt);
+    this.isDragging = false;
+    this.lastPoint = null;
+    // If left mouse is down, track the point.
+    if (evt.button == 0) {
+      this.lastPoint = evt._canvasPoint;
+    }
+    this.currentTool && this.currentTool.onmousedown(evt);
+  }
+
+  /**
+   * 'mousemove' listener
+   */
+  onmousemove(evt: MouseEvent) {
+    this.preprocessMouseEvent(evt);
+    // For drag tracking.
+    if (evt.button == 0 && this.lastPoint) {
+      var p = evt._canvasPoint;
+      evt._dx = p.x - this.lastPoint.x;
+      evt._dy = p.y - this.lastPoint.y;
+      if (this.isDragging) {
+        this.lastPoint = p;
+        this.onDragMove(evt);
+      } else {
+        var dist = Math.sqrt(Math.pow(evt._dx, 2) + Math.pow(evt._dy, 2));
+        if (dist > 10) {
+          this.isDragging = true;
+          this.lastPoint = p;
+          this.onDragStart(evt);
+        }
+      }
+    }
+
+    this.currentTool && this.currentTool.onmousemove(evt);
+  }
+
+  /**
+   * 'mouseup' listener
+   */
+  onmouseup(evt: MouseEvent) {
+    this.preprocessMouseEvent(evt);
+    var self = this;
+    self.lastPoint = null;
+    if (self.isDragging) {
+      self.onDragEnd(evt);
+    }
+    self.currentTool && self.currentTool.onmouseup(evt);
+  }
+
+  /**
+   * onclick
+   */
+  onclick(evt: MouseEvent) {
+    this.preprocessMouseEvent(evt);
+    var self = this;
+    self.currentTool && self.currentTool.onclick(evt);
+  }
+
+  /**
+   * ondblclick
+   */
+  ondblclick(evt: MouseEvent) {
+    this.preprocessMouseEvent(evt);
+    var self = this;
+    self.currentTool && self.currentTool.ondblclick(evt);
+  }
+
+  /**
+   * keydown listener
+   */
+  onkeydown(e: KeyboardEvent) {
+    var self = this;
+    //console.log('keyCode', e.key,);
+    if (e.key.toLocaleLowerCase() == 'escape') { // esc
+      self.currentTool && self.currentTool.oncancel();
+    }
+  }
+
+
+  /**
+   * 鼠标滚轮/Trackpad
+   * Trackpad双指可以上下左右移动,也可以放到缩小
+   * 鼠标滚利则只能上下移动, 放大缩小需要按住ctrl键
+   * @param evt 
+   */
+  onwheel(evt: WheelEvent) {
+    this.preprocessMouseEvent(evt);
+    evt.preventDefault();
+    //console.log('evt', evt, evt.ctrlKey, evt.deltaX, evt.deltaY, evt.detail)
+    if (evt.ctrlKey) { //Trackpad双指, 考虑deltaY即可
+      //todo 统一使用getXY()
+      let pivotX = evt.offsetX * this.scaleX;
+      let pivotY = evt.offsetY * this.scaleY;
+      let scaleBy = 1;
+      if (evt.deltaY > 0) { //缩小
+        scaleBy = 1 / (1 + Math.abs(evt.deltaY / 60)); //别问为什么是60,问就是不知道
+      } else { //放大
+        scaleBy = 1 + Math.abs(evt.deltaY / 60);
+      }
+      this.scaleBy(scaleBy, scaleBy, pivotX, pivotY);
+    } else {
+      this.pan(-evt.deltaX, -evt.deltaY);
+    }
+  }
+
+
+  /**
+   * Invoke a pending redraw.
+   * The redraw will not happend immediatly, it will wait to the
+   * next animation frame, during the waiting, other redraw request
+   * will be ignored.
+   */
+  invalidate() {
+    var self = this;
+    if (self.needDraw) {
+      return;
+    }
+    self.needDraw = true;
+    requestAnimationFrame(function () { self.draw(); });
+  }
+
+
+  private drawDummy() {
+    this.ctx.fillStyle = "red";
+    this.ctx.fillRect(0, 0, 200, 200);
+  }
+
+  /**
+   * Draw all the shapes and controls.
+   */
+  private draw() {
+    this.needDraw = false;
+
+    if (!this.backgroundLayer) {
+      return;
+    }
+
+    let self = this;
+    let ctx = self.ctx;
+
+    let rect = new Rect(0, 0, this.contentWidth, this.contentHeight);
+    rect = self.matrix.mapRect(rect);
+
+    // background.
+    ctx.clearRect(0, 0, this.width, this.height);
+    ctx.fillStyle = self.opts.bgColor || self.bgColor || 'blue';
+    //console.log('ctx.fillStyle: ', ctx.fillStyle)
+    ctx.fillRect(0, 0, this.width, this.height);
+
+    // draw shadow
+    if (this.opts.shadow) {
+      ctx.save();
+      ctx.fillStyle = 'white';
+      ctx.shadowColor = this.opts.shadowColor || "#ccc";
+      ctx.shadowBlur = 30;
+      ctx.fillRect(rect.left, rect.top, rect.width(), rect.height());
+      ctx.restore();
+    }
+
+    ctx.save();
+    // apply matrix.
+    ctx.transform.apply(ctx, self.matrix.toArray());
+
+    //this.drawDummy();
+
+    // draw background layers after transform.
+    if (self.backgroundLayer) {
+      ctx.save();
+      self.backgroundLayer.draw(ctx);
+      ctx.restore();
+    }
+
+
+    ctx.restore();
+
+
+    // draw border
+    if (this.opts.border || true) {
+      ctx.strokeStyle = this.opts.borderStyle || "#999";
+      ctx.lineWidth = window.devicePixelRatio * 1;
+      ctx.strokeRect(rect.left, rect.top, rect.width(), rect.height());
+    }
+
+    //draw mark rect.
+    if (this.theMarkRect) {
+      let r = this.theMarkRect;
+      console.log('markRect: ', r);
+      ctx.save();
+      ctx.strokeStyle = 'white';
+      ctx.lineWidth = 3;
+      ctx.strokeRect(r.left, r.top, r.width(), r.height());
+      ctx.restore();
+      //only draw once
+      this.theMarkRect = null;
+    }
+
+
+    //draw viewport.
+    if (false) {
+      ctx.save();
+      let vp = this.viewport;
+      ctx.strokeStyle = 'green';
+      ctx.lineWidth = 2;
+      ctx.strokeRect(vp.left, vp.top, vp.width(), vp.height());
+      ctx.restore();
+    }
+
+
+    //draw center cross 
+    if (false) {
+      ctx.save();
+      ctx.strokeStyle = 'red';
+      ctx.lineWidth = 2;
+      ctx.moveTo(0, this.height / 2);
+      ctx.lineTo(this.width, this.height / 2);
+      ctx.moveTo(this.width / 2, 0);
+      ctx.lineTo(this.width / 2, this.height);
+      ctx.stroke();
+      ctx.restore();
+    }
+
+  }
+
+
+
+
+  /**
+   * Add background layer
+   * TODO reset matrix when layer changes.
+   */
+  setBackgroundLayer(layer: Layer, bestFit: boolean = false) {
+    var self = this;
+    if (self.backgroundLayer != layer) {
+      if (layer) {
+        layer.trigger('load');
+        layer.on('invalidate', this.invalidateHandler);
+      }
+      if (self.backgroundLayer) {
+        self.backgroundLayer.trigger('unload');
+        self.backgroundLayer.un('invalidate', this.invalidateHandler);
+      }
+
+      self.backgroundLayer = layer;
+      if (self.backgroundLayer) {
+        self.backgroundLayer.lastToolKey && this.setToolByKey(self.backgroundLayer.lastToolKey);
+        bestFit && (this.matrix = this.bestFitMatrix);
+      }
+    }
+
+
+    this.invalidate();
+
+  }
+
+
+  getBackgroundLayer() {
+    return this.backgroundLayer;
+  }
+
+
+  /**
+   * Get event x,y
+   * @param map Map the point into innner xy.
+   */
+  /*
+  getXY(evt: MouseEvent, map: boolean = false) {
+    var self = this;
+    var p = {
+      x: (evt.pageX - $(self.canvas).offset().left) * self.scaleX,
+      y: (evt.pageY - $(self.canvas).offset().top) * self.scaleY,
+    };
+    evt.canvasPoint = p;
+    if (!map)
+      return p;
+    return self.matrix.invMapPoint(p.x, p.y);
+  }
+  */
+
+  /**
+   * For pan tool
+   */
+  pan(dx, dy) {
+    if (this.animator) this.animator.cancel();
+    this.matrix.postTranslate(dx, dy);
+    this.invalidate();
+  }
+
+
+  /**
+   * 围绕指定的中心点进行缩放
+   * @param dScaleX 
+   * @param dScaleY 
+   * @param pivotX   
+   * @param pivotY 
+   */
+  scaleBy(dScaleX: number, dScaleY: number, pivotX: number, pivotY: number) {
+    if (this.animator) this.animator.cancel();
+    let dScale = Math.max(this.opts.minScale / this.matrix.getScale().x, dScaleX);
+    this.matrix.postScaleBy(dScale, dScale, pivotX, pivotY);
+    this.invalidate();
+  }
+
+  /**
+   * Reset scale.
+   */
+  bestFit() {
+    //this.matrix = this.bestFitMatrix;
+    this.animationTo(this.bestFitMatrix);
+  }
+
+  /**
+   * Reset scale.
+   */
+  reset100() {
+    let translateX = (this.width - this.contentWidth) / 2;
+    let translateY = (this.height - this.contentHeight) / 2;
+    this.animationTo(new Matrix([1, 0, 0, 1, translateX, translateY]));
+  }
+
+  getScale() {
+    return this.matrix.getScale();
+  }
+
+  setBgColor(color: string) {
+    this.bgColor = color;
+    this.invalidate();
+  }
+
+
+  addTool(tool: Tool) {
+    console.log('addTool:', tool)
+    this.tools.push(tool);
+  }
+
+  /**
+   * get Tool hash for quick search.
+   */
+  get toolHash() {
+    return this.tools.reduce((last: any, cur: Tool) => {
+      last[cur.key] = cur;
+      return last;
+    }, {});
+  }
+
+
+  /**
+   * Set the current active tool.
+   * @param tool Tool or null
+   * @returns 
+   */
+  setTool(tool: Tool) {
+    if (this.currentTool == tool) {
+      tool.onSelect();
+      return;
+    }
+      
+    tool && tool.setEditor(this);
+    this.currentTool && this.currentTool.onUnSelect();
+    this.currentTool && this.currentTool.setEditor(null);
+    this.currentTool = tool;
+    console.log('Select tool: ', this.currentTool);
+    this.updateCursor();
+    if (tool && this.backgroundLayer) this.backgroundLayer.lastToolKey = tool.key;
+    tool && tool.onSelect();
+  }
+
+  /**
+   * Set the current tool of the editor.
+   */
+  setToolByKey(toolKey: string) {
+    console.log('all tools:', this.tools)
+    let tool = this.toolHash[toolKey];
+    if (!tool && this.backgroundLayer) {
+      tool = this.backgroundLayer.getToolByKey(toolKey);
+    }
+    this.setTool(tool);
+  }
+
+
+  updateCursor() {
+    if (this.currentTool) {
+      this.canvas.style.cursor = this.currentTool.cursor;
+    }
+  }
+
+
+  /**
+   * Get all tool status
+   * including edit's tool and current layer's tools
+   * @returns 
+   */
+  getToolsStatus() {
+    let tools = this.tools.slice();
+    if (this.backgroundLayer) {
+      tools = tools.concat(this.backgroundLayer.getTools());
+    }
+    return tools.map((tool: Tool) => {
+      return { key: tool.key, active: tool.isActive() }
+    })
+  }
+
+
+  markRect(left: number, top: number, right: number, bottom: number) {
+    let rect = new Rect(left, top, right, bottom);
+    let canvasRect = this.matrix.mapRect(rect);
+    console.log('rect: ', rect, canvasRect);
+    this.theMarkRect = canvasRect;
+    this.invalidate();
+  }
+
+  clearMarkRect() {
+    this.theMarkRect = null;
+    this.invalidate();
+  }
+
+  centerToRect(left: number, top: number, right: number, bottom: number) {
+    let rect = new Rect(left, top, right, bottom);
+    let cr = this.matrix.mapRect(rect);
+    let cx = this.canvas.width / 2;
+    let cy = this.canvas.height / 2;
+    let srcCenter = cr.center();
+    let dx = cx - srcCenter.x;
+    let dy = cy - srcCenter.y;
+
+    let scale = this.canvas.width / 3 / cr.width();
+
+    this.pan(dx, dy);
+    this.matrix.postScaleBy(scale, scale, cx, cy);
+    this.markRect(left, top, right, bottom)
+  }
+
+
+  /**
+   * 获取canvas指定点的颜色
+   * @param point 
+   * @param layerPoint 
+   * @returns rgba(xxx)
+   */
+  pickColor(point: Point, layerPoint: Point): string | null {
+    console.log('pickColor point:', point, layerPoint)
+    let x = Math.floor(point.x);
+    let y = Math.floor(point.y);
+    if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
+      return null;
+    }
+    let imgData = this.ctx.getImageData(x, y, 1, 1);
+    let color = Utils.makeColor(imgData.data);
+    console.log('color: ', color);
+    this.trigger('pick-color', color);
+    return color;
+  }
+
+
+  /**
+   * 获取编辑器可视区域
+   */
+  get viewport(): Rect {
+    return new Rect(
+      this.padding.left * this.scaleX,
+      this.padding.top * this.scaleY,
+      this.width - this.padding.right * this.scaleX,
+      this.height - this.padding.bottom * this.scaleY
+    );
+  }
+
+
+  /**
+   * bestfit the content rect to the viewport.
+   * @param rect 
+   */
+  focusToRect(rect: Rect) {
+    let center = rect.center();
+    let m = this.matrix.clone();
+    let p = m.mapPoint(center.x, center.y);
+    let viewport = this.viewport;
+
+    let vc = viewport.center();
+    let dx = vc.x - p.x;
+    let dy = vc.y - p.y;
+    m.postTranslate(dx, dy); //rect中心点移动到屏幕可见区域中心
+
+    let maxScale = Math.min(viewport.width() / rect.width(), viewport.height() / rect.height());
+    if (maxScale > 20) maxScale = 20;
+    let scaleBy = maxScale / m.getScale().x;
+    m.postScaleBy(scaleBy, scaleBy, vc.x, vc.y);
+
+
+    //this.matrix = m;
+    this.animationTo(m);
+  }
+
+  animator: Animator;
+  animationTo(endMatrix: Matrix) {
+    if (this.animator) this.animator.cancel();
+    let animator = new Animator(this.matrix.m, endMatrix.m)
+      .setDuration(400)
+      //.setEasing(Easing.easeInOutQuadBack2);
+      .setEasing(Easing.easeInOutQuad);
+    animator.on('animationUpdate', ((a, values) => {
+      this.matrix = new Matrix(values);
+    }))
+    animator.start();
+    this.animator = animator;
+  }
+
+
+
+  /**
+   * 销毁editor对象,防止内存泄漏
+   */
+  override destroy(): void {
+    super.destroy(); //event emitter
+    this.setBackgroundLayer(null); //remove layer
+    this.setTool(null); //remove tool
+
+    //remove event handlers
+    window.removeEventListener('resize', this.resizeHandler);
+    this.canvas.removeEventListener('click', this.clickHander);
+    this.canvas.removeEventListener('dblclick', this.dblclickHandler);
+    this.canvas.removeEventListener('mousedown', this.mousedownHandler);
+    this.canvas.removeEventListener('mouseup', this.mouseupHandler);
+    this.canvas.removeEventListener('wheel', this.wheelHandler);
+    this.canvas.removeEventListener('wheel', this.wheelHandler);
+    document.removeEventListener('mousemove', this.mousemoveHandler);
+    document.removeEventListener('keydown', this.keydownHandler);
+
+  }
+
+
+
+}

+ 52 - 0
zorro/src/app/lib/filler/core/eventemitter.ts

@@ -0,0 +1,52 @@
+
+/**
+ * Simple event subscrible class
+ */
+export default class EventEmitter {
+  listeners: any;
+  constructor() {
+    this.listeners = {};
+  }
+
+  /**
+   * Subscribe event.
+   */
+  on(evt: string, func: Function) {
+    if (!evt) return;
+    if (!this.listeners[evt]) {
+      this.listeners[evt] = [];
+    }
+    this.listeners[evt].push(func);
+  }
+
+  /**
+   * Unsubscribe event.
+   */
+  un(evt: string, func: Function) {
+    if (!evt) return;
+    if (!this.listeners[evt]) return;
+    var index = this.listeners[evt].indexOf(func);
+    if (index < 0) return;
+    this.listeners[evt].splice(index, 1);
+  }
+
+  /**
+   * Trigger event.
+   */
+  trigger(evt : string, ...args) {
+    //console.log(this, 'trigger:', evt)
+    if (!evt) return;
+    if (!this.listeners[evt]) return;
+    var self = this;
+    this.listeners[evt].forEach(func => func(self, ...args));
+  }
+
+
+  /**
+   * 防止内存泄漏
+   */
+  destroy(){
+    this.listeners = {};
+  }
+
+}

+ 43 - 0
zorro/src/app/lib/filler/core/image-layer.ts

@@ -0,0 +1,43 @@
+import Utils from "../common/utils";
+import ColorPickerTool from "./color-picker-tool";
+import Layer from "./layer";
+
+
+
+
+/**
+ * Simple image layer 
+ */
+export default class ImageLayer extends Layer {
+
+
+  images : HTMLImageElement[] = [];
+
+  constructor(...images: HTMLImageElement[]) {
+    super(images[0].width, images[0].height);
+    this.images = images;
+    this.addTool(new ColorPickerTool());
+  }
+
+  override get defaultToolKey(): string { return new ColorPickerTool().key }
+
+  public set image(img: HTMLImageElement) {
+    this.images[0] = img;
+    this.invalidate()
+  }
+
+  public get image(): HTMLImageElement {
+    return this.images[0];
+  }
+
+  override draw(ctx: CanvasRenderingContext2D): void {
+
+    Utils.setImageSmoothing(ctx, false);
+    this.images.forEach(img => {
+      ctx.drawImage(img, 0, 0);
+    })
+
+  }
+
+
+}

+ 19 - 0
zorro/src/app/lib/filler/core/index.d.ts

@@ -0,0 +1,19 @@
+import { Point } from "./interface";
+
+declare global {
+  interface MouseEvent {
+    _contentPoint: Point; //事件位置 - 基于内容 backgroundLayer
+    _canvasPoint : Point; //事件位置 - 基于canvas
+    _dx :number; //for drag tracking
+    _dy : number; //for drag tracking
+
+  }
+}
+
+
+
+
+
+
+
+export {};

+ 20 - 0
zorro/src/app/lib/filler/core/interface.ts

@@ -0,0 +1,20 @@
+export interface Point {
+  x: number;
+  y: number;
+}
+
+
+export interface Padding {
+  left?: number;
+  top?: number;
+  right?: number;
+  bottom?: number;
+}
+
+
+export interface UndoRedo {
+  undo() : void;
+  redo() : void;
+  undoSize() : number;
+  redoSize() : number;
+}

+ 79 - 0
zorro/src/app/lib/filler/core/layer.ts

@@ -0,0 +1,79 @@
+import EventEmitter from './eventemitter';
+import { UndoRedo } from './interface';
+import Tool from './tool';
+
+
+export default class Layer extends EventEmitter implements UndoRedo {
+  width: number;
+  height: number;
+
+  private _lastToolKey: string;
+
+  constructor(width, height) {
+    super();
+    this.width = width;
+    this.height = height;
+  }
+
+  invalidate() {
+    this.trigger('invalidate');
+  }
+
+  //记录最后一切换的工具
+  get lastToolKey(): string { return this._lastToolKey ? this._lastToolKey : this.defaultToolKey; }
+  set lastToolKey(key) { this._lastToolKey = key; }
+  get defaultToolKey(): string { return null; } //Children should overide this getter
+
+  private tools: Tool[] = []; //layer支持的所有工具列表
+
+  /**
+   * Return list of Tool.
+   * @returns 
+   */
+
+  getToolByKey(toolKey: string): Tool {
+    return this.toolHash[toolKey];
+  }
+
+  addTool(tool : Tool) {this.tools.push(tool)}
+
+  getTools(): Tool[] { return this.tools.slice(); }
+
+  get toolHash(): any {
+    return this.tools.reduce((last, tool) => {
+      last[tool.key] = tool;
+      return last;
+    }, {});
+  }
+
+
+  getWidth() {
+    return this.width;
+  }
+
+  getHeight() {
+    return this.height;
+  }
+
+  //Overide this function.
+  draw(ctx: CanvasRenderingContext2D) {
+    ctx.fillStyle = 'blue';
+    ctx.fillRect(0, 0, this.width, this.height);
+  }
+
+  undo(): void { }
+  redo(): void { }
+  undoSize(): number { return 0; }
+  redoSize(): number { return 0; }
+
+
+  private _smoothing = false;
+  set smoothing(smooth) { this._smoothing = smooth; this.invalidate() }
+  get smoothing() { return this._smoothing; }
+
+  private _showSvg = false;
+  public get showSvg() { return this._showSvg; }
+  public set showSvg(value) { this._showSvg = value; this.invalidate() }
+
+
+}

+ 277 - 0
zorro/src/app/lib/filler/core/matrix.ts

@@ -0,0 +1,277 @@
+import { Point } from './interface';
+import Rect from './rect';
+
+/**
+ *
+ * Matrix
+ *
+ */
+export default class Matrix {
+  m: Array<number>;
+  initial: Array<number>;
+  inverse: Matrix;
+  maxScale: number  = 20;
+  minScale: any;
+
+  constructor(m: Array<number>) {
+    if (m) {
+      this.m = m.slice();
+    } else {
+      this.m = [1, 0, 0, 1, 0, 0];
+    }
+    this.initial = this.m.slice();
+    //invert matrix.
+    this.inverse = null;
+  }
+
+  public static get identical() {
+    return new Matrix([1, 0, 0, 1, 0, 0]);
+  }
+
+  /**
+   * Clone matrix.
+   */
+  clone() {
+    return new Matrix(this.m.slice());
+  }
+
+  /**
+   * Invert the matrix.
+   */
+  invert() {
+    const d = 1 / (this.m[0] * this.m[3] - this.m[1] * this.m[2]);
+    const m0 = this.m[3] * d;
+    const m1 = -this.m[1] * d;
+    const m2 = -this.m[2] * d;
+    const m3 = this.m[0] * d;
+    const m4 = d * (this.m[2] * this.m[5] - this.m[3] * this.m[4]);
+    const m5 = d * (this.m[1] * this.m[4] - this.m[0] * this.m[5]);
+
+    this.m[0] = m0;
+    this.m[1] = m1;
+    this.m[2] = m2;
+    this.m[3] = m3;
+    this.m[4] = m4;
+    this.m[5] = m5;
+    return this;
+  }
+
+  /**
+   * Multiply another matrix arr.
+   */
+  multiply(marr) {
+    const m0 = this.m[0] * marr[0] + this.m[2] * marr[1];
+    const m1 = this.m[1] * marr[0] + this.m[3] * marr[1];
+    const m2 = this.m[0] * marr[2] + this.m[2] * marr[3];
+    const m3 = this.m[1] * marr[2] + this.m[3] * marr[3];
+    const m4 = this.m[0] * marr[4] + this.m[2] * marr[5] + this.m[4] * 1;
+    const m5 = this.m[1] * marr[4] + this.m[3] * marr[5] + this.m[5] * 1;
+    this.m[0] = m0;
+    this.m[1] = m1;
+    this.m[2] = m2;
+    this.m[3] = m3;
+    this.m[4] = m4;
+    this.m[5] = m5;
+    return this;
+  }
+
+
+
+
+
+
+
+  /**
+   * Post multiply
+   */
+  postMultiply(marr) {
+    const m0 = marr[0] * this.m[0] + marr[2] * this.m[1];
+    const m1 = marr[1] * this.m[0] + marr[3] * this.m[1];
+    const m2 = marr[0] * this.m[2] + marr[2] * this.m[3];
+    const m3 = marr[1] * this.m[2] + marr[3] * this.m[3];
+    const m4 = marr[0] * this.m[4] + marr[2] * this.m[5] + marr[4] * 1;
+    const m5 = marr[1] * this.m[4] + marr[3] * this.m[5] + marr[5] * 1;
+    this.m[0] = m0;
+    this.m[1] = m1;
+    this.m[2] = m2;
+    this.m[3] = m3;
+    this.m[4] = m4;
+    this.m[5] = m5;
+    return this;
+  }
+
+
+
+  /**
+   * Set max scale
+   */
+  setMaxScale(maxScale) {
+    this.maxScale = maxScale;
+    return this;
+  }
+
+  /**
+   * Set min scale
+   */
+  setMinScale(minScale) {
+    this.minScale = minScale;
+    return this;
+  }
+
+
+  /**
+   * Scale matrix
+   */
+  setScale(sx, sy) {
+    this.m[0] = sx;
+    this.m[3] = sy;
+    this.resetInverse();
+    return this;
+  }
+
+  /**
+   * Set matrix translate
+   */
+  setTranslate(tx, ty) {
+    this.m[4] = tx;
+    this.m[5] = ty;
+    this.resetInverse();
+    return this;
+  }
+
+
+  /**
+   * Get scaleX scaleY
+   */
+  getScale() {
+    var self = this;
+    return {
+      x: self.m[0],
+      y: self.m[3]
+    };
+  }
+
+  get scaleX(): number { return this.m[0] }
+  get scaleY(): number { return this.m[3] }
+
+  /**
+   * Get translateX translateY
+   */
+  getTranslate() {
+    var self = this;
+    return {
+      x: self.m[4],
+      y: self.m[5]
+    }
+  }
+
+  /**
+   * Post scale 
+   */
+  postScale(sx, sy) {
+    this.postMultiply([sx, 0, 0, sy, 0, 0]);
+    this.resetInverse();
+    return this;
+  }
+
+  /**
+   * Post scale by pivot(x,y)
+   */
+  postScaleBy(sx: number, sy: number, x: number, y: number) {
+    this.postMultiply([1, 0, 0, 1, (-1) * x, (-1) * y]);
+    this.postMultiply([sx, 0, 0, sy, 0, 0]);
+    this.postMultiply([1, 0, 0, 1, x, y]);
+    this.resetInverse();
+    return this;
+  }
+
+
+  /**
+   * Post matrix translate
+   */
+  postTranslate(tx, ty) {
+    this.postMultiply([1, 0, 0, 1, tx, ty]);
+    this.resetInverse();
+    return this;
+  }
+
+  /**
+   * Set inverse to null while matrix changed.
+   */
+  resetInverse() {
+    this.inverse = null;
+  }
+
+
+  /**
+   * Inverse Map points
+   */
+  invMapPoint(x, y): Point {
+    if (!this.inverse)
+      this.inverse = this.clone().invert();
+    return this.inverse.mapPoint(x, y)
+  }
+
+
+  /**
+   * Map rect by matrix.
+   */
+  mapRect(rect): Rect {
+    var lt = this.mapPoint(rect.left, rect.top);
+    var rb = this.mapPoint(rect.right, rect.bottom);
+    return new Rect(lt.x, lt.y, rb.x, rb.y);
+  }
+
+
+  /**
+   * Map points
+   */
+  mapPoint(x, y): Point {
+    const mx = this.m[0] * x + this.m[2] * y + this.m[4] * 1;
+    const my = this.m[1] * x + this.m[3] * y + this.m[5] * 1;
+    return {
+      x: mx,
+      y: my,
+    };
+  }
+
+
+  /**
+   * Reset to initial.
+   */
+  reset() {
+    this.m = this.initial.slice();
+  }
+
+
+  toArray(): Array<number> {
+    return this.m.slice();
+  }
+
+  toString() {
+    return 'matrix(' + this.toArray().join(',') + ')';
+  }
+
+
+  /**
+   * Check if scale between [minScale, maxScale]
+   */
+  validateScale(sx, sy) {
+    var cur = this.getScale();
+    var minX = this.minScale / cur.x;
+    var minY = this.minScale / cur.y;
+    var maxX = this.maxScale / cur.x;
+    var maxY = this.maxScale / cur.y;
+
+    sx = Math.max(minX, sx);
+    sx = Math.min(maxX, sx);
+    sy = Math.max(minY, sy);
+    sy = Math.min(maxY, sy);
+
+    return {
+      x: sx,
+      y: sy
+    }
+  }
+
+}

+ 28 - 0
zorro/src/app/lib/filler/core/pantool.ts

@@ -0,0 +1,28 @@
+import Tool from './tool';
+
+export default class PanTool extends Tool {
+
+
+  constructor() {
+    super('pan', 'move');
+  }
+
+  /**
+   * Always in progress
+   * Prevent control drag while pan.
+   */
+  override inProgress(evt : MouseEvent) {
+    return true;
+  }
+
+  override onDragStart(evt : MouseEvent) {
+    this.onDragMove(evt);
+  }
+
+  override onDragMove(evt : MouseEvent) {
+    if(this.editor) {
+      this.editor.pan(evt._dx, evt._dy);
+    }
+  }
+
+}

+ 152 - 0
zorro/src/app/lib/filler/core/rect.ts

@@ -0,0 +1,152 @@
+/**
+ * Rect
+ */
+export default class Rect {
+  left: number;
+  top: number;
+  right: number;
+  bottom: number;
+
+  constructor(left: number = 0, top: number = 0, right: number = 0, bottom: number = 0) {
+    this.left = left;
+    this.top = top;
+    this.right = right;
+    this.bottom = bottom;
+  }
+
+  set(left: number, top: number, right: number, bottom: number) {
+    this.left = left;
+    this.top = top;
+    this.right = right;
+    this.bottom = bottom;
+  }
+
+  /**
+   * Get width of the rect.
+   */
+  width() {
+    return this.right - this.left + 1;
+  }
+
+  /**
+   * Get height of the rect
+   */
+  height() {
+    return this.bottom - this.top + 1;
+  }
+
+  /*
+   * Get the center point of the rect.
+   */
+  center() {
+    return {
+      x: (this.right + this.left) / 2,
+      y: (this.bottom + this.top) / 2
+    }
+  }
+
+  /*
+   * Check if point x, y in rect.
+   */
+  contains(x: number, y: number) {
+    return x >= this.left &&
+      x <= this.right &&
+      y >= this.top &&
+      y <= this.bottom;
+  }
+
+  /**
+   * Move rect to match the center point x, y
+   */
+  centerTo(x: number, y: number) {
+    //current center
+    var center = this.center();
+    this.offset(x - center.x, y - center.y);
+  }
+
+  /**
+   * Offset rect by dx, dy
+   */
+  offset(dx: number, dy: number) {
+    this.left += dx;
+    this.right += dx;
+    this.top += dy;
+    this.bottom += dy;
+  }
+
+
+  /** 
+   * Scale the rect at(0, 0)
+   */
+  scale(scale: number) {
+    this.left *= scale;
+    this.right *= scale;
+    this.top *= scale;
+    this.bottom *= scale;
+  }
+
+
+  /**
+   * Scale the rect by certain point x,y
+   */
+  scaleBy(scale: number, x: number, y: number) {
+    this.offset((-1) * x, (-1) * y);
+    this.scale(scale);
+    this.offset(x, y);
+  }
+
+  /**
+   * Scale the rect by center 
+   */
+  scaleByCenter(scale: number) {
+    var center = this.center();
+    this.scaleBy(scale, center.x, center.y);
+  }
+
+
+  /**
+   * Scale the rect to contain x, y
+   */
+  scaleToContain(x: number, y: number) {
+    if (this.contains(x, y)) return;
+    var center = this.center();
+    var dx = Math.abs(x - center.x);
+    var dy = Math.abs(y - center.y);
+    var scale = Math.max(dx / (this.width() / 2), dy / (this.height() / 2));
+    if (scale > 1) this.scaleByCenter(scale);
+  }
+
+
+  /**
+   * Scale the rect to contain another rect. 
+   */
+  scaleToContainRect(rect: Rect) {
+    if (rect instanceof Rect) {
+      this.scaleToContain(rect.left, rect.top);
+      this.scaleToContain(rect.right, rect.top);
+      this.scaleToContain(rect.left, rect.bottom);
+      this.scaleToContain(rect.right, rect.bottom);
+    }
+  }
+
+
+  inset(left: number, top: number, right: number, bottom: number) {
+    return new Rect(
+      this.left + left,
+      this.top + top,
+      this.right - right,
+      this.bottom - bottom
+    );
+  }
+
+
+  /**
+   *
+   * Clone self.
+   */
+  clone() {
+    return new Rect(this.left, this.top, this.right, this.bottom);
+  }
+
+
+}

+ 67 - 0
zorro/src/app/lib/filler/core/tool.ts

@@ -0,0 +1,67 @@
+import Editor from "./editor";
+import Layer from "./layer";
+
+/**
+ * Tool super class.
+ */
+export default class Tool {
+
+  editor: Editor;
+  props: any;
+  key: string;
+  cursor: string;
+  _inProgress: boolean = false;
+
+
+  constructor(key, cursor) {
+    this.props = {};
+    this.key = key || 'tool';
+    this.cursor = cursor || 'default';
+  }
+
+
+  getProp(key) {
+    return this.props[key];
+  }
+
+  setProp(key, val) {
+    this.props[key] = val;
+  }
+
+  getPropKeys() {
+    return Object.keys(this.props);
+  }
+
+  cloneProps() {
+    return Object.assign({}, this.props);
+  }
+
+  setEditor(editor : Editor) {
+    this.editor = editor;
+  }
+
+  isActive() {
+    return this.editor != null;
+  }
+
+
+  inProgress(evt: MouseEvent) { return this._inProgress; }
+
+  onDragStart(evt: MouseEvent) { }
+  onDragMove(evt: MouseEvent) { }
+  onDragEnd(evt: MouseEvent) { }
+  onmousedown(evt: MouseEvent) { }
+
+  oncancel() { }
+  ondblclick(evt: MouseEvent) { }
+  onclick(evt: MouseEvent) { }
+  onmouseup(evt: MouseEvent) { }
+  onmousemove(evt: MouseEvent) { }
+  //setOnHover(arg0: boolean) { }
+
+  onSelect() {}
+
+  onUnSelect() {}
+
+
+}

+ 109 - 0
zorro/src/app/lib/filler/core/utils.ts

@@ -0,0 +1,109 @@
+function randomColor(alpha) {
+  var alpha = alpha || 0.5;
+  var r = Math.round(Math.random() * 255);
+  var g = Math.round(Math.random() * 255);
+  var b = Math.round(Math.random() * 255);
+  return 'rgba(' + [r, g, b, alpha].join(',') + ')';
+}
+
+function isFunction(functionToCheck) {
+  return functionToCheck && {}.toString.call(functionToCheck) === '[object Function]';
+}
+
+var canvas = document.createElement('canvas');
+canvas.width = canvas.height = 1;
+var ctx = canvas.getContext('2d');
+
+
+
+export default class Utils {
+
+  /**
+   * Parse color to rgba.
+   */
+  static parseColor(color) {
+    ctx.fillStyle = color;
+    ctx.fillRect(0, 0, 1, 1);
+    let pixel = ctx.getImageData(0, 0, 1, 1);
+    return pixel.data;
+  }
+
+  /**
+   * Return rgb color by the given [r,g,b,a]
+   */
+  static makeColorRgba(buf) : string {
+    return 'rgba(' + buf[0] + ',' + buf[1] + ',' + buf[2] + ',' + buf[3] / 255 + ')';
+  }
+
+  /**
+   * Return hex color by the given [r,g,b,a]
+   */
+  static makeColorHex(buf) {
+    let r = ('00' + buf[0].toString(16)).slice(-2);
+    let g = ('00' + buf[1].toString(16)).slice(-2);
+    let b = ('00' + buf[2].toString(16)).slice(-2);
+    return `#${r}${g}${b}`;
+  }
+
+  static makeColor(buf) : string {
+    return this.makeColorRgba(buf);
+  }
+
+  /**
+   * Change the give color alpha.
+   */
+  static setAlpha(color, alpha) {
+    var buf = this.parseColor(color);
+    buf[3] = alpha;
+    return this.makeColor(buf);
+  }
+
+
+
+
+  /**
+   * Promis load file as array buffer.
+   */
+  static loadFile(file) {
+    return new Promise(function (done, reject) {
+      var reader = new FileReader();
+      reader.onload = function () {
+        done(reader.result);
+      }
+      reader.onerror = reject;
+      reader.readAsArrayBuffer(file);
+    });
+  }
+
+
+  /**
+   * Request binary data
+   */
+  static ajaxBinary(url) {
+    return new Promise(function (done, reject) {
+      var oReq = new XMLHttpRequest();
+      oReq.open("GET", url, true);
+      oReq.responseType = "arraybuffer";
+      oReq.onload = function (oEvent) {
+        var arrayBuffer = oReq.response; // Note: not oReq.responseText
+        if (arrayBuffer) {
+          //var byteArray = new Uint8Array(arrayBuffer);
+          done(arrayBuffer);
+        } else {
+          reject('Empty response');
+        }
+      };
+      oReq.onerror = reject;
+      oReq.send(null);
+    });
+  }
+
+
+
+
+}
+
+export {
+  randomColor,
+  isFunction,
+}

+ 33 - 0
zorro/src/app/lib/filler/editor.directive.ts

@@ -0,0 +1,33 @@
+import { Directive, ElementRef, Input, NgZone, OnDestroy, OnInit } from '@angular/core';
+
+import Editor from './core/editor';
+
+
+@Directive({
+  selector: '[appEditor]'
+})
+export class EditorDirective implements OnInit, OnDestroy {
+  @Input() opts: any;
+  @Input() padding: any;
+
+  private canvas: HTMLCanvasElement;
+  public editor: Editor;
+
+  constructor(el: ElementRef, private zone: NgZone) {
+    //减少change detecion, 提升性能
+    //this.zone.runOutsideAngular(() => {
+    //  this.editor = new Editor(el.nativeElement as HTMLCanvasElement);
+    //});
+    // this.editor = new Editor(el.nativeElement as HTMLCanvasElement, null, this.padding);
+    this.canvas = el.nativeElement as HTMLCanvasElement;
+  }
+  ngOnInit(): void {
+    this.editor = new Editor(this.canvas, this.opts, this.padding);
+  }
+
+  ngOnDestroy(): void {
+    this.editor.destroy();
+    this.editor = null;
+  }
+
+}

+ 17 - 0
zorro/src/app/lib/filler/filler.module.ts

@@ -0,0 +1,17 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { EditorDirective } from './editor.directive';
+
+
+@NgModule({
+  declarations: [
+    EditorDirective,
+  ],
+  imports: [
+    CommonModule
+  ],
+  exports: [
+    EditorDirective,
+  ]
+})
+export class FillerModule { }

+ 24 - 0
zorro/src/app/lib/filler/map-edit/map-brush-tool.ts

@@ -0,0 +1,24 @@
+import MapEditTool from './map-edit-tool';
+
+export default class MapBrushTool extends MapEditTool {
+
+  constructor(mapEditLayer) {
+    super(mapEditLayer, `map-edit-brush`,
+      'url("/static/cursor/cursor-paint.png") 0 24, default');
+    this.setProp('lineWidth', 1);
+  }
+
+  override onDragStart(evt: MouseEvent) {
+    if (!this.mapEditLayer) return;
+    this.mapEditLayer.brushStart(evt._contentPoint);
+  }
+
+  override onDragMove(evt: MouseEvent) {
+    this.mapEditLayer.brushMove(evt._contentPoint, this.getProp('lineWidth'));
+  }
+
+  override onDragEnd(evt: MouseEvent) {
+    if (!this.mapEditLayer) return;
+    this.mapEditLayer.brushEnd();
+  }
+}

+ 849 - 0
zorro/src/app/lib/filler/map-edit/map-edit-layer.ts

@@ -0,0 +1,849 @@
+import Repeater from '../common/repeater';
+import Utils from '../common/utils';
+import Layer from '../core/layer';
+import Rect from '../core/rect';
+import Tool from '../core/tool';
+import tool from '../core/tool';
+import FillArea from '../common/fillarea';
+import FloodFill2 from '../common/floodfill2';
+import MapBrushTool from './map-brush-tool';
+import MapEditTool from './map-edit-tool';
+import MapMergeTool from './map-merge-tool';
+import MapRecoverTool from './map-recover-tool';
+import MapSplitAreaTool from './map-split-area-tool';
+import MapPencilTool from './map-pencil-tool';
+
+export default class MapEditLayer extends Layer {
+
+
+  page: HTMLImageElement;
+  map: HTMLImageElement;
+  scale: number;
+  mapData: ImageData;
+  mapPixels: Uint32Array;
+  originalMap: HTMLImageElement;
+  originalMapPixels: Uint32Array;
+  mapCanvas: HTMLCanvasElement;
+  mapCtx: CanvasRenderingContext2D;
+  undoList: any[];
+  redoList: any[];
+  saved: boolean = true;
+  areaMap: {};
+  brushBackup: boolean;
+  brushUpdate: boolean;
+  brushStartArea: any;
+  brushLastPoint: any;
+  newMap: any;
+  newMapPixels: Uint32Array;
+
+  raw: HTMLImageElement;
+
+
+  override get defaultToolKey(): string { return 'map-edit-merge'; }
+
+  private _showPage: boolean = true;
+  public get showPage(): boolean {
+    return this._showPage;
+  }
+  public set showPage(value: boolean) {
+    this._showPage = value;
+    this.invalidate();
+  }
+
+  /**
+   * @param page original image.
+   * @param map map image.
+   * @param output  output image.
+   */
+  constructor(page: HTMLImageElement, map: HTMLImageElement, raw?: HTMLImageElement) {
+    super(map.width, map.height);
+    //console.log('init map editor layer....', original, map)
+
+    this.addTool(new MapBrushTool(this));
+    this.addTool(new MapMergeTool(this));
+    this.addTool(new MapRecoverTool(this));
+    this.addTool(new MapSplitAreaTool(this));
+    this.addTool(new MapPencilTool(this));
+
+    this.raw = raw;
+    this.page = page;
+    this.map = map;
+    this.originalMap = map;
+
+    this.init();
+  }
+
+
+  init() {
+
+    this.scale = this.map.width / this.page.width;
+
+    console.log('FillerLayer@width=', this.width, ' height=', this.height,
+      ' scale=', this.scale);
+
+    this.mapData = Utils.getImageData(this.map);
+    this.mapPixels = new Uint32Array(this.mapData.data.buffer);
+
+
+    let originalMapData = Utils.getImageData(this.originalMap);
+    this.originalMapPixels = new Uint32Array(originalMapData.data.buffer);
+
+    this.mapCanvas = Utils.createCanvas(this.width, this.height);
+    this.mapCtx = this.mapCanvas.getContext('2d');
+    this.mapCtx.putImageData(this.mapData, 0, 0);
+
+    this.createAreaMap();
+
+    this.undoList = [];
+    this.redoList = [];
+
+    this.invalidate();
+  }
+
+  update(page: HTMLImageElement, map: HTMLImageElement, raw?: HTMLImageElement) {
+    this.page = page || this.page;
+    this.map = map || this.map;
+    this.raw = raw || this.raw;
+    this.originalMap = this.map;
+    this.saved = false;
+    //只有更新了map图才需要重新初始化,
+    //Fix map修改丢失的问题
+    if (map) {
+      this.init();
+    }
+
+    if (page) {
+      //Fix 线稿更新后 splitArea不生效
+      this.newMap = null;
+    }
+  }
+
+
+
+  /**
+   * Translate point.
+   * Because our cordinate system is based on map, so
+   * we don't need to translate.
+   */
+  translate(p) {
+    return p;
+    /*
+    let pp = {};
+    pp.x = p.x * this.scale;
+    pp.y = p.y * this.scale;
+    return pp;
+    */
+  }
+
+  /**
+   * redraw filler layer.
+   */
+  override invalidate() {
+    this.updateAreaList();
+    this.trigger('invalidate');
+  }
+
+  triggerMapUpdate() {
+    this.saved = false;
+    this.trigger('map-update');
+    this.invalidate();
+  }
+
+  setSaved() {
+    this.saved = true;
+  }
+
+  isSaved() {
+    return this.saved;
+  }
+
+  /**
+   * Create area map from map pixels.
+   */
+  createAreaMap() {
+    let width = this.width;
+    let height = this.height;
+    let floodArr = this.mapPixels;
+    let areaMap = {};
+    let i, x, y, color;
+    for (i = 0; i < floodArr.length; i++) {
+      color = floodArr[i];
+      if (color == 0) continue;
+      x = i % width;
+      y = Math.floor(i / width);
+      if (areaMap[color]) {
+        areaMap[color].addPoint(x, y);
+      } else {
+        areaMap[color] = new FillArea(color, x, y);
+      }
+    }
+    this.areaMap = areaMap;
+  }
+
+  getAreaMap() {
+    return this.areaMap;
+  }
+
+  /**
+   * Get area by x, y
+   */
+  getArea(x, y) {
+    x = parseInt(x);
+    y = parseInt(y);
+    if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
+      return null;
+    }
+    let color = this.mapPixels[y * this.width + x];
+    return this.areaMap[color];
+  }
+
+  /**
+   * Get area by color(areaIndex)
+   */
+  getAreaByColor(color) {
+    return this.areaMap[color];
+  }
+
+  toggleOriginal() {
+    this.showPage = !this.showPage;
+    this.invalidate();
+  }
+
+  toggleSmoothing() {
+    this.smoothing = !this.smoothing;
+    console.log('toggleSmoothing', this.smoothing)
+    this.invalidate();
+  }
+
+  /**
+   * Draw this layer.
+   * @Override Layer#draw(ctx)
+   */
+  override draw(ctx) {
+    //ctx.mozImageSmoothingEnabled = this.smoothing;
+    //ctx.webkitImageSmoothingEnabled = this.smoothing;
+    //ctx.msImageSmoothingEnabled = this.smoothing;
+    ctx.imageSmoothingEnabled = this.smoothing;
+
+    // draw map
+
+    //ctx.drawImage(this.map, 0, 0, this.map.width,
+    //  this.map.height, 0, 0, this.width, this.height);
+
+    ctx.drawImage(this.mapCanvas, 0, 0, this.mapCanvas.width,
+      this.mapCanvas.height, 0, 0, this.width, this.height);
+
+    if (this.blinkCanvas) {
+      ctx.drawImage(this.blinkCanvas, 0, 0);
+    }
+
+    if (this.showPage) {
+      if (this.showSvg && this.raw) {
+        ctx.drawImage(this.raw, 0, 0, this.page.width,
+          this.page.height, 0, 0, this.width, this.height);
+      } else {
+        ctx.drawImage(this.page, 0, 0, this.page.width,
+          this.page.height, 0, 0, this.width, this.height);
+      }
+    }
+
+    if (this.pencilCanvas && this.isPencilDrawing) {
+      ctx.drawImage(this.pencilCanvas, 0, 0);
+    }
+
+  }
+
+
+  getAreaCount() {
+    return Object.keys(this.areaMap).length;
+  }
+
+
+  sortedAreaList: FillArea[] = [];
+  updateAreaList() {
+    this.sortedAreaList = Object.keys(this.areaMap).map(key => this.areaMap[key])
+      .sort((a, b) => {
+        return a.count - b.count;
+      })
+  }
+
+  brushStart(p) {
+    this.brushBackup = false;
+    this.brushUpdate = false;
+    this.brushStartArea = this.getArea(p.x, p.y);
+    this.brushLastPoint = { x: parseInt(p.x), y: parseInt(p.y) };
+  }
+
+  brushMove(p, linewidth: number) {
+    if (!this.brushStartArea) return;
+    //let area = this.getArea(point.x, point.y);
+    let x = parseInt(p.x);
+    let y = parseInt(p.y);
+
+    let points = this.brushGetPionts({ x, y }, linewidth);
+    let startArea = this.brushStartArea;
+    let change = false;
+
+    for (let pp of points) {
+      x = pp.x;
+      y = pp.y;
+      if (x < 0 || x >= this.width || y < 0 || y >= this.height) continue;
+      let i = y * this.width + x;
+      console.log("x=" + x + " y=" + y + " i=" + i);
+      if (this.mapPixels[i] != startArea.color) {
+        if (!this.brushBackup) {
+          this.backup(); //backup on the first change.
+          this.brushBackup = true;
+        }
+        this.mapPixels[i] = startArea.color;
+        this.mapCtx.putImageData(this.mapData, 0, 0);
+        change = true;
+      }
+    }
+    if (change) {
+      this.invalidate();
+      this.brushUpdate = true;
+    }
+
+
+    // 原代码, 只支持一个点的,即线宽是1的, 采用上面的代码支持动态线宽
+    // if (x < 0 || x >= this.width || y < 0 || y >= this.height) return;
+
+    // let i = y * this.width + x;
+    // console.log("x=" + x + " y=" + y + " i=" + i);
+    // let startArea = this.brushStartArea;
+    // if (this.mapPixels[i] != startArea.color) {
+    //   if (!this.brushBackup) {
+    //     this.backup(); //backup on the first change.
+    //     this.brushBackup = true;
+    //   }
+    //   this.mapPixels[i] = startArea.color;
+    //   this.mapCtx.putImageData(this.mapData, 0, 0)
+    //   this.invalidate();
+    //   this.brushUpdate = true;
+    // }
+
+    this.brushLastPoint = { x: p.x, y: p.y };
+  }
+
+  brushEnd() {
+    if (!this.brushStartArea) return;
+    if (this.brushUpdate) {
+      this.createAreaMap();
+      this.triggerMapUpdate();
+    }
+    this.brushStartArea = null; //reset
+    this.brushLastPoint = null;
+  }
+
+  // 根据线宽取点
+  brushGetPionts(p, lineWidth: number) {
+    let points = [];
+    if (lineWidth == 1) {
+      points.push({ x: p.x, y: p.y });
+    } else if (lineWidth == 2) {
+      let x1 = this.brushLastPoint.x, y1 = this.brushLastPoint.y, x2 = p.x, y2 = p.y;
+      if (x2 != x1 && y2 == y1) { // 横
+        points.push({ x: p.x, y: p.y });
+        points.push({ x: p.x, y: p.y + 1 });
+      } else if (x2 == x1 && y2 != y1) { // 竖
+        points.push({ x: p.x, y: p.y });
+        points.push({ x: p.x + 1, y: p.y });
+      } else if (x2 > x1 && y2 > y1) { // 斜向右下
+        points.push({ x: p.x, y: p.y });
+        points.push({ x: p.x + 1, y: p.y });
+        points.push({ x: p.x, y: p.y + 1 });
+        points.push({ x: p.x + 1, y: p.y + 1 });
+      } else if (x2 > x1 && y2 < y1) { // 斜向右上
+        points.push({ x: p.x, y: p.y });
+        points.push({ x: p.x + 1, y: p.y });
+        points.push({ x: p.x, y: p.y - 1 });
+        points.push({ x: p.x + 1, y: p.y - 1 });
+      } else if (x2 < x1 && y2 < y1) { // 斜向左上
+        points.push({ x: p.x, y: p.y });
+        points.push({ x: p.x - 1, y: p.y });
+        points.push({ x: p.x, y: p.y - 1 });
+        points.push({ x: p.x - 1, y: p.y - 1 });
+      } else if (x2 < x1 && y2 > y1) { // 斜向左下
+        points.push({ x: p.x, y: p.y });
+        points.push({ x: p.x - 1, y: p.y });
+        points.push({ x: p.x, y: p.y + 1 });
+        points.push({ x: p.x - 1, y: p.y + 1 });
+      }
+    } else if (lineWidth == 3) { // 线宽是3简单处理, 3*3矩阵
+      points.push({ x: p.x, y: p.y });
+      points.push({ x: p.x, y: p.y + 1 });
+      points.push({ x: p.x, y: p.y - 1 });
+      points.push({ x: p.x + 1, y: p.y });
+      points.push({ x: p.x + 1, y: p.y + 1 });
+      points.push({ x: p.x + 1, y: p.y - 1 });
+      points.push({ x: p.x - 1, y: p.y });
+      points.push({ x: p.x - 1, y: p.y + 1 });
+      points.push({ x: p.x - 1, y: p.y - 1 });
+    } else if (lineWidth == 4) {
+      let x1 = this.brushLastPoint.x, y1 = this.brushLastPoint.y, x2 = p.x, y2 = p.y;
+      if (x2 != x1 && y2 == y1) { // 横
+        points.push({ x: p.x, y: p.y });
+        points.push({ x: p.x, y: p.y - 1 });
+        points.push({ x: p.x, y: p.y + 1 });
+        points.push({ x: p.x, y: p.y + 2 });
+      } else if (x2 == x1 && y2 != y1) { // 竖
+        points.push({ x: p.x, y: p.y });
+        points.push({ x: p.x + 1, y: p.y });
+        points.push({ x: p.x + 2, y: p.y });
+        points.push({ x: p.x - 1, y: p.y });
+      } else if (x2 > x1 && y2 > y1) { // 斜向右下
+        points.push({ x: p.x, y: p.y });
+        points.push({ x: p.x, y: p.y - 1 });
+        points.push({ x: p.x, y: p.y + 1 });
+        points.push({ x: p.x, y: p.y + 2 });
+        points.push({ x: p.x - 1, y: p.y - 1 });
+        points.push({ x: p.x - 1, y: p.y });
+        points.push({ x: p.x - 1, y: p.y + 1 });
+        points.push({ x: p.x - 1, y: p.y + 2 });
+        points.push({ x: p.x + 1, y: p.y - 1 });
+        points.push({ x: p.x + 1, y: p.y });
+        points.push({ x: p.x + 1, y: p.y + 1 });
+        points.push({ x: p.x + 1, y: p.y + 2 });
+        points.push({ x: p.x + 2, y: p.y - 1 });
+        points.push({ x: p.x + 2, y: p.y });
+        points.push({ x: p.x + 2, y: p.y + 1 });
+        points.push({ x: p.x + 2, y: p.y + 2 });
+      } else if (x2 > x1 && y2 < y1) { // 斜向右上
+        points.push({ x: p.x, y: p.y });
+        points.push({ x: p.x, y: p.y - 1 });
+        points.push({ x: p.x, y: p.y - 2 });
+        points.push({ x: p.x, y: p.y + 1 });
+        points.push({ x: p.x - 1, y: p.y });
+        points.push({ x: p.x - 1, y: p.y - 1 });
+        points.push({ x: p.x - 1, y: p.y - 2 });
+        points.push({ x: p.x - 1, y: p.y + 1 });
+        points.push({ x: p.x + 1, y: p.y });
+        points.push({ x: p.x + 1, y: p.y - 1 });
+        points.push({ x: p.x + 1, y: p.y - 2 });
+        points.push({ x: p.x + 1, y: p.y + 1 });
+        points.push({ x: p.x + 2, y: p.y });
+        points.push({ x: p.x + 2, y: p.y - 1 });
+        points.push({ x: p.x + 2, y: p.y - 2 });
+        points.push({ x: p.x + 2, y: p.y + 1 });
+      } else if (x2 < x1 && y2 < y1) { // 斜向左上
+        points.push({ x: p.x, y: p.y });
+        points.push({ x: p.x, y: p.y - 1 });
+        points.push({ x: p.x, y: p.y - 2 });
+        points.push({ x: p.x, y: p.y + 1 });
+        points.push({ x: p.x - 1, y: p.y });
+        points.push({ x: p.x - 1, y: p.y - 1 });
+        points.push({ x: p.x - 1, y: p.y - 2 });
+        points.push({ x: p.x - 1, y: p.y + 1 });
+        points.push({ x: p.x - 2, y: p.y });
+        points.push({ x: p.x - 2, y: p.y - 1 });
+        points.push({ x: p.x - 2, y: p.y - 2 });
+        points.push({ x: p.x - 2, y: p.y + 1 });
+        points.push({ x: p.x + 1, y: p.y });
+        points.push({ x: p.x + 1, y: p.y - 1 });
+        points.push({ x: p.x + 1, y: p.y - 2 });
+        points.push({ x: p.x + 1, y: p.y + 1 });
+
+      } else if (x2 < x1 && y2 > y1) { // 斜向左下
+        points.push({ x: p.x, y: p.y });
+        points.push({ x: p.x, y: p.y - 1 });
+        points.push({ x: p.x, y: p.y + 1 });
+        points.push({ x: p.x, y: p.y + 2 });
+        points.push({ x: p.x + 1, y: p.y });
+        points.push({ x: p.x + 1, y: p.y - 1 });
+        points.push({ x: p.x + 1, y: p.y + 1 });
+        points.push({ x: p.x + 1, y: p.y + 2 });
+        points.push({ x: p.x + 1, y: p.y });
+        points.push({ x: p.x - 1, y: p.y - 1 });
+        points.push({ x: p.x - 1, y: p.y + 1 });
+        points.push({ x: p.x - 1, y: p.y + 2 });
+        points.push({ x: p.x - 2, y: p.y });
+        points.push({ x: p.x - 2, y: p.y - 1 });
+        points.push({ x: p.x - 2, y: p.y + 1 });
+        points.push({ x: p.x - 2, y: p.y + 2 });
+      }
+    } else if (lineWidth == 5) {
+      points.push({ x: p.x, y: p.y });
+      points.push({ x: p.x, y: p.y - 1 });
+      points.push({ x: p.x, y: p.y - 2 });
+      points.push({ x: p.x, y: p.y + 1 });
+      points.push({ x: p.x, y: p.y + 2 });
+      points.push({ x: p.x + 1, y: p.y });
+      points.push({ x: p.x + 1, y: p.y - 1 });
+      points.push({ x: p.x + 1, y: p.y - 2 });
+      points.push({ x: p.x + 1, y: p.y + 1 });
+      points.push({ x: p.x + 1, y: p.y + 2 });
+      points.push({ x: p.x + 2, y: p.y });
+      points.push({ x: p.x + 2, y: p.y - 1 });
+      points.push({ x: p.x + 2, y: p.y - 2 });
+      points.push({ x: p.x + 2, y: p.y + 1 });
+      points.push({ x: p.x + 2, y: p.y + 2 });
+      points.push({ x: p.x - 1, y: p.y });
+      points.push({ x: p.x - 1, y: p.y - 1 });
+      points.push({ x: p.x - 1, y: p.y - 2 });
+      points.push({ x: p.x - 1, y: p.y + 1 });
+      points.push({ x: p.x - 1, y: p.y + 2 });
+      points.push({ x: p.x - 2, y: p.y });
+      points.push({ x: p.x - 2, y: p.y - 1 });
+      points.push({ x: p.x - 2, y: p.y - 2 });
+      points.push({ x: p.x - 2, y: p.y + 1 });
+      points.push({ x: p.x - 2, y: p.y + 2 });
+    }
+    return points;
+  }
+
+
+  merge(p1, p2) {
+    if (!p1 || !p2) {
+      console.warn('invalid args: ', p1, p2)
+      return;
+    }
+    let area1 = this.getArea(p1.x, p1.y);
+    let area2 = this.getArea(p2.x, p2.y);
+    if (!area1 || !area2) {
+      console.warn('area not found', area1, area2)
+    } else if (area1 == area2) {
+      console.warn('同一个区域')
+      return;
+    }
+    this.mergeArea(area1, area2);
+  }
+
+
+  mergeArea(area, area2) {
+    this.backup();
+    console.log('mergeArea', area, area2)
+    let ai, ax, ay, aw, i, x, y, ah;
+    aw = area.width();
+    ah = area.height();
+    for (ai = 0; ai < aw * ah; ai++) {
+      x = area.left + ai % aw;
+      y = area.top + Math.floor(ai / aw);
+      i = y * this.width + x;
+      if (this.mapPixels[i] == area.color) {
+        this.mapPixels[i] = area2.color;
+      }
+    }
+    this.mapCtx.putImageData(this.mapData, 0, 0);
+    this.createAreaMap();
+    this.triggerMapUpdate();
+  }
+
+
+  backup() {
+    console.log('backup................');
+    this.redoList = [];
+    let data = this.getCurrent();
+    this.undoList.push(data);
+  }
+
+  getCurrent() {
+    let data: any = {};
+    let pixels = new Uint32Array(this.mapPixels.length);
+    for (var i = 0; i < pixels.length; i++) {
+      pixels[i] = this.mapPixels[i];
+    }
+    data.pixels = pixels;
+    data.areaMap = JSON.stringify(this.areaMap);
+    return data;
+  }
+
+  setCurrent(data) {
+    let areaMap = data.areaMap;
+    let pixels = data.pixels;
+    //this.areaMap = JSON.parse(areaMap);
+    for (var i = 0; i < pixels.length; i++) {
+      this.mapPixels[i] = pixels[i];
+    }
+    this.mapCtx.putImageData(this.mapData, 0, 0);
+    this.createAreaMap();
+    this.triggerMapUpdate();
+  }
+
+  override undo() {
+    if (this.undoList.length <= 0) return;
+    this.redoList.unshift(this.getCurrent());
+    let data = this.undoList.pop();
+    this.setCurrent(data);
+  }
+
+  override redo() {
+    if (this.redoList.length <= 0) return;
+    this.undoList.push(this.getCurrent());
+    let data = this.redoList.shift();
+    this.setCurrent(data);
+  }
+
+  override undoSize() {
+    return this.undoList.length;
+  }
+
+  override redoSize() {
+    return this.redoList.length;
+  }
+
+  /**
+   * return map blob
+   */
+  getMapBlob(): Promise<Blob> {
+    return new Promise((done, reject) => {
+      this.mapCanvas.toBlob(b => {
+        done(b);
+      })
+    });
+  }
+
+  getMapImage(): Promise<HTMLImageElement> {
+    return new Promise((done, reject) => {
+      var img = new Image();
+      img.onload = function () { done(img); };
+      img.onerror = reject;
+      img.src = this.mapCanvas.toDataURL();
+    });
+  }
+
+
+  recover(p) {
+    //this.backup();
+    let area = this.getArea(p.x, p.y);
+    if (!area) return;
+    console.log('area: ', area);
+    let px = parseInt(p.x);
+    let py = parseInt(p.y);
+    let index = py * this.width + px;
+    let oldColor = this.originalMapPixels[index];
+    let curColor = this.mapPixels[index];
+    console.log('color: ', oldColor, curColor);
+    if (oldColor == curColor) {
+      console.log('color no change');
+      return;
+    }
+
+    this.backup();
+
+    let ai, aw, i, x, y, ah;
+    aw = area.width();
+    ah = area.height();
+    for (ai = 0; ai < aw * ah; ai++) {
+      x = area.left + ai % aw;
+      y = area.top + Math.floor(ai / aw);
+      i = y * this.width + x;
+      if (this.originalMapPixels[i] == oldColor) {
+        this.mapPixels[i] = oldColor;
+      }
+    }
+
+    this.mapCtx.putImageData(this.mapData, 0, 0);
+    this.createAreaMap();
+    this.triggerMapUpdate();
+  }
+
+
+  async createMapFromOriginal() {
+    if (!this.newMap) {
+      // create new map by new page, exclude current map colors
+      let areaIds: number[] = Object.keys(this.areaMap).map(key => parseInt(key));
+      this.newMap = await FloodFill2.createMapImage(this.page, this.map, 1, areaIds);
+      let newMapData = Utils.getImageData(this.newMap);
+      this.newMapPixels = new Uint32Array(newMapData.data.buffer);
+    }
+  }
+
+  async splitArea(p) {
+    let area = this.getArea(p.x, p.y);
+    if (!area) return;
+    console.log('area: ', area);
+    await this.createMapFromOriginal();
+    let px = parseInt(p.x);
+    let py = parseInt(p.y);
+    let index = py * this.width + px;
+    let curColor = this.mapPixels[index];
+    let oldColor = this.newMapPixels[index];
+    console.log('color: ', oldColor, curColor);
+    // 算法变了, 现在newmap跟原map基本保持一致,分割成两半的区块,有一半的颜色还跟原来保持一致,所以点击的位置的颜色未必有变化
+    // if (oldColor == curColor) {
+    //   console.log('color no change');
+    //   return;
+    // }
+    if (oldColor == curColor) {
+      // 还需要进一步判断,是否有颜色变化
+      let change = false;
+      let ai, aw, i, x, y, ah;
+      aw = area.width();
+      ah = area.height();
+      for (ai = 0; ai < aw * ah; ai++) {
+        x = area.left + ai % aw;
+        y = area.top + Math.floor(ai / aw);
+        i = y * this.width + x;
+        if (this.mapPixels[i] == curColor && this.newMapPixels[i] != curColor) {
+          change = true;
+          break;
+        }
+      }
+      if (!change) {
+        console.log('color no change');
+        return;
+      }
+    }
+
+    this.backup();
+
+    let ai, aw, i, x, y, ah;
+    aw = area.width();
+    ah = area.height();
+    for (ai = 0; ai < aw * ah; ai++) {
+      x = area.left + ai % aw;
+      y = area.top + Math.floor(ai / aw);
+      i = y * this.width + x;
+      // if (this.newMapPixels[i] == oldColor) {
+      //   this.mapPixels[i] = oldColor;
+      // }
+      if (this.mapPixels[i] == curColor) {
+        this.mapPixels[i] = this.newMapPixels[i];
+      }
+    }
+
+    this.mapCtx.putImageData(this.mapData, 0, 0);
+    this.createAreaMap();
+    this.triggerMapUpdate();
+  }
+
+
+  blinkCanvas: HTMLCanvasElement;
+  repeater: Repeater;
+
+
+
+  blinkByAreas(areas: FillArea[]) {
+
+    //Stop the previous running repeater.
+    if (this.repeater) this.repeater.cancel();
+
+    //没有考虑scale
+    let _blinkCanvas = Utils.createCanvas(this.width, this.height);
+    let ctx = _blinkCanvas.getContext('2d');
+    ctx.clearRect(0, 0, this.width, this.height);
+
+    let imgData = ctx.getImageData(0, 0, _blinkCanvas.width, _blinkCanvas.height);
+    let pixels = new Uint32Array(imgData.data.buffer);
+    //console.log(TAG, 'pixels=', pixels);
+
+    areas.forEach((area: FillArea) => {
+      let ai, aw, i, x, y;
+      let areaPixelCount = area.width() * area.height();
+      aw = area.width();
+      for (ai = 0; ai < areaPixelCount; ai++) {
+        x = area.left + ai % aw;
+        y = area.top + Math.floor(ai / aw);
+        i = y * this.width + x;
+        if (this.mapPixels[i] == area.color) {
+          pixels[i] = 0xffffffff;
+        }
+      }
+    })
+    ctx.putImageData(imgData, 0, 0);
+
+    let rect = new Rect(
+      Math.min.call(null, ...areas.map(a => a.left)),
+      Math.min.call(null, ...areas.map(a => a.top)),
+      Math.max.call(null, ...areas.map(a => a.right)),
+      Math.max.call(null, ...areas.map(a => a.bottom))
+    );
+
+    ctx.strokeStyle = '#dd0000';
+    //ctx.strokeStyle = '#efefef';
+    ctx.lineWidth = 2;
+    let insetRect = rect.inset(ctx.lineWidth, ctx.lineWidth, ctx.lineWidth, ctx.lineWidth);
+    ctx.strokeRect(insetRect.left, insetRect.top, insetRect.width(), insetRect.height());
+
+    this.blinkCanvas = _blinkCanvas;
+    this.invalidate();
+
+    this.repeater = new Repeater(() => {
+      this.blinkCanvas = this.blinkCanvas ? null : _blinkCanvas;
+      this.invalidate();
+    }, () => {
+      this.blinkCanvas = null;
+      this.repeater = null;
+      this.invalidate();
+    }, 5, 300).start();
+
+    return rect;
+  }
+
+
+  ///////////////////////// pencil tool /////////////////////////
+  isPencilDrawing = false;
+  pencilCanvas: HTMLCanvasElement;
+  pencilCtx: CanvasRenderingContext2D;
+  pencilPoints: any[] = [];
+
+  pencilStart(p) {
+    console.log("pencilStart");
+    if (!this.pencilCanvas) {
+      this.pencilCanvas = Utils.createCanvas(this.width, this.height);
+      this.pencilCtx = this.pencilCanvas.getContext('2d');
+      this.pencilCtx.strokeStyle = '#ffffff';
+      this.pencilCtx.lineWidth = 2;
+    }
+    this.isPencilDrawing = true;
+    this.pencilCtx.moveTo(p.x, p.y);
+    this.pencilPoints.push({ x: p.x, y: p.y });
+  }
+
+  pencilMove(p) {
+    console.log("pencilMove");
+    if (this.isPencilDrawing) {
+      this.pencilCtx.lineTo(p.x, p.y);
+      this.pencilCtx.stroke();
+      this.pencilPoints.push({ x: p.x, y: p.y });
+      this.invalidate();
+    }
+  }
+
+  pencilEnd() {
+    console.log("pencilEnd");
+  }
+
+  async pencilCreateArea(p) {
+    console.log("pencilCreateArea");
+    if (!this.pencilCanvas) {
+      this.splitArea(p);
+      return;
+    }
+
+    this.pencilCtx.clearRect(0, 0, this.width, this.height);
+    this.pencilCtx.drawImage(this.page, 0, 0);
+    this.pencilCtx.strokeStyle = '#000000';
+    this.pencilCtx.moveTo(this.pencilPoints[0].x, this.pencilPoints[0].y);
+    for (let i = 1; i < this.pencilPoints.length; i++) {
+      this.pencilCtx.lineTo(this.pencilPoints[i].x, this.pencilPoints[i].y);
+    }
+    this.pencilCtx.stroke();
+
+    let blob = await this.canvasToBlob(this.pencilCanvas);
+    let newPage = await Utils.blobToImage(blob);
+    // create new map by new page, exclude current map colors
+    let areaIds: number[] = Object.keys(this.areaMap).map(key => parseInt(key));
+    this.newMap = await FloodFill2.createMapImage(newPage, this.map, 1, areaIds);
+    let newMapData = Utils.getImageData(this.newMap);
+    this.newMapPixels = new Uint32Array(newMapData.data.buffer);
+
+    this.splitArea(p);
+
+    this.pencilPoints = [];
+    this.isPencilDrawing = false;
+    this.pencilCanvas = null;
+    this.invalidate();
+  }
+
+
+  private canvasToBlob(canvas: HTMLCanvasElement): Promise<Blob> {
+    return new Promise((done, reject) => {
+      canvas.toBlob((blob) => {
+        done(blob);
+      }, 'image/png');
+    })
+  }
+
+}

+ 14 - 0
zorro/src/app/lib/filler/map-edit/map-edit-tool.ts

@@ -0,0 +1,14 @@
+import Tool from '../core/tool';
+import MapEditLayer from './map-edit-layer';
+
+
+export default class MapEditTool extends Tool {
+  mapEditLayer: MapEditLayer;
+
+  constructor(mapEditLayer, key, cursor) {
+    super(key, cursor);
+    this.mapEditLayer = mapEditLayer;
+  }
+
+
+}

+ 33 - 0
zorro/src/app/lib/filler/map-edit/map-merge-tool.ts

@@ -0,0 +1,33 @@
+import MapEditTool from './map-edit-tool';
+
+export default class MapMergeTool extends MapEditTool {
+  startPoint: any;
+
+  constructor(mapEditLayer) {
+    super(mapEditLayer, 'map-edit-merge',
+      'url("/static/cursor/cursor-link.png") 24 24, default');
+    this.startPoint = null;
+  }
+
+  override onmousedown(evt: MouseEvent) {
+    console.log('MapMergeTool@onmousedown()');
+    this.startPoint = evt._contentPoint;
+  }
+
+
+  override onDragStart(evt: MouseEvent) {
+    if (!this.mapEditLayer)
+      return;
+    console.log('MapMergeTool@onDragStart()', evt._contentPoint);
+  }
+
+  override onDragMove(evt: MouseEvent) { }
+
+  override onDragEnd(evt: MouseEvent) {
+    if (!this.mapEditLayer || !this.startPoint)
+      return;
+    console.log('MapMergeTool@onDragEnd()', evt);
+    console.log('do merge: ', this.startPoint, evt._contentPoint);
+    this.mapEditLayer.merge(this.startPoint, evt._contentPoint);
+  }
+}

+ 28 - 0
zorro/src/app/lib/filler/map-edit/map-pencil-tool.ts

@@ -0,0 +1,28 @@
+import MapEditTool from './map-edit-tool';
+
+export default class MapPencilTool extends MapEditTool {
+
+
+  constructor(mapEditLayer) {
+    super(mapEditLayer, 'map-edit-pencil',
+      'url("/static/cursor/cursor-pencil.png") 0 24, default');
+  }
+
+  override onDragStart(evt: MouseEvent) {
+    if (!this.mapEditLayer) return;
+    this.mapEditLayer.pencilStart(evt._contentPoint);
+  }
+
+  override onDragMove(evt: MouseEvent) {
+    this.mapEditLayer.pencilMove(evt._contentPoint);
+  }
+
+  override onDragEnd(evt: MouseEvent) {
+    if (!this.mapEditLayer) return;
+    this.mapEditLayer.pencilEnd();
+  }
+
+  override ondblclick(evt: MouseEvent): void {
+    this.mapEditLayer.pencilCreateArea(evt._contentPoint);
+  }
+}

+ 18 - 0
zorro/src/app/lib/filler/map-edit/map-recover-tool.ts

@@ -0,0 +1,18 @@
+import MapEditTool from './map-edit-tool';
+
+export default class MapRecoverTool extends MapEditTool {
+
+  constructor(mapEditLayer) {
+    super(mapEditLayer, 'map-edit-recover',
+      //'url("/static/cursor/cursor-link.png") 24 24, default');
+      'pointer');
+  }
+
+
+  override onclick(evt : MouseEvent) {
+    if (!this.mapEditLayer)
+      return;
+    console.log('MapRecoverTool@onDragEnd()', evt);
+    this.mapEditLayer.recover(evt._contentPoint);
+  }
+}

+ 19 - 0
zorro/src/app/lib/filler/map-edit/map-split-area-tool.ts

@@ -0,0 +1,19 @@
+import MapEditLayer from './map-edit-layer';
+import MapEditTool from './map-edit-tool';
+
+export default class MapSplitAreaTool extends MapEditTool {
+
+  constructor(mapEditLayer: MapEditLayer) {
+    super(mapEditLayer, 'map-edit-split-area',
+      //'url("/static/cursor/cursor-link.png") 24 24, default');
+      'crosshair');
+  }
+
+
+  override onclick(evt: MouseEvent) {
+    if (!this.mapEditLayer)
+      return;
+    console.log('MapRecoverTool@onDragEnd()', evt);
+    this.mapEditLayer.splitArea(evt._contentPoint);
+  }
+}

+ 392 - 0
zorro/src/app/lib/filler/mark-edit/mark-edit-layer.ts

@@ -0,0 +1,392 @@
+import FillArea from "../common/fillarea";
+import {  ColorMap, Mark } from "../common/interfaces";
+import Utils from "../common/utils";
+import { Point } from "../core/interface";
+import Layer from "../core/layer";
+import Rect from "../core/rect";
+import MarkTool from "./mark-tool";
+
+const TAG = 'MarkEditLayer';
+
+export default class MarkEditLayer extends Layer {
+  output: HTMLCanvasElement;
+  outputCtx: CanvasRenderingContext2D;
+
+  raw: HTMLImageElement;
+  page: HTMLImageElement;
+  map: HTMLImageElement;
+  mapPixels: Uint32Array;
+  colorMap: ColorMap;
+
+  areaMap: any = [];
+  areaList: any[];
+
+  undoList: any[] = [];
+  redoList: any[] = [];
+  cacheList: any[] = [];
+
+
+  marks: Mark[]; // 标记点
+
+  innerMarks: InnerMark[] = [];  // 仅供内部使用
+
+  override get defaultToolKey(): string { return 'mark-edit-mark'; }
+
+  constructor(raw: HTMLImageElement, page: HTMLImageElement, map: HTMLImageElement, colorMap: ColorMap, marks: Mark[]) {
+    super(page.width, page.height);
+
+    this.raw = raw;
+    this.page = page;
+    this.map = map;
+    this.colorMap = colorMap;
+
+    this.marks = marks;
+
+    console.log("marks:", marks);
+
+    if (this.marks && this.marks.length > 0) {
+      this.innerMarks = this.marks.map(m => { return {x: m.x, y: m.y, radius: m.radius, note: m.note, status: 0} });
+    }
+
+    this.init();
+
+
+    this.addTool(new MarkTool(this));
+
+  }
+
+  init() {
+    this.output = Utils.createCanvas(this.width, this.height);
+    this.outputCtx = this.output.getContext('2d');
+    this.outputCtx.fillStyle = 'white';
+    this.outputCtx.fillRect(0, 0, this.width, this.height);
+
+    this.mapPixels = new Uint32Array(Utils.getImageData(this.map).data.buffer);
+    this.createAreaMap();
+
+    this.drawToOutput();
+
+    // undo redo support.
+    this.undoList = [];
+    this.redoList = [];
+    this.cacheList = [];
+  }
+
+  drawToOutput() {
+    console.time('drawToOutput');
+    this.outputCtx.fillStyle = 'white';
+    this.outputCtx.fillRect(0, 0, this.width, this.height);
+
+    //fast create output by map
+    let outData = this.outputCtx.getImageData(0, 0, this.output.width, this.output.height)
+    let outPixels = new Uint32Array(outData.data.buffer);
+    let areaIndex;
+    for (var i = 0; i < outPixels.length; i++) {
+      areaIndex = this.mapPixels[i];
+      if (this.colorMap[areaIndex]) {
+        outPixels[i] = this.colorMap[areaIndex].color;
+      }
+    }
+    this.outputCtx.putImageData(outData, 0, 0);
+    console.timeEnd('drawToOutput');
+  }
+
+
+  /**
+   * Create area map from map pixels.
+   */
+  createAreaMap() {
+    let width = this.width;
+    let height = this.height;
+    let floodArr = this.mapPixels;
+    let areaMap = {};
+    let i: number, x: number, y: number, color: number;
+    for (i = 0; i < floodArr.length; i++) {
+      color = floodArr[i];
+      if (!color) continue;
+      x = i % width;
+      //y = parseInt(i / width);
+      y = Math.floor(i / width);
+      if (areaMap[color]) {
+        areaMap[color].addPoint(x, y);
+      } else {
+        areaMap[color] = new FillArea(color, x, y);
+      }
+    }
+    this.areaMap = areaMap;
+    this.areaList = Object.keys(areaMap).map(key => {
+      return areaMap[key];
+    });
+  }
+
+  update(page?: HTMLImageElement, raw?: HTMLImageElement, map?: HTMLImageElement, colorMap?: ColorMap) {
+    this.raw = raw || this.raw;
+    this.page = page || this.page;
+    this.map = map || this.map;
+    
+    this.invalidate();
+  }
+
+
+  updateing = false;
+  editUpdate() {
+    this.marks = this.innerMarks.filter(m => m.radius > 0)
+      .map(m => {
+      return {x: m.x, y: m.y, radius: m.radius, note: m.note};
+    });
+    // 控制下触发频率
+    if (this.updateing) return;
+    this.updateing = true;
+    setTimeout(() => {
+      this.trigger('edit-update');
+      this.updateing = false;
+    }, 1000);
+  }
+
+
+  get curMarkIdx(): number {
+    return this.getSelectedMarkIdx();
+  }
+  set curMarkIdx(idx: number) {
+    if (idx >= 0 && idx < this.innerMarks.length) {
+      this.selectMarkIdx(idx);
+    }
+  }
+
+  // 获取标注文字
+  getMarkNote(idx: number) {
+    if (idx >= 0 && idx < this.innerMarks.length) {
+      let mark = this.innerMarks[idx];
+      return mark.note;
+    }
+    return null;
+  }
+
+  // 设置标注文字
+  setMarkNote(idx: number, note: string) {
+    if (idx >= 0 && idx < this.innerMarks.length) {
+      this.innerMarks[idx].note = note;
+      this.marks[idx].note = note;
+      this.trigger('edit-update');
+    }
+  }
+
+  /**
+   * Draw this layer.
+   * @Override Layer#draw(ctx)
+   */
+  override draw(ctx) {
+    ctx.imageSmoothingEnabled = this.smoothing;
+
+    ctx.fillStyle = "white";
+    ctx.fillRect(0, 0, this.width, this.height);
+
+    ctx.drawImage(this.output, 0, 0, this.output.width, this.output.height, 0, 0, this.width, this.height);
+
+    if (this.raw) {
+      ctx.drawImage(this.raw, 0, 0, this.page.width, this.page.height, 0, 0, this.width, this.height);
+    } else {
+      ctx.drawImage(this.page, 0, 0, this.page.width, this.page.height, 0, 0, this.width, this.height);
+    }
+
+    this.drawMarks(ctx);
+
+  }
+
+
+  // 生成标注效果图
+  getMarkBlob(): Promise<Blob> {
+    let scale = 2;
+    let canvas = Utils.createCanvas(this.width / scale, this.height / scale);  // 压缩下,变为原来的一半大小即可,节省空间
+    let ctx = canvas.getContext('2d');
+
+    ctx.drawImage(this.output, 0, 0, this.output.width, this.output.height, 0, 0, canvas.width, canvas.height);
+
+    if (this.raw) {
+      ctx.drawImage(this.raw, 0, 0, this.page.width, this.page.height, 0, 0, canvas.width, canvas.height);
+    } else {
+      ctx.drawImage(this.page, 0, 0, this.page.width, this.page.height, 0, 0, canvas.width, canvas.height);
+    }
+
+    this.drawMarksOut(ctx, scale);
+    return new Promise((done, reject) => { canvas.toBlob(b => { done(b) }) });
+  }
+  
+  
+  // 创建圈圈标记
+  createMark(x: number, y: number, radius: number): InnerMark {
+    let mark = {x, y, radius, note: '', status: 0};
+    this.innerMarks.push(mark);
+    this.selectMark(mark);
+    this.invalidate();
+    this.editUpdate();
+    return mark;
+  }
+
+  // 移动圈圈
+  moveMark(mark: InnerMark, x: number, y: number) {
+    mark.x = x;
+    mark.y = y;
+    this.editUpdate();
+    this.invalidate();
+  }
+
+  // 调整圈圈大小
+  resizeMark(mark: InnerMark, radius: number) {
+    mark.radius = radius;
+    this.editUpdate();
+    this.invalidate();
+  }
+
+  // 选中某个圈圈
+  selectMark(mark: InnerMark) {
+    for (let m of this.innerMarks) {
+      if (m == mark) {
+        m.status = 1;
+      } else {
+        m.status = 0;
+      }
+    }
+    this.invalidate();
+  }
+
+  // 选中指定下标的圈圈
+  selectMarkIdx(idx: number) {
+    if (idx >= 0 && idx < this.innerMarks.length) {
+      let mark = this.innerMarks[idx];
+      this.selectMark(mark);
+    }
+  }
+
+  // 全部置为非选中
+  unselectMarks() {
+    this.innerMarks.forEach(m => {
+      m.status = 0;
+    })
+    this.invalidate();
+  }
+
+  // 获取当前选中
+  getSelectedMark(): InnerMark {
+    return this.innerMarks.find(m => m.status == 1)
+  }
+
+  // 获取当前选中圈圈下标
+  getSelectedMarkIdx(): number {
+    return this.innerMarks.findIndex(m => m.status == 1)
+  }
+
+  // 删除指定
+  deleteSelectedMark() {
+    let idx = this.innerMarks.findIndex(m => m.status == 1);
+    if (idx >= 0) {
+      this.innerMarks.splice(idx, 1);
+      this.editUpdate();
+      this.invalidate();
+    }
+  }
+
+  // 判断某点是否落在某个圈圈标记内
+  checkInMarks(x: number, y: number): InnerMark {
+    let dx, dy;
+    for (let mark of this.innerMarks) {
+      dx = mark.x - x;
+      dy = mark.y - y;
+      if (mark.radius * mark.radius >= dx * dx + dy * dy) { // in circle
+        return mark;
+      }
+    }
+    return null;
+  }
+
+  // 判断某点落在哪个象限(正方形选框之内和mark圈圈之外的4个角,右上/左上/左下/右下分别对应1/2/3/4象限,对应方位东北/西北/西南/东南)
+  checkQuadrant(mark: InnerMark, x: number, y: number): number {
+    let dx = mark.x - x
+    let dy = mark.y - y;
+    if (mark.radius * mark.radius >= dx * dx + dy * dy) { // in circle
+      return 0;
+    }
+    let rect = new Rect(mark.x - mark.radius, mark.y - mark.radius, mark.x + mark.radius, mark.y + mark.radius);
+    if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) { // out of square
+      return 0;
+    }
+    if (x > mark.x && y < mark.y) return 1;
+    if (x < mark.x && y < mark.y) return 2;
+    if (x < mark.x && y > mark.y) return 3;
+    if (x > mark.x && y > mark.y) return 4;
+
+    return 0;
+  }
+
+  /**
+   * 绘制标记圈圈
+   * @param ctx 
+   */
+  drawMarks(ctx: CanvasRenderingContext2D) {
+    ctx.save();
+
+    ctx.strokeStyle = '#00cc00';
+    ctx.lineWidth = 8;
+    this.innerMarks.forEach(m => {
+      ctx.beginPath();
+      ctx.arc(m.x, m.y, m.radius, 0, 2 * Math.PI);
+      ctx.stroke();
+      if (m.status == 1) { // 选中状态,画个外框
+        let rect = new Rect(m.x - m.radius, m.y - m.radius, m.x + m.radius, m.y + m.radius);
+        ctx.save();
+        ctx.strokeStyle = 'yellow';
+        ctx.lineWidth = 2;
+        ctx.setLineDash([4, 2]);
+        ctx.strokeRect(rect.left, rect.top, rect.width(), rect.height());
+        ctx.restore();
+      }
+    });
+    ctx.restore();
+  }
+
+  /**
+   * 绘制标记圈圈, 用于导出
+   * @param ctx 
+   * @param scale 压缩比例
+   */
+   drawMarksOut(ctx: CanvasRenderingContext2D, scale = 1) {
+    ctx.save();
+
+    ctx.font = `25px sans-serif`;
+    ctx.fillStyle = 'red';
+    ctx.strokeStyle = '#00cc00';
+    ctx.lineWidth = 8 / scale;
+    this.innerMarks.forEach(m => {
+      ctx.beginPath();
+      ctx.arc(m.x / scale, m.y / scale, m.radius / scale, 0, 2 * Math.PI);
+      let pos = this.getBestTextPos(ctx, m, scale);
+      ctx.fillText(m.note, pos.x, pos.y);
+      ctx.stroke();
+    });
+    ctx.restore();
+  }
+
+
+  // 获取最佳文字排版位置
+  getBestTextPos(ctx: CanvasRenderingContext2D, mark: InnerMark, scale = 1): Point{
+    let text = ctx.measureText(mark.note); // TextMetrics object
+    let width = text.width;
+    let x = Math.max(mark.x / scale - width / 2, 0);
+    if (x + width > ctx.canvas.width) { // 文字超出了
+      x = Math.max(ctx.canvas.width - width, 0);
+    }
+    let y = Math.min(mark.y / scale + 10, this.width / scale);
+    return {x, y};
+  }
+
+}
+
+export interface InnerMark {
+  x: number;
+  y: number;
+  radius: number;
+  note: string; // 文字说明
+  status: number; //0-未选中,1-选中
+}
+
+

+ 155 - 0
zorro/src/app/lib/filler/mark-edit/mark-tool.ts

@@ -0,0 +1,155 @@
+
+import Tool from '../core/tool';
+import MarkEditLayer, { InnerMark } from './mark-edit-layer';
+
+
+export default class MarkTool extends Tool {
+  mark: InnerMark;
+  moving: boolean = false;
+  resizeing: boolean = false;
+  creating: boolean = false;
+
+  lastMouseDownX: number;
+  lastMouseDownY: number;
+
+  markEditLayer: MarkEditLayer;
+
+  constructor(markEditLayer) {
+    super('mark-edit-mark', 'crosshair');
+    this.markEditLayer = markEditLayer;
+  }
+
+
+  override ondblclick(evt: MouseEvent): void {
+    console.log('MarkTool@ondblclick()');
+    let x = evt._contentPoint.x;
+    let y = evt._contentPoint.y;
+    let mark = this.markEditLayer.checkInMarks(x, y);
+    if (mark) {
+      this.markEditLayer.selectMark(mark);
+    } else {
+      this.markEditLayer.createMark(x, y, 40);
+    }    
+  }
+
+  override onclick(evt: MouseEvent): void {
+    console.log('MarkTool@onclick()');
+    let x = evt._contentPoint.x;
+    let y = evt._contentPoint.y;
+    let mark = this.markEditLayer.checkInMarks(x, y);
+    if (mark) {
+      // 鼠标点击落在某个圈圈内,置为选中状态
+      this.markEditLayer.selectMark(mark);
+    } else {
+      // 鼠标点击未落在任何圈圈内,判断是否落当前选中的外接正方形四个边角内,如果没有,全部置为非选中
+      mark = this.markEditLayer.getSelectedMark();
+      if (mark) {
+        if (this.markEditLayer.checkQuadrant(mark, x, y) == 0) {
+          this.markEditLayer.unselectMarks();
+        }
+      }
+    }
+  }
+
+  override onmousedown(evt: MouseEvent) {
+    console.log('MarkTool@onmousedown()');
+    this.lastMouseDownX = evt._contentPoint.x;
+    this.lastMouseDownY = evt._contentPoint.y;
+  }
+
+  override onmouseup(evt: MouseEvent) { console.log('MarkTool@onmouseup()'); }
+
+  override onmousemove(evt: MouseEvent): void {
+    // console.log('MarkTool@onmousemove()');
+    let x = evt._contentPoint.x;
+    let y = evt._contentPoint.y;
+    let mark = this.markEditLayer.checkInMarks(x, y);
+    if (mark) {
+      this.cursor = 'move';
+    } else {
+      this.cursor = 'crosshair';
+    }
+    mark = this.markEditLayer.getSelectedMark();
+    if (mark) {
+      let quadrant = this.markEditLayer.checkQuadrant(mark, x, y);
+      if (quadrant == 1) this.cursor = 'ne-resize';
+      if (quadrant == 2) this.cursor = 'nw-resize';
+      if (quadrant == 3) this.cursor = 'sw-resize';
+      if (quadrant == 4) this.cursor = 'se-resize';
+    }
+
+    this.editor.updateCursor();
+  }
+
+  
+  override onDragStart(evt: MouseEvent) {
+    console.log('MarkTool@onDragStart()');
+    let x = evt._contentPoint.x;
+    let y = evt._contentPoint.y;
+    // dragstart的时候,坐标已经偏移,用最早mounsedown的坐标
+    let mark = this.markEditLayer.checkInMarks(this.lastMouseDownX, this.lastMouseDownY);
+    if (mark) { // 起始落点在某个圈圈内,这是要移动
+      this.markEditLayer.selectMark(mark);
+      this.mark = mark;
+      this.moving = true;
+      console.log("start moving...");
+    } else {
+      mark = this.markEditLayer.getSelectedMark();
+      if (mark) {
+        // dragstart的时候,坐标已经偏移,用最早mounsedown的坐标
+        let quadrant = this.markEditLayer.checkQuadrant(mark, this.lastMouseDownX, this.lastMouseDownY);
+        if (quadrant > 0) { // 起始落点是在选中圈圈的外接正方形的四个角落区域,那么标识要拖动调整大小
+          this.mark = mark;
+          this.resizeing = true;
+          console.log("start resizeing...");
+        }
+      }
+    }
+
+    // 以上情况都不是,那么表示拖动生成一个新的圈圈
+    if (!this.mark) {
+      this.creating = true;
+      this.mark = { x: this.lastMouseDownX, y: this.lastMouseDownY, radius: 0, note: '', status: 0};
+      console.log("start creating...");
+    }
+    
+  }
+
+  override onDragMove(evt: MouseEvent) {
+    console.log('ondragmove......');
+    let x = evt._contentPoint.x;
+    let y = evt._contentPoint.y;
+    if (this.moving && this.mark) { // move
+      this.markEditLayer.moveMark(this.mark, x, y);
+    } else if (this.resizeing && this.mark) { // resize
+      let dx = this.mark.x - x
+      let dy = this.mark.y - y;
+      let radius = Math.floor(Math.sqrt((dx * dx) + (dy * dy)));
+      console.log("mark resize to " + radius);
+      this.markEditLayer.resizeMark(this.mark, radius);
+    } else if (this.creating && this.mark) {
+      let dx = this.mark.x - x
+      let dy = this.mark.y - y;
+      let radius = Math.floor(Math.sqrt((dx * dx) + (dy * dy)));
+      if (radius) {
+        if (this.mark.radius == 0) {
+          this.mark = this.markEditLayer.createMark(this.mark.x, this.mark.y, radius);
+        } else {
+          this.markEditLayer.resizeMark(this.mark, radius);
+        }
+      }
+      console.log("creating");
+    }
+
+
+  }
+
+  override onDragEnd(evt: MouseEvent) {
+    this.moving = false;
+    this.resizeing = false;
+    this.creating = false;
+    this.mark = null;
+  }
+
+
+}

+ 89 - 0
zorro/src/app/lib/filler/number-edit/number-bucket-tool.ts

@@ -0,0 +1,89 @@
+
+import NumberEditLayer from './number-edit-layer';
+import NumberEditTool from './number-edit-tool';
+import { FillTask, FILL_TYPE } from '../common/filltask';
+
+export default class BucketTool extends NumberEditTool {
+  fastfill: boolean;
+
+  constructor(numberEditLayer) {
+    super(numberEditLayer, 'number-edit-bucket', 'url("/static/cursor/cursor-fill-color.png") 24 24, default');
+    this.setProp('fastfill', true);
+  }
+
+  isFastfillEnabled() { return this.getProp('fastfill'); }
+
+  isFastfilled() { }
+
+  override onclick(evt: MouseEvent) {
+    if (!this.numberEditLayer) return;
+    if (this.fastfill) return;
+
+    console.log('BucketTool@onclick');
+    let task = new FillTask(FILL_TYPE.SOLID);
+    // let task = new FillTask(FillTask.FILL_TYPE_LINEAR_GRADIENT);
+    // let task = new FillTask(FillTask.FILL_TYPE_RADIAL_GRADIENT);
+    task.color = this.numberEditLayer.color;
+    //task.color = Utils.randomColor();
+    //task.textureImage = this.getProp('textureImage');
+    //task.textureImage = null;
+    let pp = this.numberEditLayer.translate(evt._contentPoint);
+    task.x = pp.x;
+    task.y = pp.y;
+    this.numberEditLayer.fill(task);
+  }
+
+  override onmousedown(evt: MouseEvent) {
+    console.log('BucketTool@onmousedown()');
+    if (!this.numberEditLayer)
+      return;
+    this.fastfill = false;
+  }
+
+  override onmouseup(evt: MouseEvent) { console.log('BucketTool@onmouseup()'); }
+
+  override onDragStart(evt: MouseEvent) {
+    if (!this.numberEditLayer)
+      return;
+    console.log('BucketTool@onDragStart()');
+    if (!this.isFastfillEnabled())
+      return;
+
+    this.fastfill = true;
+
+    let task = new FillTask(FILL_TYPE.SOLID);
+    task.color = this.numberEditLayer.color;
+    let pp = this.numberEditLayer.translate(evt._contentPoint);
+    task.x = pp.x;
+    task.y = pp.y;
+    this.numberEditLayer.fastfillStart(task);
+  }
+
+  override onDragMove(evt: MouseEvent) {
+    console.log('ondragmove......');
+    if (!this.numberEditLayer)
+      return;
+    console.log('BucketTool@onDragMove()');
+    if (!this.isFastfillEnabled())
+      return;
+    let pp = this.numberEditLayer.translate(evt._contentPoint);
+    this.numberEditLayer.fastfillAddPoint(pp.x, pp.y);
+  }
+
+  override onDragEnd(evt: MouseEvent) {
+    if (!this.numberEditLayer)
+      return;
+    if (!this.isFastfillEnabled())
+      return;
+    console.log('BucketTool@onDragEnd()');
+    this.numberEditLayer.fastfillEnd();
+  }
+
+
+  override onSelect(): void {
+    if (!this.numberEditLayer) return;
+    this.numberEditLayer.showUnorderMask = false;
+    this.numberEditLayer.invalidate();
+  }
+
+}

+ 1672 - 0
zorro/src/app/lib/filler/number-edit/number-edit-layer.ts

@@ -0,0 +1,1672 @@
+import Animator from '../common/animator';
+import Easing from '../common/easing';
+import Repeater from '../common/repeater';
+import Utils from '../common/utils';
+import ColorPickerTool from '../core/color-picker-tool';
+import Layer from '../core/layer';
+import Rect from '../core/rect';
+import Tool from '../core/tool';
+import BucketTool from './number-bucket-tool';
+import EraseTool from './number-erase-tool';
+
+import FillArea from '../common/fillarea';
+import { FillTask, FILL_TYPE } from '../common/filltask';
+import { Centers, ColorInfo, ColorMap, ColorSumItem } from '../common/interfaces';
+import NumberEditTool from './number-edit-tool';
+import { Lab, sRGB } from '../color/color';
+import { generateOrderAuto, getColorOrder2, validateOrder } from '../common/color-order';
+import NumberPlayTool from './number-play-tool';
+import NumberSelectTool from './number-select-tool';
+import { isThisISOWeek } from 'date-fns';
+import { ThisReceiver } from '@angular/compiler';
+import { NzMessageService } from 'ng-zorro-antd/message';
+
+interface ToolHash {
+  [toolKey: string]: NumberEditTool;
+}
+
+//interface NumberEditLayerConfig {
+//  color?: string, //current color
+//}
+
+//const _defaults: NumberEditLayerConfig = {
+//  color: '#006600'
+//}
+
+const TAG = 'NumberEditLayer';
+
+export default class NumberEditLayer extends Layer {
+  message: NzMessageService;
+  output: HTMLCanvasElement;
+  outputCtx: CanvasRenderingContext2D;
+  areaOutput: HTMLCanvasElement;
+  areaOutputCtx: CanvasRenderingContext2D;
+  areaMask: HTMLCanvasElement;
+  areaMaskCtx: CanvasRenderingContext2D;
+  fastfillOutput: HTMLCanvasElement;
+  fastfillCtx: CanvasRenderingContext2D;
+  fasteraseOutput: HTMLCanvasElement;
+  fasteraseCtx: CanvasRenderingContext2D;
+  areaPixels: ImageData;
+  animator: Animator;
+  aniMask: HTMLCanvasElement;
+  aniMaskCtx: CanvasRenderingContext2D;
+  aniMaskImage: HTMLImageElement;
+  aniMaskCanvas: HTMLCanvasElement;
+  play: HTMLCanvasElement;
+  playCtx: CanvasRenderingContext2D;
+  playIdx: number;
+  unorderMask: HTMLCanvasElement;
+  unorderMaskCtx: CanvasRenderingContext2D;
+  // 编排模式下,不展示未排序颜色
+  private _showUnorderMask: boolean = false;
+  public get showUnorderMask(): boolean {
+    return this._showUnorderMask;
+  }
+  public set showUnorderMask(value: boolean) {
+    this._showUnorderMask = value;
+  }
+  mapData: ImageData;
+  map: HTMLImageElement;
+  scale: number;
+  mapPixels: Uint32Array;
+  undoList: any[] = [];
+  redoList: any[] = [];
+  cacheList: any[] = [];
+  areaMap: any = [];
+  areaList: any[];
+  aniTask: any;
+  aniRadius: number;
+  fastfillTask: FillTask;
+  fastfillAreas: {};
+  fasteraseTask: FillTask;
+  fasteraseAreas: {};
+  brushTask: any;
+  brushPendingPoints: any;
+  brushBrush: any;
+  brushLastPoint: any;
+  brushDist: number;
+  label: HTMLImageElement;
+
+  //tools: Tool[] = []; //layer支持的所有工具列表
+
+  //public config: NumberEditLayerConfig = { ..._defaults }
+  colorMap: ColorMap;
+  colorOrder: number[] = [];
+  centers: Centers; // 中心点
+  page: HTMLImageElement;
+  raw: HTMLImageElement;
+
+  uncoloredAreas: FillArea[];
+  colorSum: ColorSumItem[] = [];
+  ordered: ColorSumItem[] = [];
+  unordered: ColorSumItem[] = [];
+  progress: any = {};
+
+  private _canAuto: boolean = false;  // 是否可以展示自动排序按钮==>取决于中心点是否预备完毕
+  public get canAuto(): boolean {
+    return this._canAuto;
+  }
+  public set canAuto(value: boolean) {
+    this._canAuto = value;
+  }
+
+
+  override get defaultToolKey(): string { return 'pan'; }
+
+  private _color: string;
+  private _colorInt: number;
+
+  public get colorInt(): number { return this._colorInt; }
+  public set colorInt(value: number) {
+    this._colorInt = value;
+    this._color = Utils.getColorFromInteger(this._colorInt);
+  }
+
+  public get color(): string { return this._color; }
+  public set color(value: string) {
+    this._color = value;
+    this._colorInt = Utils.getColorInteger(this._color);
+  }
+
+  private _hideLine = false;
+  public get hideLine() { return this._hideLine; }
+  public set hideLine(value) { this._hideLine = value; this.invalidate() }
+
+  private _mystery = false;
+  public get mystery() { return this._mystery; }
+  public set mystery(value) { this._mystery = value; this.invalidate() }
+
+  /**
+   * @param page original image.
+   * @param map map image.
+   * @param output  output image.
+   */
+  constructor(message: NzMessageService, page: HTMLImageElement, map: HTMLImageElement, colorMap: any,
+    colorOrder: number[], centers: Centers, raw?: HTMLImageElement) {
+    super(page.width, page.height);
+
+    this.message = message;
+
+    this.colorMap = colorMap || {};
+    this.colorOrder = colorOrder?.filter((item, index, arr) => arr.indexOf(item, 0) == index);  // 去重
+
+    this.centers = centers;
+    this.page = page;
+    this.map = map;
+    this.raw = raw;
+
+    this.init();
+
+    this.color = '#aa0000';
+
+    this.addTool(new NumberSelectTool(this));
+    this.addTool(new NumberPlayTool(this));
+    this.addTool(new ColorPickerTool());
+    this.addTool(new BucketTool(this));
+    this.addTool(new EraseTool(this));
+
+  }
+
+  init() {
+
+    this.output = Utils.createCanvas(this.width, this.height);
+    this.outputCtx = this.output.getContext('2d');
+    this.outputCtx.fillStyle = 'white';
+    this.outputCtx.fillRect(0, 0, this.width, this.height);
+
+    this.scale = this.map.width / this.page.width;
+    this.mapData = Utils.getImageData(this.map);
+    this.mapPixels = new Uint32Array(this.mapData.data.buffer);
+    this.createAreaMap();
+
+    this.drawToOutput();
+
+    // undo redo support.
+    this.undoList = [];
+    this.redoList = [];
+    this.cacheList = [];
+
+    console.log('FillerLayer@width=', this.width, ' height=', this.height,
+      ' scale=', this.scale);
+
+
+    this.areaOutput = Utils.createCanvas(this.width, this.height);
+    this.areaOutputCtx = this.areaOutput.getContext('2d');
+    this.areaMask = Utils.createCanvas(this.width, this.height);
+    this.areaMaskCtx = this.areaMask.getContext('2d');
+
+    this.fastfillOutput = Utils.createCanvas(this.width, this.height);
+    this.fastfillCtx = this.fastfillOutput.getContext('2d');
+
+    this.fasteraseOutput = Utils.createCanvas(this.width, this.height);
+    this.fasteraseCtx = this.fasteraseOutput.getContext("2d");
+
+    this.play = Utils.createCanvas(this.width, this.height);
+    this.playCtx = this.play.getContext('2d');
+    this.playCtx.fillStyle = 'white';
+    this.playCtx.fillRect(0, 0, this.width, this.height);
+    this.playIdx = 0;
+
+    this.unorderMask = Utils.createCanvas(this.width, this.height);
+    this.unorderMaskCtx = this.unorderMask.getContext('2d');
+    // this.showUnorderMask = false;
+
+    this.areaPixels =
+      this.areaOutputCtx.createImageData(this.width, this.height);
+
+
+    // For animation
+    this.animator = new Animator(0, 1);
+    this.animator.setEasing(Easing.easeInQuad);
+    this.animator.setDuration(1000);
+
+    this.animator.on('animationUpdate', animator => {
+      this.onAnimationUpdate(animator)
+    });
+
+    this.aniMask = Utils.createCanvas(this.width, this.height);
+    this.aniMaskCtx = this.aniMask.getContext('2d');
+
+    // map图修改后可能导致colorMap发生变化, 在此重新刷新colorMap
+    let colorMapChange = this.validateColorMap();
+
+    // 没有colorOrder,算一个默认的colorOrder出来
+    if (!this.colorOrder) {
+      let orderAuto = generateOrderAuto(this.colorMap, this.centers);
+      this.colorOrder = getColorOrder2(this.colorMap, this.centers, this.colorOrder, orderAuto);
+      if (!this.colorOrder) { // 获取不到可用order,可能是没有生成中心点,这个时候根据colorMap出现的颜色顺序定个order
+        this.colorOrder = Array.from(new Set(Object.values(this.colorMap).map((ci: ColorInfo) => ci.color)));
+      }
+    }
+    let colorOrderChange = this.updateColorOrder();
+
+    this.uncoloredAreas = this.areaList.filter(area => !this.colorMap[area.color])
+      .sort((a: FillArea, b: FillArea) => {
+        return a.count - b.count;
+      });
+    this.colorSum = this.getColorSum();
+    this.updateOutputList();
+    this.updateUnorderMask(true);
+    this.progress = this.getProgress();
+
+    if (colorMapChange || colorOrderChange) {
+      console.log("number-edit-layer init colorMap or colorOrder change!");
+      this.editUpdate();
+    }
+
+    // 尚未生成中心点, 启动web worker计算中心点
+    if (!this.centers || Object.keys(this.centers).length <= 0) {
+      this.canAuto = false;
+      this.startWorker();
+    } else {
+      this.canAuto = true;
+    }
+  }
+
+  /**
+   * 启动webworker执行计算中心点的耗时任务
+   */
+  private startWorker() {
+    if (typeof Worker === 'undefined') return;
+    try {
+      const worker = new Worker(new URL('../common/centers.worker.ts', import.meta.url));
+      console.log("create centers webworker!!!");
+      worker.onmessage = (result) => {
+        console.log("get worker result:" + JSON.stringify(result.data));
+        this.centers = result.data;
+        this.canAuto = true;
+        this.editUpdate(); // 更新下中心点
+        worker.terminate();
+      };
+      worker.onerror = function (event) {
+        console.error("webworker error:" + event.message);
+        worker.terminate();
+      };
+      let pagePixels = new Uint32Array(Utils.getImageData(this.page).data.buffer);
+      let mapPixels = new Uint32Array(Utils.getImageData(this.map).data.buffer);
+      worker.postMessage({ pagePixels: pagePixels, mapPixels: mapPixels, width: this.width, height: this.height });
+    } catch (e) {
+      console.log(e.message);
+    }
+  }
+
+  /**
+   * 更新map
+   * @param map 
+   * @param triggerUpdate 初始化加载为false,如果是修改map, 则为true
+   */
+  setMap(map: HTMLImageElement) {
+    this.map = map;
+    this.editUpdate();
+    this.invalidate();
+  }
+
+  /**
+   * 
+   * @param page 
+   * @param map 
+   * @param colorMap 
+   * @param raw 
+   */
+  update(page?: HTMLImageElement, map?: HTMLImageElement, colorMap?: any,
+    colorOrder?: number[], raw?: HTMLImageElement, centers?: Centers) {
+    this.page = page || this.page;
+    this.map = map || this.map;
+    this.colorMap = colorMap || this.colorMap;
+    this.colorOrder = colorOrder || this.colorOrder;
+    this.raw = raw || this.raw;
+    this.centers = centers || this.centers;
+    if (map && !centers) { // map图有变更, 重新计算中心点
+      this.centers = null;
+    }
+    this.init();
+
+    this.editUpdate();
+    this.invalidate();
+  }
+
+  updateColorMap(colorMap) {
+    this.backupOutput();
+    this.colorMap = colorMap;
+    // todo : ajust colorOrder after color merge
+    this.updateColorOrder()
+    this.drawToOutput();
+    this.updateUnorderMask();
+    this.editUpdate();
+    this.invalidate();
+  }
+
+  drawToOutput() {
+    console.time('drawToOutput');
+    this.outputCtx.fillStyle = 'white';
+    this.outputCtx.fillRect(0, 0, this.width, this.height);
+
+    //fast create output by map
+    let outData = this.outputCtx.getImageData(0, 0, this.output.width, this.output.height)
+    let outPixels = new Uint32Array(outData.data.buffer);
+    let areaIndex;
+    for (var i = 0; i < outPixels.length; i++) {
+      areaIndex = this.mapPixels[i];
+      if (this.colorMap[areaIndex]) {
+        outPixels[i] = this.colorMap[areaIndex].color;
+      }
+    }
+    this.outputCtx.putImageData(outData, 0, 0);
+    console.timeEnd('drawToOutput');
+  }
+
+
+  lastUnorderColorMap: ColorMap = {};
+  /**
+   * 刷新unorderMask,注意性能
+   * 经过优化,可放心调用
+   * @param force: 强制全局刷新
+   */
+  updateUnorderMask(force: boolean = false) {
+    console.time('updateUnorderMask');
+
+    //获取未排序部分的colorMap
+    let curUorderColorMap: ColorMap = {};
+    Object.keys(this.colorMap).forEach(areaIndex => {
+      if (this.colorOrder.indexOf(this.colorMap[areaIndex].color) < 0) {
+        curUorderColorMap[areaIndex] = this.colorMap[areaIndex];
+      }
+    })
+
+    // 比较当前curUorderColorMap和lastUnorderColorMap的差异
+    let areaIdxSet = new Set(Object.keys(curUorderColorMap).concat(Object.keys(this.lastUnorderColorMap)));
+    let diffColorMap: ColorMap = {};
+    areaIdxSet.forEach(idx => {
+      if (!curUorderColorMap[idx]) {
+        diffColorMap[idx] = this.lastUnorderColorMap[idx];
+      }
+      if (!this.lastUnorderColorMap[idx]) {
+        diffColorMap[idx] = curUorderColorMap[idx];
+      }
+    })
+    let colors = new Set(Object.values(diffColorMap).map(e => e.color));  // 发生变化的颜色
+    let updatemode = 2;
+
+    if (Object.keys(diffColorMap).length <= 0) {
+      console.log("nothing change, no need to update");
+      updatemode = 0;
+    } else {
+      if (colors.size <= 1) {
+        console.log("only one color change, part update by area! area count: " + Object.keys(diffColorMap).length);
+        updatemode = 1;
+      } else {
+        console.log("fast update by map");
+        updatemode = 2;
+      }
+    }
+
+    if (force) {
+      updatemode = 2;
+    }
+
+    if (updatemode == 1) {
+      let areas = Object.keys(diffColorMap).map(idx => this.areaMap[idx]);
+      let ai, len, aw, i, x, y;
+      let areaIndex;
+      let outData = this.unorderMaskCtx.getImageData(0, 0, this.unorderMask.width, this.unorderMask.height)
+      let outPixels = new Uint32Array(outData.data.buffer);
+      areas.forEach(area => {
+        if (!area) return;  //加上这一句,否则可能会挂掉。在mapEdit做了区块合并后,area可能不存在了 
+        aw = area.width();
+        len = area.width() * area.height();
+        for (ai = 0; ai < len; ai++) {
+          x = area.left + ai % aw;
+          y = area.top + Math.floor(ai / aw);
+          i = y * this.width + x;
+          areaIndex = this.mapPixels[i];
+          if (areaIndex == area.color) {
+            if (curUorderColorMap[areaIndex]) {
+              outPixels[i] = this.colorMap[areaIndex].color;
+            } else {
+              outPixels[i] = 0;
+            }
+          }
+        }
+      })
+      this.unorderMaskCtx.putImageData(outData, 0, 0);
+
+    } else if (updatemode == 2) {  //fast create unordermask by map
+      this.unorderMaskCtx.clearRect(0, 0, this.unorderMask.width, this.unorderMask.height);
+      let outData = this.unorderMaskCtx.getImageData(0, 0, this.unorderMask.width, this.unorderMask.height)
+      let outPixels = new Uint32Array(outData.data.buffer);
+      let areaIndex;
+      for (var i = 0; i < outPixels.length; i++) {
+        areaIndex = this.mapPixels[i];
+        if (curUorderColorMap[areaIndex]) {
+          outPixels[i] = this.colorMap[areaIndex].color;
+        }
+      }
+      this.unorderMaskCtx.putImageData(outData, 0, 0);
+    }
+
+    this.lastUnorderColorMap = curUorderColorMap;
+
+    console.timeEnd('updateUnorderMask');
+  }
+
+  validateColorMap() {
+    let change = false;
+    Object.keys(this.colorMap).forEach(key => {
+      if (!this.areaMap[key]) {
+        console.warn(`area ${key} not exists!`)
+        delete this.colorMap[key];
+        change = true;
+      }
+    })
+    return change;
+  }
+
+  // 刷新colorOrder
+  updateColorOrder(): boolean {
+    let ret = false;
+
+    this.colorOrder = this.colorOrder?.filter((item, index, arr) => arr.indexOf(item, 0) == index);  // 去重
+
+    // 剔除不存在的颜色值
+    let colorSet = new Set(Object.values(this.colorMap).map((ci: ColorInfo) => ci.color));
+    for (let i = 0; i < this.colorOrder.length; i++) {
+      if (!colorSet.has(this.colorOrder[i])) {
+        this.colorOrder.splice(i, 1);
+        i--; // 避免漏掉
+        ret = true;
+      }
+    }
+    return ret;
+  }
+
+
+  /**
+   * Translate point.
+   * Because our cordinate system is based on map, so
+   * we don't need to translate.
+   */
+  translate(p) {
+    return p;
+    /*
+    let pp = {};
+    pp.x = p.x * this.scale;
+    pp.y = p.y * this.scale;
+    return pp;
+    */
+  }
+
+  /**
+   * redraw filler layer.
+   */
+  override invalidate() {
+    this.trigger('invalidate');
+  }
+
+  //更新对外展示的信息
+  updateOutputList() {
+    let colorSum = this.getColorSum();
+
+    this.ordered = this.colorOrder.map(colorInt => {
+      let index = colorSum.findIndex(sumItem => sumItem.color == colorInt);
+      let [item] = colorSum.splice(index, 1);
+      return item;
+    });
+    this.unordered = colorSum;
+  }
+
+
+  editUpdate() {
+    this.uncoloredAreas = this.areaList.filter(area => !this.colorMap[area.color])
+      .sort((a: FillArea, b: FillArea) => {
+        return a.count - b.count;
+      });
+    this.colorSum = this.getColorSum();
+    this.updateOutputList();
+    this.progress = this.getProgress();
+    //等待动画执行完成
+    setTimeout(() => {
+      this.trigger('edit-update');
+    }, 1000);
+  }
+
+  /**
+   * Create area map from map pixels.
+   */
+  createAreaMap() {
+    let width = this.width;
+    let height = this.height;
+    let floodArr = this.mapPixels;
+    let areaMap = {};
+    let i: number, x: number, y: number, color: number;
+    for (i = 0; i < floodArr.length; i++) {
+      color = floodArr[i];
+      if (!color) continue;
+      x = i % width;
+      //y = parseInt(i / width);
+      y = Math.floor(i / width);
+      if (areaMap[color]) {
+        areaMap[color].addPoint(x, y);
+      } else {
+        areaMap[color] = new FillArea(color, x, y);
+      }
+    }
+    this.areaMap = areaMap;
+    this.areaList = Object.keys(areaMap).map(key => {
+      return areaMap[key];
+    });
+  }
+
+  /**
+   * Get area by x, y
+   */
+  getArea(x, y) {
+    x = parseInt(x);
+    y = parseInt(y);
+    if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
+      return null;
+    }
+    let color = this.mapPixels[y * this.width + x];
+    return this.areaMap[color];
+  }
+
+  /**
+   * Get area by color(areaIndex)
+   */
+  getAreaByColor(color) {
+    return this.areaMap[color];
+  }
+
+  /**
+   * Preapare area for filling.
+   */
+  prepareArea(area, animation) {
+    //console.log('FillerLayer@prepareArea#area', area);
+    this.areaOutputCtx.clearRect(0, 0, this.width, this.height);
+    this.areaMaskCtx.clearRect(0, 0, this.width, this.height);
+    // TODO reuse area data.
+    let areaData =
+      this.areaMaskCtx.createImageData(area.width(), area.height());
+    let areaPixels = new Uint32Array(areaData.data.buffer);
+    let ai, ax, ay, aw, i, x, y;
+    aw = area.width();
+    for (ai = 0; ai < areaPixels.length; ai++) {
+      x = area.left + ai % aw;
+      y = area.top + Math.floor(ai / aw);
+      i = y * this.width + x;
+      if (this.mapPixels[i] == area.color) {
+        areaPixels[ai] = this.mapPixels[i];
+      } else {
+        areaPixels[ai] = 0;
+      }
+    }
+    this.areaMaskCtx.putImageData(areaData, area.left, area.top);
+
+    if (animation) {
+      this.aniMaskCtx.save();
+      this.aniMaskCtx.fillStyle = 'white';
+      this.aniMaskCtx.fillRect(0, 0, this.aniMask.width, this.aniMask.height);
+      this.aniMaskCtx.drawImage(this.output, area.left, area.top, area.width(),
+        area.height(), area.left, area.top,
+        area.width(), area.height());
+      this.aniMaskCtx.globalCompositeOperation = 'destination-in';
+      this.aniMaskCtx.drawImage(this.areaMask, area.left, area.top,
+        area.width(), area.height(), area.left,
+        area.top, area.width(), area.height());
+      this.aniMaskCtx.restore();
+    }
+  }
+
+  pickColor(p) {
+    let area = this.getArea(p.x, p.y);
+    if (!area) return null;
+    let colorInfo = this.colorMap[area.color];
+    if (!colorInfo) return null;
+    console.log('fillerlayer.pickColor: ', colorInfo);
+    return colorInfo.cssColor;
+  }
+
+  /**
+   * Fill task.
+   */
+  fill(task) {
+    console.log('FillerLayer@task', task);
+    if (!task) return;
+
+    // check 是否有颜色增加
+    let colorInt = Utils.getColorInteger(task.color);
+    let item = this.colorSum.find(e => e.color == colorInt);
+    if (!item) { // 有颜色增加, 是否超过199个颜色
+      if (this.height <= 2000 && this.colorSum.length >= 199) {
+        this.message.warning($localize`颜色数超过199, 无法新增新颜色!`);
+        return;
+      }
+      if (this.height > 2000 && this.colorSum.length >= 299) {
+        this.message.warning($localize`颜色数超过299, 无法新增新颜色!`);
+        return;
+      }
+    } else { // 无颜色增加,单个颜色是否超过50
+      if (item.total >= 50) {
+        this.message.warning($localize`单个颜色超过50!`);
+        return;
+      }
+    }
+
+    task.area = this.getArea(task.x, task.y);
+    console.log('FillerLayer@task.area', task.area);
+    if (!task.area)
+      return;
+
+    this.backupOutput();
+
+    task.areaIndex = task.area.color;
+    this._bucketFill(task, true);
+    let area = task.area;
+    let al = area.left;
+    let at = area.top;
+    let aw = area.width();
+    let ah = area.height();
+    // Put area output to output
+    this.outputCtx.drawImage(this.areaOutput, al, at, aw, ah, al, at, aw, ah);
+    this.invalidate();
+  }
+
+  erase(task) {
+    console.log('FillerLayer@task', task);
+    if (!task)
+      return;
+    task.area = this.getArea(task.x, task.y);
+    console.log('FillerLayer@task.area', task.area);
+    if (!task.area)
+      return;
+
+    if (!this.colorMap[task.area.color]) {
+      //area not colored.
+      return;
+    }
+
+    this.backupOutput();
+
+    //set area not colored.
+    //console.log('colorMap', this.colorMap);
+    delete this.colorMap[task.area.color];
+    this.updateColorOrder();
+    this.updateUnorderMask();
+    //console.log('colorMap', this.colorMap);
+
+    task.areaIndex = task.area.color;
+
+    this.prepareArea(task.area, false);
+
+    let area = task.area;
+    let al = area.left;
+    let at = area.top;
+    let aw = area.width();
+    let ah = area.height();
+
+    this.outputCtx.save();
+    this.outputCtx.globalCompositeOperation = 'destination-out'
+    this.outputCtx.drawImage(this.areaMask, al, at, aw, ah, al, at, aw, ah);
+    this.outputCtx.restore();
+
+    this.invalidate();
+    this.editUpdate();
+  }
+
+
+  /**
+   * 擦出所选区域的颜色
+   * @param {FillArea} area 
+   */
+  eraseArea(area) {
+    if (!area) return;
+
+    if (!this.colorMap[area.color]) {
+      //area not colored.
+      return;
+    }
+
+    //set area not colored.
+    // console.log('colorMap', this.colorMap);
+    delete this.colorMap[area.color];
+
+    this.prepareArea(area, false);
+
+    let al = area.left;
+    let at = area.top;
+    let aw = area.width();
+    let ah = area.height();
+
+    this.outputCtx.save();
+    this.outputCtx.globalCompositeOperation = 'destination-out'
+    this.outputCtx.drawImage(this.areaMask, al, at, aw, ah, al, at, aw, ah);
+    this.outputCtx.restore();
+  }
+
+
+  /**
+   * 根据css颜色获取所选区域
+   */
+  getAreasByColor(cssColor) {
+    let colorInt = Utils.getColorInteger(cssColor);
+    let areas = Object.keys(this.colorMap).map(index => {
+      let colorInfo = this.colorMap[index];
+      if (colorInfo.color == colorInt) {
+        return index
+      } else {
+        return null
+      }
+    }).filter(index => {
+      return index;
+    }).map(index => {
+      return this.areaMap[index];
+    }).filter(area => {
+      return area;
+    });
+    return areas;
+  }
+
+  /**
+   * 擦除所选颜色
+   * @param {String} cssColor 
+   */
+  eraseByColor(cssColor) {
+    let areas = this.getAreasByColor(cssColor);
+    console.log('erase areas count: ', areas.length);
+    this.backupOutput();
+    areas.forEach(area => {
+      this.eraseArea(area);
+    })
+    this.updateColorOrder();
+    this.updateUnorderMask();
+    this.invalidate();
+    this.editUpdate();
+  }
+
+
+  // 重新上色清空所有
+  eraseAll() {
+    this.backupOutput();
+    this.colorMap = {};
+    this.colorOrder = [];
+    this.outputCtx.clearRect(0, 0, this.output.width, this.output.height);
+    this.unorderMaskCtx.clearRect(0, 0, this.unorderMask.width, this.unorderMask.height);
+    this.invalidate();
+    this.editUpdate();
+  }
+
+
+  /**
+   * Bucket fill implementation.
+   */
+  _bucketFill(task, animation) {
+    console.log('animation:', animation);
+
+    this.prepareArea(task.area, animation);
+
+    //console.log('bucket fill:', task)
+    let colorIndex = Utils.getColorInteger(task.color);
+    task.area.setColored(colorIndex, task.color);
+    this.colorMap[task.area.color] = {
+      color: colorIndex,
+      cssColor: task.color,
+    };
+    // 可能有颜色增加
+    if (!this.colorOrder.includes(colorIndex)) {
+      this.colorOrder.push(colorIndex);
+    }
+    // 也可能有颜色减少
+    this.updateColorOrder();
+    this.updateUnorderMask();
+
+    //console.log('colorMap:', this.colorMap);
+
+
+    switch (task.type) {
+      default:
+        this.fillSolid(task);
+    }
+
+    // Cut area output with mask.
+    this.areaOutputCtx.save();
+    this.areaOutputCtx.globalCompositeOperation = 'destination-in';
+    this.areaOutputCtx.drawImage(this.areaMask, 0, 0);
+    this.areaOutputCtx.restore();
+
+    if (this.animator && this.animator.isRunning()) {
+      this.animator.cancel();
+      this.aniTask = null;
+    }
+
+    if (this.animator) {
+      this.aniTask = task;
+      this.aniRadius = this.getAniRadius(task.area, task.x, task.y);
+      let duration = Math.max(task.area.width(), task.area.height()) / 3;
+      console.log('duration', duration);
+      if (duration > 300)
+        duration = 300;
+      if (duration < 150)
+        duration = 150;
+      this.animator.set(0.1, 1).setDuration(duration).start();
+    }
+
+    this.editUpdate();
+
+  }
+
+  /**
+   * Fill solid
+   */
+  fillSolid(task) {
+    let ctx = this.areaOutputCtx;
+    let area = task.area;
+    ctx.save();
+    if (task.textureImage) {
+      ctx.fillStyle = ctx.createPattern(task.textureImage, 'repeat');
+      ctx.fillRect(area.left, area.top, area.width(), area.height());
+      ctx.globalCompositeOperation = 'overlay';
+      ctx.fillStyle = task.color;
+      ctx.fillRect(area.left, area.top, area.width(), area.height());
+    } else {
+      ctx.fillStyle = task.color;
+      ctx.fillRect(area.left, area.top, area.width(), area.height());
+    }
+    ctx.restore();
+  }
+
+
+
+  getAniRadius(area, x, y) {
+    return Math.max(
+      Math.sqrt(Math.pow(area.left - x, 2) + Math.pow(area.top - y, 2)),
+      Math.sqrt(Math.pow(area.right - x, 2) + Math.pow(area.top - y, 2)),
+      Math.sqrt(Math.pow(area.left - x, 2) + Math.pow(area.bottom - y, 2)),
+      Math.sqrt(Math.pow(area.right - x, 2) + Math.pow(area.bottom - y, 2)));
+  }
+
+  /**
+   * Update animation mask by progress.
+   */
+  updateAniMask(progress) {
+    if (!this.aniTask)
+      return;
+    let task = this.aniTask;
+    let area = task.area;
+    let radius = this.aniRadius * progress;
+    let ctx = this.aniMaskCtx;
+    ctx.save();
+    ctx.globalCompositeOperation = 'destination-out';
+
+    // if (Utils.isLoaded(this.aniMaskImage)) {
+    if (this.aniMaskCanvas) {
+      let l = task.x - radius;
+      let t = task.y - radius;
+      ctx.drawImage(this.aniMaskCanvas, 0, 0, this.aniMaskCanvas.width,
+        this.aniMaskCanvas.height, l, t, radius * 2, radius * 2);
+
+    } else {
+      ctx.beginPath();
+      ctx.arc(task.x, task.y, radius, 0, 2 * Math.PI, false);
+      ctx.fillStyle = 'black';
+      ctx.fill();
+    }
+
+    ctx.restore();
+  }
+
+  /**
+   * Animator callback function
+   */
+  onAnimationUpdate(animator: Animator) {
+    //console.log('onAnimationUpdate@value', animator.getValue());
+    this.updateAniMask(animator.getValue());
+    this.invalidate();
+  }
+
+  /**
+   * Fast fill
+   */
+
+  fastfillStart(task) {
+
+    this.backupOutput();
+
+    // generate random unique id.
+    // task.fastfill = Math.round(Math.random() * 100000);
+    this.fastfillTask = task;
+    this.fastfillAreas = {};
+    // clear fastfill canvas.
+    this.fastfillCtx.clearRect(0, 0, this.fastfillOutput.width,
+      this.fastfillOutput.height);
+    this.fastfillAddPoint(task.x, task.y);
+
+  }
+
+  /**
+   * Fast fill add point
+   */
+  fastfillAddPoint(x, y) {
+    if (!this.fastfillTask)
+      return;
+    let area = this.getArea(x, y);
+    if (!area)
+      return;
+    if (this.fastfillAreas[area.color])
+      return;
+
+    let task = Object.assign({}, this.fastfillTask);
+    task.area = area;
+    task.x = x;
+    task.y = y;
+    task.areaIndex = area.color;
+    this._bucketFill(task, true);
+    this.fastfillCtx.drawImage(this.areaOutput, 0, 0);
+    this.fastfillAreas[area.color] = 1;
+    this.invalidate();
+  }
+
+  /**
+   * Fast fill end.
+   */
+
+  fastfillEnd() {
+    if (!this.fastfillTask)
+      return;
+    this.outputCtx.drawImage(this.fastfillOutput, 0, 0);
+    this.fastfillTask = null;
+    this.fastfillAreas = null;
+    this.invalidate();
+  }
+
+  /**
+   * Cancel the fastfill.
+   */
+  fastfillCancel() {
+    if (!this.fastfillTask)
+      return;
+    if (Object.keys(this.fastfillAreas).length > 1) {
+      this.fastfillEnd();
+    } else {
+      this.fastfillTask = null;
+      this.fastfillAreas = null;
+    }
+    this.invalidate();
+  }
+
+
+  fasteraseStart(task) {
+
+    this.backupOutput();
+
+    // generate random unique id.
+    this.fasteraseTask = task;
+    this.fasteraseAreas = {};
+    // clear fasterase canvas.
+    this.fasteraseCtx.clearRect(0, 0, this.fasteraseOutput.width, this.fasteraseOutput.height);
+    this.fasteraseAddPoint(task.x, task.y);
+
+  }
+
+  /**
+   * Fast erase add point
+   */
+  fasteraseAddPoint(x, y) {
+    if (!this.fasteraseTask)
+      return;
+    let area = this.getArea(x, y);
+    if (!area)
+      return;
+    if (this.fasteraseAreas[area.color])
+      return;
+    if (!this.colorMap[area.color]) { //area not colored.
+      return;
+    }
+
+    delete this.colorMap[area.color];
+    this.updateColorOrder();
+    this.updateUnorderMask();
+
+    this.prepareArea(area, false);
+
+    let al = area.left;
+    let at = area.top;
+    let aw = area.width();
+    let ah = area.height();
+
+    this.fasteraseCtx.drawImage(this.areaMask, al, at, aw, ah, al, at, aw, ah);
+    this.fasteraseAreas[area.color] = 1;
+    this.invalidate();
+    this.editUpdate();
+  }
+
+  /**
+   * Fast erase end.
+   */
+
+  fasteraseEnd() {
+    if (!this.fasteraseTask)
+      return;
+    this.outputCtx.save();
+    this.outputCtx.globalCompositeOperation = 'destination-out'
+    this.outputCtx.drawImage(this.fasteraseOutput, 0, 0);
+    this.outputCtx.restore();
+    this.fasteraseTask = null;
+    this.fasteraseAreas = null;
+    this.invalidate();
+  }
+
+  /**
+   * Cancel the fasterase.
+   */
+  fasteraseCancel() {
+    if (!this.fasteraseTask)
+      return;
+    if (Object.keys(this.fasteraseAreas).length > 1) {
+      this.fasteraseEnd();
+    } else {
+      this.fasteraseTask = null;
+      this.fasteraseAreas = null;
+    }
+    this.invalidate();
+  }
+
+
+  /**
+   * Draw pending points to area output.
+   */
+  drawPendingPoints() {
+    if (!this.brushTask)
+      return;
+    if (this.brushPendingPoints.length <= 0)
+      return;
+    let p, brushImage, l, t, size;
+    size = this.brushTask.size;
+
+    while (this.brushPendingPoints.length > 0) {
+      p = this.brushPendingPoints.shift();
+      this.brushTask.points.push(p);
+      brushImage = this.brushBrush.getBrush();
+      l = p.x - size / 2;
+      t = p.y - size / 2;
+      this.areaOutputCtx.drawImage(brushImage, 0, 0, brushImage.width,
+        brushImage.height, l, t, size, size);
+    }
+
+    // cut
+    this.areaOutputCtx.save();
+    this.areaOutputCtx.globalCompositeOperation = 'destination-in';
+    this.areaOutputCtx.drawImage(this.areaMask, 0, 0);
+    this.areaOutputCtx.restore();
+
+    this.invalidate();
+  }
+
+
+
+
+  /**
+   * Check if undo avaliable
+   */
+  hasUndo() {
+    return this.undoList.length > 0;
+  }
+
+  /**
+   * Check if redo avaliable
+   */
+  hasRedo() {
+    return this.redoList.length > 0;
+  }
+
+  /**
+   * Undo.
+   */
+  override undo() {
+    console.log('FillerLayer@undo#size', this.undoList.length);
+    if (this.undoList.length <= 0)
+      return;
+    let data = {
+      output: this.output,
+      colorMap: JSON.stringify(this.colorMap),
+      colorOrder: JSON.stringify(this.colorOrder)
+    }
+    // put the current output to first of redolist
+    this.redoList.unshift(data);
+    // the last of undolist as output
+    data = this.undoList.pop();
+    this.output = data.output;
+    this.outputCtx = this.output.getContext('2d');
+    this.colorMap = JSON.parse(data.colorMap);
+    this.colorOrder = JSON.parse(data.colorOrder);
+    this.updateUnorderMask();
+    this.invalidate();
+    this.editUpdate();
+  }
+
+  override undoSize() { return this.undoList.length }
+  override redoSize() { return this.redoList.length }
+
+  /**
+   * Redo
+   */
+  override redo() {
+    if (this.redoList.length <= 0)
+      return;
+    let data = {
+      output: this.output,
+      colorMap: JSON.stringify(this.colorMap),
+      colorOrder: JSON.stringify(this.colorOrder),
+    }
+    this.undoList.push(data);
+    data = this.redoList.shift();
+    this.output = data.output;
+    this.outputCtx = this.output.getContext('2d');
+    this.colorMap = JSON.parse(data.colorMap);
+    this.colorOrder = JSON.parse(data.colorOrder);
+    this.updateUnorderMask();
+    this.invalidate();
+    this.editUpdate();
+  }
+
+  /**
+   * Backup current output to undo list.
+   */
+  backupOutput() {
+    console.log('backup output....')
+    // Clear redo list, put all redos to cache for reuse.
+    let canvasList = this.redoList.map(d => {
+      return d.output;
+    })
+    this.cacheList = this.cacheList.concat(canvasList);
+    this.redoList = [];
+    let canvas = this.getCanvas();
+    let ctx = canvas.getContext('2d');
+    ctx.clearRect(0, 0, canvas.width, canvas.height);
+    ctx.drawImage(this.output, 0, 0);
+    let colorMap = JSON.stringify(this.colorMap);
+    let colorOrder = JSON.stringify(this.colorOrder);
+    this.undoList.push({
+      output: canvas,
+      colorMap: colorMap,
+      colorOrder: colorOrder,
+
+    });
+    console.log('undoList', this.undoList);
+  }
+
+
+  /**
+   * Get canvas for undo/redo
+   */
+  getCanvas() {
+    if (this.cacheList.length > 0) {
+      console.log('getCanvsa()@from cache');
+      return this.cacheList.pop();
+    } else if (this.undoList.length > 20) {
+      // Limit undo list size
+      console.log('getCanvsa()@shift undo');
+      return this.undoList.shift().output;
+    } else {
+      console.log('getCanvsa()@create new');
+      return Utils.createCanvas(this.width, this.height);
+    }
+  }
+
+  /**
+   * Draw this layer.
+   * @Override Layer#draw(ctx)
+   */
+  override draw(ctx) {
+    ctx.imageSmoothingEnabled = this.smoothing;
+
+    //Utils.setImageSmoothing(ctx, false);
+
+    this.drawOnCtx(ctx, this.width, this.height);
+
+
+
+    // draw original
+    if (!this.hideLine) {
+      if (this.showSvg && this.raw) {
+        ctx.drawImage(this.raw, 0, 0, this.page.width,
+          this.page.height, 0, 0, this.width, this.height);
+      } else {
+        ctx.drawImage(this.page, 0, 0, this.page.width,
+          this.page.height, 0, 0, this.width, this.height);
+      }
+    }
+
+    if (this.label) {
+      ctx.drawImage(this.label, 0, 0, this.label.width, this.label.height,
+        0, 0, this.width, this.height);
+    }
+
+    //draw blink hint
+    if (this.blinkCanvas) {
+      ctx.drawImage(this.blinkCanvas, 0, 0, this.blinkCanvas.width, this.blinkCanvas.height,
+        0, 0, this.width, this.height);
+    }
+
+
+  }
+
+  drawOnCtx(ctx, width, height) {
+    ctx.save();
+    ctx.clearRect(0, 0, width, height);
+
+
+    ctx.fillStyle = "white";
+    ctx.fillRect(0, 0, this.width, this.height);
+
+    // 如果当前工具是播放,则只画play canvas
+    if (this.lastToolKey == 'number-edit-play') {
+      ctx.drawImage(this.play, 0, 0, this.play.width, this.play.height, 0, 0, width, height);
+      ctx.restore();
+      return;
+    }
+
+    /*
+    ctx.drawImage(this.map, 0, 0, this.map.width, this.map.height, 0, 0, width,
+                  height);
+
+    */
+    ctx.drawImage(this.output, 0, 0, this.output.width, this.output.height, 0, 0, width, height);
+
+    // 编排模式下,需要掩蔽未排序区块
+    if (this.showUnorderMask) {
+      ctx.save();
+      ctx.globalCompositeOperation = 'destination-out'
+      ctx.drawImage(this.unorderMask, 0, 0, this.unorderMask.width, this.unorderMask.height, 0, 0, width, height);
+      ctx.restore();
+    }
+
+    // draw fastfill
+    if (this.fastfillTask) {
+      ctx.drawImage(this.fastfillOutput, 0, 0, this.fastfillOutput.width,
+        this.fastfillOutput.height, 0, 0, width, height);
+    }
+
+    // draw fasterase
+    if (this.fasteraseTask) {
+      ctx.save();
+      ctx.globalCompositeOperation = 'destination-out'
+      ctx.drawImage(this.fasteraseOutput, 0, 0, this.fasteraseOutput.width,
+        this.fasteraseOutput.height, 0, 0, width, height);
+      ctx.restore();
+    }
+
+    // brush
+    if (this.brushTask) {
+      ctx.drawImage(this.areaOutput, 0, 0, this.areaOutput.width,
+        this.areaOutput.height, 0, 0, width, height);
+    }
+
+    // draw animation
+    if (this.animator && this.animator.isRunning()) {
+      ctx.drawImage(this.aniMask, 0, 0, this.aniMask.width, this.aniMask.height,
+        0, 0, width, height);
+    }
+
+    /*
+    ctx.strokeStyle = "black";
+    ctx.strokeRect(0, 0, width, height);
+    */
+
+    // ctx.drawImage(this.original, 0, 0, this.original.width,
+    // this.original.height,
+    //  0, 0, width, height);
+
+    ctx.restore();
+  }
+
+
+  getOutput() {
+    return new Promise((done, reject) => {
+      this.output.toBlob(b => {
+        done(b)
+      })
+    });
+  }
+
+  getWorkBlob(): Promise<Blob> {
+    let canvas = Utils.createCanvas(this.width, this.height);
+    let ctx = canvas.getContext('2d');
+    ctx.drawImage(this.output, 0, 0, this.output.width, this.output.height, 0, 0, this.width, this.height);
+    if (!this.mystery) {
+      ctx.drawImage(this.page, 0, 0, this.page.width, this.page.height, 0, 0, this.width, this.height);
+    }
+    return new Promise((done, reject) => { canvas.toBlob(b => { done(b) }) });
+  }
+
+
+  getProgress() {
+    let total = this.areaList.length;
+    let colored = Object.keys(this.colorMap).length;
+    return { total, colored }
+  }
+
+  getColorMap() {
+    return this.colorMap;
+  }
+
+  /**
+   * 统计填色情况
+   */
+  getColorSum(): ColorSumItem[] {
+    let sumHash = {};
+    Object.keys(this.colorMap).forEach(_areaIndex => {
+      let areaIndex = parseInt(_areaIndex);
+      let { color, cssColor } = this.colorMap[areaIndex];
+      if (sumHash[color]) {
+        sumHash[color].total++;
+        sumHash[color].areas.push(areaIndex);
+      } else {
+        sumHash[color] = {
+          total: 1,
+          cssColor,
+          color,
+          areas: [areaIndex]
+        }
+      }
+    })
+    let labWhite = sRGB.white().toXYZ().toLab();
+    let sum: ColorSumItem[] = Object.keys(sumHash).map(key => {
+      let item: ColorSumItem = sumHash[key];
+      item.lab = Lab.fromRGBA(item.color);
+      item.distanceFromWhite = item.lab.deltaE(labWhite);
+      return item;
+    }).sort((a, b) => a.distanceFromWhite - b.distanceFromWhite);
+
+    return sum;
+  }
+
+  blinkCanvas: HTMLCanvasElement;
+  repeater: Repeater;
+
+
+  blinkByAreas(areas: FillArea[]): Rect {
+
+    //Stop the previous running repeater.
+    if (this.repeater) this.repeater.cancel();
+    if (!areas || areas.length <= 0) return new Rect();
+
+    //没有考虑scale
+    let _blinkCanvas = Utils.createCanvas(this.width, this.height);
+    let ctx = _blinkCanvas.getContext('2d');
+    ctx.clearRect(0, 0, this.width, this.height);
+
+    let imgData = ctx.getImageData(0, 0, _blinkCanvas.width, _blinkCanvas.height);
+    let pixels = new Uint32Array(imgData.data.buffer);
+    //console.log(TAG, 'pixels=', pixels);
+
+    areas.forEach((area: FillArea) => {
+      let ai, aw, i, x, y;
+      let areaPixelCount = area.width() * area.height();
+      aw = area.width();
+      for (ai = 0; ai < areaPixelCount; ai++) {
+        x = area.left + ai % aw;
+        y = area.top + Math.floor(ai / aw);
+        i = y * this.width + x;
+        if (this.mapPixels[i] == area.color) {
+          pixels[i] = 0xffffffff;
+        }
+      }
+    })
+    ctx.putImageData(imgData, 0, 0);
+
+    let rect = new Rect(
+      Math.min.call(null, ...areas.map(a => a.left)),
+      Math.min.call(null, ...areas.map(a => a.top)),
+      Math.max.call(null, ...areas.map(a => a.right)),
+      Math.max.call(null, ...areas.map(a => a.bottom))
+    );
+
+    ctx.strokeStyle = '#dd0000';
+    //ctx.strokeStyle = '#efefef';
+    ctx.lineWidth = 2;
+    let insetRect = rect.inset(ctx.lineWidth, ctx.lineWidth, ctx.lineWidth, ctx.lineWidth);
+    ctx.strokeRect(insetRect.left, insetRect.top, insetRect.width(), insetRect.height());
+
+    this.blinkCanvas = _blinkCanvas;
+    this.invalidate();
+
+    this.repeater = new Repeater(() => {
+      this.blinkCanvas = this.blinkCanvas ? null : _blinkCanvas;
+      this.invalidate();
+    }, () => {
+      this.blinkCanvas = null;
+      this.repeater = null;
+      this.invalidate();
+    }, 5, 300).start();
+
+    return rect;
+  }
+
+  /**
+   * 闪动提示
+   * 
+   * 
+   * @param cssColor 
+   * @returns 
+   */
+  blinkByColor(cssColor: any): Rect {
+
+    //throw new Error('Method not implemented.');
+    console.log(TAG, 'blinkByColor#', cssColor);
+    let areas = this.getAreasByColor(cssColor);
+    console.log(TAG, 'blinkByColor#', areas);
+    return this.blinkByAreas(areas);
+  }
+
+
+  /**
+   * 批量修改区域的颜色 -直接修改colorMap
+   * @param areaIndexes 
+   * @param destColorCss 
+   */
+  changeAreasColor(areaIndexes: number[], destColorCss: string) {
+
+    //this.backupOutput();
+    let oldColor = this.colorMap[areaIndexes[0]].color;
+    let color = Utils.getColorInteger(destColorCss);
+    let cssColor = Utils.getColorFromInteger(color);
+    areaIndexes.forEach(areaIndex => {
+      this.colorMap[areaIndex] = { color, cssColor };
+    })
+    // colorOrder也需要同步修改
+    let index = this.colorOrder.indexOf(oldColor);
+    if (index >= 0) {
+      this.colorOrder[index] = color;
+    }
+    this.drawToOutput();
+    this.updateUnorderMask();
+    this.invalidate();
+    //this.editUpdate();
+
+  }
+
+
+
+  /**
+   * 已排序元素拖动调整顺序
+   * @param from 
+   * @param to 
+   * @returns 
+   */
+  moveOrderedItem(from: number, to: number) {
+    if (from < 0 || from >= this.colorOrder.length
+      || to < 0 || to >= this.colorOrder.length
+      || to == from) {
+      return;
+    }
+
+    this.backupOutput();
+
+    const target = this.colorOrder[from];
+    const delta = to < from ? -1 : 1;
+    let array = this.colorOrder;
+
+    for (let i = from; i !== to; i += delta) {
+      array[i] = array[i + delta];
+    }
+
+    array[to] = target;
+
+    this.editUpdate();
+    this.invalidate();
+  }
+
+  /**
+   * 如果是已排序则下降到未排序,反之如果未排序则回归到已排序
+   * @param color 
+   */
+  upOrDownColor(color: number) {
+    if (!color) return;
+    console.log("upOrDownColor: " + color);
+    let idx = this.colorOrder.indexOf(color);
+
+    this.backupOutput();
+
+    if (idx >= 0) { // 在已排序中,下降到未排序
+      this.colorOrder.splice(idx, 1);
+    } else { // 在未排序中,回到已排序末尾
+      this.colorOrder.push(color);
+    }
+    this.updateUnorderMask();
+    this.editUpdate();
+    this.invalidate();
+  }
+
+  /**
+   * 切换到播放按钮初始化
+   */
+  resetPlay() {
+    console.log("resetPlay: " + this.lastToolKey);
+    this.playCtx.fillStyle = 'white';
+    this.playCtx.fillRect(0, 0, this.width, this.height);
+    this.playIdx = 0;
+  }
+
+  /**
+   * 单步播放
+   */
+  playStep() {
+    if (this.playIdx >= this.colorOrder.length) {
+      this.playIdx = 0;
+      this.playCtx.fillStyle = 'white';
+      this.playCtx.fillRect(0, 0, this.width, this.height);
+      super.invalidate();
+      return;
+    }
+    let item = this.ordered[this.playIdx];
+    for (let i = 0; i < item.areas.length; i++) {
+      let area = this.areaMap[item.areas[i]];
+      console.log(area);
+      let areaData = this.playCtx.getImageData(area.left, area.top, area.width(), area.height());
+      let areaPixels = new Uint32Array(areaData.data.buffer);
+      let ai, aw, n, xx, yy;
+      aw = area.width();
+      for (ai = 0; ai < areaPixels.length; ai++) {
+        xx = area.left + ai % aw;
+        yy = area.top + Math.floor(ai / aw);
+        n = yy * this.width + xx;
+        if (this.mapPixels[n] == area.color) {
+          areaPixels[ai] = item.color;
+        }
+      }
+      this.playCtx.putImageData(areaData, area.left, area.top);
+    }
+    // this.colorInt = this.ordered[this.playIdx].color;
+    this.getToolByKey(this.lastToolKey).editor.trigger('pick-color', this.ordered[this.playIdx].cssColor);  // 这样才能自动滚动起来
+    super.invalidate();
+
+    this.playIdx++;
+  }
+
+
+  playInterval: any;
+  /**
+   * 自动播放
+   */
+  playAuto() {
+    this.playStop();
+    this.playInterval = setInterval(() => this.playStep(), 300);
+  }
+
+  playStop() {
+    if (this.playInterval) {
+      clearInterval(this.playInterval);
+      this.playInterval = null;
+    }
+  }
+
+  /**
+   * 指定播放位置
+   * @param idx
+   */
+  playPos(idx: number) {
+    if (idx < 0) return;
+    this.playStop();
+    this.playIdx = 0;
+    this.playCtx.fillStyle = 'white';
+    this.playCtx.fillRect(0, 0, this.width, this.height);
+    super.invalidate();
+    for (let i = 0; i <= idx; i++) {
+      this.playStep();
+    }
+  }
+
+  /**
+ * 重新排序,把所有颜色都划归到未排序去
+ */
+  resetOrder() {
+    this.backupOutput();
+    this.colorOrder = [];  // 清空
+    this.updateUnorderMask();
+    this.editUpdate();
+    this.invalidate();
+  }
+
+  // 自动排序
+  autoOrder() {
+    if (!this.canAuto) return;
+
+    let orderAuto = generateOrderAuto(this.colorMap, this.centers);
+    if (!orderAuto) return;
+
+    this.backupOutput();
+    this.colorOrder = orderAuto;
+    this.updateUnorderMask();
+    this.editUpdate();
+    this.invalidate();
+
+  }
+
+
+  orderFill(x, y) {
+    let area: FillArea = this.getArea(x, y);
+    if (!area) {
+      console.warn(TAG, 'orderFill#', `Area not found@(${x}, ${y})`);
+      return;
+    };
+    let colorInfo: ColorInfo = this.colorMap[area.color];
+    if (!colorInfo) {
+      console.warn(TAG, 'orderFill#', 'Area not colored!');
+      return;
+    }
+    if (this.colorOrder.indexOf(colorInfo.color) >= 0) {
+      console.warn(TAG, 'orderFill#', 'This color already colored');
+      return;
+    }
+
+    this.backupOutput();
+
+    this.colorOrder.push(colorInfo.color);
+    this.editUpdate();
+
+    console.log(TAG, 'orderFill#colorOrder', this.colorOrder)
+    this.updateUnorderMask();
+    this.invalidate();
+  }
+
+}

+ 16 - 0
zorro/src/app/lib/filler/number-edit/number-edit-tool.ts

@@ -0,0 +1,16 @@
+import Tool from '../core/tool';
+import NumberEditLayer from './number-edit-layer';
+
+
+export default class NumberEditTool extends Tool {
+  numberEditLayer: NumberEditLayer;
+  constructor(numberEditLayer, key, cursor) {
+    super(key, cursor);
+    this.numberEditLayer = numberEditLayer;
+  }
+
+  setFillerLayer(layer) {
+    this.numberEditLayer = layer
+  }
+
+}

+ 77 - 0
zorro/src/app/lib/filler/number-edit/number-erase-tool.ts

@@ -0,0 +1,77 @@
+import NumberEditLayer from './number-edit-layer';
+import NumberEditTool from './number-edit-tool';
+import { FillTask, FILL_TYPE } from '../common/filltask';
+
+export default class EraseTool extends NumberEditTool {
+  fasterase: boolean;
+
+  constructor(numberEditLayer) {
+    super(numberEditLayer, 'number-edit-erase', 'url("/static/cursor/cursor-erase.png") 0 22, default');
+    this.setProp('fasterase', true);
+  }
+
+  isfasteraseEnabled() { return this.getProp('fasterase'); }
+
+  override onmousedown(evt: MouseEvent) {
+    console.log('BucketTool@onmousedown()');
+    if (!this.numberEditLayer)
+      return;
+    this.fasterase = false;
+  }
+
+  override onmouseup(evt: MouseEvent) { console.log('BucketTool@onmouseup()'); }
+
+  override onDragStart(evt: MouseEvent) {
+    if (!this.numberEditLayer)
+      return;
+    console.log('BucketTool@onDragStart()');
+    if (!this.isfasteraseEnabled())
+      return;
+
+    this.fasterase = true;
+
+    let task = new FillTask(FILL_TYPE.SOLID);
+    let pp = this.numberEditLayer.translate(evt._contentPoint);
+    task.x = pp.x;
+    task.y = pp.y;
+    this.numberEditLayer.fasteraseStart(task);
+  }
+
+  override onDragMove(evt: MouseEvent) {
+    console.log('ondragmove......');
+    if (!this.numberEditLayer)
+      return;
+    console.log('BucketTool@onDragMove()');
+    if (!this.isfasteraseEnabled())
+      return;
+    let pp = this.numberEditLayer.translate(evt._contentPoint);
+    this.numberEditLayer.fasteraseAddPoint(pp.x, pp.y);
+  }
+
+  override onDragEnd(evt: MouseEvent) {
+    if (!this.numberEditLayer)
+      return;
+    if (!this.isfasteraseEnabled())
+      return;
+    console.log('BucketTool@onDragEnd()');
+    this.numberEditLayer.fasteraseEnd();
+  }
+
+  override onclick(evt : MouseEvent) {
+    if (!this.editor) return;
+    if (!this.numberEditLayer) return;
+
+    let task = new FillTask(FILL_TYPE.ERASE);
+    let pp = this.numberEditLayer.translate(evt._contentPoint);
+    task.x = pp.x;
+    task.y = pp.y;
+    this.numberEditLayer.erase(task);
+  }
+
+  override onSelect(): void {
+    if (!this.numberEditLayer) return;
+    this.numberEditLayer.showUnorderMask = false;
+    this.numberEditLayer.invalidate();
+  }
+
+}

+ 48 - 0
zorro/src/app/lib/filler/number-edit/number-play-tool.ts

@@ -0,0 +1,48 @@
+import Tool from "../core/tool"
+import NumberEditLayer from "./number-edit-layer";
+
+
+
+export default class NumberPlayTool extends Tool {
+  numberEditLayer : NumberEditLayer;
+
+  constructor(numberEditLayer : NumberEditLayer) {
+    super('number-edit-play', 'pointer');
+    this.numberEditLayer = numberEditLayer;
+  }
+  
+  override onclick(evt: MouseEvent): void {
+  
+    if (!this.numberEditLayer) return; 
+
+    console.log('NumberPlayTool@onclick');
+    if (!this.numberEditLayer.playInterval) {
+      this.numberEditLayer.playStep();
+    }
+  }
+
+  override ondblclick(evt: MouseEvent): void {
+    if (!this.numberEditLayer) return;
+
+    console.log('NumberPlayTool@ondblclick');
+    if (!this.numberEditLayer.playInterval) {
+      this.numberEditLayer.playAuto();
+    } else {
+      this.numberEditLayer.playStop();
+    }
+  }
+
+  override onSelect(): void {
+    if (!this.numberEditLayer) return;
+
+    this.numberEditLayer.resetPlay();
+    this.numberEditLayer.playAuto();
+  }
+
+  override onUnSelect(): void {
+    if (!this.numberEditLayer) return;
+
+    this.numberEditLayer.playStop();
+  }
+
+}

+ 38 - 0
zorro/src/app/lib/filler/number-edit/number-select-tool.ts

@@ -0,0 +1,38 @@
+import { FillTask, FILL_TYPE } from "../common/filltask";
+import Tool from "../core/tool"
+import NumberEditLayer from "./number-edit-layer";
+
+
+
+export default class NumberSelectTool extends Tool {
+  numberEditLayer : NumberEditLayer;
+
+  constructor(numberEditLayer : NumberEditLayer) {
+    super('number-edit-select', 'pointer');
+    this.numberEditLayer = numberEditLayer;
+  }
+  
+  override onclick(evt: MouseEvent): void {
+  
+    if (!this.numberEditLayer) return;
+
+    let pp = this.numberEditLayer.translate(evt._contentPoint);
+    console.log('NumberSelectTool@onclick', pp.x, pp.y);
+    this.numberEditLayer.orderFill(pp.x , pp.y);
+  }
+
+  override ondblclick(evt: MouseEvent): void {
+    if (!this.numberEditLayer) return;
+    let pp = this.numberEditLayer.translate(evt._contentPoint);
+    console.log('NumberSelectTool@ondblclick', pp.x, pp.y);
+    let color = this.editor.pickColor(evt._canvasPoint, evt._contentPoint);
+    this.numberEditLayer.color = color;
+  }
+
+  override onSelect(): void {
+    if (!this.numberEditLayer) return;
+    this.numberEditLayer.showUnorderMask = true;
+    this.numberEditLayer.invalidate();
+  }
+
+}

+ 5 - 0
zorro/src/app/lib/filler/order-edit/README.md

@@ -0,0 +1,5 @@
+
+# 编辑填色顺序
+
+
+

+ 786 - 0
zorro/src/app/lib/filler/order-edit/order-edit-layer.ts

@@ -0,0 +1,786 @@
+import Animator from '../common/animator';
+import Easing from '../common/easing';
+import Utils from '../common/utils';
+import Layer from '../core/layer';
+import Tool from '../core/tool';
+import { AreaMap, Centers, ColorInfo, ColorMap, ColorSumItem } from '../common/interfaces';
+
+import FillArea from '../common/fillarea';
+import { FillTask } from '../common/filltask';
+import OrderSelectTool from './order-select-tool';
+import OrderPlayTool from './order-play-tool';
+import Rect from '../core/rect';
+import Repeater from '../common/repeater';
+import { generateOrderAuto, validateOrder } from '../common/color-order';
+
+
+const TAG = 'OrderEditLayer';
+
+export default class OrderEditLayer extends Layer {
+  private colorMap: ColorMap = {};
+  colorOrder: number[] = [];
+
+  private colorSum: ColorSumItem[] = [];
+  public ordered: ColorSumItem[] = [];
+  public unordered: ColorSumItem[] = [];
+
+  page: HTMLImageElement;
+  private output: HTMLCanvasElement;
+  private outputCtx: CanvasRenderingContext2D;
+
+  private play: HTMLCanvasElement;
+  private playCtx: CanvasRenderingContext2D;
+  private playIdx: number;
+
+  private areaPixels: ImageData;
+  private mapData: ImageData;
+  private map: HTMLImageElement;
+  private scale: number;
+  private mapPixels: Uint32Array;
+  private undoList: any[] = [];
+  private redoList: any[] = [];
+  private cacheList: any[] = [];
+  private areaMap: AreaMap = {};
+  private areaList: any[];
+
+  centers: Centers; // 中心点
+  private orderAuto: number[] = [];
+  private _canAuto: boolean = false;  // 是否可以展示自动排序按钮==>取决于中心点是否预备完毕
+  public get canAuto(): boolean {
+    return this._canAuto;
+  }
+  public set canAuto(value: boolean) {
+    this._canAuto = value;
+  }
+
+  raw: HTMLImageElement;
+
+
+  override get defaultToolKey(): string { return 'order-edit-select'; }
+  
+  private _color: string;
+  private _colorInt: number;
+
+  public get colorInt(): number { return this._colorInt; }
+  public set colorInt(value: number) {
+    this._colorInt = value;
+    this._color = Utils.getColorFromInteger(this._colorInt);
+  }
+
+  public get color(): string { return this._color; }
+  public set color(value: string) {
+    this._color = value;
+    this._colorInt = Utils.getColorInteger(this._color);
+  }
+
+
+  /**
+   * @param page original image.
+   * @param map map image.
+   * @param output  output image.
+   */
+  constructor(page: HTMLImageElement, map: HTMLImageElement, colorMap: ColorMap, 
+    colorOrder: any[], orderAuto: any[], centers: Centers, raw?: HTMLImageElement) {
+    super(page.width, page.height);
+
+    this.colorMap = colorMap || {};
+    this.colorOrder = colorOrder || [];
+    this.orderAuto = orderAuto || [];
+    this.centers = centers;
+    this.map = map;
+    this.page = page;
+    this.raw = raw;
+
+    this.init();
+
+    this.color = '#aa0000';
+
+    //add tools
+    this.addTool(new OrderSelectTool(this));
+    this.addTool(new OrderPlayTool(this))
+  }
+
+  init() {
+
+    console.log('FillerLayer@width=', this.width, ' height=', this.height,
+      ' scale=');
+
+    this.output = Utils.createCanvas(this.width, this.height);
+    this.outputCtx = this.output.getContext('2d');
+    this.outputCtx.fillStyle = 'white';
+    this.outputCtx.fillRect(0, 0, this.width, this.height);
+
+    this.play = Utils.createCanvas(this.width, this.height);
+    this.playCtx = this.play.getContext('2d');
+    this.playCtx.fillStyle = 'white';
+    this.playCtx.fillRect(0, 0, this.width, this.height);
+    this.playIdx = 0;
+
+
+    //this.setMap(map);
+
+    this.scale = this.map.width / this.page.width;
+    this.mapData = Utils.getImageData(this.map);
+    this.mapPixels = new Uint32Array(this.mapData.data.buffer);
+    this.createAreaMap();
+    //this.validateColorMap();
+    this.drawToOutput();
+
+    // undo redo support.
+    this.undoList = [];
+    this.redoList = [];
+    this.cacheList = [];
+
+    // 尚未生成中心点, 启动web worker计算中心点
+    if (!this.centers || Object.keys(this.centers).length <= 0) {
+      this.canAuto = false;
+      this.startWorker();
+    } else {
+      this.canAuto = true;
+    }
+  }
+
+  /**
+   * 启动webworker执行计算中心点的耗时任务
+   */
+  private startWorker() {
+    if (typeof Worker === 'undefined') return;
+    try {
+      const worker = new Worker(new URL('../common/centers.worker.ts', import.meta.url));
+      console.log("create centers webworker!!!");
+      worker.onmessage = (result) => {
+        console.log("get worker result:" + JSON.stringify(result.data));
+        this.centers = result.data;
+        this.canAuto = true;
+        worker.terminate();
+      };
+      worker.onerror = function (event) {
+        console.error("webworker error:" + event.message);
+        worker.terminate();
+      };
+      let pagePixels = new Uint32Array(Utils.getImageData(this.page).data.buffer);
+      let mapPixels = new Uint32Array(Utils.getImageData(this.map).data.buffer);
+      worker.postMessage({pagePixels: pagePixels, mapPixels: mapPixels, width: this.width, height: this.height});
+    } catch(e) {
+      console.log(e.message);
+    }
+  }
+
+  update(page?: HTMLImageElement, map?: HTMLImageElement, colorMap?: ColorMap, colorOrder?: number[], raw?: HTMLImageElement) {
+    this.page = page || this.page;
+    this.map = map || this.map;
+    this.colorMap = colorMap || this.colorMap;
+    this.colorOrder = colorOrder || this.colorOrder;
+    this.raw = raw || this.raw;
+    if (map) {  // 如果map图有变更, 那么重新计算中心点
+      this.centers = null;
+    }
+    //re initialization
+    this.init();
+    this.editUpdate();
+  }
+
+  editUpdate() {
+    this.trigger('edit-update');
+  }
+
+
+  drawToOutput() {
+    console.time('drawToOutput');
+    this.outputCtx.fillStyle = 'white';
+    this.outputCtx.fillRect(0, 0, this.width, this.height);
+    Object.keys(this.colorMap).forEach(key => {
+      if (!this.areaMap[key]) {
+        console.warn(`area ${key} not exists!`)
+        delete this.colorMap[key];
+      }
+    })
+
+    //删除colorOrder中不存在的颜色值
+    let colorSet = new Set(Object.values(this.colorMap).map((ci: ColorInfo) => ci.color));
+    for (let i = 0; i < this.colorOrder.length; i++) {
+      if (!colorSet.has(this.colorOrder[i])) {
+        this.colorOrder.splice(i, 1);
+        i--; // 避免漏掉
+      }
+    }
+
+
+    //根据填色顺序,获取已经上色部分的colorMap
+    let coloredColorMap: ColorMap = {};
+    Object.keys(this.colorMap).forEach(areaIndex => {
+      if (this.colorOrder.indexOf(this.colorMap[areaIndex].color) >= 0) {
+        coloredColorMap[areaIndex] = this.colorMap[areaIndex];
+      }
+    })
+
+
+    //fast create output by map
+    let outData = this.outputCtx.getImageData(0, 0, this.output.width, this.output.height)
+    let outPixels = new Uint32Array(outData.data.buffer);
+    let areaIndex;
+    for (var i = 0; i < outPixels.length; i++) {
+      areaIndex = this.mapPixels[i];
+      if (coloredColorMap[areaIndex]) {
+        outPixels[i] = this.colorMap[areaIndex].color;
+      }
+    }
+    this.outputCtx.putImageData(outData, 0, 0);
+    console.timeEnd('drawToOutput');
+    this.invalidate();
+  }
+
+  validateColorMap() {
+    Object.keys(this.colorMap).forEach(key => {
+      if (!this.areaMap[key]) {
+        console.warn(`area ${key} not exists!`)
+        delete this.colorMap[key];
+      }
+    })
+    //删除colorOrder中不存在的颜色值
+    let colorSet = new Set(Object.values(this.colorMap).map((ci: ColorInfo) => ci.color));
+    for (let i = 0; i < this.colorOrder.length; i++) {
+      if (!colorSet.has(this.colorOrder[i])) {
+        this.colorOrder.splice(i, 1);
+        i--; // 避免漏掉
+      }
+    }
+  }
+
+  /**
+   * Translate point.
+   * Because our cordinate system is based on map, so
+   * we don't need to translate.
+   */
+  translate(p) {
+    return p;
+  }
+
+  /**
+   * redraw filler layer.
+   */
+  override invalidate() {
+    super.invalidate();
+    this.updateOutputList();
+  }
+
+  //更新对外展示的信息
+  updateOutputList() {
+    let colorSum = this.getColorSum();
+    this.ordered = this.colorOrder.map(colorInt => {
+      let index = colorSum.findIndex(sumItem => sumItem.color == colorInt);
+      let [item] = colorSum.splice(index, 1);
+      return item;
+    });
+    this.unordered = colorSum;
+  }
+
+
+  /**
+   * Create area map from map pixels.
+   */
+  createAreaMap() {
+    let width = this.width;
+    let height = this.height;
+    let floodArr = this.mapPixels;
+    let areaMap: AreaMap = {};
+    let i: number, x: number, y: number, color: number;
+    for (i = 0; i < floodArr.length; i++) {
+      color = floodArr[i];
+      if (!color) continue;
+      x = i % width;
+      y = Math.floor(i / width);
+      if (areaMap[color]) {
+        areaMap[color].addPoint(x, y);
+      } else {
+        areaMap[color] = new FillArea(color, x, y);
+      }
+    }
+    this.areaMap = areaMap;
+    this.areaList = Object.keys(areaMap).map(key => {
+      return areaMap[key];
+    });
+  }
+
+  /**
+   * Get area by x, y
+   */
+  getArea(x, y): FillArea {
+    x = parseInt(x);
+    y = parseInt(y);
+    if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
+      return null;
+    }
+    let color = this.mapPixels[y * this.width + x];
+    return this.areaMap[color];
+  }
+
+  /**
+   * Get area by color(areaIndex)
+   */
+  getAreaByColor(areaIndex): FillArea {
+    return this.areaMap[areaIndex];
+  }
+
+
+
+  orderFill(x, y) {
+    let area: FillArea = this.getArea(x, y);
+    if (!area) {
+      console.warn(TAG, 'orderFill#', `Area not found@(${x}, ${y})`);
+      return;
+    };
+    let colorInfo: ColorInfo = this.colorMap[area.color];
+    if (!colorInfo) {
+      console.warn(TAG, 'orderFill#', 'Area not colored!');
+      return;
+    }
+    if (this.colorOrder.indexOf(colorInfo.color) >= 0) {
+      console.warn(TAG, 'orderFill#', 'This color already colored');
+      return;
+    }
+
+    this.backupOutput();
+
+    this.colorOrder.push(colorInfo.color);
+    this.editUpdate();
+
+    console.log(TAG, 'orderFill#colorOrder', this.colorOrder)
+    this.drawToOutput();
+    this.invalidate();
+  }
+
+
+
+
+  /**
+   * 根据css颜色获取所选区域
+   */
+  getAreasByCssColor(cssColor: string) {
+    let areas = Object.keys(this.colorMap).map(index => {
+      let colorInfo = this.colorMap[index];
+      if (colorInfo.cssColor == cssColor) {
+        return index
+      } else {
+        return null
+      }
+    }).filter(index => {
+      return index;
+    }).map(index => {
+      return this.areaMap[index];
+    }).filter(area => {
+      return area;
+    });
+    return areas;
+  }
+
+
+
+
+  /**
+   * Check if undo avaliable
+   */
+  hasUndo() {
+    return this.undoList.length > 0;
+  }
+
+  /**
+   * Check if redo avaliable
+   */
+  hasRedo() {
+    return this.redoList.length > 0;
+  }
+
+  /**
+   * Undo.
+   */
+  override undo() {
+    console.log('FillerLayer@undo#size', this.undoList.length);
+    if (this.undoList.length <= 0)
+      return;
+    let data = {
+      output: this.output,
+      colorOrder: JSON.stringify(this.colorOrder)
+    }
+    // put the current output to first of redolist
+    this.redoList.unshift(data);
+    // the last of undolist as output
+
+    data = this.undoList.pop();
+    this.output = data.output;
+    this.outputCtx = this.output.getContext('2d');
+    this.colorOrder = JSON.parse(data.colorOrder);
+    this.editUpdate();
+    this.invalidate();
+  }
+
+  override undoSize() { return this.undoList.length }
+  override redoSize() { return this.redoList.length }
+
+  /**
+   * Redo
+   */
+  override redo() {
+    if (this.redoList.length <= 0)
+      return;
+    let data = {
+      output: this.output,
+      colorOrder: JSON.stringify(this.colorOrder),
+    }
+    this.undoList.push(data);
+    data = this.redoList.shift();
+    this.output = data.output;
+    this.outputCtx = this.output.getContext('2d');
+    this.colorOrder = JSON.parse(data.colorOrder);
+    this.editUpdate();
+    this.invalidate();
+  }
+
+  /**
+   * Backup current output to undo list.
+   */
+  backupOutput() {
+    console.log('backup output....')
+    // Clear redo list, put all redos to cache for reuse.
+    let canvasList = this.redoList.map(d => {
+      return d.output;
+    })
+    this.cacheList = this.cacheList.concat(canvasList);
+    this.redoList = [];
+    let canvas = this.getCanvas();
+    let ctx = canvas.getContext('2d');
+    ctx.clearRect(0, 0, canvas.width, canvas.height);
+    ctx.drawImage(this.output, 0, 0);
+    let colorOrder = JSON.stringify(this.colorOrder);
+    this.undoList.push({
+      output: canvas,
+      colorOrder: colorOrder,
+    });
+    console.log('undoList', this.undoList);
+  }
+
+
+  /**
+   * Get canvas for undo/redo
+   */
+  getCanvas() {
+    if (this.cacheList.length > 0) {
+      console.log('getCanvsa()@from cache');
+      return this.cacheList.pop();
+    } else if (this.undoList.length > 20) {
+      // Limit undo list size
+      console.log('getCanvsa()@shift undo');
+      return this.undoList.shift().output;
+    } else {
+      console.log('getCanvsa()@create new');
+      return Utils.createCanvas(this.width, this.height);
+    }
+  }
+
+  /**
+   * Draw this layer.
+   * @Override Layer#draw(ctx)
+   */
+  override draw(ctx) {
+    ctx.imageSmoothingEnabled = this.smoothing;
+
+    this.drawOnCtx(ctx, this.width, this.height);
+    // draw original
+    if (this.showSvg && this.raw) {
+      ctx.drawImage(this.raw, 0, 0, this.page.width,
+        this.page.height, 0, 0, this.width, this.height);
+    } else {
+      ctx.drawImage(this.page, 0, 0, this.page.width,
+        this.page.height, 0, 0, this.width, this.height);
+    }
+
+    //draw blink hint
+    if (this.blinkCanvas) {
+      ctx.drawImage(this.blinkCanvas, 0, 0, this.blinkCanvas.width, this.blinkCanvas.height,
+        0, 0, this.width, this.height);
+    }
+  }
+
+  drawOnCtx(ctx, width, height) {
+    ctx.save();
+    ctx.clearRect(0, 0, width, height);
+
+    ctx.fillStyle = "white";
+    ctx.fillRect(0, 0, this.width, this.height);
+
+    if (this.lastToolKey == 'order-edit-play') {
+      ctx.drawImage(this.play, 0, 0, this.play.width, this.play.height, 0, 0, width, height);
+    } else {
+      ctx.drawImage(this.output, 0, 0, this.output.width, this.output.height, 0,
+        0, width, height);
+    }
+  
+    ctx.restore();
+  }
+
+
+  getColorSum() {
+    let sumHash = {};
+    Object.keys(this.colorMap).forEach(_areaIndex => {
+      let areaIndex = parseInt(_areaIndex);
+      let { color, cssColor } = this.colorMap[areaIndex];
+      if (sumHash[color]) {
+        sumHash[color].total++;
+        sumHash[color].areas.push(areaIndex);
+      } else {
+        sumHash[color] = {
+          total: 1,
+          cssColor,
+          color,
+          areas: [areaIndex]
+        }
+      }
+    })
+    return Object.keys(sumHash).map(key => {
+      return sumHash[key];
+    })
+  }
+
+  /**
+   * 已排序元素拖动调整顺序
+   * @param from 
+   * @param to 
+   * @returns 
+   */
+  moveOrderedItem(from: number, to: number) {
+    if (from < 0 || from >= this.colorOrder.length 
+      || to < 0 || to >= this.colorOrder.length
+      || to == from) {
+        return;
+    }
+    
+    this.backupOutput();
+
+    const target = this.colorOrder[from];
+    const delta = to < from ? -1 : 1;
+    let array = this.colorOrder;
+
+    for (let i = from; i !== to; i += delta) {
+      array[i] = array[i + delta];
+    }
+
+    array[to] = target;
+
+    this.resetPlay();
+    this.editUpdate();
+    this.invalidate();
+  }
+
+  /**
+   * 移除已排序的元素,下降到未排序列表
+   * @param item 
+   * @returns 
+   */
+  removeOrderedItem(item: ColorSumItem) {
+    if (!item) return;
+    console.log(item);
+    let idx = this.colorOrder.findIndex(elem => elem == item.color);
+    if (idx < 0) return;
+
+    this.backupOutput();
+    this.colorOrder.splice(idx, 1);
+    this.editUpdate();
+    this.drawToOutput();
+    this.resetPlay();
+    this.invalidate();
+  }
+
+
+  /**
+   * 切换到播放按钮初始化
+   */
+  resetPlay() {
+    console.log("resetPlay: " + this.lastToolKey);
+    this.playCtx.fillStyle = 'white';
+    this.playCtx.fillRect(0, 0, this.width, this.height);
+    this.playIdx = 0;
+  }
+  
+  /**
+   * 单步播放
+   */
+  playStep() {
+    if (this.playIdx >= this.colorOrder.length) {
+      this.playIdx = 0;
+      this.playCtx.fillStyle = 'white';
+      this.playCtx.fillRect(0, 0, this.width, this.height);
+      super.invalidate();
+      return;
+    }
+    let item = this.ordered[this.playIdx];
+    for (let i = 0; i < item.areas.length; i++) {
+      let area = this.areaMap[item.areas[i]];
+      console.log(area);
+      let areaData = this.playCtx.getImageData(area.left, area.top, area.width(), area.height());
+      let areaPixels = new Uint32Array(areaData.data.buffer);
+      let ai, aw, n, xx, yy;
+      aw = area.width();
+      for (ai = 0; ai < areaPixels.length; ai++) {
+        xx = area.left + ai % aw;
+        yy = area.top + Math.floor(ai / aw);
+        n = yy * this.width + xx;
+        if (this.mapPixels[n] == area.color) {
+          areaPixels[ai] = item.color;
+        }
+      }
+      this.playCtx.putImageData(areaData, area.left, area.top);
+    }
+    super.invalidate();
+
+    this.playIdx++;
+  }
+
+  
+  playInterval: any;
+  /**
+   * 自动播放
+   */
+  playAuto() {
+    this.playStop();
+    this.playInterval = setInterval(() => this.playStep(), 300);
+  }
+
+  playStop() {
+    if (this.playInterval) {
+      clearInterval(this.playInterval);
+      this.playInterval = null;
+    }
+  }
+
+  
+  /**
+   * 闪动提示
+   * 
+   * 
+   * @param cssColor 
+   * @returns 
+   */
+   blinkByColor(cssColor: any): Rect {
+
+    //throw new Error('Method not implemented.');
+    console.log(TAG, 'blinkByColor#', cssColor);
+    let areas = this.getAreasByColor(cssColor);
+    console.log(TAG, 'blinkByColor#', areas);
+    return this.blinkByAreas(areas, cssColor);
+  }
+
+  /**
+   * 根据css颜色获取所选区域
+   */
+   getAreasByColor(cssColor) {
+    let colorInt = Utils.getColorInteger(cssColor);
+    let areas = Object.keys(this.colorMap).map(index => {
+      let colorInfo = this.colorMap[index];
+      if (colorInfo.color == colorInt) {
+        return index
+      } else {
+        return null
+      }
+    }).filter(index => {
+      return index;
+    }).map(index => {
+      return this.areaMap[index];
+    }).filter(area => {
+      return area;
+    });
+    return areas;
+  }
+
+  blinkCanvas: HTMLCanvasElement;
+  blinkRepeater: Repeater;
+  blinkByAreas(areas: FillArea[], cssColor: any) : Rect {
+
+    let colorInt = Utils.getColorInteger(cssColor);
+
+    //Stop the previous running repeater.
+    if (this.blinkRepeater) this.blinkRepeater.cancel();
+    if (!areas || areas.length <= 0) return new Rect();
+
+    //没有考虑scale
+    let _blinkCanvas = Utils.createCanvas(this.width, this.height);
+    let ctx = _blinkCanvas.getContext('2d');
+    ctx.clearRect(0, 0, this.width, this.height);
+
+    let imgData = ctx.getImageData(0, 0, _blinkCanvas.width, _blinkCanvas.height);
+    let pixels = new Uint32Array(imgData.data.buffer);
+    //console.log(TAG, 'pixels=', pixels);
+
+    areas.forEach((area: FillArea) => {
+      let ai, aw, i, x, y;
+      let areaPixelCount = area.width() * area.height();
+      aw = area.width();
+      for (ai = 0; ai < areaPixelCount; ai++) {
+        x = area.left + ai % aw;
+        y = area.top + Math.floor(ai / aw);
+        i = y * this.width + x;
+        if (this.mapPixels[i] == area.color) {
+          pixels[i] = this.colorOrder.findIndex(elem => elem == colorInt) >= 0 ? 0xffffffff : colorInt;
+        }
+      }
+    })
+    ctx.putImageData(imgData, 0, 0);
+
+    let rect = new Rect(
+      Math.min.call(null, ...areas.map(a => a.left)),
+      Math.min.call(null, ...areas.map(a => a.top)),
+      Math.max.call(null, ...areas.map(a => a.right)),
+      Math.max.call(null, ...areas.map(a => a.bottom))
+    );
+
+    ctx.strokeStyle = '#dd0000';
+    //ctx.strokeStyle = '#efefef';
+    ctx.lineWidth = 2;
+    let insetRect = rect.inset(ctx.lineWidth, ctx.lineWidth, ctx.lineWidth, ctx.lineWidth);
+    ctx.strokeRect(insetRect.left, insetRect.top, insetRect.width(), insetRect.height());
+
+    this.blinkCanvas = _blinkCanvas;
+    this.invalidate();
+
+    this.blinkRepeater = new Repeater(() => {
+      this.blinkCanvas = this.blinkCanvas ? null : _blinkCanvas;
+      this.invalidate();
+    }, () => {
+      this.blinkCanvas = null;
+      this.blinkRepeater = null;
+      this.invalidate();
+    }, 5, 300).start();
+
+    return rect;
+  }
+
+
+  /**
+   * 重新排序,把所有颜色都划归到未排序去
+   */
+  reset() {
+    this.backupOutput();
+    this.colorOrder = [];  // 清空
+    this.editUpdate();
+    this.invalidate();
+    this.outputCtx.fillStyle = 'white';
+    this.outputCtx.fillRect(0, 0, this.width, this.height);
+  }
+
+  // 自动排序
+  autoOrder() {
+    if (!this.canAuto) return;
+
+    if (!validateOrder(this.colorMap, this.orderAuto)) {
+      console.log("order not valid, generate order auto");
+      this.orderAuto = generateOrderAuto(this.colorMap, this.centers);
+    }
+
+    this.backupOutput();
+    this.colorOrder = this.orderAuto;
+    this.editUpdate();
+    this.invalidate();
+
+  }
+
+}
+

+ 43 - 0
zorro/src/app/lib/filler/order-edit/order-play-tool.ts

@@ -0,0 +1,43 @@
+import Tool from "../core/tool"
+import OrderEditLayer from "./order-edit-layer";
+
+
+
+export default class OrderPlayTool extends Tool {
+  orderEditLayer : OrderEditLayer;
+
+  constructor(orderEditLayer : OrderEditLayer) {
+    super('order-edit-play', 'pointer');
+    this.orderEditLayer = orderEditLayer;
+  }
+  
+  override onclick(evt: MouseEvent): void {
+  
+    if (!this.orderEditLayer) return; 
+
+    console.log('OrderPlayTool@onclick');
+    if (!this.orderEditLayer.playInterval) {
+      this.orderEditLayer.playStep();
+    }
+  }
+
+  override ondblclick(evt: MouseEvent): void {
+    if (!this.orderEditLayer) return;
+
+    console.log('OrderPlayTool@ondblclick');
+    if (!this.orderEditLayer.playInterval) {
+      this.orderEditLayer.playAuto();
+    } else {
+      this.orderEditLayer.playStop();
+    }
+  }
+
+  override onSelect(): void {
+    if (!this.orderEditLayer) return;
+
+    this.orderEditLayer.invalidate();
+    this.orderEditLayer.resetPlay();
+    this.orderEditLayer.playAuto();
+  }
+
+}

+ 37 - 0
zorro/src/app/lib/filler/order-edit/order-select-tool.ts

@@ -0,0 +1,37 @@
+import Tool from "../core/tool"
+import OrderEditLayer from "./order-edit-layer";
+
+
+
+export default class OrderSelectTool extends Tool {
+  orderEditLayer : OrderEditLayer;
+
+  constructor(orderEditLayer : OrderEditLayer) {
+    super('order-edit-select', 'pointer');
+    this.orderEditLayer = orderEditLayer;
+  }
+  
+  override onclick(evt: MouseEvent): void {
+  
+    if (!this.orderEditLayer) return;
+
+    let pp = this.orderEditLayer.translate(evt._contentPoint);
+    console.log('OrderSelectTool@onclick', pp.x, pp.y);
+    this.orderEditLayer.orderFill(pp.x , pp.y);
+  }
+
+  override ondblclick(evt: MouseEvent): void {
+    if (!this.orderEditLayer) return;
+    let pp = this.orderEditLayer.translate(evt._contentPoint);
+    console.log('OrderSelectTool@ondblclick', pp.x, pp.y);
+    let color = this.editor.pickColor(evt._canvasPoint, evt._contentPoint);
+    this.orderEditLayer.color = color;
+  }
+
+
+  override onSelect(): void {
+    this.orderEditLayer.invalidate();
+    this.orderEditLayer.playStop();
+  }
+
+}

+ 82 - 0
zorro/src/app/lib/filler/tool-config.ts

@@ -0,0 +1,82 @@
+export interface ToolConfig {
+  key? : string,
+  icon : string,
+  name : string,
+  shortcut? : string,
+  isCtrl? : boolean,
+  tooltip? : string,
+}
+
+export const ToolConfigs : {[key:string] : ToolConfig} = {
+  'pan': {
+    icon: 'fal fa-arrows',
+    name: $localize`移动`,
+    shortcut : ' ',
+    tooltip : $localize`空格键`
+  },
+  'color-picker': {
+    icon: 'fal fa-eye-dropper',
+    name: $localize`吸色`,
+    shortcut : 'i',
+  },
+  'number-edit-bucket': {
+    icon: 'fal fa-fill-drip',
+    name: $localize`填充`,
+    shortcut : 'g',
+},
+  'number-edit-erase': {
+    icon: 'fal fa-eraser',
+    name: $localize`擦除`,
+    shortcut : 'e',
+  },
+  'number-edit-select': {
+    icon: 'fal fa-plus-circle',
+    name: $localize`编排`,
+    shortcut: 'j',
+  },
+  'number-edit-play': {
+    icon: 'fal fa-play-circle',
+    name: $localize`播放`,
+    shortcut: 'k',
+  },
+  'map-edit-merge': {
+    icon: 'fal fa-link',
+    name: $localize`合并`,
+    shortcut : 'e',
+    isCtrl : true,
+    tooltip : 'Ctrl/Cmd + e'
+  },
+  'map-edit-brush': {
+    icon: 'fal fa-paint-brush-alt',
+    name: $localize`笔刷`,
+    shortcut : 'b',
+  },
+  'map-edit-recover': {
+    icon: 'fal fa-first-aid',
+    name: $localize`恢复`,
+    shortcut : 'z',
+  },
+  'map-edit-split-area': {
+    icon: 'fal fa-plus-circle',
+    name: $localize`分割`,
+    shortcut : 'x',
+  },
+  'map-edit-pencil': {
+    icon: 'fal fa-pencil',
+    name: $localize`铅笔`,
+    shortcut : 'p',
+  },
+  'order-edit-select': {
+    icon: 'fal fa-plus-circle',
+    name: $localize`选择`,
+  },
+  'order-edit-play': {
+    icon: 'fal fa-play-circle',
+    name: $localize`播放`,
+  },
+  'mark-edit-mark': {
+    icon: 'far fa-circle',
+    name: $localize`标记`,
+    shortcut : 'n',
+  },
+};

+ 35 - 0
zorro/src/app/lib/pager/cache.service.ts

@@ -0,0 +1,35 @@
+import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { concatAll, map, Observable, of } from 'rxjs';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class CacheService {
+  public cache = new Map<string, any>()
+
+  constructor(private http: HttpClient) { }
+
+  /**
+   * 先从缓存中加载数据,然后再刷新, 主要解决回退时数据加载慢导致scroll不起作用的问题.
+   * @param url 
+   * @param params 
+   * @returns 
+   */
+  get(url: string, params?: HttpParams): Observable<any> {
+    let key = params ?  `url-${JSON.stringify(params)}` : url;
+    let refresh = this.http.get(url, { params }).pipe(map(resp => {
+      this.cache.set(key, resp);
+      return resp;
+    }));
+    if (this.cache.has(key)) {
+      let old = this.cache.get(key);
+      return of(of(old), refresh).pipe(concatAll());
+    } else {
+      return refresh;
+    }
+  }
+
+
+
+}

+ 113 - 0
zorro/src/app/lib/pager/op-button.directive.ts

@@ -0,0 +1,113 @@
+import { DOCUMENT } from '@angular/common';
+import { ChangeDetectorRef, Directive, ElementRef, EventEmitter, Host, HostBinding, HostListener, Inject, Input, OnInit, Optional, Output, Renderer2 } from '@angular/core';
+import { NzButtonComponent } from 'ng-zorro-antd/button';
+import { NzMessageService } from 'ng-zorro-antd/message';
+import { NzModalService } from 'ng-zorro-antd/modal';
+import { delay, Observable } from 'rxjs';
+
+@Directive({
+  selector: '[cgOpButton]'
+})
+export class OpButtonDirective implements OnInit {
+  @Input() confirmTitle: string = $localize`确认操作`;
+  @Input() confirmContent: string = $localize`确定要执行这个操作?`;
+  @Input() successText: string = $localize`操作成功`;
+  @Input() operation: () => Observable<any>;
+
+  @Output() onSuccess = new EventEmitter<any>();
+  @Output() onFail = new EventEmitter<any>();
+  @Output() onLoadingChange = new EventEmitter<boolean>();
+
+  @HostListener('click', ['$event']) onClick($event) {
+    this.modal.confirm({
+      nzTitle: this.confirmTitle,
+      nzContent: this.confirmContent,
+      nzOnOk: this.onOk.bind(this),
+    });
+  }
+
+  @HostBinding('disabled')
+  get isDisabled() { return !this.nzButton && this.loading }
+
+
+  private _loading: boolean = false;
+  public get loading(): boolean {
+    return this._loading;
+  }
+  public set loading(value: boolean) {
+    this._loading = value;
+    this.onLoadingChange.emit(value);
+    if (this.nzButton) {
+      //this.nzButton.nzLoading = value;
+      //this.cdr.markForCheck();
+    } else {
+      if (this.icon) {
+        this.icon.style.display = this.loading ? 'none' : '';
+      }
+      this.spinner.style.display = this.loading ? '' : 'none';
+    }
+  }
+
+
+  constructor(
+    private el: ElementRef,
+    private renderer: Renderer2,
+    private modal: NzModalService,
+    private message: NzMessageService,
+    private cdr: ChangeDetectorRef,
+    @Inject(DOCUMENT) private document: Document,
+    @Optional() @Host() private nzButton?: NzButtonComponent,
+  ) {
+
+  }
+
+  private spinner: HTMLElement;
+  private icon: HTMLElement;
+
+  ngOnInit(): void {
+    let nativeEl = this.el.nativeElement as HTMLElement;
+    //找到iocn
+    this.icon = nativeEl.querySelector('i');
+
+    //创建 spinner
+    this.spinner = this.document.createElement('i');
+    //this.spinner.className = "spinner-border spinner-border-sm";
+    this.spinner.className = "spinner fal fa-spinner-third ";
+    this.spinner.style.display = 'none';
+    this.renderer.insertBefore(this.el.nativeElement, this.spinner, (this.el.nativeElement as HTMLElement).firstChild);
+  }
+
+  onOk() {
+    if (this.operation) {
+      this.loading = true;
+      this.operation().subscribe({
+        next: resp => {
+          this.message.success(this.successText);
+          this.onSuccess.emit(resp);
+        },
+        error: err => {
+          //this.message.error(err.error?.message || err.message);
+          let modalRef = this.modal.error({
+            nzTitle: $localize`操作失败`,
+            nzContent: err.error?.message || err.message,
+          });
+
+          this.loading = false;
+          this.onFail.emit(err);
+        },
+        complete: () => {
+          this.loading = false;
+        }
+      })
+    }else{
+      this.message.info($localize`没有定义操作`);
+    }
+  }
+}
+
+
+
+
+
+
+export type opFunc = () => Observable<any>;

+ 158 - 0
zorro/src/app/lib/pager/pager-filter.component.ts

@@ -0,0 +1,158 @@
+import { HttpClient } from '@angular/common/http';
+import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import { differenceInCalendarDays, endOfMonth, endOfWeek, endOfYear, startOfMonth, startOfWeek, startOfYear, subDays, subMonths, subYears } from 'date-fns';
+
+@Component({
+  selector: 'cg-pager-filter',
+  template: `
+
+  <nz-descriptions *ngIf="filterConfig" [nzColumn]="8">
+    <nz-descriptions-item  *ngFor="let config of filterConfig" [nzSpan]="config['span'] || 1" [nzTitle]="config['title']" >
+    <!--radio-->
+    <nz-radio-group *ngIf="config.filterType == filterType.radio" 
+      [(ngModel)]="filters[config['data']]" 
+      (ngModelChange)="update()" >
+        <label nz-radio-button [nzValue]="undefined" i18n>全部</label>
+        <label *ngFor="let option of config.options" nz-radio-button [nzValue]="option['value']">{{option['label']}}</label>
+    </nz-radio-group>
+
+    <!--date range picker-->
+    <!--
+      [nzDisabledDate]="disabledDate" 
+    -->
+    <nz-range-picker *ngIf="config.filterType == filterType.dateRange" 
+      [nzRanges]="ranges" 
+      [(ngModel)]="filters[config['data']]" 
+      (ngModelChange)="update()"
+      [nzAllowClear]="true" >
+    </nz-range-picker>
+
+    <!--select-->
+    <nz-select *ngIf="config.filterType == filterType.select"  [ngClass]="{'full-width' : config.fullWidth}" 
+      [nzMode]="config.mode || 'default'" 
+      [(ngModel)]="filters[config['data']]" 
+      (ngModelChange)="update()"
+      [nzPlaceHolder]="config.title" nzAllowClear>
+      <nz-option *ngFor="let option of config.options" [nzValue]="option['value']" [nzLabel]="option['label'] || option['value']"></nz-option>
+    </nz-select>
+
+
+    </nz-descriptions-item>
+
+    <nz-descriptions-item [nzSpan]="1" i18n-nzTitle nzTitle="清空筛选" >
+      <button nz-button nzSize="small" (click)="clear()" i18n>清空</button>
+    </nz-descriptions-item>
+  </nz-descriptions>
+
+
+  `,
+  styles: [
+    `
+  nz-select{
+    min-width:80px;
+  }
+
+  .full-width {
+    width : 90%;
+  }
+
+`
+  ]
+})
+export class PagerFilterComponent implements OnInit {
+  @Output() filterChange = new EventEmitter<any>();
+
+
+
+  @Input() filters: any = {};
+  @Input() set inputFilters(val) {
+    if (val) {
+      Object.assign(this.filters, val);
+      this.update();
+    }
+  }
+
+
+  private _filterConfig: FilterItem[];
+  public get filterConfig(): FilterItem[] {
+    return this._filterConfig;
+  }
+  @Input()
+  public set filterConfig(value: FilterItem[]) {
+    this._filterConfig = value;
+    if (this._filterConfig) {
+      this._filterConfig.forEach(config => {
+        if (config.optionsUrl) {
+          this.http.get(config.optionsUrl).subscribe(res => {
+            config.options = res as FilterOption[];
+          })
+        }
+      })
+    }
+  }
+
+  public filterType = FilterType;
+
+  constructor(
+    private http: HttpClient,
+  ) { }
+
+  ngOnInit(): void {
+  }
+
+
+  clear() {
+    this.filters = {};
+    this.filterChange.emit(this.filters);
+  }
+
+  update() {
+    this.filterChange.emit(this.filters);
+  }
+
+
+
+  //////date range
+  // Can not select days before today and today
+  disabledDate = (current: Date): boolean =>
+    differenceInCalendarDays(current, new Date()) > 0;
+
+
+
+  ranges = {
+    '今天': [new Date(), new Date()],
+    '昨天': [subDays(new Date(), 1), subDays(new Date(), 1)],
+    '上周': [startOfWeek(subDays(new Date(), 7)), endOfWeek(subDays(new Date(), 7))],
+    '本周': [startOfWeek(new Date()), new Date()],
+    '上月': [startOfMonth(subMonths(new Date(), 1)), endOfMonth(subMonths(new Date(), 1))],
+    '本月': [startOfMonth(new Date()), new Date()],
+    '前年': [startOfYear(subYears(new Date(), 2)), endOfYear(subYears(new Date(), 2))],
+    '去年': [startOfYear(subYears(new Date(), 1)), endOfYear(subYears(new Date(), 1))],
+    '今年': [startOfYear(new Date()), new Date()],
+  };
+
+
+}
+
+export enum FilterType {
+  dateRange = 'dateRange', //时间范围
+  radio = 'radio',
+  select = 'select',
+}
+
+export interface FilterOption {
+  value: any; //字段值
+  label?: string;
+}
+
+export interface FilterItem {
+  data: string; //字段
+  title: string; //名称
+  filterType: FilterType, //过滤类型
+  mode? : 'default' | 'tags' | 'multiple', //select
+  multiple?: boolean, //是否多选
+  options?: FilterOption[]; //选项
+  span?: number;
+  fullWidth?: boolean;
+  optionsUrl?: string; //选项url
+}

部分文件因文件數量過多而無法顯示