Compare commits

...

13 commits

Author SHA1 Message Date
39e74c69f5 Merge branch 'dev'
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-15 17:56:15 +02:00
7ff411b988 Implement webworker for save operation + Limit the history size (#29)
All checks were successful
continuous-integration/drone/push Build is passing
- Implement webworker for save operation
- Limit the history size

Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/29
2022-08-15 11:52:17 -04:00
5b20e4f2dc Revert "Replace sqrt in Dimension by Fast inverse square root"
All checks were successful
continuous-integration/drone/push Build is passing
This reverts commit 497d2173e8.
2022-08-15 16:00:04 +02:00
497d2173e8 Replace sqrt in Dimension by Fast inverse square root
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-15 15:41:38 +02:00
d6eb9ea364 Optimize history and fix nodes pollution + fix css + removes motion.framer (#28)
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/28
2022-08-12 16:31:37 -04:00
704dab7307 Implement anchor and fix bugs with rigid body (#27)
All checks were successful
continuous-integration/drone/push Build is passing
Co-authored-by: Eric NGUYEN <enguyen@techform.fr>
Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/27
2022-08-12 11:47:21 -04:00
c81a6fe44b Implement events for external use + Rename interfaces with a I prefix + add some documentation (#26)
All checks were successful
continuous-integration/drone/push Build is passing
Implement events for external use
Rename interfaces with a I prefix
Add some documentation

Co-authored-by: Eric NGUYEN <enguyen@techform.fr>
Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/26
2022-08-12 06:36:14 -04:00
Hydroxycarbamide
6c601429b9 Import NOTCHES_LENGTH for the default value of the notches
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-11 23:45:16 +02:00
Hydroxycarbamide
8e41fdad46 App.scss: Move public style to public css file
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-11 23:41:18 +02:00
Hydroxycarbamide
61b72f6a35 Separate properties operations and rigid body behaviors in different modules + Doc
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-11 23:35:40 +02:00
faa058e57d Implement new features for svg components + improve form properties (#25)
All checks were successful
continuous-integration/drone/push Build is passing
- Make Dimension an actual svg line
- Implement XPositionReference
- Select the container above after delete
- Remove DimensionLayer
- Improve form properties

Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/25
2022-08-11 11:48:31 -04:00
d11dfec22b Fix misuse of Hooks with useRef (#24)
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/24
2022-08-11 09:10:06 -04:00
ac56f84196 Add option for the properties form to only update on submit (#23)
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/23
2022-08-11 08:37:10 -04:00
61 changed files with 1389 additions and 1136 deletions

View file

@ -19,6 +19,7 @@ module.exports = {
plugins: [
'only-warn',
'react',
'react-hooks',
'@typescript-eslint'
],
rules: {
@ -29,5 +30,7 @@ module.exports = {
'@typescript-eslint/semi': ['warn', 'always'],
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'error',
'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks
'react-hooks/exhaustive-deps': 'warn' // Checks effect dependencies
}
};

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
*.drawio filter=lfs diff=lfs merge=lfs -text

BIN
docs/ComponentStructure.drawio (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="stylesheet" href="style.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>

469
package-lock.json generated
View file

@ -9,18 +9,20 @@
"version": "0.0.0",
"dependencies": {
"@heroicons/react": "^1.0.6",
"framer-motion": "^6.5.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-svg-pan-zoom": "^3.11.0"
"react-svg-pan-zoom": "^3.11.0",
"react-window": "^1.8.7"
},
"devDependencies": {
"@testing-library/dom": "^8.16.1",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^14.4.1",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@types/react-svg-pan-zoom": "^3.3.5",
"@types/react-window": "^1.8.5",
"@typescript-eslint/eslint-plugin": "^5.31.0",
"@typescript-eslint/parser": "^5.31.0",
"@vitejs/plugin-react": "^2.0.0",
@ -31,8 +33,10 @@
"eslint-config-standard-with-typescript": "^22.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.2.4",
"eslint-plugin-only-warn": "^1.0.3",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react": "^7.30.1",
"eslint-plugin-react-hooks": "^4.6.0",
"jsdom": "^20.0.0",
"postcss": "^8.4.14",
"sass": "^1.54.0",
@ -421,7 +425,6 @@
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz",
"integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==",
"dev": true,
"dependencies": {
"regenerator-runtime": "^0.13.4"
},
@ -477,21 +480,6 @@
"node": ">=6.9.0"
}
},
"node_modules/@emotion/is-prop-valid": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
"integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
"optional": true,
"dependencies": {
"@emotion/memoize": "0.7.4"
}
},
"node_modules/@emotion/memoize": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==",
"optional": true
},
"node_modules/@eslint/eslintrc": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz",
@ -614,89 +602,6 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@motionone/animation": {
"version": "10.13.1",
"resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.13.1.tgz",
"integrity": "sha512-dxQ+1wWxL6iFHDy1uv6hhcPjIdOg36eDT56jN4LI7Z5HZRyLpq8x1t7JFQclo/IEIb+6Bk4atmyinGFdXVECuA==",
"dependencies": {
"@motionone/easing": "^10.13.1",
"@motionone/types": "^10.13.0",
"@motionone/utils": "^10.13.1",
"tslib": "^2.3.1"
}
},
"node_modules/@motionone/animation/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/@motionone/dom": {
"version": "10.12.0",
"resolved": "https://registry.npmjs.org/@motionone/dom/-/dom-10.12.0.tgz",
"integrity": "sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw==",
"dependencies": {
"@motionone/animation": "^10.12.0",
"@motionone/generators": "^10.12.0",
"@motionone/types": "^10.12.0",
"@motionone/utils": "^10.12.0",
"hey-listen": "^1.0.8",
"tslib": "^2.3.1"
}
},
"node_modules/@motionone/dom/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/@motionone/easing": {
"version": "10.13.1",
"resolved": "https://registry.npmjs.org/@motionone/easing/-/easing-10.13.1.tgz",
"integrity": "sha512-INEsInHHDHVgx0dp5qlXi1lMXBqYicgLMMSn3zfGzaIvcaEbI1Uz8BoyNV4BiclTupG7RYIh+T6BU83ZcEe74g==",
"dependencies": {
"@motionone/utils": "^10.13.1",
"tslib": "^2.3.1"
}
},
"node_modules/@motionone/easing/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/@motionone/generators": {
"version": "10.13.1",
"resolved": "https://registry.npmjs.org/@motionone/generators/-/generators-10.13.1.tgz",
"integrity": "sha512-+HK5u2YcNJCckTTqfOLgSVcrWv2z1dVwrSZEMVJuAh0EnWEWGDJRvMBoPc0cFf/osbkA2Rq9bH2+vP0Ex/D8uw==",
"dependencies": {
"@motionone/types": "^10.13.0",
"@motionone/utils": "^10.13.1",
"tslib": "^2.3.1"
}
},
"node_modules/@motionone/generators/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/@motionone/types": {
"version": "10.13.0",
"resolved": "https://registry.npmjs.org/@motionone/types/-/types-10.13.0.tgz",
"integrity": "sha512-qegk4qg8U1N9ZwAJ187BG3TkZz1k9LP/pvNtCSlqdq/PMUDKlCFG4ZnjJ481P0IOH/vIw1OzIbKIuyg0A3rk9g=="
},
"node_modules/@motionone/utils": {
"version": "10.13.1",
"resolved": "https://registry.npmjs.org/@motionone/utils/-/utils-10.13.1.tgz",
"integrity": "sha512-TjDPTIppaf3ofBXQv4ZzAketJgN0sclALXfZ6mfrkjJkOy83mLls9744F+6S+VKCpBmvbZcBY4PQfrfhAfeMtA==",
"dependencies": {
"@motionone/types": "^10.13.0",
"hey-listen": "^1.0.8",
"tslib": "^2.3.1"
}
},
"node_modules/@motionone/utils/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -745,9 +650,9 @@
"dev": true
},
"node_modules/@testing-library/dom": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.16.0.tgz",
"integrity": "sha512-uxF4zmnLHHDlmW4l+0WDjcgLVwCvH+OVLpD8Dfp+Bjfz85prwxWGbwXgJdLtkgjD0qfOzkJF9SmA6YZPsMYX4w==",
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.17.1.tgz",
"integrity": "sha512-KnH2MnJUzmFNPW6RIKfd+zf2Wue8mEKX0M3cpX6aKl5ZXrJM1/c/Pc8c2xDNYQCnJO48Sm5ITbMXgqTr3h4jxQ==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
@ -1079,6 +984,15 @@
"@types/react": "*"
}
},
"node_modules/@types/react-window": {
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz",
"integrity": "sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/scheduler": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
@ -2879,6 +2793,15 @@
"eslint": ">=7.0.0"
}
},
"node_modules/eslint-plugin-only-warn": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-only-warn/-/eslint-plugin-only-warn-1.0.3.tgz",
"integrity": "sha512-XQOX/TfLoLw6h8ky51d29uUjXRTQHqBGXPylDEmy5fe/w7LIOnp8MA24b1OSMEn9BQoKow1q3g1kLe5/9uBTvw==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/eslint-plugin-promise": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.0.0.tgz",
@ -2919,6 +2842,18 @@
"eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
}
},
"node_modules/eslint-plugin-react-hooks": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz",
"integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==",
"dev": true,
"engines": {
"node": ">=10"
},
"peerDependencies": {
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0"
}
},
"node_modules/eslint-plugin-react/node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@ -3331,44 +3266,6 @@
"url": "https://www.patreon.com/infusion"
}
},
"node_modules/framer-motion": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-6.5.1.tgz",
"integrity": "sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==",
"dependencies": {
"@motionone/dom": "10.12.0",
"framesync": "6.0.1",
"hey-listen": "^1.0.8",
"popmotion": "11.0.3",
"style-value-types": "5.0.0",
"tslib": "^2.1.0"
},
"optionalDependencies": {
"@emotion/is-prop-valid": "^0.8.2"
},
"peerDependencies": {
"react": ">=16.8 || ^17.0.0 || ^18.0.0",
"react-dom": ">=16.8 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/framer-motion/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/framesync": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/framesync/-/framesync-6.0.1.tgz",
"integrity": "sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/framesync/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -3606,11 +3503,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hey-listen": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz",
"integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="
},
"node_modules/html-encoding-sniffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
@ -4428,6 +4320,11 @@
"node": ">=12"
}
},
"node_modules/memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@ -4836,22 +4733,6 @@
"node": ">=0.10.0"
}
},
"node_modules/popmotion": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.3.tgz",
"integrity": "sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==",
"dependencies": {
"framesync": "6.0.1",
"hey-listen": "^1.0.8",
"style-value-types": "5.0.0",
"tslib": "^2.1.0"
}
},
"node_modules/popmotion/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/postcss": {
"version": "8.4.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz",
@ -5129,6 +5010,22 @@
"react": ">=17.0.0"
}
},
"node_modules/react-window": {
"version": "1.8.7",
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.7.tgz",
"integrity": "sha512-JHEZbPXBpKMmoNO1bNhoXOOLg/ujhL/BU4IqVU9r8eQPcy5KQnGHIHDRkJ0ns9IM5+Aq5LNwt3j8t3tIrePQzA==",
"dependencies": {
"@babel/runtime": "^7.0.0",
"memoize-one": ">=3.1.1 <6"
},
"engines": {
"node": ">8.0.0"
},
"peerDependencies": {
"react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -5166,8 +5063,7 @@
"node_modules/regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==",
"dev": true
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
},
"node_modules/regexp.prototype.flags": {
"version": "1.4.3",
@ -5536,20 +5432,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/style-value-types": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.0.0.tgz",
"integrity": "sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==",
"dependencies": {
"hey-listen": "^1.0.8",
"tslib": "^2.1.0"
}
},
"node_modules/style-value-types/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@ -6419,7 +6301,6 @@
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz",
"integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==",
"dev": true,
"requires": {
"regenerator-runtime": "^0.13.4"
}
@ -6463,21 +6344,6 @@
"to-fast-properties": "^2.0.0"
}
},
"@emotion/is-prop-valid": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
"integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
"optional": true,
"requires": {
"@emotion/memoize": "0.7.4"
}
},
"@emotion/memoize": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==",
"optional": true
},
"@eslint/eslintrc": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz",
@ -6576,99 +6442,6 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"@motionone/animation": {
"version": "10.13.1",
"resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.13.1.tgz",
"integrity": "sha512-dxQ+1wWxL6iFHDy1uv6hhcPjIdOg36eDT56jN4LI7Z5HZRyLpq8x1t7JFQclo/IEIb+6Bk4atmyinGFdXVECuA==",
"requires": {
"@motionone/easing": "^10.13.1",
"@motionone/types": "^10.13.0",
"@motionone/utils": "^10.13.1",
"tslib": "^2.3.1"
},
"dependencies": {
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
}
}
},
"@motionone/dom": {
"version": "10.12.0",
"resolved": "https://registry.npmjs.org/@motionone/dom/-/dom-10.12.0.tgz",
"integrity": "sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw==",
"requires": {
"@motionone/animation": "^10.12.0",
"@motionone/generators": "^10.12.0",
"@motionone/types": "^10.12.0",
"@motionone/utils": "^10.12.0",
"hey-listen": "^1.0.8",
"tslib": "^2.3.1"
},
"dependencies": {
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
}
}
},
"@motionone/easing": {
"version": "10.13.1",
"resolved": "https://registry.npmjs.org/@motionone/easing/-/easing-10.13.1.tgz",
"integrity": "sha512-INEsInHHDHVgx0dp5qlXi1lMXBqYicgLMMSn3zfGzaIvcaEbI1Uz8BoyNV4BiclTupG7RYIh+T6BU83ZcEe74g==",
"requires": {
"@motionone/utils": "^10.13.1",
"tslib": "^2.3.1"
},
"dependencies": {
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
}
}
},
"@motionone/generators": {
"version": "10.13.1",
"resolved": "https://registry.npmjs.org/@motionone/generators/-/generators-10.13.1.tgz",
"integrity": "sha512-+HK5u2YcNJCckTTqfOLgSVcrWv2z1dVwrSZEMVJuAh0EnWEWGDJRvMBoPc0cFf/osbkA2Rq9bH2+vP0Ex/D8uw==",
"requires": {
"@motionone/types": "^10.13.0",
"@motionone/utils": "^10.13.1",
"tslib": "^2.3.1"
},
"dependencies": {
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
}
}
},
"@motionone/types": {
"version": "10.13.0",
"resolved": "https://registry.npmjs.org/@motionone/types/-/types-10.13.0.tgz",
"integrity": "sha512-qegk4qg8U1N9ZwAJ187BG3TkZz1k9LP/pvNtCSlqdq/PMUDKlCFG4ZnjJ481P0IOH/vIw1OzIbKIuyg0A3rk9g=="
},
"@motionone/utils": {
"version": "10.13.1",
"resolved": "https://registry.npmjs.org/@motionone/utils/-/utils-10.13.1.tgz",
"integrity": "sha512-TjDPTIppaf3ofBXQv4ZzAketJgN0sclALXfZ6mfrkjJkOy83mLls9744F+6S+VKCpBmvbZcBY4PQfrfhAfeMtA==",
"requires": {
"@motionone/types": "^10.13.0",
"hey-listen": "^1.0.8",
"tslib": "^2.3.1"
},
"dependencies": {
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
}
}
},
"@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -6708,9 +6481,9 @@
"dev": true
},
"@testing-library/dom": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.16.0.tgz",
"integrity": "sha512-uxF4zmnLHHDlmW4l+0WDjcgLVwCvH+OVLpD8Dfp+Bjfz85prwxWGbwXgJdLtkgjD0qfOzkJF9SmA6YZPsMYX4w==",
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.17.1.tgz",
"integrity": "sha512-KnH2MnJUzmFNPW6RIKfd+zf2Wue8mEKX0M3cpX6aKl5ZXrJM1/c/Pc8c2xDNYQCnJO48Sm5ITbMXgqTr3h4jxQ==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.10.4",
@ -6976,6 +6749,15 @@
"@types/react": "*"
}
},
"@types/react-window": {
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz",
"integrity": "sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/scheduler": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
@ -8254,6 +8036,12 @@
"semver": "^7.3.7"
}
},
"eslint-plugin-only-warn": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-only-warn/-/eslint-plugin-only-warn-1.0.3.tgz",
"integrity": "sha512-XQOX/TfLoLw6h8ky51d29uUjXRTQHqBGXPylDEmy5fe/w7LIOnp8MA24b1OSMEn9BQoKow1q3g1kLe5/9uBTvw==",
"dev": true
},
"eslint-plugin-promise": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.0.0.tgz",
@ -8311,6 +8099,13 @@
}
}
},
"eslint-plugin-react-hooks": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz",
"integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==",
"dev": true,
"requires": {}
},
"eslint-scope": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
@ -8510,42 +8305,6 @@
"integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==",
"dev": true
},
"framer-motion": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-6.5.1.tgz",
"integrity": "sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==",
"requires": {
"@emotion/is-prop-valid": "^0.8.2",
"@motionone/dom": "10.12.0",
"framesync": "6.0.1",
"hey-listen": "^1.0.8",
"popmotion": "11.0.3",
"style-value-types": "5.0.0",
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
}
}
},
"framesync": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/framesync/-/framesync-6.0.1.tgz",
"integrity": "sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==",
"requires": {
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
}
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -8710,11 +8469,6 @@
"has-symbols": "^1.0.2"
}
},
"hey-listen": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz",
"integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="
},
"html-encoding-sniffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
@ -9313,6 +9067,11 @@
"sourcemap-codec": "^1.4.8"
}
},
"memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
},
"merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@ -9610,24 +9369,6 @@
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
"dev": true
},
"popmotion": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.3.tgz",
"integrity": "sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==",
"requires": {
"framesync": "6.0.1",
"hey-listen": "^1.0.8",
"style-value-types": "5.0.0",
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
}
}
},
"postcss": {
"version": "8.4.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz",
@ -9796,6 +9537,15 @@
"transformation-matrix": "^2.11.1"
}
},
"react-window": {
"version": "1.8.7",
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.7.tgz",
"integrity": "sha512-JHEZbPXBpKMmoNO1bNhoXOOLg/ujhL/BU4IqVU9r8eQPcy5KQnGHIHDRkJ0ns9IM5+Aq5LNwt3j8t3tIrePQzA==",
"requires": {
"@babel/runtime": "^7.0.0",
"memoize-one": ">=3.1.1 <6"
}
},
"read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -9827,8 +9577,7 @@
"regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==",
"dev": true
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
},
"regexp.prototype.flags": {
"version": "1.4.3",
@ -10085,22 +9834,6 @@
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true
},
"style-value-types": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.0.0.tgz",
"integrity": "sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==",
"requires": {
"hey-listen": "^1.0.8",
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
}
}
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",

