diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 748a5bb..7f605ec 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -19,7 +19,8 @@ module.exports = { }, ecmaVersion: 'latest', sourceType: 'module', - project: './tsconfig.json' + project: './tsconfig.json', + tsconfigRootDir: __dirname }, plugins: [ 'only-warn', @@ -31,6 +32,7 @@ module.exports = { 'prefer-arrow-callback': 'error', 'func-style': ['error', 'declaration'], 'space-before-function-paren': ['error', 'never'], + 'max-len': ['error', { 'code': 120 }], // Import/export 'import/no-default-export': 'error', diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 446b61f..0ee06e3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,20 @@ You will be able to navigate through this document with the table of contents. - [Table of contents](#table-of-contents) - [I want to contribute](#i-want-to-contribute) - [I want to contribute to the .NETFramework API](#i-want-to-contribute-to-the-netframework-api) + - [Getting Started](#getting-started) + - [Before developing](#before-developing) + - [Testing](#testing) + - [Releasing](#releasing) - [I want to contribute to the React component](#i-want-to-contribute-to-the-react-component) + - [Getting Started](#getting-started-1) + - [Before developing](#before-developing-1) + - [CORS](#cors) + - [Develop with Vite and pnpm](#develop-with-vite-and-pnpm) + - [Develop with mprocs](#develop-with-mprocs) + - [Testing the external API without .NETFramework or Windows](#testing-the-external-api-without-netframework-or-windows) + - [Setup debugging with chrome](#setup-debugging-with-chrome) + - [Testing](#testing-1) + - [Releasing](#releasing-1) - [I want to report a bug](#i-want-to-report-a-bug) - [Before submitting a bug report](#before-submitting-a-bug-report) - [How do i submit a good bug report?](#how-do-i-submit-a-good-bug-report) @@ -102,6 +115,14 @@ Then run the following command to run the projet in a dev environment: pnpm dev ``` +### Develop with mprocs + +[Mprocs](https://github.com/pvolok/mprocs) runs multiple commands in parallel and shows output of each command separately. + +It is useful to run `vite` and the test server at the same time with `mprocs`. + +Run `pnpm d` or `pnpm mprocs` to run mprocs. + ### Testing the external API without .NETFramework or Windows Use the Node.js server in `/test-server` to simulate the api. diff --git a/csharp/SVGLDLibs/SVGLDLibs/Models/DimensionOptions.cs b/csharp/SVGLDLibs/SVGLDLibs/Models/DimensionOptions.cs index a98e500..10c6542 100644 --- a/csharp/SVGLDLibs/SVGLDLibs/Models/DimensionOptions.cs +++ b/csharp/SVGLDLibs/SVGLDLibs/Models/DimensionOptions.cs @@ -1,28 +1,31 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; -using System.Text; -using System.Threading.Tasks; - -namespace SVGLDLibs.Models -{ - - [DataContract] - public class DimensionOptions - { - - /** positions of the dimension */ - [DataMember(EmitDefaultValue = false)] - public Position[] positions; - - /** color */ - [DataMember(EmitDefaultValue = false)] - public string color; - - - - - } - -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +namespace SVGLDLibs.Models +{ + + [DataContract] + public class DimensionOptions + { + + /** positions of the dimension */ + [DataMember(EmitDefaultValue = false)] + public Position[] positions; + + /** color */ + [DataMember(EmitDefaultValue = false)] + public string color; + + /** width */ + [DataMember(EmitDefaultValue = false)] + public double width; + + /** color */ + [DataMember(EmitDefaultValue = false)] + public string dashArray; + } +} diff --git a/mprocs.yaml b/mprocs.yaml new file mode 100644 index 0000000..fa0511f --- /dev/null +++ b/mprocs.yaml @@ -0,0 +1,7 @@ +procs: + nvim: + shell: "nvim ." + vite: + shell: "npx vite" + test-server: + shell: "npx nodemon ./test-server/http.js" diff --git a/package.json b/package.json index 8fe114a..8804ff9 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,12 @@ "private": true, "version": "v1.0.0", "type": "module", + "postinstall": "npx patch-package", "scripts": { + "d": "mprocs", "dev": "vite", "build": "tsc && vite build", "build:dotnet": "dotnet build ./csharp/SVGLDLibs/SVGLDLibs/SVGLDLibs.csproj", - "preview": "vite preview", "linter": "eslint src", "test": "vitest", "test:ui": "vitest --ui", @@ -22,10 +23,11 @@ "interweave": "^13.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-svg-pan-zoom": "^3.11.0", + "react-svg-pan-zoom": "^3.12.1", "react-window": "^1.8.8", "sweetalert2": "^11.7.1", - "sweetalert2-react-content": "^5.0.7" + "sweetalert2-react-content": "^5.0.7", + "transformation-matrix": "^2.14.0" }, "devDependencies": { "@testing-library/dom": "^8.20.0", @@ -52,11 +54,18 @@ "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", "jsdom": "^21.1.0", + "mprocs": "^0.6.4", + "nodemon": "^2.0.20", "postcss": "^8.4.21", "sass": "^1.58.0", "tailwindcss": "^3.2.4", "typescript": "^4.9.5", "vite": "^4.1.1", "vitest": "^0.28.4" + }, + "pnpm": { + "patchedDependencies": { + "@types/react-svg-pan-zoom@3.3.5": "patches/@types__react-svg-pan-zoom@3.3.5.patch" + } } } diff --git a/patches/@types+react-svg-pan-zoom+3.3.5.patch b/patches/@types+react-svg-pan-zoom+3.3.5.patch new file mode 100644 index 0000000..b7fc8cc --- /dev/null +++ b/patches/@types+react-svg-pan-zoom+3.3.5.patch @@ -0,0 +1,17 @@ +diff --git a/node_modules/@types/react-svg-pan-zoom/index.d.ts b/node_modules/@types/react-svg-pan-zoom/index.d.ts +index a57d545..83ace9f 100644 +--- a/node_modules/@types/react-svg-pan-zoom/index.d.ts ++++ b/node_modules/@types/react-svg-pan-zoom/index.d.ts +@@ -256,7 +256,11 @@ export function zoom(value: Value, SVGPointX: number, SVGPointY: number, scaleFa + export function fitSelection( + value: Value, selectionSVGPointX: number, selectionSVGPointY: number, selectionWidth: number, selectionHeight: number): Value; + +-export function fitToViewer(value: Value): Value; ++export function fitToViewer( ++ value: Value, ++ SVGAlignX?: typeof ALIGN_CENTER | typeof ALIGN_LEFT | typeof ALIGN_RIGHT | undefined, ++ SVGAlignY?: typeof ALIGN_CENTER | typeof ALIGN_TOP | typeof ALIGN_BOTTOM | undefined ++): Value; + + export function zoomOnViewerCenter(value: Value, scaleFactor: number): Value; + diff --git a/patches/@types__react-svg-pan-zoom@3.3.5.patch b/patches/@types__react-svg-pan-zoom@3.3.5.patch new file mode 100644 index 0000000..506b048 --- /dev/null +++ b/patches/@types__react-svg-pan-zoom@3.3.5.patch @@ -0,0 +1,17 @@ +diff --git a/index.d.ts b/index.d.ts +index a57d545d33b2798024b9762d3d3513e58a38e19d..83ace9fc85b7354e128948402a50e00083eacd8c 100644 +--- a/index.d.ts ++++ b/index.d.ts +@@ -256,7 +256,11 @@ export function zoom(value: Value, SVGPointX: number, SVGPointY: number, scaleFa + export function fitSelection( + value: Value, selectionSVGPointX: number, selectionSVGPointY: number, selectionWidth: number, selectionHeight: number): Value; + +-export function fitToViewer(value: Value): Value; ++export function fitToViewer( ++ value: Value, ++ SVGAlignX?: typeof ALIGN_CENTER | typeof ALIGN_LEFT | typeof ALIGN_RIGHT | undefined, ++ SVGAlignY?: typeof ALIGN_CENTER | typeof ALIGN_TOP | typeof ALIGN_BOTTOM | undefined ++): Value; + + export function zoomOnViewerCenter(value: Value, scaleFactor: number): Value; + \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc7fbe9..8607471 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,5 +1,10 @@ lockfileVersion: 5.4 +patchedDependencies: + '@types/react-svg-pan-zoom@3.3.5': + hash: kv3ctd73j5hnzcxdc2ceiq5wuy + path: patches/@types__react-svg-pan-zoom@3.3.5.patch + specifiers: '@heroicons/react': ^2.0.14 '@react-hook/size': ^2.1.2 @@ -28,15 +33,18 @@ specifiers: eslint-plugin-react-hooks: ^4.6.0 interweave: ^13.0.0 jsdom: ^21.1.0 + mprocs: ^0.6.4 + nodemon: ^2.0.20 postcss: ^8.4.21 react: ^18.2.0 react-dom: ^18.2.0 - react-svg-pan-zoom: ^3.11.0 + react-svg-pan-zoom: ^3.12.1 react-window: ^1.8.8 sass: ^1.58.0 sweetalert2: ^11.7.1 sweetalert2-react-content: ^5.0.7 tailwindcss: ^3.2.4 + transformation-matrix: ^2.14.0 typescript: ^4.9.5 vite: ^4.1.1 vitest: ^0.28.4 @@ -47,10 +55,11 @@ dependencies: interweave: 13.0.0_react@18.2.0 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-svg-pan-zoom: 3.12.1 react-window: 1.8.8_biqbaboplfbrettd7655fr4n2y sweetalert2: 11.7.1 sweetalert2-react-content: 5.0.7_5cbezu6w3tvev2ldv5vdmnpfca + transformation-matrix: 2.14.0 devDependencies: '@testing-library/dom': 8.20.0 @@ -59,7 +68,7 @@ devDependencies: '@testing-library/user-event': 14.4.3_yxlyej73nftwmh2fiao7paxmlm '@types/react': 18.0.27 '@types/react-dom': 18.0.10 - '@types/react-svg-pan-zoom': 3.3.5 + '@types/react-svg-pan-zoom': 3.3.5_kv3ctd73j5hnzcxdc2ceiq5wuy '@types/react-window': 1.8.5 '@typescript-eslint/eslint-plugin': 5.51.0_b635kmla6dsb4frxfihkw4m47e '@typescript-eslint/parser': 5.51.0_4vsywjlpuriuw3tl5oq6zy5a64 @@ -77,6 +86,8 @@ devDependencies: eslint-plugin-react: 7.32.2_eslint@8.33.0 eslint-plugin-react-hooks: 4.6.0_eslint@8.33.0 jsdom: 21.1.0 + mprocs: 0.6.4 + nodemon: 2.0.20 postcss: 8.4.21 sass: 1.58.0 tailwindcss: 3.2.4_postcss@8.4.21 @@ -923,11 +934,12 @@ packages: '@types/react': 18.0.27 dev: true - /@types/react-svg-pan-zoom/3.3.5: + /@types/react-svg-pan-zoom/3.3.5_kv3ctd73j5hnzcxdc2ceiq5wuy: resolution: {integrity: sha512-W8GRFCDy7raSDr5OXGjSyvX5KmdWlIQfv0NLa1jfAYVUO4ClVbgorWeAAom7nY3Pl+4h9blXE1Bnu2CW1iMEvQ==} dependencies: '@types/react': 18.0.27 dev: true + patched: true /@types/react-window/1.8.5: resolution: {integrity: sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==} @@ -1172,6 +1184,10 @@ packages: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} dev: true + /abbrev/1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + dev: true + /acorn-globals/7.0.1: resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} dependencies: @@ -1600,6 +1616,18 @@ packages: ms: 2.1.3 dev: true + /debug/3.2.7_supports-color@5.5.0: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + supports-color: 5.5.0 + dev: true + /debug/4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -2507,6 +2535,10 @@ packages: safer-buffer: 2.1.2 dev: true + /ignore-by-default/1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + dev: true + /ignore/5.2.4: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} @@ -3010,6 +3042,12 @@ packages: ufo: 1.0.1 dev: true + /mprocs/0.6.4: + resolution: {integrity: sha512-Y4eqnAjp3mjy0eT+zPoMQ+P/ISOzjgRG/4kh4I5cRA4Tv0rPxTCBRadn3+j+boMF5id7IoLhrVq9NFWFPuzD9A==} + engines: {node: '>=0.10.0'} + hasBin: true + dev: true + /mrmime/1.0.1: resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} engines: {node: '>=10'} @@ -3041,6 +3079,30 @@ packages: resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==} dev: true + /nodemon/2.0.20: + resolution: {integrity: sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==} + engines: {node: '>=8.10.0'} + hasBin: true + dependencies: + chokidar: 3.5.3 + debug: 3.2.7_supports-color@5.5.0 + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 5.7.1 + simple-update-notifier: 1.1.0 + supports-color: 5.5.0 + touch: 3.1.0 + undefsafe: 2.0.5 + dev: true + + /nopt/1.0.10: + resolution: {integrity: sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==} + hasBin: true + dependencies: + abbrev: 1.1.1 + dev: true + /normalize-path/3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -3352,6 +3414,10 @@ packages: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} dev: true + /pstree.remy/1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + dev: true + /punycode/2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} @@ -3395,13 +3461,10 @@ packages: engines: {node: '>=0.10.0'} dev: true - /react-svg-pan-zoom/3.11.0_react@18.2.0: - resolution: {integrity: sha512-xK2tpfp4YksHOfyMZH5zXP52ARLSBgkoJgWNJmJ1B+6O1tkuf23TQp7Q4m9GG5IRSK5KWO0JEGEWlNYG9+iiug==} - peerDependencies: - react: '>=17.0.0' + /react-svg-pan-zoom/3.12.1: + resolution: {integrity: sha512-ug1LHCN5qed56C64xFypr/ClajuMFkig1OKvwJrIgGeSyHOjWM7XGgSgeP3IfHAkNw8QEc6a31ggZRpTijWYRw==} dependencies: prop-types: 15.8.1 - react: 18.2.0 transformation-matrix: 2.14.0 dev: false @@ -3549,11 +3612,21 @@ packages: dependencies: loose-envify: 1.4.0 + /semver/5.7.1: + resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} + hasBin: true + dev: true + /semver/6.3.0: resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} hasBin: true dev: true + /semver/7.0.0: + resolution: {integrity: sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==} + hasBin: true + dev: true + /semver/7.3.8: resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==} engines: {node: '>=10'} @@ -3586,6 +3659,13 @@ packages: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} dev: true + /simple-update-notifier/1.1.0: + resolution: {integrity: sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==} + engines: {node: '>=8.10.0'} + dependencies: + semver: 7.0.0 + dev: true + /sirv/2.0.2: resolution: {integrity: sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==} engines: {node: '>= 10'} @@ -3831,6 +3911,13 @@ packages: engines: {node: '>=6'} dev: true + /touch/3.1.0: + resolution: {integrity: sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==} + hasBin: true + dependencies: + nopt: 1.0.10 + dev: true + /tough-cookie/4.1.2: resolution: {integrity: sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==} engines: {node: '>=6'} @@ -3926,6 +4013,10 @@ packages: which-boxed-primitive: 1.0.2 dev: true + /undefsafe/2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + dev: true + /universalify/0.2.0: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} diff --git a/src/Components/API/api.test.tsx b/src/Components/API/api.test.tsx index 53f3ae0..08fcbad 100644 --- a/src/Components/API/api.test.tsx +++ b/src/Components/API/api.test.tsx @@ -10,7 +10,7 @@ import { IConfiguration } from '../../Interfaces/IConfiguration'; import { IContainerModel, ContainerModel } from '../../Interfaces/IContainerModel'; import { IHistoryState } from '../../Interfaces/IHistoryState'; import { IPattern } from '../../Interfaces/IPattern'; -import { DEFAULT_MAINCONTAINER_PROPS, GetDefaultContainerProps } from '../../utils/default'; +import { DEFAULT_DIMENSION_OPTION, DEFAULT_MAINCONTAINER_PROPS, GetDefaultContainerProps } from '../../utils/default'; import { FetchConfiguration } from './api'; const CSHARP_WEB_API_BASE_URL = 'http://localhost:5209/'; @@ -144,23 +144,11 @@ describe.concurrent('Models test suite', () => { PositionReference: 0, HideChildrenInTreeview: true, DimensionOptions: { - childrenDimensions: { - color: '#000000', - positions: [] - }, - selfDimensions: { - color: '#000000', - positions: [] - }, - selfMarginsDimensions: { - color: '#000000', - positions: [] - }, + childrenDimensions: DEFAULT_DIMENSION_OPTION, + selfDimensions: DEFAULT_DIMENSION_OPTION, + selfMarginsDimensions: DEFAULT_DIMENSION_OPTION, markPosition: [], - dimensionWithMarks: { - color: '#000000', - positions: [] - } + dimensionWithMarks: DEFAULT_DIMENSION_OPTION }, IsHidden: true, Blacklist: [ diff --git a/src/Components/Bar/Bar.tsx b/src/Components/Bar/Bar.tsx index 8f28635..a693fc2 100644 --- a/src/Components/Bar/Bar.tsx +++ b/src/Components/Bar/Bar.tsx @@ -17,6 +17,7 @@ import { BarIcon } from './BarIcon'; import { Text } from '../Text/Text'; interface IBarProps { + className: string isComponentsOpen: boolean isSymbolsOpen: boolean isHistoryOpen: boolean @@ -33,7 +34,7 @@ export const BAR_WIDTH = 64; // 4rem export function Bar(props: IBarProps): JSX.Element { return ( -
+
void - className?: string style?: React.CSSProperties + drawParams: DrawParams }; -function UseCanvas(draw: (context: CanvasRenderingContext2D, frameCount: number, scale: number, translatePos: IPoint) => void): React.RefObject { +function Draw( + ctx: CanvasRenderingContext2D, + frameCount: number, + scale: number, + translatePos: IPoint, + { + mainContainer, + selectorMode, + selectedContainer, + selectedSymbol, + containers, + symbols + }: DrawParams +): void { + if (mainContainer === undefined) { + return; + } + + const topDim = mainContainer.properties.y; + const leftDim = mainContainer.properties.x; + const rightDim = mainContainer.properties.x + mainContainer.properties.width; + const bottomDim = mainContainer.properties.y + mainContainer.properties.height; + + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + ctx.save(); + ctx.setTransform(scale, 0, 0, scale, translatePos.x, translatePos.y); + ctx.fillStyle = '#000000'; + + // Draw containers and symbol dimensions + RenderContainers( + ctx, + mainContainer, + containers, + leftDim, bottomDim, topDim, rightDim, scale); + + // Draw symbols and symbol dimensions + RenderSymbols(ctx, symbols, scale); + + // Draw selector + switch (selectorMode) { + case SelectorMode.Containers: + RenderContainerSelector(ctx, frameCount, { + containers, + scale, + selected: selectedContainer + }); + break; + case SelectorMode.Symbols: + RenderSymbolSelector(ctx, frameCount, { + symbols, + scale, + selected: selectedSymbol + }); + break; + } + + ctx.restore(); +} + +function UseCanvas( + draw: (context: CanvasRenderingContext2D, + frameCount: number, + scale: number, + translatePos: IPoint) => void +): React.RefObject { const canvasRef = useRef(null); const frameCount = useRef(0); const translatePos = useRef({ @@ -123,8 +192,21 @@ interface Viewer { viewerHeight: number } -export function Canvas({ width, height, draw, style, className }: ICanvasProps): JSX.Element { - const canvasRef = UseCanvas(draw); +export function Canvas({ + className, + width, + height, + style, + drawParams +}: ICanvasProps): JSX.Element { + const canvasRef = UseCanvas(( + ...CanvasProps + ) => { + Draw( + ...CanvasProps, + drawParams + ); + }); const [{ viewerWidth, viewerHeight }, setViewer] = React.useState({ viewerWidth: width, diff --git a/src/Components/Canvas/Container.ts b/src/Components/Canvas/Container.ts new file mode 100644 index 0000000..4914b4b --- /dev/null +++ b/src/Components/Canvas/Container.ts @@ -0,0 +1,18 @@ +import { type IContainerModel } from '../../Interfaces/IContainerModel'; + +export function RenderContainer( + ctx: CanvasRenderingContext2D, + container: IContainerModel, + x: number, + y: number +): void { + ctx.save(); + ctx.strokeStyle = container.properties.style?.stroke ?? '#000000'; + ctx.fillStyle = container.properties.style?.fill ?? '#000000'; + ctx.lineWidth = Number(container.properties.style?.strokeWidth ?? 1); + ctx.globalAlpha = Number(container.properties.style?.fillOpacity ?? 1); + ctx.fillRect(x, y, container.properties.width, container.properties.height); + ctx.globalAlpha = Number(container.properties.style?.strokeOpacity ?? 1); + ctx.strokeRect(x, y, container.properties.width, container.properties.height); + ctx.restore(); +} diff --git a/src/Components/Canvas/Dimension.ts b/src/Components/Canvas/Dimension.ts index e95fc92..639bc2d 100644 --- a/src/Components/Canvas/Dimension.ts +++ b/src/Components/Canvas/Dimension.ts @@ -1,4 +1,5 @@ import { NOTCHES_LENGTH } from '../../utils/default'; +import { IDimensionStyle } from '../SVG/Elements/Dimension'; interface IDimensionProps { id: string @@ -7,7 +8,7 @@ interface IDimensionProps { xEnd: number yEnd: number text: string - strokeWidth: number + style: IDimensionStyle scale?: number } @@ -26,8 +27,11 @@ function ApplyParametric(x0: number, t: number, vx: number): number { export function RenderDimension(ctx: CanvasRenderingContext2D, props: IDimensionProps): void { const scale = props.scale ?? 1; - const strokeStyle = 'black'; - const lineWidth = 2 / scale; + const strokeStyle = props.style.color ?? 'black'; + const lineWidth = (props.style.width ?? 2) / scale; + const dashArray: number[] = props.style.dashArray?.split(' ') + .flatMap(array => array.split(',')) + .map(stringValue => parseInt(stringValue)) ?? []; /// We need to find the points of the notches // Get the vector of the line @@ -59,6 +63,7 @@ export function RenderDimension(ctx: CanvasRenderingContext2D, props: IDimension ctx.lineWidth = lineWidth; ctx.strokeStyle = strokeStyle; ctx.fillStyle = strokeStyle; + ctx.setLineDash(dashArray); ctx.moveTo(startTopX, startTopY); ctx.lineTo(startBottomX, startBottomY); ctx.stroke(); @@ -68,6 +73,7 @@ export function RenderDimension(ctx: CanvasRenderingContext2D, props: IDimension ctx.moveTo(endTopX, endTopY); ctx.lineTo(endBottomX, endBottomY); ctx.stroke(); + ctx.setLineDash([]); const textX = (props.xStart + props.xEnd) / 2; const textY = (props.yStart + props.yEnd) / 2; ctx.font = `${16 / scale}px Verdana`; diff --git a/src/Components/Canvas/DimensionLayer.ts b/src/Components/Canvas/DimensionLayer.ts index 1b40961..d986de1 100644 --- a/src/Components/Canvas/DimensionLayer.ts +++ b/src/Components/Canvas/DimensionLayer.ts @@ -1,14 +1,14 @@ import { Orientation } from '../../Enums/Orientation'; import { Position } from '../../Enums/Position'; -import { IContainerModel } from '../../Interfaces/IContainerModel'; -import { SHOW_SELF_DIMENSIONS, SHOW_BORROWER_DIMENSIONS, SHOW_CHILDREN_DIMENSIONS } from '../../utils/default'; +import { type IContainerModel } from '../../Interfaces/IContainerModel'; +import { type ISymbolModel } from '../../Interfaces/ISymbolModel'; +import { SHOW_SELF_DIMENSIONS, SHOW_BORROWER_DIMENSIONS, SHOW_CHILDREN_DIMENSIONS, DIMENSION_MARGIN, SHOW_SELF_MARGINS_DIMENSIONS } from '../../utils/default'; import { FindContainerById, MakeRecursionDFSIterator, Pairwise } from '../../utils/itertools'; import { TransformX, TransformY } from '../../utils/svg'; +import { type IDimensionStyle } from '../SVG/Elements/Dimension'; import { RenderDimension } from './Dimension'; -const MODULE_STROKE_WIDTH = 1; - -export function AddDimensions( +export function AddContainerDimensions( ctx: CanvasRenderingContext2D, containers: Map, container: IContainerModel, @@ -18,7 +18,8 @@ export function AddDimensions( depth: number ): void { ctx.beginPath(); - if (SHOW_SELF_DIMENSIONS && container.properties.dimensionOptions.selfDimensions.positions.length > 0) { + if (SHOW_SELF_DIMENSIONS && + container.properties.dimensionOptions.selfDimensions.positions.length > 0) { ActionByPosition( ctx, dimMapped, @@ -32,7 +33,24 @@ export function AddDimensions( ); } - if (SHOW_BORROWER_DIMENSIONS && container.properties.dimensionOptions.dimensionWithMarks.positions.length > 0) { + if (SHOW_SELF_MARGINS_DIMENSIONS && + container.properties.dimensionOptions.selfMarginsDimensions.positions.length > 0) { + ActionByPosition( + ctx, + dimMapped, + container.properties.dimensionOptions.selfMarginsDimensions.positions, + AddHorizontalSelfMarginsDimension, + AddVerticalSelfMarginDimension, + [ + container, + currentTransform, + scale + ] + ); + } + + if (SHOW_BORROWER_DIMENSIONS && + container.properties.dimensionOptions.dimensionWithMarks.positions.length > 0) { ActionByPosition( ctx, dimMapped, @@ -48,7 +66,9 @@ export function AddDimensions( ); } - if (SHOW_CHILDREN_DIMENSIONS && container.properties.dimensionOptions.childrenDimensions.positions.length > 0 && container.children.length >= 2) { + if (SHOW_CHILDREN_DIMENSIONS && + container.properties.dimensionOptions.childrenDimensions.positions.length > 0 && + container.children.length >= 2) { ActionByPosition( ctx, dimMapped, @@ -64,6 +84,20 @@ export function AddDimensions( } } +export function AddSymbolDimensions( + ctx: CanvasRenderingContext2D, + symbol: ISymbolModel, + scale: number, + depth: number +): void { + AddHorizontalSymbolDimension( + ctx, + symbol, + scale, + depth + ); +} + /** * Fonction that call another function given the positions * @param dimMapped Position mapped depending on the Position enum in order: @@ -108,6 +142,7 @@ function AddHorizontalChildrenDimension( scale: number ): void { const childrenId = `dim-y${yDim.toFixed(0)}-children-${container.properties.id}`; + const style = container.properties.dimensionOptions.childrenDimensions; const lastChildId = container.children[container.children.length - 1]; const lastChild = FindContainerById(containers, lastChildId); @@ -115,8 +150,14 @@ function AddHorizontalChildrenDimension( if (lastChild === undefined) { return; } - let xChildrenStart = TransformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.positionReference); - let xChildrenEnd = TransformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.positionReference); + let xChildrenStart = TransformX( + lastChild.properties.x, + lastChild.properties.width, + lastChild.properties.positionReference); + let xChildrenEnd = TransformX( + lastChild.properties.x, + lastChild.properties.width, + lastChild.properties.positionReference); // Find the min and max for (let i = container.children.length - 2; i >= 0; i--) { @@ -152,9 +193,9 @@ function AddHorizontalChildrenDimension( xEnd: xChildrenEnd + offset, yStart: yDim, yEnd: yDim, - strokeWidth: MODULE_STROKE_WIDTH, text: textChildren, - scale + scale, + style }); } @@ -168,6 +209,7 @@ function AddVerticalChildrenDimension( scale: number ): void { const childrenId = `dim-x${xDim.toFixed(0)}-children-${container.properties.id}`; + const style = container.properties.dimensionOptions.childrenDimensions; const lastChildId = container.children[container.children.length - 1]; const lastChild = FindContainerById(containers, lastChildId); @@ -176,7 +218,10 @@ function AddVerticalChildrenDimension( return; } - let yChildrenStart = TransformY(lastChild.properties.y, lastChild.properties.height, lastChild.properties.positionReference); + let yChildrenStart = TransformY( + lastChild.properties.y, + lastChild.properties.height, + lastChild.properties.positionReference); let yChildrenEnd = yChildrenStart; // Find the min and max @@ -218,9 +263,9 @@ function AddVerticalChildrenDimension( xEnd: xDim, yStart: yChildrenStart + offset, yEnd: yChildrenEnd + offset, - strokeWidth: MODULE_STROKE_WIDTH, text: textChildren, - scale + scale, + style }); } @@ -234,6 +279,7 @@ function AddHorizontalBorrowerDimension( scale: number ): void { const it = MakeRecursionDFSIterator(container, containers, depth, currentTransform); + const style = container.properties.dimensionOptions.dimensionWithMarks; const marks = []; // list of vertical lines for the dimension for (const { container: childContainer, currentTransform: childCurrentTransform @@ -274,9 +320,9 @@ function AddHorizontalBorrowerDimension( xEnd: next, yStart: yDim, yEnd: yDim, - strokeWidth: MODULE_STROKE_WIDTH, text: value.toFixed(0), - scale + scale, + style }); count++; } @@ -293,6 +339,7 @@ function AddVerticalBorrowerDimension( scale: number ): void { const it = MakeRecursionDFSIterator(container, containers, depth, currentTransform); + const style = container.properties.dimensionOptions.dimensionWithMarks; const marks = []; // list of vertical lines for the dimension for (const { container: childContainer, currentTransform: childCurrentTransform @@ -338,9 +385,9 @@ function AddVerticalBorrowerDimension( xEnd: xDim, yStart: cur, yEnd: next, - strokeWidth: MODULE_STROKE_WIDTH, text: value.toFixed(0), - scale + scale, + style }); count++; } @@ -354,6 +401,7 @@ function AddVerticalSelfDimension( currentTransform: [number, number], scale: number ): void { + const style = container.properties.dimensionOptions.selfDimensions; const height = container.properties.height; const idVert = `dim-x${xDim.toFixed(0)}-${container.properties.id}`; let yStart = container.properties.y + currentTransform[1] + height; @@ -372,9 +420,9 @@ function AddVerticalSelfDimension( xEnd: xDim, yStart, yEnd, - strokeWidth: MODULE_STROKE_WIDTH, text: textVert, - scale + scale, + style }); } @@ -385,6 +433,7 @@ function AddHorizontalSelfDimension( currentTransform: [number, number], scale: number ): void { + const style = container.properties.dimensionOptions.selfDimensions; const width = container.properties.width; const id = `dim-y${yDim.toFixed(0)}-${container.properties.id}`; const xStart = container.properties.x + currentTransform[0]; @@ -398,8 +447,154 @@ function AddHorizontalSelfDimension( yStart: yDim, xEnd, yEnd: yDim, - strokeWidth: MODULE_STROKE_WIDTH, text, - scale + scale, + style + }); +} + +function AddHorizontalSelfMarginsDimension( + ctx: CanvasRenderingContext2D, + yDim: number, + container: IContainerModel, + currentTransform: [number, number], + dimensions: React.ReactNode[], + scale: number +): void { + const style = container.properties.dimensionOptions.selfMarginsDimensions; + const left = container.properties.margin.left; + if (left != null) { + const id = `dim-y-margin-left${yDim.toFixed(0)}-${container.properties.id}`; + const xStart = container.properties.x + currentTransform[0] - left; + const xEnd = xStart + left; + const text = left + .toFixed(0) + .toString(); + RenderDimension(ctx, { + id, + xStart, + yStart: yDim, + xEnd, + yEnd: yDim, + text, + scale, + style + }); + } + + const right = container.properties.margin.right; + if (right != null) { + const id = `dim-y-margin-right${yDim.toFixed(0)}-${container.properties.id}`; + const xStart = container.properties.x + container.properties.width + currentTransform[0]; + const xEnd = xStart + right; + const text = right + .toFixed(0) + .toString(); + + RenderDimension(ctx, { + id, + xStart, + yStart: yDim, + xEnd, + yEnd: yDim, + text, + scale, + style + }); + } +} + +function AddVerticalSelfMarginDimension( + ctx: CanvasRenderingContext2D, + xDim: number, + isRight: boolean, + container: IContainerModel, + currentTransform: [number, number], + scale: number +): void { + const style = container.properties.dimensionOptions.selfMarginsDimensions; + const top = container.properties.margin.top; + if (top != null) { + const idVert = `dim-x-margin-top${xDim.toFixed(0)}-${container.properties.id}`; + let yStart = container.properties.y + currentTransform[1]; + let yEnd = yStart - top; + const textVert = top + .toFixed(0) + .toString(); + + if (isRight) { + [yStart, yEnd] = [yEnd, yStart]; + } + + RenderDimension(ctx, { + id: idVert, + xStart: xDim, + yStart, + xEnd: xDim, + yEnd, + text: textVert, + scale, + style + }); + } + const bottom = container.properties.margin.bottom; + if (bottom != null) { + const idVert = `dim-x-margin-bottom${xDim.toFixed(0)}-${container.properties.id}`; + let yStart = container.properties.y + container.properties.height + bottom + currentTransform[1]; + let yEnd = yStart - bottom; + const textVert = bottom + .toFixed(0) + .toString(); + + if (isRight) { + [yStart, yEnd] = [yEnd, yStart]; + } + + RenderDimension(ctx, { + id: idVert, + xStart: xDim, + yStart, + xEnd: xDim, + yEnd, + text: textVert, + scale, + style + }); + } +} + +function AddHorizontalSymbolDimension( + ctx: CanvasRenderingContext2D, + symbol: ISymbolModel, + scale: number, + depth: number +): void { + const width = symbol.x + (symbol.width / 2); + + if (width == null || width <= 0) { + return; + } + + const id = `dim-y-margin-left${symbol.width.toFixed(0)}-${symbol.id}`; + + const offset = (DIMENSION_MARGIN * (depth + 1)) / scale; + const text = width + .toFixed(0) + .toString(); + + // TODO: Put this in default.ts + const defaultDimensionSymbolStyle: IDimensionStyle = { + color: 'black' + }; + + RenderDimension(ctx, { + id, + xStart: 0, + yStart: -offset, + xEnd: width, + yEnd: -offset, + text, + scale, + style: defaultDimensionSymbolStyle }); } diff --git a/src/Components/Canvas/Renderer.ts b/src/Components/Canvas/Renderer.ts new file mode 100644 index 0000000..c93e615 --- /dev/null +++ b/src/Components/Canvas/Renderer.ts @@ -0,0 +1,104 @@ +import { type IContainerModel } from '../../Interfaces/IContainerModel'; +import { type IHistoryState } from '../../Interfaces/IHistoryState'; +import { ISymbolModel } from '../../Interfaces/ISymbolModel'; +import { DIMENSION_MARGIN } from '../../utils/default'; +import { MakeRecursionDFSIterator } from '../../utils/itertools'; +import { RenderContainer } from './Container'; +import { AddContainerDimensions, AddSymbolDimensions } from './DimensionLayer'; +import { RenderSymbol } from './Symbol'; + +export function RenderContainers( + ctx: CanvasRenderingContext2D, + root: IContainerModel, + containers: Map, + leftDim: number, + bottomDim: number, + topDim: number, + rightDim: number, + scale: number +): void { + const it = MakeRecursionDFSIterator(root, containers, 0, [0, 0]); + for (const { container, depth, currentTransform } of it) { + const [x, y] = [ + container.properties.x + currentTransform[0], + container.properties.y + currentTransform[1] + ]; + + // Draw container + RenderContainer(ctx, container, x, y); + + // Draw dimensions + RenderContainerDimensions( + ctx, + leftDim, + bottomDim, + topDim, + rightDim, + depth, + scale, + containers, + container, + currentTransform + ); + } +} + +export function RenderContainerDimensions( + ctx: CanvasRenderingContext2D, + leftDim: number, + bottomDim: number, + topDim: number, + rightDim: number, + depth: number, + scale: number, + containers: Map, + container: IContainerModel, + currentTransform: [number, number] +): void { + ctx.save(); + const depthOffset = (DIMENSION_MARGIN * (depth + 1)) / scale; + const containerLeftDim = leftDim - depthOffset; + const containerTopDim = topDim - depthOffset; + const containerBottomDim = bottomDim + depthOffset; + const containerRightDim = rightDim + depthOffset; + const dimMapped = [containerLeftDim, containerBottomDim, containerTopDim, containerRightDim]; + AddContainerDimensions(ctx, containers, container, dimMapped, currentTransform, scale, depth); + ctx.restore(); +} + +export function RenderSymbols( + ctx: CanvasRenderingContext2D, + symbols: Map, + scale: number +): void { + let count = 0; + symbols.forEach((symbol: ISymbolModel) => { + RenderSymbol(ctx, symbol, scale); + + if (!symbol.showDimension) { + return; + } + + AddSymbolDimensions(ctx, symbol, scale, count); + count++; + }); +} + +export function RenderSymbolDimensions( + ctx: CanvasRenderingContext2D, + depth: number, + scale: number, + containers: Map, + container: IContainerModel, + currentTransform: [number, number] +): void { + ctx.save(); + const depthOffset = (DIMENSION_MARGIN * (depth + 1)) / scale; + const containerLeftDim = -depthOffset; + const containerTopDim = -depthOffset; + const containerBottomDim = depthOffset; + const containerRightDim = depthOffset; + const dimMapped = [containerLeftDim, containerBottomDim, containerTopDim, containerRightDim]; + AddContainerDimensions(ctx, containers, container, dimMapped, currentTransform, scale, depth); + ctx.restore(); +} diff --git a/src/Components/Canvas/Selector.ts b/src/Components/Canvas/Selector.ts index c9ea814..1dc1654 100644 --- a/src/Components/Canvas/Selector.ts +++ b/src/Components/Canvas/Selector.ts @@ -1,33 +1,26 @@ -import { IContainerModel } from '../../Interfaces/IContainerModel'; import { SHOW_SELECTOR_TEXT } from '../../utils/default'; -import { GetAbsolutePosition } from '../../utils/itertools'; -import { RemoveMargin } from '../../utils/svg'; interface ISelectorProps { - containers: Map - selected?: IContainerModel - scale?: number + text: string + x: number + y: number + width: number + height: number + scale: number } -export function RenderSelector(ctx: CanvasRenderingContext2D, frameCount: number, props: ISelectorProps): void { - if (props.selected === undefined || props.selected === null) { - return; - } - - const scale = (props.scale ?? 1); - let [x, y] = GetAbsolutePosition(props.containers, props.selected); - let [width, height] = [ - props.selected.properties.width, - props.selected.properties.height - ]; - - ({ x, y, width, height } = RemoveMargin(x, y, width, height, - props.selected.properties.margin.left, - props.selected.properties.margin.bottom, - props.selected.properties.margin.top, - props.selected.properties.margin.right - )); - +export function RenderSelector( + ctx: CanvasRenderingContext2D, + frameCount: number, + { + text, + x, + y, + width, + height, + scale + }: ISelectorProps +): void { const xText = x + width / 2; const yText = y + height / 2; @@ -42,7 +35,7 @@ export function RenderSelector(ctx: CanvasRenderingContext2D, frameCount: number if (SHOW_SELECTOR_TEXT) { ctx.font = `${16 / scale}px Verdana`; ctx.textAlign = 'center'; - ctx.fillText(props.selected.properties.displayedText, xText, yText); + ctx.fillText(text, xText, yText); ctx.textAlign = 'left'; } } diff --git a/src/Components/Canvas/SelectorContainer.ts b/src/Components/Canvas/SelectorContainer.ts new file mode 100644 index 0000000..c151f6f --- /dev/null +++ b/src/Components/Canvas/SelectorContainer.ts @@ -0,0 +1,48 @@ +import { type IContainerModel } from '../../Interfaces/IContainerModel'; +import { GetAbsolutePosition } from '../../utils/itertools'; +import { RemoveMargin } from '../../utils/svg'; +import { RenderSelector } from './Selector'; + +interface ISelectorProps { + containers: Map + selected?: IContainerModel + scale?: number +} + +export function RenderContainerSelector( + ctx: CanvasRenderingContext2D, + frameCount: number, + props: ISelectorProps +): void { + if (props.selected === undefined || props.selected === null) { + return; + } + + const scale = (props.scale ?? 1); + let [x, y] = GetAbsolutePosition(props.containers, props.selected); + let [width, height] = [ + props.selected.properties.width, + props.selected.properties.height + ]; + + ({ x, y, width, height } = RemoveMargin(x, y, width, height, + props.selected.properties.margin.left, + props.selected.properties.margin.bottom, + props.selected.properties.margin.top, + props.selected.properties.margin.right + )); + + const text = props.selected.properties.displayedText; + RenderSelector( + ctx, + frameCount, + { + text, + x, + y, + width, + height, + scale + } + ); +} diff --git a/src/Components/Canvas/SelectorSymbol.ts b/src/Components/Canvas/SelectorSymbol.ts new file mode 100644 index 0000000..ddfbb29 --- /dev/null +++ b/src/Components/Canvas/SelectorSymbol.ts @@ -0,0 +1,43 @@ +import { type ISymbolModel } from '../../Interfaces/ISymbolModel'; +import { SYMBOL_MARGIN } from '../../utils/default'; +import { RenderSelector } from './Selector'; + +interface ISelectorProps { + symbols: Map + selected?: ISymbolModel + scale?: number +} + +export function RenderSymbolSelector( + ctx: CanvasRenderingContext2D, + frameCount: number, + props: ISelectorProps +): void { + if (props.selected === undefined || props.selected === null) { + return; + } + + const scale = (props.scale ?? 1); + const [width, height] = [ + props.selected.width / scale, + props.selected.height / scale + ]; + + const [x, y] = [ + props.selected.x + props.selected.width / 2, + -SYMBOL_MARGIN - height]; + + const text = props.selected.displayedText; + RenderSelector( + ctx, + frameCount, + { + text, + x, + y, + width, + height, + scale + } + ); +} diff --git a/src/Components/Canvas/Symbol.ts b/src/Components/Canvas/Symbol.ts index 752eb66..c4dad94 100644 --- a/src/Components/Canvas/Symbol.ts +++ b/src/Components/Canvas/Symbol.ts @@ -1,11 +1,11 @@ -import { ISymbolModel } from '../../Interfaces/ISymbolModel'; -import { DIMENSION_MARGIN } from '../../utils/default'; +import { type ISymbolModel } from '../../Interfaces/ISymbolModel'; +import { DIMENSION_MARGIN, SYMBOL_MARGIN } from '../../utils/default'; const IMAGE_CACHE = new Map(); export function RenderSymbol( - symbol: ISymbolModel, ctx: CanvasRenderingContext2D, + symbol: ISymbolModel, scale: number): void { const href = symbol.config.Image.Base64Image ?? symbol.config.Image.Url; @@ -26,6 +26,7 @@ export function RenderSymbol( DrawImage(ctx, scale, image, symbol); } + function DrawImage( ctx: CanvasRenderingContext2D, scale: number, @@ -34,12 +35,14 @@ function DrawImage( ): void { ctx.save(); ctx.fillStyle = '#000000'; + const width = symbol.width / scale; + const height = symbol.height / scale; ctx.drawImage( image, - symbol.offset, - -DIMENSION_MARGIN, - symbol.width, - symbol.height + symbol.offset + symbol.width / 2, + -SYMBOL_MARGIN - height, + width, + height ); ctx.restore(); } diff --git a/src/Components/Components/Components.tsx b/src/Components/Components/Components.tsx index 53a3dd9..8a4581a 100644 --- a/src/Components/Components/Components.tsx +++ b/src/Components/Components/Components.tsx @@ -1,4 +1,4 @@ -import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'; +import { EyeIcon, EyeSlashIcon, XCircleIcon } from '@heroicons/react/24/outline'; import * as React from 'react'; import { IAvailableContainer } from '../../Interfaces/IAvailableContainer'; import { ICategory } from '../../Interfaces/ICategory'; @@ -6,11 +6,15 @@ import { IContainerModel } from '../../Interfaces/IContainerModel'; import { TruncateString } from '../../utils/stringtools'; import { Category } from '../Category/Category'; import { Text } from '../Text/Text'; +import { IReplaceContainer } from '../../Interfaces/IReplaceContainer'; +import { Dispatch } from 'react'; interface IComponentsProps { selectedContainer: IContainerModel | undefined componentOptions: IAvailableContainer[] categories: ICategory[] + replaceContainer: IReplaceContainer + setReplaceContainer: Dispatch> buttonOnClick: (type: string) => void } @@ -62,6 +66,10 @@ export function Components(props: IComponentsProps): JSX.Element { disabled = config.Blacklist?.find(type => type === componentOption.Type) !== undefined ?? false; } + if (props.replaceContainer.isReplacing && componentOption.Category !== props.replaceContainer.category) { + disabled = true; + } + if (disabled && hideDisabled) { return; } @@ -96,6 +104,15 @@ export function Components(props: IComponentsProps): JSX.Element { return (
+ {props.replaceContainer.isReplacing && + }
); -}; +} diff --git a/src/Components/ContainerProperties/ContainerForm.tsx b/src/Components/ContainerProperties/ContainerForm.tsx index 4bdc262..8b7a505 100644 --- a/src/Components/ContainerProperties/ContainerForm.tsx +++ b/src/Components/ContainerProperties/ContainerForm.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { PropertyType } from '../../Enums/PropertyType'; -import { IContainerProperties } from '../../Interfaces/IContainerProperties'; -import { ISymbolModel } from '../../Interfaces/ISymbolModel'; +import { type IContainerProperties } from '../../Interfaces/IContainerProperties'; +import { type ISymbolModel } from '../../Interfaces/ISymbolModel'; import { SHOW_BORROWER_DIMENSIONS, SHOW_CHILDREN_DIMENSIONS, @@ -46,7 +46,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element { inputClassName='' type='string' value={props.properties.displayedText?.toString()} - onChange={(value) => props.onChange('displayedText', value)}/> + onChange={(value) => { props.onChange('displayedText', value); }}/> props.onChange( - 'x', - ApplyXMargin( - RestoreX( - Number(value), - props.properties.width, - props.properties.positionReference - ), - props.properties.margin.left - ) - )}/> + onChange={(value) => { + props.onChange( + 'x', + ApplyXMargin( + RestoreX( + Number(value), + props.properties.width, + props.properties.positionReference + ), + props.properties.margin.left + ) + ); + }}/> props.onChange( - 'y', - ApplyXMargin( - RestoreY( - Number(value), - props.properties.height, - props.properties.positionReference - ), - props.properties.margin.top - ) - )}/> + onChange={(value) => { + props.onChange( + 'y', + ApplyXMargin( + RestoreY( + Number(value), + props.properties.height, + props.properties.positionReference + ), + props.properties.margin.top + ) + ); + }}/>
@@ -171,7 +175,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element { type='number' min={1} value={props.properties.minWidth.toString()} - onChange={(value) => props.onChange('minWidth', Number(value))}/> + onChange={(value) => { props.onChange('minWidth', Number(value)); }}/> props.onChange('width', ApplyWidthMargin(Number(value), props.properties.margin.left, props.properties.margin.right))} + onChange={(value) => { props.onChange('width', ApplyWidthMargin(Number(value), props.properties.margin.left, props.properties.margin.right)); }} isDisabled={props.properties.isFlex}/> props.onChange('maxWidth', Number(value))}/> + onChange={(value) => { props.onChange('maxWidth', Number(value)); }}/>
props.onChange('minHeight', Number(value))}/> + onChange={(value) => { props.onChange('minHeight', Number(value)); }}/> props.onChange('height', ApplyWidthMargin(Number(value), props.properties.margin.top, props.properties.margin.bottom))} + onChange={(value) => { props.onChange('height', ApplyWidthMargin(Number(value), props.properties.margin.top, props.properties.margin.bottom)); }} isDisabled={props.properties.isFlex} /> props.onChange('maxHeight', Number(value))}/> + onChange={(value) => { props.onChange('maxHeight', Number(value)); }}/>
@@ -247,7 +251,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element { type='number' min={0} value={(props.properties.margin.left ?? 0).toString()} - onChange={(value) => props.onChange('left', Number(value), PropertyType.Margin)}/> + onChange={(value) => { props.onChange('left', Number(value), PropertyType.Margin); }}/> props.onChange('bottom', Number(value), PropertyType.Margin)}/> + onChange={(value) => { props.onChange('bottom', Number(value), PropertyType.Margin); }}/> props.onChange('top', Number(value), PropertyType.Margin)}/> + onChange={(value) => { props.onChange('top', Number(value), PropertyType.Margin); }}/> props.onChange('right', Number(value), PropertyType.Margin)}/> + onChange={(value) => { props.onChange('right', Number(value), PropertyType.Margin); }}/> @@ -295,7 +299,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element { inputClassName='ml-auto mr-auto block' type={ToggleType.Full} checked={props.properties.isFlex} - onChange={(event) => props.onChange('isFlex', event.target.checked)} + onChange={(event) => { props.onChange('isFlex', event.target.checked); }} /> props.onChange('isAnchor', event.target.checked)}/> + onChange={(event) => { props.onChange('isAnchor', event.target.checked); }}/> @@ -334,7 +338,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element { value: symbol.id }))} value={props.properties.linkedSymbolId ?? ''} - onChange={(event) => props.onChange('linkedSymbolId', event.target.value)}/> + onChange={(event) => { props.onChange('linkedSymbolId', event.target.value); }}/> @@ -347,168 +351,244 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
{ SHOW_SELF_DIMENSIONS && -
- props.onChange(key, value, PropertyType.SelfDimension)} - /> - props.onChange('color', e.target.value, PropertyType.SelfDimension)}/> -
+
+ { props.onChange(key, value, PropertyType.SelfDimension); }} + /> + { props.onChange('color', e.target.value, PropertyType.SelfDimension); }}/> + { props.onChange('width', Number(value), PropertyType.SelfDimension); }}/> + { props.onChange('dashArray', value, PropertyType.SelfDimension); }}/> +
} { SHOW_SELF_MARGINS_DIMENSIONS && -
- props.onChange(key, value, PropertyType.SelfMarginDimension)} - /> - props.onChange('color', e.target.value, PropertyType.SelfMarginDimension)}/> -
+
+ { props.onChange(key, value, PropertyType.SelfMarginDimension); }} + /> + { props.onChange('color', e.target.value, PropertyType.SelfMarginDimension); }}/> + { props.onChange('width', Number(value), PropertyType.SelfMarginDimension); }}/> + { props.onChange('dashArray', value, PropertyType.SelfMarginDimension); }}/> +
} { SHOW_CHILDREN_DIMENSIONS && -
- props.onChange(key, value, PropertyType.ChildrenDimensions)} - /> - props.onChange('color', e.target.value, PropertyType.ChildrenDimensions)}/> -
+
+ { props.onChange(key, value, PropertyType.ChildrenDimensions); }} + /> + { props.onChange('color', e.target.value, PropertyType.ChildrenDimensions); }}/> + { props.onChange('width', Number(value), PropertyType.ChildrenDimensions); }}/> + { props.onChange('dashArray', value, PropertyType.ChildrenDimensions); }}/> +
} { SHOW_BORROWER_DIMENSIONS && - <> -
- props.onChange(key, value, PropertyType.DimensionOptions)} - /> -
-
- props.onChange(key, value, PropertyType.DimensionWithMarks)} - /> - props.onChange('color', e.target.value, PropertyType.DimensionWithMarks)}/> -
- + <> +
+ { props.onChange(key, value, PropertyType.DimensionOptions); }} + /> +
+
+ { props.onChange(key, value, PropertyType.DimensionWithMarks); }} + /> + { props.onChange('color', e.target.value, PropertyType.DimensionWithMarks); }}/> + { props.onChange('width', Number(value), PropertyType.DimensionWithMarks); }}/> + { props.onChange('dashArray', value, PropertyType.DimensionWithMarks); }}/> +
+ }
{props.properties.style !== undefined && - -
- props.onChange('stroke', value, PropertyType.Style)} - /> - props.onChange('strokeOpacity', Number(event.target.value), PropertyType.Style)} - /> - props.onChange('strokeWidth', Number(value), PropertyType.Style)} - /> - props.onChange('fill', value, PropertyType.Style)} - /> - props.onChange('fillOpacity', Number(event.target.value), PropertyType.Style)} - /> -
-
+ +
+ { props.onChange('stroke', value, PropertyType.Style); }} + /> + { props.onChange('strokeOpacity', Number(event.target.value), PropertyType.Style); }} + /> + { props.onChange('strokeWidth', Number(value), PropertyType.Style); }} + /> + { props.onChange('fill', value, PropertyType.Style); }} + /> + { props.onChange('fillOpacity', Number(event.target.value), PropertyType.Style); }} + /> +
+
} ); diff --git a/src/Components/ContainerProperties/ContainerProperties.test.tsx b/src/Components/ContainerProperties/ContainerProperties.test.tsx index cd2da15..01c36bd 100644 --- a/src/Components/ContainerProperties/ContainerProperties.test.tsx +++ b/src/Components/ContainerProperties/ContainerProperties.test.tsx @@ -5,6 +5,7 @@ import { PositionReference } from '../../Enums/PositionReference'; import { IContainerProperties } from '../../Interfaces/IContainerProperties'; import { Orientation } from '../../Enums/Orientation'; import { ContainerProperties } from './ContainerProperties'; +import { DEFAULT_DIMENSION_OPTION } from '../../utils/default'; describe.concurrent('Properties', () => { it('No properties', () => { @@ -43,23 +44,11 @@ describe.concurrent('Properties', () => { warning: '', hideChildrenInTreeview: false, dimensionOptions: { - childrenDimensions: { - color: '#000000', - positions: [] - }, - selfDimensions: { - color: '#000000', - positions: [] - }, - selfMarginsDimensions: { - color: '#000000', - positions: [] - }, + childrenDimensions: DEFAULT_DIMENSION_OPTION, + selfDimensions: DEFAULT_DIMENSION_OPTION, + selfMarginsDimensions: DEFAULT_DIMENSION_OPTION, markPosition: [], - dimensionWithMarks: { - color: '#000000', - positions: [] - } + dimensionWithMarks: DEFAULT_DIMENSION_OPTION } }; diff --git a/src/Components/Editor/Actions/ContainerOperations.ts b/src/Components/Editor/Actions/ContainerOperations.ts index 82162e3..684b443 100644 --- a/src/Components/Editor/Actions/ContainerOperations.ts +++ b/src/Components/Editor/Actions/ContainerOperations.ts @@ -8,6 +8,8 @@ import Swal from 'sweetalert2'; import { PropertyType } from '../../../Enums/PropertyType'; import { TransformX, TransformY } from '../../../utils/svg'; import { Orientation } from '../../../Enums/Orientation'; +import { AddContainers } from './AddContainer'; +import { IConfiguration } from '../../../Interfaces/IConfiguration'; /** * Select a container @@ -133,6 +135,58 @@ export function DeleteContainer( return history; } +/** + * Replace a container + * @param containerId containerId of the container to delete + * @param newContainerId + * @param configuration + * @param fullHistory History of the editor + * @param historyCurrentStep Current step + * @returns New history + */ +export function ReplaceByContainer( + containerId: string, + newContainerId: string, + configuration: IConfiguration, + fullHistory: IHistoryState[], + historyCurrentStep: number +): IHistoryState[] { + const history = GetCurrentHistory(fullHistory, historyCurrentStep); + const current = history[history.length - 1]; + + const containerToReplace = FindContainerById(current.containers, containerId); + if (containerToReplace === undefined) { + return history; + } + + const containerParent = FindContainerById(current.containers, containerToReplace.properties.parentId); + if (containerParent === undefined) { + return history; + } + + const historyAdd = AddContainers( + containerParent.children.indexOf(containerId), + [{ Type: newContainerId }], + containerParent.properties.id, + configuration, fullHistory, historyCurrentStep + ); + + const historyDelete = DeleteContainer(containerId, historyAdd.history, historyCurrentStep + 1); + const currentDelete = historyDelete[historyDelete.length - 1]; + + fullHistory.push({ + lastAction: `Replace ${containerId} by ${newContainerId}`, + mainContainer: currentDelete.mainContainer, + containers: currentDelete.containers, + selectedContainerId: currentDelete.selectedContainerId, + typeCounters: Object.assign({}, currentDelete.typeCounters), + symbols: current.symbols, + selectedSymbolId: current.selectedSymbolId + }); + + return fullHistory; +} + /** * Returns the next container that will be selected * after the selectedContainer is removed. diff --git a/src/Components/Editor/Actions/ContextMenuActions.ts b/src/Components/Editor/Actions/ContextMenuActions.ts index 69a3121..9fb86f4 100644 --- a/src/Components/Editor/Actions/ContextMenuActions.ts +++ b/src/Components/Editor/Actions/ContextMenuActions.ts @@ -16,6 +16,7 @@ import { AddContainers } from './AddContainer'; import { DeleteContainer } from './ContainerOperations'; import { DeleteSymbol } from './SymbolOperations'; import { Text } from '../../Text/Text'; +import { IReplaceContainer } from '../../../Interfaces/IReplaceContainer'; export function InitActions( menuActions: Map, @@ -23,7 +24,8 @@ export function InitActions( history: IHistoryState[], historyCurrentStep: number, setNewHistory: (newHistory: IHistoryState[]) => void, - setHistoryCurrentStep: Dispatch> + setHistoryCurrentStep: Dispatch>, + setIsReplacingContainer: Dispatch> ): void { menuActions.set( '', @@ -56,9 +58,24 @@ export function InitActions( menuActions.set( 'elements-sidebar-row', [{ + text: Text({ textId: '@ReplaceByContainer' }), + title: Text({ textId: '@ReplaceByContainerTitle' }), + shortcut: 'R', + action: (target: HTMLElement) => { + const targetContainer = FindContainerById(history[historyCurrentStep].containers, target.id); + const targetAvailableContainer = configuration.AvailableContainers.find((availableContainer) => availableContainer.Type === targetContainer?.properties.type); + + if (targetAvailableContainer === undefined) { + return; + } + + setIsReplacingContainer({ isReplacing: true, id: target.id, category: targetAvailableContainer.Category }); + } + }, { text: Text({ textId: '@DeleteContainer' }), title: Text({ textId: '@DeleteContainerTitle' }), shortcut: 'Suppr', + action: (target: HTMLElement) => { const id = target.id; const newHistory = DeleteContainer( diff --git a/src/Components/Editor/Actions/Save.ts b/src/Components/Editor/Actions/Save.ts index ae7b4e9..2dc2f95 100644 --- a/src/Components/Editor/Actions/Save.ts +++ b/src/Components/Editor/Actions/Save.ts @@ -53,6 +53,7 @@ export function SaveEditorAsSVG(): void { svg.replaceChildren(...mainSvg); // remove the selector + // TODO: Fix this with SelectorMode != Nothing or with some html magic const group = svg.children[svg.children.length - 1]; group.removeChild(group.children[group.children.length - 1]); if (SHOW_SELECTOR_TEXT) { diff --git a/src/Components/Editor/Actions/Shortcuts.ts b/src/Components/Editor/Actions/Shortcuts.ts index a81660c..082770e 100644 --- a/src/Components/Editor/Actions/Shortcuts.ts +++ b/src/Components/Editor/Actions/Shortcuts.ts @@ -7,7 +7,8 @@ export function OnKey( history: IHistoryState[], historyCurrentStep: number, setHistoryCurrentStep: Dispatch>, - deleteAction: () => void + deleteAction: () => void, + resetState: () => void ): void { if (!ENABLE_SHORTCUTS) { return; @@ -27,5 +28,7 @@ export function OnKey( setHistoryCurrentStep(historyCurrentStep + 1); } else if (event.key === 'Delete') { deleteAction(); + } else if (event.key === 'Escape') { + resetState(); } } diff --git a/src/Components/Editor/Editor.tsx b/src/Components/Editor/Editor.tsx index 372ab06..095f644 100644 --- a/src/Components/Editor/Editor.tsx +++ b/src/Components/Editor/Editor.tsx @@ -1,9 +1,9 @@ -import React, { Dispatch, SetStateAction, useEffect, useRef } from 'react'; +import React, { type Dispatch, type SetStateAction, useEffect, useRef } from 'react'; import './Editor.scss'; -import { IConfiguration } from '../../Interfaces/IConfiguration'; -import { IHistoryState } from '../../Interfaces/IHistoryState'; +import { type IConfiguration } from '../../Interfaces/IConfiguration'; +import { type IHistoryState } from '../../Interfaces/IHistoryState'; import { UI } from '../UI/UI'; -import { SelectContainer, DeleteContainer, OnPropertyChange } from './Actions/ContainerOperations'; +import { SelectContainer, DeleteContainer, OnPropertyChange, ReplaceByContainer } from './Actions/ContainerOperations'; import { SaveEditorAsJSON, SaveEditorAsSVG } from './Actions/Save'; import { OnKey } from './Actions/Shortcuts'; import { UseCustomEvents, UseEditorListener } from '../../Events/EditorEvents'; @@ -13,6 +13,7 @@ import { FindContainerById } from '../../utils/itertools'; import { Menu } from '../Menu/Menu'; import { InitActions } from './Actions/ContextMenuActions'; import { AddContainerToSelectedContainer, AddContainer } from './Actions/AddContainer'; +import { type IReplaceContainer } from '../../Interfaces/IReplaceContainer'; interface IEditorProps { root: Element | Document @@ -25,16 +26,18 @@ function UseShortcuts( history: IHistoryState[], historyCurrentStep: number, setHistoryCurrentStep: Dispatch>, - deleteAction: () => void + deleteAction: () => void, + resetState: () => void ): void { useEffect(() => { function OnKeyUp(event: KeyboardEvent): void { - return OnKey( + OnKey( event, history, historyCurrentStep, setHistoryCurrentStep, - deleteAction + deleteAction, + resetState ); } @@ -62,13 +65,20 @@ function UseNewHistoryState( }; } + export function Editor(props: IEditorProps): JSX.Element { // States const [history, setHistory] = React.useState(structuredClone(props.history)); const [historyCurrentStep, setHistoryCurrentStep] = React.useState(props.historyCurrentStep); + const [replaceContainer, setReplaceContainer] = React.useState({ isReplacing: false, id: undefined, category: undefined }); + const editorRef = useRef(null); const setNewHistory = UseNewHistoryState(setHistory, setHistoryCurrentStep); + function ResetState(): void { + setReplaceContainer({ isReplacing: false, id: undefined, category: undefined }); + } + // Events UseShortcuts( history, @@ -79,7 +89,8 @@ export function Editor(props: IEditorProps): JSX.Element { setNewHistory( DeleteContainer(current.selectedContainerId, history, historyCurrentStep) ); - } + }, + ResetState ); UseCustomEvents( props.root, @@ -104,7 +115,8 @@ export function Editor(props: IEditorProps): JSX.Element { history, historyCurrentStep, setNewHistory, - setHistoryCurrentStep + setHistoryCurrentStep, + setReplaceContainer ); // Render @@ -113,87 +125,118 @@ export function Editor(props: IEditorProps): JSX.Element { const selected = FindContainerById(current.containers, current.selectedContainerId); return ( -
+
setNewHistory( - SelectContainer( - container, - history, - historyCurrentStep - ))} - deleteContainer={(containerId: string) => setNewHistory( - DeleteContainer( - containerId, - history, - historyCurrentStep - ))} - onPropertyChange={(key, value, type) => setNewHistory( - OnPropertyChange( - key, value, type, - selected, - history, - historyCurrentStep - ))} - addContainer={(type) => { + replaceContainer={replaceContainer} + selectContainer={(container) => { + setNewHistory( + SelectContainer( + container, + history, + historyCurrentStep + )); + }} + deleteContainer={(containerId: string) => { + setNewHistory( + DeleteContainer( + containerId, + history, + historyCurrentStep + )); + }} + onPropertyChange={(key, value, type) => { + setNewHistory( + OnPropertyChange( + key, value, type, + selected, + history, + historyCurrentStep + )); + }} + addOrReplaceContainer={(type) => { if (selected === null || selected === undefined) { return; } - - setNewHistory(AddContainerToSelectedContainer( - type, - selected, - configuration, - history, - historyCurrentStep - )); + if (replaceContainer.isReplacing && replaceContainer.id !== undefined) { + const newHistory = ReplaceByContainer( + replaceContainer.id, + type, + configuration, + history, + historyCurrentStep + ); + setReplaceContainer({ isReplacing: false, id: undefined, category: undefined }); + setNewHistory(newHistory); + } else { + setNewHistory(AddContainerToSelectedContainer( + type, + selected, + configuration, + history, + historyCurrentStep + )); + } }} - addContainerAt={(index, type, parent) => setNewHistory( - AddContainer( - index, - type, - parent, - configuration, + addContainerAt={(index, type, parent) => { + setNewHistory( + AddContainer( + index, + type, + parent, + configuration, + history, + historyCurrentStep + ) + ); + }} + addSymbol={(type) => { + setNewHistory( + AddSymbol( + type, + configuration, + history, + historyCurrentStep + )); + }} + onSymbolPropertyChange={(key, value) => { + setNewHistory( + OnSymbolPropertyChange( + key, value, + history, + historyCurrentStep + )); + }} + selectSymbol={(symbolId) => { + setNewHistory( + SelectSymbol( + symbolId, + history, + historyCurrentStep + )); + }} + deleteSymbol={(symbolId) => { + setNewHistory( + DeleteSymbol( + symbolId, + history, + historyCurrentStep + )); + }} + saveEditorAsJSON={() => { + SaveEditorAsJSON( history, - historyCurrentStep - ) - )} - addSymbol={(type) => setNewHistory( - AddSymbol( - type, - configuration, - history, - historyCurrentStep - ))} - onSymbolPropertyChange={(key, value) => setNewHistory( - OnSymbolPropertyChange( - key, value, - history, - historyCurrentStep - ))} - selectSymbol={(symbolId) => setNewHistory( - SelectSymbol( - symbolId, - history, - historyCurrentStep - ))} - deleteSymbol={(symbolId) => setNewHistory( - DeleteSymbol( - symbolId, - history, - historyCurrentStep - ))} - saveEditorAsJSON={() => SaveEditorAsJSON( - history, - historyCurrentStep, - configuration - )} - saveEditorAsSVG={() => SaveEditorAsSVG()} - loadState={(move) => setHistoryCurrentStep(move)} + historyCurrentStep, + configuration + ); + }} + saveEditorAsSVG={() => { SaveEditorAsSVG(); }} + loadState={(move) => { setHistoryCurrentStep(move); }} + setReplaceContainer={setReplaceContainer} /> editorRef.current} diff --git a/src/Components/ElementsList/ElementsSideBar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx similarity index 81% rename from src/Components/ElementsList/ElementsSideBar.tsx rename to src/Components/ElementsSidebar/ElementsSidebar.tsx index 78d9b6b..e39bf2d 100644 --- a/src/Components/ElementsList/ElementsSideBar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -1,21 +1,22 @@ import * as React from 'react'; -import { useState } from 'react'; import useSize from '@react-hook/size'; import { FixedSizeList as List } from 'react-window'; import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'; import { ContainerProperties } from '../ContainerProperties/ContainerProperties'; -import { IContainerModel } from '../../Interfaces/IContainerModel'; +import { type IContainerModel } from '../../Interfaces/IContainerModel'; import { FindContainerById, MakeRecursionDFSIterator } from '../../utils/itertools'; -import { ISymbolModel } from '../../Interfaces/ISymbolModel'; -import { PropertyType } from '../../Enums/PropertyType'; +import { type ISymbolModel } from '../../Interfaces/ISymbolModel'; +import { type PropertyType } from '../../Enums/PropertyType'; import { ToggleSideBar } from '../Sidebar/ToggleSideBar/ToggleSideBar'; import { Text } from '../Text/Text'; +import { ExtendedSidebar } from '../UI/UI'; -interface IElementsSideBarProps { +interface IElementsSidebarProps { containers: Map mainContainer: IContainerModel symbols: Map selectedContainer: IContainerModel | undefined + selectedExtendedSidebar: ExtendedSidebar onPropertyChange: ( key: string, value: string | number | boolean | number[], @@ -23,8 +24,7 @@ interface IElementsSideBarProps { ) => void selectContainer: (containerId: string) => void addContainer: (index: number, type: string, parent: string) => void - isExpanded: boolean - onExpandChange: () => void + onExpandChange: (value: ExtendedSidebar) => void } function RemoveBorderClasses(target: HTMLButtonElement, exception: string = ''): void { @@ -124,11 +124,10 @@ function HandleOnDrop( } } -export function ElementsSideBar(props: IElementsSideBarProps): JSX.Element { +export function ElementsSidebar(props: IElementsSidebarProps): JSX.Element { // States const divRef = React.useRef(null); const [,height] = useSize(divRef); - const [showProperties, setShowProperties] = useState(props.isExpanded); // Render const it = MakeRecursionDFSIterator(props.mainContainer, props.containers, 0, [0, 0], true); @@ -167,18 +166,27 @@ export function ElementsSideBar(props: IElementsSideBarProps): JSX.Element { return (
- {showProperties && -
- -
+ {props.selectedExtendedSidebar === ExtendedSidebar.Property && +
+ +
}
- { setShowProperties(newValue); props.onExpandChange(); }} /> + { + const newValue = props.selectedExtendedSidebar !== ExtendedSidebar.Property + ? ExtendedSidebar.Property + : ExtendedSidebar.None; + props.onExpandChange(newValue); + }} + />
selectContainer(container.properties.id)} - onDrop={(event) => HandleOnDrop(event, containers, mainContainer, addContainer)} - onDragOver={(event) => HandleDragOver(event, mainContainer)} - onDragLeave={(event) => HandleDragLeave(event)} + onClick={() => { selectContainer(container.properties.id); }} + onDrop={(event) => { HandleOnDrop(event, containers, mainContainer, addContainer); }} + onDragOver={(event) => { HandleDragOver(event, mainContainer); }} + onDragLeave={(event) => { HandleDragLeave(event); }} > {verticalBars} {text} diff --git a/src/Components/Menu/Menu.tsx b/src/Components/Menu/Menu.tsx index a0fbc03..57c3445 100644 --- a/src/Components/Menu/Menu.tsx +++ b/src/Components/Menu/Menu.tsx @@ -21,6 +21,7 @@ export interface IMenuAction { /** function to be called on button click */ action: (target: HTMLElement) => void + } function UseMouseEvents( @@ -139,7 +140,7 @@ function AddClassSpecificActions( onClick={() => action.action(target)} />); }); children.push(
); - }; + } return count; } diff --git a/src/Components/SVG/Elements/DepthDimensionLayer.tsx b/src/Components/SVG/Elements/DepthDimensionLayer.tsx deleted file mode 100644 index f4bff37..0000000 --- a/src/Components/SVG/Elements/DepthDimensionLayer.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import * as React from 'react'; -import { IContainerModel } from '../../../Interfaces/IContainerModel'; -import { DIMENSION_MARGIN } from '../../../utils/default'; -import { GetAbsolutePosition, MakeBFSIterator } from '../../../utils/itertools'; -import { TransformX } from '../../../utils/svg'; -import { Dimension } from './Dimension'; - -interface IDimensionLayerProps { - containers: Map - roots: IContainerModel | IContainerModel[] | null - scale?: number -} - -function GetDimensionsNodes( - containers: Map, - root: IContainerModel, - scale: number -): React.ReactNode[] { - const it = MakeBFSIterator(root, containers); - const dimensions: React.ReactNode[] = []; - let currentDepth = 0; - let min = Infinity; - let max = -Infinity; - let lastY = 0; - for (const { container, depth } of it) { - if (currentDepth !== depth) { - AddNewDimension(currentDepth, min, max, lastY, scale, '#000000', dimensions); - - currentDepth = depth; - min = Infinity; - max = -Infinity; - } - - const absoluteX = GetAbsolutePosition(containers, container)[0]; - const x = TransformX(absoluteX, container.properties.width, container.properties.positionReference); - lastY = container.properties.y + container.properties.height; - if (x < min) { - min = x; - } - - if (x > max) { - max = x; - } - } - - AddNewDimension(currentDepth, min, max, lastY, scale, '#000000', dimensions); - - return dimensions; -} - -/** - * A layer containing all dimension - * @param props - * @returns - */ -export function DepthDimensionLayer(props: IDimensionLayerProps): JSX.Element { - let dimensions: React.ReactNode[] = []; - const scale = props.scale ?? 1; - if (Array.isArray(props.roots)) { - props.roots.forEach(child => { - dimensions.concat(GetDimensionsNodes(props.containers, child, scale)); - }); - } else if (props.roots !== null) { - dimensions = GetDimensionsNodes(props.containers, props.roots, scale); - } - return ( - - {dimensions} - - ); -} - -function AddNewDimension(currentDepth: number, min: number, max: number, lastY: number, scale: number, color: string, dimensions: React.ReactNode[]): void { - const id = `dim-depth-${currentDepth}`; - const xStart = min; - const xEnd = max; - const y = lastY + (DIMENSION_MARGIN * (currentDepth + 1)) / scale; - const width = xEnd - xStart; - const text = width - .toFixed(0) - .toString(); - - if (width === 0) { - return; - } - - dimensions.push( - - ); -} diff --git a/src/Components/SVG/Elements/Dimension.tsx b/src/Components/SVG/Elements/Dimension.tsx index 0848bea..30469be 100644 --- a/src/Components/SVG/Elements/Dimension.tsx +++ b/src/Components/SVG/Elements/Dimension.tsx @@ -1,6 +1,9 @@ import * as React from 'react'; +import { type IDimensionOptions } from '../../../Interfaces/IDimensionOptions'; import { NOTCHES_LENGTH } from '../../../utils/default'; +export type IDimensionStyle = Omit; + interface IDimensionProps { id: string xStart: number @@ -8,7 +11,7 @@ interface IDimensionProps { xEnd: number yEnd: number text: string - color: string + style: IDimensionStyle scale?: number } @@ -28,8 +31,9 @@ function ApplyParametric(x0: number, t: number, vx: number): number { export function Dimension(props: IDimensionProps): JSX.Element { const scale = props.scale ?? 1; const style: React.CSSProperties = { - stroke: props.color, - strokeWidth: 2 / scale + stroke: props.style.color, + strokeWidth: (props.style.width ?? 2) / scale, + strokeDasharray: props.style.dashArray }; /// We need to find the points of the notches @@ -79,9 +83,11 @@ export function Dimension(props: IDimensionProps): JSX.Element { x2={endBottomX} y2={endBottomY} style={style}/> - ); + style={style}/>); } function AddVerticalChildrenDimension( @@ -315,10 +315,10 @@ function AddVerticalChildrenDimension( container: IContainerModel, currentTransform: [number, number], dimensions: React.ReactNode[], - scale: number, - color: string + scale: number ): void { const childrenId = `dim-x${xDim.toFixed(0)}-children-${container.properties.id}`; + const style = container.properties.dimensionOptions.childrenDimensions; const lastChildId = container.children[container.children.length - 1]; const lastChild = FindContainerById(containers, lastChildId); @@ -373,7 +373,7 @@ function AddVerticalChildrenDimension( yEnd={yChildrenEnd + offset} text={textChildren} scale={scale} - color={color} + style={style} />); } @@ -384,9 +384,9 @@ function AddHorizontalBorrowerDimension( depth: number, currentTransform: [number, number], dimensions: React.ReactNode[], - scale: number, - color: string + scale: number ): void { + const style = container.properties.dimensionOptions.dimensionWithMarks; const it = MakeRecursionDFSIterator(container, containers, depth, currentTransform); const marks = []; // list of vertical lines for the dimension for (const { @@ -431,7 +431,7 @@ function AddHorizontalBorrowerDimension( yEnd={yDim} text={value.toFixed(0)} scale={scale} - color={color}/>); + style={style}/>); count++; } } @@ -444,9 +444,9 @@ function AddVerticalBorrowerDimension( depth: number, currentTransform: [number, number], dimensions: React.ReactNode[], - scale: number, - color: string + scale: number ): void { + const style = container.properties.dimensionOptions.dimensionWithMarks; const it = MakeRecursionDFSIterator(container, containers, depth, currentTransform); const marks = []; // list of vertical lines for the dimension for (const { @@ -496,7 +496,7 @@ function AddVerticalBorrowerDimension( yEnd={next} text={value.toFixed(0)} scale={scale} - color={color}/>); + style={style}/>); count++; } } @@ -507,9 +507,9 @@ function AddVerticalSelfDimension( container: IContainerModel, currentTransform: [number, number], dimensions: React.ReactNode[], - scale: number, - color: string + scale: number ): void { + const style = container.properties.dimensionOptions.selfDimensions; const height = container.properties.height; const idVert = `dim-x${xDim.toFixed(0)}-${container.properties.id}`; let yStart = container.properties.y + currentTransform[1] + height; @@ -532,7 +532,7 @@ function AddVerticalSelfDimension( yEnd={yEnd} text={textVert} scale={scale} - color={color}/> + style={style}/> ); } @@ -541,9 +541,9 @@ function AddHorizontalSelfDimension( container: IContainerModel, currentTransform: [number, number], dimensions: React.ReactNode[], - scale: number, - color: string + scale: number ): void { + const style = container.properties.dimensionOptions.selfDimensions; const width = container.properties.width; const id = `dim-y${yDim.toFixed(0)}-${container.properties.id}`; const xStart = container.properties.x + currentTransform[0]; @@ -561,7 +561,7 @@ function AddHorizontalSelfDimension( yEnd={yDim} text={text} scale={scale} - color={color}/> + style={style}/> ); } @@ -570,9 +570,9 @@ function AddHorizontalSelfMarginsDimension( container: IContainerModel, currentTransform: [number, number], dimensions: React.ReactNode[], - scale: number, - color: string + scale: number ): void { + const style = container.properties.dimensionOptions.selfMarginsDimensions; const left = container.properties.margin.left; if (left != null) { const id = `dim-y-margin-left${yDim.toFixed(0)}-${container.properties.id}`; @@ -591,7 +591,7 @@ function AddHorizontalSelfMarginsDimension( yEnd={yDim} text={text} scale={scale} - color={color}/> + style={style}/> ); } @@ -613,7 +613,7 @@ function AddHorizontalSelfMarginsDimension( yEnd={yDim} text={text} scale={scale} - color={color}/> + style={style}/> ); } } @@ -624,9 +624,9 @@ function AddVerticalSelfMarginDimension( container: IContainerModel, currentTransform: [number, number], dimensions: React.ReactNode[], - scale: number, - color: string + scale: number ): void { + const style = container.properties.dimensionOptions.selfMarginsDimensions; const top = container.properties.margin.top; if (top != null) { const idVert = `dim-x-margin-top${xDim.toFixed(0)}-${container.properties.id}`; @@ -650,7 +650,7 @@ function AddVerticalSelfMarginDimension( yEnd={yEnd} text={textVert} scale={scale} - color={color}/> + style={style}/> ); } const bottom = container.properties.margin.bottom; @@ -676,7 +676,44 @@ function AddVerticalSelfMarginDimension( yEnd={yEnd} text={textVert} scale={scale} - color={color}/> + style={style}/> ); } } + +function AddHorizontalSymbolDimension( + symbol: ISymbolModel, + dimensions: React.ReactNode[], + scale: number, + depth: number +): void { + const width = symbol.x + (symbol.width / 2); + + if (width == null || width <= 0) { + return; + } + + const id = `dim-y-margin-left${symbol.width.toFixed(0)}-${symbol.id}`; + + const offset = (DIMENSION_MARGIN * (depth + 1)) / scale; + const text = width + .toFixed(0) + .toString(); + + // TODO: Put this in default.ts + const defaultDimensionSymbolStyle: IDimensionStyle = { + color: 'black' + }; + dimensions.push( + + ); +} diff --git a/src/Components/SVG/Elements/Selector/Selector.tsx b/src/Components/SVG/Elements/Selector/Selector.tsx new file mode 100644 index 0000000..1a8fb3e --- /dev/null +++ b/src/Components/SVG/Elements/Selector/Selector.tsx @@ -0,0 +1,54 @@ +import '../Selector.scss'; +import * as React from 'react'; +import { SHOW_SELECTOR_TEXT } from '../../../../utils/default'; + +interface ISelectorProps { + text: string + x: number + y: number + width: number + height: number + scale: number + style?: React.CSSProperties +} + +export function Selector({ text, x, y, width, height, scale, style: overrideStyle }: ISelectorProps): JSX.Element { + const xText = x + width / 2; + const yText = y + height / 2; + + const style: React.CSSProperties = { + stroke: '#3B82F6', + strokeWidth: 4 / scale, + fillOpacity: 0, + transitionProperty: 'all', + transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)', + transitionDuration: '150ms', + animation: 'fadein 750ms ease-in alternate infinite', + ...overrideStyle + }; + + return ( + <> + + + {SHOW_SELECTOR_TEXT + ? + { text } + + : null} + + ); +} diff --git a/src/Components/SVG/Elements/SelectorContainer/SelectorContainer.tsx b/src/Components/SVG/Elements/SelectorContainer/SelectorContainer.tsx index c85f2b3..c2ad3e2 100644 --- a/src/Components/SVG/Elements/SelectorContainer/SelectorContainer.tsx +++ b/src/Components/SVG/Elements/SelectorContainer/SelectorContainer.tsx @@ -1,9 +1,9 @@ import '../Selector.scss'; import * as React from 'react'; import { type IContainerModel } from '../../../../Interfaces/IContainerModel'; -import { SHOW_SELECTOR_TEXT } from '../../../../utils/default'; import { GetAbsolutePosition } from '../../../../utils/itertools'; import { RemoveMargin } from '../../../../utils/svg'; +import { Selector } from '../Selector/Selector'; interface ISelectorContainerProps { containers: Map @@ -33,41 +33,14 @@ export function SelectorContainer(props: ISelectorContainerProps): JSX.Element { props.selected.properties.margin.right )); - const xText = x + width / 2; - const yText = y + height / 2; - - const style: React.CSSProperties = { - stroke: '#3B82F6', - strokeWidth: 4 / scale, - fillOpacity: 0, - transitionProperty: 'all', - transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)', - transitionDuration: '150ms', - animation: 'fadein 750ms ease-in alternate infinite' - }; - return ( - <> - - - {SHOW_SELECTOR_TEXT - ? - {props.selected.properties.displayedText} - - : null} - + ); } diff --git a/src/Components/SVG/Elements/SelectorSymbol/SelectorSymbol.tsx b/src/Components/SVG/Elements/SelectorSymbol/SelectorSymbol.tsx index 6a27fd8..a420e40 100644 --- a/src/Components/SVG/Elements/SelectorSymbol/SelectorSymbol.tsx +++ b/src/Components/SVG/Elements/SelectorSymbol/SelectorSymbol.tsx @@ -1,7 +1,8 @@ import '../Selector.scss'; import * as React from 'react'; -import { SHOW_SELECTOR_TEXT, SYMBOL_MARGIN } from '../../../../utils/default'; +import { SYMBOL_MARGIN } from '../../../../utils/default'; import { type ISymbolModel } from '../../../../Interfaces/ISymbolModel'; +import { Selector } from '../Selector/Selector'; interface ISelectorSymbolProps { symbols: Map @@ -19,7 +20,7 @@ export function SelectorSymbol(props: ISelectorSymbolProps): JSX.Element { const scale = (props.scale ?? 1); const [width, height] = [ - props.selected.width, + props.selected.width / scale, props.selected.height / scale ]; @@ -29,45 +30,28 @@ export function SelectorSymbol(props: ISelectorSymbolProps): JSX.Element { x = -SYMBOL_MARGIN; y = props.selected.offset; } else { - x = props.selected.offset; - y = -SYMBOL_MARGIN - height; + [x,y] = [ + props.selected.offset + props.selected.width / 2, + -SYMBOL_MARGIN - height] } const xText = x + width / 2; const yText = y + height / 2; const style: React.CSSProperties = { - stroke: '#3B82F6', - strokeWidth: 4 / scale, - fillOpacity: 0, - transitionProperty: 'all', - transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)', - transitionDuration: '150ms', - animation: 'fadein 750ms ease-in alternate infinite' + transform: 'translateX(-50%)', + transformBox: 'fill-box' }; return ( - <> - - - {SHOW_SELECTOR_TEXT - ? - {props.selected.displayedText} - - : null} - + ); } diff --git a/src/Components/SVG/SVG.tsx b/src/Components/SVG/SVG.tsx index 738a0da..2a99c74 100644 --- a/src/Components/SVG/SVG.tsx +++ b/src/Components/SVG/SVG.tsx @@ -1,14 +1,13 @@ import * as React from 'react'; -import { ReactSVGPanZoom, type Tool, TOOL_PAN, type Value } from 'react-svg-pan-zoom'; +import { ReactSVGPanZoom, type Tool, TOOL_PAN, type Value, ALIGN_CENTER } from 'react-svg-pan-zoom'; import { Container } from './Elements/Container'; -import { type IContainerModel } from '../../Interfaces/IContainerModel'; import { SelectorContainer } from './Elements/SelectorContainer/SelectorContainer'; -import { DepthDimensionLayer } from './Elements/DepthDimensionLayer'; -import { MAX_FRAMERATE, SHOW_DIMENSIONS_PER_DEPTH } from '../../utils/default'; +import { MAX_FRAMERATE } from '../../utils/default'; import { SymbolLayer } from './Elements/SymbolLayer'; -import { type ISymbolModel } from '../../Interfaces/ISymbolModel'; import { DimensionLayer } from './Elements/DimensionLayer'; import { SelectorSymbol } from './Elements/SelectorSymbol/SelectorSymbol'; +import { type IToolbarProps, Toolbar } from './SVGReactPanZoom/ui-toolbar/toolbar'; +import { type DrawParams } from '../Viewer/Viewer'; interface ISVGProps { className?: string @@ -16,19 +15,27 @@ interface ISVGProps { viewerHeight: number width: number height: number - containers: Map - children: IContainerModel - selectedContainer?: IContainerModel - symbols: Map - selectedSymbol?: ISymbolModel + drawParams: DrawParams selectContainer: (containerId: string) => void - isComponentsOpen: boolean - isSymbolsOpen: boolean +} + +export enum SelectorMode { + Nothing, + Containers, + Symbols } export const ID = 'svg'; export function SVG(props: ISVGProps): JSX.Element { + const { + mainContainer, + selectorMode, + selectedContainer, + selectedSymbol, + containers, + symbols + } = props.drawParams; const [tool, setTool] = React.useState(TOOL_PAN); // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const [value, setValue] = React.useState({} as Value); @@ -54,14 +61,33 @@ export function SVG(props: ISVGProps): JSX.Element { }; const children: React.ReactNode | React.ReactNode[] = ; + function Selector(): JSX.Element { + switch (selectorMode) { + case SelectorMode.Containers: + return ; + case SelectorMode.Symbols: + return ; + default: + return <>; + } + } + return (
{ + svgViewer?.current?.setPointOnViewerCenter(props.width / 2, props.height / 2, 0.8); + }} background={'#ffffff'} defaultTool='pan' miniatureProps={{ @@ -94,17 +123,25 @@ export function SVG(props: ISVGProps): JSX.Element { width: 120, height: 120 }} + customToolbar={(props: IToolbarProps) => ( + + )} > {children} - {SHOW_DIMENSIONS_PER_DEPTH - ? - : null} - - - {/* leave this at the end so it can be removed during the svg export */} - { props.isComponentsOpen ? : null } - { props.isSymbolsOpen ? : null } + + +
@@ -112,7 +149,8 @@ export function SVG(props: ISVGProps): JSX.Element { } function UseFitOnce(svgViewer: React.RefObject, width: number, height: number): void { - React.useEffect(() => { - svgViewer?.current?.fitToViewer(); + React.useCallback(() => { + // TODO: Fix this + svgViewer?.current?.setPointOnViewerCenter(width / 2, height / 2, 0.8); }, [svgViewer, width, height]); } diff --git a/src/Components/SVG/SVGReactPanZoom/LICENSE b/src/Components/SVG/SVGReactPanZoom/LICENSE new file mode 100644 index 0000000..2f083c0 --- /dev/null +++ b/src/Components/SVG/SVGReactPanZoom/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 https://github.com/chrvadala + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/src/Components/SVG/SVGReactPanZoom/ui-toolbar/toolbar-button.tsx b/src/Components/SVG/SVGReactPanZoom/ui-toolbar/toolbar-button.tsx new file mode 100644 index 0000000..91fed61 --- /dev/null +++ b/src/Components/SVG/SVGReactPanZoom/ui-toolbar/toolbar-button.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { POSITION_TOP, POSITION_BOTTOM } from 'react-svg-pan-zoom'; + +interface IToolbarButtonProps { + title: string + name: string + toolbarPosition: string + activeColor: string + onClick: (event: React.MouseEvent | React.TouchEvent) => void + active: boolean + children: JSX.Element | JSX.Element[] +} + +interface IToolbarButtonState { + hover: boolean +} + +export class ToolbarButton extends React.Component { + public state: IToolbarButtonState; + + constructor(props: IToolbarButtonProps) { + super(props); + this.state = { hover: false }; + } + + change(event: (React.MouseEvent | React.TouchEvent)): void { + event.preventDefault(); + event.stopPropagation(); + + switch (event.type) { + case 'mouseenter': + case 'touchstart': + this.setState({ hover: true }); + break; + case 'mouseleave': + case 'touchend': + case 'touchcancel': + this.setState({ hover: false }); + break; + default: + // noop + } + } + + render(): JSX.Element { + const style = { + display: 'block', + width: '24px', + height: '24px', + margin: [POSITION_TOP, POSITION_BOTTOM].includes(this.props.toolbarPosition) ? '2px 1px' : '1px 2px', + color: this.props.active || this.state.hover ? this.props.activeColor : '#FFF', + transition: 'color 200ms ease', + background: 'none', + padding: '0px', + border: '0px', + outline: '0px', + cursor: 'pointer' + }; + + return ( + + ); + } +} diff --git a/src/Components/SVG/SVGReactPanZoom/ui-toolbar/toolbar.tsx b/src/Components/SVG/SVGReactPanZoom/ui-toolbar/toolbar.tsx new file mode 100644 index 0000000..83bb4b0 --- /dev/null +++ b/src/Components/SVG/SVGReactPanZoom/ui-toolbar/toolbar.tsx @@ -0,0 +1,164 @@ +import { + ArrowsPointingOutIcon, + CursorArrowRaysIcon, + HandRaisedIcon, + MagnifyingGlassMinusIcon, + MagnifyingGlassPlusIcon +} from '@heroicons/react/24/outline'; +import React from 'react'; +import { fromObject, scale, transform, translate } from 'transformation-matrix'; + +import { + fitToViewer, + POSITION_TOP, + POSITION_BOTTOM, + POSITION_LEFT, + POSITION_RIGHT, + TOOL_NONE, + TOOL_PAN, + TOOL_ZOOM_IN, + TOOL_ZOOM_OUT, ALIGN_LEFT, ALIGN_TOP, + type Value, + type Tool, + type ALIGN_BOTTOM, + type ALIGN_CENTER, + type ALIGN_RIGHT, + type ToolbarPosition +} from 'react-svg-pan-zoom'; +import { ToolbarButton } from './toolbar-button'; + +export interface IToolbarProps { + tool: Tool + value: Value + onChangeValue: (value: Value) => void + onChangeTool: (tool: Tool) => void + activeToolColor?: string + position?: ToolbarPosition | undefined + SVGAlignX?: typeof ALIGN_CENTER | typeof ALIGN_LEFT | typeof ALIGN_RIGHT | undefined + SVGAlignY?: typeof ALIGN_CENTER | typeof ALIGN_TOP | typeof ALIGN_BOTTOM | undefined + fittingScale?: number | undefined +} + +/** + * Change value + * @param value + * @param patch + * @param action + * @returns {Object} + */ +function set(value: Value, patch: object, action = null): Value { + value = Object.assign({}, value, patch, { lastAction: action }); + return Object.freeze(value); +} + +export function Toolbar({ + tool, + value, + onChangeValue, + onChangeTool, + activeToolColor = '#1CA6FC', + position = POSITION_RIGHT, + SVGAlignX = ALIGN_LEFT, + SVGAlignY = ALIGN_TOP, + fittingScale = undefined +}: IToolbarProps): JSX.Element { + function handleChangeTool(event: React.MouseEvent | React.TouchEvent, tool: Tool): void { + onChangeTool(tool); + event.stopPropagation(); + event.preventDefault(); + }; + + function handleFit(event: React.MouseEvent | React.TouchEvent): void { + let fittedValue: Value = fitToViewer(value, SVGAlignX, SVGAlignY); + if (fittingScale !== undefined) { + const { viewerWidth, viewerHeight } = fittedValue; + const matrix = transform( + fromObject(fittedValue), + translate(viewerWidth, viewerHeight), + scale(fittingScale, fittingScale), + translate(-viewerWidth, -viewerHeight) + ); + + fittedValue = set(fittedValue, { + ...matrix + }); + } + + onChangeValue(fittedValue); + event.stopPropagation(); + event.preventDefault(); + }; + + const isHorizontal = [POSITION_TOP, POSITION_BOTTOM].includes(position); + + const style: React.CSSProperties = { + // position + position: 'absolute', + transform: [POSITION_TOP, POSITION_BOTTOM].includes(position) ? 'translate(-50%, 0px)' : 'none', + top: [POSITION_LEFT, POSITION_RIGHT, POSITION_TOP].includes(position) ? '5px' : 'unset', + left: [POSITION_TOP, POSITION_BOTTOM].includes(position) ? '50%' : (POSITION_LEFT === position ? '5px' : 'unset'), + right: [POSITION_RIGHT].includes(position) ? '5px' : 'unset', + bottom: [POSITION_BOTTOM].includes(position) ? '5px' : 'unset', + + // inner styling + backgroundColor: 'rgba(19, 20, 22, 0.90)', + borderRadius: '2px', + display: 'flex', + flexDirection: isHorizontal ? 'row' : 'column', + padding: isHorizontal ? '1px 2px' : '2px 1px' + }; + + return ( +
+ { handleChangeTool(event, TOOL_NONE); } }> + + + + { handleChangeTool(event, TOOL_PAN); } }> + + + + { handleChangeTool(event, TOOL_ZOOM_IN); } }> + + + + { handleChangeTool(event, TOOL_ZOOM_OUT); } }> + + + + { handleFit(event); } }> + + +
+ ); +} diff --git a/src/Components/Sidebar/ToggleSideBar/ToggleSideBar.tsx b/src/Components/Sidebar/ToggleSideBar/ToggleSideBar.tsx index 61a35d9..7d7a082 100644 --- a/src/Components/Sidebar/ToggleSideBar/ToggleSideBar.tsx +++ b/src/Components/Sidebar/ToggleSideBar/ToggleSideBar.tsx @@ -4,16 +4,16 @@ import './ToggleSideBar.scss'; interface IToggleSidebarProps { title: string checked: boolean - onChange: (newValue: boolean) => void + onClick: () => void } -export function ToggleSideBar({ title, checked, onChange }: IToggleSidebarProps): JSX.Element { +export function ToggleSideBar({ title, checked, onClick }: IToggleSidebarProps): JSX.Element { return (