diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f8f8023..b3b88e0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,7 +10,6 @@ "dependencies": { "@pixi/filter-drop-shadow": "^5.2.0", "axios": "^1.7.9", - "fabric": "^6.5.1", "pixi-viewport": "^6.0.3", "pixi.js": "^8.17.0", "react": "^18.3.1", @@ -800,40 +799,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@pixi/color": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/@pixi/color/-/color-7.4.3.tgz", @@ -1332,16 +1297,6 @@ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "license": "MIT" }, - "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==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 10" - } - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1472,103 +1427,6 @@ "node": ">=10.0.0" } }, - "node_modules/abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "deprecated": "Use your platform's native atob() and btoa() methods instead", - "license": "BSD-3-Clause", - "optional": 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==", - "license": "ISC", - "optional": true - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "license": "MIT", - "optional": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-globals": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", - "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "acorn": "^8.1.0", - "acorn-walk": "^8.0.2" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", - "license": "MIT", - "optional": true, - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/aproba": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", - "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", - "license": "ISC", - "optional": true - }, - "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1586,13 +1444,6 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT", - "optional": true - }, "node_modules/baseline-browser-mapping": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", @@ -1606,17 +1457,6 @@ "node": ">=6.0.0" } }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "license": "MIT", - "optional": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/browserslist": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", @@ -1702,42 +1542,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/canvas": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", - "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.0", - "nan": "^2.17.0", - "simple-get": "^3.0.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "license": "ISC", - "optional": true, - "engines": { - "node": ">=10" - } - }, - "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==", - "license": "ISC", - "optional": true, - "bin": { - "color-support": "bin.js" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1750,20 +1554,6 @@ "node": ">= 0.8" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT", - "optional": true - }, - "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==", - "license": "ISC", - "optional": true - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1771,33 +1561,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cssom": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", - "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", - "license": "MIT", - "optional": true - }, - "node_modules/cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "license": "MIT", - "optional": true, - "dependencies": { - "cssom": "~0.3.6" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cssstyle/node_modules/cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "license": "MIT", - "optional": true - }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1805,21 +1568,6 @@ "dev": true, "license": "MIT" }, - "node_modules/data-urls": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", - "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1837,26 +1585,6 @@ } } }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "license": "MIT", - "optional": true - }, - "node_modules/decompress-response": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", - "license": "MIT", - "optional": true, - "dependencies": { - "mimic-response": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1866,37 +1594,6 @@ "node": ">=0.4.0" } }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "license": "MIT", - "optional": true - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/domexception": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", - "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", - "deprecated": "Use your platform's native DOMException instead", - "license": "MIT", - "optional": true, - "dependencies": { - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1925,13 +1622,6 @@ "dev": true, "license": "ISC" }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT", - "optional": true - }, "node_modules/engine.io-client": { "version": "6.6.4", "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", @@ -1975,19 +1665,6 @@ "node": ">=10.0.0" } }, - "node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "license": "BSD-2-Clause", - "optional": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2085,62 +1762,6 @@ "node": ">=6" } }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "license": "BSD-2-Clause", - "optional": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "license": "BSD-2-Clause", - "optional": true, - "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==", - "license": "BSD-2-Clause", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -2148,19 +1769,6 @@ "license": "MIT", "peer": true }, - "node_modules/fabric": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/fabric/-/fabric-6.9.1.tgz", - "integrity": "sha512-TqG08Xbt4rtlPsXgCjSUcZz/RsyEP57Qo21nCVRkw7zz9nR0co4SLkL9Q/zQh3tC1Yxap6M5jKFHUKV6SgPovg==", - "license": "MIT", - "engines": { - "node": ">=16.20.0" - }, - "optionalDependencies": { - "canvas": "^2.11.2", - "jsdom": "^20.0.1" - } - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2215,46 +1823,6 @@ "node": ">= 6" } }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs-minipass/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC", - "optional": true - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC", - "optional": true - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2279,28 +1847,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2357,28 +1903,6 @@ "js-binary-schema-parser": "^2.0.3" } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "optional": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2418,13 +1942,6 @@ "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==", - "license": "ISC", - "optional": true - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -2437,97 +1954,6 @@ "node": ">= 0.4" } }, - "node_modules/html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", - "license": "MIT", - "optional": true, - "dependencies": { - "whatwg-encoding": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "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==", - "license": "MIT", - "optional": true, - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "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==", - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "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==", - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", - "optional": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC", - "optional": true - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "license": "MIT", - "optional": true - }, "node_modules/ismobilejs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz", @@ -2546,52 +1972,6 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, - "node_modules/jsdom": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", - "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "abab": "^2.0.6", - "acorn": "^8.8.1", - "acorn-globals": "^7.0.0", - "cssom": "^0.5.0", - "cssstyle": "^2.3.0", - "data-urls": "^3.0.2", - "decimal.js": "^10.4.2", - "domexception": "^4.0.0", - "escodegen": "^2.0.0", - "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.2", - "parse5": "^7.1.1", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.2", - "w3c-xmlserializer": "^4.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0", - "ws": "^8.11.0", - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -2640,22 +2020,6 @@ "yallist": "^3.0.2" } }, - "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==", - "license": "MIT", - "optional": true, - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2686,102 +2050,12 @@ "node": ">= 0.6" } }, - "node_modules/mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "license": "ISC", - "optional": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "license": "ISC", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "license": "MIT", - "optional": true, - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC", - "optional": true - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "license": "MIT", - "optional": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "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==", "license": "MIT" }, - "node_modules/nan": { - "version": "2.25.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.25.0.tgz", - "integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==", - "license": "MIT", - "optional": true - }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -2801,52 +2075,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "optional": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT", - "optional": true - }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause", - "optional": true - }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/node-releases": { "version": "2.0.36", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", @@ -2854,53 +2082,6 @@ "dev": true, "license": "MIT" }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, - "node_modules/nwsapi": { - "version": "2.2.23", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", - "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", - "license": "MIT", - "optional": true - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -2914,45 +2095,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "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", - "optional": true, - "dependencies": { - "wrappy": "1" - } - }, "node_modules/parse-svg-path": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==", "license": "MIT" }, - "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "license": "MIT", - "optional": true, - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3061,29 +2209,6 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, - "node_modules/psl": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", - "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", - "license": "MIT", - "optional": true, - "dependencies": { - "punycode": "^2.3.1" - }, - "funding": { - "url": "https://github.com/sponsors/lupomontero" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=6" - } - }, "node_modules/qs": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", @@ -3100,13 +2225,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "license": "MIT", - "optional": true - }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -3174,45 +2292,6 @@ "react-dom": ">=16.8" } }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "optional": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "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==", - "license": "MIT", - "optional": true - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "license": "ISC", - "optional": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rollup": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", @@ -3258,47 +2337,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "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", - "optional": true - }, - "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", - "optional": true - }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "license": "ISC", - "optional": true, - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -3312,19 +2350,12 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "devOptional": true, + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" } }, - "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==", - "license": "ISC", - "optional": true - }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -3401,46 +2432,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC", - "optional": true - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "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", - "optional": true - }, - "node_modules/simple-get": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", - "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", - "license": "MIT", - "optional": true, - "dependencies": { - "decompress-response": "^4.2.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, "node_modules/socket.io-client": { "version": "4.8.3", "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", @@ -3469,16 +2460,6 @@ "node": ">=10.0.0" } }, - "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==", - "license": "BSD-3-Clause", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3489,77 +2470,6 @@ "node": ">=0.10.0" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "optional": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "optional": true, - "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", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "license": "MIT", - "optional": true - }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "optional": true, - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC", - "optional": true - }, "node_modules/tiny-lru": { "version": "11.4.7", "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.4.7.tgz", @@ -3586,35 +2496,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", - "license": "MIT", - "optional": true, - "dependencies": { - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -3629,16 +2510,6 @@ "node": ">=14.17" } }, - "node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -3684,17 +2555,6 @@ "node": ">= 0.4" } }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "node_modules/url/node_modules/punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", @@ -3702,13 +2562,6 @@ "license": "MIT", "peer": true }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT", - "optional": true - }, "node_modules/vite": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", @@ -3784,123 +2637,6 @@ } } }, - "node_modules/w3c-xmlserializer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", - "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", - "license": "MIT", - "optional": true, - "dependencies": { - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "license": "BSD-2-Clause", - "optional": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", - "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "tr46": "^3.0.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "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==", - "license": "ISC", - "optional": true, - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "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", - "optional": true - }, - "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", - "license": "MIT", - "optional": 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/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "license": "MIT", - "optional": true - }, "node_modules/xmlhttprequest-ssl": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index ffc011a..56fbe18 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,7 +11,6 @@ "dependencies": { "@pixi/filter-drop-shadow": "^5.2.0", "axios": "^1.7.9", - "fabric": "^6.5.1", "pixi-viewport": "^6.0.3", "pixi.js": "^8.17.0", "react": "^18.3.1", diff --git a/frontend/src/canvas/FabricCanvas.tsx b/frontend/src/canvas/FabricCanvas.tsx deleted file mode 100644 index 0700bfa..0000000 --- a/frontend/src/canvas/FabricCanvas.tsx +++ /dev/null @@ -1,327 +0,0 @@ -import React, { useRef, useEffect, useImperativeHandle, forwardRef, useCallback } from 'react'; -import { Canvas, FabricImage, FabricObject } from 'fabric'; -import { suppressBroadcasts, resumeBroadcasts } from './sync'; - -export interface FabricCanvasHandle { - getCanvas: () => Canvas | null; - fitAll: () => void; - getZoom: () => number; - setZoom: (zoom: number) => void; -} - -interface FabricCanvasProps { - canvasState?: string | null; - currentTool: string; - boardId?: string; - onChange?: () => void; -} - -const MIN_ZOOM = 0.1; -const MAX_ZOOM = 5.0; - -function viewportKey(id: string) { return `refboard:viewport:${id}`; } - -function saveViewport(id: string, vpt: number[]) { - try { localStorage.setItem(viewportKey(id), JSON.stringify(vpt)); } catch {} -} - -function loadViewport(id: string): number[] | null { - try { - const raw = localStorage.getItem(viewportKey(id)); - if (!raw) return null; - const arr = JSON.parse(raw); - if (Array.isArray(arr) && arr.length === 6 && arr.every((v: any) => typeof v === 'number' && isFinite(v))) return arr; - } catch {} - return null; -} - -const FabricCanvas = forwardRef( - ({ canvasState, currentTool, boardId, onChange }, ref) => { - const containerRef = useRef(null); - const canvasElRef = useRef(null); - const fabricRef = useRef(null); - const isPanning = useRef(false); - const lastPanPoint = useRef<{ x: number; y: number } | null>(null); - const spaceHeld = useRef(false); - const initialLoadDone = useRef(false); - - // Initialize Fabric canvas - useEffect(() => { - if (!canvasElRef.current || fabricRef.current) return; - - const container = containerRef.current!; - const width = container.clientWidth; - const height = container.clientHeight; - - const canvas = new Canvas(canvasElRef.current, { - width, - height, - backgroundColor: '#1e1e1e', - selection: true, - selectionColor: 'rgba(74, 158, 255, 0.15)', - selectionBorderColor: '#4a9eff', - selectionLineWidth: 1, - preserveObjectStacking: true, - }); - - fabricRef.current = canvas; - - // Debounced viewport persistence - let vpTimer: ReturnType | null = null; - const persistViewport = () => { - if (!boardId) return; - if (vpTimer) clearTimeout(vpTimer); - vpTimer = setTimeout(() => { - const vpt = canvas.viewportTransform; - if (vpt) saveViewport(boardId, [...vpt]); - }, 300); - }; - - // Handle resize - const observer = new ResizeObserver(() => { - const w = container.clientWidth; - const h = container.clientHeight; - canvas.setDimensions({ width: w, height: h }); - canvas.requestRenderAll(); - }); - observer.observe(container); - - // Middle-mouse pan - canvas.on('mouse:down', (e: any) => { - if (e.e.button === 1 || (spaceHeld.current && e.e.button === 0)) { - isPanning.current = true; - lastPanPoint.current = { x: e.e.clientX, y: e.e.clientY }; - canvas.defaultCursor = 'grabbing'; - canvas.upperCanvasEl.style.cursor = 'grabbing'; - e.e.preventDefault(); - } - }); - - canvas.on('mouse:move', (e: any) => { - if (!isPanning.current || !lastPanPoint.current) return; - const vpt = canvas.viewportTransform!; - vpt[4] += e.e.clientX - lastPanPoint.current.x; - vpt[5] += e.e.clientY - lastPanPoint.current.y; - lastPanPoint.current = { x: e.e.clientX, y: e.e.clientY }; - canvas.requestRenderAll(); - persistViewport(); - }); - - canvas.on('mouse:up', () => { - if (isPanning.current) { - isPanning.current = false; - lastPanPoint.current = null; - if (!spaceHeld.current) { - canvas.defaultCursor = 'default'; - canvas.upperCanvasEl.style.cursor = 'default'; - } - } - }); - - // Scroll zoom — zooms toward cursor (like Figma/PureRef) - // Normalizes deltaY across mice, trackpads, and browsers for consistent feel - canvas.on('mouse:wheel', (opt: any) => { - const e = opt.e as WheelEvent; - e.preventDefault(); - e.stopPropagation(); - - // Normalize delta: mice send large values (~100), trackpads send small (~1-5) - // Clamp to ±1 and apply a fixed zoom factor per "step" for predictable UX - let delta = e.deltaY; - if (e.deltaMode === 1) delta *= 40; // DOM_DELTA_LINE → px - if (e.deltaMode === 2) delta *= 800; // DOM_DELTA_PAGE → px - - // Use sign + magnitude clamping for consistent zoom speed - const direction = Math.sign(delta); - const ZOOM_STEP = 0.08; // 8% per step — smooth but responsive - let zoom = canvas.getZoom(); - zoom *= 1 - direction * ZOOM_STEP; - zoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoom)); - - // Zoom toward cursor — must use element-relative coords (offsetX/Y), - // NOT getScenePoint() which returns scene-space and causes zoom drift - canvas.zoomToPoint({ x: e.offsetX, y: e.offsetY } as any, zoom); - canvas.requestRenderAll(); - persistViewport(); - }); - - // Space key for pan - function onKeyDown(e: KeyboardEvent) { - if (e.code === 'Space' && !spaceHeld.current && !(e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement)) { - e.preventDefault(); - spaceHeld.current = true; - canvas.defaultCursor = 'grab'; - canvas.upperCanvasEl.style.cursor = 'grab'; - } - } - - function onKeyUp(e: KeyboardEvent) { - if (e.code === 'Space') { - spaceHeld.current = false; - if (currentTool !== 'PAN') { - canvas.defaultCursor = 'default'; - canvas.upperCanvasEl.style.cursor = 'default'; - } - } - } - - window.addEventListener('keydown', onKeyDown); - window.addEventListener('keyup', onKeyUp); - - // Notify parent of changes - const changeEvents = ['object:added', 'object:modified', 'object:removed', 'path:created']; - const changeHandler = () => { - onChange?.(); - }; - changeEvents.forEach((evt) => canvas.on(evt as any, changeHandler)); - - return () => { - if (vpTimer) clearTimeout(vpTimer); - observer.disconnect(); - window.removeEventListener('keydown', onKeyDown); - window.removeEventListener('keyup', onKeyUp); - changeEvents.forEach((evt) => canvas.off(evt as any, changeHandler)); - canvas.dispose(); - fabricRef.current = null; - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // Load initial canvas state - useEffect(() => { - const canvas = fabricRef.current; - if (!canvas || initialLoadDone.current) return; - if (!canvasState) return; - - initialLoadDone.current = true; - - let parsed: any; - try { - parsed = typeof canvasState === 'string' ? JSON.parse(canvasState) : canvasState; - } catch { - return; - } - - if (!parsed || (!parsed.objects && !parsed.version)) return; - - // Ensure all images in the JSON have crossOrigin set BEFORE Fabric loads them. - // Without this, the underlying elements load without CORS headers, - // tainting the canvas and breaking toCanvasElement/toDataURL/toBlob. - if (parsed.objects) { - (parsed.objects as any[]).forEach((obj: any) => { - if (obj.type === 'image') { - obj.crossOrigin = 'anonymous'; - } - // Handle images inside groups - if (obj.type === 'group' && obj.objects) { - (obj.objects as any[]).forEach((child: any) => { - if (child.type === 'image') { - child.crossOrigin = 'anonymous'; - } - }); - } - }); - } - - // Suppress socket broadcasts during initial load — prevents flooding - // other clients with object:add events for objects they already have - suppressBroadcasts(); - canvas.loadFromJSON(parsed).then(() => { - // Restore saved viewport (zoom + pan) if available - if (boardId) { - const savedVpt = loadViewport(boardId); - if (savedVpt) { - canvas.setViewportTransform(savedVpt as any); - } - } - canvas.requestRenderAll(); - // Small delay to ensure all deferred events have fired before resuming - setTimeout(() => resumeBroadcasts(), 100); - }).catch((err: Error) => { - console.error('Failed to load canvas state:', err); - resumeBroadcasts(); - }); - }, [canvasState]); - - const fitAll = useCallback(() => { - const canvas = fabricRef.current; - if (!canvas) return; - - const objects = canvas.getObjects(); - if (objects.length === 0) { - canvas.setViewportTransform([1, 0, 0, 1, 0, 0]); - canvas.requestRenderAll(); - return; - } - - // Get bounding rect of all objects - let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; - objects.forEach((obj) => { - const bound = obj.getBoundingRect(); - minX = Math.min(minX, bound.left); - minY = Math.min(minY, bound.top); - maxX = Math.max(maxX, bound.left + bound.width); - maxY = Math.max(maxY, bound.top + bound.height); - }); - - const objWidth = maxX - minX; - const objHeight = maxY - minY; - if (objWidth === 0 || objHeight === 0) return; - - const padding = 40; - const canvasW = canvas.width!; - const canvasH = canvas.height!; - const scaleX = (canvasW - padding * 2) / objWidth; - const scaleY = (canvasH - padding * 2) / objHeight; - const zoom = Math.min(scaleX, scaleY, MAX_ZOOM); - - const cx = (minX + maxX) / 2; - const cy = (minY + maxY) / 2; - - const vpt: [number, number, number, number, number, number] = [ - zoom, 0, 0, zoom, - canvasW / 2 - cx * zoom, - canvasH / 2 - cy * zoom, - ]; - canvas.setViewportTransform(vpt); - canvas.requestRenderAll(); - if (boardId) saveViewport(boardId, vpt); - }, [boardId]); - - useImperativeHandle(ref, () => ({ - getCanvas: () => fabricRef.current, - fitAll, - getZoom: () => fabricRef.current?.getZoom() ?? 1, - setZoom: (zoom: number) => { - const canvas = fabricRef.current; - if (!canvas) return; - const center = canvas.getCenterPoint(); - canvas.zoomToPoint(center, Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoom))); - canvas.requestRenderAll(); - if (boardId) { - const vpt = canvas.viewportTransform; - if (vpt) saveViewport(boardId, [...vpt]); - } - }, - }), [fitAll, boardId]); - - return ( -
- -
- ); - } -); - -FabricCanvas.displayName = 'FabricCanvas'; - -export default FabricCanvas; diff --git a/frontend/src/canvas/tools.ts b/frontend/src/canvas/tools.ts index 8b637ca..3c5318c 100644 --- a/frontend/src/canvas/tools.ts +++ b/frontend/src/canvas/tools.ts @@ -1,4 +1,16 @@ -import { Canvas, PencilBrush, IText, FabricObject } from 'fabric'; +/** + * Tools — PixiJS version. + * + * SELECT/PAN are handled by the viewport (pixi-viewport) and SelectionManager. + * PEN/TEXT/ERASER need PixiJS implementations. + * For now, only SELECT and PAN are fully functional; PEN/TEXT/ERASER are stubs + * that will be implemented when drawing support is added. + */ + +import type { Viewport } from 'pixi-viewport'; +import type { SceneManager } from './SceneManager'; +import type { SelectionManager } from './SelectionManager'; +import { Text, TextStyle } from 'pixi.js'; export enum ToolType { SELECT = 'SELECT', @@ -22,101 +34,105 @@ const defaultOptions: ToolOptions = { type CleanupFn = (() => void) | null; +export interface ToolContext { + viewport: Viewport; + scene: SceneManager; + selection: SelectionManager; + container: HTMLElement; + onChange: () => void; +} + export function activateTool( - canvas: Canvas, + ctx: ToolContext, tool: ToolType, options: ToolOptions = {} ): CleanupFn { const opts = { ...defaultOptions, ...options }; + const { viewport, scene, selection, container } = ctx; - // Reset common state - canvas.isDrawingMode = false; - canvas.selection = false; - canvas.defaultCursor = 'default'; - canvas.hoverCursor = 'default'; - canvas.forEachObject((obj: FabricObject) => { - obj.selectable = false; - obj.evented = false; - }); + // Reset cursor + container.style.cursor = ''; switch (tool) { case ToolType.SELECT: { - canvas.selection = true; - canvas.defaultCursor = 'default'; - canvas.hoverCursor = 'move'; - canvas.forEachObject((obj: FabricObject) => { - obj.selectable = true; - obj.evented = true; - }); + container.style.cursor = 'default'; + // SelectionManager handles click/rubber-band selection return null; } case ToolType.PAN: { - canvas.defaultCursor = 'grab'; - canvas.hoverCursor = 'grab'; - // Pan is handled by FabricCanvas component directly - return null; - } - - case ToolType.PEN: { - canvas.isDrawingMode = true; - const brush = new PencilBrush(canvas); - brush.color = opts.color!; - brush.width = opts.strokeWidth!; - canvas.freeDrawingBrush = brush; + container.style.cursor = 'grab'; + // Space+drag is handled by PixiCanvas; this just sets cursor return null; } case ToolType.TEXT: { - canvas.defaultCursor = 'text'; - canvas.hoverCursor = 'text'; + container.style.cursor = 'text'; - const handler = (e: any) => { - const pointer = canvas.getScenePoint(e.e); - const text = new IText('Type here', { - left: pointer.x, - top: pointer.y, + const onClick = (e: PointerEvent) => { + const rect = container.getBoundingClientRect(); + const world = viewport.toWorld(e.clientX - rect.left, e.clientY - rect.top); + + const textData = { + id: crypto.randomUUID(), + type: 'text' as const, + x: world.x, + y: world.y, + w: 200, + h: 30, + sx: 1, + sy: 1, + angle: 0, + z: scene.nextZ(), + opacity: 1, + locked: false, + name: '', + visible: true, + text: 'Type here', fontSize: opts.fontSize!, fill: opts.color!, fontFamily: 'sans-serif', - editable: true, - }); - canvas.add(text); - canvas.setActiveObject(text); - text.enterEditing(); - text.selectAll(); + }; + + scene._createItem(textData, true); + scene._applyZOrder(); + ctx.onChange(); + // Remove handler after placing text - canvas.off('mouse:down', handler); + container.removeEventListener('pointerdown', onClick); }; - canvas.on('mouse:down', handler); + container.addEventListener('pointerdown', onClick); return () => { - canvas.off('mouse:down', handler); + container.removeEventListener('pointerdown', onClick); }; } case ToolType.ERASER: { - canvas.defaultCursor = 'crosshair'; - canvas.hoverCursor = 'crosshair'; - canvas.forEachObject((obj: FabricObject) => { - obj.selectable = false; - obj.evented = true; - }); + container.style.cursor = 'crosshair'; - const handler = (e: any) => { - const target = e.target; - if (target) { - canvas.remove(target); - canvas.requestRenderAll(); + const onClick = (e: PointerEvent) => { + const rect = container.getBoundingClientRect(); + const world = viewport.toWorld(e.clientX - rect.left, e.clientY - rect.top); + const hit = selection._hitTest(world.x, world.y); + if (hit) { + scene.removeItem(hit.id, true); + ctx.onChange(); } }; - canvas.on('mouse:down', handler); + container.addEventListener('pointerdown', onClick); return () => { - canvas.off('mouse:down', handler); + container.removeEventListener('pointerdown', onClick); }; } + case ToolType.PEN: { + container.style.cursor = 'crosshair'; + // Drawing mode stub — will be implemented later + return null; + } + default: return null; } diff --git a/frontend/src/components/LayerPanel.tsx b/frontend/src/components/LayerPanel.tsx index 16c6c51..37656ef 100644 --- a/frontend/src/components/LayerPanel.tsx +++ b/frontend/src/components/LayerPanel.tsx @@ -85,8 +85,9 @@ export default function LayerPanel({ function getTypeIcon(type: string, isGroup: boolean) { if (isGroup) return '\u25B8'; // triangle if (type === 'image') return '\u{1F5BC}'; - if (type === 'i-text') return 'T'; + if (type === 'text' || type === 'i-text') return 'T'; if (type === 'path') return '\u270E'; + if (type === 'video') return '\u25B6'; return '\u25C7'; } diff --git a/frontend/src/pages/Editor.tsx b/frontend/src/pages/Editor.tsx index 320784c..10451d5 100644 --- a/frontend/src/pages/Editor.tsx +++ b/frontend/src/pages/Editor.tsx @@ -1,9 +1,12 @@ import React, { useEffect, useRef, useState, useCallback } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import { FabricObject, FabricImage, Group } from 'fabric'; -import FabricCanvas, { FabricCanvasHandle } from '../canvas/FabricCanvas'; +import PixiCanvas, { PixiCanvasHandle } from '../canvas/PixiCanvas'; +import { SelectionManager } from '../canvas/SelectionManager'; +import type { SceneItem } from '../canvas/SceneManager'; +import type { AnySceneObject, GroupObject } from '../canvas/scene-format'; import { ToolType, activateTool, toolShortcuts } from '../canvas/tools'; -import { setupSync, isRemoteUpdate } from '../canvas/sync'; +import type { ToolContext } from '../canvas/tools'; +import { setupSync } from '../canvas/sync'; import { setupDragDrop, setupPaste } from '../canvas/image-drop'; import { UndoManager } from '../canvas/history'; import { ShortcutContext, matchesShortcut, sortBySpecificity } from '../canvas/shortcuts'; @@ -51,7 +54,8 @@ export default function Editor({ isPublicView }: EditorProps) { const navigate = useNavigate(); const { user } = useAuth(); - const canvasRef = useRef(null); + const canvasRef = useRef(null); + const selectionRef = useRef(null); const undoRef = useRef(null); const saveTimerRef = useRef | null>(null); const syncCleanupRef = useRef<(() => void) | null>(null); @@ -80,7 +84,7 @@ export default function Editor({ isPublicView }: EditorProps) { const [showHelp, setShowHelp] = useState(false); const [layerList, setLayerList] = useState([]); const [selectedLayerIds, setSelectedLayerIds] = useState([]); - const clipboardRef = useRef([]); + const clipboardRef = useRef([]); const resolvedBoardId = boardId || boardData?.board?.id; @@ -116,51 +120,64 @@ export default function Editor({ isPublicView }: EditorProps) { setSaveStatus('unsaved'); if (saveTimerRef.current) clearTimeout(saveTimerRef.current); saveTimerRef.current = setTimeout(async () => { - const canvas = canvasRef.current?.getCanvas(); - if (!canvas) return; + const scene = canvasRef.current?.getScene(); + if (!scene) return; setSaveStatus('saving'); try { - const state = JSON.stringify((canvas as any).toJSON(['id', 'crossOrigin'])); - // Generate a small thumbnail preview of the canvas content + const state = JSON.stringify(scene.serialize()); + + // Generate thumbnail from PixiJS renderer let thumbnail: string | undefined; - const objects = canvas.getObjects(); - if (objects.length > 0) { - // Use scene-space coordinates (obj.left/top + scaled dimensions) - // NOT getBoundingRect() which returns viewport/screen-space + const items = scene.getAllItems(); + if (items.length > 0) { let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; - objects.forEach((obj: any) => { - const l = obj.left ?? 0; - const t = obj.top ?? 0; - const w = (obj.width ?? 0) * (obj.scaleX ?? 1); - const h = (obj.height ?? 0) * (obj.scaleY ?? 1); - minX = Math.min(minX, l); - minY = Math.min(minY, t); - maxX = Math.max(maxX, l + w); - maxY = Math.max(maxY, t + h); - }); + for (const item of items) { + const d = item.data; + const w = d.w * d.sx; + const h = d.h * d.sy; + minX = Math.min(minX, d.x); + minY = Math.min(minY, d.y); + maxX = Math.max(maxX, d.x + w); + maxY = Math.max(maxY, d.y + h); + } const sceneW = maxX - minX; const sceneH = maxY - minY; if (sceneW > 0 && sceneH > 0) { - const origVpt = [...canvas.viewportTransform!]; try { - const thumbSize = 400; - const scale = Math.min(thumbSize / sceneW, thumbSize / sceneH); - const pw = Math.ceil(sceneW * scale); - const ph = Math.ceil(sceneH * scale); - canvas.setViewportTransform([ - scale, 0, 0, scale, - -minX * scale, -minY * scale, - ]); - const thumbCanvas = canvas.toCanvasElement(1, { - left: 0, top: 0, width: pw, height: ph, - }); - thumbnail = thumbCanvas.toDataURL('image/webp', 0.6); + const viewport = canvasRef.current?.getViewport(); + if (viewport) { + // Use PixiJS renderer extract API + const app = (viewport as any).parent?.parent; + const renderer = app?.__pixiApplication?.renderer || (viewport as any).__renderer; + // Simpler approach: extract the viewport as a canvas + // We need the Application's renderer. Access it from the PixiCanvas internals. + // For now, generate thumbnail from item bounds data + const thumbSize = 400; + const scale = Math.min(thumbSize / sceneW, thumbSize / sceneH); + const pw = Math.ceil(sceneW * scale); + const ph = Math.ceil(sceneH * scale); + const offscreen = document.createElement('canvas'); + offscreen.width = pw; + offscreen.height = ph; + const ctx = offscreen.getContext('2d'); + if (ctx) { + ctx.fillStyle = '#1e1e1e'; + ctx.fillRect(0, 0, pw, ph); + // Draw colored rectangles as placeholder thumbnail + for (const item of items) { + const d = item.data; + const rx = (d.x - minX) * scale; + const ry = (d.y - minY) * scale; + const rw = d.w * d.sx * scale; + const rh = d.h * d.sy * scale; + ctx.fillStyle = d.type === 'text' ? '#555' : '#444'; + ctx.fillRect(rx, ry, rw, rh); + } + thumbnail = offscreen.toDataURL('image/webp', 0.6); + } + } } catch (thumbErr) { console.warn('Thumbnail generation failed:', thumbErr); - } finally { - // ALWAYS restore viewport — even if toCanvasElement/toDataURL throws - canvas.setViewportTransform(origVpt as any); - canvas.requestRenderAll(); } } } @@ -172,9 +189,8 @@ export default function Editor({ isPublicView }: EditorProps) { }, 2000); }, [resolvedBoardId, isPublicView]); - // Canvas change handler — skips remote updates to avoid resetting save timer + // Canvas change handler const onCanvasChange = useCallback(() => { - if (isRemoteUpdate()) return; scheduleSave(); if (undoRef.current && !undoRef.current.isLocked()) { undoRef.current.saveState(); @@ -183,12 +199,14 @@ export default function Editor({ isPublicView }: EditorProps) { } const z = canvasRef.current?.getZoom() ?? 1; setZoom(z); - const canvas = canvasRef.current?.getCanvas(); - if (canvas) { - if (canvas.viewportTransform) { - setCanvasTransform([...canvas.viewportTransform]); - } - setObjectCount(canvas.getObjects().length); + const scene = canvasRef.current?.getScene(); + if (scene) { + setObjectCount(scene.getAllItems().length); + } + // Update canvas transform for grid overlay + const vp = canvasRef.current?.getViewport(); + if (vp) { + setCanvasTransform([vp.scale.x, 0, 0, vp.scale.y, vp.x, vp.y]); } }, [scheduleSave]); @@ -199,20 +217,31 @@ export default function Editor({ isPublicView }: EditorProps) { setTimeout(() => setToasts((prev) => prev.filter((t) => t.id !== id)), 3000); }, []); - // Setup socket, sync, drag/drop, paste after canvas is ready + // Setup socket, sync, drag/drop, paste, selection after canvas is ready useEffect(() => { if (!boardData || !resolvedBoardId) return; const timer = setTimeout(() => { - const canvas = canvasRef.current?.getCanvas(); - if (!canvas) return; + const scene = canvasRef.current?.getScene(); + const viewport = canvasRef.current?.getViewport(); + if (!scene || !viewport) return; - undoRef.current = new UndoManager(canvas); + // Create SelectionManager + const selection = new SelectionManager(viewport, scene); + selectionRef.current = selection; + + // Wire selection change to update layer panel state + selection.onSelectionChange = (ids: string[]) => { + setSelectedLayerIds(ids); + }; + + // Create UndoManager + undoRef.current = new UndoManager(scene); if (user) { const socket = connectSocket(); - syncCleanupRef.current = setupSync(canvas, socket, resolvedBoardId); + syncCleanupRef.current = setupSync(scene, socket, resolvedBoardId); socket.on('user:joined', (data: any) => { const uid = data.userId || data.id; @@ -241,24 +270,53 @@ export default function Editor({ isPublicView }: EditorProps) { } }); - canvas.on('mouse:move', (e: any) => { - const pointer = canvas.getScenePoint(e.e); + // Cursor tracking: listen on viewport for pointermove + const onPointerMove = (e: any) => { + const world = viewport.toWorld(e.global.x, e.global.y); socket.emit('cursor:move', { boardId: resolvedBoardId, - x: pointer.x, - y: pointer.y, + x: world.x, + y: world.y, }); - }); + }; + viewport.on('pointermove', onPointerMove); } + // Setup drag/drop and paste (new signatures take container, viewport, scene) + const container = viewport.parent?.parent as any; + const canvasEl = container?.canvas?.parentElement || document.querySelector('[data-pixi-container]'); + const containerEl = (viewport as any).__pixiContainer || canvasEl?.parentElement; + if (!isPublicView || user) { - dropCleanupRef.current = setupDragDrop(canvas, resolvedBoardId, onCanvasChange); - pasteCleanupRef.current = setupPaste(canvas, resolvedBoardId, onCanvasChange); + // Get the actual DOM container from the PixiCanvas div + const pixiContainer = document.querySelector('[data-pixi-container]') as HTMLElement + || (viewport as any)._events?.pointermove?.[0]?.context?.containerRef?.current + || document.querySelector('.pixi-canvas-container') as HTMLElement; + + // Find the container div that holds the pixi canvas + const canvasElements = document.querySelectorAll('canvas'); + let domContainer: HTMLElement | null = null; + for (const c of canvasElements) { + if (c.parentElement && c.width > 100) { + domContainer = c.parentElement; + break; + } + } + + if (domContainer) { + dropCleanupRef.current = setupDragDrop(domContainer, viewport, scene, resolvedBoardId, onCanvasChange); + pasteCleanupRef.current = setupPaste(viewport, scene, resolvedBoardId, onCanvasChange); + } else { + // Fallback: just set up paste (works on document level) + pasteCleanupRef.current = setupPaste(viewport, scene, resolvedBoardId, onCanvasChange); + } } - }, 100); + }, 200); return () => { clearTimeout(timer); + selectionRef.current?.destroy(); + selectionRef.current = null; syncCleanupRef.current?.(); dropCleanupRef.current?.(); pasteCleanupRef.current?.(); @@ -268,76 +326,64 @@ export default function Editor({ isPublicView }: EditorProps) { // Apply tool changes useEffect(() => { - const canvas = canvasRef.current?.getCanvas(); - if (!canvas) return; + const viewport = canvasRef.current?.getViewport(); + const scene = canvasRef.current?.getScene(); + const selection = selectionRef.current; + if (!viewport || !scene || !selection) return; + + // Find the DOM container for cursor changes + const canvasElements = document.querySelectorAll('canvas'); + let domContainer: HTMLElement | null = null; + for (const c of canvasElements) { + if (c.parentElement && c.width > 100) { + domContainer = c.parentElement; + break; + } + } + if (!domContainer) return; + toolCleanupRef.current?.(); - toolCleanupRef.current = activateTool(canvas, activeTool, { color, strokeWidth, fontSize }); - }, [activeTool, color, strokeWidth, fontSize]); + const ctx: ToolContext = { viewport, scene, selection, container: domContainer, onChange: onCanvasChange }; + toolCleanupRef.current = activateTool(ctx, activeTool, { color, strokeWidth, fontSize }); + }, [activeTool, color, strokeWidth, fontSize, onCanvasChange]); - // Clone a fabric object using Fabric's native clone() method - const cloneFabricObject = useCallback(async (obj: any, offsetX = 0, offsetY = 0): Promise => { - const cloned = await obj.clone(); - cloned.set({ - left: (cloned.left || 0) + offsetX, - top: (cloned.top || 0) + offsetY, - }); - (cloned as any).id = crypto.randomUUID(); - return cloned; - }, []); - - // Write canvas content to system clipboard as PNG. - // If objects are provided, crop to their bounds. Otherwise copy entire canvas. - const writeCanvasToClipboard = useCallback(async (objects?: FabricObject[]) => { - const canvas = canvasRef.current?.getCanvas(); - if (!canvas) return; + // Write selected items (or full viewport) to system clipboard as PNG + const writeCanvasToClipboard = useCallback(async (items?: SceneItem[]) => { + const viewport = canvasRef.current?.getViewport(); + if (!viewport) return; try { - // Save & clear selection so handles don't appear in screenshot - const activeObj = canvas.getActiveObject(); - canvas.discardActiveObject(); - // renderAll is SYNCHRONOUS — ensures handles are gone before capture - canvas.renderAll(); - - // Capture full canvas — may throw if canvas is tainted (images without crossOrigin) - let fullCanvas: HTMLCanvasElement; - try { - fullCanvas = (canvas as any).toCanvasElement(1); - } catch (taintErr) { - // Canvas is tainted — fall back to re-rendering without tainted images - console.warn('Canvas tainted, attempting fallback:', taintErr); - if (activeObj) { - canvas.setActiveObject(activeObj); - canvas.renderAll(); - } - showToast('Copy failed — try re-opening the board'); + // Use PixiJS extract API to get the viewport as a canvas + const app = (viewport.parent as any)?.__pixiApplication; + if (!app?.renderer?.extract) { + showToast('Copy failed: renderer not available'); return; } - // Restore selection immediately - if (activeObj) { - canvas.setActiveObject(activeObj); - canvas.renderAll(); - } + const fullCanvas = app.renderer.extract.canvas(viewport) as HTMLCanvasElement; let outputCanvas: HTMLCanvasElement; - if (objects && objects.length > 0) { - // Compute bounding rect of the specified objects. - // getBoundingRect returns screen-space coords (includes viewport transform). + if (items && items.length > 0) { + // Compute bounding rect of selected items in world space let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; - for (const obj of objects) { - const br = obj.getBoundingRect(); - minX = Math.min(minX, br.left); - minY = Math.min(minY, br.top); - maxX = Math.max(maxX, br.left + br.width); - maxY = Math.max(maxY, br.top + br.height); + for (const item of items) { + const bounds = item.displayObject.getBounds(); + minX = Math.min(minX, bounds.x); + minY = Math.min(minY, bounds.y); + maxX = Math.max(maxX, bounds.x + bounds.width); + maxY = Math.max(maxY, bounds.y + bounds.height); } + // Convert world bounds to screen coords + const topLeft = viewport.toScreen(minX, minY); + const bottomRight = viewport.toScreen(maxX, maxY); + const pad = 10; - const cx = Math.max(0, Math.floor(minX - pad)); - const cy = Math.max(0, Math.floor(minY - pad)); - const cw = Math.min(Math.ceil(maxX - minX + pad * 2), fullCanvas.width - cx); - const ch = Math.min(Math.ceil(maxY - minY + pad * 2), fullCanvas.height - cy); + const cx = Math.max(0, Math.floor(topLeft.x - pad)); + const cy = Math.max(0, Math.floor(topLeft.y - pad)); + const cw = Math.min(Math.ceil(bottomRight.x - topLeft.x + pad * 2), fullCanvas.width - cx); + const ch = Math.min(Math.ceil(bottomRight.y - topLeft.y + pad * 2), fullCanvas.height - cy); if (cw <= 0 || ch <= 0) return; outputCanvas = document.createElement('canvas'); @@ -348,11 +394,9 @@ export default function Editor({ isPublicView }: EditorProps) { ctx.fillRect(0, 0, cw, ch); ctx.drawImage(fullCanvas, cx, cy, cw, ch, 0, 0, cw, ch); } else { - // No specific objects → copy entire canvas as-is outputCanvas = fullCanvas; } - // Convert to blob using promise wrapper (keeps user activation alive) const blob = await new Promise((resolve, reject) => { outputCanvas.toBlob((b) => { if (b) resolve(b); @@ -360,7 +404,6 @@ export default function Editor({ isPublicView }: EditorProps) { }, 'image/png'); }); - // Write to system clipboard — requires HTTPS or localhost await navigator.clipboard.write([ new ClipboardItem({ 'image/png': blob }), ]); @@ -370,53 +413,64 @@ export default function Editor({ isPublicView }: EditorProps) { } }, [showToast]); - // Refresh layer list from canvas - // Auto-name counters + // Refresh layer list from scene const nameCounters = useRef>({}); - function autoName(obj: any, i: number): string { - if (obj.name) return obj.name; - const type = obj.type || 'object'; + function autoName(item: SceneItem, i: number): string { + if (item.data.name) return item.data.name; + const type = item.data.type; if (type === 'image') { nameCounters.current.image = (nameCounters.current.image || 0) + 1; return `Image ${nameCounters.current.image}`; } - if (type === 'i-text') return (obj.text || 'Text').slice(0, 20); - if (type === 'path') { - nameCounters.current.path = (nameCounters.current.path || 0) + 1; - return `Drawing ${nameCounters.current.path}`; + if (type === 'text') { + const textData = item.data as any; + return (textData.text || 'Text').slice(0, 20); + } + if (type === 'group') { + const groupData = item.data as GroupObject; + return `Group (${groupData.children?.length || 0})`; } - if (type === 'group') return `Group (${obj._objects?.length || 0})`; return `${type} ${i + 1}`; } - function buildLayerItem(obj: any, i: number): any { - const id = obj.id || `layer-${i}`; - const isGroup = obj.type === 'group'; + function buildLayerItem(item: SceneItem, i: number): any { + const isGroup = item.data.type === 'group'; let children: any[] | undefined; - if (isGroup && obj._objects) { - children = obj._objects.map((child: any, ci: number) => buildLayerItem(child, ci)); + if (isGroup) { + const groupData = item.data as GroupObject; + const scene = canvasRef.current?.getScene(); + if (scene && groupData.children) { + children = groupData.children.map((childId, ci) => { + const childItem = scene.getById(childId); + if (childItem) return buildLayerItem(childItem, ci); + return { id: childId, name: `Missing ${childId.slice(0, 6)}`, type: 'unknown', visible: true, locked: false, isGroup: false }; + }); + } } return { - id, - name: autoName(obj, i), - type: obj.type || 'object', - visible: obj.visible !== false, - locked: !obj.selectable, + id: item.id, + name: autoName(item, i), + type: item.data.type, + visible: item.data.visible, + locked: item.data.locked, isGroup, children, }; } const refreshLayers = useCallback(() => { - const canvas = canvasRef.current?.getCanvas(); - if (!canvas) return; + const scene = canvasRef.current?.getScene(); + if (!scene) return; nameCounters.current = {}; - const objects = canvas.getObjects(); - const layers = objects.map((obj: any, i: number) => buildLayerItem(obj, i)); + const items = scene.getAllItems().sort((a, b) => a.data.z - b.data.z); + const layers = items.map((item, i) => buildLayerItem(item, i)); setLayerList(layers); - const activeIds = canvas.getActiveObjects().map((o: any) => o.id).filter(Boolean); - setSelectedLayerIds(activeIds); + + const selection = selectionRef.current; + if (selection) { + setSelectedLayerIds(Array.from(selection.selectedIds)); + } }, []); // Update layers whenever canvas changes @@ -429,36 +483,85 @@ export default function Editor({ isPublicView }: EditorProps) { // Group selected objects const handleGroup = useCallback(() => { - const canvas = canvasRef.current?.getCanvas(); - if (!canvas) return; - const active = canvas.getActiveObjects(); - if (active.length < 2) return; - canvas.discardActiveObject(); - const group = new Group(active, {}); - (group as any).id = crypto.randomUUID(); - active.forEach((obj) => canvas.remove(obj)); - canvas.add(group); - canvas.setActiveObject(group); - canvas.requestRenderAll(); + const scene = canvasRef.current?.getScene(); + const selection = selectionRef.current; + if (!scene || !selection) return; + const selected = selection.getSelectedItems(); + if (selected.length < 2) return; + + // Create a group item containing the selected items' IDs + const groupData: GroupObject = { + id: crypto.randomUUID(), + type: 'group', + x: Math.min(...selected.map((s) => s.data.x)), + y: Math.min(...selected.map((s) => s.data.y)), + w: 0, + h: 0, + sx: 1, + sy: 1, + angle: 0, + z: scene.nextZ(), + opacity: 1, + locked: false, + name: '', + visible: true, + children: selected.map((s) => s.id), + }; + + // Create the group container and reparent children + scene._createItem(groupData, false); + const groupItem = scene.getById(groupData.id); + if (groupItem) { + for (const child of selected) { + // Reparent display object into group container + child.displayObject.parent?.removeChild(child.displayObject); + groupItem.displayObject.addChild(child.displayObject); + // Adjust child position relative to group + child.displayObject.position.set( + child.data.x - groupData.x, + child.data.y - groupData.y, + ); + } + } + + selection.clear(); + if (groupItem) selection.selectOnly(groupItem.id); + scene._applyZOrder(); onCanvasChange(); refreshLayers(); }, [onCanvasChange, refreshLayers]); // Ungroup selected group const handleUngroup = useCallback(() => { - const canvas = canvasRef.current?.getCanvas(); - if (!canvas) return; - const active = canvas.getActiveObject(); - if (!active || active.type !== 'group') return; - const group = active as Group; - const items = group.removeAll(); - canvas.remove(group); - items.forEach((obj: any) => { - obj.id = obj.id || crypto.randomUUID(); - canvas.add(obj); - }); - canvas.discardActiveObject(); - canvas.requestRenderAll(); + const scene = canvasRef.current?.getScene(); + const selection = selectionRef.current; + if (!scene || !selection) return; + const selected = selection.getSelectedItems(); + if (selected.length !== 1) return; + const groupItem = selected[0]; + if (groupItem.data.type !== 'group') return; + + const groupData = groupItem.data as GroupObject; + const viewport = canvasRef.current?.getViewport(); + if (!viewport) return; + + // Reparent children back to viewport + for (const childId of groupData.children) { + const childItem = scene.getById(childId); + if (childItem) { + // Convert position back to world space + childItem.data.x += groupData.x; + childItem.data.y += groupData.y; + childItem.displayObject.parent?.removeChild(childItem.displayObject); + viewport.addChild(childItem.displayObject); + childItem.displayObject.position.set(childItem.data.x, childItem.data.y); + } + } + + // Remove the group item + scene.removeItem(groupItem.id, false); + selection.clear(); + scene._applyZOrder(); onCanvasChange(); refreshLayers(); }, [onCanvasChange, refreshLayers]); @@ -470,115 +573,141 @@ export default function Editor({ isPublicView }: EditorProps) { }, []); const contextMenuItems = useCallback(() => { - const canvas = canvasRef.current?.getCanvas(); - const active = canvas ? canvas.getActiveObjects() : []; - const hasSel = active.length > 0; - const multiSel = active.length >= 2; - const run = (fn: (objs: FabricObject[]) => void) => { - if (!canvas) return; - const { objects, restore } = ops.breakSelection(canvas); - fn(objects); - restore(); - canvas.requestRenderAll(); onCanvasChange(); - }; + const scene = canvasRef.current?.getScene(); + const selection = selectionRef.current; + const selected = selection ? selection.getSelectedItems() : []; + const hasSel = selected.length > 0; + const multiSel = selected.length >= 2; + return [ { label: 'Copy', shortcut: 'Ctrl+C', onClick: () => { - if (!canvas) return; - if (hasSel) { clipboardRef.current = [...active]; writeCanvasToClipboard(active); } + if (hasSel) { clipboardRef.current = [...selected]; writeCanvasToClipboard(selected); } else writeCanvasToClipboard(); } }, { label: 'Cut', shortcut: 'Ctrl+X', onClick: () => { - if (!canvas || !hasSel) return; - clipboardRef.current = [...active]; - active.forEach((o) => canvas.remove(o)); - canvas.discardActiveObject(); canvas.requestRenderAll(); onCanvasChange(); + if (!scene || !hasSel || !selection) return; + clipboardRef.current = [...selected]; + for (const item of selected) scene.removeItem(item.id, true); + selection.clear(); + onCanvasChange(); }, disabled: !hasSel }, { label: 'Paste', shortcut: 'Ctrl+V', onClick: async () => { - if (!canvas || clipboardRef.current.length === 0) return; - for (const o of clipboardRef.current) { - try { const c = await cloneFabricObject(o, 20, 20); canvas.add(c); } catch {} + if (!scene || clipboardRef.current.length === 0) return; + for (const original of clipboardRef.current) { + const newData = { + ...original.data, + id: crypto.randomUUID(), + x: original.data.x + 20, + y: original.data.y + 20, + z: scene.nextZ(), + }; + await scene._createItem(newData, true); } - canvas.requestRenderAll(); onCanvasChange(); + scene._applyZOrder(); + onCanvasChange(); }, disabled: clipboardRef.current.length === 0 }, { label: 'Duplicate', shortcut: 'Ctrl+D', onClick: async () => { - if (!canvas || !hasSel) return; - for (const o of active) { - try { const c = await cloneFabricObject(o, 20, 20); canvas.add(c); } catch {} + if (!scene || !hasSel) return; + for (const original of selected) { + const newData = { + ...original.data, + id: crypto.randomUUID(), + x: original.data.x + 20, + y: original.data.y + 20, + z: scene.nextZ(), + }; + await scene._createItem(newData, true); } - canvas.discardActiveObject(); canvas.requestRenderAll(); onCanvasChange(); + if (selection) selection.clear(); + scene._applyZOrder(); + onCanvasChange(); }, disabled: !hasSel }, { label: '', shortcut: '', onClick: () => {}, divider: true }, - // — Alignment — - { label: 'Align Left', shortcut: 'Ctrl+\u2190', onClick: () => run((o) => ops.alignLeft(o)), disabled: !multiSel }, - { label: 'Align Right', shortcut: 'Ctrl+\u2192', onClick: () => run((o) => ops.alignRight(o)), disabled: !multiSel }, - { label: 'Align Top', shortcut: 'Ctrl+\u2191', onClick: () => run((o) => ops.alignTop(o)), disabled: !multiSel }, - { label: 'Align Bottom', shortcut: 'Ctrl+\u2193', onClick: () => run((o) => ops.alignBottom(o)), disabled: !multiSel }, - { label: 'Distribute H', shortcut: '', onClick: () => run((o) => ops.distributeHorizontal(o)), disabled: active.length < 3 }, - { label: 'Distribute V', shortcut: '', onClick: () => run((o) => ops.distributeVertical(o)), disabled: active.length < 3 }, + // -- Alignment -- + { label: 'Align Left', shortcut: 'Ctrl+\u2190', onClick: () => { ops.alignLeft(selected); onCanvasChange(); }, disabled: !multiSel }, + { label: 'Align Right', shortcut: 'Ctrl+\u2192', onClick: () => { ops.alignRight(selected); onCanvasChange(); }, disabled: !multiSel }, + { label: 'Align Top', shortcut: 'Ctrl+\u2191', onClick: () => { ops.alignTop(selected); onCanvasChange(); }, disabled: !multiSel }, + { label: 'Align Bottom', shortcut: 'Ctrl+\u2193', onClick: () => { ops.alignBottom(selected); onCanvasChange(); }, disabled: !multiSel }, + { label: 'Distribute H', shortcut: '', onClick: () => { ops.distributeHorizontal(selected); onCanvasChange(); }, disabled: selected.length < 3 }, + { label: 'Distribute V', shortcut: '', onClick: () => { ops.distributeVertical(selected); onCanvasChange(); }, disabled: selected.length < 3 }, { label: '', shortcut: '', onClick: () => {}, divider: true }, - // — Layer ordering — + // -- Layer ordering -- { label: 'Bring Forward', shortcut: ']', onClick: () => { - if (!canvas) return; - active.forEach((o) => (canvas as any).bringObjectForward(o)); - canvas.requestRenderAll(); onCanvasChange(); + if (!scene) return; + const all = scene.getAllItems().sort((a, b) => a.data.z - b.data.z); + const selectedIds = new Set(selected.map((s) => s.id)); + for (let i = all.length - 2; i >= 0; i--) { + if (selectedIds.has(all[i].id) && !selectedIds.has(all[i + 1].id)) { + const tmp = all[i].data.z; + all[i].data.z = all[i + 1].data.z; + all[i + 1].data.z = tmp; + } + } + scene._applyZOrder(); + onCanvasChange(); }, disabled: !hasSel }, { label: 'Send Backward', shortcut: '[', onClick: () => { - if (!canvas) return; - active.forEach((o) => (canvas as any).sendObjectBackwards(o)); - canvas.requestRenderAll(); onCanvasChange(); + if (!scene) return; + const all = scene.getAllItems().sort((a, b) => a.data.z - b.data.z); + const selectedIds = new Set(selected.map((s) => s.id)); + for (let i = 1; i < all.length; i++) { + if (selectedIds.has(all[i].id) && !selectedIds.has(all[i - 1].id)) { + const tmp = all[i].data.z; + all[i].data.z = all[i - 1].data.z; + all[i - 1].data.z = tmp; + } + } + scene._applyZOrder(); + onCanvasChange(); }, disabled: !hasSel }, { label: '', shortcut: '', onClick: () => {}, divider: true }, - // — Group — + // -- Group -- { label: 'Group', shortcut: 'Ctrl+G', onClick: handleGroup, disabled: !multiSel }, { label: 'Ungroup', shortcut: 'Ctrl+Shift+G', onClick: handleUngroup, - disabled: !canvas || canvas.getActiveObject()?.type !== 'group' }, + disabled: selected.length !== 1 || selected[0]?.data.type !== 'group' }, { label: '', shortcut: '', onClick: () => {}, divider: true }, - // — Arrangement — - { label: 'Arrange Pack', shortcut: 'Ctrl+Shift+P', onClick: () => run((o) => ops.arrangeOptimal(o)), disabled: !multiSel }, - { label: 'Arrange Grid', shortcut: '', onClick: () => run((o) => ops.arrangeGrid(o)), disabled: !multiSel }, - { label: 'Arrange Row', shortcut: '', onClick: () => run((o) => ops.arrangeRow(o)), disabled: !multiSel }, - { label: 'Arrange Column', shortcut: '', onClick: () => run((o) => ops.arrangeColumn(o)), disabled: !multiSel }, - { label: 'Stack', shortcut: 'Ctrl+Alt+S', onClick: () => run((o) => ops.stackObjects(o)), disabled: !multiSel }, + // -- Arrangement -- + { label: 'Arrange Pack', shortcut: 'Ctrl+Shift+P', onClick: () => { ops.arrangeOptimal(selected); onCanvasChange(); }, disabled: !multiSel }, + { label: 'Arrange Grid', shortcut: '', onClick: () => { ops.arrangeGrid(selected); onCanvasChange(); }, disabled: !multiSel }, + { label: 'Arrange Row', shortcut: '', onClick: () => { ops.arrangeRow(selected); onCanvasChange(); }, disabled: !multiSel }, + { label: 'Arrange Column', shortcut: '', onClick: () => { ops.arrangeColumn(selected); onCanvasChange(); }, disabled: !multiSel }, + { label: 'Stack', shortcut: 'Ctrl+Alt+S', onClick: () => { ops.stackObjects(selected); onCanvasChange(); }, disabled: !multiSel }, { label: '', shortcut: '', onClick: () => {}, divider: true }, - // — Normalize — - { label: 'Normalize Size', shortcut: '', onClick: () => run((o) => ops.normalizeSize(o)), disabled: !multiSel }, - { label: 'Normalize Width', shortcut: '', onClick: () => run((o) => ops.normalizeWidth(o)), disabled: !multiSel }, - { label: 'Normalize Height', shortcut: '', onClick: () => run((o) => ops.normalizeHeight(o)), disabled: !multiSel }, + // -- Normalize -- + { label: 'Normalize Size', shortcut: '', onClick: () => { ops.normalizeSize(selected); onCanvasChange(); }, disabled: !multiSel }, + { label: 'Normalize Width', shortcut: '', onClick: () => { ops.normalizeWidth(selected); onCanvasChange(); }, disabled: !multiSel }, + { label: 'Normalize Height', shortcut: '', onClick: () => { ops.normalizeHeight(selected); onCanvasChange(); }, disabled: !multiSel }, { label: '', shortcut: '', onClick: () => {}, divider: true }, - // — Image — - { label: 'Flip Horizontal', shortcut: 'Alt+Shift+H', onClick: () => run((o) => ops.flipHorizontal(o)), disabled: !hasSel }, - { label: 'Flip Vertical', shortcut: 'Alt+Shift+V', onClick: () => run((o) => ops.flipVertical(o)), disabled: !hasSel }, - { label: 'Reset Transform', shortcut: 'Ctrl+Shift+T', onClick: () => run((o) => ops.resetTransform(o)), disabled: !hasSel }, + // -- Image -- + { label: 'Flip Horizontal', shortcut: 'Alt+Shift+H', onClick: () => { ops.flipHorizontal(selected); onCanvasChange(); }, disabled: !hasSel }, + { label: 'Flip Vertical', shortcut: 'Alt+Shift+V', onClick: () => { ops.flipVertical(selected); onCanvasChange(); }, disabled: !hasSel }, + { label: 'Reset Transform', shortcut: 'Ctrl+Shift+T', onClick: () => { ops.resetTransform(selected); onCanvasChange(); }, disabled: !hasSel }, { label: '', shortcut: '', onClick: () => {}, divider: true }, { label: 'Select All', shortcut: 'Ctrl+A', onClick: () => { - if (!canvas) return; - canvas.discardActiveObject(); - const fabricNs = (window as any).fabric; - if (fabricNs?.ActiveSelection) { - const sel = new fabricNs.ActiveSelection(canvas.getObjects(), { canvas }); - canvas.setActiveObject(sel); - } - canvas.requestRenderAll(); + if (selection) selection.selectAll(); } }, { label: 'Fit All', shortcut: 'Ctrl+0', onClick: () => canvasRef.current?.fitAll() }, { label: '', shortcut: '', onClick: () => {}, divider: true }, { label: 'Delete', shortcut: 'Del', onClick: () => { - if (!canvas) return; - active.forEach((o) => canvas.remove(o)); - canvas.discardActiveObject(); canvas.requestRenderAll(); onCanvasChange(); + if (!scene || !selection) return; + for (const item of selected) scene.removeItem(item.id, true); + selection.clear(); + onCanvasChange(); }, disabled: !hasSel, danger: true }, ]; - }, [writeCanvasToClipboard, onCanvasChange, handleGroup, handleUngroup, cloneFabricObject]); + }, [writeCanvasToClipboard, onCanvasChange, handleGroup, handleUngroup]); - // Keyboard shortcuts — registry-based approach + // Keyboard shortcuts -- registry-based approach useEffect(() => { async function onKeyDown(e: KeyboardEvent) { if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return; - const canvas = canvasRef.current?.getCanvas(); - if (!canvas) return; + const scene = canvasRef.current?.getScene(); + const selection = selectionRef.current; + const viewport = canvasRef.current?.getViewport(); + if (!scene || !selection || !viewport) return; - // Tool shortcuts (bare keys 1-5, v/h/p/t/e — no modifiers) + // Tool shortcuts (bare keys 1-5, v/h/p/t/e -- no modifiers) if (!e.ctrlKey && !e.metaKey && !e.altKey) { const tool = toolShortcuts[e.key.toLowerCase()]; if (tool) { @@ -587,24 +716,23 @@ export default function Editor({ isPublicView }: EditorProps) { } } - // Special: Ctrl+V paste — if internal clipboard is empty, let browser/setupPaste handle it + // Special: Ctrl+V paste -- if internal clipboard is empty, let browser/setupPaste handle it if ((e.ctrlKey || e.metaKey) && e.key === 'v' && !e.shiftKey && !e.altKey) { if (clipboardRef.current.length === 0) return; } // Build context for shortcut handlers const ctx: ShortcutContext = { - canvas, - getActiveObjects: () => canvas.getActiveObjects(), - getActiveObject: () => canvas.getActiveObject() || null, + scene, + selection, + history: undoRef.current!, + viewport, + onChange: onCanvasChange, clipboardRef, - cloneFabricObject, writeCanvasToClipboard, - onCanvasChange, showToast, fitAll: () => canvasRef.current?.fitAll(), setActiveTool, - undoRef, setCanUndo, setCanRedo, refreshLayers, @@ -619,7 +747,7 @@ export default function Editor({ isPublicView }: EditorProps) { if (!matchesShortcut(e, def)) continue; // Check selection requirements - const activeCount = canvas.getActiveObjects().length; + const activeCount = selection.selectedIds.size; if (def.needsSelection && activeCount === 0) continue; if (def.minSelection && activeCount < def.minSelection) continue; @@ -627,21 +755,21 @@ export default function Editor({ isPublicView }: EditorProps) { await def.handler(ctx); // Update zoom display after any action - setZoom(canvas.getZoom()); + setZoom(canvasRef.current?.getZoom() ?? 1); return; } } window.addEventListener('keydown', onKeyDown); return () => window.removeEventListener('keydown', onKeyDown); - }, [onCanvasChange, writeCanvasToClipboard, handleGroup, handleUngroup, cloneFabricObject, showToast, refreshLayers]); + }, [onCanvasChange, writeCanvasToClipboard, handleGroup, handleUngroup, showToast, refreshLayers]); // Save on page unload useEffect(() => { function onBeforeUnload() { - const canvas = canvasRef.current?.getCanvas(); - if (!canvas || !resolvedBoardId) return; - const state = JSON.stringify((canvas as any).toJSON(['id', 'crossOrigin'])); + const scene = canvasRef.current?.getScene(); + if (!scene || !resolvedBoardId) return; + const state = JSON.stringify(scene.serialize()); const blob = new Blob([JSON.stringify({ canvas_state: state })], { type: 'application/json' }); navigator.sendBeacon(`/api/boards/${resolvedBoardId}/save`, blob); } @@ -751,21 +879,17 @@ export default function Editor({ isPublicView }: EditorProps) { setCanRedo(undoRef.current?.canRedo() ?? false); }} onZoomIn={() => { - const canvas = canvasRef.current?.getCanvas(); - if (!canvas) return; - const z = Math.min(canvas.getZoom() * 1.2, 5); - const center = canvas.getCenterPoint(); - canvas.zoomToPoint(center, z); - canvas.requestRenderAll(); + const vp = canvasRef.current?.getViewport(); + if (!vp) return; + const z = Math.min(vp.scale.x * 1.2, 5); + canvasRef.current?.setZoom(z); setZoom(z); }} onZoomOut={() => { - const canvas = canvasRef.current?.getCanvas(); - if (!canvas) return; - const z = Math.max(canvas.getZoom() / 1.2, 0.1); - const center = canvas.getCenterPoint(); - canvas.zoomToPoint(center, z); - canvas.requestRenderAll(); + const vp = canvasRef.current?.getViewport(); + if (!vp) return; + const z = Math.max(vp.scale.x / 1.2, 0.1); + canvasRef.current?.setZoom(z); setZoom(z); }} onlineUsers={onlineUsers} @@ -777,7 +901,7 @@ export default function Editor({ isPublicView }: EditorProps) { {/* Canvas */}
- { - const canvas = canvasRef.current?.getCanvas(); - if (!canvas) return; - const obj = canvas.getObjects().find((o: any) => o.id === id); - if (obj) { - canvas.discardActiveObject(); - canvas.setActiveObject(obj); - canvas.requestRenderAll(); + const selection = selectionRef.current; + if (selection) { + selection.selectOnly(id); } }} onToggleVisible={(id) => { - const canvas = canvasRef.current?.getCanvas(); - if (!canvas) return; - const obj = canvas.getObjects().find((o: any) => o.id === id); - if (obj) { - obj.set('visible', !obj.visible); - canvas.requestRenderAll(); + const scene = canvasRef.current?.getScene(); + if (!scene) return; + const item = scene.getById(id); + if (item) { + item.data.visible = !item.data.visible; + item.displayObject.visible = item.data.visible; onCanvasChange(); refreshLayers(); } }} onToggleLock={(id) => { - const canvas = canvasRef.current?.getCanvas(); - if (!canvas) return; - const obj = canvas.getObjects().find((o: any) => o.id === id); - if (obj) { - obj.set({ selectable: !obj.selectable, evented: !obj.evented } as any); - canvas.requestRenderAll(); + const scene = canvasRef.current?.getScene(); + if (!scene) return; + const item = scene.getById(id); + if (item) { + item.data.locked = !item.data.locked; + item.displayObject.eventMode = item.data.locked ? 'none' : 'static'; refreshLayers(); } }} onReorder={(from, to) => { - const canvas = canvasRef.current?.getCanvas(); - if (!canvas) return; - const objects = canvas.getObjects(); - if (from < 0 || from >= objects.length || to < 0 || to >= objects.length) return; - const obj = objects[from]; - canvas.remove(obj); - const insertAt = to > from ? to : to; - canvas.insertAt(insertAt, obj); - canvas.requestRenderAll(); + const scene = canvasRef.current?.getScene(); + if (!scene) return; + const items = scene.getAllItems().sort((a, b) => a.data.z - b.data.z); + if (from < 0 || from >= items.length || to < 0 || to >= items.length) return; + // Swap z values + const tmp = items[from].data.z; + items[from].data.z = items[to].data.z; + items[to].data.z = tmp; + scene._applyZOrder(); onCanvasChange(); refreshLayers(); }} onDelete={(id) => { - const canvas = canvasRef.current?.getCanvas(); - if (!canvas) return; - const obj = canvas.getObjects().find((o: any) => o.id === id); - if (obj) { - canvas.remove(obj); - canvas.discardActiveObject(); - canvas.requestRenderAll(); - onCanvasChange(); - refreshLayers(); - } + const scene = canvasRef.current?.getScene(); + const selection = selectionRef.current; + if (!scene) return; + scene.removeItem(id, true); + if (selection) selection.clear(); + onCanvasChange(); + refreshLayers(); }} onRename={(id, name) => { - const canvas = canvasRef.current?.getCanvas(); - if (!canvas) return; - const obj = canvas.getObjects().find((o: any) => o.id === id); - if (obj) { - (obj as any).name = name; + const scene = canvasRef.current?.getScene(); + if (!scene) return; + const item = scene.getById(id); + if (item) { + item.data.name = name; refreshLayers(); } }} onGroup={handleGroup} onUngroup={handleUngroup} - hasSelection={canvasRef.current?.getCanvas()?.getActiveObjects()?.length! > 1} - hasGroupSelection={canvasRef.current?.getCanvas()?.getActiveObject()?.type === 'group'} + hasSelection={(selectionRef.current?.selectedIds.size ?? 0) > 1} + hasGroupSelection={(() => { + const sel = selectionRef.current; + if (!sel || sel.selectedIds.size !== 1) return false; + const scene = canvasRef.current?.getScene(); + if (!scene) return false; + const id = Array.from(sel.selectedIds)[0]; + const item = scene.getById(id); + return item?.data.type === 'group'; + })()} /> )}