View file

@ -14,10 +14,10 @@
},
"dependencies": {
"@heroicons/react": "^1.0.6",
"framer-motion": "^6.5.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-svg-pan-zoom": "^3.11.0"
"react-svg-pan-zoom": "^3.11.0",
"react-window": "^1.8.7"
},
"devDependencies": {
"@testing-library/dom": "^8.16.1",
@ -27,6 +27,7 @@
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@types/react-svg-pan-zoom": "^3.3.5",
"@types/react-window": "^1.8.5",
"@typescript-eslint/eslint-plugin": "^5.31.0",
"@typescript-eslint/parser": "^5.31.0",
"@vitejs/plugin-react": "^2.0.0",
@ -40,6 +41,7 @@
"eslint-plugin-only-warn": "^1.0.3",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react": "^7.30.1",
"eslint-plugin-react-hooks": "^4.6.0",
"jsdom": "^20.0.0",
"postcss": "^8.4.14",
"sass": "^1.54.0",

150
pnpm-lock.yaml generated
View file

@ -9,6 +9,7 @@ specifiers:
'@types/react': ^18.0.15
'@types/react-dom': ^18.0.6
'@types/react-svg-pan-zoom': ^3.3.5
'@types/react-window': ^1.8.5
'@typescript-eslint/eslint-plugin': ^5.31.0
'@typescript-eslint/parser': ^5.31.0
'@vitejs/plugin-react': ^2.0.0
@ -22,12 +23,13 @@ specifiers:
eslint-plugin-only-warn: ^1.0.3
eslint-plugin-promise: ^6.0.0
eslint-plugin-react: ^7.30.1
framer-motion: ^6.5.1
eslint-plugin-react-hooks: ^4.6.0
jsdom: ^20.0.0
postcss: ^8.4.14
react: ^18.2.0
react-dom: ^18.2.0
react-svg-pan-zoom: ^3.11.0
react-window: ^1.8.7
sass: ^1.54.0
tailwindcss: ^3.1.7
typescript: ^4.6.4
@ -36,10 +38,10 @@ specifiers:
dependencies:
'@heroicons/react': 1.0.6_react@18.2.0
framer-motion: 6.5.1_biqbaboplfbrettd7655fr4n2y
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-svg-pan-zoom: 3.11.0_react@18.2.0
react-window: 1.8.7_biqbaboplfbrettd7655fr4n2y
devDependencies:
'@testing-library/dom': 8.16.1
@ -49,6 +51,7 @@ devDependencies:
'@types/react': 18.0.17
'@types/react-dom': 18.0.6
'@types/react-svg-pan-zoom': 3.3.5
'@types/react-window': 1.8.5
'@typescript-eslint/eslint-plugin': 5.32.0_iosr3hrei2tubxveewluhu5lhy
'@typescript-eslint/parser': 5.32.0_qugx7qdu5zevzvxaiqyxfiwquq
'@vitejs/plugin-react': 2.0.0_vite@3.0.4
@ -62,6 +65,7 @@ devDependencies:
eslint-plugin-only-warn: 1.0.3
eslint-plugin-promise: 6.0.0_eslint@8.21.0
eslint-plugin-react: 7.30.1_eslint@8.21.0
eslint-plugin-react-hooks: 4.6.0_eslint@8.21.0
jsdom: 20.0.0
postcss: 8.4.16
sass: 1.54.3
@ -312,7 +316,6 @@ packages:
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.13.9
dev: true
/@babel/template/7.18.10:
resolution: {integrity: sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==}
@ -350,19 +353,6 @@ packages:
to-fast-properties: 2.0.0
dev: true
/@emotion/is-prop-valid/0.8.8:
resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==}
requiresBuild: true
dependencies:
'@emotion/memoize': 0.7.4
dev: false
optional: true
/@emotion/memoize/0.7.4:
resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==}
dev: false
optional: true
/@esbuild/linux-loong64/0.14.54:
resolution: {integrity: sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==}
engines: {node: '>=12'}
@ -461,53 +451,6 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.14
dev: true
/@motionone/animation/10.13.2:
resolution: {integrity: sha512-YGWss58IR2X4lOjW89rv1Q+/Nq/QhfltaggI7i8sZTpKC1yUvM+XYDdvlRpWc6dk8LviMBrddBJAlLdbaqeRmw==}
dependencies:
'@motionone/easing': 10.13.2
'@motionone/types': 10.13.2
'@motionone/utils': 10.13.2
tslib: 2.4.0
dev: false
/@motionone/dom/10.12.0:
resolution: {integrity: sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw==}
dependencies:
'@motionone/animation': 10.13.2
'@motionone/generators': 10.13.2
'@motionone/types': 10.13.2
'@motionone/utils': 10.13.2
hey-listen: 1.0.8
tslib: 2.4.0
dev: false
/@motionone/easing/10.13.2:
resolution: {integrity: sha512-3HqctS5NyDfDQ+8+cZqc3Pu7I6amFCt9zDUjcozHyFXHh4PKYHK4+GJDFjJIS8bCAF2BrJmpmduDQ2V7lFEYeQ==}
dependencies:
'@motionone/utils': 10.13.2
tslib: 2.4.0
dev: false
/@motionone/generators/10.13.2:
resolution: {integrity: sha512-QMoXV1MXEEhR6D3dct/RMMS1FwJlAsW+kMPbFGzBA4NbweblgeYQCft9DcDAVpV9wIwD6qvlBG9u99sOXLfHiA==}
dependencies:
'@motionone/types': 10.13.2
'@motionone/utils': 10.13.2
tslib: 2.4.0
dev: false
/@motionone/types/10.13.2:
resolution: {integrity: sha512-yYV4q5v5F0iADhab4wHfqaRJnM/eVtQLjUPhyEcS72aUz/xyOzi09GzD/Gu+K506BDfqn5eULIilUI77QNaqhw==}
dev: false
/@motionone/utils/10.13.2:
resolution: {integrity: sha512-6Lw5bDA/w7lrPmT/jYWQ76lkHlHs9fl2NZpJ22cVy1kKDdEH+Cl1U6hMTpdphO6VQktQ6v2APngag91WBKLqlA==}
dependencies:
'@motionone/types': 10.13.2
hey-listen: 1.0.8
tslib: 2.4.0
dev: false
/@nodelib/fs.scandir/2.1.5:
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@ -643,6 +586,12 @@ packages:
'@types/react': 18.0.17
dev: true
/@types/react-window/1.8.5:
resolution: {integrity: sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==}
dependencies:
'@types/react': 18.0.17
dev: true
/@types/react/18.0.17:
resolution: {integrity: sha512-38ETy4tL+rn4uQQi7mB81G7V1g0u2ryquNmsVIOKUAEIDK+3CUjZ6rSRpdvS99dNBnkLFL83qfmtLacGOTIhwQ==}
dependencies:
@ -1740,6 +1689,15 @@ packages:
eslint: 8.21.0
dev: true
/eslint-plugin-react-hooks/4.6.0_eslint@8.21.0:
resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==}
engines: {node: '>=10'}
peerDependencies:
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0
dependencies:
eslint: 8.21.0
dev: true
/eslint-plugin-react/7.30.1_eslint@8.21.0:
resolution: {integrity: sha512-NbEvI9jtqO46yJA3wcRF9Mo0lF9T/jhdHqhCHXiXtD+Zcb98812wvokjWpU7Q4QH5edo6dmqrukxVvWWXHlsUg==}
engines: {node: '>=4'}
@ -1986,30 +1944,6 @@ packages:
resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==}
dev: true
/framer-motion/6.5.1_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==}
peerDependencies:
react: '>=16.8 || ^17.0.0 || ^18.0.0'
react-dom: '>=16.8 || ^17.0.0 || ^18.0.0'
dependencies:
'@motionone/dom': 10.12.0
framesync: 6.0.1
hey-listen: 1.0.8
popmotion: 11.0.3
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
style-value-types: 5.0.0
tslib: 2.4.0
optionalDependencies:
'@emotion/is-prop-valid': 0.8.8
dev: false
/framesync/6.0.1:
resolution: {integrity: sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==}
dependencies:
tslib: 2.4.0
dev: false
/fs.realpath/1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
dev: true
@ -2161,10 +2095,6 @@ packages:
function-bind: 1.1.1
dev: true
/hey-listen/1.0.8:
resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==}
dev: false
/html-encoding-sniffer/3.0.0:
resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==}
engines: {node: '>=12'}
@ -2549,6 +2479,10 @@ packages:
sourcemap-codec: 1.4.8
dev: true
/memoize-one/5.2.1:
resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
dev: false
/merge2/1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
@ -2819,15 +2753,6 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/popmotion/11.0.3:
resolution: {integrity: sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==}
dependencies:
framesync: 6.0.1
hey-listen: 1.0.8
style-value-types: 5.0.0
tslib: 2.4.0
dev: false
/postcss-import/14.1.0_postcss@8.4.16:
resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==}
engines: {node: '>=10.0.0'}
@ -2987,6 +2912,19 @@ packages:
transformation-matrix: 2.12.0
dev: false
/react-window/1.8.7_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-JHEZbPXBpKMmoNO1bNhoXOOLg/ujhL/BU4IqVU9r8eQPcy5KQnGHIHDRkJ0ns9IM5+Aq5LNwt3j8t3tIrePQzA==}
engines: {node: '>8.0.0'}
peerDependencies:
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
dependencies:
'@babel/runtime': 7.18.9
memoize-one: 5.2.1
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
dev: false
/react/18.2.0:
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
engines: {node: '>=0.10.0'}
@ -3016,7 +2954,6 @@ packages:
/regenerator-runtime/0.13.9:
resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==}
dev: true
/regexp.prototype.flags/1.4.3:
resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==}
@ -3227,13 +3164,6 @@ packages:
engines: {node: '>=8'}
dev: true
/style-value-types/5.0.0:
resolution: {integrity: sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==}
dependencies:
hey-listen: 1.0.8
tslib: 2.4.0
dev: false
/supports-color/5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'}
@ -3352,10 +3282,6 @@ packages:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
dev: true
/tslib/2.4.0:
resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==}
dev: false
/tsutils/3.21.0_typescript@4.7.4:
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
engines: {node: '>= 6'}

