diff --git a/frontend/package-lock.json b/frontend/package-lock.json index acb3a7f..a94beeb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,6 +19,7 @@ }, "devDependencies": { "@eslint/js": "^9.9.0", + "@types/jest": "^29.5.14", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react-swc": "^3.5.0", @@ -29,12 +30,14 @@ "eslint-plugin-react-refresh": "^0.4.9", "gh-pages": "^6.2.0", "globals": "^15.9.0", + "jsdom": "^25.0.1", "postcss": "^8.4.47", "tailwindcss": "^3.4.14", "typescript": "^5.5.3", "typescript-eslint": "^8.0.1", "vite": "^5.4.1", - "vitest": "^2.1.8" + "vitest": "^2.1.8", + "vitest-canvas-mock": "^0.3.3" } }, "node_modules/@alloc/quick-lru": { @@ -49,6 +52,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -602,6 +628,47 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -903,6 +970,12 @@ "win32" ] }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, "node_modules/@swc/core": { "version": "1.7.26", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.26.tgz", @@ -1122,6 +1195,49 @@ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/node": { + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "dev": true, + "dependencies": { + "undici-types": "~6.20.0" + } + }, "node_modules/@types/prop-types": { "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", @@ -1153,6 +1269,27 @@ "@types/react": "*" } }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.6.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz", @@ -1510,6 +1647,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1605,6 +1751,12 @@ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "dev": true }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", @@ -1838,6 +1990,21 @@ "node": ">= 6" } }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1856,6 +2023,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -1912,11 +2091,42 @@ "node": ">=4" } }, + "node_modules/cssfontparser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/cssfontparser/-/cssfontparser-1.2.1.tgz", + "integrity": "sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg==", + "dev": true + }, + "node_modules/cssstyle": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", + "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", + "dev": true, + "dependencies": { + "rrweb-cssom": "^0.7.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -1934,6 +2144,12 @@ } } }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -1949,12 +2165,30 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "dev": true }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -1997,6 +2231,18 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-module-lexer": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", @@ -2238,6 +2484,22 @@ "node": ">=0.10.0" } }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/expect-type": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", @@ -2420,6 +2682,20 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -2622,6 +2898,56 @@ "node": ">= 0.4" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2741,6 +3067,12 @@ "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==", + "dev": true + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2773,6 +3105,92 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jest-canvas-mock": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jest-canvas-mock/-/jest-canvas-mock-2.5.2.tgz", + "integrity": "sha512-vgnpPupjOL6+L5oJXzxTxFrlGEIbHdZqFU+LFNdtLxZ3lRDCl17FlTMM7IatoRQkrcyOTMlDinjUguqmQ6bR2A==", + "dev": true, + "dependencies": { + "cssfontparser": "^1.2.1", + "moo-color": "^1.0.2" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/jiti": { "version": "1.21.6", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", @@ -2828,6 +3246,46 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -3014,6 +3472,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3035,6 +3514,15 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/moo-color": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/moo-color/-/moo-color-1.0.3.tgz", + "integrity": "sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ==", + "dev": true, + "dependencies": { + "color-name": "^1.1.4" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3100,6 +3588,12 @@ "node": ">=0.10.0" } }, + "node_modules/nwsapi": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.16.tgz", + "integrity": "sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==", + "dev": true + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3192,6 +3686,18 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "dev": true, + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3520,6 +4026,32 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3583,6 +4115,12 @@ "react": "^18.3.1" } }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, "node_modules/react-konva": { "version": "18.2.10", "resolved": "https://registry.npmjs.org/react-konva/-/react-konva-18.2.10.tgz", @@ -3734,6 +4272,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -3757,6 +4301,24 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": 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", @@ -3834,6 +4396,27 @@ "node": ">=0.10.0" } }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -4015,6 +4598,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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==", + "dev": true + }, "node_modules/tailwindcss": { "version": "3.4.14", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.14.tgz", @@ -4118,6 +4707,24 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "6.1.67", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.67.tgz", + "integrity": "sha512-714VbegxoZ9WF5/IsVCy9rWXKUpPkJq87ebWLXQzNawce96l5oRrRf2eHzB4pT2g/4HQU1dYbu+sdXClYxlDKQ==", + "dev": true, + "dependencies": { + "tldts-core": "^6.1.67" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.67", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.67.tgz", + "integrity": "sha512-12K5O4m3uUW6YM5v45Z7wc6NTSmAYj4Tq3de7eXghZkp879IlfPJrUWeWFwu1FS94U5t2vwETgJ1asu8UGNKVQ==", + "dev": true + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4130,6 +4737,30 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", + "dev": true, + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dev": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/trim-repeated": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", @@ -4217,6 +4848,12 @@ } } }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -4426,6 +5063,73 @@ } } }, + "node_modules/vitest-canvas-mock": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/vitest-canvas-mock/-/vitest-canvas-mock-0.3.3.tgz", + "integrity": "sha512-3P968tYBpqYyzzOaVtqnmYjqbe13576/fkjbDEJSfQAkHtC5/UjuRHOhFEN/ZV5HVZIkaROBUWgazDKJ+Ibw+Q==", + "dev": true, + "dependencies": { + "jest-canvas-mock": "~2.5.2" + }, + "peerDependencies": { + "vitest": "*" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.0.tgz", + "integrity": "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==", + "dev": true, + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4580,6 +5284,21 @@ } } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, "node_modules/yaml": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index ec74ce0..c35d24f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,6 +25,7 @@ }, "devDependencies": { "@eslint/js": "^9.9.0", + "@types/jest": "^29.5.14", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react-swc": "^3.5.0", @@ -35,11 +36,13 @@ "eslint-plugin-react-refresh": "^0.4.9", "gh-pages": "^6.2.0", "globals": "^15.9.0", + "jsdom": "^25.0.1", "postcss": "^8.4.47", "tailwindcss": "^3.4.14", "typescript": "^5.5.3", "typescript-eslint": "^8.0.1", "vite": "^5.4.1", - "vitest": "^2.1.8" + "vitest": "^2.1.8", + "vitest-canvas-mock": "^0.3.3" } } diff --git a/frontend/src/data/items/itemAtoms.ts b/frontend/src/data/items/itemAtoms.ts index d2261b1..f042af8 100644 --- a/frontend/src/data/items/itemAtoms.ts +++ b/frontend/src/data/items/itemAtoms.ts @@ -1,3 +1,4 @@ import { atom } from "jotai"; export const SelectedItem = atom(undefined); +export const ClickToAddItem = atom(undefined); diff --git a/frontend/src/data/tiles/tileAtoms.ts b/frontend/src/data/tiles/tileAtoms.ts new file mode 100644 index 0000000..6df1383 --- /dev/null +++ b/frontend/src/data/tiles/tileAtoms.ts @@ -0,0 +1,3 @@ +import { atom } from "jotai"; + +export const SelectedTile = atom(0); diff --git a/frontend/src/editor/EditorView.tsx b/frontend/src/editor/EditorView.tsx index 70f092d..2976fa6 100644 --- a/frontend/src/editor/EditorView.tsx +++ b/frontend/src/editor/EditorView.tsx @@ -6,13 +6,13 @@ import { Stage } from "react-konva"; import { Updater, useImmer } from "use-immer"; import { FenceMenu } from "./subviews/fences/FenceMenu"; -import { useSetAtom } from "jotai"; +import { useAtomValue, useSetAtom } from "jotai"; import { SelectedFence } from "../data/fences/fenceAtoms"; import { Items } from "./subviews/Items"; import { ItemMenu } from "./subviews/items/ItemMenu"; import { Splines } from "./subviews/Splines"; import { SplineMenu } from "./subviews/splines/SplineMenu"; -import { SelectedItem } from "../data/items/itemAtoms"; +import { ClickToAddItem, SelectedItem } from "../data/items/itemAtoms"; import { SelectedSpline } from "../data/splines/splineAtoms"; import { WaterBodies } from "./subviews/WaterBodies"; import { WaterMenu } from "./subviews/water/WaterMenu"; @@ -35,10 +35,12 @@ export function EditorView({ data, setData, mapImages, + setMapImages, }: { data: ottoMaticLevel; setData: Updater; mapImages: HTMLCanvasElement[]; + setMapImages: (newCanvases: HTMLCanvasElement[]) => void; }) { const [view, setView] = useState(View.fences); const [stage, setStage] = useImmer({ @@ -50,6 +52,7 @@ export function EditorView({ const setSelectedItem = useSetAtom(SelectedItem); const setSelectedSpline = useSetAtom(SelectedSpline); const setSelectedWaterBody = useSetAtom(SelectedWaterBody); + const clickToAddItem = useAtomValue(ClickToAddItem); console.log(data); const zoomIn = () => setStage((stage) => { @@ -109,7 +112,14 @@ export function EditorView({ {view === View.water && } {view === View.items && } {view === View.splines && } - {view === View.tiles && } + {view === View.tiles && ( + + )} {view === View.topology && } { + console.log("TEST"); + if (clickToAddItem === undefined) return; + const stage = e.target.getStage(); + + const pos = stage?.getRelativePointerPosition(); + if (!pos) return; + const x = Math.round(pos.x); + const z = Math.round(pos.y); + console.log("SETDATA", x, z); + console.log(data.Itms[1000].obj); + console.log(data.Itms[1000].obj.length); + + setData((data) => { + data.Itms[1000].obj.push({ + x: x, + z: z, + type: clickToAddItem, + flags: 0, + p0: 0, + p1: 0, + p2: 0, + p3: 0, + }); + }); + }} onDblClick={() => { setSelectedFence(undefined); setSelectedItem(undefined); diff --git a/frontend/src/editor/MapPrompt.tsx b/frontend/src/editor/MapPrompt.tsx index 80a120a..8284c8b 100644 --- a/frontend/src/editor/MapPrompt.tsx +++ b/frontend/src/editor/MapPrompt.tsx @@ -14,6 +14,7 @@ import ottoPreprocessor, { newJsonProcess, } from "../data/preprocessors/ottoPreprocessor"; import { lzssCompress } from "../utils/lzss"; +import { imageDataToSixteenBit } from "../utils/imageConverter"; export function MapPrompt({ pyodide }: { pyodide: PyodideInterface }) { const [data, setData] = useImmer(null); @@ -24,9 +25,6 @@ export function MapPrompt({ pyodide }: { pyodide: PyodideInterface }) { const [mapImages, setMapImages] = useState( undefined, ); - const [mapImagesData, setMapImagesData] = useState( - undefined, - ); const [processed, setProcessed] = useState(false); useEffect(() => { const loadMap = async () => { @@ -72,21 +70,30 @@ export function MapPrompt({ pyodide }: { pyodide: PyodideInterface }) { downloadLink.click(); //Download Images - if (!mapImagesData) return; + if (!mapImages) return; //TODO: Hardcoded values that will break for other games / if actual compression is implemented const imageSize = OTTO_SUPERTILE_TEXMAP_SIZE * OTTO_SUPERTILE_TEXMAP_SIZE * 2; const compressedImageSize = imageSize + Math.ceil(imageSize / 8); const imageDownloadBuffer = new DataView( - new ArrayBuffer(mapImagesData.length * (4 + compressedImageSize)), + new ArrayBuffer(mapImages.length * (4 + compressedImageSize)), ); - for (let i = 0; i < mapImagesData.length; i++) { + for (let i = 0; i < mapImages.length; i++) { const pos = i * (compressedImageSize + 4); //New dataview //Output file has 32-bit size headers before each image, image is size^2 2-byte pixels imageDownloadBuffer.setInt32(pos, compressedImageSize); - const decompressed = lzssCompress(new DataView(mapImagesData[i])); + const canvasCtx = mapImages[i].getContext("2d"); + if (!canvasCtx) throw new Error("Could not get canvas context"); + const decompressed = lzssCompress( + imageDataToSixteenBit( + canvasCtx.getImageData(0, 0, mapImages[i].width, mapImages[i].height) + .data, + ), + ); + + //const decompressed = lzssCompress(new DataView(mapImagesData[i])); for (let j = 0; j < decompressed.byteLength; j++) { imageDownloadBuffer.setUint8(pos + 4 + j, decompressed.getUint8(j)); } @@ -108,7 +115,6 @@ export function MapPrompt({ pyodide }: { pyodide: PyodideInterface }) { setMapFile={setMapFile} setMapImagesFile={setMapImagesFile} setMapImages={setMapImages} - setMapImagesData={setMapImagesData} /> ); return ( @@ -118,6 +124,8 @@ export function MapPrompt({ pyodide }: { pyodide: PyodideInterface }) { onClick={() => { setMapFile(undefined); setData(null); + setMapImages(undefined); + setMapImagesFile(undefined); }} > ←New Map @@ -140,6 +148,7 @@ export function MapPrompt({ pyodide }: { pyodide: PyodideInterface }) { data={data} setData={setData as Updater} mapImages={mapImages} + setMapImages={setMapImages} /> ) : (

Loading...

diff --git a/frontend/src/editor/UploadPrompt.tsx b/frontend/src/editor/UploadPrompt.tsx index c81084e..581d1b8 100644 --- a/frontend/src/editor/UploadPrompt.tsx +++ b/frontend/src/editor/UploadPrompt.tsx @@ -2,6 +2,7 @@ import { Button } from "../components/Button"; import { FileUpload } from "../components/FileUpload"; import { lzssDecompress } from "../utils/lzss"; import { OTTO_SUPERTILE_TEXMAP_SIZE } from "../python/structSpecs/ottoMaticInterface"; +import { sixteenBitToImageData } from "../utils/imageConverter"; //import level1Url from "./assets/ottoMatic/terrain/EarthFarm.ter.rsrc?url"; @@ -10,13 +11,11 @@ export function UploadPrompt({ setMapFile, setMapImagesFile, setMapImages, - setMapImagesData, }: { mapFile: File | undefined; setMapFile: (file: File) => void; setMapImagesFile: (file: File) => void; setMapImages: (images: HTMLCanvasElement[]) => void; - setMapImagesData: (images: ArrayBuffer[]) => void; }) { const useFile = async (url: string) => { const rsrcName = url + ".rsrc"; //.ter to .ter.rsrc @@ -32,11 +31,10 @@ export function UploadPrompt({ const imgFile = new File([img], url.split("/").pop() ?? ""); const imgBuffer = await imgFile.arrayBuffer(); const imgDataView = new DataView(imgBuffer); - const [mapImages, mapImagesData] = loadMapImages(imgDataView); + const mapImages = loadMapImages(imgDataView); setMapImagesFile(imgFile); setMapImages(mapImages); - setMapImagesData(mapImagesData); }; return ( @@ -66,10 +64,9 @@ export function UploadPrompt({ //Uses Big Endian by default - Which is what Otto uses const dataView = new DataView(buffer); - const [mapImages, mapImagesData] = loadMapImages(dataView); + const mapImages = loadMapImages(dataView); setMapImagesFile(mapImagesFile); setMapImages(mapImages); - setMapImagesData(mapImagesData); }} /> @@ -123,9 +120,7 @@ export function UploadPrompt({ ); } -function loadMapImages( - dataView: DataView, -): [HTMLCanvasElement[], ArrayBuffer[]] { +function loadMapImages(dataView: DataView): HTMLCanvasElement[] { let offset = 0; let numSupertiles = 0; @@ -164,35 +159,15 @@ function loadMapImages( const imageData = imgCtx?.getImageData( 0, 0, - OTTO_SUPERTILE_TEXMAP_SIZE, - OTTO_SUPERTILE_TEXMAP_SIZE, - { - //colorSpace: "srgb", - }, + imgCanvas.width, + imgCanvas.height, ); if (!imageData) { throw new Error("Could not create image data"); } - for ( - let i = 0; - i < OTTO_SUPERTILE_TEXMAP_SIZE * OTTO_SUPERTILE_TEXMAP_SIZE; - i++ - ) { - const data = decompressedBuffer.getUint16(i * 2); - - //A RRRRR GGGGG BBBBB - //R - 0111 1100 0000 0000 - //G - 0000 0011 1110 0000 - //B - 0000 0000 0001 1111 - const r = ((data & 0x7c00) >> 10) * 8; - const g = ((data & 0x03e0) >> 5) * 8; - const b = (data & 0x001f) * 8; - imageData.data[4 * i] = Math.floor(r); //(data >> 8) & 0xff; - imageData.data[4 * i + 1] = Math.floor(g); //(data >> 16) & 0xff; - imageData.data[4 * i + 2] = Math.floor(b); /* & 0x80 */ //data & 0xff; - imageData.data[4 * i + 3] = data & 0x8000 ? 255 : 255; //0xff; - } + + sixteenBitToImageData(decompressedBuffer, imageData); if (!imgCtx) { throw new Error("Bad data!"); @@ -204,5 +179,5 @@ function loadMapImages( imgCanvas; mapImages.push(imgCanvas); } - return [mapImages, mapImagesData]; + return mapImages; } diff --git a/frontend/src/editor/subviews/Items.tsx b/frontend/src/editor/subviews/Items.tsx index ccadd84..186a9d7 100644 --- a/frontend/src/editor/subviews/Items.tsx +++ b/frontend/src/editor/subviews/Items.tsx @@ -1,5 +1,5 @@ import { ottoMaticLevel } from "../../python/structSpecs/ottoMaticInterface"; -import { Layer } from "react-konva"; +import { Layer, Rect } from "react-konva"; import { Updater } from "use-immer"; import { Item } from "./items/Item"; @@ -14,6 +14,7 @@ export function Items({ return ( + {data.Itms[1000].obj.map((_, itemIdx) => ( ))} diff --git a/frontend/src/editor/subviews/Tiles.tsx b/frontend/src/editor/subviews/Tiles.tsx index 2133a78..8ded029 100644 --- a/frontend/src/editor/subviews/Tiles.tsx +++ b/frontend/src/editor/subviews/Tiles.tsx @@ -3,8 +3,10 @@ import { OTTO_SUPERTILE_TEXMAP_SIZE, ottoMaticLevel, } from "../../python/structSpecs/ottoMaticInterface"; -import { Layer, Image } from "react-konva"; -import { useMemo } from "react"; +import { Layer, Image, Rect } from "react-konva"; +import { Fragment, useMemo } from "react"; +import { SelectedTile } from "../../data/tiles/tileAtoms"; +import { useAtom } from "jotai"; export function Tiles({ data, @@ -13,7 +15,8 @@ export function Tiles({ data: ottoMaticLevel; mapImages: HTMLCanvasElement[]; }) { - //if (!data.Itms) return <>; + //if (!data.Itms) return <>;\ + const [selectedTile, setSelectedTile] = useAtom(SelectedTile); const header = data.Hedr[1000].obj; const supertilesWide = header.mapWidth / OTTO_SUPERTILE_SIZE; const superTileGrid = data.STgd[1000].obj; @@ -29,19 +32,39 @@ export function Tiles({ return ( - {imageGrid.map((img, i) => ( - - ))} + {imageGrid.map((img, i) => { + const isSelected = selectedTile === i; + + return ( + + setSelectedTile(i)} + x={ + (i * OTTO_SUPERTILE_TEXMAP_SIZE) % + (OTTO_SUPERTILE_TEXMAP_SIZE * supertilesWide) + } + y={Math.floor(i / supertilesWide) * OTTO_SUPERTILE_TEXMAP_SIZE} + width={OTTO_SUPERTILE_TEXMAP_SIZE} + height={OTTO_SUPERTILE_TEXMAP_SIZE} + fill={isSelected ? "red" : ""} + /> + {isSelected && ( + setSelectedTile(i)} + x={ + (i * OTTO_SUPERTILE_TEXMAP_SIZE) % + (OTTO_SUPERTILE_TEXMAP_SIZE * supertilesWide) + } + y={Math.floor(i / supertilesWide) * OTTO_SUPERTILE_TEXMAP_SIZE} + width={OTTO_SUPERTILE_TEXMAP_SIZE} + height={OTTO_SUPERTILE_TEXMAP_SIZE} + stroke="red" + /> + )} + + ); + })} ); } diff --git a/frontend/src/editor/subviews/items/ItemMenu.tsx b/frontend/src/editor/subviews/items/ItemMenu.tsx index 3c0a019..67d44c4 100644 --- a/frontend/src/editor/subviews/items/ItemMenu.tsx +++ b/frontend/src/editor/subviews/items/ItemMenu.tsx @@ -1,14 +1,15 @@ import { Updater } from "use-immer"; import { ottoMaticLevel } from "../../../python/structSpecs/ottoMaticInterface"; -import { useAtom } from "jotai"; +import { useAtom, useSetAtom } from "jotai"; import { Button, DeleteButton } from "../../../components/Button"; -import { SelectedItem } from "../../../data/items/itemAtoms"; +import { ClickToAddItem, SelectedItem } from "../../../data/items/itemAtoms"; import { ItemType, itemTypeNames, ottoItemTypeParams, } from "../../../data/items/ottoItemType"; import { parseU16, parseU8 } from "../../../utils/numberParsers"; +import { useEffect } from "react"; export function ItemMenu({ data, @@ -29,9 +30,7 @@ export function ItemMenu({ return (
{itemData === null || itemData === undefined ? ( -
- -
+ ) : (

Item {itemData.type} ({itemTypeNames[itemData.type]}) ({itemData.x}, @@ -149,3 +148,40 @@ export function ItemMenu({

); } + +function AddItemMenu() { + const [clickToAddItem, setClickToAddItem] = useAtom(ClickToAddItem); + useEffect(() => { + return () => setClickToAddItem(undefined); + }, []); + + const itemValues = Object.keys(ItemType) + .map((key) => parseInt(key)) + .filter((key) => isNaN(key) === false); + + if (clickToAddItem !== undefined) + return ( + <> + +

Click on the Canvas to add the selected item

+ setClickToAddItem(undefined)}> + Stop Adding Items + + + ); + + return ; +} diff --git a/frontend/src/editor/subviews/tiles/Tiles.tsx b/frontend/src/editor/subviews/tiles/Tiles.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/src/editor/subviews/tiles/TilesMenu.tsx b/frontend/src/editor/subviews/tiles/TilesMenu.tsx index f1b3ec5..6bbe183 100644 --- a/frontend/src/editor/subviews/tiles/TilesMenu.tsx +++ b/frontend/src/editor/subviews/tiles/TilesMenu.tsx @@ -1,4 +1,59 @@ -export function TileMenu() { +import { useAtomValue } from "jotai"; +import { Layer, Stage, Image } from "react-konva"; +import { SelectedTile } from "../../../data/tiles/tileAtoms"; +import { Updater } from "use-immer"; +import { ottoMaticLevel } from "../../../python/structSpecs/ottoMaticInterface"; + +export function TileMenu({ + data, + setData, + mapImages, +}: { + mapImages: HTMLCanvasElement[]; + setMapImages: (newCanvases: HTMLCanvasElement[]) => void; + data: ottoMaticLevel; + setData: Updater; +}) { + const selectedTile = useAtomValue(SelectedTile); //data.YCrd[1000].obj[0] - return
; + console.log(mapImages); + if (!mapImages[20]) return <>; + console.log("Image found"); + return ( +
+
+ + + + + + +
+
+ ); +} + +function ImageDisplay({ image }: { image: HTMLCanvasElement }) { + if (!image) return <>; + + return ; } diff --git a/frontend/src/utils/imageConverter.test.ts b/frontend/src/utils/imageConverter.test.ts new file mode 100644 index 0000000..dcf804e --- /dev/null +++ b/frontend/src/utils/imageConverter.test.ts @@ -0,0 +1,33 @@ +import { expect, test } from "vitest"; +import { sixteenBitToImageData, imageDataToSixteenBit } from "./imageConverter"; + +test("convert", () => { + //Create a DataView + const data = new DataView(new ArrayBuffer(200)); + + for (let i = 0; i < 100; i++) { + //set to have random value + data.setUint16(i * 2, i); + } + + //Create canvas + const canvas = document.createElement("canvas"); + canvas.width = 10; + canvas.height = 10; + const canvasCtx = canvas.getContext("2d"); + if (!canvasCtx) throw new Error("Could not get canvas context"); + + const imageData = canvasCtx.getImageData(0, 0, canvas.width, canvas.height); + sixteenBitToImageData(data, imageData); + + console.log("imagedata", imageData); + + const output = imageDataToSixteenBit(imageData.data); + console.log(output); + + //Expect output to equal data + for (let i = 0; i < data.byteLength; i++) { + console.log(i, data, output); + expect(data.getUint8(i)).toEqual(output.getUint8(i)); + } +}); diff --git a/frontend/src/utils/imageConverter.ts b/frontend/src/utils/imageConverter.ts new file mode 100644 index 0000000..eb3fc63 --- /dev/null +++ b/frontend/src/utils/imageConverter.ts @@ -0,0 +1,47 @@ +export function sixteenBitToImageData(data: DataView, imageData: ImageData) { + if (imageData.data.length !== data.byteLength * 2) { + throw new Error("Data length does not match image data length"); + } + + for (let i = 0; i < data.byteLength; i += 2) { + const short = data.getUint16(i); + const r = ((short & 0x7c00) >> 10) * 8; + const g = ((short & 0x03e0) >> 5) * 8; + const b = (short & 0x001f) * 8; + + imageData.data[i * 2] = r; + imageData.data[i * 2 + 1] = g; + imageData.data[i * 2 + 2] = b; + imageData.data[i * 2 + 3] = short & 0x8000 ? 0 : 255; + } +} + +export function canvasDataToSixteenBit(canvas: HTMLCanvasElement): DataView { + const canvasCtx = canvas.getContext("2d"); + if (!canvasCtx) throw new Error("Could not get canvas context"); + const imageData = canvasCtx.getImageData(0, 0, canvas.width, canvas.height); + return imageDataToSixteenBit(imageData.data); +} + +export function imageDataToSixteenBit( + data: Uint8ClampedArray, +): DataView { + const output = new DataView(new ArrayBuffer(data.length / 2)); + for (let i = 0; i < data.length; i += 4) { + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + const a = data[i + 3]; + if (i == 0) { + console.log(r, g, b, a); + console.log( + ((r / 8) << 10) | ((g / 8) << 5) | (b / 8) | (a ? 0x8000 : 0x0), + ); + } + output.setUint16( + i / 2, + ((r / 8) << 10) | ((g / 8) << 5) | (b / 8) | (a ? 0x0 : 0x8000), + ); + } + return output; +} diff --git a/frontend/src/utils/lzss.ts b/frontend/src/utils/lzss.ts index a0207ed..a48f822 100644 --- a/frontend/src/utils/lzss.ts +++ b/frontend/src/utils/lzss.ts @@ -81,14 +81,13 @@ export function lzssCompress(sourceOriginalPtr: DataView) { const numHeaders = Math.ceil(sourceSize / 8); const destSize = sourceSize + numHeaders; - console.log("sourceSize", sourceSize, "destSize", destSize); + //console.log("sourceSize", sourceSize, "destSize", destSize); const outputBuffer = new DataView(new ArrayBuffer(destSize)); let destPos = 0; for (let sourcePos = 0; sourcePos < sourceSize; sourcePos++) { //Add 0xFF header if (sourcePos % 8 == 0) { outputBuffer.setUint8(destPos++, 0xff); - console.log("SETTING HEADER"); } outputBuffer.setUint8(destPos++, sourceOriginalPtr.getUint8(sourcePos)); } diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 013f544..f08d285 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,4 +1,4 @@ -import { defineConfig } from "vite"; +import { defineConfig } from "vitest/config"; import react from "@vitejs/plugin-react-swc"; // https://vitejs.dev/config/ @@ -8,4 +8,18 @@ export default defineConfig({ }, plugins: [react()], base: "/PangeaRSEdit/", + test: { + setupFiles: ["./vitest.setup.ts"], + environment: "jsdom", + deps: { + // vitest < 0.34 + inline: ["vitest-canvas-mock"], + // >= 0.34 + optimizer: { + web: { + include: ["vitest-canvas-mock"], + }, + }, + }, + }, }); diff --git a/frontend/vitest.setup.ts b/frontend/vitest.setup.ts new file mode 100644 index 0000000..4329989 --- /dev/null +++ b/frontend/vitest.setup.ts @@ -0,0 +1,2 @@ +// vitest.setup.ts +import "vitest-canvas-mock";