Add more control

This commit is contained in:
2026-06-29 09:41:02 +02:00
parent 2e62d2d810
commit 8207e84fa2
11 changed files with 890 additions and 56 deletions
+535
View File
@@ -11,6 +11,7 @@
"@stylistic/eslint-plugin": "^5.10.0", "@stylistic/eslint-plugin": "^5.10.0",
"@types/lodash": "^4.17.24", "@types/lodash": "^4.17.24",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"express": "^5.2.1",
"fast-xml-builder": "^1.1.5", "fast-xml-builder": "^1.1.5",
"fast-xml-parser": "^5.7.1", "fast-xml-parser": "^5.7.1",
"imagetracerjs": "^1.2.6", "imagetracerjs": "^1.2.6",
@@ -1329,6 +1330,19 @@
} }
} }
}, },
"node_modules/accepts": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
"license": "MIT",
"dependencies": {
"mime-types": "^3.0.0",
"negotiator": "^1.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.16.0", "version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
@@ -1608,6 +1622,30 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/body-parser": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
"integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
"license": "MIT",
"dependencies": {
"bytes": "^3.1.2",
"content-type": "^1.0.5",
"debug": "^4.4.3",
"http-errors": "^2.0.0",
"iconv-lite": "^0.7.0",
"on-finished": "^2.4.1",
"qs": "^6.14.1",
"raw-body": "^3.0.1",
"type-is": "^2.0.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.14", "version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
@@ -1652,6 +1690,15 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
} }
}, },
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind": { "node_modules/call-bind": {
"version": "1.0.9", "version": "1.0.9",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
@@ -1798,6 +1845,28 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/content-disposition": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
"integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"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==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/convert-source-map": { "node_modules/convert-source-map": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -1805,6 +1874,24 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
"license": "MIT",
"engines": {
"node": ">=6.6.0"
}
},
"node_modules/core-js": { "node_modules/core-js": {
"version": "3.49.0", "version": "3.49.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz",
@@ -2001,6 +2088,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/detect-libc": { "node_modules/detect-libc": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -2047,6 +2143,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"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==",
"license": "MIT"
},
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.338", "version": "1.5.338",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.338.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.338.tgz",
@@ -2061,6 +2163,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-abstract": { "node_modules/es-abstract": {
"version": "1.24.2", "version": "1.24.2",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz",
@@ -2240,6 +2351,12 @@
"node": ">=6" "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==",
"license": "MIT"
},
"node_modules/escape-string-regexp": { "node_modules/escape-string-regexp": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -2504,12 +2621,64 @@
"node": ">=0.10.0" "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==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/eventemitter3": { "node_modules/eventemitter3": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
"integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==", "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/express": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT",
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
"content-disposition": "^1.0.0",
"content-type": "^1.0.5",
"cookie": "^0.7.1",
"cookie-signature": "^1.2.1",
"debug": "^4.4.0",
"depd": "^2.0.0",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"etag": "^1.8.1",
"finalhandler": "^2.1.0",
"fresh": "^2.0.0",
"http-errors": "^2.0.0",
"merge-descriptors": "^2.0.0",
"mime-types": "^3.0.0",
"on-finished": "^2.4.1",
"once": "^1.4.0",
"parseurl": "^1.3.3",
"proxy-addr": "^2.0.7",
"qs": "^6.14.0",
"range-parser": "^1.2.1",
"router": "^2.2.0",
"send": "^1.1.0",
"serve-static": "^2.2.0",
"statuses": "^2.0.1",
"type-is": "^2.0.1",
"vary": "^1.1.2"
},
"engines": {
"node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -2617,6 +2786,27 @@
"node": ">=16.0.0" "node": ">=16.0.0"
} }
}, },
"node_modules/finalhandler": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
"integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.0",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"on-finished": "^2.4.1",
"parseurl": "^1.3.3",
"statuses": "^2.0.1"
},
"engines": {
"node": ">= 18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/find-up": { "node_modules/find-up": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -2667,6 +2857,24 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -2964,6 +3172,42 @@
"node": ">=8.0.0" "node": ">=8.0.0"
} }
}, },
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"dependencies": {
"depd": "~2.0.0",
"inherits": "~2.0.4",
"setprototypeof": "~1.2.0",
"statuses": "~2.0.2",
"toidentifier": "~1.0.1"
},
"engines": {
"node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/iconv-lite": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -3036,6 +3280,15 @@
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
"license": "MIT" "license": "MIT"
}, },
"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==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/is-array-buffer": { "node_modules/is-array-buffer": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -3264,6 +3517,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-promise": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT"
},
"node_modules/is-regex": { "node_modules/is-regex": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@@ -3941,6 +4200,52 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/media-typer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/merge-descriptors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mime-db": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
"license": "MIT",
"dependencies": {
"mime-db": "^1.54.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.5", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
@@ -3984,6 +4289,15 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/negotiator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/node-exports-info": { "node_modules/node-exports-info": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz",
@@ -4110,6 +4424,27 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"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==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/optionator": { "node_modules/optionator": {
"version": "0.9.4", "version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -4192,6 +4527,15 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-exists": { "node_modules/path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -4231,6 +4575,16 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/path-to-regexp": {
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/pdf-lib": { "node_modules/pdf-lib": {
"version": "1.17.1", "version": "1.17.1",
"resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
@@ -4355,6 +4709,19 @@
"react-is": "^16.13.1" "react-is": "^16.13.1"
} }
}, },
"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==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -4364,6 +4731,21 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/qs": {
"version": "6.15.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
"integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/raf": { "node_modules/raf": {
"version": "3.4.1", "version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
@@ -4374,6 +4756,30 @@
"performance-now": "^2.1.0" "performance-now": "^2.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==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"http-errors": "~2.0.1",
"iconv-lite": "~0.7.0",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/react": { "node_modules/react": {
"version": "19.2.5", "version": "19.2.5",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
@@ -4578,6 +4984,22 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/router": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.0",
"depd": "^2.0.0",
"is-promise": "^4.0.0",
"parseurl": "^1.3.3",
"path-to-regexp": "^8.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/safe-array-concat": { "node_modules/safe-array-concat": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
@@ -4636,6 +5058,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/scheduler": { "node_modules/scheduler": {
"version": "0.27.0", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@@ -4651,6 +5079,51 @@
"semver": "bin/semver.js" "semver": "bin/semver.js"
} }
}, },
"node_modules/send": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.3",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"etag": "^1.8.1",
"fresh": "^2.0.0",
"http-errors": "^2.0.1",
"mime-types": "^3.0.2",
"ms": "^2.1.3",
"on-finished": "^2.4.1",
"range-parser": "^1.2.1",
"statuses": "^2.0.2"
},
"engines": {
"node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/serve-static": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
"integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
"license": "MIT",
"dependencies": {
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"parseurl": "^1.3.3",
"send": "^1.2.0"
},
"engines": {
"node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/set-function-length": { "node_modules/set-function-length": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -4703,6 +5176,12 @@
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -4825,6 +5304,15 @@
"node": ">=0.1.14" "node": ">=0.1.14"
} }
}, },
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/stop-iteration-iterator": { "node_modules/stop-iteration-iterator": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
@@ -5098,6 +5586,15 @@
"url": "https://github.com/sponsors/SuperchupuDev" "url": "https://github.com/sponsors/SuperchupuDev"
} }
}, },
"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==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/ts-api-utils": { "node_modules/ts-api-utils": {
"version": "2.5.0", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
@@ -5131,6 +5628,20 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/type-is": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
"license": "MIT",
"dependencies": {
"content-type": "^1.0.5",
"media-typer": "^1.1.0",
"mime-types": "^3.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/typed-array-buffer": { "node_modules/typed-array-buffer": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
@@ -5268,6 +5779,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
@@ -5337,6 +5857,15 @@
"uuid": "dist-node/bin/uuid" "uuid": "dist-node/bin/uuid"
} }
}, },
"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==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "8.0.8", "version": "8.0.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
@@ -5524,6 +6053,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/xml-lexer": { "node_modules/xml-lexer": {
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/xml-lexer/-/xml-lexer-0.2.2.tgz", "resolved": "https://registry.npmjs.org/xml-lexer/-/xml-lexer-0.2.2.tgz",
+2 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "vectrace", "name": "vectrace",
"private": true, "private": true,
"version": "1.0.0", "version": "1.1.0",
"author": { "author": {
"name": "Terncode", "name": "Terncode",
"email": "[email protected]", "email": "[email protected]",
@@ -19,6 +19,7 @@
"@stylistic/eslint-plugin": "^5.10.0", "@stylistic/eslint-plugin": "^5.10.0",
"@types/lodash": "^4.17.24", "@types/lodash": "^4.17.24",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"express": "^5.2.1",
"fast-xml-builder": "^1.1.5", "fast-xml-builder": "^1.1.5",
"fast-xml-parser": "^5.7.1", "fast-xml-parser": "^5.7.1",
"imagetracerjs": "^1.2.6", "imagetracerjs": "^1.2.6",
+25
View File
@@ -0,0 +1,25 @@
import express from "express";
import path from "path";
import { fileURLToPath } from "url";
const app = express();
const port = process.env.PORT || 3000;
function createServer() {
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const distPath = path.join(__dirname, "dist");
app.use(express.static(distPath));
app.get("*a", function (_, res) {
res.sendFile(path.join(distPath, "index.html"));
});
app.listen(port, function () {
console.log("server running on " + port);
});
}
createServer();
+119 -27
View File
@@ -2,42 +2,134 @@ import { useEffect, useState } from "react";
import type { ImageEditor } from "../handler/image-editor"; import type { ImageEditor } from "../handler/image-editor";
import type { TracerOptions } from "../interface"; import type { TracerOptions } from "../interface";
import { NumericInputWithLabelTable } from "./numeric-labeld-input"; import { NumericInputWithLabelTable } from "./numeric-labeld-input";
import styled from "styled-components";
import { Button } from "../styles";
import { FaBezierCurve } from "react-icons/fa6";
import { VerticalSlider } from "./vertical-slider";
import { TRANSPARENCY_THRESHOLD_DEFAULT } from "../svg-tracer";
import type { VectraceHandler } from "../handler/vectrace-handler";
import { clamp } from "lodash";
const Table = styled.table`
margin: 2px;
padding-right: 4px;
`;
const WrapperColumn = styled.div`
display: flex;
flex-direction: column;
`;
const WrapperRow = styled.div`
display: flex;
flex-direction: row;
height: 100px;
`;
const LayerPanel = styled.div`
display: flex;
flex-direction: column;
width: 100px;
overflow: auto;
height: 100px;
`;
const ToggleButton = styled.button<{ $enabled: boolean }>`
background-color: ${({ $enabled }) => $enabled ? "var(--secondary-color)" : "var(--primary-color)"};
color: white;
border: 1px solid white;
&:hover {
color: var(--secondary-color);
background-color: white;
}
`;
export function SvgTracerOptions({ selected }: { selected: ImageEditor[] }) {
export function SvgTracerOptions({ selected, handler }: { selected: ImageEditor[], handler: VectraceHandler }) {
const [options, setOptions] = useState(selected[0]?.tracerOptions ?? {}); const [options, setOptions] = useState(selected[0]?.tracerOptions ?? {});
const [updated, setUpdated] = useState(false);
useEffect(() => { useEffect(() => {
setOptions(selected[0]?.tracerOptions ?? {}); setOptions(selected[0]?.tracerOptions ?? {});
}, [selected.map(e => e.id).join("")]); setUpdated(true);
}, [selected.map(e => `${e.id}${JSON.stringify(e.svgData.options)}`).join("")]);
const setValue = (key: keyof TracerOptions, value: number) => { const redraw = () => {
setOptions({ ...options, [key]: value });
selected.forEach(e => { selected.forEach(e => {
e.setTracerSvg({ ...options, [key]: value }); e.setTracerSvg({ ...options });
}); });
setUpdated(true);
}; };
return <tbody> const setValue = (key: keyof TracerOptions, value: any) => {
<NumericInputWithLabelTable setOptions({ ...options, [key]: value });
name="Ltres" setUpdated(false);
value={options.ltres} };
onChange={value => setValue("ltres", value)}
/> return <WrapperRow>
<NumericInputWithLabelTable <VerticalSlider
name="Qtres" max={255}
value={options.qtres} min={0}
onChange={value => setValue("qtres", value)} value={options.transparencyThreshold ?? TRANSPARENCY_THRESHOLD_DEFAULT}
/> onLabelClick={async () => {
<NumericInputWithLabelTable const value = await handler.popup.prompt(
name="Path omit" "Transparency threshold",
value={options.pathomit} "Enter transparency threshold between 0-255",
onChange={value => setValue("pathomit", value)} (options.transparencyThreshold ?? TRANSPARENCY_THRESHOLD_DEFAULT).toString());
/> if (value) {
<NumericInputWithLabelTable const int = parseInt(value, 10);
name="Round corners" if (!isNaN(int)) {
value={options.roundcoords} setValue("transparencyThreshold", clamp(int, 0, 255));
onChange={value => setValue("roundcoords", value)} }
/> }
</tbody>; }}
onChange={value => {
setValue("transparencyThreshold", value);
}} />
<WrapperColumn>
<Button $active={!updated} title="Draw" onClick={() => { redraw(); }}><FaBezierCurve /></Button>
<WrapperRow>
<Table>
<tbody>
<NumericInputWithLabelTable
name="Ltres"
value={options.ltres}
onChange={value => setValue("ltres", value)}
/>
<NumericInputWithLabelTable
name="Qtres"
value={options.qtres}
onChange={value => setValue("qtres", value)}
/>
<NumericInputWithLabelTable
name="Path omit"
value={options.pathomit}
onChange={value => setValue("pathomit", value)}
/>
<NumericInputWithLabelTable
name="Round corners"
value={options.roundcoords}
onChange={value => setValue("roundcoords", value)}
/>
</tbody>
</Table >
<LayerPanel>
{(options.allowedPaths || []).map((e, i) => {
return <ToggleButton
key={i}
$enabled={e}
onClick={() => {
const array = options?.allowedPaths ? [...options.allowedPaths] : [];
array[i] = !array[i];
setValue("allowedPaths", array);
}}
>
{`${i + 1} Layer`}
</ToggleButton>;
})}
</LayerPanel>
</WrapperRow>
</WrapperColumn></WrapperRow>;
} }
+137
View File
@@ -0,0 +1,137 @@
import React, { useState } from "react";
import styled from "styled-components";
const SliderWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 120px;
width: 40px;
gap: 8px;
`;
interface SliderProps {
min?: number;
max?: number;
step?: number;
value?: number;
onChange?: (value: number) => void;
}
interface TrackTransientProps {
$fillPercent: number;
}
const Track = styled.input.attrs({ type: "range" }) <TrackTransientProps>`
appearance: none;
-webkit-appearance: none;
writing-mode: vertical-lr;
direction: rtl;
width: 6px;
height: 100%;
background: linear-gradient(
to top,
white ${({ $fillPercent }) => $fillPercent}%,
var(--secondary-color) ${({ $fillPercent }) => $fillPercent}%
);
border-radius: 0;
outline: none;
cursor: pointer;
transition: background 0.15s ease;
&::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 22px;
height: 22px;
border-radius: 0;
background: #ffffff;
border: 3px solid var(--secondary-color);
box-shadow: 0 0 6px rgba(255, 68, 68, 0.6);
cursor: grab;
transition: transform 0.1s ease, box-shadow 0.1s ease;
&:active {
cursor: grabbing;
transform: scale(1.2);
box-shadow: 0 0 12px rgba(255, 68, 68, 0.9);
}
}
&::-moz-range-thumb {
width: 22px;
height: 22px;
border-radius: 0;
background: #ffffff;
border: 3px solid #ff4444;
box-shadow: 0 0 6px rgba(255, 68, 68, 0.6);
cursor: grab;
transition: transform 0.1s ease;
&:active {
cursor: grabbing;
transform: scale(1.2);
}
}
&::-moz-range-track {
background: transparent;
}
`;
const ValueLabel = styled.span`
font-size: 13px;
color: white;
letter-spacing: 0.05em;
`;
interface SliderProps {
min?: number;
max?: number;
step?: number;
value?: number;
onLabelClick?: () => void;
onChange?: (value: number) => void;
}
export function VerticalSlider({
min = 0,
max = 100,
step = 1,
onLabelClick,
value: controlledValue,
onChange,
}: SliderProps) {
const [internalValue, setInternalValue] = useState(
controlledValue ?? Math.floor((max - min) / 2)
);
const value = controlledValue ?? internalValue;
const fillPercent = ((value - min) / (max - min)) * 100;
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const next = Number(e.target.value);
setInternalValue(next);
onChange?.(next);
};
return (
<SliderWrapper>
<ValueLabel onDoubleClick={() => {
onLabelClick?.();
}}>{value}</ValueLabel>
<Track
min={min}
max={max}
step={step}
value={value}
$fillPercent={fillPercent}
onChange={handleChange}
/>
</SliderWrapper>
);
};
+3
View File
@@ -46,6 +46,9 @@ export class ImageEditor {
get svg() { get svg() {
return this.query().svg.data; return this.query().svg.data;
} }
get svgData() {
return this.query().svg;
}
setName(name: string) { setName(name: string) {
this.onPropsChange({ name }); this.onPropsChange({ name });
} }
+6 -8
View File
@@ -7,7 +7,7 @@ import { ImageEditor } from "./image-editor";
import { ToolHandler } from "./handler-tools"; import { ToolHandler } from "./handler-tools";
import { GlobalSettingsHandler } from "./handler-global-settings"; import { GlobalSettingsHandler } from "./handler-global-settings";
import { Popup } from "./popup"; import { Popup } from "./popup";
import { imageToLaserSVG } from "../svg-tracer"; import { imageToOutlineSVG } from "../svg-tracer";
import { urlToImage, canvasToBlob, imageToCanvas } from "../utils/image"; import { urlToImage, canvasToBlob, imageToCanvas } from "../utils/image";
import { loadFileAsDataUrl, makeFilenameSafe } from "../utils/generic"; import { loadFileAsDataUrl, makeFilenameSafe } from "../utils/generic";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
@@ -242,9 +242,7 @@ export class VectraceHandler {
async importFile(file: File) { async importFile(file: File) {
const buffer = await loadFileAsDataUrl(file); const buffer = await loadFileAsDataUrl(file);
const imageElement = await urlToImage(buffer); const imageElement = await urlToImage(buffer);
const options: TracerOptions = {}; const { svg, options } = imageToOutlineSVG(imageElement, {
const svg = imageToLaserSVG(imageElement, {
...options,
scale: 1, scale: 1,
strokewidth: this.STROKE_WIDTH strokewidth: this.STROKE_WIDTH
}); });
@@ -375,17 +373,17 @@ export class VectraceHandler {
const base = images.image; const base = images.image;
const scale = rendered.scale; const scale = rendered.scale;
const options: TracerOptions = { const oldOptions: TracerOptions = {
...rendered.svg.options, ...rendered.svg.options,
scale, scale,
strokewidth: this.STROKE_WIDTH strokewidth: this.STROKE_WIDTH
}; };
const prevOptions = cloneDeep(options); const prevOptions = cloneDeep(oldOptions);
const svg = imageToLaserSVG(rendered.image, options); const { svg, options } = imageToOutlineSVG(rendered.image, oldOptions);
const dirty = !isEqual(prevOptions, options); const dirty = !isEqual(prevOptions, oldOptions);
rendered.svg.dirty = dirty; rendered.svg.dirty = dirty;
base.svg.dirty = dirty; base.svg.dirty = dirty;
+33 -1
View File
@@ -3,6 +3,7 @@
--primary-color: #474747; --primary-color: #474747;
--secondary-color: #252525; --secondary-color: #252525;
} }
@font-face { @font-face {
font-family: 'tiny5'; font-family: 'tiny5';
src: url('./assets/fonts/Tiny5-Regular.ttf') format('truetype'); src: url('./assets/fonts/Tiny5-Regular.ttf') format('truetype');
@@ -13,6 +14,8 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
font-family: tiny5; font-family: tiny5;
scrollbar-width: thin;
scrollbar-color: var(--primary-color) var(--secondary-color);
} }
html,body,#root { html,body,#root {
@@ -27,4 +30,33 @@ html,body,#root {
#root { #root {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--secondary-color);
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: var(--primary-color);
border-radius: 10px;
border: 2px solid var(--secondary-color);
transition: background 0.3s ease;
}
::-webkit-scrollbar-thumb:hover {
background: #6a6a6a;
}
::-webkit-scrollbar-thumb:active {
background: #888888;
}
::-webkit-scrollbar-corner {
background: var(--secondary-color);
}
+2 -1
View File
@@ -6,11 +6,12 @@ export interface ObjectID {
export interface TracerOptions extends ImageTracerOptions { export interface TracerOptions extends ImageTracerOptions {
transparencyThreshold?: number; transparencyThreshold?: number;
allowedPaths?: boolean[];
} }
export interface SvgData { export interface SvgData {
svg: string; svg: string;
normalizedSvg: string; options: TracerOptions;
} }
export interface ProjectConfig { export interface ProjectConfig {
+3 -3
View File
@@ -13,7 +13,7 @@ import { ImportExportButtons } from "./components/download-buttons";
const Bar = styled.div` const Bar = styled.div`
width: 100%; width: 100%;
overflow: auto; overflow: auto;
height: 110px; height: 125px;
background-color: var(--primary-color); background-color: var(--primary-color);
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -24,7 +24,7 @@ const ImageBox = styled.table`
padding-right: 4px; padding-right: 4px;
`; `;
const SvgOption = styled.table` const SvgOption = styled.div`
margin: 2px; margin: 2px;
padding-right: 4px; padding-right: 4px;
`; `;
@@ -92,7 +92,7 @@ export function NavBar({ handler }: CanvasHandlerProps) {
</tbody> </tbody>
</ImageBox> </ImageBox>
<SvgOption style={style}> <SvgOption style={style}>
<SvgTracerOptions selected={selected} /> <SvgTracerOptions selected={selected} handler={handler} />
</SvgOption> </SvgOption>
<Line /> <Line />
<ImportExportButtons handler={handler} selected={selected} /> <ImportExportButtons handler={handler} selected={selected} />
+25 -15
View File
@@ -1,9 +1,9 @@
import ImageTracer from "imagetracerjs"; import ImageTracer from "imagetracerjs";
import type { TracerOptions } from "./interface"; import type { SvgData, TracerOptions } from "./interface";
import { VERSION } from "./constants"; import { VERSION } from "./constants";
export const TRANSPARENCY_THRESHOLD_DEFAULT = 50;
export function imageToBlackWhiteImageData(img: HTMLImageElement, transparencyThreshold: number = 50): ImageData { export function imageToBlackWhiteImageData(img: HTMLImageElement, transparencyThreshold: number = TRANSPARENCY_THRESHOLD_DEFAULT): ImageData {
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
canvas.width = img.naturalWidth; canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight; canvas.height = img.naturalHeight;
@@ -27,16 +27,19 @@ export function imageToBlackWhiteImageData(img: HTMLImageElement, transparencyTh
return imageData; return imageData;
} }
export function imageToLaserSVG(img: HTMLImageElement, options?: TracerOptions): string { export function imageToOutlineSVG(img: HTMLImageElement, options?: TracerOptions): SvgData {
const bwData = imageToBlackWhiteImageData(img, options?.transparencyThreshold); const newOptions: TracerOptions = {
const rawSVG = ImageTracer.imagedataToSVG(bwData, {
scale: 1, scale: 1,
...options, ...options,
viewbox: true, viewbox: true,
desc: false, desc: false,
layering: 0 layering: 0
}); };
const bwData = imageToBlackWhiteImageData(img, newOptions?.transparencyThreshold);
const rawSVG = ImageTracer.imagedataToSVG(bwData, newOptions);
const parser = new DOMParser(); const parser = new DOMParser();
const doc = parser.parseFromString(rawSVG, "image/svg+xml"); const doc = parser.parseFromString(rawSVG, "image/svg+xml");
@@ -44,21 +47,28 @@ export function imageToLaserSVG(img: HTMLImageElement, options?: TracerOptions):
const [_, __, width, height] = svg.getAttribute("viewBox")!.split(" "); const [_, __, width, height] = svg.getAttribute("viewBox")!.split(" ");
svg.setAttribute("width", width); svg.setAttribute("width", width);
svg.setAttribute("height", height); svg.setAttribute("height", height);
svg.setAttribute("desc", `Created with pathshop ${VERSION}v`); svg.setAttribute("desc", `Created with vectrace ${VERSION}v`);
const paths = [...doc.querySelectorAll("path")]; const paths = [...doc.querySelectorAll("path")];
// getting global path. Removing it as it creates noise const allowedPaths = newOptions?.allowedPaths ? paths.map((_, i) => newOptions?.allowedPaths?.[i] ?? true) : paths.map(() => true);
const longest = paths.sort((a, b) => (a.getAttribute("d") || "").length < (b.getAttribute("d") || "").length ? 1 : -1)[0];
console.log(paths, longest); for (let i = 0; i < paths.length; i++) {
for (const path of paths) { const path = paths[i];
path.setAttribute("fill", "none"); path.setAttribute("fill", "none");
path.setAttribute("opacity", "1"); path.setAttribute("opacity", "1");
path.setAttribute("stroke", "#000000"); path.setAttribute("stroke", "#000000");
if (longest === path) { if (!allowedPaths[i]) {
path.remove(); path.remove();
} }
} }
return new XMLSerializer().serializeToString(doc); if (newOptions) {
newOptions.allowedPaths = allowedPaths;
}
return {
svg: new XMLSerializer().serializeToString(doc),
options: newOptions,
};
} }