65
public/Interfaces.d.ts vendored Normal file
View file

@ -0,0 +1,65 @@
declare interface IHistoryState {
LastAction: string
MainContainer: IContainerModel
SelectedContainer: IContainerModel | null
SelectedContainerId: string
TypeCounters: Record<string, number>
}
declare interface IAvailableContainer {
Type: string
Width: number
Height: number
XPositionReference?: XPositionReference
Style: React.CSSProperties
}
declare interface IEditorState {
history: IHistoryState[]
historyCurrentStep: number
configuration: IConfiguration
}
declare interface IConfiguration {
AvailableContainers: IAvailableContainer[]
AvailableSymbols: IAvailableSymbol[]
MainContainer: IAvailableContainer
}
declare interface IContainerModel {
children: IContainerModel[]
parent: IContainerModel | null
properties: IProperties
userData: Record<string, string | number>
}
declare interface IProperties extends React.CSSProperties {
id: string
parentId: string | null
x: number
y: number
isRigidBody: boolean
XPositionReference?: XPositionReference
}
declare enum XPositionReference {
Left,
Center,
Right
}
declare interface IAvailableSymbol {
Name: string
XPositionReference: XPositionReference
Image: IImage
Width: number
Height: number
}
declare interface IImage {
Name: string
Url: string
Base64Image: string
Svg: string
}

6
public/style.css Normal file
View file

@ -0,0 +1,6 @@
html,
body,
#root {
width: 100%;
height: 100%;
}

View file

