diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..1fe0a72 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,20 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "extends": [ + "eslint:recommended", + "plugin:prettier/recommended" + ], + "plugins": ["prettier"], + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "rules": { + "prettier/prettier": "error", + "no-unused-vars": "warn", + "no-console": "off" + } +} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..61f6660 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,39 @@ +name: Deploy perceptron preview + +on: + push: + branches: + - feat/perceptron + +permissions: + contents: write + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v3 + with: + version: 9 + + - uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: Install deps + run: pnpm install --frozen-lockfile + + - name: Build + env: + GITHUB_REF_NAME: ${{ github.ref_name }} + run: pnpm build + + - name: Deploy + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./dist + publish_branch: gh-pages + destination_dir: feat/perceptron \ No newline at end of file diff --git a/.gitignore b/.gitignore index 06362ca..3d2fec6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ dev/train/trainData/mnist/mnistTraindata_large.js dev/train/trainData/mnist/mnist_train.csv +node_modules +dist +build +*.min.js \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..904225f --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..bd46cef --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/.idea/prettier.xml b/.idea/prettier.xml new file mode 100644 index 0000000..ea0f3cf --- /dev/null +++ b/.idea/prettier.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..bae74ff --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "printWidth": 100, + "tabWidth": 4, + "trailingComma": "all", + "singleQuote": true, + "semi": true +} \ No newline at end of file diff --git a/assets/eraser.png b/assets/eraser.png new file mode 100644 index 0000000..19ea888 Binary files /dev/null and b/assets/eraser.png differ diff --git a/assets/left_arrow.png b/assets/left_arrow.png new file mode 100644 index 0000000..2a2f658 Binary files /dev/null and b/assets/left_arrow.png differ diff --git a/assets/right_arrow.png b/assets/right_arrow.png new file mode 100644 index 0000000..6219672 Binary files /dev/null and b/assets/right_arrow.png differ diff --git a/dev/train/Tester.js b/dev/train/Tester.js index 2fc5f4f..195798d 100644 --- a/dev/train/Tester.js +++ b/dev/train/Tester.js @@ -1,22 +1,26 @@ -import {Trainer} from "./Trainer.js"; +import { Trainer } from './Trainer.js'; // data sets -import {mnistTrainData} from "./trainData/mnist/mnistTrainData.js"; -import {mnistTestData} from "./trainData/mnist/mnistTestData.js"; -import {mnistTrainData_large} from "./trainData/mnist/mnistTraindata_large.js"; - -// train utils -import {normalizeInputs, oneHotEncodeLabels} from "./utils/trainDataHandler.js"; -import {saveWeightsAsJson} from "./utils/saveWeightsAsJson.js"; +import { mnistTrainData } from './trainData/mnist/mnistTrainData.js'; +import { mnistTestData } from './trainData/mnist/mnistTestData.js'; +import { mnistTrainData_large } from './trainData/mnist/mnistTraindata_large.js'; +// train canvasUtils +import { normalizeInputs, oneHotEncodeLabels } from './utils/trainDataHandler.js'; +import { saveWeightsAsJson } from './utils/saveWeightsAsJson.js'; // export const tester = () => { - const networkConfig = {inputNodes: 784, hiddenNodes: 200, outputNodes: 10, learningRate: 0.15}; + const networkConfig = { + inputNodes: 784, + hiddenNodes: 200, + outputNodes: 10, + learningRate: 0.15, + }; const $NN = new Trainer({ inputNodes: networkConfig.inputNodes, hiddenNodes: networkConfig.hiddenNodes, outputNodes: networkConfig.outputNodes, - learningRate: networkConfig.learningRate + learningRate: networkConfig.learningRate, }); const inputs = normalizeInputs(mnistTrainData); @@ -25,16 +29,16 @@ export const tester = () => { inputs.map((array, idx) => { $NN.train(array, targets[idx]); if (idx % 5000 === 0) console.log(`Training progress: ${idx}/60000`); - console.log("✅ training complete!"); + console.log('✅ training complete!'); }); - for (let i = 0; i<9; i++){ + for (let i = 0; i < 9; i++) { console.log('Query', mnistTestData[i][0]); console.log($NN.query(normalizeInputs(mnistTestData)[i])); } saveWeightsAsJson($NN.W_inputToHidden, $NN.W_hiddenToOutput); - console.log("✅ Weight Downloaded!"); -} + console.log('✅ Weight Downloaded!'); +}; -export default tester; \ No newline at end of file +export default tester; diff --git a/dev/train/Trainer.js b/dev/train/Trainer.js index 3594d8f..0434c50 100644 --- a/dev/train/Trainer.js +++ b/dev/train/Trainer.js @@ -1,27 +1,44 @@ -import { NeuralNetworkBase } from "../../src/core/NeuralNetworkBase.js"; +import { NeuralNetworkBase } from '../../src/core/NeuralNetworkBase.ts'; // matrix operations -import {matrixMultiply, transposeMatrix} from "../../src/core/ops/matrixOps.js"; +import { matrixMultiply, transposeMatrix } from '../../src/core/ops/matrixOps.ts'; export class Trainer extends NeuralNetworkBase { - train(inputs, targets){ + train(inputs, targets) { // CNN operations const { finalOutputs, hiddenOutputs } = this.feedForward(inputs); // calculate errors - const output_errors = targets.map((v, idx) => v - finalOutputs[idx]).map(v => [v]); // 출력 계층의 오차를 목표값 - 출력값으로 지정 + const output_errors = targets.map((v, idx) => v - finalOutputs[idx]).map((v) => [v]); // 출력 계층의 오차를 목표값 - 출력값으로 지정 const hidden_errors = matrixMultiply(transposeMatrix(this.W_hiddenToOutput), output_errors); // 은닉 계층의 오차를 은닉-> 출력 계층의 가중치값과(W_hiddenToOutput.T) 출력 계층의 오차들을 재조합하여 계산 // activation function derivative - const activationDerivative_HtO = finalOutputs.map(v => 1.0 - v).map((v, idx) => v * finalOutputs[idx]); // hidden to output derivative - const outputGradient = activationDerivative_HtO.map((v, idx) => v * output_errors[idx]).map(v => [v]); - const W_HtO_Update = matrixMultiply(outputGradient, transposeMatrix(hiddenOutputs)).map(array => array.map(v => v * this.learningRate)); // 오차값을 이용해 은닉 계층과 출력 계층간의 가중치 업데이트 + const activationDerivative_HtO = finalOutputs + .map((v) => 1.0 - v) + .map((v, idx) => v * finalOutputs[idx]); // hidden to output derivative + const outputGradient = activationDerivative_HtO + .map((v, idx) => v * output_errors[idx]) + .map((v) => [v]); + const W_HtO_Update = matrixMultiply(outputGradient, transposeMatrix(hiddenOutputs)).map( + (array) => array.map((v) => v * this.learningRate), + ); // 오차값을 이용해 은닉 계층과 출력 계층간의 가중치 업데이트 - const activationDerivative_ItH = hiddenOutputs.map(v => 1.0 - v).map((v, idx) => v * hiddenOutputs[idx]); - const hiddenGradient = activationDerivative_ItH.map((v, idx) => v * hidden_errors[idx]).map(v => [v]); - const W_ItH_update = matrixMultiply(hiddenGradient, transposeMatrix(inputs.map(v => [v]))).map(array => array.map(v => v * this.learningRate)); + const activationDerivative_ItH = hiddenOutputs + .map((v) => 1.0 - v) + .map((v, idx) => v * hiddenOutputs[idx]); + const hiddenGradient = activationDerivative_ItH + .map((v, idx) => v * hidden_errors[idx]) + .map((v) => [v]); + const W_ItH_update = matrixMultiply( + hiddenGradient, + transposeMatrix(inputs.map((v) => [v])), + ).map((array) => array.map((v) => v * this.learningRate)); // update weights - this.W_hiddenToOutput = this.W_hiddenToOutput.map((row, i)=> row.map((v, j) => v + W_HtO_Update[i][j])); - this.W_inputToHidden = this.W_inputToHidden.map((row, i)=> row.map((v, j) => v + W_ItH_update[i][j])); + this.W_hiddenToOutput = this.W_hiddenToOutput.map((row, i) => + row.map((v, j) => v + W_HtO_Update[i][j]), + ); + this.W_inputToHidden = this.W_inputToHidden.map((row, i) => + row.map((v, j) => v + W_ItH_update[i][j]), + ); } -} \ No newline at end of file +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..e73b62e --- /dev/null +++ b/index.html @@ -0,0 +1,57 @@ + + + + + + + Neural Network Visualization + + + + + + +
+
+

Neural Network

+

visualization - Mobile Beta v1.0