@ -1,10 +1,10 @@
import { Configuration } from '../../Interfaces/Configuration';
import { IConfiguration } from '../../Interfaces/IConfiguration';
/**
* Fetch the configuration from the API
* @returns {Configation} The model of the configuration for the application
*/
export async function fetchConfiguration(): Promise<Configuration> {
export async function fetchConfiguration(): Promise<IConfiguration> {
const url = `${import.meta.env.VITE_API_URL}`;
// The test library cannot use the Fetch API
// @ts-expect-error
@ -15,7 +15,7 @@ export async function fetchConfiguration(): Promise<Configuration> {
})
.then(async(response) =>
await response.json()
) as Configuration;
) as IConfiguration;
}
return await new Promise((resolve) => {
const xhr = new XMLHttpRequest();

View file

@ -1,6 +0,0 @@
html,
body,
#root {
width: 100%;
height: 100%;
}

View file

@ -1,8 +1,9 @@
import React, { useEffect, useState } from 'react';
import './App.scss';
import { MainMenu } from '../MainMenu/MainMenu';
import { ContainerModel } from '../../Interfaces/ContainerModel';
import Editor, { IEditorState } from '../Editor/Editor';
import { ContainerModel } from '../../Interfaces/IContainerModel';
import Editor from '../Editor/Editor';
import { IEditorState } from '../../Interfaces/IEditorState';
import { LoadState } from './Load';
import { LoadEditor, NewEditor } from './MenuActions';
import { DEFAULT_CONFIG, DEFAULT_MAINCONTAINER_PROPS } from '../../utils/default';

View file

@ -1,6 +1,6 @@
import { Dispatch, SetStateAction } from 'react';
import { Revive } from '../../utils/saveload';
import { IEditorState } from '../Editor/Editor';
import { IEditorState } from '../../Interfaces/IEditorState';
export function LoadState(
editorState: IEditorState,

View file

@ -1,8 +1,8 @@
import { Dispatch, SetStateAction } from 'react';
import { Configuration } from '../../Interfaces/Configuration';
import { ContainerModel } from '../../Interfaces/ContainerModel';
import { IConfiguration } from '../../Interfaces/IConfiguration';
import { ContainerModel } from '../../Interfaces/IContainerModel';
import { fetchConfiguration } from '../API/api';
import { IEditorState } from '../Editor/Editor';
import { IEditorState } from '../../Interfaces/IEditorState';
import { LoadState } from './Load';
export function NewEditor(
@ -11,7 +11,7 @@ export function NewEditor(
): void {
// Fetch the configuration from the API
fetchConfiguration()
.then((configuration: Configuration) => {
.then((configuration: IConfiguration) => {
// Set the main container from the given properties of the API
const MainContainer = new ContainerModel(
null,
@ -23,6 +23,7 @@ export function NewEditor(
width: configuration.MainContainer.Width,
height: configuration.MainContainer.Height,
isRigidBody: false,
isAnchor: false,
fillOpacity: 0,
stroke: 'black'
}

View file

@ -15,7 +15,7 @@ export const BAR_WIDTH = 64; // 4rem
export const Bar: React.FC<IBarProps> = (props) => {
return (
<div className='fixed z-20 flex flex-col top-0 left-0 h-screen w-16 bg-slate-100'>
<div className='fixed z-20 flex flex-col top-0 left-0 h-full w-16 bg-slate-100'>
<BarIcon
isActive={props.isSidebarOpen}
title='Components'

View file

@ -0,0 +1,70 @@
/**
* @module AnchorBehavior
*
* An anchor is a container that takes physical priority in the representation :
* - It cannot be moved by other rigid siblings container
* - It cannot be resized by any other siblings container
* - It cannot overlap any other siblings rigid container :
* - overlapping container are shifted to the nearest available space/width
* - or resized when there is no available space left other than theirs
* - or lose their rigid body properties when there is no available space left)
* Meaning that:
* - Moving an anchor container will resize the width of an overlapping container
* or make them lose their property as a rigid body
*/
import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { constraintBodyInsideUnallocatedWidth } from './RigidBodyBehaviors';
/**
* Impose the container position to its siblings
* Apply the following modification to the overlapping rigid body container :
* @param container Container to impose its position
*/
export function ImposePosition(container: IContainerModel): IContainerModel {
if (container.parent === undefined ||
container.parent === null) {
return container;
}
const rigidBodies = container.parent.children.filter(
child => child.properties.isRigidBody && !child.properties.isAnchor
);
const overlappingContainers = getOverlappingContainers(container, rigidBodies);
for (const overlappingContainer of overlappingContainers) {
constraintBodyInsideUnallocatedWidth(overlappingContainer);
}
return container;
}
/**
* Returns the overlapping containers with container
* @param container A container
* @param containers A list of containers
* @returns A list of overlapping containers
*/
function getOverlappingContainers(
container: IContainerModel,
containers: IContainerModel[]
): IContainerModel[] {
const min1 = container.properties.x;
const max1 = container.properties.x + Number(container.properties.width);
const overlappingContainers: IContainerModel[] = [];
for (const other of containers) {
if (other === container) {
continue;
}
const min2 = other.properties.x;
const max2 = other.properties.x + Number(other.properties.width);
const isOverlapping = Math.min(max1, max2) - Math.max(min1, min2) > 0;
if (!isOverlapping) {
continue;
}
overlappingContainers.push(other);
}
return overlappingContainers;
}

View file

@ -0,0 +1,312 @@
/**
* @module RigidBodyBehaviors
* Apply the following contraints to the `container` :
* - The container must be kept inside its parent
* - The container must find an unallocated space within the parent
* If the contraints fails, an error message will be returned
*/
import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { ISizePointer } from '../../../Interfaces/ISizePointer';
/**
* "Transform the container into a rigid body"
* Apply the following contraints to the `container` :
* - The container must be kept inside its parent
* - The container must find an unallocated space within the parent
* If the contraints fails, an error message will be returned
* @param container Container to apply its rigid body properties
* @returns A rigid body container
*/
export function RecalculatePhysics(
container: IContainerModel
): IContainerModel {
container = constraintBodyInsideParent(container);
container = constraintBodyInsideUnallocatedWidth(container);
return container;
}
/**
* Limit a rect inside a parent rect by applying the following rules :
* it cannot be bigger than the parent
* it cannot go out of bound
* Mutates and returns the container
* @param container
* @returns Updated container
*/
function constraintBodyInsideParent(
container: IContainerModel
): IContainerModel {
if (container.parent === null || container.parent === undefined) {
return container;
}
const parentProperties = container.parent.properties;
const parentWidth = Number(parentProperties.width);
const parentHeight = Number(parentProperties.height);
return constraintBodyInsideSpace(container, 0, 0, parentWidth, parentHeight);
}
/**
* Limit a container inside a rectangle
* Mutates and returns the container
* @param container A container
* @param x x of top left of the rectangle
* @param y y of top left of the rectangle
* @param width width of the rectangle
* @param height height of the rectangle
* @returns Updated container
*/
function constraintBodyInsideSpace(
container: IContainerModel,
x: number,
y: number,
width: number,
height: number
): IContainerModel {
const containerProperties = container.properties;
const containerX = Number(containerProperties.x);
const containerY = Number(containerProperties.y);
const containerWidth = Number(containerProperties.width);
const containerHeight = Number(containerProperties.height);
// Check size bigger than parent
const isBodyLargerThanParent = containerWidth > width;
const isBodyTallerThanParentHeight = containerHeight > height;
if (isBodyLargerThanParent || isBodyTallerThanParentHeight) {
if (isBodyLargerThanParent) {
containerProperties.x = x;
containerProperties.width = width;
}
if (isBodyTallerThanParentHeight) {
containerProperties.y = y;
containerProperties.height = height;
}
return container;
}
// Check horizontal out of bound
if (containerX < x) {
containerProperties.x = x;
}
if (containerX + containerWidth > x + width) {
containerProperties.x = x + width - containerWidth;
}
// Check vertical out of bound
if (containerY < y) {
containerProperties.y = y;
}
if (containerY + containerHeight > y + height) {
containerProperties.y = y + height - containerHeight;
}
return container;
}
/**
* Constraint the container inside unallocated width/space of the parent container
* If there is no unalloacted width/space, an error will be thrown
* Mutates and returns the container
* @param container
* @returns Updated container
*/
export function constraintBodyInsideUnallocatedWidth(
container: IContainerModel
): IContainerModel {
if (container.parent === null) {
return container;
}
// Get the available spaces of the parent
const availableWidths = getAvailableWidths(container.parent, container);
const containerX = Number(container.properties.x);
const containerWidth = Number(container.properties.width);
// Check if there is still some space
if (availableWidths.length === 0) {
throw new Error(
'No available space found on the parent container. Try to free the parent a little before placing it inside.'
);
}
const middle = containerX + containerWidth / 2;
// Sort the available width to find the space with the closest position
availableWidths.sort(
(width1, width2) => {
let compared1X = width1.x;
if (width1.x < containerX) {
compared1X = width1.x + width1.width - containerWidth;
}
let compared2X = width2.x;
if (width2.x < containerX) {
compared2X = width2.x + width2.width - containerWidth;
}
return Math.abs(compared1X - middle) - Math.abs(compared2X - middle);
}
);
// Check if the container actually fit inside
// It will usually fit if it was alrady fitting
const availableWidthFound = availableWidths.find((width) =>
isFitting(container, width)
);
if (availableWidthFound === undefined) {
// Otherwise, it is possible that it does not fit
// There is two way to reach this part of the code
// 1) Enable isRigidBody such as width > availableWidth.width
// 2) Resize a container such as width > availableWidth.width
// We want the container to fit automatically inside the available space
// even if it means to resize the container
// The end goal is that the code never show the error message no matter what action is done
// TODO: Actually give an option to not fit and show the error message shown below
const availableWidth = availableWidths[0];
container.properties.x = availableWidth.x;
container.properties.width = availableWidth.width;
// throw new Error('[constraintBodyInsideUnallocatedWidth] BIGERR: No available space found on the parent container, even though there is some.');
return container;
}
return constraintBodyInsideSpace(
container,
availableWidthFound.x,
0,
availableWidthFound.width,
Number(container.parent.properties.height)
);
}
/**
* Get the unallocated widths inside a container
* An allocated width is defined by its the widths of the children that are rigid bodies.
* An example of this allocation system is the disk space of an hard drive
* (except the fact that disk space is divided by block).
* @param container Container where to find an available width
* @param exception Container to exclude of the widths (since a container will be moved, it might need to be excluded)
* @returns {ISizePointer[]} Array of unallocated widths (x=position of the unallocated space, width=size of the allocated space)
*/
function getAvailableWidths(
container: IContainerModel,
exception: IContainerModel
): ISizePointer[] {
// Initialize the first size pointer
// which takes full width of the available space
const x = 0;
const width = Number(container.properties.width);
let unallocatedSpaces: ISizePointer[] = [{ x, width }];
// We will only uses containers that also are rigid or are anchors
const solidBodies = container.children.filter(
(child) => child.properties.isRigidBody || child.properties.isAnchor
);
for (const child of solidBodies) {
// Ignore the exception
if (child === exception) {
continue;
}
const childX = child.properties.x;
const childWidth = Number(child.properties.width);
// get the space of the child that is inside the parent
let newUnallocatedSpace: ISizePointer[] = [];
// We will iterate on a mutable variable in order to divide it
for (const unallocatedSpace of unallocatedSpaces) {
// In order to find unallocated space,
// We need to calculate the overlap between the two containers
// We only works with widths meaning in 1D (with lines)
const newUnallocatedWidths = getAvailableWidthsTwoLines(
unallocatedSpace.x,
unallocatedSpace.x + unallocatedSpace.width,
childX,
childX + childWidth
);
// Concat the new list of SizePointer pointing to availables spaces
newUnallocatedSpace = newUnallocatedSpace.concat(newUnallocatedWidths);
}
// Finally update the availables spaces found, loop again with it
unallocatedSpaces = newUnallocatedSpace;
}
return unallocatedSpaces;
}
/**
* Returns the unallocated widths between two lines in 1D
* @param unalloctedSpaceLeft left of the first line
* @param unallocatedSpaceRight rigth of the first line
* @param rectLeft left of the second line
* @param rectRight right of the second line
* @returns Available widths
*/
function getAvailableWidthsTwoLines(
unalloctedSpaceLeft: number,
unallocatedSpaceRight: number,
rectLeft: number,
rectRight: number
): ISizePointer[] {
if (unallocatedSpaceRight < rectLeft ||
unalloctedSpaceLeft > rectRight
) {
// object 1 and 2 are not overlapping
return [{
x: unalloctedSpaceLeft,
width: unallocatedSpaceRight - unalloctedSpaceLeft
}];
}
if (rectLeft < unalloctedSpaceLeft && rectRight > unallocatedSpaceRight) {
// object 2 is overlapping full width
return [];
}
if (unalloctedSpaceLeft >= rectLeft) {
// object 2 is partially overlapping on the left
return [
{
x: rectRight,
width: unallocatedSpaceRight - rectRight
}
];
}
if (rectRight >= unallocatedSpaceRight) {
// object 2 is partially overlapping on the right
return [
{
x: unalloctedSpaceLeft,
width: rectRight - unalloctedSpaceLeft
}
];
}
// object 2 is overlapping in the middle
return [
{
x: unalloctedSpaceLeft,
width: rectLeft - unalloctedSpaceLeft
},
{
x: rectRight,
width: unallocatedSpaceRight - rectRight
}
];
}
/**
* Check if a container can fit inside a size space
* @param container Container to check
* @param sizePointer Size space to check
* @returns
*/
const isFitting = (
container: IContainerModel,
sizePointer: ISizePointer
): boolean => Number(container.properties.width) <= sizePointer.width;

View file

@ -1,10 +1,10 @@
import { Dispatch, SetStateAction } from 'react';
import { HistoryState } from '../../Interfaces/HistoryState';
import { Configuration } from '../../Interfaces/Configuration';
import { ContainerModel, IContainerModel } from '../../Interfaces/ContainerModel';
import { IHistoryState } from '../../Interfaces/IHistoryState';
import { IConfiguration } from '../../Interfaces/IConfiguration';
import { ContainerModel, IContainerModel } from '../../Interfaces/IContainerModel';
import { findContainerById } from '../../utils/itertools';
import { getCurrentHistory } from './Editor';
import { SizePointer } from '../../Interfaces/SizePointer';
import IProperties from '../../Interfaces/IProperties';
/**
* Select a container
@ -12,9 +12,9 @@ import { SizePointer } from '../../Interfaces/SizePointer';
*/
export function SelectContainer(
container: ContainerModel,
fullHistory: HistoryState[],
fullHistory: IHistoryState[],
historyCurrentStep: number,
setHistory: Dispatch<SetStateAction<HistoryState[]>>,
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void {
const history = getCurrentHistory(fullHistory, historyCurrentStep);
@ -27,21 +27,30 @@ export function SelectContainer(
throw new Error('[SelectContainer] Cannot find container among children of main container!');
}
setHistory(history.concat([{
LastAction: `Select container ${selectedContainer.properties.id}`,
history.push({
LastAction: `Select ${selectedContainer.properties.id}`,
MainContainer: mainContainerClone,
SelectedContainer: selectedContainer,
SelectedContainerId: selectedContainer.properties.id,
TypeCounters: Object.assign({}, current.TypeCounters)
}]));
setHistoryCurrentStep(history.length);
});
setHistory(history);
setHistoryCurrentStep(history.length - 1);
}
/**
* Delete a container
* @param containerId containerId of the container to delete
* @param fullHistory History of the editor
* @param historyCurrentStep Current step
* @param setHistory State setter for History
* @param setHistoryCurrentStep State setter for current step
*/
export function DeleteContainer(
containerId: string,
fullHistory: HistoryState[],
fullHistory: IHistoryState[],
historyCurrentStep: number,
setHistory: Dispatch<SetStateAction<HistoryState[]>>,
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void {
const history = getCurrentHistory(fullHistory, historyCurrentStep);
@ -54,43 +63,58 @@ export function DeleteContainer(
throw new Error(`[DeleteContainer] Tried to delete a container that is not present in the main container: ${containerId}`);
}
if (container === mainContainerClone) {
if (container === mainContainerClone ||
container.parent === undefined ||
container.parent === null) {
// TODO: Implement alert
throw new Error('[DeleteContainer] Tried to delete the main container! Deleting the main container is not allowed!');
}
if (container === null || container === undefined) {
throw new Error('[OnPropertyChange] Container model was not found among children of the main container!');
throw new Error('[DeleteContainer] Container model was not found among children of the main container!');
}
if (container.parent != null) {
const index = container.parent.children.indexOf(container);
if (index > -1) {
container.parent.children.splice(index, 1);
}
const index = container.parent.children.indexOf(container);
if (index > -1) {
container.parent.children.splice(index, 1);
} else {
throw new Error('[DeleteContainer] Could not find container among parent\'s children');
}
setHistory(history.concat([{
LastAction: `Delete container ${containerId}`,
// Select the previous container
// or select the one above
const SelectedContainer = findContainerById(mainContainerClone, current.SelectedContainerId) ??
container.parent.children.at(index - 1) ??
container.parent;
const SelectedContainerId = SelectedContainer.properties.id;
history.push({
LastAction: `Delete ${containerId}`,
MainContainer: mainContainerClone,
SelectedContainer: null,
SelectedContainerId: '',
SelectedContainer,
SelectedContainerId,
TypeCounters: Object.assign({}, current.TypeCounters)
}]));
setHistoryCurrentStep(history.length);
});
setHistory(history);
setHistoryCurrentStep(history.length - 1);
}
/**
* Add a new container to a selected container
* @param type The type of container
* @param configuration Configuration of the App
* @param fullHistory History of the editor
* @param historyCurrentStep Current step
* @param setHistory State setter for History
* @param setHistoryCurrentStep State setter for current step
* @returns void
*/
export function AddContainerToSelectedContainer(
type: string,
configuration: Configuration,
fullHistory: HistoryState[],
configuration: IConfiguration,
fullHistory: IHistoryState[],
historyCurrentStep: number,
setHistory: Dispatch<SetStateAction<HistoryState[]>>,
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void {
const history = getCurrentHistory(fullHistory, historyCurrentStep);
@ -114,14 +138,26 @@ export function AddContainerToSelectedContainer(
);
}
/**
* Create and add a new container at `index` in children of parent of `parentId`
* @param index Index where to insert to the new container
* @param type Type of container
* @param parentId Parent in which to insert the new container
* @param configuration Configuration of the app
* @param fullHistory History of the editor
* @param historyCurrentStep Current step
* @param setHistory State setter of History
* @param setHistoryCurrentStep State setter of the current step
* @returns void
*/
export function AddContainer(
index: number,
type: string,
parentId: string,
configuration: Configuration,
fullHistory: HistoryState[],
configuration: IConfiguration,
fullHistory: IHistoryState[],
historyCurrentStep: number,
setHistory: Dispatch<SetStateAction<HistoryState[]>>,
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void {
const history = getCurrentHistory(fullHistory, historyCurrentStep);
@ -170,19 +206,23 @@ export function AddContainer(
}
}
const defaultProperties: IProperties = {
id: `${type}-${count}`,
parentId: parentClone.properties.id,
x,
y: 0,
width: properties.Width,
height: parentClone.properties.height,
isRigidBody: false,
isAnchor: false,
XPositionReference: properties.XPositionReference,
...properties.Style
};
// Create the container
const newContainer = new ContainerModel(
parentClone,
{
id: `${type}-${count}`,
parentId: parentClone.properties.id,
x,
y: 0,
width: properties?.Width,
height: parentClone.properties.height,
isRigidBody: false,
...properties.Style
},
defaultProperties,
[],
{
type
@ -197,278 +237,13 @@ export function AddContainer(
}
// Update the state
setHistory(history.concat([{
history.push({
LastAction: 'Add container',
MainContainer: clone,
SelectedContainer: parentClone,
SelectedContainerId: parentClone.properties.id,
TypeCounters: newCounters
}]));
setHistoryCurrentStep(history.length);
}
/**
* Handled the property change event in the properties form
* @param key Property name
* @param value New value of the property
* @returns void
*/
export function OnPropertyChange(
key: string,
value: string | number | boolean,
fullHistory: HistoryState[],
historyCurrentStep: number,
setHistory: Dispatch<SetStateAction<HistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void {
const history = getCurrentHistory(fullHistory, historyCurrentStep);
const current = history[history.length - 1];
if (current.SelectedContainer === null ||
current.SelectedContainer === undefined) {
throw new Error('[OnPropertyChange] Property was changed before selecting a Container');
}
if (parent === null) {
const selectedContainerClone: IContainerModel = structuredClone(current.SelectedContainer);
(selectedContainerClone.properties as any)[key] = value;
setHistory(history.concat([{
LastAction: 'Change property of main',
MainContainer: selectedContainerClone,
SelectedContainer: selectedContainerClone,
SelectedContainerId: selectedContainerClone.properties.id,
TypeCounters: Object.assign({}, current.TypeCounters)
}]));
setHistoryCurrentStep(history.length);
return;
}
const mainContainerClone: IContainerModel = structuredClone(current.MainContainer);
const container: ContainerModel | undefined = findContainerById(mainContainerClone, current.SelectedContainer.properties.id);
if (container === null || container === undefined) {
throw new Error('[OnPropertyChange] Container model was not found among children of the main container!');
}
(container.properties as any)[key] = value;
if (container.properties.isRigidBody) {
RecalculatePhysics(container);
}
setHistory(history.concat([{
LastAction: `Change property of container ${container.properties.id}`,
MainContainer: mainContainerClone,
SelectedContainer: container,
SelectedContainerId: container.properties.id,
TypeCounters: Object.assign({}, current.TypeCounters)
}]));
setHistoryCurrentStep(history.length);
}
// TODO put this in a different file
export function RecalculatePhysics(container: IContainerModel): IContainerModel {
container = constraintBodyInsideParent(container);
container = constraintBodyInsideUnallocatedWidth(container);
return container;
}
/**
* Limit a rect inside a parent rect by applying the following rules :
* it cannot be bigger than the parent
* it cannot go out of bound
* @param container
* @returns
*/
function constraintBodyInsideParent(container: IContainerModel): IContainerModel {
if (container.parent === null || container.parent === undefined) {
return container;
}
const parentProperties = container.parent.properties;
const parentWidth = Number(parentProperties.width);
const parentHeight = Number(parentProperties.height);
return constraintBodyInsideSpace(container, 0, 0, parentWidth, parentHeight);
}
function constraintBodyInsideSpace(
container: IContainerModel,
x: number,
y: number,
width: number,
height: number
): IContainerModel {
const containerProperties = container.properties;
const containerX = Number(containerProperties.x);
const containerY = Number(containerProperties.y);
const containerWidth = Number(containerProperties.width);
const containerHeight = Number(containerProperties.height);
// Check size bigger than parent
const isBodyLargerThanParent = containerWidth > width;
const isBodyTallerThanParentHeight = containerHeight > height;
if (isBodyLargerThanParent || isBodyTallerThanParentHeight) {
if (isBodyLargerThanParent) {
containerProperties.x = x;
containerProperties.width = width;
}
if (isBodyTallerThanParentHeight) {
containerProperties.y = y;
containerProperties.height = height;
}
return container;
}
// Check horizontal out of bound
if (containerX < x) {
containerProperties.x = x;
}
if (containerX + containerWidth > width) {
containerProperties.x = x + width - containerWidth;
}
// Check vertical out of bound
if (containerY < y) {
containerProperties.y = y;
}
if (containerY + containerHeight > height) {
containerProperties.y = y + height - containerHeight;
}
return container;
}
/**
* Get the unallocated widths inside a container
* An allocated width is defined by its the widths of the children that are rigid bodies.
* An example of this allocation system is the disk space
* (except the fact that disk space is divided by block).
* @param container
* @returns {SizePointer[]} Array of unallocated widths (x=position of the unallocated space, width=size of the allocated space)
*/
function getAvailableWidths(container: IContainerModel, exception: IContainerModel): SizePointer[] {
const x = 0;
const width = Number(container.properties.width);
let unallocatedSpaces: SizePointer[] = [{ x, width }];
const rigidBodies = container.children.filter(child => child.properties.isRigidBody);
for (const child of rigidBodies) {
if (child === exception) {
continue;
}
// get the space of the child that is inside the parent
let newUnallocatedSpace: SizePointer[] = [];
for (const unallocatedSpace of unallocatedSpaces) {
const newUnallocatedWidths = getAvailableWidthsTwoLines(
unallocatedSpace.x,
unallocatedSpace.x + unallocatedSpace.width,
child.properties.x,
child.properties.x + Number(child.properties.width));
newUnallocatedSpace = newUnallocatedSpace.concat(newUnallocatedWidths);
}
unallocatedSpaces = newUnallocatedSpace;
}
return unallocatedSpaces;
}
/**
* Returns the unallocated widths between two lines in 1D
* @param min1 left of the first line
* @param max1 rigth of the first line
* @param min2 left of the second line
* @param max2 right of the second line
* @returns Available widths
*/
function getAvailableWidthsTwoLines(min1: number, max1: number, min2: number, max2: number): SizePointer[] {
if (min2 < min1 && max2 > max1) {
// object 2 is overlapping full width
return [];
}
if (min1 >= min2) {
// object 2 is partially overlapping on the left
return [{
x: max2,
width: max1 - max2
}];
}
if (max2 >= max1) {
// object 2 is partially overlapping on the right
return [{
x: min2,
width: max2 - min1
}];
}
// object 2 is overlapping in the middle
return [
{
x: min1,
width: min2 - min1
},
{
x: min2,
width: max1 - max2
}
];
}
/**
*
* @param container
* @returns
*/
function constraintBodyInsideUnallocatedWidth(container: IContainerModel): IContainerModel {
if (container.parent === null) {
return container;
}
const availableWidths = getAvailableWidths(container.parent, container);
const containerX = Number(container.properties.x);
// Sort the available width
availableWidths
.sort((width1, width2) => Math.abs(width1.x - containerX) - Math.abs(width2.x - containerX));
if (availableWidths.length === 0) {
throw new Error('No available space found on the parent container. Try to free the parent a little before placing it inside.');
}
const availableWidthFound = availableWidths.find(
width => isFitting(container, width)
);
if (availableWidthFound === undefined) {
// There is two way to reach this part of the code
// 1) toggle the isRigidBody such as width > availableWidth.width
// 2) resize a container such as width > availableWidth.width
// We want the container to fit automatically inside the available space
// even if it means to resize the container
// The end goal is that the code never show the error message no matter what action is done
// TODO: Actually give an option to not fit and show the error message shown below
const availableWidth = availableWidths[0];
container.properties.x = availableWidth.x;
container.properties.width = availableWidth.width;
// throw new Error('[constraintBodyInsideUnallocatedWidth] BIGERR: No available space found on the parent container, even though there is some.');
return container;
}
return constraintBodyInsideSpace(
container,
availableWidthFound.x,
0,
availableWidthFound.width,
Number(container.parent.properties.height)
);
}
function isFitting(container: IContainerModel, sizePointer: SizePointer): boolean {
const containerWidth = Number(container.properties.width);
return containerWidth <= sizePointer.width;
});
setHistory(history);
setHistoryCurrentStep(history.length - 1);
}

View file

@ -1,31 +1,35 @@
import React from 'react';
import React, { useRef } from 'react';
import './Editor.scss';
import { Configuration } from '../../Interfaces/Configuration';
import { IConfiguration } from '../../Interfaces/IConfiguration';
import { SVG } from '../SVG/SVG';
import { HistoryState } from '../../Interfaces/HistoryState';
import { IHistoryState } from '../../Interfaces/IHistoryState';
import { UI } from '../UI/UI';
import { SelectContainer, DeleteContainer, OnPropertyChange, AddContainerToSelectedContainer, AddContainer } from './ContainerOperations';
import { SelectContainer, DeleteContainer, AddContainerToSelectedContainer, AddContainer } from './ContainerOperations';
import { SaveEditorAsJSON, SaveEditorAsSVG } from './Save';
import { onKeyDown } from './Shortcuts';
import { OnPropertyChange, OnPropertiesSubmit } from './PropertiesOperations';
import EditorEvents from '../../Events/EditorEvents';
import { IEditorState } from '../../Interfaces/IEditorState';
import { MAX_HISTORY } from '../../utils/default';
interface IEditorProps {
configuration: Configuration
history: HistoryState[]
configuration: IConfiguration
history: IHistoryState[]
historyCurrentStep: number
}
export interface IEditorState {
history: HistoryState[]
historyCurrentStep: number
configuration: Configuration
}
export const getCurrentHistory = (history: IHistoryState[], historyCurrentStep: number): IHistoryState[] =>
history.slice(
Math.max(0, history.length - MAX_HISTORY), // change this to 0 for unlimited (not recommanded because of overflow)
historyCurrentStep + 1
);
export const getCurrentHistory = (history: HistoryState[], historyCurrentStep: number): HistoryState[] => history.slice(0, historyCurrentStep + 1);
export const getCurrentHistoryState = (history: HistoryState[], historyCurrentStep: number): HistoryState => history[historyCurrentStep];
export const getCurrentHistoryState = (history: IHistoryState[], historyCurrentStep: number): IHistoryState => history[historyCurrentStep];
const Editor: React.FunctionComponent<IEditorProps> = (props) => {
const [history, setHistory] = React.useState<HistoryState[]>(structuredClone(props.history));
const [history, setHistory] = React.useState<IHistoryState[]>(structuredClone(props.history));
const [historyCurrentStep, setHistoryCurrentStep] = React.useState<number>(props.historyCurrentStep);
const editorRef = useRef<HTMLDivElement>(null);
React.useEffect(() => {
const onKeyUp = (event: KeyboardEvent): void => onKeyDown(
@ -37,15 +41,37 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
window.addEventListener('keyup', onKeyUp);
const events = EditorEvents;
const editorState: IEditorState = {
history,
historyCurrentStep,
configuration: props.configuration
};
const funcs = new Map<string, () => void>();
for (const event of events) {
const func = (): void => event.func(editorState);
editorRef.current?.addEventListener(event.name, func);
funcs.set(event.name, func);
}
return () => {
window.removeEventListener('keyup', onKeyUp);
for (const event of events) {
const func = funcs.get(event.name);
if (func === undefined) {
continue;
}
editorRef.current?.removeEventListener(event.name, func);
}
};
});
const configuration = props.configuration;
const current = getCurrentHistoryState(history, historyCurrentStep);
return (
<div className="App font-sans h-full">
<div ref={editorRef} className="Editor font-sans h-full">
<UI
current={current}
history={history}
@ -72,6 +98,14 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
setHistory,
setHistoryCurrentStep
)}
OnPropertiesSubmit={(event, properties) => OnPropertiesSubmit(
event,
properties,
history,
historyCurrentStep,
setHistory,
setHistoryCurrentStep
)}
AddContainerToSelectedContainer={(type) => AddContainerToSelectedContainer(
type,
configuration,

View file

@ -0,0 +1,120 @@
import { Dispatch, SetStateAction } from 'react';
import { IContainerModel, ContainerModel } from '../../Interfaces/IContainerModel';
import { IHistoryState } from '../../Interfaces/IHistoryState';
import IProperties from '../../Interfaces/IProperties';
import { findContainerById } from '../../utils/itertools';
import { getCurrentHistory } from './Editor';
import { RecalculatePhysics } from './Behaviors/RigidBodyBehaviors';
import { INPUT_TYPES } from '../Properties/PropertiesInputTypes';
import { ImposePosition } from './Behaviors/AnchorBehaviors';
/**
* Handled the property change event in the properties form
* @param key Property name
* @param value New value of the property
* @returns void
*/
export function OnPropertyChange(
key: string,
value: string | number | boolean,
fullHistory: IHistoryState[],
historyCurrentStep: number,
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void {
const history = getCurrentHistory(fullHistory, historyCurrentStep);
const current = history[history.length - 1];
if (current.SelectedContainer === null ||
current.SelectedContainer === undefined) {
throw new Error('[OnPropertyChange] Property was changed before selecting a Container');
}
const mainContainerClone: IContainerModel = structuredClone(current.MainContainer);
const container: ContainerModel | undefined = findContainerById(mainContainerClone, current.SelectedContainer.properties.id);
if (container === null || container === undefined) {
throw new Error('[OnPropertyChange] Container model was not found among children of the main container!');
}
if (INPUT_TYPES[key] === 'number') {
(container.properties as any)[key] = Number(value);
} else {
(container.properties as any)[key] = value;
}
if (container.properties.isAnchor) {
ImposePosition(container);
}
if (container.properties.isRigidBody) {
RecalculatePhysics(container);
}
history.push({
LastAction: `Change ${key} of ${container.properties.id}`,
MainContainer: mainContainerClone,
SelectedContainer: container,
SelectedContainerId: container.properties.id,
TypeCounters: Object.assign({}, current.TypeCounters)
});
setHistory(history);
setHistoryCurrentStep(history.length - 1);
}
/**
* Handled the property change event in the properties form
* @param key Property name
* @param properties Properties of the selected container
* @returns void
*/
export function OnPropertiesSubmit(
event: React.SyntheticEvent<HTMLFormElement>,
properties: IProperties,
fullHistory: IHistoryState[],
historyCurrentStep: number,
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void {
event.preventDefault();
const history = getCurrentHistory(fullHistory, historyCurrentStep);
const current = history[history.length - 1];
if (current.SelectedContainer === null ||
current.SelectedContainer === undefined) {
throw new Error('[OnPropertyChange] Property was changed before selecting a Container');
}
const mainContainerClone: IContainerModel = structuredClone(current.MainContainer);
const container: ContainerModel | undefined = findContainerById(mainContainerClone, current.SelectedContainer.properties.id);
if (container === null || container === undefined) {
throw new Error('[OnPropertyChange] Container model was not found among children of the main container!');
}
for (const property in properties) {
const input = (event.target as HTMLFormElement).querySelector(`#${property}`);
if (input instanceof HTMLInputElement) {
(container.properties as any)[property] = input.value;
if (INPUT_TYPES[property] === 'number') {
(container.properties as any)[property] = Number(input.value);
} else {
(container.properties as any)[property] = input.value;
}
}
}
if (container.properties.isRigidBody) {
RecalculatePhysics(container);
}
history.push({
LastAction: `Change properties of ${container.properties.id}`,
MainContainer: mainContainerClone,
SelectedContainer: container,
SelectedContainerId: container.properties.id,
TypeCounters: Object.assign({}, current.TypeCounters)
});
setHistory(history);
setHistoryCurrentStep(history.length - 1);
}

View file

@ -1,29 +1,40 @@
import { HistoryState } from "../../Interfaces/HistoryState";
import { Configuration } from '../../Interfaces/Configuration';
import { IHistoryState } from '../../Interfaces/IHistoryState';
import { IConfiguration } from '../../Interfaces/IConfiguration';
import { getCircularReplacer } from '../../utils/saveload';
import { ID } from '../SVG/SVG';
import { IEditorState } from './Editor';
import { IEditorState } from '../../Interfaces/IEditorState';
import Worker from '../../workers/worker?worker';
export function SaveEditorAsJSON(
history: HistoryState[],
history: IHistoryState[],
historyCurrentStep: number,
configuration: Configuration
configuration: IConfiguration
): void {
const exportName = 'state';
const exportName = 'state.json';
const spaces = import.meta.env.DEV ? 4 : 0;
const editorState: IEditorState = {
history,
historyCurrentStep,
configuration
};
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (window.Worker) {
// use webworker for the stringify to avoid freezing
const myWorker = new Worker();
myWorker.postMessage({ editorState, spaces });
myWorker.onmessage = (event) => {
const data = event.data;
const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(data)}`;
createDownloadNode(exportName, dataStr);
myWorker.terminate();
};
return;
}
const data = JSON.stringify(editorState, getCircularReplacer(), spaces);
const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(data)}`;
const downloadAnchorNode = document.createElement('a');
downloadAnchorNode.setAttribute('href', dataStr);
downloadAnchorNode.setAttribute('download', `${exportName}.json`);
document.body.appendChild(downloadAnchorNode); // required for firefox
downloadAnchorNode.click();
downloadAnchorNode.remove();
createDownloadNode(exportName, dataStr);
}
export function SaveEditorAsSVG(): void {
@ -32,10 +43,14 @@ export function SaveEditorAsSVG(): void {
const preface = '<?xml version="1.0" standalone="no"?>\r\n';
const svgBlob = new Blob([preface, svg.outerHTML], { type: 'image/svg+xml;charset=utf-8' });
const svgUrl = URL.createObjectURL(svgBlob);
const downloadLink = document.createElement('a');
downloadLink.href = svgUrl;
downloadLink.download = 'newesttree.svg';
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
createDownloadNode('state.svg', svgUrl);
}
function createDownloadNode(filename: string, datastring: string) {
const downloadAnchorNode = document.createElement('a');
downloadAnchorNode.href = datastring;
downloadAnchorNode.download = filename;
document.body.appendChild(downloadAnchorNode); // required for firefox
downloadAnchorNode.click();
downloadAnchorNode.remove();
}

View file

@ -1,9 +1,9 @@
import { Dispatch, SetStateAction } from 'react';
import { HistoryState } from '../../Interfaces/HistoryState';
import { IHistoryState } from '../../Interfaces/IHistoryState';
export function onKeyDown(
event: KeyboardEvent,
history: HistoryState[],
history: IHistoryState[],
historyCurrentStep: number,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void {

View file

@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest';
import * as React from 'react';
import { fireEvent, render, screen } from '../../utils/test-utils';
import { ElementsSidebar } from './ElementsSidebar';
import { IContainerModel } from '../../Interfaces/ContainerModel';
import { IContainerModel } from '../../Interfaces/IContainerModel';
describe.concurrent('Elements sidebar', () => {
it('With a MainContainer', () => {
@ -17,7 +17,8 @@ describe.concurrent('Elements sidebar', () => {
y: 0,
width: 2000,
height: 100,
isRigidBody: false
isRigidBody: false,
isAnchor: false
},
userData: {}
}}
@ -25,6 +26,7 @@ describe.concurrent('Elements sidebar', () => {
isHistoryOpen={false}
SelectedContainer={null}
OnPropertyChange={() => {}}
OnPropertiesSubmit={() => {}}
SelectContainer={() => {}}
DeleteContainer={() => {}}
AddContainer={() => {}}
@ -46,7 +48,8 @@ describe.concurrent('Elements sidebar', () => {
y: 0,
width: 2000,
height: 100,
isRigidBody: false
isRigidBody: false,
isAnchor: false
},
userData: {}
};
@ -57,6 +60,7 @@ describe.concurrent('Elements sidebar', () => {
isHistoryOpen={false}
SelectedContainer={MainContainer}
OnPropertyChange={() => {}}
OnPropertiesSubmit={() => {}}
SelectContainer={() => {}}
DeleteContainer={() => {}}
AddContainer={() => {}}
@ -70,12 +74,12 @@ describe.concurrent('Elements sidebar', () => {
expect(screen.queryByText('y')).toBeDefined();
expect(screen.queryByText('width')).toBeDefined();
expect(screen.queryByText('height')).toBeDefined();
const propertyId = container.querySelector('#property-id');
const propertyParentId = container.querySelector('#property-parentId');
const propertyX = container.querySelector('#property-x');
const propertyY = container.querySelector('#property-y');
const propertyWidth = container.querySelector('#property-width');
const propertyHeight = container.querySelector('#property-height');
const propertyId = container.querySelector('#id');
const propertyParentId = container.querySelector('#parentId');
const propertyX = container.querySelector('#x');
const propertyY = container.querySelector('#y');
const propertyWidth = container.querySelector('#width');
const propertyHeight = container.querySelector('#height');
expect((propertyId as HTMLInputElement).value).toBe(MainContainer.properties.id.toString());
expect(propertyParentId).toBeDefined();
expect((propertyParentId as HTMLInputElement).value).toBe('');
@ -101,7 +105,8 @@ describe.concurrent('Elements sidebar', () => {
y: 0,
width: 2000,
height: 100,
isRigidBody: false
isRigidBody: false,
isAnchor: false
},
userData: {}
};
@ -117,7 +122,8 @@ describe.concurrent('Elements sidebar', () => {
y: 0,
width: 0,
height: 0,
isRigidBody: false
isRigidBody: false,
isAnchor: false
},
userData: {}
}
@ -134,7 +140,8 @@ describe.concurrent('Elements sidebar', () => {
y: 0,
width: 0,
height: 0,
isRigidBody: false
isRigidBody: false,
isAnchor: false
},
userData: {}
}
@ -146,6 +153,7 @@ describe.concurrent('Elements sidebar', () => {
isHistoryOpen={false}
SelectedContainer={MainContainer}
OnPropertyChange={() => {}}
OnPropertiesSubmit={() => {}}
SelectContainer={() => {}}
DeleteContainer={() => {}}
AddContainer={() => {}}
@ -170,7 +178,8 @@ describe.concurrent('Elements sidebar', () => {
y: 0,
width: 2000,
height: 100,
isRigidBody: false
isRigidBody: false,
isAnchor: false
},
userData: {}
};
@ -185,7 +194,8 @@ describe.concurrent('Elements sidebar', () => {
y: 0,
width: 0,
height: 0,
isRigidBody: false
isRigidBody: false,
isAnchor: false
},
userData: {}
};
@ -202,6 +212,7 @@ describe.concurrent('Elements sidebar', () => {
isHistoryOpen={false}
SelectedContainer={SelectedContainer}
OnPropertyChange={() => {}}
OnPropertiesSubmit={() => {}}
SelectContainer={selectContainer}
DeleteContainer={() => {}}
AddContainer={() => {}}
@ -212,8 +223,8 @@ describe.concurrent('Elements sidebar', () => {
expect(screen.getByText(/main/i));
const child1 = screen.getByText(/child-1/i);
expect(child1);
const propertyId = container.querySelector('#property-id');
const propertyParentId = container.querySelector('#property-parentId');
const propertyId = container.querySelector('#id');
const propertyParentId = container.querySelector('#parentId');
expect((propertyId as HTMLInputElement).value).toBe(MainContainer.properties.id.toString());
expect((propertyParentId as HTMLInputElement).value).toBe('');
@ -225,6 +236,7 @@ describe.concurrent('Elements sidebar', () => {
isHistoryOpen={false}
SelectedContainer={SelectedContainer}
OnPropertyChange={() => {}}
OnPropertiesSubmit={() => {}}
SelectContainer={selectContainer}
DeleteContainer={() => {}}
AddContainer={() => {}}

View file

@ -1,12 +1,13 @@
import * as React from 'react';
import { motion } from 'framer-motion';
import { FixedSizeList as List } from 'react-window';
import { Properties } from '../Properties/Properties';
import { IContainerModel } from '../../Interfaces/ContainerModel';
import ContainerProperties from '../../Interfaces/IProperties';
import { IContainerModel } from '../../Interfaces/IContainerModel';
import { getDepth, MakeIterator } from '../../utils/itertools';
import { Menu } from '../Menu/Menu';
import { MenuItem } from '../Menu/MenuItem';
import { handleDragLeave, handleDragOver, handleLeftClick, handleOnDrop, handleRightClick } from './MouseEventHandlers';
import { Point } from '../../Interfaces/Point';
import { IPoint } from '../../Interfaces/IPoint';
interface IElementsSidebarProps {
MainContainer: IContainerModel
@ -14,55 +15,17 @@ interface IElementsSidebarProps {
isHistoryOpen: boolean
SelectedContainer: IContainerModel | null
OnPropertyChange: (key: string, value: string | number | boolean) => void
OnPropertiesSubmit: (event: React.FormEvent<HTMLFormElement>, properties: ContainerProperties) => void
SelectContainer: (container: IContainerModel) => void
DeleteContainer: (containerid: string) => void
AddContainer: (index: number, type: string, parent: string) => void
}
function createRows(
container: IContainerModel,
props: IElementsSidebarProps,
containerRows: React.ReactNode[]
): void {
const depth: number = getDepth(container);
const key = container.properties.id.toString();
const text = '|\t'.repeat(depth) + key;
const selectedClass: string = props.SelectedContainer !== undefined &&
props.SelectedContainer !== null &&
props.SelectedContainer.properties.id === container.properties.id
? 'border-l-4 bg-slate-400/60 hover:bg-slate-400'
: 'bg-slate-300/60 hover:bg-slate-300';
containerRows.push(
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 1.2 }}
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{
duration: 0.150
}}
className={
`w-full border-blue-500 elements-sidebar-row whitespace-pre
text-left text-sm font-medium transition-all ${selectedClass}`
}
id={key}
key={key}
onDrop={(event) => handleOnDrop(event, props.MainContainer, props.AddContainer)}
onDragOver={(event) => handleDragOver(event, props.MainContainer)}
onDragLeave={(event) => handleDragLeave(event)}
onClick={() => props.SelectContainer(container)}
>
{ text }
</motion.button>
);
};
export const ElementsSidebar: React.FC<IElementsSidebarProps> = (props: IElementsSidebarProps): JSX.Element => {
// States
const [isContextMenuOpen, setIsContextMenuOpen] = React.useState<boolean>(false);
const [onClickContainerId, setOnClickContainerId] = React.useState<string>('');
const [contextMenuPosition, setContextMenuPosition] = React.useState<Point>({
const [contextMenuPosition, setContextMenuPosition] = React.useState<IPoint>({
x: 0,
y: 0
});
@ -105,7 +68,7 @@ export const ElementsSidebar: React.FC<IElementsSidebarProps> = (props: IElement
onLeftClick
);
};
}, []);
});
// Render
let isOpenClasses = '-right-64';
@ -115,24 +78,55 @@ export const ElementsSidebar: React.FC<IElementsSidebarProps> = (props: IElement
: 'right-0';
}
const containerRows: React.ReactNode[] = [];
const it = MakeIterator(props.MainContainer);
for (const container of it) {
createRows(
container,
props,
containerRows
);
}
const containers = [...it];
const Row = ({ index, style }: {index: number, style: React.CSSProperties}): JSX.Element => {
const container = containers[index];
const depth: number = getDepth(container);
const key = container.properties.id.toString();
const text = '|\t'.repeat(depth) + key;
const selectedClass: string = props.SelectedContainer !== undefined &&
props.SelectedContainer !== null &&
props.SelectedContainer.properties.id === container.properties.id
? 'border-l-4 bg-slate-400/60 hover:bg-slate-400'
: 'bg-slate-300/60 hover:bg-slate-300';
return (
<button
className={
`w-full border-blue-500 elements-sidebar-row whitespace-pre
text-left text-sm font-medium transition-all ${selectedClass}`
}
id={key}
key={key}
style={style}
onDrop={(event) => handleOnDrop(event, props.MainContainer, props.AddContainer)}
onDragOver={(event) => handleDragOver(event, props.MainContainer)}
onDragLeave={(event) => handleDragLeave(event)}
onClick={() => props.SelectContainer(container)}
>
{ text }
</button>
);
};
const ROW_HEIGHT = 35;
const NUMBERS_OF_ROWS = 10;
return (
<div className={`fixed flex flex-col bg-slate-100 text-gray-800 transition-all h-screen w-64 overflow-y-auto z-20 ${isOpenClasses}`}>
<div className={`fixed flex flex-col bg-slate-100 text-gray-800 transition-all h-full w-64 overflow-y-auto z-20 ${isOpenClasses}`}>
<div className='bg-slate-100 font-bold sidebar-title'>
Elements
</div>
<div ref={elementRef} className='overflow-y-auto overflow-x-hidden text-gray-800 flex-grow'>
{ containerRows }
<div ref={elementRef} className='h-96 text-gray-800'>
<List
className='List'
itemCount={containers.length}
itemSize={35}
height={384}
width={256}
>
{ Row }
</List>
</div>
<Menu
className='transition-opacity rounded bg-slate-200 py-1 drop-shadow-xl'
@ -145,7 +139,11 @@ export const ElementsSidebar: React.FC<IElementsSidebarProps> = (props: IElement
props.DeleteContainer(onClickContainerId);
}} />
</Menu>
<Properties properties={props.SelectedContainer?.properties} onChange={props.OnPropertyChange}></Properties>
<Properties
properties={props.SelectedContainer?.properties}
onChange={props.OnPropertyChange}
onSubmit={props.OnPropertiesSubmit}
/>
</div>
);
};

View file

@ -1,12 +1,12 @@
import { IContainerModel } from '../../Interfaces/ContainerModel';
import { Point } from '../../Interfaces/Point';
import { IContainerModel } from '../../Interfaces/IContainerModel';
import { IPoint } from '../../Interfaces/IPoint';
import { findContainerById } from '../../utils/itertools';
export function handleRightClick(
event: MouseEvent,
setIsContextMenuOpen: React.Dispatch<React.SetStateAction<boolean>>,
setOnClickContainerId: React.Dispatch<React.SetStateAction<string>>,
setContextMenuPosition: React.Dispatch<React.SetStateAction<Point>>
setContextMenuPosition: React.Dispatch<React.SetStateAction<IPoint>>
): void {
event.preventDefault();
@ -16,7 +16,7 @@ export function handleRightClick(
return;
}
const contextMenuPosition: Point = { x: event.pageX, y: event.pageY };
const contextMenuPosition: IPoint = { x: event.pageX, y: event.pageY };
setIsContextMenuOpen(true);
setOnClickContainerId(event.target.id);
setContextMenuPosition(contextMenuPosition);

View file

@ -1,8 +1,9 @@
import * as React from 'react';
import { HistoryState } from "../../Interfaces/HistoryState";
import { FixedSizeList as List } from 'react-window';
import { IHistoryState } from '../../Interfaces/IHistoryState';
interface IHistoryProps {
history: HistoryState[]
history: IHistoryState[]
historyCurrentStep: number
isOpen: boolean
jumpTo: (move: number) => void
@ -10,47 +11,45 @@ interface IHistoryProps {
export const History: React.FC<IHistoryProps> = (props: IHistoryProps) => {
const isOpenClasses = props.isOpen ? 'right-0' : '-right-64';
const Row = ({ index, style }: {index: number, style: React.CSSProperties}): JSX.Element => {
const reversedIndex = (props.history.length - 1) - index;
const step = props.history[reversedIndex];
const desc = step.LastAction;
const states = props.history.map((step, move) => {
const desc = move > 0
? `Go to modification n°${move}`
: 'Go to the beginning';
const isCurrent = move === props.historyCurrentStep;
const selectedClass = isCurrent
const selectedClass = reversedIndex === props.historyCurrentStep
? 'bg-blue-500 hover:bg-blue-600'
: 'bg-slate-500 hover:bg-slate-700';
const isCurrentText = isCurrent
? ' (current)'
: '';
return (
<button
key={move}
onClick={() => props.jumpTo(move)}
key={reversedIndex}
style={style}
onClick={() => props.jumpTo(reversedIndex)}
title={step.LastAction}
className={
`w-full elements-sidebar-row whitespace-pre
`w-full elements-sidebar-row whitespace-pre overflow-hidden
text-left text-sm font-medium transition-all ${selectedClass}`
}
>
{desc}{isCurrentText}
{desc}
</button>
);
});
// recent first
states.reverse();
};
return (
<div className={`fixed flex flex-col bg-slate-300 text-white transition-all h-screen w-64 overflow-y-auto z-20 ${isOpenClasses}`}>
<div className={`fixed flex flex-col bg-slate-300 text-white transition-all h-full w-64 overflow-y-auto z-20 ${isOpenClasses}`}>
<div className='bg-slate-600 font-bold sidebar-title'>
Timeline
</div>
<div className='overflow-y-auto overflow-x-hidden text-slate-300 flex-grow divide-y divide-solid divide-slate-600'>
{ states }
</div>
<List
className='List overflow-x-hidden'
itemCount={props.history.length}
itemSize={35}
height={window.innerHeight}
width={256}
>
{ Row }
</List>
</div>
);
};

View file

@ -38,13 +38,8 @@ export const MainMenu: React.FC<IMainMenuProps> = (props) => {
</form>
<button
onClick={() => setWindowState(WindowState.MAIN)}
className='block text-sm
mt-8 py-2 px-4
rounded-full border-0
font-semibold
transition-all
bg-blue-100 text-blue-700
hover:bg-blue-200'
className='normal-btn block
mt-8 '
>
Go back
</button>

View file

@ -8,6 +8,7 @@ describe.concurrent('Properties', () => {
render(<Properties
properties={undefined}
onChange={() => {}}
onSubmit={() => {}}
/>);
expect(screen.queryByText('id')).toBeNull();
@ -16,13 +17,14 @@ describe.concurrent('Properties', () => {
expect(screen.queryByText('y')).toBeNull();
});
it('Some properties', () => {
it('Some properties, change values with dynamic input', () => {
const prop = {
id: 'stuff',
parentId: 'parentId',
x: 1,
y: 1,
isRigidBody: false
isRigidBody: false,
isAnchor: false
};
const handleChange = vi.fn((key, value) => {
@ -32,6 +34,7 @@ describe.concurrent('Properties', () => {
const { container, rerender } = render(<Properties
properties={prop}
onChange={handleChange}
onSubmit={() => {}}
/>);
expect(screen.queryByText('id')).toBeDefined();
@ -39,10 +42,10 @@ describe.concurrent('Properties', () => {
expect(screen.queryByText('x')).toBeDefined();
expect(screen.queryByText('y')).toBeDefined();
let propertyId = container.querySelector('#property-id');
let propertyParentId = container.querySelector('#property-parentId');
let propertyX = container.querySelector('#property-x');
let propertyY = container.querySelector('#property-y');
let propertyId = container.querySelector('#id');
let propertyParentId = container.querySelector('#parentId');
let propertyX = container.querySelector('#x');
let propertyY = container.querySelector('#y');
expect(propertyId).toBeDefined();
expect((propertyId as HTMLInputElement).value).toBe('stuff');
expect(propertyParentId).toBeDefined();
@ -65,12 +68,13 @@ describe.concurrent('Properties', () => {
rerender(<Properties
properties={Object.assign({}, prop)}
onChange={handleChange}
onSubmit={() => {}}
/>);
propertyId = container.querySelector('#property-id');
propertyParentId = container.querySelector('#property-parentId');
propertyX = container.querySelector('#property-x');
propertyY = container.querySelector('#property-y');
propertyId = container.querySelector('#id');
propertyParentId = container.querySelector('#parentId');
propertyX = container.querySelector('#x');
propertyY = container.querySelector('#y');
expect(propertyId).toBeDefined();
expect((propertyId as HTMLInputElement).value).toBe('stuffed');
expect(propertyParentId).toBeDefined();

View file

@ -1,13 +1,17 @@
import * as React from 'react';
import ContainerProperties from '../../Interfaces/Properties';
import React, { useState } from 'react';
import ContainerProperties from '../../Interfaces/IProperties';
import { ToggleButton } from '../ToggleButton/ToggleButton';
import { INPUT_TYPES } from './PropertiesInputTypes';
interface IPropertiesProps {
properties?: ContainerProperties
onChange: (key: string, value: string | number | boolean) => void
onSubmit: (event: React.FormEvent<HTMLFormElement>, properties: ContainerProperties) => void
}
export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps) => {
const [isDynamicInput, setIsDynamicInput] = useState<boolean>(true);
if (props.properties === undefined) {
return <div></div>;
}
@ -15,11 +19,33 @@ export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps)
const groupInput: React.ReactNode[] = [];
Object
.entries(props.properties)
.forEach((pair) => handleProperties(pair, groupInput, props.onChange));
.forEach((pair) => handleProperties(pair, groupInput, isDynamicInput, props.onChange));
const form = isDynamicInput
? <div className='grid grid-cols-2 gap-4'>
{ groupInput }
</div>
: <form
key={props.properties.id}
onSubmit={(event) => props.onSubmit(event, props.properties as ContainerProperties)}
>
<input type='submit' className='normal-btn block mx-auto mb-4 border-2 border-blue-400 cursor-pointer' value='Submit'/>
<div className='grid grid-cols-2 gap-y-4'>
{ groupInput }
</div>
</form>
;
return (
<div className='p-3 bg-slate-200 h-3/5 overflow-y-auto'>
{ groupInput }
<div className='h-3/5 p-3 bg-slate-200 overflow-y-auto'>
<ToggleButton
id='isDynamic'
text='Dynamic update'
title='Enable dynamic svg update'
checked={isDynamicInput}
onChange={() => setIsDynamicInput(!isDynamicInput)}
/>
{ form }
</div>
);
};
@ -27,6 +53,7 @@ export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps)
const handleProperties = (
[key, value]: [string, string | number],
groupInput: React.ReactNode[],
isDynamicInput: boolean,
onChange: (key: string, value: string | number | boolean) => void
): void => {
const id = `property-${key}`;
@ -42,31 +69,48 @@ const handleProperties = (
type = INPUT_TYPES[key];
}
const className = `
w-full
text-xs font-medium transition-all text-gray-800 mt-1 px-3 py-2
bg-white border-2 border-white rounded-lg placeholder-gray-800
focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500
disabled:bg-slate-300 disabled:text-gray-500 disabled:border-slate-300 disabled:shadow-none`;
const isDisabled = ['id', 'parentId'].includes(key);
///
const input = isDynamicInput
? <input
key={key}
id={key}
className={className}
type={type}
value={value}
checked={checked}
onChange={(event) => {
if (type === 'checkbox') {
onChange(key, event.target.checked);
return;
}
onChange(key, event.target.value);
}}
disabled={isDisabled}
/>
: <input
key={key}
id={key}
className={className}
type={type}
defaultValue={value}
defaultChecked={checked}
disabled={isDisabled}
/>;
groupInput.push(
<div key={id} className='mt-4'>
<label className='text-sm font-medium text-gray-800' htmlFor={id}>{key}</label>
<input
className='text-base font-medium transition-all text-gray-800 mt-1 block w-full px-3 py-2
bg-white border-2 border-white rounded-lg placeholder-gray-800
focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500
disabled:bg-slate-300 disabled:text-gray-500 disabled:border-slate-300 disabled:shadow-none
'
type={type}
id={id}
value={value}
checked={checked}
onChange={(event) => {
if (type === 'checkbox') {
onChange(key, event.target.checked);
return;
}
onChange(key, event.target.value);
}}
disabled={isDisabled}
/>
</div>
<label
key={id}
className='mt-4 text-xs font-medium text-gray-800'
htmlFor={key}
>
{key}
</label>
);
groupInput.push(input);
};

View file

@ -3,5 +3,6 @@ export const INPUT_TYPES: Record<string, string> = {
y: 'number',
width: 'number',
height: 'number',
isRigidBody: 'checkbox'
isRigidBody: 'checkbox',
isAnchor: 'checkbox'
};

View file

@ -1,9 +1,10 @@
import * as React from 'react';
import { IContainerModel } from '../../../Interfaces/ContainerModel';
import { XPositionReference } from '../../../Enums/XPositionReference';
import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { getDepth } from '../../../utils/itertools';
import { Dimension } from './Dimension';
export interface IContainerProps {
interface IContainerProps {
model: IContainerModel
}
@ -17,7 +18,14 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
const containersElements = props.model.children.map(child => <Container key={`container-${child.properties.id}`} model={child} />);
const xText = Number(props.model.properties.width) / 2;
const yText = Number(props.model.properties.height) / 2;
const transform = `translate(${Number(props.model.properties.x)}, ${Number(props.model.properties.y)})`;
const [transformedX, transformedY] = transformPosition(
Number(props.model.properties.x),
Number(props.model.properties.y),
Number(props.model.properties.width),
props.model.properties.XPositionReference
);
const transform = `translate(${transformedX}, ${transformedY})`;
// g style
const defaultStyle: React.CSSProperties = {
@ -54,7 +62,8 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
id={id}
xStart={xStart}
xEnd={xEnd}
y={y}
yStart={y}
yEnd={y}
strokeWidth={strokeWidth}
text={text}
/>
@ -74,3 +83,13 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
</g>
);
};
function transformPosition(x: number, y: number, width: number, xPositionReference = XPositionReference.Left): [number, number] {
let transformedX = x;
if (xPositionReference === XPositionReference.Center) {
transformedX -= width / 2;
} else if (xPositionReference === XPositionReference.Right) {
transformedX -= width;
}
return [transformedX, y];
}

View file

@ -1,47 +1,83 @@
import * as React from 'react';
import { NOTCHES_LENGTH } from '../../../utils/default';
interface IDimensionProps {
id: string
xStart: number
yStart: number
xEnd: number
y: number
yEnd: number
text: string
strokeWidth: number
}
/**
* 2D Parametric function. Returns a new coordinate from the origin coordinate
* See for more details https://en.wikipedia.org/wiki/Parametric_equation.
* TL;DR a parametric function is a function with a parameter
* @param x0 Origin coordinate
* @param t The parameter
* @param vx Transform vector
* @returns Returns a new coordinate from the origin coordinate
*/
const applyParametric = (x0: number, t: number, vx: number): number => x0 + t * vx;
export const Dimension: React.FC<IDimensionProps> = (props: IDimensionProps) => {
const style: React.CSSProperties = {
stroke: 'black'
};
/// We need to find the points of the notches
// Get the vector of the line
const [deltaX, deltaY] = [(props.xEnd - props.xStart), (props.yEnd - props.yStart)];
// Get the unit vector
const norm = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
const [unitX, unitY] = [deltaX / norm, deltaY / norm];
// Get the perpandicular vector
const [perpVecX, perpVecY] = [unitY, -unitX];
// Use the parametric function to get the coordinates (x = x0 + t * v.x)
const startTopX = applyParametric(props.xStart, NOTCHES_LENGTH, perpVecX);
const startTopY = applyParametric(props.yStart, NOTCHES_LENGTH, perpVecY);
const startBottomX = applyParametric(props.xStart, -NOTCHES_LENGTH, perpVecX);
const startBottomY = applyParametric(props.yStart, -NOTCHES_LENGTH, perpVecY);
const endTopX = applyParametric(props.xEnd, NOTCHES_LENGTH, perpVecX);
const endTopY = applyParametric(props.yEnd, NOTCHES_LENGTH, perpVecY);
const endBottomX = applyParametric(props.xEnd, -NOTCHES_LENGTH, perpVecX);
const endBottomY = applyParametric(props.yEnd, -NOTCHES_LENGTH, perpVecY);
return (
<g key={props.id}>
<line
x1={props.xStart}
y1={props.y - 4 * props.strokeWidth}
x2={props.xStart}
y2={props.y + 4 * props.strokeWidth}
x1={startTopX}
y1={startTopY}
x2={startBottomX}
y2={startBottomY}
strokeWidth={props.strokeWidth}
style={style}
/>
<line
x1={props.xStart}
y1={props.y}
y1={props.yStart}
x2={props.xEnd}
y2={props.y}
y2={props.yEnd}
strokeWidth={props.strokeWidth}
style={style}
/>
<line
x1={props.xEnd}
y1={props.y - 4 * props.strokeWidth}
x2={props.xEnd}
y2={props.y + 4 * props.strokeWidth}
x1={endTopX}
y1={endTopY}
x2={endBottomX}
y2={endBottomY}
strokeWidth={props.strokeWidth}
style={style}
/>
<text
x={(props.xStart + props.xEnd) / 2}
y={props.y}
y={props.yStart}
>
{props.text}
</text>

View file

@ -1,66 +0,0 @@
import * as React from 'react';
import { ContainerModel } from '../../../Interfaces/ContainerModel';
import { getDepth, MakeIterator } from '../../../utils/itertools';
import { Dimension } from './Dimension';
interface IDimensionLayerProps {
isHidden: boolean
roots: ContainerModel | ContainerModel[] | null
}
const GAP: number = 50;
const getDimensionsNodes = (root: ContainerModel): React.ReactNode[] => {
const it = MakeIterator(root);
const dimensions: React.ReactNode[] = [];
for (const container of it) {
// WARN: this might be dangerous later when using other units/rules
const width = Number(container.properties.width);
const id = `dim-${container.properties.id}`;
const xStart: number = container.properties.x;
const xEnd = xStart + width;
const y = -(GAP * (getDepth(container) + 1));
const strokeWidth = 1;
const text = width.toString();
dimensions.push(
<Dimension
id={id}
xStart={xStart}
xEnd={xEnd}
y={y}
strokeWidth={strokeWidth}
text={text}
/>
);
}
return dimensions;
};
/**
* A layer containing all dimension
*
* @deprecated In order to avoid adding complexity
* with computing the position in a group hierarchy,
* use Dimension directly inside the Container,
* Currently it is glitched as
* it does not take parents into account,
* and will not work correctly
* @param props
* @returns
*/
export const DimensionLayer: React.FC<IDimensionLayerProps> = (props: IDimensionLayerProps) => {
let dimensions: React.ReactNode[] = [];
if (Array.isArray(props.roots)) {
props.roots.forEach(child => {
dimensions.concat(getDimensionsNodes(child));
});
} else if (props.roots !== null) {
dimensions = getDimensionsNodes(props.roots);
}
return (
<g visibility={props.isHidden ? 'hidden' : 'visible'}>
{ dimensions }
</g>
);
};

View file

@ -1,5 +1,5 @@
import * as React from 'react';
import { IContainerModel } from '../../../Interfaces/ContainerModel';
import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { getAbsolutePosition } from '../../../utils/itertools';
interface ISelectorProps {

View file

@ -1,7 +1,7 @@
import * as React from 'react';
import { UncontrolledReactSVGPanZoom } from 'react-svg-pan-zoom';
import { Container } from './Elements/Container';
import { ContainerModel } from '../../Interfaces/ContainerModel';
import { ContainerModel } from '../../Interfaces/IContainerModel';
import { Selector } from './Elements/Selector';
import { BAR_WIDTH } from '../Bar/Bar';
@ -30,7 +30,7 @@ function resizeViewBox(
export const SVG: React.FC<ISVGProps> = (props: ISVGProps) => {
const [viewer, setViewer] = React.useState<Viewer>({
viewerWidth: window.innerWidth,
viewerWidth: window.innerWidth - BAR_WIDTH,
viewerHeight: window.innerHeight
});

View file

@ -1,9 +1,9 @@
import * as React from 'react';
import { AvailableContainer } from '../../Interfaces/AvailableContainer';
import { IAvailableContainer } from '../../Interfaces/IAvailableContainer';
import { truncateString } from '../../utils/stringtools';
interface ISidebarProps {
componentOptions: AvailableContainer[]
componentOptions: IAvailableContainer[]
isOpen: boolean
buttonOnClick: (type: string) => void
}
@ -30,7 +30,7 @@ export const Sidebar: React.FC<ISidebarProps> = (props: ISidebarProps) => {
const isOpenClasses = props.isOpen ? 'left-16' : '-left-64';
return (
<div className={`fixed z-10 bg-slate-200
text-gray-700 transition-all h-screen w-64
text-gray-700 transition-all h-full w-64
overflow-y-auto ${isOpenClasses}`}>
<div className='bg-slate-100 sidebar-title'>
Components

View file

@ -0,0 +1,8 @@
input:checked ~ .dot {
transform: translateX(100%);
}
input:checked ~ .line {
background-color: #3B82F6;
}

View file

@ -0,0 +1,52 @@
import React, { FC } from 'react';
import './ToggleButton.scss';
interface IToggleButtonProps {
id: string
text: string
type?: TOGGLE_TYPE
title: string
checked: boolean
onChange: React.ChangeEventHandler<HTMLInputElement>
}
export enum TOGGLE_TYPE {
MATERIAL,
IOS
}
export const ToggleButton: FC<IToggleButtonProps> = (props) => {
const id = `toggle-${props.id}`;
const type = props.type ?? TOGGLE_TYPE.MATERIAL;
let classLine = 'line w-10 h-4 bg-gray-400 rounded-full shadow-inner';
let classDot = 'dot absolute w-6 h-6 bg-white rounded-full shadow -left-1 -top-1 transition';
if (type === TOGGLE_TYPE.IOS) {
classLine = 'line block bg-gray-600 w-14 h-8 rounded-full';
classDot = 'dot absolute left-1 top-1 bg-white w-6 h-6 rounded-full transition';
}
return (
<div title={props.title}>
<div className="flex items-center justify-center w-full mb-12">
<label
htmlFor={id}
className="flex items-center cursor-pointer"
>
<div className="relative">
<input
id={id}
type="checkbox"
onChange={props.onChange}
checked={props.checked}
className="sr-only" />
<div className={classLine}></div>
<div className={classDot}></div>
</div>
<div className="ml-3 text-gray-700 font-medium">
{ props.text }
</div>
</label>
</div>
</div>
);
};

View file

@ -2,21 +2,23 @@ import * as React from 'react';
import { ElementsSidebar } from '../ElementsSidebar/ElementsSidebar';
import { Sidebar } from '../Sidebar/Sidebar';
import { History } from '../History/History';
import { AvailableContainer } from '../../Interfaces/AvailableContainer';
import { ContainerModel } from '../../Interfaces/ContainerModel';
import { HistoryState } from '../../Interfaces/HistoryState';
import { IAvailableContainer } from '../../Interfaces/IAvailableContainer';
import { ContainerModel } from '../../Interfaces/IContainerModel';
import { IHistoryState } from '../../Interfaces/IHistoryState';
import { PhotographIcon, UploadIcon } from '@heroicons/react/outline';
import { FloatingButton } from '../FloatingButton/FloatingButton';
import { Bar } from '../Bar/Bar';
import IProperties from '../../Interfaces/IProperties';
interface IUIProps {
current: HistoryState
history: HistoryState[]
current: IHistoryState
history: IHistoryState[]
historyCurrentStep: number
AvailableContainers: AvailableContainer[]
AvailableContainers: IAvailableContainer[]
SelectContainer: (container: ContainerModel) => void
DeleteContainer: (containerId: string) => void
OnPropertyChange: (key: string, value: string | number | boolean) => void
OnPropertiesSubmit: (event: React.FormEvent<HTMLFormElement>, properties: IProperties) => void
AddContainerToSelectedContainer: (type: string) => void
AddContainer: (index: number, type: string, parentId: string) => void
SaveEditorAsJSON: () => void
@ -59,6 +61,7 @@ export const UI: React.FunctionComponent<IUIProps> = (props: IUIProps) => {
isOpen={isElementsSidebarOpen}
isHistoryOpen={isHistoryOpen}
OnPropertyChange={props.OnPropertyChange}
OnPropertiesSubmit={props.OnPropertiesSubmit}
SelectContainer={props.SelectContainer}
DeleteContainer={props.DeleteContainer}
AddContainer={props.AddContainer}

View file

@ -1,4 +0,0 @@
export enum AddingBehavior {
InsertInto,
Replace
}

View file

@ -0,0 +1,26 @@
import { IEditorState } from '../Interfaces/IEditorState';
import { IHistoryState } from '../Interfaces/IHistoryState';
const getEditorState = (editorState: IEditorState): void => {
const customEvent = new CustomEvent<IEditorState>('getEditorState', { detail: editorState });
document.dispatchEvent(customEvent);
};
const getCurrentHistoryState = (editorState: IEditorState): void => {
const customEvent = new CustomEvent<IHistoryState>(
'getCurrentHistoryState',
{ detail: editorState.history[editorState.historyCurrentStep] });
document.dispatchEvent(customEvent);
};
export interface IEditorEvent {
name: string
func: (editorState: IEditorState) => void
}
const events: IEditorEvent[] = [
{ name: 'getEditorState', func: getEditorState },
{ name: 'getCurrentHistoryState', func: getCurrentHistoryState }
];
export default events;

View file

@ -1,9 +0,0 @@
import { AvailableContainer } from './AvailableContainer';
import { AvailableSymbolModel } from './AvailableSymbol';
/** Model of configuration for the application to configure it */
export interface Configuration {
AvailableContainers: AvailableContainer[]
AvailableSymbols: AvailableSymbolModel[]
MainContainer: AvailableContainer
}

View file

@ -1,9 +1,11 @@
import React from 'react';
import { XPositionReference } from '../Enums/XPositionReference';
/** Model of available container used in application configuration */
export interface AvailableContainer {
export interface IAvailableContainer {
Type: string
Width: number
Height: number
XPositionReference?: XPositionReference
Style: React.CSSProperties
}

View file

@ -1,12 +1,12 @@
import { XPositionReference } from '../Enums/XPositionReference';
import { Image } from './Image';
import { IImage } from './IImage';
/**
* Model of available symbol to configure the application */
export interface AvailableSymbolModel {
export interface IAvailableSymbol {
Name: string
XPositionReference: XPositionReference
Image: Image
Image: IImage
Width: number
Height: number
}

View file

@ -0,0 +1,9 @@
import { IAvailableContainer } from './IAvailableContainer';
import { IAvailableSymbol } from './IAvailableSymbol';
/** Model of configuration for the application to configure it */
export interface IConfiguration {
AvailableContainers: IAvailableContainer[]
AvailableSymbols: IAvailableSymbol[]
MainContainer: IAvailableContainer
}

View file

@ -1,21 +1,21 @@
import Properties from './Properties';
import IProperties from './IProperties';
export interface IContainerModel {
children: IContainerModel[]
parent: IContainerModel | null
properties: Properties
properties: IProperties
userData: Record<string, string | number>
}
export class ContainerModel implements IContainerModel {
public children: IContainerModel[];
public parent: IContainerModel | null;
public properties: Properties;
public properties: IProperties;
public userData: Record<string, string | number>;
constructor(
parent: IContainerModel | null,
properties: Properties,
properties: IProperties,
children: IContainerModel[] = [],
userData = {}) {
this.parent = parent;

View file

@ -0,0 +1,8 @@
import { IConfiguration } from './IConfiguration';
import { IHistoryState } from './IHistoryState';
export interface IEditorState {
history: IHistoryState[]
historyCurrentStep: number
configuration: IConfiguration
}

View file

@ -1,6 +1,6 @@
import { IContainerModel } from './ContainerModel';
import { IContainerModel } from './IContainerModel';
export interface HistoryState {
export interface IHistoryState {
LastAction: string
MainContainer: IContainerModel
SelectedContainer: IContainerModel | null

View file

@ -1,5 +1,5 @@
/** Model of an image with multiple source */
export interface Image {
export interface IImage {
Name: string
Url: string
Base64Image: string

View file

@ -1,4 +1,4 @@
export interface Point {
export interface IPoint {
x: number
y: number
}

View file

@ -0,0 +1,21 @@
import * as React from 'react';
import { XPositionReference } from '../Enums/XPositionReference';
/**
* Properties of a container
* @property id id of the container
* @property parentId id of the parent container
* @property x horizontal offset of the container
* @property y vertical offset of the container
* @property isRigidBody if true apply rigid body behaviors
* @property isAnchor if true apply anchor behaviors
*/
export default interface IProperties extends React.CSSProperties {
id: string
parentId: string | null
x: number
y: number
isRigidBody: boolean
isAnchor: boolean
XPositionReference?: XPositionReference
}

View file

@ -0,0 +1,9 @@
/**
* A SizePointer is a pointer in a 1 dimensional array of width/space
* x being the address where the pointer is pointing
* width being the overall (un)allocated space affected to the address
*/
export interface ISizePointer {
x: number
width: number
}

View file

@ -1,9 +0,0 @@
import * as React from 'react';
export default interface Properties extends React.CSSProperties {
id: string
parentId: string | null
x: number
y: number
isRigidBody: boolean
}

View file

@ -1,4 +0,0 @@
export interface SizePointer {
x: number
width: number
}

View file

@ -23,6 +23,16 @@
@apply transition-all bg-blue-100 hover:bg-blue-200 text-blue-700 text-lg font-semibold p-8 rounded-lg
}
.normal-btn {
@apply text-sm
py-2 px-4
rounded-full border-0
font-semibold
transition-all
bg-blue-100 text-blue-700
hover:bg-blue-200
}
.floating-btn {
@apply h-full w-full text-white align-middle items-center justify-center
}

View file

@ -1,7 +1,7 @@
import { Configuration } from '../Interfaces/Configuration';
import Properties from '../Interfaces/Properties';
import { IConfiguration } from '../Interfaces/IConfiguration';
import IProperties from '../Interfaces/IProperties';
export const DEFAULT_CONFIG: Configuration = {
export const DEFAULT_CONFIG: IConfiguration = {
AvailableContainers: [
{
Type: 'Container',
@ -25,14 +25,19 @@ export const DEFAULT_CONFIG: Configuration = {
}
};
export const DEFAULT_MAINCONTAINER_PROPS: Properties = {
export const DEFAULT_MAINCONTAINER_PROPS: IProperties = {
id: 'main',
parentId: 'null',
x: 0,
y: 0,
isRigidBody: false,
width: DEFAULT_CONFIG.MainContainer.Width,
height: DEFAULT_CONFIG.MainContainer.Height,
isRigidBody: false,
isAnchor: false,
fillOpacity: 0,
stroke: 'black'
};
export const NOTCHES_LENGTH = 4;
export const MAX_HISTORY = 200;

View file

@ -1,4 +1,4 @@
import { IContainerModel } from '../Interfaces/ContainerModel';
import { IContainerModel } from '../Interfaces/IContainerModel';
/**
* Returns a Generator iterating of over the children depth-first

View file

@ -1,5 +1,5 @@
import { findContainerById, MakeIterator } from './itertools';
import { IEditorState } from '../Components/Editor/Editor';
import { IEditorState } from '../Interfaces/IEditorState';
/**
* Revive the Editor state

25
src/workers/worker.js Normal file
View file

@ -0,0 +1,25 @@
onmessage = (e) => {
const data = JSON.stringify(e.data.editorState, getCircularReplacer(), e.data.spaces);
postMessage(data);
};
const getCircularReplacer = () => {
const seen = new WeakSet();
return (key, value) => {
if (key === 'parent') {
return;
}
if (key === 'SelectedContainer') {
return;
}
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
};

View file

@ -76,9 +76,6 @@ const GetSVGLayoutConfiguration = () => {
fillOpacity: 0,
borderWidth: 2,
stroke: 'blue',
transform: 'translateX(-50%)',
transformOrigin: 'center',
transformBox: 'fill-box'
}
}
],

View file

@ -16,7 +16,7 @@
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"],
"include": ["src", "src/workers"],
"exclude": ["test-server"],
"references": [{ "path": "./tsconfig.node.json" }]
}