+
+
+
+
+
+
+
+
+
+ arrow_icon +
+
+
+
+ arrow_icon +
+
+
+ +
+ +
+ + + + + + + + +
+
+
+
+
+ + \ No newline at end of file diff --git a/jsconfig.json b/jsconfig.json index 8ecfcad..1286948 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,9 +1,8 @@ { "compilerOptions": { - "baseUrl": "src", + "baseUrl": "./src", "paths": { - "@controller/*": ["controller/*"], - "@view/*": ["view/*"] + "@/*": ["*"] } }, "include": ["src"] diff --git a/package.json b/package.json index 15fecff..7e7c6a2 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,25 @@ "version": "1.0.0", "type": "module", "description": "", - "main": "public/index.html", + "main": "index.html", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "vite", + "build": "vite build", + "deploy": "gh-pages -d dist", + "preview": "vite preview", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --write ." }, - "private": true -} + "private": true, + "devDependencies": { + "@types/node": "^24.3.0", + "eslint": "^9.32.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.3", + "gh-pages": "^6.3.0", + "prettier": "3.6.2", + "vite": "^7.0.6" + } +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..7e9ab80 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1681 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@types/node': + specifier: ^24.3.0 + version: 24.3.0 + eslint: + specifier: ^9.32.0 + version: 9.32.0 + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@9.32.0) + eslint-plugin-prettier: + specifier: ^5.5.3 + version: 5.5.3(eslint-config-prettier@10.1.8(eslint@9.32.0))(eslint@9.32.0)(prettier@3.6.2) + gh-pages: + specifier: ^6.3.0 + version: 6.3.0 + prettier: + specifier: 3.6.2 + version: 3.6.2 + vite: + specifier: ^7.0.6 + version: 7.0.6(@types/node@24.3.0) + +packages: + + '@esbuild/aix-ppc64@0.25.8': + resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.8': + resolution: {integrity: sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.8': + resolution: {integrity: sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.8': + resolution: {integrity: sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.8': + resolution: {integrity: sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.8': + resolution: {integrity: sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.8': + resolution: {integrity: sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.8': + resolution: {integrity: sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.8': + resolution: {integrity: sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.8': + resolution: {integrity: sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.8': + resolution: {integrity: sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.8': + resolution: {integrity: sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.8': + resolution: {integrity: sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.8': + resolution: {integrity: sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.8': + resolution: {integrity: sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.8': + resolution: {integrity: sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.8': + resolution: {integrity: sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.8': + resolution: {integrity: sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.8': + resolution: {integrity: sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.8': + resolution: {integrity: sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.8': + resolution: {integrity: sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.8': + resolution: {integrity: sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.8': + resolution: {integrity: sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.8': + resolution: {integrity: sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.8': + resolution: {integrity: sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.8': + resolution: {integrity: sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.7.0': + resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.3.0': + resolution: {integrity: sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.15.1': + resolution: {integrity: sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.32.0': + resolution: {integrity: sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.3.4': + resolution: {integrity: sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@rollup/rollup-android-arm-eabi@4.46.2': + resolution: {integrity: sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.46.2': + resolution: {integrity: sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.46.2': + resolution: {integrity: sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.46.2': + resolution: {integrity: sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.46.2': + resolution: {integrity: sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.46.2': + resolution: {integrity: sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.46.2': + resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.46.2': + resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.46.2': + resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.46.2': + resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.46.2': + resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.46.2': + resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.46.2': + resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.46.2': + resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.46.2': + resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.46.2': + resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.46.2': + resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.46.2': + resolution: {integrity: sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.46.2': + resolution: {integrity: sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.46.2': + resolution: {integrity: sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@24.3.0': + resolution: {integrity: sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + email-addresses@5.0.0: + resolution: {integrity: sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==} + + esbuild@0.25.8: + resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==} + engines: {node: '>=18'} + hasBin: true + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.5.3: + resolution: {integrity: sha512-NAdMYww51ehKfDyDhv59/eIItUVzU0Io9H2E8nHNGKEeeqlnci+1gCvrHib6EmZdf6GxF+LCV5K7UC65Ezvw7w==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.32.0: + resolution: {integrity: sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + filename-reserved-regex@2.0.0: + resolution: {integrity: sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==} + engines: {node: '>=4'} + + filenamify@4.3.0: + resolution: {integrity: sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==} + engines: {node: '>=8'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-cache-dir@3.3.2: + resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + fs-extra@11.3.2: + resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} + engines: {node: '>=14.14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gh-pages@6.3.0: + resolution: {integrity: sha512-Ot5lU6jK0Eb+sszG8pciXdjMXdBJ5wODvgjR+imihTqsUWF2K6dJ9HST55lgqcs8wWcw6o6wAsUzfcYRhJPXbA==} + engines: {node: '>=10'} + hasBin: true + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.46.2: + resolution: {integrity: sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-outer@1.0.1: + resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==} + engines: {node: '>=0.10.0'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + synckit@0.11.11: + resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} + engines: {node: ^14.18.0 || >=16.0.0} + + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + trim-repeated@1.0.0: + resolution: {integrity: sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==} + engines: {node: '>=0.10.0'} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + undici-types@7.10.0: + resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite@7.0.6: + resolution: {integrity: sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@esbuild/aix-ppc64@0.25.8': + optional: true + + '@esbuild/android-arm64@0.25.8': + optional: true + + '@esbuild/android-arm@0.25.8': + optional: true + + '@esbuild/android-x64@0.25.8': + optional: true + + '@esbuild/darwin-arm64@0.25.8': + optional: true + + '@esbuild/darwin-x64@0.25.8': + optional: true + + '@esbuild/freebsd-arm64@0.25.8': + optional: true + + '@esbuild/freebsd-x64@0.25.8': + optional: true + + '@esbuild/linux-arm64@0.25.8': + optional: true + + '@esbuild/linux-arm@0.25.8': + optional: true + + '@esbuild/linux-ia32@0.25.8': + optional: true + + '@esbuild/linux-loong64@0.25.8': + optional: true + + '@esbuild/linux-mips64el@0.25.8': + optional: true + + '@esbuild/linux-ppc64@0.25.8': + optional: true + + '@esbuild/linux-riscv64@0.25.8': + optional: true + + '@esbuild/linux-s390x@0.25.8': + optional: true + + '@esbuild/linux-x64@0.25.8': + optional: true + + '@esbuild/netbsd-arm64@0.25.8': + optional: true + + '@esbuild/netbsd-x64@0.25.8': + optional: true + + '@esbuild/openbsd-arm64@0.25.8': + optional: true + + '@esbuild/openbsd-x64@0.25.8': + optional: true + + '@esbuild/openharmony-arm64@0.25.8': + optional: true + + '@esbuild/sunos-x64@0.25.8': + optional: true + + '@esbuild/win32-arm64@0.25.8': + optional: true + + '@esbuild/win32-ia32@0.25.8': + optional: true + + '@esbuild/win32-x64@0.25.8': + optional: true + + '@eslint-community/eslint-utils@4.7.0(eslint@9.32.0)': + dependencies: + eslint: 9.32.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.21.0': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.1 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.3.0': {} + + '@eslint/core@0.15.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.1 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.32.0': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.3.4': + dependencies: + '@eslint/core': 0.15.1 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@pkgr/core@0.2.9': {} + + '@rollup/rollup-android-arm-eabi@4.46.2': + optional: true + + '@rollup/rollup-android-arm64@4.46.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.46.2': + optional: true + + '@rollup/rollup-darwin-x64@4.46.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.46.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.46.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.46.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.46.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.46.2': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.46.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.46.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.46.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.46.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.46.2': + optional: true + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@24.3.0': + dependencies: + undici-types: 7.10.0 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + async@3.2.6: {} + + balanced-match@1.0.2: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + callsites@3.1.0: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@13.1.0: {} + + commondir@1.0.1: {} + + concat-map@0.0.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + email-addresses@5.0.0: {} + + esbuild@0.25.8: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.8 + '@esbuild/android-arm': 0.25.8 + '@esbuild/android-arm64': 0.25.8 + '@esbuild/android-x64': 0.25.8 + '@esbuild/darwin-arm64': 0.25.8 + '@esbuild/darwin-x64': 0.25.8 + '@esbuild/freebsd-arm64': 0.25.8 + '@esbuild/freebsd-x64': 0.25.8 + '@esbuild/linux-arm': 0.25.8 + '@esbuild/linux-arm64': 0.25.8 + '@esbuild/linux-ia32': 0.25.8 + '@esbuild/linux-loong64': 0.25.8 + '@esbuild/linux-mips64el': 0.25.8 + '@esbuild/linux-ppc64': 0.25.8 + '@esbuild/linux-riscv64': 0.25.8 + '@esbuild/linux-s390x': 0.25.8 + '@esbuild/linux-x64': 0.25.8 + '@esbuild/netbsd-arm64': 0.25.8 + '@esbuild/netbsd-x64': 0.25.8 + '@esbuild/openbsd-arm64': 0.25.8 + '@esbuild/openbsd-x64': 0.25.8 + '@esbuild/openharmony-arm64': 0.25.8 + '@esbuild/sunos-x64': 0.25.8 + '@esbuild/win32-arm64': 0.25.8 + '@esbuild/win32-ia32': 0.25.8 + '@esbuild/win32-x64': 0.25.8 + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@9.32.0): + dependencies: + eslint: 9.32.0 + + eslint-plugin-prettier@5.5.3(eslint-config-prettier@10.1.8(eslint@9.32.0))(eslint@9.32.0)(prettier@3.6.2): + dependencies: + eslint: 9.32.0 + prettier: 3.6.2 + prettier-linter-helpers: 1.0.0 + synckit: 0.11.11 + optionalDependencies: + eslint-config-prettier: 10.1.8(eslint@9.32.0) + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.32.0: + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.32.0) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.0 + '@eslint/core': 0.15.1 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.32.0 + '@eslint/plugin-kit': 0.3.4 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.1 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fdir@6.4.6(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + filename-reserved-regex@2.0.0: {} + + filenamify@4.3.0: + dependencies: + filename-reserved-regex: 2.0.0 + strip-outer: 1.0.1 + trim-repeated: 1.0.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-cache-dir@3.3.2: + dependencies: + commondir: 1.0.1 + make-dir: 3.1.0 + pkg-dir: 4.2.0 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + fs-extra@11.3.2: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fsevents@2.3.3: + optional: true + + gh-pages@6.3.0: + dependencies: + async: 3.2.6 + commander: 13.1.0 + email-addresses: 5.0.0 + filenamify: 4.3.0 + find-cache-dir: 3.3.2 + fs-extra: 11.3.2 + globby: 11.1.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + isexe@2.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-try@2.2.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-type@4.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.0: + dependencies: + fast-diff: 1.3.0 + + prettier@3.6.2: {} + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + resolve-from@4.0.0: {} + + reusify@1.1.0: {} + + rollup@4.46.2: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.46.2 + '@rollup/rollup-android-arm64': 4.46.2 + '@rollup/rollup-darwin-arm64': 4.46.2 + '@rollup/rollup-darwin-x64': 4.46.2 + '@rollup/rollup-freebsd-arm64': 4.46.2 + '@rollup/rollup-freebsd-x64': 4.46.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.46.2 + '@rollup/rollup-linux-arm-musleabihf': 4.46.2 + '@rollup/rollup-linux-arm64-gnu': 4.46.2 + '@rollup/rollup-linux-arm64-musl': 4.46.2 + '@rollup/rollup-linux-loongarch64-gnu': 4.46.2 + '@rollup/rollup-linux-ppc64-gnu': 4.46.2 + '@rollup/rollup-linux-riscv64-gnu': 4.46.2 + '@rollup/rollup-linux-riscv64-musl': 4.46.2 + '@rollup/rollup-linux-s390x-gnu': 4.46.2 + '@rollup/rollup-linux-x64-gnu': 4.46.2 + '@rollup/rollup-linux-x64-musl': 4.46.2 + '@rollup/rollup-win32-arm64-msvc': 4.46.2 + '@rollup/rollup-win32-ia32-msvc': 4.46.2 + '@rollup/rollup-win32-x64-msvc': 4.46.2 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + semver@6.3.1: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + slash@3.0.0: {} + + source-map-js@1.2.1: {} + + strip-json-comments@3.1.1: {} + + strip-outer@1.0.1: + dependencies: + escape-string-regexp: 1.0.5 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + synckit@0.11.11: + dependencies: + '@pkgr/core': 0.2.9 + + tinyglobby@0.2.14: + dependencies: + fdir: 6.4.6(picomatch@4.0.3) + picomatch: 4.0.3 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + trim-repeated@1.0.0: + dependencies: + escape-string-regexp: 1.0.5 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + undici-types@7.10.0: {} + + universalify@2.0.1: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite@7.0.6(@types/node@24.3.0): + dependencies: + esbuild: 0.25.8 + fdir: 6.4.6(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.46.2 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 24.3.0 + fsevents: 2.3.3 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + yocto-queue@0.1.0: {} diff --git a/public/assets/icons/left_arrow.png b/public/assets/icons/left_arrow.png new file mode 100644 index 0000000..2a2f658 Binary files /dev/null and b/public/assets/icons/left_arrow.png differ diff --git a/public/assets/icons/right_arrow.png b/public/assets/icons/right_arrow.png new file mode 100644 index 0000000..46220fb Binary files /dev/null and b/public/assets/icons/right_arrow.png differ diff --git a/public/index.html b/public/index.html deleted file mode 100644 index 7df7fa3..0000000 --- a/public/index.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - Neural Network Visualization - - - - - - -
-
-
-
-
-
- -
-
- - - - -
-
- - \ No newline at end of file diff --git a/src/controller/AppContoller.js b/src/controller/AppContoller.js deleted file mode 100644 index 22fd9c7..0000000 --- a/src/controller/AppContoller.js +++ /dev/null @@ -1,35 +0,0 @@ -import WeightManager from "../core/WeightManager.js"; -import NeuralNetworkBase from "../core/NeuralNetworkBase.js"; -import QueryProcessController from "./queryPipeline/QueryProcessController.js"; -import UserInputCanvas from "../view/components/userQuery/UserInputCanvas.js"; -import PathTrackingCanvas from "../view/components/userQuery/PathTrackingCanvas.js"; -import AlignCanvas from "../view/components/userQuery/AlignCanvas.js"; -import ResizeCanvas from "../view/components/userQuery/ResizeCanvas.js"; -import { DataStore } from "./DataStore.js"; - -import eventBus from "./EventBus.js"; -import { DATA_EVENTS, HANDLER_EVENTS } from "./constants/events.js"; -import { NETWORK_CONFIG } from "./constants/networkConfig.js"; - -class AppController { - constructor({ dataStore }) { - this.dataStore = dataStore; - } - - async initialize() { - const $WM = new WeightManager(NETWORK_CONFIG); - const $NN = new NeuralNetworkBase(await $WM.getWeights()); - const $DS = new DataStore(); - const $QC = new QueryProcessController({ - userInputCanvas: new UserInputCanvas(), - trackingCanvas: new PathTrackingCanvas(), - alignCanvas: new AlignCanvas(), - resizeCanvas: new ResizeCanvas(), - $NN: $NN, - }); - eventBus.emit(HANDLER_EVENTS.APP_READY, this.dataStore); - eventBus.on(DATA_EVENTS.RESULT_CHANGED, (data) => {console.log("RESULT CHANGED", data)}); - }; -} - -export default AppController; \ No newline at end of file diff --git a/src/controller/AppController.ts b/src/controller/AppController.ts new file mode 100644 index 0000000..038b08f --- /dev/null +++ b/src/controller/AppController.ts @@ -0,0 +1,75 @@ +import WeightManager from '@/core/WeightManager.ts'; +import NeuralNetworkBase from '@/core/NeuralNetworkBase.ts'; +import QueryProcessController from './queryPipeline/QueryProcessController.ts'; +import PerceptronController from '@/controller/perceptron/PerceptronController.ts'; +import BaseCanvas from '@/view/components/perceptron/BaseCanvas.ts'; +import UserInputCanvas from '@/view/components/userQuery/UserInputCanvas.ts'; +import PathTrackingCanvas from '@/view/components/userQuery/PathTrackingCanvas.ts'; +import AlignCanvas from '@/view/components/userQuery/AlignCanvas.ts'; +import ResizeCanvas from '@/view/components/userQuery/ResizeCanvas.ts'; +import DataStore from './DataStore.ts'; + +import DrawingEventHandler from './queryPipeline/DrawingEventHandler.js'; +import NodeRenderer from '@/view/components/perceptron/NodeRenderer.ts'; + +import eventBus from './EventBus.ts'; +import { DATA_EVENTS, HANDLER_EVENTS } from './constants/events.ts'; +import { NETWORK_CONFIG } from './constants/networkConfig.ts'; +import { IWeightManager } from '@/core/types/WeightManager.ts'; +import { INeuralNetworkBase } from '@/core/types/NeuralNetworkBase.ts'; +import { IDataStore } from './types/DataStore.ts'; +import { Matrix2D } from '@/core/ops/types/OpsType.ts'; +import { NodeHandler } from '@/controller/perceptron/NodeHandler.ts'; +import EdgeHandler from '@/controller/perceptron/EdgeHandler.ts'; +import ScrollEventHandler from '@/controller/perceptron/ScrollEventHandler.ts'; +import EdgeRenderer from '@/view/components/perceptron/EdgeRenderer.ts'; +import GridRenderer from '@/view/components/perceptron/GridRenderer.ts'; +import GridHandler from '@/controller/perceptron/GridHandler.ts'; +import ViewPresenter from '@/view/ViewPresenter.ts'; +import ViewportAdapter from '@/view/styles/ViewportAdapter.ts'; + +class AppController { + private $WM: IWeightManager; + private $NN: INeuralNetworkBase; + private $DS: IDataStore; + + async initialize() { + const $WM = new WeightManager(NETWORK_CONFIG); + const $NN = new NeuralNetworkBase(await $WM.getWeights()); + const $DS = DataStore; + new DrawingEventHandler(); + const $QC = new QueryProcessController({ + userInputCanvas: new UserInputCanvas(), + trackingCanvas: new PathTrackingCanvas(), + alignCanvas: new AlignCanvas(), + resizeCanvas: new ResizeCanvas(), + $NN: $NN, + }); + + new ViewportAdapter(); + const perceptronBaseCanvas = new BaseCanvas(); + const nodeRenderer = new NodeRenderer(perceptronBaseCanvas.getCtx()); + const nodeHandler = new NodeHandler({ + networkConfig: NETWORK_CONFIG, + nodeRenderer, + perceptronBaseCanvas, + }); + const edgeRenderer = new EdgeRenderer(perceptronBaseCanvas.getCtx()); + const edgeHandler = new EdgeHandler({ edgeRenderer, perceptronBaseCanvas }); + const gridRenderer = new GridRenderer(perceptronBaseCanvas.getCtx()); + const gridHandler = new GridHandler({ gridRenderer, perceptronBaseCanvas }); + const $PC = new PerceptronController({ + NodeRenderer: nodeRenderer, + EdgeRenderer: edgeRenderer, + GridRenderer: gridRenderer, + NodeHandler: nodeHandler, + EdgeHandler: edgeHandler, + GridHandler: gridHandler, + BaseCanvas: perceptronBaseCanvas, + ScrollEventHandler: new ScrollEventHandler(perceptronBaseCanvas), + }); + new ViewPresenter(); + } +} + +export default AppController; diff --git a/src/controller/DataStore.ts b/src/controller/DataStore.ts new file mode 100644 index 0000000..2d8d4d7 --- /dev/null +++ b/src/controller/DataStore.ts @@ -0,0 +1,70 @@ +import eventBus from './EventBus.js'; +import { DATA_EVENTS } from './constants/events.js'; +import { RENDERER_CONFIG } from '@/controller/constants/rendererConfig.ts'; +const { displayNodes, displayEdges } = RENDERER_CONFIG; + +import { nodeState } from './types/DataStore'; +import { Matrix2D } from '@/core/ops/types/OpsType.ts'; +import { NodeObjectSet } from '@/controller/perceptron/types/nodeObjectSet.ts'; + +class DataStore { + private queryInfo: number[] | null = null; + private nodeState: nodeState | null = null; + private queryResult: Matrix2D | null = null; + + private PERCEPTRON_CONFIG: { displayNodes: number; displayEdges: number } = null; + + constructor() { + this.queryInfo = null; + this.nodeState = null; + this.queryResult = null; + + this.PERCEPTRON_CONFIG = { displayNodes: displayNodes, displayEdges: displayEdges }; + } + + setQueryInfo(queryInfo: number[]): void { + if (queryInfo === null) throw new Error('No query info found'); + this.queryInfo = queryInfo; + eventBus.emit(DATA_EVENTS.QUERY_CHANGED, this.queryInfo); + } + getQueryInfo(): number[] { + return this.queryInfo; + } + + setNodeState(nodeState: nodeState): void { + if (nodeState === null) throw new Error('No node state found'); + this.nodeState = nodeState; + eventBus.emit(DATA_EVENTS.NODE_CHANGED, { + inputs: nodeState.inputs, + hiddenOutputs: nodeState.hiddenOutputs, + finalOutputs: nodeState.finalOutputs, + }); + } + getNodeState(): nodeState { + return this.nodeState; + } + + setQueryResult(queryResult: Matrix2D): void { + if (queryResult === null) throw new Error('No query result found'); + this.queryResult = queryResult; + eventBus.emit(DATA_EVENTS.RESULT_CHANGED, queryResult); + } + getQueryResult(): Matrix2D | null { + return this.queryResult; + } + + setPerceptronConfig(perceptronConfig: { displayNodes: number; displayEdges: number }): void { + this.PERCEPTRON_CONFIG = perceptronConfig; + } + getPerceptronConfig(): { displayNodes: number; displayEdges: number } { + return this.PERCEPTRON_CONFIG; + } + + resetPerceptronState(): void { + this.queryInfo = null; + this.nodeState = null; + this.queryResult = null; + } +} + +export default new DataStore(); diff --git a/src/controller/EventBus.js b/src/controller/EventBus.js deleted file mode 100644 index e8d182a..0000000 --- a/src/controller/EventBus.js +++ /dev/null @@ -1,20 +0,0 @@ -const EventBus = { - _events: {}, - - on(eventName, subscriber) { - if(!this._events[eventName]) this._events[eventName] = []; - this._events[eventName].push(subscriber); - }, - - off(eventName, subscriber) { - if(!this._events[eventName]) return; - this._events[eventName] = this._events[eventName].filter(fn => fn !== subscriber); - }, - - emit(eventName, payload) { - if(!this._events[eventName]) return; - this._events[eventName].forEach(fn => fn(payload)); - } -} - -export default EventBus; \ No newline at end of file diff --git a/src/controller/EventBus.ts b/src/controller/EventBus.ts new file mode 100644 index 0000000..ffe271f --- /dev/null +++ b/src/controller/EventBus.ts @@ -0,0 +1,33 @@ +import { IEventBus } from './types/eventBus'; +import { eventPayloads } from './types/eventBus'; + +const EventBus: IEventBus = { + events: {}, + + on( + eventName: K, + subscriber: (payload: eventPayloads[K]) => any, + ): void { + if (!this.events[eventName]) this.events[eventName] = []; + this.events[eventName].push(subscriber); + }, + + off( + eventName: K, + subscriber: (payload: eventPayloads[K]) => any, + ): void { + if (!this.events[eventName]) return; + this.events[eventName] = this.events[eventName].filter( + (fn: (payload: eventPayloads[K]) => void): boolean => fn !== subscriber, + ); + }, + + emit(eventName: K, payload: eventPayloads[K]): void { + if (!this.events[eventName]) return; + this.events[eventName].forEach((fn: (payload: eventPayloads[K]) => any): any => + fn(payload), + ); + }, +}; + +export default EventBus; diff --git a/src/controller/constants/canvasConfig.js b/src/controller/constants/canvasConfig.ts similarity index 53% rename from src/controller/constants/canvasConfig.js rename to src/controller/constants/canvasConfig.ts index 23eef1c..dbca9a0 100644 --- a/src/controller/constants/canvasConfig.js +++ b/src/controller/constants/canvasConfig.ts @@ -2,4 +2,6 @@ export const CANVAS_CONFIG = Object.freeze({ lineWidth: 20, lineCap: 'round', lineJoin: 'round', -}); \ No newline at end of file +} as const); + +export type CanvasConfig = (typeof CANVAS_CONFIG)[keyof typeof CANVAS_CONFIG]; diff --git a/src/controller/constants/events.js b/src/controller/constants/events.js deleted file mode 100644 index edebe68..0000000 --- a/src/controller/constants/events.js +++ /dev/null @@ -1,21 +0,0 @@ -export const DATA_EVENTS = Object.freeze({ - QUERY_UPDATE: 'query:update', - QUERY_CHANGED: 'query:changed', - NODE_UPDATE: 'node:update', - NODE_CHANGED: 'node:changed', - RESULT_UPDATE: 'result:update', - RESULT_CHANGED: 'result:changed' -}); - -export const HANDLER_EVENTS = Object.freeze({ - APP_READY: 'app:ready', - NN_INITIALIZE: 'nn:initialize', - ICH_INITIALIZE: 'ich:initialize', -}) - -export const DRAWING_EVENTS = Object.freeze({ - START_DRAW: 'draw: start', - DRAW: 'draw: drawing', - END_DRAW: 'draw: end', - BOUNDINGBOX_UPDATE: 'boundingbox:update', -}) \ No newline at end of file diff --git a/src/controller/constants/events.ts b/src/controller/constants/events.ts new file mode 100644 index 0000000..84b6cf1 --- /dev/null +++ b/src/controller/constants/events.ts @@ -0,0 +1,31 @@ +export const DATA_EVENTS = Object.freeze({ + QUERY_UPDATE: 'query:update', + QUERY_CHANGED: 'query:changed', + NODE_UPDATE: 'node:update', + NODE_CHANGED: 'node:changed', + RESULT_UPDATE: 'result:update', + RESULT_CHANGED: 'result:changed', +} as const); + +export const HANDLER_EVENTS = Object.freeze({ + APP_READY: 'app:ready', + NN_INITIALIZE: 'nn:initialize', + ICH_INITIALIZE: 'ich:initialize', +} as const); + +export const SCROLL_EVENTS = Object.freeze({ + SCROLL_CHANGED: 'scroll:changed', + TOUCH_CHANGED: 'touch:changed', +}); + +export const DRAWING_EVENTS = Object.freeze({ + START_DRAW: 'draw:start', + DRAW: 'draw:drawing', + END_DRAW: 'draw:end', + CLEAR_DRAW: 'draw:clear', + BOUNDINGBOX_UPDATE: 'boundingbox:update', +} as const); + +export type DataEvent = (typeof DATA_EVENTS)[keyof typeof DATA_EVENTS]; +export type HandlerEvent = (typeof DATA_EVENTS)[keyof typeof DATA_EVENTS]; +export type DrawingEvent = (typeof DATA_EVENTS)[keyof typeof DATA_EVENTS]; diff --git a/src/controller/constants/networkConfig.js b/src/controller/constants/networkConfig.js deleted file mode 100644 index 8d6a6c8..0000000 --- a/src/controller/constants/networkConfig.js +++ /dev/null @@ -1,6 +0,0 @@ -export const NETWORK_CONFIG = { - inputNodes: 784, - hiddenNodes: 200, - outputNodes: 10, - learningRate: 0.15, -} \ No newline at end of file diff --git a/src/controller/constants/networkConfig.ts b/src/controller/constants/networkConfig.ts new file mode 100644 index 0000000..e759116 --- /dev/null +++ b/src/controller/constants/networkConfig.ts @@ -0,0 +1,7 @@ +import { networkConfig } from './types/networkConfig'; + +export const NETWORK_CONFIG: networkConfig = { + inputNodes: 784, + hiddenNodes: 200, + outputNodes: 10, +}; diff --git a/src/controller/constants/rendererConfig.ts b/src/controller/constants/rendererConfig.ts new file mode 100644 index 0000000..2b340cb --- /dev/null +++ b/src/controller/constants/rendererConfig.ts @@ -0,0 +1,12 @@ +import { rendererConfig } from '@/controller/constants/types/rendererConfig.ts'; + +export const RENDERER_CONFIG: rendererConfig = { + rotationDelta: 1.7, // 노드간 기본 간격 + degree: Math.PI / 180, // 1도(radian -> degree) + mouseScroll: 1, // 마우스 스크롤 값 + displayNodes: 29, // 화면에 표시할 노드의 개수 + displayEdges: 15, // 각 노드당 연결될 간선의 개수 + scrollDivider: 4, // 스크롤 n틱당 노드 한개 표시(ex. 스크롤 4번시 캔버스 회전) + gridWidth: 1, // 그리드간 간격 + layerHeight: [600, 635, 670], // 노드의 레이어당 높이(distance from center) +}; diff --git a/src/view/components/perceptron/Perceptron.js b/src/controller/constants/types/events.ts similarity index 100% rename from src/view/components/perceptron/Perceptron.js rename to src/controller/constants/types/events.ts diff --git a/src/controller/constants/types/networkConfig.ts b/src/controller/constants/types/networkConfig.ts new file mode 100644 index 0000000..5baddcd --- /dev/null +++ b/src/controller/constants/types/networkConfig.ts @@ -0,0 +1,5 @@ +export interface networkConfig { + inputNodes: number; + hiddenNodes: number; + outputNodes: number; +} diff --git a/src/controller/constants/types/rendererConfig.ts b/src/controller/constants/types/rendererConfig.ts new file mode 100644 index 0000000..070610c --- /dev/null +++ b/src/controller/constants/types/rendererConfig.ts @@ -0,0 +1,10 @@ +export interface rendererConfig { + rotationDelta: number; + degree: number; + mouseScroll: number; + displayNodes: number; + displayEdges: number; + scrollDivider: number; + gridWidth: number; + layerHeight: number[]; +} diff --git a/src/controller/dataStore.js b/src/controller/dataStore.js deleted file mode 100644 index 434e342..0000000 --- a/src/controller/dataStore.js +++ /dev/null @@ -1,42 +0,0 @@ -import eventBus from "./EventBus.js"; -import { DATA_EVENTS } from "./constants/events.js" - -export class DataStore { - constructor() { - this._queryInfo = null; - this._nodeState = {}; - this._queryResult = null; - - eventBus.on(DATA_EVENTS.NODE_UPDATE, this.setQueryInfo.bind(this)); - eventBus.on(DATA_EVENTS.RESULT_UPDATE, this.setQueryResult.bind(this)); - } - - setQueryInfo(queryInfo) { - this._queryInfo = queryInfo; - eventBus.emit(DATA_EVENTS.QUERY_CHANGED, queryInfo) - } - getQueryInfo() { - return this._queryInfo; - } - - setNodeState(nodeState) { - this._nodeState = nodeState; - } - getNodeState() { - return this._nodeState; - } - - setQueryResult(queryResult) { - this._queryResult = queryResult; - eventBus.emit(DATA_EVENTS.QUERY_CHANGED, queryResult); - } - getQueryResult() { - return this._queryResult; - } - - reset() { - this._queryInfo = null; - this._nodeState = {}; - this._queryResult = null; - } -} \ No newline at end of file diff --git a/src/controller/perceptron/EdgeHandler.ts b/src/controller/perceptron/EdgeHandler.ts new file mode 100644 index 0000000..b866f76 --- /dev/null +++ b/src/controller/perceptron/EdgeHandler.ts @@ -0,0 +1,47 @@ +import { RENDERER_CONFIG } from '@/controller/constants/rendererConfig.ts'; +const { displayEdges } = RENDERER_CONFIG; + +class EdgeHandler { + private EdgeRenderer: IEdgeRenderer; + private BaseCanvas: IBaseCanvas; + + constructor({ + edgeRenderer, + perceptronBaseCanvas, + }: { + edgeRenderer: IEdgeRenderer; + perceptronBaseCanvas: IBaseCanvas; + }) { + this.EdgeRenderer = edgeRenderer; + this.BaseCanvas = perceptronBaseCanvas; + } + + render(anchorPosition: any): void { + const layerKeysList = Object.keys(anchorPosition); + + const subsetByRatio = (arr: any[], ratio: number, size: number) => { + if (!Array.isArray(arr) || arr.length === 0) return []; + const maxStart = Math.max(arr.length - size, 0); + const start = Math.floor(ratio * maxStart); + return arr.slice(start, start + size); + }; + + layerKeysList.slice(0, -1).flatMap((layerKey, index) => { + const current = anchorPosition[layerKey] ?? []; + const next = anchorPosition[layerKeysList[index + 1]] ?? []; + + return current + .flatMap((a, aIndex) => { + const ratio = current.length <= 1 ? 0 : aIndex / (current.length - 1); + const subset = subsetByRatio(next, ratio, displayEdges); + + return subset.map((b) => [a, b]); + }) + .forEach(([a, b]) => { + this.EdgeRenderer.drawEdge(a.posX, a.posY, b.posX, b.posY); + }); + }); + } +} + +export default EdgeHandler; diff --git a/src/controller/perceptron/GridHandler.ts b/src/controller/perceptron/GridHandler.ts new file mode 100644 index 0000000..b8690f6 --- /dev/null +++ b/src/controller/perceptron/GridHandler.ts @@ -0,0 +1,60 @@ +import { RENDERER_CONFIG } from '@/controller/constants/rendererConfig.ts'; +const { degree } = RENDERER_CONFIG; + +class GridHandler { + private GridRenderer: IGridRenderer; + private BaseCanvas: IBaseCanvas; + + constructor({ + gridRenderer, + perceptronBaseCanvas, + }: { + gridRenderer: IGridRenderer; + perceptronBaseCanvas: IBaseCanvas; + }) { + this.GridRenderer = gridRenderer; + this.BaseCanvas = perceptronBaseCanvas; + } + + renderLayout(anchorPosition: any): void { + this.GridRenderer.drawArc(710); + + const referenceIndex = 3; // 가운데 위치를 잡기 위한 index값 + const textRenderCodition: boolean = anchorPosition.outputLayer[referenceIndex] != undefined; + if (textRenderCodition) { + // output layer가 화면에 렌더링될때만 텍스트 표기(렌더링 엔진의 특성상 현재 렌더링되고 있는 노드만 array로 들어옴.) + // ex. 현재의 경우 3번째 레이어가 화면에서 벗어나면 화면에 렌더링된 노드는 2개이므로 length가 2. 3번째 index 를 참조할 수 없음. + this.drawText( + anchorPosition.outputLayer[referenceIndex].posX + 10, + anchorPosition.outputLayer[referenceIndex].posY - 90, + anchorPosition.outputLayer[referenceIndex].angleOffset, + 'Input Nodes', + ); + + this.drawText( + anchorPosition.outputLayer[referenceIndex].posX + 10, + anchorPosition.outputLayer[referenceIndex].posY + 12, + anchorPosition.outputLayer[referenceIndex].angleOffset, + 'Output Nodes', + ); + } + } + renderGrid(gridIndex: number): void { + const gridLength: 10 | 8 = gridIndex % 2 === 0 ? 10 : 8; + this.GridRenderer.drawGrid(560, gridLength); //bottom-side gird + this.GridRenderer.drawGrid(710, -gridLength); //top-side grid + } + + drawText(x: number, y: number, angleOffset: number, text: string): void { + this.BaseCanvas.saveState(); + this.BaseCanvas.moveCanvas(x, y); + this.BaseCanvas.rotateCanvas(true, ((angleOffset * Math.PI) / 180) * 1.01); + this.BaseCanvas.rotateCanvas(true, degree * 90); + this.BaseCanvas.getCtx().fillStyle = 'rgb(133,133,133)'; + this.BaseCanvas.getCtx().fillText(`${text}`, 0, 0); + this.BaseCanvas.getCtx().fillStyle = 'rgb(255, 255, 255)'; + this.BaseCanvas.restoreState(); + } +} + +export default GridHandler; diff --git a/src/controller/perceptron/NodeHandler.ts b/src/controller/perceptron/NodeHandler.ts new file mode 100644 index 0000000..94e8988 --- /dev/null +++ b/src/controller/perceptron/NodeHandler.ts @@ -0,0 +1,85 @@ +import { networkConfig } from '@/controller/constants/types/networkConfig.ts'; +import { Node } from '@/view/components/perceptron/objectClass/Node.ts'; +import { RENDERER_CONFIG } from '@/controller/constants/rendererConfig.ts'; + +import eventBus from '@/controller/eventBus.ts'; +import { SCROLL_EVENTS } from '@/controller/constants/events.ts'; +import { nodeObjectSet } from '@/controller/perceptron/types/nodeObjectSet.ts'; +import { nodeState } from '@/controller/types/DataStore.ts'; +import { compressArr } from '@/controller/perceptron/utils/compressArr.ts'; + +export class NodeHandler { + private readonly nodeObjectSet: nodeObjectSet; + private displayNodes: number; + private NodeRenderer: INodeRenderer; + private BaseCanvas: IBaseCanvas; + + constructor({ + networkConfig, + nodeRenderer, + perceptronBaseCanvas, + }: { + networkConfig: networkConfig; + nodeRenderer: INodeRenderer; + perceptronBaseCanvas: IBaseCanvas; + }) { + this.nodeObjectSet = { + inputNodes: undefined, + hiddenNodes: undefined, + outputNodes: undefined, + }; + this.NodeRenderer = nodeRenderer; + this.BaseCanvas = perceptronBaseCanvas; + } + + initializeNode(networkInfo: any): any { + Object.keys(this.nodeObjectSet).forEach((key: string) => { + this.nodeObjectSet[key] = Array.from({ length: networkInfo[key] }, (v, i) => { + return new Node(0); + }); + }); + return this.nodeObjectSet; + } + + updateNode(changedNodeState: nodeState): nodeObjectSet { + const { inputs, ...rest } = changedNodeState; + const compressedChangedNodeState = { inputs: compressArr(inputs), ...rest }; + + const keys: string[] = Object.keys(compressedChangedNodeState); + keys.forEach((key: string) => { + compressedChangedNodeState[key].flatMap((e: number, i: number) => { + if (key === 'inputs') this.nodeObjectSet.inputNodes[i].setValue(e * 100); + if (key === 'hiddenOutputs') this.nodeObjectSet.hiddenNodes[i].setValue(e * 100); + if (key === 'finalOutputs') this.nodeObjectSet.outputNodes[i].setValue(e * 100); + }); + }); + return this.nodeObjectSet; + // TODO: ScrollEventHandler 싱글톤으로 변경하기 -> 보류(결정 문서 참고) + } + + resetNode(): nodeObjectSet { + const keys = Object.keys(this.nodeObjectSet); + + keys.forEach((key: string) => { + this.nodeObjectSet[key].forEach((_: unknown, i: number) => { + this.nodeObjectSet[key][i].setValue(0); + }); + }); + + return this.nodeObjectSet; + } + + render(anchorPosition: any): void { + Object.keys(anchorPosition).forEach((key: string) => { + const layerSize = anchorPosition[key].length; + Array.from({ length: layerSize }, (_, i) => { + this.NodeRenderer.drawNode( + anchorPosition[key][i].posX, + anchorPosition[key][i].posY, + anchorPosition[key][i].angleOffset, + anchorPosition[key][i].percent, + ); + }); + }); + } +} diff --git a/src/controller/perceptron/PerceptronController.ts b/src/controller/perceptron/PerceptronController.ts new file mode 100644 index 0000000..1b95b93 --- /dev/null +++ b/src/controller/perceptron/PerceptronController.ts @@ -0,0 +1,175 @@ +import { IPerceptronControllerProps } from '@/controller/perceptron/types/PerceptronController.ts'; +import { NodeHandler } from '@/controller/perceptron/NodeHandler.ts'; +import { EdgePositionHandler } from '@/controller/perceptron/EdgeHandler.ts'; +import { + DATA_EVENTS, + DataEvent, + DRAWING_EVENTS, + SCROLL_EVENTS, +} from '@/controller/constants/events.ts'; +import eventBus from '@/controller/EventBus.ts'; +import { eventPayloads } from '@/controller/types/eventBus.ts'; +import { compressArr } from '@/controller/perceptron/utils/compressArr.ts'; + +import { NETWORK_CONFIG } from '@/controller/constants/networkConfig'; +import { normalizeNetworkConfig } from '@/controller/perceptron/utils/normalizeNetworkInfo.ts'; + +import { RENDERER_CONFIG } from '@/controller/constants/rendererConfig.ts'; +const { rotationDelta, degree, displayNodes, scrollDivider, gridWidth, layerHeight } = + RENDERER_CONFIG; +import BaseCanvas from '@/view/components/perceptron/BaseCanvas.ts'; + +import { NodeObjectSet } from '@/controller/perceptron/types/nodeObjectSet.ts'; +import { findWidestLayer } from '@/controller/perceptron/utils/findWidestLayer.ts'; +import { IDataStore, nodeState } from '@/controller/types/DataStore.ts'; +import DataStore from '@/controller/DataStore.ts'; +class PerceptronController { + private $DS: IDataStore; + private NodeRenderer: INodeRenderer; + private NodeHandler: INodeHandler; + private EdgeHandler: IEdgeHandler; + private GridHandler: IGridHandler; + private BaseCanvas: IBaseCanvas; + private ScrollEventHandler: IScrollEventHandler; + + private normalizedNetworkInfo: any; + private nodeObjectSet: NodeObjectSet; + private anchorPosition: { inputLayer: number[]; hiddenLayer: number[]; outputLayer: number[] }; + private anchorPosKeys: string[] = ['inputLayer', 'hiddenLayer', 'outputLayer']; + private widestLayerSize?: number; + + constructor({ + NodeRenderer, + NodeHandler, + EdgeHandler, + GridHandler, + BaseCanvas, + ScrollEventHandler, + }: IPerceptronControllerProps) { + this.$DS = DataStore; + this.NodeRenderer = NodeRenderer; + this.NodeHandler = NodeHandler; + this.EdgeHandler = EdgeHandler; + this.GridHandler = GridHandler; + this.BaseCanvas = BaseCanvas; + this.ScrollEventHandler = ScrollEventHandler; + + this.anchorPosition = { inputLayer: [], hiddenLayer: [], outputLayer: [] }; + + this.initializeNodeValue(); + this.registerScrollEvent(); + this.calculateNodePosition(0); + this.calculateGridPosition(); + this.updatePerceptron(); + this.resetPerceptron(); + } + + initializeNodeValue() { + this.normalizedNetworkInfo = normalizeNetworkConfig(NETWORK_CONFIG); // 원활한 시각화를 위해 입력 레이어의 크기를 compress + this.nodeObjectSet = this.NodeHandler.initializeNode(this.normalizedNetworkInfo); // 시각화된 노드들의 기본값 할당 + const { value: widestLayer } = findWidestLayer(this.normalizedNetworkInfo); // 현재 perceptron에 가장 큰 layer 크기 가져오기 + this.widestLayerSize = widestLayer; // 그리드 렌더링에 layer 크기 활용 + } + + registerScrollEvent() { + eventBus.on(SCROLL_EVENTS.SCROLL_CHANGED, (scroll: number) => { + this.calculateNodePosition(scroll); + this.calculateGridPosition(); + }); + eventBus.on(SCROLL_EVENTS.TOUCH_CHANGED, (scroll: number) => { + this.calculateNodePosition(scroll); + this.calculateGridPosition(); + }); + } + + calculateNodePosition(scroll: number) { + this.anchorPosition = { inputLayer: [], hiddenLayer: [], outputLayer: [] }; + Object.keys(this.normalizedNetworkInfo).map((key: string, layerIndex: number) => { + this.BaseCanvas.saveState(); + + const rotateCenterPosition = + degree * 90 - (degree * rotationDelta * this.normalizedNetworkInfo[key]) / 2; + const scrollDividerInterpolation = degree * rotationDelta; + this.BaseCanvas.rotateCanvas(true, rotateCenterPosition + scrollDividerInterpolation); + + const layerSize = this.normalizedNetworkInfo[key]; + + Array.from({ length: layerSize }, (_: unknown, nodeIndex: number) => nodeIndex).map( + (nodeIndex) => { + const scrollOffset = scroll / scrollDivider; + const displayStart = + layerSize / 2 - + DataStore.getPerceptronConfig().displayNodes / 2 + + scrollOffset; + const displayEnd = + layerSize / 2 + + DataStore.getPerceptronConfig().displayNodes / 2 + + scrollOffset; + + console.log(); + + const displayCondition = nodeIndex >= displayStart && nodeIndex < displayEnd; + + this.BaseCanvas.saveState(); + + if (displayCondition) { + this.BaseCanvas.rotateCanvas(true, degree * rotationDelta * nodeIndex); + const currentMatrix = this.BaseCanvas.getCtx().getTransform(); + const globalMatrix = this.BaseCanvas.setupMatrix.multiply(currentMatrix); + + const global = globalMatrix.transformPoint( + new DOMPoint(layerHeight[layerIndex], 0), + ); + const angle = Math.atan2(globalMatrix.b, globalMatrix.a); + const angleOffset = angle * (180 / Math.PI); + + const percent = this.nodeObjectSet[key][nodeIndex].getValue(); + this.anchorPosition[this.anchorPosKeys[layerIndex]].push({ + posX: global.x, + posY: global.y, + angleOffset: angleOffset, + percent: percent, + }); + } else { + this.BaseCanvas.rotateCanvas(true, degree * rotationDelta * nodeIndex); + } + this.BaseCanvas.restoreState(); + }, + ); + this.BaseCanvas.restoreState(); + }); + this.BaseCanvas.clearCanvas(); + this.EdgeHandler.render(this.anchorPosition); + this.NodeHandler.render(this.anchorPosition); + } + + calculateGridPosition() { + const displayGird = this.widestLayerSize; + this.BaseCanvas.saveState(); + this.BaseCanvas.rotateCanvas(true, degree - (degree * gridWidth * displayGird) / 2); + Array.from({ length: displayGird }, (_: unknown, gridIndex: number) => { + this.BaseCanvas.rotateCanvas(true, degree * gridWidth); + this.GridHandler.renderGrid(gridIndex); + }); + this.BaseCanvas.restoreState(); + this.GridHandler.renderLayout(this.anchorPosition); + } + + updatePerceptron() { + eventBus.on(DATA_EVENTS.NODE_CHANGED, (changedNodeState: nodeState) => { + this.nodeObjectSet = this.NodeHandler.updateNode(changedNodeState); + this.calculateNodePosition(this.ScrollEventHandler.getScroll()); + this.calculateGridPosition(); + }); + } + + resetPerceptron() { + eventBus.on(DRAWING_EVENTS.CLEAR_DRAW, () => { + this.nodeObjectSet = this.NodeHandler.resetNode(); + this.calculateNodePosition(this.ScrollEventHandler.getScroll()); + this.calculateGridPosition(); + }); + } +} + +export default PerceptronController; diff --git a/src/controller/perceptron/ScrollEventHandler.ts b/src/controller/perceptron/ScrollEventHandler.ts new file mode 100644 index 0000000..f697cf2 --- /dev/null +++ b/src/controller/perceptron/ScrollEventHandler.ts @@ -0,0 +1,108 @@ +import { RENDERER_CONFIG } from '@/controller/constants/rendererConfig.ts'; +import { NETWORK_CONFIG } from '@/controller/constants/networkConfig.ts'; + +import eventBus from '@/controller/EventBus.ts'; +import { SCROLL_EVENTS } from '@/controller/constants/events.ts'; +import { normalizeNetworkConfig } from '@/controller/perceptron/utils/normalizeNetworkInfo.ts'; +import { findWidestLayer } from '@/controller/perceptron/utils/findWidestLayer.ts'; + +const { degree, rotationDelta, displayNodes, scrollDivider } = RENDERER_CONFIG; + +class ScrollEventHandler { + private BaseCanvas: IBaseCanvas; + private readonly canvasEl: HTMLCanvasElement; + + private scroll: number; + private touchDirectionX: number; + private touchDirectionY: number; + private rotationStack: number; + + private widestLayer: number; + private leftLimit: number; + private rightLimit: number; + private leftScrollLimit: boolean; + private rightScrollLimit: boolean; + + constructor(BaseCanvas: IBaseCanvas) { + this.BaseCanvas = BaseCanvas; + this.canvasEl = BaseCanvas.getCanvas(); // perceptron base canvas + + this.scroll = 0; + this.touchDirection = 0; + + this.widestLayer = 0; + this.leftScrollLimit = true; + this.rightScrollLimit = true; + + this.registerEvent(); + } + + registerEvent() { + this.calculateScrollLimit(); + this.handleMouseScroll(); + this.handleTouchMove(); + } + + calculateScrollLimit(): void { + const normalizedNetworkInfo = normalizeNetworkConfig(NETWORK_CONFIG); + const { value: widestLayer } = findWidestLayer(normalizedNetworkInfo); // 스크롤 최대치 계산을 위한 가장 큰 노드의 폭을 계산 + this.rightLimit = (widestLayer / 2 - displayNodes) * scrollDivider * 1.25; + this.leftLimit = -this.rightLimit; + } + + handleMouseScroll(): void { + this.canvasEl.addEventListener('wheel', (e: WheelEvent) => { + this.rightScrollLimit = this.scroll <= this.rightLimit; + this.leftScrollLimit = this.scroll >= this.leftLimit; + + if (e.deltaY > 0 && this.rightScrollLimit) { + this.BaseCanvas.rotateCanvas(false, degree * (rotationDelta / (scrollDivider * 2))); + this.scroll += 1; + this.rotationStack += 1; + eventBus.emit(SCROLL_EVENTS.SCROLL_CHANGED, this.scroll); + } else if (e.deltaY < 0 && this.leftScrollLimit) { + this.BaseCanvas.rotateCanvas(true, degree * (rotationDelta / (scrollDivider * 2))); + this.scroll -= 1; + this.rotationStack -= 1; + eventBus.emit(SCROLL_EVENTS.SCROLL_CHANGED, this.scroll); + } + }); + } + handleTouchMove(): void { + let touchStartX: number = 0; + let touchStartY: number = 0; + this.canvasEl.addEventListener('touchstart', (e: TouchEvent) => { + const touch = e.touches[0]; + touchStartX = Math.floor(touch.clientX); + touchStartY = Math.floor(touch.clientY); + }); + this.canvasEl.addEventListener('touchmove', (e: TouchEvent) => { + const touch = e.touches[0]; + const moveX = Math.floor(touch.clientX); + const moveY = Math.floor(touch.clientY); + this.touchDirectionX = moveX - touchStartX; + this.touchDirectionY = moveY - touchStartY; + + if (Math.abs(this.touchDirectionX) < Math.abs(this.touchDirectionY)) return; + + this.rightScrollLimit = this.scroll <= this.rightLimit; + this.leftScrollLimit = this.scroll >= this.leftLimit; + + if (this.touchDirectionX < 0 && this.rightScrollLimit) { + this.BaseCanvas.rotateCanvas(false, degree * (rotationDelta / (scrollDivider * 2))); + this.scroll += 1; + this.rotationStack += 1; + eventBus.emit(SCROLL_EVENTS.TOUCH_CHANGED, this.scroll); + } else if (this.touchDirectionX > 0 && this.leftScrollLimit) { + this.BaseCanvas.rotateCanvas(true, degree * (rotationDelta / (scrollDivider * 2))); + this.scroll -= 1; + this.rotationStack -= 1; + eventBus.emit(SCROLL_EVENTS.TOUCH_CHANGED, this.scroll); + } + }); + } + + getScroll = () => this.scroll; +} + +export default ScrollEventHandler; diff --git a/src/controller/perceptron/types/PerceptronController.ts b/src/controller/perceptron/types/PerceptronController.ts new file mode 100644 index 0000000..c11a555 --- /dev/null +++ b/src/controller/perceptron/types/PerceptronController.ts @@ -0,0 +1,10 @@ +export interface IPerceptronControllerProps { + NodeRenderer: INodeRenderer; + EdgeRenderer: IEdgeRenderer; + GridRenderer: IGridRenderer; + NodeHandler: INodeHandler; + EdgeHandler: INdgeHandler; + GridHandler: IGridHandler; + BaseCanvas: IBasecanvas; + ScrollEventHandler: IScrollEventHandler; +} diff --git a/src/controller/perceptron/types/nodeObjectSet.ts b/src/controller/perceptron/types/nodeObjectSet.ts new file mode 100644 index 0000000..13a09cc --- /dev/null +++ b/src/controller/perceptron/types/nodeObjectSet.ts @@ -0,0 +1,7 @@ +import { Node } from '@/view/components/perceptron/objectClass/Node.ts'; + +export interface NodeObjectSet { + inputNodes: Array; + hiddenNodes: Array; + outputNodes: Array; +} diff --git a/src/controller/perceptron/utils/compressArr.ts b/src/controller/perceptron/utils/compressArr.ts new file mode 100644 index 0000000..273493f --- /dev/null +++ b/src/controller/perceptron/utils/compressArr.ts @@ -0,0 +1,9 @@ +export const compressArr = (arr: number[]): number[] => { + const resized: number[] = arr.reduce((acc: number[], cur: number, idx: number) => { + if (idx % 8 === 0) acc.push(cur); + else acc[acc.length - 1] += cur; + return acc; + }, []); + const logScale: number[] = resized.map((v: number): number => Math.log(v + 1) / Math.log(9)); + return logScale; +}; diff --git a/src/controller/perceptron/utils/findWidestLayer.ts b/src/controller/perceptron/utils/findWidestLayer.ts new file mode 100644 index 0000000..c788802 --- /dev/null +++ b/src/controller/perceptron/utils/findWidestLayer.ts @@ -0,0 +1,7 @@ +import { networkConfig } from '@/controller/constants/types/networkConfig.ts'; + +export const findWidestLayer = (obj: networkConfig) => + Object.entries(obj).reduce((max, [key, value]) => (value > max.value ? { key, value } : max), { + key: null, + value: -Infinity, + }); diff --git a/src/controller/perceptron/utils/normalizeNetworkInfo.ts b/src/controller/perceptron/utils/normalizeNetworkInfo.ts new file mode 100644 index 0000000..38109f6 --- /dev/null +++ b/src/controller/perceptron/utils/normalizeNetworkInfo.ts @@ -0,0 +1,5 @@ +export const normalizeNetworkConfig = (networkConfig: any) => { + const { inputNodes, ...rest } = networkConfig; + const normalizedNetworkInfo = { inputNodes: inputNodes / 8, ...rest }; + return normalizedNetworkInfo; +}; diff --git a/src/controller/queryPipeline/DrawingEventHandler.js b/src/controller/queryPipeline/DrawingEventHandler.js deleted file mode 100644 index 05671c2..0000000 --- a/src/controller/queryPipeline/DrawingEventHandler.js +++ /dev/null @@ -1,64 +0,0 @@ -import BoundingBox from "../../view/components/canvasUtils/BoundingBox.js"; -import eventBus from "../EventBus.js"; -import { DRAWING_EVENTS } from "../constants/events.js"; - -class DrawingEventHandler { - constructor() { - this.inputCanvas = document.getElementById('userInputCanvas'); - this.registerEvents(); - } - - registerEvents() { - const startDraw = (x, y) => { - this.isDrawing = true; - eventBus.emit(DRAWING_EVENTS.START_DRAW, {x, y}); - } - - const draw = (x, y) => { - if (!this.isDrawing) return; - BoundingBox.update(x, y); - eventBus.emit(DRAWING_EVENTS.DRAW, {x, y}) - } - - const endDraw = () => { - this.isDrawing = false; - BoundingBox.log(); - eventBus.emit(DRAWING_EVENTS.END_DRAW); - } - - const getTouchPosition = (e) => { - e.preventDefault(); - const touch = e.touches[0]; - const rect = this.inputCanvas.getBoundingClientRect(); - return { - x: touch.clientX - rect.left, - y: touch.clientY - rect.top, - } - } - - this.inputCanvas.addEventListener("mousedown", (e) => { - startDraw(e.offsetX, e.offsetY); - }); - this.inputCanvas.addEventListener("touchstart", (e) => { - let {x, y} = getTouchPosition(e); - startDraw(x, y); - }); - - this.inputCanvas.addEventListener("mousemove", (e) => { - draw(e.offsetX, e.offsetY); - }); - this.inputCanvas.addEventListener("touchmove", (e) => { - let {x, y} = getTouchPosition(e); - draw(x, y); - }); - - this.inputCanvas.addEventListener("mouseup", endDraw); - this.inputCanvas.addEventListener("mouseout", endDraw); - this.inputCanvas.addEventListener("touchend", (e) => { - e.preventDefault(); - endDraw(); - }); - } -} - -export default DrawingEventHandler; diff --git a/src/controller/queryPipeline/DrawingEventHandler.ts b/src/controller/queryPipeline/DrawingEventHandler.ts new file mode 100644 index 0000000..f822a67 --- /dev/null +++ b/src/controller/queryPipeline/DrawingEventHandler.ts @@ -0,0 +1,92 @@ +import BoundingBox from '@/view/components/userQuery/canvasUtils/BoundingBox.ts'; +import eventBus from '../EventBus.ts'; +import { DRAWING_EVENTS } from '../constants/events.ts'; + +class DrawingEventHandler { + private inputCanvas: HTMLCanvasElement | null; + private clearButton: HTMLButtonElement | null; + private isDrawing: boolean; + + constructor() { + this.inputCanvas = document.getElementById('user-input-canvas') as HTMLCanvasElement; + this.clearButton = document.getElementById('clear') as HTMLButtonElement; + this.isDrawing = false; + this.registerEvents(); + } + + registerEvents(): void { + const bindings: [string, EventListener][] = [ + ['mousedown', this.handleStartDraw], + ['touchstart', this.handleStartDraw], + ['mousemove', this.handleDraw], + ['touchmove', this.handleDraw], + ['mouseup', this.endDraw], + ['mouseout', this.endDraw], + [ + 'touchend', + (e: TouchEvent): void => { + e.preventDefault(); + this.endDraw(); + }, + ], + ]; + + bindings.forEach(([event, handler]: [string, EventListener]): void => + this.inputCanvas.addEventListener(event, handler), + ); + + this.clearButton.addEventListener('click', () => { + eventBus.emit(DRAWING_EVENTS.CLEAR_DRAW, null); + }); + } + + // eventBus로 입력 event 전송 + startDraw = (x: number, y: number): void => { + this.isDrawing = true; + eventBus.emit(DRAWING_EVENTS.START_DRAW, { x, y }); + }; + + draw = (x: number, y: number): void => { + if (!this.isDrawing) return; + BoundingBox.update(x, y); + eventBus.emit(DRAWING_EVENTS.DRAW, { x, y }); + }; + + endDraw = (): void => { + this.isDrawing = false; + BoundingBox.log(); + eventBus.emit(DRAWING_EVENTS.END_DRAW, null); + }; + + // 캔버스에서의 터치 위치 반환 + getTouchPosition = (e: TouchEvent): { x: number; y: number } => { + e.preventDefault(); + const touch = e.touches[0]; + const rect = this.inputCanvas.getBoundingClientRect(); + return { + x: touch.clientX - rect.left, + y: touch.clientY - rect.top, + }; + }; + + // 마우스 클릭, 터치 이벤트에 대한 입력 이벤트 발생 + handleStartDraw = (e: MouseEvent | TouchEvent): void => { + if (e instanceof MouseEvent) { + this.startDraw(e.offsetX, e.offsetY); + } else if (e instanceof TouchEvent) { + let { x, y } = this.getTouchPosition(e); + this.startDraw(x, y); + } + }; + + handleDraw = (e: MouseEvent | TouchEvent): void => { + if (e instanceof MouseEvent) { + this.draw(e.offsetX, e.offsetY); + } else if (e instanceof TouchEvent) { + let { x, y } = this.getTouchPosition(e); + this.draw(x, y); + } + }; +} + +export default DrawingEventHandler; diff --git a/src/controller/queryPipeline/QueryProcessController.js b/src/controller/queryPipeline/QueryProcessController.js deleted file mode 100644 index 0bc3697..0000000 --- a/src/controller/queryPipeline/QueryProcessController.js +++ /dev/null @@ -1,48 +0,0 @@ -import eventBus from "../EventBus.js"; -import { DATA_EVENTS, HANDLER_EVENTS, DRAWING_EVENTS } from "../constants/events.js"; -import DrawingEventHandler from "./DrawingEventHandler.js"; -import {pixelExtractor} from "../queryPipeline/pixelExtractor.js"; -import {throttle} from "../queryPipeline/throttle.js"; - - -class QueryProcessController { - constructor({ userInputCanvas, trackingCanvas, alignCanvas, resizeCanvas, $NN }) { - const drawingEventHandler = new DrawingEventHandler(); - this.userInputCanvas = userInputCanvas; - this.trackingCanvas = trackingCanvas; - this.alignCanvas = alignCanvas; - this.resizeCanvas = resizeCanvas; - - this.$NN = $NN; - this.drawingEvent(); - } - - drawingEvent() { - const throttleQuery = throttle((inputs) => { - const result = this.$NN.query(inputs); - if(result) { - eventBus.emit(DATA_EVENTS.RESULT_CHANGED, result); - // Array 중 가장 값이 큰 값의 index number가 추론 결과 - } - },100); - - eventBus.on(DRAWING_EVENTS.START_DRAW, ({x, y}) => { - this.userInputCanvas.startPath(x, y); - this.trackingCanvas.startPath(x, y); - }); - eventBus.on(DRAWING_EVENTS.DRAW, ({x, y}) => { - this.userInputCanvas.drawPath(x, y); - this.trackingCanvas.drawPath(x, y); - this.alignCanvas.updateCanvasScale(); - this.alignCanvas.centralize(this.trackingCanvas.canvas); - this.resizeCanvas.downScale(this.alignCanvas.canvas); - throttleQuery(pixelExtractor(this.resizeCanvas)); - }); - eventBus.on(DRAWING_EVENTS.END_DRAW, () => { - this.userInputCanvas.endPath(); - this.trackingCanvas.endPath(); - }); - } -} - -export default QueryProcessController; \ No newline at end of file diff --git a/src/controller/queryPipeline/QueryProcessController.ts b/src/controller/queryPipeline/QueryProcessController.ts new file mode 100644 index 0000000..ae3a67a --- /dev/null +++ b/src/controller/queryPipeline/QueryProcessController.ts @@ -0,0 +1,87 @@ +import eventBus from '../EventBus.js'; +import DataStore from '../DataStore.js'; +import { DATA_EVENTS, DRAWING_EVENTS } from '../constants/events.js'; +import { pixelExtractor } from './pixelExtractor.js'; +import { throttle } from './throttle'; +import { IQueryProcessControllerProps } from './types/QueryProcessController.js'; +import { + ICanvasBase, + IUserInputCanvas, + IPathTrackingCanvas, + IAlignCanvas, + IResizeCanvas, +} from '@/view/components/userQuery/types/canvas'; +import { INeuralNetworkBase } from '@/core/types/NeuralNetworkBase'; +import { IDataStore } from '../types/DataStore'; +import { Matrix2D } from '@/core/ops/types/OpsType.ts'; +import BoundingBox from '@/view/components/userQuery/canvasUtils/BoundingBox.ts'; + +class QueryProcessController { + private readonly userInputCanvas: ICanvasBase & IUserInputCanvas; + private readonly trackingCanvas: ICanvasBase & IPathTrackingCanvas; + private readonly alignCanvas: ICanvasBase & IAlignCanvas; + private readonly resizeCanvas: ICanvasBase & IResizeCanvas; + private readonly $DS: IDataStore; + private readonly $NN: INeuralNetworkBase; + private readonly queryFrequencyMs: number; + //TODO: Canvas clear시 Skeleton 에니메이션 적용하기 + + constructor({ + userInputCanvas, + trackingCanvas, + alignCanvas, + resizeCanvas, + $NN, + }: IQueryProcessControllerProps) { + this.userInputCanvas = userInputCanvas; + this.trackingCanvas = trackingCanvas; + this.alignCanvas = alignCanvas; + this.resizeCanvas = resizeCanvas; + + this.$NN = $NN; + this.$DS = DataStore; + this.registerDrawingEvent(); + this.query(); + this.queryFrequencyMs = 200; + } + + registerDrawingEvent(): void { + eventBus.on(DRAWING_EVENTS.START_DRAW, ({ x, y }) => { + this.userInputCanvas.startPath(x, y); + this.trackingCanvas.startPath(x, y); + }); + eventBus.on(DRAWING_EVENTS.DRAW, ({ x, y }) => { + this.userInputCanvas.drawPath(x, y); + this.trackingCanvas.drawPath(x, y); + this.alignCanvas.updateCanvasScale(); + this.alignCanvas.centralize(this.trackingCanvas.canvas); + this.resizeCanvas.downScale(this.alignCanvas.canvas); + this.$DS.setQueryInfo(pixelExtractor(this.resizeCanvas)); + }); + eventBus.on(DRAWING_EVENTS.END_DRAW, () => { + this.userInputCanvas.endPath(); + this.trackingCanvas.endPath(); + }); + eventBus.on(DRAWING_EVENTS.CLEAR_DRAW, () => { + this.userInputCanvas.clear(); + this.trackingCanvas.clear(); + this.alignCanvas.clear(); + this.resizeCanvas.clear(); + BoundingBox.reset(); + }); + } + + query() { + const throttleQuery = throttle((inputs) => { + const result: Matrix2D = this.$NN.query(inputs); + if (result) { + DataStore.setQueryResult(result); + } + }, this.queryFrequencyMs); + + eventBus.on(DATA_EVENTS.QUERY_CHANGED, (inputs: number[]) => throttleQuery(inputs)); + // TODO: type interface 추가하기, 이벤트버스 구조와 쿼리 구조 다시 생각해보기, TS 마이그레이션 + } +} + +export default QueryProcessController; diff --git a/src/controller/queryPipeline/pixelExtractor.js b/src/controller/queryPipeline/pixelExtractor.js deleted file mode 100644 index d4156c7..0000000 --- a/src/controller/queryPipeline/pixelExtractor.js +++ /dev/null @@ -1,28 +0,0 @@ -export const pixelExtractor = (path) => { - const pathToMatrix = (width, height, data) => { - return Array.from({ length: height }, (_, y) => { - return Array.from({ length: width }, (_, x) => { - const i = (y * width + x) * 4; - const [r, g, b, a] = data.slice(i, i + 4); - return { r, g, b, a }; - }); - }); - } - - const grayscaleMatrix = (matrixRGB) => { - return matrixRGB.flatMap(pixelObject => - pixelObject.map(v => { - const RGB_sum = v.r + v.g + v.b; - return Math.round(RGB_sum/3); - })); - } - - const normalize = (grayscaleMatrix) => { - return grayscaleMatrix.map(v => (v/255)*0.99+0.01); - } - - const {width, height, data} = path.ctx.getImageData(0, 0, path.canvas.width, path.canvas.height); - const matrixRGB = pathToMatrix(width, height, data); - const grayScale = grayscaleMatrix(matrixRGB); - return normalize(grayScale); -}; \ No newline at end of file diff --git a/src/controller/queryPipeline/pixelExtractor.ts b/src/controller/queryPipeline/pixelExtractor.ts new file mode 100644 index 0000000..3e37ef1 --- /dev/null +++ b/src/controller/queryPipeline/pixelExtractor.ts @@ -0,0 +1,42 @@ +import { IResizeCanvas, ICanvasBase } from '@/view/components/userQuery/types/canvas'; + +export const pixelExtractor = (path: IResizeCanvas & ICanvasBase) => { + const pathToMatrix = (width: number, height: number, data: Uint8ClampedArray): object[] => { + return Array.from( + { length: height }, + (_: unknown, y: number): { r: number; g: number; b: number; a: number }[] => { + return Array.from( + { length: width }, + (_: unknown, x: number): { r: number; g: number; b: number; a: number } => { + const i: number = (y * width + x) * 4; + const [r, g, b, a] = data.slice(i, i + 4); + return { r, g, b, a }; + }, + ); + }, + ); + }; + + const grayscaleMatrix = (matrixRGB: object[]): number[] => { + return matrixRGB.flatMap((pixelObject: object[]) => + pixelObject.map((v: { r: number; g: number; b: number; a: number }): number => { + const RGB_sum: number = v.r + v.g + v.b; + return Math.round(RGB_sum / 3); + }), + ); + }; + + const normalize = (grayscaleMatrix: number[]): number[] => { + return grayscaleMatrix.map((v) => (v / 255) * 0.99 + 0.01); + }; + + const { width, height, data } = path.ctx.getImageData( + 0, + 0, + path.canvas.width, + path.canvas.height, + ); + const matrixRGB: object[] = pathToMatrix(width, height, data); + const grayScale = grayscaleMatrix(matrixRGB); + return normalize(grayScale); +}; diff --git a/src/controller/queryPipeline/throttle.js b/src/controller/queryPipeline/throttle.ts similarity index 51% rename from src/controller/queryPipeline/throttle.js rename to src/controller/queryPipeline/throttle.ts index 8d25156..bc966df 100644 --- a/src/controller/queryPipeline/throttle.js +++ b/src/controller/queryPipeline/throttle.ts @@ -1,6 +1,8 @@ -export const throttle = (fn, delay) => { +export const throttle = any> ( + fn: T, delay: number +): (...args: Parameters) => ReturnType => { let pause = false; - return (...args) => { + return (...args: Parameters): ReturnType => { if(!pause) { const result = fn(...args); pause = true; diff --git a/src/controller/queryPipeline/types/DrawingEventHandler.ts b/src/controller/queryPipeline/types/DrawingEventHandler.ts new file mode 100644 index 0000000..d4a36b5 --- /dev/null +++ b/src/controller/queryPipeline/types/DrawingEventHandler.ts @@ -0,0 +1,12 @@ +interface DrawingEventHandlerParams { + startDraw(x: number, y: number): void; + draw(x: number, y: number): void; + endDraw(): void; + getTouchPosition(e: TouchEvent): { x: number; y: number }; + + addEventListener(listener: EventListener): void; +} + +interface DrawingEventHandlerParams { + registerEvents: DrawingEventHandlerParams; +} diff --git a/src/controller/queryPipeline/types/QueryProcessController.ts b/src/controller/queryPipeline/types/QueryProcessController.ts new file mode 100644 index 0000000..585e323 --- /dev/null +++ b/src/controller/queryPipeline/types/QueryProcessController.ts @@ -0,0 +1,16 @@ +import { + ICanvasBase, + IUserInputCanvas, + IPathTrackingCanvas, + IAlignCanvas, + IResizeCanvas, +} from '@/view/components/userQuery/types/canvas'; +import { INeuralNetworkBase } from '@/core/types/NeuralNetworkBase'; + +export interface IQueryProcessControllerProps { + userInputCanvas: ICanvasBase & IUserInputCanvas; + trackingCanvas: ICanvasBase & IPathTrackingCanvas; + alignCanvas: ICanvasBase & IAlignCanvas; + resizeCanvas: ICanvasBase & IResizeCanvas; + $NN: INeuralNetworkBase; +} diff --git a/src/controller/types/DataStore.ts b/src/controller/types/DataStore.ts new file mode 100644 index 0000000..21bc2fb --- /dev/null +++ b/src/controller/types/DataStore.ts @@ -0,0 +1,19 @@ +import { Matrix2D } from '@/core/ops/types/OpsType.ts'; +import { NodeObjectSet } from '@/controller/perceptron/types/nodeObjectSet.ts'; + +export interface IDataStore { + setQueryInfo(queryInfo: number[]): void; + getQueryInfo(): number[]; + + setNodeState(nodeState: NodeObjectSet): void; + getNodeState(): NodeObjectSet; + + setQueryResult(queryResult: Matrix2D): void; + getQueryResult(): Matrix2D; +} + +export interface nodeState { + inputs: number[]; + hiddenOutputs: number[][]; + finalOutputs: number[][]; +} diff --git a/src/controller/types/eventBus.ts b/src/controller/types/eventBus.ts new file mode 100644 index 0000000..3939a2c --- /dev/null +++ b/src/controller/types/eventBus.ts @@ -0,0 +1,35 @@ +import { Matrix2D } from '@/core/ops/types/OpsType'; + +export interface eventPayloads { + // Data handling event + 'query:changed': number[]; + 'node:changed': { inputs: number[]; hiddenOutputs: Matrix2D; finalOutputs: Matrix2D }; + 'result:changed': Matrix2D; + + // Canvas handling event + 'draw:start': { x: number; y: number }; + 'draw:drawing': { x: number; y: number }; + 'draw:end': void; + 'draw:clear': void; + 'bondingbox:update': { X: number; y: number }; + + 'scroll:changed': number; + 'touch:changed': number; + + // 'node:update': { hiddenInputs: Matrix2D; hiddenOutputs: Matrix2D; finalOutputs: Matrix2D }; +} + +export interface IEventBus { + events: object; + on( + eventName: K, + subscriber: (payload: eventPayloads[K]) => void, + ): void; + + off( + eventName: K, + subscriber: (payload: eventPayloads[K]) => void, + ): void; + + emit(eventName: K, payload: eventPayloads[K]): void; +} diff --git a/src/controller/types/weights.ts b/src/controller/types/weights.ts new file mode 100644 index 0000000..a149746 --- /dev/null +++ b/src/controller/types/weights.ts @@ -0,0 +1,4 @@ +export interface weights { + W_inputToHidden: number[][]; + W_hiddenToOutput: number[][]; +} diff --git a/src/core/NeuralNetworkBase.js b/src/core/NeuralNetworkBase.js deleted file mode 100644 index adb4bfa..0000000 --- a/src/core/NeuralNetworkBase.js +++ /dev/null @@ -1,35 +0,0 @@ -import activationFunction from "./ops/activationOps.js"; -import { matrixMultiply } from "./ops/matrixOps.js"; -import eventBus from "../controller/EventBus.js"; -import { DATA_EVENTS } from "../controller/constants/events.js"; -import { NETWORK_CONFIG } from "../controller/constants/networkConfig.js"; - -class NeuralNetworkBase { - constructor(weights) { - this.inputNodes = NETWORK_CONFIG.inputNodes; - this.hiddenNodes = NETWORK_CONFIG.hiddenNodes; - this.outputNodes = NETWORK_CONFIG.outputNodes; - this.learningRate = NETWORK_CONFIG.learningRate; - - this.W_inputToHidden = weights?.W_inputToHidden ?? null; - this.W_hiddenToOutput = weights?.W_hiddenToOutput ?? null; - } - - // CNN operations - feedForward(inputs) { - console.log("inputs", inputs); - const hiddenInputs = matrixMultiply(this.W_inputToHidden, inputs.map(v => [v])); //신경망 출력 결과를 Nx1 형태의 행렬곱으로 변환. - const hiddenOutputs = activationFunction(hiddenInputs); - const finalInputs = matrixMultiply(this.W_hiddenToOutput, hiddenOutputs); - const finalOutputs = activationFunction(finalInputs); - return { hiddenInputs, hiddenOutputs, finalInputs, finalOutputs }; - } - - query(inputs){ - const { hiddenInputs, hiddenOutputs , finalOutputs } = this.feedForward(inputs); - eventBus.emit(DATA_EVENTS.NODE_UPDATE, {hiddenInputs, hiddenOutputs, finalOutputs}); - return finalOutputs; - } -} - -export default NeuralNetworkBase; \ No newline at end of file diff --git a/src/core/NeuralNetworkBase.ts b/src/core/NeuralNetworkBase.ts new file mode 100644 index 0000000..b72084c --- /dev/null +++ b/src/core/NeuralNetworkBase.ts @@ -0,0 +1,46 @@ +import activationFunction from './ops/activationOps.js'; +import { matrixMultiply } from './ops/matrixOps.js'; +import eventBus from '@/controller/EventBus.js'; +import { DATA_EVENTS } from '@/controller/constants/events.js'; + +import { weights } from '@/controller/types/weights'; +import { Matrix2D } from './ops/types/OpsType'; +import DataStore from '@/controller/DataStore.ts'; + +class NeuralNetworkBase { + private readonly W_inputToHidden: number[][] | null; + private readonly W_hiddenToOutput: number[][] | null; + + constructor(weights: Partial) { + // weight를 optional하게 처리하기 위해 Partial 사용 + this.W_inputToHidden = weights?.W_inputToHidden ?? null; + this.W_hiddenToOutput = weights?.W_hiddenToOutput ?? null; + } + + // CNN operations + feedForward(inputs: number[]): { + hiddenOutputs: Matrix2D; + finalOutputs: Matrix2D; + } { + const hiddenInputs: number[][] = matrixMultiply( + this.W_inputToHidden, + inputs.map((v) => [v]), + ); //신경망 출력 결과를 Nx1 형태의 행렬곱으로 변환. + const hiddenOutputs: Matrix2D = activationFunction(hiddenInputs); + const finalInputs: Matrix2D = matrixMultiply(this.W_hiddenToOutput, hiddenOutputs); + const finalOutputs: Matrix2D = activationFunction(finalInputs); + return { hiddenOutputs, finalOutputs }; + } + + query(inputs: number[]): Matrix2D { + const { hiddenOutputs, finalOutputs } = this.feedForward(inputs); + DataStore.setNodeState({ + inputs: inputs, + hiddenOutputs: hiddenOutputs, + finalOutputs: finalOutputs, + }); + return finalOutputs; + } +} + +export default NeuralNetworkBase; diff --git a/src/core/WeightManager.js b/src/core/WeightManager.js deleted file mode 100644 index 40b49ef..0000000 --- a/src/core/WeightManager.js +++ /dev/null @@ -1,33 +0,0 @@ -import { createRandomWeight } from "./utils/createRandomWeights.js"; -import { loadPretrainedWeights } from "./utils/loadPretrainedWeights.js"; - -class WeightManager { - _cache = null; - - constructor(networkConfig) { - this.config = networkConfig; - this.path = "assets/preTrainedWeights_h200_lr15p.json"; - } - - async getWeights() { - if(this._cache) return this._cache - try{ - const json = await loadPretrainedWeights(this.path); - this._cache = { - W_inputToHidden: json.W_inputToHidden, - W_hiddenToOutput: json.W_hiddenToOutput, - }; - }catch(err){ - console.warn(`[WeightManager] Using random weights due to error: ${err.message}`); - this._cache = { - W_inputToHidden: createRandomWeight(this.config.hiddenNodes, this.config.inputNodes), - W_hiddenToOutput: createRandomWeight(this.config.outputNodes, this.config.hiddenNodes), - }; - } - console.log("Network weights fetched"); - return this._cache; - } - -} - -export default WeightManager; \ No newline at end of file diff --git a/src/core/WeightManager.ts b/src/core/WeightManager.ts new file mode 100644 index 0000000..29aa62d --- /dev/null +++ b/src/core/WeightManager.ts @@ -0,0 +1,43 @@ +import { createRandomWeight } from './utils/createRandomWeights.js'; +import { loadPretrainedWeights } from './utils/loadPretrainedWeights.js'; +import { networkConfig } from '@/controller/constants/types/networkConfig'; + +import { weights } from '@/controller/types/weights'; + +class WeightManager { + private _cache: null | weights = null; + private config: networkConfig; + private readonly path: string; + + constructor(networkConfig: networkConfig) { + this.config = networkConfig; + this.path = 'assets/preTrainedWeights_h200_lr15p.json'; + } + + async getWeights(): Promise { + if (this._cache) return this._cache; + try { + const json = await loadPretrainedWeights(this.path); + this._cache = { + W_inputToHidden: json.W_inputToHidden, + W_hiddenToOutput: json.W_hiddenToOutput, + }; + } catch (err) { + console.warn(`[WeightManager] Using random weights due to error: ${err.message}`); + this._cache = { + W_inputToHidden: createRandomWeight( + this.config.hiddenNodes, + this.config.inputNodes, + ), + W_hiddenToOutput: createRandomWeight( + this.config.outputNodes, + this.config.hiddenNodes, + ), + }; + } + console.log('Network weights fetched'); + return this._cache; + } +} + +export default WeightManager; diff --git a/src/core/ops/activationOps.js b/src/core/ops/activationOps.js deleted file mode 100644 index 9dfb928..0000000 --- a/src/core/ops/activationOps.js +++ /dev/null @@ -1,17 +0,0 @@ -const activationFunction = matrix => - matrix.map(array => - array.map(v => { - const value = sigmoid(v) - return parseFloat(value.toFixed(5)) //활성화 함수를 적용시킨 값을 소숫점 5자리로 반올림 - }) - ) - -const sigmoid = ($x) => { - return 1 / (1 + Math.exp(-$x)); -}; - -const ReLU = ($x) => { - return Math.max(0, $x); -} - -export default activationFunction; \ No newline at end of file diff --git a/src/core/ops/activationOps.ts b/src/core/ops/activationOps.ts new file mode 100644 index 0000000..3d92e4a --- /dev/null +++ b/src/core/ops/activationOps.ts @@ -0,0 +1,19 @@ +import { Matrix2D } from './types/OpsType.ts'; + +const activationFunction = (matrix: Matrix2D): Matrix2D => + matrix.map((array: number[]): number[] => + array.map((v: number): number => { + const value: number = sigmoid(v); + return parseFloat(value.toFixed(5)); //활성화 함수를 적용시킨 값을 소숫점 5자리로 반올림 + }), + ); + +const sigmoid = ($x: number): number => { + return 1 / (1 + Math.exp(-$x)); +}; + +const ReLU = ($x: number): number => { + return Math.max(0, $x); +}; + +export default activationFunction; diff --git a/src/core/ops/matrixOps.js b/src/core/ops/matrixOps.js deleted file mode 100644 index 41247f3..0000000 --- a/src/core/ops/matrixOps.js +++ /dev/null @@ -1,17 +0,0 @@ -export const matrixMultiply = (A, B) => { - A = Array.isArray(A[0]) ? A : [A]; - - if (!Array.isArray(A) || !Array.isArray(B)) throw new Error('matrixA, B must be an array'); - else if (A[0].length !== B.length) throw new Error('rows and columns length doesn\'t match'); - - return A.map((row, i) => - B[0].map((_, j) => - row.reduce((sum, _, k) => { - return sum + A[i][k]*B[k][j]; - }, 0) - ) - ) -} - -export const transposeMatrix = matrix => - matrix[0].map((_, idx)=> matrix.map(row => row[idx])) \ No newline at end of file diff --git a/src/core/ops/matrixOps.ts b/src/core/ops/matrixOps.ts new file mode 100644 index 0000000..f6eda6e --- /dev/null +++ b/src/core/ops/matrixOps.ts @@ -0,0 +1,25 @@ +import { Matrix2D } from './types/OpsType'; + +const ensure2DArray = (matrix: number[] | number[][]): number[][] => { + return Array.isArray(matrix[0]) ? (matrix as number[][]) : [matrix as unknown as number[]]; +}; + +export const matrixMultiply = (A: Matrix2D, B: Matrix2D): Matrix2D => { + A = ensure2DArray(A); + + if (!Array.isArray(A) || !Array.isArray(B)) throw new Error('matrixA, B must be an array'); + else if (A[0].length !== B.length) throw new Error("rows and columns length doesn't match"); + + return A.map((row: number[], i: number): number[] => + B[0].map((_: unknown, j: number): number => + row.reduce((sum: number, _: unknown, k: number): number => { + return sum + A[i][k] * B[k][j]; + }, 0), + ), + ); +}; + +export const transposeMatrix = (matrix: Matrix2D): Matrix2D => + matrix[0].map((_: unknown, idx: number): number[] => + matrix.map((row: number[]): number => row[idx]), + ); diff --git a/src/core/ops/types/OpsType.ts b/src/core/ops/types/OpsType.ts new file mode 100644 index 0000000..5f6ec6d --- /dev/null +++ b/src/core/ops/types/OpsType.ts @@ -0,0 +1 @@ +export type Matrix2D = number[][]; diff --git a/src/core/types/NeuralNetworkBase.ts b/src/core/types/NeuralNetworkBase.ts new file mode 100644 index 0000000..399a911 --- /dev/null +++ b/src/core/types/NeuralNetworkBase.ts @@ -0,0 +1,9 @@ +import { Matrix2D } from '@/core/ops/types/OpsType.ts'; + +export interface INeuralNetworkBase { + feedForward(inputs: number[]): { + hiddenOutputs: Matrix2D; + finalOutputs: Matrix2D; + }; + query(inputs: number[]): Matrix2D; +} diff --git a/src/core/types/WeightManager.ts b/src/core/types/WeightManager.ts new file mode 100644 index 0000000..5fd417f --- /dev/null +++ b/src/core/types/WeightManager.ts @@ -0,0 +1,9 @@ +import { weights } from '@/controller/types/weights.ts'; +import { networkConfig } from '@/controller/constants/types/networkConfig.ts'; + +export interface IWeightManager { + _cache: null | weights; + config: networkConfig; + path: string; + getWeights(): Promise; +} diff --git a/src/core/utils/createRandomWeights.js b/src/core/utils/createRandomWeights.js deleted file mode 100644 index 8ff2a1f..0000000 --- a/src/core/utils/createRandomWeights.js +++ /dev/null @@ -1,7 +0,0 @@ -export const createRandomWeight = (col, row) => - Array.from({ length: col }, () => - Array.from({ length: row }, () => { - const value = (Math.random() - .5) * 1.99999; - return value; - }) - ); \ No newline at end of file diff --git a/src/core/utils/createRandomWeights.ts b/src/core/utils/createRandomWeights.ts new file mode 100644 index 0000000..b5d3086 --- /dev/null +++ b/src/core/utils/createRandomWeights.ts @@ -0,0 +1,7 @@ +export const createRandomWeight = (col: number, row: number): number[][] => + Array.from({ length: col }, (): number[] => + Array.from({ length: row }, (): number => { + const value: number = (Math.random() - 0.5) * 1.99999; + return value; + }), + ); diff --git a/src/core/utils/loadPretrainedWeights.js b/src/core/utils/loadPretrainedWeights.js deleted file mode 100644 index 9eab7ae..0000000 --- a/src/core/utils/loadPretrainedWeights.js +++ /dev/null @@ -1,5 +0,0 @@ -export async function loadPretrainedWeights(path) { - const response = await fetch(path); - if(!response.ok) throw new Error(`Failed to load weight datas from ${path}`); - return await response.json(); -} \ No newline at end of file diff --git a/src/core/utils/loadPretrainedWeights.ts b/src/core/utils/loadPretrainedWeights.ts new file mode 100644 index 0000000..3e89e03 --- /dev/null +++ b/src/core/utils/loadPretrainedWeights.ts @@ -0,0 +1,7 @@ +import { weights } from '@/controller/types/weights'; + +export async function loadPretrainedWeights(path: string): Promise { + const response: Response = await fetch(path); + if (!response.ok) throw new Error(`Failed to load weight data from ${path}`); + return await response.json(); +} diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 807ff3c..0000000 --- a/src/index.js +++ /dev/null @@ -1,6 +0,0 @@ -import AppController from './controller/AppContoller.js'; -import { DataStore } from "./controller/DataStore.js"; - -const App = new AppController({DataStore}); -await App.initialize(); -console.log("App initialized!"); \ No newline at end of file diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..70a5ce8 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,5 @@ +import AppController from './controller/AppController.js'; + +const App = new AppController(); +await App.initialize(); +console.log('App initialized!'); diff --git a/src/view/ViewHandler.js b/src/view/ViewHandler.js deleted file mode 100644 index e2ded8b..0000000 --- a/src/view/ViewHandler.js +++ /dev/null @@ -1,7 +0,0 @@ -class ViewHandler { - constructor() { - - } -} - -export default ViewHandler; \ No newline at end of file diff --git a/src/view/ViewPresenter.ts b/src/view/ViewPresenter.ts new file mode 100644 index 0000000..2a703ef --- /dev/null +++ b/src/view/ViewPresenter.ts @@ -0,0 +1,38 @@ +import eventBus from '@/controller/EventBus.ts'; +import { DATA_EVENTS, DRAWING_EVENTS } from '@/controller/constants/events.ts'; + +import { Matrix2D } from '@/core/ops/types/OpsType.ts'; + +class ViewPresenter { + private readonly El: HTMLElement | null; + + constructor() { + this.El = document.getElementById('result') as HTMLDivElement; + this.normalizeResult(); + this.renderResult(undefined); + } + + normalizeResult() { + eventBus.on(DATA_EVENTS.RESULT_CHANGED, (data: Matrix2D) => { + const queryResults = data.flatMap((v: number[], index) => v[0] * 100); + const result = queryResults.reduce( + (acc: number, cur: number, index: number, arr: number[]) => { + return arr[acc] < cur ? index : acc; + }, + 0, + ); + // const result: number = Math.max(...queryResults); + this.renderResult(result); + }); + eventBus.on(DRAWING_EVENTS.CLEAR_DRAW, () => { + this.renderResult(undefined); + }); + } + + renderResult(result: number | undefined): void { + const networkAnswer = + (this.El.innerHTML = `
${result == undefined ? '?' : result}
`); + } +} + +export default ViewPresenter; diff --git a/src/view/components/CanvasComponentBase.js b/src/view/components/CanvasComponentBase.js index bf544ff..2af8a95 100644 --- a/src/view/components/CanvasComponentBase.js +++ b/src/view/components/CanvasComponentBase.js @@ -30,7 +30,6 @@ class CanvasComponentBase { clear() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); } - } -export default CanvasComponentBase; \ No newline at end of file +export default CanvasComponentBase; diff --git a/src/view/components/canvasUtils/BoundingBox.js b/src/view/components/canvasUtils/BoundingBox.js deleted file mode 100644 index 7fe4911..0000000 --- a/src/view/components/canvasUtils/BoundingBox.js +++ /dev/null @@ -1,38 +0,0 @@ -class BoundingBox { - constructor() { - this.initialCoords = { - minX: Infinity, - minY: Infinity, - maxX: -Infinity, - maxY: -Infinity, - } - this.coordinate = {...this.initialCoords}; - this.originalObject = {}; - } - - update(currentX, currentY) { - this.coordinate = { - minX: Math.round(Math.min(this.coordinate.minX, currentX)), - minY: Math.round(Math.min(this.coordinate.minY, currentY)), - maxX: Math.round(Math.max(this.coordinate.maxX, currentX)), - maxY: Math.round(Math.max(this.coordinate.maxY, currentY)), - } - Object.assign(this.originalObject, - { - x: this.coordinate.minX-20, - y: this.coordinate.minY-20, - width: this.coordinate.maxX+40 - this.coordinate.minX, - height: this.coordinate.maxY+40 - this.coordinate.minY,}); - } - - log() { - console.log(this.coordinate); - console.log(this.originalObject); - } - - reset() { - this.coordinate = {...this.initialCoords}; - } -} - -export default new BoundingBox; \ No newline at end of file diff --git a/src/view/components/perceptron/BaseCanvas.ts b/src/view/components/perceptron/BaseCanvas.ts new file mode 100644 index 0000000..bd67631 --- /dev/null +++ b/src/view/components/perceptron/BaseCanvas.ts @@ -0,0 +1,66 @@ +import eventBus from '@/controller/EventBus.ts'; +import { RENDERER_CONFIG } from '@/controller/constants/rendererConfig.ts'; +import { nodePath } from '@/view/components/perceptron/shapeVector/nodePath.ts'; + +class BaseCanvas { + private readonly canvas: HTMLCanvasElement | null; + private readonly ctx: CanvasRenderingContext2D; + private canvasCenter: { x: number; y: number }; + + private setupMatrix: any; + + constructor() { + this.canvas = document.getElementById('perceptron') as HTMLCanvasElement | null; + this.ctx = this.canvas.getContext('2d', { willReadFrequently: true }); + + this.setupCanvas(); // canvas element setup + this.setupRenderTransform(); + // this.setupRenderTransform(this.ctx, RENDERER_CONFIG.degree); // apply state for rendering context(transform/rotate..) + } + + getCanvas = (): HTMLCanvasElement => this.canvas; + getCtx = (): CanvasRenderingContext2D => this.ctx; + + setupCanvas() { + this.canvas.width = 2000; + this.canvas.height = 720; + + this.ctx.fillStyle = 'rgb(255,255,255)'; + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + this.canvasCenter = { x: this.canvas.width / 2, y: this.canvas.height }; + } + + setupRenderTransform(): void { + this.ctx.setTransform(1, 0, 0, 1, 0, 0); + this.ctx.translate(this.canvasCenter.x, this.canvasCenter.y); + this.rotateCanvas(true, RENDERER_CONFIG.degree * 180); + this.setupMatrix = this.ctx.getTransform(); + console.log('셋업 렌더 셋팅을 실행함.'); + } + + rotateCanvas = (clockWise: boolean, value: number) => { + const rotationVector = clockWise ? 1 : -1; + this.ctx.rotate(rotationVector * value); + }; + moveCanvas = (x: number, y: number) => { + this.ctx.translate(x, y); + }; + + clearCanvas() { + this.saveState(); + this.ctx.translate(-this.canvasCenter.x, -this.canvasCenter.y); + this.ctx.fillStyle = 'rgb(255,255,255)'; + this.ctx.fillRect(0, 0, this.canvasCenter.x * 2, this.canvasCenter.y * 2); + this.restoreState(); + } + + saveState() { + this.ctx.save(); + } + + restoreState() { + this.ctx.restore(); + } +} + +export default BaseCanvas; diff --git a/src/view/components/perceptron/EdgeRenderer.ts b/src/view/components/perceptron/EdgeRenderer.ts new file mode 100644 index 0000000..52e3052 --- /dev/null +++ b/src/view/components/perceptron/EdgeRenderer.ts @@ -0,0 +1,25 @@ +class EdgeRenderer { + private renderCtx: CanvasRenderingContext2D; + + constructor(ctx: CanvasRenderingContext2D) { + this.renderCtx = ctx; + } + + drawEdge = (aX: number, aY: number, bX: number, bY: number): void => { + const dX: number = Math.abs(aX - bX); + const dY: number = Math.abs(aY - bY); + const distance = Math.sqrt(dX * dX + dX * dY); + if (distance > 490) return; + + this.renderCtx.strokeStyle = '#000000'; + this.renderCtx.lineWidth = 0.3; + + this.renderCtx.beginPath(); + this.renderCtx.moveTo(aX, aY); + this.renderCtx.lineTo(bX, bY); + this.renderCtx.stroke(); + this.renderCtx.moveTo(aX, aY); + }; +} + +export default EdgeRenderer; diff --git a/src/view/components/perceptron/GridRenderer.ts b/src/view/components/perceptron/GridRenderer.ts new file mode 100644 index 0000000..cc43971 --- /dev/null +++ b/src/view/components/perceptron/GridRenderer.ts @@ -0,0 +1,28 @@ +class GridRenderer { + private renderCtx: CanvasRenderingContext2D; + + constructor(ctx: CanvasRenderingContext2D) { + this.renderCtx = ctx; + } + + drawGrid(position: number, gridLength: number): void { + this.renderCtx.strokeStyle = '#c3c3c3'; + this.renderCtx.lineWidth = 1; + + this.renderCtx.save(); + + this.renderCtx.beginPath(); + this.renderCtx.moveTo(0, position); + this.renderCtx.lineTo(0, position + gridLength); + this.renderCtx.stroke(); + this.renderCtx.restore(); + } + + drawArc(radius: number): void { + this.renderCtx.beginPath(); + this.renderCtx.arc(0, 0, radius, 0, Math.PI * 2); + this.renderCtx.stroke(); + } +} + +export default GridRenderer; diff --git a/src/view/components/perceptron/NodeRenderer.ts b/src/view/components/perceptron/NodeRenderer.ts new file mode 100644 index 0000000..3c9ea3e --- /dev/null +++ b/src/view/components/perceptron/NodeRenderer.ts @@ -0,0 +1,41 @@ +import { rendererConfig } from '@/controller/constants/types/rendererConfig.ts'; +import { nodePath } from '@/view/components/perceptron/shapeVector/nodePath.ts'; + +class NodeRenderer { + private canvasCenter!: { x: number; y: number }; + private rotationDelta!: number; + private degree!: number; + private displayNodes!: number; + private scrollSpeed!: number; + + private mouseScroll!: number; + private touchMove!: number; + private touchDirection!: number; + private nodes!: number; + + private renderCtx: CanvasRenderingContext2D; + + constructor(ctx: CanvasRenderingContext2D) { + this.renderCtx = ctx; + } + + initializeRenderState(RENDERER_CONFIG: rendererConfig): void { + this.rotationDelta = RENDERER_CONFIG.rotationDelta; // scrollHandler + this.degree = RENDERER_CONFIG.degree; // scrollHandler + this.displayNodes = RENDERER_CONFIG.displayNodes; // nodeHandler + this.scrollSpeed = RENDERER_CONFIG.scrollDivider; // scrollHandler + } + + drawNode = (x: number, y: number, angle: number, p: number): void => { + nodePath(this.renderCtx, { + x: x, + y: y, + width: 13, + height: 13, + angleOffset: angle, + percent: p, + }); + }; +} + +export default NodeRenderer; diff --git a/src/view/components/perceptron/objectClass/Edge.ts b/src/view/components/perceptron/objectClass/Edge.ts new file mode 100644 index 0000000..b2ffa42 --- /dev/null +++ b/src/view/components/perceptron/objectClass/Edge.ts @@ -0,0 +1 @@ +class Edge {} diff --git a/src/view/components/perceptron/objectClass/Node.ts b/src/view/components/perceptron/objectClass/Node.ts new file mode 100644 index 0000000..254101c --- /dev/null +++ b/src/view/components/perceptron/objectClass/Node.ts @@ -0,0 +1,14 @@ +import { value } from '@/view/components/perceptron/types/Node.ts'; + +export class Node { + private value: Number; + constructor(value: number) { + this.value = value; + } + setValue(value: number): void { + this.value = value; + } + getValue(): number { + return this.value; + } +} diff --git a/src/view/components/perceptron/shapeVector/nodePath.ts b/src/view/components/perceptron/shapeVector/nodePath.ts new file mode 100644 index 0000000..871e606 --- /dev/null +++ b/src/view/components/perceptron/shapeVector/nodePath.ts @@ -0,0 +1,54 @@ +import { INodePath } from '@/view/components/perceptron/shape/types/nodePath.ts'; + +export const nodePath = ( + ctx: CanvasRenderingContext2D, + { x, y, width, height, radius = 5, angleOffset, percent }: INodePath, +) => { + const node: Path2D = roundedRect(-width / 2, -height / 2, width, height, radius); + + ctx.save(); + ctx.translate(x, y); + ctx.rotate((angleOffset * Math.PI) / 180); // atan값이 반환하는 라디안 값을 degree로 변환 + + ctx.fillStyle = '#fff'; + ctx.strokeStyle = '#000'; + ctx.lineWidth = 1; + ctx.fill(node); + ctx.stroke(node); + + if (percent > 0) { + const filledHeight = (height * percent) / 100; + + const fillPath = roundedRect(-width / 2, -height / 2, filledHeight, height, radius); + ctx.save(); + ctx.clip(node); + ctx.fillStyle = '#000'; + ctx.fill(fillPath); + ctx.restore(); + } + + ctx.restore(); +}; + +const roundedRect = ( + x: number, + y: number, + width: number, + height: number, + radius: number, +): Path2D => { + const path = new Path2D(); + const r = Math.min(radius, width / 2, height / 2); + + path.moveTo(x + r, y); + path.lineTo(x + width - r, y); + path.quadraticCurveTo(x + width, y, x + width, y + r); + path.lineTo(x + width, y + height - r); + path.quadraticCurveTo(x + width, y + height, x + width - r, y + height); + path.lineTo(x + r, y + height); + path.quadraticCurveTo(x, y + height, x, y + height - r); + path.lineTo(x, y + r); + path.quadraticCurveTo(x, y, x + r, y); + + return path; +}; diff --git a/src/view/components/perceptron/shapeVector/types/nodePath.ts b/src/view/components/perceptron/shapeVector/types/nodePath.ts new file mode 100644 index 0000000..6e8616b --- /dev/null +++ b/src/view/components/perceptron/shapeVector/types/nodePath.ts @@ -0,0 +1,8 @@ +export interface INodePath { + x: number; + y: number; + width: number; + height: number; + radius?: number; + percent: number; +} diff --git a/src/view/components/perceptron/types/Edge.ts b/src/view/components/perceptron/types/Edge.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/view/components/perceptron/types/Node.ts b/src/view/components/perceptron/types/Node.ts new file mode 100644 index 0000000..3fd8de0 --- /dev/null +++ b/src/view/components/perceptron/types/Node.ts @@ -0,0 +1 @@ +export type value = number; diff --git a/src/view/components/userQuery/AlignCanvas.js b/src/view/components/userQuery/AlignCanvas.js deleted file mode 100644 index c5ae35e..0000000 --- a/src/view/components/userQuery/AlignCanvas.js +++ /dev/null @@ -1,43 +0,0 @@ -import BoundingBox from "../canvasUtils/BoundingBox.js"; - -class AlignCanvas { - constructor() { - // this.canvas = document.createElement('alignCanvas'); - this.canvas = document.getElementById('alignCanvas'); - this.ctx = this.canvas.getContext('2d', { willReadFrequently: true }); - this.canvas.style.border = '2px solid red'; - - const { minX, minY, maxX, maxY } = BoundingBox.coordinate; - this.setupCanvas(); - } - setupCanvas() { - this.ctx.fillStyle = 'rgba(0, 0, 0)'; - this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); - this.updateCanvasScale(); - } - updateCanvasScale() { - if(BoundingBox.originalObject.width > BoundingBox.originalObject.height){ - this.canvas.width = BoundingBox.originalObject.width*1.3; - this.canvas.height = BoundingBox.originalObject.width*1.3; - } else { - this.canvas.width = BoundingBox.originalObject.height*1.3; - this.canvas.height = BoundingBox.originalObject.height*1.3; - } - } - - centralize(path) { - this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); - const alignStartPosition = { - x: (this.canvas.width - BoundingBox.originalObject.width) / 2, - y: (this.canvas.height - BoundingBox.originalObject.height) / 2, - } - this.ctx.drawImage(path, - BoundingBox.originalObject.x, BoundingBox.originalObject.y, - BoundingBox.originalObject.width, BoundingBox.originalObject.height, - alignStartPosition.x, alignStartPosition.y, - BoundingBox.originalObject.width, BoundingBox.originalObject.height - ); - } -} - -export default AlignCanvas; \ No newline at end of file diff --git a/src/view/components/userQuery/AlignCanvas.ts b/src/view/components/userQuery/AlignCanvas.ts new file mode 100644 index 0000000..25cbef0 --- /dev/null +++ b/src/view/components/userQuery/AlignCanvas.ts @@ -0,0 +1,53 @@ +import BoundingBox from '@/view/components/userQuery/canvasUtils/BoundingBox.ts'; + +class AlignCanvas { + public readonly canvas: HTMLCanvasElement | null; + public readonly ctx: CanvasRenderingContext2D; + + constructor() { + this.canvas = document.createElement('canvas') as HTMLCanvasElement; + this.ctx = this.canvas.getContext('2d', { willReadFrequently: true }); + this.canvas.style.border = '2px solid red'; + this.setupCanvas(); + } + setupCanvas(): void { + this.ctx.fillStyle = 'rgba(0, 0, 0)'; + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + this.updateCanvasScale(); + } + updateCanvasScale(): void { + if (BoundingBox.getOriginalObject().width > BoundingBox.getOriginalObject().height) { + this.canvas.width = BoundingBox.getOriginalObject().width * 1.3; + this.canvas.height = BoundingBox.getOriginalObject().width * 1.3; + } else { + this.canvas.width = BoundingBox.getOriginalObject().height * 1.3; + this.canvas.height = BoundingBox.getOriginalObject().height * 1.3; + } + } + + centralize(path: HTMLCanvasElement): void { + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + const alignStartPosition = { + x: (this.canvas.width - BoundingBox.getOriginalObject().width) / 2, + y: (this.canvas.height - BoundingBox.getOriginalObject().height) / 2, + }; + this.ctx.drawImage( + path, + BoundingBox.getOriginalObject().x, + BoundingBox.getOriginalObject().y, + BoundingBox.getOriginalObject().width, + BoundingBox.getOriginalObject().height, + alignStartPosition.x, + alignStartPosition.y, + BoundingBox.getOriginalObject().width, + BoundingBox.getOriginalObject().height, + ); + } + + clear(): void { + this.canvas.width = this.canvas.height = 1; + this.setupCanvas(); + } +} + +export default AlignCanvas; diff --git a/src/view/components/userQuery/PathTrackingCanvas.js b/src/view/components/userQuery/PathTrackingCanvas.js deleted file mode 100644 index 2f0248e..0000000 --- a/src/view/components/userQuery/PathTrackingCanvas.js +++ /dev/null @@ -1,40 +0,0 @@ -import { CANVAS_CONFIG } from "../../../controller/constants/canvasConfig.js"; - -class PathTrackingCanvas { - constructor() { - // this.strokeCanvas = document.createElement('pathTrackingCanvas'); - this.canvas = document.getElementById('pathTrackingCanvas'); - this.ctx = this.canvas.getContext('2d', { willReadFrequently: true }); - this.setupCanvas() - } - - setupCanvas() { - this.canvas.width = window.innerWidth; - this.canvas.height = window.innerHeight / 3; - - this.ctx.fillStyle = 'rgba(0, 0, 0)'; - this.ctx.strokeStyle = 'rgba(255, 255, 255)'; - - this.ctx.lineWidth = CANVAS_CONFIG.lineWidth; - this.ctx.lineCap = CANVAS_CONFIG.lineCap; - this.ctx.lineJoin = CANVAS_CONFIG.lineJoin; - - this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); - } - - startPath(x, y) { - this.ctx.beginPath(); - this.ctx.moveTo(x, y); - } - - drawPath(x, y) { - this.ctx.lineTo(x, y); - this.ctx.stroke(); - } - - endPath() { - this.ctx.closePath(); - } -} - -export default PathTrackingCanvas; \ No newline at end of file diff --git a/src/view/components/userQuery/PathTrackingCanvas.ts b/src/view/components/userQuery/PathTrackingCanvas.ts new file mode 100644 index 0000000..5645de8 --- /dev/null +++ b/src/view/components/userQuery/PathTrackingCanvas.ts @@ -0,0 +1,48 @@ +import { CANVAS_CONFIG } from '@/controller/constants/canvasConfig.ts'; + +class PathTrackingCanvas { + public readonly canvas: HTMLCanvasElement | null; + public readonly ctx: CanvasRenderingContext2D; + + constructor() { + this.canvas = document.createElement('canvas') as HTMLCanvasElement; + this.ctx = this.canvas.getContext('2d', { willReadFrequently: true }); + this.setupCanvas(); + } + + setupCanvas(): void { + // this.canvas.width = window.innerWidth; + // this.canvas.height = window.innerHeight * (7 / 20); + this.canvas.width = 1120; + this.canvas.height = 560; + + this.ctx.fillStyle = 'rgba(0, 0, 0)'; + this.ctx.strokeStyle = 'rgba(255, 255, 255)'; + + this.ctx.lineWidth = CANVAS_CONFIG.lineWidth; + this.ctx.lineCap = CANVAS_CONFIG.lineCap; + this.ctx.lineJoin = CANVAS_CONFIG.lineJoin; + + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + } + + startPath(x: number, y: number): void { + this.ctx.beginPath(); + this.ctx.moveTo(x, y); + } + + drawPath(x: number, y: number): void { + this.ctx.lineTo(x, y); + this.ctx.stroke(); + } + + endPath(): void { + this.ctx.closePath(); + } + + clear(): void { + this.setupCanvas(); + } +} + +export default PathTrackingCanvas; diff --git a/src/view/components/userQuery/ResizeCanvas.js b/src/view/components/userQuery/ResizeCanvas.js deleted file mode 100644 index c424a57..0000000 --- a/src/view/components/userQuery/ResizeCanvas.js +++ /dev/null @@ -1,15 +0,0 @@ -class ResizeCanvas { - constructor() { - this.canvas = document.getElementById("resizeCanvas"); - this.ctx = this.canvas.getContext("2d", { willReadFrequently: true }); - this.canvas.width = this.canvas.height = 28; - } - - downScale(path) { - this.ctx.fillStyle = 'rgba(255,255,255)'; - this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); - this.ctx.drawImage(path, 0, 0, path.width, path.height, 0, 0, this.canvas.width, this.canvas.height); - } -} - -export default ResizeCanvas; \ No newline at end of file diff --git a/src/view/components/userQuery/ResizeCanvas.ts b/src/view/components/userQuery/ResizeCanvas.ts new file mode 100644 index 0000000..96a4275 --- /dev/null +++ b/src/view/components/userQuery/ResizeCanvas.ts @@ -0,0 +1,37 @@ +class ResizeCanvas { + public readonly canvas: HTMLCanvasElement | null; + public readonly ctx: CanvasRenderingContext2D; + + constructor() { + this.canvas = document.createElement('canvas') as HTMLCanvasElement; + this.ctx = this.canvas.getContext('2d', { willReadFrequently: true }); + this.setupCanvas(); + } + + setupCanvas(): void { + this.canvas.width = this.canvas.height = 28; + this.ctx.fillStyle = 'rgba(255,255,255)'; + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + } + + downScale(path: HTMLCanvasElement): void { + this.ctx.drawImage( + path, + 0, + 0, + path.width, + path.height, + 0, + 0, + this.canvas.width, + this.canvas.height, + ); + } + + clear(): void { + this.setupCanvas(); + // this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + } +} + +export default ResizeCanvas; diff --git a/src/view/components/userQuery/UserInputCanvas.js b/src/view/components/userQuery/UserInputCanvas.ts similarity index 54% rename from src/view/components/userQuery/UserInputCanvas.js rename to src/view/components/userQuery/UserInputCanvas.ts index 6693753..b23d25b 100644 --- a/src/view/components/userQuery/UserInputCanvas.js +++ b/src/view/components/userQuery/UserInputCanvas.ts @@ -1,9 +1,21 @@ -import { CANVAS_CONFIG } from "../../../controller/constants/canvasConfig.js"; +import { CANVAS_CONFIG } from '@/controller/constants/canvasConfig.ts'; +import { userInputClipPath } from '@/view/components/userQuery/uiUtils/userInputClipPath.ts'; class UserInputCanvas { + public readonly canvas: HTMLCanvasElement | null; + public readonly ctx: CanvasRenderingContext2D; + private isDrawing: boolean; + constructor() { - this.canvas = document.getElementById('userInputCanvas'); + this.canvas = document.getElementById('user-input-canvas') as HTMLCanvasElement; this.ctx = this.canvas.getContext('2d'); + + const pathElem = document.getElementById('arc-path'); + pathElem.setAttribute( + 'd', + userInputClipPath({ radius: 560, canvasWidth: 1120, canvasHeight: 560 }), + ); + this.isDrawing = false; this.setupCanvas(); @@ -13,13 +25,16 @@ class UserInputCanvas { this.ctx.lineJoin = CANVAS_CONFIG.lineJoin; } - clear = () => { + clear = (): void => { + this.ctx.fillStyle = 'rgba(40,40,40)'; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); - } + this.drawGridDots(); + }; - setupCanvas() { - this.canvas.width = window.innerWidth; - this.canvas.height = window.innerHeight / 3; + setupCanvas(): void { + // this.canvas.width = window.innerWidth; + this.canvas.width = 1120; + this.canvas.height = 560; this.ctx.fillStyle = 'rgba(40,40,40)'; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); @@ -33,7 +48,7 @@ class UserInputCanvas { this.ctx.fillStyle = 'rgba(255,255,255,0.3)'; this.ctx.arc(x, y, 1, 0, 2 * Math.PI); this.ctx.fill(); - } + }; for (let x = 5; x < this.canvas.width; x += 12) { for (let y = 5; y < this.canvas.height; y += 12) { @@ -42,19 +57,19 @@ class UserInputCanvas { } } - startPath(x, y) { + startPath(x: number, y: number): void { this.ctx.beginPath(); this.ctx.moveTo(x, y); } - drawPath(x, y) { + drawPath(x: number, y: number): void { this.ctx.lineTo(x, y); this.ctx.stroke(); } - endPath() { + endPath(): void { this.ctx.closePath(); } } -export default UserInputCanvas; \ No newline at end of file +export default UserInputCanvas; diff --git a/src/view/components/userQuery/canvasUtils/BoundingBox.ts b/src/view/components/userQuery/canvasUtils/BoundingBox.ts new file mode 100644 index 0000000..f99faa1 --- /dev/null +++ b/src/view/components/userQuery/canvasUtils/BoundingBox.ts @@ -0,0 +1,53 @@ +import { IinitialCoords, originalObject } from '../types/types.ts'; + +class BoundingBox { + private readonly initialCoords: IinitialCoords; + private coordinate: IinitialCoords; + private originalObject: originalObject; + + constructor() { + this.initialCoords = { + minX: Infinity, + minY: Infinity, + maxX: -Infinity, + maxY: -Infinity, + }; + this.coordinate = { ...this.initialCoords }; + this.originalObject = { + x: 0, + y: 0, + width: 0, + height: 0, + }; + } + + getOriginalObject(): Readonly { + return { ...this.originalObject }; + } + + update(currentX: number, currentY: number): void { + this.coordinate = { + minX: Math.round(Math.min(this.coordinate.minX, currentX)), + minY: Math.round(Math.min(this.coordinate.minY, currentY)), + maxX: Math.round(Math.max(this.coordinate.maxX, currentX)), + maxY: Math.round(Math.max(this.coordinate.maxY, currentY)), + }; + Object.assign(this.originalObject, { + x: this.coordinate.minX - 20, + y: this.coordinate.minY - 20, + width: this.coordinate.maxX + 40 - this.coordinate.minX, + height: this.coordinate.maxY + 40 - this.coordinate.minY, + }); + } + + log(): void { + console.log(this.coordinate); + console.log(this.originalObject); + } + + reset(): void { + this.coordinate = { ...this.initialCoords }; + } +} + +export default new BoundingBox(); diff --git a/src/view/components/userQuery/types/canvas.ts b/src/view/components/userQuery/types/canvas.ts new file mode 100644 index 0000000..7d0936a --- /dev/null +++ b/src/view/components/userQuery/types/canvas.ts @@ -0,0 +1,23 @@ +export interface ICanvasBase { + setupCanvas(): void; + clear(): void; + canvas: HTMLCanvasElement; + ctx: CanvasRenderingContext2D; +} + +export interface IUserInputCanvas extends IPathTrackingCanvas { + drawGridDots(): void; +} +export interface IPathTrackingCanvas { + startPath(x: number, y: number): void; + drawPath(x: number, y: number): void; + endPath(): void; +} +export interface IAlignCanvas { + updateCanvasScale(): void; + centralize(path: HTMLCanvasElement): void; +} + +export interface IResizeCanvas { + downScale(path: HTMLCanvasElement): void; +} diff --git a/src/view/components/userQuery/types/types.ts b/src/view/components/userQuery/types/types.ts new file mode 100644 index 0000000..b9e8646 --- /dev/null +++ b/src/view/components/userQuery/types/types.ts @@ -0,0 +1,19 @@ +export interface IinitialCoords { + minX: number; + minY: number; + maxX: number; + maxY: number; +} + +export interface originalObject { + x: number; + y: number; + width: number; + height: number; +} + +interface BoundingBox { + update(currentX: number, currentY: number): void; + log(): void; + reset(): void; +} diff --git a/src/view/components/userQuery/uiUtils/userInputClipPath.ts b/src/view/components/userQuery/uiUtils/userInputClipPath.ts new file mode 100644 index 0000000..eaf3d1f --- /dev/null +++ b/src/view/components/userQuery/uiUtils/userInputClipPath.ts @@ -0,0 +1,27 @@ +export const userInputClipPath = ({ radius = 560, canvasWidth, canvasHeight }) => { + const w = canvasWidth; + const h = canvasHeight; + + // 반지름과 호각 설정 + const cx = w / 2; + const cy = radius; + const startAngle = Math.PI; // 180도 + const endAngle = 2 * Math.PI; // 360도 + + // 반원 양 끝 좌표 + const x1 = cx + radius * Math.cos(startAngle); + const y1 = cy + radius * Math.sin(startAngle); + const x2 = cx + radius * Math.cos(endAngle); + const y2 = cy + radius * Math.sin(endAngle); + + // SVG arc path (절대좌표 기준) + const path = ` + M ${x1} ${y1} + A ${radius} ${radius} 0 0 1 ${x2} ${y2} + L ${w} ${h} + L 0 ${h} + Z + `; + + return path; +}; diff --git a/src/view/styles/ViewportAdapter.ts b/src/view/styles/ViewportAdapter.ts new file mode 100644 index 0000000..e127623 --- /dev/null +++ b/src/view/styles/ViewportAdapter.ts @@ -0,0 +1,68 @@ +import { RENDERER_CONFIG } from '@/controller/constants/rendererConfig.ts'; +const { displayNodes, displayEdges } = RENDERER_CONFIG; + +import DataStore from '@/controller/DataStore.ts'; + +class ViewportAdapter { + private readonly root: HTMLElement; + + constructor() { + this.root = document.documentElement; + this.setupRendererConfig(); + } + + getViewportSize() { + return { + width: window.innerWidth, + height: window.innerHeight, + ratio: window.innerWidth / window.innerHeight, + }; + } + + setupRendererConfig() { + const { width, height } = this.getViewportSize(); + this.root.style.setProperty('--viewport-width', `${width}px`); + this.root.style.setProperty('--indicator-margin', `-15px`); + + const t = width / 4 / 560; + const y = (1 - Math.cos(t * Math.PI)) / 2; + const margin = y * 560; + + this.root.style.setProperty('--clear-button-y-margin', `${margin}px`); + + if (width > 450) { + const appendedDisplayNodes = width / 50 - 9; //50px을 단위로 하나씩 노드 추가 + DataStore.setPerceptronConfig({ + displayNodes: + displayNodes + appendedDisplayNodes > 70 + ? 70 + : displayNodes + appendedDisplayNodes, // 성능 및 최적화, UI 고려, 렌더링 노드의 수를 70개로 제한. + displayEdges: displayEdges, + }); + + const indicatorHeight = 550 - (width - 450) * 0.33 + Math.pow(width / 900, 3); + this.root.style.setProperty( + '--indicator-height', + `${indicatorHeight < 280 ? 280 : indicatorHeight}px`, + ); // 화면 너비에 따라 인디케이터를 내림.(arc 모양에 따라) + console.log(indicatorHeight < 250 ? 250 : indicatorHeight); + if (width > 1060) { + this.root.style.setProperty( + '--indicator-margin', + `${-15 + Math.round(width - 1060) / 2}px`, + ); + console.log('TEST:', Math.round(-15 + (width - 1020) / 2)); + } + } else { + DataStore.setPerceptronConfig({ + displayNodes: displayNodes, + displayEdges: displayEdges, + }); + this.root.style.setProperty('--indicator-height', `550px`); + } + this.root.style.setProperty('--left-indicator-angle', `${-width / 40}deg`); + this.root.style.setProperty('--right-indicator-angle', `${width / 40}deg`); + } +} + +export default ViewportAdapter; diff --git a/src/view/styles/main.css b/src/view/styles/main.css index b02f871..4bdf37c 100644 --- a/src/view/styles/main.css +++ b/src/view/styles/main.css @@ -1,3 +1,248 @@ #canvas { border: 1px solid black; -} \ No newline at end of file +} +#app { + position: relative; + width: 100vw; + height: 100vh; + overflow: hidden; +} +header { + display: block; + position: sticky; + top: 0; + left: 0; + + z-index: 10; + + width: 100%; + height: 60px; + + padding: 20px 20px 20px 20px; + #main-title { + display: block; + font-size: 1.3rem; + font-weight: 800; + } + #sub-title { + display: block; + font-size: 0.65rem; + font-weight: 300; + } +} + +main { + display: block; + width: 100%; + height: 100%; + position: relative; +} + +#query-result { + position: relative; + width: 100%; + height: 35%; + /*background: #757575;*/ + + display: flex; + justify-content: center; + /*justify-items: flex-end;*/ + align-items: center; + z-index: 5; + div { + display: block; + + width: fit-content; + height: fit-content; + + font-size: 11rem; + font-weight: 700; + font-family: "Noto Sans Syriac Western", sans-serif; + color: #171717; + } +} + +#canvas-unit { + position: relative; + display: block; + width: 100%; + height: 720px; + /*height: 720px;*/ + z-index: 2; + + #position-aligner { + position: absolute; + width: 100%; + height: 100%; + + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } +} +.indicator { + width: 50px; + height: 110px; + position: fixed; + + bottom: var(--indicator-height); + /*bottom: 550px;*/ + display: block; + z-index: 15; + + backdrop-filter: blur(3px); + -webkit-backdrop-filter: blur(5px); + +} +#left-indicator{ + /*left: -15px;*/ + left: var(--indicator-margin); + transform: rotate(var(--left-indicator-angle)); + + -webkit-mask-image: linear-gradient(to right, white 70%, transparent 100%); + mask-image: linear-gradient(to right, white 70%, transparent 100%); +} +#right-indicator { + right: var(--indicator-margin); + /*right: -15px;*/ + transform: rotate(var(--right-indicator-angle)); + + -webkit-mask-image: linear-gradient(to left, white 70%, transparent 100%); + mask-image: linear-gradient(to left, white 70%, transparent 100%); +} +.indicator-blur { + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4x); + background: rgba(255, 255, 255, 0.3); + + position: relative; + width: 100%; + height: 100%; +} + +#left-arrow { + height: 15px; + + position: absolute; + top: 40%; + right: 40%; + transform: translate(50%, -50%); + animation: left-float 1s ease-in-out infinite; +} +#right-arrow { + height: 15px; + + position: absolute; + top: 40%; + left: 40%; + transform: translate(-50%, -50%); + animation: right-float 1s ease-in-out infinite; +} + + +#perceptron-section { + position: absolute; + left: 50%; + bottom: 0; + + transform: translateX(-50%); + width: auto; + z-index: 1; +} + +#user-input-section { + /*display: none;*/ + position: absolute; + display: block; + left: 50%; + bottom: 0; + width: 1120px; + transform: translateX(-50%); + height: 560px; + z-index: 2; +} + +#user-input-canvas { + display: block; + width: 100%; + height: 100%; + + z-index: 2; + /* safari client 대응 */ + transform: translateZ(0); + will-change: transform; + overflow: hidden; + + clip-path: url(#arc-clip); + -webkit-clip-path: url(#arc-clip); +} + + +svg{ + display: block; +} + +#clear { + position: absolute; + z-index: 20; + + top: 480px; + right: 10px; + + width: 30px; + height: 30px; + + background: none; + border: none; + img { + display: block; + width: 25px; + + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + + } +} + +@media (width > 900px ) { + #clear { + bottom: 280px; + } +} +@media (width <= 900px ) { + #clear { + top: calc(160px + var(--clear-button-y-margin)); + } +} +@media (width >= 960px ) { + #clear { + right: calc((var(--viewport-width) - 960px + 10px) / 2); + } +} + + +@keyframes left-float { + 0% { + transform: translateX(1px); + } + 50% { + transform: translateX(-2px); + } + 100% { + transform: translateX(1px); + } +} +@keyframes right-float { + 0% { + transform: translateX(-1px); + } + 50% { + transform: translateX(1px); + } + 100% { + transform: translateX(-1px); + } +} + diff --git a/src/view/styles/settings.css b/src/view/styles/settings.css index 45f760e..210a1ab 100644 --- a/src/view/styles/settings.css +++ b/src/view/styles/settings.css @@ -1,5 +1,19 @@ +@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable.min.css"); +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@100..900&family=Roboto:ital,wght@0,100..900;1,100..900&display=swap'); + * { margin: 0; padding: 0; box-sizing: border-box; + + font-family: "Pretendard Variable", Pretendard,"Roboto", sans-serif, -apple-system, BlinkMacSystemFont, system-ui, Roboto, "Helvetica Neue", "Segoe UI", "Apple SD Gothic Neo", "Noto Sans KR", "Malgun Gothic", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", sans-serif; + color: #2B2B2B; +} + +html { + font-size: 100%; /* 16px = 1rem */ +} + +canvas { + display: block; } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..26f4f5c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2022", + "moduleResolution": "Node", + "allowImportingTsExtensions": true, + "noEmit": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": false, + "allowJs": true, + "checkJs": false, + "esModuleInterop": true, + "baseUrl": "./src", + "paths": { + "@/*": ["*"] + }, + "types": ["node"] + }, + "include": ["src/**/*"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..02b184e --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from 'vite'; +import { fileURLToPath } from 'url'; +import path from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const repo = 'NeuralNetwork-Visualization'; +const branch = process.env.GITHUB_REF_NAME || ''; +let base = `/${repo}/`; + +if (branch.startsWith('feat/')) { + base = './'; +} + +export default defineConfig({ + base: base, + resolve: { + alias: [{ find: '@', replacement: path.resolve(__dirname, 'src') }], + }, + publicDir: 'public', + build: { + outDir: 'dist', + target: 'esnext', + }, + server: { + port: 5173, + open: true, + }, +});